From c4a03d69c617ce258aa76a249c643839385a7c48 Mon Sep 17 00:00:00 2001 From: Tyler Allen Date: Thu, 5 Feb 2026 15:47:16 -0500 Subject: [PATCH 01/27] Gatekeep fix (#465) * Fix Gatekeep to constitional requirements * Gatekeep does all constitutional things now * rename variable for legibility * fix json response * lint fix --------- Co-authored-by: Noah Hanford (spaced) --- conditional/__init__.py | 11 +++- conditional/util/member.py | 105 ++++++++++++++++++++++--------------- 2 files changed, 72 insertions(+), 44 deletions(-) diff --git a/conditional/__init__.py b/conditional/__init__.py index 1df474ed..83c02a63 100644 --- a/conditional/__init__.py +++ b/conditional/__init__.py @@ -108,7 +108,7 @@ def database_processor(logger, log_method, event_dict): # pylint: disable=unuse # pylint: disable=wrong-import-order from conditional.util import context_processors from conditional.util.auth import get_user -from conditional.util.member import gatekeep_status +from conditional.util.member import gatekeep_status, get_voting_members from .blueprints.dashboard import dashboard_bp # pylint: disable=ungrouped-imports from .blueprints.attendance import attendance_bp from .blueprints.major_project_submission import major_project_bp @@ -167,7 +167,7 @@ def health(): @app.route("/gatekeep/") -def gatekeep(username): +def gatekeep_user(username): token = request.headers.get("X-VOTE-TOKEN", "") if token != app.config["VOTE_TOKEN"]: return "Users cannot access this page", 403 @@ -178,6 +178,13 @@ def gatekeep(username): return gatekeep_data, 200 +@app.route("/gatekeep") +def gatekeep_all(): + token = request.headers.get("X-VOTE-TOKEN", "") + if token != app.config["VOTE_TOKEN"]: + return "Users cannot access this page", 403 + return list(get_voting_members()), 200 + @app.errorhandler(404) @app.errorhandler(500) diff --git a/conditional/util/member.py b/conditional/util/member.py index 8dc3d69a..b485d193 100644 --- a/conditional/util/member.py +++ b/conditional/util/member.py @@ -2,7 +2,7 @@ from sqlalchemy import func, or_ from conditional import start_of_year -from conditional.models.models import CommitteeMeeting +from conditional.models.models import CommitteeMeeting, FreshmanAccount from conditional.models.models import CurrentCoops from conditional.models.models import FreshmanEvalData from conditional.models.models import HouseMeeting @@ -21,6 +21,7 @@ from conditional.util.ldap import ldap_is_intromember from conditional.util.ldap import ldap_get_member + @service_cache(maxsize=1024) def get_members_info(): members = ldap_get_current_students() @@ -57,18 +58,18 @@ def get_freshman_data(user_name): MemberCommitteeAttendance.query.filter( MemberCommitteeAttendance.uid == user_name ) if CommitteeMeeting.query.filter( - CommitteeMeeting.id == m.meeting_id).first().approved] + CommitteeMeeting.id == m.meeting_id).first().approved] freshman['committee_meetings'] = len(c_meetings) # technical seminar total t_seminars = [s.seminar_id for s in MemberSeminarAttendance.query.filter( MemberSeminarAttendance.uid == user_name ) if TechnicalSeminar.query.filter( - TechnicalSeminar.id == s.seminar_id).first().approved] + TechnicalSeminar.id == s.seminar_id).first().approved] freshman['ts_total'] = len(t_seminars) attendance = [m.name for m in TechnicalSeminar.query.filter( TechnicalSeminar.id.in_(t_seminars) - )] + )] freshman['ts_list'] = attendance @@ -114,20 +115,20 @@ def get_cm(member): def get_hm(member, only_absent=False): h_meetings = MemberHouseMeetingAttendance.query.outerjoin( - HouseMeeting, - MemberHouseMeetingAttendance.meeting_id == HouseMeeting.id).with_entities( - MemberHouseMeetingAttendance.meeting_id, - MemberHouseMeetingAttendance.attendance_status, - HouseMeeting.date).filter( - HouseMeeting.date > start_of_year(), - MemberHouseMeetingAttendance.uid == member.uid) + HouseMeeting, + MemberHouseMeetingAttendance.meeting_id == HouseMeeting.id).with_entities( + MemberHouseMeetingAttendance.meeting_id, + MemberHouseMeetingAttendance.attendance_status, + HouseMeeting.date).filter( + HouseMeeting.date > start_of_year(), + MemberHouseMeetingAttendance.uid == member.uid) if only_absent: h_meetings = h_meetings.filter(MemberHouseMeetingAttendance.attendance_status == "Absent") return h_meetings # @service_cache(maxsize=128) -def req_cm(uid, members_on_coop = None): +def req_cm(uid, members_on_coop=None): # Get the number of required committee meetings based on if the member # is going on co-op in the current operating session. on_coop = False @@ -143,14 +144,16 @@ def req_cm(uid, members_on_coop = None): return 15 return 30 + @service_cache(maxsize=256) def get_voting_members(): - if datetime.today() < datetime(start_of_year().year, 12, 31): + today = datetime.today() + if today < datetime(start_of_year().year, 12, 31): semester = "Fall" - semester_start = datetime(start_of_year().year,6,1) + semester_start = datetime(start_of_year().year, 6, 1) else: semester = "Spring" - semester_start = datetime(start_of_year().year + 1,1,1) + semester_start = datetime(start_of_year().year + 1, 1, 1) active_members = set(ldap_get_active_members()) intro_members = set(ldap_get_intro_members()) @@ -169,8 +172,8 @@ def get_voting_members(): coop_members = set(coop_members) passed_fall_members = FreshmanEvalData.query.filter( - FreshmanEvalData.freshman_eval_result == "Passed", - FreshmanEvalData.eval_date > start_of_year(), + FreshmanEvalData.freshman_eval_result == "Passed", + FreshmanEvalData.eval_date > start_of_year(), ).with_entities( func.array_agg(FreshmanEvalData.uid) ).scalar() @@ -185,6 +188,12 @@ def get_voting_members(): elligible_members = (active_not_intro - coop_members) | passed_fall_members + # Check to see if there's an Intro Evals in the future of this semester. If there is, everyone gets to vote! + before_evals_one = len(FreshmanAccount.query.filter(FreshmanAccount.eval_date > today).limit(1).all()) + before_evals_two = len(FreshmanEvalData.query.filter(FreshmanEvalData.eval_date > today).limit(1).all()) + if before_evals_one > 0 or before_evals_two > 0: + return elligible_members + passing_dm = set(member.uid for member in MemberCommitteeAttendance.query.join( CommitteeMeeting, MemberCommitteeAttendance.meeting_id == CommitteeMeeting.id @@ -200,7 +209,7 @@ def get_voting_members(): ).group_by( MemberCommitteeAttendance.uid ).having( - func.count(MemberCommitteeAttendance.uid) >= 6 #pylint: disable=not-callable + func.count(MemberCommitteeAttendance.uid) >= 6 # pylint: disable=not-callable ).with_entities( MemberCommitteeAttendance.uid ).all()) @@ -216,36 +225,47 @@ def get_voting_members(): ).group_by( MemberSeminarAttendance.uid ).having( - func.count(MemberSeminarAttendance.uid) >= 2 #pylint: disable=not-callable + func.count(MemberSeminarAttendance.uid) >= 2 # pylint: disable=not-callable ).all()) - passing_hm = set(member.uid for member in MemberHouseMeetingAttendance.query.join( + absent_hm = set(member.uid for member in MemberHouseMeetingAttendance.query.join( HouseMeeting, MemberHouseMeetingAttendance.meeting_id == HouseMeeting.id ).filter( HouseMeeting.date >= semester_start, or_( - MemberHouseMeetingAttendance.attendance_status == 'Attended', - # MemberHouseMeetingAttendance.attendance_status == 'Excused' + MemberHouseMeetingAttendance.attendance_status == 'Absent', ) ).with_entities( MemberHouseMeetingAttendance.uid ).group_by( MemberHouseMeetingAttendance.uid ).having( - func.count(MemberHouseMeetingAttendance.uid) >= 6 #pylint: disable=not-callable + func.count(MemberHouseMeetingAttendance.uid) > 1 # pylint: disable=not-callable ).all()) - passing_reqs = passing_dm & passing_ts & passing_hm + passing_reqs = (passing_dm & passing_ts) - absent_hm return elligible_members & passing_reqs + def gatekeep_status(username): - if datetime.today() < datetime(start_of_year().year, 12, 31): + today = datetime.today() + # Check to see if there's an Intro Evals in the future of this semester. If there is, everyone gets to vote! + before_evals_one = len(FreshmanAccount.query.filter(FreshmanAccount.eval_date > today).limit(1).all()) + before_evals_two = len(FreshmanEvalData.query.filter(FreshmanEvalData.eval_date > today).limit(1).all()) + if before_evals_one > 0 or before_evals_two > 0: + return { + "result": True, + "h_meetings_missed": 0, + "c_meetings": 0, + "t_seminars": 0, + } + if today < datetime(start_of_year().year, 12, 31): semester = "Fall" - semester_start = datetime(start_of_year().year,6,1) + semester_start = datetime(start_of_year().year, 6, 1) else: semester = "Spring" - semester_start = datetime(start_of_year().year + 1,1,1) + semester_start = datetime(start_of_year().year + 1, 1, 1) # groups ldap_member = ldap_get_member(username) @@ -253,21 +273,21 @@ def gatekeep_status(username): is_active_member = ldap_is_active(ldap_member) and not is_intro_member is_on_coop = ( - CurrentCoops.query.filter( - CurrentCoops.date_created > start_of_year(), - CurrentCoops.semester == semester, - CurrentCoops.uid == username, - ).first() - is not None + CurrentCoops.query.filter( + CurrentCoops.date_created > start_of_year(), + CurrentCoops.semester == semester, + CurrentCoops.uid == username, + ).first() + is not None ) passed_fall = ( - FreshmanEvalData.query.filter( - FreshmanEvalData.freshman_eval_result == "Passed", - FreshmanEvalData.eval_date > start_of_year(), - FreshmanEvalData.uid == username, - ).first() - is not None + FreshmanEvalData.query.filter( + FreshmanEvalData.freshman_eval_result == "Passed", + FreshmanEvalData.eval_date > start_of_year(), + FreshmanEvalData.uid == username, + ).first() + is not None ) eligibility_of_groups = (is_active_member and not is_on_coop) or passed_fall @@ -298,22 +318,23 @@ def gatekeep_status(username): .count() ) # number of house meetings attended in the current semester - h_meetings = ( + h_meetings_missed = ( MemberHouseMeetingAttendance.query.join( HouseMeeting, MemberHouseMeetingAttendance.meeting_id == HouseMeeting.id, ) .filter( + MemberHouseMeetingAttendance.attendance_status == 'Absent', MemberHouseMeetingAttendance.uid == username, HouseMeeting.date >= semester_start ) .count() ) - result = eligibility_of_groups and (d_meetings >= 6 and t_seminars >= 2 and h_meetings >= 6) + result = eligibility_of_groups and (d_meetings >= 6 and t_seminars >= 2 and h_meetings_missed < 2) # pylint: disable=chained-comparison return { "result": result, - "h_meetings": h_meetings, + "h_meetings_missed": h_meetings_missed, "c_meetings": d_meetings, "t_seminars": t_seminars, } From 8c637fb2416dfec59012aa40068306d3d755e2c0 Mon Sep 17 00:00:00 2001 From: Noah Hanford Date: Thu, 12 Feb 2026 22:17:11 -0500 Subject: [PATCH 02/27] Fix frosh attendance (#464) * attempt to fix upgrading accounts breaking frosh attendance * actually fix hm attendance and reorder opperations to create then delete * local db explaination * trailing whitespace :)))))))) * fix typo * fix variable name --- README.md | 14 ++- conditional/blueprints/member_management.py | 9 +- conditional/models/models.py | 6 +- conditional/util/member.py | 6 +- config.env.py | 2 +- docker-compose.yaml | 24 ++++ .../7a3904cac24b_freshmen_data_cascade.py | 36 ++++++ migrations/versions/e38beaf3e875_update_db.py | 111 ++++++++++++++++++ 8 files changed, 196 insertions(+), 12 deletions(-) create mode 100644 docker-compose.yaml create mode 100644 migrations/versions/7a3904cac24b_freshmen_data_cascade.py create mode 100644 migrations/versions/e38beaf3e875_update_db.py diff --git a/README.md b/README.md index 1dcf9a51..61bf97ef 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,19 @@ This will run the asset pipeline, start the Python server, and start BrowserSync 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. -after app initialization +### Local database + +You can run the database locally using the docker compose, make sure to upgrade it as explained below + +To populate it with dev data for example, you can use the command + +``` +PGPASSWORD='[DB PASSWORD]' pg_dump -h postgres.csh.rit.edu -p 5432 -U conditional-dev conditional-dev | PGPASSWORD='fancypantspassword' psql -h localhost -p 5432 -U conditional conditional +``` + +This can be helpful for changing the database schema + +NOTE: to use flask db commands with a database running in the compose file, you will have to update your url to point to localhost, not conditional-postgres ### Database Migrations diff --git a/conditional/blueprints/member_management.py b/conditional/blueprints/member_management.py index 1927e440..a0973dfc 100644 --- a/conditional/blueprints/member_management.py +++ b/conditional/blueprints/member_management.py @@ -462,22 +462,20 @@ def member_management_upgrade_user(user_dict=None): db.session.add(new_acct) for fca in FreshmanCommitteeAttendance.query.filter(FreshmanCommitteeAttendance.fid == fid): db.session.add(MemberCommitteeAttendance(uid, fca.meeting_id)) - db.session.delete(fca) for fts in FreshmanSeminarAttendance.query.filter(FreshmanSeminarAttendance.fid == fid): db.session.add(MemberSeminarAttendance(uid, fts.seminar_id)) - db.session.delete(fts) for fhm in FreshmanHouseMeetingAttendance.query.filter(FreshmanHouseMeetingAttendance.fid == fid): # Don't duplicate HM attendance records mhm = MemberHouseMeetingAttendance.query.filter( - MemberHouseMeetingAttendance.meeting_id == fhm.meeting_id).first() + MemberHouseMeetingAttendance.meeting_id == fhm.meeting_id, + MemberHouseMeetingAttendance.uid == uid).first() if mhm is None: db.session.add(MemberHouseMeetingAttendance( uid, fhm.meeting_id, fhm.excuse, fhm.attendance_status)) else: 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) if acct.onfloor_status: @@ -487,6 +485,9 @@ def member_management_upgrade_user(user_dict=None): if acct.room_number: ldap_set_roomnumber(new_account, acct.room_number) + db.session.flush() + db.session.commit() + db.session.delete(acct) db.session.flush() diff --git a/conditional/models/models.py b/conditional/models/models.py index 97e8875f..69014b8a 100644 --- a/conditional/models/models.py +++ b/conditional/models/models.py @@ -83,7 +83,7 @@ def __init__(self, uid, meeting_id): class FreshmanCommitteeAttendance(db.Model): __tablename__ = 'freshman_committee_attendance' id = Column(Integer, primary_key=True) - fid = Column(ForeignKey('freshman_accounts.id'), nullable=False) + fid = Column(ForeignKey('freshman_accounts.id', ondelete="cascade"), nullable=False) meeting_id = Column(ForeignKey('committee_meetings.id'), nullable=False) def __init__(self, fid, meeting_id): @@ -120,7 +120,7 @@ def __init__(self, uid, seminar_id): class FreshmanSeminarAttendance(db.Model): __tablename__ = 'freshman_seminar_attendance' id = Column(Integer, primary_key=True) - fid = Column(ForeignKey('freshman_accounts.id'), nullable=False) + fid = Column(ForeignKey('freshman_accounts.id', ondelete="cascade"), nullable=False) seminar_id = Column(ForeignKey('technical_seminars.id'), nullable=False) def __init__(self, fid, seminar_id): @@ -178,7 +178,7 @@ def __init__(self, uid, meeting_id, excuse, status): class FreshmanHouseMeetingAttendance(db.Model): __tablename__ = 'freshman_hm_attendance' id = Column(Integer, primary_key=True) - fid = Column(ForeignKey('freshman_accounts.id'), nullable=False) + fid = Column(ForeignKey('freshman_accounts.id', ondelete="cascade"), nullable=False) meeting_id = Column(ForeignKey('house_meetings.id'), nullable=False) excuse = Column(Text) attendance_status = Column(attendance_enum) diff --git a/conditional/util/member.py b/conditional/util/member.py index b485d193..e9b28e3e 100644 --- a/conditional/util/member.py +++ b/conditional/util/member.py @@ -186,13 +186,13 @@ def get_voting_members(): active_not_intro = active_members - intro_members active_not_intro = set(map(lambda member: member.uid, active_not_intro)) - elligible_members = (active_not_intro - coop_members) | passed_fall_members + eligible_members = (active_not_intro - coop_members) | passed_fall_members # Check to see if there's an Intro Evals in the future of this semester. If there is, everyone gets to vote! before_evals_one = len(FreshmanAccount.query.filter(FreshmanAccount.eval_date > today).limit(1).all()) before_evals_two = len(FreshmanEvalData.query.filter(FreshmanEvalData.eval_date > today).limit(1).all()) if before_evals_one > 0 or before_evals_two > 0: - return elligible_members + return eligible_members passing_dm = set(member.uid for member in MemberCommitteeAttendance.query.join( CommitteeMeeting, @@ -245,7 +245,7 @@ def get_voting_members(): passing_reqs = (passing_dm & passing_ts) - absent_hm - return elligible_members & passing_reqs + return eligible_members & passing_reqs def gatekeep_status(username): diff --git a/config.env.py b/config.env.py index c0ede697..705b2454 100644 --- a/config.env.py +++ b/config.env.py @@ -18,7 +18,7 @@ PROFILING = env.get("CONDITIONAL_PROFILING", "false").lower() == "true" # DB Info -SQLALCHEMY_DATABASE_URI = env.get("SQLALCHEMY_DATABASE_URI", "") +SQLALCHEMY_DATABASE_URI = "postgresql://conditional:fancypantspassword@conditional-postgres:5432/conditional" SQLALCHEMY_TRACK_MODIFICATIONS = False # LDAP config diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..c5ff7a4b --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,24 @@ +version: "3" +services: + conditional: + build: . + container_name: conditional + depends_on: + - conditional-postgres + ports: + - "127.0.0.1:8080:8080" + conditional-postgres: + image: docker.io/postgres + container_name: conditional-postgres + environment: + POSTGRES_PASSWORD: fancypantspassword + POSTGRES_USER: conditional + POSTGRES_DATABASE: conditional + ports: + - "127.0.0.1:5432:5432" + volumes: + - pgdata:/var/lib/postgresql + +volumes: + pgdata: + diff --git a/migrations/versions/7a3904cac24b_freshmen_data_cascade.py b/migrations/versions/7a3904cac24b_freshmen_data_cascade.py new file mode 100644 index 00000000..18cac087 --- /dev/null +++ b/migrations/versions/7a3904cac24b_freshmen_data_cascade.py @@ -0,0 +1,36 @@ +"""Freshmen data cascade + +Revision ID: 7a3904cac24b +Revises: e38beaf3e875 +Create Date: 2026-02-03 12:14:37.119352 + +""" + +# revision identifiers, used by Alembic. +revision = '7a3904cac24b' +down_revision = 'e38beaf3e875' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint('freshman_committee_attendance_fid_fkey', 'freshman_committee_attendance', type_='foreignkey') + op.create_foreign_key(None, 'freshman_committee_attendance', 'freshman_accounts', ['fid'], ['id'], ondelete='cascade') + op.drop_constraint('freshman_hm_attendance_fid_fkey', 'freshman_hm_attendance', type_='foreignkey') + op.create_foreign_key(None, 'freshman_hm_attendance', 'freshman_accounts', ['fid'], ['id'], ondelete='cascade') + op.drop_constraint('freshman_seminar_attendance_fid_fkey', 'freshman_seminar_attendance', type_='foreignkey') + op.create_foreign_key(None, 'freshman_seminar_attendance', 'freshman_accounts', ['fid'], ['id'], ondelete='cascade') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'freshman_seminar_attendance', type_='foreignkey') + op.create_foreign_key('freshman_seminar_attendance_fid_fkey', 'freshman_seminar_attendance', 'freshman_accounts', ['fid'], ['id']) + op.drop_constraint(None, 'freshman_hm_attendance', type_='foreignkey') + op.create_foreign_key('freshman_hm_attendance_fid_fkey', 'freshman_hm_attendance', 'freshman_accounts', ['fid'], ['id']) + op.drop_constraint(None, 'freshman_committee_attendance', type_='foreignkey') + op.create_foreign_key('freshman_committee_attendance_fid_fkey', 'freshman_committee_attendance', 'freshman_accounts', ['fid'], ['id']) + # ### end Alembic commands ### diff --git a/migrations/versions/e38beaf3e875_update_db.py b/migrations/versions/e38beaf3e875_update_db.py new file mode 100644 index 00000000..2a367f9a --- /dev/null +++ b/migrations/versions/e38beaf3e875_update_db.py @@ -0,0 +1,111 @@ +"""update db + +Revision ID: e38beaf3e875 +Revises: 757e18146d16 +Create Date: 2026-02-03 12:12:11.451367 + +""" + +# revision identifiers, used by Alembic. +revision = 'e38beaf3e875' +down_revision = '757e18146d16' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index('member_batch_users_id_idx', table_name='member_batch_users') + op.drop_table('member_batch_users') + op.drop_index('freshman_batch_pulls_id_idx', table_name='freshman_batch_pulls') + op.drop_table('freshman_batch_pulls') + op.drop_index('member_batch_pulls_id_idx', table_name='member_batch_pulls') + op.drop_table('member_batch_pulls') + op.drop_index('freshman_batch_users_id_pkey', table_name='freshman_batch_users') + op.drop_table('freshman_batch_users') + op.drop_table('batch_conditions') + op.alter_column('freshman_accounts', 'onfloor_status', + existing_type=sa.BOOLEAN(), + nullable=True) + op.alter_column('freshman_accounts', 'rit_username', + existing_type=sa.VARCHAR(length=10), + nullable=True) + op.alter_column('freshman_hm_attendance', 'attendance_status', + existing_type=postgresql.ENUM('Attended', 'Excused', 'Absent', name='attendance_enum'), + nullable=True) + op.alter_column('member_hm_attendance', 'attendance_status', + existing_type=postgresql.ENUM('Attended', 'Excused', 'Absent', name='attendance_enum'), + nullable=True) + op.drop_table('batch') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('member_hm_attendance', 'attendance_status', + existing_type=postgresql.ENUM('Attended', 'Excused', 'Absent', name='attendance_enum'), + nullable=False) + op.alter_column('freshman_hm_attendance', 'attendance_status', + existing_type=postgresql.ENUM('Attended', 'Excused', 'Absent', name='attendance_enum'), + nullable=False) + op.alter_column('freshman_accounts', 'rit_username', + existing_type=sa.VARCHAR(length=10), + nullable=False) + op.alter_column('freshman_accounts', 'onfloor_status', + existing_type=sa.BOOLEAN(), + nullable=False) + op.create_table('batch_conditions', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('value', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('condition', postgresql.ENUM('packet', 'seminar', 'committee', 'house', name='batch_ctype_enum'), autoincrement=False, nullable=False), + sa.Column('comparison', postgresql.ENUM('less', 'equal', 'greater', name='batch_comparison'), autoincrement=False, nullable=False), + sa.Column('batch_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['batch_id'], ['batch.id'], name='batch_conditions_fk'), + sa.PrimaryKeyConstraint('id', name='batch_conditions_pkey') + ) + op.create_table('freshman_batch_users', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('fid', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('batch_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['batch_id'], ['batch.id'], name='freshman_batch_users_fk'), + sa.ForeignKeyConstraint(['fid'], ['freshman_accounts.id'], name='freshman_batch_users_fk_1'), + sa.PrimaryKeyConstraint('id', name='freshman_batch_users_pkey') + ) + op.create_index('freshman_batch_users_id_pkey', 'freshman_batch_users', ['id'], unique=True) + op.create_table('batch', + sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('batch_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('name', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('uid', sa.VARCHAR(length=32), autoincrement=False, nullable=False), + sa.Column('approved', sa.BOOLEAN(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name='batch_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('member_batch_pulls', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('uid', sa.VARCHAR(length=32), autoincrement=False, nullable=False), + sa.Column('approved', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=False), + sa.Column('reason', sa.TEXT(), server_default=sa.text("''::text"), autoincrement=False, nullable=False), + sa.Column('puller', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name='member_batch_pulls_pkey') + ) + op.create_index('member_batch_pulls_id_idx', 'member_batch_pulls', ['id'], unique=True) + op.create_table('freshman_batch_pulls', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('fid', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('approved', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=False), + sa.Column('reason', sa.TEXT(), server_default=sa.text("''::text"), autoincrement=False, nullable=False), + sa.Column('puller', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['fid'], ['freshman_accounts.id'], name='freshman_batch_pulls_fk_1'), + sa.PrimaryKeyConstraint('id', name='freshman_batch_pulls_pkey') + ) + op.create_index('freshman_batch_pulls_id_idx', 'freshman_batch_pulls', ['id'], unique=True) + op.create_table('member_batch_users', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('uid', sa.VARCHAR(length=32), autoincrement=False, nullable=False), + sa.Column('batch_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['batch_id'], ['batch.id'], name='member_batch_users_fk'), + sa.PrimaryKeyConstraint('id', name='member_batch_users_pkey') + ) + op.create_index('member_batch_users_id_idx', 'member_batch_users', ['id'], unique=False) + # ### end Alembic commands ### From 45c3351bdfffcf3e178a39dc038117c4b92e3fb2 Mon Sep 17 00:00:00 2001 From: "Noah Hanford (spaced)" Date: Thu, 12 Feb 2026 22:35:53 -0500 Subject: [PATCH 03/27] guys i'm dumb --- config.env.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.env.py b/config.env.py index 705b2454..77da644b 100644 --- a/config.env.py +++ b/config.env.py @@ -18,7 +18,7 @@ PROFILING = env.get("CONDITIONAL_PROFILING", "false").lower() == "true" # DB Info -SQLALCHEMY_DATABASE_URI = "postgresql://conditional:fancypantspassword@conditional-postgres:5432/conditional" +SQLALCHEMY_DATABASE_URI = env.get("SQLALCHEMY_DATABASE_URI", "postgresql://conditional:fancypantspassword@conditional-postgres:5432/conditional") SQLALCHEMY_TRACK_MODIFICATIONS = False # LDAP config From 7bbcf7792b888e9cff2aada85657d825c0ee2a63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:28:14 -0500 Subject: [PATCH 04/27] Bump ddtrace from 3.2.3 to 4.1.0 (#450) Bumps [ddtrace](https://github.com/DataDog/dd-trace-py) from 3.2.3 to 4.1.0. - [Release notes](https://github.com/DataDog/dd-trace-py/releases) - [Changelog](https://github.com/DataDog/dd-trace-py/blob/main/CHANGELOG.md) - [Commits](https://github.com/DataDog/dd-trace-py/compare/v3.2.3...v4.1.0) --- updated-dependencies: - dependency-name: ddtrace dependency-version: 4.1.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.in | 2 +- requirements.txt | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/requirements.in b/requirements.in index 14fc826d..99d38438 100644 --- a/requirements.in +++ b/requirements.in @@ -2,7 +2,7 @@ alembic~=1.15.1 astroid~=3.3.9 blinker~=1.4 csh_ldap>=2.5.3 -ddtrace~=4.2.1 +ddtrace~=4.4.0 Flask~=3.1.0 Flask-Migrate~=2.1.1 Flask-Gzip~=0.2 diff --git a/requirements.txt b/requirements.txt index dc93cb82..58830350 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,7 +36,7 @@ cryptography==46.0.3 # via oic csh-ldap==2.5.3 # via -r requirements.in -ddtrace==4.2.1 +ddtrace==4.4.0 # via -r requirements.in defusedxml==0.7.1 # via oic @@ -117,8 +117,6 @@ packaging==25.0 # via # build # gunicorn -pip==25.3 - # via pip-tools pip-tools==7.4.1 # via -r requirements.in platformdirs==4.5.1 @@ -164,10 +162,8 @@ requests==2.32.5 # flask-pyoidc # oic # pyjwkest -sentry-sdk==2.24.1 +sentry-sdk[flask]==2.24.1 # via -r requirements.in -setuptools==80.9.0 - # via pip-tools six==1.17.0 # via # -r requirements.in From ed9df9863f2aec1accf51e4b591acbec7efe075e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:52:07 -0500 Subject: [PATCH 05/27] Bump cryptography from 46.0.3 to 46.0.5 (#475) Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.3 to 46.0.5. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/46.0.3...46.0.5) --- updated-dependencies: - dependency-name: cryptography dependency-version: 46.0.5 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 58830350..bf1d0973 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,7 +32,7 @@ click==8.3.1 # -r requirements.in # flask # pip-tools -cryptography==46.0.3 +cryptography==46.0.5 # via oic csh-ldap==2.5.3 # via -r requirements.in From 18f1161d3a9c20b4de155ebad5e4395ed104c126 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:57:35 -0500 Subject: [PATCH 06/27] Bump wheel from 0.45.1 to 0.46.2 (#476) Bumps [wheel](https://github.com/pypa/wheel) from 0.45.1 to 0.46.2. - [Release notes](https://github.com/pypa/wheel/releases) - [Changelog](https://github.com/pypa/wheel/blob/main/docs/news.rst) - [Commits](https://github.com/pypa/wheel/compare/0.45.1...0.46.2) --- updated-dependencies: - dependency-name: wheel dependency-version: 0.46.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bf1d0973..c066dd06 100644 --- a/requirements.txt +++ b/requirements.txt @@ -117,6 +117,7 @@ packaging==25.0 # via # build # gunicorn + # wheel pip-tools==7.4.1 # via -r requirements.in platformdirs==4.5.1 @@ -201,7 +202,7 @@ werkzeug==3.1.5 # via # -r requirements.in # flask -wheel==0.45.1 +wheel==0.46.2 # via pip-tools wrapt==1.17.3 # via From a0968cc706d2f58ba2585df5ac7d6a5664e025e7 Mon Sep 17 00:00:00 2001 From: Noah Hanford Date: Fri, 20 Feb 2026 10:53:17 -0500 Subject: [PATCH 07/27] fixed not all frosh attendance was showing up (#472) Co-authored-by: Tyler Allen --- conditional/blueprints/intro_evals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conditional/blueprints/intro_evals.py b/conditional/blueprints/intro_evals.py index cfda1e22..b6b60a7a 100644 --- a/conditional/blueprints/intro_evals.py +++ b/conditional/blueprints/intro_evals.py @@ -76,7 +76,7 @@ def get_intro_members_without_accounts(): if not row[0] in freshman_ts_attendance_dict: freshman_ts_attendance_dict[row[0]] = [] - freshman_ts_attendance_dict[row[0]].append(row[1]) + freshman_ts_attendance_dict[row[0]].append(row[1]) # freshmen who don't have accounts freshman_accounts = list(FreshmanAccount.query.filter( From 4714fa73bde438957a8af5d47b1bac39bdbefcfe Mon Sep 17 00:00:00 2001 From: Noah Hanford Date: Fri, 20 Feb 2026 10:55:34 -0500 Subject: [PATCH 08/27] moved gunicorn config out of dockerfile args (#477) * moved gunicorn config out of dockerfile args * moved gunicorn config out of dockerfile Now you can just run it with `gunicorn` --- Dockerfile | 3 ++- gunicorn.conf.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 gunicorn.conf.py diff --git a/Dockerfile b/Dockerfile index 2817415d..d6b15b39 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,4 +40,5 @@ COPY --from=build-frontend /opt/conditional/conditional/static /opt/conditional/ RUN ln -sf /usr/share/zoneinfo/America/New_York /etc/localtime -CMD ["sh", "-c", "gunicorn conditional:app --bind=0.0.0.0:${PORT} --access-logfile=- --timeout=256"] +CMD ["sh", "-c", "gunicorn"] + diff --git a/gunicorn.conf.py b/gunicorn.conf.py new file mode 100644 index 00000000..9fb31c45 --- /dev/null +++ b/gunicorn.conf.py @@ -0,0 +1,7 @@ +import os + +wsgi_app = 'conditional:app' +bind = f'0.0.0.0:{os.getenv('PORT', 8080)}' +workers = os.getenv('CONDITIONAL_WORKERS', 1) +accesslog = '-' +timeout = 256 From 69d78485b5bdc75234e02cfd941b3f036b373fde Mon Sep 17 00:00:00 2001 From: pikachu0542 Date: Fri, 20 Feb 2026 11:08:11 -0500 Subject: [PATCH 09/27] Fixed redirect_uri --- config.env.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config.env.py b/config.env.py index 77da644b..554f8e08 100644 --- a/config.env.py +++ b/config.env.py @@ -42,10 +42,11 @@ OIDC_CLIENT_CONFIG = { 'client_id': env.get("CONDITIONAL_OIDC_CLIENT_ID", "conditional"), 'client_secret': env.get("CONDITIONAL_OIDC_CLIENT_SECRET", ""), - 'redirect_uri': env.get("CONDITIONAL_OIDC_REDIRECT_URI", "http://localhost:8080/redirect_uri"), 'post_logout_redirect_uris': [env.get("CONDITIONAL_OIDC_CLIENT_LOGOUT", "http://0.0.0.0:8080/logout")], } +OIDC_REDIRECT_URI = env.get("CONDITIONAL_OIDC_REDIRECT_URI", "http://localhost:8080/redirect_uri") + # Openshift secret SECRET_KEY = env.get("CONDITIONAL_SECRET_KEY", default=''.join(secrets.token_hex(16))) From ea77e7df3f4a644beff881182aceeace0298a851 Mon Sep 17 00:00:00 2001 From: Noah Hanford Date: Sun, 22 Feb 2026 21:34:57 -0500 Subject: [PATCH 10/27] Speedup pt 2 (#469) * Added uids and fids as indicies in the database to speed up queries * speedup member management * use user dict instead of ldap where possible * fix linting :) * fix eboard group names SIGH * moved migrations out of dockerfile and updated documentation --- README.md | 85 +++++++++++++------ conditional/__init__.py | 6 +- conditional/blueprints/attendance.py | 26 +++--- conditional/blueprints/cache_management.py | 7 +- conditional/blueprints/co_op.py | 13 ++- conditional/blueprints/conditional.py | 8 +- conditional/blueprints/dashboard.py | 44 +++++----- conditional/blueprints/logs.py | 5 +- conditional/blueprints/member_management.py | 51 ++++++----- conditional/blueprints/slideshow.py | 11 +-- conditional/models/models.py | 14 +-- conditional/util/auth.py | 7 +- conditional/util/flask.py | 24 ++---- conditional/util/member.py | 44 +++++++--- conditional/util/user_dict.py | 32 +++++++ docker-compose.yaml | 2 + ...3b870_set_uid_and_fid_as_table_indicies.py | 38 +++++++++ 17 files changed, 265 insertions(+), 152 deletions(-) create mode 100644 conditional/util/user_dict.py create mode 100644 migrations/versions/f1d08673b870_set_uid_and_fid_as_table_indicies.py diff --git a/README.md b/README.md index 61bf97ef..033275be 100644 --- a/README.md +++ b/README.md @@ -8,54 +8,82 @@ A comprehensive membership evaluations solution for Computer Science House. Development ----------- -To run the application, you must have the latest version of [Python 3](https://www.python.org/downloads/) and [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) installed. Once you have those installed, create a new virtualenv and install the Python dependencies: +### Config + +You must create `config.py` in the top-level directory with the appropriate credentials for the application to run. See `config.env.py` for an example. +#### Add OIDC Config +Reach out to an RTP to get OIDC credentials that will allow you to develop locally behind OIDC auth +```py +# OIDC Config +OIDC_ISSUER = "https://sso.csh.rit.edu/auth/realms/csh" +OIDC_CLIENT_CONFIG = { + 'client_id': '', + 'client_secret': '', + 'post_logout_redirect_uris': ['http://0.0.0.0:6969/logout'] +} ``` + +#### Database +You can either develop using the dev database, or use the local database provided in the docker compose file + +Using the local database is detailed below, but both options will require the dev database password, so you will have to ask an RTP for this too + +### Run (Without Docker) + +To run the application without using containers, you must have the latest version of [Python 3](https://www.python.org/downloads/) and [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) installed. Once you have those installed, create a new virtualenv and install the Python dependencies: + +```sh virtualenv .conditionalenv -p `which python3` source .conditionalenv/bin/activate pip install -r requirements.txt -export FLASK_APP=app.py ``` -In addition, you must have Node, NPM, and Gulp CLI installed to properly execute the asset pipeline. If you don't have Node installed, we recommending installing with [NVM](https://github.com/creationix/nvm): +In addition, you must have Node, NPM, and Weback CLI installed to properly execute the asset pipeline. If you don't have Node installed, we recommending installing with [NVM](https://github.com/creationix/nvm): -``` +```sh nvm install nvm use -npm install -g gulp +npm install -g webpack ``` -Then, install the pipeline and frontend dependencies: +Then, install the pipeline and frontend dependencies: (do this in the `frontend` directory) -``` +```sh npm install ``` -### Config +Once you have all of the dependencies installed, run -You must create `config.py` in the top-level directory with the appropriate credentials for the application to run. See `config.sample.py` for an example. +```sh +npm webpack +``` -#### Add OIDC Config -Reach out to an RTP to get OIDC credentials that will allow you to develop locally behind OIDC auth +This will build the frontend assets and put them in the correct place for use with flask + +Finally, start the flask app with `gunicorn` + +```sh +gunicorn ``` -# OIDC Config -OIDC_ISSUER = "https://sso.csh.rit.edu/auth/realms/csh" -OIDC_CLIENT_CONFIG = { - 'client_id': '', - 'client_secret': '', - 'post_logout_redirect_uris': ['http://0.0.0.0:6969/logout'] -} + +or + +```sh +python -m gunicorn ``` -### Run +### Run (containerized) -Once you have all of the dependencies installed, simply run: +It is likely easier to use containers like `podman` or `docker` or the corresponding compose file -``` -npm start +With podman, I have been using + +```sh +podman compose up --force-recreate --build ``` -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. +Which can be restarted every time changes are made ### Dependencies @@ -63,17 +91,20 @@ To add new dependencies, add them to `requirements.in` and then run `pip-compile ### Local database -You can run the database locally using the docker compose, make sure to upgrade it as explained below +You can run the database locally using the docker compose To populate it with dev data for example, you can use the command -``` -PGPASSWORD='[DB PASSWORD]' pg_dump -h postgres.csh.rit.edu -p 5432 -U conditional-dev conditional-dev | PGPASSWORD='fancypantspassword' psql -h localhost -p 5432 -U conditional conditional +```sh +PGPASSWORD='[DB PASSWORD]' pg_dump -h postgres.csh.rit.edu -p 5432 -U conditionaldev conditionaldev | PGPASSWORD='fancypantspassword' psql -h localhost -p 5432 -U conditional conditional ``` This can be helpful for changing the database schema -NOTE: to use flask db commands with a database running in the compose file, you will have to update your url to point to localhost, not conditional-postgres +To run migration commands in the local database, you can run the commands inside the docker container. Any migrations created will also be in the local repository since migrations are mounted in the docker compose +```sh +podman exec conditional flask db upgrade +``` ### Database Migrations diff --git a/conditional/__init__.py b/conditional/__init__.py index 83c02a63..4d7cf1df 100644 --- a/conditional/__init__.py +++ b/conditional/__init__.py @@ -1,6 +1,7 @@ import os from datetime import datetime +from pstats import SortKey import structlog from csh_ldap import CSHLDAP from flask import Flask, redirect, render_template, request, g @@ -33,7 +34,8 @@ if app.config['PROFILING']: app.wsgi_app = ProfilerMiddleware( app.wsgi_app, - restrictions=[30] + sort_by=('cumulative',), + restrictions=[80] ) # Sentry setup @@ -195,7 +197,7 @@ def route_errors(error, user_dict=None): # Handle the case where the header isn't present if user_dict['username'] is not None: - data['username'] = user_dict['account'].uid + data['username'] = user_dict['username'] data['name'] = user_dict['account'].cn else: data['username'] = "unknown" diff --git a/conditional/blueprints/attendance.py b/conditional/blueprints/attendance.py index 22475703..ebc05415 100644 --- a/conditional/blueprints/attendance.py +++ b/conditional/blueprints/attendance.py @@ -21,7 +21,7 @@ from conditional.util.ldap import ldap_get_current_students from conditional.util.ldap import ldap_get_member from conditional.util.ldap import ldap_is_eboard -from conditional.util.ldap import ldap_is_eval_director +from conditional.util.user_dict import user_dict_is_eboard, user_dict_is_eval_director logger = structlog.get_logger() @@ -160,7 +160,7 @@ def display_attendance_hm(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) log.info('Display House Meeting Attendance Page') - if not ldap_is_eval_director(user_dict['account']): + if not user_dict_is_eval_director(user_dict): return redirect("/dashboard") return render_template('attendance_hm.html', @@ -175,7 +175,7 @@ def display_attendance_hm(user_dict=None): def submit_committee_attendance(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - approved = ldap_is_eboard(user_dict['account']) + approved = user_dict_is_eval_director(user_dict) post_data = request.get_json() committee = post_data['committee'] @@ -211,7 +211,7 @@ def submit_seminar_attendance(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) log.info('Submit Technical Seminar Attendance') - approved = ldap_is_eboard(user_dict['account']) + approved = user_dict_is_eboard(user_dict) post_data = request.get_json() @@ -248,7 +248,7 @@ def submit_house_attendance(user_dict=None): # status: Attended | Excused | Absent - if not ldap_is_eval_director(user_dict['account']): + if not user_dict_is_eval_director(user_dict): return "must be evals", 403 post_data = request.get_json() @@ -289,7 +289,7 @@ def submit_house_attendance(user_dict=None): def alter_house_attendance(uid, hid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - if not ldap_is_eval_director(user_dict['account']): + if not user_dict_is_eval_director(user_dict): return "must be evals", 403 if not uid.isdigit(): @@ -319,7 +319,7 @@ def alter_house_attendance(uid, hid, user_dict=None): def alter_house_excuse(uid, hid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - if not ldap_is_eval_director(user_dict['account']): + if not user_dict_is_eval_director(user_dict): return "must be eval director", 403 post_data = request.get_json() @@ -381,7 +381,7 @@ def get_seminar_attendees(meeting_id): log = logger.new(request=request, auth_dict=user_dict) - if not ldap_is_eboard(user_dict['account']): + if not user_dict_is_eboard(user_dict): return jsonify({"success": False, "error": "Not EBoard"}), 403 @@ -444,7 +444,7 @@ def alter_committee_attendance(cid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) log.info('Edit Committee Meeting Attendance') - if not ldap_is_eboard(user_dict['account']): + if not user_dict_is_eboard(user_dict): return jsonify({"success": False, "error": "Not EBoard"}), 403 post_data = request.get_json() @@ -476,7 +476,7 @@ def alter_seminar_attendance(sid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) log.info('Edit Technical Seminar Attendance') - if not ldap_is_eboard(user_dict['account']): + if not user_dict_is_eboard(user_dict): return jsonify({"success": False, "error": "Not EBoard"}), 403 post_data = request.get_json() @@ -559,7 +559,7 @@ def get_ts_attendees(cid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) log.info(f'Delete Committee Meeting {cid}') - if not ldap_is_eboard(user_dict['account']): + if not user_dict_is_eboard(user_dict): return jsonify({"success": False, "error": "Not EBoard"}), 403 FreshmanCommitteeAttendance.query.filter( @@ -582,7 +582,7 @@ def approve_cm(cid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) log.info(f'Approve Committee Meeting {cid} Attendance') - if not ldap_is_eboard(user_dict['account']): + if not user_dict_is_eboard(user_dict): return jsonify({"success": False, "error": "Not EBoard"}), 403 CommitteeMeeting.query.filter( @@ -600,7 +600,7 @@ def approve_ts(sid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) log.info(f'Approve Technical Seminar {sid} Attendance') - if not ldap_is_eboard(user_dict['account']): + if not user_dict_is_eboard(user_dict): return jsonify({"success": False, "error": "Not EBoard"}), 403 TechnicalSeminar.query.filter( diff --git a/conditional/blueprints/cache_management.py b/conditional/blueprints/cache_management.py index 14290a51..fad5924c 100644 --- a/conditional/blueprints/cache_management.py +++ b/conditional/blueprints/cache_management.py @@ -13,8 +13,7 @@ from conditional.util.ldap import ldap_get_intro_members from conditional.util.ldap import ldap_get_member from conditional.util.ldap import ldap_get_onfloor_members -from conditional.util.ldap import ldap_is_eval_director -from conditional.util.ldap import ldap_is_rtp +from conditional.util.user_dict import user_dict_is_eval_director, user_dict_is_rtp logger = structlog.get_logger() cache_bp = Blueprint('cache_bp', __name__) @@ -24,7 +23,7 @@ @auth.oidc_auth("default") @get_user def restart_app(user_dict=None): - if not ldap_is_rtp(user_dict['account']): + if not user_dict_is_rtp(user_dict): return redirect("/dashboard") log = logger.new(request=request, auth_dict=user_dict) @@ -37,7 +36,7 @@ def restart_app(user_dict=None): @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']): + if not user_dict_is_eval_director(user_dict) and not user_dict_is_rtp(user_dict): return redirect("/dashboard") log = logger.new(request=request, auth_dict=user_dict) diff --git a/conditional/blueprints/co_op.py b/conditional/blueprints/co_op.py index 94bf107a..84207422 100644 --- a/conditional/blueprints/co_op.py +++ b/conditional/blueprints/co_op.py @@ -6,10 +6,9 @@ from conditional.util.member import req_cm from conditional.util.auth import get_user from conditional.util.flask import render_template -from conditional.util.ldap import ldap_is_eval_director, ldap_is_current_student from conditional.util.ldap import _ldap_add_member_to_group as ldap_add_member_to_group from conditional.util.ldap import _ldap_remove_member_from_group as ldap_remove_member_from_group -from conditional.util.ldap import _ldap_is_member_of_group as ldap_is_member_of_group +from conditional.util.user_dict import user_dict_is_current_student, user_dict_is_eval_director, user_dict_is_in_group co_op_bp = Blueprint('co_op_bp', __name__) @@ -43,7 +42,7 @@ def submit_co_op_form(user_dict=None): semester = post_data['semester'] if post_data['semester'] not in valid_semesters: return "Invalid semester submitted", 400 - if not ldap_is_current_student(user_dict['account']): + if not user_dict_is_current_student(user_dict): return "Must be current student", 403 log.info(f'Submit {semester} Co-Op') @@ -70,15 +69,15 @@ def submit_co_op_form(user_dict=None): def delete_co_op(uid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - if not ldap_is_eval_director(user_dict['account']): + if not user_dict_is_eval_director(user_dict): return "must be eval director", 403 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'): + if user_dict_is_in_group(user_dict, 'fall_coop'): ldap_remove_member_from_group(user_dict['account'], 'fall_coop') - if ldap_is_member_of_group(user_dict['account'], 'spring_coop'): + if user_dict_is_in_group(user_dict, 'spring_coop'): ldap_remove_member_from_group(user_dict['account'], 'spring_coop') CurrentCoops.query.filter(CurrentCoops.uid == uid, CurrentCoops.date_created > start_of_year()).delete() @@ -97,7 +96,7 @@ def display_co_op_management(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) log.info('Display Co-Op Management') - if not ldap_is_eval_director(user_dict['account']): + if not user_dict_is_eval_director(user_dict): return "must be eval director", 403 co_op_list = [(member.semester, member.uid) diff --git a/conditional/blueprints/conditional.py b/conditional/blueprints/conditional.py index 1082ca4f..74a4913f 100644 --- a/conditional/blueprints/conditional.py +++ b/conditional/blueprints/conditional.py @@ -7,7 +7,7 @@ from conditional.models.models import Conditional, SpringEval, FreshmanEvalData from conditional.util.auth import get_user from conditional.util.flask import render_template -from conditional.util.ldap import ldap_is_eval_director +from conditional.util.user_dict import user_dict_is_eval_director conditionals_bp = Blueprint('conditionals_bp', __name__) @@ -44,7 +44,7 @@ def display_conditionals(user_dict=None): def create_conditional(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - if not ldap_is_eval_director(user_dict['account']): + if not user_dict_is_eval_director(user_dict): return "must be eval director", 403 post_data = request.get_json() @@ -81,7 +81,7 @@ def create_conditional(user_dict=None): def conditional_review(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - if not ldap_is_eval_director(user_dict['account']): + if not user_dict_is_eval_director(user_dict): return redirect("/dashboard", code=302) post_data = request.get_json() @@ -119,7 +119,7 @@ def conditional_delete(cid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) log.info(f'Delete conditional-{cid}') - if ldap_is_eval_director(user_dict['account']): + if user_dict_is_eval_director(user_dict): Conditional.query.filter( Conditional.id == cid ).delete() diff --git a/conditional/blueprints/dashboard.py b/conditional/blueprints/dashboard.py index a0a5c865..c080ddaf 100644 --- a/conditional/blueprints/dashboard.py +++ b/conditional/blueprints/dashboard.py @@ -12,11 +12,9 @@ from conditional.util.auth import get_user from conditional.util.flask import render_template from conditional.util.housing import get_queue_position -from conditional.util.ldap import ldap_get_active_members, ldap_is_bad_standing -from conditional.util.ldap import ldap_is_active -from conditional.util.ldap import ldap_is_intromember -from conditional.util.ldap import ldap_is_onfloor -from conditional.util.member import get_freshman_data, get_voting_members, get_cm, get_hm, req_cm +from conditional.util.member import get_active_members, get_freshman_data, get_voting_members, get_cm, get_hm, req_cm +from conditional.util.user_dict import user_dict_is_active, user_dict_is_bad_standing, user_dict_is_intromember, \ + user_dict_is_onfloor logger = structlog.get_logger() @@ -35,32 +33,34 @@ def display_dashboard(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) log.info('display dashboard') - # Get the list of voting members. + uid = user_dict["username"] can_vote = get_voting_members() + on_floor = user_dict_is_onfloor(user_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']) - data['onfloor'] = ldap_is_onfloor(user_dict['account']) - data['voting'] = bool(user_dict['account'].uid in can_vote) + data['username'] = uid + data['active'] = user_dict_is_active(user_dict) + data['bad_standing'] = user_dict_is_bad_standing(user_dict) + data['onfloor'] = on_floor + data['voting'] = bool(uid in can_vote) data['voting_count'] = {"Voting Members": len(can_vote), - "Active Members": len(ldap_get_active_members())} + "Active Members": len(get_active_members())} # freshman shit - if ldap_is_intromember(user_dict['account']): - data['freshman'] = get_freshman_data(user_dict['account'].uid) + if user_dict_is_intromember(user_dict): + data['freshman'] = get_freshman_data(uid) else: data['freshman'] = None spring = {} c_meetings = get_cm(user_dict['account']) spring['committee_meetings'] = len(c_meetings) - spring['req_meetings'] = req_cm(user_dict['account'].uid) + spring['req_meetings'] = req_cm(uid) h_meetings = [(m.meeting_id, m.attendance_status) for m in get_hm(user_dict['account'])] spring['hm_missed'] = len([h for h in h_meetings if h[1] == "Absent"]) - eval_entry = SpringEval.query.filter(SpringEval.uid == user_dict['account'].uid, + eval_entry = SpringEval.query.filter(SpringEval.uid == uid, SpringEval.date_created > start_of_year(), SpringEval.active == True).first() # pylint: disable=singleton-comparison if eval_entry is not None: @@ -71,11 +71,11 @@ def display_dashboard(user_dict=None): data['spring'] = spring # only show housing if member has onfloor status - if ldap_is_onfloor(user_dict['account']): + if on_floor: housing = {} housing['points'] = user_dict['account'].housingPoints housing['room'] = user_dict['account'].roomNumber - housing['queue_pos'] = get_queue_position(user_dict['account'].uid) + housing['queue_pos'] = get_queue_position(uid) else: housing = None @@ -88,7 +88,7 @@ def display_dashboard(user_dict=None): 'status': p.status, 'description': p.description } for p in - MajorProject.query.filter(MajorProject.uid == user_dict['account'].uid, + MajorProject.query.filter(MajorProject.uid == uid, MajorProject.date > start_of_year())] data['major_projects_count'] = len(data['major_projects']) @@ -96,7 +96,7 @@ def display_dashboard(user_dict=None): # technical seminar total t_seminars = [s.seminar_id for s in MemberSeminarAttendance.query.filter( - MemberSeminarAttendance.uid == user_dict['account'].uid, + MemberSeminarAttendance.uid == uid, ) if is_seminar_attendance_valid(s)] data['ts_total'] = len(t_seminars) attendance = [m.name for m in TechnicalSeminar.query.filter( @@ -122,7 +122,7 @@ def display_dashboard(user_dict=None): 'status': c.status } for c in Conditional.query.filter( - Conditional.uid == user_dict['account'].uid, + Conditional.uid == uid, Conditional.date_due > start_of_year())] data['conditionals'] = conditionals data['conditionals_len'] = len(conditionals) @@ -137,7 +137,7 @@ def display_dashboard(user_dict=None): MemberHouseMeetingAttendance.meeting_id == HouseMeeting.id).with_entities( MemberHouseMeetingAttendance.excuse, HouseMeeting.date).filter( - MemberHouseMeetingAttendance.uid == user_dict['account'].uid, + MemberHouseMeetingAttendance.uid == uid, MemberHouseMeetingAttendance.attendance_status == "Absent", HouseMeeting.date > start_of_year())] diff --git a/conditional/blueprints/logs.py b/conditional/blueprints/logs.py index 92b671be..5bed2616 100644 --- a/conditional/blueprints/logs.py +++ b/conditional/blueprints/logs.py @@ -5,8 +5,7 @@ from conditional.models.models import UserLog from conditional.util.auth import get_user from conditional.util.flask import render_template -from conditional.util.ldap import ldap_is_eboard -from conditional.util.ldap import ldap_is_rtp +from conditional.util.user_dict import user_dict_is_eboard, user_dict_is_rtp logger = structlog.get_logger() @@ -22,7 +21,7 @@ def display_logs(user_dict=None): log.info(user_dict['account'].displayName) - if not ldap_is_eboard(user_dict['account']) and not ldap_is_rtp(user_dict['account']): + if not user_dict_is_eboard(user_dict) and not user_dict_is_rtp(user_dict): return "must be rtp or eboard", 403 logs = UserLog.query.all() diff --git a/conditional/blueprints/member_management.py b/conditional/blueprints/member_management.py index a0973dfc..17adaddc 100644 --- a/conditional/blueprints/member_management.py +++ b/conditional/blueprints/member_management.py @@ -24,11 +24,9 @@ from conditional.blueprints.cache_management import clear_members_cache -from conditional.util.ldap import ldap_is_eval_director, ldap_is_bad_standing -from conditional.util.ldap import ldap_is_financial_director +from conditional.util.ldap import ldap_is_eval_director from conditional.util.ldap import ldap_is_active from conditional.util.ldap import ldap_is_onfloor -from conditional.util.ldap import ldap_is_current_student from conditional.util.ldap import ldap_set_roomnumber from conditional.util.ldap import ldap_set_active from conditional.util.ldap import ldap_set_inactive @@ -42,9 +40,12 @@ from conditional.util.ldap import _ldap_add_member_to_group as ldap_add_member_to_group from conditional.util.ldap import _ldap_remove_member_from_group as ldap_remove_member_from_group +from conditional.util.member import get_members_info_active_and_onfloor + from conditional.util.flask import render_template from conditional.models.models import attendance_enum -from conditional.util.member import get_members_info, get_onfloor_members +from conditional.util.user_dict import user_dict_is_active, user_dict_is_bad_standing, user_dict_is_current_student, \ + user_dict_is_eval_director, user_dict_is_financial_director logger = structlog.get_logger() @@ -58,11 +59,10 @@ def display_member_management(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) log.info('Display Member Management') - if not ldap_is_eval_director(user_dict['account']) and not ldap_is_financial_director(user_dict['account']): + if not user_dict_is_eval_director(user_dict) and not user_dict_is_financial_director(user_dict): return "must be eval director", 403 - member_list = get_members_info() - onfloor_list = get_onfloor_members() + member_list, active_members, onfloor_members = get_members_info_active_and_onfloor() freshmen = FreshmanAccount.query freshmen_list = [] @@ -90,9 +90,9 @@ def display_member_management(user_dict=None): username=user_dict['username'], active=member_list, num_current=len(member_list), - num_active=len(ldap_get_active_members()), + num_active=len(active_members), num_fresh=len(freshmen_list), - num_onfloor=len(onfloor_list), + num_onfloor=len(onfloor_members), freshmen=freshmen_list, site_lockdown=lockdown, accept_dues_until=accept_dues_until, @@ -105,7 +105,7 @@ def display_member_management(user_dict=None): def member_management_eval(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - if not ldap_is_eval_director(user_dict['account']): + if not user_dict_is_eval_director(user_dict): return "must be eval director", 403 post_data = request.get_json() @@ -135,7 +135,7 @@ def member_management_eval(user_dict=None): def member_management_financial(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - if not ldap_is_financial_director(user_dict['account']): + if not user_dict_is_financial_director(user_dict): return "must be financial director", 403 post_data = request.get_json() @@ -159,7 +159,7 @@ def member_management_financial(user_dict=None): def member_management_adduser(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - if not ldap_is_eval_director(user_dict['account']): + if not user_dict_is_eval_director(user_dict): return "must be eval director", 403 post_data = request.get_json() @@ -185,7 +185,7 @@ def member_management_adduser(user_dict=None): def member_management_uploaduser(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - if not ldap_is_eval_director(user_dict['account']): + if not user_dict_is_eval_director(user_dict): return "must be eval director", 403 f = request.files['file'] @@ -224,7 +224,7 @@ def member_management_uploaduser(user_dict=None): @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']): + if not user_dict_is_eval_director(user_dict) and not user_dict_is_financial_director(user_dict): return "must be eval director", 403 if not uid.isdigit(): @@ -292,7 +292,6 @@ def edit_fid(uid, flask_request): 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'] if post_data['roomNumber'] == "": @@ -324,7 +323,7 @@ def member_management_getuserinfo(uid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) 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']): + if not user_dict_is_eval_director(user_dict) and not user_dict_is_financial_director(user_dict): return "must be eval or financial director", 403 acct = None @@ -368,7 +367,7 @@ def get_hm_date(hm_id): account = ldap_get_member(uid) - if ldap_is_eval_director(ldap_get_member(user_dict['username'])): + if user_dict_is_eval_director(user_dict): missed_hm = [ { 'date': get_hm_date(hma.meeting_id), @@ -409,7 +408,7 @@ def member_management_deleteuser(fid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) log.info(f'Delete freshman-{fid}') - if not ldap_is_eval_director(user_dict['account']): + if not user_dict_is_eval_director(user_dict): return "must be eval director", 403 if not fid.isdigit(): @@ -442,7 +441,7 @@ def member_management_deleteuser(fid, user_dict=None): def member_management_upgrade_user(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - if not ldap_is_eval_director(user_dict['account']): + if not user_dict_is_eval_director(user_dict): return "must be eval director", 403 post_data = request.get_json() @@ -504,9 +503,9 @@ def member_management_upgrade_user(user_dict=None): def member_management_make_user_active(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - if not ldap_is_current_student(user_dict['account']) \ - or ldap_is_active(user_dict['account']) \ - or ldap_is_bad_standing(user_dict['account']): + if not user_dict_is_current_student(user_dict) \ + or user_dict_is_active(user_dict) \ + or user_dict_is_bad_standing(user_dict): return "must be current student, not in bad standing and not active", 403 ldap_set_active(user_dict['account']) @@ -523,7 +522,7 @@ def get_member(uid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) log.info(f'Get {uid}\'s Information') - if not ldap_is_eval_director(user_dict['account']): + if not user_dict_is_eval_director(user_dict): return "must be eval director", 403 member = ldap_get_member(uid) @@ -542,7 +541,7 @@ def get_member(uid, user_dict=None): def clear_active_members(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - if not ldap_is_eval_director(user_dict['account']): + if not user_dict_is_eval_director(user_dict): return "must be eval director", 403 # Get the active group. members = ldap_get_active_members() @@ -588,7 +587,7 @@ def export_active_list(): def remove_current_student(uid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - if not ldap_is_eval_director(user_dict['account']): + if not user_dict_is_eval_director(user_dict): return "must be eval director", 403 member = ldap_get_member(uid) @@ -608,7 +607,7 @@ def new_year(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) log.info('Display New Year Page') - if not ldap_is_eval_director(user_dict['account']): + if not user_dict_is_eval_director(user_dict): return "must be eval director", 403 current_students = ldap_get_current_students() diff --git a/conditional/blueprints/slideshow.py b/conditional/blueprints/slideshow.py index 1ea953d3..ec290657 100644 --- a/conditional/blueprints/slideshow.py +++ b/conditional/blueprints/slideshow.py @@ -11,8 +11,9 @@ from conditional.models.models import SpringEval from conditional.util.auth import get_user from conditional.util.flask import render_template -from conditional.util.ldap import ldap_is_eval_director, ldap_is_intromember, ldap_set_failed, ldap_set_bad_standing, \ +from conditional.util.ldap import ldap_is_intromember, ldap_set_failed, ldap_set_bad_standing, \ ldap_set_inactive, ldap_get_member, ldap_set_not_intro_member +from conditional.util.user_dict import user_dict_is_eval_director logger = structlog.get_logger() @@ -26,7 +27,7 @@ def slideshow_intro_display(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) log.info('Display Intro Slideshow') - if not ldap_is_eval_director(user_dict['account']): + if not user_dict_is_eval_director(user_dict): return redirect("/dashboard") return render_template('intro_eval_slideshow.html', @@ -54,7 +55,7 @@ def slideshow_intro_members(user_dict=None): def slideshow_intro_review(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - if not ldap_is_eval_director(user_dict['account']): + if not user_dict_is_eval_director(user_dict): return redirect("/dashboard", code=302) post_data = request.get_json() @@ -82,7 +83,7 @@ def slideshow_spring_display(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) log.info('Display Membership Evaluations Slideshow') - if not ldap_is_eval_director(user_dict['account']): + if not user_dict_is_eval_director(user_dict): return redirect("/dashboard") return render_template('spring_eval_slideshow.html', @@ -110,7 +111,7 @@ def slideshow_spring_members(user_dict=None): def slideshow_spring_review(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - if not ldap_is_eval_director(user_dict['account']): + if not user_dict_is_eval_director(user_dict): return redirect("/dashboard", code=302) post_data = request.get_json() diff --git a/conditional/models/models.py b/conditional/models/models.py index 69014b8a..2683a599 100644 --- a/conditional/models/models.py +++ b/conditional/models/models.py @@ -72,7 +72,7 @@ def __init__(self, committee, timestamp, approved): class MemberCommitteeAttendance(db.Model): __tablename__ = 'member_committee_attendance' id = Column(Integer, primary_key=True) - uid = Column(String(32), nullable=False) + uid = Column(String(32), nullable=False, index=True) meeting_id = Column(ForeignKey('committee_meetings.id'), nullable=False) def __init__(self, uid, meeting_id): @@ -83,7 +83,7 @@ def __init__(self, uid, meeting_id): class FreshmanCommitteeAttendance(db.Model): __tablename__ = 'freshman_committee_attendance' id = Column(Integer, primary_key=True) - fid = Column(ForeignKey('freshman_accounts.id', ondelete="cascade"), nullable=False) + fid = Column(ForeignKey('freshman_accounts.id', ondelete="cascade"), nullable=False, index=True) meeting_id = Column(ForeignKey('committee_meetings.id'), nullable=False) def __init__(self, fid, meeting_id): @@ -109,7 +109,7 @@ def __init__(self, name, timestamp, approved): class MemberSeminarAttendance(db.Model): __tablename__ = 'member_seminar_attendance' id = Column(Integer, primary_key=True) - uid = Column(String(32), nullable=False) + uid = Column(String(32), nullable=False, index=True) seminar_id = Column(ForeignKey('technical_seminars.id'), nullable=False) def __init__(self, uid, seminar_id): @@ -120,7 +120,7 @@ def __init__(self, uid, seminar_id): class FreshmanSeminarAttendance(db.Model): __tablename__ = 'freshman_seminar_attendance' id = Column(Integer, primary_key=True) - fid = Column(ForeignKey('freshman_accounts.id', ondelete="cascade"), nullable=False) + fid = Column(ForeignKey('freshman_accounts.id', ondelete="cascade"), nullable=False, index=True) seminar_id = Column(ForeignKey('technical_seminars.id'), nullable=False) def __init__(self, fid, seminar_id): @@ -132,7 +132,7 @@ class MajorProject(db.Model): __tablename__ = 'major_projects' id = Column(Integer, primary_key=True) date = Column(Date, nullable=False) - uid = Column(String(32), nullable=False) + uid = Column(String(32), nullable=False, index=True) name = Column(String(64), nullable=False) description = Column(Text) active = Column(Boolean, nullable=False) @@ -163,7 +163,7 @@ def __init__(self, hm_date): class MemberHouseMeetingAttendance(db.Model): __tablename__ = 'member_hm_attendance' id = Column(Integer, primary_key=True) - uid = Column(String(32), nullable=False) + uid = Column(String(32), nullable=False, index=True) meeting_id = Column(ForeignKey('house_meetings.id'), nullable=False) excuse = Column(Text) attendance_status = Column(attendance_enum) @@ -178,7 +178,7 @@ def __init__(self, uid, meeting_id, excuse, status): class FreshmanHouseMeetingAttendance(db.Model): __tablename__ = 'freshman_hm_attendance' id = Column(Integer, primary_key=True) - fid = Column(ForeignKey('freshman_accounts.id', ondelete="cascade"), nullable=False) + fid = Column(ForeignKey('freshman_accounts.id', ondelete="cascade"), nullable=False, index=True) meeting_id = Column(ForeignKey('house_meetings.id'), nullable=False) excuse = Column(Text) attendance_status = Column(attendance_enum) diff --git a/conditional/util/auth.py b/conditional/util/auth.py index bee8e261..83b51e4a 100644 --- a/conditional/util/auth.py +++ b/conditional/util/auth.py @@ -2,19 +2,20 @@ from flask import session -from conditional.util.ldap import ldap_get_member, ldap_is_current_student +from conditional.util.ldap import ldap_get_member def get_user(func): @wraps(func) def wrapped_function(*args, **kwargs): username = str(session["userinfo"].get("preferred_username", "")) account = ldap_get_member(username) - current_student = ldap_is_current_student(account) + + print(session["userinfo"]) user_dict = { 'username': username, 'account': account, - 'student': current_student + 'groups': session["userinfo"]["groups"], } kwargs["user_dict"] = user_dict diff --git a/conditional/util/flask.py b/conditional/util/flask.py index 1ebc0ddc..dbb337d3 100644 --- a/conditional/util/flask.py +++ b/conditional/util/flask.py @@ -4,18 +4,12 @@ from conditional.models.models import EvalSettings from conditional.util.auth import get_user -from conditional.util.ldap import ldap_is_active -from conditional.util.ldap import ldap_is_alumni -from conditional.util.ldap import ldap_is_eboard -from conditional.util.ldap import ldap_is_financial_director -from conditional.util.ldap import ldap_is_eval_director -from conditional.util.ldap import ldap_is_intromember -from conditional.util.ldap import ldap_is_rtp - from conditional.models.models import CommitteeMeeting from conditional.models.models import TechnicalSeminar from conditional import db +from conditional.util.user_dict import user_dict_is_active, user_dict_is_alumni, user_dict_is_eboard, \ + user_dict_is_eval_director, user_dict_is_financial_director, user_dict_is_intromember, user_dict_is_rtp @get_user @@ -27,13 +21,13 @@ def render_template(template_name, user_dict=None, **kwargs): db.session.commit() lockdown = EvalSettings.query.first().site_lockdown accepting_dues = EvalSettings.query.first().accept_dues_until > date.today() - is_active = ldap_is_active(user_dict['account']) - is_alumni = ldap_is_alumni(user_dict['account']) - is_eboard = ldap_is_eboard(user_dict['account']) - is_financial = ldap_is_financial_director(user_dict['account']) - is_eval = ldap_is_eval_director(user_dict['account']) - is_intromember = ldap_is_intromember(user_dict['account']) - is_rtp = ldap_is_rtp(user_dict['account']) + is_active = user_dict_is_active(user_dict) + is_alumni = user_dict_is_alumni(user_dict) + is_eboard = user_dict_is_eboard(user_dict) + is_financial = user_dict_is_financial_director(user_dict) + is_eval = user_dict_is_eval_director(user_dict) + is_intromember = user_dict_is_intromember(user_dict) + is_rtp = user_dict_is_rtp(user_dict) cm_review = len(CommitteeMeeting.query.filter( CommitteeMeeting.approved == False).all()) # pylint: disable=singleton-comparison diff --git a/conditional/util/member.py b/conditional/util/member.py index e9b28e3e..fd68205a 100644 --- a/conditional/util/member.py +++ b/conditional/util/member.py @@ -15,24 +15,33 @@ from conditional.util.ldap import ldap_get_current_students from conditional.util.ldap import ldap_get_intro_members from conditional.util.ldap import ldap_get_onfloor_members -from conditional.util.ldap import ldap_get_roomnumber from conditional.util.ldap import ldap_is_active -from conditional.util.ldap import ldap_is_onfloor from conditional.util.ldap import ldap_is_intromember from conditional.util.ldap import ldap_get_member @service_cache(maxsize=1024) -def get_members_info(): +def get_members_info_active_and_onfloor(): members = ldap_get_current_students() member_list = [] + onfloor_set = set() + active_set = set() + for account in members: uid = account.uid name = account.cn - active = ldap_is_active(account) - onfloor = ldap_is_onfloor(account) - room = ldap_get_roomnumber(account) + groups = "".join(account.groups()) + active = "active" in groups + onfloor = "onfloor" in groups + + if active: + active_set.add(uid) + + if onfloor: + onfloor_set.add(uid) + + room = account.roomNumber hp = account.housingPoints member_list.append({ "uid": uid, @@ -43,8 +52,7 @@ def get_members_info(): "hp": hp }) - return member_list - + return member_list, active_set, onfloor_set def get_freshman_data(user_name): freshman = {} @@ -83,12 +91,21 @@ def get_freshman_data(user_name): freshman['eval_date'] = freshman_data.eval_date return freshman +@service_cache(maxsize=1024) +def get_active_members() -> set[str]: + return {members.uid for members in ldap_get_active_members()} + +@service_cache(maxsize=1024) +def get_intro_members() -> set[str]: + return {member.uid for member in ldap_get_intro_members()} @service_cache(maxsize=1024) -def get_onfloor_members(): - return [uid for uid in [members.uid for members in ldap_get_active_members()] - if uid in [members.uid for members in ldap_get_onfloor_members()]] +def get_all_onfloor_members() -> set[str]: + return {members.uid for members in ldap_get_onfloor_members()} +@service_cache(maxsize=1024) +def get_onfloor_members() -> set[str]: + return get_active_members() & get_all_onfloor_members() def get_cm(member): query_result = CommitteeMeeting.query.join( @@ -155,8 +172,8 @@ def get_voting_members(): semester = "Spring" semester_start = datetime(start_of_year().year + 1, 1, 1) - active_members = set(ldap_get_active_members()) - intro_members = set(ldap_get_intro_members()) + active_members = get_active_members() + intro_members = get_intro_members() coop_members = CurrentCoops.query.filter( CurrentCoops.date_created > start_of_year(), @@ -184,7 +201,6 @@ def get_voting_members(): passed_fall_members = set(passed_fall_members) active_not_intro = active_members - intro_members - active_not_intro = set(map(lambda member: member.uid, active_not_intro)) eligible_members = (active_not_intro - coop_members) | passed_fall_members diff --git a/conditional/util/user_dict.py b/conditional/util/user_dict.py new file mode 100644 index 00000000..bfc9ad3a --- /dev/null +++ b/conditional/util/user_dict.py @@ -0,0 +1,32 @@ +def user_dict_is_in_group(user_dict, group) -> bool: + return group in user_dict['groups'] + +def user_dict_is_active(user_dict) -> bool: + return user_dict_is_in_group(user_dict, 'active') + +def user_dict_is_bad_standing(user_dict) -> bool: + return user_dict_is_in_group(user_dict, 'bad_standing') + +def user_dict_is_alumni(user_dict) -> bool: + return not user_dict_is_active(user_dict) + +def user_dict_is_eboard(user_dict) -> bool: + return user_dict_is_in_group(user_dict, 'eboard') + +def user_dict_is_rtp(user_dict) -> bool: + return user_dict_is_in_group(user_dict, 'active_rtp') + +def user_dict_is_intromember(user_dict) -> bool: + return user_dict_is_in_group(user_dict, 'intromembers') + +def user_dict_is_onfloor(user_dict) -> bool: + return user_dict_is_in_group(user_dict, 'onfloor') + +def user_dict_is_financial_director(user_dict) -> bool: + return user_dict_is_in_group(user_dict, 'eboard-financial') + +def user_dict_is_eval_director(user_dict) -> bool: + return user_dict_is_in_group(user_dict, 'eboard-evaluations') + +def user_dict_is_current_student(user_dict) -> bool: + return user_dict_is_in_group(user_dict, 'current_student') diff --git a/docker-compose.yaml b/docker-compose.yaml index c5ff7a4b..755ee6df 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -7,6 +7,8 @@ services: - conditional-postgres ports: - "127.0.0.1:8080:8080" + volumes: + - ./migrations:/opt/conditional/migrations conditional-postgres: image: docker.io/postgres container_name: conditional-postgres diff --git a/migrations/versions/f1d08673b870_set_uid_and_fid_as_table_indicies.py b/migrations/versions/f1d08673b870_set_uid_and_fid_as_table_indicies.py new file mode 100644 index 00000000..1f3ee0bd --- /dev/null +++ b/migrations/versions/f1d08673b870_set_uid_and_fid_as_table_indicies.py @@ -0,0 +1,38 @@ +"""Set uid and fid as table indicies + +Revision ID: f1d08673b870 +Revises: 7a3904cac24b +Create Date: 2026-02-17 15:02:08.721333 + +""" + +# revision identifiers, used by Alembic. +revision = 'f1d08673b870' +down_revision = '7a3904cac24b' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_index(op.f('ix_freshman_committee_attendance_fid'), 'freshman_committee_attendance', ['fid'], unique=False) + op.create_index(op.f('ix_freshman_hm_attendance_fid'), 'freshman_hm_attendance', ['fid'], unique=False) + op.create_index(op.f('ix_freshman_seminar_attendance_fid'), 'freshman_seminar_attendance', ['fid'], unique=False) + op.create_index(op.f('ix_major_projects_uid'), 'major_projects', ['uid'], unique=False) + op.create_index(op.f('ix_member_committee_attendance_uid'), 'member_committee_attendance', ['uid'], unique=False) + op.create_index(op.f('ix_member_hm_attendance_uid'), 'member_hm_attendance', ['uid'], unique=False) + op.create_index(op.f('ix_member_seminar_attendance_uid'), 'member_seminar_attendance', ['uid'], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_member_seminar_attendance_uid'), table_name='member_seminar_attendance') + op.drop_index(op.f('ix_member_hm_attendance_uid'), table_name='member_hm_attendance') + op.drop_index(op.f('ix_member_committee_attendance_uid'), table_name='member_committee_attendance') + op.drop_index(op.f('ix_major_projects_uid'), table_name='major_projects') + op.drop_index(op.f('ix_freshman_seminar_attendance_fid'), table_name='freshman_seminar_attendance') + op.drop_index(op.f('ix_freshman_hm_attendance_fid'), table_name='freshman_hm_attendance') + op.drop_index(op.f('ix_freshman_committee_attendance_fid'), table_name='freshman_committee_attendance') + # ### end Alembic commands ### From 4b553b6f8b4edd0907776a22b6ef8c8b392e8173 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 17:49:54 -0500 Subject: [PATCH 11/27] Bump isort from 4.3.21 to 6.1.0 (#482) Bumps [isort](https://github.com/PyCQA/isort) from 4.3.21 to 6.1.0. - [Release notes](https://github.com/PyCQA/isort/releases) - [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md) - [Commits](https://github.com/PyCQA/isort/compare/4.3.21...6.1.0) --- updated-dependencies: - dependency-name: isort dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.in | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.in b/requirements.in index 99d38438..8122ed63 100644 --- a/requirements.in +++ b/requirements.in @@ -9,7 +9,7 @@ Flask-Gzip~=0.2 Flask-pyoidc~=3.14.3 Flask-SQLAlchemy~=3.1.1 gunicorn~=22.0.0 -isort~=4.3.4 +isort~=6.1.0 itsdangerous~=2.2.0 Jinja2~=3.1.6 lazy-object-proxy~=1.4.0 diff --git a/requirements.txt b/requirements.txt index c066dd06..07ba14a5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -76,7 +76,7 @@ importlib-metadata==8.7.1 # via opentelemetry-api importlib-resources==6.5.2 # via flask-pyoidc -isort==4.3.21 +isort==6.1.0 # via # -r requirements.in # pylint From d3c9aec735d78d238a719f96060b6973a5fbe2c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 17:54:16 -0500 Subject: [PATCH 12/27] Bump werkzeug from 3.1.5 to 3.1.6 (#480) Bumps [werkzeug](https://github.com/pallets/werkzeug) from 3.1.5 to 3.1.6. - [Release notes](https://github.com/pallets/werkzeug/releases) - [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/werkzeug/compare/3.1.5...3.1.6) --- updated-dependencies: - dependency-name: werkzeug dependency-version: 3.1.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.in | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.in b/requirements.in index 8122ed63..ce565f2e 100644 --- a/requirements.in +++ b/requirements.in @@ -26,6 +26,6 @@ sentry-sdk[flask]~=2.24.1 six~=1.17.0 SQLAlchemy~=2.0.40 structlog~=18.1.0 -Werkzeug~=3.1.3 +Werkzeug~=3.1.6 wrapt~=1.17.2 click~=8.1 diff --git a/requirements.txt b/requirements.txt index 07ba14a5..0435ea76 100644 --- a/requirements.txt +++ b/requirements.txt @@ -198,7 +198,7 @@ urllib3==2.6.3 # via # requests # sentry-sdk -werkzeug==3.1.5 +werkzeug==3.1.6 # via # -r requirements.in # flask From 65da067926f786ba51c6651a77b88f04f5d4ecec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 17:58:18 -0500 Subject: [PATCH 13/27] Bump mako from 1.0.14 to 1.3.10 (#484) Bumps [mako](https://github.com/sqlalchemy/mako) from 1.0.14 to 1.3.10. - [Release notes](https://github.com/sqlalchemy/mako/releases) - [Changelog](https://github.com/sqlalchemy/mako/blob/main/CHANGES) - [Commits](https://github.com/sqlalchemy/mako/commits) --- updated-dependencies: - dependency-name: mako dependency-version: 1.3.10 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.in | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.in b/requirements.in index ce565f2e..2715fe43 100644 --- a/requirements.in +++ b/requirements.in @@ -13,7 +13,7 @@ isort~=6.1.0 itsdangerous~=2.2.0 Jinja2~=3.1.6 lazy-object-proxy~=1.4.0 -Mako~=1.0.7 +Mako~=1.3.10 MarkupSafe~=3.0.2 mccabe~=0.6.1 oic~=1.6.1 diff --git a/requirements.txt b/requirements.txt index 0435ea76..b0d883bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -90,7 +90,7 @@ jinja2==3.1.6 # flask lazy-object-proxy==1.4.3 # via -r requirements.in -mako==1.0.14 +mako==1.3.10 # via # -r requirements.in # alembic From 1a1d5f74277b28bc3d8e61942ad8219db30ab2a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:03:26 -0500 Subject: [PATCH 14/27] Bump mccabe from 0.6.1 to 0.7.0 (#483) Bumps [mccabe](https://github.com/pycqa/mccabe) from 0.6.1 to 0.7.0. - [Commits](https://github.com/pycqa/mccabe/compare/0.6.1...0.7.0) --- updated-dependencies: - dependency-name: mccabe dependency-version: 0.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.in | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.in b/requirements.in index 2715fe43..cbdffdd5 100644 --- a/requirements.in +++ b/requirements.in @@ -15,7 +15,7 @@ Jinja2~=3.1.6 lazy-object-proxy~=1.4.0 Mako~=1.3.10 MarkupSafe~=3.0.2 -mccabe~=0.6.1 +mccabe~=0.7.0 oic~=1.6.1 pip-tools~=7.4.1 psycopg2-binary~=2.9.3 diff --git a/requirements.txt b/requirements.txt index b0d883bd..b2c57668 100644 --- a/requirements.txt +++ b/requirements.txt @@ -103,7 +103,7 @@ markupsafe==3.0.3 # mako # sentry-sdk # werkzeug -mccabe==0.6.1 +mccabe==0.7.0 # via # -r requirements.in # pylint From aefd56f1bbe75c4ae31f18e60fc1269b30c3ed7d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:08:04 -0500 Subject: [PATCH 15/27] Bump structlog from 18.1.0 to 25.5.0 (#481) Bumps [structlog](https://github.com/hynek/structlog) from 18.1.0 to 25.5.0. - [Release notes](https://github.com/hynek/structlog/releases) - [Changelog](https://github.com/hynek/structlog/blob/main/CHANGELOG.md) - [Commits](https://github.com/hynek/structlog/compare/18.1.0...25.5.0) --- updated-dependencies: - dependency-name: structlog dependency-version: 25.5.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.in | 2 +- requirements.txt | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/requirements.in b/requirements.in index cbdffdd5..48c51eb3 100644 --- a/requirements.in +++ b/requirements.in @@ -25,7 +25,7 @@ python-editor~=1.0.3 sentry-sdk[flask]~=2.24.1 six~=1.17.0 SQLAlchemy~=2.0.40 -structlog~=18.1.0 +structlog~=25.5.0 Werkzeug~=3.1.6 wrapt~=1.17.2 click~=8.1 diff --git a/requirements.txt b/requirements.txt index b2c57668..7036755e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -170,7 +170,6 @@ six==1.17.0 # -r requirements.in # pyjwkest # python-dateutil - # structlog sqlalchemy==2.0.45 # via # -r requirements.in @@ -178,7 +177,7 @@ sqlalchemy==2.0.45 # flask-sqlalchemy srvlookup==3.0.0 # via csh-ldap -structlog==18.1.0 +structlog==25.5.0 # via -r requirements.in tomlkit==0.14.0 # via pylint From 636dff4e55d53a70bd340670c94024454b6b04f3 Mon Sep 17 00:00:00 2001 From: Ata Noor <43101822+atauln@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:23:31 -0500 Subject: [PATCH 16/27] slack: Added more to Slack message (#385) * slack: Added more to Slack message * Removed trailing whitespace on line 74 in major_project_submission.py --------- Co-authored-by: Noah Hanford --- conditional/blueprints/major_project_submission.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/conditional/blueprints/major_project_submission.py b/conditional/blueprints/major_project_submission.py index b0e2a32f..aa3ba66c 100644 --- a/conditional/blueprints/major_project_submission.py +++ b/conditional/blueprints/major_project_submission.py @@ -79,7 +79,9 @@ def submit_major_project(user_dict=None): send_slack_ping( { "text": f" *{get_member_name(username)}* ({username})" - f" submitted their major project, *{name}*!" + f" submitted their major project, *{name}*! Please be sure to reach out" + f" to E-Board members to answer any questions they may have regarding" + f" your project!" } ) db.session.add(project) From b05bdd621ddace0d65c4914aea5603cbc3a08da9 Mon Sep 17 00:00:00 2001 From: "Noah Hanford (spaced)" Date: Wed, 25 Feb 2026 13:49:30 -0500 Subject: [PATCH 17/27] added gatekeep page as currently implemented in constitution --- conditional/blueprints/dashboard.py | 17 ++++++- conditional/templates/dashboard.html | 72 +++++++++++++++++++++++++++- conditional/util/member.py | 44 ++++++++++------- 3 files changed, 113 insertions(+), 20 deletions(-) diff --git a/conditional/blueprints/dashboard.py b/conditional/blueprints/dashboard.py index c080ddaf..32587a77 100644 --- a/conditional/blueprints/dashboard.py +++ b/conditional/blueprints/dashboard.py @@ -12,7 +12,8 @@ from conditional.util.auth import get_user from conditional.util.flask import render_template from conditional.util.housing import get_queue_position -from conditional.util.member import get_active_members, get_freshman_data, get_voting_members, get_cm, get_hm, req_cm +from conditional.util.member import gatekeep_values, get_active_members, get_freshman_data, get_voting_members, \ + get_cm, get_hm, is_gatekeep_active, req_cm from conditional.util.user_dict import user_dict_is_active, user_dict_is_bad_standing, user_dict_is_intromember, \ user_dict_is_onfloor @@ -146,4 +147,18 @@ def display_dashboard(user_dict=None): data['hm_attendance'] = hm_attendance data['hm_attendance_len'] = len(hm_attendance) + gatekeep_info = gatekeep_values(uid) + gatekeep_result = 'disenfranchised' + + if gatekeep_info['result']: + gatekeep_result = 'passing' + + data['gatekeep_active'] = is_gatekeep_active() + data['gatekeep'] = { + 'status': gatekeep_result, + 'committee_meetings': gatekeep_info['c_meetings'], + 'technical_seminars': gatekeep_info['t_seminars'], + 'hm_missed': gatekeep_info['h_meetings_missed'] + } + return render_template('dashboard.html', **data) diff --git a/conditional/templates/dashboard.html b/conditional/templates/dashboard.html index 693e228c..397c8ef4 100644 --- a/conditional/templates/dashboard.html +++ b/conditional/templates/dashboard.html @@ -233,7 +233,6 @@

Member Statistics

- {% if major_projects_count == 0 and not active%} {% elif major_projects_count > 0 %} @@ -318,6 +317,77 @@

Housing Status

+ {% if active %} +
+
+

Gatekeep + {% if gatekeep['status'] == "passing" %} + Ok! + {% elif gatekeep['status'] == "disenfranchised" and gatekeep_active%} + Disenfranchised + {% elif active %} + Pending + {% endif %} +

+
+
+ + + {% if not gatekeep_active %} + + + + + {% endif %} + + + + + + + + + + + + + +
Status + + Gatekeep Inactive Until 6 Weeks + +
Technical Seminars + + {% if gatekeep['technical_seminars'] >= 2 %} + + {% else %} + + {% endif %} + {{ gatekeep['technical_seminars'] }} / 2 + +
Directorship Meetings + + {% if gatekeep['committee_meetings'] >= 6 %} + + {% else %} + + {% endif %} + {{ gatekeep['committee_meetings']}} / 6 + +
House Meetings Missed + {% if gatekeep['hm_missed'] == 0 %} + None + {% elif gatekeep['hm_missed'] <= 1 %} + {{ gatekeep['hm_missed'] }} Missed + {% else %} + {{gatekeep['hm_missed']}} + {% endif %} + +
+
+
+ {% endif %} + {% if hm_attendance_len == 0 and active%}
You haven't missed any house meetings. diff --git a/conditional/util/member.py b/conditional/util/member.py index fd68205a..c859eddc 100644 --- a/conditional/util/member.py +++ b/conditional/util/member.py @@ -129,7 +129,6 @@ def get_cm(member): return c_meetings - def get_hm(member, only_absent=False): h_meetings = MemberHouseMeetingAttendance.query.outerjoin( HouseMeeting, @@ -144,7 +143,7 @@ def get_hm(member, only_absent=False): return h_meetings -# @service_cache(maxsize=128) +@service_cache(maxsize=128) def req_cm(uid, members_on_coop=None): # Get the number of required committee meetings based on if the member # is going on co-op in the current operating session. @@ -162,8 +161,15 @@ def req_cm(uid, members_on_coop=None): return 30 -@service_cache(maxsize=256) -def get_voting_members(): +def is_gatekeep_active(): + today = datetime.today() + before_evals_one = len(FreshmanAccount.query.filter(FreshmanAccount.eval_date > today).limit(1).all()) + before_evals_two = len(FreshmanEvalData.query.filter(FreshmanEvalData.eval_date > today).limit(1).all()) + + return not (before_evals_one or before_evals_two) + + +def get_semester_info() -> tuple[str, datetime]: today = datetime.today() if today < datetime(start_of_year().year, 12, 31): semester = "Fall" @@ -172,6 +178,12 @@ def get_voting_members(): semester = "Spring" semester_start = datetime(start_of_year().year + 1, 1, 1) + return (semester, semester_start) + +@service_cache(maxsize=256) +def get_voting_members(): + semester, semester_start = get_semester_info() + active_members = get_active_members() intro_members = get_intro_members() @@ -205,9 +217,7 @@ def get_voting_members(): eligible_members = (active_not_intro - coop_members) | passed_fall_members # Check to see if there's an Intro Evals in the future of this semester. If there is, everyone gets to vote! - before_evals_one = len(FreshmanAccount.query.filter(FreshmanAccount.eval_date > today).limit(1).all()) - before_evals_two = len(FreshmanEvalData.query.filter(FreshmanEvalData.eval_date > today).limit(1).all()) - if before_evals_one > 0 or before_evals_two > 0: + if not is_gatekeep_active(): return eligible_members passing_dm = set(member.uid for member in MemberCommitteeAttendance.query.join( @@ -265,23 +275,19 @@ def get_voting_members(): def gatekeep_status(username): - today = datetime.today() - # Check to see if there's an Intro Evals in the future of this semester. If there is, everyone gets to vote! - before_evals_one = len(FreshmanAccount.query.filter(FreshmanAccount.eval_date > today).limit(1).all()) - before_evals_two = len(FreshmanEvalData.query.filter(FreshmanEvalData.eval_date > today).limit(1).all()) - if before_evals_one > 0 or before_evals_two > 0: + if not is_gatekeep_active(): return { "result": True, "h_meetings_missed": 0, "c_meetings": 0, "t_seminars": 0, } - if today < datetime(start_of_year().year, 12, 31): - semester = "Fall" - semester_start = datetime(start_of_year().year, 6, 1) - else: - semester = "Spring" - semester_start = datetime(start_of_year().year + 1, 1, 1) + + return gatekeep_values(username) + + +def gatekeep_values(username): + semester, semester_start = get_semester_info() # groups ldap_member = ldap_get_member(username) @@ -346,6 +352,8 @@ def gatekeep_status(username): ) .count() ) + + h_meetings_missed = 1 result = eligibility_of_groups and (d_meetings >= 6 and t_seminars >= 2 and h_meetings_missed < 2) # pylint: disable=chained-comparison return { From 98bfac9fbc0619b231caa6ffe7638b3a8caeb080 Mon Sep 17 00:00:00 2001 From: "Noah Hanford (spaced)" Date: Wed, 25 Feb 2026 15:00:06 -0500 Subject: [PATCH 18/27] gatekeep page --- conditional/__init__.py | 4 +- conditional/blueprints/gatekeep.py | 127 +++++++++++++++ conditional/templates/gatekeep.html | 190 +++++++++++++++++++++++ conditional/templates/nav.html | 1 + conditional/templates/nav_protected.html | 1 + conditional/util/user_dict.py | 1 + 6 files changed, 322 insertions(+), 2 deletions(-) create mode 100644 conditional/blueprints/gatekeep.py create mode 100644 conditional/templates/gatekeep.html diff --git a/conditional/__init__.py b/conditional/__init__.py index 4d7cf1df..c2644948 100644 --- a/conditional/__init__.py +++ b/conditional/__init__.py @@ -118,6 +118,7 @@ def database_processor(logger, log_method, event_dict): # pylint: disable=unuse from .blueprints.intro_evals_form import intro_evals_form_bp from .blueprints.housing import housing_bp from .blueprints.spring_evals import spring_evals_bp +from .blueprints.gatekeep import gatekeep_bp from .blueprints.conditional import conditionals_bp from .blueprints.member_management import member_management_bp from .blueprints.slideshow import slideshow_bp @@ -132,6 +133,7 @@ def database_processor(logger, log_method, event_dict): # pylint: disable=unuse app.register_blueprint(intro_evals_form_bp) app.register_blueprint(housing_bp) app.register_blueprint(spring_evals_bp) +app.register_blueprint(gatekeep_bp) app.register_blueprint(conditionals_bp) app.register_blueprint(member_management_bp) app.register_blueprint(slideshow_bp) @@ -139,8 +141,6 @@ def database_processor(logger, log_method, event_dict): # pylint: disable=unuse app.register_blueprint(co_op_bp) app.register_blueprint(log_bp) -from .util.ldap import ldap_get_member - @app.route('/') def static_proxy(path): diff --git a/conditional/blueprints/gatekeep.py b/conditional/blueprints/gatekeep.py new file mode 100644 index 00000000..406c6282 --- /dev/null +++ b/conditional/blueprints/gatekeep.py @@ -0,0 +1,127 @@ +import structlog +from flask import Blueprint, request +from sqlalchemy import func + +from conditional import start_of_year, auth +from conditional.models.models import CommitteeMeeting, CurrentCoops, HouseMeeting, MemberCommitteeAttendance, MemberSeminarAttendance, TechnicalSeminar +from conditional.models.models import MemberHouseMeetingAttendance +from conditional.util.auth import get_user +from conditional.util.flask import render_template +from conditional.util.ldap import ldap_get_active_members +from conditional.util.member import get_semester_info + +gatekeep_bp = Blueprint('gatekeep_bp', __name__) + +logger = structlog.get_logger() + +@gatekeep_bp.route('/gatekeep_status/') +@auth.oidc_auth("default") +@get_user +def display_spring_evals(internal=False, user_dict=None): + log = logger.new(request=request, auth_dict=user_dict) + log.info('Display Gatekeep Status Listing') + + _, semester_start = get_semester_info() + active_members = ldap_get_active_members() + + cm_count = dict([tuple(row) for row in MemberCommitteeAttendance.query.join( + CommitteeMeeting, + MemberCommitteeAttendance.meeting_id == CommitteeMeeting.id + ).with_entities( + MemberCommitteeAttendance.uid, + CommitteeMeeting.timestamp, + CommitteeMeeting.approved, + ).filter( + CommitteeMeeting.approved, + CommitteeMeeting.timestamp >= semester_start + ).with_entities( + MemberCommitteeAttendance.uid, + func.count(MemberCommitteeAttendance.uid) #pylint: disable=not-callable + ).group_by( + MemberCommitteeAttendance.uid + ).all()]) + + ts_count = dict([tuple(row) for row in MemberSeminarAttendance.query.join( + TechnicalSeminar, + MemberSeminarAttendance.seminar_id == TechnicalSeminar.id + ).with_entities( + MemberSeminarAttendance.uid, + TechnicalSeminar.timestamp, + TechnicalSeminar.approved, + ).filter( + TechnicalSeminar.approved, + TechnicalSeminar.timestamp >= semester_start + ).with_entities( + MemberSeminarAttendance.uid, + func.count(MemberSeminarAttendance.uid) #pylint: disable=not-callable + ).group_by( + MemberSeminarAttendance.uid + ).all()]) + + hm_missed = dict([tuple(row) for row in MemberHouseMeetingAttendance.query.join( + HouseMeeting, + MemberHouseMeetingAttendance.meeting_id == HouseMeeting.id + ).filter( + HouseMeeting.date >= semester_start, + MemberHouseMeetingAttendance.attendance_status == 'Absent' + ).with_entities( + MemberHouseMeetingAttendance.uid, + func.count(MemberHouseMeetingAttendance.uid) #pylint: disable=not-callable + ).group_by( + MemberHouseMeetingAttendance.uid + ).all()]) + + gk_members = [] + for account in active_members: + uid = account.uid + name = account.cn + + member_missed_hms = [] + + if hm_missed.get(uid, 0) != 0: + member_missed_hms = MemberHouseMeetingAttendance.query.join( + HouseMeeting, + MemberHouseMeetingAttendance.meeting_id == HouseMeeting.id + ).filter( + HouseMeeting.date >= start_of_year(), + MemberHouseMeetingAttendance.attendance_status == 'Absent', + MemberHouseMeetingAttendance.uid == uid, + ).with_entities( + func.array_agg(HouseMeeting.date) + ).scalar() + + cm_attended_count = cm_count.get(uid, 0) + ts_attended_count = ts_count.get(uid, 0) + + passing = len(member_missed_hms) <= 1 and cm_attended_count >= 6 and ts_attended_count >= 2 + + status = 'disenfranchised' + + if passing: + status = 'passed' + + member = { + 'name': name, + 'uid': uid, + 'status': status, + 'committee_meetings': cm_attended_count, + 'technical_seminars': ts_attended_count, + 'req_meetings': 6, + 'req_seminars': 2, + 'house_meetings_missed': member_missed_hms, + } + + gk_members.append(member) + + gk_members.sort(key=lambda x: x['committee_meetings'], reverse=True) + gk_members.sort(key=lambda x: x['technical_seminars'], reverse=True) + gk_members.sort(key=lambda x: len(x['house_meetings_missed'])) + # return names in 'first last (username)' format + if internal: + return gk_members + + return render_template('gatekeep.html', + username=user_dict['username'], + members=gk_members, + req_meetings=6, + req_seminars=2) diff --git a/conditional/templates/gatekeep.html b/conditional/templates/gatekeep.html new file mode 100644 index 00000000..35013242 --- /dev/null +++ b/conditional/templates/gatekeep.html @@ -0,0 +1,190 @@ +{% extends "nav_protected.html" %} +{% block title %} +Gatekeep Results +{% endblock %} +{% block body %} +
+
+
+

Gatekeep Results

+
+
+
+
Table View
+ + +
+
+
+
+ {% for m in members %} +
+
+ + +
+
+
+
+ {{m['uid']}} + + {% if m['status'] == "passed" %} + + {% elif not gatekeep_active %} + + {% else %} + + {% endif %} + +
+
+
+

{{m['name']}}

+
{{m['uid']}}
+
+ +
+
+
+ {% if m['committee_meetings'] < req_meetings %} +
+ Directorship Meetings + {{m['committee_meetings']}} / {{ req_meetings }} +
+ {% else %} +
+ Directorship Meetings + {{m['committee_meetings']}} / {{ req_meetings }} +
+ {% endif %} +
+
+ {% if m['technical_seminars'] < req_seminars %} +
+ Technical Seminars + {{m['technical_seminars']}} / {{ req_seminars }} +
+ {% else %} +
+ Technical Seminars + {{m['technical_seminars']}} / {{ req_seminars }} +
+ {% endif %} +
+
+ {% if m['house_meetings_missed']|length > 1 %} +
+ House Meetings Missed + {{m['house_meetings_missed']|length}} +
+ {% else %} +
+ House Meetings Missed + {{m['house_meetings_missed']|length}} +
+ {% endif %} +
+
+
+
+ + {% if m['house_meetings_missed']|length > 0%} +
+ +
+ {% endif %} +
+ {% if m['house_meetings_missed']|length > 0 %} + +
+

Missed House Meetings

+ + + + + + + + + {% for hm_missed in m['house_meetings_missed'] %} + + + + + {% endfor %} + +
DateDescription
{{hm_missed}}
+ {% endif %} + +
+
+
+
+ + + {% endfor %} +
+ +
+{% endblock %} diff --git a/conditional/templates/nav.html b/conditional/templates/nav.html index 96049dc9..0b0b400b 100644 --- a/conditional/templates/nav.html +++ b/conditional/templates/nav.html @@ -18,6 +18,7 @@ diff --git a/conditional/templates/nav_protected.html b/conditional/templates/nav_protected.html index 7032353d..0e8bddc0 100644 --- a/conditional/templates/nav_protected.html +++ b/conditional/templates/nav_protected.html @@ -18,6 +18,7 @@ diff --git a/conditional/util/user_dict.py b/conditional/util/user_dict.py index bfc9ad3a..39e5b3fa 100644 --- a/conditional/util/user_dict.py +++ b/conditional/util/user_dict.py @@ -11,6 +11,7 @@ def user_dict_is_alumni(user_dict) -> bool: return not user_dict_is_active(user_dict) def user_dict_is_eboard(user_dict) -> bool: + return True return user_dict_is_in_group(user_dict, 'eboard') def user_dict_is_rtp(user_dict) -> bool: From 156c04a92ef11088b248fd607da2c5a1f5454f59 Mon Sep 17 00:00:00 2001 From: "Noah Hanford (spaced)" Date: Wed, 25 Feb 2026 15:48:00 -0500 Subject: [PATCH 19/27] fix gatekeep page displaying the right thing when it's active --- conditional/blueprints/gatekeep.py | 5 ++++- conditional/templates/gatekeep.html | 12 +++++------- conditional/util/member.py | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/conditional/blueprints/gatekeep.py b/conditional/blueprints/gatekeep.py index 406c6282..42f9b390 100644 --- a/conditional/blueprints/gatekeep.py +++ b/conditional/blueprints/gatekeep.py @@ -8,7 +8,7 @@ from conditional.util.auth import get_user from conditional.util.flask import render_template from conditional.util.ldap import ldap_get_active_members -from conditional.util.member import get_semester_info +from conditional.util.member import get_semester_info, is_gatekeep_active gatekeep_bp = Blueprint('gatekeep_bp', __name__) @@ -120,8 +120,11 @@ def display_spring_evals(internal=False, user_dict=None): if internal: return gk_members + gatekeep_active = is_gatekeep_active() + return render_template('gatekeep.html', username=user_dict['username'], members=gk_members, + gatekeep_active=gatekeep_active, req_meetings=6, req_seminars=2) diff --git a/conditional/templates/gatekeep.html b/conditional/templates/gatekeep.html index 35013242..3ed0519d 100644 --- a/conditional/templates/gatekeep.html +++ b/conditional/templates/gatekeep.html @@ -9,19 +9,17 @@

Gatekeep Results

-
+
Table View
- - -
-
+ + +
+
{% for m in members %}
- -
diff --git a/conditional/util/member.py b/conditional/util/member.py index c859eddc..bc69936f 100644 --- a/conditional/util/member.py +++ b/conditional/util/member.py @@ -166,7 +166,7 @@ def is_gatekeep_active(): before_evals_one = len(FreshmanAccount.query.filter(FreshmanAccount.eval_date > today).limit(1).all()) before_evals_two = len(FreshmanEvalData.query.filter(FreshmanEvalData.eval_date > today).limit(1).all()) - return not (before_evals_one or before_evals_two) + return not (before_evals_one > 0 or before_evals_two > 0) def get_semester_info() -> tuple[str, datetime]: From ae0f47e6bb4da491f846499e94a5f1cc7157b111 Mon Sep 17 00:00:00 2001 From: "Noah Hanford (spaced)" Date: Wed, 25 Feb 2026 15:56:53 -0500 Subject: [PATCH 20/27] Fix some formatting --- conditional/templates/gatekeep.html | 8 ++++---- conditional/util/user_dict.py | 1 - frontend/stylesheets/pages/_management.scss | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/conditional/templates/gatekeep.html b/conditional/templates/gatekeep.html index 3ed0519d..168eec94 100644 --- a/conditional/templates/gatekeep.html +++ b/conditional/templates/gatekeep.html @@ -6,7 +6,7 @@
-

Gatekeep Results

+

Gatekeep Results

@@ -27,11 +27,11 @@

Gatekeep Results

{{m['uid']}} {% if m['status'] == "passed" %} - + {% elif not gatekeep_active %} - + {% else %} - + {% endif %}
diff --git a/conditional/util/user_dict.py b/conditional/util/user_dict.py index 39e5b3fa..bfc9ad3a 100644 --- a/conditional/util/user_dict.py +++ b/conditional/util/user_dict.py @@ -11,7 +11,6 @@ def user_dict_is_alumni(user_dict) -> bool: return not user_dict_is_active(user_dict) def user_dict_is_eboard(user_dict) -> bool: - return True return user_dict_is_in_group(user_dict, 'eboard') def user_dict_is_rtp(user_dict) -> bool: diff --git a/frontend/stylesheets/pages/_management.scss b/frontend/stylesheets/pages/_management.scss index ba5d2d07..8a67cbe8 100644 --- a/frontend/stylesheets/pages/_management.scss +++ b/frontend/stylesheets/pages/_management.scss @@ -1,6 +1,6 @@ .switch-label { display: block; - padding: 17px 10px 10px; + padding: 0px 10px 10px; text-align: center; } From 3bba7828755f11f481d2d73187e483801ea493f6 Mon Sep 17 00:00:00 2001 From: "Noah Hanford (spaced)" Date: Wed, 25 Feb 2026 16:01:10 -0500 Subject: [PATCH 21/27] fix linting --- conditional/blueprints/gatekeep.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/conditional/blueprints/gatekeep.py b/conditional/blueprints/gatekeep.py index 42f9b390..f037516f 100644 --- a/conditional/blueprints/gatekeep.py +++ b/conditional/blueprints/gatekeep.py @@ -3,7 +3,8 @@ from sqlalchemy import func from conditional import start_of_year, auth -from conditional.models.models import CommitteeMeeting, CurrentCoops, HouseMeeting, MemberCommitteeAttendance, MemberSeminarAttendance, TechnicalSeminar +from conditional.models.models import CommitteeMeeting, HouseMeeting, MemberCommitteeAttendance, \ + MemberSeminarAttendance, TechnicalSeminar from conditional.models.models import MemberHouseMeetingAttendance from conditional.util.auth import get_user from conditional.util.flask import render_template From 995cb1334ad1e2c10bad34935753f536624679db Mon Sep 17 00:00:00 2001 From: "Noah Hanford (spaced)" Date: Wed, 25 Feb 2026 16:34:49 -0500 Subject: [PATCH 22/27] sonarqube for cole and stella --- .github/workflows/sonarqube.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index f7f96e00..5c157a50 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -4,6 +4,8 @@ on: push: branches: - develop + - master + pull_request: jobs: From 769fa77c205fc8f5f656d12b53d9e6a9936ae41c Mon Sep 17 00:00:00 2001 From: "Noah Hanford (spaced)" Date: Wed, 25 Feb 2026 16:50:21 -0500 Subject: [PATCH 23/27] trying to make sonarqube happy --- conditional/blueprints/gatekeep.py | 12 ++++++------ conditional/templates/gatekeep.html | 4 ++-- conditional/util/member.py | 1 - 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/conditional/blueprints/gatekeep.py b/conditional/blueprints/gatekeep.py index f037516f..c7fb0ae6 100644 --- a/conditional/blueprints/gatekeep.py +++ b/conditional/blueprints/gatekeep.py @@ -25,7 +25,7 @@ def display_spring_evals(internal=False, user_dict=None): _, semester_start = get_semester_info() active_members = ldap_get_active_members() - cm_count = dict([tuple(row) for row in MemberCommitteeAttendance.query.join( + cm_count = {row[0]: row[1] for row in MemberCommitteeAttendance.query.join( CommitteeMeeting, MemberCommitteeAttendance.meeting_id == CommitteeMeeting.id ).with_entities( @@ -40,9 +40,9 @@ def display_spring_evals(internal=False, user_dict=None): func.count(MemberCommitteeAttendance.uid) #pylint: disable=not-callable ).group_by( MemberCommitteeAttendance.uid - ).all()]) + ).all()} - ts_count = dict([tuple(row) for row in MemberSeminarAttendance.query.join( + ts_count = {row[0]: row[1] for row in MemberSeminarAttendance.query.join( TechnicalSeminar, MemberSeminarAttendance.seminar_id == TechnicalSeminar.id ).with_entities( @@ -57,9 +57,9 @@ def display_spring_evals(internal=False, user_dict=None): func.count(MemberSeminarAttendance.uid) #pylint: disable=not-callable ).group_by( MemberSeminarAttendance.uid - ).all()]) + ).all()} - hm_missed = dict([tuple(row) for row in MemberHouseMeetingAttendance.query.join( + hm_missed = {row[0]: row[1] for row in MemberHouseMeetingAttendance.query.join( HouseMeeting, MemberHouseMeetingAttendance.meeting_id == HouseMeeting.id ).filter( @@ -70,7 +70,7 @@ def display_spring_evals(internal=False, user_dict=None): func.count(MemberHouseMeetingAttendance.uid) #pylint: disable=not-callable ).group_by( MemberHouseMeetingAttendance.uid - ).all()]) + ).all()} gk_members = [] for account in active_members: diff --git a/conditional/templates/gatekeep.html b/conditional/templates/gatekeep.html index 168eec94..848f5f5f 100644 --- a/conditional/templates/gatekeep.html +++ b/conditional/templates/gatekeep.html @@ -88,9 +88,9 @@
{{m['uid']}}
{% if m['house_meetings_missed']|length > 0%}
-
{% endif %}
diff --git a/conditional/util/member.py b/conditional/util/member.py index bc69936f..f091cd4f 100644 --- a/conditional/util/member.py +++ b/conditional/util/member.py @@ -353,7 +353,6 @@ def gatekeep_values(username): .count() ) - h_meetings_missed = 1 result = eligibility_of_groups and (d_meetings >= 6 and t_seminars >= 2 and h_meetings_missed < 2) # pylint: disable=chained-comparison return { From 502fb2c1dbf7a74d197313f3829b006f64c188a8 Mon Sep 17 00:00:00 2001 From: "Noah Hanford (spaced)" Date: Wed, 25 Feb 2026 17:00:23 -0500 Subject: [PATCH 24/27] more trying to make it happy --- conditional/templates/gatekeep.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conditional/templates/gatekeep.html b/conditional/templates/gatekeep.html index 848f5f5f..d5cb053e 100644 --- a/conditional/templates/gatekeep.html +++ b/conditional/templates/gatekeep.html @@ -88,7 +88,7 @@
{{m['uid']}}
{% if m['house_meetings_missed']|length > 0%}
-
From 4eb3dd29d17c38c3be6ae1de5549cd3633906a41 Mon Sep 17 00:00:00 2001 From: "Noah Hanford (spaced)" Date: Wed, 25 Feb 2026 17:21:40 -0500 Subject: [PATCH 25/27] update readme --- README.md | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 033275be..0a245208 100644 --- a/README.md +++ b/README.md @@ -29,13 +29,16 @@ You can either develop using the dev database, or use the local database provide Using the local database is detailed below, but both options will require the dev database password, so you will have to ask an RTP for this too +#### Forcing evals/rtp or anything else +All of the role checking is done in `conditional/utils/user_dict.py`, and you can change the various functions to `return True` for debugging + ### Run (Without Docker) To run the application without using containers, you must have the latest version of [Python 3](https://www.python.org/downloads/) and [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) installed. Once you have those installed, create a new virtualenv and install the Python dependencies: ```sh -virtualenv .conditionalenv -p `which python3` -source .conditionalenv/bin/activate +virtualenv .venv +source .venv/bin/activate pip install -r requirements.txt ``` @@ -89,7 +92,9 @@ Which can be restarted every time changes are made 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. -### Local database +### Database Stuff + +#### Local database You can run the database locally using the docker compose @@ -106,18 +111,23 @@ To run migration commands in the local database, you can run the commands inside podman exec conditional flask db upgrade ``` -### Database Migrations +#### Database Migrations If the database schema is changed after initializing the database, you must migrate it to the new schema by running: -``` +```sh flask db upgrade +# or, to run it inside the container for use with local databases (DO THIS +podman exec conditional flask db upgrade ``` + At the same time, if you change the database schema, you must generate a new migration by running: -``` +```sh flask db migrate +# or, to run it inside the container for use with local databases (DO THIS +podman exec conditional flask db migrate ``` The new migration script in `migrations/versions` should be verified before being committed, as Alembic may not detect every change you make to the models. @@ -128,7 +138,7 @@ For more information, refer to the [Flask-Migrate](https://flask-migrate.readthe Conditional includes a utility to facilitate data migrations from the old Evals DB. This isn't necessary to run Conditional. To perform this migration, run the following commands before starting the application: -``` +```sh pip install pymysql flask zoo ``` From 3858339f91806c90ab5ff1a9ef96cd935562065f Mon Sep 17 00:00:00 2001 From: Will Hellinger Date: Thu, 26 Feb 2026 15:37:28 -0500 Subject: [PATCH 26/27] Add PR support to sonarqube workflow --- .github/workflows/sonarqube.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index f7f96e00..d9816c5b 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -4,6 +4,7 @@ on: push: branches: - develop + pull_request: jobs: From 33ddcc659cbc90be12ccecd7212b8f08b7cf8673 Mon Sep 17 00:00:00 2001 From: Noah Hanford Date: Fri, 27 Feb 2026 21:25:54 -0500 Subject: [PATCH 27/27] fix frosh stuff more (#490) * fix frosh stuff more Only this semesters frosh on intro page and fix ts attendance for frosh with accounts * guys I fixed it --- conditional/blueprints/intro_evals.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/conditional/blueprints/intro_evals.py b/conditional/blueprints/intro_evals.py index b6b60a7a..e4464c3d 100644 --- a/conditional/blueprints/intro_evals.py +++ b/conditional/blueprints/intro_evals.py @@ -19,12 +19,14 @@ from conditional.util.auth import get_user from conditional.util.flask import render_template from conditional.util.ldap import ldap_get_intro_members +from conditional.util.member import get_semester_info intro_evals_bp = Blueprint('intro_evals_bp', __name__) logger = structlog.get_logger() def get_intro_members_without_accounts(): + _, semester_start = get_semester_info() freshman_cm_count = dict([tuple(row) for row in FreshmanCommitteeAttendance.query.join( CommitteeMeeting, FreshmanCommitteeAttendance.meeting_id == CommitteeMeeting.id @@ -34,7 +36,7 @@ def get_intro_members_without_accounts(): CommitteeMeeting.approved, ).filter( CommitteeMeeting.approved, - CommitteeMeeting.timestamp >= start_of_year() + CommitteeMeeting.timestamp >= semester_start ).with_entities( FreshmanCommitteeAttendance.fid, func.count(FreshmanCommitteeAttendance.fid) #pylint: disable=not-callable @@ -64,7 +66,7 @@ def get_intro_members_without_accounts(): TechnicalSeminar.approved ).filter( TechnicalSeminar.approved, - TechnicalSeminar.timestamp >= start_of_year() + TechnicalSeminar.timestamp >= semester_start ).with_entities( FreshmanSeminarAttendance.fid, TechnicalSeminar.name @@ -80,7 +82,7 @@ def get_intro_members_without_accounts(): # freshmen who don't have accounts freshman_accounts = list(FreshmanAccount.query.filter( - FreshmanAccount.eval_date > start_of_year(), + FreshmanAccount.eval_date > semester_start, FreshmanAccount.eval_date > datetime.now())) ie_members = [] @@ -92,7 +94,7 @@ def get_intro_members_without_accounts(): HouseMeeting, FreshmanHouseMeetingAttendance.meeting_id == HouseMeeting.id ).filter( - HouseMeeting.date >= start_of_year(), # TODO: this needs to be fixed + HouseMeeting.date >= semester_start, FreshmanHouseMeetingAttendance.attendance_status == 'Absent', FreshmanHouseMeetingAttendance.fid == freshman_account.id, ).with_entities( @@ -135,6 +137,8 @@ def display_intro_evals(internal=False, user_dict=None): ie_members = get_intro_members_without_accounts() + _, semester_start = get_semester_info() + account_cm_count = dict([tuple(row) for row in MemberCommitteeAttendance.query.join( CommitteeMeeting, MemberCommitteeAttendance.meeting_id == CommitteeMeeting.id @@ -144,7 +148,7 @@ def display_intro_evals(internal=False, user_dict=None): CommitteeMeeting.approved, ).filter( CommitteeMeeting.approved, - CommitteeMeeting.timestamp >= start_of_year() + CommitteeMeeting.timestamp >= semester_start ).with_entities( MemberCommitteeAttendance.uid, func.count(MemberCommitteeAttendance.uid) #pylint: disable=not-callable @@ -156,7 +160,7 @@ def display_intro_evals(internal=False, user_dict=None): HouseMeeting, MemberHouseMeetingAttendance.meeting_id == HouseMeeting.id ).filter( - HouseMeeting.date >= start_of_year(), + HouseMeeting.date >= semester_start, MemberHouseMeetingAttendance.attendance_status == 'Absent' ).with_entities( MemberHouseMeetingAttendance.uid, @@ -174,7 +178,7 @@ def display_intro_evals(internal=False, user_dict=None): TechnicalSeminar.approved ).filter( TechnicalSeminar.approved, - TechnicalSeminar.timestamp >= start_of_year() + TechnicalSeminar.timestamp >= semester_start ).with_entities( MemberSeminarAttendance.uid, TechnicalSeminar.name @@ -186,14 +190,14 @@ def display_intro_evals(internal=False, user_dict=None): if not row[0] in account_ts_attendance_dict: account_ts_attendance_dict[row[0]] = [] - account_ts_attendance_dict[row[0]].append(row[1]) + account_ts_attendance_dict[row[0]].append(row[1]) # freshmen who have accounts for member in members: uid = member.uid name = member.cn freshman_data = FreshmanEvalData.query.filter( - FreshmanEvalData.eval_date > start_of_year(), + FreshmanEvalData.eval_date > semester_start, FreshmanEvalData.uid == uid).first() if freshman_data is None: @@ -208,7 +212,7 @@ def display_intro_evals(internal=False, user_dict=None): HouseMeeting, MemberHouseMeetingAttendance.meeting_id == HouseMeeting.id ).filter( - HouseMeeting.date >= start_of_year(), + HouseMeeting.date >= semester_start, MemberHouseMeetingAttendance.attendance_status == 'Absent', MemberHouseMeetingAttendance.uid == uid, ).with_entities(