post merge bracket

bracket-feature
Raz 8 months ago
commit a6e3c0292d
  1. 13
      api/utils.py
  2. 151
      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. 387
      tournaments/services/email_service.py
  7. 203
      tournaments/signals.py
  8. 29
      tournaments/static/tournaments/css/style.css
  9. 25
      tournaments/templates/tournaments/broadcast/broadcasted_match.html
  10. 48
      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,10 +7,13 @@ def is_valid_email(email):
def check_version_smaller_than_1_1_12(version_str):
# Remove the parentheses part if it exists, example of version: 1.1.12 (2)
version_str = version_str.split()[0]
if version_str:
# Split version into components
version_parts = [int(x) for x in version_str.split('.')]
target_parts = [1, 1, 12]
# Split version into components
version_parts = [int(x) for x in version_str.split('.')]
target_parts = [1, 1, 12]
# Compare version components
return version_parts < target_parts
# Compare version components
return version_parts < target_parts
else:
return False

@ -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,52 +175,63 @@ 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)
if len(team_scores) == 0:
if (self.round and self.round.tournament.round_set.count() == self.round.index -1):
return teams
if (self.group_stage):
return teams
loser_top_match = self.loser_precedent_match(True)
loser_bottom_match = self.loser_precedent_match(False)
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)
teams.append(team)
names = ["Perdant Demi #2", '']
team = self.default_live_team(names)
teams.append(team)
return teams
else:
return teams
# No team scores at all
if previous_top_match:
names = [f"Gagnant {previous_top_match.computed_name()}", '']
if len(team_scores) == 0:
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)
if previous_bottom_match:
names = [f"Gagnant {previous_bottom_match.computed_name()}", '']
names = ["Qualifié", '']
team = self.default_live_team(names)
teams.append(team)
if len(teams) == 0:
names = ['', '']
return teams
if (self.group_stage):
names = ["Équipe de poule", '']
team = self.default_live_team(names)
teams.append(team)
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)
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 = ["É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 loser_bottom_match:
names = [f"Perdant {loser_bottom_match.computed_name()}", '']
team = self.default_live_team(names)
teams.append(existing_team)
teams.append(team)
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)
@ -205,15 +241,22 @@ class Match(models.Model):
team = self.default_live_team(names)
teams.append(existing_team)
teams.append(team)
else:
teams.append(existing_team)
names = ["Qualifié Entrant", '']
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)
teams.append(team)
else:
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
@ -346,7 +389,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, 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():
livematch.add_team(team)
@ -376,7 +419,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
@ -384,7 +427,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 {
@ -394,10 +441,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, disabled):
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 = []
@ -409,10 +458,12 @@ class LiveMatch:
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):
@ -427,12 +478,14 @@ class LiveMatch:
"has_walk_out": self.has_walk_out,
"group_stage_name": self.group_stage_name,
"format": self.format,
"disabled": self.disabled
"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

@ -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

@ -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

@ -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)
@ -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

@ -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'<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."
)
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'<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(
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'<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(
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(
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'<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(
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'<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(
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)

@ -1,18 +1,19 @@
import random
import string
from django.db.models.signals import post_save, pre_delete, post_delete
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete
from django.dispatch import receiver
from django.conf import settings
from tournaments.models.tournament import Tournament
from tournaments.models.unregistered_player import UnregisteredPlayer
from django.utils import timezone
from .models import Club, FailedApiCall, CustomUser, Log, TeamRegistration, PlayerRegistration, UnregisteredTeam, UnregisteredPlayer
from .models import Club, FailedApiCall, CustomUser, Log, TeamRegistration, PlayerRegistration, UnregisteredTeam, UnregisteredPlayer, TeamSortingType
import requests
from tournaments.services.email_service import TournamentEmailService
from tournaments.services.email_service import TournamentEmailService, TeamEmailType
from tournaments.models import PlayerDataSource
from shared.discord import send_discord_log_message, send_discord_failed_calls_message
from datetime import datetime
def generate_unique_code():
characters = string.ascii_lowercase + string.digits
@ -48,95 +49,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)

@ -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;
@ -827,9 +820,17 @@ h-margin {
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: 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 */
}
@ -837,6 +838,14 @@ h-margin {
background-color: #90ee90; /* Light green color */
}
.bubble {
position: relative; /* Add this */
.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;
}

@ -16,15 +16,28 @@
<div>
<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">
<div :class="match.teams[i-1].is_winner ? 'winner' : ''">
<span x-text="name === '' ? '\u00A0' : name"></span>
<div :class="{
'bold': true,
'strikethrough': match.teams[i-1].walk_out === 1,
'winner': match.teams[i-1].is_winner
}">
<span x-text="name === '' ? ' ' : name"></span>
</div>
</template>
</div>
<div class="scores">
<template x-for="score in match.teams[i-1].scores">
<template x-if="match.has_walk_out === false ">
<span class="score ws"
:class="{
'w35px': score.tiebreak,
@ -37,6 +50,7 @@
<sup x-text="score.tiebreak"></sup>
</template>
</span>
</template>
</template>
<span x-data="{
@ -44,7 +58,7 @@
let html = ``
if (match.has_walk_out) {
html += `<span class='score bold w60px'>`
if (team.walk_out) html += `WO`
if (team.walk_out == 0) html += `WO`
html += `</span>`
}
return html
@ -60,8 +74,7 @@
</div>
</template>
<div class="status-container {% if not match.ended and match.started %}running{% endif %}">
<div class="status-container" :class="{'running': !match.ended && match.started}">
<div class="top-margin flex-row">
<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>

@ -16,10 +16,17 @@
<div class="match-result {% cycle 'bottom-border' '' %}">
<div class="player">
{% 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 %}
{% 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 %}
<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 %}
{{ name }}
{% else %}
@ -32,7 +39,11 @@
{% endif %}
</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">
{% for score in team.scores %}
<span class="score ws {% if score.tiebreak %}w35px{% else %}w30px{% endif %}{% if team.is_winner %} winner{% endif %}">
@ -43,10 +54,6 @@
</span>
{% endfor %}
</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 %}
<span class="score ws numbers">{{ team.weight }}</span>
{% endif %}
@ -55,22 +62,19 @@
{% endfor %}
</div>
<div class="status-container {% if not match.ended and match.started %}running{% endif %}">
<div class="flex-row top-margin">
<label class="left-label minor-info bold">
{% if match.show_time_indication %}
{{ match.time_indication }}
{% endif %}
</label>
<label class="right-label minor-info">
{% if not match.ended %}
{{ match.format }}
{% endif %}
</label>
<!-- <a href="" class="right-label">{{ match.court }}</a> -->
</div>
<div class="flex-row top-margin">
<label class="left-label minor-info bold">
{% if match.show_time_indication %}
{{ match.time_indication }}
{% endif %}
</label>
<label class="right-label minor-info">
{% if not match.ended %}
{{ match.format }}
{% endif %}
</label>
</div>
</div>
</div>
</div>

@ -1,7 +1,7 @@
<nav class="margin10">
<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 %}
<a href="{% url 'tournament' tournament.id %}" class="topmargin5 orange">Matches</a>

@ -3,15 +3,13 @@
<label class="matchtitle">{{ player.name }}</label>
<div>
<div class="match-result bottom-border" style="padding-right: 10px;">
<div class="match-result" style="padding-right: 10px;">
<div class="player">
<div class="single-line">
<strong>{{ player.clean_club_name }}</strong>
</div>
</div>
</div>
<div class="match-result bottom-border">
<div class="match-result top-border">
<div class="player">
<div class="semibold">
<strong>Classement</strong>
@ -22,7 +20,8 @@
</div>
</div>
<div class="match-result bottom-border">
{% if player.calculate_age %}
<div class="match-result top-border">
<div class="player">
<div class="semibold">
<strong>Age</strong>
@ -30,14 +29,15 @@
</div>
<div class="scores">
<span class="score ws numbers">
{{ player.calculate_age|default:"?" }} ans
{{ player.calculate_age }} ans
</span>
</div>
</div>
{% endif %}
{% if player.points %}
<div class="match-result bottom-border">
<div class="match-result top-border">
<div class="player">
<div class="semibold">
<strong>Points</strong>
@ -50,7 +50,7 @@
{% endif %}
{% if player.tournament_played %}
<div class="match-result">
<div class="match-result top-border">
<div class="player">
<div class="semibold">
<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>
{% with stats=team.get_statistics %}
<div class="match-result bottom-border">
<div class="match-result">
<div class="player">
<div class="semibold">
<strong>Poids de la paire</strong>
@ -16,7 +16,7 @@
</div>
</div>
<div class="match-result bottom-border">
<div class="match-result top-border">
<div class="player">
<div class="semibold">
<strong>Position initiale</strong>
@ -29,7 +29,7 @@
{% if stats.final_ranking %}
<div class="match-result bottom-border">
<div class="match-result top-border">
<div class="player">
<div class="semibold">
<strong>Classement final</strong>
@ -42,7 +42,7 @@
{% endif %}
{% if stats.points_earned %}
<div class="match-result bottom-border">
<div class="match-result top-border">
<div class="player">
<div class="semibold">
<strong>Points gagnés</strong>
@ -55,7 +55,7 @@
{% endif %}
{% if stats.initial_stage %}
<div class="match-result bottom-border">
<div class="match-result top-border">
<div class="player">
<div class="semibold">
<strong>Départ</strong>
@ -67,7 +67,7 @@
</div>
{% endif %}
<div class="match-result bottom-border">
<div class="match-result top-border">
<div class="player">
<div class="semibold">
<strong>Matchs joués</strong>
@ -79,7 +79,7 @@
</div>
{% if stats.victory_ratio %}
<div class="match-result">
<div class="match-result top-border">
<div class="player">
<div class="semibold">
<strong>Ratio victoires</strong>

@ -18,6 +18,7 @@ urlpatterns = [
path('teams/', views.tournament_teams, name='tournament-teams'),
path('info/', views.tournament_info, name='tournament-info'),
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('broadcast/summons/', views.tournament_broadcasted_summons, name='broadcasted-summons'),
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)
teams = tournament.teams(True)
selected_teams = [team for team in teams if team.stage != 'Attente']
waiting_teams = [team for team in teams if team.stage == 'Attente']
selected_teams = [team for team in teams if team.stage != "Attente"]
waiting_teams = [team for team in teams if team.stage == "Attente"]
return render(request, 'tournaments/teams.html', {
'tournament': tournament,
@ -1029,6 +1029,32 @@ def tournament_bracket(request, tournament_id):
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):
def get(self, request, *args, **kwargs):

Loading…
Cancel
Save