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

620 lines
23 KiB

from django.db import models
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from tournaments.models import group_stage
from . import Event, TournamentPayment, FederalMatchCategory, FederalCategory, FederalLevelCategory, FederalAgeCategory
import uuid
from django.utils import timezone, formats
from datetime import datetime, timedelta
from .. import config_local
from ..utils.crypto import decrypt_aes_gcm
class TeamSortingType(models.IntegerChoices):
RANK = 1, 'Rank'
INSCRIPTION_DATE = 2, 'Inscription Date'
class Tournament(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)
event = models.ForeignKey(Event, blank=True, null=True, on_delete=models.CASCADE)
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)
def __str__(self):
if self.name:
return self.name
else:
return self.display_name()
def is_canceled(self):
if self.local_id and config_local.CRYPTO_KEY:
decrypted = decrypt_aes_gcm(self.local_id, config_local.CRYPTO_KEY)
value = int(decrypted[18])
if 0 <= value <= 4:
return True
else:
return False
else:
return False
def payment(self):
if self.global_id and config_local.CRYPTO_KEY:
decrypted = decrypt_aes_gcm(self.global_id, config_local.CRYPTO_KEY)
value = int(decrypted[18])
return TournamentPayment(value)
else:
return None
def display_name(self):
if self.name:
return self.base_name() + " " + self.name
else:
return self.base_name()
def base_name(self):
return f"{self.level()} {self.category()}"
def level(self):
return self.get_federal_level_category_display()
def category(self):
return self.get_federal_category_display()
def formatted_start_date(self):
return self.start_date.strftime("%d/%m/%y")
def in_progress(self):
return self.end_date is None
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()
if teams is not None and len(teams) > 0:
return f"{len(teams)} équipes inscrites"
else:
return None
def team_count_display(self):
teams = self.teams()
if teams is not None and len(teams) > 0:
return f"{len(teams)} inscriptions"
else:
return None
def name_and_event(self):
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 = []
for team_registration in self.teamregistration_set.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(names, next_match.start_date, weight, stage, team_registration.logo)
summons.append(summon)
summons.sort(key=lambda s: s.date)
return summons
def rankings(self):
rankings = []
for team_registration in self.teamregistration_set.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(names, ranking, points, team_registration.logo)
rankings.append(team)
rankings.sort(key=lambda s: s.ranking)
return rankings
def teams(self):
bracket_teams = []
group_stage_teams = []
waiting_teams = []
teams = []
wildcard_bracket = []
wildcard_group_stage = []
complete_teams = []
for team_registration in self.teamregistration_set.all():
if team_registration.walk_out is False:
names = team_registration.team_names()
weight = team_registration.weight
initial_weight = team_registration.initial_weight()
date = team_registration.registration_date
team = TeamList(names, weight, date, initial_weight, team_registration.logo)
team.set_stage("Attente")
teams.append(team)
if team_registration.wild_card_bracket:
wildcard_bracket.append(team)
elif team_registration.wild_card_group_stage:
wildcard_group_stage.append(team)
else:
complete_teams.append(team)
if len(teams) < self.team_count:
teams.sort(key=lambda s: (s.initial_weight, s.date))
return teams
seeds_count = min(self.team_count, len(teams)) - self.group_stage_count * self.teams_per_group_stage - len(wildcard_bracket)
group_stage_members_count = self.group_stage_count * self.teams_per_group_stage - len(wildcard_group_stage)
if group_stage_members_count < 0:
group_stage_members_count = 0
if seeds_count < 0:
seeds_count = 0
if self.team_sorting == TeamSortingType.INSCRIPTION_DATE:
complete_teams.sort(key=lambda s: (s.date, s.initial_weight))
else:
complete_teams.sort(key=lambda s: (s.initial_weight, s.date))
selected_teams = complete_teams[:self.team_count]
selected_teams.sort(key=lambda s: s.initial_weight)
if seeds_count > 0:
bracket_teams = selected_teams[:seeds_count] + wildcard_bracket
else:
bracket_teams = []
if group_stage_members_count:
group_stage_teams = selected_teams[-group_stage_members_count:] + wildcard_group_stage
else:
group_stage_teams = []
waiting_list_count = len(complete_teams) - self.team_count
if waiting_list_count < 0:
waiting_list_count = 0
if waiting_list_count > 0:
waiting_teams = complete_teams[-waiting_list_count:]
if self.team_sorting == TeamSortingType.INSCRIPTION_DATE:
waiting_teams.sort(key=lambda s: (s.date, s.initial_weight))
else:
waiting_teams.sort(key=lambda s: (s.initial_weight, s.date))
else:
waiting_teams = []
bracket_teams.sort(key=lambda s: s.weight)
group_stage_teams.sort(key=lambda s: s.weight)
for team in bracket_teams:
team.set_stage("Tableau")
for team in group_stage_teams:
team.set_stage("Poule")
for team in waiting_teams:
team.set_stage("Attente")
final_teams = bracket_teams + group_stage_teams + waiting_teams
return final_teams
def match_groups(self, broadcasted, group_stage_id, round_id):
match_groups = []
if group_stage_id:
group_stage = self.groupstage_set.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.round_set.filter(id=round_id).first()
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 = []
for round in self.round_set.filter(parent=None).all().order_by('index'):
groups.extend(self.round_match_groups(round, broadcasted, hide_empty_matches=True))
for group_stage in self.groupstage_set.all():
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):
matches = group_stage.match_set.all()
if hide_empty_matches:
matches = [m for m in matches if m.should_appear()]
if matches:
return self.create_match_group(group_stage.display_name(), matches, broadcasted)
else:
return None
def round_match_groups(self, round, broadcasted, hide_empty_matches):
groups = []
matches = round.match_set.order_by('index').all()
if hide_empty_matches:
matches = [m for m in matches if m.should_appear()]
if matches:
group = self.create_match_group(round.name(), round.match_set.all(), broadcasted)
groups.append(group)
ranking_matches = round.ranking_matches(hide_empty_matches)
if len(ranking_matches) > 0:
group = self.create_match_group('Matchs de classement', ranking_matches, broadcasted)
groups.append(group)
return groups
def create_match_group(self, name, matches, broadcasted):
matches = list(matches)
# if not broadcasted:
# matches = [m for m in matches if m.should_appear()]
matches.sort(key=lambda m: m.index)
live_matches = [match.live_match() for match in matches]
return MatchGroup(name, live_matches)
def live_group_stages(self):
group_stages = list(self.groupstage_set.all())
group_stages.sort(key=lambda gs: gs.index)
return [gs.live_group_stages() for gs in group_stages]
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.
if datetime.now().date() < self.start_date.date():
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,
}
else:
live_matches_dicts = [match.live_match().to_dict() for match in matches]
return {
'matches': live_matches_dicts,
'group_stages': [],
'summons': team_summons_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': [],
}
def broadcasted_matches_and_group_stages(self):
matches = []
group_stages = []
if self.group_stages_running():
group_stages = self.live_group_stages()
matches = self.group_stages_matches()
else:
# last_started_match = self.first_unfinished_match()
current_round = self.round_to_show()
previous_round = self.round_for_index(current_round.index + 1)
if previous_round:
matches.extend(current_round.get_matches_recursive(True))
matches.extend(previous_round.get_matches_recursive(True))
else:
matches.extend(current_round.all_matches(True))
group_stages = self.live_group_stages()
return matches, group_stages
def all_matches(self, hide_empty_matches):
matches = []
for round in self.round_set.all():
matches.extend(round.all_matches(hide_empty_matches))
for group_stage in self.groupstage_set.all():
matches.extend(group_stage.match_set.all())
if hide_empty_matches:
matches = [m for m in matches if m.should_appear()]
return matches
def group_stage_matches(self):
matches = []
for group_stage in self.groupstage_set.all():
matches.extend(group_stage.match_set.all())
return matches
def group_stages_running(self):
if len(self.groupstage_set.all()) > 0:
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]
matches.sort(key=lambda m: m.start_date)
if matches:
return matches[0]
else:
return None
def round_to_show(self):
last_started_match = self.first_unfinished_match()
if last_started_match:
current_round = last_started_match.round.root_round()
if current_round:
return current_round
main_rounds = list(self.round_set.filter(parent=None).all())
main_rounds.sort(key=lambda r: r.index)
if main_rounds:
return main_rounds[0]
else:
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]
def round_for_index(self, index):
return self.round_set.filter(index=index,parent=None).first()
def group_stages_matches(self):
matches = []
for group_stage in self.groupstage_set.all():
matches.extend(group_stage.match_set.all())
matches = [m for m in matches if m.should_appear()]
matches.sort(key=lambda m: m.start_date, reverse=True)
return matches
def display_rankings(self):
if self.end_date is not None:
return True
return False
def display_teams(self):
if self.end_date is not None:
return True
if self.publish_teams:
return True
if datetime.now().date() >= self.start_date.date():
return True
return False
def display_summons(self):
if self.end_date is not None:
return False
if self.publish_summons:
return True
if datetime.now().date() >= self.start_date.date():
return True
return False
def display_group_stages(self):
if self.end_date is not None:
return True
if len(self.groupstage_set.all()) == 0:
return False
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 False
group_stage_start_date = self.getEightAm(first_group_stage_start_date)
if group_stage_start_date < self.start_date:
group_stage_start_date = self.start_date
if datetime.now().date() >= group_stage_start_date.date():
return True
return False
def group_stage_start_date(self):
group_stages = [gs for gs in self.groupstage_set.all() if gs.start_date is not None]
if len(group_stages) == 0:
return None
return min(group_stages, key=lambda gs: gs.start_date).start_date
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 False
bracket_start_date = self.getEightAm(first_match_start_date)
if bracket_start_date < self.start_date:
bracket_start_date = self.start_date
if datetime.now().date() >= bracket_start_date.date():
return True
return False
def bracket_matches(self):
matches = []
for round in self.round_set.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).start_date
def getEightAm(self, date):
return date.replace(hour=8, minute=0, second=0, microsecond=0)
def supposedly_in_progress(self):
end = self.start_date + timedelta(days=self.day_duration)
return timezone.now() > self.start_date.replace(hour=0, minute=0) and timezone.now() < end
class MatchGroup:
def __init__(self, name, matches):
self.name = name
self.matches = matches
def add_match(self, match):
self.matches.append(match)
def add_matches(self, matches):
self.matches = matches
class TeamSummon:
def __init__(self, names, date, weight, stage, image):
self.names = names
self.date = date
self.weight = weight
self.stage = stage
self.image = image
def formatted_date(self):
if self.date:
timezoned_datetime = timezone.localtime(self.date)
return formats.date_format(timezoned_datetime, format='H:i')
else:
return None
def to_dict(self):
return {
"names": self.names,
"date": self.formatted_date(),
"weight": self.weight,
"stage": self.stage,
"image": self.image,
}
class TeamList:
def __init__(self, names, weight, date, initial_weight, image):
self.names = names
self.date = date
self.weight = weight
self.initial_weight = initial_weight
self.image = image
self.stage = ""
def set_stage(self, stage):
self.stage = stage
def to_dict(self):
return {
"names": self.names,
"date": self.date,
"weight": self.weight,
"initial_weight": self.initial_weight,
"image": self.image,
"stage": self.stage,
}
class TeamRanking:
def __init__(self, names, ranking, points, image):
self.names = names
self.ranking = ranking
self.points = points
self.image = image
def ranking_display(self):
return self.ordinal(self.ranking)
def points_earned_display(self):
if self.points is None:
return ""
return f"+{self.points} pt{self.plural_suffix(self.points)}"
def plural_suffix(self, n):
if n > 1:
return 's'
else:
return ''
def ordinal(self, n):
suffixes = {1: 'er', 2: 'ème', 3: 'rd'}
if n == 1:
suffix = 'er'
else:
suffix = 'ème'
return str(n) + suffix
def to_dict(self):
return {
"names": self.names,
"ranking": self.ranking_display(),
"points": self.points_earned_display(),
"image": self.image,
}