diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 852cb726..38ed5fa9 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -15,11 +15,11 @@ jobs: strategy: matrix: - python-version: [3.8, 3.9] + python-version: [3.12] steps: - name: Install ldap dependencies - run: sudo apt-get install libldap2-dev libsasl2-dev + run: sudo apt-get update && sudo apt-get install libldap2-dev libsasl2-dev - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 @@ -31,4 +31,4 @@ jobs: if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with pylint run: | - pylint conditional + pylint conditional --disable=logging-fstring-interpolation diff --git a/.pylintrc b/.pylintrc index 407369b7..22e31bcb 100644 --- a/.pylintrc +++ b/.pylintrc @@ -9,7 +9,6 @@ disable = duplicate-code, no-member, parse-error, - bad-continuation, too-few-public-methods, global-statement, cyclic-import, diff --git a/Dockerfile b/Dockerfile index cae1b260..5fa9b20b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -FROM docker.io/python:3.8-buster -MAINTAINER Devin Matte +FROM docker.io/python:3.12-bookworm +MAINTAINER Computer Science House RUN mkdir /opt/conditional @@ -8,21 +8,25 @@ ADD requirements.txt /opt/conditional WORKDIR /opt/conditional RUN apt-get -yq update && \ - apt-get -yq install libsasl2-dev libldap2-dev libssl-dev gcc g++ make && \ + apt-get -yq install libsasl2-dev libldap2-dev libldap-common libssl-dev gcc g++ make && \ pip install -r requirements.txt && \ apt-get -yq clean all +ENV NVM_DIR /usr/local/nvm +ENV NODE_VERSION v10.24.1 +RUN mkdir -p $NVM_DIR + +RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash + +RUN /bin/bash -c "source $NVM_DIR/nvm.sh && nvm install $NODE_VERSION" + ADD . /opt/conditional -RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - && \ - apt-get -yq update && \ - apt-get -yq install nodejs && \ - npm install && \ - npm run production && \ - rm -rf node_modules && \ - apt-get -yq remove nodejs npm && \ +RUN /bin/bash -c "source $NVM_DIR/nvm.sh && nvm use --delete-prefix $NODE_VERSION && npm install && npm run production" + +RUN rm -rf node_modules && \ apt-get -yq clean all RUN ln -sf /usr/share/zoneinfo/America/New_York /etc/localtime -CMD ["gunicorn", "conditional:app", "--bind=0.0.0.0:8080", "--access-logfile=-", "--timeout=256"] +CMD ["ddtrace-run", "gunicorn", "conditional:app", "--bind=0.0.0.0:8080", "--access-logfile=-", "--timeout=256"] diff --git a/README.md b/README.md index 7d069a94..7377f98a 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,10 @@ npm start This will run the asset pipeline, start the Python server, and start BrowserSync. Your default web browser will open automatically. If it doesn't, navigate to `http://127.0.0.1:3000`. Any changes made to the frontend files in `frontend` or the Jinja templates in `conditional/templates` will cause the browser to reload automatically. +### Dependencies + +To add new dependencies, add them to `requirements.in` and then run `pip-compile requirements.in` to produce a new locked `requirements.txt`. Do not edit `requirements.txt` directly as it will be overwritten by future PRs. + ### Database Migrations If the database schema is changed after initializing the database, you must migrate it to the new schema by running: diff --git a/conditional/__init__.py b/conditional/__init__.py index 041218ae..5fe72694 100644 --- a/conditional/__init__.py +++ b/conditional/__init__.py @@ -7,6 +7,7 @@ from flask_migrate import Migrate from flask_gzip import Gzip from flask_pyoidc.flask_pyoidc import OIDCAuthentication +from flask_pyoidc.provider_configuration import ProviderConfiguration, ClientMetadata from flask_sqlalchemy import SQLAlchemy import sentry_sdk @@ -30,17 +31,26 @@ # Sentry setup sentry_sdk.init( - dsn=app.config['SENTRY_DSN'], + dsn=app.config["SENTRY_DSN"], integrations=[FlaskIntegration(), SqlalchemyIntegration()], - environment=app.config['SENTRY_ENV'], + environment=app.config["SENTRY_ENV"], ) -ldap = CSHLDAP(app.config['LDAP_BIND_DN'], - app.config['LDAP_BIND_PW'], - ro=app.config['LDAP_RO']) +ldap = CSHLDAP( + app.config["LDAP_BIND_DN"], app.config["LDAP_BIND_PW"], ro=app.config["LDAP_RO"] +) + +client_metadata = ClientMetadata(app.config["OIDC_CLIENT_CONFIG"]) +provider_config = ProviderConfiguration( + issuer=app.config["OIDC_ISSUER"], client_registration_info=client_metadata +) +frosh_provider_config = ProviderConfiguration( + issuer=app.config["FROSH_OIDC_ISSUER"], client_registration_info=client_metadata +) -auth = OIDCAuthentication(app, issuer=app.config["OIDC_ISSUER"], - client_registration_info=app.config["OIDC_CLIENT_CONFIG"]) +auth = OIDCAuthentication( + {"default": provider_config, "frosh": frosh_provider_config}, app +) app.secret_key = app.config["SECRET_KEY"] @@ -57,42 +67,48 @@ def start_of_year(): # Configure Logging -def request_processor(logger, log_method, event_dict): # pylint: disable=unused-argument, redefined-outer-name - if 'request' in event_dict: - flask_request = event_dict['request'] - event_dict['ip'] = flask_request.remote_addr - event_dict['method'] = flask_request.method - event_dict['blueprint'] = flask_request.blueprint - event_dict['path'] = flask_request.full_path - if 'auth_dict' in event_dict: - auth_dict = event_dict['auth_dict'] - event_dict['user'] = auth_dict['username'] +def request_processor( + logger, log_method, event_dict +): # pylint: disable=unused-argument, redefined-outer-name + if "request" in event_dict: + flask_request = event_dict["request"] + event_dict["ip"] = flask_request.remote_addr + event_dict["method"] = flask_request.method + event_dict["blueprint"] = flask_request.blueprint + event_dict["path"] = flask_request.full_path + if "auth_dict" in event_dict: + auth_dict = event_dict["auth_dict"] + event_dict["user"] = auth_dict["username"] return event_dict -def database_processor(logger, log_method, event_dict): # pylint: disable=unused-argument, redefined-outer-name - if 'request' in event_dict: - if event_dict['method'] != 'GET': +def database_processor( + logger, log_method, event_dict +): # pylint: disable=unused-argument, redefined-outer-name + if "request" in event_dict: + if event_dict["method"] != "GET": log = UserLog( - ipaddr=event_dict['ip'], - user=event_dict['user'], - method=event_dict['method'], - blueprint=event_dict['blueprint'], - path=event_dict['path'], - description=event_dict['event'] + ipaddr=event_dict["ip"], + user=event_dict["user"], + method=event_dict["method"], + blueprint=event_dict["blueprint"], + path=event_dict["path"], + description=event_dict["event"], ) db.session.add(log) db.session.flush() db.session.commit() - del event_dict['request'] + del event_dict["request"] return event_dict -structlog.configure(processors=[ - request_processor, - database_processor, - structlog.processors.KeyValueRenderer() -]) +structlog.configure( + processors=[ + request_processor, + database_processor, + structlog.processors.KeyValueRenderer(), + ] +) logger = structlog.get_logger() @@ -112,6 +128,7 @@ def database_processor(logger, log_method, event_dict): # pylint: disable=unuse from .blueprints.cache_management import cache_bp from .blueprints.co_op import co_op_bp from .blueprints.logs import log_bp +from .blueprints.packet import packet_bp app.register_blueprint(dashboard_bp) app.register_blueprint(attendance_bp) @@ -126,20 +143,21 @@ def database_processor(logger, log_method, event_dict): # pylint: disable=unuse app.register_blueprint(cache_bp) app.register_blueprint(co_op_bp) app.register_blueprint(log_bp) +app.register_blueprint(packet_bp) from .util.ldap import ldap_get_member -@app.route('/') +@app.route("/") def static_proxy(path): # send_static_file will guess the correct MIME type return app.send_static_file(path) -@app.route('/') -@auth.oidc_auth +@app.route("/") +@auth.oidc_auth("default") def default_route(): - return redirect('/dashboard') + return redirect("/dashboard") @app.route("/logout") @@ -153,28 +171,28 @@ def health(): """ Shows an ok status if the application is up and running """ - return {'status': 'ok'} + return {"status": "ok"} @app.errorhandler(404) @app.errorhandler(500) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def route_errors(error, user_dict=None): - data = dict() + data = {} # Handle the case where the header isn't present - if user_dict['username'] is not None: - data['username'] = user_dict['account'].uid - data['name'] = user_dict['account'].cn + if user_dict["username"] is not None: + data["username"] = user_dict["account"].uid + data["name"] = user_dict["account"].cn else: - data['username'] = "unknown" - data['name'] = "Unknown" + data["username"] = "unknown" + data["name"] = "Unknown" # Figure out what kind of error was passed if isinstance(error, int): code = error - elif hasattr(error, 'code'): + elif hasattr(error, "code"): code = error.code else: # Unhandled exception @@ -186,11 +204,13 @@ def route_errors(error, user_dict=None): else: error_desc = type(error).__name__ - return render_template('errors.html', - error=error_desc, - error_code=code, - event_id=sentry_sdk.last_event_id(), - **data), int(code) + return render_template( + "errors.html", + error=error_desc, + error_code=code, + event_id=sentry_sdk.last_event_id(), + **data + ), int(code) -logger.info('conditional started') +logger.info("conditional started") diff --git a/conditional/blueprints/attendance.py b/conditional/blueprints/attendance.py index 7adbf995..22475703 100644 --- a/conditional/blueprints/attendance.py +++ b/conditional/blueprints/attendance.py @@ -29,7 +29,7 @@ @attendance_bp.route('/attendance/ts_members') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def get_all_members(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -57,7 +57,7 @@ def get_all_members(user_dict=None): @attendance_bp.route('/attendance/hm_members') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def get_non_alumni_non_coop(internal=False, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -102,7 +102,7 @@ def get_non_alumni_non_coop(internal=False, user_dict=None): @attendance_bp.route('/attendance/cm_members') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def get_non_alumni(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -130,7 +130,7 @@ def get_non_alumni(user_dict=None): @attendance_bp.route('/attendance_cm') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_attendance_cm(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -142,7 +142,7 @@ def display_attendance_cm(user_dict=None): @attendance_bp.route('/attendance_ts') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_attendance_ts(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -154,7 +154,7 @@ def display_attendance_ts(user_dict=None): @attendance_bp.route('/attendance_hm') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_attendance_hm(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -170,7 +170,7 @@ def display_attendance_hm(user_dict=None): @attendance_bp.route('/attendance/submit/cm', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def submit_committee_attendance(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -183,7 +183,7 @@ def submit_committee_attendance(user_dict=None): f_attendees = post_data['freshmen'] timestamp = post_data['timestamp'] - log.info('Submit {} Meeting Attendance'.format(committee)) + log.info(f'Submit {committee} Meeting Attendance') timestamp = datetime.strptime(timestamp, "%Y-%m-%d") meeting = CommitteeMeeting(committee, timestamp, approved) @@ -193,11 +193,11 @@ def submit_committee_attendance(user_dict=None): db.session.refresh(meeting) for m in m_attendees: - log.info('Gave Attendance to {} for {}'.format(m, committee)) + log.info(f'Gave Attendance to {m} for {committee}') db.session.add(MemberCommitteeAttendance(m, meeting.id)) for f in f_attendees: - log.info('Gave Attendance to freshman-{} for {}'.format(f, committee)) + log.info(f'Gave Attendance to freshman-{f} for {committee}') db.session.add(FreshmanCommitteeAttendance(f, meeting.id)) db.session.commit() @@ -205,7 +205,7 @@ def submit_committee_attendance(user_dict=None): @attendance_bp.route('/attendance/submit/ts', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def submit_seminar_attendance(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -228,11 +228,11 @@ def submit_seminar_attendance(user_dict=None): db.session.refresh(seminar) for m in m_attendees: - log.info('Gave Attendance to {} for {}'.format(m, seminar_name)) + log.info(f'Gave Attendance to {m} for {seminar_name}') db.session.add(MemberSeminarAttendance(m, seminar.id)) for f in f_attendees: - log.info('Gave Attendance to freshman-{} for {}'.format(f, seminar_name)) + log.info(f'Gave Attendance to freshman-{f} for {seminar_name}') db.session.add(FreshmanSeminarAttendance(f, seminar.id)) db.session.commit() @@ -240,7 +240,7 @@ def submit_seminar_attendance(user_dict=None): @attendance_bp.route('/attendance/submit/hm', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def submit_house_attendance(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -263,10 +263,7 @@ def submit_house_attendance(user_dict=None): if "members" in post_data: for m in post_data['members']: - log.info('Marked {} {} for House Meeting on {}'.format( - m['uid'], - m['status'], - timestamp.strftime("%Y-%m-%d"))) + log.info(f'Marked {m['uid']} {m['status']} for House Meeting on {timestamp.strftime("%Y-%m-%d")}') db.session.add(MemberHouseMeetingAttendance( m['uid'], meeting.id, @@ -275,10 +272,7 @@ def submit_house_attendance(user_dict=None): if "freshmen" in post_data: for f in post_data['freshmen']: - log.info('Marked freshman-{} {} for House Meeting on {}'.format( - f['id'], - f['status'], - timestamp.strftime("%Y-%m-%d"))) + log.info(f'Marked freshman-{f['id']} {f['status']} for House Meeting on {timestamp.strftime("%Y-%m-%d")}') db.session.add(FreshmanHouseMeetingAttendance( f['id'], meeting.id, @@ -290,7 +284,7 @@ def submit_house_attendance(user_dict=None): @attendance_bp.route('/attendance/alter/hm//', methods=['GET']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def alter_house_attendance(uid, hid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -299,7 +293,7 @@ def alter_house_attendance(uid, hid, user_dict=None): return "must be evals", 403 if not uid.isdigit(): - log.info('Mark {} Present for House Meeting ID: {}'.format(uid, hid)) + log.info(f'Mark {uid} Present for House Meeting ID: {hid}') member_meeting = MemberHouseMeetingAttendance.query.filter( MemberHouseMeetingAttendance.uid == uid, MemberHouseMeetingAttendance.meeting_id == hid @@ -308,7 +302,7 @@ def alter_house_attendance(uid, hid, user_dict=None): db.session.commit() return jsonify({"success": True}), 200 - log.info('Mark freshman-{} Present for House Meeting ID: {}'.format(uid, hid)) + log.info(f'Mark freshman-{uid} Present for House Meeting ID: {hid}') freshman_meeting = FreshmanHouseMeetingAttendance.query.filter( FreshmanHouseMeetingAttendance.fid == uid, FreshmanHouseMeetingAttendance.meeting_id == hid @@ -320,7 +314,7 @@ def alter_house_attendance(uid, hid, user_dict=None): @attendance_bp.route('/attendance/alter/hm//', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def alter_house_excuse(uid, hid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -333,7 +327,7 @@ def alter_house_excuse(uid, hid, user_dict=None): hm_excuse = post_data['excuse'] if not uid.isdigit(): - log.info('Mark {} as {} for HM ID: {}'.format(uid, hm_status, hid)) + log.info(f'Mark {uid} as {hm_status} for HM ID: {hid}') MemberHouseMeetingAttendance.query.filter( MemberHouseMeetingAttendance.uid == uid, MemberHouseMeetingAttendance.meeting_id == hid @@ -342,7 +336,7 @@ def alter_house_excuse(uid, hid, user_dict=None): 'attendance_status': hm_status }) else: - log.info('Mark {} as {} for HM ID: {}'.format(uid, hm_status, hid)) + log.info(f'Mark {uid} as {hm_status} for HM ID: {hid}') FreshmanHouseMeetingAttendance.query.filter( FreshmanHouseMeetingAttendance.fid == uid, FreshmanHouseMeetingAttendance.meeting_id == hid @@ -357,7 +351,7 @@ def alter_house_excuse(uid, hid, user_dict=None): @attendance_bp.route('/attendance/history', methods=['GET']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def attendance_history(user_dict=None): @@ -431,9 +425,9 @@ def get_seminar_attendees(meeting_id): TechnicalSeminar.approved == False).all()] # pylint: disable=singleton-comparison all_meetings = sorted((all_cm + all_ts), key=lambda k: k['dt_obj'], reverse=True)[offset:limit] if len(all_cm) % 10 != 0: - total_pages = (int(len(all_cm) / 10) + 1) + total_pages = int(len(all_cm) / 10) + 1 else: - total_pages = (int(len(all_cm) / 10)) + total_pages = int(len(all_cm) / 10) return render_template('attendance_history.html', username=user_dict['username'], history=all_meetings, @@ -444,7 +438,7 @@ def get_seminar_attendees(meeting_id): @attendance_bp.route('/attendance/alter/cm/', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def alter_committee_attendance(cid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -476,7 +470,7 @@ def alter_committee_attendance(cid, user_dict=None): @attendance_bp.route('/attendance/alter/ts/', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def alter_seminar_attendance(sid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -508,7 +502,7 @@ def alter_seminar_attendance(sid, user_dict=None): @attendance_bp.route('/attendance/ts/', methods=['GET', 'DELETE']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def get_cm_attendees(sid, user_dict=None): if request.method == 'GET': @@ -526,7 +520,7 @@ def get_cm_attendees(sid, user_dict=None): return jsonify({"attendees": attendees}), 200 log = logger.new(request=request, auth_dict=user_dict) - log.info('Delete Technical Seminar {}'.format(sid)) + log.info(f'Delete Technical Seminar {sid}') if not ldap_is_eboard(user_dict['account']): return jsonify({"success": False, "error": "Not EBoard"}), 403 @@ -545,7 +539,7 @@ def get_cm_attendees(sid, user_dict=None): @attendance_bp.route('/attendance/cm/', methods=['GET', 'DELETE']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def get_ts_attendees(cid, user_dict=None): if request.method == 'GET': @@ -563,7 +557,7 @@ def get_ts_attendees(cid, user_dict=None): return jsonify({"attendees": attendees}), 200 log = logger.new(request=request, auth_dict=user_dict) - log.info('Delete Committee Meeting {}'.format(cid)) + log.info(f'Delete Committee Meeting {cid}') if not ldap_is_eboard(user_dict['account']): return jsonify({"success": False, "error": "Not EBoard"}), 403 @@ -582,11 +576,11 @@ def get_ts_attendees(cid, user_dict=None): @attendance_bp.route('/attendance/cm//approve', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def approve_cm(cid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - log.info('Approve Committee Meeting {} Attendance'.format(cid)) + log.info(f'Approve Committee Meeting {cid} Attendance') if not ldap_is_eboard(user_dict['account']): return jsonify({"success": False, "error": "Not EBoard"}), 403 @@ -600,11 +594,11 @@ def approve_cm(cid, user_dict=None): @attendance_bp.route('/attendance/ts//approve', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def approve_ts(sid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - log.info('Approve Technical Seminar {} Attendance'.format(sid)) + log.info(f'Approve Technical Seminar {sid} Attendance') if not ldap_is_eboard(user_dict['account']): return jsonify({"success": False, "error": "Not EBoard"}), 403 diff --git a/conditional/blueprints/cache_management.py b/conditional/blueprints/cache_management.py index fc4a1866..14290a51 100644 --- a/conditional/blueprints/cache_management.py +++ b/conditional/blueprints/cache_management.py @@ -21,7 +21,7 @@ @cache_bp.route('/restart') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def restart_app(user_dict=None): if not ldap_is_rtp(user_dict['account']): @@ -34,7 +34,7 @@ def restart_app(user_dict=None): @cache_bp.route('/clearcache') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def clear_cache(user_dict=None): if not ldap_is_eval_director(user_dict['account']) and not ldap_is_rtp(user_dict['account']): diff --git a/conditional/blueprints/co_op.py b/conditional/blueprints/co_op.py index 27b86dce..94bf107a 100644 --- a/conditional/blueprints/co_op.py +++ b/conditional/blueprints/co_op.py @@ -17,7 +17,7 @@ @co_op_bp.route('/co_op/') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_co_op_form(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -33,7 +33,7 @@ def display_co_op_form(user_dict=None): @co_op_bp.route('/co_op/submit', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def submit_co_op_form(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -46,7 +46,7 @@ def submit_co_op_form(user_dict=None): if not ldap_is_current_student(user_dict['account']): return "Must be current student", 403 - log.info('Submit {} Co-Op'.format(semester)) + log.info(f'Submit {semester} Co-Op') if CurrentCoops.query.filter(CurrentCoops.uid == user_dict['username'], CurrentCoops.date_created > start_of_year()).first(): @@ -65,7 +65,7 @@ def submit_co_op_form(user_dict=None): @co_op_bp.route('/co_op/', methods=['DELETE']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def delete_co_op(uid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -73,7 +73,7 @@ def delete_co_op(uid, user_dict=None): if not ldap_is_eval_director(user_dict['account']): return "must be eval director", 403 - log.info('Delete {}\'s Co-Op'.format(uid)) + log.info(f'Delete {uid}\'s Co-Op') # Remove from corresponding co-op ldap group if ldap_is_member_of_group(user_dict['account'], 'fall_coop'): @@ -91,7 +91,7 @@ def delete_co_op(uid, user_dict=None): @co_op_bp.route('/co_op/manage') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_co_op_management(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) diff --git a/conditional/blueprints/conditional.py b/conditional/blueprints/conditional.py index a5bd005c..1082ca4f 100644 --- a/conditional/blueprints/conditional.py +++ b/conditional/blueprints/conditional.py @@ -15,7 +15,7 @@ @conditionals_bp.route('/conditionals/') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_conditionals(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -39,7 +39,7 @@ def display_conditionals(user_dict=None): @conditionals_bp.route('/conditionals/create', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def create_conditional(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -52,7 +52,7 @@ def create_conditional(user_dict=None): uid = post_data['uid'] description = post_data['description'] due_date = datetime.strptime(post_data['dueDate'], "%Y-%m-%d") - log.info('Create a new conditional for {}'.format(uid)) + log.info(f'Create a new conditional for {uid}') if post_data['evaluation'] == 'spring': current_eval = SpringEval.query.filter(SpringEval.status == "Pending", SpringEval.uid == uid, @@ -76,7 +76,7 @@ def create_conditional(user_dict=None): @conditionals_bp.route('/conditionals/review', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def conditional_review(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -88,7 +88,7 @@ def conditional_review(user_dict=None): cid = post_data['id'] status = post_data['status'] - log.info('Updated conditional-{} to {}'.format(cid, status)) + log.info(f'Updated conditional-{cid} to {status}') conditional = Conditional.query.filter(Conditional.id == cid) cond_obj = conditional.first() @@ -113,11 +113,11 @@ def conditional_review(user_dict=None): @conditionals_bp.route('/conditionals/delete/', methods=['DELETE']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def conditional_delete(cid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - log.info('Delete conditional-{}'.format(cid)) + log.info(f'Delete conditional-{cid}') if ldap_is_eval_director(user_dict['account']): Conditional.query.filter( diff --git a/conditional/blueprints/dashboard.py b/conditional/blueprints/dashboard.py index fbffde84..527e8dd8 100644 --- a/conditional/blueprints/dashboard.py +++ b/conditional/blueprints/dashboard.py @@ -6,6 +6,8 @@ from conditional.models.models import HouseMeeting from conditional.models.models import MajorProject from conditional.models.models import MemberHouseMeetingAttendance +from conditional.models.models import MemberSeminarAttendance +from conditional.models.models import TechnicalSeminar from conditional.models.models import SpringEval from conditional.util.auth import get_user from conditional.util.flask import render_template @@ -20,9 +22,14 @@ dashboard_bp = Blueprint('dashboard_bp', __name__) +def is_seminar_attendance_valid(attendance): + seminar = TechnicalSeminar.query.filter( + TechnicalSeminar.id == attendance.seminar_id).first() + return seminar and seminar.approved and seminar.timestamp > start_of_year() +# pylint: disable=too-many-statements @dashboard_bp.route('/dashboard/') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_dashboard(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -31,7 +38,7 @@ def display_dashboard(user_dict=None): # Get the list of voting members. can_vote = get_voting_members() - data = dict() + data = {} data['username'] = user_dict['account'].uid data['active'] = ldap_is_active(user_dict['account']) data['bad_standing'] = ldap_is_bad_standing(user_dict['account']) @@ -64,7 +71,7 @@ def display_dashboard(user_dict=None): # only show housing if member has onfloor status if ldap_is_onfloor(user_dict['account']): - housing = dict() + housing = {} housing['points'] = user_dict['account'].housingPoints housing['room'] = user_dict['account'].roomNumber housing['queue_pos'] = get_queue_position(user_dict['account'].uid) @@ -85,6 +92,18 @@ def display_dashboard(user_dict=None): data['major_projects_count'] = len(data['major_projects']) + # technical seminar total + t_seminars = [s.seminar_id for s in + MemberSeminarAttendance.query.filter( + MemberSeminarAttendance.uid == user_dict['account'].uid, + ) if is_seminar_attendance_valid(s)] + data['ts_total'] = len(t_seminars) + attendance = [m.name for m in TechnicalSeminar.query.filter( + TechnicalSeminar.id.in_(t_seminars) + )] + + data['ts_list'] = attendance + spring['mp_status'] = "Failed" for mp in data['major_projects']: if mp['status'] == "Pending": diff --git a/conditional/blueprints/housing.py b/conditional/blueprints/housing.py index b4ea5466..45e29f90 100644 --- a/conditional/blueprints/housing.py +++ b/conditional/blueprints/housing.py @@ -20,7 +20,7 @@ @housing_bp.route('/housing') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_housing(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -62,7 +62,7 @@ def display_housing(user_dict=None): @housing_bp.route('/housing/in_queue', methods=['PUT']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def change_queue_state(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -75,11 +75,11 @@ def change_queue_state(user_dict=None): if uid: if post_data.get('inQueue', False): - log.info('Add {} to Housing Queue'.format(uid)) + log.info(f'Add {uid} to Housing Queue') queue_obj = InHousingQueue(uid=uid) db.session.add(queue_obj) else: - log.info('Remove {} from Housing Queue'.format(uid)) + log.info(f'Remove {uid} from Housing Queue') InHousingQueue.query.filter_by(uid=uid).delete() db.session.flush() @@ -88,7 +88,7 @@ def change_queue_state(user_dict=None): @housing_bp.route('/housing/update/', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def change_room_numbers(rmnumber, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -107,21 +107,21 @@ def change_room_numbers(rmnumber, user_dict=None): if occupant != "": account = ldap_get_member(occupant) account.roomNumber = rmnumber - log.info('{} assigned to room {}'.format(occupant, rmnumber)) + log.info(f'{occupant} assigned to room {rmnumber}') ldap_set_active(account) - log.info('{} marked as active because of room assignment'.format(occupant)) + log.info(f'{occupant} marked as active because of room assignment') # Delete any old occupants that are no longer in room. for old_occupant in [account for account in current_students if ldap_get_roomnumber(account) == str(rmnumber) and account.uid not in update["occupants"]]: - log.info('{} removed from room {}'.format(old_occupant.uid, old_occupant.roomNumber)) + log.info(f'{old_occupant.uid} removed from room {old_occupant.roomNumber}') old_occupant.roomNumber = None return jsonify({"success": True}), 200 @housing_bp.route('/housing/room/', methods=['GET']) -@auth.oidc_auth +@auth.oidc_auth("default") def get_occupants(rmnumber): # Get the current list of people living on-floor. @@ -134,7 +134,7 @@ def get_occupants(rmnumber): @housing_bp.route('/housing', methods=['DELETE']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def clear_all_rooms(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -146,6 +146,6 @@ def clear_all_rooms(user_dict=None): # Find the current occupants and clear them. for occupant in current_students: - log.info('{} removed from room {}'.format(occupant.uid, occupant.roomNumber)) + log.info(f'{occupant.uid} removed from room {occupant.roomNumber}') occupant.roomNumber = None return jsonify({"success": True}), 200 diff --git a/conditional/blueprints/intro_evals.py b/conditional/blueprints/intro_evals.py index 7cd276b0..3349bcb5 100644 --- a/conditional/blueprints/intro_evals.py +++ b/conditional/blueprints/intro_evals.py @@ -25,7 +25,7 @@ @intro_evals_bp.route('/intro_evals/') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_intro_evals(internal=False, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) diff --git a/conditional/blueprints/intro_evals_form.py b/conditional/blueprints/intro_evals_form.py index 94efa60b..d90f161a 100644 --- a/conditional/blueprints/intro_evals_form.py +++ b/conditional/blueprints/intro_evals_form.py @@ -15,7 +15,7 @@ @intro_evals_form_bp.route('/intro_evals_form/') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_intro_evals_form(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -36,7 +36,7 @@ def display_intro_evals_form(user_dict=None): @intro_evals_form_bp.route('/intro_evals/submit', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def submit_intro_evals(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) diff --git a/conditional/blueprints/logs.py b/conditional/blueprints/logs.py index 1e78fc18..92b671be 100644 --- a/conditional/blueprints/logs.py +++ b/conditional/blueprints/logs.py @@ -14,7 +14,7 @@ @log_bp.route('/logs') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_logs(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) diff --git a/conditional/blueprints/major_project_submission.py b/conditional/blueprints/major_project_submission.py index 137b1067..6fb9649f 100644 --- a/conditional/blueprints/major_project_submission.py +++ b/conditional/blueprints/major_project_submission.py @@ -1,16 +1,28 @@ -import structlog +import json +import os + +import requests +import boto3 -from flask import Blueprint, request, jsonify, redirect +from flask import Blueprint +from flask import request +from flask import jsonify +from flask import redirect from sqlalchemy import desc +import structlog +from werkzeug.utils import secure_filename + +from conditional.util.context_processors import get_member_name + from conditional.models.models import MajorProject from conditional.util.ldap import ldap_is_eval_director from conditional.util.ldap import ldap_get_member from conditional.util.flask import render_template -from conditional import db, start_of_year, get_user, auth +from conditional import db, start_of_year, get_user, auth, app logger = structlog.get_logger() @@ -18,7 +30,7 @@ @major_project_bp.route('/major_project/') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_major_project(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -45,29 +57,85 @@ def display_major_project(user_dict=None): major_projects_len=major_projects_len, username=user_dict['username']) +@major_project_bp.route('/major_project/upload', methods=['POST']) +@auth.oidc_auth("default") +@get_user +def upload_major_project_files(user_dict=None): + log = logger.new(request=request, auth_dict=user_dict) + log.info('Uploading Major Project File(s)') + + + if len(list(request.files.keys())) < 1: + return "No file", 400 + + # Temporarily save files to a place, to be uploaded on submit + + for _, file in request.files.lists(): + file = file[0] # remove it from the list because this is not the best + safe_name = secure_filename(file.filename) + filename = f"/tmp/{user_dict['username']}/{safe_name}" + + os.makedirs(os.path.dirname(filename), exist_ok=True) + file.save(filename) + + return jsonify({"success": True}), 200 + + @major_project_bp.route('/major_project/submit', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def submit_major_project(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) log.info('Submit Major Project') post_data = request.get_json() + print(post_data) name = post_data['projectName'] + tldr = post_data['projectTldr'] + time_spent = post_data['projectTimeSpent'] + skills = post_data['projectSkills'] description = post_data['projectDescription'] - if name == "" or description == "": + user_id = user_dict['username'] + + print(skills) + + if name == "" or len(description.strip().split()) < 50: # check for 50 word minimum return jsonify({"success": False}), 400 - project = MajorProject(user_dict['username'], name, description) + project = MajorProject(user_id, name, tldr, time_spent, description) db.session.add(project) db.session.commit() + + # This allows us to get a project with a database ID + project = MajorProject.query.filter( + MajorProject.name == name and MajorProject.uid == user_id + ).first() + + if project is None: + return jsonify({"success": False}), 500 + + # Don't send slack ping until after we are sure the DB worked fine + send_slack_ping({"text":f" *{get_member_name(user_id)}* ({user_id})" + f" submitted their major project, *{name}*!"}) + + # Acquire S3 Bucket instance + s3 = boto3.resource("s3", endpoint_url="https://s3.csh.rit.edu") + bucket = s3.create_bucket(Bucket="major-project-media") + # Collect all the locally cached files and put them in the bucket + for file in os.listdir(f"/tmp/{user_id}"): + filepath = f"/tmp/{user_id}/{file}" + print(filepath) + bucket.upload_file(filepath, f"{project.id}-{file}") + os.remove(filepath) + os.rmdir(f"/tmp/{user_id}") + return jsonify({"success": True}), 200 @major_project_bp.route('/major_project/review', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def major_project_review(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -79,7 +147,7 @@ def major_project_review(user_dict=None): pid = post_data['id'] status = post_data['status'] - log.info('{} Major Project ID: {}'.format(status, pid)) + log.info(f'{status} Major Project ID: {pid}') print(post_data) MajorProject.query.filter( @@ -94,11 +162,11 @@ def major_project_review(user_dict=None): @major_project_bp.route('/major_project/delete/', methods=['DELETE']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def major_project_delete(pid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - log.info('Delete Major Project ID: {}'.format(pid)) + log.info(f'Delete Major Project ID: {pid}') major_project = MajorProject.query.filter( MajorProject.id == pid @@ -114,3 +182,6 @@ def major_project_delete(pid, user_dict=None): return jsonify({"success": True}), 200 return "Must be project owner to delete!", 401 + +def send_slack_ping(payload): + requests.post(app.config['WEBHOOK_URL'], json.dumps(payload), timeout=120) diff --git a/conditional/blueprints/member_management.py b/conditional/blueprints/member_management.py index 3f5d3d90..1927e440 100644 --- a/conditional/blueprints/member_management.py +++ b/conditional/blueprints/member_management.py @@ -1,7 +1,7 @@ import csv import io from datetime import datetime -from distutils.util import strtobool # pylint: disable=no-name-in-module,import-error +from distutils.util import strtobool # pylint: disable=no-name-in-module,import-error,deprecated-module import structlog from flask import Blueprint, request, jsonify, make_response @@ -52,7 +52,7 @@ @member_management_bp.route('/manage') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_member_management(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -100,7 +100,7 @@ def display_member_management(user_dict=None): @member_management_bp.route('/manage/settings', methods=['PUT']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def member_management_eval(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -111,14 +111,14 @@ def member_management_eval(user_dict=None): post_data = request.get_json() if 'siteLockdown' in post_data: - log.info('Changed Site Lockdown: {}'.format(post_data['siteLockdown'])) + log.info(f'Changed Site Lockdown: {post_data['siteLockdown']}') EvalSettings.query.update( { 'site_lockdown': post_data['siteLockdown'] }) if 'introForm' in post_data: - log.info('Changed Intro Form: {}'.format(post_data['introForm'])) + log.info(f'Changed Intro Form: {post_data['introForm']}') EvalSettings.query.update( { 'intro_form_active': post_data['introForm'] @@ -130,7 +130,7 @@ def member_management_eval(user_dict=None): @member_management_bp.route('/manage/accept_dues_until', methods=['PUT']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def member_management_financial(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -142,7 +142,7 @@ def member_management_financial(user_dict=None): if 'acceptDuesUntil' in post_data: date = datetime.strptime(post_data['acceptDuesUntil'], "%Y-%m-%d") - log.info('Changed Dues Accepted Until: {}'.format(date)) + log.info(f'Changed Dues Accepted Until: {date}') EvalSettings.query.update( { 'accept_dues_until': date @@ -154,7 +154,7 @@ def member_management_financial(user_dict=None): @member_management_bp.route('/manage/user', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def member_management_adduser(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -167,7 +167,7 @@ def member_management_adduser(user_dict=None): name = post_data['name'] onfloor_status = post_data['onfloor'] room_number = post_data['roomNumber'] - log.info('Create Freshman Account for {}'.format(name)) + log.info(f'Create Freshman Account for {name}') # empty room numbers should be NULL if room_number == "": @@ -180,7 +180,7 @@ def member_management_adduser(user_dict=None): @member_management_bp.route('/manage/user/upload', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def member_management_uploaduser(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -210,7 +210,7 @@ def member_management_uploaduser(user_dict=None): else: rit_username = None - log.info('Create Freshman Account for {} via CSV Upload'.format(name)) + log.info(f'Create Freshman Account for {name} via CSV Upload') db.session.add(FreshmanAccount(name, onfloor_status, room_number, None, rit_username)) db.session.flush() @@ -221,7 +221,7 @@ def member_management_uploaduser(user_dict=None): @member_management_bp.route('/manage/user/', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def member_management_edituser(uid, user_dict=None): if not ldap_is_eval_director(user_dict['account']) and not ldap_is_financial_director(user_dict['account']): @@ -248,11 +248,7 @@ def edit_uid(uid, flask_request, username): room_number = post_data['roomNumber'] onfloor_status = post_data['onfloorStatus'] housing_points = post_data['housingPoints'] - log.info('Edit {} - Room: {} On-Floor: {} Points: {}'.format( - uid, - post_data['roomNumber'], - post_data['onfloorStatus'], - post_data['housingPoints'])) + log.info(f'Edit {uid} - Room: {post_data['roomNumber']} On-Floor: {post_data['onfloorStatus']} Points: {post_data['housingPoints']}') #pylint: disable=line-too-long ldap_set_roomnumber(account, room_number) if onfloor_status: @@ -271,7 +267,7 @@ def edit_uid(uid, flask_request, username): ldap_set_housingpoints(account, housing_points) # Only update if there's a diff - log.info('Set {} Active: {}'.format(uid, active_member)) + log.info(f'Set {uid} Active: {active_member}') if ldap_is_active(account) != active_member: if active_member: ldap_set_active(account) @@ -293,12 +289,9 @@ def edit_uid(uid, flask_request, username): def edit_fid(uid, flask_request): log = logger.new(request=flask_request, auth_dict={'username': uid}) post_data = flask_request.get_json() - log.info('Edit freshman-{} - Room: {} On-Floor: {} Eval: {} SigMiss: {}'.format( - uid, - post_data['roomNumber'], - post_data['onfloorStatus'], - post_data['evalDate'], - post_data['sigMissed'])) + + log.info(f'Edit freshman-{uid} - Room: {post_data['roomNumber']} On-Floor: {post_data['onfloorStatus']} Eval: {post_data['evalDate']} SigMiss: {post_data['sigMissed']}') #pylint: disable=line-too-long + name = post_data['name'] @@ -325,11 +318,11 @@ def edit_fid(uid, flask_request): @member_management_bp.route('/manage/user/', methods=['GET']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def member_management_getuserinfo(uid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - log.info('Get {}\'s Information'.format(uid)) + log.info(f'Get {uid}\'s Information') if not ldap_is_eval_director(user_dict['account']) and not ldap_is_financial_director(user_dict['account']): return "must be eval or financial director", 403 @@ -410,11 +403,11 @@ def get_hm_date(hm_id): @member_management_bp.route('/manage/user/', methods=['DELETE']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def member_management_deleteuser(fid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - log.info('Delete freshman-{}'.format(fid)) + log.info(f'Delete freshman-{fid}') if not ldap_is_eval_director(user_dict['account']): return "must be eval director", 403 @@ -422,7 +415,7 @@ def member_management_deleteuser(fid, user_dict=None): if not fid.isdigit(): return "can only delete freshman accounts", 400 - log.info('backend', action="delete freshman account %s" % fid) + log.info('backend', action=f"delete freshman account {fid}") for fca in FreshmanCommitteeAttendance.query.filter(FreshmanCommitteeAttendance.fid == fid): db.session.delete(fca) @@ -444,7 +437,7 @@ def member_management_deleteuser(fid, user_dict=None): # user creation script. There's no reason that the evals director should ever # manually need to do this @member_management_bp.route('/manage/upgrade_user', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def member_management_upgrade_user(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -458,7 +451,7 @@ def member_management_upgrade_user(user_dict=None): uid = post_data['uid'] signatures_missed = post_data['sigsMissed'] - log.info('Upgrade freshman-{} to Account: {}'.format(fid, uid)) + log.info(f'Upgrade freshman-{fid} to Account: {uid}') acct = FreshmanAccount.query.filter( FreshmanAccount.id == fid).first() @@ -483,10 +476,7 @@ def member_management_upgrade_user(user_dict=None): db.session.add(MemberHouseMeetingAttendance( uid, fhm.meeting_id, fhm.excuse, fhm.attendance_status)) else: - log.info('Duplicate house meeting attendance! fid: {}, uid: {}, id: {}'.format( - fid, - uid, - fhm.meeting_id)) + log.info(f'Duplicate house meeting attendance! fid: {fid}, uid: {uid}, id: {fhm.meeting_id}') db.session.delete(fhm) new_account = ldap_get_member(uid) @@ -508,7 +498,7 @@ def member_management_upgrade_user(user_dict=None): @member_management_bp.route('/manage/make_user_active', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def member_management_make_user_active(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -519,18 +509,18 @@ def member_management_make_user_active(user_dict=None): return "must be current student, not in bad standing and not active", 403 ldap_set_active(user_dict['account']) - log.info("Make user {} active".format(user_dict['username'])) + log.info(f"Make user {user_dict['username']} active") clear_members_cache() return jsonify({"success": True}), 200 @member_management_bp.route('/member/', methods=['GET']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def get_member(uid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - log.info('Get {}\'s Information'.format(uid)) + log.info(f'Get {uid}\'s Information') if not ldap_is_eval_director(user_dict['account']): return "must be eval director", 403 @@ -546,7 +536,7 @@ def get_member(uid, user_dict=None): @member_management_bp.route('/manage/active', methods=['DELETE']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def clear_active_members(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -559,7 +549,7 @@ def clear_active_members(user_dict=None): # Clear the active group. for account in members: if account.uid != user_dict['username']: - log.info('Remove {} from Active Status'.format(account.uid)) + log.info(f'Remove {account.uid} from Active Status') ldap_set_inactive(account) return jsonify({"success": True}), 200 @@ -592,7 +582,7 @@ def export_active_list(): @member_management_bp.route('/manage/current/', methods=['POST', 'DELETE']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def remove_current_student(uid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -602,16 +592,16 @@ def remove_current_student(uid, user_dict=None): member = ldap_get_member(uid) if request.method == 'DELETE': - log.info('Remove {} from Current Student'.format(uid)) + log.info(f'Remove {uid} from Current Student') ldap_set_non_current_student(member) elif request.method == 'POST': - log.info('Add {} to Current Students'.format(uid)) + log.info(f'Add {uid} to Current Students') ldap_set_current_student(member) return jsonify({"success": True}), 200 @member_management_bp.route('/manage/new', methods=['GET']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def new_year(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) diff --git a/conditional/blueprints/packet.py b/conditional/blueprints/packet.py new file mode 100644 index 00000000..3a2a5337 --- /dev/null +++ b/conditional/blueprints/packet.py @@ -0,0 +1,443 @@ +import json +from datetime import datetime +from operator import itemgetter + +import structlog +from flask import Blueprint, redirect, render_template, request, session + +from conditional import auth, app, db +from conditional.util import stats as stats_module +from conditional.util.context_processors import get_freshman_name +from conditional.util.mail import send_report_mail +from conditional.util.auth import get_user, needs_auth +from conditional.util.ldap import ldap_is_eval_director +from conditional.util.packet import ( + create_new_packets, + sync_freshman_list, + sync_with_ldap, +) +from conditional.models.models import ( + MiscSignature, + Packet, + Freshman, +) + +logger = structlog.get_logger() + +packet_bp = Blueprint("packet_bp", __name__, url_prefix="/packet") + + +class POSTFreshman: + def __init__(self, freshman): + self.name = freshman["name"].strip() + self.rit_username = freshman["rit_username"].strip() + self.onfloor = freshman["onfloor"].strip() == "TRUE" + + +@packet_bp.route("/admin/packets") +@auth.oidc_auth("default") +@get_user +def admin_packets(user_dict=None): + if not ldap_is_eval_director(user_dict["account"]): + return redirect("/dashboard") + + 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( + user_dict["username"], session["provider"] == "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=user_dict + ) + + +@packet_bp.route("/admin/freshmen") +@auth.oidc_auth("default") +@get_user +def admin_freshmen(user_dict=None): + if not ldap_is_eval_director(user_dict["account"]): + return redirect("/dashboard") + + all_freshmen = Freshman.get_all() + + return render_template( + "admin_freshmen.html", all_freshmen=all_freshmen, info=user_dict + ) + + +@packet_bp.route("/api/v1/freshmen", methods=["POST"]) +@auth.oidc_auth("default") +@get_user +def sync_freshman(user_dict=None): + """ + Create or update freshmen entries from a list + + Body parameters: [ + { + rit_username: string + name: string + onfloor: boolean + } + ] + """ + + # Only allow evals to create new frosh + if not ldap_is_eval_director(user_dict["account"]): + redirect("/dashboard") + + freshmen_in_post = { + freshman.rit_username: freshman for freshman in map(POSTFreshman, request.json) + } + sync_freshman_list(freshmen_in_post) + return json.dumps("Done"), 200 + + +@packet_bp.route("/api/v1/packets", methods=["POST"]) +@auth.oidc_auth("default") +@get_user +def create_packet(user_dict=None): + """ + Create a new packet. + + Body parameters: { + start_date: the start date of the packets in MM/DD/YYYY format + freshmen: [ + { + rit_username: string + name: string + onfloor: boolean + } + ] + } + """ + + # Only allow evals to create new packets + if not ldap_is_eval_director(user_dict["account"]): + redirect("/dashboard") + + base_date = datetime.strptime(request.json["start_date"], "%m/%d/%Y").date() + + freshmen_in_post = { + freshman.rit_username: freshman + for freshman in map(POSTFreshman, request.json["freshmen"]) + } + + create_new_packets(base_date, freshmen_in_post) + + return json.dumps("Done"), 201 + + +@packet_bp.route("/api/v1/sync", methods=["POST"]) +@auth.oidc_auth("default") +@get_user +def sync_ldap(user_dict=None): + # Only allow evals to sync ldap + if not ldap_is_eval_director(user_dict["account"]): + redirect("/dashboard") + sync_with_ldap() + return json.dumps("Done"), 201 + + +@packet_bp.route("/api/v1/packets/", methods=["GET"]) +@auth.oidc_auth("default") +@get_user +def get_packets_by_user(username: str, user_dict=None) -> dict: + """ + Return a dictionary of packets for a freshman by username, giving packet start and end date by packet id + """ + + if user_dict["ritdn"] != username: + redirect("/dashboard") + frosh = Freshman.by_username(username) + + return { + packet.id: { + "start": packet.start, + "end": packet.end, + } + for packet in frosh.packets + } + + +@packet_bp.route("/api/v1/packets//newest", methods=["GET"]) +@auth.oidc_auth("default") +@get_user +def get_newest_packet_by_user(username: str, user_dict=None) -> dict: + """ + Return a user's newest packet + """ + + if not user_dict["is_upper"] and user_dict["ritdn"] != username: + redirect("/dashboard") + + frosh = Freshman.by_username(username) + + packet = frosh.packets[-1] + + return { + packet.id: { + "start": packet.start, + "end": packet.end, + "required": vars(packet.signatures_required()), + "received": vars(packet.signatures_received()), + } + } + + +@packet_bp.route("/api/v1/packet/", methods=["GET"]) +@auth.oidc_auth("default") +@get_user +def get_packet_by_id(packet_id: int, user_dict=None) -> dict: + """ + Return the scores of the packet in question + """ + + packet = Packet.by_id(packet_id) + + if user_dict["ritdn"] != packet.freshman.rit_username: + redirect("/dashboard") + + return { + "required": vars(packet.signatures_required()), + "received": vars(packet.signatures_received()), + } + + +@packet_bp.route("/api/v1/sign//", methods=["POST"]) +@needs_auth +def sign(packet_id, user_dict=None): + packet = Packet.by_id(packet_id) + + if packet is not None and packet.is_open(): + if session["provider"] == "csh": + # Check if the CSHer is an upperclassman and if so, sign that row + for sig in filter( + lambda sig: sig.member == user_dict["uid"], packet.upper_signatures + ): + sig.signed = True + app.logger.info( + f"Member {user_dict['uid']} signed packet {packet_id} as an upperclassman" + ) + return commit_sig(packet) + + # The CSHer is a misc so add a new row + db.session.add(MiscSignature(packet=packet, member=user_dict["uid"])) + app.logger.info( + f"Member {user_dict['uid']} signed packet {packet_id} as a misc" + ) + return commit_sig(packet) + if session["provider"] == "frosh": + # Check if the freshman is onfloor and if so, sign that row + for sig in filter( + lambda sig: sig.freshman_username == user_dict["uid"], + packet.fresh_signatures, + ): + sig.signed = True + app.logger.info( + f"Freshman {user_dict['uid']} signed packet {packet_id}" + ) + return commit_sig(packet) + + app.logger.warning( + f"Failed to add {user_dict['uid']}'s signature to packet {packet_id}" + ) + return "Error: Signature not valid. Reason: Unknown" + + +@packet_bp.route("/api/v1/report/", methods=["POST"]) +@needs_auth +def report(user_dict=None): + if session["provider"] != "frosh": + return "Failure", 403 + + form_results = request.form + send_report_mail(form_results, get_freshman_name(user_dict["username"])) + return "Success: " + get_freshman_name(user_dict["username"]) + " sent a report" + + +@packet_bp.route("/api/v1/stats/packet/") +@auth.oidc_auth("default") +@get_user +def packet_stats(packet_id, user_dict=None): + if user_dict["ritdn"] != Packet.by_id(packet_id).freshman.rit_username: + return redirect("/dashboard") + return stats_module.packet_stats(packet_id) + + +@packet_bp.route("/api/v1/stats/upperclassman/") +@auth.oidc_auth("default") +@get_user +def upperclassman_stats(uid): + return stats_module.upperclassman_stats(uid) + + +def commit_sig(packet): + db.session.commit() + + return "Success: Signed Packet: " + packet.freshman_username + + +@packet_bp.route("/packet//") +@needs_auth +def freshman_packet(packet_id, user_dict=None): + packet = Packet.by_id(packet_id) + + if packet is None: + return "Invalid packet or freshman", 404 + + # The current user's freshman signature on this packet + fresh_sig = list( + filter( + lambda sig: ( + sig.freshman_username == user_dict["ritdn"] if user_dict else "" + ), + packet.fresh_signatures, + ) + ) + + return render_template( + "packet.html", + info=user_dict, + packet=packet, + did_sign=packet.did_sign(user_dict["uid"], session["provider"] == "csh"), + required=packet.signatures_required(), + received=packet.signatures_received(), + upper=packet.upper_signatures, + fresh_sig=fresh_sig, + ) + + +def packet_sort_key(packet): + """ + Utility function for generating keys for sorting packets + """ + return ( + packet.freshman.name, + -packet.signatures_received_result.total, + not packet.did_sign_result, + ) + + +@packet_bp.route("/packets/") +@needs_auth +def packets(user_dict=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( + user_dict["uid"], session["provider"] == "csh" + ) + packet.signatures_received_result = packet.signatures_received() + packet.signatures_required_result = packet.signatures_required() + + open_packets.sort(key=packet_sort_key) + + return render_template("active_packets.html", info=user_dict, packets=open_packets) + + +@packet_bp.route("/") +def index(): + return """ +

Hello, world! 2

+ Click here 4 frosh + Click here 4 upper + """ + + +@packet_bp.route("/upperclassmen/") +@auth.oidc_auth("default") +@get_user +def upperclassmen_total(user_dict=None): + open_packets = Packet.open_packets() + + # Sum up the signed packets per upperclassman + upperclassmen = {} + misc = {} + for packet in open_packets: + for sig in packet.upper_signatures: + if sig.member not in upperclassmen: + upperclassmen[sig.member] = 0 + + if sig.signed: + upperclassmen[sig.member] += 1 + for sig in packet.misc_signatures: + misc[sig.member] = 1 + misc.get(sig.member, 0) + + return render_template( + "upperclassmen_totals.html", + info=user_dict, + num_open_packets=len(open_packets), + upperclassmen=sorted(upperclassmen.items(), key=itemgetter(1), reverse=True), + misc=sorted(misc.items(), key=itemgetter(1), reverse=True), + ) + + +@packet_bp.route("/stats/packet/") +@auth.oidc_auth("default") +@get_user +def packet_graphs(packet_id, user_dict=None): + stats = packet_stats(packet_id) + fresh = [] + misc = [] + upper = [] + + # Make a rolling sum of signatures over time + def agg(l, attr, date): + l.append((l[-1] if l else 0) + len(stats["dates"][date][attr])) + + dates = list(stats["dates"].keys()) + for date in dates: + agg(fresh, "fresh", date) + agg(misc, "misc", date) + agg(upper, "upper", date) + + # Stack misc and upper on top of fresh for a nice stacked line graph + for i in range(len(dates)): + misc[i] = misc[i] + fresh[i] + upper[i] = upper[i] + misc[i] + + return render_template( + "packet_stats.html", + info=user_dict, + data=json.dumps( + { + "dates": dates, + "accum": { + "fresh": fresh, + "misc": misc, + "upper": upper, + }, + "daily": {}, + } + ), + fresh=stats["freshman"], + packet=Packet.by_id(packet_id), + ) + + +@packet_bp.route("/auth/csh") +@auth.oidc_auth("default") +def csh_login(): + session["provider"] = "csh" + return redirect("/packet", code=301) + + +@packet_bp.route("/auth/frosh") +@auth.oidc_auth("frosh") +def frosh_login(): + session["provider"] = "frosh" + return redirect("/packet", code=301) + + +@packet_bp.route("/logout") +@auth.oidc_logout +def logout(): + return redirect("/", 302) diff --git a/conditional/blueprints/slideshow.py b/conditional/blueprints/slideshow.py index c745e24c..1ea953d3 100644 --- a/conditional/blueprints/slideshow.py +++ b/conditional/blueprints/slideshow.py @@ -20,7 +20,7 @@ @slideshow_bp.route('/slideshow/intro') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def slideshow_intro_display(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -36,7 +36,7 @@ def slideshow_intro_display(user_dict=None): @slideshow_bp.route('/slideshow/intro/members') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def slideshow_intro_members(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -49,7 +49,7 @@ def slideshow_intro_members(user_dict=None): @slideshow_bp.route('/slideshow/intro/review', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def slideshow_intro_review(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -61,7 +61,7 @@ def slideshow_intro_review(user_dict=None): uid = post_data['uid'] status = post_data['status'] - log.info('Intro Eval for {}: {}'.format(uid, status)) + log.info(f'Intro Eval for {uid}: {status}') FreshmanEvalData.query.filter( FreshmanEvalData.uid == uid and FreshmanEvalData.active). \ @@ -76,7 +76,7 @@ def slideshow_intro_review(user_dict=None): @slideshow_bp.route('/slideshow/spring') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def slideshow_spring_display(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -92,7 +92,7 @@ def slideshow_spring_display(user_dict=None): @slideshow_bp.route('/slideshow/spring/members') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def slideshow_spring_members(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -105,7 +105,7 @@ def slideshow_spring_members(user_dict=None): @slideshow_bp.route('/slideshow/spring/review', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def slideshow_spring_review(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -117,7 +117,7 @@ def slideshow_spring_review(user_dict=None): uid = post_data['uid'] status = post_data['status'] - log.info('Spring Eval for {}: {}'.format(uid, status)) + log.info(f'Spring Eval for {uid}: {status}') SpringEval.query.filter( SpringEval.uid == uid and diff --git a/conditional/blueprints/spring_evals.py b/conditional/blueprints/spring_evals.py index e8c6e1db..212ca88e 100644 --- a/conditional/blueprints/spring_evals.py +++ b/conditional/blueprints/spring_evals.py @@ -17,7 +17,7 @@ @spring_evals_bp.route('/spring_evals/') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_spring_evals(internal=False, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) diff --git a/conditional/models/models.py b/conditional/models/models.py index e3124f2a..5bb3dd64 100644 --- a/conditional/models/models.py +++ b/conditional/models/models.py @@ -1,15 +1,27 @@ +from typing import cast, Optional import time from datetime import date, timedelta, datetime -from sqlalchemy import Column, Integer, String, Enum, ForeignKey, DateTime, \ - Date, Text, Boolean +from itertools import chain +from sqlalchemy import ( + Column, + Integer, + String, + Enum, + ForeignKey, + DateTime, + Date, + Text, + Boolean, +) from sqlalchemy.dialects import postgresql +from sqlalchemy.orm import relationship from conditional import db -attendance_enum = Enum('Attended', 'Excused', 'Absent', name='attendance_enum') +attendance_enum = Enum("Attended", "Excused", "Absent", name="attendance_enum") class FreshmanAccount(db.Model): - __tablename__ = 'freshman_accounts' + __tablename__ = "freshman_accounts" id = Column(Integer, primary_key=True) name = Column(String(64), nullable=False) eval_date = Column(Date, nullable=False) @@ -18,7 +30,9 @@ class FreshmanAccount(db.Model): signatures_missed = Column(Integer) rit_username = Column(String(10), nullable=True) - def __init__(self, name, onfloor, room=None, missed=None, rit_username=None): + def __init__( + self, name, onfloor, room=None, missed=None, rit_username=None + ): # pylint: disable=too-many-positional-arguments self.name = name today = date.fromtimestamp(time.time()) self.eval_date = today + timedelta(weeks=6) @@ -29,23 +43,25 @@ def __init__(self, name, onfloor, room=None, missed=None, rit_username=None): class FreshmanEvalData(db.Model): - __tablename__ = 'freshman_eval_data' + __tablename__ = "freshman_eval_data" id = Column(Integer, primary_key=True) uid = Column(String(32), nullable=False) - freshman_project = Column(Enum('Pending', 'Passed', 'Failed', - name="freshman_project_enum"), nullable=True) + freshman_project = Column( + Enum("Pending", "Passed", "Failed", name="freshman_project_enum"), nullable=True + ) eval_date = Column(DateTime, nullable=False) signatures_missed = Column(Integer, nullable=False) social_events = Column(Text) other_notes = Column(Text) - freshman_eval_result = Column(Enum('Pending', 'Passed', 'Failed', - name="freshman_eval_enum"), nullable=False) + freshman_eval_result = Column( + Enum("Pending", "Passed", "Failed", name="freshman_eval_enum"), nullable=False + ) active = Column(Boolean) def __init__(self, uid, signatures_missed): self.uid = uid self.freshman_project = None - self.freshman_eval_result = 'Pending' + self.freshman_eval_result = "Pending" self.signatures_missed = signatures_missed self.social_events = "" self.other_notes = "" @@ -53,11 +69,24 @@ def __init__(self, uid, signatures_missed): class CommitteeMeeting(db.Model): - __tablename__ = 'committee_meetings' + __tablename__ = "committee_meetings" id = Column(Integer, primary_key=True) - committee = Column(Enum('Evaluations', 'History', 'Social', 'Opcomm', - 'R&D', 'House Improvements', 'Financial', 'Chairman', 'Ad-Hoc', name="committees_enum"), - nullable=False) + committee = Column( + Enum( + "Evaluations", + "History", + "Social", + "Opcomm", + "R&D", + "House Improvements", + "Financial", + "Public Relations", + "Chairman", + "Ad-Hoc", + name="committees_enum", + ), + nullable=False, + ) timestamp = Column(DateTime, nullable=False) approved = Column(Boolean, nullable=False) active = Column(Boolean) @@ -70,10 +99,10 @@ def __init__(self, committee, timestamp, approved): class MemberCommitteeAttendance(db.Model): - __tablename__ = 'member_committee_attendance' + __tablename__ = "member_committee_attendance" id = Column(Integer, primary_key=True) uid = Column(String(32), nullable=False) - meeting_id = Column(ForeignKey('committee_meetings.id'), nullable=False) + meeting_id = Column(ForeignKey("committee_meetings.id"), nullable=False) def __init__(self, uid, meeting_id): self.uid = uid @@ -81,10 +110,10 @@ def __init__(self, uid, meeting_id): class FreshmanCommitteeAttendance(db.Model): - __tablename__ = 'freshman_committee_attendance' + __tablename__ = "freshman_committee_attendance" id = Column(Integer, primary_key=True) - fid = Column(ForeignKey('freshman_accounts.id'), nullable=False) - meeting_id = Column(ForeignKey('committee_meetings.id'), nullable=False) + fid = Column(ForeignKey("freshman_accounts.id"), nullable=False) + meeting_id = Column(ForeignKey("committee_meetings.id"), nullable=False) def __init__(self, fid, meeting_id): self.fid = fid @@ -92,7 +121,7 @@ def __init__(self, fid, meeting_id): class TechnicalSeminar(db.Model): - __tablename__ = 'technical_seminars' + __tablename__ = "technical_seminars" id = Column(Integer, primary_key=True) name = Column(String(128), nullable=False) timestamp = Column(DateTime, nullable=False) @@ -107,10 +136,10 @@ def __init__(self, name, timestamp, approved): class MemberSeminarAttendance(db.Model): - __tablename__ = 'member_seminar_attendance' + __tablename__ = "member_seminar_attendance" id = Column(Integer, primary_key=True) uid = Column(String(32), nullable=False) - seminar_id = Column(ForeignKey('technical_seminars.id'), nullable=False) + seminar_id = Column(ForeignKey("technical_seminars.id"), nullable=False) def __init__(self, uid, seminar_id): self.uid = uid @@ -118,10 +147,10 @@ def __init__(self, uid, seminar_id): class FreshmanSeminarAttendance(db.Model): - __tablename__ = 'freshman_seminar_attendance' + __tablename__ = "freshman_seminar_attendance" id = Column(Integer, primary_key=True) - fid = Column(ForeignKey('freshman_accounts.id'), nullable=False) - seminar_id = Column(ForeignKey('technical_seminars.id'), nullable=False) + fid = Column(ForeignKey("freshman_accounts.id"), nullable=False) + seminar_id = Column(ForeignKey("technical_seminars.id"), nullable=False) def __init__(self, fid, seminar_id): self.fid = fid @@ -129,28 +158,34 @@ def __init__(self, fid, seminar_id): class MajorProject(db.Model): - __tablename__ = 'major_projects' + __tablename__ = "major_projects" id = Column(Integer, primary_key=True) date = Column(Date, nullable=False) uid = Column(String(32), nullable=False) name = Column(String(64), nullable=False) - description = Column(Text) + tldr = Column(String(128), nullable=False) + time = Column(Text, nullable=False) + description = Column(Text, nullable=False) active = Column(Boolean, nullable=False) - status = Column(Enum('Pending', 'Passed', 'Failed', - name="major_project_enum"), - nullable=False) + status = Column( + Enum("Pending", "Passed", "Failed", name="major_project_enum"), nullable=False + ) - def __init__(self, uid, name, desc): + def __init__( + self, uid, name, tldr, time, desc + ): # pylint: disable=too-many-positional-arguments,redefined-outer-name self.uid = uid self.date = datetime.now() self.name = name + self.tldr = tldr + self.time = time self.description = desc - self.status = 'Pending' + self.status = "Pending" self.active = True class HouseMeeting(db.Model): - __tablename__ = 'house_meetings' + __tablename__ = "house_meetings" id = Column(Integer, primary_key=True) date = Column(Date, nullable=False) active = Column(Boolean, nullable=False) @@ -161,10 +196,10 @@ def __init__(self, hm_date): class MemberHouseMeetingAttendance(db.Model): - __tablename__ = 'member_hm_attendance' + __tablename__ = "member_hm_attendance" id = Column(Integer, primary_key=True) uid = Column(String(32), nullable=False) - meeting_id = Column(ForeignKey('house_meetings.id'), nullable=False) + meeting_id = Column(ForeignKey("house_meetings.id"), nullable=False) excuse = Column(Text) attendance_status = Column(attendance_enum) @@ -176,10 +211,10 @@ def __init__(self, uid, meeting_id, excuse, status): class FreshmanHouseMeetingAttendance(db.Model): - __tablename__ = 'freshman_hm_attendance' + __tablename__ = "freshman_hm_attendance" id = Column(Integer, primary_key=True) - fid = Column(ForeignKey('freshman_accounts.id'), nullable=False) - meeting_id = Column(ForeignKey('house_meetings.id'), nullable=False) + fid = Column(ForeignKey("freshman_accounts.id"), nullable=False) + meeting_id = Column(ForeignKey("house_meetings.id"), nullable=False) excuse = Column(Text) attendance_status = Column(attendance_enum) @@ -191,11 +226,11 @@ def __init__(self, fid, meeting_id, excuse, status): class CurrentCoops(db.Model): - __tablename__ = 'current_coops' + __tablename__ = "current_coops" id = Column(Integer, primary_key=True) uid = Column(String(32), nullable=False) date_created = Column(Date, nullable=False) - semester = Column(Enum('Fall', 'Spring', name="co_op_enum"), nullable=False) + semester = Column(Enum("Fall", "Spring", name="co_op_enum"), nullable=False) def __init__(self, uid, semester): self.uid = uid @@ -205,7 +240,7 @@ def __init__(self, uid, semester): class OnFloorStatusAssigned(db.Model): - __tablename__ = 'onfloor_datetime' + __tablename__ = "onfloor_datetime" uid = Column(String(32), primary_key=True) onfloor_granted = Column(DateTime, primary_key=True) @@ -215,20 +250,22 @@ def __init__(self, uid, time_granted): class Conditional(db.Model): - __tablename__ = 'conditional' + __tablename__ = "conditional" id = Column(Integer, primary_key=True) uid = Column(String(32), nullable=False) description = Column(String(512), nullable=False) date_created = Column(Date, nullable=False) date_due = Column(Date, nullable=False) active = Column(Boolean, nullable=False) - status = Column(Enum('Pending', 'Passed', 'Failed', - name="conditional_enum"), - nullable=False) - s_evaluation = Column(ForeignKey('spring_evals.id')) - i_evaluation = Column(ForeignKey('freshman_eval_data.id')) - - def __init__(self, uid, description, due, s_eval=None, i_eval=None): + status = Column( + Enum("Pending", "Passed", "Failed", name="conditional_enum"), nullable=False + ) + s_evaluation = Column(ForeignKey("spring_evals.id")) + i_evaluation = Column(ForeignKey("freshman_eval_data.id")) + + def __init__( + self, uid, description, due, s_eval=None, i_eval=None + ): # pylint: disable=too-many-positional-arguments self.uid = uid self.description = description self.date_due = due @@ -240,7 +277,7 @@ def __init__(self, uid, description, due, s_eval=None, i_eval=None): class EvalSettings(db.Model): - __tablename__ = 'settings' + __tablename__ = "settings" id = Column(Integer, primary_key=True) housing_form_active = Column(Boolean) intro_form_active = Column(Boolean) @@ -255,14 +292,14 @@ def __init__(self): class SpringEval(db.Model): - __tablename__ = 'spring_evals' + __tablename__ = "spring_evals" id = Column(Integer, primary_key=True) uid = Column(String(32), nullable=False) active = Column(Boolean, nullable=False) date_created = Column(Date, nullable=False) - status = Column(Enum('Pending', 'Passed', 'Failed', - name="spring_eval_enum"), - nullable=False) + status = Column( + Enum("Pending", "Passed", "Failed", name="spring_eval_enum"), nullable=False + ) def __init__(self, uid): self.uid = uid @@ -272,13 +309,26 @@ def __init__(self, uid): class InHousingQueue(db.Model): - __tablename__ = 'in_housing_queue' + __tablename__ = "in_housing_queue" uid = Column(String(32), primary_key=True) -http_enum = Enum('GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH', name='http_enum') + +http_enum = Enum( + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "CONNECT", + "OPTIONS", + "TRACE", + "PATCH", + name="http_enum", +) + class UserLog(db.Model): - __tablename__ = 'user_log' + __tablename__ = "user_log" id = Column(Integer, primary_key=True) ipaddr = Column(postgresql.INET, nullable=False) timestamp = Column(DateTime, nullable=False) @@ -288,7 +338,9 @@ class UserLog(db.Model): path = Column(String(128), nullable=False) description = Column(String(128), nullable=False) - def __init__(self, ipaddr, user, method, blueprint, path, description): + def __init__( + self, ipaddr, user, method, blueprint, path, description + ): # pylint: disable=too-many-positional-arguments self.ipaddr = ipaddr self.timestamp = datetime.now() self.uid = user @@ -296,3 +348,213 @@ def __init__(self, ipaddr, user, method, blueprint, path, description): self.blueprint = blueprint self.path = path self.description = description + + +# The required number of honorary member, advisor, and alumni signatures +REQUIRED_MISC_SIGNATURES = 10 + + +class SigCounts: + """ + Utility class for returning counts of signatures broken out by type + """ + + def __init__(self, upper: int, fresh: int, misc: int): + # Base fields + self.upper = upper + self.fresh = fresh + self.misc = misc + + # Capped version of misc so it will never be greater than REQUIRED_MISC_SIGNATURES + self.misc_capped = ( + misc if misc <= REQUIRED_MISC_SIGNATURES else REQUIRED_MISC_SIGNATURES + ) + + # Totals (calculated using misc_capped) + self.member_total = upper + self.misc_capped + self.total = upper + fresh + self.misc_capped + + +class Freshman(db.Model): + __tablename__ = "freshman" + rit_username = cast(str, Column(String(10), primary_key=True)) + name = cast(str, Column(String(64), nullable=False)) + onfloor = cast(bool, Column(Boolean, nullable=False)) + fresh_signatures = cast("FreshSignature", relationship("FreshSignature")) + + # One freshman can have multiple packets if they repeat the intro process + packets = cast("Packet", relationship("Packet", order_by="desc(Packet.id)")) + + @classmethod + def by_username(cls, username: str) -> "Packet": + """ + Helper method to retrieve a freshman by their RIT username + """ + return cls.query.filter_by(rit_username=username).first() + + @classmethod + def get_all(cls) -> list["Packet"]: + """ + Helper method to get all freshmen easily + """ + return cls.query.all() + + +class Packet(db.Model): + __tablename__ = "packet" + id = cast(int, Column(Integer, primary_key=True, autoincrement=True)) + freshman_username = cast(str, Column(ForeignKey("freshman.rit_username"))) + start = cast(datetime, Column(DateTime, nullable=False)) + end = cast(datetime, Column(DateTime, nullable=False)) + + freshman = cast(Freshman, relationship("Freshman", back_populates="packets")) + + # The `lazy='subquery'` kwarg enables eager loading for signatures which makes signature calculations much faster + # See the docs here for details: https://docs.sqlalchemy.org/en/latest/orm/loading_relationships.html + upper_signatures = cast( + "UpperSignature", + relationship( + "UpperSignature", + lazy="subquery", + order_by="UpperSignature.signed.desc(), UpperSignature.updated", + ), + ) + fresh_signatures = cast( + "FreshSignature", + relationship( + "FreshSignature", + lazy="subquery", + order_by="FreshSignature.signed.desc(), FreshSignature.updated", + ), + ) + misc_signatures = cast( + "MiscSignature", + relationship( + "MiscSignature", lazy="subquery", order_by="MiscSignature.updated" + ), + ) + + def is_open(self) -> bool: + return self.start < datetime.now() < self.end + + def signatures_required(self) -> SigCounts: + """ + :return: A SigCounts instance with the fields set to the number of signatures received by this packet + """ + upper = len(self.upper_signatures) + fresh = len(self.fresh_signatures) + + return SigCounts(upper, fresh, REQUIRED_MISC_SIGNATURES) + + def signatures_received(self) -> SigCounts: + """ + :return: A SigCounts instance with the fields set to the number of required signatures for this packet + """ + 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(upper, fresh, len(self.misc_signatures)) + + def did_sign(self, username: str, is_csh: bool) -> bool: + """ + :param username: The CSH or RIT username to check for + :param is_csh: Set to True for CSH accounts and False for freshmen + :return: Boolean value for if the given account signed this packet + """ + if is_csh: + for sig in filter( + lambda sig: sig.member == username, + chain(self.upper_signatures, self.misc_signatures), + ): + if isinstance(sig, MiscSignature): + return True + return sig.signed + else: + for sig in filter( + lambda sig: sig.freshman_username == username, self.fresh_signatures + ): + return sig.signed + + # The user must be a misc CSHer that hasn't signed this packet or an off-floor freshmen + return False + + def is_100(self) -> bool: + """ + Checks if this packet has reached 100% + """ + return self.signatures_required().total == self.signatures_received().total + + @classmethod + def open_packets(cls) -> list["Packet"]: + """ + Helper method for fetching all currently open packets + """ + return cls.query.filter( + cls.start < datetime.now(), cls.end > datetime.now() + ).all() + + @classmethod + def by_id(cls, packet_id: int) -> "Packet": + """ + Helper method for fetching 1 packet by its id + """ + return cls.query.filter_by(id=packet_id).first() + + +class UpperSignature(db.Model): + __tablename__ = "signature_upper" + packet_id = cast(int, Column(Integer, ForeignKey("packet.id"), primary_key=True)) + member = cast(str, Column(String(36), primary_key=True)) + signed = cast(bool, Column(Boolean, default=False, nullable=False)) + eboard = cast(Optional[str], Column(String(12), nullable=True)) + active_rtp = cast(bool, Column(Boolean, default=False, nullable=False)) + three_da = cast(bool, Column(Boolean, default=False, nullable=False)) + webmaster = cast(bool, Column(Boolean, default=False, nullable=False)) + c_m = cast(bool, Column(Boolean, default=False, nullable=False)) + w_m = cast(bool, Column(Boolean, default=False, nullable=False)) + drink_admin = cast(bool, Column(Boolean, default=False, nullable=False)) + updated = cast( + datetime, + Column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False), + ) + + packet = cast(Packet, relationship("Packet", back_populates="upper_signatures")) + + +class FreshSignature(db.Model): + __tablename__ = "signature_fresh" + packet_id = cast(int, Column(Integer, ForeignKey("packet.id"), primary_key=True)) + freshman_username = cast( + str, Column(ForeignKey("freshman.rit_username"), primary_key=True) + ) + signed = cast(bool, Column(Boolean, default=False, nullable=False)) + updated = cast( + datetime, + Column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False), + ) + + packet = cast(Packet, relationship("Packet", back_populates="fresh_signatures")) + freshman = cast( + Freshman, relationship("Freshman", back_populates="fresh_signatures") + ) + + +class MiscSignature(db.Model): + __tablename__ = "signature_misc" + packet_id = cast(int, Column(Integer, ForeignKey("packet.id"), primary_key=True)) + member = cast(str, Column(String(36), primary_key=True)) + updated = cast( + datetime, + Column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False), + ) + + packet = cast(Packet, relationship("Packet", back_populates="misc_signatures")) + + +class NotificationSubscription(db.Model): + __tablename__ = "notification_subscriptions" + member = cast(str, Column(String(36), nullable=True)) + freshman_username = cast( + str, Column(ForeignKey("freshman.rit_username"), nullable=True) + ) + token = cast(str, Column(String(256), primary_key=True, nullable=False)) diff --git a/conditional/templates/active_packets.html b/conditional/templates/active_packets.html new file mode 100644 index 00000000..3fa65640 --- /dev/null +++ b/conditional/templates/active_packets.html @@ -0,0 +1,117 @@ +{% extends "extend/base.html" %} + +{% block body %} +
+
+
+
+

Active Packets

+
+ {% if info.is_upper %} +
+ +
+ {% endif %} +
+
+
+ {% if packets|length > 0 %} +
+
+
+
+ + + + + {% if info.is_upper %} + + + + {% endif %} + + + + + {% for packet in packets %} + + + {% if info.is_upper %} + + + + {% endif %} + + + {% endfor %} + +
NameSignaturesSignaturesSignaturesSign
+ {% if info.is_upper %} + + {% endif %} + {{ get_rit_name(packet.freshman_username) }} {{ get_rit_name(packet.freshman_username) }} + {% if info.is_upper %} + + {% endif %} + + {% if packet.signatures_received_result.member_total == packet.signatures_required_result.member_total %} + 💯 {# 100% emoji #} + {% else %} + {{ packet.signatures_received_result.member_total }} / + {{ packet.signatures_required_result.member_total }} + {% 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 #} + {% else %} + {{ packet.signatures_received_result.total }} / + {{ packet.signatures_required_result.total }} + {% endif %} + + {% if not packet.did_sign_result and info.ritdn != packet.freshman_username %} + + {% elif info.ritdn != packet.freshman_username %} + + {% endif %} +
+
+
+
+
+ {% else %} + + {% endif %} +
+
+{% endblock %} + +{% block scripts %} + {{ super() }} + {% if info.realm == "csh" %} + + {% endif %} +{% endblock %} diff --git a/conditional/templates/admin_freshmen.html b/conditional/templates/admin_freshmen.html new file mode 100644 index 00000000..e922375f --- /dev/null +++ b/conditional/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/conditional/templates/admin_packets.html b/conditional/templates/admin_packets.html new file mode 100644 index 00000000..9db36fd2 --- /dev/null +++ b/conditional/templates/admin_packets.html @@ -0,0 +1,30 @@ +{% 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/conditional/templates/attendance_cm.html b/conditional/templates/attendance_cm.html index ab15b0b8..80c26ea1 100644 --- a/conditional/templates/attendance_cm.html +++ b/conditional/templates/attendance_cm.html @@ -23,6 +23,7 @@

Meeting Attendance

+ diff --git a/conditional/templates/dashboard.html b/conditional/templates/dashboard.html index 3069b9e9..3bfd8197 100644 --- a/conditional/templates/dashboard.html +++ b/conditional/templates/dashboard.html @@ -109,6 +109,21 @@

Membership Evaluations {% endif %}

+
+ Technical Seminars {% if ts_total == 0 %} +
+ +
+ {% else %} +
+
    + {% for ts in ts_list %} +
  • {{ts}}
  • + {% endfor %} +
+
+ {% endif %} +
diff --git a/conditional/templates/error.html b/conditional/templates/error.html new file mode 100644 index 00000000..de33536b --- /dev/null +++ b/conditional/templates/error.html @@ -0,0 +1,20 @@ +{% extends 'extend/base.html' %} + +{% block body %} +
+

Oops!

+
+
+
+ I guess this is what you get when you trust a bunch of college kids. +
+

+

{{ e }}
+

+
+ Do us a favor, try again. If you end up here on the second try, shoot us an email. +
+
+
+
+{% endblock %} diff --git a/conditional/templates/extend/base.html b/conditional/templates/extend/base.html new file mode 100644 index 00000000..418e93d6 --- /dev/null +++ b/conditional/templates/extend/base.html @@ -0,0 +1,29 @@ + + + +{% block head %} + {% include "include/head.html" %} +{% endblock %} + + + +{% block nav %} + {% include "include/nav.html" %} +{% endblock %} + +{% block body %} +{% endblock %} + +{% block footer %} + {% include "include/footer.html" %} +{% endblock %} + +{% block includes %} +{% endblock %} + +{% block scripts %} + {% include "include/scripts.html" %} +{% endblock %} + + + diff --git a/conditional/templates/extend/email.html b/conditional/templates/extend/email.html new file mode 100644 index 00000000..20fb7de7 --- /dev/null +++ b/conditional/templates/extend/email.html @@ -0,0 +1,17 @@ + + + +{% block head %} + + CSH Packet + + +{% endblock %} + + +{% block body %} +{% endblock %} + + diff --git a/conditional/templates/include/admin/all_freshmen.html b/conditional/templates/include/admin/all_freshmen.html new file mode 100644 index 00000000..a3e79e3b --- /dev/null +++ b/conditional/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/conditional/templates/include/admin/new_packets.html b/conditional/templates/include/admin/new_packets.html new file mode 100644 index 00000000..c6dd3075 --- /dev/null +++ b/conditional/templates/include/admin/new_packets.html @@ -0,0 +1,23 @@ + diff --git a/conditional/templates/include/admin/open_packets.html b/conditional/templates/include/admin/open_packets.html new file mode 100644 index 00000000..2db5d4b8 --- /dev/null +++ b/conditional/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/conditional/templates/include/admin/sync_freshmen.html b/conditional/templates/include/admin/sync_freshmen.html new file mode 100644 index 00000000..6f9b4806 --- /dev/null +++ b/conditional/templates/include/admin/sync_freshmen.html @@ -0,0 +1,22 @@ + diff --git a/conditional/templates/include/footer.html b/conditional/templates/include/footer.html new file mode 100644 index 00000000..b1e9bdcf --- /dev/null +++ b/conditional/templates/include/footer.html @@ -0,0 +1,6 @@ + diff --git a/conditional/templates/include/head.html b/conditional/templates/include/head.html new file mode 100644 index 00000000..20d0f420 --- /dev/null +++ b/conditional/templates/include/head.html @@ -0,0 +1,98 @@ + + + + + + + CSH Packet + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conditional/templates/include/nav.html b/conditional/templates/include/nav.html new file mode 100644 index 00000000..6c035623 --- /dev/null +++ b/conditional/templates/include/nav.html @@ -0,0 +1,67 @@ + diff --git a/conditional/templates/include/scripts.html b/conditional/templates/include/scripts.html new file mode 100644 index 00000000..af0b7632 --- /dev/null +++ b/conditional/templates/include/scripts.html @@ -0,0 +1,15 @@ + + + + + + + + + + + + +{% if info.realm == "intro" %} + +{% endif %} diff --git a/conditional/templates/mail/packet_start.html b/conditional/templates/mail/packet_start.html new file mode 100644 index 00000000..722e5768 --- /dev/null +++ b/conditional/templates/mail/packet_start.html @@ -0,0 +1,15 @@ +{% extends "extend/email.html" %} + +{% block body %} +
+

Hello {{ packet.freshman.name }},

+

Welcome to Computer Science House!

+

Soon you'll starting the introductory process for CSH, and the first part of that is Packet.

+

Your packet will start on {{ packet.start.strftime('%A, %B %-d') }} at {{ packet.start.strftime('%-I:%M %p') }}

+

You can view your packet at {{ config["PACKET_INTRO"] }} with + the credentials you should have been sent.

+

If you don't know your credentials, reach out to an RTP

+

If you have any questions about Packet or the introductory process, email evals@csh.rit.edu

+

If you have any questions about login credentials or any technical issues, email rtp@csh.rit.edu

+
+{% endblock %} diff --git a/conditional/templates/mail/packet_start.txt b/conditional/templates/mail/packet_start.txt new file mode 100644 index 00000000..162ec147 --- /dev/null +++ b/conditional/templates/mail/packet_start.txt @@ -0,0 +1,14 @@ +Hello {{ packet.freshman.name }}, + +Welcome to Computer Science House! + +Soon you'll starting the introductory process for CSH, and the first part of that is Packet. + +Your packet will start on {{ packet.start.strftime('%A, %B %-d') }} at {{ packet.start.strftime('%-I:%M %p') }} + +You can view your packet at {{ config["PROTOCOL"] + config["PACKET_INTRO"] }} with the credentials you should have been sent. +If you don't know your credentials, reach out to an RTP + +If you have any questions about Packet or the introductory process, email evals@csh.rit.edu + +If you have any questions about login credentials or any technical issues, email rtp@csh.rit.edu diff --git a/conditional/templates/mail/report.html b/conditional/templates/mail/report.html new file mode 100644 index 00000000..dbfedcbc --- /dev/null +++ b/conditional/templates/mail/report.html @@ -0,0 +1,10 @@ +{% extends "extend/email.html" %} + +{% block body %} +
+

Hello,

+

{{ reporter }} just made a report against {{ person }}

+

The report reads:

+
{{ report }}
+
+{% endblock %} diff --git a/conditional/templates/mail/report.txt b/conditional/templates/mail/report.txt new file mode 100644 index 00000000..7a5576c7 --- /dev/null +++ b/conditional/templates/mail/report.txt @@ -0,0 +1,7 @@ +Hello, + +{{ reporter }} just made a report against {{ person }} + +The report reads: + +{{ report }} diff --git a/conditional/templates/major_project_submission.html b/conditional/templates/major_project_submission.html index 8705d2ad..256688b7 100644 --- a/conditional/templates/major_project_submission.html +++ b/conditional/templates/major_project_submission.html @@ -1,94 +1,162 @@ {% extends "nav.html" %} +{% block extraHeader %} + +{% endblock %} {% block title %} -Major Project Form + Major Project Form {% endblock %} {% block body %} -
-

Major Project Form

-
+
+

Major Project Form

-
- +

Welcome to the Major Project submission form! We're excited to read about what you've + been working on. For us (E-Board) to best evaluate your project, please give us as much detail as + possible. Don't feel pressured to write full paragraphs though, good bullet points are plenty! +
Generally, a major project is something that you make with the goal of challenging yourself, + learning new things, and doing something you would be proud of. Major projects are most likely to + pass when they meet at least 2 of the 3 + Major Project Pillars - + considerable time on your project, benefiting House, and meaningfully applying skills. And of course, + after you submit, please try to talk to E-Board members (in-person or over Slack) so we are familiar + with your project and can ask you questions!

+
+
+ +
+
+ + placeholder="A clever name for your project, sometimes people will come up with an acronym."> + + +
+
+ +
+ + List what skills you meaningfully used while working on this project (at least 2!)
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
Upload Media
+
Drag files here or click to upload.
+
+
+
+
-
+ + + +

All Major Projects

{% if major_projects_len + <=0 %}
-
-
- - -
+
+

No Pending Major Projects

- - -

All Major Projects

{% if major_projects_len - <=0 %}
-
-

No Pending Major Projects

-
-
-{% else %} + {% else %} - {% for p in major_projects %} -
-
-
-

{{p['proj_name']}}

- - {{p['name']}} ({{p['username']}}) -
-
+ {% for p in major_projects %} +
+
+
+

{{ p['proj_name'] }}

+ + {{ p['name'] }} ({{ p['username'] }}) +
+
- {% if is_eval_director %} + {% if is_eval_director %} -
- - + + {% else %} + {% if p['status'] == 'Passed' %} +
+ {% elif p['status'] == 'Failed' %} +
+ {% else %} +
+ {% endif %} + {% if p.is_owner and p['status'] == 'Pending' %} + + {% endif %} + {% endif %} +
+ +
+ {{ p['description'] }} +
- {% else %} - {% if p['status'] == 'Passed' %} -
- {% elif p['status'] == 'Failed' %} -
- {% else %} -
- {% endif %} - {% if p.is_owner and p['status'] == 'Pending' %} - - {% endif %} - {% endif %} -
- -
- {{p['description']}}
-
-
- {% endfor %} + {% endfor %} -{% endif %} + {% endif %} -
+
{% endblock %} diff --git a/conditional/templates/nav.html b/conditional/templates/nav.html index 6e4173f7..ffb3ec9d 100644 --- a/conditional/templates/nav.html +++ b/conditional/templates/nav.html @@ -54,6 +54,8 @@ +
  • Packet
  • + {% if is_eboard or is_rtp%}