diff --git a/tournaments/models/match.py b/tournaments/models/match.py index 17d29a8..1995428 100644 --- a/tournaments/models/match.py +++ b/tournaments/models/match.py @@ -85,12 +85,26 @@ class Match(models.Model): previous_index = self.round.index + 1 # Check if the next index is within the bounds of the rounds array - if previous_index < self.round.tournament.round_set.count(): - return self.round.tournament.round_set.filter(index=previous_index, parent = None).first() + return self.round.tournament.round_set.filter(index=previous_index, parent = self.round.parent).first() # Return None or an appropriate value if the index is out of bounds return None + def get_loser_previous_round(self): + # Calculate the next index + if self.round is None: + return None + + previous_index = self.round.index + 1 + + previous_round = None + # Check if the next index is within the bounds of the rounds array + previous_round = self.round.tournament.round_set.filter(index=previous_index, parent = self.round.parent).first() + if previous_round is None and self.round.parent is not None: + previous_round = self.round.tournament.round_set.filter(id=self.round.parent.id).first() + return previous_round + return None + def precedent_match(self, top): previous_round = self.get_previous_round() #print(previous_round) @@ -103,6 +117,17 @@ class Match(models.Model): match = matches.filter(index=match_index).first() return match + def loser_precedent_match(self, top): + previous_round = self.get_loser_previous_round() + match = None + if previous_round: + matches = previous_round.match_set.all() # Retrieve the QuerySet + match_index = self.index * 2 + 1 + if top == False: + match_index += 1 + match = matches.filter(index=match_index).first() + return match + def computed_name(self): if self.round and self.round.parent is None: return self.backup_name() @@ -134,7 +159,7 @@ class Match(models.Model): is_winner = False scores = [] walk_out = None - team = Team(None, image, names, scores, weight, is_winner, walk_out) + team = Team(None, image, names, scores, weight, is_winner, walk_out, False) return team def is_ready(self): @@ -150,47 +175,89 @@ class Match(models.Model): team_scores = list(self.team_scores.all()) previous_top_match = self.precedent_match(True) previous_bottom_match = self.precedent_match(False) + loser_top_match = self.loser_precedent_match(True) + loser_bottom_match = self.loser_precedent_match(False) + if len(team_scores) == 0: - if (self.round and self.round.tournament.round_set.count() == self.round.index -1): + if (self.round and self.round.parent is None and self.round.tournament.round_set.filter(parent__isnull=True, group_stage_loser_bracket=False).count() - 1 == self.round.index): + names = ["Qualifié", ''] + team = self.default_live_team(names) + teams.append(team) + names = ["Qualifié", ''] + team = self.default_live_team(names) + teams.append(team) return teams if (self.group_stage): - return teams - - if self.round and self.round.parent: - return teams - - # No team scores at all - if previous_top_match: - names = [f"Gagnants {previous_top_match.computed_name()}", ''] + names = ["Équipe de poule", ''] team = self.default_live_team(names) teams.append(team) - if previous_bottom_match: - names = [f"Gagnants {previous_bottom_match.computed_name()}", ''] + names = ["Équipe de poule", ''] team = self.default_live_team(names) teams.append(team) + return teams + elif self.round and self.round.parent: + if loser_top_match: + names = [f"Perdant {loser_top_match.computed_name()}", ''] + team = self.default_live_team(names) + teams.append(team) + if loser_bottom_match: + names = [f"Perdant {loser_bottom_match.computed_name()}", ''] + team = self.default_live_team(names) + teams.append(team) + elif self.round and self.round.parent is None: + if previous_top_match: + names = [f"Gagnant {previous_top_match.computed_name()}", ''] + team = self.default_live_team(names) + teams.append(team) + if previous_bottom_match: + names = [f"Gagnant {previous_bottom_match.computed_name()}", ''] + team = self.default_live_team(names) + teams.append(team) elif len(team_scores) == 1: # Only one team score, handle missing one existing_team = team_scores[0].live_team(self) - if self.round and self.round.parent: - teams.append(existing_team) - elif (self.group_stage): + if (self.group_stage): teams.append(existing_team) - else: - if previous_top_match and previous_top_match.disabled == False and previous_top_match.end_date is None: - names = [f"Gagnants {previous_top_match.computed_name()}", ''] + names = ["Équipe de poule", ''] + team = self.default_live_team(names) + teams.append(team) + elif self.round: + if loser_top_match and loser_top_match.disabled == False and loser_top_match.end_date is None: + names = [f"Perdant {loser_top_match.computed_name()}", ''] team = self.default_live_team(names) teams.append(team) teams.append(existing_team) - elif previous_bottom_match: - names = [f"Gagnants {previous_bottom_match.computed_name()}", ''] + elif loser_bottom_match: + names = [f"Perdant {loser_bottom_match.computed_name()}", ''] team = self.default_live_team(names) teams.append(existing_team) teams.append(team) - else: + elif previous_top_match and previous_top_match.disabled == False and previous_top_match.end_date is None: + names = [f"Gagnant {previous_top_match.computed_name()}", ''] + team = self.default_live_team(names) + teams.append(team) teams.append(existing_team) - else: + elif previous_bottom_match: + names = [f"Gagnant {previous_bottom_match.computed_name()}", ''] + team = self.default_live_team(names) + teams.append(existing_team) + teams.append(team) + elif (self.round.parent is None and self.round.tournament.round_set.filter(parent__isnull=True, group_stage_loser_bracket=False).count() - 1 == self.round.index): + match_index_within_round = self.index - (int(2 ** self.round.index) - 1) + names = ["Qualifié", ''] + team = self.default_live_team(names) + if match_index_within_round < int(2 ** self.round.index) / 2: + teams.append(existing_team) + teams.append(team) + else: + teams.append(team) + teams.append(existing_team) + + elif len(team_scores) == 2: # Both team scores present teams.extend([team_score.live_team(self) for team_score in team_scores]) + else: + teams.extend([team_score.live_team(self) for team_score in team_scores if team_score.walk_out != 1]) return teams @@ -323,7 +390,7 @@ class Match(models.Model): ended = self.end_date is not None live_format = "Format " + FederalMatchCategory(self.format).format_label_short - livematch = LiveMatch(title, date, time_indication, court, self.started(), ended, group_stage_name, live_format) + livematch = LiveMatch(title, date, time_indication, court, self.started(), ended, group_stage_name, live_format, self.start_date, self.court_index, self.disabled) for team in self.live_teams(): livematch.add_team(team) @@ -353,7 +420,7 @@ class Match(models.Model): # return sort_score class Team: - def __init__(self, id, image, names, scores, weight, is_winner, walk_out): + def __init__(self, id, image, names, scores, weight, is_winner, walk_out, is_lucky_loser): # print(f"image = {image}, names= {names}, scores ={scores}, weight={weight}, win={is_winner}") self.id = str(id) self.image = image @@ -361,7 +428,11 @@ class Team: self.scores = scores self.weight = weight self.is_winner = is_winner - self.walk_out = walk_out is not None + self.walk_out = walk_out + self.is_lucky_loser = is_lucky_loser + + def is_walk_out(self): + return self.walk_out is not None def to_dict(self): return { @@ -371,10 +442,12 @@ class Team: "weight": self.weight, "is_winner": self.is_winner, "walk_out": self.walk_out, + "is_walk_out": self.is_walk_out(), + "is_lucky_loser": self.is_lucky_loser } class LiveMatch: - def __init__(self, title, date, time_indication, court, started, ended, group_stage_name, format): + def __init__(self, title, date, time_indication, court, started, ended, group_stage_name, format, start_date, court_index, disabled): self.title = title self.date = date self.teams = [] @@ -385,10 +458,13 @@ class LiveMatch: self.has_walk_out = False self.group_stage_name = group_stage_name self.format = format + self.disabled = disabled + self.start_date = start_date + self.court_index = court_index def add_team(self, team): self.teams.append(team) - if team.walk_out is True: + if team.is_walk_out() is True: self.has_walk_out = True def to_dict(self): @@ -402,12 +478,15 @@ class LiveMatch: "ended": self.ended, "has_walk_out": self.has_walk_out, "group_stage_name": self.group_stage_name, - "format": self.format + "format": self.format, + "disabled": self.disabled, + "start_date": self.start_date, + "court_index": self.court_index } def show_time_indication(self): for team in self.teams: - if team.walk_out and len(team.scores) == 0: + if team.is_walk_out() and len(team.scores) == 0: return False return True diff --git a/tournaments/models/round.py b/tournaments/models/round.py index befe57a..ea89370 100644 --- a/tournaments/models/round.py +++ b/tournaments/models/round.py @@ -31,9 +31,9 @@ class Round(models.Model): if self.index == 0: return "Finale" elif self.index == 1: - return "Demi-Finales" + return "Demi" elif self.index == 2: - return "Quarts de finale" + return "Quart" else: squared = 2 ** self.index return f"{squared}ème" diff --git a/tournaments/models/team_registration.py b/tournaments/models/team_registration.py index 2bc141a..846b593 100644 --- a/tournaments/models/team_registration.py +++ b/tournaments/models/team_registration.py @@ -225,7 +225,7 @@ class TeamRegistration(models.Model): if team_rank > self.final_ranking: sign = "+" if team_rank == self.final_ranking: - sign = "" + sign = "+" return f" ({sign}"+f"{abs(self.final_ranking - team_rank)})" def get_points_earned(self): @@ -257,3 +257,9 @@ class TeamRegistration(models.Model): # ratio = (wins / total_matches) * 100 return f"{wins}/{total_matches}" return None + + def has_registered_online(self): + for p in self.playerregistration_set.all(): + if p.registered_online: + return True + return False diff --git a/tournaments/models/team_score.py b/tournaments/models/team_score.py index e23c225..9e22b12 100644 --- a/tournaments/models/team_score.py +++ b/tournaments/models/team_score.py @@ -98,5 +98,6 @@ class TeamScore(models.Model): scores = self.parsed_scores() walk_out = self.walk_out from .match import Team # Import Team only when needed - team = Team(id, image, names, scores, weight, is_winner, walk_out) + is_lucky_loser = self.lucky_loser is not None + team = Team(id, image, names, scores, weight, is_winner, walk_out, is_lucky_loser) return team diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index 37ed021..3740561 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -154,7 +154,7 @@ class Tournament(models.Model): return self.start_date.astimezone(timezone) def local_start_date_formatted(self): - return formats.date_format(self.local_start_date(), format='j F Y H:i') + return formats.date_format(self.local_start_date(), format='l j F Y H:i').capitalize() def level(self): if self.federal_level_category == 0: @@ -328,16 +328,12 @@ 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): - """ - Get sorted list of teams for the tournament. - - Args: - include_waiting_list (bool): Whether to include teams in waiting list + def teams(self, include_waiting_list, un_walk_out_team=None): + complete_teams, wildcard_bracket, wildcard_group_stage, waiting_teams = self.computed_teams(un_walk_out_team) + all_teams = self.sort_teams(include_waiting_list, complete_teams, wildcard_bracket, wildcard_group_stage, waiting_teams) + return all_teams - Returns: - list: List of TeamItem objects sorted according to tournament rules - """ + def computed_teams(self, un_walk_out_team=None): # Initialize team categories complete_teams = [] wildcard_bracket = [] @@ -348,7 +344,10 @@ class Tournament(models.Model): closed_date = self.closed_registration_date # Process each team registration - for team_reg in self.teamregistration_set.all(): + for db_team_reg in self.teamregistration_set.all(): + team_reg = db_team_reg + if un_walk_out_team and team_reg.id == un_walk_out_team.id: + team_reg = un_walk_out_team if team_reg.out_of_tournament(): continue @@ -379,8 +378,10 @@ class Tournament(models.Model): complete_teams.append(team) else: waiting_teams.append(team) + wildcard_bracket = [] + return complete_teams, wildcard_bracket, wildcard_group_stage, waiting_teams - + def sort_teams(self, include_waiting_list, complete_teams, wildcard_bracket, wildcard_group_stage, waiting_teams): # Initialize group stage spots group_stage_spots = self.group_stage_spots() bracket_seeds = self.team_count - group_stage_spots - len(wildcard_bracket) @@ -1188,7 +1189,7 @@ class Tournament(models.Model): current_year += 1 user_age = current_year - int(birth_year) - print(user_age) + print("user_age", user_age) # Check age category restrictions if self.federal_age_category == FederalAgeCategory.A11_12 and user_age > 12: @@ -1221,15 +1222,12 @@ class Tournament(models.Model): def min_player_rank(self): return FederalLevelCategory.min_player_rank(self.federal_level_category, self.federal_category, self.federal_age_category) - def first_waiting_list_team(self): - teams = self.teams(True) - + def first_waiting_list_team(self, teams): 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 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..a43a2fe 100644 --- a/tournaments/services/email_service.py +++ b/tournaments/services/email_service.py @@ -1,6 +1,32 @@ 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' + + def email_subject(self) -> str: + subjects = { + self.UNREGISTERED: "Désistement", + self.OUT_OF_WAITING_LIST: "Participation confirmée", + self.TOURNAMENT_CANCELED: "Tournoi annulé", + self.IN_TOURNAMENT_STRUCTURE: "Participation confirmée", + self.OUT_OF_TOURNAMENT_STRUCTURE: "Participation annulée", + self.OUT_OF_WALKOUT_IS_IN: "Participation confirmée", + self.OUT_OF_WALKOUT_WAITING_LIST: "Liste d'attente", + self.WALKOUT: "Participation annulée", + self.UNEXPECTED_OUT_OF_TOURNAMENT: "Participation annulée", + } + return subjects.get(self, "Tournament Notification") class TournamentEmailService: @staticmethod @@ -36,22 +62,14 @@ 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): if waiting_list_position >= 0: - base_subject = "En liste d'attente du tournoi" + base_subject = "Liste d'attente" else: - base_subject = "Confirmation d'inscription au tournoi" + base_subject = "Participation confirmé" return TournamentEmailService.email_subject(tournament, base_subject) @staticmethod @@ -85,81 +103,6 @@ class TournamentEmailService: return "\n".join(body_parts) - @staticmethod - def send_unregistration_confirmation(captain, tournament, other_player): - tournament_details_str = tournament.build_tournament_details_str() - - email_subject = TournamentEmailService.email_subject(tournament, "Désistement du tournoi") - email_body = TournamentEmailService._build_unregistration_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_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() - @staticmethod def _build_unregistration_email_body(tournament, captain, tournament_details_str, other_player): body_parts = [ @@ -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 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 + + topic = message_type.email_subject() + email_subject = TournamentEmailService.email_subject(tournament, topic) + 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.UNREGISTERED: + body = TournamentEmailService._build_unregistration_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_out_of_tournament_email_body( + tournament, recipient, tournament_details_str, other_player + ) + elif message_type == TeamEmailType.IN_TOURNAMENT_STRUCTURE: + body = TournamentEmailService._build_in_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() + print("TournamentEmailService._send_email", to, subject) + + @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 368b768..917c766 100644 --- a/tournaments/signals.py +++ b/tournaments/signals.py @@ -8,12 +8,13 @@ from tournaments.models.unregistered_player import UnregisteredPlayer from django.utils import timezone from django.db.utils import IntegrityError -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 @receiver(pre_save, sender=CustomUser) def username_check(instance, sender, **kwargs): @@ -55,95 +56,143 @@ 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}' -# # ) - -@receiver(pre_delete, sender=TeamRegistration) -def unregister_team(sender, instance, **kwargs): - team_registration = instance - tournament = instance.tournament - if tournament.is_deleted is True: +def notify_team(team, tournament, message_type): + #print(team, message_type) + if tournament.enable_online_registration is False: + return + if team.has_registered_online() is False: + return + if tournament.should_be_over(): + return + if tournament.supposedly_in_progress(): 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: + + for player in team.playerregistration_set.all(): + if player.captain: 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) + if captain: + TournamentEmailService.notify(captain, other_player, tournament, message_type) + if not captain.registered_online or not captain.email: + TournamentEmailService.notify(other_player, captain, 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) - # Handle waiting list notifications - 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 +@receiver(pre_delete, sender=TeamRegistration) +def unregister_team(sender, instance, **kwargs): + if instance.tournament.is_deleted: + return + if instance.tournament.enable_online_registration is False: + return - 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(instance, instance.tournament, TeamEmailType.UNREGISTERED) + teams = instance.tournament.teams(True) + first_waiting_list_team = instance.tournament.first_waiting_list_team(teams) + if first_waiting_list_team and first_waiting_list_team.id != instance.id: + 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.enable_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.enable_online_registration is False: + return + + try: + previous_state = Tournament.objects.get(id=instance.id) + except Tournament.DoesNotExist: + previous_state = None + + 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 + previous_state_teams = previous_state.teams(True) + sorted_teams = sorted( + [team for team in previous_state_teams 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 = [ + team for team in previous_state.teams(True)[(instance.team_count - previous_state.team_count):] + if team.stage == "Attente" + ] + + for team in teams_in_to_warn: + notify_team(team.team_registration, instance, TeamEmailType.IN_TOURNAMENT_STRUCTURE) + + for team in teams_out_to_warn: + notify_team(team.team_registration, 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 or instance.tournament.enable_online_registration is False: + return + + previous_instance = None + try: + previous_instance = TeamRegistration.objects.get(id=instance.id) + except TeamRegistration.DoesNotExist: + return + + previous_teams = instance.tournament.teams(True) + current_teams = instance.tournament.teams(True, instance) + previous_retrieved_teams = [team for team in previous_teams if team.team_registration.id == previous_instance.id] + current_retrieved_teams = [team for team in current_teams if team.team_registration.id == instance.id] + was_out = previous_instance.out_of_tournament() + is_out = instance.out_of_tournament() + if len(previous_retrieved_teams) > 0 and previous_retrieved_teams[0].stage == "Attente": + was_out = True + + if len(current_retrieved_teams) > 0 and current_retrieved_teams[0].stage == "Attente": + is_out = True + + print(was_out, previous_instance.out_of_tournament(), is_out, instance.out_of_tournament()) + if not instance.out_of_tournament() and is_out and (previous_instance.out_of_tournament() or not was_out): + notify_team(instance, instance.tournament, TeamEmailType.OUT_OF_WALKOUT_WAITING_LIST) + elif was_out and not is_out: + notify_team(instance, instance.tournament, TeamEmailType.OUT_OF_WALKOUT_IS_IN) + elif not previous_instance.out_of_tournament() and instance.out_of_tournament(): + notify_team(instance, instance.tournament, TeamEmailType.WALKOUT) + + if was_out and not is_out: + first_out_of_list = instance.tournament.first_waiting_list_team(current_teams) + if first_out_of_list: + notify_team(first_out_of_list, instance.tournament, TeamEmailType.UNEXPECTED_OUT_OF_TOURNAMENT) + elif not was_out and is_out: + first_waiting_list_team = instance.tournament.first_waiting_list_team(previous_teams) + if first_waiting_list_team: + notify_team(first_waiting_list_team, instance.tournament, TeamEmailType.OUT_OF_WAITING_LIST) diff --git a/tournaments/static/tournaments/css/style.css b/tournaments/static/tournaments/css/style.css index a112a8b..035357a 100644 --- a/tournaments/static/tournaments/css/style.css +++ b/tournaments/static/tournaments/css/style.css @@ -326,7 +326,6 @@ tr { flex: 1; display: flex; flex-direction: column; - /* Stack player names vertically */ } .scores { @@ -803,12 +802,6 @@ h-margin { color: #707070; } -.single-line { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - .group-stage-link { text-decoration: none; color: inherit; @@ -826,3 +819,33 @@ h-margin { .tournament-info a:hover { color: #f39200; } + +.top-border { + border-top: 1px solid #ccc; +} + +.strikethrough { + text-decoration: line-through; +} + +.status-container { + margin: 0 -20px -20px -20px; /* Negative margin to counter the bubble padding, including bottom */ + padding: 10px 20px 20px 20px; /* Add padding back to maintain text alignment, including bottom */ + border-radius: 0 0 24px 24px; /* Match the bubble's bottom corners */ +} + +.status-container.running { + background-color: #90ee90; /* Light green color */ +} + +.player { + position: relative; /* Ensures the overlay is positioned within this block */ +} + +.overlay-text { + position: absolute; + top: 50%; + right: 0%; + transform: translate(-50%, -50%); + white-space: nowrap; +} diff --git a/tournaments/templates/tournaments/bracket_match_cell.html b/tournaments/templates/tournaments/bracket_match_cell.html new file mode 100644 index 0000000..578c1fd --- /dev/null +++ b/tournaments/templates/tournaments/bracket_match_cell.html @@ -0,0 +1,80 @@ +{% load static %} + +
+
+ +
+ + {% if not match.ended %} + + {% endif %} +
+ +
+ + {% for team in match.teams %} +
+ + + {% if match.has_walk_out %} + + {% if team.is_walk_out %}WO{% endif %} + + {% elif match.should_show_scores %} +
+ {% for score in team.scores %} + + {{ score.main }} + {% if score.tiebreak %} + {{ score.tiebreak }} + {% endif %} + + {% endfor %} +
+ {% elif not tournament.hide_weight and team.weight %} + {{ team.weight }} + {% endif %} +
+ + {% endfor %} + +
+
+
+ + +
+
+
+
diff --git a/tournaments/templates/tournaments/broadcast/broadcasted_match.html b/tournaments/templates/tournaments/broadcast/broadcasted_match.html index 9e3d0cc..4f48928 100644 --- a/tournaments/templates/tournaments/broadcast/broadcasted_match.html +++ b/tournaments/templates/tournaments/broadcast/broadcasted_match.html @@ -16,15 +16,28 @@
-
+
+ + + +
+
diff --git a/tournaments/templates/tournaments/group_stages.html b/tournaments/templates/tournaments/group_stages.html index ae084d7..670e096 100644 --- a/tournaments/templates/tournaments/group_stages.html +++ b/tournaments/templates/tournaments/group_stages.html @@ -4,9 +4,9 @@ {% block first_title %}{{ tournament.event.display_name }}{% endblock %} {% block second_title %}{{ tournament.display_name }}{% endblock %} -{% if tournament.display_group_stages %} {% block content %} +{% if tournament.display_group_stages %} {% include 'tournaments/navigation_tournament.html' %} @@ -20,6 +20,5 @@
{% endfor %} + {% endif %} {% endblock %} - -{% endif %} diff --git a/tournaments/templates/tournaments/match_cell.html b/tournaments/templates/tournaments/match_cell.html index fc0e17c..578c1fd 100644 --- a/tournaments/templates/tournaments/match_cell.html +++ b/tournaments/templates/tournaments/match_cell.html @@ -16,10 +16,17 @@
diff --git a/tournaments/templates/tournaments/matches.html b/tournaments/templates/tournaments/matches.html index f93fc75..625423e 100644 --- a/tournaments/templates/tournaments/matches.html +++ b/tournaments/templates/tournaments/matches.html @@ -4,9 +4,9 @@ {% block first_title %}{{ tournament.event.display_name }}{% endblock %} {% block second_title %}{{ tournament.display_name }}{% endblock %} -{% if tournament.display_matches %} {% block content %} +{% if tournament.display_matches %} {% include 'tournaments/navigation_tournament.html' %} {% if tournament.display_matches or tournament.display_group_stages %} @@ -44,5 +44,5 @@ {% endfor %} {% endif %} -{% endblock %} {% endif %} +{% endblock %} diff --git a/tournaments/templates/tournaments/navigation_tournament.html b/tournaments/templates/tournaments/navigation_tournament.html index b102ceb..f4f1573 100644 --- a/tournaments/templates/tournaments/navigation_tournament.html +++ b/tournaments/templates/tournaments/navigation_tournament.html @@ -1,6 +1,7 @@