@ -184,7 +184,7 @@ class Tournament(BaseModel):
return self . get_federal_age_category_display ( )
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 ) :
return self . end_date is None
@ -549,7 +549,7 @@ class Tournament(BaseModel):
return groups
def create_match_group ( self , name , matches ) :
def create_match_group ( self , name , matches , round_id = None ) :
matches = list ( matches )
live_matches = [ match . live_match ( ) for match in matches ]
# Filter out matches that have a start_date of None
@ -566,7 +566,7 @@ class Tournament(BaseModel):
time_format = ' l d M '
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 ) :
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
# 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 ( ) ]
if group_stages :
return {
@ -693,6 +697,12 @@ class Tournament(BaseModel):
if previous_round :
# print('previous_round')
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 :
# print('group_stages')
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 . sort ( key = lambda gs : ( gs . index , gs . start_date is None , gs . start_date ) )
for group_stage in group_stages :
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 ) )
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 :
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
minimum_publish_date = self . creation_date . replace ( hour = 9 , minute = 0 ) + timedelta ( days = 1 )
return timezone . now ( ) > = timezone . localtime ( minimum_publish_date )
@ -846,7 +866,7 @@ class Tournament(BaseModel):
return self . has_team_registrations ( )
if self . publish_teams :
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 False
@ -858,7 +878,7 @@ class Tournament(BaseModel):
return False
if self . publish_summons :
return self . has_summons ( )
if timezone . now ( ) > = self . start_date :
if timezone . now ( ) > = self . local_ start_date( ) :
return self . has_summons ( )
return False
@ -872,7 +892,7 @@ class Tournament(BaseModel):
first_group_stage_start_date = self . group_stage_start_date ( )
if first_group_stage_start_date is None :
return timezone . now ( ) > = self . start_date
return timezone . now ( ) > = self . local_ start_date( )
else :
return timezone . now ( ) > = first_group_stage_start_date
@ -881,7 +901,8 @@ class Tournament(BaseModel):
if len ( group_stages ) == 0 :
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 ) :
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 )
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 )
if bracket_start_date < self . start_date :
bracket_start_date = self . start_date
if bracket_start_date < self . local_ start_date( ) :
bracket_start_date = self . local_ start_date( )
group_stage_start_date = self . group_stage_start_date ( )
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 ]
if len ( matches ) == 0 :
return None
return min ( matches , key = lambda m : m . start_date ) . start_date
return min ( matches , key = lambda m : m . start_date ) . local_start_date ( )
def getEightAm ( self , date ) :
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)
# 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 )
now = timezone . now ( )
@ -946,33 +966,41 @@ class Tournament(BaseModel):
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 ) :
if self . end_date is not None :
return True
timezoned_datetime = timezone . localtime ( self . start_date )
timezoned_datetime = self . local_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 :
group_stages = list ( self . group_stages . all ( ) ) # Use prefetched data
if group_stages :
# 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 ( ) :
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
semifinals = self . rounds . filter ( index = 1 , parent = None ) . first ( ) # Use prefetched data
if semifinals :
# Check if any match in semi-finals has started
for match in semifinals . matches . all ( ) : # Use prefetched data
if match . start_date is not None and match . is_ready ( ) :
return True
return False
@ -983,7 +1011,18 @@ class Tournament(BaseModel):
return self . hide_teams_weight
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 ) :
return plural_format ( " jour " , self . day_duration )
@ -1315,12 +1354,51 @@ class Tournament(BaseModel):
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 :
def __init__ ( self , name , matches , formatted_schedule ) :
def __init__ ( self , name , matches , formatted_schedule , round_id = None ) :
self . name = name
self . matches = matches
self . formatted_schedule = formatted_schedule
self . round_id = round_id
def add_match ( self , match ) :
self . matches . append ( match )