You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
padelclub_backend/tournaments/models/tournament.py

1534 lines
61 KiB

from time import daylight
from zoneinfo import ZoneInfo
from django.db import models
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from tournaments.models import group_stage
from . import BaseModel, Event, TournamentPayment, FederalMatchCategory, FederalCategory, FederalLevelCategory, FederalAgeCategory, OnlineRegistrationStatus
import uuid
from django.utils import timezone, formats
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from tournaments.utils.player_search import get_player_name_from_csv
from shared.cryptography import encryption_util
from ..utils.extensions import plural_format
class TeamSortingType(models.IntegerChoices):
RANK = 1, 'Rank'
INSCRIPTION_DATE = 2, 'Inscription Date'
class Tournament(BaseModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)
event = models.ForeignKey(Event, blank=True, null=True, on_delete=models.SET_NULL, related_name='tournaments')
name = models.CharField(max_length=200, null=True, blank=True)
start_date = models.DateTimeField()
end_date = models.DateTimeField(null=True, blank=True)
creation_date = models.DateTimeField()
is_private = models.BooleanField(default=False)
# format = models.IntegerField(default=FederalMatchCategory.NINE_GAMES, choices=FederalMatchCategory.choices, null=True, blank=True)
round_format = models.IntegerField(default=FederalMatchCategory.NINE_GAMES, choices=FederalMatchCategory.choices, null=True, blank=True)
group_stage_format = models.IntegerField(default=FederalMatchCategory.NINE_GAMES, choices=FederalMatchCategory.choices, null=True, blank=True)
loser_round_format = models.IntegerField(default=FederalMatchCategory.NINE_GAMES, choices=FederalMatchCategory.choices, null=True, blank=True)
group_stage_sort_mode = models.IntegerField(default=0)
group_stage_count = models.IntegerField(default=0)
rank_source_date = models.DateTimeField(null=True, blank=True)
day_duration = models.IntegerField(default=0)
team_count = models.IntegerField(default=0)
team_sorting = models.IntegerField(default=TeamSortingType.INSCRIPTION_DATE, choices=TeamSortingType.choices)
federal_category = models.IntegerField(default=FederalCategory.MEN, choices=FederalCategory.choices) # optional ?
federal_level_category = models.IntegerField(default=FederalLevelCategory.P100, choices=FederalLevelCategory.choices)
federal_age_category = models.IntegerField(default=FederalAgeCategory.SENIOR, choices=FederalAgeCategory.choices)
#group_stage_court_count = models.IntegerField(null=True, blank=True)
#seed_count = models.IntegerField(default=0)
closed_registration_date = models.DateTimeField(null=True, blank=True)
group_stage_additional_qualified = models.IntegerField(default=0)
court_count = models.IntegerField(default=2)
prioritize_club_members = models.BooleanField()
qualified_per_group_stage = models.IntegerField(default=0)
teams_per_group_stage = models.IntegerField(default=0)
entry_fee = models.FloatField(default=20.0, null=True, blank=True)
global_id = models.CharField(max_length=100, null=True, blank=True) #represent the payment crypted string
is_deleted = models.BooleanField(default=False)
local_id = models.CharField(max_length=100, null=True, blank=True) #represent the is_canceled crypted string
additional_estimation_duration = models.IntegerField(default=0)
publish_teams = models.BooleanField(default=False)
hide_teams_weight = models.BooleanField(default=False)
publish_summons = models.BooleanField(default=False)
publish_group_stages = models.BooleanField(default=False)
publish_brackets = models.BooleanField(default=False)
should_verify_bracket = models.BooleanField(default=False)
should_verify_group_stage = models.BooleanField(default=False)
publish_tournament = models.BooleanField(default=False)
hide_points_earned = models.BooleanField(default=False)
publish_rankings = models.BooleanField(default=False)
loser_bracket_mode = models.IntegerField(default=0)
initial_seed_round = models.IntegerField(default=0)
initial_seed_count = models.IntegerField(default=0)
enable_online_registration = models.BooleanField(default=False) # Equivalent to Bool = false
registration_date_limit = models.DateTimeField(null=True, blank=True) # Equivalent to Date? = nil
opening_registration_date = models.DateTimeField(null=True, blank=True) # Equivalent to Date? = nil
waiting_list_limit = models.IntegerField(null=True, blank=True) # Equivalent to Int? = nil
account_is_required = models.BooleanField(default=True)
license_is_required = models.BooleanField(default=True)
minimum_player_per_team = models.IntegerField(default=2)
maximum_player_per_team = models.IntegerField(default=2)
information = models.CharField(max_length=4000, null=True, blank=True)
def delete_dependencies(self):
for team_registration in self.team_registrations.all():
team_registration.delete_dependencies()
team_registration.delete()
for gs in self.group_stages.all():
gs.delete_dependencies()
gs.delete()
for round in self.rounds.all():
round.delete_dependencies()
round.delete()
for draw_log in self.draw_logs.all():
# draw_log.delete_dependencies()
draw_log.delete()
def __str__(self):
if self.name:
return self.name
else:
return self.display_name()
def is_canceled(self):
if self.local_id:
decrypted = encryption_util.decrypt_aes_gcm(self.local_id)
value = int(decrypted[18])
if 0 <= value <= 4:
return True
else:
return False
else:
return False
def payment(self):
if self.global_id:
decrypted = encryption_util.decrypt_aes_gcm(self.global_id)
value = int(decrypted[18])
return TournamentPayment(value)
else:
return None
def display_name(self):
if self.name:
if self.federal_level_category == FederalLevelCategory.UNLISTED:
return self.name
return self.base_name() + " " + self.name
else:
return self.base_name()
def broadcast_display_name(self):
if self.name:
if self.federal_level_category == FederalLevelCategory.UNLISTED:
return self.name
return self.short_base_name() + " " + self.name
else:
return self.base_name()
def broadcast_event_display_name(self):
if self.event is not None:
return self.event.display_name()
else:
return " "
def base_name(self):
return f"{self.level()} {self.category()}"
def short_base_name(self):
category = self.category()
if len(category) > 0:
return f"{self.level()}{category[0]}"
else:
return self.level()
def filter_name(self):
components = [self.formatted_start_date(), self.short_base_name()]
if self.event and self.event.club and self.event.club.name:
components.append(self.event.club.name)
elif self.event and self.event.name:
components.append(self.event.name)
elif self.name:
components.append(self.name)
return (' ').join(components)
def timezone(self):
tz = 'CET'
if self.event and self.event.club:
tz = self.event.club.timezone
return ZoneInfo(tz)
def local_start_date(self):
timezone = self.timezone()
return self.start_date.astimezone(timezone)
def local_start_date_formatted(self):
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:
return "Anim."
return self.get_federal_level_category_display()
def category(self):
return self.get_federal_category_display()
def age(self):
if self.federal_age_category == 0:
return None
return self.get_federal_age_category_display()
def formatted_start_date(self):
return self.local_start_date().strftime("%d/%m/%y")
def in_progress(self):
return self.end_date is None
def creator(self):
if self.event and self.event.creator:
return self.event.creator.username
else:
return None
def private_label(self):
if self.is_private:
return "Privé"
else:
return "Public"
def summon_count_display(self):
teams = self.team_summons()
if teams is not None and len(teams) > 0:
return f"{len(teams)} équipes convoquées"
else:
return None
def ranking_count_display(self):
teams = self.rankings()
if teams is not None and len(teams) > 0:
return f"{len(teams)} équipes"
else:
return None
def registration_count_display(self):
teams = self.teams(True)
if teams is not None and len(teams) == 1:
return f"{len(teams)} équipe inscrite"
elif teams is not None and len(teams) > 1:
return f"{len(teams)} équipes inscrites"
else:
return None
def tournament_status_display(self):
if self.is_canceled() is True:
return "Annulé"
teams = self.teams(True)
if self.supposedly_in_progress() or self.end_date is not None or self.should_be_over():
teams = [t for t in teams if t.stage != "Attente"]
if teams is not None and len(teams) > 0:
word = "équipe"
if len(teams) > 1:
word = word + "s"
return f"{len(teams)} {word}"
else:
return None
registration_status = None
if self.enable_online_registration == True:
registration_status = self.get_online_registration_status().status_localized()
if teams is not None and len(teams) > 0:
word = "inscription"
if len(teams) > 1:
word = word + "s"
if registration_status is not None:
return f"{registration_status}\n{len(teams)} {word}"
else:
return f"{len(teams)} {word}"
else:
if registration_status is not None:
return f"{registration_status}"
return None
def name_and_event(self):
event_name = None
if self.event:
event_name = self.event.name
if event_name and self.name:
return event_name + " : " + self.name
elif event_name:
return event_name
elif self.name:
return self.name
else:
return None
def team_summons(self):
summons = []
if self.supposedly_in_progress() and self.end_date is None:
print('>>> team_summons supposedly_in_progress')
for team in self.teams(False):
names = team.names
stage = team.stage
weight = team.weight
summon = TeamSummon(team.team_registration.id, names, team.date, weight, stage, "", team.image, self.day_duration)
summons.append(summon)
else:
print('>>> team_summons')
for team_registration in self.team_registrations.all():
if team_registration.is_valid_for_summon():
next_match = team_registration.next_match()
if next_match and next_match.start_date is not None:
names = team_registration.team_names()
stage = next_match.summon_stage_name()
weight = team_registration.weight
summon = TeamSummon(team_registration.id, names, next_match.local_start_date(), weight, stage, next_match.court_name(next_match.court_index), team_registration.logo, self.day_duration)
summons.append(summon)
summons.sort(key=lambda s: (s.date is None, s.date or datetime.min))
return summons
def has_summons(self):
for team_registration in self.team_registrations.all():
if team_registration.is_valid_for_summon():
next_match = team_registration.next_match()
if next_match and next_match.start_date is not None:
return True
return False
def rankings(self):
rankings = []
for team_registration in self.team_registrations.all():
if team_registration.walk_out is False and team_registration.final_ranking is not None:
names = team_registration.team_names()
ranking = team_registration.final_ranking
points = team_registration.points_earned
team = TeamRanking(team_registration.id, names, ranking, points, team_registration.logo)
rankings.append(team)
rankings.sort(key=lambda r: r.ranking)
return rankings
def get_team_waiting_list_position(self, team_registration):
# Use the teams method to get sorted list of teams
all_teams = self.teams(True)
index = -1
# Find position of team in all teams list
for i, team in enumerate(all_teams):
if team.team_registration.id == team_registration.id:
index = i
# Check if team_count exists
if self.team_count:
# Team is not in list
if index < self.team_count:
print("Team is not in list", index, self.team_count)
return -1
# Return position in waiting list relative to target count
print("Return position in waiting list relative to target count", index, self.team_count)
return index - self.team_count
else:
print("else", index, self.team_count)
return -1
def group_stage_spots(self):
"""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, 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
def computed_teams(self, un_walk_out_team=None):
# Initialize team categories
complete_teams = []
wildcard_bracket = []
wildcard_group_stage = []
waiting_teams = []
# Get registration cutoff date
closed_date = self.closed_registration_date
# Process each team registration
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
# Create team item
team = TeamItem(team_reg)
# Determine if registration is valid based on date
is_valid = (
closed_date is None or
team_reg.registration_date is None or
(team_reg.registration_date and team_reg.registration_date <= closed_date)
)
# Set initial stage
if team_reg.group_stage_position is not None:
team.set_stage("Poule")
elif team_reg.bracket_position is not None:
team.set_stage("Tableau")
else:
team.set_stage("Attente")
# Categorize team
if team_reg.wild_card_bracket:
wildcard_bracket.append(team)
elif team_reg.wild_card_group_stage:
wildcard_group_stage.append(team)
elif is_valid:
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)
group_stage_team_count = group_stage_spots - len(wildcard_group_stage)
if group_stage_team_count < 0:
group_stage_team_count = 0
if bracket_seeds < 0:
bracket_seeds = 0
# Sort teams based on tournament rules
if self.team_sorting == TeamSortingType.INSCRIPTION_DATE:
complete_teams.sort(key=lambda t: (
t.registration_date is None,
t.registration_date or datetime.min,
t.initial_weight,
t.team_registration.id
))
waiting_teams.sort(key=lambda t: (
t.registration_date is None,
t.registration_date or datetime.min,
t.initial_weight,
t.team_registration.id
))
else:
complete_teams.sort(key=lambda t: (t.initial_weight, t.team_registration.id))
waiting_teams.sort(key=lambda t: (t.initial_weight, t.team_registration.id))
wildcard_group_stage.sort(key=lambda t: (t.initial_weight, t.team_registration.id))
wildcard_bracket.sort(key=lambda t: (t.initial_weight, t.team_registration.id))
# Split teams into main bracket and waiting list
computed_team_count = self.team_count - len(wildcard_bracket) - len(wildcard_group_stage)
if computed_team_count < 0:
computed_team_count = 0
qualified_teams = complete_teams[:computed_team_count]
excess_teams = complete_teams[computed_team_count:]
qualified_teams.sort(key=lambda t: (t.initial_weight, t.team_registration.id))
excess_teams.sort(key=lambda t: (t.initial_weight, t.team_registration.id))
# Combine all waiting list teams
waiting_list = excess_teams + waiting_teams
if self.team_sorting == TeamSortingType.INSCRIPTION_DATE:
waiting_list.sort(key=lambda t: (
t.registration_date is None,
t.registration_date or datetime.min,
t.initial_weight,
t.team_registration.id
))
else:
waiting_list.sort(key=lambda t: (t.initial_weight, t.team_registration.id))
# Return final sorted list
bracket_teams = qualified_teams[:bracket_seeds] + wildcard_bracket
gs_teams = qualified_teams[bracket_seeds:(bracket_seeds+group_stage_team_count)] + wildcard_group_stage
bracket_teams.sort(key=lambda t: (t.initial_weight, t.team_registration.id))
gs_teams.sort(key=lambda t: (t.initial_weight, t.team_registration.id))
all_teams = bracket_teams + gs_teams
for team in bracket_teams:
team.set_stage("Tableau")
for team in gs_teams:
team.set_stage("Poule")
if include_waiting_list:
all_teams.extend(waiting_list)
return all_teams
def match_groups(self, broadcasted, group_stage_id, round_id):
display_brackets = self.display_matches()
match_groups = []
if group_stage_id:
group_stage = self.group_stages.filter(id=group_stage_id).first()
match_groups.append(self.group_stage_match_group(group_stage, broadcasted, hide_empty_matches=False))
elif round_id:
round = self.rounds.filter(id=round_id).first()
if round and display_brackets is True:
match_groups = self.round_match_groups(round, broadcasted, hide_empty_matches=False)
else:
match_groups = self.all_groups(broadcasted)
return match_groups
def all_groups(self, broadcasted):
groups = []
if self.display_matches():
rounds = self.rounds.filter(parent=None, group_stage_loser_bracket=False).all().order_by('index')
for round in rounds:
groups.extend(self.round_match_groups(round, broadcasted, hide_empty_matches=True))
if self.display_group_stages():
for round in self.rounds.filter(parent=None, group_stage_loser_bracket=True).all().order_by('index'):
groups.extend(self.round_match_groups(round, broadcasted, hide_empty_matches=True))
group_stages = sorted(self.sorted_group_stages(), key=lambda s: (-s.step, s.index))
for group_stage in group_stages:
group = self.group_stage_match_group(group_stage, broadcasted, hide_empty_matches=True)
if group:
groups.append(group)
return groups
def group_stage_match_group(self, group_stage, broadcasted, hide_empty_matches):
if group_stage is None:
return None
matches = group_stage.matches.all()
if hide_empty_matches:
matches = [m for m in matches if m.should_appear()]
else:
matches = [m for m in matches if m.disabled is False]
matches.sort(key=lambda m: (m.start_date is None, m.end_date is not None, m.start_date, m.index))
if matches:
return self.create_match_group(group_stage.display_name(), matches)
else:
return None
def round_match_groups(self, round, broadcasted, hide_empty_matches):
groups = []
matches = round.matches.order_by('index').all()
if hide_empty_matches:
matches = [m for m in matches if m.should_appear()]
else:
matches = [m for m in matches if m.disabled is False]
if round and matches:
matches.sort(key=lambda m: m.index)
group = self.create_match_group(round.name(), matches)
groups.append(group)
ranking_matches = round.ranking_matches(hide_empty_matches)
if hide_empty_matches:
ranking_matches = [m for m in ranking_matches if m.should_appear()]
else:
ranking_matches = [m for m in ranking_matches if m.disabled is False]
if len(ranking_matches) > 0:
ranking_matches.sort(
key=lambda m: (
m.round.index, # Sort by Round index first
m.round.get_depth(),
m.name or '', # Then by Round depth, using empty string if name is None
)
)
group = self.create_match_group('Matchs de classement', ranking_matches)
groups.append(group)
return groups
def create_match_group(self, name, matches, round_id=None):
matches = list(matches)
live_matches = [match.live_match() for match in matches]
# Filter out matches that have a start_date of None
valid_matches = [match for match in matches if match.start_date is not None]
formatted_schedule = ''
if valid_matches and self.day_duration >= 7:
# Find the first match by start date
first_match = min(valid_matches, key=lambda match: match.start_date)
# Format the date
timezone = first_match.tournament().timezone()
local_start = first_match.start_date.astimezone(timezone)
time_format = 'l d M'
formatted_schedule = f" - {formats.date_format(local_start, format=time_format)}"
return MatchGroup(name, live_matches, formatted_schedule, round_id)
def live_group_stages(self):
group_stages = self.sorted_group_stages()
return [gs.live_group_stages() for gs in group_stages]
def sorted_group_stages(self):
# Get all group stages and sort by step (descending) and index (ascending)
group_stages = self.group_stages.all().order_by('-step', 'index')
# List to collect live group stages from finished steps
filtered = []
for group_stage in group_stages:
if group_stage.step > 0:
# Check the previous step's group stages
previous_step_group_stages = self.group_stages.filter(step=group_stage.step - 1)
# Check if all previous step group stages are completed
if all(gs.is_completed() for gs in previous_step_group_stages):
filtered.append(group_stage)
else:
# Always include step 0
filtered.append(group_stage)
return filtered
def get_previous_live_group_stages(self, step):
previous_step_group_stages = self.group_stages.filter(step=step).order_by('index')
return [gs.live_group_stages() for gs in previous_step_group_stages]
def last_group_stage_step(self):
live_group_stages = self.sorted_group_stages()
# Filter to find the last running step
last_running_step = max(gs.step for gs in live_group_stages) if live_group_stages else None
if last_running_step is not None:
# Get only group stages from the last running step
group_stages_last_step = [gs for gs in live_group_stages if gs.step == last_running_step]
return group_stages_last_step
else:
return []
def broadcast_content(self):
matches, group_stages = self.broadcasted_matches_and_group_stages()
group_stages_dicts = [gs.to_dict() for gs in group_stages]
# if now is before the first match, we want to show the summons + group stage or first matches
# change timezone to datetime to avoid the bug RuntimeWarning: DateTimeField Tournament.start_date received a naive datetime (2024-05-16 00:00:00) while time zone support is active.
current_time = timezone.now()
tournament_start = self.local_start_date()
one_hour_before_start = tournament_start - timedelta(hours=1)
if current_time < one_hour_before_start:
team_summons_dicts = [summon.to_dict() for summon in self.team_summons()]
if group_stages:
return {
'matches': [],
'group_stages': group_stages_dicts,
'summons': team_summons_dicts,
'event_title' : self.broadcast_event_display_name(),
'tournament_title' : self.broadcast_display_name(),
'rankings' : []
}
else:
live_matches_dicts = [match.live_match().to_dict() for match in matches]
return {
'matches': live_matches_dicts,
'group_stages': [],
'summons': team_summons_dicts,
'event_title' : self.broadcast_event_display_name(),
'tournament_title' : self.broadcast_display_name(),
'rankings' : []
}
elif self.end_date is not None:
live_matches_dicts = [match.live_match().to_dict() for match in matches]
team_rankings_dicts = [ranking.to_dict() for ranking in self.rankings()]
return {
'matches': live_matches_dicts,
'group_stages': [],
'summons': [],
'event_title' : self.broadcast_event_display_name(),
'tournament_title' : self.broadcast_display_name(),
'rankings' : team_rankings_dicts
}
else: # we want to display the broadcasted content
live_matches_dicts = [match.live_match().to_dict() for match in matches]
return {
'matches': live_matches_dicts,
'group_stages': group_stages_dicts,
'summons': [],
'rankings' : [],
'event_title' : self.broadcast_event_display_name(),
'tournament_title' : self.broadcast_display_name(),
}
def broadcasted_matches_and_group_stages(self):
matches = []
group_stages = []
if self.group_stages.count() > 0 and self.no_bracket_match_has_started():
group_stages = [gs.live_group_stages() for gs in self.last_group_stage_step()]
matches = self.broadcasted_group_stages_matches()
first_round = self.first_round()
if first_round and self.has_all_group_stages_started():
matches.extend(first_round.get_matches_recursive(True))
else:
current_round = self.round_to_show()
if current_round:
print(f'current_round = {current_round.index} / parent = {current_round.parent}')
all_upper_matches_are_over = current_round.all_matches_are_over()
if all_upper_matches_are_over is False:
matches.extend(current_round.get_matches_recursive(True))
# Add full matches from the next rounds
next_round = self.round_for_index(current_round.index - 1)
if next_round:
# print('next round')
matches.extend(next_round.get_matches_recursive(True))
if all_upper_matches_are_over is True:
# print('all_upper_matches_are_over')
matches.extend(current_round.get_matches_recursive(True))
# Add matches from the previous round or group_stages
previous_round = self.round_for_index(current_round.index + 1)
if previous_round:
# print('previous_round')
matches.extend(previous_round.get_matches_recursive(True))
previous_previous_round = self.round_for_index(current_round.index + 2)
if previous_previous_round:
previous_previous_matches = previous_previous_round.get_matches_recursive(True)
previous_previous_matches = [m for m in previous_previous_matches if m.end_date is None]
matches.extend(previous_previous_matches)
else:
print('group_stages')
group_stages = [gs.live_group_stages() for gs in self.last_group_stage_step()]
else:
first_round = self.first_round()
if first_round:
matches.extend(first_round.get_matches_recursive(True))
return matches, group_stages
def no_bracket_match_has_started(self):
matches = []
for round in self.rounds.all():
for match in round.matches.all():
if match.started():
return False
return True
def all_matches(self, hide_empty_matches):
matches = []
for round in self.rounds.all():
matches.extend(round.all_matches(hide_empty_matches))
for group_stage in self.group_stages.all():
matches.extend(group_stage.matches.all())
matches = [m for m in matches if m.should_appear()]
return matches
def group_stage_matches(self):
matches = []
for group_stage in self.group_stages.all():
matches.extend(group_stage.matches.all())
return matches
def group_stages_running(self):
if len(self.group_stages.all()) > 0:
# check le debut des match de Round
matches = self.group_stage_matches()
running_group_stage_matches = [m for m in matches if m.end_date is None]
return len(running_group_stage_matches) > 0
else:
return False
def first_unfinished_match(self):
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:
return None
def round_to_show(self):
# print('===== round_to_show')
last_started_match = self.first_unfinished_match()
if last_started_match and last_started_match.round:
# print(f'last_started_match = {last_started_match.name}')
current_round = last_started_match.round.root_round()
# print(f'round_to_show > current_round: {current_round.name()}')
if current_round:
return current_round
# all started matches have ended, possibly
last_finished_match = self.last_finished_match()
if last_finished_match:
round = last_finished_match.round
if round is None: # when the last finished match is in the group stage
round = self.rounds.filter(parent__isnull=True).order_by('-index').first()
if round:
# print(f'last_finished_match = {last_finished_match.name}')
round_root_index = round.root_round().index
# print(f'round_index = {round_root_index}')
if round_root_index == 0:
return round
else:
round = self.rounds.filter(parent=None,index=round_root_index-1).first()
if round:
return round
else:
return None
return None
def last_started_match(self):
matches = [m for m in self.all_matches(False) if m.start_date]
matches.sort(key=lambda m: m.start_date, reverse=True)
return matches[0] if matches else None
def last_finished_match(self):
matches = [m for m in self.all_matches(False) if m.end_date]
matches.sort(key=lambda m: m.end_date, reverse=True)
return matches[0] if matches else None
def round_for_index(self, index):
return self.rounds.filter(index=index, parent=None).first()
def first_round(self):
main_rounds = list(self.rounds.filter(parent=None))
main_rounds.sort(key=lambda r: r.index, reverse=True)
return main_rounds[0] if main_rounds else None
def broadcasted_group_stages_matches(self):
matches = []
group_stages = self.elected_broadcast_group_stages()
group_stages.sort(key=lambda gs: (gs.index, gs.start_date is None, gs.start_date))
for group_stage in group_stages:
matches.extend(group_stage.matches.all())
if len(matches) > 16:
# if more than 16 groupstage matches
now = timezone.now()
future_threshold = now + timedelta(hours=1)
past_threshold = now - timedelta(hours=1)
matches = [m for m in matches if m.should_appear() and
(m.start_date is None or m.start_date <= future_threshold) and # Not starting in more than 1h
(m.end_date is None or m.end_date >= past_threshold)] # Not finished for more than 1h
matches = matches[:16]
matches.sort(key=lambda m: (m.start_date is None, m.end_date is not None, m.start_date, m.index))
group_stage_loser_bracket = list(self.rounds.filter(parent=None, group_stage_loser_bracket=True).all())
if len(group_stage_loser_bracket) > 0:
loser_matches = group_stage_loser_bracket[0].all_matches(True)
loser_matches = [m for m in loser_matches if m.should_appear()]
loser_matches.sort(key=lambda m: (m.start_date is None, m.end_date is not None, m.start_date, m.index))
matches.extend(loser_matches)
return matches
def elected_broadcast_group_stages(self):
group_stages = list(self.group_stages())
started = [gs for gs in group_stages if gs.starts_soon()]
if len(started) > 0:
return started
else:
return group_stages
def display_rankings(self):
if self.supposedly_in_progress():
return True
if self.end_date is not None:
return True
return False
def display_tournament(self):
if self.publish_tournament:
return True
is_build_and_not_empty = self.is_build_and_not_empty()
if self.end_date is not None:
return is_build_and_not_empty
if timezone.now() >= self.local_start_date():
return is_build_and_not_empty
minimum_publish_date = self.creation_date.replace(hour=9, minute=0) + timedelta(days=1)
return timezone.now() >= timezone.localtime(minimum_publish_date)
def display_teams(self):
if self.end_date is not None:
return self.has_team_registrations()
if self.publish_teams:
return self.has_team_registrations()
if timezone.now() >= self.local_start_date():
return self.has_team_registrations()
return False
def has_team_registrations(self):
return self.team_registrations.count() > 0
def display_summons(self):
if self.end_date is not None:
return False
if self.publish_summons:
return self.has_summons()
if timezone.now() >= self.local_start_date():
return self.has_summons()
return False
def display_group_stages(self):
if len(self.group_stages.all()) == 0:
return False
if self.end_date is not None:
return True
if self.publish_group_stages:
return True
first_group_stage_start_date = self.group_stage_start_date()
if first_group_stage_start_date is None:
return timezone.now() >= self.local_start_date()
else:
return timezone.now() >= first_group_stage_start_date
def group_stage_start_date(self):
group_stages = [gs for gs in self.group_stages.all() if gs.start_date is not None]
if len(group_stages) == 0:
return None
timezone = self.timezone()
return min(group_stages, key=lambda gs: gs.start_date).start_date.astimezone(timezone)
def display_matches(self):
if self.end_date is not None:
return True
bracket_matches = self.bracket_matches()
if len(bracket_matches) == 0:
return self.display_group_stages()
if self.publish_brackets:
return True
first_match_start_date = self.first_match_start_date(bracket_matches)
if first_match_start_date is None:
return timezone.now() >= self.local_start_date()
bracket_start_date = self.getEightAm(first_match_start_date)
if bracket_start_date < self.local_start_date():
bracket_start_date = self.local_start_date()
group_stage_start_date = self.group_stage_start_date()
if group_stage_start_date is not None:
if bracket_start_date < group_stage_start_date:
return timezone.now() >=first_match_start_date
if timezone.now() >= bracket_start_date:
return True
return False
def bracket_matches(self):
matches = []
for round in self.rounds.all():
matches.extend(round.all_matches(False))
return matches
def first_match_start_date(self, bracket_matches):
matches = [m for m in bracket_matches if m.start_date is not None]
if len(matches) == 0:
return None
return min(matches, key=lambda m: m.start_date).local_start_date()
def getEightAm(self, date):
return date.replace(hour=8, minute=0, second=0, microsecond=0, tzinfo=date.tzinfo)
def supposedly_in_progress(self):
# end = self.start_date + timedelta(days=self.day_duration + 1)
# return self.start_date.replace(hour=0, minute=0) <= timezone.now() <= end
timezoned_datetime = self.local_start_date()
end = timezoned_datetime + timedelta(days=self.day_duration + 1)
now = timezone.now()
start = timezoned_datetime.replace(hour=0, minute=0)
# print(f"timezoned_datetime: {timezoned_datetime}")
# print(f"tournament end date: {end}")
# print(f"current time: {now}")
# print(f"tournament start: {start}")
# print(f"start <= now <= end: {start <= now <= end}")
return start <= now <= end
def starts_in_the_future(self):
# tomorrow = datetime.now().date() + timedelta(days=1)
timezoned_datetime = self.local_start_date()
start = timezoned_datetime.replace(hour=0, minute=0)
now = timezone.now()
return start >= now
def should_be_over(self):
if self.end_date is not None:
return True
timezoned_datetime = self.local_start_date()
end = timezoned_datetime + timedelta(days=self.day_duration + 1)
now = timezone.now()
return now >= end and self.is_build_and_not_empty() and self.nearly_over()
def nearly_over(self):
# First check group stages if they exist
group_stages = list(self.group_stages.all()) # Use prefetched data
if group_stages:
# Check if all group stages are completed
for group_stage in group_stages:
# Use the is_completed method
if group_stage.is_completed():
return True
# If no group stages, check semi-finals
semifinals = self.rounds.filter(index=1, parent=None).first() # Use prefetched data
if semifinals:
# Check if any match in semi-finals has started
for match in semifinals.matches.all(): # Use prefetched data
if match.start_date is not None and match.is_ready():
return True
return False
def display_points_earned(self):
return self.federal_level_category != FederalLevelCategory.UNLISTED and self.hide_points_earned is False
def hide_weight(self):
return self.hide_teams_weight
def is_build_and_not_empty(self):
if hasattr(self, '_prefetched_objects_cache'):
# Use prefetched data if available
has_group_stages = 'group_stages' in self._prefetched_objects_cache and len(self.group_stages.all()) > 0
has_rounds = 'rounds' in self._prefetched_objects_cache and len(self.rounds.all()) > 0
has_team_registrations = 'team_registrations' in self._prefetched_objects_cache and len(self.team_registrations.all()) >= 4
else:
# Fall back to database queries if not prefetched
has_group_stages = self.group_stages.count() > 0
has_rounds = self.rounds.count() > 0
has_team_registrations = self.team_registrations.count() >= 4
return (has_group_stages or has_rounds) and has_team_registrations
def day_duration_formatted(self):
return plural_format("jour", self.day_duration)
def has_club_address(self):
if self.event.club:
return self.event.club.has_address()
else:
return False
def has_all_group_stages_started(self):
for group_stage in self.group_stages.all():
if group_stage.has_at_least_one_started_match() is False:
return False
return True
def options_online_registration(self):
options = []
# Date d'ouverture
if self.opening_registration_date:
date = formats.date_format(timezone.localtime(self.opening_registration_date), format='j F Y H:i')
options.append(f"Ouverture des inscriptions le {date}")
# Date limite
if self.registration_date_limit:
date = formats.date_format(timezone.localtime(self.registration_date_limit), format='j F Y H:i')
options.append(f"Clôture des inscriptions le {date}")
# Cible d'équipes
if self.team_count:
options.append(f"Maximum {self.team_count} équipes")
# Liste d'attente
if self.waiting_list_limit:
options.append(f"Liste d'attente limitée à {self.waiting_list_limit} équipes")
# Options d'inscription
if self.account_is_required:
options.append("Compte requis")
if self.license_is_required:
options.append("Licence requise")
# Joueurs par équipe
min_players = self.minimum_player_per_team
max_players = self.maximum_player_per_team
if min_players == max_players:
options.append(f"{min_players} joueurs par équipe")
else:
options.append(f"Entre {min_players} et {max_players} joueurs par équipe")
return options
def online_register_is_enabled(self):
if self.supposedly_in_progress():
return False
if self.closed_registration_date is not None:
return False
if self.end_date is not None:
return False
now = timezone.now()
# Check if online registration is enabled
if not self.enable_online_registration:
return False
# Check opening registration date
if self.opening_registration_date is not None:
timezoned_datetime = timezone.localtime(self.opening_registration_date)
if now < timezoned_datetime:
return False
# Check registration date limit
if self.registration_date_limit is not None:
timezoned_datetime = timezone.localtime(self.registration_date_limit)
if now > timezoned_datetime:
return False
# Check target team count and waiting list limit
if self.team_count is not None:
current_team_count = self.team_registrations.exclude(walk_out=True).count()
if current_team_count >= self.team_count:
if self.waiting_list_limit is not None:
waiting_list_count = current_team_count - self.team_count
if waiting_list_count >= self.waiting_list_limit:
return False
return True
def get_online_registration_status(self):
if self.supposedly_in_progress():
return OnlineRegistrationStatus.ENDED
if self.closed_registration_date is not None:
return OnlineRegistrationStatus.ENDED
if self.end_date is not None:
return OnlineRegistrationStatus.ENDED_WITH_RESULTS
now = timezone.now()
if self.opening_registration_date is not None:
timezoned_datetime = timezone.localtime(self.opening_registration_date)
if now < timezoned_datetime:
return OnlineRegistrationStatus.NOT_STARTED
if self.registration_date_limit is not None:
timezoned_datetime = timezone.localtime(self.registration_date_limit)
if now > timezoned_datetime:
return OnlineRegistrationStatus.ENDED
if self.team_count is not None:
# Get all team registrations excluding walk_outs
current_team_count = self.team_registrations.exclude(walk_out=True).count()
if current_team_count >= self.team_count:
if self.waiting_list_limit is not None:
waiting_list_count = current_team_count - self.team_count
if waiting_list_count >= self.waiting_list_limit:
return OnlineRegistrationStatus.WAITING_LIST_FULL
return OnlineRegistrationStatus.WAITING_LIST_POSSIBLE
return OnlineRegistrationStatus.OPEN
def is_unregistration_possible(self):
# Check if tournament has started
if self.supposedly_in_progress():
return False
if self.closed_registration_date is not None:
return False
# Check if tournament is finished
if self.end_date is not None:
return False
# Check if registration is closed
if self.registration_date_limit is not None:
if timezone.now() > timezone.localtime(self.registration_date_limit):
return False
# Otherwise unregistration is allowed
return True
def get_waiting_list_position(self):
# If no target team count exists, no one goes to waiting list
if self.team_count is None:
return -1
# Get count of active teams (not walked out)
current_team_count = self.team_registrations.exclude(walk_out=True).count()
# If current count is less than target count, next team is not in waiting list
if current_team_count < self.team_count:
return -1
# If we have a waiting list limit
if self.waiting_list_limit is not None:
waiting_list_count = current_team_count - self.team_count
# If waiting list is full
if waiting_list_count >= self.waiting_list_limit:
return -1
# Return waiting list position
return waiting_list_count
# In waiting list with no limit
return current_team_count - self.team_count
def build_tournament_type_array(self):
tournament_details = []
if self.federal_level_category > 0:
tournament_details.append(self.level())
if self.category():
tournament_details.append(self.category())
if self.age() and self.federal_age_category != FederalAgeCategory.SENIOR:
tournament_details.append(self.age())
return tournament_details
def build_tournament_type_str(self):
tournament_details = self.build_tournament_type_array()
return " ".join(filter(None, tournament_details))
def build_tournament_details_str(self):
tournament_details = self.build_tournament_type_array()
name_str = self.build_name_details_str()
if len(name_str) > 0:
tournament_details.append(name_str)
return " ".join(filter(None, tournament_details))
def build_name_details_str(self):
name_details = []
if self.name:
name_details.append(self.name)
if self.event.name:
name_details.append(self.event.name)
name_str = " - ".join(filter(None, name_details))
if name_str:
name_str = f"{name_str}"
return name_str
def player_register_check(self, licence_id):
reasons = []
if not licence_id:
return None
data, found = get_player_name_from_csv(self.federal_category, licence_id)
if not found or not data:
print("not found or not data")
return None
birth_year = data.get('birth_year', None)
is_woman = data.get('is_woman', None)
# Check gender category restrictions
if is_woman is not None and self.federal_category == FederalCategory.WOMEN and is_woman is False:
reasons.append("Ce tournoi est réservé aux femmes")
if birth_year is None:
return reasons if reasons else None
current_year = timezone.now().year
if timezone.now().month >= 9: # Check if current month is September or later
current_year += 1
user_age = current_year - int(birth_year)
print("user_age", user_age)
# Check age category restrictions
if self.federal_age_category == FederalAgeCategory.A11_12 and user_age > 12:
reasons.append("Ce tournoi est réservé aux -12 ans")
if self.federal_age_category == FederalAgeCategory.A13_14 and user_age > 14:
reasons.append("Ce tournoi est réservé aux -14 ans")
if self.federal_age_category == FederalAgeCategory.A15_16 and user_age > 16:
reasons.append("Ce tournoi est réservé aux -16 ans")
if self.federal_age_category == FederalAgeCategory.A17_18 and user_age > 18:
reasons.append("Ce tournoi est réservé aux -18 ans")
if self.federal_age_category == FederalAgeCategory.SENIOR and user_age < 11:
reasons.append("Ce tournoi est réservé aux 11 ans et plus")
if self.federal_age_category == FederalAgeCategory.A45 and user_age < 45:
reasons.append("Ce tournoi est réservé aux +45 ans")
if self.federal_age_category == FederalAgeCategory.A55 and user_age < 55:
reasons.append("Ce tournoi est réservé aux +55 ans")
addon = 0
computedRank = int(data.get("rank", 0))
if is_woman and self.federal_category == FederalCategory.MEN:
addon = FederalCategory.female_in_male_assimilation_addition(computedRank)
computedRank = computedRank + addon
if computedRank <= self.min_player_rank():
name = data['first_name'] + " " + data['last_name'].upper()
reasons.append(f"{name} ({licence_id}): trop bien classé pour ce tournoi")
return reasons if reasons else None
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):
if len(teams)<=self.team_count:
return None
waiting_teams = [team for team in teams if team.stage == "Attente"]
if len(waiting_teams) > 0:
return waiting_teams[0].team_registration
def broadcasted_prog(self):
# Get matches from broadcasted_matches_and_group_stages
matches, _ = self.broadcasted_matches_and_group_stages()
if not matches:
return []
# Get all unfinished matches for courts
active_matches = [
m for m in matches
if m.end_date is None # Not finished
and m.court_index is not None
]
# Group matches by court
matches_by_court = {}
courts = set()
for match in active_matches:
if match.court_index not in matches_by_court:
matches_by_court[match.court_index] = []
courts.add(match.court_index)
matches_by_court[match.court_index].append(match)
# Sort matches within each court by start time
for court in matches_by_court:
matches_by_court[court].sort(key=lambda m: (
m.start_date is None, # None dates come last
m.start_date if m.start_date else timezone.now()
))
# Sort courts and organize them into groups of 4
sorted_courts = sorted(list(courts))
court_groups = [sorted_courts[i:i+4] for i in range(0, len(sorted_courts), 4)]
ordered_matches = []
# For each group of up to 4 courts
for court_group in court_groups:
# First row: earliest match for each court
for court in court_group:
if court in matches_by_court and matches_by_court[court]:
ordered_matches.append(matches_by_court[court][0])
else:
ordered_matches.append({"empty": True})
# Pad to 4 courts if needed
while len(ordered_matches) % 4 != 0:
ordered_matches.append({"empty": True})
# Second row: next match for each court
for court in court_group:
if court in matches_by_court and len(matches_by_court[court]) > 1:
ordered_matches.append(matches_by_court[court][1])
else:
ordered_matches.append({"empty": True})
# Pad to 4 courts if needed
while len(ordered_matches) % 4 != 0:
ordered_matches.append({"empty": True})
# Add unassigned matches at the end if needed
unassigned_matches = [
m for m in matches
if m.end_date is None and m.court_index is None
]
if unassigned_matches:
ordered_matches.extend(unassigned_matches)
return ordered_matches
def get_butterfly_bracket_match_group(self, parent_round=None, double_butterfly_mode=False, display_loser_final=False):
loser_final = None
main_rounds_reversed = []
# Get main bracket rounds (excluding children/ranking matches)
main_rounds = self.rounds.filter(
parent=parent_round,
group_stage_loser_bracket=False
).order_by('-index')
count = main_rounds.count()
if display_loser_final and count > 1:
semi = main_rounds[count - 2]
loser_final = self.rounds.filter(
parent=semi,
group_stage_loser_bracket=False
).order_by('index').first()
# Create serializable match groups data
serializable_match_groups = []
# Add first half of each round (from last to semi-finals)
for round in main_rounds:
next_round = main_rounds.filter(index=round.index - 1).first()
match_group = round.prepare_match_group(next_round, parent_round, loser_final, double_butterfly_mode, False)
if match_group:
serializable_match_groups.append(match_group)
if double_butterfly_mode:
main_rounds_reversed = list(main_rounds)
main_rounds_reversed.reverse()
for round in main_rounds_reversed:
if round.index > 0:
next_round = main_rounds.filter(index=round.index - 1).first()
match_group = round.prepare_match_group(next_round, parent_round, None, double_butterfly_mode, True)
if match_group:
serializable_match_groups.append(match_group)
return serializable_match_groups
def has_bracket(self):
main_rounds = self.round_set.filter(
parent=None,
group_stage_loser_bracket=False
)
count = main_rounds.count()
if count == 0:
return False
else:
return True
class MatchGroup:
def __init__(self, name, matches, formatted_schedule, round_id=None):
self.name = name
self.matches = matches
self.formatted_schedule = formatted_schedule
self.round_id = round_id
def add_match(self, match):
self.matches.append(match)
def add_matches(self, matches):
self.matches = matches
def to_dict(self):
return {
'name': self.name,
'round_id': self.round_id,
'matches': [match.to_dict() for match in self.matches]
}
class TeamSummon:
def __init__(self, id, names, date, weight, stage, court, image, day_duration):
self.id = str(id)
self.names = names
self.date = date
self.weight = weight
self.stage = stage
self.court = court
self.image = image
self.day_duration = day_duration
def formatted_date(self):
if self.date:
if self.day_duration >= 7:
return formats.date_format(self.date, format='l d M H:i')
else:
return formats.date_format(self.date, format='l H:i')
else:
return ''
def to_dict(self):
return {
"names": self.names,
"date": self.formatted_date(),
"weight": self.weight,
"stage": self.stage,
"court": self.court,
"image": self.image,
}
class TeamItem:
def __init__(self, team_registration):
self.names = team_registration.team_names()
self.date = team_registration.local_call_date()
self.registration_date = team_registration.registration_date
if team_registration.player_registrations.count() == 0:
weight = None
else:
weight = team_registration.weight
self.weight = weight
self.initial_weight = team_registration.initial_weight()
self.image = team_registration.logo
self.stage = ""
self.team_registration = team_registration
self.wildcard_bracket = team_registration.wild_card_bracket
self.wildcard_groupstage = team_registration.wild_card_group_stage
def set_stage(self, stage):
self.stage = stage
def to_dict(self):
return {
"names": self.names,
"date": self.date,
"registration_date": self.registration_date,
"weight": self.weight,
"initial_weight": self.initial_weight,
"image": self.image,
"stage": self.stage,
"wildcard_bracket": self.wildcard_bracket,
"wildcard_groupstage": self.wildcard_groupstage,
}
class TeamRanking:
def __init__(self, id, names, ranking, points, image):
self.id = str(id)
self.names = names
self.ranking = ranking
self.formatted_ranking = self.ordinal(ranking)
self.points = self.points_earned_display(points)
self.image = image
# def ranking_display(self):
# return self.ordinal(self.ranking)
def points_earned_display(self, points):
if points is None:
return ""
return f"+{points} pt{self.plural_suffix(points)}"
def plural_suffix(self, n):
if n > 1:
return 's'
else:
return ''
def ordinal(self, n):
if n == 1:
suffix = 'er'
else:
suffix = 'ème'
return str(n) + suffix
def to_dict(self):
return {
"names": self.names,
"ranking": self.ranking,
"formatted_ranking": self.formatted_ranking,
"points": self.points,
"image": self.image,
}