chhangement du comportement du discord bot sur modif des matchs
CI - Security, Lint & Tests / validate (push) Failing after 59s

This commit is contained in:
cedrick2711
2026-09-08 12:46:44 -04:00
parent 4a2e2f053f
commit b9791d1547
4 changed files with 145 additions and 55 deletions
+32 -11
View File
@@ -686,6 +686,7 @@ class TeamTryoutsBot(commands.Bot):
event_date: str, event_date: str,
event_time: str, event_time: str,
reference_id: int, reference_id: int,
change_kind: str = 'scheduled',
) -> int: ) -> int:
"""Send a schedule addition notification to a player. """Send a schedule addition notification to a player.
@@ -696,6 +697,7 @@ class TeamTryoutsBot(commands.Bot):
event_date: Date string. event_date: Date string.
event_time: Time string. event_time: Time string.
reference_id: ID of the MatchParticipant or TryoutRegistration record. 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 # Look up the DB user to get their Discord user ID
from app.extensions import db from app.extensions import db
@@ -712,16 +714,29 @@ class TeamTryoutsBot(commands.Bot):
event_name = "Match" if event_type == 'match' else "Tryout" event_name = "Match" if event_type == 'match' else "Tryout"
message = ( if change_kind == 'modified':
f"📅 **{event_name} Scheduled**\n\n" message = (
f"You have been added to the following {event_type}:\n" f"📅 **{event_name} Modified**\n\n"
f"**{event_title}**\n" f"The {event_type} date and/or time has been modified for "
f"**Date:** {event_date}\n" f"**{event_title}**.\n"
f"**Time:** {event_time}\n\n" f"The new date and/or time is:\n"
"Please confirm your attendance:\n" f"**Date:** {event_date}\n"
f"{CHECK_EMOJI} - Confirm attendance\n" f"**Time:** {event_time}\n\n"
f"{CROSS_EMOJI} - Decline" "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( msg = await self._send_dm(
db_user.discord_user_id, db_user.discord_user_id,
@@ -1444,8 +1459,13 @@ def send_schedule_notification(
event_date: str, event_date: str,
event_time: str, event_time: str,
reference_id: int, reference_id: int,
change_kind: str = 'scheduled',
) -> bool: ) -> 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( return _enqueue(
'schedule_addition', 'schedule_addition',
{ {
@@ -1455,6 +1475,7 @@ def send_schedule_notification(
'event_date': event_date, 'event_date': event_date,
'event_time': event_time, 'event_time': event_time,
'reference_id': reference_id, 'reference_id': reference_id,
'change_kind': change_kind,
}, },
description=f'the {event_type} notification for user {user_id}', description=f'the {event_type} notification for user {user_id}',
) )
+92 -43
View File
@@ -570,6 +570,12 @@ def edit_match(match_id):
flash_validation_errors(err) flash_validation_errors(err)
return rerender() 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 # Assigned only once the whole form has been accepted. Assigning as
# each field was read meant a form rejected halfway had already # each field was read meant a form rejected halfway had already
# changed the record in the session. # changed the record in the session.
@@ -581,60 +587,103 @@ def edit_match(match_id):
match.location = data['location'] match.location = data['location']
match.status = data['status'] 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_player_ids = []
notified_participant_ids = [] notified_participant_ids = []
kept_ids = set()
# Capture old participant ids before any deletion, so we can purge for player_id, participant in list(existing.items()):
# their pending Discord notifications once the change is committed. if player_id in desired:
stale_participant_ids = [ kept_ids.add(player_id)
p.id for p in MatchParticipant.query.filter_by(match_id=match.id).all() notified_player_ids.append(player_id)
] notified_participant_ids.append(participant.id)
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)
else: else:
# Same teams: the roster stands, but everyone is told again, removed_ids.append(participant.id)
# because the date or the time may have moved. db.session.delete(participant)
# Reuse the existing participant ids so reference_id is always
# a real participant row, never the match id (fallback). # New players get new participant rows.
for team_id in (match.team1_id, match.team2_id): for player_id in desired - set(existing.keys()):
if team_id: participant = MatchParticipant(match_id=match.id, player_id=player_id)
for member in TeamMember.query.filter_by(team_id=team_id).all(): db.session.add(participant)
notified_player_ids.append(member.player_id) db.session.flush()
existing = MatchParticipant.query.filter_by( notified_player_ids.append(player_id)
match_id=match.id, player_id=member.player_id notified_participant_ids.append(participant.id)
).first() kept_ids.add(player_id)
if existing:
notified_participant_ids.append(existing.id) # When the date or time changed, reset attendance on everyone who
stale_participant_ids = [ # stayed. They'll be asked to re-confirm via the modified message.
pid for pid in stale_participant_ids if pid != existing.id if time_changed:
] for participant in MatchParticipant.query.filter_by(match_id=match.id).all():
else: if participant.player_id in kept_ids:
MatchParticipant.query.filter_by(match_id=match.id).delete() participant.attendance_confirmed = False
notified_player_ids, notified_participant_ids = create_participants(match, data)
db.session.commit() db.session.commit()
# Purge Discord pending entries for participants that no longer exist. # Purge Discord pending entries for participants that no longer exist.
# This runs after the commit so the bot only sees the durable state. if removed_ids:
if stale_participant_ids:
from app.discord_bot import purge_pending_for_participants from app.discord_bot import purge_pending_for_participants
purge_pending_for_participants(stale_participant_ids) purge_pending_for_participants(removed_ids)
notify_participants( # Decide whether and what to notify.
title=match.title, if time_changed:
date=match.date, # All current participants get the "modified" message.
start_time=match.start_time, all_current = list(zip(notified_player_ids, notified_participant_ids))
end_time=match.end_time, notify_participants(
participants=zip_participants(notified_player_ids, notified_participant_ids), title=match.title,
fallback_id=match.id, 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') flash(_('Match updated successfully!'), 'success')
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout.id)) return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout.id))
+6 -1
View File
@@ -60,7 +60,9 @@ def zip_participants(player_ids, participant_ids):
yield player_id, participant_ids[index] if index < len(participant_ids) else None 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. """Tell each participant that a match has been scheduled or changed.
Args: Args:
@@ -73,6 +75,8 @@ def notify_participants(*, title, date, start_time, end_time, participants, fall
attendance lands on the right row. attendance lands on the right row.
fallback_id: Reference to use when a participant row has no id — fallback_id: Reference to use when a participant row has no id —
the match's own, which the bot can still act on. 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: Returns:
int: How many notifications were handed to the bot. 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_date=event_date,
event_time=event_time, event_time=event_time,
reference_id=participant_id or fallback_id, reference_id=participant_id or fallback_id,
change_kind=change_kind,
) )
sent += 1 sent += 1
return sent return sent
+15
View File
@@ -69,6 +69,21 @@ class TestNotifying:
assert [call['reference_id'] for call in sent] == [11, 22] assert [call['reference_id'] for call in sent] == [11, 22]
assert {call['event_date'] for call in sent} == {'2030-04-01'} 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['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): 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 """A Discord reaction resolves the reference back to a record; the