bracket-feature
Raz 9 months ago
parent 274cd248f6
commit 8de5758650
  1. 28
      tournaments/models/match.py
  2. 4
      tournaments/models/round.py
  3. 14
      tournaments/static/tournaments/css/style.css
  4. 2
      tournaments/templates/tournaments/broadcast/broadcasted_match.html
  5. 2
      tournaments/templates/tournaments/match_cell.html
  6. 1
      tournaments/templates/tournaments/navigation_tournament.html
  7. 181
      tournaments/templates/tournaments/tournament_bracket.html
  8. 1
      tournaments/urls.py
  9. 33
      tournaments/views.py

@ -161,13 +161,21 @@ class Match(models.Model):
# No team scores at all # No team scores at all
if previous_top_match: if previous_top_match:
names = [f"Gagnants {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"Gagnants {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)
if len(teams) == 0:
names = ['', '']
team = self.default_live_team(names)
teams.append(team)
teams.append(team)
return teams
elif len(team_scores) == 1: elif len(team_scores) == 1:
# Only one team score, handle missing one # Only one team score, handle missing one
existing_team = team_scores[0].live_team(self) existing_team = team_scores[0].live_team(self)
@ -177,17 +185,21 @@ class Match(models.Model):
teams.append(existing_team) teams.append(existing_team)
else: else:
if previous_top_match and previous_top_match.disabled == False and previous_top_match.end_date is None: if previous_top_match and previous_top_match.disabled == False and previous_top_match.end_date is None:
names = [f"Gagnants {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"Gagnants {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)
else: else:
teams.append(existing_team) teams.append(existing_team)
names = ["Qualifié Entrant", '']
team = self.default_live_team(names)
teams.append(team)
else: else:
# Both team scores present # Both team scores present
teams.extend([team_score.live_team(self) for team_score in team_scores]) teams.extend([team_score.live_team(self) for team_score in team_scores])
@ -323,7 +335,7 @@ class Match(models.Model):
ended = self.end_date is not None ended = self.end_date is not None
live_format = "Format " + FederalMatchCategory(self.format).format_label_short live_format = "Format " + FederalMatchCategory(self.format).format_label_short
livematch = LiveMatch(title, date, time_indication, court, self.started(), ended, group_stage_name, live_format) livematch = LiveMatch(title, date, time_indication, court, self.started(), ended, group_stage_name, live_format, self.disabled)
for team in self.live_teams(): for team in self.live_teams():
livematch.add_team(team) livematch.add_team(team)
@ -374,7 +386,7 @@ class Team:
} }
class LiveMatch: class LiveMatch:
def __init__(self, title, date, time_indication, court, started, ended, group_stage_name, format): def __init__(self, title, date, time_indication, court, started, ended, group_stage_name, format, disabled):
self.title = title self.title = title
self.date = date self.date = date
self.teams = [] self.teams = []
@ -385,6 +397,7 @@ class LiveMatch:
self.has_walk_out = False self.has_walk_out = False
self.group_stage_name = group_stage_name self.group_stage_name = group_stage_name
self.format = format self.format = format
self.disabled = disabled
def add_team(self, team): def add_team(self, team):
self.teams.append(team) self.teams.append(team)
@ -402,7 +415,8 @@ class LiveMatch:
"ended": self.ended, "ended": self.ended,
"has_walk_out": self.has_walk_out, "has_walk_out": self.has_walk_out,
"group_stage_name": self.group_stage_name, "group_stage_name": self.group_stage_name,
"format": self.format "format": self.format,
"disabled": self.disabled
} }
def show_time_indication(self): def show_time_indication(self):

@ -31,9 +31,9 @@ class Round(models.Model):
if self.index == 0: if self.index == 0:
return "Finale" return "Finale"
elif self.index == 1: elif self.index == 1:
return "Demi-Finales" return "Demi"
elif self.index == 2: elif self.index == 2:
return "Quarts de finale" return "Quart"
else: else:
squared = 2 ** self.index squared = 2 ** self.index
return f"{squared}ème" return f"{squared}ème"

@ -826,3 +826,17 @@ h-margin {
.tournament-info a:hover { .tournament-info a:hover {
color: #f39200; color: #f39200;
} }
.status-container {
margin: 0 -20px -20px -20px; /* Negative margin to counter the bubble padding, including bottom */
padding: 0 20px 20px 20px; /* Add padding back to maintain text alignment, including bottom */
border-radius: 0 0 24px 24px; /* Match the bubble's bottom corners */
}
.status-container.running {
background-color: #90ee90; /* Light green color */
}
.bubble {
position: relative; /* Add this */
}

@ -61,9 +61,11 @@
</div> </div>
</template> </template>
<div class="status-container {% if not match.ended and match.started %}running{% endif %}">
<div class="top-margin flex-row"> <div class="top-margin flex-row">
<label class="left-label minor-info bold"><span x-text="match.time_indication"></span></label> <label class="left-label minor-info bold"><span x-text="match.time_indication"></span></label>
<label class="right-label minor-info semibold"><span x-text="match.format"></span></label> <label class="right-label minor-info semibold"><span x-text="match.format"></span></label>
</div> </div>
</div>
</div> </div>

@ -56,6 +56,7 @@
</div> </div>
<div class="status-container {% if not match.ended and match.started %}running{% endif %}">
<div class="flex-row top-margin"> <div class="flex-row top-margin">
<label class="left-label minor-info bold"> <label class="left-label minor-info bold">
{% if match.show_time_indication %} {% if match.show_time_indication %}
@ -69,6 +70,7 @@
</label> </label>
<!-- <a href="" class="right-label">{{ match.court }}</a> --> <!-- <a href="" class="right-label">{{ match.court }}</a> -->
</div> </div>
</div>
</div> </div>
</div> </div>

@ -1,6 +1,7 @@
<nav class="margin10"> <nav class="margin10">
<a href="{% url 'tournament-info' tournament.id %}" class="topmargin5 orange">Informations</a> <a href="{% url 'tournament-info' tournament.id %}" class="topmargin5 orange">Informations</a>
<a href="{% url 'tournament-bracket' tournament.id %}" class="topmargin5 orange">Tableau</a>
{% if tournament.display_matches or tournament.display_group_stages %} {% if tournament.display_matches or tournament.display_group_stages %}
<a href="{% url 'tournament' tournament.id %}" class="topmargin5 orange">Matches</a> <a href="{% url 'tournament' tournament.id %}" class="topmargin5 orange">Matches</a>

@ -0,0 +1,181 @@
{% extends 'tournaments/base.html' %}
{% block head_title %}Matchs du {{ tournament.display_name }}{% endblock %}
{% block first_title %}{{ tournament.event.display_name }}{% endblock %}
{% block second_title %}{{ tournament.display_name }}{% endblock %}
{% if tournament.display_matches %}
{% block content %}
{% include 'tournaments/navigation_tournament.html' %}
<div class="butterfly-bracket" id="bracket"></div>
<div id="match-templates" style="display: none;">
{% for match_group in match_groups %}
{% if match_group.matches %}
{% for match in match_group.matches %}
<div data-match-round="{{ forloop.parentloop.counter0 }}"
data-match-index="{{ forloop.counter0 }}"
data-disabled="{{ match.disabled|lower }}"
class="match-template">
{% include 'tournaments/match_cell.html' %}
</div>
{% endfor %}
{% endif %}
{% endfor %}
</div>
<script>
function renderBracket() {
const bracket = document.getElementById('bracket');
const matchTemplates = document.getElementById('match-templates').children;
const rounds = [];
const matchPositions = [];
// 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 = `<div class="match-content">${rounds[0][0].innerHTML}</div>`;
bracket.appendChild(firstMatch);
const matchHeight = firstMatch.offsetHeight;
const matchSpacing = 10;
bracket.innerHTML = '';
rounds.forEach((roundMatches, roundIndex) => {
const roundDiv = document.createElement('div');
roundDiv.className = 'butterfly-round';
matchPositions[roundIndex] = [];
roundMatches.forEach((matchTemplate, matchIndex) => {
const matchDiv = document.createElement('div');
matchDiv.className = 'butterfly-match';
matchDiv.style.position = 'absolute';
const isDisabled = matchTemplate.dataset.disabled === 'true';
let top;
if (roundIndex === 0) {
top = matchIndex * (matchHeight + matchSpacing);
} 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;
}
const baseDistance = matchHeight + matchSpacing;
const distance = baseDistance * Math.pow(2, roundIndex);
matchDiv.style.setProperty('--next-match-distance', `${distance}px`);
matchDiv.style.top = `${top}px`;
matchPositions[roundIndex][matchIndex] = top;
matchDiv.innerHTML = `
<div class="incoming-line ${isDisabled ? 'disabled' : ''}"></div>
<div class="match-content ${isDisabled ? 'disabled' : ''}">${matchTemplate.innerHTML}</div>
`;
roundDiv.appendChild(matchDiv);
});
bracket.appendChild(roundDiv);
});
}
renderBracket();
</script>
<style>
.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;
}
.butterfly-round {
position: relative;
width: 25%; /* 300px for match + 20px on each side for lines */
}
.butterfly-match {
position: absolute;
width: 100%;
}
/* 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: #666;
}
/* Vertical line connecting pair of matches */
.butterfly-match:nth-child(2n+1)::before {
content: "";
position: absolute;
left: calc(100% + 20px); /* After horizontal line */
top: 50%;
width: 2px;
height: calc((var(--next-match-distance)) / 2);
background: #666;
}
.butterfly-match:nth-child(2n)::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: #666;
}
/* Horizontal line to next round match */
.butterfly-match .incoming-line {
position: absolute;
left: -20px;
top: 50%;
width: 20px;
height: 2px;
background: #666;
}
/* Hide incoming line for first round */
.butterfly-round:first-child .incoming-line {
display: none;
}
/* Hide outgoing lines for last round */
.butterfly-round:last-child .butterfly-match::after,
.butterfly-round:last-child .butterfly-match::before {
display: none;
}
</style>
{% endblock %}
{% endif %}

@ -17,6 +17,7 @@ urlpatterns = [
path('', views.tournament, name='tournament'), path('', views.tournament, name='tournament'),
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('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'),
path('summons/json/', views.tournament_summons_json, name='tournament-summons-json'), path('summons/json/', views.tournament_summons_json, name='tournament-summons-json'),

@ -954,6 +954,39 @@ def team_details(request, tournament_id, team_id):
'debug': False # Set to False in production 'debug': False # Set to False in production
}) })
def tournament_bracket(request, tournament_id):
"""
View to display tournament bracket structure.
"""
tournament = get_object_or_404(Tournament, pk=tournament_id)
# Get main bracket rounds (excluding children/ranking matches)
main_rounds = tournament.round_set.filter(
parent=None,
group_stage_loser_bracket=False
).order_by('-index')
# Create serializable match groups data
serializable_match_groups = []
for round in main_rounds:
matches = round.match_set.all()
if matches:
# Create MatchGroup
match_group = tournament.create_match_group(
name=round.name(),
matches=matches
)
serializable_match_groups.append(match_group)
context = {
'tournament': tournament,
'match_groups': serializable_match_groups
}
return render(request, 'tournaments/tournament_bracket.html', context)
class UserListExportView(LoginRequiredMixin, View): class UserListExportView(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
users = CustomUser.objects.order_by('date_joined') users = CustomUser.objects.order_by('date_joined')

Loading…
Cancel
Save