sync
Laurent 8 months ago
commit 15231d9299
  1. 8
      api/serializers.py
  2. 2
      api/views.py
  3. 2
      tournaments/admin.py
  4. 8
      tournaments/forms.py
  5. 2
      tournaments/models/enums.py
  6. 127
      tournaments/models/match.py
  7. 4
      tournaments/models/round.py
  8. 24
      tournaments/models/team_registration.py
  9. 3
      tournaments/models/team_score.py
  10. 38
      tournaments/models/tournament.py
  11. 11
      tournaments/repositories.py
  12. 373
      tournaments/services/email_service.py
  13. 20
      tournaments/services/tournament_registration.py
  14. 2
      tournaments/services/tournament_unregistration.py
  15. 197
      tournaments/signals.py
  16. 37
      tournaments/static/tournaments/css/style.css
  17. 24
      tournaments/templates/register_tournament.html
  18. 57
      tournaments/templates/tournaments/bracket_match_cell.html
  19. 1
      tournaments/templates/tournaments/broadcast/broadcasted_auto.html
  20. 1
      tournaments/templates/tournaments/broadcast/broadcasted_auto_event.html
  21. 6
      tournaments/templates/tournaments/broadcast/broadcasted_group_stage.html
  22. 1
      tournaments/templates/tournaments/broadcast/broadcasted_group_stages.html
  23. 25
      tournaments/templates/tournaments/broadcast/broadcasted_match.html
  24. 4
      tournaments/templates/tournaments/group_stages.html
  25. 26
      tournaments/templates/tournaments/match_cell.html
  26. 2
      tournaments/templates/tournaments/matches.html
  27. 1
      tournaments/templates/tournaments/navigation_tournament.html
  28. 16
      tournaments/templates/tournaments/player_row.html
  29. 40
      tournaments/templates/tournaments/prog.html
  30. 6
      tournaments/templates/tournaments/rankings.html
  31. 5
      tournaments/templates/tournaments/summons.html
  32. 14
      tournaments/templates/tournaments/team_stats.html
  33. 6
      tournaments/templates/tournaments/teams.html
  34. 417
      tournaments/templates/tournaments/tournament_bracket.html
  35. 2
      tournaments/urls.py
  36. 4
      tournaments/utils/player_search.py
  37. 123
      tournaments/views.py

@ -3,12 +3,16 @@ from tournaments.models.court import Court
from tournaments.models import Club, LiveMatch, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, FailedApiCall, DateInterval, Log, DeviceToken, UnregisteredTeam, UnregisteredPlayer
from django.contrib.auth import password_validation
from django.utils.translation import gettext_lazy as _
from django.db.utils import IntegrityError
from django.conf import settings
# email
from django.template.loader import render_to_string
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes
from django.core.mail import EmailMessage
from django.contrib.sites.shortcuts import get_current_site
from api.tokens import account_activation_token
# from tournaments.models.data_access import DataAccess
@ -49,6 +53,9 @@ class UserSerializer(serializers.ModelSerializer):
if 'country' in validated_data:
country = validated_data['country']
if CustomUser.objects.filter(username__iexact=validated_data['username'].lower()):
raise IntegrityError("Le nom d'utilisateur existe déjà")
user = CustomUser.objects.create_user(
username=validated_data['username'],
last_update=validated_data.get('last_update'),
@ -75,6 +82,7 @@ class UserSerializer(serializers.ModelSerializer):
origin=UserOrigin.APP,
)
if not settings.DEBUG:
self.send_email(self.context['request'], user)
# RegistrationProfile.objects.filter(user=user).send_activation_email()

@ -80,7 +80,7 @@ class CustomAuthToken(APIView):
user = CustomUser.objects.get(email=email)
return user.username
except ObjectDoesNotExist:
return None # or handle the case where the user doesn't exist
return None
class Logout(APIView):
permission_classes = (IsAuthenticated,)

@ -51,7 +51,7 @@ class TournamentAdmin(SyncedObjectAdmin):
search_fields = ['id']
class TeamRegistrationAdmin(SyncedObjectAdmin):
list_display = ['player_names', 'group_stage_position', 'name', 'tournament', 'registration_date']
list_display = ['player_names', 'group_stage', 'name', 'tournament', 'registration_date']
list_filter = [SimpleTournamentListFilter]
search_fields = ['id']

@ -58,6 +58,14 @@ class SimpleCustomUserCreationForm(UserCreationForm):
'password2': 'Confirmer le mot de passe',
}
def clean_username(self):
username = self.cleaned_data.get('username')
if username:
username = username.lower()
if CustomUser.objects.filter(username__iexact=username).exists():
raise forms.ValidationError("Le nom d'utilisateur existe déjà")
return username
class CustomUserChangeForm(UserChangeForm):

@ -15,6 +15,8 @@ class FederalCategory(models.IntegerChoices):
@staticmethod
def female_in_male_assimilation_addition(rank: int) -> int:
if rank is None:
return 0
if 1 <= rank <= 10:
return 400
elif 11 <= rank <= 30:

@ -100,10 +100,21 @@ class Match(SideStoreModel):
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.rounds.count():
return self.round.tournament.rounds.filter(index=previous_index, parent = None).first()
return self.round.tournament.rounds.filter(index=previous_index, parent = self.round.parent).first()
# Return None or an appropriate value if the index is out of bounds
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):
@ -118,6 +129,17 @@ class Match(SideStoreModel):
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()
@ -149,7 +171,7 @@ class Match(SideStoreModel):
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):
@ -165,47 +187,89 @@ class Match(SideStoreModel):
team_scores = list(self.team_scores.all())
previous_top_match = self.precedent_match(True)
previous_bottom_match = self.precedent_match(False)
loser_top_match = self.loser_precedent_match(True)
loser_bottom_match = self.loser_precedent_match(False)
if len(team_scores) == 0:
if (self.round and self.round.tournament.rounds.count() == self.round.index -1):
if (self.round and self.round.parent is None and self.round.tournament.rounds.filter(parent__isnull=True, group_stage_loser_bracket=False).count() - 1 == self.round.index):
names = ["Qualifié", '']
team = self.default_live_team(names)
teams.append(team)
names = ["Qualifié", '']
team = self.default_live_team(names)
teams.append(team)
return teams
if (self.group_stage):
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
if self.round and self.round.parent:
return teams
# No team scores at all
elif self.round and self.round.parent:
if loser_top_match:
names = [f"Perdant {loser_top_match.computed_name()}", '']
team = self.default_live_team(names)
teams.append(team)
if loser_bottom_match:
names = [f"Perdant {loser_bottom_match.computed_name()}", '']
team = self.default_live_team(names)
teams.append(team)
elif self.round and self.round.parent is None:
if previous_top_match:
names = [f"Gagnants {previous_top_match.computed_name()}", '']
names = [f"Gagnant {previous_top_match.computed_name()}", '']
team = self.default_live_team(names)
teams.append(team)
if previous_bottom_match:
names = [f"Gagnants {previous_bottom_match.computed_name()}", '']
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:
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)
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)
else:
if previous_top_match and previous_top_match.disabled == False and previous_top_match.end_date is None:
names = [f"Gagnants {previous_top_match.computed_name()}", '']
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)
teams.append(existing_team)
elif previous_bottom_match:
names = [f"Gagnants {previous_bottom_match.computed_name()}", '']
names = [f"Gagnant {previous_bottom_match.computed_name()}", '']
team = self.default_live_team(names)
teams.append(existing_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):
match_index_within_round = self.index - (int(2 ** self.round.index) - 1)
names = ["Qualifié", '']
team = self.default_live_team(names)
if match_index_within_round < int(2 ** self.round.index) / 2:
teams.append(existing_team)
teams.append(team)
else:
teams.append(team)
teams.append(existing_team)
elif len(team_scores) == 2:
# Both team scores present
teams.extend([team_score.live_team(self) for team_score in team_scores])
else:
teams.extend([team_score.live_team(self) for team_score in team_scores if team_score.walk_out != 1])
return teams
@ -338,7 +402,7 @@ class Match(SideStoreModel):
ended = self.end_date is not None
live_format = "Format " + FederalMatchCategory(self.format).format_label_short
livematch = LiveMatch(title, date, time_indication, court, self.started(), ended, group_stage_name, live_format)
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)
@ -368,7 +432,7 @@ class Match(SideStoreModel):
# 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
@ -376,7 +440,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 {
@ -386,10 +454,12 @@ class Team:
"weight": self.weight,
"is_winner": self.is_winner,
"walk_out": self.walk_out,
"is_walk_out": self.is_walk_out(),
"is_lucky_loser": self.is_lucky_loser
}
class LiveMatch:
def __init__(self, title, date, time_indication, court, started, ended, group_stage_name, format):
def __init__(self, title, date, time_indication, court, started, ended, group_stage_name, format, start_date, court_index, disabled):
self.title = title
self.date = date
self.teams = []
@ -400,10 +470,13 @@ class LiveMatch:
self.has_walk_out = False
self.group_stage_name = group_stage_name
self.format = format
self.disabled = disabled
self.start_date = start_date
self.court_index = court_index
def add_team(self, team):
self.teams.append(team)
if team.walk_out is True:
if team.is_walk_out() is True:
self.has_walk_out = True
def to_dict(self):
@ -417,12 +490,14 @@ class LiveMatch:
"ended": self.ended,
"has_walk_out": self.has_walk_out,
"group_stage_name": self.group_stage_name,
"format": self.format
"format": self.format,
"disabled": self.disabled,
"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

@ -42,9 +42,9 @@ class Round(SideStoreModel):
if self.index == 0:
return "Finale"
elif self.index == 1:
return "Demi-Finales"
return "Demi"
elif self.index == 2:
return "Quarts de finale"
return "Quart"
else:
squared = 2 ** self.index
return f"{squared}ème"

@ -53,20 +53,28 @@ class TeamRegistration(SideStoreModel):
return self.tournament.id
def player_names_as_list(self):
return [pr.name() for pr in self.player_registrations.all()]
players = list(self.player_registrations.all())
if len(players) == 0:
return ['', '']
elif len(players) == 1:
return [players[0].name(), '']
else:
return [pr.name() for pr in players]
def team_names(self):
if self.name:
return [self.name]
return [self.name, ''] #add an empty line if it's a team name
else:
return self.player_names_as_list()
def shortened_team_names(self):
if self.name:
return [self.name]
return [self.name, ''] #add an empty line if it's a team name
else:
players = list(self.player_registrations.all())
if len(players) == 1:
if len(players) == 0:
return ['', '']
elif len(players) == 1:
return [players[0].shortened_name(), '']
else:
return [pr.shortened_name() for pr in players]
@ -239,7 +247,7 @@ class TeamRegistration(SideStoreModel):
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):
@ -271,3 +279,9 @@ class TeamRegistration(SideStoreModel):
# 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

@ -116,5 +116,6 @@ class TeamScore(SideStoreModel):
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

@ -168,7 +168,7 @@ class Tournament(BaseModel):
return self.start_date.astimezone(timezone)
def local_start_date_formatted(self):
return formats.date_format(self.local_start_date(), format='j F Y H:i')
return formats.date_format(self.local_start_date(), format='l j F Y H:i').capitalize()
def level(self):
if self.federal_level_category == 0:
@ -341,16 +341,12 @@ class Tournament(BaseModel):
"""Returns the total number of spots in all group stages."""
return sum(gs.size for gs in self.group_stages.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 = []
@ -361,7 +357,10 @@ class Tournament(BaseModel):
closed_date = self.closed_registration_date
# Process each team registration
for team_reg in self.team_registrations.all():
for db_team_reg in self.team_registrations.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
@ -392,8 +391,10 @@ class Tournament(BaseModel):
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)
@ -736,6 +737,10 @@ class Tournament(BaseModel):
matches = [m for m in self.all_matches(False) if m.start_date and m.end_date is None]
# print(f'first_unfinished_match > match len: {len(matches)}')
matches.sort(key=lambda m: m.start_date)
main_bracket_matches = [m for m in matches if m.round and m.round.parent is None]
if main_bracket_matches:
return main_bracket_matches[0]
if matches:
return matches[0]
else:
@ -1203,7 +1208,7 @@ class Tournament(BaseModel):
current_year += 1
user_age = current_year - int(birth_year)
print(user_age)
print("user_age", user_age)
# Check age category restrictions
if self.federal_age_category == FederalAgeCategory.A11_12 and user_age > 12:
@ -1236,15 +1241,12 @@ class Tournament(BaseModel):
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

@ -23,7 +23,7 @@ class TournamentRegistrationRepository:
is_captain = False
player_licence_id = player_data['licence_id']
if player_licence_id and stripped_license:
if player_licence_id.startswith(stripped_license):
if stripped_license.lower() in player_licence_id.lower():
is_captain = True
sex, rank, computed_rank = TournamentRegistrationRepository._compute_rank_and_sex(
@ -31,6 +31,7 @@ class TournamentRegistrationRepository:
player_data
)
print("create_player_registrations", player_data.get('last_name'), sex, rank, computed_rank)
data_source = None
if player_data.get('found_in_french_federation', False) == True:
data_source = PlayerDataSource.FRENCH_FEDERATION
@ -66,14 +67,18 @@ class TournamentRegistrationRepository:
@staticmethod
def _compute_rank_and_sex(tournament, player_data):
is_woman = player_data.get('is_woman', False)
rank = player_data.get('rank', 0)
rank = player_data.get('rank', None)
if rank is None:
computed_rank = 100000
else:
computed_rank = rank
sex = PlayerSexType.MALE
sex = PlayerSexType.MALE
if is_woman:
sex = PlayerSexType.FEMALE
if tournament.federal_category == FederalCategory.MEN:
computed_rank = str(int(computed_rank) +
FederalCategory.female_in_male_assimilation_addition(int(rank)))
print("_compute_rank_and_sex", sex, rank, computed_rank)
return sex, rank, computed_rank

@ -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ée"
return TournamentEmailService.email_subject(tournament, base_subject)
@staticmethod
@ -86,103 +104,132 @@ 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
)
def _build_unregistration_email_body(tournament, captain, tournament_details_str, other_player):
body_parts = [
"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"
]
email = EmailMessage(
subject=email_subject,
body=TournamentEmailService._convert_newlines_to_html(email_body),
to=[captain.email]
if other_player is not None:
body_parts.append(
f"\n\nVous étiez inscrit avec {other_player.name()}, n'oubliez pas de prévenir votre partenaire."
)
email.content_subtype = "html"
email.send()
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>'
if other_player.email is not None:
email_body = TournamentEmailService._build_unregistration_email_body(
tournament,
other_player,
tournament_details_str,
captain
body_parts.append(
f"\n\nVoir les {absolute_url}",
)
email = EmailMessage(
subject=email_subject,
body=TournamentEmailService._convert_newlines_to_html(email_body),
to=[other_player.email]
)
body_parts.extend([
"\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}",
"\n\nCeci est un e-mail automatique, veuillez ne pas y répondre."
])
email.content_subtype = "html"
email.send()
return "".join(body_parts)
@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
def _build_out_of_waiting_list_email_body(tournament, captain, tournament_details_str, other_player):
body_parts = [
"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}"
]
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."
)
email = EmailMessage(
subject=email_subject,
body=TournamentEmailService._convert_newlines_to_html(email_body),
to=[captain.email]
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."
])
if other_player.email is not None:
email_body = TournamentEmailService._build_out_of_waiting_list_email_body(
tournament,
other_player,
tournament_details_str,
captain
)
return "".join(body_parts)
email = EmailMessage(
subject=email_subject,
body=TournamentEmailService._convert_newlines_to_html(email_body),
to=[other_player.email]
@staticmethod
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."
]
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"
email.send()
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_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 = [
"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:
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"
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"\n\nVoir les {absolute_url}",
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. "
"Si vous n'êtes pas à l'origine de cette désinscription, merci de le contacter rapidement.",
"\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."
])
@ -190,61 +237,97 @@ class TournamentEmailService:
return "".join(body_parts)
@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 = [
"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"
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"\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(
"\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."
f"\nVous étiez inscrit avec {other_player.name()}, n'oubliez pas de prévenir votre partenaire."
)
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."
])
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_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}"
]
email = EmailMessage(
subject=email_subject,
body=TournamentEmailService._convert_newlines_to_html(email_body),
to=[player.email]
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."
)
email.content_subtype = "html"
email.send()
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_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 = [
"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:
body_parts.append(
f"\nVous étiez inscrit avec {other_player.name()}, n'oubliez pas de prévenir votre partenaire."
@ -257,3 +340,99 @@ class TournamentEmailService:
])
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)

@ -31,9 +31,18 @@ class TournamentRegistrationService:
if 'add_player' in self.request.POST:
self.handle_add_player()
if 'remove_player' in self.request.POST:
self.handle_remove_player()
elif 'register_team' in self.request.POST:
self.handle_team_registration()
def handle_remove_player(self):
team_registration = self.request.session.get('team_registration', [])
if team_registration: # Check if list is not empty
team_registration.pop() # Remove last element
self.request.session['team_registration'] = team_registration
self.context['current_players'] = team_registration
def handle_add_player(self):
if not self.context['add_player_form'].is_valid():
return
@ -106,6 +115,7 @@ class TournamentRegistrationService:
self.initialize_session_data()
def add_player_to_session(self, player_data):
print("add_player_to_session", player_data)
if not self.request.session.get('team_registration'):
self.request.session['team_registration'] = []
@ -216,6 +226,7 @@ class TournamentRegistrationService:
return False
def _handle_valid_names(self, player_data):
print("_handle_valid_names", player_data)
if player_data.get('rank') is None:
self._set_default_rank(player_data)
@ -224,8 +235,8 @@ class TournamentRegistrationService:
self.context['add_player_form'].first_tournament = False
def _handle_invalid_names(self, licence_id, player_data):
if not self.context['add_player_form'].first_tournament:
data, found = get_player_name_from_csv(self.tournament.federal_category, licence_id)
print("_handle_invalid_names get_player_name_from_csv", data, found)
if found and data:
self._update_player_data_from_csv(player_data, data)
player_check = self._player_check(player_data)
@ -235,6 +246,7 @@ class TournamentRegistrationService:
else:
return
else:
print("_handle_first_tournament_case")
self._handle_first_tournament_case(data)
def _set_default_rank(self, player_data):
@ -245,7 +257,7 @@ class TournamentRegistrationService:
self.request.session['is_woman'] = data['is_woman']
self.request.session.modified = True
player_data['rank'] = self.request.session.get('last_rank', 0)
player_data['rank'] = self.request.session.get('last_rank', None)
player_data['is_woman'] = self.request.session.get('is_woman', False)
def _update_user_license(self, licence_id):
@ -261,6 +273,7 @@ class TournamentRegistrationService:
self.context['add_player_form'].first_tournament = False
def _update_player_data_from_csv(self, player_data, csv_data):
print("_update_player_data_from_csv", player_data, csv_data)
player_data.update({
'first_name': csv_data['first_name'],
'last_name': csv_data['last_name'],
@ -278,6 +291,7 @@ class TournamentRegistrationService:
})
def _handle_first_tournament_case(self, data):
print("_handle_first_tournament_case", data)
if data:
self.request.session['last_rank'] = data['rank']
self.request.session['is_woman'] = data['is_woman']
@ -307,6 +321,6 @@ class TournamentRegistrationService:
def _license_already_registered(self, stripped_license):
return PlayerRegistration.objects.filter(
team_registration__tournament=self.tournament,
licence_id__startswith=stripped_license,
licence_id__icontains=stripped_license,
team_registration__walk_out=False
).exists()

@ -53,7 +53,7 @@ class TournamentUnregistrationService:
def _find_player_registration(self):
self.player_registration = PlayerRegistration.objects.filter(
licence_id__startswith=self.request.user.licence_id,
licence_id__icontains=self.request.user.licence_id,
team_registration__tournament_id=self.tournament.id,
).first()

@ -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 django.utils import timezone
from .models import Club, Tournament, FailedApiCall, CustomUser, Log, TeamRegistration, PlayerRegistration, UnregisteredTeam, UnregisteredPlayer, PlayerDataSource
import requests
from .models import Club, Tournament, FailedApiCall, CustomUser, Log, TeamRegistration, PlayerRegistration, UnregisteredTeam, UnregisteredPlayer, TeamSortingType, PlayerDataSource
from tournaments.services.email_service import TournamentEmailService
# Others
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
@ -52,94 +53,144 @@ def notify_object_creation_on_discord(created, instance):
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 not tournament or 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.player_registrations.all():
if player.captain is True:
for player in team.player_registrations.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
)
if captain:
TournamentEmailService.notify(captain, other_player, tournament, message_type)
if not captain.registered_online or not captain.email:
TournamentEmailService.notify(other_player, captain, tournament, message_type)
else:
# Notify both players separately if there is no captain or the captain is unavailable
players = list(team.player_registrations.all())
if len(players) == 2:
first_player, second_player = players
TournamentEmailService.notify(first_player, second_player, tournament, message_type)
TournamentEmailService.notify(second_player, first_player, tournament, message_type)
elif len(players) == 1:
# If there's only one player, just send them the notification
TournamentEmailService.notify(players[0], None, tournament, message_type)
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.player_registrations.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.team_registrations.all()
for team_registration in instance.team_registrations.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.player_registrations.all():
print(player, player.registered_online)
if player.captain:
captain = player
else:
other_player = player
if instance.enable_online_registration is False:
return
# Send email to captain
if captain and captain.registered_online and captain.email:
TournamentEmailService.send_tournament_cancellation_notification(
captain,
tournament,
other_player
)
try:
previous_state = Tournament.objects.get(id=instance.id)
except Tournament.DoesNotExist:
previous_state = None
if previous_state is None:
return
# 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
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 {
@ -843,12 +842,6 @@ h-margin {
color: #707070;
}
.single-line {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.group-stage-link {
text-decoration: none;
color: inherit;
@ -866,3 +859,33 @@ h-margin {
.tournament-info a:hover {
color: #f39200;
}
.top-border {
border-top: 1px solid #ccc;
}
.strikethrough {
text-decoration: line-through;
}
.status-container {
margin: 0 -20px -20px -20px; /* Negative margin to counter the bubble padding, including bottom */
padding: 10px 20px 20px 20px; /* Add padding back to maintain text alignment, including bottom */
border-radius: 0 0 24px 24px; /* Match the bubble's bottom corners */
}
.status-container.running {
background-color: #90ee90; /* Light green color */
}
.player {
position: relative; /* Ensures the overlay is positioned within this block */
}
.overlay-text {
position: absolute;
top: 50%;
right: 0%;
transform: translate(-50%, -50%);
white-space: nowrap;
}

@ -23,7 +23,7 @@
{% if registration_successful %}
<p>Merci, l'inscription a bien été envoyée au juge-arbitre.</p>
<p style="text-align: justify;">
Un email de confirmation a été envoyé à {{ user.email }} pour confirmer votre inscription. Pensez à vérifier vos spams si vous ne recevez pas l'email. En cas de problème, contactez le juge-arbitre.
Un email de confirmation a été envoyé à l'adresse associée à votre compte Padel Club ({{ user.email }}). Pensez à vérifier vos spams si vous ne recevez pas l'email. En cas de problème, contactez le juge-arbitre.
</p>
{% else %}
<form method="post">
@ -48,7 +48,24 @@
</p>
<ul>
{% for player in current_players %}
<li>{{ player.first_name }} {{ player.last_name }}{% if player.licence_id %} ({{ player.licence_id }}){% endif %}</li>
<li>
<div>
{{ player.first_name }} {{ player.last_name }}{% if player.licence_id %} ({{ player.licence_id }}){% endif %}
</div>
<div>
{{ player.club_name }}
</div>
<div>
Classement à ce jour : {{ player.rank }}
</div>
{% if not forloop.first %} <!-- Only show remove button if not the first player -->
<div>
<button type="submit" name="remove_player" class="btn small-button">
modifier
</button>
</div>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
@ -73,6 +90,9 @@
{% endif %}
{% if add_player_form.first_tournament or add_player_form.user_without_licence or tournament.license_is_required is False %}
{% if not add_player_form.user_without_licence and tournament.license_is_required is True %}
<div class="semibold">
Padel Club n'a pas trouvé votre partenaire, il se peut qu'il s'agisse de son premier tournoi. Contacter le juge-arbitre après l'inscription si ce n'est pas le cas.
</div>
<div class="semibold">
Précisez les informations du joueur :
</div>

@ -0,0 +1,57 @@
{% load static %}
<div class="cell medium-12 large-3 my-block">
<div class="bubble">
<div>
{% for team in match.teams %}
<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">
{% 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.walk_out == 1 %} strikethrough{% endif %}{% if team.is_winner %} winner{% endif %}">
{% if name|length > 0 %}
{{ name }}
{% else %}
&nbsp;
{% endif %}
</div>
{% endfor %}
{% if team.id %}
</a>
{% endif %}
</div>
{% 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 %}">
{{ score.main }}
{% if score.tiebreak %}
<sup>{{ score.tiebreak }}</sup>
{% endif %}
</span>
{% endfor %}
</div>
{% elif not tournament.hide_weight and team.weight %}
<span class="score ws numbers">{{ team.weight }}</span>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>

@ -38,6 +38,7 @@
paginatedSummons: null,
paginatedRankings: null,
active: 1,
hide_weight: {{ tournament.hide_weight|lower }},
prefixTitle: '',
retrieveData() {
fetch('/tournament/{{ tournament.id }}/broadcast/json/')

@ -39,6 +39,7 @@
paginatedSummons: null,
paginatedRankings: null,
active: 1,
hide_weight: {{ tournament.hide_weight|lower }},
prefixTitle: '',
eventTitle: '',
title: '',

@ -31,9 +31,9 @@
<div class="flex-right">
<div x-show="group_stage.teams[i-1].match_count == 0">
{% if not tournament.hide_weight %}
<div class="score ws numbers"><span x-text="group_stage.teams[i-1].weight"></span></div>
{% endif %}
<div class="score ws numbers" x-show="hide_weight == false">
<span x-text="group_stage.teams[i-1].weight"></span>
</div>
</div>
<div x-show="group_stage.teams[i-1].match_count > 0">

@ -38,6 +38,7 @@
<body x-data="{
paginatedGroupStages: null,
active: 1,
hide_weight: {{ tournament.hide_weight|lower }},
retrieveMatches() {
fetch('/tournament/{{ tournament.id }}/group-stages/json/')
.then(res => res.json())

@ -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,
@ -38,13 +51,14 @@
</template>
</span>
</template>
</template>
<span x-data="{
showWalkOut(match, team) {
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,10 +74,11 @@
</div>
</template>
<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>
</div>
</div>
</div>

@ -7,6 +7,7 @@
{% if tournament.display_group_stages %}
{% block content %}
{% if tournament.display_group_stages %}
{% include 'tournaments/navigation_tournament.html' %}
@ -20,6 +21,5 @@
</div>
{% endfor %}
{% endblock %}
{% endif %}
{% endblock %}

@ -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,7 +62,7 @@
{% 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 %}
@ -67,8 +74,7 @@
{{ match.format }}
{% endif %}
</label>
<!-- <a href="" class="right-label">{{ match.court }}</a> -->
</div>
</div>
</div>
</div>

@ -5,7 +5,6 @@
{% block second_title %}{{ tournament.display_name }}{% endblock %}
{% block title_link %}{% url 'tournament' tournament.id %}{% endblock %}
{% if tournament.display_matches %}
{% block content %}
{% include 'tournaments/navigation_tournament.html' %}
@ -46,4 +45,3 @@
{% endif %}
{% endblock %}
{% endif %}

@ -1,6 +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> -->
{% 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 %}
{% block content %}
{% if tournament.display_matches %}
{% 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 %}
{% endif %}
{% endblock %}

@ -5,11 +5,10 @@
{% block second_title %}{{ tournament.display_name }}{% endblock %}
{% block title_link %}{% url 'tournament' tournament.id %}{% endblock %}
{% if tournament.display_rankings %}
{% block content %}
{% load static %}
{% if tournament.display_rankings %}
{% include 'tournaments/navigation_tournament.html' %}
@ -36,6 +35,5 @@
</div>
</div>
</div>
{% endblock %}
{% endif %}
{% endblock %}

@ -5,12 +5,11 @@
{% block second_title %}{{ tournament.display_name }}{% endblock %}
{% block title_link %}{% url 'tournament' tournament.id %}{% endblock %}
{% if tournament.display_summons %}
{% block content %}
{% load static %}
{% if tournament.display_summons %}
{% include 'tournaments/navigation_tournament.html' %}
{% if team_summons %}
@ -36,5 +35,5 @@
{% endif %}
{% endblock %}
{% endif %}
{% endblock %}

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

@ -5,12 +5,10 @@
{% block second_title %}{{ tournament.display_name }}{% endblock %}
{% block title_link %}{% url 'tournament' tournament.id %}{% endblock %}
{% if tournament.display_teams %}
{% block content %}
{% load static %}
{% if tournament.display_teams %}
{% include 'tournaments/navigation_tournament.html' %}
<div class="grid-x padding-bottom">
<div class="cell medium-6 large-6 my-block">
@ -37,5 +35,5 @@
{% endif %}
</div>
</div>
{% endblock %}
{% endif %}
{% endblock %}

@ -0,0 +1,417 @@
{% 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 %}
{% block content %}
{% if tournament.display_matches %}
{% include 'tournaments/navigation_tournament.html' %}
<div class="butterfly-bracket" id="bracket"></div>
<div id="match-templates" style="display: none;">
{% for match_group in match_groups %}
{% if match_group.matches %}
{% for match in match_group.matches %}
<div data-match-round="{{ forloop.parentloop.counter0 }}"
data-match-index="{{ forloop.counter0 }}"
data-disabled="{{ match.disabled|lower }}"
data-match-group-name="{{ match_group.name }}"
data-match-format="{{ match.format }}"
class="match-template">
{% include 'tournaments/bracket_match_cell.html' %}
</div>
{% endfor %}
{% endif %}
{% endfor %}
</div>
<script>
function renderBracket() {
const bracket = document.getElementById('bracket');
const matchTemplates = document.getElementById('match-templates').children;
const rounds = [];
const matchPositions = [];
// Group matches by round
Array.from(matchTemplates).forEach(template => {
const roundIndex = parseInt(template.dataset.matchRound);
if (!rounds[roundIndex]) {
rounds[roundIndex] = [];
}
rounds[roundIndex].push(template);
});
// First create a test match to get natural height
const firstMatch = document.createElement('div');
firstMatch.className = 'butterfly-match';
firstMatch.innerHTML = `<div class="match-content">${rounds[0][0].innerHTML}</div>`;
bracket.appendChild(firstMatch);
const matchHeight = firstMatch.offsetHeight;
const matchSpacing = 10;
const baseDistance = matchHeight + matchSpacing;
bracket.innerHTML = '';
const roundCount = rounds.length;
const finalRoundIndex = (roundCount - 1) / 2;
let nextMatchDistance = baseDistance;
let minimumMatchDistance = 1;
if (rounds[0].length <= 2) {
minimumMatchDistance = 2
nextMatchDistance = baseDistance * 2;
}
rounds.forEach((roundMatches, roundIndex) => {
const roundDiv = document.createElement('div');
roundDiv.className = 'butterfly-round';
roundDiv.style.setProperty('--match-width', `${365}px`);
// Create title
const titleDiv = document.createElement('div');
titleDiv.className = 'round-title';
// Get the match group name and format
const firstMatchTemplate = roundMatches[0].closest('.match-template');
const matchGroupName = firstMatchTemplate.dataset.matchGroupName;
const matchFormat = firstMatchTemplate.dataset.matchFormat;
const nameSpan = document.createElement('div');
nameSpan.className = 'round-name';
nameSpan.textContent = matchGroupName;
const formatSpan = document.createElement('div');
formatSpan.className = 'round-format';
formatSpan.textContent = matchFormat;
titleDiv.appendChild(nameSpan);
titleDiv.appendChild(formatSpan);
// Create matches container
const matchesContainer = document.createElement('div');
matchesContainer.className = 'matches-container';
if (roundIndex >= finalRoundIndex - 1) {
if (roundCount > 5) {
matchesContainer.style.transform = `translateX(-50%)`;
if (roundIndex >= finalRoundIndex + 2) {
matchesContainer.style.transform = `translateX(-100%)`;
}
}
}
roundDiv.appendChild(matchesContainer);
matchPositions[roundIndex] = [];
roundMatches.forEach((matchTemplate, matchIndex) => {
const matchDiv = document.createElement('div');
matchDiv.className = 'butterfly-match';
matchDiv.style.position = 'absolute';
const isDisabled = matchTemplate.dataset.disabled === 'true';
let top;
let left;
let right;
const currentMatchesCount = roundMatches.length;
if (roundIndex > finalRoundIndex) {
matchDiv.classList.add('reverse-bracket');
top = matchPositions[roundCount - roundIndex - 1][matchIndex];
}
if (roundIndex === 0) {
const nextMatchesCount = rounds[roundIndex + 1].length;
if (currentMatchesCount == nextMatchesCount) {
nextMatchDistance = 0;
}
top = matchIndex * (matchHeight + matchSpacing) * minimumMatchDistance;
} else if (roundIndex === roundCount - 1) {
const nextMatchesCount = rounds[roundIndex - 1].length;
if (currentMatchesCount == nextMatchesCount) {
nextMatchDistance = 0;
}
} else if (roundIndex == finalRoundIndex) {
let lgth = matchPositions[0].length / 2;
let index = lgth + matchIndex - 1;
// If index goes negative, use 0 instead
if (matchIndex == 0) {
top = matchPositions[roundIndex - 1][0] - baseDistance / 2;
} else {
top = matchPositions[roundIndex - 1][0] + baseDistance / 2;
}
nextMatchDistance = baseDistance;
} else if (roundIndex < finalRoundIndex) {
const previousMatchesCount = rounds[roundIndex - 1].length;
const nextMatchesCount = rounds[roundIndex + 1].length;
if (currentMatchesCount == nextMatchesCount) {
nextMatchDistance = 0;
} else if (matchPositions.length > roundIndex - 1) {
nextMatchDistance = (matchPositions[roundIndex - 1][1] - matchPositions[roundIndex - 1][0]);
nextMatchDistance = nextMatchDistance * (previousMatchesCount / currentMatchesCount);
}
if (currentMatchesCount == previousMatchesCount) {
top = matchPositions[roundIndex - 1][matchIndex];
} else {
const parentIndex1 = matchIndex * 2;
const parentIndex2 = parentIndex1 + 1;
const parentPos1 = matchPositions[roundIndex - 1][parentIndex1];
const parentPos2 = matchPositions[roundIndex - 1][parentIndex2];
top = (parentPos1 + parentPos2) / 2;
}
} else if (roundIndex < roundCount) {
const nextMatchesCount = rounds[roundIndex - 1].length;
const previousMatchesCount = rounds[roundIndex + 1].length;
if (currentMatchesCount == nextMatchesCount) {
nextMatchDistance = 0;
} else if (matchPositions.length > roundCount - roundIndex - 1 - 1) {
nextMatchDistance = (matchPositions[roundCount - roundIndex - 1 - 1][1] - matchPositions[roundCount - roundIndex - 1 - 1][0]);
nextMatchDistance = nextMatchDistance * (previousMatchesCount / currentMatchesCount);
}
}
if (roundIndex >= finalRoundIndex - 1) {
if (roundCount > 5) {
if (roundIndex == finalRoundIndex - 1) {
matchDiv.classList.add('inward');
}
if (roundIndex == finalRoundIndex + 1) {
matchDiv.classList.add('outward');
}
}
}
if (roundIndex === finalRoundIndex - 2 || roundIndex === finalRoundIndex + 2) {
nextMatchDistance = nextMatchDistance - baseDistance;
}
else if (roundIndex == finalRoundIndex - 1 || roundIndex == finalRoundIndex + 1) {
nextMatchDistance = baseDistance;
}
matchDiv.style.setProperty('--next-match-distance', `${nextMatchDistance}px`);
matchDiv.style.top = `${top}px`;
matchPositions[roundIndex][matchIndex] = top;
if (matchIndex === 0) {
// // Add logo for final round
// if (roundIndex == finalRoundIndex) {
// const logoDiv = document.createElement('div');
// logoDiv.className = 'round-logo';
// const logoImg = document.createElement('img');
// logoImg.src = '/static/tournaments/images/PadelClub_logo_512.png';
// logoImg.alt = 'PadelClub Logo';
// logoDiv.appendChild(logoImg);
// logoDiv.style.transform = `translateX(-50%)`;
// matchesContainer.appendChild(logoDiv);
// }
// Position title above the first match
titleDiv.style.top = `${top - 80}px`; // Adjust the 60px offset as needed
titleDiv.style.position = 'absolute';
if (roundIndex == finalRoundIndex - 1) {
titleDiv.style.marginLeft = '50px';
} else if (roundIndex == finalRoundIndex + 1) {
titleDiv.style.marginLeft = '-50px';
}
matchesContainer.appendChild(titleDiv);
}
matchDiv.innerHTML = `
<div class="incoming-line ${isDisabled ? 'disabled' : ''}"></div>
<div class="match-content ${isDisabled ? 'disabled' : ''}">${matchTemplate.innerHTML}</div>
`;
if (roundIndex == finalRoundIndex - 1) {
const matchDiv2 = document.createElement('div');
matchDiv2.className = 'butterfly-match';
matchDiv2.classList.add('inward');
matchDiv2.classList.add('semi-final');
matchDiv2.style.setProperty('--next-match-distance', `${baseDistance}px`);
matchDiv2.style.top = `${top}px`;
matchDiv2.innerHTML = `<div class="match-content">${rounds[0][0].innerHTML}</div>`;
matchesContainer.appendChild(matchDiv2); // Append to matchesContainer instead of roundDiv
}
matchesContainer.appendChild(matchDiv); // Append to matchesContainer instead of roundDiv
});
bracket.appendChild(roundDiv);
});
}
renderBracket();
</script>
<style>
.round-logo img {
width: 50px;
height: auto;
display: block;
margin: 0 auto;
position: relative;
top: -100px; /* Increased negative value to move it higher up */
}
.round-logo img {
width: 100px; /* Adjust size as needed */
height: auto;
display: block;
margin: 0 auto;
}
.butterfly-match.same-level::before {
display: none;
}
/* Adjust styling for matches with single parent */
.match-content.disabled {
visibility: hidden;
}
.incoming-line.disabled,
.butterfly-match:has(.match-content.disabled)::after,
.butterfly-match:has(.match-content.disabled)::before {
visibility: hidden;
}
.butterfly-bracket {
display: flex;
gap: 40px; /* Increased to account for horizontal lines (20px on each side) */
position: relative;
margin-bottom: 80px;
}
.round-title {
position: absolute;
top: 0px; /* Adjust this value to position the title where you want it */
padding: 5px 10px;
text-align: center;
font-weight: bold;
width: 100%; /* Change from 100% to auto */
}
.round-name {
font-size: 1.5em; /* Make the round name bigger */
margin-bottom: 5px;
}
.round-format {
font-size: 0.9em;
color: #666;
}
.matches-container {
position: relative;
width: 100%;
flex-grow: 1;
}
.butterfly-round {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px; /* Space between title and matches */
position: relative;
width: var(--match-width);
flex-shrink: 0;
margin-top: 100px; /* Add padding to account for absolute positioned title */
}
.butterfly-match {
position: absolute;
width: 100%;
}
/* Horizontal line after match */
.butterfly-match::after {
content: "";
position: absolute;
left: 100%; /* Start from end of match cell */
top: 50%;
width: 20px;
height: 2px;
background: #666;
}
.semi-final::after {
content: "";
position: absolute;
left: calc(100% + 20px); /* After horizontal line */
width: 2px;
height: calc((var(--next-match-distance)) / 2);
background: #666;
}
/* Vertical line connecting pair of matches */
.butterfly-match:nth-child(2n)::before {
content: "";
position: absolute;
left: calc(100% + 20px); /* After horizontal line */
top: 50%;
width: 2px;
height: calc((var(--next-match-distance)) / 2);
background: #666;
}
.butterfly-match:nth-child(2n+1)::before {
content: "";
position: absolute;
left: calc(100% + 20px);
bottom: calc(50% - 2px); /* Account for half of horizontal line height */
width: 2px;
height: calc((var(--next-match-distance)) / 2); /* Add half of horizontal line height */
background: #666;
}
/* Vertical line connecting pair of matches */
.butterfly-match.reverse-bracket:nth-child(2n)::before {
content: "";
position: absolute;
left: calc(0% - 20px); /* After horizontal line */
top: 50%;
width: 2px;
height: calc((var(--next-match-distance)) / 2);
background: #666;
}
.butterfly-match.reverse-bracket:nth-child(2n+1)::before {
content: "";
position: absolute;
left: calc(0% - 20px);
bottom: 50%; /* Account for half of horizontal line height */
width: 2px;
height: calc((var(--next-match-distance)) / 2); /* Add half of horizontal line height */
background: #666;
}
/* Horizontal line to next round match */
.butterfly-match .incoming-line {
position: absolute;
left: -20px;
top: 50%;
width: 20px;
height: 2px;
background: #666;
}
.inward .incoming-line {
display: none;
}
.butterfly-match.outward::after {
display: none;
}
.butterfly-round:last-child .butterfly-match::after,
.butterfly-round:first-child .incoming-line {
display: none;
}
</style>
{% endif %}
{% endblock %}

@ -17,6 +17,8 @@ urlpatterns = [
path('', views.tournament, name='tournament'),
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'),

@ -9,7 +9,7 @@ from tournaments.models.enums import FederalCategory
def clean_licence_id(licence_id):
# This regex matches the trailing letters (non-digits) and removes them
cleaned_licence_id = re.sub(r'\D+$', '', str(licence_id)) # \D+ matches non-digits at the end
return cleaned_licence_id
return cleaned_licence_id.lstrip("0")
def get_player_name_from_csv(category, licence_id, base_folder=None):
"""
@ -29,6 +29,8 @@ def get_player_name_from_csv(category, licence_id, base_folder=None):
else:
cleaned_licence_id = None
print("get_player_name_from_csv", cleaned_licence_id)
def extract_date(file_name):
"""
Extract the date (MM-YYYY) from the file name and return it as a datetime object.

@ -1,17 +1,21 @@
# Standard library imports
import os
import csv
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse
from django.http import JsonResponse, HttpResponse
from django.utils.encoding import force_str
from django.utils.http import urlsafe_base64_decode
from django.urls import reverse
from django.conf import settings
from django.views.decorators.csrf import csrf_exempt
from django.contrib.admin.views.decorators import staff_member_required
from django.core.files.storage import default_storage
from django.core.files.base import ContentFile
from django.views.generic import View
from django.db.models import Q
from django.template import loader
from tournaments.models.device_token import DeviceToken
@ -20,10 +24,7 @@ from .models import TeamSummon
from datetime import datetime, timedelta
import time
# from django.template import loader
from datetime import date
from django.http import JsonResponse, HttpResponse
from django.db.models import Q
import json
import time
import asyncio
@ -169,7 +170,7 @@ def tournament_info(request, tournament_id):
stripped_license = validator.stripped_license
# Check if there is a PlayerRegistration for this user in this tournament
registered_user = PlayerRegistration.objects.filter(
licence_id__startswith=stripped_license,
licence_id__icontains=stripped_license,
team_registration__tournament=tournament,
team_registration__walk_out=False,
).first()
@ -270,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,
@ -355,9 +356,9 @@ def broadcast_json(request, tournament_id):
def tournament_matches_json(request, tournament_id):
tournament = get_object_or_404(Tournament, pk=tournament_id)
matches, group_stages = tournament.broadcasted_matches_and_group_stages()
live_matches = [match.live_match() for match in matches]
data = json.dumps(live_matches, default=vars)
live_matches_dicts = [match.live_match().to_dict() for match in matches]
data = json.dumps(live_matches_dicts, default=vars)
return HttpResponse(data, content_type='application/json')
def tournament_prog_json(request, tournament_id):
@ -370,7 +371,7 @@ def tournament_prog_json(request, tournament_id):
if match is None or isinstance(match, dict) and match.get('empty'):
live_matches.append({"empty": True})
else:
live_matches.append(match.live_match())
live_matches.append(match.live_match().to_dict())
data = json.dumps(live_matches, default=vars)
return HttpResponse(data, content_type='application/json')
@ -637,6 +638,7 @@ def signup(request):
user.origin = UserOrigin.SITE
user.save()
if not settings.DEBUG:
send_verification_email(request, user, next_url)
return render(request, 'registration/signup_success.html', {
@ -744,7 +746,7 @@ def my_tournaments(request):
def filter_user_tournaments(tournaments):
return [t for t in tournaments if t.team_registrations.filter(
player_registrations__licence_id__startswith=stripped_license,
playerregistration__licence_id__icontains=stripped_license,
walk_out=False
).exists()]
@ -896,6 +898,105 @@ def team_details(request, tournament_id, team_id):
'debug': False # Set to False in production
})
def tournament_bracket(request, tournament_id):
"""
View to display tournament bracket structure.
"""
tournament = get_object_or_404(Tournament, pk=tournament_id)
# Get main bracket rounds (excluding children/ranking matches)
main_rounds = tournament.round_set.filter(
parent=None,
group_stage_loser_bracket=False
).order_by('-index')
main_rounds_reversed = tournament.round_set.filter(
parent=None,
group_stage_loser_bracket=False
).order_by('index') # Removed the minus sign before 'index'
loser_final = None
if len(main_rounds_reversed) >= 1:
semi = main_rounds_reversed[1]
loser_round = tournament.round_set.filter(
parent=semi,
group_stage_loser_bracket=False
).order_by('index')
if len(loser_round) >= 1:
loser_final = loser_round[0]
# Create serializable match groups data
serializable_match_groups = []
# Add first half of each round (from last to semi-finals)
for round in main_rounds:
matches = round.match_set.filter(disabled=False).order_by('index')
if matches:
if len(matches) > 1:
midpoint = int(len(matches) / 2)
first_half_matches = matches[:midpoint]
else:
first_half_matches = list(matches) # Convert QuerySet to a list
if loser_final:
loser_matches = loser_final.match_set.all()
if len(loser_matches) >= 1:
first_half_matches.append(loser_matches[0])
if first_half_matches:
match_group = tournament.create_match_group(
name=round.name(),
matches=first_half_matches
)
serializable_match_groups.append(match_group)
for round in main_rounds_reversed:
matches = round.match_set.filter(disabled=False).order_by('index')
if matches:
if len(matches) > 1:
midpoint = int(len(matches) / 2)
first_half_matches = matches[midpoint:]
match_group = tournament.create_match_group(
name=round.name(),
matches=first_half_matches
)
serializable_match_groups.append(match_group)
context = {
'tournament': tournament,
'match_groups': serializable_match_groups
}
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):
users = CustomUser.objects.order_by('date_joined')

Loading…
Cancel
Save