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):
exclude = ('data_access_ids',)
raw_id_fields = ['related_user']
def save_model(self, request, obj, form, change):
if isinstance(obj, BaseModel):
obj.last_updated_by = request.user
obj.last_update = timezone.now()
if obj.related_user is None:
obj.related_user = request.user
# if obj.related_user is None:
# obj.related_user = request.user
super().save_model(request, obj, form, change)
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_filter = [StartDateRangeFilter, 'is_deleted', 'event__creator']
ordering = ['-start_date']
search_fields = ['id']
search_fields = ['id', 'display_name', 'federal_level_category']
def dashboard_view(self, request):
"""Tournament dashboard view with comprehensive statistics"""

@ -77,10 +77,10 @@ class SimpleCustomUserCreationForm(UserCreationForm):
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):
# Remove all spaces, dots, dashes, and parentheses
phone = re.sub(r'[\s\.\-\(\)]', '', phone)
# Basic regex for phone numbers, allowing 6-15 digits for international numbers
if not re.match(r"^\+?\d{6,15}$", phone):
raise forms.ValidationError("Entrer un numéro de téléphone valide.")
return phone
@ -181,10 +181,9 @@ class TournamentRegistrationForm(forms.Form):
def clean_mobile_number(self):
mobile_number = self.cleaned_data.get('mobile_number')
if mobile_number:
# Basic regex for mobile numbers, matching common formats
# Remove spaces from the number first
mobile_number = mobile_number.replace(' ', '')
if not re.match(r"^\+?\d{10,15}$", mobile_number):
# Remove spaces, dots, dashes, and parentheses from the number first
mobile_number = re.sub(r'[\s\.\-\(\)]', '', mobile_number)
if not re.match(r"^\+?\d{6,15}$", mobile_number):
raise forms.ValidationError("Entrer un numéro de téléphone valide.")
return mobile_number
@ -292,10 +291,10 @@ class ProfileUpdateForm(forms.ModelForm):
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):
# Remove all spaces, dots, dashes, and parentheses
phone = re.sub(r'[\s\.\-\(\)]', '', phone)
# Basic regex for phone numbers, allowing 6-15 digits for international numbers
if not re.match(r"^\+?\d{6,15}$", phone):
raise forms.ValidationError("Entrer un numéro de téléphone valide.")
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)
time_format ='l H:i'
if self.get_tournament().day_duration >= 7:
time_format = 'l d M à H:i'
time_format = 'D. d F à H:i'
if self.confirmed:
return formats.date_format(local_start, format=time_format)
else:
@ -464,7 +464,10 @@ class Match(TournamentSubModel):
bracket_name = "Match #" + f"{self.index_in_round() + 1}"
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
if event_mode is True:

@ -6,6 +6,7 @@ from .enums import RegistrationStatus
from .player_enums import PlayerPaymentType
from ..services.email_service import TournamentEmailService, TeamEmailType
from ..utils.extensions import format_seconds
from ..services.currency_service import CurrencyService
import uuid
@ -412,6 +413,13 @@ class TeamRegistration(TournamentSubModel):
payment_statuses = [player.get_remaining_fee() for player in player_registrations]
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):
"""
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 django.apps import apps
from django.conf import settings
from tournaments.services.currency_service import CurrencyService
class TeamSortingType(models.IntegerChoices):
RANK = 1, 'Rank'
@ -94,6 +95,7 @@ class Tournament(BaseModel):
show_teams_in_prog = models.BooleanField(default=False)
club_member_fee_deduction = models.FloatField(null=True, blank=True)
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):
for team_registration in self.team_registrations.all():
@ -596,7 +598,7 @@ class Tournament(BaseModel):
# Format the date
timezone = first_match.get_tournament().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)}"
return MatchGroup(name, live_matches, formatted_schedule, round_id, round_index)
@ -911,8 +913,6 @@ class Tournament(BaseModel):
return False
def display_prog(self):
if self.end_date is not None:
return True
if self.publish_prog:
return True
if self.has_started():
@ -1108,18 +1108,18 @@ class Tournament(BaseModel):
return True
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 = []
currency_service = CurrencyService()
# Entry fee
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
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
@ -1952,6 +1952,11 @@ class Tournament(BaseModel):
if event_mode is True and self.event.tournaments.count() == 1:
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
all_matches = set()
@ -1959,6 +1964,15 @@ class Tournament(BaseModel):
if event_mode is True:
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:
# Get matches only from top-level rounds to avoid duplicates
for round in tournament.rounds.filter(parent=None).all():
@ -2020,7 +2034,7 @@ class Tournament(BaseModel):
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
for hour, matches in sorted(matches_by_hour.items()):
# Sort matches by court if available
@ -2065,7 +2079,7 @@ class Tournament(BaseModel):
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
for hour, matches in sorted(matches_by_hour.items()):
# 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.tournament import TeamSortingType
from django.utils import timezone
from tournaments.services.currency_service import CurrencyService
class TeamEmailType(Enum):
REGISTERED = "registered"
@ -666,14 +667,16 @@ class TournamentEmailService:
if payment_status == 'PAID':
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:
return ""
currency_service = CurrencyService()
# For unpaid teams, add payment instructions
formatted_fee = currency_service.format_amount(tournament.entry_fee, tournament.currency_code)
payment_info = [
"\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.",
f"Lien pour payer: https://padelclub.app/tournament/{tournament.id}/info"
]
@ -694,12 +697,17 @@ class TournamentEmailService:
# Calculate payment amount
payment_amount = None
if payment and 'amount' in payment:
# Convert cents to euros
payment_amount = payment['amount'] / 100
currency_service = CurrencyService()
if payment_amount is None:
payment_amount = team_registration.get_remaining_fee()
if payment and 'amount' in payment:
# 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)
tournament_word = federal_level_category.localized_word()
@ -725,7 +733,7 @@ class TournamentEmailService:
# Add payment details
body_parts.append(
f"\n\nMontant payé : {payment_amount:.2f}"
f"\n\nMontant payé : {formatted_amount}"
)
payment_date = timezone.now().strftime("%d/%m/%Y")
@ -768,6 +776,19 @@ class TournamentEmailService:
"""
player_registrations = team_registration.players_sorted_by_rank
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:
# Convert cents to euros
refund_amount = refund_details['amount'] / 100
@ -803,7 +824,7 @@ class TournamentEmailService:
# Add refund details
body_parts.append(
f"\n\nMontant remboursé : {refund_amount:.2f}"
f"\n\nMontant remboursé : {formatted_amount}"
)
refund_date = timezone.now().strftime("%d/%m/%Y")

@ -11,6 +11,7 @@ from ..models.player_registration import PlayerPaymentType
from .email_service import TournamentEmailService
from .tournament_registration import RegistrationCartManager
from ..utils.extensions import is_not_sqlite_backend
from tournaments.services.currency_service import CurrencyService
class PaymentService:
"""
@ -28,10 +29,20 @@ class PaymentService:
stripe.api_key = self.stripe_api_key
tournament = get_object_or_404(Tournament, id=tournament_id)
currency_service = CurrencyService()
# Check if payments are enabled for this tournament
if not tournament.should_request_payment():
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
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,
'payment_source': 'tournament', # Identify payment source
'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:
@ -98,12 +110,12 @@ class PaymentService:
'payment_method_types': ['card'],
'line_items': [{
'price_data': {
'currency': 'eur',
'currency': currency_code.lower(), # Use tournament currency
'product_data': {
'name': f'{tournament.broadcast_display_name()} du {tournament.formatted_start_date()}',
'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,
}],
@ -122,18 +134,20 @@ class PaymentService:
# Calculate commission
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 = {
'payment_method_types': ['card'],
'line_items': [{
'price_data': {
'currency': 'eur',
'currency': currency_code.lower(), # Use tournament currency
'product_data': {
'name': f'{tournament.broadcast_display_name()} du {tournament.formatted_start_date()}',
'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,
}],
@ -151,13 +165,6 @@ class PaymentService:
'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
if 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.conf import settings
from django.utils.dateparse import parse_datetime
from .currency_service import CurrencyService
class RegistrationCartManager:
"""
@ -47,7 +48,9 @@ class RegistrationCartManager:
expiry_str = self.session['registration_cart_expiry']
try:
expiry = datetime.datetime.fromisoformat(expiry_str)
expiry = parse_datetime(expiry_str)
if expiry is None:
return True
return timezone.now() > expiry
except (ValueError, TypeError):
return True
@ -124,6 +127,7 @@ class RegistrationCartManager:
'expiry': expiry_datetime, # Now a datetime object, not a string
'is_cart_expired': self.is_cart_expired(),
'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)
}
@ -154,6 +158,19 @@ class RegistrationCartManager:
else:
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):
"""Add a player to the registration cart"""
print("add_player", player_data)

@ -1,4 +1,5 @@
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;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;

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

@ -13,7 +13,35 @@
{% block content %}
<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 -->
<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>
<!-- 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>
<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>
{% 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>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 %}
<p class="alert alert-danger">
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 :
</div>
<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>
{% endif %}
{% 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;
}
// Get the expiry date from the data attribute
const expiryDateStr = countdownElement.getAttribute('data-expiry');
if (!expiryDateStr) return;
// Get the expiry date from the data attribute (Unix timestamp)
const expiryTimestamp = countdownElement.getAttribute('data-expiry');
if (!expiryTimestamp) return;
// Parse the expiry date properly (keeping local time interpretation)
// Format received: "YYYY-MM-DD HH:MM:SS"
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);
// Convert Unix timestamp to Date object
const expiryDate = new Date(parseInt(expiryTimestamp) * 1000);
// Function to update countdown text
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>
{% if tournament.supposedly_in_progress %}
<a href="{% url 'tournament-live' tournament.id %}" class="topmargin5 orange">Live</a>
{% endif %}
{% if tournament.display_prog %}
<a href="{% url 'tournament-prog' tournament.id %}" class="topmargin5 orange">Programmation</a>
{% endif %}

@ -102,9 +102,9 @@
{% else %}
<a href="{% url 'proceed_to_payment' tournament.id %}" class="rounded-button positive-button">
{% if team.needs_confirmation %}
Confirmer en payant {{ team.get_remaining_fee|floatformat:2 }}€
Confirmer en payant {{ team.get_remaining_fee_formatted }}
{% else %}
Procéder au paiement de {{ team.get_remaining_fee|floatformat:2 }}€
Procéder au paiement de {{ team.get_remaining_fee_formatted }}
{% endif %}
</a>
{% endif %}
@ -182,7 +182,7 @@
<hr/>
<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>
{% if tournament.has_club_address %}
<div>{{ tournament.event.club.address }}</div>
@ -193,7 +193,7 @@
{% if tournament.event.tournaments.count > 1 %}
<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>
</a>
</p>

@ -7,12 +7,9 @@
{% block content %}
{% include 'tournaments/navigation_base.html' %}
<div class="grid-x">
{% if tournaments %}
<div class="cell medium-12 large-6 topblock padding10">
<div>
{% for tournament in tournaments %}
@ -23,6 +20,21 @@
</div>
{% 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>
{% endblock %}

@ -45,6 +45,7 @@ urlpatterns = [
path('rankings/json/', views.tournament_rankings_json, name='tournament-rankings-json'),
path('broadcast/rankings/', views.tournament_broadcast_rankings, name='broadcasted-rankings'),
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'),

@ -69,6 +69,10 @@ from .services.tournament_registration import RegistrationCartManager
from .services.payment_service import PaymentService
from django.views.decorators.csrf import csrf_exempt
import logging
logger = logging.getLogger(__name__)
def index(request):
now = timezone.now()
thirty_days_ago = now - timedelta(days=30)
@ -258,18 +262,29 @@ def club(request, club_id):
def event(request, 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:
name = event.name
else:
name = 'Événement'
return render(
request,
"tournaments/tournaments_list.html",
{
'tournaments': event.tournaments.all().order_by('start_date'),
'tournaments': tournaments,
'first_title': event.club.name,
'second_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)
except(TypeError, ValueError, OverflowError, CustomUser.DoesNotExist):
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.save()
@ -1738,7 +1761,7 @@ def private_tournaments(request):
# Get all tournaments matching our criteria (similar to index but for private tournaments)
tournaments = private_tournaments_query(
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
@ -1747,11 +1770,14 @@ def private_tournaments(request):
# Categorize tournaments by status
live = []
future = []
ended = []
for t in display_tournament:
if t.supposedly_in_progress():
live.append(t)
elif t.starts_in_the_future():
future.append(t)
else:
ended.append(t)
# Get ended tournaments
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()]
# 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
finished.sort(key=lambda t: t.sorting_finished_date(), reverse=True)
future.sort(key=lambda t: t.sorting_finished_date(), reverse=False)
return render(
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):
def get(self, request, *args, **kwargs):
users = CustomUser.objects.order_by('date_joined')

Loading…
Cancel
Save