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 import uuid from django.utils import timezone, formats from datetime import datetime, timedelta from .. import config_local from ..utils.cryptography import decrypt_aes_gcm 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.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) publish_tournament = models.BooleanField(default=False) hide_points_earned = models.BooleanField(default=False) publish_rankings = 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 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.name: components.append(self.event.name) elif self.name: components.append(self.name) return (' ').join(components) 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 formatted_start_date(self): return self.start_date.strftime("%d/%m/%y") def in_progress(self): return self.end_date is None def creator(self): return self.event.creator.username 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() if teams is not None and len(teams) > 0: 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() if self.supposedly_in_progress() or self.end_date is not None: 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 if teams is not None and len(teams) > 0: word = "inscription" if len(teams) > 1: word = word + "s" return f"{len(teams)} {word}" else: 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 = [] 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 r: r.ranking) return rankings def teams(self): bracket_teams = [] group_stage_teams = [] waiting_teams = [] teams = [] wildcard_bracket = [] wildcard_group_stage = [] complete_teams = [] closed_registration_date = self.closed_registration_date for team_registration in self.teamregistration_set.all(): is_valid = False if closed_registration_date is not None and team_registration.registration_date is not None and team_registration.registration_date <= closed_registration_date: is_valid = True if closed_registration_date is None: is_valid = True if team_registration.registration_date is None: is_valid = True 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) if team_registration.group_stage_position is not None: team.set_stage("Poule") elif team_registration.bracket_position is not None: team.set_stage("Tableau") else: 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) elif is_valid is True: complete_teams.append(team) else: waiting_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 or len(waiting_teams) > 0: if waiting_list_count > 0: waiting_teams = 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: if team.stage == "Attente": team.set_stage("Tableau") for team in group_stage_teams: if team.stage == "Attente": 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() if round: 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)) if self.display_group_stages(): for group_stage in self.groupstage_set.all().order_by('index'): 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()] else: matches = [m for m in matches if m.disabled is False] 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.match_set.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 matches: 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: group = self.create_match_group('Matchs de classement', ranking_matches) groups.append(group) return groups def create_match_group(self, name, matches): matches = list(matches) 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 len(self.groupstage_set.all()) > 0 and self.no_bracket_match_has_started(): group_stages = self.live_group_stages() 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: # Add full matches from the next rounds next_round = self.round_for_index(current_round.index - 1) if next_round: matches.extend(next_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: 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 no_bracket_match_has_started(self): matches = [] for round in self.round_set.all(): for match in round.match_set.all(): if match.started(): return False return True 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()) 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: # 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] 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 first_round(self): main_rounds = list(self.round_set.filter(parent=None)) main_rounds.sort(key=lambda r: r.index, reverse=True) return main_rounds[0] 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.match_set.all()) matches = [m for m in matches if m.should_appear()] matches.sort(key=lambda m: (m.start_date is None, m.end_date is not None, m.start_date, m.index)) return matches def elected_broadcast_group_stages(self): group_stages = list(self.groupstage_set.all()) 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.publish_rankings is True and 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: if self.is_canceled is True: return is_build_and_not_empty return True if is_build_and_not_empty is True: if datetime.now().date() >= self.start_date.date(): return True minimum_publish_date = self.creation_date.replace(hour=9, minute=0) + timedelta(days=1) return timezone.now() >= minimum_publish_date return False def display_teams(self): if self.end_date is not None: return True if self.publish_teams: return True if timezone.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 timezone.now() >= self.start_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 timezone.now() >= self.start_date else: return timezone.now() >= first_group_stage_start_date 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 datetime.now().date() >= self.start_date.date() 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 + 1) return self.start_date.replace(hour=0, minute=0) <= timezone.now() <= end 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.federal_level_category == FederalLevelCategory.UNLISTED def is_build_and_not_empty(self): return (len(self.groupstage_set.all()) > 0 or len(self.round_set.all()) > 0) and len(self.teamregistration_set.all()) >= 4 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.groupstage_set.all(): if group_stage.has_at_least_one_started_match() == False: return False return True 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='l 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.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): 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, "formatted_ranking": self.formatted_ranking, "points": self.points, "image": self.image, }