diff --git a/.gitignore b/.gitignore index 5652971e..55484af2 100644 --- a/.gitignore +++ b/.gitignore @@ -106,6 +106,9 @@ ENV/ # Configurations config.py +# Setup +node_modules + # Generated Assets packet/static/css/packet.css *.min.css @@ -124,3 +127,4 @@ packet/static/mstile-70x70.png packet/static/safari-pinned-tab.svg packet/static/site.webmanifest faviconData.json + diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..1e8b3149 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +6 diff --git a/.pylintrc b/.pylintrc index 4c7709f4..cd54984b 100644 --- a/.pylintrc +++ b/.pylintrc @@ -15,7 +15,8 @@ disable = cyclic-import, locally-disabled, file-ignored, - no-else-return + no-else-return, + unnecessary-lambda [REPORTS] output-format = text diff --git a/.travis.yml b/.travis.yml index 917a82ff..666c103f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,4 +5,4 @@ python: install: - "pip install -r requirements.txt" script: - - "pylint packet" + - "pylint packet/routes packet" diff --git a/README.md b/README.md index c5190d91..be53b307 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # CSH Web Packet [![Python 3.6](https://img.shields.io/badge/python-3.6-blue.svg)](https://www.python.org/downloads/release/python-360/) -[![Build Status](https://travis-ci.org/ComputerScienceHouse/packet.svg?branch=develop)](https://travis-ci.org/ComputerScienceHouse/packet) +[![Build Status](https://travis-ci.com/ComputerScienceHouse/packet.svg?branch=develop)](https://travis-ci.com/ComputerScienceHouse/packet) Packet is used by CSH to facilitate the freshmen packet portion of our introductory member evaluation process. This is the second major iteration of packet on the web. The first version was diff --git a/config.env.py b/config.env.py index a6ccc480..02caccb1 100644 --- a/config.env.py +++ b/config.env.py @@ -14,6 +14,7 @@ # Logging config LOG_LEVEL = environ.get("PACKET_LOG_LEVEL", "INFO") +ANALYTICS_ID = environ.get("ANALYTICS_ID", "UA-420696-9") # OpenID Connect SSO config REALM = environ.get("PACKET_REALM", "csh") diff --git a/frontend/scss/components/badges.scss b/frontend/scss/components/badges.scss new file mode 100644 index 00000000..5285e3fb --- /dev/null +++ b/frontend/scss/components/badges.scss @@ -0,0 +1,34 @@ +span { + &.badge { + font-size: 80%; + } + &.badge-eboard { + color: #fff; + background-color: #4CAF50; + } + + &.badge-rtp { + color: #fff; + background-color: #ff9800; + } + + &.badge-three_da { + color: #fff; + background-color: #e83e8c; + } + + &.badge-webmaster { + color: #fff; + background-color: #2196F3; + } + + &.badge-cm { + color: #fff; + background-color: #e51c23; + } + + &.badge-drink { + color: #fff; + background-color: #b0197e; + } +} diff --git a/frontend/scss/packet.scss b/frontend/scss/packet.scss index d5f8d6fa..d89fc83f 100644 --- a/frontend/scss/packet.scss +++ b/frontend/scss/packet.scss @@ -6,3 +6,4 @@ $csh-pink: #b0197e; @import "components/datatables"; @import "components/buttons"; @import "components/signatures"; +@import "components/badges"; diff --git a/frontend/scss/partials/_base.scss b/frontend/scss/partials/_base.scss index 6ba84a3b..0a756194 100644 --- a/frontend/scss/partials/_base.scss +++ b/frontend/scss/partials/_base.scss @@ -1,9 +1,9 @@ body { - padding-top: 50px; + padding-top: 40px; } .main { - margin-top: 50px; + margin-top: 40px; } @import "global"; diff --git a/migrations/versions/eecf30892d0e_demote_eboard_deluxe.py b/migrations/versions/eecf30892d0e_demote_eboard_deluxe.py new file mode 100644 index 00000000..bee48e16 --- /dev/null +++ b/migrations/versions/eecf30892d0e_demote_eboard_deluxe.py @@ -0,0 +1,44 @@ +"""Demote Eboard Deluxe + +Revision ID: eecf30892d0e +Revises: fe83600ef3fa +Create Date: 2019-02-14 17:41:18.469840 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'eecf30892d0e' +down_revision = 'fe83600ef3fa' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('signature_upper', sa.Column('active_rtp', sa.Boolean(), nullable=False, server_default='f')) + op.add_column('signature_upper', sa.Column('c_m', sa.Boolean(), nullable=False, server_default='f')) + op.add_column('signature_upper', sa.Column('drink_admin', sa.Boolean(), nullable=False, server_default='f')) + op.add_column('signature_upper', sa.Column('three_da', sa.Boolean(), nullable=False, server_default='f')) + op.add_column('signature_upper', sa.Column('webmaster', sa.Boolean(), nullable=False, server_default='f')) + op.alter_column('signature_upper', 'eboard', + existing_type=sa.BOOLEAN(), + type_=sa.String(length=12), + nullable=True) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('signature_upper', 'eboard', + existing_type=sa.String(length=12), + type_=sa.BOOLEAN(), + nullable=False) + op.drop_column('signature_upper', 'webmaster') + op.drop_column('signature_upper', 'three_da') + op.drop_column('signature_upper', 'drink_admin') + op.drop_column('signature_upper', 'c_m') + op.drop_column('signature_upper', 'active_rtp') + # ### end Alembic commands ### diff --git a/package.json b/package.json index 3004b19b..3a6ff113 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "title": "CSH Packet", "name": "csh-packet", - "version": "3.1.1", + "version": "3.2", "description": "A web app implementation of the CSH introductory packet.", "bugs": { "url": "https://github.com/ComputerScienceHouse/packet/issues", diff --git a/packet/__init__.py b/packet/__init__.py index 7a35582f..4e55681a 100644 --- a/packet/__init__.py +++ b/packet/__init__.py @@ -10,6 +10,7 @@ from flask import Flask from flask_migrate import Migrate from flask_pyoidc.flask_pyoidc import OIDCAuthentication +from flask_pyoidc.provider_configuration import ProviderConfiguration, ClientMetadata from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) @@ -37,11 +38,11 @@ migrate = Migrate(app, db) app.logger.info("SQLAlchemy pointed at " + repr(db.engine.url)) -auth = OIDCAuthentication(app, issuer=app.config["OIDC_ISSUER"], client_registration_info={ - "client_id": app.config["OIDC_CLIENT_ID"], - "client_secret": app.config["OIDC_CLIENT_SECRET"], - "post_logout_redirect_uris": "/logout/" -}) +APP_CONFIG = ProviderConfiguration(issuer=app.config["OIDC_ISSUER"], + client_metadata=ClientMetadata(app.config["OIDC_CLIENT_ID"], + app.config["OIDC_CLIENT_SECRET"])) + +auth = OIDCAuthentication({'app': APP_CONFIG}, app) # LDAP _ldap = csh_ldap.CSHLDAP(app.config["LDAP_BIND_DN"], app.config["LDAP_BIND_PASS"]) diff --git a/packet/commands.py b/packet/commands.py index c2e3f37b..9a310d98 100644 --- a/packet/commands.py +++ b/packet/commands.py @@ -9,7 +9,9 @@ from . import app, db from .models import Freshman, Packet, FreshSignature, UpperSignature, MiscSignature -from .ldap import ldap_get_active_members, ldap_is_eboard, ldap_is_intromember +from .ldap import ldap_get_eboard_role, ldap_get_active_rtps, ldap_get_3das, ldap_get_webmasters, \ + ldap_get_drink_admins, ldap_get_constitutional_maintainers, ldap_is_intromember, ldap_get_active_members, \ + ldap_is_on_coop @app.cli.command("create-secret") @@ -113,7 +115,14 @@ def create_packets(freshmen_csv): end = datetime.combine(base_date, packet_end_time) + timedelta(days=14) print("Fetching data from LDAP...") - all_upper = list(filter(lambda member: not ldap_is_intromember(member), ldap_get_active_members())) + all_upper = list(filter( + lambda member: not ldap_is_intromember(member) and not ldap_is_on_coop(member), ldap_get_active_members())) + + rtp = ldap_get_active_rtps() + three_da = ldap_get_3das() + webmaster = ldap_get_webmasters() + c_m = ldap_get_constitutional_maintainers() + drink = ldap_get_drink_admins() # Create the new packets and the signatures for each freshman in the given CSV freshmen_in_csv = parse_csv(freshmen_csv) @@ -123,7 +132,14 @@ def create_packets(freshmen_csv): db.session.add(packet) for member in all_upper: - db.session.add(UpperSignature(packet=packet, member=member.uid, eboard=ldap_is_eboard(member))) + sig = UpperSignature(packet=packet, member=member.uid) + sig.eboard = ldap_get_eboard_role(member) + sig.active_rtp = member.uid in rtp + sig.three_da = member.uid in three_da + sig.webmaster = member.uid in webmaster + sig.c_m = member.uid in c_m + sig.drink_admin = member.uid in drink + db.session.add(sig) for onfloor_freshman in Freshman.query.filter_by(onfloor=True).filter(Freshman.rit_username != freshman.rit_username).all(): @@ -139,32 +155,57 @@ def ldap_sync(): Updates the upper and misc sigs in the DB to match ldap. """ print("Fetching data from LDAP...") - all_upper = {member.uid: member for member in filter(lambda member: not ldap_is_intromember(member), - ldap_get_active_members())} + all_upper = {member.uid: member for member in filter( + lambda member: not ldap_is_intromember(member) and not ldap_is_on_coop(member), ldap_get_active_members())} + + rtp = ldap_get_active_rtps() + three_da = ldap_get_3das() + webmaster = ldap_get_webmasters() + c_m = ldap_get_constitutional_maintainers() + drink = ldap_get_drink_admins() print("Applying updates to the DB...") for packet in Packet.query.filter(Packet.end > datetime.now()).all(): - # Update the eboard state of all UpperSignatures + # Update the role state of all UpperSignatures for sig in filter(lambda sig: sig.member in all_upper, packet.upper_signatures): - sig.eboard = ldap_is_eboard(all_upper[sig.member]) + sig.eboard = ldap_get_eboard_role(all_upper[sig.member]) + sig.active_rtp = sig.member in rtp + sig.three_da = sig.member in three_da + sig.webmaster = sig.member in webmaster + sig.c_m = sig.member in c_m + sig.drink_admin = sig.member in drink # Migrate UpperSignatures that are from accounts that are not active anymore for sig in filter(lambda sig: sig.member not in all_upper, packet.upper_signatures): UpperSignature.query.filter_by(packet_id=packet.id, member=sig.member).delete() if sig.signed: - db.session.add(MiscSignature(packet=packet, member=sig.member)) + sig = MiscSignature(packet=packet, member=sig.member) + db.session.add(sig) # Migrate MiscSignatures that are from accounts that are now active members for sig in filter(lambda sig: sig.member in all_upper, packet.misc_signatures): MiscSignature.query.filter_by(packet_id=packet.id, member=sig.member).delete() - db.session.add(UpperSignature(packet=packet, member=sig.member, - eboard=ldap_is_eboard(all_upper[sig.member]), signed=True)) + sig = UpperSignature(packet=packet, member=sig.member, signed=True) + sig.eboard = ldap_get_eboard_role(all_upper[sig.member]) + sig.active_rtp = sig.member in rtp + sig.three_da = sig.member in three_da + sig.webmaster = sig.member in webmaster + sig.c_m = sig.member in c_m + sig.drink_admin = sig.member in drink + db.session.add(sig) # Create UpperSignatures for any new active members # pylint: disable=cell-var-from-loop upper_sigs = set(map(lambda sig: sig.member, packet.upper_signatures)) for member in filter(lambda member: member not in upper_sigs, all_upper): - db.session.add(UpperSignature(packet=packet, member=member, eboard=ldap_is_eboard(all_upper[member]))) + UpperSignature(packet=packet, member=member) + sig.eboard = ldap_get_eboard_role(all_upper[sig.member]) + sig.active_rtp = sig.member in rtp + sig.three_da = sig.member in three_da + sig.webmaster = sig.member in webmaster + sig.c_m = sig.member in c_m + sig.drink_admin = sig.member in drink + db.session.add(sig) db.session.commit() print("Done!") @@ -190,7 +231,6 @@ def fetch_results(): print("\tTotal score: {:0.2f}%".format(received.total / required.total * 100)) print() - print("\tEboard: {}/{}".format(received.eboard, required.eboard)) print("\tUpperclassmen: {}/{}".format(received.upper, required.upper)) print("\tFreshmen: {}/{}".format(received.fresh, required.fresh)) print("\tMiscellaneous: {}/{}".format(received.misc, required.misc)) diff --git a/packet/context_processors.py b/packet/context_processors.py index 66dcb456..52347203 100644 --- a/packet/context_processors.py +++ b/packet/context_processors.py @@ -18,6 +18,26 @@ def get_csh_name(username): except: return username +def get_roles(sig): + """ + Converts a signature's role fields to a dict for ease of access. + :return: A dictionary of role short names to role long names + """ + out = {} + if sig.eboard: + out["eboard"] = sig.eboard + if sig.active_rtp: + out["rtp"] = "RTP" + if sig.three_da: + out["three_da"] = "3DA" + if sig.webmaster: + out["webmaster"] = "Webmaster" + if sig.c_m: + out["cm"] = "Constitutional Maintainer" + if sig.drink_admin: + out["drink"] = "Drink Admin" + return out + # pylint: disable=bare-except @lru_cache(maxsize=128) @@ -38,4 +58,4 @@ def log_time(label): @app.context_processor def utility_processor(): - return dict(get_csh_name=get_csh_name, get_rit_name=get_rit_name, log_time=log_time) + return dict(get_csh_name=get_csh_name, get_rit_name=get_rit_name, log_time=log_time, get_roles=get_roles) diff --git a/packet/ldap.py b/packet/ldap.py index e91b2ab1..79a1abfd 100644 --- a/packet/ldap.py +++ b/packet/ldap.py @@ -3,6 +3,7 @@ """ from functools import lru_cache +from datetime import date from packet import _ldap @@ -79,6 +80,73 @@ def ldap_get_live_onfloor(): return members +def ldap_get_active_rtps(): + """ + All active RTPs + :return: A list of CSHMember instances + """ + return [member.uid for member in _ldap_get_group_members("active_rtp")] + + +def ldap_get_3das(): + """ + All 3das + :return: A list of CSHMember instances + """ + return [member.uid for member in _ldap_get_group_members("3da")] + + +def ldap_get_webmasters(): + """ + All webmasters + :return: A list of CSHMember instances + """ + return [member.uid for member in _ldap_get_group_members("webmaster")] + + +def ldap_get_constitutional_maintainers(): + """ + All constitutional maintainers + :return: A list of CSHMember instances + """ + return [member.uid for member in _ldap_get_group_members("constitutional_maintainers")] + + +def ldap_get_drink_admins(): + """ + All drink admins + :return: A list of CSHMember instances + """ + return [member.uid for member in _ldap_get_group_members("drink")] + + +def ldap_get_eboard_role(member): + """ + :param member: A CSHMember instance + :return: A String or None + """ + + return_val = None + + if _ldap_is_member_of_group(member, "eboard-chairman"): + return_val = "Chairman" + elif _ldap_is_member_of_group(member, "eboard-evaluations"): + return_val = "Evals" + elif _ldap_is_member_of_group(member, "eboard-financial"): + return_val = "Financial" + elif _ldap_is_member_of_group(member, "eboard-history"): + return_val = "History" + elif _ldap_is_member_of_group(member, "eboard-imps"): + return_val = "Imps" + elif _ldap_is_member_of_group(member, "eboard-opcomm"): + return_val = "OpComm" + elif _ldap_is_member_of_group(member, "eboard-research"): + return_val = "R&D" + elif _ldap_is_member_of_group(member, "eboard-social"): + return_val = "Social" + + return return_val + # Status checkers def ldap_is_eboard(member): @@ -95,6 +163,16 @@ def ldap_is_intromember(member): return _ldap_is_member_of_group(member, "intromembers") +def ldap_is_on_coop(member): + """ + :param member: A CSHMember instance + """ + if date.today().month > 6: + return _ldap_is_member_of_group(member, "fall_coop") + else: + return _ldap_is_member_of_group(member, "spring_coop") + + def ldap_get_roomnumber(member): """ :param member: A CSHMember instance diff --git a/packet/models.py b/packet/models.py index 1e10d757..c3e6b4dc 100644 --- a/packet/models.py +++ b/packet/models.py @@ -18,10 +18,9 @@ class SigCounts: """ Utility class for returning counts of signatures broken out by type """ - def __init__(self, eboard, upper, fresh, misc): + def __init__(self, upper, fresh, misc): # Base fields - self.eboard = eboard - self.upper = upper # Upperclassmen excluding eboard + self.upper = upper self.fresh = fresh self.misc = misc @@ -29,8 +28,8 @@ def __init__(self, eboard, upper, fresh, misc): self.misc_capped = misc if misc <= REQUIRED_MISC_SIGNATURES else REQUIRED_MISC_SIGNATURES # Totals (calculated using misc_capped) - self.member_total = eboard + upper + self.misc_capped - self.total = eboard + upper + fresh + self.misc_capped + self.member_total = upper + self.misc_capped + self.total = upper + fresh + self.misc_capped class Freshman(db.Model): @@ -68,21 +67,19 @@ def signatures_required(self): """ :return: A SigCounts instance with the fields set to the number of signatures received by this packet """ - eboard = sum(map(lambda sig: 1 if sig.eboard else 0, self.upper_signatures)) - upper = len(self.upper_signatures) - eboard + upper = len(self.upper_signatures) fresh = len(self.fresh_signatures) - return SigCounts(eboard, upper, fresh, REQUIRED_MISC_SIGNATURES) + return SigCounts(upper, fresh, REQUIRED_MISC_SIGNATURES) def signatures_received(self): """ :return: A SigCounts instance with the fields set to the number of required signatures for this packet """ - eboard = sum(map(lambda sig: 1 if sig.eboard and sig.signed else 0, self.upper_signatures)) - upper = sum(map(lambda sig: 1 if not sig.eboard and sig.signed else 0, self.upper_signatures)) + upper = sum(map(lambda sig: 1 if sig.signed else 0, self.upper_signatures)) fresh = sum(map(lambda sig: 1 if sig.signed else 0, self.fresh_signatures)) - return SigCounts(eboard, upper, fresh, len(self.misc_signatures)) + return SigCounts(upper, fresh, len(self.misc_signatures)) def did_sign(self, username, is_csh): """ @@ -128,7 +125,12 @@ class UpperSignature(db.Model): packet_id = Column(Integer, ForeignKey("packet.id"), primary_key=True) member = Column(String(36), primary_key=True) signed = Column(Boolean, default=False, nullable=False) - eboard = Column(Boolean, default=False, nullable=False) + eboard = Column(String(12), nullable=True) + active_rtp = Column(Boolean, default=False, nullable=False) + three_da = Column(Boolean, default=False, nullable=False) + webmaster = Column(Boolean, default=False, nullable=False) + c_m = Column(Boolean, default=False, nullable=False) + drink_admin = Column(Boolean, default=False, nullable=False) updated = Column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False) packet = relationship("Packet", back_populates="upper_signatures") diff --git a/packet/routes/freshmen.py b/packet/routes/freshmen.py index 7e0df5a2..7414c16d 100644 --- a/packet/routes/freshmen.py +++ b/packet/routes/freshmen.py @@ -2,9 +2,9 @@ Routes available to freshmen only """ -from flask import redirect, render_template, request, url_for +from flask import redirect, url_for -from packet import app, db +from packet import app from packet.models import Packet from packet.utils import before_request, packet_auth diff --git a/packet/routes/shared.py b/packet/routes/shared.py index 29513234..3d894baa 100644 --- a/packet/routes/shared.py +++ b/packet/routes/shared.py @@ -41,8 +41,7 @@ def freshman_packet(packet_id, info=None): did_sign=packet.did_sign(info["uid"], app.config["REALM"] == "csh"), required=packet.signatures_required(), received=packet.signatures_received(), - eboard=filter(lambda sig: sig.eboard, packet.upper_signatures), - upper=filter(lambda sig: not sig.eboard, packet.upper_signatures)) + upper=packet.upper_signatures) def packet_sort_key(packet): diff --git a/packet/routes/upperclassmen.py b/packet/routes/upperclassmen.py index e0ad8421..9b7dc807 100644 --- a/packet/routes/upperclassmen.py +++ b/packet/routes/upperclassmen.py @@ -2,9 +2,9 @@ Routes available to CSH users only """ -from flask import redirect, render_template, url_for from itertools import chain from operator import itemgetter +from flask import redirect, render_template, url_for from packet import app from packet.models import Packet, MiscSignature diff --git a/packet/static/js/signing-0.js b/packet/static/js/signing.js similarity index 55% rename from packet/static/js/signing-0.js rename to packet/static/js/signing.js index b63f288b..90855808 100644 --- a/packet/static/js/signing-0.js +++ b/packet/static/js/signing.js @@ -1,23 +1,34 @@ $(document).ready(function () { + const dialogs = Swal.mixin({ + customClass: { + confirmButton: 'btn m-1 btn-primary', + cancelButton: 'btn btn-light' + }, + buttonsStyling: false, + }); + $('.sign-button').click(function () { var packetData = $(this).get(0).dataset; var userData = $("#userInfo").val(); - swal({ + dialogs.fire({ title: "Are you sure?", text: "Once a packet is signed it can only be unsigned from request to the Evals Director", - icon: "warning", - buttons: true, - dangerMode: true, + type: "warning", + confirmButtonText: 'Sign', + showCancelButton: true, + reverseButtons: true }) .then((willSign) => { - if (willSign) { + if (willSign.value) { $.ajax({ url: "/api/v1/sign/" + packetData.packet_id + "/", method: "POST", success: function (data) { - swal("Congratulations or I'm sorry\nYou've signed " + packetData.freshman_name + "'s packet.", { - icon: "success", + dialogs.fire({ + title: "Congratulations or I'm sorry", + text: "You've signed " + packetData.freshman_name + "'s packet", + type: "success", }) .then(() => { location.reload(); diff --git a/packet/static/js/tables.js b/packet/static/js/tables.js index 1f156d6c..44169a9a 100644 --- a/packet/static/js/tables.js +++ b/packet/static/js/tables.js @@ -7,9 +7,45 @@ $(document).ready(function () { "info": false, "columnDefs": [ { - "type": "num-fmt", "targets": 1 + "targets": 0, + "max-width": "50%", + }, + { + "type": "num-fmt", + "targets": 1, + "visible": false, + "max-width": "15%", + }, + { + "type": "num-fmt", + "targets": 2, + "visible": false, + "max-width": "15%", + }, + { + "type": "num-fmt", + "targets": 3, + "max-width": "15%", } ] }); + var table = $('#active_packets_table'); + + $("#sig-filter").on('change', function () { + if ($(this).val() === 'Total') { + table.DataTable().column(1).visible(false); + table.DataTable().column(2).visible(false); + table.DataTable().column(3).visible(true); + } else if ($(this).val() === 'Upperclassmen') { + table.DataTable().column(1).visible(true); + table.DataTable().column(2).visible(false); + table.DataTable().column(3).visible(false); + } else if ($(this).val() === 'Freshmen') { + table.DataTable().column(1).visible(false); + table.DataTable().column(2).visible(true); + table.DataTable().column(3).visible(false); + } + }); + }); diff --git a/packet/templates/active_packets.html b/packet/templates/active_packets.html index 67b3de29..06e6b99a 100644 --- a/packet/templates/active_packets.html +++ b/packet/templates/active_packets.html @@ -2,9 +2,18 @@ {% block body %}
-
-
-

Active Packets

+
+
+
+

Active Packets

+
+
+ +
@@ -18,6 +27,10 @@

Active Packets

Name + {% if info.realm == "csh" %} + Signatures + Signatures + {% endif %} Signatures {% if can_sign %} Sign @@ -27,7 +40,7 @@

Active Packets

{% for packet in packets %} - + {{ get_rit_name(packet.freshman_username) }}Active Packets height="25"/> {{ get_rit_name(packet.freshman_username) }} + + {% if packet.signatures_received_result.upper == packet.signatures_required_result.upper %} + 💯 {# 100% emoji #} + {% else %} + {{ packet.signatures_received_result.upper }} / + {{ packet.signatures_required_result.upper }} + {% endif %} + + + {% if packet.signatures_received_result.fresh == packet.signatures_required_result.fresh %} + 💯 {# 100% emoji #} + {% else %} + {{ packet.signatures_received_result.fresh }} / + {{ packet.signatures_required_result.fresh }} + {% endif %} + {% if packet.signatures_received_result.total == packet.signatures_required_result.total %} 💯 {# 100% emoji #} @@ -45,7 +74,7 @@

Active Packets

{% endif %} {% if can_sign %} - + {% if not packet.did_sign_result and info.uid != packet.freshman_username %}