From 3066f184f6e85bfc52f43418af4d4e3afe783f87 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 15 Nov 2024 07:40:06 +0100 Subject: [PATCH] initial work in progress --- padelclub_backend/settings.py | 4 + tournaments/forms.py | 88 +++++++ tournaments/models/tournament.py | 7 + .../templates/register_tournament.html | 80 ++++++ tournaments/templates/registration/login.html | 30 +++ .../templates/registration/profile.html | 30 +++ .../templates/registration/signup.html | 24 ++ tournaments/templates/tournaments/base.html | 4 + .../tournaments/navigation_base.html | 6 + .../tournaments/tournament_info.html | 11 + tournaments/urls.py | 8 + tournaments/utils/licence_validator.py | 44 ++++ tournaments/utils/player_search.py | 33 +++ tournaments/views.py | 234 +++++++++++++++++- 14 files changed, 602 insertions(+), 1 deletion(-) create mode 100644 tournaments/templates/register_tournament.html create mode 100644 tournaments/templates/registration/login.html create mode 100644 tournaments/templates/registration/profile.html create mode 100644 tournaments/templates/registration/signup.html create mode 100644 tournaments/utils/licence_validator.py create mode 100644 tournaments/utils/player_search.py diff --git a/padelclub_backend/settings.py b/padelclub_backend/settings.py index deff1f2..0fa79f8 100644 --- a/padelclub_backend/settings.py +++ b/padelclub_backend/settings.py @@ -143,3 +143,7 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' from .settings_local import * from .settings_app import * + +# settings.py +LOGIN_REDIRECT_URL = '/' # Redirect to the homepage after login +LOGOUT_REDIRECT_URL = '/' # Redirect to the homepage after logout diff --git a/tournaments/forms.py b/tournaments/forms.py index 0e0473b..49895d9 100644 --- a/tournaments/forms.py +++ b/tournaments/forms.py @@ -1,6 +1,8 @@ from django.contrib.auth.forms import UserCreationForm, UserChangeForm from django import forms from .models import CustomUser +import re # Import the re module for regular expressions +from .utils.licence_validator import LicenseValidator class CustomUserCreationForm(UserCreationForm): @@ -8,6 +10,24 @@ class CustomUserCreationForm(UserCreationForm): model = CustomUser fields = UserCreationForm.Meta.fields + ('umpire_code', 'clubs', 'phone', 'first_name', 'last_name', 'licence_id', 'country') +class SimpleCustomUserCreationForm(UserCreationForm): + + class Meta: + model = CustomUser + fields = UserCreationForm.Meta.fields + ('email', 'phone', 'first_name', 'last_name', 'licence_id', 'country') + labels = { + 'username': 'Nom d’utilisateur', + 'email': 'E-mail', + 'phone': 'Numéro de téléphone', + 'first_name': 'Prénom', + 'last_name': 'Nom de famille', + 'licence_id': 'Numéro de licence', + 'country': 'Pays', + 'password1': 'Mot de passe', + 'password2': 'Confirmer le mot de passe', + } + + class CustomUserChangeForm(UserChangeForm): class Meta: @@ -17,3 +37,71 @@ class CustomUserChangeForm(UserChangeForm): class SimpleForm(forms.Form): # A single field for user input (e.g., a text field) name = forms.CharField(label='Enter your name!', max_length=100) + +class TournamentRegistrationForm(forms.Form): + #first_name = forms.CharField(label='Prénom', max_length=50) + #last_name = forms.CharField(label='Nom', max_length=50) + email = forms.EmailField(label='E-mail') + mobile_number = forms.CharField( + label='Téléphone (facultatif)', + max_length=15, + required=False + ) + + def is_registration_complete(self): + # Check if the minimum number of players (2) is met + return len(self.players) >= 2 + + + def clean_mobile_number(self): + mobile_number = self.cleaned_data.get('mobile_number') + if mobile_number: + # Basic regex for mobile numbers, matching common formats + if not re.match(r"^\+?\d{10,15}$", mobile_number): + raise forms.ValidationError("Entrer un numéro de téléphone valide.") + return mobile_number + +class AddPlayerForm(forms.Form): + licence_id = forms.CharField(label='Numéro de license (avec la lettre)', max_length=20) + first_name = forms.CharField(label='Prénom', min_length=2, max_length=50, required=False) + last_name = forms.CharField(label='Nom', min_length=2, max_length=50, required=False) + first_tournament = False + + def names_is_valid(self): + first_name = self.cleaned_data.get('first_name') + last_name = self.cleaned_data.get('last_name') + return len(first_name) >= 2 and len(last_name) >= 2 + + def clean_licence_id(self): + licence_id = self.cleaned_data.get('licence_id') + + # Convert to uppercase + licence_id = licence_id.upper() + + # Update the cleaned_data with the modified licence_id + self.cleaned_data['licence_id'] = licence_id + + # Optionally, print the cleaned license ID for debugging + print(f"Cleaned Licence ID (inside clean_licence_id): {licence_id}") + + return licence_id + + def clean_last_name(self): + last_name = self.cleaned_data.get('last_name') + + # Convert to uppercase + last_name = last_name.upper() + + # Update the cleaned_data with the modified licence_id + self.cleaned_data['last_name'] = last_name + return last_name + + def clean(self): + cleaned_data = super().clean() + + # Retrieve the cleaned licence_id (from the previous step) + licence_id = cleaned_data.get('licence_id') + last_name = cleaned_data.get('last_name') + + # Return the cleaned data with any modifications applied + return cleaned_data diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index 36627ff..86d319c 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -870,6 +870,13 @@ class Tournament(models.Model): return False return True + def online_register_is_enabled(self): + if self.supposedly_in_progress(): + return False + if self.end_date is not None: + return False + return True + class MatchGroup: def __init__(self, name, matches, formatted_schedule): self.name = name diff --git a/tournaments/templates/register_tournament.html b/tournaments/templates/register_tournament.html new file mode 100644 index 0000000..c1021cb --- /dev/null +++ b/tournaments/templates/register_tournament.html @@ -0,0 +1,80 @@ +{% extends 'tournaments/base.html' %} + +{% block head_title %}{{ tournament.display_name }} : Informations{% endblock %} +{% block first_title %}{{ tournament.event.display_name }}{% endblock %} +{% block second_title %}{{ tournament.display_name }}{% endblock %} + +{% block content %} +{% load static %} +{% load tz %} + + + +
+ +
+

Inscription au {{ tournament.display_name }} {{ tournament.get_federal_age_category_display}}

+ + +
+ + {% if registration_successful %} +

Merci, l'inscription a bien été envoyé au juge-arbitre.

+ + {% else %} +
+ {% csrf_token %} + + +
+ {{ team_form.as_p }} +
+ + + {% if current_players %} +
    + {% for player in current_players %} +
  • {{ player.first_name }} {{ player.last_name }} ({{ player.licence_id }})
  • + {% endfor %} +
+ {% endif %} + + + {% if current_players|length < 2 %} +
+ {{ add_player_form.licence_id.label_tag }} + {{ add_player_form.licence_id }} + {% if add_player_form.first_tournament %} + Précisez les informations du joueur : + {{ add_player_form.first_name.label_tag }} + {{ add_player_form.first_name }} + {{ add_player_form.last_name.label_tag }} + {{ add_player_form.last_name }} + {% endif %} + + +
+ {% endif %} + + + {% if current_players|length >= 2 %} + + {% endif %} +
+ + {% if registration_successful %} +

Registration was successful! Your team and players have been registered.

+ {% endif %} + + + {% for message in messages %} +
{{ message }}
+ {% endfor %} + + {% endif %} +
+
+
+{% endblock %} diff --git a/tournaments/templates/registration/login.html b/tournaments/templates/registration/login.html new file mode 100644 index 0000000..6e4e449 --- /dev/null +++ b/tournaments/templates/registration/login.html @@ -0,0 +1,30 @@ + +{% extends 'tournaments/base.html' %} +{% block head_title %} {% endblock %} +{% block first_title %}{% endblock %} +{% block second_title %}{% endblock %} + +{% block content %} +{% load static %} +{% load tz %} + +
+
+
+ {% csrf_token %} + + + + + + +
+

Pas encore de compte ? Créer le tout de suite !.

+
+
+ +{% for message in messages %} +
{{ message }}
+{% endfor %} + +{% endblock %} diff --git a/tournaments/templates/registration/profile.html b/tournaments/templates/registration/profile.html new file mode 100644 index 0000000..81ef067 --- /dev/null +++ b/tournaments/templates/registration/profile.html @@ -0,0 +1,30 @@ +{% extends 'tournaments/base.html' %} +{% block head_title %} {% endblock %} +{% block first_title %}{% endblock %} +{% block second_title %}{% endblock %} + +{% block content %} + +{% include 'tournaments/navigation_base.html' %} + +{% load static %} +{% load tz %} + +
+ + {% if tournaments %} +
+

Vos tournois, {{ user_name }}

+ +
+ + {% for tournament in tournaments %} + {% include 'tournaments/tournament_row.html' %} + {% endfor %} + +
+
+ {% endif %} + +
+{% endblock %} diff --git a/tournaments/templates/registration/signup.html b/tournaments/templates/registration/signup.html new file mode 100644 index 0000000..6f5d96f --- /dev/null +++ b/tournaments/templates/registration/signup.html @@ -0,0 +1,24 @@ + +{% extends 'tournaments/base.html' %} +{% block first_title %}{% endblock %} +{% block second_title %}{% endblock %} + +{% block content %} +{% load static %} +{% load tz %} + +
+
+
+ {% csrf_token %} + {{ form.as_p }} + +
+
+
+ +{% for message in messages %} +
{{ message }}
+{% endfor %} + +{% endblock %} diff --git a/tournaments/templates/tournaments/base.html b/tournaments/templates/tournaments/base.html index 14cf3de..99a1214 100644 --- a/tournaments/templates/tournaments/base.html +++ b/tournaments/templates/tournaments/base.html @@ -49,7 +49,11 @@ class="logo inline" />
+ {% if user.is_authenticated %} +

Bienvenue sur Padel Club, {{ user.username }}

+ {% else %}

{% block first_title %}Page Title{% endblock %}

+ {% endif %}

{% block second_title %}Page Title{% endblock %}

diff --git a/tournaments/templates/tournaments/navigation_base.html b/tournaments/templates/tournaments/navigation_base.html index fc5eec6..7f12a87 100644 --- a/tournaments/templates/tournaments/navigation_base.html +++ b/tournaments/templates/tournaments/navigation_base.html @@ -2,4 +2,10 @@ diff --git a/tournaments/templates/tournaments/tournament_info.html b/tournaments/templates/tournaments/tournament_info.html index 8ab6c16..3f36fb8 100644 --- a/tournaments/templates/tournaments/tournament_info.html +++ b/tournaments/templates/tournaments/tournament_info.html @@ -38,6 +38,17 @@

{% endif %} + {% if tournament.online_register_is_enabled %} + {% if is_registered %} +

+ Unregister from Tournament +

+ {% else %} +

+ Register for Tournament +

+ {% endif %} + {% endif %} diff --git a/tournaments/urls.py b/tournaments/urls.py index e4b316b..59fc932 100644 --- a/tournaments/urls.py +++ b/tournaments/urls.py @@ -1,3 +1,4 @@ +from django.contrib.auth import views as auth_views from django.urls import include, path from . import views @@ -38,4 +39,11 @@ urlpatterns = [ path('terms-of-use/', views.terms_of_use, name='terms-of-use'), path('utils/xls-to-csv/', views.xls_to_csv, name='xls-to-csv'), path('mail-test/', views.simple_form_view, name='mail-test'), + path('login/', auth_views.LoginView.as_view(), name='login'), + path('logout/', auth_views.LogoutView.as_view(), name='logout'), + path('signup/', views.signup, name='signup'), # URL pattern for signup + path('profile/', views.profile, name='profile'), # URL pattern for signup + path('tournaments//register/', views.register_tournament, name='register_tournament'), + path('tournaments//unregister/', views.unregister_tournament, name='unregister_tournament'), + ] diff --git a/tournaments/utils/licence_validator.py b/tournaments/utils/licence_validator.py new file mode 100644 index 0000000..9b096c9 --- /dev/null +++ b/tournaments/utils/licence_validator.py @@ -0,0 +1,44 @@ +import re + +class LicenseValidator: + def __init__(self, license_id: str): + self.license_id = license_id.upper() # Ensure uppercase for consistency + + @property + def stripped_license(self) -> str: + # Remove leading zero if present and match only the numeric part + license_without_leading_zero = self.license_id.lstrip("0") + match = re.match(r"^[0-9]{6,8}", license_without_leading_zero) + if match: + return match.group(0) + return 'licence invalide' + + @property + def computed_license_key(self) -> str: + stripped = self.stripped_license + if stripped and stripped.isdigit(): + int_value = int(stripped) + value = (int_value - 1) % 23 + char_code = ord('A') + value + + # Adjust for letters to skip: I, O, Q + if char_code >= ord('I'): + char_code += 1 + if char_code >= ord('O'): + char_code += 1 + if char_code >= ord('Q'): + char_code += 1 + + return chr(char_code) + return 'aucune clé de licence' + + def validate_license(self) -> bool: + if not self.license_id or len(self.license_id) < 7: + return False # Invalid length for a license ID + + # Separate the numeric part and the letter + numeric_part = self.license_id[:-1] + given_letter = self.license_id[-1] + + # Verify that the last character matches the computed license key + return self.computed_license_key == given_letter diff --git a/tournaments/utils/player_search.py b/tournaments/utils/player_search.py new file mode 100644 index 0000000..b14d799 --- /dev/null +++ b/tournaments/utils/player_search.py @@ -0,0 +1,33 @@ +import csv +import os +import re + +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 + +def get_player_name_from_csv(licence_id): + # Define the file path + file_path = '/Users/razmig/Documents/XLR Sport/padelclub_backend/tournaments/static/rankings/CLASSEMENT-PADEL-MESSIEURS-11-2024.csv' + cleaned_licence_id = clean_licence_id(licence_id) + print(cleaned_licence_id) + # Open the file and read it + with open(file_path, newline='', encoding='utf-8') as file: + reader = csv.reader(file, delimiter=';') + + # Iterate through each row in the CSV + for row in reader: + # Ensure the row is not empty and has the expected number of columns + if len(row) >= 13: + current_licence_id = row[5] # The 5th column contains the licence_id + + # Check if the current row matches the given licence_id + if current_licence_id == str(cleaned_licence_id): + # Return first name and last name from the row (3rd and 4th columns) + first_name = row[3] # 4th column: first name + last_name = row[2] # 3rd column: last name + return first_name, last_name + + # Return None if no match is found + return None, None diff --git a/tournaments/views.py b/tournaments/views.py index e30e420..4bb728a 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -33,6 +33,23 @@ from .forms import SimpleForm from django.core.mail import EmailMessage from datetime import timedelta from django.utils import timezone +from django.shortcuts import render, redirect +from django.contrib.auth.forms import UserCreationForm +from django.contrib.auth import login + +from django.urls import reverse +from django.contrib import messages +from .forms import TournamentRegistrationForm, AddPlayerForm + +from .utils.licence_validator import LicenseValidator +from .utils.player_search import get_player_name_from_csv + +from django.contrib.auth.decorators import login_required +from .forms import SimpleCustomUserCreationForm +from django.contrib.sites.shortcuts import get_current_site +from django.template.loader import render_to_string +from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode +from django.utils.encoding import force_bytes def index(request): @@ -85,10 +102,23 @@ def future_tournaments(club_id): def tournament_info(request, tournament_id): tournament = get_object_or_404(Tournament, pk=tournament_id) + is_registered = False + + if request.user.is_authenticated: + # Assuming user's licence_id is stored in the user profile (e.g., request.user.profile.licence_id) + user_licence_id = request.user.licence_id + # Check if there is a PlayerRegistration for this user in this tournament + is_registered = PlayerRegistration.objects.filter( + licence_id__startswith=user_licence_id, + team_registration__tournament=tournament + ).exists() + return render(request, 'tournaments/tournament_info.html', { 'tournament': tournament, + 'is_registered': is_registered, }) + def tournaments(request): filter = int(request.GET.get('filter')) @@ -315,7 +345,8 @@ def activate(request, uidb64, token): if user is not None and account_activation_token.check_token(user, token): user.is_active = True user.save() - return HttpResponse('Votre email est confirmé. Vous pouvez maintenant vous connecter.') + login(request, user) + return redirect('index') # Redirect to the homepage or any other page you prefer else: return HttpResponse('Le lien est invalide.') @@ -483,3 +514,204 @@ def send_email(mail, name): email = EmailMessage(subject, body, to=[mail]) email.send() + +@csrf_exempt +def signup(request): + if request.method == 'POST': + form = SimpleCustomUserCreationForm(request.POST) + if form.is_valid(): + user = form.save(commit=False) + user.is_active = False # Deactivate account until email is verified + user.save() + + # Send verification email + send_verification_email(request, user) + request.session['pre_login_username'] = user.username + request.session['pre_login_password'] = user.password # Store hashed password, or handle with caution + + messages.success(request, "Votre compte a été créé ! Veuillez vérifier votre e-mail pour confirmer votre compte.") + return redirect('login') # Redirect to login page or a custom message page + else: + form = SimpleCustomUserCreationForm() + return render(request, 'registration/signup.html', {'form': form}) + +def send_verification_email(request, user): + + current_site = get_current_site(request) + mail_subject = 'Activez votre compte Padel Club !' + message = render_to_string('tournaments/acc_active_email.html', { + 'user': user, + 'domain': current_site.domain, + 'uid': urlsafe_base64_encode(force_bytes(user.pk)), + 'token': account_activation_token.make_token(user), + }) + email = EmailMessage(mail_subject, message, to=[user.email]) + email.send() + +@login_required +def profile(request): + user = request.user # Get the currently authenticated user + + # Query tournaments where the user is registered + tournaments = Tournament.objects.all() + + return render(request, 'registration/profile.html', { + 'tournaments': tournaments, + 'user_name': user.username + }) + +def register_tournament(request, tournament_id): + tournament = get_object_or_404(Tournament, id=tournament_id) + registration_successful = False # Flag for registration status + team_form = None + add_player_form = None + + # Process forms + if request.method == 'POST': + team_form = TournamentRegistrationForm(request.POST) + add_player_form = AddPlayerForm(request.POST) + # Check if the add player form is submitted + if 'add_player' in request.POST and add_player_form.is_valid(): + player_data = add_player_form.cleaned_data + + # Validate the license ID before adding the player + licence_id = player_data['licence_id'].upper() + + # Instantiate your custom validator and validate the license ID + validator = LicenseValidator(licence_id) + + if validator.validate_license() is False: + messages.error(request, f"Le numéro de licence est invalide, la lettre ne correspond pas. {validator.computed_license_key}") + return render(request, 'register_tournament.html', { + 'team_form': team_form, + 'add_player_form': add_player_form, + 'tournament': tournament, + 'registration_successful': registration_successful, + 'current_players': request.session['team_registration'], + }) + + # Check if the player with the same licence_id already exists in the session + existing_players = [player['licence_id'] for player in request.session['team_registration']] + if licence_id in existing_players: + messages.error(request, 'This player is already added to the team.') + return render(request, 'register_tournament.html', { + 'team_form': team_form, + 'add_player_form': add_player_form, + 'tournament': tournament, + 'registration_successful': registration_successful, + 'current_players': request.session['team_registration'], + }) + else: + # Check if a PlayerRegistration with the same licence_id already exists in the database + stripped_license = validator.stripped_license + if validate_license_id(stripped_license, tournament): + messages.error(request, 'A player with this licence ID is already registered in a team.') + return render(request, 'register_tournament.html', { + 'team_form': team_form, + 'add_player_form': add_player_form, + 'tournament': tournament, + 'registration_successful': registration_successful, + 'current_players': request.session['team_registration'], + }) + elif add_player_form.names_is_valid(): + request.session['team_registration'].append(player_data) + request.session.modified = True # Ensure session is updated + else: + if add_player_form.first_tournament is False: + # Retrieve player names from the CSV file + first_name, last_name = get_player_name_from_csv(licence_id) + if first_name and last_name: + player_data['first_name'] = first_name + player_data['last_name'] = last_name + # If validation passes, add the player to the session without clearing previous ones + request.session['team_registration'].append(player_data) + request.session.modified = True # Ensure session is updated + else: + add_player_form.first_tournament = True + + + + # Check if the team registration form is valid and finalize the registration + elif 'register_team' in request.POST and team_form.is_valid(): + team_registration = TeamRegistration.objects.create( + tournament=tournament, + registration_date=timezone.now() + ) + # Create PlayerRegistration objects for each player in the session + for player_data in request.session['team_registration']: + player_registration = PlayerRegistration.objects.create( + team_registration=team_registration, + first_name=player_data['first_name'], + last_name=player_data['last_name'], + licence_id=player_data['licence_id'], + email = player_data.get('email', None), + phone_number = player_data.get('phone_number', None) + ) + + request.session['team_registration'] = [] + registration_successful = True + else: + request.session['team_registration'] = [] + initial_data = {} + # Add the authenticated user to the session as the first player if not already added + if request.user.is_authenticated: + initial_data = { + 'email': request.user.email, + 'phone': request.user.phone, + } + existing_players = [player['licence_id'] for player in request.session['team_registration']] + if request.user.licence_id not in existing_players: + # Add the authenticated user as the first player in the session + player_data = { + 'first_name': request.user.first_name, + 'last_name': request.user.last_name.upper(), + 'email': request.user.email, + 'phone': request.user.phone, + 'licence_id': request.user.licence_id, + } + request.session['team_registration'].insert(0, player_data) # Add them as the first player + request.session.modified = True # Ensure session is updated + + team_form = TournamentRegistrationForm(initial=initial_data) + add_player_form = AddPlayerForm() + + return render(request, 'register_tournament.html', { + 'team_form': team_form, + 'add_player_form': add_player_form, + 'tournament': tournament, + 'registration_successful': registration_successful, + 'current_players': request.session['team_registration'], + }) + + + + +@login_required +def unregister_tournament(request, tournament_id): + user_licence_id = request.user.licence_id + player_registration = PlayerRegistration.objects.filter( + licence_id__startswith=user_licence_id, + team_registration__tournament_id=tournament_id + ).first() # Get the first match, if any + + if player_registration: + team_registration = player_registration.team_registration # Get the related TeamRegistration + team_registration.delete() # Delete the team registration + + request.session['team_registration'] = [] + + return redirect('tournament-info', tournament_id=tournament_id) + +def validate_license_id(licence_id, tournament): + teams = TeamRegistration.objects.filter(tournament=tournament) + + # Check if any player in any team in the tournament already has this licence_id + # Normalize the licence ID before querying + # Loop through each team and check if any of its players has the same licence_id + for team in teams: + for player in team.playerregistration_set.all(): + if player.licence_id.startswith(licence_id): + return True + + # If all checks pass, return True (you can add further logic here if needed) + return False