bracket-feature
laurent 10 months ago
commit 9e832fa447
  1. 1
      padelclub_backend/settings.py
  2. 2
      tournaments/models/group_stage.py
  3. 9
      tournaments/models/match.py
  4. 28
      tournaments/models/player_registration.py
  5. 88
      tournaments/models/team_registration.py
  6. 4
      tournaments/models/team_score.py
  7. 21
      tournaments/models/tournament.py
  8. 77
      tournaments/static/tournaments/css/style.css
  9. 5
      tournaments/templates/tournaments/broadcast/broadcasted_group_stage.html
  10. 13
      tournaments/templates/tournaments/group_stage_cell.html
  11. 7
      tournaments/templates/tournaments/match_cell.html
  12. 61
      tournaments/templates/tournaments/player_row.html
  13. 10
      tournaments/templates/tournaments/ranking_row.html
  14. 29
      tournaments/templates/tournaments/rankings.html
  15. 10
      tournaments/templates/tournaments/summon_row.html
  16. 54
      tournaments/templates/tournaments/team_details.html
  17. 9
      tournaments/templates/tournaments/team_row.html
  18. 72
      tournaments/templates/tournaments/team_stats.html
  19. 2
      tournaments/urls.py
  20. 18
      tournaments/views.py

@ -142,6 +142,7 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
CSRF_TRUSTED_ORIGINS = ['http://127.0.0.1:8000'] # Ajoutez vos domaines de confiance
from .settings_local import *
from .settings_app import *

@ -197,6 +197,7 @@ class GroupStageTeam:
self.display_set_difference = False
self.weight = team_registration.weight
self.team_registration = team_registration
self.qualified = team_registration.qualified
def wins_losses(self):
return f"{self.wins}/{self.losses}"
@ -241,4 +242,5 @@ class GroupStageTeam:
"diff": self.formatted_diff(),
"weight": self.weight,
"match_count": self.match_count(),
"qualified": self.qualified,
}

@ -52,7 +52,8 @@ class Match(models.Model):
items = []
if self.round:
items.append(self.round.name())
items.append(f" #{self.index_in_round() + 1}")
if self.round.index > 0:
items.append(f" #{self.index_in_round() + 1}")
elif self.group_stage:
items.append(self.group_stage.name())
items.append(f"Match #{self.index + 1}")
@ -133,7 +134,7 @@ class Match(models.Model):
is_winner = False
scores = []
walk_out = None
team = Team(image, names, scores, weight, is_winner, walk_out)
team = Team(None, image, names, scores, weight, is_winner, walk_out)
return team
def live_teams(self):
@ -347,8 +348,9 @@ class Match(models.Model):
# return sort_score
class Team:
def __init__(self, image, names, scores, weight, is_winner, walk_out):
def __init__(self, id, image, names, scores, weight, is_winner, walk_out):
# print(f"image = {image}, names= {names}, scores ={scores}, weight={weight}, win={is_winner}")
self.id = id
self.image = image
self.names = names
self.scores = scores
@ -358,6 +360,7 @@ class Team:
def to_dict(self):
return {
"id": self.id,
"image": self.image,
"names": self.names,
"scores": self.scores,

@ -1,6 +1,7 @@
from django.db import models
from . import TeamRegistration, PlayerSexType, PlayerDataSource, PlayerPaymentType
import uuid
from django.utils import timezone
class PlayerRegistration(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)
@ -48,3 +49,30 @@ class PlayerRegistration(models.Model):
name_parts = self.last_name.split(" ")
name = f"{self.first_name[0]}. {name_parts[0]}"
return name
def clean_club_name(self):
if self.club_name:
return self.club_name.split(' (')[0]
return "Non renseigné"
def calculate_age(self):
if self.birthdate:
try:
birth_year = int(self.birthdate[-4:]) # Assumes birthdate ends with YYYY
current_year = timezone.now().year
return current_year - birth_year
except (ValueError, TypeError, IndexError):
return None
return None
def format_ordinal(self):
if self.rank is None:
if self.sex == PlayerSexType.FEMALE:
return "Non Classée"
return "Non Classé"
if self.rank == 1:
if self.sex == PlayerSexType.FEMALE:
return "1ère"
return "1er"
return f"{self.rank}ème"

@ -3,6 +3,7 @@ from django.db.models.sql.query import Q
from . import Tournament, GroupStage, Match
import uuid
from django.utils import timezone
from django.db.models import Count
class TeamRegistration(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)
@ -61,6 +62,15 @@ class TeamRegistration(models.Model):
else:
return "no players"
def formatted_team_names(self):
if self.name:
return self.name
names = [pr.last_name for pr in self.playerregistration_set.all()][:2] # Take max first 2
joined_names = " / ".join(names)
if joined_names:
return f"Paire {joined_names}"
return "Détail de l'équipe"
def next_match(self):
all_matches = [ts.match for ts in self.teamscore_set.all()]
now = timezone.now()
@ -102,3 +112,81 @@ class TeamRegistration(models.Model):
else:
# print("no date")
return None
def get_matches(self):
matches = Match.objects.filter(team_scores__team_registration=self).distinct()
print(f"All matches for team {self.id}: {matches.count()}")
for match in matches:
print(f"Match {match.id}: start_date={match.start_date}, end_date={match.end_date}")
return matches
def get_upcoming_matches(self):
matches = self.get_matches()
upcoming = matches.filter(end_date__isnull=True).order_by('start_date')
print(f"Upcoming matches count: {upcoming.count()}")
return [match.live_match() for match in upcoming]
def get_completed_matches(self):
matches = self.get_matches()
completed = matches.filter(end_date__isnull=False).order_by('-end_date')
print(f"Completed matches count: {completed.count()}")
return [match.live_match() for match in completed]
def get_statistics(self):
stats = {
'final_ranking': self.get_final_ranking(),
'points_earned': self.get_points_earned(),
'initial_stage': self.get_initial_stage(),
'matches_played': self.count_matches_played(),
'victory_ratio': self.calculate_victory_ratio()
}
return stats
def get_initial_stage(self):
matches = self.get_matches().order_by('start_date')
first_match = matches.first()
if first_match:
if first_match.group_stage:
return "Poule"
elif first_match.round:
return first_match.round.name()
return None
def get_final_ranking(self):
if self.final_ranking:
if self.final_ranking == 1:
return "1er"
return f"{self.final_ranking}e"
return None
def get_points_earned(self):
return self.points_earned
def calculate_total_duration(self):
total_seconds = 0
for match in self.get_matches().filter(end_date__isnull=False):
if match.start_date and match.end_date:
duration = (match.end_date - match.start_date).total_seconds()
total_seconds += duration
if total_seconds > 0:
hours = int(total_seconds // 3600)
minutes = int((total_seconds % 3600) // 60)
if hours > 0:
return f"{hours}h{minutes:02d}"
return f"{minutes}min"
return None
def count_matches_played(self):
return self.get_matches().filter(end_date__isnull=False).count()
def calculate_victory_ratio(self):
matches = self.get_matches().filter(end_date__isnull=False)
total_matches = matches.count()
if total_matches > 0:
wins = matches.filter(winning_team_id=self.id).count()
ratio = (wins / total_matches) * 100
return f"{wins}/{total_matches}"
return None

@ -71,10 +71,12 @@ class TeamScore(models.Model):
def live_team(self, match):
if self.team_registration:
id = self.team_registration.id
image = self.team_registration.logo
weight = self.team_registration.weight
is_winner = self.team_registration.id == match.winning_team_id
else:
id = None
image = None
weight= None
is_winner = False
@ -82,5 +84,5 @@ class TeamScore(models.Model):
scores = self.scores_array()
walk_out = self.walk_out
from .match import Team # Import Team only when needed
team = Team(image, names, scores, weight, is_winner, walk_out)
team = Team(id, image, names, scores, weight, is_winner, walk_out)
return team

@ -239,7 +239,7 @@ class Tournament(models.Model):
names = team.names
stage = team.stage
weight = team.weight
summon = TeamSummon(names, team.date, weight, stage, "", team.image, self.day_duration)
summon = TeamSummon(team.team_registration.id, names, team.date, weight, stage, "", team.image, self.day_duration)
summons.append(summon)
else:
print('>>> team_summons')
@ -250,7 +250,7 @@ class Tournament(models.Model):
names = team_registration.team_names()
stage = next_match.summon_stage_name()
weight = team_registration.weight
summon = TeamSummon(names, next_match.local_start_date(), weight, stage, next_match.court_name(next_match.court_index), team_registration.logo, self.day_duration)
summon = TeamSummon(team_registration.id, names, next_match.local_start_date(), weight, stage, next_match.court_name(next_match.court_index), team_registration.logo, self.day_duration)
summons.append(summon)
summons.sort(key=lambda s: (s.date is None, s.date or datetime.min))
@ -271,7 +271,7 @@ class Tournament(models.Model):
names = team_registration.team_names()
ranking = team_registration.final_ranking
points = team_registration.points_earned
team = TeamRanking(names, ranking, points, team_registration.logo)
team = TeamRanking(team_registration.id, names, ranking, points, team_registration.logo)
rankings.append(team)
rankings.sort(key=lambda r: r.ranking)
@ -750,9 +750,8 @@ class Tournament(models.Model):
return group_stages
def display_rankings(self):
if self.publish_rankings is True and self.end_date is not None:
return True
return False
return True
def display_tournament(self):
if self.publish_tournament:
@ -765,7 +764,7 @@ class Tournament(models.Model):
if datetime.now().date() >= self.start_date.date():
return is_build_and_not_empty
minimum_publish_date = self.creation_date.replace(hour=9, minute=0) + timedelta(days=1)
return timezone.now() >= minimum_publish_date
return timezone.now() >= timezone.localtime(minimum_publish_date)
def display_teams(self):
if self.end_date is not None:
@ -910,7 +909,8 @@ class MatchGroup:
self.matches = matches
class TeamSummon:
def __init__(self, names, date, weight, stage, court, image, day_duration):
def __init__(self, id, names, date, weight, stage, court, image, day_duration):
self.id = id
self.names = names
self.date = date
self.weight = weight
@ -930,6 +930,7 @@ class TeamSummon:
def to_dict(self):
return {
"id": self.id,
"names": self.names,
"date": self.formatted_date(),
"weight": self.weight,
@ -968,7 +969,8 @@ class TeamItem:
}
class TeamRanking:
def __init__(self, names, ranking, points, image):
def __init__(self, id, names, ranking, points, image):
self.id = id
self.names = names
self.ranking = ranking
self.formatted_ranking = self.ordinal(ranking)
@ -999,6 +1001,7 @@ class TeamRanking:
def to_dict(self):
return {
"id": self.id,
"names": self.names,
"ranking": self.ranking,
"formatted_ranking": self.formatted_ranking,

@ -327,6 +327,10 @@ tr {
color: #f39200;
}
.qualified {
color: #f9d348;
}
.ws {
font-family: "Montserrat-SemiBold";
/* text-align: right; */
@ -714,3 +718,76 @@ h-margin {
.right-content {
margin-left: auto;
}
.styled-link {
text-decoration: underline; /* Ensures the link is underlined */
color: #f39200; /* Use your main color variable if defined */
font-weight: bold; /* Optional: To make the link more prominent */
}
.styled-link:hover {
color: #f39200; /* Optional: Define a hover color */
text-decoration: none; /* Optional: Remove underline on hover */
}
.sup {
font-size: x-small;
vertical-align: super;
}
.alert {
color: #e84038; /* Make the text red */
font-weight: bold; /* Optional: Make the text bold */
}
.destructive-button {
background-color: #ff4d4d; /* Red background */
color: white; /* White text */
}
.destructive-button:hover {
background-color: #cc0000; /* Darker red on hover */
color: white; /* White text on hover */
}
.download-button {
margin-right: 6px;
color: #1a223a;
padding: 8px 12px;
background-color: white;
border-radius: 12px;
text-decoration: none;
font-size: 12px;
font-weight: 600;
}
.download-button:hover {
color: orange;
}
.match-result a {
text-decoration: none;
color: inherit;
display: block;
padding: 2px 8px;
border-radius: 6px;
}
.match-result a:hover {
background-color: #fae7ce;
color: #707070;
}
.single-line {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.group-stage-link {
text-decoration: none;
color: inherit;
}
.group-stage-link:hover {
color: #f39200; /* Or whatever hover color you prefer */
}

@ -7,12 +7,11 @@
<template x-for="i in group_stage.teams.length">
<div>
<div class="flex">
<div class="flex" :class="group_stage.teams[i-1].qualified ? 'qualified' : ''">
<div class="flex-left">
<template x-for="name in group_stage.teams[i-1].names">
<div class="bold" :class="group_stage.teams[i-1].is_winner ? 'winner' : ''" x-text="name"></div>
<div class="bold" x-text="name"></div>
<!-- <div class="semibold" x-data="{

@ -12,13 +12,22 @@
{% for team in group_stage.teams %}
<div class="flex">
<div class="flex {% if team.qualified %}qualified{% endif %}">
<div class="flex-left">
{% if team.team_registration.id %}
<a href="{% url 'team-details' tournament.id team.team_registration.id %}" class="group-stage-link"> <!-- Add this anchor tag -->
{% endif %}
{% for name in team.names %}
<div class="semibold {% if team.is_winner %}winner{% endif %}">
<div class="semibold">
{{ name }}
</div>
{% endfor %}
{% if team.team_registration.id %}
</a>
{% endif %}
</div>
<div class="flex-right">
{% if group_stage.started %}

@ -11,9 +11,11 @@
<div>
{% for team in match.teams %}
<div class="match-result {% cycle 'bottom-border' '' %}">
<div class="player">
{% if team.id %}
<a href="{% url 'team-details' tournament.id team.id %}" class="player-link"> <!-- Add this anchor tag -->
{% endif %}
{% for name in team.names %}
<div class="semibold {% if team.is_winner %}winner{% endif %}">
{% if name|length > 0 %}
@ -23,6 +25,9 @@
{% endif %}
</div>
{% endfor %}
{% if team.id %}
</a>
{% endif %}
</div>
{% if match.should_show_scores %}

@ -0,0 +1,61 @@
<div class="cell medium-12 large-3 my-block">
<div class="bubble">
<label class="matchtitle">{{ player.name }}</label>
<div>
<div class="match-result bottom-border" style="padding-right: 10px;">
<div class="player">
<div class="single-line">
<strong>{{ player.clean_club_name }}</strong>
</div>
</div>
</div>
<div class="match-result bottom-border">
<div class="player">
<div class="semibold">
<strong>Classement</strong>
</div>
</div>
<div class="scores">
<span class="score ws numbers">{{ player.format_ordinal }}</span>
</div>
</div>
<div class="match-result bottom-border">
<div class="player">
<div class="semibold">
<strong>Age</strong>
</div>
</div>
<div class="scores">
<span class="score ws numbers">
{{ player.calculate_age|default:"?" }} ans
</span>
</div>
</div>
<div class="match-result bottom-border">
<div class="player">
<div class="semibold">
<strong>Points</strong>
</div>
</div>
<div class="scores">
<span class="score ws numbers">{{ player.points|default:"0" }} pts</span>
</div>
</div>
<div class="match-result">
<div class="player">
<div class="semibold">
<strong>Tournois joués</strong>
</div>
</div>
<div class="scores">
<span class="score ws numbers">{{ player.tournament_played|default:"0" }}</span>
</div>
</div>
</div>
</div>
</div>

@ -4,9 +4,19 @@
<div class="table-cell"><div class="mybox center">{{ ranking.formatted_ranking }}</div></div>
<div class="table-cell table-cell-large padding-left semibold">
{% if ranking.id %}
<a href="{% url 'team-details' tournament.id ranking.id %}" class="player-link"> <!-- Add this anchor tag -->
{% endif %}
{% for name in ranking.names %}
<div>{{ name }}</div>
{% endfor %}
{% if ranking.id %}
</a>
{% endif %}
</div>
{% if tournament.display_points_earned %}

@ -12,26 +12,29 @@
{% include 'tournaments/navigation_tournament.html' %}
{% if rankings %}
<div class="grid-x padding-bottom">
<div class="cell medium-6 large-6 my-block">
<div class="bubble">
<div class="grid-x padding-bottom">
<div class="cell medium-6 large-6 my-block">
<div class="bubble">
<label class="title">{{ tournament.ranking_count_display }}</label>
{% if rankings %}
<label class="title">{{ tournament.ranking_count_display }}</label>
{% for ranking in rankings %}
{% include 'tournaments/ranking_row.html' %}
{% include 'tournaments/ranking_row.html' %}
{% endfor %}
{% else %}
<div class="ranking">
<div class="cell medium-12">
<div class="semibold">
Aucun classement disponible
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% endif %}

@ -4,9 +4,19 @@
<div class="summons-left">
<div class="semibold">
{% if summon.id %}
<a href="{% url 'team-details' tournament.id summon.id %}" class="player-link"> <!-- Add this anchor tag -->
{% endif %}
{% for name in summon.names %}
<div>{{ name }}</div>
{% endfor %}
{% if summon.id %}
</a>
{% endif %}
</div>
</div>
<div class="summons-right">

@ -0,0 +1,54 @@
{% extends 'tournaments/base.html' %}
{% block head_title %}Équipes du {{ tournament.display_name }}{% endblock %}
{% block first_title %}{{ tournament.event.display_name }}{% endblock %}
{% block second_title %}{{ tournament.display_name }}{% endblock %}
{% block content %}
<div class="grid-x grid-margin-x">
<style>
.bubble {
height: 100%;
}
</style>
<div class="cell medium-12">
<h1 class="club my-block topmargin20">{{ team.formatted_team_names }}</h1>
<div class="grid-x">
{% for player in team.playerregistration_set.all %}
{% include 'tournaments/player_row.html' with player=player %}
{% endfor %}
{% include 'tournaments/team_stats.html' %}
</div>
</div>
</div>
<div class="grid-x grid-margin-x">
{% with upcoming_matches=team.get_upcoming_matches %}
{% if upcoming_matches %}
<!-- Upcoming Matches -->
<div class="cell medium-12">
<h1 class="club my-block topmargin20">Prochains matchs</h1>
<div class="grid-x">
{% for match in upcoming_matches %}
{% include 'tournaments/match_cell.html' %}
{% endfor %}
</div>
</div>
{% endif %}
{% endwith %}
{% with completed_matches=team.get_completed_matches %}
{% if completed_matches %}
<!-- Completed Matches -->
<div class="cell medium-12">
<h1 class="club my-block topmargin20">Matchs terminés</h1>
<div class="grid-x">
{% for match in completed_matches %}
{% include 'tournaments/match_cell.html' %}
{% endfor %}
</div>
</div>
{% endif %}
{% endwith %}
</div>
{% endblock %}

@ -8,9 +8,18 @@
{% endif %}
{% if team.names %}
<div class="table-cell table-cell-large semibold">
{% if team.team_registration.id %}
<a href="{% url 'team-details' tournament.id team.team_registration.id %}" class="player-link"> <!-- Add this anchor tag -->
{% endif %}
{% for name in team.names %}
<div>{{ name }}</div>
{% endfor %}
{% if team.team_registration.id %}
</a>
{% endif %}
</div>
{% else %}
<div class="table-cell table-cell-large semibold">

@ -0,0 +1,72 @@
<div class="cell medium-12 large-3 my-block">
<div class="bubble">
<label class="matchtitle">Statistiques du tournoi</label>
<div>
{% with stats=team.get_statistics %}
{% if stats.final_ranking %}
<div class="match-result bottom-border">
<div class="player">
<div class="semibold">
<strong>Classement final</strong>
</div>
</div>
<div class="scores">
<span class="score ws numbers">{{ stats.final_ranking }}</span>
</div>
</div>
{% endif %}
{% if stats.points_earned %}
<div class="match-result bottom-border">
<div class="player">
<div class="semibold">
<strong>Points gagnés</strong>
</div>
</div>
<div class="scores">
<span class="score ws numbers">{{ stats.points_earned }} pts</span>
</div>
</div>
{% endif %}
{% if stats.initial_stage %}
<div class="match-result bottom-border">
<div class="player">
<div class="semibold">
<strong>Départ</strong>
</div>
</div>
<div class="scores">
<span class="score ws numbers">{{ stats.initial_stage }}</span>
</div>
</div>
{% endif %}
<div class="match-result bottom-border">
<div class="player">
<div class="semibold">
<strong>Matchs joués</strong>
</div>
</div>
<div class="scores">
<span class="score ws numbers">{{ stats.matches_played }}</span>
</div>
</div>
{% if stats.victory_ratio %}
<div class="match-result">
<div class="player">
<div class="semibold">
<strong>Ratio victoires</strong>
</div>
</div>
<div class="scores">
<span class="score ws numbers">{{ stats.victory_ratio }}</span>
</div>
</div>
{% endif %}
{% endwith %}
</div>
</div>
</div>

@ -28,6 +28,7 @@ urlpatterns = [
path('rankings/', views.tournament_rankings, name='tournament-rankings'),
path('rankings/json/', views.tournament_rankings_json, name='tournament-rankings-json'),
path('broadcast/rankings/', views.tournament_broadcast_rankings, name='broadcasted-rankings'),
path('team/<str:team_id>/', views.team_details, name='team-details'),
])
),
path("event/<str:event_id>/broadcast/auto/", views.automatic_broadcast_event, name='automatic-broadcast-event'),
@ -39,4 +40,5 @@ urlpatterns = [
path('utils/xls-to-csv/', views.xls_to_csv, name='xls-to-csv'),
path('mail-test/', views.simple_form_view, name='mail-test'),
path('admin/tournament-import/', views.tournament_import_view, name='tournament_import'),
]

@ -576,3 +576,21 @@ def get_file_data(zip_file, file_path):
except json.JSONDecodeError as e:
print(f"JSON Error for {file_path}: {str(e)}")
raise Exception(f"Invalid JSON in file {file_path}")
def team_details(request, tournament_id, team_id):
tournament = get_object_or_404(Tournament, id=tournament_id)
team = get_object_or_404(TeamRegistration, id=team_id)
print(f"Processing team {team_id} in tournament {tournament_id}")
# Get all matches for this team
all_matches = team.get_matches()
print(f"Total matches found: {all_matches.count()}")
print("Match details:")
for match in all_matches:
print(f"- Match {match.id}: start={match.start_date}, end={match.end_date}")
return render(request, 'tournaments/team_details.html', {
'tournament': tournament,
'team': team,
'debug': True # Set to False in production
})

Loading…
Cancel
Save