diff --git a/shop/static/shop/css/shop.css b/shop/static/shop/css/shop.css index 105aaf3..824a705 100644 --- a/shop/static/shop/css/shop.css +++ b/shop/static/shop/css/shop.css @@ -95,7 +95,7 @@ .add-to-cart-button, .checkout-button { background-color: #90ee90; - color: #707070; + color: #505050; border: none; border-radius: 12px; font-size: 12px; @@ -120,7 +120,7 @@ } .coupon-section { - color: #707070; + color: #505050; font-size: 12px; font-weight: 600; text-decoration: none; @@ -129,7 +129,7 @@ .confirm-nav-button { background-color: #90ee90; - color: #707070; + color: #505050; font-size: 12px; font-weight: 600; text-decoration: none; diff --git a/shop/templates/shop/product_item.html b/shop/templates/shop/product_item.html index 4cdb4bb..bb7c275 100644 --- a/shop/templates/shop/product_item.html +++ b/shop/templates/shop/product_item.html @@ -185,7 +185,7 @@ function addToCartAjax(productId) { notification.style.right = '20px'; notification.style.padding = '20px'; notification.style.backgroundColor = '#90ee90'; - notification.style.color = '#707070'; + notification.style.color = '#505050'; notification.style.borderRadius = '12px'; notification.style.zIndex = '9999'; notification.style.opacity = '0'; diff --git a/tournaments/custom_views.py b/tournaments/custom_views.py index 01d54bb..4e3d2f1 100644 --- a/tournaments/custom_views.py +++ b/tournaments/custom_views.py @@ -10,19 +10,33 @@ class CustomLoginView(auth_views.LoginView): def get_success_url(self): # First check the 'next' parameter which has higher priority 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(): + # 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 # Then check if we have a stored referrer URL referrer = self.request.session.get('login_referrer') if referrer: - # Clear the stored referrer to prevent reuse - del self.request.session['login_referrer'] - return 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 + del self.request.session['login_referrer'] + return referrer # Fall back to default return reverse('index') 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 return super().get(request, *args, **kwargs) diff --git a/tournaments/forms.py b/tournaments/forms.py index cc4d314..872514b 100644 --- a/tournaments/forms.py +++ b/tournaments/forms.py @@ -12,6 +12,11 @@ from django.utils.encoding import force_bytes class CustomUserCreationForm(UserCreationForm): 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: model = CustomUser @@ -33,6 +38,22 @@ class CustomUserCreationForm(UserCreationForm): class SimpleCustomUserCreationForm(UserCreationForm): 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: model = CustomUser 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): 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}") - + if licence_id: + licence_id = licence_id.replace(' ', '').strip().upper() return licence_id def clean_last_name(self): @@ -193,6 +206,22 @@ class ProfileUpdateForm(forms.ModelForm): # Remove autofocus from the 'username' field 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: model = CustomUser fields = ['first_name', 'last_name', 'licence_id', 'username', 'email', 'phone'] diff --git a/tournaments/migrations/0113_tournament_team_count_limit.py b/tournaments/migrations/0113_tournament_team_count_limit.py new file mode 100644 index 0000000..2b113cb --- /dev/null +++ b/tournaments/migrations/0113_tournament_team_count_limit.py @@ -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), + ), + ] diff --git a/tournaments/models/enums.py b/tournaments/models/enums.py index d2e5187..8a2f5e4 100644 --- a/tournaments/models/enums.py +++ b/tournaments/models/enums.py @@ -146,6 +146,7 @@ class OnlineRegistrationStatus(models.IntegerChoices): WAITING_LIST_FULL = 6, 'Waiting List Full' IN_PROGRESS = 7, 'In Progress' ENDED_WITH_RESULTS = 8, 'Ended with Results' + CANCELED = 9, 'Canceled' def status_localized(self) -> str: status_map = { @@ -156,10 +157,58 @@ class OnlineRegistrationStatus(models.IntegerChoices): OnlineRegistrationStatus.WAITING_LIST_POSSIBLE: "Liste d'attente ouverte", OnlineRegistrationStatus.WAITING_LIST_FULL: "Liste d'attente complète", 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, "") + 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): ADMIN = 0, 'Admin' SITE = 1, 'Site' diff --git a/tournaments/models/player_registration.py b/tournaments/models/player_registration.py index e23be85..9d00faa 100644 --- a/tournaments/models/player_registration.py +++ b/tournaments/models/player_registration.py @@ -1,5 +1,5 @@ from django.db import models -from . import SideStoreModel, TeamRegistration, PlayerSexType, PlayerDataSource, PlayerPaymentType, FederalCategory +from . import SideStoreModel, TeamRegistration, PlayerSexType, PlayerDataSource, PlayerPaymentType, OnlineRegistrationStatus import uuid from django.utils import timezone @@ -92,3 +92,52 @@ class PlayerRegistration(SideStoreModel): return "1ère" return "1er" 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 diff --git a/tournaments/models/team_registration.py b/tournaments/models/team_registration.py index 5d468d1..ebb2e08 100644 --- a/tournaments/models/team_registration.py +++ b/tournaments/models/team_registration.py @@ -240,10 +240,16 @@ class TeamRegistration(SideStoreModel): 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 == 1: - return "1er" + self.ranking_delta() - return f"{self.final_ranking}ème" + self.ranking_delta() + return "1er" + return f"{self.final_ranking}ème" return None def ranking_delta(self): diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index 51dcd7f..00dde56 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -1,4 +1,3 @@ -from time import daylight from zoneinfo import ZoneInfo from django.db import models from typing import TYPE_CHECKING @@ -8,12 +7,13 @@ if TYPE_CHECKING: from . import BaseModel, Event, TournamentPayment, FederalMatchCategory, FederalCategory, FederalLevelCategory, FederalAgeCategory, OnlineRegistrationStatus import uuid from django.utils import timezone, formats -from datetime import datetime, timedelta -from zoneinfo import ZoneInfo +from datetime import datetime, timedelta, time from tournaments.utils.player_search import get_player_name_from_csv from shared.cryptography import encryption_util from ..utils.extensions import plural_format +from django.utils.formats import date_format +from ..utils.licence_validator import LicenseValidator class TeamSortingType(models.IntegerChoices): RANK = 1, 'Rank' @@ -67,6 +67,7 @@ class Tournament(BaseModel): initial_seed_round = models.IntegerField(default=0) initial_seed_count = models.IntegerField(default=0) 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 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 @@ -230,36 +231,12 @@ class Tournament(BaseModel): else: return None - def tournament_status_display(self): - if self.is_canceled() is True: - return "Annulé" - - teams = self.teams(True) - if self.supposedly_in_progress() or self.end_date is not None or self.should_be_over(): - teams = [t for t in teams if t.stage != "Attente"] - 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 get_tournament_status(self): + return self.get_online_registration_status().status_localized() + + def get_tournament_status_team_count(self): + active_teams_count = self.team_registrations.filter(walk_out=False).count() + return min(active_teams_count, self.team_count) def name_and_event(self): event_name = None @@ -331,9 +308,9 @@ class Tournament(BaseModel): index = i # Check if team_count exists - if self.team_count: + if self.team_count_limit == True: # Team is not in list - if index < self.team_count: + if index < 0: print("Team is not in list", index, self.team_count) return -1 # Return position in waiting list relative to target count @@ -397,7 +374,7 @@ class Tournament(BaseModel): complete_teams.append(team) else: waiting_teams.append(team) - wildcard_bracket = [] + 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): @@ -1051,19 +1028,22 @@ class Tournament(BaseModel): def options_online_registration(self): options = [] + timezone = self.timezone() # Date d'ouverture 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}") # Date limite 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(self.get_selection_status_localized) + # Cible d'équipes - if self.team_count: + if self.team_count_limit is True: options.append(f"Maximum {self.team_count} équipes") # Liste d'attente @@ -1122,37 +1102,65 @@ class Tournament(BaseModel): return False 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): - if self.supposedly_in_progress(): - return OnlineRegistrationStatus.ENDED - if self.closed_registration_date is not None: - return OnlineRegistrationStatus.ENDED - if self.end_date is not None: - return OnlineRegistrationStatus.ENDED_WITH_RESULTS + 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(): + return OnlineRegistrationStatus.ENDED + if self.closed_registration_date is not None: + return OnlineRegistrationStatus.WAITING_LIST_POSSIBLE - now = timezone.now() + now = timezone.now() - if self.opening_registration_date is not None: - timezoned_datetime = timezone.localtime(self.opening_registration_date) - if now < timezoned_datetime: - return OnlineRegistrationStatus.NOT_STARTED - - if self.registration_date_limit is not None: - timezoned_datetime = timezone.localtime(self.registration_date_limit) - if now > timezoned_datetime: - return OnlineRegistrationStatus.ENDED - - if self.team_count is not None: - # Get all team registrations excluding walk_outs - current_team_count = self.team_registrations.exclude(walk_out=True).count() - if current_team_count >= self.team_count: - if self.waiting_list_limit is not None: - waiting_list_count = current_team_count - self.team_count - if waiting_list_count >= self.waiting_list_limit: - return OnlineRegistrationStatus.WAITING_LIST_FULL - return OnlineRegistrationStatus.WAITING_LIST_POSSIBLE + if self.opening_registration_date is not None: + timezoned_datetime = timezone.localtime(self.opening_registration_date) + if now < timezoned_datetime: + return OnlineRegistrationStatus.NOT_STARTED + + if self.registration_date_limit is not None: + timezoned_datetime = timezone.localtime(self.registration_date_limit) + if now > timezoned_datetime: + return OnlineRegistrationStatus.WAITING_LIST_POSSIBLE + + if self.team_sorting == TeamSortingType.RANK: return OnlineRegistrationStatus.OPEN + if self.team_count_limit is True: + # Get all team registrations excluding walk_outs + current_team_count = self.team_registrations.exclude(walk_out=True).count() + if current_team_count >= self.team_count: + if self.waiting_list_limit is not None: + waiting_list_count = current_team_count - self.team_count + if waiting_list_count >= self.waiting_list_limit: + return OnlineRegistrationStatus.WAITING_LIST_FULL + return OnlineRegistrationStatus.WAITING_LIST_POSSIBLE + 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): # Check if tournament has started if self.supposedly_in_progress(): @@ -1174,8 +1182,14 @@ class Tournament(BaseModel): return True 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 self.team_count is None: + if self.team_count_limit is False: return -1 # Get count of active teams (not walked out) @@ -1257,7 +1271,6 @@ class Tournament(BaseModel): current_year += 1 user_age = current_year - int(birth_year) - print("user_age", user_age) # Check age category restrictions if self.federal_age_category == FederalAgeCategory.A11_12 and user_age > 12: @@ -1290,12 +1303,49 @@ class Tournament(BaseModel): def min_player_rank(self): 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: return None waiting_teams = [team for team in teams if team.stage == "Attente"] - if len(waiting_teams) > 0: - return waiting_teams[0].team_registration + return waiting_teams + + 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): # Get matches from broadcasted_matches_and_group_stages @@ -1418,7 +1468,6 @@ class Tournament(BaseModel): def umpire_contact(self): if self.umpire_custom_contact is not None: - print(self.umpire_custom_contact) return self.umpire_custom_contact if self.event and self.event.creator: return self.event.creator.full_name() @@ -1427,17 +1476,96 @@ class Tournament(BaseModel): def umpire_mail(self): if self.umpire_custom_mail is not None: - print(self.umpire_custom_mail) return self.umpire_custom_mail return self.event.creator.email def umpire_phone(self): if self.umpire_custom_phone is not None: - print(self.umpire_custom_phone) return self.umpire_custom_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: def __init__(self, name, matches, formatted_schedule, round_id=None): self.name = name @@ -1489,6 +1617,12 @@ class TeamSummon: } 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): self.names = team_registration.team_names() self.date = team_registration.local_call_date() diff --git a/tournaments/services/email_service.py b/tournaments/services/email_service.py index e24aff8..f8f904c 100644 --- a/tournaments/services/email_service.py +++ b/tournaments/services/email_service.py @@ -2,6 +2,7 @@ from django.core.mail import EmailMessage from django.utils import timezone from django.urls import reverse from enum import Enum +from ..models.tournament import TeamSortingType class TeamEmailType(Enum): 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.") else: 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" link_text = "informations sur le tournoi" diff --git a/tournaments/services/tournament_registration.py b/tournaments/services/tournament_registration.py index 0233edb..d42ae9e 100644 --- a/tournaments/services/tournament_registration.py +++ b/tournaments/services/tournament_registration.py @@ -48,6 +48,13 @@ class TournamentRegistrationService: if not self.context['add_player_form'].is_valid(): 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 licence_id = player_data.get('licence_id', '').upper() @@ -112,6 +119,12 @@ class TournamentRegistrationService: self.context['registration_successful'] = True 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['team_form'] = self.initialize_team_form() self.initialize_session_data() diff --git a/tournaments/signals.py b/tournaments/signals.py index 48859cb..d51286c 100644 --- a/tournaments/signals.py +++ b/tournaments/signals.py @@ -113,21 +113,16 @@ def check_waiting_list(sender, instance, **kwargs): teams_out_to_warn = [] teams_in_to_warn = [] - if previous_state.team_count > instance.team_count: - teams_to_remove_count = previous_state.team_count - instance.team_count - previous_state_teams = previous_state.teams(True) - sorted_teams = sorted( - [team for team in previous_state_teams if team.stage != "Attente" and not (team.wildcard_bracket or team.wildcard_groupstage)], - 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:] + teams_that_will_be_out = instance.teams(True)[instance.team_count:] + teams_out_to_warn = [ + team for team in teams_that_will_be_out + 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_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" ] diff --git a/tournaments/static/tournaments/css/style.css b/tournaments/static/tournaments/css/style.css index 82b4f40..93e177f 100644 --- a/tournaments/static/tournaments/css/style.css +++ b/tournaments/static/tournaments/css/style.css @@ -35,7 +35,7 @@ body { } label { - color: #707070; + color: #505050; font-size: 1.1em; } @@ -55,7 +55,7 @@ footer { } a { - color: #707070; + color: #505050; } a:hover { @@ -73,7 +73,7 @@ nav { } nav a { - color: #707070; + color: #505050; padding: 8px 12px; background-color: #fae7ce; border-radius: 12px; @@ -161,7 +161,7 @@ tr { .rounded-button { background-color: #fae7ce; /* Green background */ - color: #707070; /* White text */ + color: #505050; /* White text */ padding: 15px 32px; /* Some padding */ font-size: 1em; font-weight: 800; @@ -193,7 +193,7 @@ tr { } .mybox { - color: #707070; + color: #505050; padding: 8px 12px; background-color: #fae7ce; border-radius: 12px; @@ -260,6 +260,11 @@ tr { font-size: 1.2em; } +.very-large { + font-family: "Montserrat-SemiBold"; + font-size: 1.4em; +} + @media screen and (max-width: 40em) { .large { font-size: 0.9em; @@ -278,7 +283,7 @@ tr { .info { font-family: "Montserrat-SemiBold"; font-size: 0.9em; - color: #707070; + color: #505050; } .small { @@ -286,7 +291,7 @@ tr { } .minor-info { - color: #707070; + color: #505050; font-size: 0.85em; } @@ -362,7 +367,7 @@ tr { .separator { height: 1px; - background-color: #707070; + background-color: #505050; margin: 5px 0px; } @@ -608,12 +613,134 @@ h-margin { padding: 5px 0px; } -.table-row-4-colums-tournament { +.table-row-5-colums-tournament { display: grid; - grid-template-columns: auto 1fr auto auto; + grid-template-columns: 75px 90px 1fr 120px; align-items: center; - /* Vertically center the content within each column */ - padding: 5px 0px; + gap: 4px; +} + +.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 { @@ -867,7 +994,7 @@ h-margin { .match-result a:hover { background-color: #fae7ce; - color: #707070; + color: #505050; } .group-stage-link { diff --git a/tournaments/static/tournaments/css/tournament_bracket.css b/tournaments/static/tournaments/css/tournament_bracket.css index ed1a1a4..5593b51 100644 --- a/tournaments/static/tournaments/css/tournament_bracket.css +++ b/tournaments/static/tournaments/css/tournament_bracket.css @@ -58,7 +58,7 @@ } .round-name { - color: #707070; + color: #505050; font-size: 1.5em; padding: 8px 12px; white-space: nowrap; /* Prevent text wrapping */ @@ -67,7 +67,7 @@ .round-format { font-size: 0.9em; - color: #707070; + color: #505050; margin-top: -5px; /* Reduced from -10px to bring it closer */ white-space: nowrap; /* Prevent text wrapping */ display: block; /* Ensure proper centering */ @@ -199,7 +199,7 @@ .broadcast-mode .round-name, .broadcast-mode .round-format { padding: 0px; - color: #707070; + color: #505050; } .broadcast-mode .round-title { @@ -215,7 +215,7 @@ .outgoing-line, .outgoing-line-upward, .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 */ diff --git a/tournaments/templates/profile.html b/tournaments/templates/profile.html index 89fd5f4..2e6a1a7 100644 --- a/tournaments/templates/profile.html +++ b/tournaments/templates/profile.html @@ -20,27 +20,34 @@ {% load static %} {% load tz %} +{% if form.errors or password_change_form.errors %}
{{ error }}
+{{ error }}
- {% endfor %} -