From 14abf81de5bd52baf0d75ed48e648f8c70145f3f Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 14 Mar 2025 19:32:44 +0100 Subject: [PATCH] add wip bracket broadcast and bug fixes --- tournaments/models/match.py | 10 + tournaments/models/round.py | 4 +- tournaments/models/tournament.py | 18 +- tournaments/static/tournaments/css/style.css | 8 + .../tournaments/css/tournament_bracket.css | 231 +++++++++ .../tournaments/js/tournament_bracket.js | 333 ++++++++++++ tournaments/templates/tournaments/base.html | 1 + .../tournaments/bracket_match_cell.html | 2 +- .../tournaments/broadcast/broadcast_club.html | 3 +- .../broadcast/broadcasted_bracket.html | 160 ++++++ .../tournaments/tournament_bracket.html | 483 +----------------- tournaments/urls.py | 2 + tournaments/views.py | 43 +- 13 files changed, 816 insertions(+), 482 deletions(-) create mode 100644 tournaments/static/tournaments/css/tournament_bracket.css create mode 100644 tournaments/static/tournaments/js/tournament_bracket.js create mode 100644 tournaments/templates/tournaments/broadcast/broadcasted_bracket.html diff --git a/tournaments/models/match.py b/tournaments/models/match.py index 71571c4..329fab6 100644 --- a/tournaments/models/match.py +++ b/tournaments/models/match.py @@ -268,6 +268,16 @@ class Match(models.Model): elif len(team_scores) == 2: # Both team scores present teams.extend([team_score.live_team(self) for team_score in team_scores]) + + if self.round is not None and self.round.parent is None: + pos1 = team_scores[0].team_registration.bracket_position if hasattr(team_scores[0], 'team_registration') and team_scores[0].team_registration else None + pos2 = team_scores[1].team_registration.bracket_position if hasattr(team_scores[1], 'team_registration') and team_scores[1].team_registration else None + if pos1 is not None and pos2 is not None and pos1 // 2 == self.index and pos2 // 2 == self.index: + if pos1 > pos2: + teams = [team_scores[1].live_team(self), team_scores[0].live_team(self)] + else: + teams = [team_scores[0].live_team(self), team_scores[1].live_team(self)] + else: teams.extend([team_score.live_team(self) for team_score in team_scores if team_score.walk_out != 1]) diff --git a/tournaments/models/round.py b/tournaments/models/round.py index 7d740bc..0115a33 100644 --- a/tournaments/models/round.py +++ b/tournaments/models/round.py @@ -95,7 +95,7 @@ class Round(models.Model): return True - def prepare_match_group(self, next_round, parent_round, loser_final, double_butterfly_mode): + def prepare_match_group(self, next_round, parent_round, loser_final, double_butterfly_mode, secondHalf): matches = self.match_set.filter(disabled=False).order_by('index') if len(matches) == 0: return None @@ -138,6 +138,8 @@ class Round(models.Model): if len(matches) > 1 and double_butterfly_mode: midpoint = int(len(matches) / 2) first_half_matches = matches[:midpoint] + if secondHalf: + first_half_matches = matches[midpoint:] else: first_half_matches = list(matches) # Convert QuerySet to a list if self.index == 0 and loser_final: diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index 270530a..2a69060 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -1362,7 +1362,7 @@ class Tournament(models.Model): # 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) + match_group = round.prepare_match_group(next_round, parent_round, loser_final, double_butterfly_mode, False) if match_group: serializable_match_groups.append(match_group) @@ -1370,10 +1370,11 @@ class Tournament(models.Model): 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) + if round.index > 0: + next_round = main_rounds.filter(index=round.index - 1).first() + match_group = round.prepare_match_group(next_round, parent_round, None, double_butterfly_mode, True) + if match_group: + serializable_match_groups.append(match_group) return serializable_match_groups @@ -1390,6 +1391,13 @@ class MatchGroup: def add_matches(self, matches): self.matches = matches + def to_dict(self): + return { + 'name': self.name, + 'round_id': self.round_id, + 'matches': [match.to_dict() for match in self.matches] + } + class TeamSummon: def __init__(self, id, names, date, weight, stage, court, image, day_duration): self.id = str(id) diff --git a/tournaments/static/tournaments/css/style.css b/tournaments/static/tournaments/css/style.css index 30c0c3d..6e006b6 100644 --- a/tournaments/static/tournaments/css/style.css +++ b/tournaments/static/tournaments/css/style.css @@ -869,3 +869,11 @@ h-margin { transform: translate(-50%, -50%); white-space: nowrap; } + +.even-row { + background-color: #ffffff; /* White */ +} + +.odd-row { + background-color: #e6f2ff; /* Light blue */ +} diff --git a/tournaments/static/tournaments/css/tournament_bracket.css b/tournaments/static/tournaments/css/tournament_bracket.css new file mode 100644 index 0000000..6632ca8 --- /dev/null +++ b/tournaments/static/tournaments/css/tournament_bracket.css @@ -0,0 +1,231 @@ +.round-logo img { + width: 50px; + height: auto; + display: block; + margin: 0 auto; + position: relative; + top: -100px; /* Increased negative value to move it higher up */ +} + +.round-logo img { + width: 100px; /* Adjust size as needed */ + height: auto; + display: block; + margin: 0 auto; +} +.butterfly-match.same-level::before { + display: none; +} + +/* Adjust styling for matches with single parent */ +.match-content.disabled { + visibility: hidden; +} + +.incoming-line.disabled, +.butterfly-match:has(.match-content.disabled)::after, +.butterfly-match:has(.match-content.disabled)::before { + visibility: hidden; +} + +.butterfly-bracket { + display: flex; + gap: 40px; /* Increased to account for horizontal lines (20px on each side) */ + position: relative; + margin-bottom: 80px; +} + +.round-title { + position: absolute; + top: 0px; /* Adjust this value to position the title where you want it */ + left: 50%; /* Center horizontally */ + transform: translateX(-50%); /* Center it exactly */ + text-align: center; + font-weight: bold; + width: auto; /* Change from 100% to auto */ + padding: 5px 10px; + + white-space: nowrap; /* Prevent text from wrapping */ + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.round-name { + color: #707070; + font-size: 1.5em; + padding: 8px 12px; + white-space: nowrap; /* Prevent text wrapping */ + display: block; /* Ensure proper centering */ +} + +.round-format { + font-size: 0.9em; + color: #707070; + margin-top: -5px; /* Reduced from -10px to bring it closer */ + white-space: nowrap; /* Prevent text wrapping */ + display: block; /* Ensure proper centering */ +} + +.round-name.button { + border-radius: 16px; + width: 100%; + display: inline-block; + background-color: #fae7ce; +} + +.button:hover { + color: white; + background-color: #f39200; +} + +.matches-container { + position: relative; + width: 100%; + flex-grow: 1; +} + +.butterfly-round { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; /* Space between title and matches */ + position: relative; + width: var(--match-width); + flex-shrink: 0; + margin-top: 100px; /* Add padding to account for absolute positioned title */ +} + +.butterfly-match { + position: absolute; + width: 100%; + padding: 10px 10px; +} + +/* Horizontal line after match */ +.butterfly-match::after { + content: ""; + position: absolute; + left: 100%; /* Start from end of match cell */ + top: 50%; + width: 20px; + height: 2px; + background: #707070; +} + +.semi-final::after { + content: ""; + position: absolute; + left: calc(100% + 20px); /* After horizontal line */ + width: 2px; + height: calc((var(--next-match-distance)) / 2); + background: #707070; +} + +/* Vertical line connecting pair of matches */ +.butterfly-match:nth-child(2n)::before { + content: ""; + position: absolute; + left: calc(100% + 20px); /* After horizontal line */ + top: 50%; + width: 2px; + height: calc((var(--next-match-distance)) / 2); + background: #707070; +} + +.butterfly-match:nth-child(2n + 1)::before { + content: ""; + position: absolute; + left: calc(100% + 20px); + bottom: calc(50% - 2px); /* Account for half of horizontal line height */ + width: 2px; + height: calc( + ((var(--next-match-distance)) / 2) + ); /* Add half of horizontal line height */ + background: #707070; +} + +/* Vertical line connecting pair of matches */ +.butterfly-match.reverse-bracket:nth-child(2n)::before { + content: ""; + position: absolute; + left: calc(0% - 20px); /* After horizontal line */ + top: 50%; + width: 2px; + height: calc((var(--next-match-distance)) / 2); + background: #707070; +} + +.butterfly-match.reverse-bracket:nth-child(2n + 1)::before { + content: ""; + position: absolute; + left: calc(0% - 20px); + bottom: 50%; /* Account for half of horizontal line height */ + width: 2px; + height: calc( + (var(--next-match-distance)) / 2 + ); /* Add half of horizontal line height */ + background: #707070; +} + +/* Horizontal line to next round match */ +.butterfly-match .incoming-line { + position: absolute; + left: -20px; + top: 50%; + width: 20px; + height: 2px; + background: #707070; +} + +.inward .incoming-line { + display: none; +} + +.butterfly-match.outward::after { + display: none; +} + +.butterfly-round:last-child .butterfly-match::after, +.butterfly-round:first-child .incoming-line { + display: none; +} + +.broadcast-mode .round-name, +.broadcast-mode .round-format { + padding: 0px; + color: #707070; +} + +.broadcast-mode .round-title { + padding: 8px 20px; /* Slightly more horizontal padding */ + background-color: white; + align-content: center; + border-radius: 24px; +} +/* Broadcast mode styling for all lines */ +.broadcast-mode .butterfly-match::after, +.broadcast-mode .butterfly-match:nth-child(2n)::before, +.broadcast-mode .butterfly-match:nth-child(2n + 1)::before, +.broadcast-mode .butterfly-match.reverse-bracket:nth-child(2n)::before, +.broadcast-mode .butterfly-match.reverse-bracket:nth-child(2n + 1)::before, +.broadcast-mode .butterfly-match .incoming-line, +.broadcast-mode .semi-final::after { + background-color: black; /* Bright yellow - change to your preferred color */ +} + +.bubble.match-running { + position: relative; +} + +.bubble.match-running::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 20px; /* Height of the green indicator */ + background-color: #90ee90; /* Light green color */ + border-radius: 0 0 24px 24px; /* Match the bubble's bottom corners */ +} diff --git a/tournaments/static/tournaments/js/tournament_bracket.js b/tournaments/static/tournaments/js/tournament_bracket.js new file mode 100644 index 0000000..159c70b --- /dev/null +++ b/tournaments/static/tournaments/js/tournament_bracket.js @@ -0,0 +1,333 @@ +function renderBracket(options) { + const bracket = document.getElementById("bracket"); + const matchTemplates = document.getElementById("match-templates").children; + const rounds = []; + const matchPositions = []; + const matchDisabled = []; + const doubleButterflyMode = options.doubleButterflyMode; + const displayLoserFinal = options.displayLoserFinal; + const tournamentId = options.tournamentId; + const isBroadcast = options.isBroadcast; + // Group matches by round + Array.from(matchTemplates).forEach((template) => { + const roundIndex = parseInt(template.dataset.matchRound); + if (!rounds[roundIndex]) { + rounds[roundIndex] = []; + } + rounds[roundIndex].push(template); + }); + + // First create a test match to get natural height + const firstMatch = document.createElement("div"); + firstMatch.className = "butterfly-match"; + firstMatch.innerHTML = `
${rounds[0][0].innerHTML}
`; + bracket.appendChild(firstMatch); + const matchHeight = firstMatch.offsetHeight; + const matchSpacing = 10; + const baseDistance = matchHeight + matchSpacing; + bracket.innerHTML = ""; + const roundCount = rounds.length; + let finalRoundIndex = roundCount - 1; + if (doubleButterflyMode == true) { + finalRoundIndex = finalRoundIndex / 2; + } + let nextMatchDistance = baseDistance; + let minimumMatchDistance = 1; + + const totalRounds = document.querySelectorAll(".butterfly-round").length; + const screenWidth = window.innerWidth; + let roundTotalCount = roundCount; + if (doubleButterflyMode == true && roundCount > 1) { + roundTotalCount = roundCount - 1; + } + const padding = 50 * roundTotalCount; // Account for some padding/margin + const availableWidth = screenWidth - padding; + let responsiveMatchWidth = Math.min( + 365, + Math.floor(availableWidth / roundTotalCount), + ); + + rounds.forEach((roundMatches, roundIndex) => { + if (rounds[0].length <= 2) { + minimumMatchDistance = 2; + nextMatchDistance = baseDistance * 2; + } + const roundDiv = document.createElement("div"); + roundDiv.className = "butterfly-round"; + roundDiv.style.setProperty("--match-width", `${responsiveMatchWidth}px`); + + // Create title + const titleDiv = document.createElement("div"); + titleDiv.className = "round-title"; + + // Get the match group name and format + const firstMatchTemplate = roundMatches[0].closest(".match-template"); + const matchGroupName = firstMatchTemplate.dataset.matchGroupName; + const matchFormat = firstMatchTemplate.dataset.matchFormat; + const roundId = firstMatchTemplate.dataset.roundId; // Add this line + + let nameSpan = document.createElement("div"); + nameSpan.className = "round-name"; + nameSpan.textContent = matchGroupName; + if ( + roundIndex == finalRoundIndex || + (roundIndex == finalRoundIndex - 1 && displayLoserFinal) || + (roundIndex == finalRoundIndex + 1 && displayLoserFinal) || + isBroadcast + ) { + } else { + nameSpan = document.createElement("a"); + nameSpan.className = "round-name"; + nameSpan.classList.add("button"); + nameSpan.textContent = matchGroupName; + if (roundId) { + nameSpan.href = `/tournament/${tournamentId}/round/${roundId}/bracket/`; + nameSpan.style.cursor = "pointer"; + } + } + + const formatSpan = document.createElement("div"); + formatSpan.className = "round-format"; + formatSpan.textContent = matchFormat; + + titleDiv.appendChild(nameSpan); + titleDiv.appendChild(formatSpan); + + // Create matches container + const matchesContainer = document.createElement("div"); + matchesContainer.className = "matches-container"; + if (roundCount > 5 && doubleButterflyMode == true) { + if (roundIndex >= finalRoundIndex - 1) { + matchesContainer.style.transform = `translateX(-50%)`; + if (roundIndex >= finalRoundIndex + 2) { + matchesContainer.style.transform = `translateX(-100%)`; + } + } + } + roundDiv.appendChild(matchesContainer); + + matchPositions[roundIndex] = []; + matchDisabled[roundIndex] = []; // Initialize array for this round + roundMatches.forEach((matchTemplate, matchIndex) => { + const matchTitle = matchTemplate.dataset.matchTitle; + const matchDiv = document.createElement("div"); + matchDiv.className = "butterfly-match"; + + matchDiv.style.position = "absolute"; + const isDisabled = matchTemplate.dataset.disabled === "true"; + matchDisabled[roundIndex][matchIndex] = isDisabled; + let isIncomingLineIsDisabled = isDisabled; + + let top; + let left; + let right; + const currentMatchesCount = roundMatches.length; + if (roundIndex > finalRoundIndex) { + matchDiv.classList.add("reverse-bracket"); + top = matchPositions[roundCount - roundIndex - 1][matchIndex]; + } + + if (roundIndex === 0) { + if (roundCount > 1) { + const nextMatchesCount = rounds[roundIndex + 1].length; + + if (currentMatchesCount == nextMatchesCount && roundCount > 2) { + nextMatchDistance = 0; + } + } else { + nextMatchDistance = 0; + } + + top = matchIndex * (matchHeight + matchSpacing) * minimumMatchDistance; + + if (roundCount == 3 && doubleButterflyMode) { + top = top + (matchHeight + matchSpacing) / 2; + } + } else if (roundIndex === roundCount - 1 && doubleButterflyMode == true) { + const nextMatchesCount = rounds[roundIndex - 1].length; + if (currentMatchesCount == nextMatchesCount) { + nextMatchDistance = 0; + } + } else if (roundIndex == finalRoundIndex) { + if (doubleButterflyMode == true) { + let lgth = matchPositions[0].length / 2; + let index = lgth + matchIndex - 1; + // If index goes negative, use 0 instead + if (displayLoserFinal == true) { + if (matchIndex == 0) { + top = matchPositions[roundIndex - 1][0] - baseDistance / 2; + } else { + top = matchPositions[roundIndex - 1][0] + baseDistance / 2; + } + nextMatchDistance = baseDistance; + } else { + top = matchPositions[roundIndex - 1][0]; + nextMatchDistance = 0; + } + } 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) { + const previousMatchesCount = rounds[roundIndex - 1].length; + const nextMatchesCount = rounds[roundIndex + 1].length; + + if ( + currentMatchesCount == nextMatchesCount && + displayLoserFinal == false + ) { + nextMatchDistance = 0; + } else if (matchPositions.length > roundIndex - 1) { + nextMatchDistance = + matchPositions[roundIndex - 1][1] - + matchPositions[roundIndex - 1][0]; + nextMatchDistance = + nextMatchDistance * (previousMatchesCount / currentMatchesCount); + } + + if (currentMatchesCount == previousMatchesCount) { + if (matchDisabled[roundIndex - 1][matchIndex] == true) { + isIncomingLineIsDisabled = true; + } + top = matchPositions[roundIndex - 1][matchIndex]; + } 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; + } + } else if (roundIndex < roundCount) { + const nextMatchesCount = rounds[roundIndex - 1].length; + const previousMatchesCount = rounds[roundIndex + 1].length; + + if (currentMatchesCount == nextMatchesCount) { + nextMatchDistance = 0; + } else if (matchPositions.length > roundCount - roundIndex - 1 - 1) { + nextMatchDistance = + matchPositions[roundCount - roundIndex - 1 - 1][1] - + matchPositions[roundCount - roundIndex - 1 - 1][0]; + nextMatchDistance = + nextMatchDistance * (previousMatchesCount / currentMatchesCount); + } + } + + if (roundCount > 5 && doubleButterflyMode == true) { + if (roundIndex >= finalRoundIndex - 2) { + if (roundIndex == finalRoundIndex - 1) { + matchDiv.classList.add("inward"); + } + if (roundIndex == finalRoundIndex + 1) { + matchDiv.classList.add("outward"); + } + if ( + roundIndex === finalRoundIndex - 2 || + roundIndex === finalRoundIndex + 2 + ) { + nextMatchDistance = nextMatchDistance - baseDistance; + } + } + } + + if (displayLoserFinal == true) { + if ( + doubleButterflyMode == true && + (roundIndex == finalRoundIndex - 1 || + roundIndex == finalRoundIndex + 1) + ) { + nextMatchDistance = baseDistance; + } + } + + matchDiv.style.setProperty( + "--next-match-distance", + `${nextMatchDistance}px`, + ); + matchDiv.style.top = `${top}px`; + matchPositions[roundIndex][matchIndex] = top; + if (matchIndex === 0) { + // // Add logo for final round + // if (roundIndex == finalRoundIndex) { + // const logoDiv = document.createElement('div'); + // logoDiv.className = 'round-logo'; + // const logoImg = document.createElement('img'); + // logoImg.src = '/static/tournaments/images/PadelClub_logo_512.png'; + // logoImg.alt = 'PadelClub Logo'; + // logoDiv.appendChild(logoImg); + // logoDiv.style.transform = `translateX(-50%)`; + // matchesContainer.appendChild(logoDiv); + // } + + // Position title above the first match + titleDiv.style.top = `${top - 80}px`; // Adjust the 60px offset as needed + titleDiv.style.position = "absolute"; + if (roundCount >= 5 && doubleButterflyMode == true) { + if (roundIndex == finalRoundIndex - 1) { + titleDiv.style.marginLeft = "60px"; + } else if (roundIndex == finalRoundIndex + 1) { + titleDiv.style.marginLeft = "-60px"; + } + } + 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 = ` +
+
${matchTemplate.innerHTML}
+ `; + + if ( + roundIndex == finalRoundIndex - 1 && + displayLoserFinal == true && + doubleButterflyMode == true + ) { + const matchDiv2 = document.createElement("div"); + matchDiv2.className = "butterfly-match"; + matchDiv2.classList.add("inward"); + matchDiv2.classList.add("semi-final"); + matchDiv2.style.setProperty( + "--next-match-distance", + `${baseDistance}px`, + ); + matchDiv2.style.top = `${top}px`; + matchDiv2.innerHTML = `
${rounds[0][0].innerHTML}
`; + matchesContainer.appendChild(matchDiv2); // Append to matchesContainer instead of roundDiv + } + matchesContainer.appendChild(matchDiv); // Append to matchesContainer instead of roundDiv + }); + + bracket.appendChild(roundDiv); + }); +} diff --git a/tournaments/templates/tournaments/base.html b/tournaments/templates/tournaments/base.html index ee1dec2..a7d2b24 100644 --- a/tournaments/templates/tournaments/base.html +++ b/tournaments/templates/tournaments/base.html @@ -13,6 +13,7 @@ /> + {% block extra_css %}{% endblock %} {% for team in match.teams %}
-
+
{% if team.id %} {% endif %} diff --git a/tournaments/templates/tournaments/broadcast/broadcast_club.html b/tournaments/templates/tournaments/broadcast/broadcast_club.html index 6b3d617..8f5f87e 100644 --- a/tournaments/templates/tournaments/broadcast/broadcast_club.html +++ b/tournaments/templates/tournaments/broadcast/broadcast_club.html @@ -13,7 +13,7 @@ {% for tournament in tournaments %} -
+
{{ tournament.formatted_start_date }}
@@ -31,6 +31,7 @@
Automatic | + Programmation | Matchs | Poules | diff --git a/tournaments/templates/tournaments/broadcast/broadcasted_bracket.html b/tournaments/templates/tournaments/broadcast/broadcasted_bracket.html new file mode 100644 index 0000000..06fb3b3 --- /dev/null +++ b/tournaments/templates/tournaments/broadcast/broadcasted_bracket.html @@ -0,0 +1,160 @@ + +{% load static %} +{% load qr_code %} + + + + + + + + + + + + Tableau + + + + + + + + +
+ +
+ +
+
+
+
+
+
+
+ + + + + diff --git a/tournaments/templates/tournaments/tournament_bracket.html b/tournaments/templates/tournaments/tournament_bracket.html index b334bfd..fadaf9e 100644 --- a/tournaments/templates/tournaments/tournament_bracket.html +++ b/tournaments/templates/tournaments/tournament_bracket.html @@ -1,4 +1,9 @@ {% extends 'tournaments/base.html' %} +{% load static %} + +{% block extra_css %} + +{% endblock %} {% block head_title %}Matchs du {{ tournament.display_name }}{% endblock %} {% block first_title %}{{ tournament.event.display_name }}{% endblock %} @@ -30,479 +35,15 @@ {% endfor %}
+ - - {% endif %} {% endblock %} diff --git a/tournaments/urls.py b/tournaments/urls.py index 2bede6a..4f315c2 100644 --- a/tournaments/urls.py +++ b/tournaments/urls.py @@ -31,6 +31,8 @@ urlpatterns = [ 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/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'), path('group-stages/json/', views.tournament_live_group_stage_json, name='group-stages-json'), path('rankings/', views.tournament_rankings, name='tournament-rankings'), diff --git a/tournaments/views.py b/tournaments/views.py index 3de6532..1a5df8a 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -449,6 +449,31 @@ def tournament_broadcasted_prog(request, tournament_id): 'qr_code_options': qr_code_options(), }) +def tournament_broadcasted_bracket(request, tournament_id): + tournament = get_object_or_404(Tournament, pk=tournament_id) + return render(request, 'tournaments/broadcast/broadcasted_bracket.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): + """ + JSON endpoint for tournament bracket data. + """ + tournament = get_object_or_404(Tournament, pk=tournament_id) + context = get_butterfly_bracket_view_context(tournament, parent_round=None, double_butterfly_mode=True, display_loser_final=True) + + data = { + 'match_groups': [match_group.to_dict() for match_group in context['match_groups']], + 'double_butterfly_mode': context['double_butterfly_mode'], + 'display_loser_final': context['display_loser_final'] + } + + return JsonResponse(data) + + def tournament_live_group_stage_json(request, tournament_id): tournament = get_object_or_404(Tournament, pk=tournament_id) @@ -578,8 +603,20 @@ def xls_to_csv(request): file_path = os.path.join(directory, uploaded_file.name) file_name = default_storage.save(file_path, ContentFile(uploaded_file.read())) - # convert to csv and save - data_xls = pd.read_excel(file_name, sheet_name=0, index_col=None) + # Check available sheets and look for 'inscriptions' + xls = pd.ExcelFile(file_name) + sheet_names = xls.sheet_names + + # Determine which sheet to use + target_sheet = 0 # Default to first sheet + if 'inscriptions' in [name.lower() for name in sheet_names]: + for i, name in enumerate(sheet_names): + if name.lower() == 'inscriptions': + target_sheet = i # or use the name directly: target_sheet = name + break + + # Convert to csv and save + data_xls = pd.read_excel(file_name, sheet_name=target_sheet, index_col=None) csv_file_name = create_random_filename('players', 'csv') output_path = os.path.join(directory, csv_file_name) data_xls.to_csv(output_path, sep=';', index=False, encoding='utf-8') @@ -1018,7 +1055,7 @@ def round_bracket(request, tournament_id, round_id): context = get_butterfly_bracket_view_context(tournament, round, double_butterfly_mode=False, display_loser_final=False) return render(request, 'tournaments/tournament_bracket.html', context) -def get_butterfly_bracket_view_context(tournament, parent_round=None, double_butterfly_mode=False, display_loser_final=False): +def get_butterfly_bracket_view_context(tournament, parent_round=None, double_butterfly_mode=False, display_loser_final=True): if parent_round: double_butterfly_mode = False display_loser_final = True