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
openpyxl==3.1.5
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 .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
@ -46,12 +46,12 @@ class EventAdmin(SyncedObjectAdmin):
class TournamentAdmin(SyncedObjectAdmin):
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']
search_fields = ['id']
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]
search_fields = ['id']
@ -68,8 +68,8 @@ class RoundAdmin(SyncedObjectAdmin):
class PlayerRegistrationAdmin(SyncedObjectAdmin):
list_display = ['first_name', 'last_name', 'licence_id', 'rank']
search_fields = ('id', 'first_name', 'last_name')
list_filter = [TeamScoreTournamentListFilter]
search_fields = ('id', 'first_name', 'last_name', 'licence_id__icontains')
list_filter = ['registered_online', TeamScoreTournamentListFilter]
ordering = ['last_name', 'first_name']
class MatchAdmin(SyncedObjectAdmin):
@ -109,6 +109,7 @@ class LogAdmin(admin.ModelAdmin):
class DeviceTokenAdmin(admin.ModelAdmin):
list_display = ['user', 'value']
list_filter = ['user']
class DrawLogAdmin(SyncedObjectAdmin):
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 django.db.models import Q
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from datetime import timedelta
import uuid
from enum import Enum
@ -101,3 +103,36 @@ class SimpleIndexListFilter(admin.SimpleListFilter):
return queryset.filter(index=self.value())
else:
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
def events_count(self):
return len(self.events.all())
return self.events.count()
def court_name(self, index):
for court in self.courts.all():

@ -1,4 +1,5 @@
from django.db import models
from django.apps import apps
from django.contrib.auth.models import AbstractUser
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}"
def event_count(self):
return len(self.events.all())
return self.events.count()
def full_name(self):
return f"{self.first_name} {self.last_name}"
@ -68,4 +69,12 @@ class CustomUser(AbstractUser):
latest_event = self.events.order_by('-creation_date').first()
if latest_event and latest_event.club:
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

@ -96,6 +96,26 @@ class FederalMatchCategory(models.IntegerChoices):
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'
@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):
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

@ -156,7 +156,7 @@ class GroupStage(SideStoreModel):
return False
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:
def __init__(self, title, step, index):

@ -97,7 +97,7 @@ class Match(SideStoreModel):
previous_index = self.round.index + 1
# Check if the next index is within the bounds of the rounds array
if previous_index < 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 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)
return team
def is_ready(self):
return self.team_scores.count() == 2
def live_teams(self):
#print('player names from match')
##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_bottom_match = self.precedent_match(False)
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
if (self.group_stage):
return teams
@ -292,7 +295,7 @@ class Match(SideStoreModel):
def should_appear(self):
if self.disabled is True:
return False
return len(self.team_scores.all()) > 0
return self.team_scores.count() > 0
# elif self.group_stage is None:
# if len(self.team_scores.all()) == 2:
@ -330,7 +333,9 @@ class Match(SideStoreModel):
group_stage_name = self.group_stage.display_name()
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():
livematch.add_team(team)
@ -381,7 +386,7 @@ class Team:
}
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.date = date
self.teams = []
@ -391,6 +396,7 @@ class LiveMatch:
self.ended = ended
self.has_walk_out = False
self.group_stage_name = group_stage_name
self.format = format
def add_team(self, team):
self.teams.append(team)
@ -408,6 +414,7 @@ class LiveMatch:
"ended": self.ended,
"has_walk_out": self.has_walk_out,
"group_stage_name": self.group_stage_name,
"format": self.format
}
def show_time_indication(self):

@ -123,7 +123,7 @@ class TeamRegistration(SideStoreModel):
self.save() # Save the updated weight if necessary
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):
if self.locked_weight is None:
@ -186,10 +186,32 @@ class TeamRegistration(SideStoreModel):
'points_earned': self.get_points_earned(),
'initial_stage': self.get_initial_stage(),
'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
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):
matches = self.get_matches().order_by('start_date')
first_match = matches.first()
@ -204,10 +226,22 @@ class TeamRegistration(SideStoreModel):
def get_final_ranking(self):
if self.final_ranking:
if self.final_ranking == 1:
return "1er"
return f"{self.final_ranking}e"
return "1er" + self.ranking_delta()
return f"{self.final_ranking}ème" + self.ranking_delta()
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):
return self.points_earned
@ -234,6 +268,6 @@ class TeamRegistration(SideStoreModel):
total_matches = matches.count()
if total_matches > 0:
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 None

@ -229,7 +229,7 @@ class Tournament(BaseModel):
return "Annulé"
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"]
if teams is not None and len(teams) > 0:
word = "équipe"
@ -337,126 +337,94 @@ class Tournament(BaseModel):
print("else", index, self.team_count)
return -1
def teams(self, includeWaitingList):
# print("Starting teams method")
bracket_teams = []
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}")
def teams(self, include_waiting_list):
"""
Get sorted list of teams for the tournament.
for team_registration in self.team_registrations.all():
# print(f"Processing team registration: {team_registration}")
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}")
Args:
include_waiting_list (bool): Whether to include teams in waiting list
if self.team_sorting == TeamSortingType.INSCRIPTION_DATE:
complete_teams.sort(key=lambda s: (s.registration_date is None, s.registration_date or datetime.min, s.initial_weight, s.team_registration.id))
else:
complete_teams.sort(key=lambda s: (s.initial_weight, s.team_registration.id))
selected_teams = complete_teams[:self.team_count]
selected_teams.sort(key=lambda s: (s.initial_weight, s.team_registration.id))
Returns:
list: List of TeamItem objects sorted according to tournament rules
"""
# Initialize team categories
complete_teams = []
wildcard_bracket = []
wildcard_group_stage = []
waiting_teams = []
if seeds_count > 0:
bracket_teams = selected_teams[:seeds_count] + wildcard_bracket
else:
bracket_teams = []
# print(f"Bracket teams: {len(bracket_teams)}")
# Get registration cutoff date
closed_date = self.closed_registration_date
# Process each team registration
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:
group_stage_end = seeds_count + group_stage_members_count
group_stage_teams = selected_teams[seeds_count:group_stage_end] + wildcard_group_stage
else:
group_stage_teams = []
# 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))
# 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:
waiting_teams.sort(key=lambda s: (s.initial_weight, s.team_registration.id))
else:
waiting_teams = []
# print(f"Final waiting teams: {len(waiting_teams)}")
team.set_stage("Attente")
bracket_teams.sort(key=lambda s: (s.weight, s.team_registration.id))
group_stage_teams.sort(key=lambda s: (s.weight, s.team_registration.id))
# 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))
for team in bracket_teams:
if team.stage == "Attente":
team.set_stage("Tableau")
# Determine final team list based on tournament settings
if len(complete_teams) <= self.team_count:
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:
if team.stage == "Attente":
team.set_stage("Poule")
# Split teams into main bracket and waiting list
qualified_teams = complete_teams[:self.team_count]
excess_teams = complete_teams[self.team_count:]
for team in waiting_teams:
team.set_stage("Attente")
# Combine all waiting list teams
waiting_list = excess_teams + waiting_teams
if includeWaitingList is True:
final_teams = bracket_teams + group_stage_teams + waiting_teams
# print(f"Final teams with waiting list: {len(final_teams)}")
else:
final_teams = bracket_teams + group_stage_teams
# print(f"Final teams without waiting list: {len(final_teams)}")
return final_teams
# Return final sorted list
if include_waiting_list:
return wildcard_bracket + wildcard_group_stage + qualified_teams + waiting_list
return wildcard_bracket + wildcard_group_stage + qualified_teams
def match_groups(self, broadcasted, group_stage_id, round_id):
@ -660,7 +628,7 @@ class Tournament(BaseModel):
matches = []
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()]
matches = self.broadcasted_group_stages_matches()
first_round = self.first_round()
@ -750,24 +718,24 @@ class Tournament(BaseModel):
# all started matches have ended, possibly
last_finished_match = self.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.round_set.filter(parent=None,index=round_root_index-1).first()
if round:
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:
return None
else:
return None
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]
@ -843,7 +811,7 @@ class Tournament(BaseModel):
return False
def has_team_registrations(self):
return len(self.team_registrations.all()) > 0
return self.team_registrations.count() > 0
def display_summons(self):
if self.end_date is not None:
@ -938,6 +906,35 @@ class Tournament(BaseModel):
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):
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
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):
return plural_format("jour", self.day_duration)
@ -1028,7 +1025,7 @@ class Tournament(BaseModel):
# Check target team count and waiting list limit
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 self.waiting_list_limit is not None:
waiting_list_count = current_team_count - self.team_count
@ -1058,7 +1055,7 @@ class Tournament(BaseModel):
if self.team_count is not None:
# 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 self.waiting_list_limit is not None:
waiting_list_count = current_team_count - self.team_count
@ -1093,7 +1090,7 @@ class Tournament(BaseModel):
return -1
# 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_team_count < self.team_count:

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

@ -246,6 +246,7 @@ tr {
font-family: "Anybody-ExtraBold";
font-size: 1.2em;
color: #1b223a;
line-height: 24px; /* Match the height of flex-row */
}
.title {
@ -816,9 +817,9 @@ h-margin {
}
.download-button {
margin-right: 6px;
color: #1a223a;
color: #fff7ed;
padding: 8px 12px;
background-color: white;
background-color: #1a223a;
border-radius: 12px;
text-decoration: none;
font-size: 12px;
@ -826,7 +827,7 @@ h-margin {
}
.download-button:hover {
color: orange;
color: #f39200;
}
.match-result a {
@ -856,3 +857,12 @@ h-margin {
.group-stage-link:hover {
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">
<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 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>
<template x-for="i in match.teams.length">
@ -59,7 +63,7 @@
<div class="top-margin flex-row">
<label class="left-label minor-info bold"><span x-text="match.time_indication"></span></label>
<label class="right-label minor-info semibold"><span x-text="match.court"></span></label>
<label class="right-label minor-info semibold"><span x-text="match.format"></span></label>
</div>
</div>

@ -3,7 +3,11 @@
<div>
<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">
<template x-for="name in ranking.names" >
@ -11,7 +15,9 @@
</template>
</div>
{% 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 %}
</div>
<div x-show="index < column.length - 1">

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

@ -16,6 +16,18 @@
</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 %}
<div class="match-result bottom-border">
<div class="player">

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

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

Loading…
Cancel
Save