Merge branch 'main' into sync3

apikeys
Laurent 4 months ago
commit 25c3b3b71b
  1. 5
      sync/admin.py
  2. 2
      tournaments/admin.py
  3. 23
      tournaments/forms.py
  4. 18
      tournaments/migrations/0129_tournament_currency_code.py
  5. 7
      tournaments/models/match.py
  6. 8
      tournaments/models/team_registration.py
  7. 36
      tournaments/models/tournament.py
  8. 189
      tournaments/services/currency_service.py
  9. 39
      tournaments/services/email_service.py
  10. 31
      tournaments/services/payment_service.py
  11. 19
      tournaments/services/tournament_registration.py
  12. 1
      tournaments/static/rankings/CLASSEMENT-PADEL-MESSIEURS-06-2025.csv
  13. 60
      tournaments/templates/admin/tournaments/dashboard.html
  14. 20
      tournaments/templates/register_tournament.html
  15. 26
      tournaments/templates/tournaments/live_matches.html
  16. 4
      tournaments/templates/tournaments/navigation_tournament.html
  17. 8
      tournaments/templates/tournaments/tournament_info.html
  18. 18
      tournaments/templates/tournaments/tournaments_list.html
  19. 1
      tournaments/urls.py
  20. 56
      tournaments/views.py

@ -6,13 +6,14 @@ from .models import BaseModel, ModelLog, DataAccess
class SyncedObjectAdmin(admin.ModelAdmin): class SyncedObjectAdmin(admin.ModelAdmin):
exclude = ('data_access_ids',) exclude = ('data_access_ids',)
raw_id_fields = ['related_user']
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
if isinstance(obj, BaseModel): if isinstance(obj, BaseModel):
obj.last_updated_by = request.user obj.last_updated_by = request.user
obj.last_update = timezone.now() obj.last_update = timezone.now()
if obj.related_user is None: # if obj.related_user is None:
obj.related_user = request.user # obj.related_user = request.user
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
def delete_model(self, request, obj): def delete_model(self, request, obj):

@ -90,7 +90,7 @@ class TournamentAdmin(SyncedObjectAdmin):
list_display = ['display_name', 'event', 'is_private', 'start_date', 'payment', 'creator', 'is_canceled'] list_display = ['display_name', 'event', 'is_private', 'start_date', 'payment', 'creator', 'is_canceled']
list_filter = [StartDateRangeFilter, 'is_deleted', 'event__creator'] list_filter = [StartDateRangeFilter, 'is_deleted', 'event__creator']
ordering = ['-start_date'] ordering = ['-start_date']
search_fields = ['id'] search_fields = ['id', 'display_name', 'federal_level_category']
def dashboard_view(self, request): def dashboard_view(self, request):
"""Tournament dashboard view with comprehensive statistics""" """Tournament dashboard view with comprehensive statistics"""

@ -77,10 +77,10 @@ class SimpleCustomUserCreationForm(UserCreationForm):
def clean_phone(self): def clean_phone(self):
phone = self.cleaned_data.get('phone') phone = self.cleaned_data.get('phone')
if phone: if phone:
# Remove all spaces # Remove all spaces, dots, dashes, and parentheses
phone = phone.replace(' ', '') phone = re.sub(r'[\s\.\-\(\)]', '', phone)
# Basic regex for phone numbers, matching common formats # Basic regex for phone numbers, allowing 6-15 digits for international numbers
if not re.match(r"^\+?\d{10,15}$", phone): if not re.match(r"^\+?\d{6,15}$", phone):
raise forms.ValidationError("Entrer un numéro de téléphone valide.") raise forms.ValidationError("Entrer un numéro de téléphone valide.")
return phone return phone
@ -181,10 +181,9 @@ class TournamentRegistrationForm(forms.Form):
def clean_mobile_number(self): def clean_mobile_number(self):
mobile_number = self.cleaned_data.get('mobile_number') mobile_number = self.cleaned_data.get('mobile_number')
if mobile_number: if mobile_number:
# Basic regex for mobile numbers, matching common formats # Remove spaces, dots, dashes, and parentheses from the number first
# Remove spaces from the number first mobile_number = re.sub(r'[\s\.\-\(\)]', '', mobile_number)
mobile_number = mobile_number.replace(' ', '') if not re.match(r"^\+?\d{6,15}$", mobile_number):
if not re.match(r"^\+?\d{10,15}$", mobile_number):
raise forms.ValidationError("Entrer un numéro de téléphone valide.") raise forms.ValidationError("Entrer un numéro de téléphone valide.")
return mobile_number return mobile_number
@ -292,10 +291,10 @@ class ProfileUpdateForm(forms.ModelForm):
def clean_phone(self): def clean_phone(self):
phone = self.cleaned_data.get('phone') phone = self.cleaned_data.get('phone')
if phone: if phone:
# Remove all spaces # Remove all spaces, dots, dashes, and parentheses
phone = phone.replace(' ', '') phone = re.sub(r'[\s\.\-\(\)]', '', phone)
# Basic regex for phone numbers, matching common formats # Basic regex for phone numbers, allowing 6-15 digits for international numbers
if not re.match(r"^\+?\d{10,15}$", phone): if not re.match(r"^\+?\d{6,15}$", phone):
raise forms.ValidationError("Entrer un numéro de téléphone valide.") raise forms.ValidationError("Entrer un numéro de téléphone valide.")
return phone return phone

@ -0,0 +1,18 @@
# Generated by Django 5.1 on 2025-06-23 16:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0128_club_data_access_ids_court_data_access_ids_and_more'),
]
operations = [
migrations.AddField(
model_name='tournament',
name='currency_code',
field=models.CharField(blank=True, default='EUR', max_length=3, null=True),
),
]

@ -339,7 +339,7 @@ class Match(TournamentSubModel):
local_start = self.start_date.astimezone(timezone) local_start = self.start_date.astimezone(timezone)
time_format ='l H:i' time_format ='l H:i'
if self.get_tournament().day_duration >= 7: if self.get_tournament().day_duration >= 7:
time_format = 'l d M à H:i' time_format = 'D. d F à H:i'
if self.confirmed: if self.confirmed:
return formats.date_format(local_start, format=time_format) return formats.date_format(local_start, format=time_format)
else: else:
@ -464,7 +464,10 @@ class Match(TournamentSubModel):
bracket_name = "Match #" + f"{self.index_in_round() + 1}" bracket_name = "Match #" + f"{self.index_in_round() + 1}"
ended = self.end_date is not None ended = self.end_date is not None
live_format = "Format " + FederalMatchCategory(self.format).format_label_short live_format = "Format "
if self.get_tournament().day_duration >= 7:
live_format = ""
live_format = live_format + FederalMatchCategory(self.format).format_label_short
tournament_title = None tournament_title = None
if event_mode is True: if event_mode is True:

@ -6,6 +6,7 @@ from .enums import RegistrationStatus
from .player_enums import PlayerPaymentType from .player_enums import PlayerPaymentType
from ..services.email_service import TournamentEmailService, TeamEmailType from ..services.email_service import TournamentEmailService, TeamEmailType
from ..utils.extensions import format_seconds from ..utils.extensions import format_seconds
from ..services.currency_service import CurrencyService
import uuid import uuid
@ -412,6 +413,13 @@ class TeamRegistration(TournamentSubModel):
payment_statuses = [player.get_remaining_fee() for player in player_registrations] payment_statuses = [player.get_remaining_fee() for player in player_registrations]
return sum(payment_statuses) return sum(payment_statuses)
def get_remaining_fee_formatted(self):
"""Get the remaining fee formatted with the tournament's currency."""
remaining_fee = self.get_remaining_fee()
tournament = self.get_tournament()
currency_code = tournament.currency_code if tournament and tournament.currency_code else 'EUR'
return CurrencyService.format_amount(remaining_fee, currency_code)
def is_confirmation_expired(self): def is_confirmation_expired(self):
""" """
Check if the confirmation deadline has expired. Check if the confirmation deadline has expired.

@ -12,6 +12,7 @@ from django.utils.formats import date_format
from ..utils.licence_validator import LicenseValidator from ..utils.licence_validator import LicenseValidator
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
from tournaments.services.currency_service import CurrencyService
class TeamSortingType(models.IntegerChoices): class TeamSortingType(models.IntegerChoices):
RANK = 1, 'Rank' RANK = 1, 'Rank'
@ -94,6 +95,7 @@ class Tournament(BaseModel):
show_teams_in_prog = models.BooleanField(default=False) show_teams_in_prog = models.BooleanField(default=False)
club_member_fee_deduction = models.FloatField(null=True, blank=True) club_member_fee_deduction = models.FloatField(null=True, blank=True)
unregister_delta_in_hours = models.IntegerField(default=24) unregister_delta_in_hours = models.IntegerField(default=24)
currency_code = models.CharField(null=True, blank=True, max_length=3, default='EUR')
def delete_dependencies(self): def delete_dependencies(self):
for team_registration in self.team_registrations.all(): for team_registration in self.team_registrations.all():
@ -596,7 +598,7 @@ class Tournament(BaseModel):
# Format the date # Format the date
timezone = first_match.get_tournament().timezone() timezone = first_match.get_tournament().timezone()
local_start = first_match.start_date.astimezone(timezone) local_start = first_match.start_date.astimezone(timezone)
time_format = 'l d M' time_format = 'l d F'
formatted_schedule = f" - {formats.date_format(local_start, format=time_format)}" formatted_schedule = f" - {formats.date_format(local_start, format=time_format)}"
return MatchGroup(name, live_matches, formatted_schedule, round_id, round_index) return MatchGroup(name, live_matches, formatted_schedule, round_id, round_index)
@ -911,8 +913,6 @@ class Tournament(BaseModel):
return False return False
def display_prog(self): def display_prog(self):
if self.end_date is not None:
return True
if self.publish_prog: if self.publish_prog:
return True return True
if self.has_started(): if self.has_started():
@ -1108,18 +1108,18 @@ class Tournament(BaseModel):
return True return True
def options_fee(self): def options_fee(self):
def format_currency(amount):
"""Format currency amount, removing unnecessary decimals"""
return f"{amount:g}" if amount % 1 == 0 else f"{amount:.2f}"
options = [] options = []
currency_service = CurrencyService()
# Entry fee # Entry fee
if self.entry_fee is not None and self.entry_fee > 0: if self.entry_fee is not None and self.entry_fee > 0:
options.append(f"Frais d'inscription: {format_currency(self.entry_fee)} € par joueur") formatted_fee = currency_service.format_amount(self.entry_fee, self.currency_code)
options.append(f"Frais d'inscription: {formatted_fee} par joueur")
# Club member fee reduction # Club member fee reduction
if self.club_member_fee_deduction and self.club_member_fee_deduction > 0: if self.club_member_fee_deduction and self.club_member_fee_deduction > 0:
options.append(f"Réduction de {format_currency(self.club_member_fee_deduction)} € pour les membres du club") formatted_deduction = currency_service.format_amount(self.club_member_fee_deduction, self.currency_code)
options.append(f"Réduction de {formatted_deduction} pour les membres du club")
return options return options
@ -1952,6 +1952,11 @@ class Tournament(BaseModel):
if event_mode is True and self.event.tournaments.count() == 1: if event_mode is True and self.event.tournaments.count() == 1:
event_mode = False event_mode = False
if self.event.tournaments.count() == 1:
show_teams_in_prog = self.show_teams_in_prog
else:
show_teams_in_prog = self.event.tournaments.filter(show_teams_in_prog=True).first() is not None
# Get all matches from rounds and group stages - use a set to avoid duplicates # Get all matches from rounds and group stages - use a set to avoid duplicates
all_matches = set() all_matches = set()
@ -1959,6 +1964,15 @@ class Tournament(BaseModel):
if event_mode is True: if event_mode is True:
tournaments = self.event.tournaments.all() tournaments = self.event.tournaments.all()
# Check if all tournaments have started - if so, always show teams
all_started = True
for t in tournaments:
if not t.has_started():
all_started = False
break
if all_started:
show_teams_in_prog = True
for tournament in tournaments: for tournament in tournaments:
# Get matches only from top-level rounds to avoid duplicates # Get matches only from top-level rounds to avoid duplicates
for round in tournament.rounds.filter(parent=None).all(): for round in tournament.rounds.filter(parent=None).all():
@ -2020,7 +2034,7 @@ class Tournament(BaseModel):
matches_by_hour[hour_key].append(match) matches_by_hour[hour_key].append(match)
hide_teams = self.show_teams_in_prog == False hide_teams = show_teams_in_prog == False
# Create match groups for each hour # Create match groups for each hour
for hour, matches in sorted(matches_by_hour.items()): for hour, matches in sorted(matches_by_hour.items()):
# Sort matches by court if available # Sort matches by court if available
@ -2065,7 +2079,7 @@ class Tournament(BaseModel):
matches_by_hour[hour_key].append(match) matches_by_hour[hour_key].append(match)
hide_teams = self.show_teams_in_prog == False hide_teams = show_teams_in_prog == False
# Create match groups for each hour # Create match groups for each hour
for hour, matches in sorted(matches_by_hour.items()): for hour, matches in sorted(matches_by_hour.items()):
# Sort matches by court if available # Sort matches by court if available

@ -0,0 +1,189 @@
"""
Currency service for handling multi-currency support in tournaments.
"""
class CurrencyService:
"""Service for handling currency formatting, symbols, and conversions."""
# Currency symbols mapping
CURRENCY_SYMBOLS = {
'EUR': '',
'USD': '$',
'GBP': '£',
'CHF': 'CHF',
'CAD': 'C$',
'AUD': 'A$',
'JPY': '¥',
'CNY': '¥',
'SEK': 'kr',
'NOK': 'kr',
'DKK': 'kr',
'PLN': '',
'CZK': '',
'HUF': 'Ft',
'RON': 'lei',
'BGN': 'лв',
'HRK': 'kn',
'RSD': 'RSD',
'BAM': 'KM',
'MKD': 'ден',
'ALL': 'L',
'MDL': 'L',
'UAH': '',
'RUB': '',
'BYN': 'Br',
'LTL': 'Lt',
'LVL': 'Ls',
'EEK': 'kr',
'ISK': 'kr',
'TRY': '',
'ILS': '',
'AED': 'د.إ',
'SAR': 'ر.س',
'QAR': 'ر.ق',
'KWD': 'د.ك',
'BHD': '.د.ب',
'OMR': 'ر.ع.',
'JOD': 'د.أ',
'LBP': 'ل.ل',
'EGP': 'ج.م',
'MAD': 'د.م.',
'TND': 'د.ت',
'DZD': 'د.ج',
'LYD': 'ل.د',
'ZAR': 'R',
'NGN': '',
'GHS': '',
'KES': 'KSh',
'UGX': 'USh',
'TZS': 'TSh',
'ETB': 'Br',
'XOF': 'CFA',
'XAF': 'FCFA',
'MZN': 'MT',
'BWP': 'P',
'ZMW': 'ZK',
'INR': '',
'PKR': '',
'BDT': '',
'LKR': '',
'MVR': 'Rf',
'NPR': '',
'BTN': 'Nu.',
'THB': '฿',
'MYR': 'RM',
'SGD': 'S$',
'IDR': 'Rp',
'PHP': '',
'VND': '',
'KHR': '',
'LAK': '',
'MMK': 'K',
'KRW': '',
'TWD': 'NT$',
'HKD': 'HK$',
'MOP': 'MOP$',
'BND': 'B$',
'FJD': 'FJ$',
'PGK': 'K',
'WST': 'WS$',
'TOP': 'T$',
'VUV': 'VT',
'SBD': 'SI$',
'MXN': '$',
'GTQ': 'Q',
'BZD': 'BZ$',
'SVC': '$',
'HNL': 'L',
'NIO': 'C$',
'CRC': '',
'PAB': 'B/.',
'COP': '$',
'VES': 'Bs.S',
'GYD': 'GY$',
'SRD': 'Sr$',
'UYU': '$U',
'PYG': '',
'BOB': 'Bs.',
'BRL': 'R$',
'PEN': 'S/',
'ECU': '$',
'CLP': '$',
'ARS': '$',
'FKP': '£',
'XPF': ''
}
# Zero-decimal currencies (amounts are in their base unit, not subdivided)
ZERO_DECIMAL_CURRENCIES = {
'BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF',
'UGX', 'VND', 'VUV', 'XAF', 'XOF', 'XPF'
}
@classmethod
def get_currency_symbol(cls, currency_code):
"""Get the currency symbol for a given currency code."""
if not currency_code:
return '' # Default to Euro symbol
return cls.CURRENCY_SYMBOLS.get(currency_code.upper(), currency_code.upper())
@classmethod
def format_amount(cls, amount, currency_code, show_symbol=True):
"""Format amount with proper currency symbol and decimal places."""
if amount is None:
return "0"
currency_code = currency_code.upper() if currency_code else 'EUR'
# Handle zero-decimal currencies
if currency_code in cls.ZERO_DECIMAL_CURRENCIES:
formatted_amount = f"{int(amount)}"
else:
# Format with 2 decimal places, removing unnecessary zeros
if amount == int(amount):
formatted_amount = f"{int(amount)}"
else:
formatted_amount = f"{amount:.2f}"
if show_symbol:
symbol = cls.get_currency_symbol(currency_code)
return f"{formatted_amount} {symbol}"
return formatted_amount
@classmethod
def convert_to_stripe_amount(cls, amount, currency_code):
"""Convert amount to Stripe's expected format (minor units)."""
if not amount:
return 0
currency_code = currency_code.upper() if currency_code else 'EUR'
# Zero-decimal currencies don't need conversion
if currency_code in cls.ZERO_DECIMAL_CURRENCIES:
return int(amount)
# Two-decimal currencies need to be multiplied by 100
return int(amount * 100)
@classmethod
def convert_from_stripe_amount(cls, stripe_amount, currency_code):
"""Convert Stripe amount (minor units) back to standard decimal format."""
if not stripe_amount:
return 0.0
currency_code = currency_code.upper() if currency_code else 'EUR'
# Zero-decimal currencies are already in the correct format
if currency_code in cls.ZERO_DECIMAL_CURRENCIES:
return float(stripe_amount)
# Two-decimal currencies need to be divided by 100
return float(stripe_amount) / 100
@classmethod
def validate_currency_code(cls, currency_code):
"""Validate if a currency code is supported."""
if not currency_code:
return False
return currency_code.upper() in cls.CURRENCY_SYMBOLS

@ -3,6 +3,7 @@ from enum import Enum
from ..models.enums import RegistrationStatus, FederalLevelCategory from ..models.enums import RegistrationStatus, FederalLevelCategory
from ..models.tournament import TeamSortingType from ..models.tournament import TeamSortingType
from django.utils import timezone from django.utils import timezone
from tournaments.services.currency_service import CurrencyService
class TeamEmailType(Enum): class TeamEmailType(Enum):
REGISTERED = "registered" REGISTERED = "registered"
@ -666,14 +667,16 @@ class TournamentEmailService:
if payment_status == 'PAID': if payment_status == 'PAID':
return "\n\n✅ Le paiement de votre inscription a bien été reçu." return "\n\n✅ Le paiement de votre inscription a bien été reçu."
# If the team is on the waiting list, don't mention payment
if team_registration.is_in_waiting_list() >= 0: if team_registration.is_in_waiting_list() >= 0:
return "" return ""
currency_service = CurrencyService()
# For unpaid teams, add payment instructions # For unpaid teams, add payment instructions
formatted_fee = currency_service.format_amount(tournament.entry_fee, tournament.currency_code)
payment_info = [ payment_info = [
"\n\n Paiement des frais d'inscription requis", "\n\n Paiement des frais d'inscription requis",
f"Les frais d'inscription de {tournament.entry_fee:.2f} par joueur doivent être payés pour confirmer votre participation.", f"Les frais d'inscription de {formatted_fee} par joueur doivent être payés pour confirmer votre participation.",
"Vous pouvez effectuer le paiement en vous connectant à votre compte Padel Club.", "Vous pouvez effectuer le paiement en vous connectant à votre compte Padel Club.",
f"Lien pour payer: https://padelclub.app/tournament/{tournament.id}/info" f"Lien pour payer: https://padelclub.app/tournament/{tournament.id}/info"
] ]
@ -694,12 +697,17 @@ class TournamentEmailService:
# Calculate payment amount # Calculate payment amount
payment_amount = None payment_amount = None
if payment and 'amount' in payment: currency_service = CurrencyService()
# Convert cents to euros
payment_amount = payment['amount'] / 100
if payment_amount is None: if payment and 'amount' in payment:
payment_amount = team_registration.get_remaining_fee() # Get currency from payment metadata or tournament
payment_currency = payment.get('currency', tournament.currency_code or 'EUR')
# Convert cents/minor units to standard format
payment_amount = currency_service.convert_from_stripe_amount(payment['amount'], payment_currency)
formatted_amount = currency_service.format_amount(payment_amount, payment_currency)
else:
# Fallback to tournament fee
formatted_amount = currency_service.format_amount(team_registration.get_team_registration_fee(), tournament.currency_code)
federal_level_category = FederalLevelCategory(tournament.federal_level_category) federal_level_category = FederalLevelCategory(tournament.federal_level_category)
tournament_word = federal_level_category.localized_word() tournament_word = federal_level_category.localized_word()
@ -725,7 +733,7 @@ class TournamentEmailService:
# Add payment details # Add payment details
body_parts.append( body_parts.append(
f"\n\nMontant payé : {payment_amount:.2f}" f"\n\nMontant payé : {formatted_amount}"
) )
payment_date = timezone.now().strftime("%d/%m/%Y") payment_date = timezone.now().strftime("%d/%m/%Y")
@ -768,6 +776,19 @@ class TournamentEmailService:
""" """
player_registrations = team_registration.players_sorted_by_rank player_registrations = team_registration.players_sorted_by_rank
refund_amount = None refund_amount = None
currency_service = CurrencyService()
if refund_details and 'amount' in refund_details:
# Get currency from refund details or tournament
refund_currency = refund_details.get('currency', tournament.currency_code or 'EUR')
# Convert cents/minor units to standard format
refund_amount = currency_service.convert_from_stripe_amount(refund_details['amount'], refund_currency)
formatted_amount = currency_service.format_amount(refund_amount, refund_currency)
else:
# Fallback to tournament fee
formatted_amount = currency_service.format_amount(tournament.entry_fee, tournament.currency_code)
if refund_details and 'amount' in refund_details: if refund_details and 'amount' in refund_details:
# Convert cents to euros # Convert cents to euros
refund_amount = refund_details['amount'] / 100 refund_amount = refund_details['amount'] / 100
@ -803,7 +824,7 @@ class TournamentEmailService:
# Add refund details # Add refund details
body_parts.append( body_parts.append(
f"\n\nMontant remboursé : {refund_amount:.2f}" f"\n\nMontant remboursé : {formatted_amount}"
) )
refund_date = timezone.now().strftime("%d/%m/%Y") refund_date = timezone.now().strftime("%d/%m/%Y")

@ -11,6 +11,7 @@ from ..models.player_registration import PlayerPaymentType
from .email_service import TournamentEmailService from .email_service import TournamentEmailService
from .tournament_registration import RegistrationCartManager from .tournament_registration import RegistrationCartManager
from ..utils.extensions import is_not_sqlite_backend from ..utils.extensions import is_not_sqlite_backend
from tournaments.services.currency_service import CurrencyService
class PaymentService: class PaymentService:
""" """
@ -28,10 +29,20 @@ class PaymentService:
stripe.api_key = self.stripe_api_key stripe.api_key = self.stripe_api_key
tournament = get_object_or_404(Tournament, id=tournament_id) tournament = get_object_or_404(Tournament, id=tournament_id)
currency_service = CurrencyService()
# Check if payments are enabled for this tournament # Check if payments are enabled for this tournament
if not tournament.should_request_payment(): if not tournament.should_request_payment():
raise Exception("Les paiements ne sont pas activés pour ce tournoi.") raise Exception("Les paiements ne sont pas activés pour ce tournoi.")
# Get currency code and validate it
currency_code = tournament.currency_code or 'EUR'
if not currency_service.validate_currency_code(currency_code):
raise Exception(f"Devise non supportée: {currency_code}")
# Convert amount to Stripe format
stripe_amount = currency_service.convert_to_stripe_amount(team_fee, currency_code)
# Get user email if authenticated # Get user email if authenticated
customer_email = self.request.user.email if self.request.user.is_authenticated else None customer_email = self.request.user.email if self.request.user.is_authenticated else None
@ -52,6 +63,7 @@ class PaymentService:
'user_id': str(self.request.user.id) if self.request.user.is_authenticated else None, 'user_id': str(self.request.user.id) if self.request.user.is_authenticated else None,
'payment_source': 'tournament', # Identify payment source 'payment_source': 'tournament', # Identify payment source
'source_page': 'tournament_info' if team_registration_id else 'register_tournament', 'source_page': 'tournament_info' if team_registration_id else 'register_tournament',
'currency_code': currency_code, # Store currency for later reference
} }
if tournament.is_corporate_tournament: if tournament.is_corporate_tournament:
@ -98,12 +110,12 @@ class PaymentService:
'payment_method_types': ['card'], 'payment_method_types': ['card'],
'line_items': [{ 'line_items': [{
'price_data': { 'price_data': {
'currency': 'eur', 'currency': currency_code.lower(), # Use tournament currency
'product_data': { 'product_data': {
'name': f'{tournament.broadcast_display_name()} du {tournament.formatted_start_date()}', 'name': f'{tournament.broadcast_display_name()} du {tournament.formatted_start_date()}',
'description': f'Lieu {tournament.event.club.name}', 'description': f'Lieu {tournament.event.club.name}',
}, },
'unit_amount': int(team_fee * 100), # Amount in cents 'unit_amount': stripe_amount, # Amount in proper currency format
}, },
'quantity': 1, 'quantity': 1,
}], }],
@ -122,18 +134,20 @@ class PaymentService:
# Calculate commission # Calculate commission
commission_rate = tournament.event.creator.effective_commission_rate() commission_rate = tournament.event.creator.effective_commission_rate()
platform_amount = int((team_fee * commission_rate) * 100) # Commission in cents platform_amount = currency_service.convert_to_stripe_amount(
team_fee * commission_rate, currency_code
)
checkout_session_params = { checkout_session_params = {
'payment_method_types': ['card'], 'payment_method_types': ['card'],
'line_items': [{ 'line_items': [{
'price_data': { 'price_data': {
'currency': 'eur', 'currency': currency_code.lower(), # Use tournament currency
'product_data': { 'product_data': {
'name': f'{tournament.broadcast_display_name()} du {tournament.formatted_start_date()}', 'name': f'{tournament.broadcast_display_name()} du {tournament.formatted_start_date()}',
'description': f'Lieu {tournament.event.club.name}', 'description': f'Lieu {tournament.event.club.name}',
}, },
'unit_amount': int(team_fee * 100), # Amount in cents 'unit_amount': stripe_amount, # Amount in proper currency format
}, },
'quantity': 1, 'quantity': 1,
}], }],
@ -151,13 +165,6 @@ class PaymentService:
'metadata': metadata 'metadata': metadata
} }
# # Add cart or team data to metadata based on payment context
# if cart_data:
# checkout_session_params['metadata']['registration_cart_id'] = str(cart_data['cart_id']) # Convert to string
# elif team_registration_id:
# checkout_session_params['metadata']['team_registration_id'] = str(team_registration_id) # Convert to string
# self.request.session['team_registration_id'] = str(team_registration_id) # Convert to string
# Add customer_email if available # Add customer_email if available
if customer_email: if customer_email:
checkout_session_params['customer_email'] = customer_email checkout_session_params['customer_email'] = customer_email

@ -9,6 +9,7 @@ from ..models.player_enums import PlayerSexType, PlayerDataSource
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.conf import settings from django.conf import settings
from django.utils.dateparse import parse_datetime from django.utils.dateparse import parse_datetime
from .currency_service import CurrencyService
class RegistrationCartManager: class RegistrationCartManager:
""" """
@ -47,7 +48,9 @@ class RegistrationCartManager:
expiry_str = self.session['registration_cart_expiry'] expiry_str = self.session['registration_cart_expiry']
try: try:
expiry = datetime.datetime.fromisoformat(expiry_str) expiry = parse_datetime(expiry_str)
if expiry is None:
return True
return timezone.now() > expiry return timezone.now() > expiry
except (ValueError, TypeError): except (ValueError, TypeError):
return True return True
@ -124,6 +127,7 @@ class RegistrationCartManager:
'expiry': expiry_datetime, # Now a datetime object, not a string 'expiry': expiry_datetime, # Now a datetime object, not a string
'is_cart_expired': self.is_cart_expired(), 'is_cart_expired': self.is_cart_expired(),
'team_fee_from_cart_players': self.team_fee_from_cart_players(), 'team_fee_from_cart_players': self.team_fee_from_cart_players(),
'team_fee_from_cart_players_formatted': self.team_fee_from_cart_players_formatted(),
'mobile_number': self.session.get('registration_mobile_number', user_phone) 'mobile_number': self.session.get('registration_mobile_number', user_phone)
} }
@ -154,6 +158,19 @@ class RegistrationCartManager:
else: else:
return 0 return 0
def team_fee_from_cart_players_formatted(self):
"""Get the team fee formatted with the tournament's currency."""
team_fee = self.team_fee_from_cart_players()
tournament_id = self.session.get('registration_tournament_id')
try:
tournament = Tournament.objects.get(id=tournament_id)
currency_code = tournament.currency_code if tournament and tournament.currency_code else 'EUR'
except Tournament.DoesNotExist:
currency_code = 'EUR'
return CurrencyService.format_amount(team_fee, currency_code)
def add_player(self, player_data): def add_player(self, player_data):
"""Add a player to the registration cart""" """Add a player to the registration cart"""
print("add_player", player_data) print("add_player", player_data)

@ -1,4 +1,5 @@
max-players:97914 max-players:97914
unrank-male-value:90415
;1;ZAPATA PIZZARO;Teodoro Victor;ESP;2098059;4145;Oui;6;ILE DE FRANCE;57 93 0505;AS PADEL AFICIONADOS;0;1;1995; ;1;ZAPATA PIZZARO;Teodoro Victor;ESP;2098059;4145;Oui;6;ILE DE FRANCE;57 93 0505;AS PADEL AFICIONADOS;0;1;1995;
;1;HERNANDEZ QUESADA;luis;ESP;2744589;1200;Oui;1;NOUVELLE AQUITAINE;59 33 0723;BIG PADEL;0;1;1999; ;1;HERNANDEZ QUESADA;luis;ESP;2744589;1200;Oui;1;NOUVELLE AQUITAINE;59 33 0723;BIG PADEL;0;1;1999;
;1;LIJO;Pablo ;ESP;3306546;900;Oui;1;PROVENCE ALPES COTE D'AZUR;62 13 0603;ALL IN PADEL ASSOCIATION;0;1;1991; ;1;LIJO;Pablo ;ESP;3306546;900;Oui;1;PROVENCE ALPES COTE D'AZUR;62 13 0603;ALL IN PADEL ASSOCIATION;0;1;1991;

Can't render this file because it is too large.

@ -13,7 +13,35 @@
{% block content %} {% block content %}
<div class="tournament-dashboard"> <div class="tournament-dashboard">
<h1>🏆 Tournament Dashboard</h1> <!-- Quick Actions - Déplacé en haut -->
<div class="quick-actions" style="background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 12px; padding: 25px; margin-bottom: 20px;">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
<a href="{% url 'admin:tournaments_tournament_changelist' %}"
style="display: block; padding: 12px 15px; background: #007bff; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
Tournaments
</a>
<a href="{% url 'admin:tournaments_teamregistration_changelist' %}"
style="display: block; padding: 12px 15px; background: #28a745; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
Teams
</a>
<a href="{% url 'admin:tournaments_playerregistration_changelist' %}"
style="display: block; padding: 12px 15px; background: #6c757d; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
Players
</a>
<a href="{% url 'admin:tournaments_match_changelist' %}"
style="display: block; padding: 12px 15px; background: #fd7e14; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
Matches
</a>
<a href="{% url 'admin:tournaments_event_changelist' %}"
style="display: block; padding: 12px 15px; background: #17a2b8; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
Events
</a>
<a href="{% url 'admin:tournaments_club_changelist' %}"
style="display: block; padding: 12px 15px; background: #ffc107; color: #212529; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
Clubs
</a>
</div>
</div>
<!-- Summary Statistics Cards --> <!-- Summary Statistics Cards -->
<div class="dashboard-stats" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin: 20px 0;"> <div class="dashboard-stats" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin: 20px 0;">
@ -377,36 +405,6 @@
</div> </div>
</div> </div>
<!-- Quick Actions -->
<div class="quick-actions" style="background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 12px; padding: 25px; margin-top: 20px;">
<h3 style="margin: 0 0 20px 0; color: #495057;">🚀 Quick Actions</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
<a href="{% url 'admin:tournaments_tournament_changelist' %}"
style="display: block; padding: 12px 15px; background: #007bff; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
View All Tournaments
</a>
<a href="{% url 'admin:tournaments_teamregistration_changelist' %}"
style="display: block; padding: 12px 15px; background: #28a745; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
Manage Teams
</a>
<a href="{% url 'admin:tournaments_playerregistration_changelist' %}"
style="display: block; padding: 12px 15px; background: #6c757d; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
Manage Players
</a>
<a href="{% url 'admin:tournaments_match_changelist' %}"
style="display: block; padding: 12px 15px; background: #fd7e14; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
View Matches
</a>
<a href="{% url 'admin:tournaments_event_changelist' %}"
style="display: block; padding: 12px 15px; background: #17a2b8; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
Manage Events
</a>
<a href="{% url 'admin:tournaments_club_changelist' %}"
style="display: block; padding: 12px 15px; background: #ffc107; color: #212529; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
Manage Clubs
</a>
</div>
</div>
</div> </div>
<style> <style>

@ -38,7 +38,7 @@
<p>Votre session d'inscription est active. Complétez le formulaire dans le délai accordé pour confirmer votre participation et garantir votre place.</p> <p>Votre session d'inscription est active. Complétez le formulaire dans le délai accordé pour confirmer votre participation et garantir votre place.</p>
{% if not cart_data.is_cart_expired %} {% if not cart_data.is_cart_expired %}
<p class="semibold highlight">Votre session d'inscription expirera le {{ cart_data.expiry|date:"d/m/Y à H:i" }}</p> <p class="semibold highlight">Votre session d'inscription expirera le {{ cart_data.expiry|date:"d/m/Y à H:i" }}</p>
<p>Temps restant: <span id="countdown" data-expiry="{{ cart_data.expiry|date:'Y-m-d H:i:s' }}">{{ cart_data.expiry|timeuntil }}</span></p> <p>Temps restant: <span id="countdown" data-expiry="{{ cart_data.expiry|date:'U' }}">{{ cart_data.expiry|timeuntil }}</span></p>
{% else %} {% else %}
<p class="alert alert-danger"> <p class="alert alert-danger">
Votre session d'inscription a expiré. Veuillez recommencer le processus d'inscription. Votre place n'est plus garantie. Votre session d'inscription a expiré. Veuillez recommencer le processus d'inscription. Votre place n'est plus garantie.
@ -197,7 +197,7 @@
Confirmer votre inscription en payant immédiatement : Confirmer votre inscription en payant immédiatement :
</div> </div>
<button type="submit" name="proceed_to_payment" class="rounded-button"> <button type="submit" name="proceed_to_payment" class="rounded-button">
Procéder au paiement de {{ cart_data.team_fee_from_cart_players|floatformat:2 }}€ Procéder au paiement de {{ cart_data.team_fee_from_cart_players_formatted }}
</button> </button>
{% endif %} {% endif %}
{% if tournament.should_request_payment is False or tournament.online_payment_is_mandatory is False or cart_data.waiting_list_position >= 0 %} {% if tournament.should_request_payment is False or tournament.online_payment_is_mandatory is False or cart_data.waiting_list_position >= 0 %}
@ -236,18 +236,12 @@ document.addEventListener('DOMContentLoaded', function() {
return; return;
} }
// Get the expiry date from the data attribute // Get the expiry date from the data attribute (Unix timestamp)
const expiryDateStr = countdownElement.getAttribute('data-expiry'); const expiryTimestamp = countdownElement.getAttribute('data-expiry');
if (!expiryDateStr) return; if (!expiryTimestamp) return;
// Parse the expiry date properly (keeping local time interpretation) // Convert Unix timestamp to Date object
// Format received: "YYYY-MM-DD HH:MM:SS" const expiryDate = new Date(parseInt(expiryTimestamp) * 1000);
const [datePart, timePart] = expiryDateStr.split(' ');
const [year, month, day] = datePart.split('-').map(Number);
const [hours, minutes, seconds] = timePart.split(':').map(Number);
// Create date object using local time components (month is 0-indexed in JS)
const expiryDate = new Date(year, month-1, day, hours, minutes, seconds);
// Function to update countdown text // Function to update countdown text
function updateCountdown() { function updateCountdown() {

@ -0,0 +1,26 @@
{% extends 'tournaments/base.html' %}
{% block head_title %}Matchs du {{ tournament.display_name }}{% endblock %}
{% block first_title %}{{ tournament.event.display_name }}{% endblock %}
{% block second_title %}{{ tournament.display_name }}{% endblock %}
{% block title_link %}{% url 'tournament' tournament.id %}{% endblock %}
{% block content %}
{% include 'tournaments/navigation_tournament.html' %}
{% if live_matches %}
<h1 class="club padding10 topmargin20">En cours</h1>
<div class="grid-x">
{% for match in live_matches %}
{% include 'tournaments/match_cell.html' %}
{% endfor %}
</div>
{% else %}
<div class="grid-x">
<h1 class="club padding10 topmargin20">Aucun match en cours actuellement.</h1>
</div>
{% endif %}
{% endblock %}

@ -4,6 +4,10 @@
<a href="{% url 'tournament-info' tournament.id %}" class="topmargin5 orange">Informations</a> <a href="{% url 'tournament-info' tournament.id %}" class="topmargin5 orange">Informations</a>
{% if tournament.supposedly_in_progress %}
<a href="{% url 'tournament-live' tournament.id %}" class="topmargin5 orange">Live</a>
{% endif %}
{% if tournament.display_prog %} {% if tournament.display_prog %}
<a href="{% url 'tournament-prog' tournament.id %}" class="topmargin5 orange">Programmation</a> <a href="{% url 'tournament-prog' tournament.id %}" class="topmargin5 orange">Programmation</a>
{% endif %} {% endif %}

@ -102,9 +102,9 @@
{% else %} {% else %}
<a href="{% url 'proceed_to_payment' tournament.id %}" class="rounded-button positive-button"> <a href="{% url 'proceed_to_payment' tournament.id %}" class="rounded-button positive-button">
{% if team.needs_confirmation %} {% if team.needs_confirmation %}
Confirmer en payant {{ team.get_remaining_fee|floatformat:2 }}€ Confirmer en payant {{ team.get_remaining_fee_formatted }}
{% else %} {% else %}
Procéder au paiement de {{ team.get_remaining_fee|floatformat:2 }}€ Procéder au paiement de {{ team.get_remaining_fee_formatted }}
{% endif %} {% endif %}
</a> </a>
{% endif %} {% endif %}
@ -182,7 +182,7 @@
<hr/> <hr/>
<p> <p>
<a href="{% url 'index' %}?club={{ tournament.event.club.id }}"> <a href="{% url 'index' %}?club={{ tournament.event.club.id }}" class="topmargin5 orange">
<div class="semibold">{{ tournament.event.club.name }}</div> <div class="semibold">{{ tournament.event.club.name }}</div>
{% if tournament.has_club_address %} {% if tournament.has_club_address %}
<div>{{ tournament.event.club.address }}</div> <div>{{ tournament.event.club.address }}</div>
@ -193,7 +193,7 @@
{% if tournament.event.tournaments.count > 1 %} {% if tournament.event.tournaments.count > 1 %}
<p> <p>
<a href="{% url 'event' tournament.event.id %}"> <a href="{% url 'event' tournament.event.id %}" class="topmargin5 orange">
<div class="semibold">Voir les autres tournois de l'événement{% if tournament.event.name %} {{ tournament.event.name }}{% endif %}</div> <div class="semibold">Voir les autres tournois de l'événement{% if tournament.event.name %} {{ tournament.event.name }}{% endif %}</div>
</a> </a>
</p> </p>

@ -7,12 +7,9 @@
{% block content %} {% block content %}
{% include 'tournaments/navigation_base.html' %} {% include 'tournaments/navigation_base.html' %}
<div class="grid-x"> <div class="grid-x">
{% if tournaments %} {% if tournaments %}
<div class="cell medium-12 large-6 topblock padding10"> <div class="cell medium-12 large-6 topblock padding10">
<div> <div>
{% for tournament in tournaments %} {% for tournament in tournaments %}
@ -23,6 +20,21 @@
</div> </div>
{% endif %} {% endif %}
{% if first_tournament_prog_url and tournaments %}
<div class="cell medium-6 large-6 topblock padding10">
<div class="bubble">
<a href="{{ first_tournament_prog_url }}" class="large-button semibold orange topmargin5">Voir la programmation de l'événement</a>
<hr/>
<p>
<div class="semibold">Infos</div>
<div class="tournament-info">
{{ tournaments.count }} tournois
</div>
</p>
</div>
</div>
{% endif %}
</div> </div>
{% endblock %} {% endblock %}

@ -45,6 +45,7 @@ urlpatterns = [
path('rankings/json/', views.tournament_rankings_json, name='tournament-rankings-json'), path('rankings/json/', views.tournament_rankings_json, name='tournament-rankings-json'),
path('broadcast/rankings/', views.tournament_broadcast_rankings, name='broadcasted-rankings'), path('broadcast/rankings/', views.tournament_broadcast_rankings, name='broadcasted-rankings'),
path('team/<str:team_id>/', views.team_details, name='team-details'), path('team/<str:team_id>/', views.team_details, name='team-details'),
path('live/', views.tournament_live_matches, name='tournament-live'),
]) ])
), ),
path("event/<str:event_id>/", views.event, name='event'), path("event/<str:event_id>/", views.event, name='event'),

@ -69,6 +69,10 @@ from .services.tournament_registration import RegistrationCartManager
from .services.payment_service import PaymentService from .services.payment_service import PaymentService
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
import logging
logger = logging.getLogger(__name__)
def index(request): def index(request):
now = timezone.now() now = timezone.now()
thirty_days_ago = now - timedelta(days=30) thirty_days_ago = now - timedelta(days=30)
@ -258,18 +262,29 @@ def club(request, club_id):
def event(request, event_id): def event(request, event_id):
event = get_object_or_404(Event, pk=event_id) event = get_object_or_404(Event, pk=event_id)
tournaments = event.tournaments.all().order_by('start_date')
# Get the first tournament for the prog link
first_tournament_prog_url = None
if tournaments.exists():
first_tournament = tournaments.first()
if first_tournament.display_prog():
first_tournament_prog_url = reverse('tournament-prog', kwargs={'tournament_id': first_tournament.id})
if event.name and len(event.name) > 0: if event.name and len(event.name) > 0:
name = event.name name = event.name
else: else:
name = 'Événement' name = 'Événement'
return render( return render(
request, request,
"tournaments/tournaments_list.html", "tournaments/tournaments_list.html",
{ {
'tournaments': event.tournaments.all().order_by('start_date'), 'tournaments': tournaments,
'first_title': event.club.name, 'first_title': event.club.name,
'second_title': name, 'second_title': name,
'head_title': name, 'head_title': name,
'first_tournament_prog_url': first_tournament_prog_url,
} }
) )
@ -541,7 +556,15 @@ def activate(request, uidb64, token):
user = CustomUser.objects.get(pk=uid) user = CustomUser.objects.get(pk=uid)
except(TypeError, ValueError, OverflowError, CustomUser.DoesNotExist): except(TypeError, ValueError, OverflowError, CustomUser.DoesNotExist):
user = None user = None
if user is not None and account_activation_token.check_token(user, token): # if user is not None and account_activation_token.check_token(user, token):
# removed for Philippe Morin Nouvelle Calédonie / Serge Dion user / il faut remettre
if user is not None:
token_valid = account_activation_token.check_token(user, token)
if not token_valid:
# Log the failure reason for debugging
logger.warning(f"Token validation would have failed for user {user.username}")
print(f"Token validation would have failed for user {user.username}")
user.is_active = True user.is_active = True
user.save() user.save()
@ -1738,7 +1761,7 @@ def private_tournaments(request):
# Get all tournaments matching our criteria (similar to index but for private tournaments) # Get all tournaments matching our criteria (similar to index but for private tournaments)
tournaments = private_tournaments_query( tournaments = private_tournaments_query(
Q(end_date__isnull=True, start_date__gte=thirty_days_ago, start_date__lte=thirty_days_future), Q(end_date__isnull=True, start_date__gte=thirty_days_ago, start_date__lte=thirty_days_future),
True, 50 False, 50
) )
# Filter tournaments that should be displayed # Filter tournaments that should be displayed
@ -1747,11 +1770,14 @@ def private_tournaments(request):
# Categorize tournaments by status # Categorize tournaments by status
live = [] live = []
future = [] future = []
ended = []
for t in display_tournament: for t in display_tournament:
if t.supposedly_in_progress(): if t.supposedly_in_progress():
live.append(t) live.append(t)
elif t.starts_in_the_future(): elif t.starts_in_the_future():
future.append(t) future.append(t)
else:
ended.append(t)
# Get ended tournaments # Get ended tournaments
clean_ended_tournaments = private_tournaments_query(Q(end_date__isnull=False), False, 50) clean_ended_tournaments = private_tournaments_query(Q(end_date__isnull=False), False, 50)
@ -1759,10 +1785,11 @@ def private_tournaments(request):
ended_tournaments = [t for t in display_tournament if t.should_be_over()] ended_tournaments = [t for t in display_tournament if t.should_be_over()]
# Combine both lists # Combine both lists
finished = clean_ended_tournaments + ended_tournaments finished = clean_ended_tournaments + ended_tournaments + ended
# Sort the combined list by start_date in descending order # Sort the combined list by start_date in descending order
finished.sort(key=lambda t: t.sorting_finished_date(), reverse=True) finished.sort(key=lambda t: t.sorting_finished_date(), reverse=True)
future.sort(key=lambda t: t.sorting_finished_date(), reverse=False)
return render( return render(
request, request,
@ -1776,6 +1803,27 @@ def private_tournaments(request):
} }
) )
def tournament_live_matches(request, tournament_id):
tournament = get_object_or_404(Tournament, pk=tournament_id)
# Get all matches from the tournament
current_time = timezone.now()
matches = Match.objects.filter(
Q(round__tournament=tournament) | Q(group_stage__tournament=tournament),
start_date__isnull=False, # Match has a start date
start_date__lte=current_time,
confirmed=True, # Match is confirmed
end_date__isnull=True # Match hasn't ended yet
).order_by('start_date')
# Convert to live match format
live_matches = [match.live_match() for match in matches]
return render(request, 'tournaments/live_matches.html', {
'tournament': tournament,
'live_matches': live_matches,
})
class UserListExportView(LoginRequiredMixin, View): class UserListExportView(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
users = CustomUser.objects.order_by('date_joined') users = CustomUser.objects.order_by('date_joined')

Loading…
Cancel
Save