post merge bracket

bracket-feature
Raz 8 months ago
commit a6e3c0292d
  1. 3
      api/utils.py
  2. 131
      tournaments/models/match.py
  3. 8
      tournaments/models/team_registration.py
  4. 3
      tournaments/models/team_score.py
  5. 30
      tournaments/models/tournament.py
  6. 375
      tournaments/services/email_service.py
  7. 197
      tournaments/signals.py
  8. 29
      tournaments/static/tournaments/css/style.css
  9. 25
      tournaments/templates/tournaments/broadcast/broadcasted_match.html
  10. 24
      tournaments/templates/tournaments/match_cell.html
  11. 2
      tournaments/templates/tournaments/navigation_tournament.html
  12. 16
      tournaments/templates/tournaments/player_row.html
  13. 40
      tournaments/templates/tournaments/prog.html
  14. 14
      tournaments/templates/tournaments/team_stats.html
  15. 1
      tournaments/urls.py
  16. 30
      tournaments/views.py

@ -7,6 +7,7 @@ def is_valid_email(email):
def check_version_smaller_than_1_1_12(version_str): def check_version_smaller_than_1_1_12(version_str):
# Remove the parentheses part if it exists, example of version: 1.1.12 (2) # Remove the parentheses part if it exists, example of version: 1.1.12 (2)
version_str = version_str.split()[0] version_str = version_str.split()[0]
if version_str:
# Split version into components # Split version into components
version_parts = [int(x) for x in version_str.split('.')] version_parts = [int(x) for x in version_str.split('.')]
@ -14,3 +15,5 @@ def check_version_smaller_than_1_1_12(version_str):
# Compare version components # Compare version components
return version_parts < target_parts return version_parts < target_parts
else:
return False

@ -85,12 +85,26 @@ class Match(models.Model):
previous_index = self.round.index + 1 previous_index = self.round.index + 1
# Check if the next index is within the bounds of the rounds array # 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 = self.round.parent).first()
return self.round.tournament.round_set.filter(index=previous_index, parent = None).first()
# Return None or an appropriate value if the index is out of bounds # Return None or an appropriate value if the index is out of bounds
return None 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): def precedent_match(self, top):
previous_round = self.get_previous_round() previous_round = self.get_previous_round()
#print(previous_round) #print(previous_round)
@ -103,6 +117,17 @@ class Match(models.Model):
match = matches.filter(index=match_index).first() match = matches.filter(index=match_index).first()
return match 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): def computed_name(self):
if self.round and self.round.parent is None: if self.round and self.round.parent is None:
return self.backup_name() return self.backup_name()
@ -134,7 +159,7 @@ class Match(models.Model):
is_winner = False is_winner = False
scores = [] scores = []
walk_out = None 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 return team
def is_ready(self): def is_ready(self):
@ -150,27 +175,35 @@ class Match(models.Model):
team_scores = list(self.team_scores.all()) team_scores = list(self.team_scores.all())
previous_top_match = self.precedent_match(True) previous_top_match = self.precedent_match(True)
previous_bottom_match = self.precedent_match(False) 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 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 return teams
if (self.group_stage): if (self.group_stage):
return teams names = ["Équipe de poule", '']
if self.round and self.round.parent:
print("self.round.parent.index", self.round.parent.index)
print("self.round.parent.parent", self.round.parent.parent)
if self.round.parent.index == 1 and self.round.parent.parent is None:
names = ["Perdant Demi #1", '']
team = self.default_live_team(names) team = self.default_live_team(names)
teams.append(team) teams.append(team)
names = ["Perdant Demi #2", ''] names = ["Équipe de poule", '']
team = self.default_live_team(names) team = self.default_live_team(names)
teams.append(team) teams.append(team)
return teams return teams
else: elif self.round and self.round.parent:
return teams if loser_top_match:
names = [f"Perdant {loser_top_match.computed_name()}", '']
# No team scores at all 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)
if previous_top_match: if previous_top_match:
names = [f"Gagnant {previous_top_match.computed_name()}", ''] names = [f"Gagnant {previous_top_match.computed_name()}", '']
team = self.default_live_team(names) team = self.default_live_team(names)
@ -179,23 +212,26 @@ class Match(models.Model):
names = [f"Gagnant {previous_bottom_match.computed_name()}", ''] names = [f"Gagnant {previous_bottom_match.computed_name()}", '']
team = self.default_live_team(names) team = self.default_live_team(names)
teams.append(team) teams.append(team)
if len(teams) == 0:
names = ['', '']
team = self.default_live_team(names)
teams.append(team)
teams.append(team)
return teams
elif len(team_scores) == 1: elif len(team_scores) == 1:
# Only one team score, handle missing one # Only one team score, handle missing one
existing_team = team_scores[0].live_team(self) existing_team = team_scores[0].live_team(self)
if self.round and self.round.parent: if (self.group_stage):
teams.append(existing_team)
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) teams.append(existing_team)
elif (self.group_stage): elif loser_bottom_match:
names = [f"Perdant {loser_bottom_match.computed_name()}", '']
team = self.default_live_team(names)
teams.append(existing_team) teams.append(existing_team)
else: teams.append(team)
if previous_top_match and previous_top_match.disabled == False and previous_top_match.end_date is None: 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()}", ''] names = [f"Gagnant {previous_top_match.computed_name()}", '']
team = self.default_live_team(names) team = self.default_live_team(names)
teams.append(team) teams.append(team)
@ -205,15 +241,22 @@ class Match(models.Model):
team = self.default_live_team(names) team = self.default_live_team(names)
teams.append(existing_team) teams.append(existing_team)
teams.append(team) teams.append(team)
else: 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):
teams.append(existing_team) match_index_within_round = self.index - (int(2 ** self.round.index) - 1)
names = ["Qualifié Entrant", ''] names = ["Qualifié", '']
team = self.default_live_team(names) team = self.default_live_team(names)
if match_index_within_round < int(2 ** self.round.index) / 2:
teams.append(existing_team)
teams.append(team) teams.append(team)
else: else:
teams.append(team)
teams.append(existing_team)
elif len(team_scores) == 2:
# Both team scores present # Both team scores present
teams.extend([team_score.live_team(self) for team_score in team_scores]) 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 return teams
@ -346,7 +389,7 @@ class Match(models.Model):
ended = self.end_date is not None ended = self.end_date is not None
live_format = "Format " + FederalMatchCategory(self.format).format_label_short 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.disabled) 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(): for team in self.live_teams():
livematch.add_team(team) livematch.add_team(team)
@ -376,7 +419,7 @@ class Match(models.Model):
# return sort_score # return sort_score
class Team: 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}") # print(f"image = {image}, names= {names}, scores ={scores}, weight={weight}, win={is_winner}")
self.id = str(id) self.id = str(id)
self.image = image self.image = image
@ -384,7 +427,11 @@ class Team:
self.scores = scores self.scores = scores
self.weight = weight self.weight = weight
self.is_winner = is_winner 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): def to_dict(self):
return { return {
@ -394,10 +441,12 @@ class Team:
"weight": self.weight, "weight": self.weight,
"is_winner": self.is_winner, "is_winner": self.is_winner,
"walk_out": self.walk_out, "walk_out": self.walk_out,
"is_walk_out": self.is_walk_out(),
"is_lucky_loser": self.is_lucky_loser
} }
class LiveMatch: class LiveMatch:
def __init__(self, title, date, time_indication, court, started, ended, group_stage_name, format, disabled): def __init__(self, title, date, time_indication, court, started, ended, group_stage_name, format, start_date, court_index, disabled):
self.title = title self.title = title
self.date = date self.date = date
self.teams = [] self.teams = []
@ -409,10 +458,12 @@ class LiveMatch:
self.group_stage_name = group_stage_name self.group_stage_name = group_stage_name
self.format = format self.format = format
self.disabled = disabled self.disabled = disabled
self.start_date = start_date
self.court_index = court_index
def add_team(self, team): def add_team(self, team):
self.teams.append(team) self.teams.append(team)
if team.walk_out is True: if team.is_walk_out() is True:
self.has_walk_out = True self.has_walk_out = True
def to_dict(self): def to_dict(self):
@ -427,12 +478,14 @@ class LiveMatch:
"has_walk_out": self.has_walk_out, "has_walk_out": self.has_walk_out,
"group_stage_name": self.group_stage_name, "group_stage_name": self.group_stage_name,
"format": self.format, "format": self.format,
"disabled": self.disabled "disabled": self.disabled,
"start_date": self.start_date,
"court_index": self.court_index
} }
def show_time_indication(self): def show_time_indication(self):
for team in self.teams: 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 False
return True return True

@ -225,7 +225,7 @@ class TeamRegistration(models.Model):
if team_rank > self.final_ranking: if team_rank > self.final_ranking:
sign = "+" sign = "+"
if team_rank == self.final_ranking: if team_rank == self.final_ranking:
sign = "" sign = "+"
return f" ({sign}"+f"{abs(self.final_ranking - team_rank)})" return f" ({sign}"+f"{abs(self.final_ranking - team_rank)})"
def get_points_earned(self): def get_points_earned(self):
@ -257,3 +257,9 @@ class TeamRegistration(models.Model):
# ratio = (wins / total_matches) * 100 # ratio = (wins / total_matches) * 100
return f"{wins}/{total_matches}" return f"{wins}/{total_matches}"
return None return None
def has_registered_online(self):
for p in self.playerregistration_set.all():
if p.registered_online:
return True
return False

@ -98,5 +98,6 @@ class TeamScore(models.Model):
scores = self.parsed_scores() scores = self.parsed_scores()
walk_out = self.walk_out walk_out = self.walk_out
from .match import Team # Import Team only when needed 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 return team

@ -328,16 +328,12 @@ class Tournament(models.Model):
"""Returns the total number of spots in all group stages.""" """Returns the total number of spots in all group stages."""
return sum(gs.size for gs in self.groupstage_set.all()) 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):
""" complete_teams, wildcard_bracket, wildcard_group_stage, waiting_teams = self.computed_teams(un_walk_out_team)
Get sorted list of teams for the tournament. all_teams = self.sort_teams(include_waiting_list, complete_teams, wildcard_bracket, wildcard_group_stage, waiting_teams)
return all_teams
Args:
include_waiting_list (bool): Whether to include teams in waiting list
Returns: def computed_teams(self, un_walk_out_team=None):
list: List of TeamItem objects sorted according to tournament rules
"""
# Initialize team categories # Initialize team categories
complete_teams = [] complete_teams = []
wildcard_bracket = [] wildcard_bracket = []
@ -348,7 +344,10 @@ class Tournament(models.Model):
closed_date = self.closed_registration_date closed_date = self.closed_registration_date
# Process each team registration # 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(): if team_reg.out_of_tournament():
continue continue
@ -379,8 +378,10 @@ class Tournament(models.Model):
complete_teams.append(team) complete_teams.append(team)
else: else:
waiting_teams.append(team) 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 # Initialize group stage spots
group_stage_spots = self.group_stage_spots() group_stage_spots = self.group_stage_spots()
bracket_seeds = self.team_count - group_stage_spots - len(wildcard_bracket) bracket_seeds = self.team_count - group_stage_spots - len(wildcard_bracket)
@ -1221,15 +1222,12 @@ class Tournament(models.Model):
def min_player_rank(self): def min_player_rank(self):
return FederalLevelCategory.min_player_rank(self.federal_level_category, self.federal_category, self.federal_age_category) return FederalLevelCategory.min_player_rank(self.federal_level_category, self.federal_category, self.federal_age_category)
def first_waiting_list_team(self): def first_waiting_list_team(self, teams):
teams = self.teams(True)
if len(teams)<=self.team_count: if len(teams)<=self.team_count:
return None return None
waiting_teams = [team for team in teams if team.stage == "Attente"] 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 waiting_teams[0].team_registration
return None
def broadcasted_prog(self): def broadcasted_prog(self):
# Get matches from broadcasted_matches_and_group_stages # Get matches from broadcasted_matches_and_group_stages

@ -1,6 +1,32 @@
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from django.utils import timezone from django.utils import timezone
from django.urls import reverse 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: class TournamentEmailService:
@staticmethod @staticmethod
@ -36,22 +62,14 @@ class TournamentEmailService:
tournament_details_str, tournament_details_str,
waiting_list_position waiting_list_position
) )
TournamentEmailService._send_email(request.user.email, email_subject, email_body)
email = EmailMessage(
subject=email_subject,
body=TournamentEmailService._convert_newlines_to_html(email_body),
to=[request.user.email]
)
email.content_subtype = "html"
email.send()
@staticmethod @staticmethod
def _build_email_subject(tournament, tournament_details_str, waiting_list_position): def _build_email_subject(tournament, tournament_details_str, waiting_list_position):
if waiting_list_position >= 0: if waiting_list_position >= 0:
base_subject = "En liste d'attente du tournoi" base_subject = "Liste d'attente"
else: else:
base_subject = "Confirmation d'inscription au tournoi" base_subject = "Participation confirmé"
return TournamentEmailService.email_subject(tournament, base_subject) return TournamentEmailService.email_subject(tournament, base_subject)
@staticmethod @staticmethod
@ -86,103 +104,132 @@ class TournamentEmailService:
return "\n".join(body_parts) return "\n".join(body_parts)
@staticmethod @staticmethod
def send_unregistration_confirmation(captain, tournament, other_player): def _build_unregistration_email_body(tournament, captain, tournament_details_str, other_player):
tournament_details_str = tournament.build_tournament_details_str() body_parts = [
"Bonjour,\n\n",
email_subject = TournamentEmailService.email_subject(tournament, "Désistement du tournoi") f"Votre inscription au tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name} a été annulée"
email_body = TournamentEmailService._build_unregistration_email_body( ]
tournament,
captain,
tournament_details_str,
other_player
)
email = EmailMessage( if other_player is not None:
subject=email_subject, body_parts.append(
body=TournamentEmailService._convert_newlines_to_html(email_body), f"\n\nVous étiez inscrit avec {other_player.name()}, n'oubliez pas de prévenir votre partenaire."
to=[captain.email]
) )
email.content_subtype = "html" absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info"
email.send() link_text = "informations sur le tournoi"
absolute_url = f'<a href="{absolute_url}">{link_text}</a>'
if other_player.email is not None: body_parts.append(
email_body = TournamentEmailService._build_unregistration_email_body( f"\n\nVoir les {absolute_url}",
tournament,
other_player,
tournament_details_str,
captain
) )
email = EmailMessage( body_parts.extend([
subject=email_subject, "\n\nPour toute question, veuillez contacter votre juge-arbitre. "
body=TournamentEmailService._convert_newlines_to_html(email_body), "Si vous n'êtes pas à l'origine de cette désinscription, merci de le contacter rapidement.",
to=[other_player.email] 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."
])
email.content_subtype = "html" return "".join(body_parts)
email.send()
@staticmethod @staticmethod
def send_out_of_waiting_list_confirmation(captain, tournament, other_player): def _build_out_of_waiting_list_email_body(tournament, captain, tournament_details_str, other_player):
tournament_details_str = tournament.build_tournament_details_str() body_parts = [
email_subject = TournamentEmailService.email_subject(tournament, "Participation au tournoi") "Bonjour,\n\n",
email_body = TournamentEmailService._build_out_of_waiting_list_email_body( f"Suite au désistement d'une paire, vous êtes maintenant inscrit au tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name}"
tournament, ]
captain,
tournament_details_str, absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info"
other_player link_text = "accéder au tournoi"
absolute_url = f'<a href="{absolute_url}">{link_text}</a>'
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."
) )
email = EmailMessage( body_parts.append(
subject=email_subject, "\n\nSi vous n'êtes plus disponible pour participer à ce tournoi, cliquez sur ce lien ou contactez rapidement le juge-arbitre."
body=TournamentEmailService._convert_newlines_to_html(email_body), f"\n{absolute_url}"
to=[captain.email] "\nPour vous désinscrire en ligne vous devez avoir un compte Padel Club."
) )
email.content_subtype = "html" body_parts.extend([
email.send() 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."
])
if other_player.email is not None: return "".join(body_parts)
email_body = TournamentEmailService._build_out_of_waiting_list_email_body(
tournament,
other_player,
tournament_details_str,
captain
)
email = EmailMessage( @staticmethod
subject=email_subject, def _build_tournament_cancellation_email_body(tournament, player, tournament_details_str, other_player):
body=TournamentEmailService._convert_newlines_to_html(email_body), body_parts = [
to=[other_player.email] "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."
]
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."
) )
email.content_subtype = "html" body_parts.extend([
email.send() "\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 @staticmethod
def _build_unregistration_email_body(tournament, captain, tournament_details_str, other_player): def _build_in_tournament_email_body(tournament, captain, tournament_details_str, other_player):
body_parts = [ body_parts = [
"Bonjour,\n\n", "Bonjour,\n\n",
f"Votre inscription au tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name} a été annulée" 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'<a href="{absolute_url}">{link_text}</a>'
if other_player is not None: if other_player is not None:
body_parts.append( body_parts.append(
f"\n\nVous étiez inscrit avec {other_player.name()}, n'oubliez pas de prévenir votre partenaire." 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."
)
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_out_of_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 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" absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info"
link_text = "informations sur le tournoi" link_text = "informations sur le tournoi"
absolute_url = f'<a href="{absolute_url}">{link_text}</a>' absolute_url = f'<a href="{absolute_url}">{link_text}</a>'
body_parts.append(f"\n\nVoir les {absolute_url}")
if other_player is not None:
body_parts.append( body_parts.append(
f"\n\nVoir les {absolute_url}", f"\nVous étiez inscrit avec {other_player.name()}, n'oubliez pas de prévenir votre partenaire."
) )
body_parts.extend([ body_parts.extend([
"\n\nPour toute question, veuillez contacter votre juge-arbitre. " "\n\nPour toute question, veuillez contacter votre juge-arbitre : ",
"Si vous n'êtes pas à l'origine de cette désinscription, merci de le contacter rapidement.",
f"\n{tournament.event.creator.full_name()}\n{tournament.event.creator.email}", 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." "\n\nCeci est un e-mail automatique, veuillez ne pas y répondre."
]) ])
@ -190,70 +237,202 @@ class TournamentEmailService:
return "".join(body_parts) return "".join(body_parts)
@staticmethod @staticmethod
def _build_out_of_waiting_list_email_body(tournament, captain, tournament_details_str, other_player): def _build_walk_out_email_body(tournament, captain, tournament_details_str, other_player):
body_parts = [ body_parts = [
"Bonjour,\n\n", "Bonjour,\n\n",
f"Suite au désistement d'une paire, vous êtes maintenant inscrit au tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name}" 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" absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info"
link_text = "accéder au tournoi" link_text = "accéder au tournoi"
absolute_url = f'<a href="{absolute_url}">{link_text}</a>' absolute_url = f'<a href="{absolute_url}">{link_text}</a>'
body_parts.append(f"\n\nVoir les {absolute_url}")
if other_player is not None: if other_player is not None:
body_parts.append( body_parts.append(
f"\nVoici le partenaire indiqué dans l'inscription : {other_player.name()}, n'oubliez pas de le prévenir." 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'<a href="{absolute_url}">{link_text}</a>'
body_parts.append(f"\n\nVoir les {absolute_url}")
if other_player is not None:
body_parts.append( 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"\nVous étiez inscrit avec {other_player.name()}, n'oubliez pas de prévenir votre partenaire."
f"\n{absolute_url}"
"\nPour vous désinscrire en ligne vous devez avoir un compte Padel Club."
) )
body_parts.extend([ body_parts.extend([
f"\n\n{tournament.event.creator.full_name()}\n{tournament.event.creator.email}", "\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." "\n\nCeci est un e-mail automatique, veuillez ne pas y répondre."
]) ])
return "".join(body_parts) return "".join(body_parts)
@staticmethod @staticmethod
def send_tournament_cancellation_notification(player, tournament, other_player): def _build_unexpected_out_of_tournament_email_body(tournament, captain, tournament_details_str, other_player):
tournament_details_str = tournament.build_tournament_details_str() body_parts = [
email_subject = TournamentEmailService.email_subject(tournament, "Annulation du tournoi") "Bonjour,\n\n",
email_body = TournamentEmailService._build_tournament_cancellation_email_body( 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}"
tournament, ]
player,
tournament_details_str,
other_player
)
email = EmailMessage( absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info"
subject=email_subject, link_text = "informations sur le tournoi"
body=TournamentEmailService._convert_newlines_to_html(email_body), absolute_url = f'<a href="{absolute_url}">{link_text}</a>'
to=[player.email]
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."
) )
email.content_subtype = "html" body_parts.extend([
email.send() "\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 @staticmethod
def _build_tournament_cancellation_email_body(tournament, player, tournament_details_str, other_player): def _build_out_of_walkout_waiting_list_email_body(tournament, captain, tournament_details_str, other_player):
body_parts = [ body_parts = [
"Bonjour,\n\n", "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"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'<a href="{absolute_url}">{link_text}</a>'
body_parts.append(f"\n\nVoir les {absolute_url}")
if other_player is not None: if other_player is not None:
body_parts.append( body_parts.append(
f"\nVous étiez inscrit avec {other_player.name()}, n'oubliez pas de prévenir votre partenaire." f"\nVous étiez inscrit avec {other_player.name()}, n'oubliez pas de prévenir votre partenaire."
) )
body_parts.extend([ 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}", 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." "\n\nCeci est un e-mail automatique, veuillez ne pas y répondre."
]) ])
return "".join(body_parts) 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)

@ -1,18 +1,19 @@
import random import random
import string 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.dispatch import receiver
from django.conf import settings from django.conf import settings
from tournaments.models.tournament import Tournament from tournaments.models.tournament import Tournament
from tournaments.models.unregistered_player import UnregisteredPlayer from tournaments.models.unregistered_player import UnregisteredPlayer
from django.utils import timezone 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 import requests
from tournaments.services.email_service import TournamentEmailService from tournaments.services.email_service import TournamentEmailService, TeamEmailType
from tournaments.models import PlayerDataSource from tournaments.models import PlayerDataSource
from shared.discord import send_discord_log_message, send_discord_failed_calls_message from shared.discord import send_discord_log_message, send_discord_failed_calls_message
from datetime import datetime
def generate_unique_code(): def generate_unique_code():
characters = string.ascii_lowercase + string.digits characters = string.ascii_lowercase + string.digits
@ -48,95 +49,143 @@ def notify_object_creation_on_discord(created, instance):
else: else:
send_discord_log_message(message) send_discord_log_message(message)
def notify_team(team, tournament, message_type):
# def send_discord_message(webhook_url, content): #print(team, message_type)
# data = { if tournament.enable_online_registration is False:
# "content": content return
# } if team.has_registered_online() is False:
# requests.post(webhook_url, json=data) return
# # if response.status_code != 204: if tournament.should_be_over():
# # raise ValueError( return
# # f'Error sending message to Discord webhook: {response.status_code}, {response.text}' if tournament.supposedly_in_progress():
# # )
@receiver(pre_delete, sender=TeamRegistration)
def unregister_team(sender, instance, **kwargs):
team_registration = instance
tournament = instance.tournament
if tournament.is_deleted is True:
return return
# Create unregistered player records and track captain/other player
captain = None captain = None
other_player = 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 captain = player
else: else:
other_player = player other_player = player
# Send unregistration confirmation if captain:
if captain and captain.registered_online and captain.email: TournamentEmailService.notify(captain, other_player, tournament, message_type)
TournamentEmailService.send_unregistration_confirmation( if not captain.registered_online or not captain.email:
captain, TournamentEmailService.notify(other_player, captain, tournament, message_type)
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
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: else:
waiting_other_player = player # 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)
if waiting_captain and waiting_captain.registered_online and waiting_captain.email: @receiver(pre_delete, sender=TeamRegistration)
TournamentEmailService.send_out_of_waiting_list_confirmation( def unregister_team(sender, instance, **kwargs):
waiting_captain, if instance.tournament.is_deleted:
tournament, return
waiting_other_player if instance.tournament.enable_online_registration is False:
) return
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) @receiver(post_save, sender=Tournament)
def notify_players_of_tournament_cancellation(sender, instance, **kwargs): 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 return
# Get all team registrations for team_registration in instance.teamregistration_set.all():
team_registrations = tournament.teamregistration_set.all() notify_team(team_registration, instance, TeamEmailType.TOURNAMENT_CANCELED)
for team_registration in team_registrations: @receiver(pre_save, sender=Tournament)
captain = None def check_waiting_list(sender, instance, **kwargs):
other_player = None if instance.id is None:
return
# Get players who registered online and have email if instance.enable_online_registration is False:
for player in team_registration.playerregistration_set.all(): return
print(player, player.registered_online)
if player.captain:
captain = player
else:
other_player = player
# Send email to captain try:
if captain and captain.registered_online and captain.email: previous_state = Tournament.objects.get(id=instance.id)
TournamentEmailService.send_tournament_cancellation_notification( except Tournament.DoesNotExist:
captain, previous_state = None
tournament,
other_player
)
# Send email to other player if they exist and registered online if previous_state is None:
if other_player and other_player.registered_online and other_player.email: return
TournamentEmailService.send_tournament_cancellation_notification(
other_player, teams_out_to_warn = []
tournament, teams_in_to_warn = []
captain
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)

@ -326,7 +326,6 @@ tr {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
/* Stack player names vertically */
} }
.scores { .scores {
@ -803,12 +802,6 @@ h-margin {
color: #707070; color: #707070;
} }
.single-line {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.group-stage-link { .group-stage-link {
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
@ -827,9 +820,17 @@ h-margin {
color: #f39200; color: #f39200;
} }
.top-border {
border-top: 1px solid #ccc;
}
.strikethrough {
text-decoration: line-through;
}
.status-container { .status-container {
margin: 0 -20px -20px -20px; /* Negative margin to counter the bubble padding, including bottom */ margin: 0 -20px -20px -20px; /* Negative margin to counter the bubble padding, including bottom */
padding: 0 20px 20px 20px; /* Add padding back to maintain text alignment, 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 */ border-radius: 0 0 24px 24px; /* Match the bubble's bottom corners */
} }
@ -837,6 +838,14 @@ h-margin {
background-color: #90ee90; /* Light green color */ background-color: #90ee90; /* Light green color */
} }
.bubble { .player {
position: relative; /* Add this */ 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;
} }

@ -16,15 +16,28 @@
<div> <div>
<div class="match-result"> <div class="match-result">
<div class="player bold"> <div class="player">
<!-- Show lucky loser or walkout status -->
<template x-if="match.teams[i-1].is_lucky_loser">
<div class="overlay-text right-label minor-info semibold">(LL)</div>
</template>
<template x-if="match.teams[i-1].walk_out === 1">
<div class="overlay-text right-label minor-info semibold">(WO)</div>
</template>
<template x-for="name in match.teams[i-1].names"> <template x-for="name in match.teams[i-1].names">
<div :class="match.teams[i-1].is_winner ? 'winner' : ''"> <div :class="{
<span x-text="name === '' ? '\u00A0' : name"></span> 'bold': true,
'strikethrough': match.teams[i-1].walk_out === 1,
'winner': match.teams[i-1].is_winner
}">
<span x-text="name === '' ? ' ' : name"></span>
</div> </div>
</template> </template>
</div> </div>
<div class="scores"> <div class="scores">
<template x-for="score in match.teams[i-1].scores"> <template x-for="score in match.teams[i-1].scores">
<template x-if="match.has_walk_out === false ">
<span class="score ws" <span class="score ws"
:class="{ :class="{
'w35px': score.tiebreak, 'w35px': score.tiebreak,
@ -38,13 +51,14 @@
</template> </template>
</span> </span>
</template> </template>
</template>
<span x-data="{ <span x-data="{
showWalkOut(match, team) { showWalkOut(match, team) {
let html = `` let html = ``
if (match.has_walk_out) { if (match.has_walk_out) {
html += `<span class='score bold w60px'>` html += `<span class='score bold w60px'>`
if (team.walk_out) html += `WO` if (team.walk_out == 0) html += `WO`
html += `</span>` html += `</span>`
} }
return html return html
@ -60,8 +74,7 @@
</div> </div>
</template> </template>
<div class="status-container" :class="{'running': !match.ended && match.started}">
<div class="status-container {% if not match.ended and match.started %}running{% endif %}">
<div class="top-margin flex-row"> <div class="top-margin flex-row">
<label class="left-label minor-info bold"><span x-text="match.time_indication"></span></label> <label class="left-label minor-info bold"><span x-text="match.time_indication"></span></label>
<label class="right-label minor-info semibold"><span x-text="match.format"></span></label> <label class="right-label minor-info semibold"><span x-text="match.format"></span></label>

@ -16,10 +16,17 @@
<div class="match-result {% cycle 'bottom-border' '' %}"> <div class="match-result {% cycle 'bottom-border' '' %}">
<div class="player"> <div class="player">
{% if team.id %} {% if team.id %}
<a href="{% url 'team-details' tournament.id team.id %}" class="player-link"> <!-- Add this anchor tag --> <a href="{% url 'team-details' tournament.id team.id %}" class="player-link">
{% endif %} {% endif %}
{% if team.is_lucky_loser or team.walk_out == 1 %}
<div class="overlay-text">
{% if team.is_lucky_loser %}(LL){% elif team.walk_out == 1 %}(WO){% endif %}
</div>
{% endif %}
{% for name in team.names %} {% for name in team.names %}
<div class="semibold {% if team.is_winner %}winner{% endif %}"> <div class="semibold{% if team.walk_out == 1 %} strikethrough{% endif %}{% if team.is_winner %} winner{% endif %}">
{% if name|length > 0 %} {% if name|length > 0 %}
{{ name }} {{ name }}
{% else %} {% else %}
@ -32,7 +39,11 @@
{% endif %} {% endif %}
</div> </div>
{% if match.should_show_scores %} {% if match.has_walk_out %}
<span class="score ws w60px">
{% if team.is_walk_out %}WO{% endif %}
</span>
{% elif match.should_show_scores %}
<div class="scores"> <div class="scores">
{% for score in team.scores %} {% for score in team.scores %}
<span class="score ws {% if score.tiebreak %}w35px{% else %}w30px{% endif %}{% if team.is_winner %} winner{% endif %}"> <span class="score ws {% if score.tiebreak %}w35px{% else %}w30px{% endif %}{% if team.is_winner %} winner{% endif %}">
@ -43,10 +54,6 @@
</span> </span>
{% endfor %} {% endfor %}
</div> </div>
{% elif match.has_walk_out %}
<span class="score ws w60px">
{% if team.walk_out %}WO{% endif %}
</span>
{% elif not tournament.hide_weight and team.weight %} {% elif not tournament.hide_weight and team.weight %}
<span class="score ws numbers">{{ team.weight }}</span> <span class="score ws numbers">{{ team.weight }}</span>
{% endif %} {% endif %}
@ -55,7 +62,6 @@
{% endfor %} {% endfor %}
</div> </div>
<div class="status-container {% if not match.ended and match.started %}running{% endif %}"> <div class="status-container {% if not match.ended and match.started %}running{% endif %}">
<div class="flex-row top-margin"> <div class="flex-row top-margin">
<label class="left-label minor-info bold"> <label class="left-label minor-info bold">
@ -68,9 +74,7 @@
{{ match.format }} {{ match.format }}
{% endif %} {% endif %}
</label> </label>
<!-- <a href="" class="right-label">{{ match.court }}</a> -->
</div> </div>
</div> </div>
</div> </div>
</div> </div>

@ -1,7 +1,7 @@
<nav class="margin10"> <nav class="margin10">
<a href="{% url 'tournament-info' tournament.id %}" class="topmargin5 orange">Informations</a> <a href="{% url 'tournament-info' tournament.id %}" class="topmargin5 orange">Informations</a>
<a href="{% url 'tournament-bracket' tournament.id %}" class="topmargin5 orange">Tableau</a> <!-- <a href="{% url 'tournament-bracket' tournament.id %}" class="topmargin5 orange">Tableau</a> -->
{% if tournament.display_matches or tournament.display_group_stages %} {% if tournament.display_matches or tournament.display_group_stages %}
<a href="{% url 'tournament' tournament.id %}" class="topmargin5 orange">Matches</a> <a href="{% url 'tournament' tournament.id %}" class="topmargin5 orange">Matches</a>

@ -3,15 +3,13 @@
<label class="matchtitle">{{ player.name }}</label> <label class="matchtitle">{{ player.name }}</label>
<div> <div>
<div class="match-result bottom-border" style="padding-right: 10px;"> <div class="match-result" style="padding-right: 10px;">
<div class="player"> <div class="player">
<div class="single-line">
<strong>{{ player.clean_club_name }}</strong> <strong>{{ player.clean_club_name }}</strong>
</div> </div>
</div> </div>
</div>
<div class="match-result bottom-border"> <div class="match-result top-border">
<div class="player"> <div class="player">
<div class="semibold"> <div class="semibold">
<strong>Classement</strong> <strong>Classement</strong>
@ -22,7 +20,8 @@
</div> </div>
</div> </div>
<div class="match-result bottom-border"> {% if player.calculate_age %}
<div class="match-result top-border">
<div class="player"> <div class="player">
<div class="semibold"> <div class="semibold">
<strong>Age</strong> <strong>Age</strong>
@ -30,14 +29,15 @@
</div> </div>
<div class="scores"> <div class="scores">
<span class="score ws numbers"> <span class="score ws numbers">
{{ player.calculate_age|default:"?" }} ans {{ player.calculate_age }} ans
</span> </span>
</div> </div>
</div> </div>
{% endif %}
{% if player.points %} {% if player.points %}
<div class="match-result bottom-border"> <div class="match-result top-border">
<div class="player"> <div class="player">
<div class="semibold"> <div class="semibold">
<strong>Points</strong> <strong>Points</strong>
@ -50,7 +50,7 @@
{% endif %} {% endif %}
{% if player.tournament_played %} {% if player.tournament_played %}
<div class="match-result"> <div class="match-result top-border">
<div class="player"> <div class="player">
<div class="semibold"> <div class="semibold">
<strong>Tournois joués</strong> <strong>Tournois joués</strong>

@ -0,0 +1,40 @@
{% extends 'tournaments/base.html' %}
{% block head_title %}Matchs du {{ tournament.display_name }}{% endblock %}
{% block first_title %}{{ tournament.event.display_name }}{% endblock %}
{% block second_title %}{{ tournament.display_name }}{% endblock %}
{% if tournament.display_matches %}
{% block content %}
{% include 'tournaments/navigation_tournament.html' %}
{% if tournament.display_matches or tournament.display_group_stages %}
{% regroup match_groups.matches by start_date|date:"l d F Y" as matches_by_date %}
{% for date in matches_by_date %}
{% regroup date.list by start_date|date:"H:i" as matches_by_hour %}
{% for hour_group in matches_by_hour %}
<h1 class="club my-block topmargin20">{{ date.grouper }} {{ hour_group.grouper }}</h1>
{% regroup hour_group.list by court_index as matches_by_court %}
<div class="grid-x">
{% for court in matches_by_court|dictsort:"grouper" %}
{% for match_data in court.list %}
{% with match=match_data.match %}
{% include 'tournaments/match_cell.html' %}
{% endwith %}
{% endfor %}
{% endfor %}
</div>
{% endfor %}
{% endfor %}
{% endif %}
{% endblock %}
{% endif %}

@ -5,7 +5,7 @@
<div> <div>
{% with stats=team.get_statistics %} {% with stats=team.get_statistics %}
<div class="match-result bottom-border"> <div class="match-result">
<div class="player"> <div class="player">
<div class="semibold"> <div class="semibold">
<strong>Poids de la paire</strong> <strong>Poids de la paire</strong>
@ -16,7 +16,7 @@
</div> </div>
</div> </div>
<div class="match-result bottom-border"> <div class="match-result top-border">
<div class="player"> <div class="player">
<div class="semibold"> <div class="semibold">
<strong>Position initiale</strong> <strong>Position initiale</strong>
@ -29,7 +29,7 @@
{% if stats.final_ranking %} {% if stats.final_ranking %}
<div class="match-result bottom-border"> <div class="match-result top-border">
<div class="player"> <div class="player">
<div class="semibold"> <div class="semibold">
<strong>Classement final</strong> <strong>Classement final</strong>
@ -42,7 +42,7 @@
{% endif %} {% endif %}
{% if stats.points_earned %} {% if stats.points_earned %}
<div class="match-result bottom-border"> <div class="match-result top-border">
<div class="player"> <div class="player">
<div class="semibold"> <div class="semibold">
<strong>Points gagnés</strong> <strong>Points gagnés</strong>
@ -55,7 +55,7 @@
{% endif %} {% endif %}
{% if stats.initial_stage %} {% if stats.initial_stage %}
<div class="match-result bottom-border"> <div class="match-result top-border">
<div class="player"> <div class="player">
<div class="semibold"> <div class="semibold">
<strong>Départ</strong> <strong>Départ</strong>
@ -67,7 +67,7 @@
</div> </div>
{% endif %} {% endif %}
<div class="match-result bottom-border"> <div class="match-result top-border">
<div class="player"> <div class="player">
<div class="semibold"> <div class="semibold">
<strong>Matchs joués</strong> <strong>Matchs joués</strong>
@ -79,7 +79,7 @@
</div> </div>
{% if stats.victory_ratio %} {% if stats.victory_ratio %}
<div class="match-result"> <div class="match-result top-border">
<div class="player"> <div class="player">
<div class="semibold"> <div class="semibold">
<strong>Ratio victoires</strong> <strong>Ratio victoires</strong>

@ -18,6 +18,7 @@ urlpatterns = [
path('teams/', views.tournament_teams, name='tournament-teams'), path('teams/', views.tournament_teams, name='tournament-teams'),
path('info/', views.tournament_info, name='tournament-info'), path('info/', views.tournament_info, name='tournament-info'),
path('bracket/', views.tournament_bracket, name='tournament-bracket'), path('bracket/', views.tournament_bracket, name='tournament-bracket'),
path('prog/', views.tournament_prog, name='tournament-prog'),
path('summons/', views.tournament_summons, name='tournament-summons'), path('summons/', views.tournament_summons, name='tournament-summons'),
path('broadcast/summons/', views.tournament_broadcasted_summons, name='broadcasted-summons'), path('broadcast/summons/', views.tournament_broadcasted_summons, name='broadcasted-summons'),
path('summons/json/', views.tournament_summons_json, name='tournament-summons-json'), path('summons/json/', views.tournament_summons_json, name='tournament-summons-json'),

@ -271,8 +271,8 @@ def tournament_teams(request, tournament_id):
tournament = get_object_or_404(Tournament, pk=tournament_id) tournament = get_object_or_404(Tournament, pk=tournament_id)
teams = tournament.teams(True) teams = tournament.teams(True)
selected_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'] waiting_teams = [team for team in teams if team.stage == "Attente"]
return render(request, 'tournaments/teams.html', { return render(request, 'tournaments/teams.html', {
'tournament': tournament, 'tournament': tournament,
@ -1029,6 +1029,32 @@ def tournament_bracket(request, tournament_id):
return render(request, 'tournaments/tournament_bracket.html', context) return render(request, 'tournaments/tournament_bracket.html', context)
def tournament_prog(request, tournament_id):
tournament = get_object_or_404(Tournament, id=tournament_id)
# Get matches from all_groups
match_groups = tournament.all_groups(broadcasted=False)
# Flatten matches and add necessary attributes
all_matches = []
for group in match_groups:
for match in group.matches:
if match.start_date: # Only include matches with start dates
all_matches.append({
'start_date': match.start_date,
'court_index': match.court_index,
# Add other match attributes needed for match_cell.html
'match': match
})
# Sort matches by date and court
all_matches.sort(key=lambda x: (x['start_date'], x['court_index'] or 999))
context = {
'tournament': tournament,
'match_groups': {'matches': all_matches}
}
return render(request, 'tournaments/prog.html', context)
class UserListExportView(LoginRequiredMixin, View): class UserListExportView(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):

Loading…
Cancel
Save