Laurent 8 months ago
commit d5a6625b9a
  1. 2
      tournaments/models/enums.py
  2. 2
      tournaments/models/group_stage.py
  3. 36
      tournaments/models/match.py
  4. 19
      tournaments/models/team_registration.py
  5. 21
      tournaments/models/tournament.py
  6. 11
      tournaments/repositories.py
  7. 20
      tournaments/services/tournament_registration.py
  8. 2
      tournaments/services/tournament_unregistration.py
  9. 54
      tournaments/static/tournaments/css/broadcast.css
  10. 37
      tournaments/static/tournaments/css/style.css
  11. 24
      tournaments/templates/register_tournament.html
  12. 1
      tournaments/templates/tournaments/broadcast/broadcasted_auto.html
  13. 1
      tournaments/templates/tournaments/broadcast/broadcasted_auto_event.html
  14. 17
      tournaments/templates/tournaments/broadcast/broadcasted_group_stage.html
  15. 1
      tournaments/templates/tournaments/broadcast/broadcasted_group_stages.html
  16. 10
      tournaments/templates/tournaments/broadcast/broadcasted_match.html
  17. 4
      tournaments/templates/tournaments/group_stage_cell.html
  18. 6
      tournaments/templates/tournaments/match_cell.html
  19. 4
      tournaments/utils/player_search.py
  20. 4
      tournaments/views.py

@ -15,6 +15,8 @@ class FederalCategory(models.IntegerChoices):
@staticmethod
def female_in_male_assimilation_addition(rank: int) -> int:
if rank is None:
return 0
if 1 <= rank <= 10:
return 400
elif 11 <= rank <= 30:

@ -135,7 +135,7 @@ class GroupStage(models.Model):
def has_at_least_one_started_match(self):
for match in self.match_set.all():
if match.start_date is not None and match.start_date <= timezone.now():
if match.start_date is not None and match.start_date <= timezone.now() and match.confirmed is True:
return True
return False

@ -180,37 +180,45 @@ class Match(models.Model):
if len(team_scores) == 0:
if (self.round and self.round.parent is None and self.round.tournament.round_set.filter(parent__isnull=True, group_stage_loser_bracket=False).count() - 1 == self.round.index):
names = ["Qualifié", '']
names = ["Qualifié"]
team = self.default_live_team(names)
teams.append(team)
names = ["Qualifié", '']
names = ["Qualifié"]
team = self.default_live_team(names)
teams.append(team)
return teams
if (self.group_stage):
names = ["Équipe de poule", '']
names = ["Équipe de poule"]
team = self.default_live_team(names)
teams.append(team)
names = ["Équipe de poule", '']
names = ["Équipe de poule"]
team = self.default_live_team(names)
teams.append(team)
return teams
elif self.round and self.round.parent:
if loser_top_match:
names = [f"Perdant {loser_top_match.computed_name()}", '']
names = [f"Perdant {loser_top_match.computed_name()}"]
team = self.default_live_team(names)
teams.append(team)
if loser_bottom_match:
names = [f"Perdant {loser_bottom_match.computed_name()}", '']
names = [f"Perdant {loser_bottom_match.computed_name()}"]
team = self.default_live_team(names)
teams.append(team)
if previous_top_match:
names = [f"Gagnant {previous_top_match.computed_name()}"]
team = self.default_live_team(names)
teams.append(team)
if previous_bottom_match:
names = [f"Gagnant {previous_bottom_match.computed_name()}"]
team = self.default_live_team(names)
teams.append(team)
elif self.round and self.round.parent is None:
if previous_top_match:
names = [f"Gagnant {previous_top_match.computed_name()}", '']
names = [f"Gagnant {previous_top_match.computed_name()}"]
team = self.default_live_team(names)
teams.append(team)
if previous_bottom_match:
names = [f"Gagnant {previous_bottom_match.computed_name()}", '']
names = [f"Gagnant {previous_bottom_match.computed_name()}"]
team = self.default_live_team(names)
teams.append(team)
elif len(team_scores) == 1:
@ -218,33 +226,33 @@ class Match(models.Model):
existing_team = team_scores[0].live_team(self)
if (self.group_stage):
teams.append(existing_team)
names = ["Équipe de poule", '']
names = ["Équipe de poule"]
team = self.default_live_team(names)
teams.append(team)
elif self.round:
if loser_top_match and loser_top_match.disabled == False and loser_top_match.end_date is None:
names = [f"Perdant {loser_top_match.computed_name()}", '']
names = [f"Perdant {loser_top_match.computed_name()}"]
team = self.default_live_team(names)
teams.append(team)
teams.append(existing_team)
elif loser_bottom_match:
names = [f"Perdant {loser_bottom_match.computed_name()}", '']
names = [f"Perdant {loser_bottom_match.computed_name()}"]
team = self.default_live_team(names)
teams.append(existing_team)
teams.append(team)
elif previous_top_match and previous_top_match.disabled == False and previous_top_match.end_date is None:
names = [f"Gagnant {previous_top_match.computed_name()}", '']
names = [f"Gagnant {previous_top_match.computed_name()}"]
team = self.default_live_team(names)
teams.append(team)
teams.append(existing_team)
elif previous_bottom_match:
names = [f"Gagnant {previous_bottom_match.computed_name()}", '']
names = [f"Gagnant {previous_bottom_match.computed_name()}"]
team = self.default_live_team(names)
teams.append(existing_team)
teams.append(team)
elif (self.round.parent is None and self.round.tournament.round_set.filter(parent__isnull=True, group_stage_loser_bracket=False).count() - 1 == self.round.index):
match_index_within_round = self.index - (int(2 ** self.round.index) - 1)
names = ["Qualifié", '']
names = ["Qualifié"]
team = self.default_live_team(names)
if match_index_within_round < int(2 ** self.round.index) / 2:
teams.append(existing_team)

@ -38,22 +38,29 @@ class TeamRegistration(models.Model):
return self.player_names()
def player_names_as_list(self):
return [pr.name() for pr in self.playerregistration_set.all()]
players = list(self.playerregistration_set.all())
if len(players) == 0:
return []
elif len(players) == 1:
return [players[0].name()]
else:
return [pr.name() for pr in players]
def team_names(self):
if self.name:
return [self.name]
return [self.name] #add an empty line if it's a team name
else:
return self.player_names_as_list()
def shortened_team_names(self):
if self.name:
return [self.name]
return [self.name] #add an empty line if it's a team name
else:
players = list(self.playerregistration_set.all())
if len(players) == 1:
return [players[0].shortened_name(), '']
if len(players) == 0:
return []
elif len(players) == 1:
return [players[0].shortened_name()]
else:
return [pr.shortened_name() for pr in players]

@ -678,6 +678,12 @@ class Tournament(models.Model):
if previous_round:
# print('previous_round')
matches.extend(previous_round.get_matches_recursive(True))
previous_previous_round = self.round_for_index(current_round.index + 2)
if previous_previous_round:
previous_previous_matches = previous_previous_round.get_matches_recursive(True)
previous_previous_matches = [m for m in previous_previous_matches if m.end_date is None]
matches.extend(previous_previous_matches)
else:
# print('group_stages')
group_stages = [gs.live_group_stages() for gs in self.last_group_stage_step()]
@ -722,6 +728,10 @@ class Tournament(models.Model):
matches = [m for m in self.all_matches(False) if m.start_date and m.end_date is None]
# print(f'first_unfinished_match > match len: {len(matches)}')
matches.sort(key=lambda m: m.start_date)
main_bracket_matches = [m for m in matches if m.round and m.round.parent is None]
if main_bracket_matches:
return main_bracket_matches[0]
if matches:
return matches[0]
else:
@ -782,7 +792,16 @@ class Tournament(models.Model):
group_stages.sort(key=lambda gs: (gs.index, gs.start_date is None, gs.start_date))
for group_stage in group_stages:
matches.extend(group_stage.match_set.all())
matches = [m for m in matches if m.should_appear()]
if len(matches) > 16:
# if more than 16 groupstage matches
now = timezone.now()
future_threshold = now + timedelta(hours=1)
past_threshold = now - timedelta(hours=1)
matches = [m for m in matches if m.should_appear() and
(m.start_date is None or m.start_date <= future_threshold) and # Not starting in more than 1h
(m.end_date is None or m.end_date >= past_threshold)] # Not finished for more than 1h
matches = matches[:16]
matches.sort(key=lambda m: (m.start_date is None, m.end_date is not None, m.start_date, m.index))
group_stage_loser_bracket = list(self.round_set.filter(parent=None, group_stage_loser_bracket=True).all())

@ -23,7 +23,7 @@ class TournamentRegistrationRepository:
is_captain = False
player_licence_id = player_data['licence_id']
if player_licence_id and stripped_license:
if player_licence_id.startswith(stripped_license):
if stripped_license.lower() in player_licence_id.lower():
is_captain = True
sex, rank, computed_rank = TournamentRegistrationRepository._compute_rank_and_sex(
@ -31,6 +31,7 @@ class TournamentRegistrationRepository:
player_data
)
print("create_player_registrations", player_data.get('last_name'), sex, rank, computed_rank)
data_source = None
if player_data.get('found_in_french_federation', False) == True:
data_source = PlayerDataSource.FRENCH_FEDERATION
@ -66,14 +67,18 @@ class TournamentRegistrationRepository:
@staticmethod
def _compute_rank_and_sex(tournament, player_data):
is_woman = player_data.get('is_woman', False)
rank = player_data.get('rank', 0)
rank = player_data.get('rank', None)
if rank is None:
computed_rank = 100000
else:
computed_rank = rank
sex = PlayerSexType.MALE
sex = PlayerSexType.MALE
if is_woman:
sex = PlayerSexType.FEMALE
if tournament.federal_category == FederalCategory.MEN:
computed_rank = str(int(computed_rank) +
FederalCategory.female_in_male_assimilation_addition(int(rank)))
print("_compute_rank_and_sex", sex, rank, computed_rank)
return sex, rank, computed_rank

@ -31,9 +31,18 @@ class TournamentRegistrationService:
if 'add_player' in self.request.POST:
self.handle_add_player()
if 'remove_player' in self.request.POST:
self.handle_remove_player()
elif 'register_team' in self.request.POST:
self.handle_team_registration()
def handle_remove_player(self):
team_registration = self.request.session.get('team_registration', [])
if team_registration: # Check if list is not empty
team_registration.pop() # Remove last element
self.request.session['team_registration'] = team_registration
self.context['current_players'] = team_registration
def handle_add_player(self):
if not self.context['add_player_form'].is_valid():
return
@ -106,6 +115,7 @@ class TournamentRegistrationService:
self.initialize_session_data()
def add_player_to_session(self, player_data):
print("add_player_to_session", player_data)
if not self.request.session.get('team_registration'):
self.request.session['team_registration'] = []
@ -216,6 +226,7 @@ class TournamentRegistrationService:
return False
def _handle_valid_names(self, player_data):
print("_handle_valid_names", player_data)
if player_data.get('rank') is None:
self._set_default_rank(player_data)
@ -224,8 +235,8 @@ class TournamentRegistrationService:
self.context['add_player_form'].first_tournament = False
def _handle_invalid_names(self, licence_id, player_data):
if not self.context['add_player_form'].first_tournament:
data, found = get_player_name_from_csv(self.tournament.federal_category, licence_id)
print("_handle_invalid_names get_player_name_from_csv", data, found)
if found and data:
self._update_player_data_from_csv(player_data, data)
player_check = self._player_check(player_data)
@ -235,6 +246,7 @@ class TournamentRegistrationService:
else:
return
else:
print("_handle_first_tournament_case")
self._handle_first_tournament_case(data)
def _set_default_rank(self, player_data):
@ -245,7 +257,7 @@ class TournamentRegistrationService:
self.request.session['is_woman'] = data['is_woman']
self.request.session.modified = True
player_data['rank'] = self.request.session.get('last_rank', 0)
player_data['rank'] = self.request.session.get('last_rank', None)
player_data['is_woman'] = self.request.session.get('is_woman', False)
def _update_user_license(self, licence_id):
@ -261,6 +273,7 @@ class TournamentRegistrationService:
self.context['add_player_form'].first_tournament = False
def _update_player_data_from_csv(self, player_data, csv_data):
print("_update_player_data_from_csv", player_data, csv_data)
player_data.update({
'first_name': csv_data['first_name'],
'last_name': csv_data['last_name'],
@ -278,6 +291,7 @@ class TournamentRegistrationService:
})
def _handle_first_tournament_case(self, data):
print("_handle_first_tournament_case", data)
if data:
self.request.session['last_rank'] = data['rank']
self.request.session['is_woman'] = data['is_woman']
@ -307,6 +321,6 @@ class TournamentRegistrationService:
def _license_already_registered(self, stripped_license):
return PlayerRegistration.objects.filter(
team_registration__tournament=self.tournament,
licence_id__startswith=stripped_license,
licence_id__icontains=stripped_license,
team_registration__walk_out=False
).exists()

@ -53,7 +53,7 @@ class TournamentUnregistrationService:
def _find_player_registration(self):
self.player_registration = PlayerRegistration.objects.filter(
licence_id__startswith=self.request.user.licence_id,
licence_id__icontains=self.request.user.licence_id,
team_registration__tournament_id=self.tournament.id,
).first()

@ -25,3 +25,57 @@ body {
.bold {
font-family: "Montserrat-Bold";
}
.player {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
min-height: 2.8em; /* This ensures minimum height for 2 lines */
justify-content: center;
overflow: hidden;
}
/* Add this if you want empty lines to take up space */
.player div {
min-height: 1.4em; /* Height for single line */
}
/* For single player teams */
.player.single-player .bold {
max-height: 2.8em; /* Adjust based on your needs */
line-height: 1.4em;
overflow: hidden;
position: relative;
text-overflow: ellipsis;
}
/* For two player teams */
.player.two-players .bold {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.flex {
display: flex;
align-items: center;
}
.flex-left {
flex: 1;
text-align: left;
justify-content: center;
min-height: 4em;
padding-right: 5px;
}
.flex-right {
flex: initial;
text-align: right;
justify-content: center;
}
.center {
align-items: center;
}

@ -323,9 +323,34 @@ tr {
}
.player {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
min-height: 2.8em; /* This ensures minimum height for 2 lines */
justify-content: center;
overflow: hidden;
}
/* Add this if you want empty lines to take up space */
.player div {
min-height: 1.4em; /* Height for single line */
}
/* For single player teams */
.player.single-player .semibold {
max-height: 2.8em; /* Adjust based on your needs */
line-height: 1.4em;
overflow: hidden;
position: relative;
text-overflow: ellipsis;
}
/* For two player teams */
.player.two-players .semibold {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.scores {
@ -344,7 +369,6 @@ tr {
font-size: 1.3em;
vertical-align: middle;
text-align: center;
padding: 0px 5px;
/* width: 30px; */
}
@ -718,14 +742,15 @@ h-margin {
.flex-left {
flex: 1;
text-align: left;
padding: 5px 0px;
justify-content: center;
min-height: 4em;
padding-right: 5px;
}
.flex-right {
flex: initial;
text-align: right;
vertical-align: middle;
padding: 5px 0px;
justify-content: center;
}
#header {
@ -838,10 +863,6 @@ h-margin {
background-color: #90ee90; /* Light green color */
}
.player {
position: relative; /* Ensures the overlay is positioned within this block */
}
.overlay-text {
position: absolute;
top: 50%;

@ -23,7 +23,7 @@
{% if registration_successful %}
<p>Merci, l'inscription a bien été envoyée au juge-arbitre.</p>
<p style="text-align: justify;">
Un email de confirmation a été envoyé à {{ user.email }} pour confirmer votre inscription. Pensez à vérifier vos spams si vous ne recevez pas l'email. En cas de problème, contactez le juge-arbitre.
Un email de confirmation a été envoyé à l'adresse associée à votre compte Padel Club ({{ user.email }}). Pensez à vérifier vos spams si vous ne recevez pas l'email. En cas de problème, contactez le juge-arbitre.
</p>
{% else %}
<form method="post">
@ -48,7 +48,24 @@
</p>
<ul>
{% for player in current_players %}
<li>{{ player.first_name }} {{ player.last_name }}{% if player.licence_id %} ({{ player.licence_id }}){% endif %}</li>
<li>
<div>
{{ player.first_name }} {{ player.last_name }}{% if player.licence_id %} ({{ player.licence_id }}){% endif %}
</div>
<div>
{{ player.club_name }}
</div>
<div>
Classement à ce jour : {{ player.rank }}
</div>
{% if not forloop.first %} <!-- Only show remove button if not the first player -->
<div>
<button type="submit" name="remove_player" class="btn small-button">
modifier
</button>
</div>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
@ -73,6 +90,9 @@
{% endif %}
{% if add_player_form.first_tournament or add_player_form.user_without_licence or tournament.license_is_required is False %}
{% if not add_player_form.user_without_licence and tournament.license_is_required is True %}
<div class="semibold">
Padel Club n'a pas trouvé votre partenaire, il se peut qu'il s'agisse de son premier tournoi. Contacter le juge-arbitre après l'inscription si ce n'est pas le cas.
</div>
<div class="semibold">
Précisez les informations du joueur :
</div>

@ -38,6 +38,7 @@
paginatedSummons: null,
paginatedRankings: null,
active: 1,
hide_weight: {{ tournament.hide_weight|lower }},
prefixTitle: '',
retrieveData() {
fetch('/tournament/{{ tournament.id }}/broadcast/json/')

@ -39,6 +39,7 @@
paginatedSummons: null,
paginatedRankings: null,
active: 1,
hide_weight: {{ tournament.hide_weight|lower }},
prefixTitle: '',
eventTitle: '',
title: '',

@ -8,7 +8,10 @@
<div>
<div class="flex" :class="group_stage.teams[i-1].qualified ? 'qualified' : ''">
<div class="flex-left">
<div class="flex-left player" :class="{
'single-player': group_stage.teams[i-1].names.length === 1,
'two-players': group_stage.teams[i-1].names.length === 2
}">
<template x-for="name in group_stage.teams[i-1].names">
<div class="bold" x-text="name"></div>
@ -30,18 +33,16 @@
</div>
<div class="flex-right">
<div x-show="group_stage.teams[i-1].match_count == 0">
{% if not tournament.hide_weight %}
<div class="score ws numbers"><span x-text="group_stage.teams[i-1].weight"></span></div>
{% endif %}
<div x-show="group_stage.started === true">
<div class="score ws numbers" x-show="hide_weight === false">
<span x-text="group_stage.teams[i-1].weight"></span>
</div>
</div>
<div x-show="group_stage.teams[i-1].match_count > 0">
<div class="center">
<div x-show="group_stage.teams[i-1].match_count > 0 and group_stage.started === false">
<div class="score ws numbers"><span x-text="group_stage.teams[i-1].win_loss"></span></div>
<div class="ws numbers"><span x-text="group_stage.teams[i-1].diff"></span></div>
</div>
</div>
</div>
</div>

@ -38,6 +38,7 @@
<body x-data="{
paginatedGroupStages: null,
active: 1,
hide_weight: {{ tournament.hide_weight|lower }},
retrieveMatches() {
fetch('/tournament/{{ tournament.id }}/group-stages/json/')
.then(res => res.json())

@ -16,7 +16,10 @@
<div>
<div class="match-result">
<div class="player">
<div class="player" :class="{
'single-player': match.teams[i-1].names.length === 1,
'two-players': match.teams[i-1].names.length === 2
}">
<!-- Show lucky loser or walkout status -->
<template x-if="match.teams[i-1].is_lucky_loser">
<div class="overlay-text right-label minor-info semibold">(LL)</div>
@ -65,6 +68,11 @@
},
}" x-html="showWalkOut(match, match.teams[i-1])">
</span>
<template x-if="!match.tournament?.hide_weight && match.teams[i-1].weight && !match.has_walk_out && match.teams[i-1].scores.length === 0">
<span class="score ws numbers" x-text="match.teams[i-1].weight"></span>
</template>
</div>
</div>

@ -13,7 +13,7 @@
{% for team in group_stage.teams %}
<div class="flex {% if team.qualified %}qualified{% endif %}">
<div class="flex-left">
<div class="flex-left player {% if team.names|length == 1 %}single-player{% else %}two-players{% endif %}">
{% 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 %}
@ -31,10 +31,8 @@
</div>
<div class="flex-right">
{% if group_stage.started %}
<div class="center">
<div class="score ws numbers">{{ team.wins_losses }}</div>
<div class="ws numbers">{{ team.formatted_diff }}</div>
</div>
{% else %}
{% if tournament.hide_weight %}
<div class="score ws"></div>

@ -14,7 +14,7 @@
{% for team in match.teams %}
<div class="match-result {% cycle 'bottom-border' '' %}">
<div class="player">
<div class="player {% if team.names|length == 1 %}single-player{% else %}two-players{% endif %}">
{% if team.id %}
<a href="{% url 'team-details' tournament.id team.id %}" class="player-link">
{% endif %}
@ -27,11 +27,7 @@
{% for name in team.names %}
<div class="semibold{% if team.walk_out == 1 %} strikethrough{% endif %}{% if team.is_winner %} winner{% endif %}">
{% if name|length > 0 %}
{{ name }}
{% else %}
&nbsp;
{% endif %}
</div>
{% endfor %}
{% if team.id %}

@ -9,7 +9,7 @@ from tournaments.models.enums import FederalCategory
def clean_licence_id(licence_id):
# This regex matches the trailing letters (non-digits) and removes them
cleaned_licence_id = re.sub(r'\D+$', '', str(licence_id)) # \D+ matches non-digits at the end
return cleaned_licence_id
return cleaned_licence_id.lstrip("0")
def get_player_name_from_csv(category, licence_id, base_folder=None):
"""
@ -29,6 +29,8 @@ def get_player_name_from_csv(category, licence_id, base_folder=None):
else:
cleaned_licence_id = None
print("get_player_name_from_csv", cleaned_licence_id)
def extract_date(file_name):
"""
Extract the date (MM-YYYY) from the file name and return it as a datetime object.

@ -170,7 +170,7 @@ def tournament_info(request, tournament_id):
stripped_license = validator.stripped_license
# Check if there is a PlayerRegistration for this user in this tournament
registered_user = PlayerRegistration.objects.filter(
licence_id__startswith=stripped_license,
licence_id__icontains=stripped_license,
team_registration__tournament=tournament,
team_registration__walk_out=False,
).first()
@ -747,7 +747,7 @@ def my_tournaments(request):
def filter_user_tournaments(tournaments):
return [t for t in tournaments if t.teamregistration_set.filter(
playerregistration__licence_id__startswith=stripped_license,
playerregistration__licence_id__icontains=stripped_license,
walk_out=False
).exists()]

Loading…
Cancel
Save