Laurent 8 months ago
commit 98781235a1
  1. 24
      tournaments/admin.py
  2. 10
      tournaments/models/group_stage.py
  3. 49
      tournaments/models/match.py
  4. 72
      tournaments/models/round.py
  5. 12
      tournaments/models/team_registration.py
  6. 138
      tournaments/models/tournament.py
  7. 54
      tournaments/static/tournaments/css/broadcast.css
  8. 38
      tournaments/static/tournaments/css/style.css
  9. 17
      tournaments/templates/tournaments/broadcast/broadcasted_group_stage.html
  10. 10
      tournaments/templates/tournaments/broadcast/broadcasted_match.html
  11. 8
      tournaments/templates/tournaments/group_stage_cell.html
  12. 8
      tournaments/templates/tournaments/match_cell.html
  13. 158
      tournaments/templates/tournaments/tournament_bracket.html
  14. 1
      tournaments/urls.py
  15. 155
      tournaments/views.py

@ -59,23 +59,47 @@ class TeamScoreAdmin(SyncedObjectAdmin):
list_display = ['team_registration', 'score', 'walk_out', 'match'] list_display = ['team_registration', 'score', 'walk_out', 'match']
list_filter = [TeamScoreTournamentListFilter] list_filter = [TeamScoreTournamentListFilter]
search_fields = ['id'] search_fields = ['id']
raw_id_fields = ['team_registration', 'match'] # Add this line
list_per_page = 50 # Controls pagination on the list view
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('team_registration', 'match')
class RoundAdmin(SyncedObjectAdmin): class RoundAdmin(SyncedObjectAdmin):
list_display = ['tournament', 'name', 'parent', 'index'] list_display = ['tournament', 'name', 'parent', 'index']
list_filter = [SimpleTournamentListFilter, SimpleIndexListFilter] list_filter = [SimpleTournamentListFilter, SimpleIndexListFilter]
search_fields = ['id'] search_fields = ['id']
ordering = ['parent', 'index'] ordering = ['parent', 'index']
raw_id_fields = ['parent'] # Add this line
list_per_page = 50 # Controls pagination on the list view
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('parent')
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', 'licence_id__icontains') search_fields = ('id', 'first_name', 'last_name', 'licence_id__icontains')
list_filter = ['registered_online', TeamScoreTournamentListFilter] list_filter = ['registered_online', TeamScoreTournamentListFilter]
ordering = ['last_name', 'first_name'] ordering = ['last_name', 'first_name']
raw_id_fields = ['team_registration'] # Add this line
list_per_page = 50 # Controls pagination on the list view
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('team_registration')
class MatchAdmin(SyncedObjectAdmin): class MatchAdmin(SyncedObjectAdmin):
list_display = ['__str__', 'round', 'group_stage', 'start_date', 'end_date', 'index'] list_display = ['__str__', 'round', 'group_stage', 'start_date', 'end_date', 'index']
list_filter = [MatchTypeListFilter, MatchTournamentListFilter, SimpleIndexListFilter] list_filter = [MatchTypeListFilter, MatchTournamentListFilter, SimpleIndexListFilter]
ordering = ['-group_stage', 'round', 'index'] ordering = ['-group_stage', 'round', 'index']
raw_id_fields = ['round', 'group_stage'] # Add this line
list_per_page = 50 # Controls pagination on the list view
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('round', 'group_stage')
class GroupStageAdmin(SyncedObjectAdmin): class GroupStageAdmin(SyncedObjectAdmin):
list_display = ['tournament', 'start_date', 'index'] list_display = ['tournament', 'start_date', 'index']

@ -6,6 +6,7 @@ import uuid
from ..utils.extensions import format_seconds from ..utils.extensions import format_seconds
# from datetime import datetime, timedelta # from datetime import datetime, timedelta
from django.utils import timezone from django.utils import timezone
from django.db.models import Count, Q
class GroupStage(SideStoreModel): class GroupStage(SideStoreModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)
@ -151,12 +152,14 @@ class GroupStage(SideStoreModel):
def has_at_least_one_started_match(self): def has_at_least_one_started_match(self):
for match in self.matches.all(): for match in self.matches.all():
if match.start_date is not None and match.start_date <= timezone.now(): if match.start_date is not None and match.start_date <= timezone.now() and match.confirmed is True:
return True return True
return False return False
def is_completed(self): def is_completed(self):
return not self.matches.filter(end_date__isnull=True).exists() and self.matches.count() > 0 # This will use the prefetched matches if available
matches = list(self.matches.all())
return len(matches) > 0 and all(match.end_date is not None for match in matches)
class LiveGroupStage: class LiveGroupStage:
def __init__(self, title, step, index): def __init__(self, title, step, index):
@ -190,7 +193,8 @@ class LiveGroupStage:
return { return {
"title": self.title, "title": self.title,
"teams": [team.to_dict() for team in self.teams], "teams": [team.to_dict() for team in self.teams],
"duration": self.formatted_duration() "duration": self.formatted_duration(),
"started": self.started()
} }
def started(self): def started(self):

@ -191,38 +191,51 @@ class Match(SideStoreModel):
loser_bottom_match = self.loser_precedent_match(False) loser_bottom_match = self.loser_precedent_match(False)
if len(team_scores) == 0: if len(team_scores) == 0:
if (self.round and self.round.parent is None and self.round.tournament.rounds.filter(parent__isnull=True, group_stage_loser_bracket=False).count() - 1 == self.round.index): if (self.round and self.round.parent is None and self.round.tournament.rounds.filter(parent__isnull=True, group_stage_loser_bracket=False).count() - 1 == self.round.index):
names = ["Qualifié", ''] names = ["Qualifié"]
team = self.default_live_team(names) team = self.default_live_team(names)
teams.append(team) teams.append(team)
names = ["Qualifié", ''] names = ["Qualifié"]
team = self.default_live_team(names) team = self.default_live_team(names)
teams.append(team) teams.append(team)
return teams return teams
if (self.group_stage): if (self.group_stage):
names = ["Équipe de poule", ''] names = ["Équipe de poule"]
team = self.default_live_team(names) team = self.default_live_team(names)
teams.append(team) teams.append(team)
names = ["Équipe de poule", ''] names = ["Équipe de poule"]
team = self.default_live_team(names) team = self.default_live_team(names)
teams.append(team) teams.append(team)
return teams return teams
elif self.round and self.round.parent: elif self.round and self.round.parent:
if loser_top_match: if loser_top_match:
names = [f"Perdant {loser_top_match.computed_name()}", ''] names = [f"Perdant {loser_top_match.computed_name()}"]
team = self.default_live_team(names) team = self.default_live_team(names)
teams.append(team) teams.append(team)
if loser_bottom_match: if loser_bottom_match:
names = [f"Perdant {loser_bottom_match.computed_name()}", ''] names = [f"Perdant {loser_bottom_match.computed_name()}"]
team = self.default_live_team(names)
teams.append(team)
if previous_top_match:
names = [f"Gagnant {previous_top_match.computed_name()}"]
if previous_top_match.disabled:
names = ["Perdant tableau principal"]
team = self.default_live_team(names)
teams.append(team)
if previous_bottom_match:
names = [f"Gagnant {previous_bottom_match.computed_name()}"]
if previous_bottom_match.disabled:
names = ["Perdant tableau principal"]
team = self.default_live_team(names) team = self.default_live_team(names)
teams.append(team) teams.append(team)
elif self.round and self.round.parent is None: elif self.round and self.round.parent is None:
if previous_top_match: if previous_top_match:
names = [f"Gagnant {previous_top_match.computed_name()}", ''] names = [f"Gagnant {previous_top_match.computed_name()}"]
team = self.default_live_team(names) team = self.default_live_team(names)
teams.append(team) teams.append(team)
if previous_bottom_match: if previous_bottom_match:
names = [f"Gagnant {previous_bottom_match.computed_name()}", ''] names = [f"Gagnant {previous_bottom_match.computed_name()}"]
team = self.default_live_team(names) team = self.default_live_team(names)
teams.append(team) teams.append(team)
elif len(team_scores) == 1: elif len(team_scores) == 1:
@ -230,33 +243,33 @@ class Match(SideStoreModel):
existing_team = team_scores[0].live_team(self) existing_team = team_scores[0].live_team(self)
if (self.group_stage): if (self.group_stage):
teams.append(existing_team) teams.append(existing_team)
names = ["Équipe de poule", ''] names = ["Équipe de poule"]
team = self.default_live_team(names) team = self.default_live_team(names)
teams.append(team) teams.append(team)
elif self.round: elif self.round:
if loser_top_match and loser_top_match.disabled == False and loser_top_match.end_date is None: if loser_top_match and loser_top_match.disabled == False and loser_top_match.end_date is None:
names = [f"Perdant {loser_top_match.computed_name()}", ''] names = [f"Perdant {loser_top_match.computed_name()}"]
team = self.default_live_team(names) team = self.default_live_team(names)
teams.append(team) teams.append(team)
teams.append(existing_team) teams.append(existing_team)
elif loser_bottom_match: elif loser_bottom_match:
names = [f"Perdant {loser_bottom_match.computed_name()}", ''] names = [f"Perdant {loser_bottom_match.computed_name()}"]
team = self.default_live_team(names) team = self.default_live_team(names)
teams.append(existing_team) teams.append(existing_team)
teams.append(team) teams.append(team)
elif previous_top_match and previous_top_match.disabled == False and previous_top_match.end_date is None: elif previous_top_match and previous_top_match.disabled == False and previous_top_match.end_date is None:
names = [f"Gagnant {previous_top_match.computed_name()}", ''] names = [f"Gagnant {previous_top_match.computed_name()}"]
team = self.default_live_team(names) team = self.default_live_team(names)
teams.append(team) teams.append(team)
teams.append(existing_team) teams.append(existing_team)
elif previous_bottom_match: elif previous_bottom_match:
names = [f"Gagnant {previous_bottom_match.computed_name()}", ''] names = [f"Gagnant {previous_bottom_match.computed_name()}"]
team = self.default_live_team(names) team = self.default_live_team(names)
teams.append(existing_team) teams.append(existing_team)
teams.append(team) teams.append(team)
elif (self.round.parent is None and self.round.tournament.rounds.filter(parent__isnull=True, group_stage_loser_bracket=False).count() - 1 == self.round.index): elif (self.round.parent is None and self.round.tournament.rounds.filter(parent__isnull=True, group_stage_loser_bracket=False).count() - 1 == self.round.index):
match_index_within_round = self.index - (int(2 ** self.round.index) - 1) match_index_within_round = self.index - (int(2 ** self.round.index) - 1)
names = ["Qualifié", ''] names = ["Qualifié"]
team = self.default_live_team(names) team = self.default_live_team(names)
if match_index_within_round < int(2 ** self.round.index) / 2: if match_index_within_round < int(2 ** self.round.index) / 2:
teams.append(existing_team) teams.append(existing_team)
@ -434,7 +447,10 @@ class Match(SideStoreModel):
class Team: class Team:
def __init__(self, id, image, names, scores, weight, is_winner, walk_out, is_lucky_loser): def __init__(self, id, image, names, scores, weight, is_winner, walk_out, is_lucky_loser):
# print(f"image = {image}, names= {names}, scores ={scores}, weight={weight}, win={is_winner}") # print(f"image = {image}, names= {names}, scores ={scores}, weight={weight}, win={is_winner}")
self.id = str(id) if id is not None:
self.id = str(id)
else:
self.id = None
self.image = image self.image = image
self.names = names self.names = names
self.scores = scores self.scores = scores
@ -455,7 +471,8 @@ class Team:
"is_winner": self.is_winner, "is_winner": self.is_winner,
"walk_out": self.walk_out, "walk_out": self.walk_out,
"is_walk_out": self.is_walk_out(), "is_walk_out": self.is_walk_out(),
"is_lucky_loser": self.is_lucky_loser "is_lucky_loser": self.is_lucky_loser,
"id": self.id
} }
class LiveMatch: class LiveMatch:

@ -34,10 +34,12 @@ class Round(SideStoreModel):
return self.tournament.id return self.tournament.id
def name(self): def name(self):
if self.parent: if self.parent and self.parent.parent is None:
return "Matchs de classement" return f"Classement {self.parent.name()}"
elif self.parent and self.parent.parent is not None:
return f"{self.parent.name()}"
elif self.group_stage_loser_bracket is True: elif self.group_stage_loser_bracket is True:
return "Matchs de classement de poule" return "Classement de poule"
else: else:
if self.index == 0: if self.index == 0:
return "Finale" return "Finale"
@ -103,3 +105,67 @@ class Round(SideStoreModel):
return False return False
return True return True
def prepare_match_group(self, next_round, parent_round, loser_final, double_butterfly_mode):
matches = self.match_set.filter(disabled=False).order_by('index')
if len(matches) == 0:
return None
if next_round:
next_round_matches = next_round.match_set.filter(disabled=False).order_by('index')
else:
next_round_matches = []
if len(matches) < len(next_round_matches):
all_matches = self.match_set.order_by('index')
filtered_matches = []
# Process matches in pairs
i = 0
while i < len(all_matches):
# Get the current match and its pair (if available)
current_match = all_matches[i]
pair_match = all_matches[i+1] if i+1 < len(all_matches) else None
# Only filter out the pair if both matches are disabled
if current_match.disabled and pair_match and pair_match.disabled:
# Skip one of the matches in the pair
filtered_matches.append(current_match)
pass
else:
# Keep the current match
if current_match.disabled == False:
filtered_matches.append(current_match)
# If there's a pair match, keep it too
if pair_match and pair_match.disabled == False:
filtered_matches.append(pair_match)
# Move to the next pair
i += 2
# Replace the matches list with our filtered list
matches = filtered_matches
if matches:
if len(matches) > 1 and double_butterfly_mode:
midpoint = int(len(matches) / 2)
first_half_matches = matches[:midpoint]
else:
first_half_matches = list(matches) # Convert QuerySet to a list
if self.index == 0 and loser_final:
loser_match = loser_final.match_set.first()
if loser_match:
first_half_matches.append(loser_match)
if first_half_matches:
name = self.name()
if parent_round and first_half_matches[0].name is not None:
name = first_half_matches[0].name
match_group = self.tournament.create_match_group(
name=name,
matches=first_half_matches,
round_id=self.id
)
return match_group
return None

@ -55,27 +55,27 @@ class TeamRegistration(SideStoreModel):
def player_names_as_list(self): def player_names_as_list(self):
players = list(self.player_registrations.all()) players = list(self.player_registrations.all())
if len(players) == 0: if len(players) == 0:
return ['', ''] return []
elif len(players) == 1: elif len(players) == 1:
return [players[0].name(), ''] return [players[0].name()]
else: else:
return [pr.name() for pr in players] return [pr.name() for pr in players]
def team_names(self): def team_names(self):
if self.name: if self.name:
return [self.name, ''] #add an empty line if it's a team name return [self.name] #add an empty line if it's a team name
else: else:
return self.player_names_as_list() return self.player_names_as_list()
def shortened_team_names(self): def shortened_team_names(self):
if self.name: if self.name:
return [self.name, ''] #add an empty line if it's a team name return [self.name] #add an empty line if it's a team name
else: else:
players = list(self.player_registrations.all()) players = list(self.player_registrations.all())
if len(players) == 0: if len(players) == 0:
return ['', ''] return []
elif len(players) == 1: elif len(players) == 1:
return [players[0].shortened_name(), ''] return [players[0].shortened_name()]
else: else:
return [pr.shortened_name() for pr in players] return [pr.shortened_name() for pr in players]

@ -184,7 +184,7 @@ class Tournament(BaseModel):
return self.get_federal_age_category_display() return self.get_federal_age_category_display()
def formatted_start_date(self): def formatted_start_date(self):
return self.start_date.strftime("%d/%m/%y") return self.local_start_date().strftime("%d/%m/%y")
def in_progress(self): def in_progress(self):
return self.end_date is None return self.end_date is None
@ -549,7 +549,7 @@ class Tournament(BaseModel):
return groups return groups
def create_match_group(self, name, matches): def create_match_group(self, name, matches, round_id=None):
matches = list(matches) matches = list(matches)
live_matches = [match.live_match() for match in matches] live_matches = [match.live_match() for match in matches]
# Filter out matches that have a start_date of None # Filter out matches that have a start_date of None
@ -566,7 +566,7 @@ class Tournament(BaseModel):
time_format = 'l d M' time_format = 'l d M'
formatted_schedule = f" - {formats.date_format(local_start, format=time_format)}" formatted_schedule = f" - {formats.date_format(local_start, format=time_format)}"
return MatchGroup(name, live_matches, formatted_schedule) return MatchGroup(name, live_matches, formatted_schedule, round_id)
def live_group_stages(self): def live_group_stages(self):
group_stages = self.sorted_group_stages() group_stages = self.sorted_group_stages()
@ -617,7 +617,11 @@ class Tournament(BaseModel):
# if now is before the first match, we want to show the summons + group stage or first matches # 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. # 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 timezone.now() < self.start_date: current_time = timezone.now()
tournament_start = self.local_start_date()
one_hour_before_start = tournament_start - timedelta(hours=1)
if current_time < one_hour_before_start:
team_summons_dicts = [summon.to_dict() for summon in self.team_summons()] team_summons_dicts = [summon.to_dict() for summon in self.team_summons()]
if group_stages: if group_stages:
return { return {
@ -693,6 +697,12 @@ class Tournament(BaseModel):
if previous_round: if previous_round:
# print('previous_round') # print('previous_round')
matches.extend(previous_round.get_matches_recursive(True)) matches.extend(previous_round.get_matches_recursive(True))
previous_previous_round = self.round_for_index(current_round.index + 2)
if previous_previous_round:
previous_previous_matches = previous_previous_round.get_matches_recursive(True)
previous_previous_matches = [m for m in previous_previous_matches if m.end_date is None]
matches.extend(previous_previous_matches)
else: else:
# print('group_stages') # print('group_stages')
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()]
@ -800,8 +810,18 @@ class Tournament(BaseModel):
group_stages = self.elected_broadcast_group_stages() group_stages = self.elected_broadcast_group_stages()
group_stages.sort(key=lambda gs: (gs.index, gs.start_date is None, gs.start_date)) group_stages.sort(key=lambda gs: (gs.index, gs.start_date is None, gs.start_date))
for group_stage in group_stages: for group_stage in group_stages:
matches.extend(group_stage.matches.all()) matches.extend(group_stage.matches.all())
matches = [m for m in matches if m.should_appear()]
if len(matches) > 16:
# if more than 16 groupstage matches
now = timezone.now()
future_threshold = now + timedelta(hours=1)
past_threshold = now - timedelta(hours=1)
matches = [m for m in matches if m.should_appear() and
(m.start_date is None or m.start_date <= future_threshold) and # Not starting in more than 1h
(m.end_date is None or m.end_date >= past_threshold)] # Not finished for more than 1h
matches = matches[:16]
matches.sort(key=lambda m: (m.start_date is None, m.end_date is not None, m.start_date, m.index)) matches.sort(key=lambda m: (m.start_date is None, m.end_date is not None, m.start_date, m.index))
group_stage_loser_bracket = list(self.rounds.filter(parent=None, group_stage_loser_bracket=True).all()) group_stage_loser_bracket = list(self.rounds.filter(parent=None, group_stage_loser_bracket=True).all())
@ -836,7 +856,7 @@ class Tournament(BaseModel):
if self.end_date is not None: if self.end_date is not None:
return is_build_and_not_empty return is_build_and_not_empty
if timezone.now() >= timezone.localtime(self.start_date): if timezone.now() >= self.local_start_date():
return is_build_and_not_empty return is_build_and_not_empty
minimum_publish_date = self.creation_date.replace(hour=9, minute=0) + timedelta(days=1) minimum_publish_date = self.creation_date.replace(hour=9, minute=0) + timedelta(days=1)
return timezone.now() >= timezone.localtime(minimum_publish_date) return timezone.now() >= timezone.localtime(minimum_publish_date)
@ -846,7 +866,7 @@ class Tournament(BaseModel):
return self.has_team_registrations() return self.has_team_registrations()
if self.publish_teams: if self.publish_teams:
return self.has_team_registrations() return self.has_team_registrations()
if timezone.now().date() >= self.start_date.date(): if timezone.now() >= self.local_start_date():
return self.has_team_registrations() return self.has_team_registrations()
return False return False
@ -858,7 +878,7 @@ class Tournament(BaseModel):
return False return False
if self.publish_summons: if self.publish_summons:
return self.has_summons() return self.has_summons()
if timezone.now() >= self.start_date: if timezone.now() >= self.local_start_date():
return self.has_summons() return self.has_summons()
return False return False
@ -872,7 +892,7 @@ class Tournament(BaseModel):
first_group_stage_start_date = self.group_stage_start_date() first_group_stage_start_date = self.group_stage_start_date()
if first_group_stage_start_date is None: if first_group_stage_start_date is None:
return timezone.now() >= self.start_date return timezone.now() >= self.local_start_date()
else: else:
return timezone.now() >= first_group_stage_start_date return timezone.now() >= first_group_stage_start_date
@ -881,7 +901,8 @@ class Tournament(BaseModel):
if len(group_stages) == 0: if len(group_stages) == 0:
return None return None
return min(group_stages, key=lambda gs: gs.start_date).start_date timezone = self.timezone()
return min(group_stages, key=lambda gs: gs.start_date).start_date.astimezone(timezone)
def display_matches(self): def display_matches(self):
if self.end_date is not None: if self.end_date is not None:
@ -894,12 +915,12 @@ class Tournament(BaseModel):
first_match_start_date = self.first_match_start_date(bracket_matches) first_match_start_date = self.first_match_start_date(bracket_matches)
if first_match_start_date is None: if first_match_start_date is None:
return timezone.now() >= self.start_date return timezone.now() >= self.local_start_date()
bracket_start_date = self.getEightAm(first_match_start_date) bracket_start_date = self.getEightAm(first_match_start_date)
if bracket_start_date < self.start_date: if bracket_start_date < self.local_start_date():
bracket_start_date = self.start_date bracket_start_date = self.local_start_date()
group_stage_start_date = self.group_stage_start_date() group_stage_start_date = self.group_stage_start_date()
if group_stage_start_date is not None: if group_stage_start_date is not None:
@ -922,8 +943,7 @@ class Tournament(BaseModel):
matches = [m for m in bracket_matches if m.start_date is not None] matches = [m for m in bracket_matches if m.start_date is not None]
if len(matches) == 0: if len(matches) == 0:
return None return None
return min(matches, key=lambda m: m.start_date).local_start_date()
return min(matches, key=lambda m: m.start_date).start_date
def getEightAm(self, date): def getEightAm(self, date):
return date.replace(hour=8, minute=0, second=0, microsecond=0, tzinfo=date.tzinfo) return date.replace(hour=8, minute=0, second=0, microsecond=0, tzinfo=date.tzinfo)
@ -932,7 +952,7 @@ class Tournament(BaseModel):
# end = self.start_date + timedelta(days=self.day_duration + 1) # end = self.start_date + timedelta(days=self.day_duration + 1)
# return self.start_date.replace(hour=0, minute=0) <= timezone.now() <= end # return self.start_date.replace(hour=0, minute=0) <= timezone.now() <= end
timezoned_datetime = timezone.localtime(self.start_date) timezoned_datetime = self.local_start_date()
end = timezoned_datetime + timedelta(days=self.day_duration + 1) end = timezoned_datetime + timedelta(days=self.day_duration + 1)
now = timezone.now() now = timezone.now()
@ -946,33 +966,41 @@ class Tournament(BaseModel):
return start <= now <= end return start <= now <= end
def starts_in_the_future(self):
# tomorrow = datetime.now().date() + timedelta(days=1)
timezoned_datetime = self.local_start_date()
start = timezoned_datetime.replace(hour=0, minute=0)
now = timezone.now()
return start >= now
def should_be_over(self): def should_be_over(self):
if self.end_date is not None: if self.end_date is not None:
return True return True
timezoned_datetime = timezone.localtime(self.start_date) timezoned_datetime = self.local_start_date()
end = timezoned_datetime + timedelta(days=self.day_duration + 1) end = timezoned_datetime + timedelta(days=self.day_duration + 1)
now = timezone.now() now = timezone.now()
return now >= end and self.is_build_and_not_empty() and self.nearly_over() return now >= end and self.is_build_and_not_empty() and self.nearly_over()
def nearly_over(self): def nearly_over(self):
# First check group stages if they exist # First check group stages if they exist
if self.group_stages.count() > 0: group_stages = list(self.group_stages.all()) # Use prefetched data
if group_stages:
# Check if all group stages are completed # Check if all group stages are completed
for group_stage in self.group_stages.all(): for group_stage in group_stages:
# Use the is_completed method
if group_stage.is_completed(): if group_stage.is_completed():
return True return True
# If no group stages, check semi-finals # If no group stages, check semi-finals
if self.rounds.count() > 0: semifinals = self.rounds.filter(index=1, parent=None).first() # Use prefetched data
# Get round with index 1 (semi-finals) and no parent if semifinals:
semifinals = self.rounds.filter(index=1, parent=None).first() # Check if any match in semi-finals has started
if semifinals: for match in semifinals.matches.all(): # Use prefetched data
# Check if any match in semi-finals has started if match.start_date is not None and match.is_ready():
for match in semifinals.matches.all(): return True
if match.start_date is not None and match.is_ready():
return True
return False
return False return False
@ -983,7 +1011,18 @@ 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 self.group_stages.count() > 0 or self.rounds.count() > 0 and self.team_registrations.count() >= 4 if hasattr(self, '_prefetched_objects_cache'):
# Use prefetched data if available
has_group_stages = 'groupstage_set' in self._prefetched_objects_cache and len(self.group_stages.all()) > 0
has_rounds = 'round_set' in self._prefetched_objects_cache and len(self.rounds.all()) > 0
has_team_registrations = 'teamregistration_set' in self._prefetched_objects_cache and len(self.team_registrations.all()) >= 4
else:
# Fall back to database queries if not prefetched
has_group_stages = self.group_stages.count() > 0
has_rounds = self.rounds.count() > 0
has_team_registrations = self.team_registrations.count() >= 4
return (has_group_stages or has_rounds) and has_team_registrations
def day_duration_formatted(self): def day_duration_formatted(self):
return plural_format("jour", self.day_duration) return plural_format("jour", self.day_duration)
@ -1315,12 +1354,51 @@ class Tournament(BaseModel):
return ordered_matches return ordered_matches
def get_butterfly_bracket_match_group(self, parent_round=None, double_butterfly_mode=False, display_loser_final=False):
loser_final = None
main_rounds_reversed = []
# Get main bracket rounds (excluding children/ranking matches)
main_rounds = self.rounds.filter(
parent=parent_round,
group_stage_loser_bracket=False
).order_by('-index')
count = main_rounds.count()
if display_loser_final and count > 1:
semi = main_rounds[count - 2]
loser_final = self.rounds.filter(
parent=semi,
group_stage_loser_bracket=False
).order_by('index').first()
# Create serializable match groups data
serializable_match_groups = []
# Add first half of each round (from last to semi-finals)
for round in main_rounds:
next_round = main_rounds.filter(index=round.index - 1).first()
match_group = round.prepare_match_group(next_round, parent_round, loser_final, double_butterfly_mode)
if match_group:
serializable_match_groups.append(match_group)
if double_butterfly_mode:
main_rounds_reversed = list(main_rounds)
main_rounds_reversed.reverse()
for round in main_rounds_reversed:
next_round = main_rounds.filter(index=round.index - 1).first()
match_group = round.prepare_match_group(next_round, parent_round, None, double_butterfly_mode)
if match_group:
serializable_match_groups.append(match_group)
return serializable_match_groups
class MatchGroup: class MatchGroup:
def __init__(self, name, matches, formatted_schedule): def __init__(self, name, matches, formatted_schedule, round_id=None):
self.name = name self.name = name
self.matches = matches self.matches = matches
self.formatted_schedule = formatted_schedule self.formatted_schedule = formatted_schedule
self.round_id = round_id
def add_match(self, match): def add_match(self, match):
self.matches.append(match) self.matches.append(match)

@ -25,3 +25,57 @@ body {
.bold { .bold {
font-family: "Montserrat-Bold"; font-family: "Montserrat-Bold";
} }
.player {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
min-height: 3.2em; /* This ensures minimum height for 2 lines */
justify-content: center;
overflow: hidden;
}
/* Add this if you want empty lines to take up space */
.player div {
min-height: 1.4em; /* Height for single line */
}
/* For single player teams */
.player.single-player .bold {
line-height: 1.4em;
overflow: hidden;
position: relative;
text-overflow: ellipsis;
}
/* For two player teams */
.player.two-players .bold {
line-height: 1.4em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.flex {
display: flex;
align-items: center;
}
.flex-left {
flex: 1;
text-align: left;
justify-content: center;
min-height: 4em;
padding-right: 5px;
}
.flex-right {
flex: initial;
text-align: right;
justify-content: center;
}
.center {
align-items: center;
}

@ -323,9 +323,34 @@ tr {
} }
.player { .player {
position: relative;
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 3.2em; /* This ensures minimum height for 2 lines */
justify-content: center;
overflow: hidden;
}
/* Add this if you want empty lines to take up space */
.player div {
min-height: 1.4em; /* Height for single line */
}
/* For single player teams */
.player.single-player .semibold {
line-height: 1.4em;
overflow: hidden;
position: relative;
text-overflow: ellipsis;
}
/* For two player teams */
.player.two-players .semibold {
line-height: 1.4em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.scores { .scores {
@ -344,7 +369,6 @@ tr {
font-size: 1.3em; font-size: 1.3em;
vertical-align: middle; vertical-align: middle;
text-align: center; text-align: center;
padding: 0px 5px;
/* width: 30px; */ /* width: 30px; */
} }
@ -718,14 +742,15 @@ h-margin {
.flex-left { .flex-left {
flex: 1; flex: 1;
text-align: left; text-align: left;
padding: 5px 0px; justify-content: center;
min-height: 4em;
padding-right: 5px;
} }
.flex-right { .flex-right {
flex: initial; flex: initial;
text-align: right; text-align: right;
vertical-align: middle; justify-content: center;
padding: 5px 0px;
} }
#header { #header {
@ -833,7 +858,6 @@ h-margin {
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
display: block; display: block;
padding: 2px 8px;
border-radius: 6px; border-radius: 6px;
} }
@ -878,10 +902,6 @@ h-margin {
background-color: #90ee90; /* Light green color */ background-color: #90ee90; /* Light green color */
} }
.player {
position: relative; /* Ensures the overlay is positioned within this block */
}
.overlay-text { .overlay-text {
position: absolute; position: absolute;
top: 50%; top: 50%;

@ -8,7 +8,10 @@
<div> <div>
<div class="flex" :class="group_stage.teams[i-1].qualified ? 'qualified' : ''"> <div class="flex" :class="group_stage.teams[i-1].qualified ? 'qualified' : ''">
<div class="flex-left"> <div class="flex-left player" :class="{
'single-player': group_stage.teams[i-1].names.length === 1,
'two-players': group_stage.teams[i-1].names.length === 2
}">
<template x-for="name in group_stage.teams[i-1].names"> <template x-for="name in group_stage.teams[i-1].names">
<div class="bold" x-text="name"></div> <div class="bold" x-text="name"></div>
@ -30,17 +33,15 @@
</div> </div>
<div class="flex-right"> <div class="flex-right">
<div x-show="group_stage.teams[i-1].match_count == 0"> <div x-show="group_stage.started === false">
<div class="score ws numbers" x-show="hide_weight == false"> <div class="score ws numbers" x-show="hide_weight === false">
<span x-text="group_stage.teams[i-1].weight"></span> <span x-text="group_stage.teams[i-1].weight"></span>
</div> </div>
</div> </div>
<div x-show="group_stage.teams[i-1].match_count > 0"> <div x-show="group_stage.teams[i-1].match_count > 0 && group_stage.started === true">
<div class="center"> <div class="score ws numbers"><span x-text="group_stage.teams[i-1].win_loss"></span></div>
<div class="score ws numbers"><span x-text="group_stage.teams[i-1].win_loss"></span></div> <div class="ws numbers"><span x-text="group_stage.teams[i-1].diff"></span></div>
<div class="ws numbers"><span x-text="group_stage.teams[i-1].diff"></span></div>
</div>
</div> </div>
</div> </div>

@ -16,7 +16,10 @@
<div> <div>
<div class="match-result"> <div class="match-result">
<div class="player"> <div class="player" :class="{
'single-player': match.teams[i-1].names.length === 1,
'two-players': match.teams[i-1].names.length === 2
}">
<!-- Show lucky loser or walkout status --> <!-- Show lucky loser or walkout status -->
<template x-if="match.teams[i-1].is_lucky_loser"> <template x-if="match.teams[i-1].is_lucky_loser">
<div class="overlay-text right-label minor-info semibold">(LL)</div> <div class="overlay-text right-label minor-info semibold">(LL)</div>
@ -65,6 +68,11 @@
}, },
}" x-html="showWalkOut(match, match.teams[i-1])"> }" x-html="showWalkOut(match, match.teams[i-1])">
</span> </span>
<template x-if="!match.tournament?.hide_weight && match.teams[i-1].weight && !match.has_walk_out && match.teams[i-1].scores.length === 0">
<span class="score ws numbers" x-text="match.teams[i-1].weight"></span>
</template>
</div> </div>
</div> </div>

@ -13,7 +13,7 @@
{% for team in group_stage.teams %} {% for team in group_stage.teams %}
<div class="flex {% if team.qualified %}qualified{% endif %}"> <div class="flex {% if team.qualified %}qualified{% endif %}">
<div class="flex-left"> <div class="flex-left player {% if team.names|length == 1 %}single-player{% else %}two-players{% endif %}">
{% if team.team_registration.id %} {% if team.team_registration.id %}
<a href="{% url 'team-details' tournament.id team.team_registration.id %}" class="group-stage-link"> <!-- Add this anchor tag --> <a href="{% url 'team-details' tournament.id team.team_registration.id %}" class="group-stage-link"> <!-- Add this anchor tag -->
{% endif %} {% endif %}
@ -31,10 +31,8 @@
</div> </div>
<div class="flex-right"> <div class="flex-right">
{% if group_stage.started %} {% if group_stage.started %}
<div class="center"> <div class="score ws numbers">{{ team.wins_losses }}</div>
<div class="score ws numbers">{{ team.wins_losses }}</div> <div class="ws numbers">{{ team.formatted_diff }}</div>
<div class="ws numbers">{{ team.formatted_diff }}</div>
</div>
{% else %} {% else %}
{% if tournament.hide_weight %} {% if tournament.hide_weight %}
<div class="score ws"></div> <div class="score ws"></div>

@ -14,7 +14,7 @@
{% for team in match.teams %} {% for team in match.teams %}
<div class="match-result {% cycle 'bottom-border' '' %}"> <div class="match-result {% cycle 'bottom-border' '' %}">
<div class="player"> <div class="player {% if team.names|length == 1 %}single-player{% else %}two-players{% endif %}">
{% if team.id %} {% if team.id %}
<a href="{% url 'team-details' tournament.id team.id %}" class="player-link"> <a href="{% url 'team-details' tournament.id team.id %}" class="player-link">
{% endif %} {% endif %}
@ -27,11 +27,7 @@
{% for name in team.names %} {% for name in team.names %}
<div class="semibold{% if team.walk_out == 1 %} strikethrough{% endif %}{% if team.is_winner %} winner{% endif %}"> <div class="semibold{% if team.walk_out == 1 %} strikethrough{% endif %}{% if team.is_winner %} winner{% endif %}">
{% if name|length > 0 %} {{ name }}
{{ name }}
{% else %}
&nbsp;
{% endif %}
</div> </div>
{% endfor %} {% endfor %}
{% if team.id %} {% if team.id %}

@ -20,6 +20,8 @@
data-disabled="{{ match.disabled|lower }}" data-disabled="{{ match.disabled|lower }}"
data-match-group-name="{{ match_group.name }}" data-match-group-name="{{ match_group.name }}"
data-match-format="{{ match.format }}" data-match-format="{{ match.format }}"
data-match-title="{{ match.title }}"
data-round-id="{{ match_group.round_id }}"
class="match-template"> class="match-template">
{% include 'tournaments/bracket_match_cell.html' %} {% include 'tournaments/bracket_match_cell.html' %}
</div> </div>
@ -29,13 +31,16 @@
</div> </div>
<script> <script>
const tournamentId = "{{ tournament.id }}";
function renderBracket() { function renderBracket() {
const bracket = document.getElementById('bracket'); const bracket = document.getElementById('bracket');
const matchTemplates = document.getElementById('match-templates').children; const matchTemplates = document.getElementById('match-templates').children;
const rounds = []; const rounds = [];
const matchPositions = []; const matchPositions = [];
const matchDisabled = []; // New array to track disabled matches
const doubleButterflyMode = {{ double_butterfly_mode|lower }};
const displayLoserFinal = {{ display_loser_final|lower }};
// Group matches by round // Group matches by round
Array.from(matchTemplates).forEach(template => { Array.from(matchTemplates).forEach(template => {
@ -56,15 +61,18 @@ function renderBracket() {
const baseDistance = matchHeight + matchSpacing; const baseDistance = matchHeight + matchSpacing;
bracket.innerHTML = ''; bracket.innerHTML = '';
const roundCount = rounds.length; const roundCount = rounds.length;
const finalRoundIndex = (roundCount - 1) / 2; let finalRoundIndex = (roundCount - 1);
if (doubleButterflyMode == true) {
finalRoundIndex = finalRoundIndex / 2;
}
let nextMatchDistance = baseDistance; let nextMatchDistance = baseDistance;
let minimumMatchDistance = 1; let minimumMatchDistance = 1;
if (rounds[0].length <= 2) {
minimumMatchDistance = 2
nextMatchDistance = baseDistance * 2;
}
rounds.forEach((roundMatches, roundIndex) => { rounds.forEach((roundMatches, roundIndex) => {
if (rounds[0].length <= 2) {
minimumMatchDistance = 2
nextMatchDistance = baseDistance * 2;
}
const roundDiv = document.createElement('div'); const roundDiv = document.createElement('div');
roundDiv.className = 'butterfly-round'; roundDiv.className = 'butterfly-round';
roundDiv.style.setProperty('--match-width', `${365}px`); roundDiv.style.setProperty('--match-width', `${365}px`);
@ -77,10 +85,22 @@ function renderBracket() {
const firstMatchTemplate = roundMatches[0].closest('.match-template'); const firstMatchTemplate = roundMatches[0].closest('.match-template');
const matchGroupName = firstMatchTemplate.dataset.matchGroupName; const matchGroupName = firstMatchTemplate.dataset.matchGroupName;
const matchFormat = firstMatchTemplate.dataset.matchFormat; const matchFormat = firstMatchTemplate.dataset.matchFormat;
const roundId = firstMatchTemplate.dataset.roundId; // Add this line
const nameSpan = document.createElement('div'); let nameSpan = document.createElement('div');
nameSpan.className = 'round-name'; nameSpan.className = 'round-name';
nameSpan.textContent = matchGroupName; nameSpan.textContent = matchGroupName;
if (roundIndex == finalRoundIndex || roundIndex == finalRoundIndex - 1 && displayLoserFinal ||roundIndex == finalRoundIndex + 1 && displayLoserFinal) {
} else {
nameSpan = document.createElement('a');
nameSpan.className = 'round-name btn small-button';
nameSpan.textContent = matchGroupName;
if (roundId) {
nameSpan.href = `/tournament/${tournamentId}/round/${roundId}/bracket/`;
nameSpan.style.cursor = 'pointer';
}
nameSpan.style.textDecoration = 'None';
}
const formatSpan = document.createElement('div'); const formatSpan = document.createElement('div');
formatSpan.className = 'round-format'; formatSpan.className = 'round-format';
@ -92,23 +112,28 @@ function renderBracket() {
// Create matches container // Create matches container
const matchesContainer = document.createElement('div'); const matchesContainer = document.createElement('div');
matchesContainer.className = 'matches-container'; matchesContainer.className = 'matches-container';
if (roundIndex >= finalRoundIndex - 1) { if (roundCount > 5 && doubleButterflyMode == true) {
if (roundCount > 5) { if (roundIndex >= finalRoundIndex - 1) {
matchesContainer.style.transform = `translateX(-50%)`; matchesContainer.style.transform = `translateX(-50%)`;
if (roundIndex >= finalRoundIndex + 2) { if (roundIndex >= finalRoundIndex + 2) {
matchesContainer.style.transform = `translateX(-100%)`; matchesContainer.style.transform = `translateX(-100%)`;
}
} }
}
} }
roundDiv.appendChild(matchesContainer); roundDiv.appendChild(matchesContainer);
matchPositions[roundIndex] = []; matchPositions[roundIndex] = [];
matchDisabled[roundIndex] = []; // Initialize array for this round
roundMatches.forEach((matchTemplate, matchIndex) => { roundMatches.forEach((matchTemplate, matchIndex) => {
const matchTitle = matchTemplate.dataset.matchTitle;
const matchDiv = document.createElement('div'); const matchDiv = document.createElement('div');
matchDiv.className = 'butterfly-match'; matchDiv.className = 'butterfly-match';
matchDiv.style.position = 'absolute'; matchDiv.style.position = 'absolute';
const isDisabled = matchTemplate.dataset.disabled === 'true'; const isDisabled = matchTemplate.dataset.disabled === 'true';
matchDisabled[roundIndex][matchIndex] = isDisabled;
let isIncomingLineIsDisabled = isDisabled;
let top; let top;
let left; let left;
let right; let right;
@ -119,32 +144,63 @@ function renderBracket() {
} }
if (roundIndex === 0) { if (roundIndex === 0) {
const nextMatchesCount = rounds[roundIndex + 1].length;
if (currentMatchesCount == nextMatchesCount) { if (roundCount > 1) {
const nextMatchesCount = rounds[roundIndex + 1].length;
if (currentMatchesCount == nextMatchesCount && roundCount > 2) {
nextMatchDistance = 0;
}
} else {
nextMatchDistance = 0; nextMatchDistance = 0;
} }
top = matchIndex * (matchHeight + matchSpacing) * minimumMatchDistance; top = matchIndex * (matchHeight + matchSpacing) * minimumMatchDistance;
} else if (roundIndex === roundCount - 1) {
if (roundCount == 3 && doubleButterflyMode) {
top = top + (matchHeight + matchSpacing) / 2
}
} else if (roundIndex === roundCount - 1 && doubleButterflyMode == true) {
const nextMatchesCount = rounds[roundIndex - 1].length; const nextMatchesCount = rounds[roundIndex - 1].length;
if (currentMatchesCount == nextMatchesCount) { if (currentMatchesCount == nextMatchesCount) {
nextMatchDistance = 0; nextMatchDistance = 0;
} }
} else if (roundIndex == finalRoundIndex) { } else if (roundIndex == finalRoundIndex) {
if (doubleButterflyMode == true) {
let lgth = matchPositions[0].length / 2; let lgth = matchPositions[0].length / 2;
let index = lgth + matchIndex - 1; let index = lgth + matchIndex - 1;
// If index goes negative, use 0 instead // If index goes negative, use 0 instead
if (matchIndex == 0) { if (displayLoserFinal == true) {
top = matchPositions[roundIndex - 1][0] - baseDistance / 2; if (matchIndex == 0) {
top = matchPositions[roundIndex - 1][0] - baseDistance / 2;
} else {
top = matchPositions[roundIndex - 1][0] + baseDistance / 2;
}
nextMatchDistance = baseDistance;
} else { } else {
top = matchPositions[roundIndex - 1][0] + baseDistance / 2; top = matchPositions[roundIndex - 1][0];
nextMatchDistance = 0;
} }
nextMatchDistance = baseDistance; } else {
const parentIndex1 = matchIndex * 2;
const parentIndex2 = parentIndex1 + 1;
const parentPos1 = matchPositions[roundIndex - 1][parentIndex1];
const parentPos2 = matchPositions[roundIndex - 1][parentIndex2];
top = (parentPos1 + parentPos2) / 2;
nextMatchDistance = 0;
if (displayLoserFinal == true) {
if (matchIndex == 1) {
top = matchPositions[roundIndex][0] + baseDistance + 80;
isIncomingLineIsDisabled = true;
}
}
}
} else if (roundIndex < finalRoundIndex) { } else if (roundIndex < finalRoundIndex) {
const previousMatchesCount = rounds[roundIndex - 1].length; const previousMatchesCount = rounds[roundIndex - 1].length;
const nextMatchesCount = rounds[roundIndex + 1].length; const nextMatchesCount = rounds[roundIndex + 1].length;
if (currentMatchesCount == nextMatchesCount) { if (currentMatchesCount == nextMatchesCount && displayLoserFinal == false) {
nextMatchDistance = 0; nextMatchDistance = 0;
} else if (matchPositions.length > roundIndex - 1) { } else if (matchPositions.length > roundIndex - 1) {
nextMatchDistance = (matchPositions[roundIndex - 1][1] - matchPositions[roundIndex - 1][0]); nextMatchDistance = (matchPositions[roundIndex - 1][1] - matchPositions[roundIndex - 1][0]);
@ -152,6 +208,9 @@ function renderBracket() {
} }
if (currentMatchesCount == previousMatchesCount) { if (currentMatchesCount == previousMatchesCount) {
if (matchDisabled[roundIndex - 1][matchIndex] == true) {
isIncomingLineIsDisabled = true
}
top = matchPositions[roundIndex - 1][matchIndex]; top = matchPositions[roundIndex - 1][matchIndex];
} else { } else {
const parentIndex1 = matchIndex * 2; const parentIndex1 = matchIndex * 2;
@ -172,23 +231,24 @@ function renderBracket() {
} }
} }
if (roundIndex >= finalRoundIndex - 1) { if (roundCount > 5 && doubleButterflyMode == true) {
if (roundCount > 5) { if (roundIndex >= finalRoundIndex - 2) {
if (roundIndex == finalRoundIndex - 1) { if (roundIndex == finalRoundIndex - 1) {
matchDiv.classList.add('inward'); matchDiv.classList.add('inward');
} }
if (roundIndex == finalRoundIndex + 1) { if (roundIndex == finalRoundIndex + 1) {
matchDiv.classList.add('outward'); matchDiv.classList.add('outward');
} }
if (roundIndex === finalRoundIndex - 2 || roundIndex === finalRoundIndex + 2) {
nextMatchDistance = nextMatchDistance - baseDistance;
}
} }
} }
if (roundIndex === finalRoundIndex - 2 || roundIndex === finalRoundIndex + 2) { if (displayLoserFinal == true) {
nextMatchDistance = nextMatchDistance - baseDistance; if (doubleButterflyMode == true && (roundIndex == finalRoundIndex - 1 || roundIndex == finalRoundIndex + 1)) {
} nextMatchDistance = baseDistance;
else if (roundIndex == finalRoundIndex - 1 || roundIndex == finalRoundIndex + 1) { }
nextMatchDistance = baseDistance;
} }
matchDiv.style.setProperty('--next-match-distance', `${nextMatchDistance}px`); matchDiv.style.setProperty('--next-match-distance', `${nextMatchDistance}px`);
@ -212,21 +272,38 @@ function renderBracket() {
// Position title above the first match // Position title above the first match
titleDiv.style.top = `${top - 80}px`; // Adjust the 60px offset as needed titleDiv.style.top = `${top - 80}px`; // Adjust the 60px offset as needed
titleDiv.style.position = 'absolute'; titleDiv.style.position = 'absolute';
if (roundIndex == finalRoundIndex - 1) { if (roundCount >= 5 && doubleButterflyMode == true) {
titleDiv.style.marginLeft = '50px'; if (roundIndex == finalRoundIndex - 1) {
} else if (roundIndex == finalRoundIndex + 1) { titleDiv.style.marginLeft = '50px';
titleDiv.style.marginLeft = '-50px'; } else if (roundIndex == finalRoundIndex + 1) {
titleDiv.style.marginLeft = '-50px';
}
} }
matchesContainer.appendChild(titleDiv); matchesContainer.appendChild(titleDiv);
} }
if (roundIndex == finalRoundIndex && matchIndex === 1 && displayLoserFinal == true && doubleButterflyMode == false) {
let nameSpan = document.createElement('div');
nameSpan.className = 'round-name';
nameSpan.textContent = matchTitle;
const formatSpan = document.createElement('div');
formatSpan.className = 'round-format';
formatSpan.textContent = matchFormat;
const titleDiv = document.createElement('div');
titleDiv.className = 'round-title';
titleDiv.appendChild(nameSpan);
titleDiv.appendChild(formatSpan);
titleDiv.style.top = `${top - 80}px`; // Adjust the 60px offset as needed
titleDiv.style.position = 'absolute';
matchesContainer.appendChild(titleDiv);
}
matchDiv.innerHTML = ` matchDiv.innerHTML = `
<div class="incoming-line ${isDisabled ? 'disabled' : ''}"></div> <div class="incoming-line ${isIncomingLineIsDisabled ? 'disabled' : ''}"></div>
<div class="match-content ${isDisabled ? 'disabled' : ''}">${matchTemplate.innerHTML}</div> <div class="match-content ${isDisabled ? 'disabled' : ''}">${matchTemplate.innerHTML}</div>
`; `;
if (roundIndex == finalRoundIndex - 1) { if (roundIndex == finalRoundIndex - 1 && displayLoserFinal == true && doubleButterflyMode == true) {
const matchDiv2 = document.createElement('div'); const matchDiv2 = document.createElement('div');
matchDiv2.className = 'butterfly-match'; matchDiv2.className = 'butterfly-match';
matchDiv2.classList.add('inward'); matchDiv2.classList.add('inward');
@ -295,8 +372,13 @@ function renderBracket() {
} }
.round-name { .round-name {
font-size: 1.5em; /* Make the round name bigger */ font-size: 1.5em;
margin-bottom: 5px; margin-bottom: 5px;
transition: color 0.2s;
}
.round-name a:hover {
color: orange;
} }
.round-format { .round-format {
@ -363,7 +445,7 @@ function renderBracket() {
left: calc(100% + 20px); left: calc(100% + 20px);
bottom: calc(50% - 2px); /* Account for half of horizontal line height */ bottom: calc(50% - 2px); /* Account for half of horizontal line height */
width: 2px; width: 2px;
height: calc((var(--next-match-distance)) / 2); /* Add half of horizontal line height */ height: calc(((var(--next-match-distance)) / 2)); /* Add half of horizontal line height */
background: #666; background: #666;
} }

@ -18,6 +18,7 @@ urlpatterns = [
path('teams/', views.tournament_teams, name='tournament-teams'), path('teams/', views.tournament_teams, name='tournament-teams'),
path('info/', views.tournament_info, name='tournament-info'), path('info/', views.tournament_info, name='tournament-info'),
path('bracket/', views.tournament_bracket, name='tournament-bracket'), path('bracket/', views.tournament_bracket, name='tournament-bracket'),
path('round/<str:round_id>/bracket/', views.round_bracket, name='round-bracket'),
path('prog/', views.tournament_prog, name='tournament-prog'), path('prog/', views.tournament_prog, name='tournament-prog'),
path('summons/', views.tournament_summons, name='tournament-summons'), path('summons/', views.tournament_summons, name='tournament-summons'),
path('broadcast/summons/', views.tournament_broadcasted_summons, name='broadcasted-summons'), path('broadcast/summons/', views.tournament_broadcasted_summons, name='broadcasted-summons'),

@ -106,11 +106,29 @@ from django.views.generic.edit import UpdateView
from .forms import CustomPasswordChangeForm from .forms import CustomPasswordChangeForm
def index(request): def index(request):
now = timezone.now()
thirty_days_ago = now - timedelta(days=30)
thirty_days_future = now + timedelta(days=30)
club_id = request.GET.get('club') club_id = request.GET.get('club')
future = future_tournaments(club_id) tournaments = tournaments_query(Q(end_date__isnull=True, start_date__gte=thirty_days_ago, start_date__lte=thirty_days_future), club_id, True, 50)
live = live_tournaments(club_id) display_tournament = [t for t in tournaments if t.display_tournament()]
finished = finished_tournaments(club_id) live = []
future = []
for t in display_tournament:
if t.supposedly_in_progress():
live.append(t)
elif t.starts_in_the_future():
future.append(t)
clean_ended_tournaments = tournaments_query(Q(end_date__isnull=False), club_id, False, 50)
clean_ended_tournaments = [t for t in clean_ended_tournaments if t.display_tournament()]
ended_tournaments = [t for t in display_tournament if t.should_be_over()]
# Combine both lists
finished = clean_ended_tournaments + ended_tournaments
# Sort the combined list by start_date in descending order
finished.sort(key=lambda t: t.start_date, reverse=True)
club = None club = None
if club_id: if club_id:
@ -127,7 +145,7 @@ def index(request):
} }
) )
def tournaments_query(query, club_id, ascending): def tournaments_query(query, club_id, ascending, limit=None):
queries = [query, Q(is_private=False, is_deleted=False, event__club__isnull=False)] queries = [query, Q(is_private=False, is_deleted=False, event__club__isnull=False)]
club = None club = None
@ -139,20 +157,47 @@ def tournaments_query(query, club_id, ascending):
sortkey = 'start_date' sortkey = 'start_date'
if not ascending: if not ascending:
sortkey = '-start_date' sortkey = '-start_date'
return Tournament.objects.filter(*queries).order_by(sortkey)
def finished_tournaments(club_id): queryset = Tournament.objects.filter(*queries).prefetch_related(
ended_tournaments = tournaments_query(Q(is_private=False, is_deleted=False, event__club__isnull=False), club_id, False) 'groupstage_set',
return [t for t in ended_tournaments if t.display_tournament() and t.should_be_over()] 'round_set',
'teamregistration_set',
).order_by(sortkey)
def live_tournaments(club_id): # Apply limit directly in the database query
tournaments = tournaments_query(Q(end_date__isnull=True), club_id, True) if limit is not None and isinstance(limit, int) and limit > 0:
queryset = queryset[:limit]
return queryset
def finished_tournaments(club_id, limit=None):
clean_ended_tournaments = tournaments_query(Q(end_date__isnull=False), club_id, False, limit)
clean_ended_tournaments = [t for t in clean_ended_tournaments if t.display_tournament()]
one_day_ago = timezone.now() - timedelta(days=1)
ended_tournaments = tournaments_query(
Q(end_date__isnull=True, start_date__lt=one_day_ago),
club_id,
False,
limit
)
ended_tournaments = [t for t in ended_tournaments if t.display_tournament() and t.should_be_over()]
# Combine both lists
all_tournaments = clean_ended_tournaments + ended_tournaments
# Sort the combined list by start_date in descending order
all_tournaments.sort(key=lambda t: t.start_date, reverse=True)
return all_tournaments
def live_tournaments(club_id, limit=None):
tournaments = tournaments_query(Q(end_date__isnull=True), club_id, True, limit)
return [t for t in tournaments if t.display_tournament() and t.supposedly_in_progress()] return [t for t in tournaments if t.display_tournament() and t.supposedly_in_progress()]
def future_tournaments(club_id): def future_tournaments(club_id, limit=None):
tomorrow = datetime.now().date() + timedelta(days=1) tournaments = tournaments_query(Q(end_date__isnull=True), club_id, True, limit)
tournaments = tournaments_query(Q(end_date__isnull=True, start_date__gte=tomorrow), club_id, True) return [t for t in tournaments if t.display_tournament() and t.starts_in_the_future()]
return [t for t in tournaments if t.display_tournament()]
def tournament_info(request, tournament_id): def tournament_info(request, tournament_id):
tournament = get_object_or_404(Tournament, pk=tournament_id) tournament = get_object_or_404(Tournament, pk=tournament_id)
@ -616,7 +661,6 @@ def send_email_to_jap_list():
log_error(error_message) log_error(error_message)
print(error_message) print(error_message)
def send_email(mail, name): def send_email(mail, name):
if name.lower() not in mail.lower(): if name.lower() not in mail.lower():
name = "" name = ""
@ -903,72 +947,33 @@ def tournament_bracket(request, tournament_id):
View to display tournament bracket structure. View to display tournament bracket structure.
""" """
tournament = get_object_or_404(Tournament, pk=tournament_id) tournament = get_object_or_404(Tournament, pk=tournament_id)
context = get_butterfly_bracket_view_context(tournament, parent_round=None, double_butterfly_mode=False, display_loser_final=True)
return render(request, 'tournaments/tournament_bracket.html', context)
# Get main bracket rounds (excluding children/ranking matches) def round_bracket(request, tournament_id, round_id):
main_rounds = tournament.round_set.filter( """
parent=None, View to display round bracket structure.
group_stage_loser_bracket=False """
).order_by('-index') tournament = get_object_or_404(Tournament, pk=tournament_id)
main_rounds_reversed = tournament.round_set.filter( round = get_object_or_404(Round, pk=round_id)
parent=None, context = get_butterfly_bracket_view_context(tournament, round, double_butterfly_mode=False, display_loser_final=False)
group_stage_loser_bracket=False return render(request, 'tournaments/tournament_bracket.html', context)
).order_by('index') # Removed the minus sign before 'index'
def get_butterfly_bracket_view_context(tournament, parent_round=None, double_butterfly_mode=False, display_loser_final=False):
loser_final = None if parent_round:
if len(main_rounds_reversed) >= 1: double_butterfly_mode = False
semi = main_rounds_reversed[1] display_loser_final = True
loser_round = tournament.round_set.filter(
parent=semi, serializable_match_groups = tournament.get_butterfly_bracket_match_group(parent_round, double_butterfly_mode, display_loser_final)
group_stage_loser_bracket=False
).order_by('index')
if len(loser_round) >= 1:
loser_final = loser_round[0]
# Create serializable match groups data
serializable_match_groups = []
# Add first half of each round (from last to semi-finals)
for round in main_rounds:
matches = round.match_set.filter(disabled=False).order_by('index')
if matches:
if len(matches) > 1:
midpoint = int(len(matches) / 2)
first_half_matches = matches[:midpoint]
else:
first_half_matches = list(matches) # Convert QuerySet to a list
if loser_final:
loser_matches = loser_final.matches.all()
if len(loser_matches) >= 1:
first_half_matches.append(loser_matches[0])
if first_half_matches:
match_group = tournament.create_match_group(
name=round.name(),
matches=first_half_matches
)
serializable_match_groups.append(match_group)
for round in main_rounds_reversed:
matches = round.match_set.filter(disabled=False).order_by('index')
if matches:
if len(matches) > 1:
midpoint = int(len(matches) / 2)
first_half_matches = matches[midpoint:]
match_group = tournament.create_match_group(
name=round.name(),
matches=first_half_matches
)
serializable_match_groups.append(match_group)
context = { context = {
'tournament': tournament, 'tournament': tournament,
'match_groups': serializable_match_groups 'match_groups': serializable_match_groups,
'double_butterfly_mode': double_butterfly_mode,
'display_loser_final': display_loser_final
} }
return render(request, 'tournaments/tournament_bracket.html', context) return context
def tournament_prog(request, tournament_id): def tournament_prog(request, tournament_id):
tournament = get_object_or_404(Tournament, id=tournament_id) tournament = get_object_or_404(Tournament, id=tournament_id)

Loading…
Cancel
Save