add planning event

sync_v2
Raz 6 months ago
parent 96fe35a742
commit c6f5571d43
  1. 18
      tournaments/models/match.py
  2. 75
      tournaments/models/tournament.py
  3. 2
      tournaments/static/tournaments/css/style.css
  4. 1
      tournaments/templates/tournaments/broadcast/broadcast.html
  5. 301
      tournaments/templates/tournaments/broadcast/broadcasted_planning.html
  6. 2
      tournaments/urls.py
  7. 41
      tournaments/views.py

@ -420,7 +420,13 @@ class Match(SideStoreModel):
# _minutes = int((_seconds % 3600) / 60)
# return f"{_hours:02d}h{_minutes:02d}min"
def live_match(self, hide_teams=False):
def tournament_title(self):
if self.group_stage:
return self.group_stage.tournament.full_name()
else:
return self.round.tournament.full_name()
def live_match(self, hide_teams=False, event_mode=False):
title = self.computed_name()
date = self.formatted_start_date()
time_indication = self.time_indication()
@ -435,7 +441,11 @@ class Match(SideStoreModel):
ended = self.end_date is not None
live_format = "Format " + FederalMatchCategory(self.format).format_label_short
livematch = LiveMatch(self.index, title, date, time_indication, court, self.started(), ended, group_stage_name, live_format, self.start_date, self.court_index, self.disabled, bracket_name, self.should_show_lucky_loser_status())
tournament_title = None
if event_mode is True:
tournament_title = self.tournament_title()
livematch = LiveMatch(self.index, title, date, time_indication, court, self.started(), ended, group_stage_name, live_format, self.start_date, self.court_index, self.disabled, bracket_name, self.should_show_lucky_loser_status(), tournament_title)
for team in self.live_teams(hide_teams):
livematch.add_team(team)
@ -502,7 +512,7 @@ class Team:
}
class LiveMatch:
def __init__(self, index, title, date, time_indication, court, started, ended, group_stage_name, format, start_date, court_index, disabled, bracket_name, should_show_lucky_loser_status):
def __init__(self, index, title, date, time_indication, court, started, ended, group_stage_name, format, start_date, court_index, disabled, bracket_name, should_show_lucky_loser_status, tournament_title):
self.index = index
self.title = title
self.date = date
@ -519,6 +529,7 @@ class LiveMatch:
self.court_index = court_index
self.bracket_name = bracket_name
self.should_show_lucky_loser_status = should_show_lucky_loser_status
self.tournament_title = tournament_title
def add_team(self, team):
self.teams.append(team)
@ -542,6 +553,7 @@ class LiveMatch:
"court_index": self.court_index,
"bracket_name": self.bracket_name,
"should_show_lucky_loser_status": self.should_show_lucky_loser_status,
"tournament_title": self.tournament_title,
}
def show_time_indication(self):

@ -564,9 +564,9 @@ class Tournament(BaseModel):
return groups
def create_match_group(self, name, matches, round_id=None, round_index=None, hide_teams=False):
def create_match_group(self, name, matches, round_id=None, round_index=None, hide_teams=False, event_mode=False):
matches = list(matches)
live_matches = [match.live_match(hide_teams) for match in matches]
live_matches = [match.live_match(hide_teams, event_mode) for match in matches]
# Filter out matches that have a start_date of None
valid_matches = [match for match in matches if match.start_date is not None]
@ -1906,7 +1906,7 @@ class Tournament(BaseModel):
return teams_processed
def planned_matches_by_day(self, day=None):
def planned_matches_by_day(self, day=None, all=False, event_mode=False):
"""
Collect all matches from tournaments and group them by their planned_start_date.
@ -1917,20 +1917,29 @@ class Tournament(BaseModel):
- days: List of unique days found (datetime.date objects)
- match_groups: Dictionary of match groups by date and hour or just for the selected day
"""
if event_mode is True and self.event.tournaments.count() == 1:
event_mode = False
# Get all matches from rounds and group stages - use a set to avoid duplicates
all_matches = set()
# Get matches only from top-level rounds to avoid duplicates
for round in self.rounds.filter(parent=None).all():
round_matches = round.get_matches_recursive(False)
# Add to set using IDs to avoid duplicates
for match in round_matches:
all_matches.add(match)
tournaments = [self]
if event_mode is True:
tournaments = self.event.tournaments.all()
# Get matches from group stages
for group_stage in self.group_stages.all():
for match in group_stage.matches.all():
all_matches.add(match)
for tournament in tournaments:
# Get matches only from top-level rounds to avoid duplicates
for round in tournament.rounds.filter(parent=None).all():
round_matches = round.get_matches_recursive(False)
# Add to set using IDs to avoid duplicates
for match in round_matches:
all_matches.add(match)
# Get matches from group stages
for group_stage in tournament.group_stages.all():
for match in group_stage.matches.all():
all_matches.add(match)
# Filter matches with planned_start_date - convert back to list
planned_matches = [match for match in all_matches if match.planned_start_date and not match.disabled]
@ -1955,6 +1964,40 @@ class Tournament(BaseModel):
# Sort days
sorted_days = sorted(list(days))
# Create match groups for the selected day
match_groups = []
if all:
for selected_day in sorted_days:
# Group matches by hour
matches_by_hour = {}
for match in matches_by_day[selected_day]:
local_time = timezone.localtime(match.planned_start_date)
hour_key = local_time.strftime('%H:%M')
if hour_key not in matches_by_hour:
matches_by_hour[hour_key] = []
matches_by_hour[hour_key].append(match)
hide_teams = self.show_teams_in_prog == False
# Create match groups for each hour
for hour, matches in sorted(matches_by_hour.items()):
# Sort matches by court if available
matches.sort(key=lambda m: (m.court_index if m.court_index is not None else 999))
local_date = matches[0].local_start_date()
formatted_name = formats.date_format(local_date, format='l j F à H:i').capitalize()
mg = self.create_match_group(
name=formatted_name,
matches=matches,
round_id=None,
round_index=None,
hide_teams=hide_teams,
event_mode=event_mode
)
match_groups.append(mg)
return sorted_days, match_groups
# If specific day requested, filter to that day
selected_day = None
@ -1969,9 +2012,6 @@ class Tournament(BaseModel):
else:
selected_day = sorted_days[0] if sorted_days else None
# Create match groups for the selected day
match_groups = []
if selected_day and selected_day in matches_by_day:
# Group matches by hour
matches_by_hour = {}
@ -1997,7 +2037,8 @@ class Tournament(BaseModel):
matches=matches,
round_id=None,
round_index=None,
hide_teams=hide_teams
hide_teams=hide_teams,
event_mode=event_mode
)
match_groups.append(mg)

@ -930,7 +930,7 @@ h-margin {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 40px;
padding: 30px 30px;
}
.left-content {

@ -30,6 +30,7 @@
<div><a href="{% url 'broadcasted-summons' tournament.id %}">Convocations</a></div>
<div><a href="{% url 'broadcasted-rankings' tournament.id %}">Classement</a></div>
<div><a href="{% url 'broadcasted-prog' tournament.id %}">(beta) Programmation</a></div>
<div><a href="{% url 'broadcasted-planning' tournament.id %}">(beta) Planning</a></div>
<div><a href="{% url 'broadcasted-bracket' tournament.id %}">(beta) Tableau</a></div>
</div>

@ -0,0 +1,301 @@
<!DOCTYPE html>
{% load static %}
{% load qr_code %}
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" href="{% static 'tournaments/css/foundation.min.css' %}" />
<link rel="stylesheet" href="{% static 'tournaments/css/basics.css' %}" />
<link rel="stylesheet" href="{% static 'tournaments/css/style.css' %}" />
<link rel="stylesheet" href="{% static 'tournaments/css/broadcast.css' %}" />
<style>
.running {
background-color: #90ee90 !important;
}
.bubble-footer.score, .match-cell .bubble {
height: 8rem;
display: flex;
flex-direction: column;
justify-content: center;
box-sizing: border-box;
font-size: 1.2em;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
color: black;
}
.court-label .bubble {
height: 4rem;
font-weight: bold;
margin: 0;
width: 100%;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
font-size: 1em;
color: black;
}
.match-cell .bubble {
background-color: white; /* Light blue */
}
.match-cell .bubble.even {
background-color: white; /* Light mint */
}
.match-cell .bubble.empty {
background-color: rgba(173, 216, 230, 0.3); /* Light blue */
color: white;
}
.match-cell, .court-label {
width: 10rem;
display: flex;
}
/* Adjust width when court count is 5 or more */
.court-label .bubble,
.match-cell .bubble {
width: 100%;
margin: 0;
}
.court-label .bubble,
.cell.large-6 {
width: 50%;
padding: 0px;
}
.court-label .bubble,
.cell.large-12 {
width: 100%;
padding: 0px;
}
.grid-x {
display: flex;
flex-wrap: wrap;
}
.courts-row,
.matches-row {
display: flex;
width: 100%;
gap: 20px;
}
</style>
<link rel="icon" type="image/png" href="{% static 'tournaments/images/favicon.png' %}" />
<title>Programmation</title>
<script src="{% static 'tournaments/js/alpine.min.js' %}"></script>
<!-- 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 x-data="{
days: [],
currentDayIndex: 0,
currentPageIndex: 0,
matchGroups: [],
courtCount: {{ tournament.court_count|default:1 }},
groupsPerPage: 8,
retrieveData() {
fetch('/tournament/{{ tournament.id }}/planning/json/')
.then(res => res.json())
.then((data) => {
this.days = data.days || [];
this.matchGroups = data.match_groups || [];
this.currentPageIndex = 0;
if (this.days.length > 0 && this.currentDayIndex >= this.days.length) {
this.currentDayIndex = 0;
}
});
},
getMatchGroupsForDay(day) {
const formattedDay = day;
const filteredGroups = this.matchGroups.filter(group => {
if (!group.matches || group.matches.length === 0) return false;
return group.name && formattedDay && group.name.includes(formattedDay);
});
// If court count is 5 or more, show fewer groups per page
let groupsPerPageThreshold = this.courtCount >= 5 ? Math.ceil(this.groupsPerPage / 2) : this.groupsPerPage;
const paginatedGroups = [];
for (let i = 0; i < Math.ceil(filteredGroups.length / groupsPerPageThreshold); i++) {
paginatedGroups.push(filteredGroups.slice(i * groupsPerPageThreshold, (i + 1) * groupsPerPageThreshold));
}
return paginatedGroups;
},
getCourtNumber(courtName) {
if (!courtName) return 999;
const match = courtName.match(/(\d+)/);
return match ? parseInt(match[1]) : 999;
},
organizeMatchesByCourt(matches) {
// Create an array of court positions, each containing the match or null
const courtMatches = Array(this.courtCount).fill(null);
if (matches && matches.length > 0) {
matches.forEach(match => {
if (match && match.court) {
const courtNum = this.getCourtNumber(match.court);
if (courtNum > 0 && courtNum <= this.courtCount) {
courtMatches[courtNum - 1] = match;
}
}
});
}
return courtMatches;
},
loop() {
this.retrieveData();
setInterval(() => {
if (this.days.length > 0) {
const currentDay = this.days[this.currentDayIndex];
const pagesForDay = this.getMatchGroupsForDay(currentDay);
if (pagesForDay && pagesForDay.length > 1) {
const _currentPageIndex = this.currentPageIndex;
this.currentPageIndex = (this.currentPageIndex + 1) % pagesForDay.length;
if (_currentPageIndex >= 1 && this.currentPageIndex === 0) {
this.currentDayIndex = (this.currentDayIndex + 1) % this.days.length;
}
} else {
this.currentPageIndex = 0;
this.currentDayIndex = (this.currentDayIndex + 1) % this.days.length;
}
} else {
this.currentDayIndex = 0;
this.currentPageIndex = 0;
}
}, 15000);
}
}" x-init="loop()">
<header>
<div id="header">
<div class="left-content bubble-header">
<img src="{% static 'tournaments/images/PadelClub_logo_512.png' %}" alt="logo" class="logo">
<div class="left-margin">
<h1 class="club">{{ tournament.broadcast_event_display_name }}</h1>
<h1 class="event" x-text="days[currentDayIndex]"></h1>
</div>
</div>
<div class="right-content">{% qr_from_text qr_code_url options=qr_code_options %}</div>
</div>
</header>
<div class="wrapper">
<main>
<div class="grid-x">
<div class="cell" :class="{'large-12': courtCount >= 5, 'large-6': courtCount < 5}">
<div style="display: flex; margin-bottom: 20px;">
<div class="bubble-footer score ws bold" style="visibility: hidden; display: flex; align-items: center; justify-content: center; margin: 0; width: 7em; margin-right: 15px; height: 40px;">
<h1 class="score ws bold" style="margin: 0; padding: 0;">00:00</h1>
</div>
<div class="courts-row">
<template x-for="courtNum in Array.from({length: courtCount || 1}, (_, i) => i + 1)" :key="courtNum">
<div class="court-label">
<div class="bubble">
<div class="score ws bold">Terrain <span x-text="courtNum"></span></div>
</div>
</div>
</template>
</div>
</div>
</div>
<div class="cell" :class="{'large-12': courtCount >= 5, 'large-6': courtCount < 5}" x-show="courtCount < 5">
<div style="display: flex; margin-bottom: 20px;">
<div class="bubble-footer score ws bold" style="visibility: hidden; display: flex; align-items: center; justify-content: center; margin: 0; width: 7em; margin-right: 15px; height: 40px;">
<h1 class="score ws bold" style="margin: 0; padding: 0;">00:00</h1>
</div>
<div class="courts-row">
<template x-for="courtNum in Array.from({length: courtCount || 1}, (_, i) => i + 1)" :key="courtNum">
<div class="court-label">
<div class="bubble">
<div class="score ws bold">Terrain <span x-text="courtNum"></span></div>
</div>
</div>
</template>
</div>
</div>
</div>
</div>
<template x-for="(day, dayIndex) in days" :key="day">
<div x-show="currentDayIndex === dayIndex">
<template x-for="(groupPage, pageIndex) in getMatchGroupsForDay(day)" :key="'page-' + pageIndex">
<div x-show="currentPageIndex === pageIndex">
<div class="grid-x">
<template x-for="(group, groupIndex) in groupPage" :key="groupIndex">
<div class="cell" :class="{'large-12': courtCount >= 5, 'large-6': courtCount < 5}">
<div style="display: flex; margin-bottom: 20px;">
<!-- Group name container -->
<div class="bubble-footer score ws bold" style="display: flex; align-items: center; justify-content: center; margin: 0; width: 7em; margin-right: 15px;">
<h1 class="score ws bold" style="margin: 0; padding: 0;" x-text="group.name.slice(-5)"></h1>
</div>
<!-- Matches container -->
<div class="matches-row">
<template x-for="(match, courtIndex) in organizeMatchesByCourt(group.matches)" :key="courtIndex">
<div class="match-cell">
<template x-if="match">
<div class="bubble" :class="{'running': !match.ended && match.started, 'even': courtIndex % 2 === 1}" style="text-align: center;">
<div class="score ws bold" x-text="match.group_stage_name ? match.group_stage_name : match.title"></div>
<template x-if="match.tournament_title">
<div class="minor-info semibold" x-text="match.tournament_title"></div>
</template>
</div>
</template>
<template x-if="!match">
<div class="bubble empty" style="text-align: center;">
</div>
</template>
</div>
</template>
</div>
</div>
</div>
</template>
</div>
</div>
</template>
</div>
</template>
</main>
</div>
<footer class="footer-broadcast">
{% if tournament.event.images.exists %}
<div class="bubble-footer">
<div class="bubble-sponsor">
{% for image in tournament.event.images.all %}
<img src="{{ image.image.url }}" alt="{{ image.title|default:'Sponsor' }}"
class="sponsor-logo-broadcast">
{% endfor %}
</div>
</div>
{% endif %}
</footer>
</body>
</html>

@ -32,9 +32,11 @@ urlpatterns = [
path('broadcast/auto/', views.automatic_broadcast, name='automatic-broadcast'),
path('matches/json/', views.tournament_matches_json, name='tournament-matches-json'),
path('prog/json/', views.tournament_prog_json, name='tournament-prog-json'),
path('planning/json/', views.tournament_planning_json, name='tournament-planning-json'),
path('broadcast/json/', views.broadcast_json, name='broadcast-json'),
path('broadcast/group-stages/', views.tournament_broadcasted_group_stages, name='broadcasted-group-stages'),
path('broadcast/prog/', views.tournament_broadcasted_prog, name='broadcasted-prog'),
path('broadcast/planning/', views.tournament_broadcasted_planning, name='broadcasted-planning'),
path('broadcast/bracket/', views.tournament_broadcasted_bracket, name='broadcasted-bracket'),
path('bracket/json/', views.tournament_bracket_json, name='tournament-bracket-json'),
path('group-stages/', views.tournament_group_stages, name='group-stages'),

@ -31,6 +31,7 @@ import pandas as pd
from tournaments.utils.extensions import create_random_filename
from django.core.files.storage import default_storage
from django.core.files.base import ContentFile
from django.utils import formats
from api.tokens import account_activation_token
@ -386,6 +387,37 @@ def tournament_prog_json(request, tournament_id):
data = json.dumps(live_matches, default=vars)
return HttpResponse(data, content_type='application/json')
def tournament_planning_json(request, tournament_id):
tournament = get_object_or_404(Tournament, id=tournament_id)
day_param = request.GET.get('day', None)
# Get days and match groups using the planned_matches_by_day method
days, match_groups = tournament.planned_matches_by_day(day=day_param, all=True, event_mode=True)
# Format data for JSON response
formatted_days = [formats.date_format(day, format='l j F').capitalize() for day in days]
# Convert match groups to JSON-serializable format
match_groups_data = []
for match_group in match_groups:
if not hasattr(match_group, 'matches') or not match_group.matches:
continue
live_matches = []
for match in match_group.matches:
live_matches.append(match.to_dict())
match_groups_data.append({
'name': match_group.name,
'matches': live_matches
})
response_data = {
'days': formatted_days,
'match_groups': match_groups_data,
}
return JsonResponse(response_data, safe=False)
def tournament_group_stages(request, tournament_id):
tournament = get_object_or_404(Tournament, pk=tournament_id)
live_group_stages = list(tournament.live_group_stages())
@ -421,6 +453,15 @@ def tournament_broadcasted_bracket(request, tournament_id):
'qr_code_options': qr_code_options(),
})
def tournament_broadcasted_planning(request, tournament_id):
tournament = get_object_or_404(Tournament, pk=tournament_id)
return render(request, 'tournaments/broadcast/broadcasted_planning.html', {
'tournament': tournament,
'qr_code_url': qr_code_url(request, tournament_id),
'qr_code_options': qr_code_options(),
})
def tournament_bracket_json(request, tournament_id):
"""

Loading…
Cancel
Save