diff --git a/.gitignore b/.gitignore index 2dde3669..709237b0 100644 --- a/.gitignore +++ b/.gitignore @@ -103,6 +103,9 @@ ENV/ # JetBrains .idea/ +# vscode +.vscode + # Configurations config.py @@ -128,3 +131,5 @@ packet/static/safari-pinned-tab.svg packet/static/site.webmanifest faviconData.json +# csvs +*.csv diff --git a/packet/__init__.py b/packet/__init__.py index c369efdc..32e9fe74 100644 --- a/packet/__init__.py +++ b/packet/__init__.py @@ -80,6 +80,7 @@ if app.config['REALM'] == 'csh': from .routes import upperclassmen + from .routes import admin else: from .routes import freshmen diff --git a/packet/commands.py b/packet/commands.py index d4654f7d..1d1f87d0 100644 --- a/packet/commands.py +++ b/packet/commands.py @@ -16,6 +16,7 @@ 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 +from .utils import sync_freshman @app.cli.command('create-secret') @@ -66,40 +67,7 @@ def sync_freshmen(freshmen_csv): freshmen_in_csv = parse_csv(freshmen_csv) print('Syncing contents with the DB...') - freshmen_in_db = {freshman.rit_username: freshman for freshman in Freshman.query.all()} - - for csv_freshman in freshmen_in_csv.values(): - if csv_freshman.rit_username not in freshmen_in_db: - # This is a new freshman so add them to the DB - freshmen_in_db[csv_freshman.rit_username] = Freshman(rit_username=csv_freshman.rit_username, - name=csv_freshman.name, onfloor=csv_freshman.onfloor) - db.session.add(freshmen_in_db[csv_freshman.rit_username]) - else: - # This freshman is already in the DB so just update them - freshmen_in_db[csv_freshman.rit_username].onfloor = csv_freshman.onfloor - freshmen_in_db[csv_freshman.rit_username].name = csv_freshman.name - - # Update all freshmen entries that represent people who are no longer freshmen - for freshman in filter(lambda freshman: freshman.rit_username not in freshmen_in_csv, freshmen_in_db.values()): - freshman.onfloor = False - - # Update the freshmen signatures of each open or future packet - for packet in Packet.query.filter(Packet.end > datetime.now()).all(): - # Handle the freshmen that are no longer onfloor - for fresh_sig in filter(lambda fresh_sig: not fresh_sig.freshman.onfloor, packet.fresh_signatures): - FreshSignature.query.filter_by(packet_id=fresh_sig.packet_id, - freshman_username=fresh_sig.freshman_username).delete() - - # Add any new onfloor freshmen - # pylint: disable=cell-var-from-loop - current_fresh_sigs = set(map(lambda fresh_sig: fresh_sig.freshman_username, packet.fresh_signatures)) - for csv_freshman in filter(lambda csv_freshman: csv_freshman.rit_username not in current_fresh_sigs and - csv_freshman.onfloor and - csv_freshman.rit_username != packet.freshman_username, - freshmen_in_csv.values()): - db.session.add(FreshSignature(packet=packet, freshman=freshmen_in_db[csv_freshman.rit_username])) - - db.session.commit() + sync_freshman(freshmen_in_csv) print('Done!') diff --git a/packet/context_processors.py b/packet/context_processors.py index 604d931b..935106b6 100644 --- a/packet/context_processors.py +++ b/packet/context_processors.py @@ -43,7 +43,7 @@ def get_roles(sig): # pylint: disable=bare-except -@lru_cache(maxsize=128) +@lru_cache(maxsize=256) def get_rit_name(username): try: freshman = Freshman.query.filter_by(rit_username=username).first() @@ -53,7 +53,7 @@ def get_rit_name(username): # pylint: disable=bare-except -@lru_cache(maxsize=128) +@lru_cache(maxsize=256) def get_rit_image(username): if username: addresses = [username + '@rit.edu', username + '@g.rit.edu'] diff --git a/packet/ldap.py b/packet/ldap.py index 30b2f285..02a38dc9 100644 --- a/packet/ldap.py +++ b/packet/ldap.py @@ -149,8 +149,8 @@ def ldap_get_eboard_role(member): return return_val -# Status checkers +# Status checkers def ldap_is_eboard(member): """ :param member: A CSHMember instance @@ -158,6 +158,14 @@ def ldap_is_eboard(member): return _ldap_is_member_of_group(member, 'eboard') +def ldap_is_evals(member): + return _ldap_is_member_of_group(member, 'eboard-evaluations') + + +def ldap_is_rtp(member): + return _ldap_is_member_of_group(member, 'rtp') + + def ldap_is_intromember(member): """ :param member: A CSHMember instance diff --git a/packet/models.py b/packet/models.py index 6f8cbb6d..355a9976 100644 --- a/packet/models.py +++ b/packet/models.py @@ -49,6 +49,13 @@ def by_username(cls, username: str): """ return cls.query.filter_by(rit_username=username).first() + @classmethod + def get_all(cls): + """ + Helper method to get all freshmen easily + """ + return cls.query.all() + class Packet(db.Model): __tablename__ = 'packet' diff --git a/packet/routes/admin.py b/packet/routes/admin.py new file mode 100644 index 00000000..96a877b1 --- /dev/null +++ b/packet/routes/admin.py @@ -0,0 +1,43 @@ +from flask import render_template + +from packet import app +from packet.models import Packet, Freshman +from packet.routes.shared import packet_sort_key +from packet.utils import before_request, packet_auth, admin_auth +from packet.log_utils import log_cache, log_time + + +@app.route('/admin/packets') +@log_cache +@packet_auth +@admin_auth +@before_request +@log_time +def admin_packets(info=None): + open_packets = Packet.open_packets() + + # Pre-calculate and store the return values of did_sign(), signatures_received(), and signatures_required() + for packet in open_packets: + packet.did_sign_result = packet.did_sign(info['uid'], app.config['REALM'] == 'csh') + packet.signatures_received_result = packet.signatures_received() + packet.signatures_required_result = packet.signatures_required() + + open_packets.sort(key=packet_sort_key, reverse=True) + + return render_template('admin_packets.html', + open_packets=open_packets, + info=info) + + +@app.route('/admin/freshmen') +@log_cache +@packet_auth +@admin_auth +@before_request +@log_time +def admin_freshmen(info=None): + all_freshmen = Freshman.get_all() + + return render_template('admin_freshmen.html', + all_freshmen=all_freshmen, + info=info) diff --git a/packet/routes/api.py b/packet/routes/api.py index 246502cd..cb70e30b 100644 --- a/packet/routes/api.py +++ b/packet/routes/api.py @@ -12,14 +12,22 @@ from packet.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, _ldap_is_member_of_group, ldap_get_member +from packet.log_utils import log_time from packet.mail import send_report_mail, send_start_packet_mail -from packet.utils import before_request, packet_auth, notify_slack +from packet.utils import before_request, packet_auth, notify_slack, sync_freshman as sync_freshman_list from packet.models import Packet, MiscSignature, NotificationSubscription, Freshman, FreshSignature, UpperSignature from packet.notifications import packet_signed_notification, packet_100_percent_notification, \ - packet_starting_notification, packets_starting_notification + packet_starting_notification, packets_starting_notification import packet.stats as stats +class POSTFreshman: + def __init__(self, freshman): + self.name = freshman['name'].strip() + self.rit_username = freshman['rit_username'].strip() + self.onfloor = freshman['onfloor'].strip() == 'TRUE' + + @app.route('/api/v1/freshmen', methods=['POST']) @packet_auth def sync_freshman(): @@ -40,47 +48,14 @@ def sync_freshman(): if not _ldap_is_member_of_group(ldap_get_member(username), 'eboard-evaluations'): return 'Forbidden: not Evaluations Director', 403 - freshmen = request.json - results = list() - - packets = Packet.query.filter(Packet.end > datetime.now()).all() - - for freshman in freshmen: - rit_username = freshman['rit_username'] - name = freshman['name'] - onfloor = freshman['onfloor'] - - frosh = Freshman.query.filter_by(rit_username=rit_username).first() - if frosh: - if onfloor and not frosh.onfloor: - # Add new onfloor signature - for packet in packets: - db.session.add(FreshSignature(packet=packet, freshman=frosh)) - elif not onfloor and frosh.onfloor: - # Remove outdated onfloor signature - for packet in packets: - FreshSignature.query.filter_by(packet_id=packet.id, freshman_username=frosh.rit_username).delete() - - frosh.name = name - frosh.onfloor = onfloor - - results.append(f"'{name} ({rit_username})' updated") - else: - frosh = Freshman(rit_username=rit_username, name=name, onfloor=onfloor) - db.session.add(frosh) - if onfloor: - # Add onfloor signature - for packet in packets: - db.session.add(FreshSignature(packet=packet, freshman=frosh)) - - results.append(f"Freshman '{name} ({rit_username})' created") - - db.session.commit() - return dumps(results), 200 + freshmen_in_post = {freshman.rit_username: freshman for freshman in map(POSTFreshman, request.json)} + sync_freshman_list(freshmen_in_post) + return dumps('Done'), 200 @app.route('/api/v1/packets', methods=['POST']) @packet_auth +@log_time def create_packet(): """ Create a new packet. @@ -163,7 +138,7 @@ def get_packets_by_user(username: str) -> dict: return {packet.id: { 'start': packet.start, 'end': packet.end, - } for packet in frosh.packets} + } for packet in frosh.packets} @app.route('/api/v1/packets//newest', methods=['GET']) @@ -177,13 +152,13 @@ def get_newest_packet_by_user(username: str) -> dict: packet = frosh.packets[-1] return { - packet.id: { - 'start': packet.start, - 'end': packet.end, - 'required': vars(packet.signatures_required()), - 'received': vars(packet.signatures_received()), - } - } + packet.id: { + 'start': packet.start, + 'end': packet.end, + 'required': vars(packet.signatures_required()), + 'received': vars(packet.signatures_received()), + } + } @app.route('/api/v1/packet/', methods=['GET']) @@ -196,9 +171,10 @@ def get_packet_by_id(packet_id: int) -> dict: packet = Packet.by_id(packet_id) return { - 'required': vars(packet.signatures_required()), - 'received': vars(packet.signatures_received()), - } + 'required': vars(packet.signatures_required()), + 'received': vars(packet.signatures_received()), + } + @app.route('/api/v1/sign//', methods=['POST']) @packet_auth @@ -264,6 +240,7 @@ def packet_stats(packet_id): def upperclassman_stats(uid): return stats.upperclassman_stats(uid) + def commit_sig(packet, was_100, uid): packet_signed_notification(packet, uid) db.session.commit() diff --git a/packet/routes/shared.py b/packet/routes/shared.py index 97241815..5b872786 100644 --- a/packet/routes/shared.py +++ b/packet/routes/shared.py @@ -13,7 +13,7 @@ @app.route('/logout/') @auth.oidc_logout def logout(): - return redirect('http://csh.rit.edu') + return redirect('https://csh.rit.edu') @app.route('/packet//') diff --git a/packet/static/js/admin.js b/packet/static/js/admin.js new file mode 100644 index 00000000..31dc888e --- /dev/null +++ b/packet/static/js/admin.js @@ -0,0 +1,131 @@ +$(document).ready(function () { + + let openPacketsTable = $('#open_packets_table'); + openPacketsTable.DataTable({ + "searching": true, + "order": [], + "scrollX": false, + "paging": true, + "info": false, + "columnDefs": [ + { + "targets": 0, + "max-width": "50%", + }, + { + "type": "num-fmt", + "targets": 1, + "visible": true, + "max-width": "15%", + } + ] + }); + + let allFreshmenTable = $('#all_freshmen_table'); + allFreshmenTable.DataTable({ + "searching": true, + "order": [], + "scrollX": false, + "paging": true, + "info": false, + }); + + $("#create-packets").click(() => { + makePackets(); + }); + + $("#sync-freshmen").click(() => { + syncFreshmen(); + }) + +}); + +// Is this gross, yes. Do I feel like cleaning it up yet, no. + +let makePackets = () => { + let freshmen = []; + let fileUpload = document.getElementById("newPacketsFile"); + let regex = /^([a-zA-Z0-9\s_\\.\-:])+(.csv|.txt)$/; + if (regex.test(fileUpload.value.toLowerCase())) { + if (typeof (FileReader) != "undefined") { + let reader = new FileReader(); + reader.onload = (e) => { + let rows = e.target.result.split("\n"); + for (let i = 0; i < rows.length; i++) { + let cells = rows[i].split(","); + if (cells.length > 1) { + freshmen.push(cells[3]); + } + } + const payload = {start_date: $('#packet-start-date').val(), freshmen: freshmen} + if (freshmen.length >= 1) { + $("#create-packets").append(" "); + $("#create-packets").attr('disabled', true); + fetch('/api/v1/packets', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + } + ).then(response => { + if (response.status < 300) { + $('#new-packets-modal').modal('hide'); + location.reload(); + } else { + alert("There was an error creating packets") + } + }) + } + } + reader.readAsText(fileUpload.files[0]); + } + } +} + + +let syncFreshmen = () => { + let freshmen = []; + let fileUpload = document.getElementById("currentFroshFile"); + let regex = /^([a-zA-Z0-9\s_\\.\-:])+(.csv|.txt)$/; + if (regex.test(fileUpload.value.toLowerCase())) { + if (typeof (FileReader) != "undefined") { + let reader = new FileReader(); + reader.onload = (e) => { + let rows = e.target.result.split("\n"); + for (let i = 0; i < rows.length; i++) { + let cells = rows[i].split(","); + if (cells.length > 1) { + freshmen.push({ + rit_username: cells[3], + name: cells[0], + onfloor: cells[1] + }); + } + } + if (freshmen.length >= 1) { + $("#sync-freshmen").append(" "); + $("#sync-freshmen").attr('disabled', true); + fetch('/api/v1/freshmen', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(freshmen) + } + ).then(response => { + if (response.status < 300) { + $('#sync-freshmen-modal').modal('hide'); + location.reload(); + } else { + alert("There was an syncing freshmen") + } + }) + } + } + reader.readAsText(fileUpload.files[0]); + } + } +} diff --git a/packet/templates/admin_freshmen.html b/packet/templates/admin_freshmen.html new file mode 100644 index 00000000..e922375f --- /dev/null +++ b/packet/templates/admin_freshmen.html @@ -0,0 +1,27 @@ +{% extends "extend/base.html" %} + +{% block body %} +
+
+
+
+

All Freshmen

+
+
+ + {% include 'include/admin/sync_freshmen.html' %} +
+
+
+
+ {% include 'include/admin/all_freshmen.html' %} +
+
+{% endblock %} + +{% block scripts %} + {{ super() }} + +{% endblock %} diff --git a/packet/templates/admin_packets.html b/packet/templates/admin_packets.html new file mode 100644 index 00000000..576428de --- /dev/null +++ b/packet/templates/admin_packets.html @@ -0,0 +1,27 @@ +{% extends "extend/base.html" %} + +{% block body %} +
+
+
+
+

Active Packets

+
+
+ + {% include 'include/admin/new_packets.html' %} +
+
+
+
+ {% include 'include/admin/open_packets.html' %} +
+
+{% endblock %} + +{% block scripts %} + {{ super() }} + +{% endblock %} diff --git a/packet/templates/include/admin/all_freshmen.html b/packet/templates/include/admin/all_freshmen.html new file mode 100644 index 00000000..a3e79e3b --- /dev/null +++ b/packet/templates/include/admin/all_freshmen.html @@ -0,0 +1,33 @@ +
+
+
+
+ + + + + + + + + {% for freshman in all_freshmen %} + {% set freshman_name = freshman.name + ' (' + freshman.rit_username + ')' %} + + + + + {% endfor %} + +
NameOn-Floor
+ {{ freshman_name }} {{ freshman_name }} + + {{ freshman.onfloor }} +
+
+
+
+
diff --git a/packet/templates/include/admin/new_packets.html b/packet/templates/include/admin/new_packets.html new file mode 100644 index 00000000..c6dd3075 --- /dev/null +++ b/packet/templates/include/admin/new_packets.html @@ -0,0 +1,23 @@ + diff --git a/packet/templates/include/admin/open_packets.html b/packet/templates/include/admin/open_packets.html new file mode 100644 index 00000000..275b622c --- /dev/null +++ b/packet/templates/include/admin/open_packets.html @@ -0,0 +1,39 @@ +
+
+
+
+ + + + + + + + + {% for packet in open_packets %} + + + + + {% endfor %} + +
NameSignatures
+ + {{ get_rit_name(packet.freshman_username) }} {{ get_rit_name(packet.freshman_username) }} + + + {% if packet.signatures_received_result.total == packet.signatures_required_result.total %} + 💯 {# 100% emoji #} + {% else %} + {{ packet.signatures_received_result.total }} / + {{ packet.signatures_required_result.total }} + {% endif %} +
+
+
+
+
diff --git a/packet/templates/include/admin/sync_freshmen.html b/packet/templates/include/admin/sync_freshmen.html new file mode 100644 index 00000000..6f9b4806 --- /dev/null +++ b/packet/templates/include/admin/sync_freshmen.html @@ -0,0 +1,22 @@ + diff --git a/packet/templates/include/nav.html b/packet/templates/include/nav.html index fb8f2a62..470764dc 100644 --- a/packet/templates/include/nav.html +++ b/packet/templates/include/nav.html @@ -18,6 +18,18 @@ + {% if info.admin %} + + {% endif %} {% else %}