diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index 37ed021..4bd4d17 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -328,7 +328,7 @@ class Tournament(models.Model): """Returns the total number of spots in all group stages.""" return sum(gs.size for gs in self.groupstage_set.all()) - def teams(self, include_waiting_list): + def teams(self, include_waiting_list, un_walk_out_team=None): """ Get sorted list of teams for the tournament. @@ -349,7 +349,7 @@ class Tournament(models.Model): # Process each team registration for team_reg in self.teamregistration_set.all(): - if team_reg.out_of_tournament(): + if team_reg.out_of_tournament() and ((un_walk_out_team and team_reg.id is not un_walk_out_team.id) or un_walk_out_team is None): continue # Create team item @@ -1227,9 +1227,25 @@ class Tournament(models.Model): if len(teams)<=self.team_count: return None waiting_teams = [team for team in teams if team.stage == "Attente"] - if waiting_teams: + if len(waiting_teams) > 0: return waiting_teams[0].team_registration - return None + + def first_out_of_list(self, un_walk_out_team): + teams = self.teams(True, un_walk_out_team) + waiting_teams = [team for team in teams if team.stage == "Attente"] + + retrieved_teams = [team for team in teams if team.team_registration.id == un_walk_out_team.id] + waiting_teams = [team for team in teams if team.stage == "Attente"] + waiting_team = None + is_in = True + if len(retrieved_teams) > 0: + if retrieved_teams[0].stage == "Attente": + is_in = False + if len(waiting_teams) > 0: + waiting_team = waiting_teams[0].team_registration + + return is_in, waiting_team + def broadcasted_prog(self): # Get matches from broadcasted_matches_and_group_stages diff --git a/tournaments/services/email_service.py b/tournaments/services/email_service.py index 6a3e001..580d671 100644 --- a/tournaments/services/email_service.py +++ b/tournaments/services/email_service.py @@ -1,6 +1,18 @@ from django.core.mail import EmailMessage from django.utils import timezone from django.urls import reverse +from enum import Enum + +class TeamEmailType(Enum): + UNREGISTERED = "unregistered" + OUT_OF_WAITING_LIST = "out_of_waiting_list" + TOURNAMENT_CANCELED = "tournament_canceled" + IN_TOURNAMENT_STRUCTURE = "in_tournament_structure" + OUT_OF_TOURNAMENT_STRUCTURE = "out_of_tournament_structure" + OUT_OF_WALKOUT_IS_IN = "out_of_walkout_is_in" + OUT_OF_WALKOUT_WAITING_LIST = "out_of_walkout_waiting_list" + WALKOUT = "walkout" + UNEXPECTED_OUT_OF_TOURNAMENT = 'unexpected_out_of_tournament' class TournamentEmailService: @staticmethod @@ -36,15 +48,7 @@ class TournamentEmailService: tournament_details_str, waiting_list_position ) - - email = EmailMessage( - subject=email_subject, - body=TournamentEmailService._convert_newlines_to_html(email_body), - to=[request.user.email] - ) - - email.content_subtype = "html" - email.send() + TournamentEmailService._send_email(request.user.email, email_subject, email_body) @staticmethod def _build_email_subject(tournament, tournament_details_str, waiting_list_position): @@ -97,68 +101,7 @@ class TournamentEmailService: other_player ) - email = EmailMessage( - subject=email_subject, - body=TournamentEmailService._convert_newlines_to_html(email_body), - to=[captain.email] - ) - - email.content_subtype = "html" - email.send() - - if other_player.email is not None: - email_body = TournamentEmailService._build_unregistration_email_body( - tournament, - other_player, - tournament_details_str, - captain - ) - - email = EmailMessage( - subject=email_subject, - body=TournamentEmailService._convert_newlines_to_html(email_body), - to=[other_player.email] - ) - - email.content_subtype = "html" - email.send() - - @staticmethod - def send_out_of_waiting_list_confirmation(captain, tournament, other_player): - tournament_details_str = tournament.build_tournament_details_str() - email_subject = TournamentEmailService.email_subject(tournament, "Participation au tournoi") - email_body = TournamentEmailService._build_out_of_waiting_list_email_body( - tournament, - captain, - tournament_details_str, - other_player - ) - - email = EmailMessage( - subject=email_subject, - body=TournamentEmailService._convert_newlines_to_html(email_body), - to=[captain.email] - ) - - email.content_subtype = "html" - email.send() - - if other_player.email is not None: - email_body = TournamentEmailService._build_out_of_waiting_list_email_body( - tournament, - other_player, - tournament_details_str, - captain - ) - - email = EmailMessage( - subject=email_subject, - body=TournamentEmailService._convert_newlines_to_html(email_body), - to=[other_player.email] - ) - - email.content_subtype = "html" - email.send() + TournamentEmailService._send_email(captain.email, email_subject, email_body) @staticmethod def _build_unregistration_email_body(tournament, captain, tournament_details_str, other_player): @@ -219,41 +162,277 @@ class TournamentEmailService: return "".join(body_parts) @staticmethod - def send_tournament_cancellation_notification(player, tournament, other_player): - tournament_details_str = tournament.build_tournament_details_str() - email_subject = TournamentEmailService.email_subject(tournament, "Annulation du tournoi") - email_body = TournamentEmailService._build_tournament_cancellation_email_body( - tournament, - player, - tournament_details_str, - other_player - ) + def _build_tournament_cancellation_email_body(tournament, player, tournament_details_str, other_player): + body_parts = [ + "Bonjour,\n\n", + f"Le tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name} a été annulé par le juge-arbitre." + ] - email = EmailMessage( - subject=email_subject, - body=TournamentEmailService._convert_newlines_to_html(email_body), - to=[player.email] + if other_player is not None: + body_parts.append( + f"\nVous étiez inscrit avec {other_player.name()}, n'oubliez pas de prévenir votre partenaire." + ) + + body_parts.extend([ + "\n\nPour toute question, veuillez contacter votre juge-arbitre:", + f"\n{tournament.event.creator.full_name()}\n{tournament.event.creator.email}", + "\n\nCeci est un e-mail automatique, veuillez ne pas y répondre." + ]) + + return "".join(body_parts) + + @staticmethod + def _build_in_tournament_email_body(tournament, captain, tournament_details_str, other_player): + body_parts = [ + "Bonjour,\n\n", + f"Suite à une modification de la taille du tournoi, vous pouvez participer au tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name}" + ] + + absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info" + link_text = "accéder au tournoi" + absolute_url = f'{link_text}' + + if other_player is not None: + body_parts.append( + f"\nVoici le partenaire indiqué dans l'inscription : {other_player.name()}, n'oubliez pas de le prévenir." + ) + + body_parts.append( + "\n\nSi vous n'êtes plus disponible pour participer à ce tournoi, cliquez sur ce lien ou contactez rapidement le juge-arbitre." + f"\n{absolute_url}" + "\nPour vous désinscrire en ligne vous devez avoir un compte Padel Club." ) - email.content_subtype = "html" - email.send() + body_parts.extend([ + f"\n\n{tournament.event.creator.full_name()}\n{tournament.event.creator.email}", + "\n\nCeci est un e-mail automatique, veuillez ne pas y répondre." + ]) + + return "".join(body_parts) @staticmethod - def _build_tournament_cancellation_email_body(tournament, player, tournament_details_str, other_player): + def _build_out_of_tournament_email_body(tournament, captain, tournament_details_str, other_player): body_parts = [ "Bonjour,\n\n", - f"Le tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name} a été annulé par le juge-arbitre." + f"Suite à une modification de la taille du tournoi, vous ne faites plus partie des équipes participant au tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name}" ] + absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info" + link_text = "informations sur le tournoi" + absolute_url = f'{link_text}' + + body_parts.append(f"\n\nVoir les {absolute_url}") + if other_player is not None: body_parts.append( f"\nVous étiez inscrit avec {other_player.name()}, n'oubliez pas de prévenir votre partenaire." ) body_parts.extend([ - "\n\nPour toute question, veuillez contacter votre juge-arbitre:", + "\n\nPour toute question, veuillez contacter votre juge-arbitre : ", f"\n{tournament.event.creator.full_name()}\n{tournament.event.creator.email}", "\n\nCeci est un e-mail automatique, veuillez ne pas y répondre." ]) return "".join(body_parts) + + @staticmethod + def _build_walk_out_email_body(tournament, captain, tournament_details_str, other_player): + body_parts = [ + "Bonjour,\n\n", + f"Le Juge-Arbitre a annulé votre participation au tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name}" + ] + + absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info" + link_text = "accéder au tournoi" + absolute_url = f'{link_text}' + + body_parts.append(f"\n\nVoir les {absolute_url}") + + + if other_player is not None: + body_parts.append( + f"\nVous étiez inscrit avec {other_player.name()}, n'oubliez pas de prévenir votre partenaire." + ) + + body_parts.extend([ + "\n\nPour toute question, veuillez contacter votre juge-arbitre : ", + f"\n{tournament.event.creator.full_name()}\n{tournament.event.creator.email}", + "\n\nCeci est un e-mail automatique, veuillez ne pas y répondre." + ]) + + return "".join(body_parts) + + @staticmethod + def _build_out_of_walkout_is_in_email_body(tournament, captain, tournament_details_str, other_player): + body_parts = [ + "Bonjour,\n\n", + f"Le juge-arbitre vous a ré-intégré au tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name}" + ] + + absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info" + link_text = "informations sur le tournoi" + absolute_url = f'{link_text}' + + body_parts.append(f"\n\nVoir les {absolute_url}") + + if other_player is not None: + body_parts.append( + f"\nVous étiez inscrit avec {other_player.name()}, n'oubliez pas de prévenir votre partenaire." + ) + + body_parts.extend([ + "\n\nPour toute question, veuillez contacter votre juge-arbitre : ", + f"\n{tournament.event.creator.full_name()}\n{tournament.event.creator.email}", + "\n\nCeci est un e-mail automatique, veuillez ne pas y répondre." + ]) + + return "".join(body_parts) + + @staticmethod + def _build_unexpected_out_of_tournament_email_body(tournament, captain, tournament_details_str, other_player): + body_parts = [ + "Bonjour,\n\n", + f"En raison d'une décision du juge-arbitre, vous ne faites plus partie des équipes participant au tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name}" + ] + + absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info" + link_text = "informations sur le tournoi" + absolute_url = f'{link_text}' + + body_parts.append(f"\n\nVoir les {absolute_url}") + + if other_player is not None: + body_parts.append( + f"\nVous étiez inscrit avec {other_player.name()}, n'oubliez pas de prévenir votre partenaire." + ) + + body_parts.extend([ + "\n\nPour toute question, veuillez contacter votre juge-arbitre : ", + f"\n{tournament.event.creator.full_name()}\n{tournament.event.creator.email}", + "\n\nCeci est un e-mail automatique, veuillez ne pas y répondre." + ]) + + return "".join(body_parts) + + @staticmethod + def _build_out_of_walkout_waiting_list_email_body(tournament, captain, tournament_details_str, other_player): + body_parts = [ + "Bonjour,\n\n", + f"Le juge-arbitre vous a ré-intégré au tournoi en liste d'attente {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name}" + ] + + absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info" + link_text = "informations sur le tournoi" + absolute_url = f'{link_text}' + + body_parts.append(f"\n\nVoir les {absolute_url}") + + if other_player is not None: + body_parts.append( + f"\nVous étiez inscrit avec {other_player.name()}, n'oubliez pas de prévenir votre partenaire." + ) + + body_parts.extend([ + "\n\nPour toute question, veuillez contacter votre juge-arbitre : ", + f"\n{tournament.event.creator.full_name()}\n{tournament.event.creator.email}", + "\n\nCeci est un e-mail automatique, veuillez ne pas y répondre." + ]) + + return "".join(body_parts) + + @staticmethod + def notify(captain, other_player, tournament, message_type: TeamEmailType): + if tournament.should_be_over(): + return + + if tournament.supposedly_in_progress(): + return + + if not captain or not captain.registered_online or not captain.email: + return + + tournament_details_str = tournament.build_tournament_details_str() + + email_body = TournamentEmailService._build_email_content( + message_type, captain, tournament, tournament_details_str, other_player + ) + + if email_body is None: + return + + email_subject = TournamentEmailService.email_subject(tournament, "Participation au tournoi") + TournamentEmailService._send_email(captain.email, email_subject, email_body) + + @staticmethod + def _build_email_content(message_type, recipient, tournament, tournament_details_str, other_player): + if message_type == TeamEmailType.OUT_OF_WAITING_LIST: + body = TournamentEmailService._build_out_of_waiting_list_email_body( + tournament, recipient, tournament_details_str, other_player + ) + elif message_type == TeamEmailType.TOURNAMENT_CANCELED: + body = TournamentEmailService._build_tournament_cancellation_email_body( + tournament, recipient, tournament_details_str, other_player + ) + elif message_type == TeamEmailType.WALKOUT: + body = TournamentEmailService._build_walk_out_email_body( + tournament, recipient, tournament_details_str, other_player + ) + elif message_type == TeamEmailType.OUT_OF_WALKOUT_IS_IN: + body = TournamentEmailService._build_out_of_walkout_is_in_email_body( + tournament, recipient, tournament_details_str, other_player + ) + elif message_type == TeamEmailType.OUT_OF_WALKOUT_WAITING_LIST: + body = TournamentEmailService._build_out_of_walkout_waiting_list_email_body( + tournament, recipient, tournament_details_str, other_player + ) + elif message_type == TeamEmailType.UNEXPECTED_OUT_OF_TOURNAMENT: + body = TournamentEmailService._build_unexpected_out_of_tournament_email_body( + tournament, recipient, tournament_details_str, other_player + ) + elif message_type == TeamEmailType.OUT_OF_TOURNAMENT_STRUCTURE: + body = TournamentEmailService._build_tournament_cancellation_email_body( + tournament, recipient, tournament_details_str, other_player + ) + elif message_type == TeamEmailType.IN_TOURNAMENT_STRUCTURE: + body = TournamentEmailService._build_out_of_tournament_email_body( + tournament, recipient, tournament_details_str, other_player + ) + else: + return None + + return body + + @staticmethod + def _send_email(to, subject, body): + email = EmailMessage( + subject=subject, + body=TournamentEmailService._convert_newlines_to_html(body), + to=[to] + ) + email.content_subtype = "html" + email.send() + + @staticmethod + def notify_team(team, tournament, message_type: TeamEmailType): + captain = None + other_player = None + + for player in team.playerregistration_set.all(): + if player.captain: + captain = player + else: + other_player = player + + if captain: + TournamentEmailService.notify(captain, other_player, tournament, message_type) + else: + # Notify both players separately if there is no captain or the captain is unavailable + players = list(team.playerregistration_set.all()) + if len(players) == 2: + first_player, second_player = players + TournamentEmailService.notify(first_player, second_player, tournament, message_type) + TournamentEmailService.notify(second_player, first_player, tournament, message_type) + elif len(players) == 1: + # If there's only one player, just send them the notification + TournamentEmailService.notify(players[0], None, tournament, message_type) diff --git a/tournaments/signals.py b/tournaments/signals.py index 494a623..ee3cb66 100644 --- a/tournaments/signals.py +++ b/tournaments/signals.py @@ -1,18 +1,19 @@ import random import string -from django.db.models.signals import post_save, pre_delete, post_delete +from django.db.models.signals import pre_save, post_save, pre_delete, post_delete from django.dispatch import receiver from django.conf import settings from tournaments.models.tournament import Tournament from tournaments.models.unregistered_player import UnregisteredPlayer from django.utils import timezone -from .models import Club, FailedApiCall, CustomUser, Log, TeamRegistration, PlayerRegistration, UnregisteredTeam, UnregisteredPlayer +from .models import Club, FailedApiCall, CustomUser, Log, TeamRegistration, PlayerRegistration, UnregisteredTeam, UnregisteredPlayer, TeamSortingType import requests -from tournaments.services.email_service import TournamentEmailService +from tournaments.services.email_service import TournamentEmailService, TeamEmailType from tournaments.models import PlayerDataSource from shared.discord import send_discord_log_message, send_discord_failed_calls_message +from datetime import datetime def generate_unique_code(): characters = string.ascii_lowercase + string.digits @@ -48,95 +49,91 @@ def notify_object_creation_on_discord(created, instance): else: send_discord_log_message(message) - -# def send_discord_message(webhook_url, content): -# data = { -# "content": content -# } -# requests.post(webhook_url, json=data) -# # if response.status_code != 204: -# # raise ValueError( -# # f'Error sending message to Discord webhook: {response.status_code}, {response.text}' -# # ) +def notify_team(team, tournament, message_type): + if tournament.online_registration is False: + return + TournamentEmailService.notify_team(team, tournament, message_type) @receiver(pre_delete, sender=TeamRegistration) def unregister_team(sender, instance, **kwargs): - team_registration = instance - tournament = instance.tournament - if tournament.is_deleted is True: + if instance.tournament.is_deleted: + return + if instance.online_registration is False: return - # Create unregistered player records and track captain/other player - captain = None - other_player = None - for player in team_registration.playerregistration_set.all(): - if player.captain is True: - captain = player - else: - other_player = player - - # Send unregistration confirmation - if captain and captain.registered_online and captain.email: - TournamentEmailService.send_unregistration_confirmation( - captain, - tournament, - other_player - ) - - first_waiting_list_team = tournament.first_waiting_list_team() - print("first_waiting_list_team", first_waiting_list_team) - - # Handle waiting list notifications + notify_team(instance, instance.tournament, TeamEmailType.UNREGISTERED) + first_waiting_list_team = instance.tournament.first_waiting_list_team() if first_waiting_list_team: - waiting_captain = None - waiting_other_player = None - for player in first_waiting_list_team.playerregistration_set.all(): - if player.captain is True: - waiting_captain = player - else: - waiting_other_player = player - - if waiting_captain and waiting_captain.registered_online and waiting_captain.email: - TournamentEmailService.send_out_of_waiting_list_confirmation( - waiting_captain, - tournament, - waiting_other_player - ) + notify_team(first_waiting_list_team, instance.tournament, TeamEmailType.OUT_OF_WAITING_LIST) @receiver(post_save, sender=Tournament) def notify_players_of_tournament_cancellation(sender, instance, **kwargs): - tournament = instance + if not instance.is_deleted: + return - if tournament.is_deleted is False: + if instance.online_registration is False: return - # Get all team registrations - team_registrations = tournament.teamregistration_set.all() + for team_registration in instance.teamregistration_set.all(): + notify_team(team_registration, instance, TeamEmailType.TOURNAMENT_CANCELED) - for team_registration in team_registrations: - captain = None - other_player = None +@receiver(pre_save, sender=Tournament) +def check_waiting_list(sender, instance, **kwargs): + if instance.id is None: + return - # Get players who registered online and have email - for player in team_registration.playerregistration_set.all(): - print(player, player.registered_online) - if player.captain: - captain = player - else: - other_player = player - - # Send email to captain - if captain and captain.registered_online and captain.email: - TournamentEmailService.send_tournament_cancellation_notification( - captain, - tournament, - other_player - ) - - # Send email to other player if they exist and registered online - if other_player and other_player.registered_online and other_player.email: - TournamentEmailService.send_tournament_cancellation_notification( - other_player, - tournament, - captain - ) + if instance.online_registration is False: + return + + previous_state = Tournament.objects.get(id=instance.id) + if previous_state is None: + return + + teams_out_to_warn = [] + teams_in_to_warn = [] + + if previous_state.team_count > instance.team_count: + teams_to_remove_count = previous_state.team_count - instance.team_count + sorted_teams = sorted( + [team for team in previous_state.teams(True) if team.stage != "Attente" and not (team.wildcard_bracket or team.wildcard_groupstage)], + key=lambda t: ( + t.registration_date is None, t.registration_date or datetime.min, t.initial_weight, t.team_registration.id + ) if previous_state.team_sorting == TeamSortingType.INSCRIPTION_DATE else + (t.initial_weight, t.team_registration.id) + ) + teams_out_to_warn = sorted_teams[-teams_to_remove_count:] + elif previous_state.team_count < instance.team_count: + teams_in_to_warn = previous_state.teams(True)[:(instance.team_count - previous_state.team_count)] + + for team in teams_in_to_warn: + notify_team(team, instance, TeamEmailType.IN_TOURNAMENT_STRUCTURE) + + for team in teams_out_to_warn: + notify_team(team, instance, TeamEmailType.OUT_OF_TOURNAMENT_STRUCTURE) + +@receiver(pre_save, sender=TeamRegistration) +def warn_team_walkout_status_change(sender, instance, **kwargs): + if instance.id is None: + return + + if instance.tournament.online_registration is False: + return + + previous_instance = TeamRegistration.objects.get(id=instance.id) + if previous_instance is None: + return + + if previous_instance.out_of_tournament() and not instance.out_of_tournament(): + is_in, first_out_of_list = instance.tournament.first_out_of_list(instance) + if is_in: + notify_team(instance, instance.tournament, TeamEmailType.OUT_OF_WALKOUT_IS_IN) + else: + notify_team(instance, instance.tournament, TeamEmailType.OUT_OF_WALKOUT_WAITING_LIST) + if first_out_of_list: + notify_team(first_out_of_list, instance.tournament, TeamEmailType.UNEXPECTED_OUT_OF_TOURNAMENT) + + elif not previous_instance.out_of_tournament() and instance.out_of_tournament(): + notify_team(instance, instance.tournament, TeamEmailType.WALKOUT) + first_waiting_list_team = instance.tournament.first_waiting_list_team() + if first_waiting_list_team: + notify_team(first_waiting_list_team, instance.tournament, TeamEmailType.OUT_OF_WAITING_LIST) diff --git a/tournaments/views.py b/tournaments/views.py index 0340b4c..936d89a 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -271,8 +271,8 @@ def tournament_teams(request, tournament_id): tournament = get_object_or_404(Tournament, pk=tournament_id) teams = tournament.teams(True) - selected_teams = [team for team in teams if team.stage != 'Attente'] - waiting_teams = [team for team in teams if team.stage == 'Attente'] + selected_teams = [team for team in teams if team.stage != "Attente"] + waiting_teams = [team for team in teams if team.stage == "Attente"] return render(request, 'tournaments/teams.html', { 'tournament': tournament,