sync
Laurent 9 months ago
commit 75994cc86c
  1. 1
      requirements.txt
  2. 11
      tournaments/admin.py
  3. 35
      tournaments/filters.py
  4. 2
      tournaments/models/club.py
  5. 11
      tournaments/models/custom_user.py
  6. 20
      tournaments/models/enums.py
  7. 2
      tournaments/models/group_stage.py
  8. 17
      tournaments/models/match.py
  9. 44
      tournaments/models/team_registration.py
  10. 263
      tournaments/models/tournament.py
  11. 10
      tournaments/signals.py
  12. 1
      tournaments/static/misc/required-version.txt
  13. 16
      tournaments/static/tournaments/css/style.css
  14. 10
      tournaments/templates/tournaments/broadcast/broadcasted_match.html
  15. 10
      tournaments/templates/tournaments/broadcast/broadcasted_ranking.html
  16. 10
      tournaments/templates/tournaments/match_cell.html
  17. 12
      tournaments/templates/tournaments/team_stats.html
  18. 3
      tournaments/templates/tournaments/tournament_info.html
  19. 60
      tournaments/views.py

@ -14,3 +14,4 @@ pandas==2.2.2
xlrd==2.0.1 xlrd==2.0.1
openpyxl==3.1.5 openpyxl==3.1.5
django-filter==24.3 django-filter==24.3
cryptography==41.0.7

@ -5,7 +5,7 @@ from django.utils import timezone
from .models import Club, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, Court, DateInterval, FailedApiCall, Log, DeviceToken, DrawLog, UnregisteredTeam, UnregisteredPlayer from .models import Club, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, Court, DateInterval, FailedApiCall, Log, DeviceToken, DrawLog, UnregisteredTeam, UnregisteredPlayer
from .forms import CustomUserCreationForm, CustomUserChangeForm from .forms import CustomUserCreationForm, CustomUserChangeForm
from .filters import TeamScoreTournamentListFilter, MatchTournamentListFilter, SimpleTournamentListFilter, MatchTypeListFilter, SimpleIndexListFilter from .filters import TeamScoreTournamentListFilter, MatchTournamentListFilter, SimpleTournamentListFilter, MatchTypeListFilter, SimpleIndexListFilter, StartDateRangeFilter
from sync.admin import SyncedObjectAdmin from sync.admin import SyncedObjectAdmin
@ -46,12 +46,12 @@ class EventAdmin(SyncedObjectAdmin):
class TournamentAdmin(SyncedObjectAdmin): class TournamentAdmin(SyncedObjectAdmin):
list_display = ['display_name', 'event', 'is_private', 'start_date', 'payment', 'creator'] list_display = ['display_name', 'event', 'is_private', 'start_date', 'payment', 'creator']
list_filter = ['is_deleted', 'event__creator'] list_filter = [StartDateRangeFilter, 'is_deleted', 'event__creator']
ordering = ['-start_date'] ordering = ['-start_date']
search_fields = ['id'] search_fields = ['id']
class TeamRegistrationAdmin(SyncedObjectAdmin): class TeamRegistrationAdmin(SyncedObjectAdmin):
list_display = ['player_names', 'group_stage_position', 'name', 'tournament'] list_display = ['player_names', 'group_stage_position', 'name', 'tournament', 'registration_date']
list_filter = [SimpleTournamentListFilter] list_filter = [SimpleTournamentListFilter]
search_fields = ['id'] search_fields = ['id']
@ -68,8 +68,8 @@ class RoundAdmin(SyncedObjectAdmin):
class PlayerRegistrationAdmin(SyncedObjectAdmin): class PlayerRegistrationAdmin(SyncedObjectAdmin):
list_display = ['first_name', 'last_name', 'licence_id', 'rank'] list_display = ['first_name', 'last_name', 'licence_id', 'rank']
search_fields = ('id', 'first_name', 'last_name') search_fields = ('id', 'first_name', 'last_name', 'licence_id__icontains')
list_filter = [TeamScoreTournamentListFilter] list_filter = ['registered_online', TeamScoreTournamentListFilter]
ordering = ['last_name', 'first_name'] ordering = ['last_name', 'first_name']
class MatchAdmin(SyncedObjectAdmin): class MatchAdmin(SyncedObjectAdmin):
@ -109,6 +109,7 @@ class LogAdmin(admin.ModelAdmin):
class DeviceTokenAdmin(admin.ModelAdmin): class DeviceTokenAdmin(admin.ModelAdmin):
list_display = ['user', 'value'] list_display = ['user', 'value']
list_filter = ['user']
class DrawLogAdmin(SyncedObjectAdmin): class DrawLogAdmin(SyncedObjectAdmin):
list_display = ['tournament', 'draw_date', 'draw_seed', 'draw_match_index', 'draw_team_position'] list_display = ['tournament', 'draw_date', 'draw_seed', 'draw_match_index', 'draw_team_position']

@ -2,6 +2,8 @@ from django.contrib import admin
from .models import Club, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, Court, DateInterval, FailedApiCall from .models import Club, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, Court, DateInterval, FailedApiCall
from django.db.models import Q from django.db.models import Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from datetime import timedelta
import uuid import uuid
from enum import Enum from enum import Enum
@ -101,3 +103,36 @@ class SimpleIndexListFilter(admin.SimpleListFilter):
return queryset.filter(index=self.value()) return queryset.filter(index=self.value())
else: else:
return queryset return queryset
class StartDateRangeFilter(admin.SimpleListFilter):
title = 'tournament time range' # displayed in the admin UI
parameter_name = 'date_range' # URL parameter
def lookups(self, request, model_admin):
return (
('upcoming', 'Next 30 days'),
('recent', 'Last 30 days'),
('current', 'Current (±3 days)'),
)
def queryset(self, request, queryset):
if not self.value():
return queryset
today = timezone.now().date()
if self.value() == 'upcoming':
return queryset.filter(
start_date__gte=today,
start_date__lte=today + timedelta(days=30)
)
elif self.value() == 'recent':
return queryset.filter(
start_date__gte=today - timedelta(days=30),
start_date__lte=today
)
elif self.value() == 'current':
return queryset.filter(
start_date__gte=today - timedelta(days=3),
start_date__lte=today + timedelta(days=3)
)

@ -34,7 +34,7 @@ class Club(BaseModel):
return self.name return self.name
def events_count(self): def events_count(self):
return len(self.events.all()) return self.events.count()
def court_name(self, index): def court_name(self, index):
for court in self.courts.all(): for court in self.courts.all():

@ -1,4 +1,5 @@
from django.db import models from django.db import models
from django.apps import apps
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.utils.timezone import now from django.utils.timezone import now
@ -59,7 +60,7 @@ class CustomUser(AbstractUser):
return f"{self.username} : {self.first_name} {self.last_name} | {self.email} | {self.phone}" return f"{self.username} : {self.first_name} {self.last_name} | {self.email} | {self.phone}"
def event_count(self): def event_count(self):
return len(self.events.all()) return self.events.count()
def full_name(self): def full_name(self):
return f"{self.first_name} {self.last_name}" return f"{self.first_name} {self.last_name}"
@ -68,4 +69,12 @@ class CustomUser(AbstractUser):
latest_event = self.events.order_by('-creation_date').first() latest_event = self.events.order_by('-creation_date').first()
if latest_event and latest_event.club: if latest_event and latest_event.club:
return latest_event.club.name return latest_event.club.name
if self.licence_id:
PlayerRegistration = apps.get_model('tournaments', 'PlayerRegistration')
player_registration = PlayerRegistration.objects.filter(licence_id=self.licence_id).order_by('team_registration__registration_date').first()
try:
return player_registration.team_registration.tournament.event.club.name
except AttributeError:
return None
return None return None

@ -96,6 +96,26 @@ class FederalMatchCategory(models.IntegerChoices):
SINGLE_SET_OF_FOUR_GAMES = 13, 'Single set of four games' SINGLE_SET_OF_FOUR_GAMES = 13, 'Single set of four games'
SINGLE_SET_OF_FOUR_GAMES_DECISIVE_POINT = 14, 'Single set of four games with decisive point' SINGLE_SET_OF_FOUR_GAMES_DECISIVE_POINT = 14, 'Single set of four games with decisive point'
@property
def format_label_short(self):
format_mapping = {
self.TWO_SETS: "A1",
self.TWO_SETS_SUPER_TIE: "B1",
self.TWO_SETS_FOUR_GAME: "C1",
self.NINE_GAMES: "D1",
self.SUPER_TIE: "E",
self.TWO_SETS_OF_SUPER_TIE: "G",
self.MEGA_TIE: "F",
self.SINGLE_SET: "H1",
self.SINGLE_SET_DECISIVE_POINT: "H2",
self.TWO_SETS_DECISIVE_POINT: "A2",
self.TWO_SETS_DECISIVE_POINT_SUPER_TIE: "B2",
self.TWO_SETS_FOUR_GAME_DECISIVE_POINT: "C2",
self.NINE_GAMES_DECISIVE_POINT: "D2",
self.SINGLE_SET_OF_FOUR_GAMES: "I1"
}
return format_mapping.get(self, "")
def last_set_is_tie_break(value): def last_set_is_tie_break(value):
if value == FederalMatchCategory.TWO_SETS_FOUR_GAME or value == FederalMatchCategory.TWO_SETS_FOUR_GAME_DECISIVE_POINT or value == FederalMatchCategory.TWO_SETS_SUPER_TIE or value == FederalMatchCategory.SUPER_TIE or value == FederalMatchCategory.MEGA_TIE or value == FederalMatchCategory.TWO_SETS_DECISIVE_POINT_SUPER_TIE: if value == FederalMatchCategory.TWO_SETS_FOUR_GAME or value == FederalMatchCategory.TWO_SETS_FOUR_GAME_DECISIVE_POINT or value == FederalMatchCategory.TWO_SETS_SUPER_TIE or value == FederalMatchCategory.SUPER_TIE or value == FederalMatchCategory.MEGA_TIE or value == FederalMatchCategory.TWO_SETS_DECISIVE_POINT_SUPER_TIE:
return True return True

@ -156,7 +156,7 @@ class GroupStage(SideStoreModel):
return False return False
def is_completed(self): def is_completed(self):
return not self.matches.filter(end_date__isnull=True).exists() return not self.matches.filter(end_date__isnull=True).exists() and self.matches.count() > 0
class LiveGroupStage: class LiveGroupStage:
def __init__(self, title, step, index): def __init__(self, title, step, index):

@ -97,7 +97,7 @@ class Match(SideStoreModel):
previous_index = self.round.index + 1 previous_index = self.round.index + 1
# Check if the next index is within the bounds of the rounds array # Check if the next index is within the bounds of the rounds array
if previous_index < len(self.round.tournament.rounds.all()): 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 = None).first()
# Return None or an appropriate value if the index is out of bounds # Return None or an appropriate value if the index is out of bounds
@ -149,6 +149,9 @@ class Match(SideStoreModel):
team = Team(None, image, names, scores, weight, is_winner, walk_out) team = Team(None, image, names, scores, weight, is_winner, walk_out)
return team return team
def is_ready(self):
return self.team_scores.count() == 2
def live_teams(self): def live_teams(self):
#print('player names from match') #print('player names from match')
##return map(lambda ts: ts.player_names(), self.team_scores.all()) ##return map(lambda ts: ts.player_names(), self.team_scores.all())
@ -160,7 +163,7 @@ class Match(SideStoreModel):
previous_top_match = self.precedent_match(True) previous_top_match = self.precedent_match(True)
previous_bottom_match = self.precedent_match(False) previous_bottom_match = self.precedent_match(False)
if len(team_scores) == 0: if len(team_scores) == 0:
if (self.round and len(self.round.tournament.rounds.all()) == self.round.index -1): if (self.round and self.round.tournament.rounds.count() == self.round.index -1):
return teams return teams
if (self.group_stage): if (self.group_stage):
return teams return teams
@ -292,7 +295,7 @@ class Match(SideStoreModel):
def should_appear(self): def should_appear(self):
if self.disabled is True: if self.disabled is True:
return False return False
return len(self.team_scores.all()) > 0 return self.team_scores.count() > 0
# elif self.group_stage is None: # elif self.group_stage is None:
# if len(self.team_scores.all()) == 2: # if len(self.team_scores.all()) == 2:
@ -330,7 +333,9 @@ class Match(SideStoreModel):
group_stage_name = self.group_stage.display_name() group_stage_name = self.group_stage.display_name()
ended = self.end_date is not None ended = self.end_date is not None
livematch = LiveMatch(title, date, time_indication, court, self.started(), ended, group_stage_name) live_format = "Format " + FederalMatchCategory(self.format).format_label_short
livematch = LiveMatch(title, date, time_indication, court, self.started(), ended, group_stage_name, live_format)
for team in self.live_teams(): for team in self.live_teams():
livematch.add_team(team) livematch.add_team(team)
@ -381,7 +386,7 @@ class Team:
} }
class LiveMatch: class LiveMatch:
def __init__(self, title, date, time_indication, court, started, ended, group_stage_name): def __init__(self, title, date, time_indication, court, started, ended, group_stage_name, format):
self.title = title self.title = title
self.date = date self.date = date
self.teams = [] self.teams = []
@ -391,6 +396,7 @@ class LiveMatch:
self.ended = ended self.ended = ended
self.has_walk_out = False self.has_walk_out = False
self.group_stage_name = group_stage_name self.group_stage_name = group_stage_name
self.format = format
def add_team(self, team): def add_team(self, team):
self.teams.append(team) self.teams.append(team)
@ -408,6 +414,7 @@ class LiveMatch:
"ended": self.ended, "ended": self.ended,
"has_walk_out": self.has_walk_out, "has_walk_out": self.has_walk_out,
"group_stage_name": self.group_stage_name, "group_stage_name": self.group_stage_name,
"format": self.format
} }
def show_time_indication(self): def show_time_indication(self):

@ -123,7 +123,7 @@ class TeamRegistration(SideStoreModel):
self.save() # Save the updated weight if necessary self.save() # Save the updated weight if necessary
def is_valid_for_summon(self): def is_valid_for_summon(self):
return len(self.player_registrations.all()) > 0 return self.player_registrations.count() > 0 or self.name is not None
def initial_weight(self): def initial_weight(self):
if self.locked_weight is None: if self.locked_weight is None:
@ -186,10 +186,32 @@ class TeamRegistration(SideStoreModel):
'points_earned': self.get_points_earned(), 'points_earned': self.get_points_earned(),
'initial_stage': self.get_initial_stage(), 'initial_stage': self.get_initial_stage(),
'matches_played': self.count_matches_played(), 'matches_played': self.count_matches_played(),
'victory_ratio': self.calculate_victory_ratio() 'victory_ratio': self.calculate_victory_ratio(),
'team_rank': self.team_rank_label(),
'total_teams': self.total_teams(),
} }
return stats return stats
def team_rank(self):
teams = self.tournament.teams(False) # Get list of TeamItem objects
try:
# Find the TeamItem that corresponds to this TeamRegistration
team_index = next(i for i, team in enumerate(teams)
if team.team_registration.id == self.id)
return team_index + 1
except (StopIteration, ValueError):
return None
def team_rank_label(self):
team_rank = self.team_rank()
if team_rank is None:
return "--"
return f"{team_rank}"
def total_teams(self):
teams = self.tournament.teams(False)
return f"{len(teams)}"
def get_initial_stage(self): def get_initial_stage(self):
matches = self.get_matches().order_by('start_date') matches = self.get_matches().order_by('start_date')
first_match = matches.first() first_match = matches.first()
@ -204,10 +226,22 @@ class TeamRegistration(SideStoreModel):
def get_final_ranking(self): def get_final_ranking(self):
if self.final_ranking: if self.final_ranking:
if self.final_ranking == 1: if self.final_ranking == 1:
return "1er" return "1er" + self.ranking_delta()
return f"{self.final_ranking}e" return f"{self.final_ranking}ème" + self.ranking_delta()
return None return None
def ranking_delta(self):
team_rank = self.team_rank()
if team_rank is None or self.final_ranking is None:
return ""
sign = "-"
if team_rank > self.final_ranking:
sign = "+"
if team_rank == self.final_ranking:
sign = ""
return f" ({sign}"+f"{abs(self.final_ranking - team_rank)})"
def get_points_earned(self): def get_points_earned(self):
return self.points_earned return self.points_earned
@ -234,6 +268,6 @@ class TeamRegistration(SideStoreModel):
total_matches = matches.count() total_matches = matches.count()
if total_matches > 0: if total_matches > 0:
wins = matches.filter(winning_team_id=self.id).count() wins = matches.filter(winning_team_id=self.id).count()
ratio = (wins / total_matches) * 100 # ratio = (wins / total_matches) * 100
return f"{wins}/{total_matches}" return f"{wins}/{total_matches}"
return None return None

@ -229,7 +229,7 @@ class Tournament(BaseModel):
return "Annulé" return "Annulé"
teams = self.teams(True) teams = self.teams(True)
if self.supposedly_in_progress() or self.end_date is not None: 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"] teams = [t for t in teams if t.stage != "Attente"]
if teams is not None and len(teams) > 0: if teams is not None and len(teams) > 0:
word = "équipe" word = "équipe"
@ -337,126 +337,94 @@ class Tournament(BaseModel):
print("else", index, self.team_count) print("else", index, self.team_count)
return -1 return -1
def teams(self, includeWaitingList): def teams(self, include_waiting_list):
# print("Starting teams method") """
bracket_teams = [] Get sorted list of teams for the tournament.
group_stage_teams = []
waiting_teams = []
teams = []
wildcard_bracket = []
wildcard_group_stage = []
complete_teams = []
closed_registration_date = self.closed_registration_date
# print(f"Closed registration date: {closed_registration_date}")
for team_registration in self.team_registrations.all(): Args:
# print(f"Processing team registration: {team_registration}") include_waiting_list (bool): Whether to include teams in waiting list
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
# print(f"Is valid: {is_valid}")
if team_registration.out_of_tournament() is False:
team = TeamItem(team_registration)
# print(f"Created team: {team}")
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")
# print(f"Team stage: {team.stage}")
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)
# print(f"Total teams: {len(teams)}")
# print(f"Wildcard bracket: {len(wildcard_bracket)}")
# print(f"Wildcard group stage: {len(wildcard_group_stage)}")
# print(f"Complete teams: {len(complete_teams)}")
# print(f"Waiting teams: {len(waiting_teams)}")
if len(teams) < self.team_count:
teams.sort(key=lambda s: (s.initial_weight, s.team_registration.id))
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
# print(f"Seeds count: {seeds_count}")
# print(f"Group stage members count: {group_stage_members_count}")
if self.team_sorting == TeamSortingType.INSCRIPTION_DATE: Returns:
complete_teams.sort(key=lambda s: (s.registration_date is None, s.registration_date or datetime.min, s.initial_weight, s.team_registration.id)) list: List of TeamItem objects sorted according to tournament rules
else: """
complete_teams.sort(key=lambda s: (s.initial_weight, s.team_registration.id)) # Initialize team categories
complete_teams = []
selected_teams = complete_teams[:self.team_count] wildcard_bracket = []
selected_teams.sort(key=lambda s: (s.initial_weight, s.team_registration.id)) wildcard_group_stage = []
waiting_teams = []
if seeds_count > 0: # Get registration cutoff date
bracket_teams = selected_teams[:seeds_count] + wildcard_bracket closed_date = self.closed_registration_date
else:
bracket_teams = [] # Process each team registration
# print(f"Bracket teams: {len(bracket_teams)}") for team_reg in self.team_registrations.all():
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)
)
# 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)
if group_stage_members_count: # Set initial stage
group_stage_end = seeds_count + group_stage_members_count if team_reg.group_stage_position is not None:
group_stage_teams = selected_teams[seeds_count:group_stage_end] + wildcard_group_stage team.set_stage("Poule")
else: elif team_reg.bracket_position is not None:
group_stage_teams = [] team.set_stage("Tableau")
# print(f"Group stage teams: {len(group_stage_teams)}")
waiting_list_count = len(teams) - self.team_count
if waiting_list_count < 0:
waiting_list_count = 0
# print(f"Waiting list count: {waiting_list_count}")
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.registration_date is None, s.registration_date or datetime.min, s.initial_weight, s.team_registration.id))
else: else:
waiting_teams.sort(key=lambda s: (s.initial_weight, s.team_registration.id)) team.set_stage("Attente")
else:
waiting_teams = []
# print(f"Final waiting teams: {len(waiting_teams)}")
bracket_teams.sort(key=lambda s: (s.weight, s.team_registration.id)) # Sort teams based on tournament rules
group_stage_teams.sort(key=lambda s: (s.weight, s.team_registration.id)) 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))
for team in bracket_teams: # Determine final team list based on tournament settings
if team.stage == "Attente": if len(complete_teams) <= self.team_count:
team.set_stage("Tableau") all_teams = wildcard_bracket + wildcard_group_stage + complete_teams
if include_waiting_list:
all_teams.extend(waiting_teams)
return all_teams
for team in group_stage_teams: # Split teams into main bracket and waiting list
if team.stage == "Attente": qualified_teams = complete_teams[:self.team_count]
team.set_stage("Poule") excess_teams = complete_teams[self.team_count:]
for team in waiting_teams: # Combine all waiting list teams
team.set_stage("Attente") waiting_list = excess_teams + waiting_teams
if includeWaitingList is True: # Return final sorted list
final_teams = bracket_teams + group_stage_teams + waiting_teams if include_waiting_list:
# print(f"Final teams with waiting list: {len(final_teams)}") return wildcard_bracket + wildcard_group_stage + qualified_teams + waiting_list
else: return wildcard_bracket + wildcard_group_stage + qualified_teams
final_teams = bracket_teams + group_stage_teams
# print(f"Final teams without waiting list: {len(final_teams)}")
return final_teams
def match_groups(self, broadcasted, group_stage_id, round_id): def match_groups(self, broadcasted, group_stage_id, round_id):
@ -660,7 +628,7 @@ class Tournament(BaseModel):
matches = [] matches = []
group_stages = [] group_stages = []
if len(self.group_stages.all()) > 0 and self.no_bracket_match_has_started(): 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()] group_stages = [gs.live_group_stages() for gs in self.last_group_stage_step()]
matches = self.broadcasted_group_stages_matches() matches = self.broadcasted_group_stages_matches()
first_round = self.first_round() first_round = self.first_round()
@ -750,24 +718,24 @@ class Tournament(BaseModel):
# all started matches have ended, possibly # all started matches have ended, possibly
last_finished_match = self.last_finished_match() last_finished_match = self.last_finished_match()
round = last_finished_match.round if last_finished_match:
if round is None: # when the last finished match is in the group stage round = last_finished_match.round
round = self.rounds.filter(parent__isnull=True).order_by('-index').first() 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}') if round:
round_root_index = round.root_round().index # print(f'last_finished_match = {last_finished_match.name}')
# print(f'round_index = {round_root_index}') round_root_index = round.root_round().index
if round_root_index == 0: # print(f'round_index = {round_root_index}')
return round if round_root_index == 0:
else:
round = self.round_set.filter(parent=None,index=round_root_index-1).first()
if round:
return round return round
else: else:
return None round = self.rounds.filter(parent=None,index=round_root_index-1).first()
else: if round:
return None return round
else:
return None
return None
def last_started_match(self): def last_started_match(self):
matches = [m for m in self.all_matches(False) if m.start_date] matches = [m for m in self.all_matches(False) if m.start_date]
@ -843,7 +811,7 @@ class Tournament(BaseModel):
return False return False
def has_team_registrations(self): def has_team_registrations(self):
return len(self.team_registrations.all()) > 0 return self.team_registrations.count() > 0
def display_summons(self): def display_summons(self):
if self.end_date is not None: if self.end_date is not None:
@ -938,6 +906,35 @@ class Tournament(BaseModel):
return start <= now <= end return start <= now <= end
def should_be_over(self):
if self.end_date is not None:
return True
timezoned_datetime = timezone.localtime(self.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
if self.group_stages.count() > 0:
# Check if all group stages are completed
for group_stage in self.group_stages.all():
if group_stage.is_completed():
return True
# If no group stages, check semi-finals
if self.rounds.count() > 0:
# Get round with index 1 (semi-finals) and no parent
semifinals = self.rounds.filter(index=1, parent=None).first()
if semifinals:
# Check if any match in semi-finals has started
for match in semifinals.matches.all():
if match.start_date is not None and match.is_ready():
return True
return False
return False
def display_points_earned(self): def display_points_earned(self):
return self.federal_level_category != FederalLevelCategory.UNLISTED and self.hide_points_earned is False return self.federal_level_category != FederalLevelCategory.UNLISTED and self.hide_points_earned is False
@ -946,7 +943,7 @@ class Tournament(BaseModel):
return self.hide_teams_weight return self.hide_teams_weight
def is_build_and_not_empty(self): def is_build_and_not_empty(self):
return (len(self.group_stages.all()) > 0 or len(self.rounds.all()) > 0) and len(self.team_registrations.all()) >= 4 return self.group_stages.count() > 0 or self.rounds.count() > 0 and self.team_registrations.count() >= 4
def day_duration_formatted(self): def day_duration_formatted(self):
return plural_format("jour", self.day_duration) return plural_format("jour", self.day_duration)
@ -1028,7 +1025,7 @@ class Tournament(BaseModel):
# Check target team count and waiting list limit # Check target team count and waiting list limit
if self.team_count is not None: if self.team_count is not None:
current_team_count = len([tr for tr in self.team_registrations.all() if tr.out_of_tournament() is False]) current_team_count = self.team_registrations.exclude(walk_out=True).count()
if current_team_count >= self.team_count: if current_team_count >= self.team_count:
if self.waiting_list_limit is not None: if self.waiting_list_limit is not None:
waiting_list_count = current_team_count - self.team_count waiting_list_count = current_team_count - self.team_count
@ -1058,7 +1055,7 @@ class Tournament(BaseModel):
if self.team_count is not None: if self.team_count is not None:
# Get all team registrations excluding walk_outs # Get all team registrations excluding walk_outs
current_team_count = len([tr for tr in self.team_registrations.all() if tr.out_of_tournament() is False]) current_team_count = self.team_registrations.exclude(walk_out=True).count()
if current_team_count >= self.team_count: if current_team_count >= self.team_count:
if self.waiting_list_limit is not None: if self.waiting_list_limit is not None:
waiting_list_count = current_team_count - self.team_count waiting_list_count = current_team_count - self.team_count
@ -1093,7 +1090,7 @@ class Tournament(BaseModel):
return -1 return -1
# Get count of active teams (not walked out) # Get count of active teams (not walked out)
current_team_count = len([tr for tr in self.team_registrations.all() if tr.out_of_tournament() is False]) 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 count is less than target count, next team is not in waiting list
if current_team_count < self.team_count: if current_team_count < self.team_count:

@ -52,11 +52,11 @@ def send_discord_message(webhook_url, content):
data = { data = {
"content": content "content": content
} }
response = requests.post(webhook_url, json=data) requests.post(webhook_url, json=data)
if response.status_code != 204: # if response.status_code != 204:
raise ValueError( # raise ValueError(
f'Error sending message to Discord webhook: {response.status_code}, {response.text}' # f'Error sending message to Discord webhook: {response.status_code}, {response.text}'
) # )
@receiver(pre_delete, sender=TeamRegistration) @receiver(pre_delete, sender=TeamRegistration)
def unregister_team(sender, instance, **kwargs): def unregister_team(sender, instance, **kwargs):

@ -246,6 +246,7 @@ tr {
font-family: "Anybody-ExtraBold"; font-family: "Anybody-ExtraBold";
font-size: 1.2em; font-size: 1.2em;
color: #1b223a; color: #1b223a;
line-height: 24px; /* Match the height of flex-row */
} }
.title { .title {
@ -816,9 +817,9 @@ h-margin {
} }
.download-button { .download-button {
margin-right: 6px; margin-right: 6px;
color: #1a223a; color: #fff7ed;
padding: 8px 12px; padding: 8px 12px;
background-color: white; background-color: #1a223a;
border-radius: 12px; border-radius: 12px;
text-decoration: none; text-decoration: none;
font-size: 12px; font-size: 12px;
@ -826,7 +827,7 @@ h-margin {
} }
.download-button:hover { .download-button:hover {
color: orange; color: #f39200;
} }
.match-result a { .match-result a {
@ -856,3 +857,12 @@ h-margin {
.group-stage-link:hover { .group-stage-link:hover {
color: #f39200; /* Or whatever hover color you prefer */ color: #f39200; /* Or whatever hover color you prefer */
} }
.tournament-info a {
color: #f39200;
text-decoration: underline;
font-weight: bold;
}
.tournament-info a:hover {
color: #f39200;
}

@ -2,9 +2,13 @@
<div class="flex-row"> <div class="flex-row">
<label class="left-label matchtitle"> <label class="left-label matchtitle">
<span x-text="match.group_stage_name"></span><span x-show="match.group_stage_name"> :</span> <span x-text="match.title"></span> <span x-text="match.group_stage_name"></span>
<span x-show="match.group_stage_name"> :</span>
<span x-text="match.title"></span>
</label> </label>
<!-- <label class="right-label info"><span x-text="match.date"></span></label> --> <label class="right-label minor-info bold" x-show="match.ended === false">
<span x-text="match.court"></span>
</label>
</div> </div>
<template x-for="i in match.teams.length"> <template x-for="i in match.teams.length">
@ -59,7 +63,7 @@
<div class="top-margin flex-row"> <div class="top-margin flex-row">
<label class="left-label minor-info bold"><span x-text="match.time_indication"></span></label> <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.court"></span></label> <label class="right-label minor-info semibold"><span x-text="match.format"></span></label>
</div> </div>
</div> </div>

@ -3,7 +3,11 @@
<div> <div>
<div class="table-row-3-colums-ranks"> <div class="table-row-3-colums-ranks">
<div class="table-cell"><div class="mybox center"><span x-text="ranking.formatted_ranking"></div></div> <div class="table-cell">
<div class="mybox center">
<span style="font-size: 1.4em;" x-text="ranking.formatted_ranking"></span>
</div>
</div>
<div class="table-cell table-cell-large padding-left semibold"> <div class="table-cell table-cell-large padding-left semibold">
<template x-for="name in ranking.names" > <template x-for="name in ranking.names" >
@ -11,7 +15,9 @@
</template> </template>
</div> </div>
{% if tournament.display_points_earned %} {% if tournament.display_points_earned %}
<div class="table-cell right horizontal-padding numbers"><span x-text="ranking.points"></div> <div class="table-cell right horizontal-padding numbers">
<span style="font-size: 1.4em;" x-text="ranking.points"></span>
</div>
{% endif %} {% endif %}
</div> </div>
<div x-show="index < column.length - 1"> <div x-show="index < column.length - 1">

@ -3,10 +3,12 @@
<div class="cell medium-12 large-3 my-block"> <div class="cell medium-12 large-3 my-block">
<div class="bubble"> <div class="bubble">
<!-- <div class="flex-row"> --> <div class="flex-row">
<label class="matchtitle">{{ match.title }}</label> <label class="matchtitle">{{ match.title }}</label>
<!-- <label class="right-label info">{{ match.date }}</label> --> {% if not match.ended %}
<!-- </div> --> <label class="right-label minor-info bold">{{ match.court }}</label>
{% endif %}
</div>
<div> <div>
@ -62,7 +64,7 @@
</label> </label>
<label class="right-label minor-info"> <label class="right-label minor-info">
{% if not match.ended %} {% if not match.ended %}
{{ match.court }} {{ match.format }}
{% endif %} {% endif %}
</label> </label>
<!-- <a href="" class="right-label">{{ match.court }}</a> --> <!-- <a href="" class="right-label">{{ match.court }}</a> -->

@ -16,6 +16,18 @@
</div> </div>
</div> </div>
<div class="match-result bottom-border">
<div class="player">
<div class="semibold">
<strong>Position initiale</strong>
</div>
</div>
<div class="scores">
<span class="score ws numbers">{{ stats.team_rank }} / {{ stats.total_teams }}</span>
</div>
</div>
{% if stats.final_ranking %} {% if stats.final_ranking %}
<div class="match-result bottom-border"> <div class="match-result bottom-border">
<div class="player"> <div class="player">

@ -99,7 +99,8 @@
{% if tournament.information %} {% if tournament.information %}
<p> <p>
<div class="semibold">Infos</div> <div class="semibold">Infos</div>
<div>{{ tournament.information|linebreaksbr }}</div> <div class="tournament-info">
{{ tournament.information|linebreaksbr|urlize }}</div>
</p> </p>
{% endif %} {% endif %}

@ -141,8 +141,8 @@ def tournaments_query(query, club_id, ascending):
return Tournament.objects.filter(*queries).order_by(sortkey) return Tournament.objects.filter(*queries).order_by(sortkey)
def finished_tournaments(club_id): def finished_tournaments(club_id):
ended_tournaments = tournaments_query(Q(end_date__isnull=False), club_id, False) ended_tournaments = tournaments_query(Q(is_private=False, is_deleted=False, event__club__isnull=False), club_id, False)
return [t for t in ended_tournaments if t.display_tournament()] return [t for t in ended_tournaments if t.display_tournament() and t.should_be_over()]
def live_tournaments(club_id): def live_tournaments(club_id):
tournaments = tournaments_query(Q(end_date__isnull=True), club_id, True) tournaments = tournaments_query(Q(end_date__isnull=True), club_id, True)
@ -498,9 +498,12 @@ def download(request):
return render(request, 'tournaments/download.html') return render(request, 'tournaments/download.html')
def test_apns(request): def test_apns(request):
token = DeviceToken.objects.first()
asyncio.run(send_push_notification(token.value, 'hello!')) user = CustomUser.objects.filter(username='laurent').first()
for device_token in user.device_tokens.all():
asyncio.run(send_push_notification(device_token.value, 'LOL?!'))
# token = DeviceToken.objects.first()
return HttpResponse('OK!') return HttpResponse('OK!')
@ -737,7 +740,7 @@ def my_tournaments(request):
stripped_license = validator.stripped_license stripped_license = validator.stripped_license
def filter_user_tournaments(tournaments): def filter_user_tournaments(tournaments):
return [t for t in tournaments if t.teamregistration_set.filter( return [t for t in tournaments if t.team_registrations.filter(
playerregistration__licence_id__startswith=stripped_license, playerregistration__licence_id__startswith=stripped_license,
walk_out=False walk_out=False
).exists()] ).exists()]
@ -791,26 +794,27 @@ def tournament_import_view(request):
with zipfile.ZipFile(zip_file) as z: with zipfile.ZipFile(zip_file) as z:
# First, process rounds # First, process rounds
rounds_data = get_file_data(z, f"{tournament_id}/rounds.json") rounds_data = get_file_data(z, f"{tournament_id}/rounds.json")
rounds_data = get_file_data(z, f"{tournament_id}/rounds.json")
if rounds_data: if rounds_data:
# First pass: Create rounds without parent relationships # First pass: Create rounds with preserved UUIDs
rounds_without_parent = []
for item in rounds_data: for item in rounds_data:
item['tournament'] = tournament.id item['tournament'] = tournament.id
# Temporarily remove parent field round_id = item['id'] # Preserve the original UUID
parent = item.pop('parent', None) Round.objects.create(
rounds_without_parent.append({'data': item, 'parent': parent}) id=round_id,
tournament_id=tournament.id,
serializer = RoundSerializer(data=[item['data'] for item in rounds_without_parent], many=True) index=item['index'],
serializer.is_valid(raise_exception=True) format=item.get('format'),
created_rounds = serializer.save() start_date=item.get('start_date'),
group_stage_loser_bracket=item.get('group_stage_loser_bracket', False),
# Create a mapping of round IDs loser_bracket_mode=item.get('loser_bracket_mode', 0)
round_mapping = {round_obj.id: round_obj for round_obj in created_rounds} )
# Second pass: Update parent relationships # Second pass: Set parent relationships
for round_obj, original_data in zip(created_rounds, rounds_without_parent): for item in rounds_data:
if original_data['parent']: if item.get('parent'):
round_obj.parent = round_mapping.get(original_data['parent']) round_obj = Round.objects.get(id=item['id'])
round_obj.parent_id = item['parent']
round_obj.save() round_obj.save()
# Then process all other files # Then process all other files
@ -891,18 +895,15 @@ def team_details(request, tournament_id, team_id):
class UserListExportView(LoginRequiredMixin, View): class UserListExportView(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
# Get users, excluding those with origin=UserOrigin.SITE, ordered by date_joined users = CustomUser.objects.order_by('date_joined')
users = CustomUser.objects.exclude(
origin=UserOrigin.SITE
).order_by('date_joined')
# Prepare the response # Prepare the response
response = HttpResponse(content_type='text/plain; charset=utf-8') response = HttpResponse(content_type='text/plain; charset=utf-8')
# Write header # Write header
headers = [ headers = [
'Prenom', 'Nom', 'Club', 'Email', 'Telephone', 'Prenom', 'Nom', 'Club', 'Email', 'Telephone', 'Username',
'Login', 'Actif', 'Inscription', 'Tournois' 'Origine', 'Actif', 'Inscription', 'Tournois'
] ]
response.write('\t'.join(headers) + '\n') response.write('\t'.join(headers) + '\n')
@ -914,7 +915,8 @@ class UserListExportView(LoginRequiredMixin, View):
str(user.latest_event_club_name() or ''), str(user.latest_event_club_name() or ''),
str(user.email or ''), str(user.email or ''),
str(user.phone or ''), str(user.phone or ''),
str(user.username or ''), user.username,
str(user.get_origin_display()),
'Oui' if user.is_active else 'Non', 'Oui' if user.is_active else 'Non',
user.date_joined.strftime('%Y-%m-%d %H:%M:%S'), user.date_joined.strftime('%Y-%m-%d %H:%M:%S'),
str(user.event_count()) str(user.event_count())

Loading…
Cancel
Save