diff --git a/README.md b/README.md index c61b838a..5c07e710 100644 --- a/README.md +++ b/README.md @@ -1 +1,48 @@ -*Coming soon to a repo near you* \ No newline at end of file +CSH Web Packet +============== + +[](https://www.python.org/downloads/release/python-360/) +[](https://travis-ci.org/ComputerScienceHouse/packet) + +Web Packet is used by CSH to facilitate the evaluations of our members and keep track of packet signatures on the web + +Authorization +------------- + +Authentication happens via pyOIDC with CSH SSO, authenticating as the user who is viewing the page. +We have two different realms, and the site changes depending which realm is in use. + +The server uses heavy caching via lru_cache to speed up the results as much as possible + +Setup +------ + +For local development setup follow these steps: + +1. ```pip install -r requirements.txt``` +2. `Create config.py` or set environment variables + - Several of these variables require keys and information, please reach out to an RTP for testing information +3. Run `wsgi.py` + + +Commands +-------- + +The flask CLI provides all the methods needed to setup a packet and a packet season + +``` + create-packets Creates a new packet season for each of the freshmen in the given CSV. + create-secret Generates a securely random token. + db Perform database migrations. + ldap-sync Updates the upper and misc sigs in the DB to match ldap. + sync-freshmen Updates the freshmen entries in the DB to match the given CSV. + fetch-results Fetches and prints the results from a given packet season. +``` + +Code Standards +------------ + +Use Pylint to ensure your code follows standards. Commits will be pylinted by Travis CI, if your +build fails you must fix whatever it tells you is wrong before it will be merged. + +To check locally, run ```pylint packet``` diff --git a/frontend/scss/partials/_base.scss b/frontend/scss/partials/_base.scss index c580cd36..6ba84a3b 100644 --- a/frontend/scss/partials/_base.scss +++ b/frontend/scss/partials/_base.scss @@ -12,3 +12,7 @@ body { display: flex; justify-content: space-between; } + +.right-align { + float: right; +} diff --git a/package.json b/package.json index d7910e6c..d17a09dd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "title": "CSH Packet", "name": "csh-packet", - "version": "3.0.2", + "version": "3.0.3", "description": "A webpacket for CSH", "bugs": { "url": "https://github.com/ComputerScienceHouse/packet/issues", diff --git a/packet/_version.py b/packet/_version.py index 131942e7..8d1c8625 100644 --- a/packet/_version.py +++ b/packet/_version.py @@ -1 +1 @@ -__version__ = "3.0.2" +__version__ = "3.0.3" diff --git a/packet/commands.py b/packet/commands.py index 9f922412..ae3fb8c5 100644 --- a/packet/commands.py +++ b/packet/commands.py @@ -19,6 +19,9 @@ def create_secret(): print("Here's your random secure token:") print(token_hex()) +packet_start_time = time(hour=19) +packet_end_time = time(hour=21) + class CSVFreshman: def __init__(self, row): self.name = row[0] @@ -98,8 +101,8 @@ def create_packets(freshmen_csv): except ValueError: pass - start = datetime.combine(base_date, time(hour=19)) - end = datetime.combine(base_date, time(hour=23, minute=59)) + timedelta(days=14) + start = datetime.combine(base_date, packet_start_time) + end = datetime.combine(base_date, packet_end_time) + timedelta(days=14) print("Fetching data from LDAP...") eboard = set(member.uid for member in ldap_get_eboard()) @@ -158,3 +161,45 @@ def ldap_sync(): db.session.commit() print("Done!") + +@app.cli.command("fetch-results") +def fetch_results(): + """ + Fetches and prints the results from a given packet season. + """ + end_date = None + while end_date is None: + try: + date_str = input("Enter the last day of the packet season you'd like to retrieve results from " + + "(format: MM/DD/YYYY): ") + end_date = datetime.strptime(date_str, "%m/%d/%Y").date() + except ValueError: + pass + + end_date = datetime.combine(end_date, packet_end_time) + + for packet in Packet.query.filter_by(end=end_date).all(): + print() + + print("{} ({}):".format(packet.freshman.name, packet.freshman.rit_username)) + + received = packet.signatures_received() + required = packet.signatures_required() + + upper_ratio = sum((received["eboard"], received["upperclassmen"], received["miscellaneous"])) / \ + sum((required["eboard"], required["upperclassmen"], required["misc"])) + print("\tUpperclassmen score: {}%".format(round(upper_ratio * 100, 2))) + + total_ratio = sum(received.values()) / sum(required.values()) + print("\tTotal score: {}%".format(round(total_ratio * 100, 2))) + + print() + + print("\tEboard: {}/{}".format(received["eboard"], required["eboard"])) + print("\tUpperclassmen: {}/{}".format(received["upperclassmen"], required["upperclassmen"])) + print("\tFreshmen: {}/{}".format(received["freshmen"], required["freshmen"])) + print("\tMisc: {}/{}".format(len(packet.misc_signatures), required["misc"])) + + print() + + print("\tTotal missed:", sum(required.values()) - sum(received.values())) diff --git a/packet/ldap.py b/packet/ldap.py index 1e4f153a..25551044 100644 --- a/packet/ldap.py +++ b/packet/ldap.py @@ -8,7 +8,6 @@ def _ldap_get_group_members(group): return _ldap.get_group(group).get_members() -@lru_cache(maxsize=2048) def _ldap_is_member_of_group(member, group): group_list = member.get("memberOf") for group_dn in group_list: diff --git a/packet/member.py b/packet/member.py deleted file mode 100644 index 9e4a75b6..00000000 --- a/packet/member.py +++ /dev/null @@ -1,11 +0,0 @@ -from .models import Freshman, FreshSignature, UpperSignature, MiscSignature - - -def signed_packets(member): - # Checks whether or not member is a freshman - if Freshman.query.filter_by(rit_username=member).first() is not None: - return FreshSignature.query.filter_by(freshman_username=member, signed=True).all() - # Checks whether or not member is an upperclassman - if UpperSignature.query.filter_by(member=member).first() is not None: - return UpperSignature.query.filter_by(member=member, signed=True).all() - return MiscSignature.query.filter_by(member=member).all() diff --git a/packet/models.py b/packet/models.py index 62f07de2..33eb63c5 100644 --- a/packet/models.py +++ b/packet/models.py @@ -51,7 +51,7 @@ def is_open(self): @lru_cache(maxsize=1024) def signatures_required(self): - eboard = UpperSignature.query.filter_by(eboard=True).count() + eboard = UpperSignature.query.with_parent(self).filter_by(eboard=True).count() return {'eboard': eboard, 'upperclassmen': len(self.upper_signatures) - eboard, 'freshmen': len(self.fresh_signatures), diff --git a/packet/packet.py b/packet/packet.py index a0827237..ea138108 100644 --- a/packet/packet.py +++ b/packet/packet.py @@ -6,16 +6,10 @@ def sign(signer_username, freshman_username): - if signer_username == freshman_username: - return False - - freshman_signed = Freshman.query.filter_by(rit_username=freshman_username).first() - if freshman_signed is None: - return False - packet = freshman_signed.current_packet() - if packet is None or not packet.is_open(): + if not valid_signature(signer_username, freshman_username): return False + packet = get_freshman(freshman_username).current_packet() upper_signature = UpperSignature.query.filter(UpperSignature.member == signer_username, UpperSignature.packet == packet).first() fresh_signature = FreshSignature.query.filter(FreshSignature.freshman_username == signer_username, @@ -27,7 +21,7 @@ def sign(signer_username, freshman_username): upper_signature.signed = True elif fresh_signature: # Make sure only on floor freshmen can sign packets - freshman_signer = Freshman.query.filter_by(rit_username=signer_username).first() + freshman_signer = get_freshman(signer_username) if freshman_signer and not freshman_signer.onfloor: return False fresh_signature.signed = True @@ -35,17 +29,14 @@ def sign(signer_username, freshman_username): db.session.add(MiscSignature(packet=packet, member=signer_username)) db.session.commit() - # Clear functions that read signatures cache - get_number_signed.cache_clear() - get_signatures.cache_clear() - get_upperclassmen_percent.cache_clear() + clear_cache() return True @lru_cache(maxsize=2048) def get_signatures(freshman_username): - packet = Freshman.query.filter_by(rit_username=freshman_username).first().current_packet() + packet = get_current_packet(freshman_username) eboard = UpperSignature.query.filter_by(packet_id=packet.id, eboard=True).order_by(UpperSignature.signed.desc()) upper_signatures = UpperSignature.query.filter_by(packet_id=packet.id, eboard=False).order_by( UpperSignature.signed.desc()) @@ -57,14 +48,40 @@ def get_signatures(freshman_username): 'misc': misc_signatures} +@lru_cache(maxsize=2048) +def valid_signature(signer_username, freshman_username): + if signer_username == freshman_username: + return False + + freshman_signed = get_freshman(freshman_username) + if freshman_signed is None: + return False + + packet = freshman_signed.current_packet() + if packet is None or not packet.is_open(): + return False + + return True + + +@lru_cache(maxsize=512) +def get_freshman(freshman_username): + return Freshman.query.filter_by(rit_username=freshman_username).first() + + +@lru_cache(maxsize=512) +def get_current_packet(freshman_username): + return get_freshman(freshman_username).current_packet() + + @lru_cache(maxsize=2048) def get_number_signed(freshman_username): - return Freshman.query.filter_by(rit_username=freshman_username).first().current_packet().signatures_received() + return get_current_packet(freshman_username).signatures_received() -@lru_cache(maxsize=4096) +@lru_cache(maxsize=2048) def get_number_required(freshman_username): - return Freshman.query.filter_by(rit_username=freshman_username).first().current_packet().signatures_required() + return get_current_packet(freshman_username).signatures_required() @lru_cache(maxsize=2048) @@ -78,3 +95,27 @@ def get_upperclassmen_percent(uid): upperclassmen_signature = sum(upperclassmen_signature.values()) return upperclassmen_signature / upperclassmen_required * 100 + + +@lru_cache(maxsize=512) +def signed_packets(member): + # Checks whether or not member is a freshman + if get_freshman(member) is not None: + return FreshSignature.query.filter_by(freshman_username=member, signed=True).all() + # Checks whether or not member is an upperclassman + if UpperSignature.query.filter_by(member=member).first() is not None: + return UpperSignature.query.filter_by(member=member, signed=True).all() + return MiscSignature.query.filter_by(member=member).all() + + +def clear_cache(): + """ + Clear cache of all frequently changing data + """ + get_number_signed.cache_clear() + get_signatures.cache_clear() + get_number_required.cache_clear() + get_upperclassmen_percent.cache_clear() + get_freshman.cache_clear() + get_current_packet.cache_clear() + signed_packets.cache_clear() diff --git a/packet/routes/freshmen.py b/packet/routes/freshmen.py index f12234a4..4dc41556 100644 --- a/packet/routes/freshmen.py +++ b/packet/routes/freshmen.py @@ -1,7 +1,7 @@ from flask import redirect, render_template, request from packet import auth, app, db -from packet.models import Packet +from packet.packet import get_current_packet from packet.utils import before_request @@ -16,7 +16,7 @@ def index(info=None): @auth.oidc_auth @before_request def essays(info=None): - packet = Packet.query.filter_by(freshman_username=info['uid']).first() + packet = get_current_packet(info['uid']) return render_template("essays.html", info=info, packet=packet) @@ -25,7 +25,7 @@ def essays(info=None): @before_request def submit_essay(info=None): formdata = request.form - packet = Packet.query.filter_by(freshman_username=info['uid']).first() + packet = get_current_packet(info['uid']) packet.info_eboard = formdata['info_eboard'] packet.info_events = formdata['info_events'] diff --git a/packet/static/js/tables.js b/packet/static/js/tables.js index f6c2be10..1f156d6c 100644 --- a/packet/static/js/tables.js +++ b/packet/static/js/tables.js @@ -2,7 +2,7 @@ $(document).ready(function () { $('#active_packets_table').DataTable({ "searching": true, - "order": [[2, 'desc']], + "order": [], "paging": false, "info": false, "columnDefs": [ diff --git a/packet/templates/active_packets.html b/packet/templates/active_packets.html index 1832f2f5..a49cca61 100644 --- a/packet/templates/active_packets.html +++ b/packet/templates/active_packets.html @@ -1,5 +1,7 @@ {% extends "extend/base.html" %} +{% set can_sign = (info.onfloor and info.uid != packet.freshman.rit_username) or info.realm == "csh" %} + {% block body %} <div class="container main"> <div class="row mobile-hide"> @@ -12,57 +14,60 @@ <h3 class="page-title">Active Packets</h3> <div id="eval-table"> <div class="card"> <div class="card-body table-fill"> - <div class="card-body table-fill"> - <div class="table-responsive"> - <table id="active_packets_table" class="table table-striped no-bottom-margin"> - <thead> - <tr> - <th>Name</th> - <th>Signatures</th> + <div class="table-responsive"> + <table id="active_packets_table" class="table table-striped no-bottom-margin"> + <thead> + <tr> + <th>Name</th> + <th>Signatures</th> + {% if can_sign %} <th>Sign</th> - </tr> - </thead> - <tbody> - {% for packet in packets %} - {% if packet.is_open() %} - <tr {% if packet.did_sign %}style="background-color: #4caf505e" {% endif %}> - <td> - <a href="/packet/{{ packet.freshman.rit_username }}"> - <img class="eval-user-img" - alt="{{ packet.freshman.name }}" - src="https://www.gravatar.com/avatar/freshmen?d=mp&f=y" - width="25" - height="25"/> {{ packet.freshman.name }} ({{ packet.freshman.rit_username }}) - </a> - </td> - <td data-sort="{{ packet.total_signatures }}"> - {% if packet.total_signatures == packet.required_signatures %} - 💯 {# 100% emoji #} + {% endif %} + </tr> + </thead> + <tbody> + {% for packet in packets %} + {% if packet.is_open() %} + <tr {% if packet.did_sign %}style="background-color: #4caf505e" {% endif %}> + <td> + <a href="/packet/{{ packet.freshman.rit_username }}"> + <img class="eval-user-img" + alt="{{ packet.freshman.name }}" + src="https://www.gravatar.com/avatar/freshmen?d=mp&f=y" + width="25" + height="25"/> {{ packet.freshman.name }} + ({{ packet.freshman.rit_username }}) + </a> + </td> + <td data-sort="{{ packet.total_signatures }}"> + {% if packet.total_signatures == packet.required_signatures %} + 💯 {# 100% emoji #} + {% else %} + {{ packet.total_signatures }}/ + {{ packet.required_signatures }} + {% endif %} + </td> + {% if can_sign %} + <td class="sign-packet" align="right"> + {% if not packet.did_sign %} + <button class="btn btn-sm btn-primary sign-button" + data-freshman_uid="{{ packet.freshman.rit_username }}" + data-freshman_name="{{ packet.freshman.name }}"> + Sign + </button> {% else %} - {{ packet.total_signatures }}/{{ packet.required_signatures }} + <button class="btn btn-sm btn-primary signed-button" + disabled="disabled"><i + class="fa fa-check"></i> Signed + </button> {% endif %} </td> - {% if (info.onfloor and info.uid != packet.freshman.rit_username) or info.realm == "csh" %} - <td class="sign-packet" align="right"> - {% if not packet.did_sign %} - <button class="btn btn-sm btn-primary sign-button" - data-freshman_uid="{{ packet.freshman.rit_username }}" - data-freshman_name="{{ packet.freshman.name }}"> - Sign - </button> - {% else %} - <button class="btn btn-sm btn-primary signed-button" disabled="disabled"><i - class="fa fa-check"></i> Signed - </button> - {% endif %} - </td> - {% endif %} - </tr> - {% endif %} - {% endfor %} - </tbody> - </table> - </div> + {% endif %} + </tr> + {% endif %} + {% endfor %} + </tbody> + </table> </div> </div> </div> diff --git a/packet/templates/essays.html b/packet/templates/essays.html index 96aa0378..f9b4ccf1 100644 --- a/packet/templates/essays.html +++ b/packet/templates/essays.html @@ -10,23 +10,29 @@ <h3 class="page-title">Essays</h3> <div id="eval-blocks"> <div id="eval-table"> <div class="card"> - <div class="card-body table-fill"> - <form action="/essay" method="post"> - <div class="form-group"> - <label for="info_eboard">EBoard Members</label> - <textarea class="form-control" name="info_eboard">{{ packet.info_eboard }}</textarea> + + <form action="/essay" method="post"> + <div class="card-body table-fill"> + <div class="form-group card-body"> + <label for="info_eboard">Name and list all EBoard Members and their positions</label> + <textarea class="form-control" + name="info_eboard">{{ packet.info_eboard if packet.info_eboard != None }}</textarea> </div> - <div class="form-group"> - <label for="info_events">CSH Events</label> - <textarea class="form-control" name="info_events">{{ packet.info_events }}</textarea> + <div class="form-group card-body"> + <label for="info_events">Name some CSH Events that we host yearly (min 3)</label> + <textarea class="form-control" + name="info_events">{{ packet.info_events if packet.info_events != None }}</textarea> </div> - <div class="form-group"> - <label for="info_achieve">Technical Achievements</label> - <textarea class="form-control" name="info_achieve">{{ packet.info_achieve }}</textarea> + <div class="form-group card-body"> + <label for="info_achieve">Name some of CSH's Technical Achievements (min 3)</label> + <textarea class="form-control" + name="info_achieve">{{ packet.info_achieve if packet.info_achieve != None }}</textarea> </div> + </div> + <div class="card-footer mx-auto"> <input type="submit" value="Submit" class="btn btn-primary"> - </form> - </div> + </div> + </form> </div> </div> </div> diff --git a/packet/templates/packet.html b/packet/templates/packet.html index 1638834e..1a4fb608 100644 --- a/packet/templates/packet.html +++ b/packet/templates/packet.html @@ -1,5 +1,7 @@ {% extends "extend/base.html" %} +{% set packet_end = freshman.current_packet().end.strftime('%m/%d/%Y') %} + {% block body %} <div class="container main"> <div class="mb-2"> @@ -26,26 +28,26 @@ <h3>{{ freshman.name }}</h3> <div class="col"> <h5>Signatures: {{ signed }}/{{ required }}</h5> </div> + <div class="col"> + <h5 class="right-align">Packet Ends: {{ packet_end }}</h5> + </div> </div> <div class="card card-body"> <div class="row justify-content-between"> <div class="col"> - <b class="signature-count">{{ '%0.2f' % (signed/required * 100) }}%</b> - <h5>Total Score</h5> + <h5>Total Score - {{ '%0.2f' % (signed/required * 100) }}%</h5> <div class="progress"> <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="{{ signed/required * 100 }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ signed/required * 100 }}%"></div> </div> - <b class="signature-count">{{ '%0.2f' % upperclassmen_percent }}%</b> - <h5>Upperclassmen Score</h5> + <h5>Upperclassmen Score - {{ '%0.2f' % upperclassmen_percent }}%</h5> <div class="progress"> <div class="progress-bar bg-warning progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="{{ upperclassmen_percent }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ upperclassmen_percent }}%"></div> </div> - </div> </div> </div> diff --git a/packet/utils.py b/packet/utils.py index 67730aec..0e6d0f40 100644 --- a/packet/utils.py +++ b/packet/utils.py @@ -11,7 +11,8 @@ ldap_is_onfloor, ldap_get_roomnumber, ldap_get_groups) -from packet.models import Freshman, FreshSignature, Packet, UpperSignature, MiscSignature +from packet.models import FreshSignature, UpperSignature, MiscSignature +from packet.packet import get_current_packet, get_freshman INTRO_REALM = "https://sso.csh.rit.edu/auth/realms/intro" @@ -63,12 +64,12 @@ def get_member_info(uid): @lru_cache(maxsize=2048) def is_on_floor(uid): - return (Freshman.query.filter_by(rit_username=uid)).first().onfloor + return get_freshman(uid).onfloor -@lru_cache(maxsize=4096) +@lru_cache(maxsize=2048) def signed_packet(signer, freshman): - packet = Packet.query.filter_by(freshman_username=freshman).first() + packet = get_current_packet(freshman) freshman_signature = FreshSignature.query.filter_by(packet=packet, freshman_username=signer, signed=True).first() upper_signature = UpperSignature.query.filter_by(packet=packet, member=signer, signed=True).first() misc_signature = MiscSignature.query.filter_by(packet=packet, member=signer).first()