Laurent 7 months ago
commit f1cfabcbe6
  1. 6
      shop/static/shop/css/shop.css
  2. 2
      shop/templates/shop/product_item.html
  3. 14
      tournaments/custom_views.py
  4. 49
      tournaments/forms.py
  5. 18
      tournaments/migrations/0113_tournament_team_count_limit.py
  6. 51
      tournaments/models/enums.py
  7. 51
      tournaments/models/player_registration.py
  8. 10
      tournaments/models/team_registration.py
  9. 236
      tournaments/models/tournament.py
  10. 8
      tournaments/services/email_service.py
  11. 13
      tournaments/services/tournament_registration.py
  12. 19
      tournaments/signals.py
  13. 153
      tournaments/static/tournaments/css/style.css
  14. 8
      tournaments/static/tournaments/css/tournament_bracket.css
  15. 53
      tournaments/templates/profile.html
  16. 47
      tournaments/templates/register_tournament.html
  17. 4
      tournaments/templates/registration/login.html
  18. 33
      tournaments/templates/registration/my_tournaments.html
  19. 4
      tournaments/templates/registration/password_reset_complete.html
  20. 18
      tournaments/templates/registration/password_reset_confirm.html
  21. 4
      tournaments/templates/registration/password_reset_done.html
  22. 17
      tournaments/templates/registration/password_reset_form.html
  23. 23
      tournaments/templates/registration/signup.html
  24. 9
      tournaments/templates/tournaments/tournament_info.html
  25. 59
      tournaments/templates/tournaments/tournament_row.html
  26. 43
      tournaments/templates/tournaments/tournaments.html
  27. 4
      tournaments/templates/tournaments/tournaments_list.html
  28. 0
      tournaments/templatetags/__init__.py
  29. 7
      tournaments/templatetags/tournament_tags.py
  30. 12
      tournaments/urls.py
  31. 53
      tournaments/views.py

@ -95,7 +95,7 @@
.add-to-cart-button, .add-to-cart-button,
.checkout-button { .checkout-button {
background-color: #90ee90; background-color: #90ee90;
color: #707070; color: #505050;
border: none; border: none;
border-radius: 12px; border-radius: 12px;
font-size: 12px; font-size: 12px;
@ -120,7 +120,7 @@
} }
.coupon-section { .coupon-section {
color: #707070; color: #505050;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
text-decoration: none; text-decoration: none;
@ -129,7 +129,7 @@
.confirm-nav-button { .confirm-nav-button {
background-color: #90ee90; background-color: #90ee90;
color: #707070; color: #505050;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
text-decoration: none; text-decoration: none;

@ -185,7 +185,7 @@ function addToCartAjax(productId) {
notification.style.right = '20px'; notification.style.right = '20px';
notification.style.padding = '20px'; notification.style.padding = '20px';
notification.style.backgroundColor = '#90ee90'; notification.style.backgroundColor = '#90ee90';
notification.style.color = '#707070'; notification.style.color = '#505050';
notification.style.borderRadius = '12px'; notification.style.borderRadius = '12px';
notification.style.zIndex = '9999'; notification.style.zIndex = '9999';
notification.style.opacity = '0'; notification.style.opacity = '0';

@ -10,12 +10,20 @@ class CustomLoginView(auth_views.LoginView):
def get_success_url(self): def get_success_url(self):
# First check the 'next' parameter which has higher priority # First check the 'next' parameter which has higher priority
next_url = self.request.POST.get('next') or self.request.GET.get('next') next_url = self.request.POST.get('next') or self.request.GET.get('next')
# Check if the next URL is a password reset page and avoid that redirect
if next_url and next_url.strip(): if next_url and next_url.strip():
# Avoid redirecting to password reset pages after login
if 'reset' in next_url or 'password_reset' in next_url:
# Redirect to profile or index instead
return reverse('profile')
return next_url return next_url
# Then check if we have a stored referrer URL # Then check if we have a stored referrer URL
referrer = self.request.session.get('login_referrer') referrer = self.request.session.get('login_referrer')
if referrer: if referrer:
# Avoid redirecting to password reset pages from stored referrer
if 'reset' not in referrer and 'password_reset' not in referrer:
# Clear the stored referrer to prevent reuse # Clear the stored referrer to prevent reuse
del self.request.session['login_referrer'] del self.request.session['login_referrer']
return referrer return referrer
@ -24,5 +32,11 @@ class CustomLoginView(auth_views.LoginView):
return reverse('index') return reverse('index')
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
# Clear any potential password reset session data
keys_to_clear = [key for key in request.session.keys()
if 'reset' in key or 'password' in key]
for key in keys_to_clear:
del request.session[key]
messages.get_messages(request).used = True messages.get_messages(request).used = True
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)

@ -12,6 +12,11 @@ from django.utils.encoding import force_bytes
class CustomUserCreationForm(UserCreationForm): class CustomUserCreationForm(UserCreationForm):
usable_password = None usable_password = None
def clean_licence_id(self):
licence_id = self.cleaned_data.get('licence_id')
if licence_id:
return licence_id.replace(' ', '').strip().upper()
return licence_id
class Meta: class Meta:
model = CustomUser model = CustomUser
@ -33,6 +38,22 @@ class CustomUserCreationForm(UserCreationForm):
class SimpleCustomUserCreationForm(UserCreationForm): class SimpleCustomUserCreationForm(UserCreationForm):
usable_password = None usable_password = None
def clean_licence_id(self):
licence_id = self.cleaned_data.get('licence_id')
if licence_id:
return licence_id.replace(' ', '').strip().upper()
return licence_id
def clean_phone(self):
phone = self.cleaned_data.get('phone')
if phone:
# Remove all spaces
phone = phone.replace(' ', '')
# Basic regex for phone numbers, matching common formats
if not re.match(r"^\+?\d{10,15}$", phone):
raise forms.ValidationError("Entrer un numéro de téléphone valide.")
return phone
class Meta: class Meta:
model = CustomUser model = CustomUser
fields = UserCreationForm.Meta.fields + ('email', 'phone', 'first_name', 'last_name', 'licence_id', 'country') fields = UserCreationForm.Meta.fields + ('email', 'phone', 'first_name', 'last_name', 'licence_id', 'country')
@ -125,16 +146,8 @@ class AddPlayerForm(forms.Form):
def clean_licence_id(self): def clean_licence_id(self):
licence_id = self.cleaned_data.get('licence_id') licence_id = self.cleaned_data.get('licence_id')
if licence_id:
# Convert to uppercase licence_id = licence_id.replace(' ', '').strip().upper()
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 return licence_id
def clean_last_name(self): def clean_last_name(self):
@ -193,6 +206,22 @@ class ProfileUpdateForm(forms.ModelForm):
# Remove autofocus from the 'username' field # Remove autofocus from the 'username' field
self.fields['username'].widget.attrs.pop("autofocus", None) self.fields['username'].widget.attrs.pop("autofocus", None)
def clean_licence_id(self):
licence_id = self.cleaned_data.get('licence_id')
if licence_id:
return licence_id.replace(' ', '').upper()
return licence_id
def clean_phone(self):
phone = self.cleaned_data.get('phone')
if phone:
# Remove all spaces
phone = phone.replace(' ', '')
# Basic regex for phone numbers, matching common formats
if not re.match(r"^\+?\d{10,15}$", phone):
raise forms.ValidationError("Entrer un numéro de téléphone valide.")
return phone
class Meta: class Meta:
model = CustomUser model = CustomUser
fields = ['first_name', 'last_name', 'licence_id', 'username', 'email', 'phone'] fields = ['first_name', 'last_name', 'licence_id', 'username', 'email', 'phone']

@ -0,0 +1,18 @@
# Generated by Django 5.1 on 2025-03-29 08:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0112_tournament_disable_ranking_federal_ruling_and_more'),
]
operations = [
migrations.AddField(
model_name='tournament',
name='team_count_limit',
field=models.BooleanField(default=True),
),
]

@ -146,6 +146,7 @@ class OnlineRegistrationStatus(models.IntegerChoices):
WAITING_LIST_FULL = 6, 'Waiting List Full' WAITING_LIST_FULL = 6, 'Waiting List Full'
IN_PROGRESS = 7, 'In Progress' IN_PROGRESS = 7, 'In Progress'
ENDED_WITH_RESULTS = 8, 'Ended with Results' ENDED_WITH_RESULTS = 8, 'Ended with Results'
CANCELED = 9, 'Canceled'
def status_localized(self) -> str: def status_localized(self) -> str:
status_map = { status_map = {
@ -156,10 +157,58 @@ class OnlineRegistrationStatus(models.IntegerChoices):
OnlineRegistrationStatus.WAITING_LIST_POSSIBLE: "Liste d'attente ouverte", OnlineRegistrationStatus.WAITING_LIST_POSSIBLE: "Liste d'attente ouverte",
OnlineRegistrationStatus.WAITING_LIST_FULL: "Liste d'attente complète", OnlineRegistrationStatus.WAITING_LIST_FULL: "Liste d'attente complète",
OnlineRegistrationStatus.IN_PROGRESS: "Tournoi en cours", OnlineRegistrationStatus.IN_PROGRESS: "Tournoi en cours",
OnlineRegistrationStatus.ENDED_WITH_RESULTS: "Tournoi terminé" OnlineRegistrationStatus.ENDED_WITH_RESULTS: "Tournoi terminé",
OnlineRegistrationStatus.CANCELED: "Tournoi annulé"
} }
return status_map.get(self, "") return status_map.get(self, "")
def short_label(self) -> str:
"""Returns a short, concise label for the status box"""
label_map = {
OnlineRegistrationStatus.OPEN: "ouvert",
OnlineRegistrationStatus.NOT_ENABLED: "désactivé",
OnlineRegistrationStatus.NOT_STARTED: "à venir",
OnlineRegistrationStatus.ENDED: "clôturé",
OnlineRegistrationStatus.WAITING_LIST_POSSIBLE: "ouvert",
OnlineRegistrationStatus.WAITING_LIST_FULL: "complet",
OnlineRegistrationStatus.IN_PROGRESS: "en cours",
OnlineRegistrationStatus.ENDED_WITH_RESULTS: "résultats",
OnlineRegistrationStatus.CANCELED: "annulé"
}
return label_map.get(self, "")
def box_class(self) -> str:
"""Returns the CSS class for the status box"""
class_map = {
OnlineRegistrationStatus.OPEN: "light-green",
OnlineRegistrationStatus.NOT_ENABLED: "gray",
OnlineRegistrationStatus.NOT_STARTED: "light-green",
OnlineRegistrationStatus.ENDED: "gray",
OnlineRegistrationStatus.WAITING_LIST_POSSIBLE: "light-orange",
OnlineRegistrationStatus.WAITING_LIST_FULL: "light-red",
OnlineRegistrationStatus.IN_PROGRESS: "blue",
OnlineRegistrationStatus.ENDED_WITH_RESULTS: "dark-gray",
OnlineRegistrationStatus.CANCELED: "light-red",
}
return class_map.get(self, "gray")
def display_box(self) -> bool:
"""
Determines whether this status should display a status box
Returns True if the status should be displayed, False otherwise
"""
# List the statuses that should display a box
display_statuses = [
OnlineRegistrationStatus.OPEN,
OnlineRegistrationStatus.NOT_STARTED,
OnlineRegistrationStatus.WAITING_LIST_POSSIBLE,
OnlineRegistrationStatus.WAITING_LIST_FULL,
OnlineRegistrationStatus.CANCELED,
# You can add or remove statuses as needed
]
return self in display_statuses
class UserOrigin(models.IntegerChoices): class UserOrigin(models.IntegerChoices):
ADMIN = 0, 'Admin' ADMIN = 0, 'Admin'
SITE = 1, 'Site' SITE = 1, 'Site'

@ -1,5 +1,5 @@
from django.db import models from django.db import models
from . import SideStoreModel, TeamRegistration, PlayerSexType, PlayerDataSource, PlayerPaymentType, FederalCategory from . import SideStoreModel, TeamRegistration, PlayerSexType, PlayerDataSource, PlayerPaymentType, OnlineRegistrationStatus
import uuid import uuid
from django.utils import timezone from django.utils import timezone
@ -92,3 +92,52 @@ class PlayerRegistration(SideStoreModel):
return "1ère" return "1ère"
return "1er" return "1er"
return f"{self.rank}ème" return f"{self.rank}ème"
def get_registration_status(self):
"""
Returns a status object with information about the player's registration status.
This object contains display_box, box_class, and short_label properties
used in the tournament row template.
Returns None if no relevant status can be determined.
"""
# If no team registration exists, return None
if not self.team_registration:
return None
status = {
'display_box': True,
'box_class': 'gray',
'short_label': 'inscrit'
}
tournament = self.team_registration.tournament
team = self.team_registration
# Tournament is ended with results
if tournament.get_online_registration_status() is OnlineRegistrationStatus.ENDED_WITH_RESULTS:
if team.get_final_ranking_component():
status['box_class'] = 'light-green'
status['short_label'] = team.get_final_ranking_component()
return status
# Team has walked out
if team.walk_out:
status['box_class'] = 'light-red'
status['short_label'] = 'forfait'
return status
# Tournament is in progress
if tournament.supposedly_in_progress():
status['box_class'] = 'light-green'
status['short_label'] = 'en lice'
return status
# Tournament hasn't started yet
if team.is_in_waiting_list() >= 0:
status['box_class'] = 'light-yellow'
status['short_label'] = "en attente"
else:
status['box_class'] = 'light-green'
status['short_label'] = 'inscrit'
return status

@ -240,10 +240,16 @@ class TeamRegistration(SideStoreModel):
def get_final_ranking(self): def get_final_ranking(self):
get_final_ranking_component = self.get_final_ranking_component()
if get_final_ranking_component:
return get_final_ranking_component + self.ranking_delta()
return None
def get_final_ranking_component(self):
if self.final_ranking: if self.final_ranking:
if self.final_ranking == 1: if self.final_ranking == 1:
return "1er" + self.ranking_delta() return "1er"
return f"{self.final_ranking}ème" + self.ranking_delta() return f"{self.final_ranking}ème"
return None return None
def ranking_delta(self): def ranking_delta(self):

@ -1,4 +1,3 @@
from time import daylight
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from django.db import models from django.db import models
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -8,12 +7,13 @@ if TYPE_CHECKING:
from . import BaseModel, Event, TournamentPayment, FederalMatchCategory, FederalCategory, FederalLevelCategory, FederalAgeCategory, OnlineRegistrationStatus from . import BaseModel, Event, TournamentPayment, FederalMatchCategory, FederalCategory, FederalLevelCategory, FederalAgeCategory, OnlineRegistrationStatus
import uuid import uuid
from django.utils import timezone, formats from django.utils import timezone, formats
from datetime import datetime, timedelta from datetime import datetime, timedelta, time
from zoneinfo import ZoneInfo
from tournaments.utils.player_search import get_player_name_from_csv from tournaments.utils.player_search import get_player_name_from_csv
from shared.cryptography import encryption_util from shared.cryptography import encryption_util
from ..utils.extensions import plural_format from ..utils.extensions import plural_format
from django.utils.formats import date_format
from ..utils.licence_validator import LicenseValidator
class TeamSortingType(models.IntegerChoices): class TeamSortingType(models.IntegerChoices):
RANK = 1, 'Rank' RANK = 1, 'Rank'
@ -67,6 +67,7 @@ class Tournament(BaseModel):
initial_seed_round = models.IntegerField(default=0) initial_seed_round = models.IntegerField(default=0)
initial_seed_count = models.IntegerField(default=0) initial_seed_count = models.IntegerField(default=0)
enable_online_registration = models.BooleanField(default=False) # Equivalent to Bool = false enable_online_registration = models.BooleanField(default=False) # Equivalent to Bool = false
team_count_limit = models.BooleanField(default=True)
registration_date_limit = models.DateTimeField(null=True, blank=True) # Equivalent to Date? = nil registration_date_limit = models.DateTimeField(null=True, blank=True) # Equivalent to Date? = nil
opening_registration_date = models.DateTimeField(null=True, blank=True) # Equivalent to Date? = nil opening_registration_date = models.DateTimeField(null=True, blank=True) # Equivalent to Date? = nil
waiting_list_limit = models.IntegerField(null=True, blank=True) # Equivalent to Int? = nil waiting_list_limit = models.IntegerField(null=True, blank=True) # Equivalent to Int? = nil
@ -230,36 +231,12 @@ class Tournament(BaseModel):
else: else:
return None return None
def tournament_status_display(self): def get_tournament_status(self):
if self.is_canceled() is True: return self.get_online_registration_status().status_localized()
return "Annulé"
teams = self.teams(True) def get_tournament_status_team_count(self):
if self.supposedly_in_progress() or self.end_date is not None or self.should_be_over(): active_teams_count = self.team_registrations.filter(walk_out=False).count()
teams = [t for t in teams if t.stage != "Attente"] return min(active_teams_count, self.team_count)
if teams is not None and len(teams) > 0:
word = "équipe"
if len(teams) > 1:
word = word + "s"
return f"{len(teams)} {word}"
else:
return None
registration_status = None
if self.enable_online_registration == True:
registration_status = self.get_online_registration_status().status_localized()
if teams is not None and len(teams) > 0:
word = "inscription"
if len(teams) > 1:
word = word + "s"
if registration_status is not None:
return f"{registration_status}\n{len(teams)} {word}"
else:
return f"{len(teams)} {word}"
else:
if registration_status is not None:
return f"{registration_status}"
return None
def name_and_event(self): def name_and_event(self):
event_name = None event_name = None
@ -331,9 +308,9 @@ class Tournament(BaseModel):
index = i index = i
# Check if team_count exists # Check if team_count exists
if self.team_count: if self.team_count_limit == True:
# Team is not in list # Team is not in list
if index < self.team_count: if index < 0:
print("Team is not in list", index, self.team_count) print("Team is not in list", index, self.team_count)
return -1 return -1
# Return position in waiting list relative to target count # Return position in waiting list relative to target count
@ -397,7 +374,7 @@ class Tournament(BaseModel):
complete_teams.append(team) complete_teams.append(team)
else: else:
waiting_teams.append(team) waiting_teams.append(team)
wildcard_bracket = []
return complete_teams, wildcard_bracket, wildcard_group_stage, waiting_teams return complete_teams, wildcard_bracket, wildcard_group_stage, waiting_teams
def sort_teams(self, include_waiting_list, complete_teams, wildcard_bracket, wildcard_group_stage, waiting_teams): def sort_teams(self, include_waiting_list, complete_teams, wildcard_bracket, wildcard_group_stage, waiting_teams):
@ -1051,19 +1028,22 @@ class Tournament(BaseModel):
def options_online_registration(self): def options_online_registration(self):
options = [] options = []
timezone = self.timezone()
# Date d'ouverture # Date d'ouverture
if self.opening_registration_date: if self.opening_registration_date:
date = formats.date_format(timezone.localtime(self.opening_registration_date), format='j F Y H:i') date = formats.date_format(self.opening_registration_date.astimezone(timezone), format='j F Y H:i')
options.append(f"Ouverture des inscriptions le {date}") options.append(f"Ouverture des inscriptions le {date}")
# Date limite # Date limite
if self.registration_date_limit: if self.registration_date_limit:
date = formats.date_format(timezone.localtime(self.registration_date_limit), format='j F Y H:i') date = formats.date_format(self.registration_date_limit.astimezone(timezone), format='j F Y H:i')
options.append(f"Clôture des inscriptions le {date}") options.append(f"Clôture des inscriptions le {date}")
options.append(self.get_selection_status_localized)
# Cible d'équipes # Cible d'équipes
if self.team_count: if self.team_count_limit is True:
options.append(f"Maximum {self.team_count} équipes") options.append(f"Maximum {self.team_count} équipes")
# Liste d'attente # Liste d'attente
@ -1122,13 +1102,23 @@ class Tournament(BaseModel):
return False return False
return True return True
def get_selection_status_localized(self):
if self.team_sorting == TeamSortingType.RANK:
return "La sélection se fait par le poids de l'équipe"
else:
return "La sélection se fait par date d'inscription"
def get_online_registration_status(self): def get_online_registration_status(self):
if self.is_canceled():
return OnlineRegistrationStatus.CANCELED
if self.end_date is not None:
return OnlineRegistrationStatus.ENDED_WITH_RESULTS
if self.enable_online_registration is False:
return OnlineRegistrationStatus.NOT_ENABLED
if self.supposedly_in_progress(): if self.supposedly_in_progress():
return OnlineRegistrationStatus.ENDED return OnlineRegistrationStatus.ENDED
if self.closed_registration_date is not None: if self.closed_registration_date is not None:
return OnlineRegistrationStatus.ENDED return OnlineRegistrationStatus.WAITING_LIST_POSSIBLE
if self.end_date is not None:
return OnlineRegistrationStatus.ENDED_WITH_RESULTS
now = timezone.now() now = timezone.now()
@ -1140,9 +1130,12 @@ class Tournament(BaseModel):
if self.registration_date_limit is not None: if self.registration_date_limit is not None:
timezoned_datetime = timezone.localtime(self.registration_date_limit) timezoned_datetime = timezone.localtime(self.registration_date_limit)
if now > timezoned_datetime: if now > timezoned_datetime:
return OnlineRegistrationStatus.ENDED return OnlineRegistrationStatus.WAITING_LIST_POSSIBLE
if self.team_count is not None: if self.team_sorting == TeamSortingType.RANK:
return OnlineRegistrationStatus.OPEN
if self.team_count_limit is True:
# Get all team registrations excluding walk_outs # Get all team registrations excluding walk_outs
current_team_count = self.team_registrations.exclude(walk_out=True).count() current_team_count = self.team_registrations.exclude(walk_out=True).count()
if current_team_count >= self.team_count: if current_team_count >= self.team_count:
@ -1153,6 +1146,21 @@ class Tournament(BaseModel):
return OnlineRegistrationStatus.WAITING_LIST_POSSIBLE return OnlineRegistrationStatus.WAITING_LIST_POSSIBLE
return OnlineRegistrationStatus.OPEN return OnlineRegistrationStatus.OPEN
def get_registration_status_short_label(self):
"""Returns a short label for the registration status"""
status = self.get_online_registration_status()
return status.short_label()
def get_registration_status_class(self):
"""Returns the CSS class for the registration status box"""
status = self.get_online_registration_status()
return status.box_class()
def should_display_status_box(self):
"""Returns whether the registration status box should be displayed"""
status = self.get_online_registration_status()
return status.display_box()
def is_unregistration_possible(self): def is_unregistration_possible(self):
# Check if tournament has started # Check if tournament has started
if self.supposedly_in_progress(): if self.supposedly_in_progress():
@ -1174,8 +1182,14 @@ class Tournament(BaseModel):
return True return True
def get_waiting_list_position(self): def get_waiting_list_position(self):
current_time = timezone.now()
local_registration_federal_limit = self.local_registration_federal_limit()
if self.team_sorting == TeamSortingType.RANK and local_registration_federal_limit is not None:
if current_time < local_registration_federal_limit:
return -1
# If no target team count exists, no one goes to waiting list # If no target team count exists, no one goes to waiting list
if self.team_count is None: if self.team_count_limit is False:
return -1 return -1
# Get count of active teams (not walked out) # Get count of active teams (not walked out)
@ -1257,7 +1271,6 @@ class Tournament(BaseModel):
current_year += 1 current_year += 1
user_age = current_year - int(birth_year) user_age = current_year - int(birth_year)
print("user_age", user_age)
# Check age category restrictions # Check age category restrictions
if self.federal_age_category == FederalAgeCategory.A11_12 and user_age > 12: if self.federal_age_category == FederalAgeCategory.A11_12 and user_age > 12:
@ -1290,12 +1303,49 @@ class Tournament(BaseModel):
def min_player_rank(self): def min_player_rank(self):
return FederalLevelCategory.min_player_rank(self.federal_level_category, self.federal_category, self.federal_age_category) return FederalLevelCategory.min_player_rank(self.federal_level_category, self.federal_category, self.federal_age_category)
def first_waiting_list_team(self, teams): def local_registration_federal_limit(self):
timezone = self.timezone()
if self.registration_date_limit is not None:
return self.registration_date_limit.astimezone(timezone)
if self.closed_registration_date is not None:
return self.closed_registration_date.astimezone(timezone)
local_start_date = self.local_start_date()
if local_start_date is None:
return None
if self.federal_level_category == FederalLevelCategory.P500:
# 7 days before at 23:59
return (local_start_date - timedelta(days=7)).replace(hour=23, minute=59, second=59)
elif self.federal_level_category in [FederalLevelCategory.P1000,
FederalLevelCategory.P1500,
FederalLevelCategory.P2000]:
# 14 days before at 23:59
return (local_start_date - timedelta(days=14)).replace(hour=23, minute=59, second=59)
return None
def waiting_list_teams(self, teams):
current_time = timezone.now()
local_registration_federal_limit = self.local_registration_federal_limit()
if self.team_sorting == TeamSortingType.RANK and local_registration_federal_limit is not None:
if current_time < local_registration_federal_limit:
return None
if len(teams)<=self.team_count: if len(teams)<=self.team_count:
return None return None
waiting_teams = [team for team in teams if team.stage == "Attente"] waiting_teams = [team for team in teams if team.stage == "Attente"]
if len(waiting_teams) > 0: return waiting_teams
return waiting_teams[0].team_registration
def first_waiting_list_team(self, teams):
waiting_list_team = self.waiting_list_teams(teams)
if waiting_list_team is None:
return None
if len(waiting_list_team) > 0:
return waiting_list_team[0].team_registration
else:
return None
def broadcasted_prog(self): def broadcasted_prog(self):
# Get matches from broadcasted_matches_and_group_stages # Get matches from broadcasted_matches_and_group_stages
@ -1418,7 +1468,6 @@ class Tournament(BaseModel):
def umpire_contact(self): def umpire_contact(self):
if self.umpire_custom_contact is not None: if self.umpire_custom_contact is not None:
print(self.umpire_custom_contact)
return self.umpire_custom_contact return self.umpire_custom_contact
if self.event and self.event.creator: if self.event and self.event.creator:
return self.event.creator.full_name() return self.event.creator.full_name()
@ -1427,17 +1476,96 @@ class Tournament(BaseModel):
def umpire_mail(self): def umpire_mail(self):
if self.umpire_custom_mail is not None: if self.umpire_custom_mail is not None:
print(self.umpire_custom_mail)
return self.umpire_custom_mail return self.umpire_custom_mail
return self.event.creator.email return self.event.creator.email
def umpire_phone(self): def umpire_phone(self):
if self.umpire_custom_phone is not None: if self.umpire_custom_phone is not None:
print(self.umpire_custom_phone)
return self.umpire_custom_phone return self.umpire_custom_phone
return self.event.creator.phone return self.event.creator.phone
@property
def week_day(self):
"""Return the weekday name (e.g., 'Monday')"""
date = self.local_start_date()
return date_format(date, format='D') + '.' # 'l' gives full weekday name
@property
def day(self):
"""Return the day of the month"""
date = self.local_start_date()
return date.day
@property
def month(self):
"""
Return the month name in lowercase:
- If full month name is 4 letters or fewer, return as is
- If more than 4 letters, return first 4 letters with a dot
"""
date = self.local_start_date()
# Get full month name and convert to lowercase
full_month = date_format(date, format='F').lower()
# Check if the month name is 5 letters or fewer
if len(full_month) <= 5:
return full_month
else:
# Truncate to 5 letters and add a dot
return f"{full_month[:5]}."
@property
def year(self):
"""Return the year"""
date = self.local_start_date()
return date.year
@property
def localized_day_duration(self):
"""
Return localized day duration in French:
- If multiple days: '2 jours', '3 jours', etc.
- If 1 day and starts after 18:00: 'soirée'
- If 1 day and starts before 18:00: 'journée'
"""
# Assuming day_duration is a property or field that returns the number of days
days = self.day_duration
if days > 1:
return f"{days} jours"
else:
# For single day events, check the starting hour
start_time = self.local_start_date().time()
evening_threshold = time(18, 0) # 18:00 (6 PM)
if start_time >= evening_threshold:
return "soirée"
else:
return "journée"
def get_player_registration_status_by_licence(self, user):
from . import PlayerRegistration
licence_id = user.licence_id
if not licence_id:
return None
validator = LicenseValidator(licence_id)
stripped_license = validator.stripped_license
# Check if there is a PlayerRegistration for this user in this tournament
user_player = PlayerRegistration.objects.filter(
licence_id__icontains=stripped_license,
team_registration__tournament=self,
).first()
if user_player:
return user_player.get_registration_status()
else:
return None
class MatchGroup: class MatchGroup:
def __init__(self, name, matches, formatted_schedule, round_id=None): def __init__(self, name, matches, formatted_schedule, round_id=None):
self.name = name self.name = name
@ -1489,6 +1617,12 @@ class TeamSummon:
} }
class TeamItem: class TeamItem:
def __str__(self):
return f"TeamItem({self.team_registration.id}, names={self.names}, stage={self.stage})"
def __repr__(self):
return self.__str__()
def __init__(self, team_registration): def __init__(self, team_registration):
self.names = team_registration.team_names() self.names = team_registration.team_names()
self.date = team_registration.local_call_date() self.date = team_registration.local_call_date()

@ -2,6 +2,7 @@ from django.core.mail import EmailMessage
from django.utils import timezone from django.utils import timezone
from django.urls import reverse from django.urls import reverse
from enum import Enum from enum import Enum
from ..models.tournament import TeamSortingType
class TeamEmailType(Enum): class TeamEmailType(Enum):
REGISTERED = "registered" REGISTERED = "registered"
@ -74,6 +75,13 @@ class TournamentEmailService:
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 du tournoi {tournament_details_str} est confirmée.")
else: else:
body_parts.append(f"Votre inscription au tournoi {tournament_details_str} est confirmée.") body_parts.append(f"Votre inscription au tournoi {tournament_details_str} est confirmée.")
if tournament.team_sorting == TeamSortingType.RANK:
cloture_date = tournament.local_registration_federal_limit().strftime("%d/%m/%Y à %H:%M")
loc = ""
if cloture_date is not None:
loc = f", prévu le {cloture_date}"
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" absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info"
link_text = "informations sur le tournoi" link_text = "informations sur le tournoi"

@ -48,6 +48,13 @@ class TournamentRegistrationService:
if not self.context['add_player_form'].is_valid(): if not self.context['add_player_form'].is_valid():
return return
# Clear existing messages if the form is valid
from django.contrib.messages import get_messages
storage = get_messages(self.request)
# Iterate through the storage to clear it
for _ in storage:
pass
player_data = self.context['add_player_form'].cleaned_data player_data = self.context['add_player_form'].cleaned_data
licence_id = player_data.get('licence_id', '').upper() licence_id = player_data.get('licence_id', '').upper()
@ -112,6 +119,12 @@ class TournamentRegistrationService:
self.context['registration_successful'] = True self.context['registration_successful'] = True
def handle_get_request(self): def handle_get_request(self):
from django.contrib.messages import get_messages
storage = get_messages(self.request)
# Iterate through the storage to clear it
for _ in storage:
pass
self.context['add_player_form'] = AddPlayerForm() self.context['add_player_form'] = AddPlayerForm()
self.context['team_form'] = self.initialize_team_form() self.context['team_form'] = self.initialize_team_form()
self.initialize_session_data() self.initialize_session_data()

@ -113,21 +113,16 @@ def check_waiting_list(sender, instance, **kwargs):
teams_out_to_warn = [] teams_out_to_warn = []
teams_in_to_warn = [] teams_in_to_warn = []
if previous_state.team_count > instance.team_count: if previous_state.team_count > instance.team_count:
teams_to_remove_count = previous_state.team_count - instance.team_count teams_that_will_be_out = instance.teams(True)[instance.team_count:]
previous_state_teams = previous_state.teams(True) teams_out_to_warn = [
sorted_teams = sorted( team for team in teams_that_will_be_out
[team for team in previous_state_teams if team.stage != "Attente" and not (team.wildcard_bracket or team.wildcard_groupstage)], if team.stage != "Attente"
key=lambda t: ( ]
t.registration_date is None, t.registration_date or datetime.min, t.initial_weight, t.team_registration.id
) if previous_state.team_sorting == TeamSortingType.INSCRIPTION_DATE else
(t.initial_weight, t.team_registration.id)
)
teams_out_to_warn = sorted_teams[-teams_to_remove_count:]
elif previous_state.team_count < instance.team_count: 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_in_to_warn = [ teams_in_to_warn = [
team for team in previous_state.teams(True)[(instance.team_count - previous_state.team_count):] team for team in teams_that_will_be_in
if team.stage == "Attente" if team.stage == "Attente"
] ]

@ -35,7 +35,7 @@ body {
} }
label { label {
color: #707070; color: #505050;
font-size: 1.1em; font-size: 1.1em;
} }
@ -55,7 +55,7 @@ footer {
} }
a { a {
color: #707070; color: #505050;
} }
a:hover { a:hover {
@ -73,7 +73,7 @@ nav {
} }
nav a { nav a {
color: #707070; color: #505050;
padding: 8px 12px; padding: 8px 12px;
background-color: #fae7ce; background-color: #fae7ce;
border-radius: 12px; border-radius: 12px;
@ -161,7 +161,7 @@ tr {
.rounded-button { .rounded-button {
background-color: #fae7ce; /* Green background */ background-color: #fae7ce; /* Green background */
color: #707070; /* White text */ color: #505050; /* White text */
padding: 15px 32px; /* Some padding */ padding: 15px 32px; /* Some padding */
font-size: 1em; font-size: 1em;
font-weight: 800; font-weight: 800;
@ -193,7 +193,7 @@ tr {
} }
.mybox { .mybox {
color: #707070; color: #505050;
padding: 8px 12px; padding: 8px 12px;
background-color: #fae7ce; background-color: #fae7ce;
border-radius: 12px; border-radius: 12px;
@ -260,6 +260,11 @@ tr {
font-size: 1.2em; font-size: 1.2em;
} }
.very-large {
font-family: "Montserrat-SemiBold";
font-size: 1.4em;
}
@media screen and (max-width: 40em) { @media screen and (max-width: 40em) {
.large { .large {
font-size: 0.9em; font-size: 0.9em;
@ -278,7 +283,7 @@ tr {
.info { .info {
font-family: "Montserrat-SemiBold"; font-family: "Montserrat-SemiBold";
font-size: 0.9em; font-size: 0.9em;
color: #707070; color: #505050;
} }
.small { .small {
@ -286,7 +291,7 @@ tr {
} }
.minor-info { .minor-info {
color: #707070; color: #505050;
font-size: 0.85em; font-size: 0.85em;
} }
@ -362,7 +367,7 @@ tr {
.separator { .separator {
height: 1px; height: 1px;
background-color: #707070; background-color: #505050;
margin: 5px 0px; margin: 5px 0px;
} }
@ -608,12 +613,134 @@ h-margin {
padding: 5px 0px; padding: 5px 0px;
} }
.table-row-4-colums-tournament { .table-row-5-colums-tournament {
display: grid; display: grid;
grid-template-columns: auto 1fr auto auto; grid-template-columns: 75px 90px 1fr 120px;
align-items: center; align-items: center;
/* Vertically center the content within each column */ gap: 4px;
padding: 5px 0px; }
.very-large.club-name {
font-size: 1.2em;
}
.table-row-5-colums-tournament.header {
grid-template-columns: 1fr auto; /* Override to just 2 columns for header */
justify-content: space-between;
}
.table-row-5-colums-tournament.footer {
grid-template-columns: 1fr; /* Override to just 2 columns for header */
text-align: center; /* Center the text content */
width: 100%;
color: gray;
text-decoration: underline !important; /* Ensures the link is underlined */
}
@media screen and (max-width: 800px) {
/* Adjust breakpoint as needed */
.table-row-5-colums-tournament {
grid-template-columns: 60px 70px 1fr 80px;
gap: 2px;
}
.small {
font-size: 1em;
}
.very-large {
font-size: 1.4em;
}
.very-large.club-name {
font-size: 1.2em;
}
}
@media screen and (max-width: 400px) {
/* Adjust breakpoint as needed */
.table-row-5-colums-tournament {
grid-template-columns: 55px 65px 1fr 75px;
gap: 2px;
}
.small {
font-size: 0.9em;
}
.very-large {
font-size: 1.3em;
}
.very-large.club-name {
font-size: 1em;
}
}
.light-green {
background-color: #90ee90 !important;
}
.light-yellow {
background-color: #fed300 !important;
}
.light-orange {
color: white !important;
background-color: #f39200 !important;
}
.light-red {
background-color: #e84039 !important;
color: white !important;
}
.table-row-element {
width: 100%;
line-height: 1.2;
padding: 8px 8px;
align-items: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; /* Prevents text from wrapping to a new line */
max-width: 100%; /* Ensures children don't overflow */
}
.table-row-element.tournament-date {
grid-column: 1;
color: #505050;
background-color: #fae7ce;
border-radius: 12px;
}
.table-row-element.tournament-type {
grid-column: 2;
}
.table-row-element.tournament-name {
grid-column: 3;
align-self: center; /* Align in grid cell vertically */
margin: auto 0; /* Alternative vertical centering */
}
.very-large.club-name {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3; /* Limit to 2 lines */
-webkit-box-orient: vertical;
white-space: normal;
/* Keep any existing styling for .large */
}
.table-row-element.tournament-status {
grid-column: 4;
}
.box {
color: #505050;
border-radius: 12px;
padding: 4px;
} }
.table-row-6-colums-club-tournament { .table-row-6-colums-club-tournament {
@ -867,7 +994,7 @@ h-margin {
.match-result a:hover { .match-result a:hover {
background-color: #fae7ce; background-color: #fae7ce;
color: #707070; color: #505050;
} }
.group-stage-link { .group-stage-link {

@ -58,7 +58,7 @@
} }
.round-name { .round-name {
color: #707070; color: #505050;
font-size: 1.5em; font-size: 1.5em;
padding: 8px 12px; padding: 8px 12px;
white-space: nowrap; /* Prevent text wrapping */ white-space: nowrap; /* Prevent text wrapping */
@ -67,7 +67,7 @@
.round-format { .round-format {
font-size: 0.9em; font-size: 0.9em;
color: #707070; color: #505050;
margin-top: -5px; /* Reduced from -10px to bring it closer */ margin-top: -5px; /* Reduced from -10px to bring it closer */
white-space: nowrap; /* Prevent text wrapping */ white-space: nowrap; /* Prevent text wrapping */
display: block; /* Ensure proper centering */ display: block; /* Ensure proper centering */
@ -199,7 +199,7 @@
.broadcast-mode .round-name, .broadcast-mode .round-name,
.broadcast-mode .round-format { .broadcast-mode .round-format {
padding: 0px; padding: 0px;
color: #707070; color: #505050;
} }
.broadcast-mode .round-title { .broadcast-mode .round-title {
@ -215,7 +215,7 @@
.outgoing-line, .outgoing-line,
.outgoing-line-upward, .outgoing-line-upward,
.outgoing-line-downward { .outgoing-line-downward {
background-color: #707070 !important; /* Bright yellow - change to your preferred color */ background-color: #505050 !important; /* Bright yellow - change to your preferred color */
} }
/* Broadcast mode styling for all lines */ /* Broadcast mode styling for all lines */

@ -20,27 +20,34 @@
{% load static %} {% load static %}
{% load tz %} {% load tz %}
{% if form.errors or password_change_form.errors %}
<div class="cell medium-6 large-6 topblock my-block"> <div class="cell medium-6 large-6 topblock my-block">
<div class="bubble"> <div class="bubble">
<label class="title">Mes informations</label> <div>
{% if form.errors %} {% for field in form %}
<div class="alert"> {% if field.errors %}
{% for field, errors in form.errors.items %} {% for error in field.errors %}
{% for error in errors %} <div class="alert">{{ field.label }} : {{ error }}</div>
<p>{{ error }}</p>
{% endfor %} {% endfor %}
{% endif %}
{% endfor %} {% endfor %}
</div> </div>
<div>
{% for field in password_change_form %}
{% if field.errors %}
{% for error in field.errors %}
<div class="alert">{{ field.label }} : {{ error }}</div>
{% endfor %}
{% endif %} {% endif %}
<!-- Add non-field errors (if any) -->
{% if form.non_field_errors %}
<div class="alert">
{% for error in form.non_field_errors %}
<p>{{ error }}</p>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} </div>
</div>
{% endif %}
<div class="cell medium-6 large-6 topblock my-block">
<div class="bubble">
<label class="title">Mes informations</label>
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{{ form.as_p }} {{ form.as_p }}
@ -51,25 +58,7 @@
<div class="cell medium-6 large-6 topblock my-block"> <div class="cell medium-6 large-6 topblock my-block">
<div class="bubble"> <div class="bubble">
<label class="title">Mot de passe</label> <label class="title">Mot de passe</label>
{% if password_change_form.errors %} <form method="post" action="{% url 'custom_password_change' %}">
<div class="alert">
{% for field, errors in password_change_form.errors.items %}
{% for error in errors %}
<p>{{ error }}</p>
{% endfor %}
{% endfor %}
</div>
{% endif %}
{% if password_change_form.non_field_errors %}
<div class="alert">
{% for error in password_change_form.non_field_errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<form method="post" action="{% url 'password_change' %}">
{% csrf_token %} {% csrf_token %}
{{ password_change_form.as_p }} {{ password_change_form.as_p }}
<button type="submit" class="rounded-button">Modifier le mot de passe</button> <button type="submit" class="rounded-button">Modifier le mot de passe</button>

@ -26,6 +26,29 @@
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. Un email de confirmation a été envoyé à l'adresse associée à votre compte Padel Club ({{ user.email }}). Pensez à vérifier vos spams si vous ne recevez pas l'email. En cas de problème, contactez le juge-arbitre.
</p> </p>
{% else %} {% else %}
{% if team_form.errors %}
<div>
{% for field in team_form %}
{% if field.errors %}
{% for error in field.errors %}
<div class="alert">{{ field.label }} : {{ error }}</div>
{% endfor %}
{% endif %}
{% endfor %}
</div>
{% endif %}
{% if messages %}
<div class="messages">
{% for message in messages %}
<div class="alert {% if message.tags %}alert-{{ message.tags }}{% endif %}">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
@ -36,6 +59,12 @@
Informations de contact Informations de contact
</div> </div>
</p> </p>
{% if team_form.non_field_errors %}
{% for error in team_form.non_field_errors %}
<p>{{ error }}</p>
{% endfor %}
{% endif %}
{{ team_form.as_p }} <!-- Render team registration form fields here --> {{ team_form.as_p }} <!-- Render team registration form fields here -->
</div> </div>
@ -108,24 +137,6 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
<div class="margin10">
{% if form.errors %}
<div class="alert alert-error">
{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
<p>{{ error }}</p>
{% endfor %}
{% endif %}
{% for field in form %}
{% for error in field.errors %}
<p>{{ error }}</p>
{% endfor %}
{% endfor %}
</div>
{% endif %}
</div>
<button type="submit" name="add_player" class="rounded-button"> <button type="submit" name="add_player" class="rounded-button">
{% if add_player_form.user_without_licence %} {% if add_player_form.user_without_licence %}
Confirmer Confirmer

@ -11,7 +11,7 @@
<div class="grid-x"> <div class="grid-x">
<div class="bubble"> <div class="bubble">
<div class="cell medium-6 large-6 my-block"> <div class="cell medium-6 large-6 my-block">
{% if form.errors %} {% if form.non_field_errors %}
<div class="alert alert-error"> <div class="alert alert-error">
{% if form.non_field_errors %} {% if form.non_field_errors %}
{% for error in form.non_field_errors %} {% for error in form.non_field_errors %}
@ -28,7 +28,9 @@
{% endif %} {% endif %}
<form method="post" action="{% url 'custom-login' %}"> <form method="post" action="{% url 'custom-login' %}">
{% csrf_token %} {% csrf_token %}
{% if request.GET.next and 'reset' not in request.GET.next and 'password_reset' not in request.GET.next %}
<input type="hidden" name="next" value="{{ request.GET.next }}"> <input type="hidden" name="next" value="{{ request.GET.next }}">
{% endif %}
<label for="username">Identifiant ou e-mail </label> <label for="username">Identifiant ou e-mail </label>
<input type="text" name="username" id="username" required> <input type="text" name="username" id="username" required>

@ -11,20 +11,8 @@
{% load tz %} {% load tz %}
<div class="grid-x"> <div class="grid-x">
<div class="cell medium-6 large-6 topblock my-block"> <div class="cell medium-10 large-5 topblock my-block">
<div class="bubble"> <div>
<label class="title">Vos tournois à venir</label>
{% if upcoming_tournaments %}
{% for tournament in upcoming_tournaments %}
{% include 'tournaments/tournament_row.html' %}
{% endfor %}
{% else %}
Aucun tournoi à venir
{% endif %}
</div>
</div>
<div class="cell medium-6 large-6 topblock my-block">
<div class="bubble">
<label class="title">Vos tournois en cours</label> <label class="title">Vos tournois en cours</label>
{% if running_tournaments %} {% if running_tournaments %}
{% for tournament in running_tournaments %} {% for tournament in running_tournaments %}
@ -35,9 +23,22 @@
{% endif %} {% endif %}
</div> </div>
<div>
<label class="title">Vos tournois à venir</label>
{% if upcoming_tournaments %}
{% for tournament in upcoming_tournaments %}
{% include 'tournaments/tournament_row.html' %}
{% endfor %}
{% else %}
Aucun tournoi à venir
{% endif %}
</div>
</div> </div>
<div class="cell medium-6 large-6 topblock my-block">
<div class="bubble"> <div class="cell large-offset-1 medium-10 large-5 topblock my-block">
<div>
<label class="title">Vos tournois terminés</label> <label class="title">Vos tournois terminés</label>
{% if ended_tournaments %} {% if ended_tournaments %}
{% for tournament in ended_tournaments %} {% for tournament in ended_tournaments %}

@ -10,9 +10,9 @@
<p> <p>
Votre mot de passe a été réinitialisé avec succès. Vous pouvez maintenant vous connecter avec votre nouveau mot de passe. Votre mot de passe a été réinitialisé avec succès. Vous pouvez maintenant vous connecter avec votre nouveau mot de passe.
</p> </p>
<p> <div class="topmargin20">
<a href="{% url 'login' %}" class="rounded-button">Se connecter</a> <a href="{% url 'login' %}" class="rounded-button">Se connecter</a>
</p> </div>
</div> </div>
</div> </div>
</div> </div>

@ -7,17 +7,6 @@
<div class="grid-x"> <div class="grid-x">
<div class="bubble"> <div class="bubble">
<div class="cell medium-6 large-6 my-block"> <div class="cell medium-6 large-6 my-block">
{% if form.errors %}
<div class="alert">
{% for field, errors in form.errors.items %}
{% for error in errors %}
<p>{{ error }}</p>
{% endfor %}
{% endfor %}
</div>
{% endif %}
<!-- Add non-field errors (if any) -->
{% if form.non_field_errors %} {% if form.non_field_errors %}
<div class="alert"> <div class="alert">
{% for error in form.non_field_errors %} {% for error in form.non_field_errors %}
@ -35,13 +24,10 @@
<button type="submit" class="rounded-button">Réinitialiser le mot de passe</button> <button type="submit" class="rounded-button">Réinitialiser le mot de passe</button>
</form> </form>
<p> <div class="topmargin20">
<a href="{% url 'login' %}" class="styled-link">Retour à la connexion</a> <a href="{% url 'login' %}" class="styled-link">Retour à la connexion</a>
</p>
</div> </div>
{% for message in messages %} </div>
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

@ -11,9 +11,9 @@
Un e-mail contenant un lien pour réinitialiser votre mot de passe a été envoyé à votre adresse. Un e-mail contenant un lien pour réinitialiser votre mot de passe a été envoyé à votre adresse.
Veuillez vérifier votre boîte de réception. Veuillez vérifier votre boîte de réception.
</p> </p>
<p> <div class="topmargin20">
<a href="{% url 'login' %}" class="styled-link">Retour à la connexion</a> <a href="{% url 'login' %}" class="styled-link">Retour à la connexion</a>
</p> </div>
</div> </div>
</div> </div>
</div> </div>

@ -7,16 +7,6 @@
<div class="grid-x"> <div class="grid-x">
<div class="bubble"> <div class="bubble">
<div class="cell medium-6 large-6 my-block"> <div class="cell medium-6 large-6 my-block">
{% if form.errors %}
<div class="alert">
{% for field, errors in form.errors.items %}
{% for error in errors %}
<p>{{ error }}</p>
{% endfor %}
{% endfor %}
</div>
{% endif %}
<!-- Add non-field errors (if any) --> <!-- Add non-field errors (if any) -->
{% if form.non_field_errors %} {% if form.non_field_errors %}
<div class="alert"> <div class="alert">
@ -31,13 +21,10 @@
<input type="email" name="email" id="email" required> <input type="email" name="email" id="email" required>
<button type="submit" class="rounded-button">Envoyer le lien de réinitialisation</button> <button type="submit" class="rounded-button">Envoyer le lien de réinitialisation</button>
</form> </form>
<p> <div class="topmargin20">
<a href="{% url 'login' %}" class="styled-link">Retour à la connexion</a> <a href="{% url 'login' %}" class="styled-link">Retour à la connexion</a>
</p>
</div> </div>
{% for message in messages %} </div>
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

@ -8,20 +8,25 @@
{% load static %} {% load static %}
{% load tz %} {% load tz %}
<div class="grid-x"> <div class="grid">
<div class="bubble">
<div class="cell medium-6 large-6 my-block">
{% if form.errors %} {% if form.errors %}
<div class="alert"> <div class="cell medium-6 large-6 topblock my-block">
{% for field, errors in form.errors.items %} <div class="bubble">
{% for error in errors %} <div>
<p>{{ error }}</p> {% for field in form %}
{% if field.errors %}
{% for error in field.errors %}
<div class="alert">{{ field.label }} : {{ error }}</div>
{% endfor %} {% endfor %}
{% endif %}
{% endfor %} {% endfor %}
</div> </div>
</div>
</div>
{% endif %} {% endif %}
<!-- Add non-field errors (if any) --> <div class="cell medium-6 large-6 my-block">
<div class="bubble">
{% if form.non_field_errors %} {% if form.non_field_errors %}
<div class="alert"> <div class="alert">
{% for error in form.non_field_errors %} {% for error in form.non_field_errors %}
@ -36,7 +41,7 @@
<button type="submit" class="rounded-button">Créer votre compte</button> <button type="submit" class="rounded-button">Créer votre compte</button>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

@ -47,12 +47,6 @@
{% if tournament.is_unregistration_possible %} {% if tournament.is_unregistration_possible %}
<p> <p>
<div class="margin10">
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
</div>
<a href="{% url 'unregister_tournament' tournament.id %}" <a href="{% url 'unregister_tournament' tournament.id %}"
class="rounded-button destructive-button" class="rounded-button destructive-button"
onclick="return confirm('Êtes-vous sûr de vouloir vous désinscrire ?');"> onclick="return confirm('Êtes-vous sûr de vouloir vous désinscrire ?');">
@ -164,9 +158,6 @@
</p> </p>
{% endif %} {% endif %}
{% else %} {% else %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
<p> <p>
<div> <div>
<a href="{% url 'login' %}?next={{ request.path }}" class="styled-link">Connectez-vous !</a> <a href="{% url 'login' %}?next={{ request.path }}" class="styled-link">Connectez-vous !</a>

@ -1,43 +1,56 @@
{% load tournament_tags %}
{% with status=tournament.get_online_registration_status %}
<a href="{% url 'tournament' tournament.id %}"> <a href="{% url 'tournament' tournament.id %}">
<div class="table-row-4-colums-tournament vertical-padding"> <div class="table-row-5-colums-tournament vertical-padding">
<div class="tight table-cell"> <div class="table-row-element tournament-date center">
<div class="large">{{ tournament.level }}</div> <div class="small">{{ tournament.week_day }}</div>
<div class="very-large">{{ tournament.day }}</div>
<div class="small">{{ tournament.month }}</div>
</div>
<div class="table-row-element tournament-type">
<div class="very-large">{{ tournament.level }}</div>
{% if tournament.category %}
<div class="small">{{ tournament.category }}</div> <div class="small">{{ tournament.category }}</div>
{% endif %}
{% if tournament.age %} {% if tournament.age %}
<div class="small">{{ tournament.age }}</div> <div class="small">{{ tournament.age }}</div>
{% endif %} {% endif %}
</div> </div>
<div class="table-cell-responsive-large table-cell-large horizontal-padding semibold"> <div class="table-row-element tournament-name">
<div><span>{{ tournament.event.club.name }}</span></div> <div class="very-large club-name">{{ tournament.event.club.name }}</div>
{% if tournament.name_and_event %} <div class="small">{{ tournament.localized_day_duration }}</div>
<div> </div>
<span>{{ tournament.name_and_event }}</span> <div class="table-row-element tournament-status center">
<div class="small">Équipes</div>
<div class="very-large">{{ tournament.get_tournament_status_team_count }}</div>
{% if user.is_authenticated %}
{% with user_registration_status=tournament|get_player_status:user %}
{% if user_registration_status %}
{% if user_registration_status.display_box %}
<div class="box small {{ user_registration_status.box_class }}">
{{ user_registration_status.short_label }}
</div> </div>
{% endif %} {% endif %}
{% else %}
{% if status.display_box %}
<div class="box small {{ status.box_class }}">
{{ status.short_label }}
</div> </div>
<div class="table-cell-responsive-short table-cell-large horizontal-padding semibold">
<div><span>{{ tournament.event.club.computedShortTitle }}</span></div>
{% if tournament.event.name %}
<div><span>{{ tournament.event.name }}</span></div>
{% endif %} {% endif %}
{% if tournament.name %}
<div><span>{{ tournament.name }}</span></div>
{% endif %} {% endif %}
</div> {% endwith %}
{% else %}
{% if tournament.tournament_status_display %} {% if status.display_box %}
<div class="table-cell-responsive-large right horizontal-padding"> <div class="box small {{ status.box_class }}">
{{ tournament.tournament_status_display|linebreaksbr }} {{ status.short_label }}
</div> </div>
{% endif %} {% endif %}
<div class="table-cell">
<div class="mybox center">{{ tournament.formatted_start_date }}</div>
{% if tournament.tournament_status_display %}
<div class="table-cell-responsive-short small center">{{ tournament.tournament_status_display|linebreaksbr }}</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</a> </a>
{% if not forloop.last %} {% if not forloop.last %}
<hr/> <hr/>
{% endif %} {% endif %}
{% endwith %}

@ -15,47 +15,55 @@
<div class="grid-x"> <div class="grid-x">
{% if live or future %} {% if live or future %}
<div class="cell medium-6 large-6 topblock my-block"> <div class="cell medium-10 large-5 topblock my-block">
<div class="bubble"> <div>
{% if live %} {% if live %}
<div class="table-row-5-colums-tournament header">
<label class="title">En cours</label> <label class="title">En cours</label>
</div>
{% for tournament in live %} {% for tournament in live %}
{% include 'tournaments/tournament_row.html' %} {% include 'tournaments/tournament_row.html' %}
{% endfor %} {% endfor %}
<div class="table-row-5-colums-tournament footer">
{% if live|length >= 10 %} {% if live|length >= 10 %}
<div class="right"> <div class="small">
{% if club %} {% if club %}
<a href="{% url 'tournaments' %}?filter=1&club={{ club.id }}">Voir tous...</a> <a href="{% url 'tournaments' %}?filter=1&club={{ club.id }}">voir tous les tournois en cours</a>
{% else %} {% else %}
<a href="{% url 'tournaments' %}?filter=1">Voir tous...</a> <a href="{% url 'tournaments' %}?filter=1">voir tous les tournois en cours</a>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
</div>
{% endif %} {% endif %}
{% if future %} {% if future %}
<div class="table-row-5-colums-tournament header">
<label class="title">À venir</label> <label class="title">À venir</label>
</div>
{% for tournament in future %} {% for tournament in future %}
{% include 'tournaments/tournament_row.html' %} {% include 'tournaments/tournament_row.html' %}
{% endfor %} {% endfor %}
<div class="table-row-5-colums-tournament footer">
{% if future|length >= 10 %} {% if future|length >= 10 %}
<div class="right"> <div class="small">
{% if club %} {% if club %}
<a href="{% url 'tournaments' %}?filter=0&club={{ club.id }}">Voir tous...</a> <a href="{% url 'tournaments' %}?filter=0&club={{ club.id }}">voir tous les tournois à venir</a>
{% else %} {% else %}
<a href="{% url 'tournaments' %}?filter=0">Voir tous...</a> <a href="{% url 'tournaments' %}?filter=0">voir tous les tournois à venir</a>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
</div>
{% endif %} {% endif %}
@ -65,25 +73,26 @@
{% endif %} {% endif %}
{% if ended %} {% if ended %}
<div class="cell medium-6 large-6 topblock my-block"> <div class="cell large-offset-1 medium-10 large-5 topblock my-block">
<div class="bubble"> <div>
<div class="table-row-5-colums-tournament header">
<label class="title">Terminés</label> <label class="title">Terminés</label>
</div>
{% for tournament in ended %} {% for tournament in ended %}
{% include 'tournaments/tournament_row.html' %} {% include 'tournaments/tournament_row.html' %}
{% endfor %} {% endfor %}
<div class="table-row-5-colums-tournament footer">
{% if ended|length >= 10 %} {% if ended|length >= 10 %}
<div class="right"> <div class="small">
{% if club %} {% if club %}
<a href="{% url 'tournaments' %}?filter=2&club={{ club.id }}">Voir tous...</a> <a href="{% url 'tournaments' %}?filter=2&club={{ club.id }}">voir tous les tournois terminés</a>
{% else %} {% else %}
<a href="{% url 'tournaments' %}?filter=2">Voir tous...</a> <a href="{% url 'tournaments' %}?filter=2">voir tous les tournois terminés</a>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
</div>
</div> </div>
</div> </div>
{% endif %} {% endif %}

@ -11,9 +11,9 @@
<div class="grid-x"> <div class="grid-x">
{% if tournaments %} {% if tournaments %}
<div class="cell medium-6 large-6 topblock my-block"> <div class="cell medium-10 large-5 topblock my-block">
<div class="bubble"> <div>
{% for tournament in tournaments %} {% for tournament in tournaments %}
{% include 'tournaments/tournament_row.html' %} {% include 'tournaments/tournament_row.html' %}

@ -0,0 +1,7 @@
from django import template
register = template.Library()
@register.filter
def get_player_status(tournament, user):
return tournament.get_player_registration_status_by_licence(user)

@ -55,13 +55,7 @@ urlpatterns = [
path('utils/xls-to-csv/', views.xls_to_csv, name='xls-to-csv'), path('utils/xls-to-csv/', views.xls_to_csv, name='xls-to-csv'),
path('mail-test/', views.simple_form_view, name='mail-test'), path('mail-test/', views.simple_form_view, name='mail-test'),
path('login/', CustomLoginView.as_view(), name='custom-login'), path('login/', CustomLoginView.as_view(), name='custom-login'),
path('password_change/', path('custom_password_change/', views.custom_password_change, name='custom_password_change'),
auth_views.PasswordChangeView.as_view(
success_url='/profile/', # Redirect back to profile after success
form_class=CustomPasswordChangeForm
),
name='password_change'
),
path('logout/', views.custom_logout, name='custom_logout'), path('logout/', views.custom_logout, name='custom_logout'),
path('signup/', views.signup, name='signup'), # URL pattern for signup path('signup/', views.signup, name='signup'), # URL pattern for signup
# path('profile/', views.profile, name='profile'), # URL pattern for signup # path('profile/', views.profile, name='profile'), # URL pattern for signup
@ -70,7 +64,9 @@ urlpatterns = [
path('tournaments/<str:tournament_id>/unregister/', views.unregister_tournament, name='unregister_tournament'), path('tournaments/<str:tournament_id>/unregister/', views.unregister_tournament, name='unregister_tournament'),
path('password_reset/', auth_views.PasswordResetView.as_view(), name='password_reset'), path('password_reset/', auth_views.PasswordResetView.as_view(), name='password_reset'),
path('password_reset_done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'), path('password_reset_done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'),
path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'), path('reset/done/',
views.CustomPasswordResetCompleteView.as_view(),
name='password_reset_complete'),
path('profile/', views.ProfileUpdateView.as_view(), name='profile'), path('profile/', views.ProfileUpdateView.as_view(), name='profile'),
path('admin/tournament-import/', views.tournament_import_view, name='tournament_import'), path('admin/tournament-import/', views.tournament_import_view, name='tournament_import'),
path('admin/status/', views.status_page, name='status_page'), path('admin/status/', views.status_page, name='status_page'),

@ -2,6 +2,11 @@
import os import os
import csv import csv
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.views import PasswordResetCompleteView
from django.shortcuts import redirect
from django.contrib.auth import login
from django.contrib.auth import get_user_model
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.http import JsonResponse, HttpResponse from django.http import JsonResponse, HttpResponse
from django.utils.encoding import force_str from django.utils.encoding import force_str
@ -818,6 +823,54 @@ class CustomPasswordResetConfirmView(PasswordResetConfirmView):
except (TypeError, ValueError, User.DoesNotExist): except (TypeError, ValueError, User.DoesNotExist):
raise Http404("User not found") raise Http404("User not found")
class CustomPasswordResetCompleteView(PasswordResetCompleteView):
template_name = 'registration/password_reset_complete.html'
def get(self, request, *args, **kwargs):
# Get the user from the session
username = request.session.get('reset_username')
if username:
try:
# Get the user
User = get_user_model()
user = User.objects.get(username=username)
# Log the user in
login(request, user, backend='django.contrib.auth.backends.ModelBackend')
# Clean up the session
if 'reset_username' in request.session:
del request.session['reset_username']
# Redirect to the profile page
return redirect('profile')
except User.DoesNotExist:
pass
# If no username in session or user not found, proceed with normal view
return super().get(request, *args, **kwargs)
@login_required
def custom_password_change(request):
if request.method == 'POST':
form = CustomPasswordChangeForm(user=request.user, data=request.POST)
if form.is_valid():
user = form.save()
update_session_auth_hash(request, user) # Important to keep user logged in
messages.success(request, 'Votre mot de passe a été mis à jour avec succès!')
return redirect('profile')
else:
# Form is invalid, show errors
profile_form = ProfileUpdateForm(instance=request.user)
return render(request, 'profile.html', {
'form': profile_form,
'password_change_form': form
})
# If not POST, redirect to profile page
return redirect('profile')
@login_required @login_required
def my_tournaments(request): def my_tournaments(request):
user = request.user user = request.user

Loading…
Cancel
Save