diff --git a/tournaments/models/match.py b/tournaments/models/match.py index b8afac8..c4f7e67 100644 --- a/tournaments/models/match.py +++ b/tournaments/models/match.py @@ -286,6 +286,16 @@ class Match(SideStoreModel): elif len(team_scores) == 2: # Both team scores present teams.extend([team_score.live_team(self) for team_score in team_scores]) + + if self.round is not None and self.round.parent is None: + pos1 = team_scores[0].team_registration.bracket_position if hasattr(team_scores[0], 'team_registration') and team_scores[0].team_registration else None + pos2 = team_scores[1].team_registration.bracket_position if hasattr(team_scores[1], 'team_registration') and team_scores[1].team_registration else None + if pos1 is not None and pos2 is not None and pos1 // 2 == self.index and pos2 // 2 == self.index: + if pos1 > pos2: + teams = [team_scores[1].live_team(self), team_scores[0].live_team(self)] + else: + teams = [team_scores[0].live_team(self), team_scores[1].live_team(self)] + else: teams.extend([team_score.live_team(self) for team_score in team_scores if team_score.walk_out != 1]) @@ -420,7 +430,7 @@ class Match(SideStoreModel): 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, self.start_date, self.court_index, self.disabled) + livematch = LiveMatch(self.index, 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) @@ -481,7 +491,8 @@ class Team: } class LiveMatch: - def __init__(self, title, date, time_indication, court, started, ended, group_stage_name, format, start_date, court_index, disabled): + def __init__(self, index, title, date, time_indication, court, started, ended, group_stage_name, format, start_date, court_index, disabled): + self.index = index self.title = title self.date = date self.teams = [] @@ -503,6 +514,7 @@ class LiveMatch: def to_dict(self): return { + "index": self.index, "title": self.title, "date": self.date, "teams": [team.to_dict() for team in self.teams], diff --git a/tournaments/models/round.py b/tournaments/models/round.py index a6c0250..33ee27e 100644 --- a/tournaments/models/round.py +++ b/tournaments/models/round.py @@ -109,7 +109,7 @@ class Round(SideStoreModel): return True - def prepare_match_group(self, next_round, parent_round, loser_final, double_butterfly_mode): + def prepare_match_group(self, next_round, parent_round, loser_final, double_butterfly_mode, secondHalf): matches = self.matches.filter(disabled=False).order_by('index') if len(matches) == 0: return None @@ -132,7 +132,9 @@ class Round(SideStoreModel): # Only filter out the pair if both matches are disabled if current_match.disabled and pair_match and pair_match.disabled: # Skip one of the matches in the pair - filtered_matches.append(current_match) + if next_round_matches.filter(index=current_match.index // 2).exists(): + filtered_matches.append(current_match) + filtered_matches.append(pair_match) pass else: # Keep the current match @@ -152,6 +154,8 @@ class Round(SideStoreModel): if len(matches) > 1 and double_butterfly_mode: midpoint = int(len(matches) / 2) first_half_matches = matches[:midpoint] + if secondHalf: + first_half_matches = matches[midpoint:] else: first_half_matches = list(matches) # Convert QuerySet to a list if self.index == 0 and loser_final: diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index a782e10..0024b6b 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -676,8 +676,8 @@ class Tournament(BaseModel): matches.extend(first_round.get_matches_recursive(True)) else: current_round = self.round_to_show() - # print(f'current_round = {current_round.index} / parent = {current_round.parent}') if current_round: + print(f'current_round = {current_round.index} / parent = {current_round.parent}') all_upper_matches_are_over = current_round.all_matches_are_over() if all_upper_matches_are_over is False: matches.extend(current_round.get_matches_recursive(True)) @@ -704,8 +704,12 @@ class Tournament(BaseModel): previous_previous_matches = [m for m in previous_previous_matches if m.end_date is None] matches.extend(previous_previous_matches) else: - # print('group_stages') + print('group_stages') group_stages = [gs.live_group_stages() for gs in self.last_group_stage_step()] + else: + first_round = self.first_round() + if first_round: + matches.extend(first_round.get_matches_recursive(True)) return matches, group_stages @@ -1378,7 +1382,7 @@ class Tournament(BaseModel): # Add first half of each round (from last to semi-finals) for round in main_rounds: next_round = main_rounds.filter(index=round.index - 1).first() - match_group = round.prepare_match_group(next_round, parent_round, loser_final, double_butterfly_mode) + match_group = round.prepare_match_group(next_round, parent_round, loser_final, double_butterfly_mode, False) if match_group: serializable_match_groups.append(match_group) @@ -1386,10 +1390,11 @@ class Tournament(BaseModel): main_rounds_reversed = list(main_rounds) main_rounds_reversed.reverse() for round in main_rounds_reversed: - next_round = main_rounds.filter(index=round.index - 1).first() - match_group = round.prepare_match_group(next_round, parent_round, None, double_butterfly_mode) - if match_group: - serializable_match_groups.append(match_group) + if round.index > 0: + next_round = main_rounds.filter(index=round.index - 1).first() + match_group = round.prepare_match_group(next_round, parent_round, None, double_butterfly_mode, True) + if match_group: + serializable_match_groups.append(match_group) return serializable_match_groups @@ -1406,6 +1411,13 @@ class MatchGroup: def add_matches(self, matches): self.matches = matches + def to_dict(self): + return { + 'name': self.name, + 'round_id': self.round_id, + 'matches': [match.to_dict() for match in self.matches] + } + class TeamSummon: def __init__(self, id, names, date, weight, stage, court, image, day_duration): self.id = str(id) diff --git a/tournaments/repositories.py b/tournaments/repositories.py index cd394bd..634a835 100644 --- a/tournaments/repositories.py +++ b/tournaments/repositories.py @@ -53,12 +53,10 @@ class TournamentRegistrationRepository: rank=rank, computed_rank=computed_rank, licence_id=player_data['licence_id'], + email=player_data.get('email'), + phone_number=player_data.get('mobile_number'), ) - if is_captain is True: - player_registration.email=team_form_data['email'] - player_registration.phone_number=team_form_data['mobile_number'] - player_registration.save() team_registration.set_weight() diff --git a/tournaments/services/email_service.py b/tournaments/services/email_service.py index f334fbd..2f370de 100644 --- a/tournaments/services/email_service.py +++ b/tournaments/services/email_service.py @@ -4,6 +4,8 @@ from django.urls import reverse from enum import Enum class TeamEmailType(Enum): + REGISTERED = "registered" + WAITING_LIST = "waiting_list" UNREGISTERED = "unregistered" OUT_OF_WAITING_LIST = "out_of_waiting_list" TOURNAMENT_CANCELED = "tournament_canceled" @@ -16,6 +18,8 @@ class TeamEmailType(Enum): def email_subject(self) -> str: subjects = { + self.REGISTERED: "Participation confirmée", + self.WAITING_LIST: "Liste d'attente", self.UNREGISTERED: "Désistement", self.OUT_OF_WAITING_LIST: "Participation confirmée", self.TOURNAMENT_CANCELED: "Tournoi annulé", @@ -47,52 +51,37 @@ class TournamentEmailService: @staticmethod def send_registration_confirmation(request, tournament, team_registration, waiting_list_position): - tournament_details_str = tournament.build_tournament_details_str() - - email_subject = TournamentEmailService._build_email_subject( - tournament, - tournament_details_str, - waiting_list_position - ) - - email_body = TournamentEmailService._build_email_body( - request, - tournament, - team_registration, - tournament_details_str, - waiting_list_position - ) - 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 = "Liste d'attente" + TournamentEmailService.notify_team(team_registration, tournament, TeamEmailType.WAITING_LIST) else: - base_subject = "Participation confirmée" - return TournamentEmailService.email_subject(tournament, base_subject) + TournamentEmailService.notify_team(team_registration, tournament, TeamEmailType.REGISTERED) @staticmethod - def _build_email_body(request, tournament, team_registration, tournament_details_str, waiting_list_position): - inscription_date = team_registration.local_registration_date().strftime("%d/%m/%Y à %H:%M") - team_members = [player.name() for player in team_registration.player_registrations.all()] - team_members_str = " et ".join(team_members) + def _build_registration_confirmation_email_body(tournament, captain, tournament_details_str, other_player): + return TournamentEmailService._build_registration_email_body(tournament, captain, tournament_details_str, other_player, False) + @staticmethod + def _build_waiting_list_confirmation_email_body(tournament, captain, tournament_details_str, other_player): + return TournamentEmailService._build_registration_email_body(tournament, captain, tournament_details_str, other_player, True) + + @staticmethod + def _build_registration_email_body(tournament, captain, tournament_details_str, other_player, waiting_list): + inscription_date = captain.team_registration.local_registration_date().strftime("%d/%m/%Y à %H:%M") body_parts = [] body_parts.append("Bonjour,\n") - if waiting_list_position >= 0: + if waiting_list: body_parts.append(f"Votre inscription en liste d'attente du tournoi {tournament_details_str} est confirmée.") else: body_parts.append(f"Votre inscription au tournoi {tournament_details_str} est confirmée.") - absolute_url = f"{request.build_absolute_uri(f'/tournament/{tournament.id}/')}" + absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info" link_text = "informations sur le tournoi" absolute_url = f'{link_text}' body_parts.extend([ f"\nDate d'inscription: {inscription_date}", - f"\nÉquipe inscrite: {team_members_str}", + f"\nÉquipe inscrite: {captain.name()} et {other_player.name()}", f"\nLe tournoi commencera le {tournament.formatted_start_date()} au club {tournament.event.club.name}", f"\nVoir les {absolute_url}", "\nPour toute question, veuillez contacter votre juge-arbitre. Si vous n'êtes pas à l'origine de cette inscription, merci de le contacter rapidement.", @@ -343,6 +332,7 @@ class TournamentEmailService: @staticmethod def notify(captain, other_player, tournament, message_type: TeamEmailType): + print("TournamentEmailService.notify", captain.email, captain.registered_online, tournament, message_type) if not captain or not captain.registered_online or not captain.email: return @@ -360,8 +350,16 @@ class TournamentEmailService: 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: + def _build_email_content(message_type, recipient, tournament, tournament_details_str, other_player, request=None, waiting_list_position=None): + if message_type == TeamEmailType.REGISTERED: + body = TournamentEmailService._build_registration_confirmation_email_body( + tournament, recipient, tournament_details_str, other_player + ) + elif message_type == TeamEmailType.WAITING_LIST: + body = TournamentEmailService._build_waiting_list_confirmation_email_body( + tournament, recipient, tournament_details_str, other_player + ) + elif message_type == TeamEmailType.OUT_OF_WAITING_LIST: body = TournamentEmailService._build_out_of_waiting_list_email_body( tournament, recipient, tournament_details_str, other_player ) @@ -415,24 +413,14 @@ class TournamentEmailService: @staticmethod def notify_team(team, tournament, message_type: TeamEmailType): - captain = None - other_player = None - - for player in team.player_registrations.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.player_registrations.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) + # Notify both players separately if there is no captain or the captain is unavailable + players = list(team.player_registrations.all()) + if len(players) == 2: + print("TournamentEmailService.notify_team 2p", team) + 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: + print("TournamentEmailService.notify_team 1p", team) + # If there's only one player, just send them the notification + TournamentEmailService.notify(players[0], None, tournament, message_type) diff --git a/tournaments/services/tournament_registration.py b/tournaments/services/tournament_registration.py index 8f033f0..6d01eb9 100644 --- a/tournaments/services/tournament_registration.py +++ b/tournaments/services/tournament_registration.py @@ -290,6 +290,28 @@ class TournamentRegistrationService: 'phone': None, }) + from django.contrib.auth import get_user_model + User = get_user_model() + + # Get the license ID from player_data + licence_id = player_data.get('licence_id') + validator = LicenseValidator(licence_id) + if validator and validator.stripped_license: + try: + # Try to find a user with matching license + user_with_same_license = User.objects.get(licence_id__icontains=validator.stripped_license) + + # If found, update the email and phone + if user_with_same_license: + player_data.update({ + 'email': user_with_same_license.email, + 'phone': user_with_same_license.phone + }) + print(f"Found user with license {licence_id}, updated email and phone") + except User.DoesNotExist: + # No user found with this license, continue with None email and phone + pass + def _handle_first_tournament_case(self, data): print("_handle_first_tournament_case", data) if data: diff --git a/tournaments/signals.py b/tournaments/signals.py index 70c85e3..ae6cbd1 100644 --- a/tournaments/signals.py +++ b/tournaments/signals.py @@ -64,30 +64,7 @@ def notify_team(team, tournament, message_type): if tournament.supposedly_in_progress(): return - captain = None - other_player = None - - for player in team.player_registrations.all(): - if player.captain: - captain = player - else: - other_player = player - - 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.player_registrations.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) - + TournamentEmailService.notify_team(team, tournament, message_type) @receiver(pre_delete, sender=TeamRegistration) def unregister_team(sender, instance, **kwargs): diff --git a/tournaments/static/tournaments/css/style.css b/tournaments/static/tournaments/css/style.css index e3073b3..205c41c 100644 --- a/tournaments/static/tournaments/css/style.css +++ b/tournaments/static/tournaments/css/style.css @@ -69,10 +69,10 @@ nav { /* Allow items to wrap onto multiple lines */ justify-content: flex-start; /* Align items to the start */ + gap: 6px; /* This adds spacing between items in all directions */ } nav a { - margin-right: 6px; color: #707070; padding: 8px 12px; background-color: #fae7ce; @@ -339,10 +339,13 @@ tr { /* For single player teams */ .player.single-player .semibold { - line-height: 1.4em; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; overflow: hidden; - position: relative; + line-height: 1.4em; text-overflow: ellipsis; + max-height: 2.8em; /* 2 lines × 1.4em line-height */ } /* For two player teams */ @@ -661,8 +664,9 @@ h-margin { /* Summons */ .table-row-3-colums-summons { - display: flex; + grid-template-columns: 50% 25% 25%; align-items: center; + display: grid; } .summons-left, @@ -909,3 +913,11 @@ h-margin { transform: translate(-50%, -50%); white-space: nowrap; } + +.even-row { + background-color: #ffffff; /* White */ +} + +.odd-row { + background-color: #e6f2ff; /* Light blue */ +} diff --git a/tournaments/static/tournaments/css/tournament_bracket.css b/tournaments/static/tournaments/css/tournament_bracket.css new file mode 100644 index 0000000..1be8f50 --- /dev/null +++ b/tournaments/static/tournaments/css/tournament_bracket.css @@ -0,0 +1,240 @@ +.round-logo img { + width: 50px; + height: auto; + display: block; + margin: 0 auto; + position: relative; + top: -100px; /* Increased negative value to move it higher up */ +} + +.round-logo img { + width: 100px; /* Adjust size as needed */ + height: auto; + display: block; + margin: 0 auto; +} +.butterfly-match.same-level::before { + display: none; +} + +/* Adjust styling for matches with single parent */ +.match-content.disabled { + visibility: hidden; +} + +.incoming-line.disabled, +.outgoing-line.disabled, +.butterfly-match:has(.match-content.disabled)::after, +.butterfly-match:has(.match-content.disabled)::before { + visibility: hidden; +} + +.butterfly-bracket { + display: flex; + gap: 40px; /* Increased to account for horizontal lines (20px on each side) */ + position: relative; + margin-bottom: 80px; +} + +.round-title { + position: absolute; + top: 0px; /* Adjust this value to position the title where you want it */ + left: 50%; /* Center horizontally */ + transform: translateX(-50%); /* Center it exactly */ + text-align: center; + font-weight: bold; + width: auto; /* Change from 100% to auto */ + padding: 5px 10px; + + white-space: nowrap; /* Prevent text from wrapping */ + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.round-name { + color: #707070; + font-size: 1.5em; + padding: 8px 12px; + white-space: nowrap; /* Prevent text wrapping */ + display: block; /* Ensure proper centering */ +} + +.round-format { + font-size: 0.9em; + color: #707070; + margin-top: -5px; /* Reduced from -10px to bring it closer */ + white-space: nowrap; /* Prevent text wrapping */ + display: block; /* Ensure proper centering */ +} + +.round-name.button { + border-radius: 16px; + width: 100%; + display: inline-block; + background-color: #fae7ce; +} + +.button:hover { + color: white; + background-color: #f39200; +} + +.matches-container { + position: relative; + width: 100%; + flex-grow: 1; +} + +.butterfly-round { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; /* Space between title and matches */ + position: relative; + width: var(--match-width); + flex-shrink: 0; + margin-top: 100px; /* Add padding to account for absolute positioned title */ +} + +.butterfly-match { + position: absolute; + width: 100%; + padding: 10px 10px; +} + +/* Horizontal line after match */ +.outgoing-line { + content: ""; + position: absolute; + left: 100%; /* Start from end of match cell */ + top: 50%; + width: 20px; + height: 2px; + background: orange; +} + +.butterfly-match::before { + content: ""; + position: absolute; + left: calc(0% - 20px); + width: 2px; + top: calc(50% - (var(--next-match-distance) / 2)); + height: calc(var(--next-match-distance) + 2px); + background: pink; +} + +/* Vertical line connecting pair of matches */ +.butterfly-match.reverse-bracket::before { + content: ""; + position: absolute; + left: calc(100% + 20px); + width: 2px; + top: calc(50% - (var(--next-match-distance) / 2)); + height: calc(var(--next-match-distance) + 2px); + background: red; +} + +/* Horizontal line to next round match */ +.incoming-line { + position: absolute; + left: -20px; + top: 50%; + width: 20px; + height: 2px; + background: blue; +} + +/* Horizontal line to next round match */ +.butterfly-match .outgoing-line-downward { + position: absolute; + right: -20px; + top: 50%; /* Start from middle of match */ + width: 2px; + height: var(--semi-final-distance); + background: black; +} + +/* Horizontal line to next round match */ +.butterfly-match .outgoing-line-upward { + position: absolute; + right: -20px; + bottom: 50%; + width: 2px; + height: var(--semi-final-distance); + background: black; +} + +/* Horizontal line to next round match */ +.butterfly-match.reverse-bracket .outgoing-line-downward { + position: absolute; + left: -20px; + top: 50%; /* Start from middle of match */ + width: 2px; + height: var(--semi-final-distance); + background: black; +} + +/* Horizontal line to next round match */ +.butterfly-match.reverse-bracket .outgoing-line-upward { + position: absolute; + left: -20px; + bottom: 50%; + width: 2px; + height: var(--semi-final-distance); + background: black; +} + +.butterfly-round:last-child .butterfly-match.reverse-bracket::before, +.butterfly-round:last-child .outgoing-line, +.butterfly-round:first-child .incoming-line { + display: none; +} + +.broadcast-mode .round-name, +.broadcast-mode .round-format { + padding: 0px; + color: #707070; +} + +.broadcast-mode .round-title { + padding: 8px 20px; /* Slightly more horizontal padding */ + background-color: white; + align-content: center; + border-radius: 24px; +} + +.butterfly-match::before, +.butterfly-match.reverse-bracket::before, +.incoming-line, +.outgoing-line, +.outgoing-line-upward, +.outgoing-line-downward { + background-color: #707070 !important; /* Bright yellow - change to your preferred color */ +} + +/* Broadcast mode styling for all lines */ +.broadcast-mode .butterfly-match::before, +.broadcast-mode .butterfly-match.reverse-bracket::before, +.broadcast-mode .incoming-line, +.broadcast-mode .outgoing-line, +.broadcast-mode .outgoing-line-upward, +.broadcast-mode .outgoing-line-downward { + background-color: black !important; /* Bright yellow - change to your preferred color */ +} + +.bubble.match-running { + position: relative; +} + +.bubble.match-running::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 20px; /* Height of the green indicator */ + background-color: #90ee90; /* Light green color */ + border-radius: 0 0 24px 24px; /* Match the bubble's bottom corners */ +} diff --git a/tournaments/static/tournaments/js/tournament_bracket.js b/tournaments/static/tournaments/js/tournament_bracket.js new file mode 100644 index 0000000..3f610f1 --- /dev/null +++ b/tournaments/static/tournaments/js/tournament_bracket.js @@ -0,0 +1,424 @@ +function renderBracket(options) { + const bracket = document.getElementById("bracket"); + const matchTemplates = document.getElementById("match-templates").children; + const rounds = []; + const matchPositions = []; + const matchDisabled = []; + const doubleButterflyMode = options.doubleButterflyMode; + const displayLoserFinal = options.displayLoserFinal; + const tournamentId = options.tournamentId; + const isBroadcast = options.isBroadcast; + // Group matches by round + Array.from(matchTemplates).forEach((template) => { + const roundIndex = parseInt(template.dataset.matchRound); + if (!rounds[roundIndex]) { + rounds[roundIndex] = []; + } + rounds[roundIndex].push(template); + }); + + // First create a test match to get natural height + const firstMatch = document.createElement("div"); + firstMatch.className = "butterfly-match"; + firstMatch.innerHTML = `
Le lien d'activation est invalide ou a expiré.
+ + +Votre compte a été activé et vous êtes maintenant connecté.
+ Aller à la page d'accueil +
+