From 260692da751c497c5bb50e395485daf5632d9906 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 25 Apr 2025 15:03:02 +0200 Subject: [PATCH] add the ability to register animation --- ...ion_user_teamregistration_user_and_more.py | 35 ++++ tournaments/models/enums.py | 38 +++++ tournaments/models/player_registration.py | 3 +- tournaments/models/team_registration.py | 3 +- tournaments/models/tournament.py | 97 ++++++----- tournaments/models/unregistered_player.py | 3 +- tournaments/models/unregistered_team.py | 3 +- tournaments/services/email_service.py | 160 +++++++++++++----- .../services/tournament_registration.py | 21 ++- .../services/tournament_unregistration.py | 33 ++-- .../tournaments/tournament_info.html | 2 +- .../templates/tournaments/tournament_row.html | 5 +- tournaments/views.py | 150 +++++++--------- 13 files changed, 351 insertions(+), 202 deletions(-) create mode 100644 tournaments/migrations/0117_playerregistration_user_teamregistration_user_and_more.py diff --git a/tournaments/migrations/0117_playerregistration_user_teamregistration_user_and_more.py b/tournaments/migrations/0117_playerregistration_user_teamregistration_user_and_more.py new file mode 100644 index 0000000..c81d5c8 --- /dev/null +++ b/tournaments/migrations/0117_playerregistration_user_teamregistration_user_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 5.1 on 2025-04-25 07:48 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0116_customuser_disable_ranking_federal_ruling_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='playerregistration', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='player_registrations', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='teamregistration', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='team_registrations', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='unregisteredplayer', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='unregistered_players', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='unregisteredteam', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='unregistered_teams', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/tournaments/models/enums.py b/tournaments/models/enums.py index 61ba26c..22abf9e 100644 --- a/tournaments/models/enums.py +++ b/tournaments/models/enums.py @@ -49,6 +49,44 @@ class FederalLevelCategory(models.IntegerChoices): P2000 = 2000, 'P2000' CHPT = 1, 'Championnat' + def localized_word(self): + if self == FederalLevelCategory.UNLISTED: + return "animation" + elif self == FederalLevelCategory.CHPT: + return "championnat" + else: + return "tournoi" + + def is_feminine_word(self): + if self == FederalLevelCategory.UNLISTED: + return True + else: + return False + + def localized_prefix_at(self): + if self == FederalLevelCategory.UNLISTED: + return "à l'" + else: + return "au " + + def localized_prefix_of(self): + if self == FederalLevelCategory.UNLISTED: + return "de l'" + else: + return "du " + + def localized_prefix_this(self): + if self == FederalLevelCategory.UNLISTED: + return "cette" + else: + return "ce " + + def localized_prefix_that(self): + if self == FederalLevelCategory.UNLISTED: + return "l'" + else: + return "le " + @staticmethod def min_player_rank(level=None, category=None, age_category=None) -> int: if level == FederalLevelCategory.P25: diff --git a/tournaments/models/player_registration.py b/tournaments/models/player_registration.py index be0b947..e7f3546 100644 --- a/tournaments/models/player_registration.py +++ b/tournaments/models/player_registration.py @@ -1,5 +1,5 @@ from django.db import models -from . import SideStoreModel, TeamRegistration, PlayerSexType, PlayerDataSource, PlayerPaymentType, OnlineRegistrationStatus +from . import SideStoreModel, TeamRegistration, PlayerSexType, PlayerDataSource, PlayerPaymentType, OnlineRegistrationStatus, CustomUser from .enums import RegistrationStatus import uuid from django.utils import timezone @@ -7,6 +7,7 @@ from django.utils import timezone class PlayerRegistration(SideStoreModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) team_registration = models.ForeignKey(TeamRegistration, on_delete=models.SET_NULL, related_name='player_registrations', null=True) + user = models.ForeignKey(CustomUser, null=True, blank=True, on_delete=models.SET_NULL, related_name='player_registrations') first_name = models.CharField(max_length=50, blank=True) last_name = models.CharField(max_length=50, blank=True) licence_id = models.CharField(max_length=50, null=True, blank=True) diff --git a/tournaments/models/team_registration.py b/tournaments/models/team_registration.py index 5833210..76ce959 100644 --- a/tournaments/models/team_registration.py +++ b/tournaments/models/team_registration.py @@ -1,5 +1,5 @@ from django.db import models -from . import SideStoreModel, Tournament, GroupStage, Match +from . import SideStoreModel, Tournament, GroupStage, Match, CustomUser import uuid from django.utils import timezone from .enums import RegistrationStatus @@ -10,6 +10,7 @@ class TeamRegistration(SideStoreModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) tournament = models.ForeignKey(Tournament, on_delete=models.SET_NULL, related_name='team_registrations', null=True) group_stage = models.ForeignKey(GroupStage, null=True, blank=True, on_delete=models.SET_NULL, related_name='team_registrations') + user = models.ForeignKey(CustomUser, null=True, blank=True, on_delete=models.SET_NULL, related_name='team_registrations') registration_date = models.DateTimeField(null=True, blank=True) call_date = models.DateTimeField(null=True, blank=True) bracket_position = models.IntegerField(null=True, blank=True) diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index 71e5e47..0b04b83 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -131,16 +131,12 @@ class Tournament(BaseModel): def display_name(self): if self.name: - if self.federal_level_category == FederalLevelCategory.UNLISTED: - return self.name - return self.base_name() + " " + self.name + return self.short_base_name() + " " + self.name else: return self.base_name() def broadcast_display_name(self): if self.name: - if self.federal_level_category == FederalLevelCategory.UNLISTED: - return self.name return self.short_base_name() + " " + self.name else: return self.base_name() @@ -154,12 +150,17 @@ class Tournament(BaseModel): def base_name(self): return f"{self.level()} {self.category()}" + def full_name(self): + if self.name: + return f"{self.level()} {self.name} {self.category()} {self.age()}" + return f"{self.level()} {self.category()} {self.age()}" + def short_base_name(self): category = self.category() - if len(category) > 0: - return f"{self.level()}{category[0]}" + if len(category) > 0 and self.federal_level_category > 1: + return f"{self.short_level()}{category[0]}" else: - return self.level() + return self.short_level() def filter_name(self): components = [self.formatted_start_date(), self.short_base_name()] @@ -185,6 +186,9 @@ class Tournament(BaseModel): return formats.date_format(self.local_start_date(), format='l j F Y H:i').capitalize() def level(self): + return self.get_federal_level_category_display() + + def short_level(self): if self.federal_level_category == 0: return "Anim." if self.federal_level_category == 1: @@ -192,6 +196,11 @@ class Tournament(BaseModel): return self.get_federal_level_category_display() def category(self): + if self.federal_age_category > 100 and self.federal_age_category < 200: + if self.federal_category == 0: + return "Garçon" + if self.federal_category == 1: + return "Fille" return self.get_federal_category_display() def age(self): @@ -1250,24 +1259,25 @@ class Tournament(BaseModel): # In waiting list with no limit return current_team_count - self.team_count - def build_tournament_type_array(self): + def build_tournament_type_str(self): tournament_details = [] - if self.federal_level_category > 0: - tournament_details.append(self.level()) + tournament_details.append(self.level()) if self.category(): tournament_details.append(self.category()) if self.age() and self.federal_age_category != FederalAgeCategory.SENIOR: tournament_details.append(self.age()) - return tournament_details - def build_tournament_type_str(self): - tournament_details = self.build_tournament_type_array() return " ".join(filter(None, tournament_details)) def build_tournament_details_str(self): - tournament_details = self.build_tournament_type_array() - name_str = self.build_name_details_str() + tournament_details = [] + if self.federal_level_category > 0: + tournament_details.append(self.level()) + if self.category(): + tournament_details.append(self.category()) + if self.age() and self.federal_age_category != FederalAgeCategory.SENIOR: + tournament_details.append(self.age()) if len(name_str) > 0: tournament_details.append(name_str) @@ -1313,13 +1323,13 @@ class Tournament(BaseModel): # Check age category restrictions if self.federal_age_category == FederalAgeCategory.A11_12 and user_age > 12: - reasons.append("Ce tournoi est réservé aux -12 ans") + reasons.append("Ce tournoi est réservé aux 12 ans et moins") if self.federal_age_category == FederalAgeCategory.A13_14 and user_age > 14: - reasons.append("Ce tournoi est réservé aux -14 ans") + reasons.append("Ce tournoi est réservé aux 14 ans et moins") if self.federal_age_category == FederalAgeCategory.A15_16 and user_age > 16: - reasons.append("Ce tournoi est réservé aux -16 ans") + reasons.append("Ce tournoi est réservé aux 16 ans et moins") if self.federal_age_category == FederalAgeCategory.A17_18 and user_age > 18: - reasons.append("Ce tournoi est réservé aux -18 ans") + reasons.append("Ce tournoi est réservé aux 18 ans et moins") if self.federal_age_category == FederalAgeCategory.SENIOR and user_age < 11: reasons.append("Ce tournoi est réservé aux 11 ans et plus") if self.federal_age_category == FederalAgeCategory.A45 and user_age < 45: @@ -1728,26 +1738,29 @@ class Tournament(BaseModel): return "journée" def get_player_registration_status_by_licence(self, user): - licence_id = user.licence_id - if not licence_id: + user_player = self.get_user_registered(user) + if user_player: + return user_player.get_registration_status() + return None + + def get_user_registered(self, user): + if not user.is_authenticated: return None - validator = LicenseValidator(licence_id) - if validator.validate_license(): - stripped_license = validator.stripped_license - # Check if there is a PlayerRegistration for this user in this tournament - PlayerRegistration = apps.get_model('tournaments', 'PlayerRegistration') - user_player = PlayerRegistration.objects.filter( - licence_id__icontains=stripped_license, - team_registration__tournament=self, - ).first() - if user_player: - return user_player.get_registration_status() + PlayerRegistration = apps.get_model('tournaments', 'PlayerRegistration') - return None + # First, try to find a registration directly linked to the user + direct_registration = PlayerRegistration.objects.filter( + team_registration__tournament=self, + user=user, + team_registration__walk_out=False + ).first() - def get_user_registered(self, user): - if not user.is_authenticated or not user.licence_id: + if direct_registration: + return direct_registration + + # If no direct registration found and user has no license, return None + if not user.licence_id: return None # Validate the license format @@ -1757,10 +1770,8 @@ class Tournament(BaseModel): # Get the stripped license (without check letter) stripped_license = validator.stripped_license - PlayerRegistration = apps.get_model('tournaments', 'PlayerRegistration') - # Check if there's a player registration with this license in the tournament - # that hasn't walked out + # Fall back to checking by license ID return PlayerRegistration.objects.filter( team_registration__tournament=self, licence_id__icontains=stripped_license, @@ -1809,6 +1820,14 @@ class Tournament(BaseModel): else: return 0 + def is_free(self): + if self.entry_fee is not None and self.entry_fee == 0: + return True + elif self.entry_fee is None: + return True + else: + return False + def effective_commission_rate(self): """Get the commission rate for this tournament, falling back to the umpire default if not set""" return 1.00 # Fallback default diff --git a/tournaments/models/unregistered_player.py b/tournaments/models/unregistered_player.py index a50c629..4295447 100644 --- a/tournaments/models/unregistered_player.py +++ b/tournaments/models/unregistered_player.py @@ -1,11 +1,12 @@ from django.db import models -from . import UnregisteredTeam +from . import UnregisteredTeam, CustomUser from .player_enums import PlayerPaymentType import uuid class UnregisteredPlayer(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) unregistered_team = models.ForeignKey(UnregisteredTeam, on_delete=models.SET_NULL, related_name='unregistered_players', null=True, blank=True) + user = models.ForeignKey(CustomUser, null=True, blank=True, on_delete=models.SET_NULL, related_name='unregistered_players') first_name = models.CharField(max_length=50, blank=True) last_name = models.CharField(max_length=50, blank=True) licence_id = models.CharField(max_length=50, null=True, blank=True) diff --git a/tournaments/models/unregistered_team.py b/tournaments/models/unregistered_team.py index a5b91e7..7f9c04f 100644 --- a/tournaments/models/unregistered_team.py +++ b/tournaments/models/unregistered_team.py @@ -1,9 +1,10 @@ from django.db import models -from . import Tournament +from . import Tournament, CustomUser import uuid class UnregisteredTeam(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) + user = models.ForeignKey(CustomUser, null=True, blank=True, on_delete=models.SET_NULL, related_name='unregistered_teams') tournament = models.ForeignKey(Tournament, on_delete=models.SET_NULL, related_name='unregistered_teams', null=True, blank=True) unregistration_date = models.DateTimeField(null=True, blank=True) diff --git a/tournaments/services/email_service.py b/tournaments/services/email_service.py index 1bdf904..f3398a4 100644 --- a/tournaments/services/email_service.py +++ b/tournaments/services/email_service.py @@ -1,6 +1,6 @@ from django.core.mail import EmailMessage from enum import Enum -from ..models.enums import RegistrationStatus +from ..models.enums import RegistrationStatus, FederalLevelCategory from ..models.tournament import TeamSortingType from django.utils import timezone @@ -18,7 +18,7 @@ class TeamEmailType(Enum): UNEXPECTED_OUT_OF_TOURNAMENT = 'unexpected_out_of_tournament' REQUIRES_TIME_CONFIRMATION = 'requires_time_confirmation' - def email_subject(self, time_to_confirm=None) -> str: + def email_topic(self, category=None, time_to_confirm=None) -> str: confirmation_types = [ self.REGISTERED, self.OUT_OF_WAITING_LIST, @@ -26,6 +26,12 @@ class TeamEmailType(Enum): self.OUT_OF_WALKOUT_IS_IN, self.REQUIRES_TIME_CONFIRMATION, ] + word = "Tournoi" + grammar = '' + if category is not None: + federal_category = FederalLevelCategory(category) + word = federal_category.localized_word().capitalize() + grammar = 'e' if federal_category.is_feminine_word() else '' if time_to_confirm and self in confirmation_types: return "Participation en attente de confirmation" @@ -36,7 +42,7 @@ class TeamEmailType(Enum): self.UNREGISTERED: "Désistement", self.OUT_OF_WAITING_LIST: "Participation confirmée", self.REQUIRES_TIME_CONFIRMATION: "Participation en attente de confirmation", - self.TOURNAMENT_CANCELED: "Tournoi annulé", + self.TOURNAMENT_CANCELED: f"{word} annulé{grammar}", self.IN_TOURNAMENT_STRUCTURE: "Participation confirmée", self.OUT_OF_TOURNAMENT_STRUCTURE: "Participation annulée", self.OUT_OF_WALKOUT_IS_IN: "Participation confirmée", @@ -81,34 +87,39 @@ class TournamentEmailService: @staticmethod def _build_registration_email_body(tournament, captain, tournament_details_str, other_player, waiting_list): inscription_date = captain.team_registration.local_registration_date().strftime("%d/%m/%Y à %H:%M") + federal_level_category = FederalLevelCategory(tournament.federal_level_category) + tournament_word = federal_level_category.localized_word() + tournament_prefix_at = federal_level_category.localized_prefix_at() + tournament_prefix_of = federal_level_category.localized_prefix_of() + tournament_prefix_that = federal_level_category.localized_prefix_that() + body_parts = [] body_parts.append("Bonjour,\n") if waiting_list: - body_parts.append(f"Votre inscription en liste d'attente du tournoi {tournament_details_str} est confirmée.") + body_parts.append(f"Votre inscription en liste d'attente {tournament_prefix_of}{tournament_word} {tournament_details_str} prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name} est confirmée.") else: - body_parts.append(f"Votre inscription au tournoi {tournament_details_str} est confirmée.") + body_parts.append(f"Votre inscription {tournament_prefix_at}{tournament_word} {tournament_details_str} prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name} est confirmée.") if tournament.team_sorting == TeamSortingType.RANK: - cloture_date = tournament.local_registration_federal_limit().strftime("%d/%m/%Y à %H:%M") + cloture_date = tournament.local_registration_federal_limit() loc = "" if cloture_date is not None: - loc = f", prévu le {cloture_date}" + loc = f", prévu le {cloture_date.strftime("%d/%m/%Y à %H:%M")}" body_parts.append(f"Attention, la sélection définitive se fera par poids d'équipe à la clôture des inscriptions{loc}.") absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info" - link_text = "informations sur le tournoi" + link_text = f"informations sur {tournament_prefix_that}{tournament_word}" absolute_url = f'{link_text}' body_parts.extend([ f"\nDate d'inscription: {inscription_date}", f"\nÉquipe inscrite: {captain.name()} et {other_player.name()}", - f"\nLe tournoi commencera le {tournament.formatted_start_date()} au club {tournament.event.club.name}", f"\nVoir les {absolute_url}" ]) # Add payment information if applicable - if tournament.should_request_payment: + if tournament.should_request_payment(): payment_info = TournamentEmailService._build_payment_info(tournament, captain.team_registration) body_parts.append(payment_info) @@ -123,9 +134,14 @@ class TournamentEmailService: @staticmethod def _build_unregistration_email_body(tournament, captain, tournament_details_str, other_player): + federal_level_category = FederalLevelCategory(tournament.federal_level_category) + tournament_word = federal_level_category.localized_word() + tournament_prefix_at = federal_level_category.localized_prefix_at() + tournament_prefix_that = federal_level_category.localized_prefix_that() + body_parts = [ "Bonjour,\n\n", - f"Votre inscription au tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name} a été annulée" + f"Votre inscription {tournament_prefix_at}{tournament_word} {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name} a été annulée" ] if other_player is not None: @@ -134,7 +150,7 @@ class TournamentEmailService: ) absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info" - link_text = "informations sur le tournoi" + link_text = f"informations sur {tournament_prefix_that}{tournament_word}" absolute_url = f'{link_text}' body_parts.append( @@ -152,13 +168,17 @@ class TournamentEmailService: @staticmethod def _build_out_of_waiting_list_email_body(tournament, captain, tournament_details_str, other_player): + federal_level_category = FederalLevelCategory(tournament.federal_level_category) + tournament_word = federal_level_category.localized_word() + tournament_prefix_at = federal_level_category.localized_prefix_at() + body_parts = [ "Bonjour,\n\n", - f"Suite au désistement d'une paire, vous êtes maintenant inscrit au tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name}" + f"Suite au désistement d'une paire, vous êtes maintenant inscrit {tournament_prefix_at}{tournament_word} {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name}" ] absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info" - link_text = "accéder au tournoi" + link_text = f"accéder {tournament_prefix_at}{tournament_word}" absolute_url = f'{link_text}' if other_player is not None: @@ -178,13 +198,17 @@ class TournamentEmailService: @staticmethod def _build_requires_confirmation_email_body(tournament, captain, tournament_details_str, other_player): + federal_level_category = FederalLevelCategory(tournament.federal_level_category) + tournament_word = federal_level_category.localized_word() + tournament_prefix_at = federal_level_category.localized_prefix_at() + body_parts = [ "Bonjour,\n\n", - f"Vous n'avez toujours pas confirmé votre participation au tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name}" + f"Vous n'avez toujours pas confirmé votre participation {tournament_prefix_at}{tournament_word} {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name}" ] absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info" - link_text = "accéder au tournoi" + link_text = f"accéder {tournament_prefix_at}{tournament_word}" absolute_url = f'{link_text}' if other_player is not None: @@ -204,9 +228,13 @@ class TournamentEmailService: @staticmethod def _build_tournament_cancellation_email_body(tournament, player, tournament_details_str, other_player): + federal_level_category = FederalLevelCategory(tournament.federal_level_category) + tournament_word = federal_level_category.localized_word() + tournament_prefix_that = federal_level_category.localized_prefix_that() + body_parts = [ "Bonjour,\n\n", - f"Le tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name} a été annulé par le juge-arbitre." + f"{tournament_prefix_that.capitalize()}{tournament_word} {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name} a été annulé par le juge-arbitre." ] if other_player is not None: @@ -224,13 +252,18 @@ class TournamentEmailService: @staticmethod def _build_in_tournament_email_body(tournament, captain, tournament_details_str, other_player): + federal_level_category = FederalLevelCategory(tournament.federal_level_category) + tournament_word = federal_level_category.localized_word() + tournament_prefix_at = federal_level_category.localized_prefix_at() + tournament_prefix_of = federal_level_category.localized_prefix_of() + body_parts = [ "Bonjour,\n\n", - f"Suite à une modification de la taille du tournoi, vous pouvez participer au tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name}" + f"Suite à une modification de la taille {tournament_prefix_of}{tournament_word}, vous pouvez participer {tournament_prefix_at}{tournament_word} {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name}" ] absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info" - link_text = "accéder au tournoi" + link_text = f"accéder {tournament_prefix_at}{tournament_word}" absolute_url = f'{link_text}' if other_player is not None: @@ -250,13 +283,19 @@ class TournamentEmailService: @staticmethod def _build_out_of_tournament_email_body(tournament, captain, tournament_details_str, other_player): + federal_level_category = FederalLevelCategory(tournament.federal_level_category) + tournament_word = federal_level_category.localized_word() + tournament_prefix_at = federal_level_category.localized_prefix_at() + tournament_prefix_of = federal_level_category.localized_prefix_of() + tournament_prefix_that = federal_level_category.localized_prefix_that() + body_parts = [ "Bonjour,\n\n", - f"Suite à une modification de la taille du tournoi, vous avez été placé en liste d'attente. Votre participation au tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name} n'est plus confirmée." + f"Suite à une modification de la taille {tournament_prefix_of}{tournament_word}, vous avez été placé en liste d'attente. Votre participation {tournament_prefix_at}{tournament_word} {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name} n'est plus confirmée." ] absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info" - link_text = "informations sur le tournoi" + link_text = f"informations sur {tournament_prefix_that}{tournament_word}" absolute_url = f'{link_text}' body_parts.append(f"\n\nVoir les {absolute_url}") @@ -276,13 +315,17 @@ class TournamentEmailService: @staticmethod def _build_walk_out_email_body(tournament, captain, tournament_details_str, other_player): + federal_level_category = FederalLevelCategory(tournament.federal_level_category) + tournament_word = federal_level_category.localized_word() + tournament_prefix_at = federal_level_category.localized_prefix_at() + body_parts = [ "Bonjour,\n\n", - f"Le juge-arbitre a annulé votre participation au tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name}" + f"Le juge-arbitre a annulé votre participation {tournament_prefix_at}{tournament_word} {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name}" ] absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info" - link_text = "accéder au tournoi" + link_text = f"accéder {tournament_prefix_at}{tournament_word}" absolute_url = f'{link_text}' body_parts.append(f"\n\nVoir les {absolute_url}") @@ -303,13 +346,18 @@ class TournamentEmailService: @staticmethod def _build_out_of_walkout_is_in_email_body(tournament, captain, tournament_details_str, other_player): + federal_level_category = FederalLevelCategory(tournament.federal_level_category) + tournament_word = federal_level_category.localized_word() + tournament_prefix_at = federal_level_category.localized_prefix_at() + tournament_prefix_that = federal_level_category.localized_prefix_that() + body_parts = [ "Bonjour,\n\n", - f"Le juge-arbitre vous a ré-intégré au tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name}" + f"Le juge-arbitre vous a ré-intégré {tournament_prefix_at}{tournament_word} {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name}" ] absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info" - link_text = "informations sur le tournoi" + link_text = f"informations sur {tournament_prefix_that}{tournament_word}" absolute_url = f'{link_text}' body_parts.append(f"\n\nVoir les {absolute_url}") @@ -332,13 +380,18 @@ class TournamentEmailService: @staticmethod def _build_unexpected_out_of_tournament_email_body(tournament, captain, tournament_details_str, other_player): + federal_level_category = FederalLevelCategory(tournament.federal_level_category) + tournament_word = federal_level_category.localized_word() + tournament_prefix_at = federal_level_category.localized_prefix_at() + tournament_prefix_that = federal_level_category.localized_prefix_that() + body_parts = [ "Bonjour,\n\n", - f"En raison d'une décision du juge-arbitre, vous avez été placé en liste d'attente. Votre participation au tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name} n'est plus confirmée." + f"En raison d'une décision du juge-arbitre, vous avez été placé en liste d'attente. Votre participation {tournament_prefix_at}{tournament_word} {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name} n'est plus confirmée." ] absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info" - link_text = "informations sur le tournoi" + link_text = f"informations sur {tournament_prefix_that}{tournament_word}" absolute_url = f'{link_text}' body_parts.append(f"\n\nVoir les {absolute_url}") @@ -358,17 +411,22 @@ class TournamentEmailService: @staticmethod def _build_out_of_walkout_waiting_list_email_body(tournament, captain, tournament_details_str, other_player): + federal_level_category = FederalLevelCategory(tournament.federal_level_category) + tournament_word = federal_level_category.localized_word() + tournament_prefix_of = federal_level_category.localized_prefix_of() + tournament_prefix_that = federal_level_category.localized_prefix_that() + body_parts = [ "Bonjour,\n\n", ] if captain.registration_status == RegistrationStatus.CANCELED: body_parts.append("Le temps accordé pour confirmer votre inscription s'est écoulé.") - body_parts.append(f"Vous avez été replacé en liste d'attente du tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name}") + body_parts.append(f"Vous avez été replacé en liste d'attente {tournament_prefix_of}{tournament_word} {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name}") else: - body_parts.append(f"Le juge-arbitre vous a placé en liste d'attente du tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name}") + body_parts.append(f"Le juge-arbitre vous a placé en liste d'attente {tournament_prefix_of}{tournament_word} {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name}") absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info" - link_text = "informations sur le tournoi" + link_text = f"informations sur {tournament_prefix_that}{tournament_word}" absolute_url = f'{link_text}' body_parts.append(f"\n\nVoir les {absolute_url}") @@ -424,20 +482,25 @@ class TournamentEmailService: account_info = "\nVous devez avoir un compte Padel Club." url_info = f"\n{absolute_url}" + federal_level_category = FederalLevelCategory(tournament.federal_level_category) + tournament_word = federal_level_category.localized_word() + tournament_prefix_at = federal_level_category.localized_prefix_at() + tournament_prefix_this = federal_level_category.localized_prefix_this() + # Base message varies based on whether confirmation is needed if time_to_confirm is not None: # Format the deadline time with proper timezone deadline_str = time_to_confirm.astimezone(tournament.timezone()).strftime("%d/%m/%Y à %H:%M (%Z)") # Confirmation required message - action_text = f"Pour confirmer votre participation au tournoi, cliquez sur ce lien pour {url_info} ou contactez rapidement le juge-arbitre." + action_text = f"Pour confirmer votre participation {tournament_prefix_at}{tournament_word}, cliquez sur ce lien pour {url_info} ou contactez rapidement le juge-arbitre." warning_text = f"⚠️ ATTENTION : Vous avez jusqu'au {deadline_str} pour confirmer votre participation. Passé ce délai, votre place sera automatiquement proposée à l'équipe suivante sur liste d'attente.\n\n" elif captain.registration_status == RegistrationStatus.PENDING: - action_text = f"Pour confirmer votre participation au tournoi, cliquez sur ce lien pour {url_info} ou contactez rapidement le juge-arbitre." - warning_text = "⚠️ ATTENTION : Actuellement, il n'y a pas de liste d'attente pour ce tournoi. Dès qu'une liste d'attente se formera, vous recevrez un email avec un délai précis pour confirmer votre participation.\n\n" + action_text = f"Pour confirmer votre participation {tournament_prefix_at}{tournament_word}, cliquez sur ce lien pour {url_info} ou contactez rapidement le juge-arbitre." + warning_text = f"⚠️ ATTENTION : Actuellement, il n'y a pas de liste d'attente pour {tournament_prefix_this}{tournament_word}. Dès qu'une liste d'attente se formera, vous recevrez un email avec un délai précis pour confirmer votre participation.\n\n" else: # Standard message for teams already confirmed - action_text = f"Si vous n'êtes plus disponible pour participer à ce tournoi, cliquez sur ce lien pour {url_info} ou contactez rapidement le juge-arbitre." + action_text = f"Si vous n'êtes plus disponible pour participer à {tournament_prefix_this}{tournament_word}, cliquez sur ce lien pour {url_info} ou contactez rapidement le juge-arbitre." warning_text = "" # Construct the complete message @@ -458,7 +521,7 @@ class TournamentEmailService: if email_body is None: return - topic = message_type.email_subject(captain.time_to_confirm) + topic = message_type.email_topic(tournament.federal_level_category, captain.time_to_confirm) email_subject = TournamentEmailService.email_subject(tournament, topic) TournamentEmailService._send_email(captain.email, email_subject, email_body) @@ -536,7 +599,8 @@ class TournamentEmailService: print("TournamentEmailService.notify_team 2p", team) first_player, second_player = players TournamentEmailService.notify(first_player, second_player, tournament, message_type) - TournamentEmailService.notify(second_player, first_player, tournament, message_type) + if first_player.email != second_player.email: + TournamentEmailService.notify(second_player, first_player, tournament, message_type) elif len(players) == 1: print("TournamentEmailService.notify_team 1p", team) # If there's only one player, just send them the notification @@ -547,7 +611,10 @@ class TournamentEmailService: """ Build payment information section for emails """ - if not tournament.should_request_payment: + if not tournament.should_request_payment(): + return "" + + if tournament.is_free(): return "" # Check payment status @@ -591,6 +658,10 @@ class TournamentEmailService: if payment_amount is None: payment_amount = tournament.team_fee() + federal_level_category = FederalLevelCategory(tournament.federal_level_category) + tournament_word = federal_level_category.localized_word() + tournament_prefix_that = federal_level_category.localized_prefix_that() + for player in player_registrations: if not player.email or not player.registered_online: continue @@ -600,7 +671,7 @@ class TournamentEmailService: body_parts = [ "Bonjour,\n\n", - f"Votre paiement pour le tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name} a été reçu avec succès." + f"Votre paiement pour {tournament_prefix_that}{tournament_word} {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name} a été reçu avec succès." ] # Add information about the other player if available @@ -620,7 +691,7 @@ class TournamentEmailService: ) absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info" - link_text = "informations sur le tournoi" + link_text = f"informations sur {tournament_prefix_that}{tournament_word}" absolute_url = f'{link_text}' body_parts.append(f"\n\nVoir les {absolute_url}") @@ -662,16 +733,23 @@ class TournamentEmailService: if refund_amount is None: refund_amount = tournament.team_fee() + federal_level_category = FederalLevelCategory(tournament.federal_level_category) + tournament_word = federal_level_category.localized_word() + tournament_prefix_that = federal_level_category.localized_prefix_that() + processed_emails = set() for player in player_registrations: if not player.email or not player.registered_online: continue + if player.email in processed_emails: + continue + processed_emails.add(player.email) tournament_details_str = tournament.build_tournament_details_str() other_player = team_registration.get_other_player(player) if len(player_registrations) > 1 else None body_parts = [ "Bonjour,\n\n", - f"Votre remboursement pour le tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name} a été traité avec succès." + f"Votre remboursement pour {tournament_prefix_that}{tournament_word} {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name} a été traité avec succès." ] # Add information about the other player if available @@ -691,7 +769,7 @@ class TournamentEmailService: ) absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info" - link_text = "informations sur le tournoi" + link_text = f"informations sur {tournament_prefix_that}{tournament_word}" absolute_url = f'{link_text}' body_parts.append(f"\n\nVoir les {absolute_url}") diff --git a/tournaments/services/tournament_registration.py b/tournaments/services/tournament_registration.py index e8fba29..d36c0c7 100644 --- a/tournaments/services/tournament_registration.py +++ b/tournaments/services/tournament_registration.py @@ -208,8 +208,7 @@ class RegistrationCartManager: elif not tournament.license_is_required: # License not required, check if name is provided if not first_name or not last_name: - self.first_tournament = True - return False, "Le prénom et le nom sont obligatoires." + return False, "Le prénom et le nom sont obligatoires." else: # License is required but not provided return False, "Le numéro de licence est obligatoire." @@ -335,6 +334,11 @@ class RegistrationCartManager: return False, f"Vous avez besoin d'au moins {tournament.minimum_player_per_team} joueurs pour vous inscrire." # Identify captain from user's license + # # Update user phone if provided + if self.request.user.is_authenticated and mobile_number: + self.request.user.phone = mobile_number + self.request.user.save(update_fields=['phone']) + stripped_license = None if self.request.user.is_authenticated and self.request.user.licence_id: validator = LicenseValidator(self.request.user.licence_id) @@ -348,6 +352,7 @@ class RegistrationCartManager: registration_date=timezone.now(), walk_out=False, weight=weight, + user=self.request.user ) for player_data in players: # Compute rank and sex using the original logic @@ -364,17 +369,20 @@ class RegistrationCartManager: data_source = PlayerDataSource.FRENCH_FEDERATION # Now using the enum value User = get_user_model() - matching_user = None - if player_licence_id: + matching_user = self.request.user + if player_licence_id and (stripped_license is None or is_captain is False): try: # Using icontains for case-insensitive match matching_user = User.objects.get(licence_id__icontains=player_licence_id) + if matching_user is None: + matching_user = self.request.user except User.DoesNotExist: pass # Create player registration with all the original fields PlayerRegistration.objects.create( team_registration=team_registration, + user=matching_user, captain=is_captain, source=data_source, registered_online=True, @@ -395,11 +403,6 @@ class RegistrationCartManager: registration_status=RegistrationStatus.CONFIRMED if self.session.get('waiting_list_position', 0) < 0 else RegistrationStatus.WAITING ) - # Update user phone if provided - if self.request.user.is_authenticated and mobile_number: - self.request.user.phone = mobile_number - self.request.user.save(update_fields=['phone']) - # Clear the cart self.clear_cart() tournament.reserved_spots = max(0, tournament.reserved_spots - 1) diff --git a/tournaments/services/tournament_unregistration.py b/tournaments/services/tournament_unregistration.py index c22b927..f143aee 100644 --- a/tournaments/services/tournament_unregistration.py +++ b/tournaments/services/tournament_unregistration.py @@ -18,10 +18,10 @@ class TournamentUnregistrationService: messages.error(self.request, "Le désistement n'est plus possible pour ce tournoi. Si vous souhaitez vous désinscrire, veuillez contacter le juge-arbitre.") return False - if not self.request.user.licence_id: - messages.error(self.request, - "Vous ne pouvez pas vous désinscrire car vous n'avez pas de numéro de licence associé.") - return False + # if not self.request.user.licence_id: + # messages.error(self.request, + # "Vous ne pouvez pas vous désinscrire car vous n'avez pas de numéro de licence associé.") + # return False return True @@ -80,12 +80,14 @@ class TournamentUnregistrationService: unregistered_team = UnregisteredTeam.objects.create( tournament=team_registration.tournament, + user=team_registration.user, unregistration_date=timezone.now(), ) for player in team_registration.players_sorted_by_rank: UnregisteredPlayer.objects.create( unregistered_team=unregistered_team, + user=player.user, first_name=player.first_name, last_name=player.last_name, licence_id=player.licence_id, @@ -95,23 +97,14 @@ class TournamentUnregistrationService: ) def _find_player_registration(self): - if not self.request.user.licence_id: - return False - validator = LicenseValidator(self.request.user.licence_id) - is_license_valid = validator.validate_license() + # First check if we can find the player registration directly by user + if self.request.user.is_authenticated: + self.player_registration = self.tournament.get_user_registered(self.request.user) - if not is_license_valid: - return False - - self.player_registration = PlayerRegistration.objects.filter( - licence_id__icontains=validator.stripped_license, - team_registration__tournament_id=self.tournament.id, - ).first() - - if self.player_registration: - self.team_registration = self.player_registration.team_registration - self.other_player = self.team_registration.get_other_player(self.player_registration) - return True + if self.player_registration: + self.team_registration = self.player_registration.team_registration + self.other_player = self.team_registration.get_other_player(self.player_registration) + return True return False def _delete_registered_team(self): diff --git a/tournaments/templates/tournaments/tournament_info.html b/tournaments/templates/tournaments/tournament_info.html index 90abf3b..d190335 100644 --- a/tournaments/templates/tournaments/tournament_info.html +++ b/tournaments/templates/tournaments/tournament_info.html @@ -148,7 +148,7 @@ {% endif %}
-

{{ tournament.display_name }} {{ tournament.get_federal_age_category_display}}

+

{{ tournament.full_name }}

{{ tournament.local_start_date_formatted }}
{{ tournament.day_duration_formatted }}
diff --git a/tournaments/templates/tournaments/tournament_row.html b/tournaments/templates/tournaments/tournament_row.html index 04d017b..d86b27a 100644 --- a/tournaments/templates/tournaments/tournament_row.html +++ b/tournaments/templates/tournaments/tournament_row.html @@ -8,7 +8,7 @@
{{ tournament.month }}
-
{{ tournament.level }}
+
{{ tournament.short_level }}
{% if tournament.category %}
{{ tournament.category }}
{% endif %} @@ -21,6 +21,9 @@ {% if tournament.event.name %}
{{ tournament.event.name }}
{% endif %} + {% if tournament.name %} +
{{ tournament.name }}
+ {% endif %}
{{ tournament.localized_day_duration }}
diff --git a/tournaments/views.py b/tournaments/views.py index fd7759b..41eaecd 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -172,28 +172,19 @@ def tournament_info(request, tournament_id): team_registration = None is_captain = False player_register_check = None - storage = messages.get_messages(request) - for _ in storage: - pass - - if len(storage._loaded_messages) == 1: - del storage._loaded_messages[0] + if request.method == 'POST': + storage = messages.get_messages(request) + for _ in storage: + pass + if len(storage._loaded_messages) == 1: + del storage._loaded_messages[0] if request.user.is_authenticated: # Assuming user's licence_id is stored in the user profile (e.g., request.user.licence_id) user_licence_id = request.user.licence_id player_register_check = tournament.player_register_check(user_licence_id) - if user_licence_id is not None and player_register_check is None: - validator = LicenseValidator(user_licence_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__icontains=stripped_license, - team_registration__tournament=tournament, - team_registration__walk_out=False, - ).first() - + registered_user = tournament.get_user_registered(request.user) # If the user is registered, retrieve their team registration if registered_user: is_captain = registered_user.captain @@ -774,63 +765,21 @@ def custom_password_change(request): @login_required def my_tournaments(request): - user = request.user - user_licence_id = request.user.licence_id - - # If no licence_id, return empty lists - if user_licence_id is None: - return render(request, 'registration/my_tournaments.html', { - 'upcoming_tournaments': [], - 'running_tournaments': [], - 'ended_tournaments': [], - 'user_name': user.username - }) - - # Get all tournaments for the user based on their licence - validator = LicenseValidator(user_licence_id) - stripped_license = validator.stripped_license - - def filter_user_tournaments(tournaments): - return [t for t in tournaments if t.team_registrations.filter( - player_registrations__licence_id__icontains=stripped_license, - walk_out=False - ).exists()] - # Get filtered tournaments using the helper function - upcoming_tournaments = filter_user_tournaments(future_tournaments(None)) - running_tournaments = filter_user_tournaments(live_tournaments(None)) - ended_tournaments = filter_user_tournaments([t for t in finished_tournaments(None) if not t.is_canceled()]) + upcoming_tournaments = get_user_tournaments(request.user, future_tournaments(None)) + running_tournaments = get_user_tournaments(request.user, live_tournaments(None)) + ended_tournaments = get_user_tournaments(request.user, [t for t in finished_tournaments(None) if not t.is_canceled()]) return render(request, 'registration/my_tournaments.html', { 'upcoming_tournaments': upcoming_tournaments, 'running_tournaments': running_tournaments, 'ended_tournaments': ended_tournaments[:12], - 'user_name': user.username + 'user_name': request.user.username }) @login_required def all_my_ended_tournaments(request): - user_licence_id = request.user.licence_id - - # If no licence_id, return empty lists - if user_licence_id is None: - return render(request, '/tournaments.html', { - 'tournaments': [], - 'title': "Palmarès", - }) - - # Get all tournaments for the user based on their licence - validator = LicenseValidator(user_licence_id) - stripped_license = validator.stripped_license - - def filter_user_tournaments(tournaments): - return [t for t in tournaments if t.team_registrations.filter( - player_registrations__licence_id__icontains=stripped_license, - walk_out=False - ).exists()] - - ended_tournaments = filter_user_tournaments(finished_tournaments(None)) - + ended_tournaments = get_user_tournaments(request.user, finished_tournaments(None)) return render(request, "tournaments/tournaments_list.html", { @@ -1072,33 +1021,21 @@ def custom_logout(request): def confirm_tournament_registration(request, tournament_id): if request.method == 'POST': tournament = get_object_or_404(Tournament, pk=tournament_id) - # Find the team registration for this user - user_licence_id = request.user.licence_id - if user_licence_id is not None: - validator = LicenseValidator(user_licence_id) - stripped_license = validator.stripped_license - # Check if there is a PlayerRegistration for this user in this tournament - team_registrations = TeamRegistration.objects.filter( - tournament=tournament, - player_registrations__licence_id__icontains=stripped_license, - ) - - if not team_registrations.exists(): - messages.error(request, "Aucune inscription trouvée pour ce tournoi.") - return redirect('tournament-info', tournament_id=tournament_id) - - team_registration = team_registrations.first() + team_registration = tournament.get_user_team_registration(request.user) + if team_registration is None: + messages.error(request, "Aucune inscription trouvée pour ce tournoi.") + return redirect('tournament-info', tournament_id=tournament_id) - if team_registration.is_confirmation_expired(): - team_registration.check_confirmation_deadline() - messages.error(request, "Le délai de confirmation est expiré. Vous ne pouvez plus confirmer votre participation.") - return redirect('tournament-info', tournament_id=tournament_id) + if team_registration.is_confirmation_expired(): + team_registration.check_confirmation_deadline() + messages.error(request, "Le délai de confirmation est expiré. Vous ne pouvez plus confirmer votre participation.") + return redirect('tournament-info', tournament_id=tournament_id) - # Confirm registration - team_registration.confirm_registration() + # Confirm registration + team_registration.confirm_registration() - messages.success(request, "Votre participation est confirmée !") - return redirect('tournament-info', tournament_id=tournament_id) + messages.success(request, "Votre participation est confirmée !") + return redirect('tournament-info', tournament_id=tournament_id) return redirect('tournament-info', tournament_id=tournament_id) @@ -1563,6 +1500,45 @@ def xls_to_csv(request): else: return HttpResponse("No file was uploaded", status=400) +def get_user_tournaments(user, tournaments): + user_tournaments = [] + if user.is_authenticated is False: + return user_tournaments + + user_licence_id = user.licence_id + stripped_license = None + # If no licence_id, return empty lists + if user_licence_id is not None: + # Get all tournaments for the user based on their licence + validator = LicenseValidator(user_licence_id) + stripped_license = validator.stripped_license + + for t in tournaments: + # First check for direct user relationship + direct_registration_exists = t.team_registrations.filter( + player_registrations__user=user, + walk_out=False + ).exists() + + # If direct relationship exists, add the tournament + if direct_registration_exists: + user_tournaments.append(t) + continue + + # Otherwise, check by license + license_registration_exists = None + if stripped_license: + license_registration_exists = t.team_registrations.filter( + player_registrations__licence_id__icontains=stripped_license, + walk_out=False + ).exists() + + if license_registration_exists: + user_tournaments.append(t) + + return user_tournaments + + class UserListExportView(LoginRequiredMixin, View): def get(self, request, *args, **kwargs): users = CustomUser.objects.order_by('date_joined')