diff --git a/padelclub_backend/settings.py b/padelclub_backend/settings.py index e2ef565..b6c8262 100644 --- a/padelclub_backend/settings.py +++ b/padelclub_backend/settings.py @@ -64,6 +64,7 @@ MIDDLEWARE = [ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'tournaments.middleware.ReferrerMiddleware', # Add this line + 'tournaments.middleware.RegistrationCartCleanupMiddleware', ] diff --git a/tournaments/middleware.py b/tournaments/middleware.py index ad517b2..ad4b5d2 100644 --- a/tournaments/middleware.py +++ b/tournaments/middleware.py @@ -1,5 +1,6 @@ -from django.conf import settings -from django.urls import resolve, reverse +from django.urls import reverse +from django.utils import timezone +import datetime class ReferrerMiddleware: def __init__(self, get_response): @@ -17,3 +18,36 @@ class ReferrerMiddleware: response = self.get_response(request) return response + +class RegistrationCartCleanupMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + self._check_and_clean_expired_cart(request) + response = self.get_response(request) + return response + + def _check_and_clean_expired_cart(self, request): + if 'registration_cart_expiry' in request.session: + try: + expiry_str = request.session['registration_cart_expiry'] + expiry = datetime.datetime.fromisoformat(expiry_str) + if timezone.now() > expiry: + # Clear expired cart + keys_to_delete = [ + 'registration_cart_id', + 'registration_tournament_id', + 'registration_cart_players', + 'registration_cart_expiry', + 'registration_mobile_number' + ] + for key in keys_to_delete: + if key in request.session: + del request.session[key] + request.session.modified = True + except (ValueError, TypeError): + # Invalid expiry format, clear it + if 'registration_cart_expiry' in request.session: + del request.session['registration_cart_expiry'] + request.session.modified = True diff --git a/tournaments/migrations/0114_playerregistration_payment_status_and_more.py b/tournaments/migrations/0116_playerregistration_payment_status_and_more.py similarity index 67% rename from tournaments/migrations/0114_playerregistration_payment_status_and_more.py rename to tournaments/migrations/0116_playerregistration_payment_status_and_more.py index 92fcaf2..4593c49 100644 --- a/tournaments/migrations/0114_playerregistration_payment_status_and_more.py +++ b/tournaments/migrations/0116_playerregistration_payment_status_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1 on 2025-03-29 14:28 +# Generated by Django 5.1 on 2025-04-08 08:43 from django.db import migrations, models @@ -6,7 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('tournaments', '0113_tournament_team_count_limit'), + ('tournaments', '0115_auto_20250403_1503'), ] operations = [ @@ -25,4 +25,9 @@ class Migration(migrations.Migration): name='time_to_confirm', field=models.DateTimeField(blank=True, null=True), ), + migrations.AlterField( + model_name='tournament', + name='federal_level_category', + field=models.IntegerField(choices=[(0, 'Animation'), (25, 'P25'), (100, 'P100'), (250, 'P250'), (500, 'P500'), (1000, 'P1000'), (1500, 'P1500'), (2000, 'P2000'), (1, 'Championnat')], default=100), + ), ] diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index 5d63190..e58ed9c 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -1674,6 +1674,31 @@ class Tournament(BaseModel): return None + def is_user_registered(self, user): + """ + Check if a user is already registered for this tournament. + Returns True if the user is registered, False otherwise. + """ + if not user.is_authenticated or not user.licence_id: + return False + + # Validate the license format + validator = LicenseValidator(user.licence_id) + if not validator.validate_license(): + return False + + # 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 + return PlayerRegistration.objects.filter( + team_registration__tournament=self, + licence_id__icontains=stripped_license, + team_registration__walk_out=False + ).exists() + class MatchGroup: def __init__(self, name, matches, formatted_schedule, round_id=None): diff --git a/tournaments/services/registration_cart.py b/tournaments/services/registration_cart.py new file mode 100644 index 0000000..ab67176 --- /dev/null +++ b/tournaments/services/registration_cart.py @@ -0,0 +1,346 @@ +from django.utils import timezone +import uuid +import datetime +from ..models import PlayerRegistration, TeamRegistration, Tournament +from ..utils.licence_validator import LicenseValidator +from ..utils.player_search import get_player_name_from_csv + +def get_or_create_registration_cart_id(request): + """Get or create a registration cart ID in the session""" + if 'registration_cart_id' not in request.session: + request.session['registration_cart_id'] = str(uuid.uuid4()) + return request.session['registration_cart_id'] + +def get_cart_expiry(request): + """Get the cart expiry time from the session""" + if 'registration_cart_expiry' not in request.session: + # Set default expiry to 30 minutes from now + expiry = timezone.now() + datetime.timedelta(minutes=30) + request.session['registration_cart_expiry'] = expiry.isoformat() + return request.session['registration_cart_expiry'] + +def is_cart_expired(request): + """Check if the registration cart is expired""" + if 'registration_cart_expiry' not in request.session: + return False + + expiry_str = request.session['registration_cart_expiry'] + try: + expiry = datetime.datetime.fromisoformat(expiry_str) + return timezone.now() > expiry + except (ValueError, TypeError): + return True + +def reset_cart_expiry(request): + """Reset the cart expiry time""" + expiry = timezone.now() + datetime.timedelta(minutes=30) + request.session['registration_cart_expiry'] = expiry.isoformat() + request.session.modified = True + +def get_tournament_from_cart(request): + """Get the tournament ID associated with the current cart""" + return request.session.get('registration_tournament_id') + +def initialize_registration_cart(request, tournament_id): + """Initialize a new registration cart for a tournament""" + # Clear any existing cart + clear_registration_cart(request) + + # Set up the new cart + request.session['registration_cart_id'] = str(uuid.uuid4()) + request.session['registration_tournament_id'] = tournament_id + request.session['registration_cart_players'] = [] + reset_cart_expiry(request) + request.session.modified = True + +def get_registration_cart_data(request): + """Get the data for the current registration cart""" + # Ensure cart players array exists + if 'registration_cart_players' not in request.session: + request.session['registration_cart_players'] = [] + request.session.modified = True + + # Ensure tournament ID exists + if 'registration_tournament_id' not in request.session: + # If no tournament ID but we have players, this is an inconsistency + if request.session.get('registration_cart_players'): + print("WARNING: Found players but no tournament ID - clearing players") + request.session['registration_cart_players'] = [] + request.session.modified = True + + cart_data = { + 'cart_id': get_or_create_registration_cart_id(request), + 'tournament_id': request.session.get('registration_tournament_id'), + 'players': request.session.get('registration_cart_players', []), + 'expiry': get_cart_expiry(request), + 'mobile_number': request.session.get('registration_mobile_number', + request.user.phone if hasattr(request.user, 'phone') else '') + } + + # Debug: print the cart content + print(f"Cart data - Tournament ID: {cart_data['tournament_id']}") + print(f"Cart data - Players count: {len(cart_data['players'])}") + + return cart_data + +def add_player_to_cart(request, player_data): + """Add a player to the registration cart""" + if is_cart_expired(request): + return False, "Votre session d'inscription a expiré, veuillez réessayer." + + # Get cart data + tournament_id = request.session.get('registration_tournament_id') + if not tournament_id: + return False, "Pas d'inscription active." + + # Get tournament + try: + tournament = Tournament.objects.get(id=tournament_id) + except Tournament.DoesNotExist: + return False, "Tournoi introuvable." + + # Get existing players directly from session + players = request.session.get('registration_cart_players', []) + + # Debug: Initial players count + print(f"Before adding - Players in session: {len(players)}") + + # Check if we've reached the team limit (usually 2 for padel) + if len(players) >= 2: # Assuming teams of 2 for padel + return False, "Nombre maximum de joueurs déjà ajouté." + + # Process player data + licence_id = player_data.get('licence_id', '').upper() if player_data.get('licence_id') else None + first_name = player_data.get('first_name', '') + last_name = player_data.get('last_name', '').upper() + + # Handle case where user is authenticated, has no license, and license is required + if tournament.license_is_required: + # If license is required but not provided + if not licence_id: + # First player (authentication check) or partner + user_message = "Le numéro de licence est obligatoire." if len(players) == 0 else "Le numéro de licence de votre partenaire est obligatoire." + return False, user_message + + # Validate the license format + validator = LicenseValidator(licence_id) + if not validator.validate_license(): + return False, "Le numéro de licence est invalide, la lettre ne correspond pas." + + # Check if player is already registered in tournament + stripped_license = validator.stripped_license + if _is_player_already_registered(stripped_license, tournament): + return False, "Un joueur avec ce numéro de licence est déjà inscrit dans une équipe." + + # Check if this is the authenticated user trying to register as first player + if request.user.is_authenticated and len(players) == 0 and request.user.licence_id is None: + # Try to update the user's license ID in the database + try: + request.user.licence_id = validator.computed_licence_id + request.user.save() + request.user.refresh_from_db() + except: + return False, "Erreur lors de la mise à jour de votre licence: cette licence est déjà utilisée par un autre joueur." + + # Check for duplicate licenses in cart + existing_licenses = [p.get('licence_id') for p in players if p.get('licence_id')] + if licence_id and licence_id in existing_licenses: + return False, "Ce joueur est déjà dans l'équipe." + + # Process based on whether license ID was provided and tournament rules + if licence_id: + # Get federation data + fed_data, found = get_player_name_from_csv(tournament.federal_category, licence_id) + if found and fed_data: + # Use federation data (including check for eligibility) + player_register_check = tournament.player_register_check(licence_id) + if player_register_check: + return False, ", ".join(player_register_check) + + # Update player data from federation data + player_data.update({ + 'first_name': fed_data['first_name'], + 'last_name': fed_data['last_name'], + 'rank': fed_data['rank'], + 'is_woman': fed_data['is_woman'], + 'points': fed_data.get('points'), + 'assimilation': fed_data.get('assimilation'), + 'tournament_count': fed_data.get('tournament_count'), + 'ligue_name': fed_data.get('ligue_name'), + 'club_name': fed_data.get('club_name'), + 'birth_year': fed_data.get('birth_year'), + 'found_in_french_federation': True, + }) + elif tournament.license_is_required: + # License required but not found in federation data + return False, "La licence fournit n'a pas été trouvée dans la base FFT. Contactez le juge arbitre si cette licence est valide." + elif not first_name or not last_name: + # License not required or not found, but name is needed + return False, "Le prénom et le nom sont obligatoires pour les joueurs sans licence." + elif not tournament.license_is_required: + # License not required, check if name is provided + if not first_name or not last_name: + return False, "Le prénom et le nom sont obligatoires pour les joueurs sans licence." + + # Set default rank for players without a license + if player_data.get('rank') is None: + default_data, _ = get_player_name_from_csv(tournament.federal_category, None) + if default_data: + player_data['rank'] = default_data.get('rank') + player_data['is_woman'] = default_data.get('is_woman', False) + else: + # License is required but not provided + return False, "Le numéro de licence est obligatoire." + + # Add player to cart + players.append(player_data) + request.session['registration_cart_players'] = players + reset_cart_expiry(request) + request.session.modified = True + + return True, "Joueur ajouté avec succès." + +def remove_player_from_cart(request): + """Remove the last player from the cart""" + if is_cart_expired(request): + return False, "Votre session d'inscription a expiré, veuillez réessayer." + + players = request.session.get('registration_cart_players', []) + if not players: + return False, "Pas de joueur à supprimer." + + # Remove last player + players.pop() + request.session['registration_cart_players'] = players + reset_cart_expiry(request) + request.session.modified = True + + # If cart is now empty and user is authenticated with license, re-add them automatically + if not players: + add_authenticated_user_to_cart(request) + + return True, "Joueur retiré." + +def update_cart_contact_info(request, mobile_number=None): + """Update contact info for the cart""" + if is_cart_expired(request): + return False, "Votre session d'inscription a expiré, veuillez réessayer." + + if mobile_number is not None: + request.session['registration_mobile_number'] = mobile_number + + reset_cart_expiry(request) + request.session.modified = True + + return True, "Informations de contact mises à jour." + +def checkout_registration_cart(request): + """Convert cart to an actual tournament registration""" + if is_cart_expired(request): + return False, "Votre session d'inscription a expiré, veuillez réessayer." + + # Get cart data + cart_data = get_registration_cart_data(request) + tournament_id = cart_data.get('tournament_id') + players = cart_data.get('players') + mobile_number = cart_data.get('mobile_number') + + # Validate cart data + if not tournament_id: + return False, "Aucun tournoi sélectionné." + + if not players: + return False, "Aucun joueur dans l'inscription." + + # Get tournament + try: + tournament = Tournament.objects.get(id=tournament_id) + except Tournament.DoesNotExist: + return False, "Tournoi introuvable." + + # Check minimum players + if len(players) < tournament.minimum_player_per_team: + return False, f"Vous avez besoin d'au moins {tournament.minimum_player_per_team} joueurs pour vous inscrire." + + # Create team registration + team_registration = TeamRegistration.objects.create( + tournament=tournament, + registration_date=timezone.now(), + walk_out=False + ) + + # Create player registrations + for idx, player_data in enumerate(players): + PlayerRegistration.objects.create( + team_registration=team_registration, + first_name=player_data.get('first_name', ''), + last_name=player_data.get('last_name', '').upper(), + licence_id=player_data.get('licence_id'), + rank=player_data.get('rank'), + points=player_data.get('points'), + club_name=player_data.get('club_name'), + ligue_name=player_data.get('ligue_name'), + email=player_data.get('email'), + phone_number=player_data.get('phone'), + assimilation=player_data.get('assimilation'), + tournament_played=player_data.get('tournament_count'), + birthdate=str(player_data.get('birth_year', '')), + captain=(idx == 0), # First player is captain + registered_online=True, + registration_status='CONFIRMED' if tournament.get_waiting_list_position() < 0 else 'WAITING' + ) + + # Update user phone if provided + if request.user.is_authenticated and mobile_number: + request.user.phone = mobile_number + request.user.save(update_fields=['phone']) + + # Clear the cart + clear_registration_cart(request) + + return True, team_registration + +def clear_registration_cart(request): + """Clear the registration cart""" + keys_to_clear = [ + 'registration_cart_id', + 'registration_tournament_id', + 'registration_cart_players', + 'registration_cart_expiry', + 'registration_mobile_number' + ] + + for key in keys_to_clear: + if key in request.session: + del request.session[key] + + request.session.modified = True + +def _is_player_already_registered(stripped_license, tournament): + """Check if a player is already registered in the tournament""" + return PlayerRegistration.objects.filter( + team_registration__tournament=tournament, + licence_id__icontains=stripped_license, + team_registration__walk_out=False + ).exists() + +def add_authenticated_user_to_cart(request): + """ + Adds the authenticated user to the cart if they have a valid license. + Returns True if added, False otherwise. + """ + if not request.user.is_authenticated or not request.user.licence_id: + return False + + # Create player data for the authenticated user + player_data = { + 'first_name': request.user.first_name, + 'last_name': request.user.last_name, + 'licence_id': request.user.licence_id, + 'email': request.user.email, + 'phone': request.user.phone + } + + # Add the user to the cart + success, _ = add_player_to_cart(request, player_data) + return success diff --git a/tournaments/signals.py b/tournaments/signals.py index b352c49..8144283 100644 --- a/tournaments/signals.py +++ b/tournaments/signals.py @@ -109,7 +109,7 @@ def check_waiting_list(sender, instance, **kwargs): teams_out_to_warn = [] teams_in_to_warn = [] - + previous_state_teams = previous_state.teams(True) if previous_state.team_count > instance.team_count: teams_that_will_be_out = instance.teams(True)[instance.team_count:] teams_out_to_warn = [ @@ -117,7 +117,7 @@ def check_waiting_list(sender, instance, **kwargs): if team.stage != "Attente" ] elif previous_state.team_count < instance.team_count: - teams_that_will_be_in = previous_state.teams(True)[previous_state.team_count:instance.team_count] + teams_that_will_be_in = previous_state_teams[previous_state.team_count:instance.team_count] teams_in_to_warn = [ team for team in teams_that_will_be_in if team.stage == "Attente" diff --git a/tournaments/templates/register_tournament.html b/tournaments/templates/register_tournament.html index 66c4dd7..456ca64 100644 --- a/tournaments/templates/register_tournament.html +++ b/tournaments/templates/register_tournament.html @@ -26,6 +26,11 @@ 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.
{% else %} + {% if not registration_successful %} +Votre session d'inscription est active. Complétez le formulaire pour confirmer votre participation.
+