From b9791d15478e0cf1703cda68804c812d4d495253 Mon Sep 17 00:00:00 2001 From: cedrick2711 Date: Tue, 8 Sep 2026 12:46:44 -0400 Subject: [PATCH] chhangement du comportement du discord bot sur modif des matchs --- app/discord_bot.py | 43 +++++++--- app/routes/matches.py | 135 +++++++++++++++++++++---------- app/services/scheduling.py | 7 +- tests/test_scheduling_service.py | 15 ++++ 4 files changed, 145 insertions(+), 55 deletions(-) diff --git a/app/discord_bot.py b/app/discord_bot.py index 3b196a7..3d9cd0d 100644 --- a/app/discord_bot.py +++ b/app/discord_bot.py @@ -686,6 +686,7 @@ class TeamTryoutsBot(commands.Bot): event_date: str, event_time: str, reference_id: int, + change_kind: str = 'scheduled', ) -> int: """Send a schedule addition notification to a player. @@ -696,6 +697,7 @@ class TeamTryoutsBot(commands.Bot): event_date: Date string. event_time: Time string. reference_id: ID of the MatchParticipant or TryoutRegistration record. + change_kind: 'scheduled' (new match / added player) or 'modified' (date/time changed). """ # Look up the DB user to get their Discord user ID from app.extensions import db @@ -712,16 +714,29 @@ class TeamTryoutsBot(commands.Bot): event_name = "Match" if event_type == 'match' else "Tryout" - message = ( - f"📅 **{event_name} Scheduled**\n\n" - f"You have been added to the following {event_type}:\n" - f"**{event_title}**\n" - f"**Date:** {event_date}\n" - f"**Time:** {event_time}\n\n" - "Please confirm your attendance:\n" - f"{CHECK_EMOJI} - Confirm attendance\n" - f"{CROSS_EMOJI} - Decline" - ) + if change_kind == 'modified': + message = ( + f"📅 **{event_name} Modified**\n\n" + f"The {event_type} date and/or time has been modified for " + f"**{event_title}**.\n" + f"The new date and/or time is:\n" + f"**Date:** {event_date}\n" + f"**Time:** {event_time}\n\n" + "Please confirm your attendance:\n" + f"{CHECK_EMOJI} - Confirm attendance\n" + f"{CROSS_EMOJI} - Decline" + ) + else: + message = ( + f"📅 **{event_name} Scheduled**\n\n" + f"You have been added to the following {event_type}:\n" + f"**{event_title}**\n" + f"**Date:** {event_date}\n" + f"**Time:** {event_time}\n\n" + "Please confirm your attendance:\n" + f"{CHECK_EMOJI} - Confirm attendance\n" + f"{CROSS_EMOJI} - Decline" + ) msg = await self._send_dm( db_user.discord_user_id, @@ -1444,8 +1459,13 @@ def send_schedule_notification( event_date: str, event_time: str, reference_id: int, + change_kind: str = 'scheduled', ) -> bool: - """Queue a schedule addition notification to be sent by the bot.""" + """Queue a schedule addition notification to be sent by the bot. + + Args: + change_kind: 'scheduled' (new match / added player) or 'modified' (date/time changed). + """ return _enqueue( 'schedule_addition', { @@ -1455,6 +1475,7 @@ def send_schedule_notification( 'event_date': event_date, 'event_time': event_time, 'reference_id': reference_id, + 'change_kind': change_kind, }, description=f'the {event_type} notification for user {user_id}', ) diff --git a/app/routes/matches.py b/app/routes/matches.py index 906366f..b282015 100644 --- a/app/routes/matches.py +++ b/app/routes/matches.py @@ -570,6 +570,12 @@ def edit_match(match_id): flash_validation_errors(err) return rerender() + # Snapshot the date/time before we overwrite them, so we can detect + # whether only these fields changed and choose the right Discord message. + old_date = match.date + old_start = match.start_time + old_end = match.end_time + # Assigned only once the whole form has been accepted. Assigning as # each field was read meant a form rejected halfway had already # changed the record in the session. @@ -581,60 +587,103 @@ def edit_match(match_id): match.location = data['location'] match.status = data['status'] + time_changed = ( + match.date != old_date or match.start_time != old_start or match.end_time != old_end + ) + + # Build the desired participant set and reconcile it against the + # existing participants, rather than deleting and recreating everyone. + # This preserves attendance_confirmed for players who stay. + desired = set() + if match.match_type == 'team_vs_team': + match.team1_id = data['team1_id'] + match.team2_id = data['team2_id'] + for team_id in (data['team1_id'], data['team2_id']): + if team_id: + for member in TeamMember.query.filter_by(team_id=team_id).all(): + desired.add(member.player_id) + elif match.match_type == 'player_vs_player': + for pid in data['team1_player_ids']: + desired.add(pid) + for pid in data['team2_player_ids']: + desired.add(pid) + else: + for pid in data['player_ids']: + desired.add(pid) + + existing = { + p.player_id: p for p in MatchParticipant.query.filter_by(match_id=match.id).all() + } + + removed_ids = [] notified_player_ids = [] notified_participant_ids = [] + kept_ids = set() - # Capture old participant ids before any deletion, so we can purge - # their pending Discord notifications once the change is committed. - stale_participant_ids = [ - p.id for p in MatchParticipant.query.filter_by(match_id=match.id).all() - ] - - if match.match_type == 'team_vs_team': - teams_changed = data['team1_id'] != match.team1_id or data['team2_id'] != match.team2_id - if teams_changed: - MatchParticipant.query.filter_by(match_id=match.id).delete() - match.team1_id = data['team1_id'] - match.team2_id = data['team2_id'] - notified_player_ids, notified_participant_ids = create_participants(match, data) + for player_id, participant in list(existing.items()): + if player_id in desired: + kept_ids.add(player_id) + notified_player_ids.append(player_id) + notified_participant_ids.append(participant.id) else: - # Same teams: the roster stands, but everyone is told again, - # because the date or the time may have moved. - # Reuse the existing participant ids so reference_id is always - # a real participant row, never the match id (fallback). - for team_id in (match.team1_id, match.team2_id): - if team_id: - for member in TeamMember.query.filter_by(team_id=team_id).all(): - notified_player_ids.append(member.player_id) - existing = MatchParticipant.query.filter_by( - match_id=match.id, player_id=member.player_id - ).first() - if existing: - notified_participant_ids.append(existing.id) - stale_participant_ids = [ - pid for pid in stale_participant_ids if pid != existing.id - ] - else: - MatchParticipant.query.filter_by(match_id=match.id).delete() - notified_player_ids, notified_participant_ids = create_participants(match, data) + removed_ids.append(participant.id) + db.session.delete(participant) + + # New players get new participant rows. + for player_id in desired - set(existing.keys()): + participant = MatchParticipant(match_id=match.id, player_id=player_id) + db.session.add(participant) + db.session.flush() + notified_player_ids.append(player_id) + notified_participant_ids.append(participant.id) + kept_ids.add(player_id) + + # When the date or time changed, reset attendance on everyone who + # stayed. They'll be asked to re-confirm via the modified message. + if time_changed: + for participant in MatchParticipant.query.filter_by(match_id=match.id).all(): + if participant.player_id in kept_ids: + participant.attendance_confirmed = False db.session.commit() # Purge Discord pending entries for participants that no longer exist. - # This runs after the commit so the bot only sees the durable state. - if stale_participant_ids: + if removed_ids: from app.discord_bot import purge_pending_for_participants - purge_pending_for_participants(stale_participant_ids) + purge_pending_for_participants(removed_ids) - notify_participants( - title=match.title, - date=match.date, - start_time=match.start_time, - end_time=match.end_time, - participants=zip_participants(notified_player_ids, notified_participant_ids), - fallback_id=match.id, - ) + # Decide whether and what to notify. + if time_changed: + # All current participants get the "modified" message. + all_current = list(zip(notified_player_ids, notified_participant_ids)) + notify_participants( + title=match.title, + date=match.date, + start_time=match.start_time, + end_time=match.end_time, + participants=all_current, + fallback_id=match.id, + change_kind='modified', + ) + elif desired - set(existing.keys()): + # Only the newly added players are told about the match. + new_player_ids = desired - set(existing.keys()) + new_pairs = [ + (pid, nid) + for pid, nid in zip(notified_player_ids, notified_participant_ids) + if pid in new_player_ids + ] + notify_participants( + title=match.title, + date=match.date, + start_time=match.start_time, + end_time=match.end_time, + participants=new_pairs, + fallback_id=match.id, + change_kind='scheduled', + ) + # else: nothing changed that needs a notification. flash(_('Match updated successfully!'), 'success') return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout.id)) diff --git a/app/services/scheduling.py b/app/services/scheduling.py index 96fac5a..83f4eb0 100644 --- a/app/services/scheduling.py +++ b/app/services/scheduling.py @@ -60,7 +60,9 @@ def zip_participants(player_ids, participant_ids): yield player_id, participant_ids[index] if index < len(participant_ids) else None -def notify_participants(*, title, date, start_time, end_time, participants, fallback_id): +def notify_participants( + *, title, date, start_time, end_time, participants, fallback_id, change_kind='scheduled' +): """Tell each participant that a match has been scheduled or changed. Args: @@ -73,6 +75,8 @@ def notify_participants(*, title, date, start_time, end_time, participants, fall attendance lands on the right row. fallback_id: Reference to use when a participant row has no id — the match's own, which the bot can still act on. + change_kind: 'scheduled' (new match / new player added) or + 'modified' (date/time changed for existing players). Returns: int: How many notifications were handed to the bot. @@ -91,6 +95,7 @@ def notify_participants(*, title, date, start_time, end_time, participants, fall event_date=event_date, event_time=event_time, reference_id=participant_id or fallback_id, + change_kind=change_kind, ) sent += 1 return sent diff --git a/tests/test_scheduling_service.py b/tests/test_scheduling_service.py index 38687e9..a4966ab 100644 --- a/tests/test_scheduling_service.py +++ b/tests/test_scheduling_service.py @@ -69,6 +69,21 @@ class TestNotifying: assert [call['reference_id'] for call in sent] == [11, 22] assert {call['event_date'] for call in sent} == {'2030-04-01'} assert {call['event_time'] for call in sent} == {'07:00 PM - 09:00 PM'} + assert {call['change_kind'] for call in sent} == {'scheduled'} + + def test_modified_kind_is_forwarded(self, sent): + """A modified notification carries 'modified' to the bot.""" + scheduling.notify_participants( + title='Scrim', + date=date(2030, 4, 1), + start_time=time(19, 0), + end_time=time(21, 0), + participants=[(1, 11)], + fallback_id=99, + change_kind='modified', + ) + + assert sent[0]['change_kind'] == 'modified' def test_a_participant_without_a_row_falls_back_to_the_match(self, sent): """A Discord reaction resolves the reference back to a record; the