Laurent 2 months ago
commit 47c50780a4
  1. 31
      tournaments/models/match.py
  2. 66
      tournaments/models/tournament.py
  3. 263
      tournaments/templates/tournaments/broadcast/broadcasted_auto_event.html
  4. 84
      tournaments/templates/tournaments/broadcast/broadcasted_event_auto.html
  5. 110
      tournaments/views.py

@ -423,6 +423,37 @@ class Match(TournamentSubModel):
return timezone.now() > self.start_date
return False
def will_start(self):
"""
Returns True if match will start within the next hour
"""
if self.disabled or self.end_date:
return False
# Check start_date first
if self.start_date:
now = timezone.now()
time_until_start = (self.start_date - now).total_seconds()
return 0 < time_until_start <= 3600 # Within 1 hour (3600 seconds)
# Check planned_start_date as fallback
if self.planned_start_date:
now = timezone.now()
time_until_start = (self.planned_start_date - now).total_seconds()
return 0 < time_until_start <= 3600 # Within 1 hour (3600 seconds)
return False
def is_ongoing(self):
"""
Returns True if match has started but not finished
"""
if self.disabled:
return False
# Match is ongoing if it has started but hasn't ended
return self.started() and self.end_date is None
def should_appear(self):
if self.disabled is True:
return False

@ -1031,6 +1031,72 @@ class Tournament(BaseModel):
def will_start_soon(self, hour_delta=2):
return self.has_started(hour_delta=hour_delta)
def has_ongoing_matches(self):
"""
Returns True if tournament has any matches that are currently ongoing
"""
# Check matches in rounds
for round_obj in self.rounds.all():
for match in round_obj.matches.all():
if match.is_ongoing():
return True
# Check matches in group stages
for group_stage in self.group_stages.all():
for match in group_stage.matches.all():
if match.is_ongoing():
return True
return False
def has_matches_starting_soon(self):
"""
Returns True if tournament has any matches starting within the next hour
"""
# Check matches in rounds
for round_obj in self.rounds.all():
for match in round_obj.matches.all():
if match.will_start():
return True
# Check matches in group stages
for group_stage in self.group_stages.all():
for match in group_stage.matches.all():
if match.will_start():
return True
return False
def get_next_match_start_time(self):
"""
Returns the datetime of the earliest upcoming match, or None if no upcoming matches
"""
next_times = []
# Check matches in rounds
for round_obj in self.rounds.all():
for match in round_obj.matches.all():
if match.disabled or match.end_date:
continue
if match.start_date and match.start_date > timezone.now():
next_times.append(match.start_date)
elif match.planned_start_date and match.planned_start_date > timezone.now():
next_times.append(match.planned_start_date)
# Check matches in group stages
for group_stage in self.group_stages.all():
for match in group_stage.matches.all():
if match.disabled or match.end_date:
continue
if match.start_date and match.start_date > timezone.now():
next_times.append(match.start_date)
elif match.planned_start_date and match.planned_start_date > timezone.now():
next_times.append(match.planned_start_date)
return min(next_times) if next_times else None
def are_teams_positioned(self):
teams = self.team_registrations.all()
filtered_teams = [t for t in teams if t.is_positioned()]

@ -1,263 +0,0 @@
<!DOCTYPE html>
<html>
{% load static %}
{% load qr_code %}
<head>
{% include 'tournaments/broadcast/base_head.html' %}
<script src="{% static 'tournaments/js/alpine.min.js' %}" defer></script>
<title>Broadcast</title>
<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["setDocumentTitle", document.domain + "/" + document.title]);
_paq.push(["setDoNotTrack", true]);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//matomo.padelclub.app/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '1']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<!-- End Matomo Code -->
</head>
<body>
<div x-data="{
tournamentIds: {{ tournament_ids|safe }},
activeTournamentIndex: 0,
paginatedMatches: null,
paginatedGroupStages: null,
paginatedSummons: null,
paginatedRankings: null,
has_sponsors: {{ tournament.has_sponsors|lower }},
active: 1,
hide_weight: {{ tournament.hide_weight|lower }},
prefixTitle: '',
eventTitle: '',
title: '',
retrieveDataForActiveTournament() {
const activeTournamentId = this.tournamentIds[this.activeTournamentIndex];
fetch(`/tournament/${activeTournamentId}/broadcast/json/`)
.then(res => res.json())
.then((data) => {
const hasContent = data.matches.length > 0 || data.group_stages.length > 0 || data.summons.length > 0 || data.rankings.length > 0;
if (!hasContent) {
console.warn(`Tournament ${activeTournamentId} has no content, skipping.`);
// Move to the next tournament and retry fetching data
this.activeTournamentIndex = (this.activeTournamentIndex + 1) % this.tournamentIds.length;
this.retrieveDataForActiveTournament(); // Recursively fetch the next tournament
if (this.activeTournamentIndex === this.tournamentIds.length - 1) {
this.prefixTitle = ''
this.eventTitle = '{{ club_name }}'
this.title = 'Aucun tournoi en cours'
}
return;
}
console.log('Fetched Data:', data); // Add this to debug the fetch result
this.paginatedMatches = this.paginate(data.matches, 8);
this.paginatedGroupStages = this.paginate(data.group_stages, 4);
this.paginatedSummons = this.paginateSummons(data.summons);
this.paginatedRankings = this.paginateRankings(data.rankings);
this.setPrefixTitle();
this.eventTitle = data.event_title
this.title = data.tournament_title
if (this.active > this.pageCount()) {
this.active = 1;
}
})
.catch((error) => {
console.error('Error fetching data:', error); // Handle fetch errors
});
},
paginateSummons(array) {
let pageSize = 16
if (window.innerHeight <= 720) {
pageSize = 12;
if (this.has_sponsors) {
pageSize = 10
}
} else if (window.innerHeight <= 1080) {
if (this.has_sponsors) {
pageSize = 12
}
}
pages = this.paginate(array, pageSize);
const splitGroups = [];
pages.forEach(group => {
const firstHalf = group.slice(0, pageSize / 2);
const secondHalf = group.slice(pageSize / 2);
if (secondHalf.length > 0) {
splitGroups.push([firstHalf, secondHalf]);
} else {
splitGroups.push([firstHalf]);
}
});
return splitGroups;
},
paginateRankings(array) {
let pageSize = 16
if (window.innerHeight <= 720) {
pageSize = 12;
if (this.has_sponsors) {
pageSize = 10
}
} else if (window.innerHeight <= 1080) {
if (this.has_sponsors) {
pageSize = 12
}
}
pages = this.paginate(array, pageSize);
const splitGroups = [];
pages.forEach(group => {
const firstHalf = group.slice(0, pageSize / 2);
const secondHalf = group.slice(pageSize / 2);
if (secondHalf.length > 0) {
splitGroups.push([firstHalf, secondHalf]);
} else {
splitGroups.push([firstHalf]);
}
});
return splitGroups;
},
paginate(array, pageSize) {
let paginatedArray = [];
for (let i = 0; i < array.length; i += pageSize) {
paginatedArray.push(array.slice(i, i + pageSize));
}
return paginatedArray;
},
loop() {
console.log('Loop function is called');
this.retrieveDataForActiveTournament();
if (this.pageCount() === 0 && this.tournamentIds.length > 0) {
console.warn(`Tournament ${this.tournamentIds[this.activeTournamentIndex]} has no pages, skipping to next.`);
this.skipToNextTournament();
}
setInterval(() => {
// After fetching the data, check if the current tournament has no pages
if (this.pageCount() === 0 && this.tournamentIds.length > 0) {
console.warn(`Tournament ${this.tournamentIds[this.activeTournamentIndex]} has no pages, skipping to next.`);
this.skipToNextTournament();
} else if (this.active === this.pageCount()) {
this.active = 1;
// Move to the next tournament
this.skipToNextTournament();
} else {
this.active += 1;
}
this.setPrefixTitle();
}, 15000);
},
skipToNextTournament() {
this.activeTournamentIndex = (this.activeTournamentIndex + 1) % this.tournamentIds.length;
this.retrieveDataForActiveTournament(); // Fetch data for the new tournament
},
pageCount() {
// Safely handle null or undefined values
const totalCount = (this.paginatedMatches?.length || 0) +
(this.paginatedGroupStages?.length || 0) +
(this.paginatedSummons?.length || 0) +
(this.paginatedRankings?.length || 0);
return totalCount;
},
setPrefixTitle() {
if (this.active < 1 + this.paginatedSummons.length) {
this.prefixTitle = 'Convocations';
} else if (this.active < 1 + this.paginatedSummons.length + this.paginatedMatches.length) {
this.prefixTitle = 'Matchs';
} else if (this.active < 1 + this.paginatedSummons.length + this.paginatedMatches.length + this.paginatedGroupStages.length) {
this.prefixTitle = 'Poules';
} else if (paginatedRankings.length > 0) {
this.prefixTitle = 'Classement';
}
}
}" x-init="loop()">
<header>
<div id="header">
<div class="left-content bubble">
<img src="{% static 'tournaments/images/PadelClub_logo_512.png' %}" alt="logo" class="logo">
<div class="left-margin">
<h1 class="club" x-text="eventTitle"></h1>
<h1 class="event">
<span x-text="prefixTitle"></span>
<span x-text="title"></span>
</h1>
</div >
</div>
{% if qr_code_options %}
<div class="right-content">{% qr_from_text qr_code_url options=qr_code_options %}</div>
{% endif %}
</div>
</header>
<div class="wrapper">
<main>
<div class="grid-x">
<template x-for="i in paginatedSummons.length">
<template x-for="column in paginatedSummons[i-1]">
<div class="cell medium-6 large-6 topblock" x-show="active === i">
{% include 'tournaments/broadcast/broadcasted_summon.html' %}
</div>
</template>
</template>
<template x-for="i in paginatedMatches.length" >
<template x-for="match in paginatedMatches[i-1]" >
<div class="cell medium-6 large-3" x-show="active === i + paginatedSummons.length">
{% include 'tournaments/broadcast/broadcasted_match.html' %}
</div>
</template>
</template>
<template x-for="i in paginatedGroupStages.length">
<template x-for="group_stage in paginatedGroupStages[i-1]">
<div class="cell medium-6 large-3" x-show="active === i + paginatedSummons.length + paginatedMatches.length">
{% include 'tournaments/broadcast/broadcasted_group_stage.html' %}
</div>
</template>
</template>
<template x-for="i in paginatedRankings.length">
<template x-for="column in paginatedRankings[i-1]">
<div class="cell medium-6 large-6 topblock padding10" x-show="active === i + paginatedSummons.length + paginatedMatches.length + paginatedGroupStages.length">
{% include 'tournaments/broadcast/broadcasted_ranking.html' %}
</div>
</template>
</template>
</div>
</main>
</div>
</div>
</body>
</html>

@ -22,46 +22,112 @@
display: block;
background-color: #000;
}
.error-message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 18px;
text-align: center;
z-index: 1000;
display: none;
}
</style>
</head>
<body>
<div class="error-message" id="errorMessage">
Rechargement du tournoi...
</div>
<iframe id="tournamentFrame" src="about:blank"></iframe>
<script>
const tournamentIds = {{ tournament_ids|safe }};
let currentIndex = 0;
const frame = document.getElementById('tournamentFrame');
const errorMessage = document.getElementById('errorMessage');
let retryCount = 0;
const maxRetries = 2;
function loadNextTournament() {
function loadNextTournament(isRetry = false) {
if (tournamentIds.length === 0) {
document.body.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100vh; font-size: 24px; color: white;">Aucun tournoi disponible</div>';
return;
}
const tournamentId = tournamentIds[currentIndex];
if (!isRetry) {
retryCount = 0;
// Move to next tournament only if not retrying
currentIndex = (currentIndex + 1) % tournamentIds.length;
}
// Hide error message when loading new tournament
errorMessage.style.display = 'none';
const tournamentId = tournamentIds[currentIndex === 0 ? tournamentIds.length - 1 : currentIndex - 1];
const url = `/tournament/${tournamentId}/broadcast/auto/`;
console.log(`Loading tournament ${currentIndex + 1}/${tournamentIds.length}: ${tournamentId.substring(0, 8)}`);
console.log(`${isRetry ? 'Retrying' : 'Loading'} tournament ${currentIndex}/${tournamentIds.length}: ${tournamentId.substring(0, 8)} (attempt ${retryCount + 1})`);
frame.src = url;
currentIndex = (currentIndex + 1) % tournamentIds.length;
// Set a timeout to detect if iframe fails to load
const loadTimeout = setTimeout(() => {
console.warn(`Tournament ${tournamentId.substring(0, 8)} failed to load (attempt ${retryCount + 1})`);
handleLoadFailure();
}, 8000); // 8 second timeout
// Clear timeout when iframe loads successfully
frame.onload = function() {
clearTimeout(loadTimeout);
retryCount = 0; // Reset retry count on successful load
console.log(`Tournament ${tournamentId.substring(0, 8)} loaded successfully`);
};
frame.onerror = function() {
clearTimeout(loadTimeout);
console.error(`Tournament ${tournamentId.substring(0, 8)} failed to load with error`);
handleLoadFailure();
};
}
function handleLoadFailure() {
if (retryCount < maxRetries) {
retryCount++;
errorMessage.textContent = `Rechargement du tournoi... (tentative ${retryCount + 1})`;
errorMessage.style.display = 'block';
console.log(`Retrying in 2 seconds... (attempt ${retryCount + 1}/${maxRetries + 1})`);
setTimeout(() => {
loadNextTournament(true); // Retry same tournament
}, 2000);
} else {
// Max retries reached, move to next tournament
console.error('Max retries reached, moving to next tournament');
errorMessage.textContent = 'Tournoi indisponible, passage au suivant...';
errorMessage.style.display = 'block';
setTimeout(() => {
loadNextTournament(false); // Move to next tournament
}, 2000);
}
}
// Listen for cycle completion messages from tournament iframe
window.addEventListener('message', function(event) {
if (event.data.type === 'tournamentCycleComplete') {
console.log(`Tournament completed (${event.data.reason})`);
setTimeout(loadNextTournament, 1000); // Switch after 1 second
setTimeout(() => loadNextTournament(false), 1000); // Switch after 1 second
} else if (event.data.type === 'tournamentError') {
console.error(`Tournament ${event.data.tournamentId.substring(0, 8)} failed: ${event.data.error}`);
console.log('Switching to next tournament due to error...');
setTimeout(loadNextTournament, 2000); // Switch after 2 seconds on error
console.error(`Tournament ${event.data.tournamentId.substring(0, 8)} reported error: ${event.data.error}`);
handleLoadFailure(); // Treat as load failure
}
});
// Load first tournament immediately
loadNextTournament();
loadNextTournament(false);
</script>
</body>
</html>

@ -532,12 +532,26 @@ def automatic_broadcast(request, tournament_id):
def automatic_broadcast_event(request, event_id):
event = get_object_or_404(Event, pk=event_id)
tournaments = Tournament.objects.filter(event=event).order_by('start_date')
now = timezone.now()
# Filter tournaments that have broadcast content
tournaments_with_content = []
for tournament in tournaments:
# Get all tournaments for the event (expanded time range for better coverage)
past_cutoff = now - timedelta(hours=72) # Extended past search
future_cutoff = now + timedelta(hours=72) # Extended future search
all_tournaments = Tournament.objects.filter(
event=event,
start_date__gte=past_cutoff,
start_date__lte=future_cutoff
).order_by('start_date')
# Categorize tournaments by priority
ongoing_tournaments = []
starting_soon_tournaments = []
other_tournaments_with_content = []
for tournament in all_tournaments:
try:
# First check if tournament has broadcast content
content = tournament.broadcast_content()
has_content = (
len(content.get('matches', [])) > 0 or
@ -545,13 +559,93 @@ def automatic_broadcast_event(request, event_id):
len(content.get('summons', [])) > 0 or
len(content.get('rankings', [])) > 0
)
if has_content:
tournaments_with_content.append(tournament)
except:
# Skip tournaments that error when getting content
# Categorize based on match activity (highest priority first)
if tournament.has_ongoing_matches():
ongoing_tournaments.append(tournament)
elif tournament.has_matches_starting_soon():
starting_soon_tournaments.append(tournament)
else:
other_tournaments_with_content.append(tournament)
except Exception as e:
print(f"Error processing tournament {tournament.id}: {e}")
continue
tournament_ids = [str(tournament.id) for tournament in tournaments_with_content]
# Sort each category by next match start time and tournament start time
def sort_by_next_match_time(tournament):
next_match_time = tournament.get_next_match_start_time()
return (
next_match_time if next_match_time else tournament.start_date,
tournament.start_date
)
# Sort ongoing tournaments by next match time (for consistency)
ongoing_tournaments.sort(key=sort_by_next_match_time)
# Sort starting soon tournaments by next match time (earliest first)
starting_soon_tournaments.sort(key=sort_by_next_match_time)
# Sort other tournaments by tournament start time
other_tournaments_with_content.sort(key=lambda t: t.start_date)
# Build final list prioritizing ongoing matches above all else
tournaments_to_display = []
# Priority 1: Tournaments with ongoing matches (take max 2)
tournaments_to_display.extend(ongoing_tournaments[:2])
# If we need more tournaments and have space
if len(tournaments_to_display) < 2:
remaining_slots = 2 - len(tournaments_to_display)
# Priority 2: Tournaments with matches starting soon
tournaments_to_display.extend(starting_soon_tournaments[:remaining_slots])
# If still need more tournaments
if len(tournaments_to_display) < 2:
remaining_slots = 2 - len(tournaments_to_display)
# Priority 3: Other tournaments with content
tournaments_to_display.extend(other_tournaments_with_content[:remaining_slots])
# Fallback: If still don't have enough tournaments, expand search
if len(tournaments_to_display) < 2:
# Get tournaments from wider time range
extended_tournaments = Tournament.objects.filter(
event=event
).exclude(
id__in=[t.id for t in tournaments_to_display]
).order_by('-start_date') # Most recent first
remaining_needed = 2 - len(tournaments_to_display)
for tournament in extended_tournaments[:remaining_needed]:
try:
# Check if tournament has any content
content = tournament.broadcast_content()
has_content = (
len(content.get('matches', [])) > 0 or
len(content.get('group_stages', [])) > 0 or
len(content.get('summons', [])) > 0 or
len(content.get('rankings', [])) > 0
)
if has_content:
tournaments_to_display.append(tournament)
except:
# If no content, add anyway as last resort
tournaments_to_display.append(tournament)
# Ensure we have exactly what we need (limit to 2)
tournaments_to_display = tournaments_to_display[:2]
# Log the prioritization for debugging
print(f"Event {event_id} broadcast prioritization:")
print(f" - Ongoing tournaments: {len(ongoing_tournaments)}")
print(f" - Starting soon tournaments: {len(starting_soon_tournaments)}")
print(f" - Other tournaments with content: {len(other_tournaments_with_content)}")
print(f" - Final selection: {[str(t.id) for t in tournaments_to_display]}")
tournament_ids = [str(tournament.id) for tournament in tournaments_to_display]
# Create QR code URL for the event broadcast
qr_code_url_val = reverse('automatic-broadcast-event', args=[event_id])

Loading…
Cancel
Save