From 5a7b7c0c07b0c41e512b2f25735190c991e8fdde Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Wed, 8 Jul 2026 10:17:48 -0600 Subject: [PATCH 01/19] migrate existing configurable setting to new database schema --- src/data_access/config_dao.py | 14 ++++++++++---- src/data_access/db_manager.py | 9 ++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/data_access/config_dao.py b/src/data_access/config_dao.py index d913f15..b75af90 100644 --- a/src/data_access/config_dao.py +++ b/src/data_access/config_dao.py @@ -23,10 +23,14 @@ async def _get_queue_times() -> tuple[int, int, int, int]: conn: aiomysql.Connection async with conn.cursor(DictCursor) as cursor: cursor: DictCursor - await cursor.execute("SELECT open_hour, open_minute, close_hour, close_minute FROM config WHERE name = %s", (Config.QUEUE_SCHEDULE,)) + await cursor.execute("SELECT value FROM config WHERE name = %s", (Config.QUEUE_SCHEDULE,)) row = await cursor.fetchone() if row: - return int(row["open_hour"]), int(row["open_minute"]), int(row["close_hour"]), int(row["close_minute"]) + value: str = row["value"] + open_time, close_time = value.split(",") + open_time = datetime.fromisoformat(open_time) + close_time = datetime.fromisoformat(close_time) + return open_time.hour, open_time.minute, close_time.hour, close_time.minute return 8, 0, 20, 0 # Default to 8:00am-8:00pm @@ -41,13 +45,15 @@ async def set_queue_times(open_hour: int, open_minute: int, close_hour: int, clo """ if not (0 <= open_hour <= 23 and 0 <= open_minute <= 59 and 0 <= close_hour <= 23 and 0 <= close_minute <= 59): raise ValueError("Hours must be 0-23 and minutes must be 0-59") + open_time = datetime(2000, 1, 1, hour=open_hour, minute=open_minute) + close_time = datetime(2000, 1, 1, hour=close_hour, minute=close_minute) async with db_manager.get_conn() as conn: conn: aiomysql.Connection async with conn.cursor() as cursor: cursor: aiomysql.Cursor await cursor.execute( - "UPDATE config SET open_hour = %s, open_minute = %s, close_hour = %s, close_minute = %s WHERE name = %s", - (open_hour, open_minute, close_hour, close_minute, Config.QUEUE_SCHEDULE) + "UPDATE config SET value = %s WHERE name = %s", + (f"{open_time.isoformat()},{close_time.isoformat()}", Config.QUEUE_SCHEDULE) ) diff --git a/src/data_access/db_manager.py b/src/data_access/db_manager.py index 6d6c89d..d5dd140 100644 --- a/src/data_access/db_manager.py +++ b/src/data_access/db_manager.py @@ -1,5 +1,6 @@ import aiomysql import warnings +from datetime import datetime from ui.helpers.constants import Config from os import getenv @@ -95,13 +96,11 @@ async def _initialize_database(self): ) await cursor.execute( + # make config table a key-value pair where value is text that is parsed by the accessing object according to the expected format of the data for that particular configuration """ CREATE TABLE IF NOT EXISTS config ( name VARCHAR(100) PRIMARY KEY, - open_hour INT DEFAULT 8, - open_minute INT DEFAULT 0, - close_hour INT DEFAULT 20, - close_minute INT DEFAULT 0 + value TEXT NOT NULL ) """ ) @@ -137,6 +136,6 @@ async def _initialize_database(self): # Ensure config has a row for default opening/closing await cursor.execute("SELECT COUNT(*) FROM config") if (await cursor.fetchone())[0] == 0: - await cursor.execute("INSERT INTO config (name, open_hour, open_minute, close_hour, close_minute) VALUES (%s, 8, 0, 20, 0)", (Config.QUEUE_SCHEDULE,)) + await cursor.execute("INSERT INTO config (name, value) VALUES (%s, %s)", (Config.QUEUE_SCHEDULE, f"{datetime(2000, 1, 1, hour=8, minute=0).isoformat()},{datetime(2000, 1, 1, hour=20, minute=0).isoformat()}")) db_manager = _DBManager() From 6424e20df0da64caf7212dbe5c87f4c2a42d81dd Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Wed, 8 Jul 2026 11:07:08 -0600 Subject: [PATCH 02/19] Edit return values of getters for configurable times --- src/data_access/config_dao.py | 62 ++++++++++++++++++++++++++++++----- src/ui/helpers/constants.py | 2 ++ src/ui/modals.py | 6 ++++ src/ui/views/ta_view.py | 14 +++++--- 4 files changed, 71 insertions(+), 13 deletions(-) diff --git a/src/data_access/config_dao.py b/src/data_access/config_dao.py index b75af90..712191c 100644 --- a/src/data_access/config_dao.py +++ b/src/data_access/config_dao.py @@ -30,8 +30,8 @@ async def _get_queue_times() -> tuple[int, int, int, int]: open_time, close_time = value.split(",") open_time = datetime.fromisoformat(open_time) close_time = datetime.fromisoformat(close_time) - return open_time.hour, open_time.minute, close_time.hour, close_time.minute - return 8, 0, 20, 0 # Default to 8:00am-8:00pm + return open_time, close_time + return datetime(2000, 1, 1, hour=8, minute=0), datetime(2000, 1, 1, hour=20, minute=0) # Default to 8:00am-8:00pm async def set_queue_times(open_hour: int, open_minute: int, close_hour: int, close_minute: int) -> None: @@ -56,12 +56,56 @@ async def set_queue_times(open_hour: int, open_minute: int, close_hour: int, clo (f"{open_time.isoformat()},{close_time.isoformat()}", Config.QUEUE_SCHEDULE) ) +async def set_ta_meeting(ta_meeting_hour: int, ta_meeting_minute: int): + if not (0 <= ta_meeting_hour <= 23 and 0 <= ta_meeting_minute <= 59): + raise ValueError("Hours must be 0-23 and minutes must be 0-59") + ta_meeting_time = datetime(2000, 1, 1, hour=ta_meeting_hour, minute=ta_meeting_minute) + async with db_manager.get_conn() as conn: + conn: aiomysql.Connection + async with conn.cursor() as cursor: + cursor: aiomysql.Cursor + cursor.execute("UPDATE config SET value = %s WHERE name = %s", (ta_meeting_time.isoformat(), Config.TA_MEETING)) + +async def _get_ta_meeting() -> datetime: + async with db_manager.get_conn() as conn: + conn: aiomysql.Connection + async with conn.cursor(DictCursor) as cursor: + cursor: DictCursor + await cursor.execute("SELECT value FROM config WHERE name = %s", (Config.TA_MEETING,)) + row = await cursor.fetchone() + if row: + value: str = row[0] + ta_meeting_time = datetime.fromisoformat(value) + return ta_meeting_time + + # if no TA meeting has been configured, set the queue closing time to one hour before opening time so that when it opens after the meeting it will not conflict. + open_time, _ = await _get_queue_times() + return open_time + +async def set_devotional_hours(): + pass + +async def _get_devotional_hours(): + pass + +def _should_open(current_time: datetime, open_time: datetime, meeting_time: datetime): + begin_queue_hours: bool = current_time.hour == open_time.hour and current_time.minute == open_time.minute + finish_ta_meeting: bool = current_time.hour == meeting_time.hour+1 and current_time.minute == meeting_time.minute + return begin_queue_hours or finish_ta_meeting + +def _should_close(current_time: datetime, close_time: datetime, meeting_time: datetime): + finish_queue_hours: bool = current_time.hour == close_time.hour and current_time.minute == close_time.minute + begin_ta_meeting: bool = current_time.hour == meeting_time.hour and current_time.minute == meeting_time.minute + return finish_queue_hours or begin_ta_meeting + + # Queue auto-open/close scheduled tasks @tasks.loop(minutes=1) async def auto_queue_scheduler(bot_client: discord.Client) -> None: """Check if queue should be auto-opened or auto-closed every minute on weekdays only.""" - open_hour, open_minute, close_hour, close_minute = await _get_queue_times() + open_time, close_time = await _get_queue_times() + meeting_time = await _get_ta_meeting() denver_tz = ZoneInfo("America/Denver") current_time = datetime.now(denver_tz) @@ -71,21 +115,21 @@ async def auto_queue_scheduler(bot_client: discord.Client) -> None: message: str | None = None # Check if we should open (at the configured open time) - if current_time.hour == open_hour and current_time.minute == open_minute and not bot_client.queue.is_open: + if _should_open(current_time, open_time, meeting_time) and not bot_client.queue.is_open: bot_client.queue.is_open = True message = f"Queue auto-opened at {current_time.strftime('%H:%M')}" # Check if we should close (at the configured close time) - elif current_time.hour == close_hour and current_time.minute == close_minute and bot_client.queue.is_open: + elif _should_close(current_time, close_time, meeting_time) and bot_client.queue.is_open: bot_client.queue.is_open = False message = f"Queue auto-closed at {current_time.strftime('%H:%M')}" # Get TA text channel ta_channel = None - for guild in bot_client.guilds: - channel_id = await get_id(Channels.TA_TEXT_CHANNEL_NAME, guild.id) - ta_channel = get(guild.text_channels, id=channel_id) - if message: + if message: + for guild in bot_client.guilds: + channel_id = await get_id(Channels.TA_TEXT_CHANNEL_NAME, guild.id) + ta_channel = get(guild.text_channels, id=channel_id) if ta_channel: await ta_channel.send(message, delete_after=30) else: diff --git a/src/ui/helpers/constants.py b/src/ui/helpers/constants.py index e10c7a7..ee7e412 100644 --- a/src/ui/helpers/constants.py +++ b/src/ui/helpers/constants.py @@ -34,4 +34,6 @@ class Roles: class Config: QUEUE_SCHEDULE = "daily_queue_hours" + TA_MEETING = "ta_meeting_hours" + DEVOTIONAL = "devotional_hours" \ No newline at end of file diff --git a/src/ui/modals.py b/src/ui/modals.py index 3f72f9d..1ef21d9 100644 --- a/src/ui/modals.py +++ b/src/ui/modals.py @@ -271,3 +271,9 @@ async def on_submit(self, interaction: discord.Interaction): wait=True ) await msg.delete(delay=Messages.SHORT_TIMEOUT) + +class EditMeetingHoursModal(discord.ui.Modal, title="Edit TA Meeting Hours"): + open_hour = discord.ui.TextDisplay("Not implemented yet!") + + async def on_submit(interaction: discord.Interaction): + await interaction.response.defer() \ No newline at end of file diff --git a/src/ui/views/ta_view.py b/src/ui/views/ta_view.py index 916cc62..04dcc44 100644 --- a/src/ui/views/ta_view.py +++ b/src/ui/views/ta_view.py @@ -7,7 +7,7 @@ from data_access.user_stats_dao import increment_help, get_student_info from data_access.server_info_dao import get_id from records import QueueEntry -from ui.modals import ClearConfirmModal, RemoveConfirmModal, EditQueueHoursModal +from ui.modals import ClearConfirmModal, RemoveConfirmModal, EditQueueHoursModal, EditMeetingHoursModal from ui.helpers.constants import Channels, Messages, Roles from ui.helpers.utils import fixed_width from ui.helpers.discord_helpers import move_to_breakout, notify_next_if_changed, update_queue_messages @@ -274,9 +274,6 @@ def row_to_line(items): builder = f"```Student Info:\n{row_to_line(headers)}\n{divider}\n{body}```" await interaction.followup.send(builder, ephemeral=True) - @discord.ui.button(label="Edit Hours", style=discord.ButtonStyle.secondary, custom_id="edit_hours", emoji="🕐") - async def edit_queue_hours(self, interaction: discord.Interaction, button: discord.ui.Button): - await interaction.response.send_modal(EditQueueHoursModal()) @discord.ui.button(label="See Queue History", style=discord.ButtonStyle.secondary, custom_id="queue_history", emoji="🏛️") async def display_queue_history(self, interaction: discord.Interaction, button: discord.ui.Button): @@ -284,6 +281,15 @@ async def display_queue_history(self, interaction: discord.Interaction, button: csv_file = await get_queue_history_as_csv() await interaction.followup.send(file=csv_file) +class TAConfigView(discord.ui.ActionRow[discord.ui.LayoutView]): + view: "TAView" + @discord.ui.button(label="Edit Queue Hours", style=discord.ButtonStyle.secondary, custom_id="edit_hours", emoji="🕐") + async def edit_queue_hours(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.send_modal(EditQueueHoursModal()) + + @discord.ui.button(label="Edit TA Meeting", style=discord.ButtonStyle.blurple, custom_id="edit_ta_meeting", emoji="🤝") + async def edit_ta_meeting_hours(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.send_modal(EditMeetingHoursModal()) class TAView(discord.ui.LayoutView): From 4e23ed5ce5c0bad76212fa77f165c8c9457f61da Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Wed, 8 Jul 2026 11:36:52 -0600 Subject: [PATCH 03/19] open and close for configured ta meeting time --- src/data_access/config_dao.py | 4 ++-- src/data_access/db_manager.py | 3 ++- src/ui/modals.py | 38 +++++++++++++++++++++++++++++++---- src/ui/views/ta_view.py | 9 +++++++-- 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/data_access/config_dao.py b/src/data_access/config_dao.py index 712191c..c991287 100644 --- a/src/data_access/config_dao.py +++ b/src/data_access/config_dao.py @@ -64,7 +64,7 @@ async def set_ta_meeting(ta_meeting_hour: int, ta_meeting_minute: int): conn: aiomysql.Connection async with conn.cursor() as cursor: cursor: aiomysql.Cursor - cursor.execute("UPDATE config SET value = %s WHERE name = %s", (ta_meeting_time.isoformat(), Config.TA_MEETING)) + await cursor.execute("UPDATE config SET value = %s WHERE name = %s", (ta_meeting_time.isoformat(), Config.TA_MEETING)) async def _get_ta_meeting() -> datetime: async with db_manager.get_conn() as conn: @@ -74,7 +74,7 @@ async def _get_ta_meeting() -> datetime: await cursor.execute("SELECT value FROM config WHERE name = %s", (Config.TA_MEETING,)) row = await cursor.fetchone() if row: - value: str = row[0] + value: str = row["value"] ta_meeting_time = datetime.fromisoformat(value) return ta_meeting_time diff --git a/src/data_access/db_manager.py b/src/data_access/db_manager.py index d5dd140..a12851c 100644 --- a/src/data_access/db_manager.py +++ b/src/data_access/db_manager.py @@ -137,5 +137,6 @@ async def _initialize_database(self): await cursor.execute("SELECT COUNT(*) FROM config") if (await cursor.fetchone())[0] == 0: await cursor.execute("INSERT INTO config (name, value) VALUES (%s, %s)", (Config.QUEUE_SCHEDULE, f"{datetime(2000, 1, 1, hour=8, minute=0).isoformat()},{datetime(2000, 1, 1, hour=20, minute=0).isoformat()}")) + await cursor.execute("INSERT INTO config (name, value) VALUES (%s, %s)", (Config.TA_MEETING, datetime(2000, 1, 1, hour = 7, minute = 0).isoformat())) -db_manager = _DBManager() +db_manager = _DBManager() \ No newline at end of file diff --git a/src/ui/modals.py b/src/ui/modals.py index 1ef21d9..df438ed 100644 --- a/src/ui/modals.py +++ b/src/ui/modals.py @@ -2,7 +2,7 @@ from discord.utils import get from data_access.user_stats_dao import get_times_helped_today from data_access.bot_incidents_dao import record_bot_issue -from data_access.config_dao import set_queue_times +from data_access.config_dao import set_queue_times, set_ta_meeting, set_devotional_hours from data_access.server_info_dao import get_id from ui.helpers.discord_helpers import get_channel, get_role, update_queue_messages, notify_next_if_changed from ui.helpers.constants import Channels, Messages @@ -273,7 +273,37 @@ async def on_submit(self, interaction: discord.Interaction): await msg.delete(delay=Messages.SHORT_TIMEOUT) class EditMeetingHoursModal(discord.ui.Modal, title="Edit TA Meeting Hours"): - open_hour = discord.ui.TextDisplay("Not implemented yet!") + meeting_hour = discord.ui.TextInput(label="Meeting Hour (0-23)", placeholder="12", required=True, max_length=2) + meeting_minute = discord.ui.TextInput(label="Meeting Minute (0-59)", placeholder="00", required=False, max_length=2) - async def on_submit(interaction: discord.Interaction): - await interaction.response.defer() \ No newline at end of file + async def on_submit(self, interaction: discord.Interaction): + await interaction.response.defer(thinking=True) + try: + hour = int(self.meeting_hour.value) + if self.meeting_minute.value: + minute = int(self.meeting_minute.value) + else: + minute = 0 + + if not (0 <= hour <= 23 and 0 <= minute <= 59): + msg = await interaction.followup.send( + "Hour must be 0-23 and minute must be 0-59.", + ephemeral=True, + wait=True + ) + await msg.delete(delay=Messages.SHORT_TIMEOUT) + return + + await set_ta_meeting(hour, minute) + ta_role = get_role(interaction, "TA") + await interaction.followup.send( + f"{ta_role.mention} ANNOUNCEMENT: TA meeting time updated: {hour:02d}:{minute:02d}", + ephemeral=False + ) + except ValueError: + msg = await interaction.followup.send( + "Please enter valid integers for hours and minutes.", + ephemeral=True, + wait=True + ) + await msg.delete(delay=Messages.SHORT_TIMEOUT) \ No newline at end of file diff --git a/src/ui/views/ta_view.py b/src/ui/views/ta_view.py index 04dcc44..79e5971 100644 --- a/src/ui/views/ta_view.py +++ b/src/ui/views/ta_view.py @@ -281,7 +281,7 @@ async def display_queue_history(self, interaction: discord.Interaction, button: csv_file = await get_queue_history_as_csv() await interaction.followup.send(file=csv_file) -class TAConfigView(discord.ui.ActionRow[discord.ui.LayoutView]): +class TAConfig(discord.ui.ActionRow[discord.ui.LayoutView]): view: "TAView" @discord.ui.button(label="Edit Queue Hours", style=discord.ButtonStyle.secondary, custom_id="edit_hours", emoji="🕐") async def edit_queue_hours(self, interaction: discord.Interaction, button: discord.ui.Button): @@ -314,6 +314,11 @@ def __init__(self): discord.ui.Separator(visible=False, spacing=discord.SeparatorSpacing.large), discord.ui.TextDisplay("### Information/Upkeep"), discord.ui.Separator(visible=True, spacing=discord.SeparatorSpacing.small), - TAQueueInformation() + TAQueueInformation(), + discord.ui.Separator(visible=True, spacing=discord.SeparatorSpacing.large), + discord.ui.TextDisplay("### Configuration"), + discord.ui.Separator(visible=True, spacing=discord.SeparatorSpacing.small), + TAConfig() + ) self.add_item(container) From db7d45dccc1528c439662f95fce595ccb6f0fc22 Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Wed, 8 Jul 2026 12:35:11 -0600 Subject: [PATCH 04/19] Require specific day of the week for meeting --- src/data_access/config_dao.py | 27 +++++++++++++++------------ src/data_access/db_manager.py | 17 +++++++++++------ src/ui/modals.py | 17 ++++++++--------- 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/data_access/config_dao.py b/src/data_access/config_dao.py index c991287..d81abba 100644 --- a/src/data_access/config_dao.py +++ b/src/data_access/config_dao.py @@ -56,7 +56,7 @@ async def set_queue_times(open_hour: int, open_minute: int, close_hour: int, clo (f"{open_time.isoformat()},{close_time.isoformat()}", Config.QUEUE_SCHEDULE) ) -async def set_ta_meeting(ta_meeting_hour: int, ta_meeting_minute: int): +async def set_ta_meeting(day: str, ta_meeting_hour: int, ta_meeting_minute: int): if not (0 <= ta_meeting_hour <= 23 and 0 <= ta_meeting_minute <= 59): raise ValueError("Hours must be 0-23 and minutes must be 0-59") ta_meeting_time = datetime(2000, 1, 1, hour=ta_meeting_hour, minute=ta_meeting_minute) @@ -64,7 +64,7 @@ async def set_ta_meeting(ta_meeting_hour: int, ta_meeting_minute: int): conn: aiomysql.Connection async with conn.cursor() as cursor: cursor: aiomysql.Cursor - await cursor.execute("UPDATE config SET value = %s WHERE name = %s", (ta_meeting_time.isoformat(), Config.TA_MEETING)) + await cursor.execute("UPDATE config SET value = %s WHERE name = %s", (f"{day},{ta_meeting_time.isoformat()}", Config.TA_MEETING)) async def _get_ta_meeting() -> datetime: async with db_manager.get_conn() as conn: @@ -75,8 +75,9 @@ async def _get_ta_meeting() -> datetime: row = await cursor.fetchone() if row: value: str = row["value"] - ta_meeting_time = datetime.fromisoformat(value) - return ta_meeting_time + day, ta_meeting_time = value.split(",") + ta_meeting_time = datetime.fromisoformat(ta_meeting_time) + return day, ta_meeting_time # if no TA meeting has been configured, set the queue closing time to one hour before opening time so that when it opens after the meeting it will not conflict. open_time, _ = await _get_queue_times() @@ -88,24 +89,26 @@ async def set_devotional_hours(): async def _get_devotional_hours(): pass -def _should_open(current_time: datetime, open_time: datetime, meeting_time: datetime): +def _should_open(current_time: datetime, open_time: datetime, meeting_time: datetime, meeting_day: str): begin_queue_hours: bool = current_time.hour == open_time.hour and current_time.minute == open_time.minute - finish_ta_meeting: bool = current_time.hour == meeting_time.hour+1 and current_time.minute == meeting_time.minute + finish_ta_meeting: bool = current_time.hour == meeting_time.hour+1 and current_time.minute == meeting_time.minute and current_time.weekday() == day_to_int(meeting_day) return begin_queue_hours or finish_ta_meeting -def _should_close(current_time: datetime, close_time: datetime, meeting_time: datetime): +def _should_close(current_time: datetime, close_time: datetime, meeting_time: datetime, meeting_day: str): finish_queue_hours: bool = current_time.hour == close_time.hour and current_time.minute == close_time.minute - begin_ta_meeting: bool = current_time.hour == meeting_time.hour and current_time.minute == meeting_time.minute + begin_ta_meeting: bool = current_time.hour == meeting_time.hour and current_time.minute == meeting_time.minute and current_time.weekday() == day_to_int(meeting_day) return finish_queue_hours or begin_ta_meeting - +def day_to_int(day: str): + days = ("MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN") + return days.index(day) # Queue auto-open/close scheduled tasks @tasks.loop(minutes=1) async def auto_queue_scheduler(bot_client: discord.Client) -> None: """Check if queue should be auto-opened or auto-closed every minute on weekdays only.""" open_time, close_time = await _get_queue_times() - meeting_time = await _get_ta_meeting() + meeting_day, meeting_time = await _get_ta_meeting() denver_tz = ZoneInfo("America/Denver") current_time = datetime.now(denver_tz) @@ -115,12 +118,12 @@ async def auto_queue_scheduler(bot_client: discord.Client) -> None: message: str | None = None # Check if we should open (at the configured open time) - if _should_open(current_time, open_time, meeting_time) and not bot_client.queue.is_open: + if _should_open(current_time, open_time, meeting_time, meeting_day) and not bot_client.queue.is_open: bot_client.queue.is_open = True message = f"Queue auto-opened at {current_time.strftime('%H:%M')}" # Check if we should close (at the configured close time) - elif _should_close(current_time, close_time, meeting_time) and bot_client.queue.is_open: + elif _should_close(current_time, close_time, meeting_time, meeting_day) and bot_client.queue.is_open: bot_client.queue.is_open = False message = f"Queue auto-closed at {current_time.strftime('%H:%M')}" diff --git a/src/data_access/db_manager.py b/src/data_access/db_manager.py index a12851c..d1a59db 100644 --- a/src/data_access/db_manager.py +++ b/src/data_access/db_manager.py @@ -133,10 +133,15 @@ async def _initialize_database(self): """ ) - # Ensure config has a row for default opening/closing - await cursor.execute("SELECT COUNT(*) FROM config") - if (await cursor.fetchone())[0] == 0: - await cursor.execute("INSERT INTO config (name, value) VALUES (%s, %s)", (Config.QUEUE_SCHEDULE, f"{datetime(2000, 1, 1, hour=8, minute=0).isoformat()},{datetime(2000, 1, 1, hour=20, minute=0).isoformat()}")) - await cursor.execute("INSERT INTO config (name, value) VALUES (%s, %s)", (Config.TA_MEETING, datetime(2000, 1, 1, hour = 7, minute = 0).isoformat())) + await cursor.execute("TRUNCATE TABLE config") + await default_configuration(cursor) -db_manager = _DBManager() \ No newline at end of file +db_manager = _DBManager() + + +async def default_configuration(cursor: aiomysql.Cursor): + await cursor.execute("SELECT COUNT(*) FROM config") + if (await cursor.fetchone())[0] == 0: + await cursor.execute("INSERT INTO config (name, value) VALUES (%s, %s)", (Config.QUEUE_SCHEDULE, f"{datetime(2000, 1, 1, hour=8, minute=0).isoformat()},{datetime(2000, 1, 1, hour=20, minute=0).isoformat()}")) + await cursor.execute("INSERT INTO config (name, value) VALUES (%s, %s)", (Config.TA_MEETING, f"MON,{datetime(2000, 1, 1, hour = 7, minute = 0).isoformat()}")) + await cursor.execute("INSERT INTO config (name, value) VALUES (%s, %s)", (Config.DEVOTIONAL, f"TUE,{datetime(200, 1, 1, hour=11, minute=0)}")) diff --git a/src/ui/modals.py b/src/ui/modals.py index df438ed..75ea9b7 100644 --- a/src/ui/modals.py +++ b/src/ui/modals.py @@ -273,28 +273,27 @@ async def on_submit(self, interaction: discord.Interaction): await msg.delete(delay=Messages.SHORT_TIMEOUT) class EditMeetingHoursModal(discord.ui.Modal, title="Edit TA Meeting Hours"): + day = discord.ui.TextInput(label="Day of Meeting", placeholder="Mon/Tue/Wed/Thu/Fri", min_length=3, max_length=3, required=True) meeting_hour = discord.ui.TextInput(label="Meeting Hour (0-23)", placeholder="12", required=True, max_length=2) - meeting_minute = discord.ui.TextInput(label="Meeting Minute (0-59)", placeholder="00", required=False, max_length=2) + meeting_minute = discord.ui.TextInput(label="Meeting Minute (0-59)", placeholder="00", required=False, max_length=2, default=0) async def on_submit(self, interaction: discord.Interaction): await interaction.response.defer(thinking=True) try: + day = self.day.value.upper() hour = int(self.meeting_hour.value) - if self.meeting_minute.value: - minute = int(self.meeting_minute.value) - else: - minute = 0 + minute = int(self.meeting_minute.value) - if not (0 <= hour <= 23 and 0 <= minute <= 59): + if not (0 <= hour <= 23 and 0 <= minute <= 59 and day in ("MON", "TUE", "WED", "THU", "FRI")): msg = await interaction.followup.send( - "Hour must be 0-23 and minute must be 0-59.", + "Day must be the first three letters of a weekday. Hour must be 0-23 and minute must be 0-59.", ephemeral=True, wait=True - ) + ) await msg.delete(delay=Messages.SHORT_TIMEOUT) return - await set_ta_meeting(hour, minute) + await set_ta_meeting(day, hour, minute) ta_role = get_role(interaction, "TA") await interaction.followup.send( f"{ta_role.mention} ANNOUNCEMENT: TA meeting time updated: {hour:02d}:{minute:02d}", From 83af3adffe336ca21283fdda906cc576e129ee79 Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Wed, 8 Jul 2026 13:08:37 -0600 Subject: [PATCH 05/19] Add devotional hours --- src/data_access/config_dao.py | 54 +++++++++++++++++++++++++---------- src/ui/modals.py | 43 ++++++++++++++++++++++++++-- src/ui/views/ta_view.py | 7 ++++- 3 files changed, 86 insertions(+), 18 deletions(-) diff --git a/src/data_access/config_dao.py b/src/data_access/config_dao.py index d81abba..9250337 100644 --- a/src/data_access/config_dao.py +++ b/src/data_access/config_dao.py @@ -13,7 +13,7 @@ from ui.helpers.constants import Channels, Config -async def _get_queue_times() -> tuple[int, int, int, int]: +async def _get_queue_times() -> tuple[datetime, datetime]: """Get the configured queue open and close times. Returns: @@ -66,7 +66,7 @@ async def set_ta_meeting(day: str, ta_meeting_hour: int, ta_meeting_minute: int) cursor: aiomysql.Cursor await cursor.execute("UPDATE config SET value = %s WHERE name = %s", (f"{day},{ta_meeting_time.isoformat()}", Config.TA_MEETING)) -async def _get_ta_meeting() -> datetime: +async def _get_ta_meeting() -> tuple[str, datetime]: async with db_manager.get_conn() as conn: conn: aiomysql.Connection async with conn.cursor(DictCursor) as cursor: @@ -79,27 +79,50 @@ async def _get_ta_meeting() -> datetime: ta_meeting_time = datetime.fromisoformat(ta_meeting_time) return day, ta_meeting_time - # if no TA meeting has been configured, set the queue closing time to one hour before opening time so that when it opens after the meeting it will not conflict. + # if no TA meeting has been configured, set the queue closing time to one hour before opening time, effectively causing no effect on queue hours open_time, _ = await _get_queue_times() - return open_time + meeting_time = datetime(2000, 1, 1, open_time.hour-1, open_time.minute) + return "MON", meeting_time -async def set_devotional_hours(): - pass +async def set_devotional_hours(day, devotional_hour, devotional_minute): + if not (0 <= devotional_hour <= 23 and 0 <= devotional_minute <= 59): + raise ValueError("Hours must be 0-23 and minutes must be 0-59") + devotional_time = datetime(2000, 1, 1, hour=devotional_hour, minute=devotional_minute) + async with db_manager.get_conn() as conn: + conn: aiomysql.Connection + async with conn.cursor() as cursor: + cursor: aiomysql.Cursor + await cursor.execute("UPDATE config SET value = %s WHERE name = %s", (f"{day},{devotional_time.isoformat()}", Config.DEVOTIONAL)) -async def _get_devotional_hours(): - pass +async def _get_devotional_hours() -> tuple[str, datetime]: + async with db_manager.get_conn() as conn: + conn: aiomysql.Connection + async with conn.cursor(DictCursor) as cursor: + cursor: DictCursor + await cursor.execute("SELECT value FROM config WHERE name = %s", (Config.DEVOTIONAL,)) + row = await cursor.fetchone() + if row: + value: str = row["value"] + day, devotional_time = value.split(",") + devotional_time = datetime.fromisoformat(devotional_time) + return day, devotional_time + + # if no devotional has been configured, set to tuesday at 11am + return "TUE", datetime(2000, 1, 1, hour=11, minute=0) -def _should_open(current_time: datetime, open_time: datetime, meeting_time: datetime, meeting_day: str): +def _should_open(current_time: datetime, open_time: datetime, meeting_time: datetime, meeting_day: str, devo_time: datetime, devo_day: str) -> bool: begin_queue_hours: bool = current_time.hour == open_time.hour and current_time.minute == open_time.minute finish_ta_meeting: bool = current_time.hour == meeting_time.hour+1 and current_time.minute == meeting_time.minute and current_time.weekday() == day_to_int(meeting_day) - return begin_queue_hours or finish_ta_meeting + finish_devo: bool = current_time.hour == devo_time.hour+1 and current_time.minute == devo_time.minute and current_time.weekday() == day_to_int(devo_day) + return begin_queue_hours or finish_ta_meeting or finish_devo -def _should_close(current_time: datetime, close_time: datetime, meeting_time: datetime, meeting_day: str): +def _should_close(current_time: datetime, close_time: datetime, meeting_time: datetime, meeting_day: str, devo_time: datetime, devo_day: str) -> bool: finish_queue_hours: bool = current_time.hour == close_time.hour and current_time.minute == close_time.minute begin_ta_meeting: bool = current_time.hour == meeting_time.hour and current_time.minute == meeting_time.minute and current_time.weekday() == day_to_int(meeting_day) - return finish_queue_hours or begin_ta_meeting + begin_devo: bool = current_time.hour == devo_time.hour and current_time.minute == devo_time .minute and current_time.weekday() == day_to_int(devo_day) + return finish_queue_hours or begin_ta_meeting or begin_devo -def day_to_int(day: str): +def day_to_int(day: str) -> int: days = ("MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN") return days.index(day) @@ -109,6 +132,7 @@ async def auto_queue_scheduler(bot_client: discord.Client) -> None: """Check if queue should be auto-opened or auto-closed every minute on weekdays only.""" open_time, close_time = await _get_queue_times() meeting_day, meeting_time = await _get_ta_meeting() + devo_day, devo_time = await _get_devotional_hours() denver_tz = ZoneInfo("America/Denver") current_time = datetime.now(denver_tz) @@ -118,12 +142,12 @@ async def auto_queue_scheduler(bot_client: discord.Client) -> None: message: str | None = None # Check if we should open (at the configured open time) - if _should_open(current_time, open_time, meeting_time, meeting_day) and not bot_client.queue.is_open: + if _should_open(current_time, open_time, meeting_time, meeting_day, devo_time, devo_day) and not bot_client.queue.is_open: bot_client.queue.is_open = True message = f"Queue auto-opened at {current_time.strftime('%H:%M')}" # Check if we should close (at the configured close time) - elif _should_close(current_time, close_time, meeting_time, meeting_day) and bot_client.queue.is_open: + elif _should_close(current_time, close_time, meeting_time, meeting_day, devo_time, devo_day) and bot_client.queue.is_open: bot_client.queue.is_open = False message = f"Queue auto-closed at {current_time.strftime('%H:%M')}" diff --git a/src/ui/modals.py b/src/ui/modals.py index 75ea9b7..52dfea8 100644 --- a/src/ui/modals.py +++ b/src/ui/modals.py @@ -5,7 +5,7 @@ from data_access.config_dao import set_queue_times, set_ta_meeting, set_devotional_hours from data_access.server_info_dao import get_id from ui.helpers.discord_helpers import get_channel, get_role, update_queue_messages, notify_next_if_changed -from ui.helpers.constants import Channels, Messages +from ui.helpers.constants import Channels, Messages, Roles class HelpModal(discord.ui.Modal, title="Request Help"): @@ -294,11 +294,50 @@ async def on_submit(self, interaction: discord.Interaction): return await set_ta_meeting(day, hour, minute) - ta_role = get_role(interaction, "TA") + ta_id = await get_id(Roles.TA_ROLE, interaction.guild.id) + ta_role = get(interaction.guild.roles, id=ta_id) await interaction.followup.send( f"{ta_role.mention} ANNOUNCEMENT: TA meeting time updated: {hour:02d}:{minute:02d}", ephemeral=False ) + except ValueError: + msg = await interaction.followup.send( + "Please enter valid integers for hours and minutes.", + ephemeral=True, + wait=True + ) + await msg.delete(delay=Messages.SHORT_TIMEOUT) + + + +class EditDevotionalTimeModal(discord.ui.Modal, title="Edit Devotional Time"): + day = discord.ui.TextInput(label="Day of Devotional", placeholder="Mon/Tue/Wed/Thu/Fri", min_length=3, max_length=3, required=True) + meeting_hour = discord.ui.TextInput(label="Devotional Hour (0-23)", placeholder="11", required=True, max_length=2) + meeting_minute = discord.ui.TextInput(label="Devotional Minute (0-59)", placeholder="00", required=False, max_length=2, default=0) + + async def on_submit(self, interaction: discord.Interaction): + await interaction.response.defer(thinking=True) + try: + day = self.day.value.upper() + hour = int(self.meeting_hour.value) + minute = int(self.meeting_minute.value) + + if not (0 <= hour <= 23 and 0 <= minute <= 59 and day in ("MON", "TUE", "WED", "THU", "FRI")): + msg = await interaction.followup.send( + "Day must be the first three letters of a weekday. Hour must be 0-23 and minute must be 0-59.", + ephemeral=True, + wait=True + ) + await msg.delete(delay=Messages.SHORT_TIMEOUT) + return + + await set_devotional_hours(day, hour, minute) + ta_id = await get_id(Roles.TA_ROLE, interaction.guild.id) + ta_role = get(interaction.guild.roles, id=ta_id) + await interaction.followup.send( + f"{ta_role.mention} ANNOUNCEMENT: Devotional time updated: {hour:02d}:{minute:02d}", + ephemeral=False + ) except ValueError: msg = await interaction.followup.send( "Please enter valid integers for hours and minutes.", diff --git a/src/ui/views/ta_view.py b/src/ui/views/ta_view.py index 79e5971..92d0b55 100644 --- a/src/ui/views/ta_view.py +++ b/src/ui/views/ta_view.py @@ -7,7 +7,7 @@ from data_access.user_stats_dao import increment_help, get_student_info from data_access.server_info_dao import get_id from records import QueueEntry -from ui.modals import ClearConfirmModal, RemoveConfirmModal, EditQueueHoursModal, EditMeetingHoursModal +from ui.modals import ClearConfirmModal, RemoveConfirmModal, EditQueueHoursModal, EditMeetingHoursModal, EditDevotionalTimeModal from ui.helpers.constants import Channels, Messages, Roles from ui.helpers.utils import fixed_width from ui.helpers.discord_helpers import move_to_breakout, notify_next_if_changed, update_queue_messages @@ -291,6 +291,11 @@ async def edit_queue_hours(self, interaction: discord.Interaction, button: disco async def edit_ta_meeting_hours(self, interaction: discord.Interaction, button: discord.ui.Button): await interaction.response.send_modal(EditMeetingHoursModal()) + @discord.ui.button(label="Edit Devotional Time", style=discord.ButtonStyle.red, custom_id="edit_devotional", emoji="🙏") + async def edit_devotional_time(self, interaction: discord.Interaction, button): + await interaction.response.send_modal(EditDevotionalTimeModal()) + + class TAView(discord.ui.LayoutView): def __init__(self): From 64e7d76c3558fb5de0ba66ab7f0ac2c86caf5e50 Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Wed, 8 Jul 2026 15:08:12 -0600 Subject: [PATCH 06/19] Add configurable Saturday hours --- src/data_access/config_dao.py | 51 ++++++++++++++++++++++++++++++++++- src/ui/helpers/constants.py | 1 + src/ui/modals.py | 48 ++++++++++++++++++++++++++++++++- src/ui/views/ta_view.py | 25 ++++++++++++++++- 4 files changed, 122 insertions(+), 3 deletions(-) diff --git a/src/data_access/config_dao.py b/src/data_access/config_dao.py index 9250337..a4537b4 100644 --- a/src/data_access/config_dao.py +++ b/src/data_access/config_dao.py @@ -110,6 +110,49 @@ async def _get_devotional_hours() -> tuple[str, datetime]: # if no devotional has been configured, set to tuesday at 11am return "TUE", datetime(2000, 1, 1, hour=11, minute=0) +async def set_saturday_hours(open_hour: int, open_minute: int, close_hour: int, close_minute: int) -> None: + open_at: datetime = datetime(2000, 1, 1, hour=open_hour, minute = open_minute) + close_at: datetime = datetime(2000, 1, 1, hour=open_hour, minute=close_minute) + + async with db_manager.get_conn() as conn: + conn: aiomysql.Connection + async with conn.cursor(DictCursor) as cursor: + cursor: DictCursor + await cursor.execute( + """ + INSERT INTO config (name, value) + VALUES (%s, %s) AS new + ON DUPLICATE KEY UPDATE value = new.value + """, + (Config.SATURDAY_HOURS, f"{open_at.isoformat()},{close_at.isoformat()}") + ) + + +async def remove_saturday_hours() -> None: + async with db_manager.get_conn() as conn: + conn: aiomysql.Connection + async with conn.cursor(DictCursor) as cursor: + cursor: DictCursor + await cursor.execute("DELETE FROM config WHERE name = %s", (Config.SATURDAY_HOURS,)) + + +async def _get_saturday_hours() -> tuple[datetime, datetime] | None: + async with db_manager.get_conn() as conn: + conn: aiomysql.Connection + async with conn.cursor(DictCursor) as cursor: + cursor: DictCursor + await cursor.execute("SELECT value FROM config WHERE name = %s", (Config.SATURDAY_HOURS,)) + row = await cursor.fetchone() + if row: + value: str = row["value"] + open, close = value.split(",") + open = datetime.fromisoformat(open) + close = datetime.fromisoformat(close) + return open, close + + return None + + def _should_open(current_time: datetime, open_time: datetime, meeting_time: datetime, meeting_day: str, devo_time: datetime, devo_day: str) -> bool: begin_queue_hours: bool = current_time.hour == open_time.hour and current_time.minute == open_time.minute finish_ta_meeting: bool = current_time.hour == meeting_time.hour+1 and current_time.minute == meeting_time.minute and current_time.weekday() == day_to_int(meeting_day) @@ -135,10 +178,16 @@ async def auto_queue_scheduler(bot_client: discord.Client) -> None: devo_day, devo_time = await _get_devotional_hours() denver_tz = ZoneInfo("America/Denver") current_time = datetime.now(denver_tz) + current_time = datetime(2024, 1, 6, hour=current_time.hour, minute=current_time.minute, second=current_time.second) # Only run on weekdays (Monday-Friday; 5=Saturday, 6=Sunday) - if current_time.weekday() >= 5: + if current_time.weekday() == 6: return + elif current_time.weekday() == 5: + try: + open_time, close_time = await _get_saturday_hours() + except TypeError: + return message: str | None = None # Check if we should open (at the configured open time) diff --git a/src/ui/helpers/constants.py b/src/ui/helpers/constants.py index ee7e412..f7b4bd2 100644 --- a/src/ui/helpers/constants.py +++ b/src/ui/helpers/constants.py @@ -36,4 +36,5 @@ class Config: QUEUE_SCHEDULE = "daily_queue_hours" TA_MEETING = "ta_meeting_hours" DEVOTIONAL = "devotional_hours" + SATURDAY_HOURS = "saturday_hours" \ No newline at end of file diff --git a/src/ui/modals.py b/src/ui/modals.py index 52dfea8..1628664 100644 --- a/src/ui/modals.py +++ b/src/ui/modals.py @@ -2,7 +2,7 @@ from discord.utils import get from data_access.user_stats_dao import get_times_helped_today from data_access.bot_incidents_dao import record_bot_issue -from data_access.config_dao import set_queue_times, set_ta_meeting, set_devotional_hours +from data_access.config_dao import set_queue_times, set_ta_meeting, set_devotional_hours, set_saturday_hours from data_access.server_info_dao import get_id from ui.helpers.discord_helpers import get_channel, get_role, update_queue_messages, notify_next_if_changed from ui.helpers.constants import Channels, Messages, Roles @@ -344,4 +344,50 @@ async def on_submit(self, interaction: discord.Interaction): ephemeral=True, wait=True ) + await msg.delete(delay=Messages.SHORT_TIMEOUT) + + + +class EditSaturdayHoursModal(discord.ui.Modal, title="Edit Saturday Hours"): + open = discord.ui.TextInput( + label="Saturday Open Time (24-hour time)", + placeholder="08:00", + min_length=3, + max_length=5 + ) + close = discord.ui.TextInput( + label="Saturday Close Time (24-hour time)", + placeholder="20:00", + min_length=3, + max_length=5 + ) + + async def on_submit(self, interaction: discord.Interaction): + response: discord.InteractionCallbackResponse = await interaction.response.defer(thinking=True, ephemeral=True) + try: + open_h, open_m = [int(time) for time in self.open.value.split(":")] + close_h, close_m = [int(time) for time in self.close.value.split(":")] + + if not (0 <= open_h <= 23 and 0 <= open_m <= 59 and 0 <= open_h <= 23 and 0 <= open_m <= 59): + msg = await interaction.followup.send( + "Hour must be 0-23 and minute must be 0-59.", + ephemeral=True, + wait=True + ) + await msg.delete(delay=Messages.SHORT_TIMEOUT) + return + + await set_saturday_hours(open_h, open_m, close_h, close_m) + ta_id = await get_id(Roles.TA_ROLE, interaction.guild.id) + ta_role = get(interaction.guild.roles, id=ta_id) + await response.resource.delete() + await interaction.channel.send( + f"{ta_role.mention} ANNOUNCEMENT: Saturday hours updated: {open_h:02d}:{open_m:02d} to {close_h:02d}:{close_m:02d}" + ) + except ValueError: + msg: discord.WebhookMessage = await interaction.followup.send( + "Input should be in the following format: `##:##`. Please enter valid integers for hours and minutes.", + ephemeral=True, + wait=True + ) await msg.delete(delay=Messages.SHORT_TIMEOUT) \ No newline at end of file diff --git a/src/ui/views/ta_view.py b/src/ui/views/ta_view.py index 92d0b55..c0529de 100644 --- a/src/ui/views/ta_view.py +++ b/src/ui/views/ta_view.py @@ -6,8 +6,9 @@ from data_access.bot_incidents_dao import get_last_incident_info from data_access.user_stats_dao import increment_help, get_student_info from data_access.server_info_dao import get_id +from data_access.config_dao import remove_saturday_hours from records import QueueEntry -from ui.modals import ClearConfirmModal, RemoveConfirmModal, EditQueueHoursModal, EditMeetingHoursModal, EditDevotionalTimeModal +from ui.modals import ClearConfirmModal, RemoveConfirmModal, EditQueueHoursModal, EditMeetingHoursModal, EditDevotionalTimeModal, EditSaturdayHoursModal from ui.helpers.constants import Channels, Messages, Roles from ui.helpers.utils import fixed_width from ui.helpers.discord_helpers import move_to_breakout, notify_next_if_changed, update_queue_messages @@ -295,6 +296,28 @@ async def edit_ta_meeting_hours(self, interaction: discord.Interaction, button: async def edit_devotional_time(self, interaction: discord.Interaction, button): await interaction.response.send_modal(EditDevotionalTimeModal()) + @discord.ui.button(label="Saturday Hours", style=discord.ButtonStyle.green, custom_id="saturday_hours", emoji="🗓️") + async def get_saturday_hours_view(self, interaction: discord.Interaction, button): + await interaction.response.send_message(view=SaturdayView(), ephemeral=True, delete_after=Messages.DEFAULT_TIMEOUT) + +class SaturdayView(discord.ui.View): + def __init__(self): + super().__init__(timeout=30) + + @discord.ui.button(label="Edit Hours", style=discord.ButtonStyle.green) + async def edit_hours(self, interaction: discord.Interaction, button): + await interaction.response.send_modal(EditSaturdayHoursModal()) + + @discord.ui.button(label="Remove Saturday Hours", style=discord.ButtonStyle.danger) + async def saturday_offline(self, interaction: discord.Interaction, button): + await interaction.response.defer(thinking=True) + await remove_saturday_hours() + + ta_id = await get_id(Roles.TA_ROLE, interaction.guild.id) + ta_role = get(interaction.guild.roles, id=ta_id) + await interaction.followup.send( + f"{ta_role.mention} ANNOUNCEMENT: Saturday hours removed! The queue will no longer auto-start on saturdays." + ) class TAView(discord.ui.LayoutView): From 8966fadab15369d2ff352bea5db483f6e4ea3823 Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Fri, 10 Jul 2026 11:01:08 -0600 Subject: [PATCH 07/19] Make settings viewable --- src/data_access/config_dao.py | 39 +++++++++++++++++++++++++++++++++++ src/ui/views/ta_view.py | 12 +++++++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/data_access/config_dao.py b/src/data_access/config_dao.py index a4537b4..4344648 100644 --- a/src/data_access/config_dao.py +++ b/src/data_access/config_dao.py @@ -153,6 +153,45 @@ async def _get_saturday_hours() -> tuple[datetime, datetime] | None: return None +async def get_config_data(): + """ + Fetch all config table rows and return a readable string. + """ + async with db_manager.get_conn() as conn: + conn: aiomysql.Connection + async with conn.cursor() as cursor: + cursor: aiomysql.Cursor + await cursor.execute("SELECT name, value FROM config ORDER BY name") + rows = await cursor.fetchall() + + # Build readable output + lines = [] + for name, raw_value in rows: + raw_value: str + parts = raw_value.split(",") + + formatted_parts = [] + for part in parts: + formatted_parts.append(_try_format_datetime(part)) + + formatted_value = ", ".join(formatted_parts) + lines.append(f"{name} = {formatted_value}") + + return "\n".join(lines) + +def _try_format_datetime(value: str): + """ + Try to parse a datetime string and return HH:MM. + If parsing fails, return the original string. + """ + + try: + dt = datetime.fromisoformat(value) + return dt.strftime("%H:%M") + except ValueError: + return value # Not a datetime + + def _should_open(current_time: datetime, open_time: datetime, meeting_time: datetime, meeting_day: str, devo_time: datetime, devo_day: str) -> bool: begin_queue_hours: bool = current_time.hour == open_time.hour and current_time.minute == open_time.minute finish_ta_meeting: bool = current_time.hour == meeting_time.hour+1 and current_time.minute == meeting_time.minute and current_time.weekday() == day_to_int(meeting_day) diff --git a/src/ui/views/ta_view.py b/src/ui/views/ta_view.py index c0529de..eefcaa5 100644 --- a/src/ui/views/ta_view.py +++ b/src/ui/views/ta_view.py @@ -6,7 +6,7 @@ from data_access.bot_incidents_dao import get_last_incident_info from data_access.user_stats_dao import increment_help, get_student_info from data_access.server_info_dao import get_id -from data_access.config_dao import remove_saturday_hours +from data_access.config_dao import remove_saturday_hours, get_config_data from records import QueueEntry from ui.modals import ClearConfirmModal, RemoveConfirmModal, EditQueueHoursModal, EditMeetingHoursModal, EditDevotionalTimeModal, EditSaturdayHoursModal from ui.helpers.constants import Channels, Messages, Roles @@ -300,6 +300,14 @@ async def edit_devotional_time(self, interaction: discord.Interaction, button): async def get_saturday_hours_view(self, interaction: discord.Interaction, button): await interaction.response.send_message(view=SaturdayView(), ephemeral=True, delete_after=Messages.DEFAULT_TIMEOUT) + @discord.ui.button(label="See Current Settings", style=discord.ButtonStyle.gray, custom_id="see_current_config") + async def see_current_config(self, interaction: discord.Interaction, button): + await interaction.response.defer(thinking=True, ephemeral=True) + current_config = await get_config_data() + msg = await interaction.followup.send(current_config, wait=True) + await msg.delete(delay=Messages.DEFAULT_TIMEOUT) + + class SaturdayView(discord.ui.View): def __init__(self): super().__init__(timeout=30) @@ -343,7 +351,7 @@ def __init__(self): discord.ui.TextDisplay("### Information/Upkeep"), discord.ui.Separator(visible=True, spacing=discord.SeparatorSpacing.small), TAQueueInformation(), - discord.ui.Separator(visible=True, spacing=discord.SeparatorSpacing.large), + discord.ui.Separator(visible=False, spacing=discord.SeparatorSpacing.large), discord.ui.TextDisplay("### Configuration"), discord.ui.Separator(visible=True, spacing=discord.SeparatorSpacing.small), TAConfig() From c7b92831d4351377c5aecb7ab5ce5c0ed893a68c Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Fri, 10 Jul 2026 11:07:05 -0600 Subject: [PATCH 08/19] Fix bug, ensure that queue bot doesn't miss opening/closing times due to latency --- src/data_access/config_dao.py | 5 ++--- src/ui/views/ta_view.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/data_access/config_dao.py b/src/data_access/config_dao.py index 4344648..01080ab 100644 --- a/src/data_access/config_dao.py +++ b/src/data_access/config_dao.py @@ -112,7 +112,7 @@ async def _get_devotional_hours() -> tuple[str, datetime]: async def set_saturday_hours(open_hour: int, open_minute: int, close_hour: int, close_minute: int) -> None: open_at: datetime = datetime(2000, 1, 1, hour=open_hour, minute = open_minute) - close_at: datetime = datetime(2000, 1, 1, hour=open_hour, minute=close_minute) + close_at: datetime = datetime(2000, 1, 1, hour=close_hour, minute=close_minute) async with db_manager.get_conn() as conn: conn: aiomysql.Connection @@ -209,7 +209,7 @@ def day_to_int(day: str) -> int: return days.index(day) # Queue auto-open/close scheduled tasks -@tasks.loop(minutes=1) +@tasks.loop(minutes=0.9) async def auto_queue_scheduler(bot_client: discord.Client) -> None: """Check if queue should be auto-opened or auto-closed every minute on weekdays only.""" open_time, close_time = await _get_queue_times() @@ -217,7 +217,6 @@ async def auto_queue_scheduler(bot_client: discord.Client) -> None: devo_day, devo_time = await _get_devotional_hours() denver_tz = ZoneInfo("America/Denver") current_time = datetime.now(denver_tz) - current_time = datetime(2024, 1, 6, hour=current_time.hour, minute=current_time.minute, second=current_time.second) # Only run on weekdays (Monday-Friday; 5=Saturday, 6=Sunday) if current_time.weekday() == 6: diff --git a/src/ui/views/ta_view.py b/src/ui/views/ta_view.py index eefcaa5..f36c75a 100644 --- a/src/ui/views/ta_view.py +++ b/src/ui/views/ta_view.py @@ -300,7 +300,7 @@ async def edit_devotional_time(self, interaction: discord.Interaction, button): async def get_saturday_hours_view(self, interaction: discord.Interaction, button): await interaction.response.send_message(view=SaturdayView(), ephemeral=True, delete_after=Messages.DEFAULT_TIMEOUT) - @discord.ui.button(label="See Current Settings", style=discord.ButtonStyle.gray, custom_id="see_current_config") + @discord.ui.button(label="See Current Settings", style=discord.ButtonStyle.gray, custom_id="see_current_config", emoji="⚙️") async def see_current_config(self, interaction: discord.Interaction, button): await interaction.response.defer(thinking=True, ephemeral=True) current_config = await get_config_data() From 3d49d512257d839bad96ca0b0f1b0cc869865924 Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Fri, 10 Jul 2026 11:22:39 -0600 Subject: [PATCH 09/19] cleanup modal input format --- src/ui/modals.py | 56 ++++++++++++++++-------------------------------- 1 file changed, 19 insertions(+), 37 deletions(-) diff --git a/src/ui/modals.py b/src/ui/modals.py index 1628664..736e3f8 100644 --- a/src/ui/modals.py +++ b/src/ui/modals.py @@ -216,38 +216,24 @@ async def on_submit(self, interaction: discord.Interaction): class EditQueueHoursModal(discord.ui.Modal, title="Edit Queue Hours"): - open_hour = discord.ui.TextInput( - label="Queue Open Hour (0-23)", - placeholder="8", - min_length=1, - max_length=2 - ) - open_minute = discord.ui.TextInput( - label="Queue Open Minute (0-59)", - placeholder="00", - min_length=1, - max_length=2 - ) - close_hour = discord.ui.TextInput( - label="Queue Close Hour (0-23)", - placeholder="20", - min_length=1, - max_length=2 + open = discord.ui.TextInput( + label="Weekday Open Time (24-hour time)", + placeholder="08:00", + min_length=3, + max_length=5 ) - close_minute = discord.ui.TextInput( - label="Queue Close Minute (0-59)", - placeholder="00", - min_length=1, - max_length=2 + close = discord.ui.TextInput( + label="Weekday Close Time (24-hour time)", + placeholder="20:00", + min_length=3, + max_length=5 ) async def on_submit(self, interaction: discord.Interaction): await interaction.response.defer(thinking=True) try: - open_h = int(self.open_hour.value) - open_m = int(self.open_minute.value) - close_h = int(self.close_hour.value) - close_m = int(self.close_minute.value) + open_h, open_m = [int(time) for time in self.open.value.split(":")] + close_h, close_m = [int(time) for time in self.close.value.split(":")] if not (0 <= open_h <= 23 and 0 <= open_m <= 59 and 0 <= close_h <= 23 and 0 <= close_m <= 59): msg = await interaction.followup.send( @@ -266,7 +252,7 @@ async def on_submit(self, interaction: discord.Interaction): ) except ValueError: msg = await interaction.followup.send( - "Please enter valid integers for hours and minutes.", + "Input should be in the following format: `##:##`. Please enter valid integers for hours and minutes.", ephemeral=True, wait=True ) @@ -274,15 +260,13 @@ async def on_submit(self, interaction: discord.Interaction): class EditMeetingHoursModal(discord.ui.Modal, title="Edit TA Meeting Hours"): day = discord.ui.TextInput(label="Day of Meeting", placeholder="Mon/Tue/Wed/Thu/Fri", min_length=3, max_length=3, required=True) - meeting_hour = discord.ui.TextInput(label="Meeting Hour (0-23)", placeholder="12", required=True, max_length=2) - meeting_minute = discord.ui.TextInput(label="Meeting Minute (0-59)", placeholder="00", required=False, max_length=2, default=0) + time = discord.ui.TextInput(label="Meeting Time (24-hour time)", placeholder="12:00", required=True, min_length=3, max_length=5) async def on_submit(self, interaction: discord.Interaction): await interaction.response.defer(thinking=True) try: day = self.day.value.upper() - hour = int(self.meeting_hour.value) - minute = int(self.meeting_minute.value) + hour, minute = [int(value) for value in self.time.value.split(":")] if not (0 <= hour <= 23 and 0 <= minute <= 59 and day in ("MON", "TUE", "WED", "THU", "FRI")): msg = await interaction.followup.send( @@ -302,7 +286,7 @@ async def on_submit(self, interaction: discord.Interaction): ) except ValueError: msg = await interaction.followup.send( - "Please enter valid integers for hours and minutes.", + "Input should be in the following format: `##:##`. Please enter valid integers for hours and minutes.", ephemeral=True, wait=True ) @@ -312,15 +296,13 @@ async def on_submit(self, interaction: discord.Interaction): class EditDevotionalTimeModal(discord.ui.Modal, title="Edit Devotional Time"): day = discord.ui.TextInput(label="Day of Devotional", placeholder="Mon/Tue/Wed/Thu/Fri", min_length=3, max_length=3, required=True) - meeting_hour = discord.ui.TextInput(label="Devotional Hour (0-23)", placeholder="11", required=True, max_length=2) - meeting_minute = discord.ui.TextInput(label="Devotional Minute (0-59)", placeholder="00", required=False, max_length=2, default=0) + time = discord.ui.TextInput(label="Devotional Time (24-hour time)", placeholder="11:00", required=True, min_length=3, max_length=5) async def on_submit(self, interaction: discord.Interaction): await interaction.response.defer(thinking=True) try: day = self.day.value.upper() - hour = int(self.meeting_hour.value) - minute = int(self.meeting_minute.value) + hour, minute = [int(value) for value in self.time.value.split(":")] if not (0 <= hour <= 23 and 0 <= minute <= 59 and day in ("MON", "TUE", "WED", "THU", "FRI")): msg = await interaction.followup.send( @@ -340,7 +322,7 @@ async def on_submit(self, interaction: discord.Interaction): ) except ValueError: msg = await interaction.followup.send( - "Please enter valid integers for hours and minutes.", + "Input should be in the following format: `##:##`. Please enter valid integers for hours and minutes.", ephemeral=True, wait=True ) From 609bdcca4d1a02b09b8511e5e89ea951b2f8b368 Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Fri, 10 Jul 2026 11:34:38 -0600 Subject: [PATCH 10/19] Improve code consistency --- src/ui/helpers/discord_helpers.py | 19 ++++++++----------- src/ui/modals.py | 9 ++++++--- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/ui/helpers/discord_helpers.py b/src/ui/helpers/discord_helpers.py index d67d8c1..895bffa 100644 --- a/src/ui/helpers/discord_helpers.py +++ b/src/ui/helpers/discord_helpers.py @@ -1,15 +1,10 @@ import discord -from discord.utils import get as discord_get +from discord.utils import get as get from typing import Optional from records import QueueEntry +from data_access.server_info_dao import get_id from ui.helpers.constants import Channels, Messages -def get_channel(interaction: discord.Interaction, channel_name: str) -> Optional[discord.abc.GuildChannel]: - return discord_get(interaction.guild.channels, name=channel_name) - -def get_role(interaction: discord.Interaction, role_name: str) -> Optional[discord.Role]: - return discord_get(interaction.guild.roles, name=role_name) - def get_next_available_breakout(interaction: discord.Interaction): for vc in interaction.guild.voice_channels: if vc.name in Channels.BREAKOUT_NAMES and vc.members == []: @@ -23,7 +18,7 @@ def count_tas_in_voice_channel(voice_channel: Optional[discord.VoiceChannel], ta if voice_channel is None: return 0 - ta_role = discord_get(voice_channel.guild.roles, name=ta_role_name) + ta_role = get(voice_channel.guild.roles, name=ta_role_name) if ta_role is None: return 0 @@ -39,7 +34,7 @@ def count_total_tas_in_voice(interaction: Optional[discord.Interaction] = None, return 0 guild = guild or interaction.guild - ta_role = discord_get(guild.roles, name=ta_role_name) + ta_role = get(guild.roles, name=ta_role_name) if ta_role is None: return 0 @@ -78,9 +73,11 @@ async def move_to_breakout(interaction: discord.Interaction, entry: QueueEntry): ta: discord.Member = interaction.user if entry.in_person: try: - await ta.move_to(get_channel(interaction, Channels.IN_PERSON_CHANNEL_NAME)) + channel_id = await get_id(Channels.IN_PERSON_CHANNEL_NAME, interaction.guild.id) + in_person_channel = get(interaction.guild.voice_channels, id=channel_id) + await ta.move_to(in_person_channel) except Exception: - await ta.send("Because you weren't in the Online TAs voice channel, you need to join the In Person with Student channel manually. Please do so now.") + await ta.send(f"Because you weren't in the Online TAs voice channel, you need to join the {in_person_channel.mention} channel manually. Please do so now.") else: breakout_channel: discord.VoiceChannel = get_next_available_breakout(interaction) diff --git a/src/ui/modals.py b/src/ui/modals.py index 736e3f8..df62336 100644 --- a/src/ui/modals.py +++ b/src/ui/modals.py @@ -4,7 +4,7 @@ from data_access.bot_incidents_dao import record_bot_issue from data_access.config_dao import set_queue_times, set_ta_meeting, set_devotional_hours, set_saturday_hours from data_access.server_info_dao import get_id -from ui.helpers.discord_helpers import get_channel, get_role, update_queue_messages, notify_next_if_changed +from ui.helpers.discord_helpers import update_queue_messages, notify_next_if_changed from ui.helpers.constants import Channels, Messages, Roles class HelpModal(discord.ui.Modal, title="Request Help"): @@ -118,8 +118,10 @@ async def on_submit(self, interaction: discord.Interaction): mode = "In-person" if value == "p" else "Online" pos = await interaction.client.queue.get_position(interaction.user.id) + waiting_room_id = await get_id(Channels.WAITING_ROOM_NAME, interaction.guild.id) + waiting_room = get(interaction.guild.voice_channels, id=waiting_room_id) msg = await interaction.followup.send( - f"You are #{pos} in the queue.{f" Please join the {get_channel(interaction, "Waiting Room").mention} voice channel." if not value=="p" else ""}", + f"You are #{pos} in the queue.{f" Please join the {waiting_room.mention} voice channel." if not value=="p" else ""}", ephemeral=True, wait=True ) @@ -245,7 +247,8 @@ async def on_submit(self, interaction: discord.Interaction): return await set_queue_times(open_h, open_m, close_h, close_m) - ta_role = get_role(interaction, "TA") + ta_role_id = await get_id(Roles.TA_ROLE, interaction.guild.id) + ta_role = get(interaction.guild.roles, id=ta_role_id) await interaction.followup.send( f"{ta_role.mention} ANNOUNCEMENT: Queue hours updated: Opens at {open_h:02d}:{open_m:02d}, closes at {close_h:02d}:{close_m:02d}", ephemeral=False From ec0c96a04fe9db0ecf2b1a295a4b7e0ca22c35d3 Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Fri, 10 Jul 2026 12:27:36 -0600 Subject: [PATCH 11/19] Ignore newlines in student questions --- src/ui/helpers/queue_helpers.py | 4 ++++ src/ui/modals.py | 11 ++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/ui/helpers/queue_helpers.py b/src/ui/helpers/queue_helpers.py index 66517a2..ca3ed9c 100644 --- a/src/ui/helpers/queue_helpers.py +++ b/src/ui/helpers/queue_helpers.py @@ -1,4 +1,5 @@ import discord +import re async def already_in_queue(interaction: discord.Interaction)->bool: return await interaction.client.queue.is_in_queue(interaction.user.id) @@ -23,3 +24,6 @@ async def can_join_queue(interaction: discord.Interaction) -> bool: return False return True + +def sanitize_details(details: str): + return re.sub(r"[\r\n]+", " ", details).strip() \ No newline at end of file diff --git a/src/ui/modals.py b/src/ui/modals.py index df62336..7a936c7 100644 --- a/src/ui/modals.py +++ b/src/ui/modals.py @@ -4,8 +4,9 @@ from data_access.bot_incidents_dao import record_bot_issue from data_access.config_dao import set_queue_times, set_ta_meeting, set_devotional_hours, set_saturday_hours from data_access.server_info_dao import get_id -from ui.helpers.discord_helpers import update_queue_messages, notify_next_if_changed from ui.helpers.constants import Channels, Messages, Roles +from ui.helpers.discord_helpers import update_queue_messages, notify_next_if_changed +from ui.helpers.queue_helpers import sanitize_details class HelpModal(discord.ui.Modal, title="Request Help"): @@ -54,7 +55,7 @@ async def on_submit(self, interaction: discord.Interaction): await interaction.client.queue_handler( interaction, - self.question.value, + sanitize_details(self.question.value), False, value == "p", student_name @@ -76,7 +77,7 @@ async def on_submit(self, interaction: discord.Interaction): channel_id = await get_id(Channels.TA_TEXT_CHANNEL_NAME, interaction.guild.id) ta_channel: discord.TextChannel = get(interaction.guild.channels, id=channel_id) await ta_channel.send( - f"{interaction.user.display_name} ({student_name}) has joined the help queue - {mode} - {self.question.value} " + f"{interaction.user.display_name} ({student_name}) has joined the help queue - {mode} - {sanitize_details(self.question.value)} " f"(helped {times_helped} time{'s' if times_helped != 1 else ''} today)", delete_after=30 ) @@ -109,7 +110,7 @@ async def on_submit(self, interaction: discord.Interaction): await interaction.client.queue_handler( interaction, - self.phase.value, + sanitize_details(self.phase.value), True, self.in_person.value.lower() == "p", student_name @@ -129,7 +130,7 @@ async def on_submit(self, interaction: discord.Interaction): channel_id = await get_id(Channels.TA_TEXT_CHANNEL_NAME, interaction.guild.id) ta_channel: discord.TextChannel = get(interaction.guild.text_channels, id=channel_id) await ta_channel.send( - f"{interaction.user.display_name} ({student_name}) has requested a passoff - {mode} - {self.phase.value}", + f"{interaction.user.display_name} ({student_name}) has requested a passoff - {mode} - {sanitize_details(self.phase.value)}", delete_after=30 ) await msg.delete(delay=60) From 3f0236f05f3491b4509973b794a758bf558243c0 Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Fri, 10 Jul 2026 12:39:31 -0600 Subject: [PATCH 12/19] Improve finish helping student error messages --- src/ui/views/ta_view.py | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/ui/views/ta_view.py b/src/ui/views/ta_view.py index f36c75a..4d9a2c9 100644 --- a/src/ui/views/ta_view.py +++ b/src/ui/views/ta_view.py @@ -167,25 +167,18 @@ async def finish_button(self, interaction: discord.Interaction, button): try: ta_voice_state: discord.VoiceState = await interaction.user.fetch_voice() voice_channel: discord.VoiceChannel = ta_voice_state.channel + ta_role_id: int = await get_id(Roles.TA_ROLE, interaction.guild.id) + ta_role: discord.Role = get(interaction.guild.roles, id=ta_role_id) + for member in voice_channel.members: + if ta_role in member.roles: + continue + else: + await member.move_to(None) + await interaction.user.move_to(online_ta_vc) except discord.NotFound: - msg = await interaction.followup.send("You must be in a voice channel to use this command.", ephemeral=True, wait=True) + msg = await interaction.followup.send(f"Rejoin the {online_ta_vc.mention} channel!", ephemeral=True, wait=True) await msg.delete(delay=Messages.SHORT_TIMEOUT) - return - if voice_channel == online_ta_vc: - msg = await interaction.followup.send("You're not currently helping anyone!", ephemeral=True, wait=True) - await msg.delete(delay=Messages.SHORT_TIMEOUT) - return - - ta_role_id: int = await get_id(Roles.TA_ROLE, interaction.guild.id) - ta_role: discord.Role = get(interaction.guild.roles, id=ta_role_id) - for member in voice_channel.members: - if ta_role in member.roles: - continue - else: - await member.move_to(None) - await interaction.user.move_to(online_ta_vc) - ta_name = interaction.user.name try: await set_time_finished(interaction.client.help_map.pop(ta_name)[0]) From 80a2d72a4d91040e492818b33844884431ecbe34 Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Fri, 10 Jul 2026 12:50:16 -0600 Subject: [PATCH 13/19] remove redundant checks --- src/data_access/config_dao.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/data_access/config_dao.py b/src/data_access/config_dao.py index 01080ab..587a85a 100644 --- a/src/data_access/config_dao.py +++ b/src/data_access/config_dao.py @@ -43,8 +43,6 @@ async def set_queue_times(open_hour: int, open_minute: int, close_hour: int, clo close_hour: Hour to close (0-23) close_minute: Minute to close (0-59) """ - if not (0 <= open_hour <= 23 and 0 <= open_minute <= 59 and 0 <= close_hour <= 23 and 0 <= close_minute <= 59): - raise ValueError("Hours must be 0-23 and minutes must be 0-59") open_time = datetime(2000, 1, 1, hour=open_hour, minute=open_minute) close_time = datetime(2000, 1, 1, hour=close_hour, minute=close_minute) async with db_manager.get_conn() as conn: @@ -57,8 +55,6 @@ async def set_queue_times(open_hour: int, open_minute: int, close_hour: int, clo ) async def set_ta_meeting(day: str, ta_meeting_hour: int, ta_meeting_minute: int): - if not (0 <= ta_meeting_hour <= 23 and 0 <= ta_meeting_minute <= 59): - raise ValueError("Hours must be 0-23 and minutes must be 0-59") ta_meeting_time = datetime(2000, 1, 1, hour=ta_meeting_hour, minute=ta_meeting_minute) async with db_manager.get_conn() as conn: conn: aiomysql.Connection @@ -85,8 +81,6 @@ async def _get_ta_meeting() -> tuple[str, datetime]: return "MON", meeting_time async def set_devotional_hours(day, devotional_hour, devotional_minute): - if not (0 <= devotional_hour <= 23 and 0 <= devotional_minute <= 59): - raise ValueError("Hours must be 0-23 and minutes must be 0-59") devotional_time = datetime(2000, 1, 1, hour=devotional_hour, minute=devotional_minute) async with db_manager.get_conn() as conn: conn: aiomysql.Connection From cd703a0489f39f263bec3501215760e7d97f04cc Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Mon, 13 Jul 2026 10:17:00 -0600 Subject: [PATCH 14/19] Add list of students currently being helped by TAs to TA status message --- src/bot.py | 16 ++++++++++++++-- src/data_access/queue_history_dao.py | 12 ++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/bot.py b/src/bot.py index e7867bd..4ccc016 100644 --- a/src/bot.py +++ b/src/bot.py @@ -11,7 +11,7 @@ from datetime import datetime, UTC from data_access.db_manager import db_manager from data_access.user_stats_dao import daily_reset -from data_access.queue_history_dao import set_time_finished +from data_access.queue_history_dao import set_time_finished, get_students_not_finished from data_access.config_dao import auto_queue_scheduler from data_access.server_info_dao import get_id @@ -174,8 +174,10 @@ async def _build_ta_status_message(self, guild: discord.Guild) -> str: status = "OPEN" if self.queue.is_open else "CLOSED" queue_text = await self.queue.view() wait_text = await self._get_wait_time(guild) + students_being_helped: str = await self._get_active_students() + currently_helped = f"\n\n**Currently Being Helped:**\n{students_being_helped}" if students_being_helped else "" - return f"**Help Queue Status: {status}{wait_text}**\n{queue_text}" + return f"**Help Queue Status: {status}{wait_text}**\n{queue_text}{currently_helped}" async def _get_ta_status_message(self, guild: discord.Guild) -> discord.Message | None: ta_channel = await self._get_ta_channel(guild) @@ -189,6 +191,7 @@ async def _get_ta_status_message(self, guild: discord.Guild) -> discord.Message self.queue_status_message_id = None async for message in ta_channel.history(limit=50): + message: discord.Message if message.author == self.user and message.content.startswith("**Help Queue Status:"): self.queue_status_message_id = message.id return message @@ -231,6 +234,15 @@ async def update_student_status_message(self, guild: discord.Guild) -> None: await student_status_message.edit(content=await self._build_student_status_message(guild)) + async def _get_active_students(self) -> str | None: + students: list[tuple[str, str, str]] = await get_students_not_finished() + if len(students) == 0: + return None + str_builder = "" + for student in students: + str_builder += f"- {student["TA_name"]} is helping {student["student_discord_name"]} with {student["question"]}\n" + return str_builder + async def _refresh_queue_status_messages(self) -> None: while not self.is_closed(): try: diff --git a/src/data_access/queue_history_dao.py b/src/data_access/queue_history_dao.py index 0993ac3..5ed3a02 100644 --- a/src/data_access/queue_history_dao.py +++ b/src/data_access/queue_history_dao.py @@ -119,6 +119,18 @@ async def get_queue_history() -> list: cursor: DictCursor await cursor.execute("SELECT * FROM queue_history") return [row for row in await cursor.fetchall()] + + +async def get_students_not_finished() -> list[tuple[str, str, str]]: + """Gets all students without a time_finished in their queue history + Returns: + A list of dictionary tuples with keys 'student_discord_name', 'TA_name', and 'question'""" + async with db_manager.get_conn() as conn: + conn: aiomysql.Connection + async with conn.cursor(DictCursor) as cursor: + cursor: DictCursor + await cursor.execute("SELECT student_discord_name, TA_name, question FROM queue_history WHERE time_finished IS NULL") + return [row for row in await cursor.fetchall()] def _to_denver_time(time: datetime) -> datetime | None: From 50a510d7cec308cc9462a14c94dff604d5ec3cc5 Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Mon, 13 Jul 2026 10:21:34 -0600 Subject: [PATCH 15/19] update queue messages as part of finish helping student button --- src/ui/views/ta_view.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/views/ta_view.py b/src/ui/views/ta_view.py index 4d9a2c9..e1f7d84 100644 --- a/src/ui/views/ta_view.py +++ b/src/ui/views/ta_view.py @@ -187,6 +187,7 @@ async def finish_button(self, interaction: discord.Interaction, button): await msg.delete(delay=Messages.SHORT_TIMEOUT) return await response.resource.delete() + await update_queue_messages(interaction.client, interaction.guild) class TAQueueManagement(discord.ui.ActionRow[discord.ui.LayoutView]): view: "TAView" From 13a31e68a63185607819a491ecf5ade5c3d26b01 Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Mon, 13 Jul 2026 10:44:51 -0600 Subject: [PATCH 16/19] Allow helping/finishing multiple students in-person --- src/bot.py | 2 +- src/ui/views/ta_view.py | 24 ++++++++++++++++-------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/bot.py b/src/bot.py index 4ccc016..7c86593 100644 --- a/src/bot.py +++ b/src/bot.py @@ -38,7 +38,7 @@ def __init__(self): self.queue_status_message_id: int | None = None self.help_queue_count_message_id: int | None = None self._player_task: Optional[asyncio.Task] = None - self.help_map: map[str, tuple[int, int]] = {} + self.help_map: map[str, list[tuple[int, int]]] = {} async def setup_hook(self): """ diff --git a/src/ui/views/ta_view.py b/src/ui/views/ta_view.py index e1f7d84..949423d 100644 --- a/src/ui/views/ta_view.py +++ b/src/ui/views/ta_view.py @@ -119,12 +119,14 @@ async def next_online_passoff(self, interaction: discord.Interaction, button: di async def help_next_student(interaction: discord.Interaction, passoff_only: bool = False, online_only: bool = False, error_msg: str = "Queue is empty."): msg: discord.InteractionCallbackResponse = await interaction.response.defer(thinking=True, ephemeral=True) if (interaction.user.name in interaction.client.help_map.keys()): - msg = await interaction.followup.send( - "You are currently helping a student! Use \"Finish Helping Student\" to be able to help more students!", - ephemeral=True, wait=True - ) - await msg.delete(delay=Messages.SHORT_TIMEOUT) - return + # get confirmation +# msg = await interaction.followup.send( +# "You are currently helping a student! Use \"Finish Helping Student\" to be able to help more students!", +# ephemeral=True, wait=True +# ) +# await msg.delete(delay=Messages.SHORT_TIMEOUT) +# return + pass front_before = await interaction.client.queue.get_front() @@ -144,7 +146,11 @@ async def help_next_student(interaction: discord.Interaction, passoff_only: bool async def dequeue_student(interaction: discord.Interaction, front_before: Optional[QueueEntry], entry: QueueEntry): student = await interaction.guild.fetch_member(entry.user_id) - interaction.client.help_map[interaction.user.name] = (await add_queue_history_item(entry, student.display_name, interaction.user.name), entry.user_id) + new_entry = (await add_queue_history_item(entry, student.display_name, interaction.user.name), entry.user_id) + if interaction.user.name in interaction.client.help_map: + interaction.client.help_map[interaction.user.name].append(new_entry) + else: + interaction.client.help_map[interaction.user.name] = [new_entry] await move_to_breakout(interaction, entry) @@ -181,7 +187,9 @@ async def finish_button(self, interaction: discord.Interaction, button): ta_name = interaction.user.name try: - await set_time_finished(interaction.client.help_map.pop(ta_name)[0]) + for entry in interaction.client.help_map[ta_name]: + await set_time_finished(entry[0]) + except (KeyError, TypeError): msg = await interaction.followup.send("Error: Could not find the student you were helping.", ephemeral=True, wait=True) await msg.delete(delay=Messages.SHORT_TIMEOUT) From fbde56e0b0bc3ba74efe19dd0f2dc408222abbdf Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Mon, 13 Jul 2026 12:05:21 -0600 Subject: [PATCH 17/19] Put multiple online helps into the same breakout --- src/ui/helpers/discord_helpers.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/ui/helpers/discord_helpers.py b/src/ui/helpers/discord_helpers.py index 895bffa..145385c 100644 --- a/src/ui/helpers/discord_helpers.py +++ b/src/ui/helpers/discord_helpers.py @@ -5,9 +5,16 @@ from data_access.server_info_dao import get_id from ui.helpers.constants import Channels, Messages -def get_next_available_breakout(interaction: discord.Interaction): +async def get_next_available_breakout(interaction: discord.Interaction): + breakout_ids = [] + for name in Channels.BREAKOUT_NAMES: + breakout_ids.append(await get_id(name, interaction.guild.id)) + + if (channel := interaction.user.voice.channel) and channel.id in breakout_ids: + return interaction.user.voice.channel + for vc in interaction.guild.voice_channels: - if vc.name in Channels.BREAKOUT_NAMES and vc.members == []: + if vc.id in breakout_ids and vc.members == []: return vc return None @@ -67,7 +74,7 @@ async def update_queue_messages(client: discord.Client, guild: discord.Guild) -> async def move_to_breakout(interaction: discord.Interaction, entry: QueueEntry): student: discord.Member = interaction.guild.get_member(entry.user_id) if student is None: - student: discord.User = await interaction.client.fetch_user(entry.user_id) + student = await interaction.client.fetch_user(entry.user_id) ta: discord.Member = interaction.guild.get_member(interaction.user.id) if ta is None: ta: discord.Member = interaction.user @@ -80,12 +87,10 @@ async def move_to_breakout(interaction: discord.Interaction, entry: QueueEntry): await ta.send(f"Because you weren't in the Online TAs voice channel, you need to join the {in_person_channel.mention} channel manually. Please do so now.") else: - breakout_channel: discord.VoiceChannel = get_next_available_breakout(interaction) + breakout_channel: discord.VoiceChannel = await get_next_available_breakout(interaction) if breakout_channel is None: - interaction.response.send_message( + interaction.followup.send( "No breakout rooms available at this time. Tough luck.", - ephemeral=True, - delete_after=Messages.SHORT_TIMEOUT ) try: From a2ba7ac1fc293042a64ff4cee8aeb22019dcb4b8 Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Mon, 13 Jul 2026 13:28:28 -0600 Subject: [PATCH 18/19] Helping multiple students requires confirmation --- src/ui/views/ta_view.py | 58 +++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/src/ui/views/ta_view.py b/src/ui/views/ta_view.py index 949423d..b51934e 100644 --- a/src/ui/views/ta_view.py +++ b/src/ui/views/ta_view.py @@ -97,37 +97,38 @@ class TAQueueControls1(discord.ui.ActionRow[discord.ui.LayoutView]): view: "TAView" @discord.ui.button(label="Next Student", style=discord.ButtonStyle.blurple, custom_id="next", emoji="➡️") async def next(self, interaction: discord.Interaction, button): - await help_next_student(interaction) + await confirm_help(interaction) @discord.ui.button(label="Next Student (Online)", style=discord.ButtonStyle.blurple, custom_id="next_online", emoji="💻") async def next_online(self, interaction: discord.Interaction, button: discord.ui.Button): - await help_next_student(interaction, online_only=True, error_msg="No online students in the queue.") + await confirm_help(interaction, online_only=True, error_msg="No online students in the queue.") class TAQueueControls2(discord.ui.ActionRow[discord.ui.LayoutView]): @discord.ui.button(label="Next Passoff", style=discord.ButtonStyle.blurple, custom_id="next_passoff", emoji="✅") async def next_passoff(self, interaction: discord.Interaction, button: discord.ui.Button): - await help_next_student(interaction, passoff_only=True, error_msg="No students awaiting passoff.") + await confirm_help(interaction, passoff_only=True, error_msg="No students awaiting passoff.") @discord.ui.button(label="Next Passoff (Online)", style=discord.ButtonStyle.blurple, custom_id="next_online_passoff", emoji="☑️") async def next_online_passoff(self, interaction: discord.Interaction, button: discord.ui.Button): - await help_next_student(interaction, passoff_only=True, online_only=True, error_msg="No online students awaiting passoff.") + await confirm_help(interaction, passoff_only=True, online_only=True, error_msg="No online students awaiting passoff.") -async def help_next_student(interaction: discord.Interaction, passoff_only: bool = False, online_only: bool = False, error_msg: str = "Queue is empty."): - msg: discord.InteractionCallbackResponse = await interaction.response.defer(thinking=True, ephemeral=True) +async def confirm_help(interaction: discord.Interaction, passoff_only: bool = False, online_only: bool = False, error_msg: str = "Queue is empty."): if (interaction.user.name in interaction.client.help_map.keys()): - # get confirmation -# msg = await interaction.followup.send( -# "You are currently helping a student! Use \"Finish Helping Student\" to be able to help more students!", -# ephemeral=True, wait=True -# ) -# await msg.delete(delay=Messages.SHORT_TIMEOUT) -# return - pass + await interaction.response.send_message( + "You are already helping at least one student. Are you sure you want to help another one at the same time?", + view=MultiHelpView(passoff_only, online_only, error_msg), ephemeral=True, delete_after=Messages.DEFAULT_TIMEOUT + ) + return + await help_next_student(interaction, passoff_only, online_only, error_msg) + + +async def help_next_student(interaction: discord.Interaction, passoff_only: bool = False, online_only: bool = False, error_msg: str = "Queue is empty."): + msg: discord.InteractionCallbackResponse = await interaction.response.defer(thinking=True, ephemeral=True) front_before = await interaction.client.queue.get_front() entry: Optional[QueueEntry] = await interaction.client.queue.next(passoff_only=passoff_only, online_only=online_only) @@ -144,6 +145,7 @@ async def help_next_student(interaction: discord.Interaction, passoff_only: bool await msg.resource.delete() + async def dequeue_student(interaction: discord.Interaction, front_before: Optional[QueueEntry], entry: QueueEntry): student = await interaction.guild.fetch_member(entry.user_id) new_entry = (await add_queue_history_item(entry, student.display_name, interaction.user.name), entry.user_id) @@ -163,6 +165,33 @@ async def dequeue_student(interaction: discord.Interaction, front_before: Option await notify_next_if_changed(interaction.client, front_before) await update_queue_messages(interaction.client, interaction.guild) + + +class MultiHelpView(discord.ui.View): + def __init__(self, passoff_only: bool, online_only: bool, error_msg: str): + super().__init__(timeout=30) + self.passoff_only = passoff_only + self.online_only = online_only + self.error_msg = error_msg + + @discord.ui.button(label="Continue", style=discord.ButtonStyle.green) + async def continue_button(self, interaction: discord.Interaction, button: discord.ui.Button): + await help_next_student(interaction, self.passoff_only, self.online_only, self.error_msg) + await self.disable(interaction) + + @discord.ui.button(label="Cancel", style=discord.ButtonStyle.red) + async def cancel_button(self, interaction: discord.Interaction, button: discord.ui.Button): + msg: discord.InteractionCallbackResponse = await interaction.response.defer(thinking=True, ephemeral=True) + await self.disable(interaction) + await msg.resource.delete() + + async def disable(self, interaction: discord.Interaction): + for button in self.children: + button: discord.ui.Button + button.disabled = True + await interaction.followup.edit_message(interaction.message.id, content="Done!", view=self) + + class TAQueueControls3(discord.ui.ActionRow[discord.ui.LayoutView]): @discord.ui.button(label="Finish Helping Student", style=discord.ButtonStyle.green, custom_id="finish", emoji="🔚") async def finish_button(self, interaction: discord.Interaction, button): @@ -189,6 +218,7 @@ async def finish_button(self, interaction: discord.Interaction, button): try: for entry in interaction.client.help_map[ta_name]: await set_time_finished(entry[0]) + interaction.client.help_map.pop(ta_name) except (KeyError, TypeError): msg = await interaction.followup.send("Error: Could not find the student you were helping.", ephemeral=True, wait=True) From 6b0b3202ae010a1eb09afe8c74567612972c9211 Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Mon, 13 Jul 2026 13:42:13 -0600 Subject: [PATCH 19/19] Fix moving to in-person vc when already in breakout vc --- src/ui/helpers/discord_helpers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ui/helpers/discord_helpers.py b/src/ui/helpers/discord_helpers.py index 145385c..8dcc998 100644 --- a/src/ui/helpers/discord_helpers.py +++ b/src/ui/helpers/discord_helpers.py @@ -80,9 +80,10 @@ async def move_to_breakout(interaction: discord.Interaction, entry: QueueEntry): ta: discord.Member = interaction.user if entry.in_person: try: - channel_id = await get_id(Channels.IN_PERSON_CHANNEL_NAME, interaction.guild.id) - in_person_channel = get(interaction.guild.voice_channels, id=channel_id) - await ta.move_to(in_person_channel) + if ta.voice.channel.id not in [await get_id(breakout_name, interaction.guild.id) for breakout_name in Channels.BREAKOUT_NAMES]: + channel_id = await get_id(Channels.IN_PERSON_CHANNEL_NAME, interaction.guild.id) + in_person_channel = get(interaction.guild.voice_channels, id=channel_id) + await ta.move_to(in_person_channel) except Exception: await ta.send(f"Because you weren't in the Online TAs voice channel, you need to join the {in_person_channel.mention} channel manually. Please do so now.")