diff --git a/conditional/blueprints/attendance.py b/conditional/blueprints/attendance.py index 62135f2c..2d0e5570 100644 --- a/conditional/blueprints/attendance.py +++ b/conditional/blueprints/attendance.py @@ -177,6 +177,23 @@ def display_attendance_hm(user_dict=None): members=get_non_alumni_non_coop(internal=True)) +@attendance_bp.route('/attendance/adhoc_history', methods=['GET']) +@auth.oidc_auth("default") +@get_user +def get_adhoc_history(user_dict=None): + log = logger.new(request=request, auth_dict=user_dict) + log.info('Retrieve Ad-Hoc Committee Meeting History') + + adhoc_meetings = db.session.query(CommitteeMeeting.adhoc).filter( + CommitteeMeeting.adhoc.isnot(None), + CommitteeMeeting.timestamp > start_of_year() + ).distinct().all() + + adhoc_names = [row[0] for row in adhoc_meetings] + + return jsonify({"adhocs": adhoc_names}), 200 + + @attendance_bp.route('/attendance/submit/cm', methods=['POST']) @auth.oidc_auth("default") @get_user @@ -190,11 +207,12 @@ def submit_committee_attendance(user_dict=None): m_attendees = post_data['members'] f_attendees = post_data['freshmen'] timestamp = post_data['timestamp'] + adhoc = post_data.get('adhoc', None) log.info(f'Submit {committee} Meeting Attendance') timestamp = datetime.strptime(timestamp, "%Y-%m-%d") - meeting = CommitteeMeeting(committee, timestamp, approved) + meeting = CommitteeMeeting(committee, adhoc, timestamp, approved) db.session.add(meeting) db.session.flush() @@ -425,6 +443,7 @@ def get_seminar_hosts(meeting_id): limit = int(page)*10 all_cm = [{"id": m.id, "name": m.committee, + "adhoc": m.adhoc, "dt_obj": m.timestamp, "date": m.timestamp.strftime("%a %m/%d/%Y"), "attendees": get_meeting_attendees(m.id), @@ -444,6 +463,7 @@ def get_seminar_hosts(meeting_id): TechnicalSeminar.approved).all()] pend_cm = [{"id": m.id, "name": m.committee, + "adhoc": m.adhoc, "dt_obj": m.timestamp, "date": m.timestamp.strftime("%a %m/%d/%Y"), "attendees": get_meeting_attendees(m.id) diff --git a/conditional/models/docs.md b/conditional/models/docs.md index 33697e10..1fb7e471 100644 --- a/conditional/models/docs.md +++ b/conditional/models/docs.md @@ -33,6 +33,7 @@ This table stores a list of committee meetings. | ------------- | ------------- | ------------------- | | `id` | `INTEGER` | Autoincrementing primary key. | `committee` | `ENUM` | The committee the meeting belongs to. +| `adhoc` | `STRING` | The Ad-Hoc committee's name when applicable. | `timestamp` | `TIMESTAMP` | The date and time of the meeting. | `active` | `BOOLEAN` | Whether the meeting applies to the current year or not. diff --git a/conditional/models/models.py b/conditional/models/models.py index 51b58cc6..604ea423 100644 --- a/conditional/models/models.py +++ b/conditional/models/models.py @@ -58,12 +58,14 @@ class CommitteeMeeting(db.Model): 'R&D', 'House Improvements', 'Financial', 'Public Relations', 'Chairman', 'Ad-Hoc', name="committees_enum"), nullable=False) + adhoc = Column(String, nullable=True) timestamp = Column(DateTime, nullable=False) approved = Column(Boolean, nullable=False) active = Column(Boolean) - def __init__(self, committee, timestamp, approved): + def __init__(self, committee, adhoc, timestamp, approved): self.committee = committee + self.adhoc = adhoc self.timestamp = timestamp self.approved = approved self.active = True diff --git a/conditional/templates/attendance_cm.html b/conditional/templates/attendance_cm.html index 80c26ea1..cb012b6c 100644 --- a/conditional/templates/attendance_cm.html +++ b/conditional/templates/attendance_cm.html @@ -30,13 +30,27 @@

Meeting Attendance

+ +
- +
@@ -49,7 +63,7 @@

Meeting Attendance

- +
diff --git a/conditional/templates/attendance_history.html b/conditional/templates/attendance_history.html index 0b534f1f..aa0245e8 100644 --- a/conditional/templates/attendance_history.html +++ b/conditional/templates/attendance_history.html @@ -11,7 +11,12 @@

Pending Submissions

-
{{meeting["name"]}}
+
+ {{meeting["name"]}} + {% if meeting["name"] == "Ad-Hoc" %} + - {{meeting["adhoc"]}} + {% endif %} +

{{meeting["date"]}}

@@ -69,7 +74,12 @@

Attendance History

-
{{meeting["name"]}}
+
+ {{meeting["name"]}} + {% if meeting["name"] == "Ad-Hoc" %} + - {{meeting["adhoc"]}} + {% endif %} +

{{meeting["date"]}}

diff --git a/conditional/templates/dashboard.html b/conditional/templates/dashboard.html index 572055e4..91d7f684 100644 --- a/conditional/templates/dashboard.html +++ b/conditional/templates/dashboard.html @@ -505,7 +505,12 @@

Directorship Meeting Attendance

{% for meeting in cm_attendance %} - {{meeting['committee']}} + + {{meeting['committee']}} + {% if meeting['committee'] == "Ad-Hoc" %} + - {{meeting['adhoc']}} + {% endif %} + {{meeting['timestamp'].strftime('%Y-%m-%d')}} {% endfor %} diff --git a/conditional/util/member.py b/conditional/util/member.py index d3154267..6e3d5f67 100644 --- a/conditional/util/member.py +++ b/conditional/util/member.py @@ -74,7 +74,8 @@ def get_cm(member): ).with_entities( MemberCommitteeAttendance.uid, CommitteeMeeting.timestamp, - CommitteeMeeting.committee + CommitteeMeeting.committee, + CommitteeMeeting.adhoc ).filter( CommitteeMeeting.timestamp > start_of_year(), MemberCommitteeAttendance.uid == member.uid, @@ -84,7 +85,8 @@ def get_cm(member): c_meetings = [{ "uid": cm.uid, "timestamp": cm.timestamp, - "committee": cm.committee + "committee": cm.committee, + "adhoc": cm.adhoc } for cm in query_result] return c_meetings diff --git a/frontend/javascript/exceptions/adhocException.js b/frontend/javascript/exceptions/adhocException.js new file mode 100644 index 00000000..2aa52b06 --- /dev/null +++ b/frontend/javascript/exceptions/adhocException.js @@ -0,0 +1,12 @@ +import { Enumify } from 'enumify'; + +export default class AdHocException extends Enumify { + NO_FOLLOW_ATTRIBUTE = AdHocException("Unable to find follow query selector for adhoc module"); + NO_PARENT_ATTRIBUTE = AdHocException("Unable to find parent element id for adhoc module"); + _ = AdHocException.closeEnum(); + + constructor(message) { + super(); + this.message = message; + } +} diff --git a/frontend/javascript/modules/adhoc.js b/frontend/javascript/modules/adhoc.js new file mode 100644 index 00000000..f0e53a1d --- /dev/null +++ b/frontend/javascript/modules/adhoc.js @@ -0,0 +1,68 @@ +/* global fetch */ +import "whatwg-fetch"; +import "@selectize/selectize"; +import FetchUtil from "../utils/fetchUtil"; +import Exception from "../exceptions/exception"; +import AttendanceException from "../exceptions/attendanceException"; +import AdHocException from "../exceptions/adhocException"; +import FetchException from "../exceptions/fetchException"; + +export default class AdHoc { + constructor(element) { + this.element = element; + this.dataSrc = element.dataset.src; + this.followSelector = element.dataset.follow; + this.parentId = element.dataset.parent; + this.list = element.list; + + if (!this.followSelector) { + throw new Exception(AdHocException.NO_FOLLOW_ATTRIBUTE); + } else if (!this.parentId) { + throw new Exception(AdHocException.NO_PARENT_ATTRIBUTE); + } + + if (this.dataSrc) { + fetch('/attendance/' + this.dataSrc, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + credentials: 'same-origin' + }) + .then(FetchUtil.checkResponse) + .then(FetchUtil.parseJSON) + .then(response => { + response.adhocs.forEach(item => { + const option = document.createElement('option'); + option.value = item + element.list.appendChild(option); + }); + this.render(); + }) + .catch(error => { + throw new Exception(FetchException.REQUEST_FAILED, error); + }); + } else { + throw new Exception(AttendanceException.NO_SRC_ATTRIBUTE); + } + } + + render() { + const parentElem = document.getElementById(this.parentId); + + document.querySelectorAll(this.followSelector).forEach(input => { + if (input.value === "Ad-Hoc") { + parentElem.style.display = "block"; + } + + input.addEventListener("change", event => { + if (event.target.value === "Ad-Hoc") { + parentElem.style.display = "block"; + } else { + parentElem.style.display = "none"; + } + }); + }); + } +} diff --git a/frontend/javascript/modules/attendanceForm.js b/frontend/javascript/modules/attendanceForm.js index 0af8e0d7..b8ebb3d2 100644 --- a/frontend/javascript/modules/attendanceForm.js +++ b/frontend/javascript/modules/attendanceForm.js @@ -41,6 +41,11 @@ export default class AttendanceForm { e.preventDefault(); let payload = {}; + + if (this.fields.committee.value === "Ad-Hoc") { + this.fields.adhoc = this.form.elements.adhoc; + } + Object.keys(this.fields).forEach(field => { if (field === "attendees") { let membersSplit = MemberUtil.splitFreshmenUpperclassmen( diff --git a/migrations/versions/3f094d78df95_add_adhoc_column_to_committee_meetings.py b/migrations/versions/3f094d78df95_add_adhoc_column_to_committee_meetings.py new file mode 100644 index 00000000..fdb5c626 --- /dev/null +++ b/migrations/versions/3f094d78df95_add_adhoc_column_to_committee_meetings.py @@ -0,0 +1,30 @@ +"""add adhoc column to committee_meetings + +Revision ID: 3f094d78df95 +Revises: 5e8db174ce30 +Create Date: 2026-09-30 02:03:08.999839 + +""" + +# revision identifiers, used by Alembic. +revision = '3f094d78df95' +down_revision = '5e8db174ce30' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('committee_meetings', schema=None) as batch_op: + batch_op.add_column(sa.Column('adhoc', sa.String(), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('committee_meetings', schema=None) as batch_op: + batch_op.drop_column('adhoc') + + # ### end Alembic commands ###