timetoconfirm
Raz 7 months ago
commit 78e324158b
  1. 2
      crm/templates/crm/add_prospect.html
  2. 2
      crm/templates/crm/base.html
  3. 2
      crm/templates/crm/event_form.html
  4. 4
      crm/templates/crm/events.html
  5. 13
      shop/management/commands/create_initial_shop_data.py
  6. 11
      shop/static/shop/css/shop.css
  7. BIN
      shop/static/shop/images/products/PC001/blanc/PS_KP912-F_WHITE.png
  8. BIN
      shop/static/shop/images/products/PC001/blanc/PS_KP912_WHITE.png.avif
  9. BIN
      shop/static/shop/images/products/PC001/bleu-sport/CASQUETTE - KP912 MARINE-F_.png
  10. BIN
      shop/static/shop/images/products/PC001/bleu-sport/PS_KP912_NAVY.png.avif
  11. 0
      shop/static/shop/images/products/PC001/noir/noir_hat-F_.png.avif
  12. BIN
      shop/static/shop/images/products/PC002/blanc/PS_K473-B_WHITE.png.avif
  13. BIN
      shop/static/shop/images/products/PC002/blanc/PS_K473_WHITE.png.avif
  14. BIN
      shop/static/shop/images/products/PC002/blanc/SWEAT - K473 BLANC- DEVANT-F_.png
  15. BIN
      shop/static/shop/images/products/PC002/blanc/SWEAT - K473-B_BLANC.png
  16. 0
      shop/static/shop/images/products/PC002/bleu-sport/PS_K473_NAVY-F_.png.avif
  17. 0
      shop/static/shop/images/products/PC002/fuchsia/PS_K473_FUCHSIA-F_.png.avif
  18. BIN
      shop/static/shop/images/products/PC002/kaki-fonce/SWEAT - K473 DARK KAKI - DEVANT-F_.png
  19. BIN
      shop/static/shop/images/products/PC002/kaki-fonce/SWEAT - K473-B_DARK-KAKI.png
  20. 0
      shop/static/shop/images/products/PC002/noir/PS_K473_BLACK-F_.png.avif
  21. BIN
      shop/static/shop/images/products/PC003/blanc/PS_K476-B_WHITE.png.avif
  22. BIN
      shop/static/shop/images/products/PC003/blanc/PS_K476_WHITE.png.avif
  23. BIN
      shop/static/shop/images/products/PC003/blanc/SWEAT - K476 BLANC - DEVANT-F_.png
  24. BIN
      shop/static/shop/images/products/PC003/blanc/SWEAT - K476-B_BLANC.png
  25. 0
      shop/static/shop/images/products/PC003/bleu-sport/PS_K476_NAVY-F_.png.avif
  26. BIN
      shop/static/shop/images/products/PC003/fuchsia/PS_K476-B_FUCHSIA.png.avif
  27. BIN
      shop/static/shop/images/products/PC003/fuchsia/PS_K476_FUCHSIA.png.avif
  28. BIN
      shop/static/shop/images/products/PC003/fuchsia/SWEAT - K476-B_FUSHIA.png
  29. BIN
      shop/static/shop/images/products/PC003/fuchsia/SWEAT - K476-FUSHIA-F_.png
  30. BIN
      shop/static/shop/images/products/PC003/kaki-fonce/SWEAT - K476 DARK KAKI - DEVANT-F_.png
  31. BIN
      shop/static/shop/images/products/PC003/kaki-fonce/SWEAT - K476-B_DARK-KAKI.png
  32. 0
      shop/static/shop/images/products/PC003/noir/PS_K476_BLACK-F_.png.avif
  33. BIN
      shop/static/shop/images/products/PC004/blanc-bleu-sport/DEBARDEUR PA4031-F_.png
  34. BIN
      shop/static/shop/images/products/PC004/blanc-bleu-sport/PS_PA4031_WHITE-SPORTYNAVY.png.avif
  35. 0
      shop/static/shop/images/products/PC004/noir-corail/PS_PA4031_BLACK-CORAL-F_.png.avif
  36. 0
      shop/static/shop/images/products/PC004/noir-gris-fonce-chine/PS_PA4031_BLACK-MARLDARKGREY-F_.png.avif
  37. BIN
      shop/static/shop/images/products/PC005/blanc-bleu-sport/JUPE PA1031-F_.png
  38. BIN
      shop/static/shop/images/products/PC005/blanc-bleu-sport/PS_PA1031_WHITE-SPORTYNAVY.png.avif
  39. 0
      shop/static/shop/images/products/PC005/bleu-sport-blanc/PS_PA1031_SPORTYNAVY-WHITE-F_.png.avif
  40. 0
      shop/static/shop/images/products/PC005/corail-noir/PS_PA1031_CORAL-BLACK-F_.png.avif
  41. 0
      shop/static/shop/images/products/PC005/noir-gris-fonce-chine/PS_PA1031_BLACK-MARLDARKGREY-F_.png.avif
  42. 0
      shop/static/shop/images/products/PC006/blanc-gris-clair/PS_PA4030_WHITE-FINEGREY-F_.png.avif
  43. BIN
      shop/static/shop/images/products/PC006/bleu-sport-blanc/PS_PA4030_SPORTYNAVY-WHITE.png.avif
  44. BIN
      shop/static/shop/images/products/PC006/bleu-sport-blanc/T.SHIRT PA4030-F_.png
  45. 0
      shop/static/shop/images/products/PC006/bleu-sport-bleu-sport-chine/PS_PA4030_SPORTYNAVY-MARLSPORTYNAVY-F_.png.avif
  46. 0
      shop/static/shop/images/products/PC006/noir-gris-fonce-chine/PS_PA4030_BLACK-MARLDARKGREY-F_.png.avif
  47. 0
      shop/static/shop/images/products/PC006/noir/PS_PA4030_BLACK-F_.png.avif
  48. BIN
      shop/static/shop/images/products/PC007/blanc-bleu-sport/PS_PA1030_WHITE-SPORTYNAVY.png.avif
  49. BIN
      shop/static/shop/images/products/PC007/blanc-bleu-sport/SHORT PA1030-F_.png
  50. 0
      shop/static/shop/images/products/PC007/blanc-gris-clair/PS_PA1030_WHITE-FINEGREY-F_.png.avif
  51. 0
      shop/static/shop/images/products/PC007/gris-fonce-chine-noir/PS_PA1030_MARLDARKGREY-BLACK-F_.png.avif
  52. 0
      shop/static/shop/images/products/PC007/noir/PS_PA1030_BLACK-F_.png.avif
  53. 4
      shop/templates/shop/cart.html
  54. 4
      shop/templates/shop/checkout.html
  55. 4
      shop/templates/shop/payment.html
  56. 4
      shop/templates/shop/payment_cancel.html
  57. 4
      shop/templates/shop/payment_success.html
  58. 8
      shop/templates/shop/product_item.html
  59. 6
      shop/templates/shop/product_list.html
  60. 16
      shop/templatetags/shop_extras.py
  61. 1
      sync/utils.py
  62. 4
      sync/views.py
  63. 4
      tournaments/admin.py
  64. 3
      tournaments/filters.py
  65. 94
      tournaments/forms.py
  66. 36
      tournaments/migrations/0114_purchase_creation_date_purchase_last_update_and_more.py
  67. 45
      tournaments/migrations/0115_auto_20250403_1503.py
  68. 88
      tournaments/models/enums.py
  69. 2
      tournaments/models/failed_api_call.py
  70. 2
      tournaments/models/log.py
  71. 1
      tournaments/models/player_enums.py
  72. 67
      tournaments/models/player_registration.py
  73. 3
      tournaments/models/purchase.py
  74. 10
      tournaments/models/team_registration.py
  75. 4
      tournaments/models/team_score.py
  76. 225
      tournaments/models/tournament.py
  77. 2
      tournaments/models/unregistered_team.py
  78. 2
      tournaments/services/email_service.py
  79. 52
      tournaments/services/tournament_registration.py
  80. 13
      tournaments/services/tournament_unregistration.py
  81. 14
      tournaments/signals.py
  82. 13655
      tournaments/static/rankings/CLASSEMENT-PADEL-DAMES-04-2025.csv
  83. 80001
      tournaments/static/rankings/CLASSEMENT-PADEL-MESSIEURS-04-2025.csv
  84. 8
      tournaments/static/tournaments/css/basics.css
  85. 202
      tournaments/static/tournaments/css/style.css
  86. 8
      tournaments/static/tournaments/css/tournament_bracket.css
  87. 6
      tournaments/templates/profile.html
  88. 4
      tournaments/templates/register_tournament.html
  89. 2
      tournaments/templates/registration/activation_failed.html
  90. 2
      tournaments/templates/registration/activation_success.html
  91. 2
      tournaments/templates/registration/login.html
  92. 74
      tournaments/templates/registration/my_tournaments.html
  93. 2
      tournaments/templates/registration/password_reset_complete.html
  94. 2
      tournaments/templates/registration/password_reset_confirm.html
  95. 2
      tournaments/templates/registration/password_reset_done.html
  96. 2
      tournaments/templates/registration/password_reset_form.html
  97. 4
      tournaments/templates/registration/signup.html
  98. 2
      tournaments/templates/registration/signup_success.html
  99. 2
      tournaments/templates/tournaments/admin/mail_test.html
  100. 4
      tournaments/templates/tournaments/base.html
  101. Some files were not shown because too many files have changed in this diff Show More

@ -3,7 +3,7 @@
{% block content %}
<div class="container padding-bottom">
<div class="grid-x padding-bottom">
<div class="cell medium-6 large-6 my-block bubble">
<div class="cell medium-6 large-6 padding10 bubble">
<h1 class="title">Add New Prospect</h1>
<form method="post">

@ -44,7 +44,7 @@
<body class="wrapper">
<header>
<div class="grid-x">
<div class="medium-6 large-9 cell topblock my-block ">
<div class="medium-6 large-9 cell topblock padding10 ">
<a href="{% url 'index' %}">
<img
src="{% static 'tournaments/images/PadelClub_logo_512.png' %}"

@ -1,7 +1,7 @@
{% extends "crm/base.html" %} {% block content %}
<div class="container">
<div class="grid-x padding-bottom">
<div class="cell medium-6 large-6 my-block bubble">
<div class="cell medium-6 large-6 padding10 bubble">
<h1 class="title">
{% if form.instance.pk %}Edit{% else %}Add{% endif %} Event
</h1>

@ -23,7 +23,7 @@
</div>
<div class="container grid-x padding-bottom">
<div class="cell medium-6 large-6 my-block bubble">
<div class="cell medium-6 large-6 padding10 bubble">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="title">Completed Events</h1>
@ -39,7 +39,7 @@
</div>
<div class="cell medium-6 large-6 my-block bubble">
<div class="cell medium-6 large-6 padding10 bubble">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="title">Planned Events</h1>

@ -9,16 +9,17 @@ class Command(BaseCommand):
# Create colors
self.stdout.write('Creating colors...')
colors = [
{'name': 'Blanc', 'hex': '#FFFFFF', 'secondary_hex': None, 'ordering': 10},
{'name': 'Blanc / Bleu Sport', 'hex': '#FFFFFF', 'secondary_hex': '#112B44', 'ordering': 11},
{'name': 'Blanc', 'hex': '#FFFFFF', 'secondary_hex': None, 'ordering': 9},
{'name': 'Blanc / Bleu Sport', 'hex': '#FFFFFF', 'secondary_hex': '#112B44', 'ordering': 10},
{'name': 'Blanc / Gris Clair', 'hex': '#FFFFFF', 'secondary_hex': '#D3D3D3', 'ordering': 12},
{'name': 'Bleu Sport', 'hex': '#112B44', 'secondary_hex': None, 'ordering': 20},
{'name': 'Bleu Sport / Blanc', 'hex': '#112B44', 'secondary_hex': '#FFFFFF', 'ordering': 21},
{'name': 'Bleu Sport / Blanc', 'hex': '#112B44', 'secondary_hex': '#FFFFFF', 'ordering': 11},
{'name': 'Bleu Sport / Bleu Sport Chiné', 'hex': '#112B44', 'secondary_hex': '#16395A', 'ordering': 22},
{'name': 'Fuchsia', 'hex': '#C1366B', 'secondary_hex': None, 'ordering': 30},
{'name': 'Corail / Noir', 'hex': '#FF7F50', 'secondary_hex': '#000000', 'ordering': 40},
{'name': 'Gris Foncé Chiné / Noir', 'hex': '#4D4D4D', 'secondary_hex': '#000000', 'ordering': 50},
{'name': 'Noir', 'hex': '#333333', 'secondary_hex': None, 'ordering': 60},
{'name': 'Kaki Foncé', 'hex': '#707163', 'secondary_hex': None, 'ordering': 55},
{'name': 'Noir', 'hex': '#000000', 'secondary_hex': None, 'ordering': 60},
{'name': 'Noir / Corail', 'hex': '#000000', 'secondary_hex': '#FF7F50', 'ordering': 61},
{'name': 'Noir / Gris Foncé Chiné', 'hex': '#000000', 'secondary_hex': '#4D4D4D', 'ordering': 62},
]
@ -77,7 +78,7 @@ class Command(BaseCommand):
'price': 50.00,
'ordering_value': 10,
'cut': 1,
'colors': ['Blanc', 'Bleu Sport', 'Noir', 'Fuchsia'],
'colors': ['Blanc', 'Bleu Sport', 'Kaki Foncé', 'Noir', 'Fuchsia'],
'sizes': ['XS', 'S', 'M', 'L', 'XL', 'XXL'],
'image_filename': 'PS_K473_WHITE.png.avif'
},
@ -88,7 +89,7 @@ class Command(BaseCommand):
'price': 50.00,
'ordering_value': 11,
'cut': 2,
'colors': ['Blanc', 'Bleu Sport', 'Noir', 'Fuchsia'],
'colors': ['Blanc', 'Bleu Sport', 'Kaki Foncé', 'Noir', 'Fuchsia'],
'sizes': ['XS', 'S', 'M', 'L', 'XL', 'XXL', '3XL', '4XL'],
'image_filename': 'PS_K476_WHITE.png.avif'
},

@ -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;
@ -541,3 +541,8 @@ v .cart-table {
.next:hover {
opacity: 1;
}
.color-sample.selected {
border: 3px solid #90ee90 !important; /* Use your light-green color */
transform: scale(1.1); /* Makes the selected color slightly larger */
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

@ -25,8 +25,8 @@
{% endif %}
<div class="grid-x">
<div class="small-12 medium-9 large-6 my-block">
<h1 class="club my-block topmargin20">Votre panier</h1>
<div class="small-12 medium-9 large-6 padding10">
<h1 class="club padding10 topmargin20">Votre panier</h1>
<div class="bubble">
{% if display_data.items %}
<div class="info-box" style="background-color: #f8f9fa; border-left: 4px solid #4e73df; padding: 15px; margin: 15px 0; border-radius: 5px;">

@ -17,9 +17,9 @@
<a href="{% url 'login' %}">Se connecter</a>
{% endif %}
</nav>
<h1 class="club my-block topmargin20">Validation de la commande</h1>
<h1 class="club padding10 topmargin20">Validation de la commande</h1>
<div class="grid-x">
<div class="small-12 medium-6 large-6 my-block">
<div class="small-12 medium-6 large-6 padding10">
<div class="bubble checkout-container">
{% if request.user.is_authenticated %}
<div class="checkout-section">

@ -25,8 +25,8 @@
</nav>
<div class="grid-x">
<div class="cell medium-6 large-6 my-block">
<h1 class="club my-block topmargin20">Résumé de votre commande</h1>
<div class="cell medium-6 large-6 padding10">
<h1 class="club padding10 topmargin20">Résumé de votre commande</h1>
<div class="bubble">
{% include 'shop/partials/order_items_display.html' with items=display_data.items total_quantity=display_data.total_quantity total_price=display_data.total_price edit_mode=False %}

@ -20,8 +20,8 @@
</nav>
<div class="grid-x">
<div class="cell medium-6 large-6 my-block">
<h1 class="club my-block topmargin20">Paiement</h1>
<div class="cell medium-6 large-6 padding10">
<h1 class="club padding10 topmargin20">Paiement</h1>
<div class="bubble">
<h2>Le paiement a été annulé</h2>
<p>Votre commande n'a pas été finalisée car le paiement a été annulé.</p>

@ -18,8 +18,8 @@
</nav>
<div class="grid-x">
<div class="cell medium-6 large-6 my-block">
<h1 class="club my-block topmargin20">Paiement réussi</h1>
<div class="cell medium-6 large-6 padding10">
<h1 class="club padding10 topmargin20">Paiement réussi</h1>
<div class="bubble">
<h2>Merci pour votre commande !</h2>
<p>Votre paiement a été traité avec succès.</p>

@ -1,5 +1,5 @@
{% load shop_extras %}
<div class="small-12 medium-6 large-3 my-block">
<div class="small-12 medium-6 large-3 padding10">
<div class="bubble">
{% if product.image %}
<div class="slider-container" id="slider-{{ product.id }}">
@ -137,7 +137,9 @@ function selectColor(productId, colorId, colorName, element) {
// Remove selected class from all colors
const colorSamples = element.parentElement.querySelectorAll('.color-sample');
colorSamples.forEach(sample => sample.classList.remove('selected'));
colorSamples.forEach(sample => {
sample.classList.remove('selected');
});
// Add selected class to clicked color
element.classList.add('selected');
@ -185,7 +187,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';

@ -18,6 +18,12 @@
{% endif %}
</nav>
<div class="info-box" style="border-left: 4px solid #f39200; padding: 12px; margin: 20px 12px;">
<h3 style="color: #505050; margin-top: 0;">Bienvenue sur la boutique Padel Club des copains !</h3>
<p style="margin-top: 10px; margin-bottom: 0;"><strong>Photos :</strong> Les photos des vêtements n'ont pas encore tous le logo, la description indique où il situera.
<p style="margin-top: 10px; margin-bottom: 0;"><strong>Livraison :</strong> Commandez en ligne et récupérez votre commande en main propre lors de votre prochaine session de padel au club !</p>
</div>
<nav class="margin10">
<a class="confirm-nav-button" href="{% url 'shop:view_cart' %}">Voir mon panier ({{ total }} €)</a>
{% if cart_items %}

@ -36,9 +36,11 @@ def color_images_url(default_image, color_name, sku):
if files:
# Sort files by specific prefix rules
files.sort(key=lambda x: (
1 if '-B_' in x else
2 if '-S_' in x else
0
2 if '-B_' in x else
3 if '-S_' in x else
1 if '-F_' in x else
0 if '-logo' in x else
4
))
return [f'{base_path}{color_folder}/{file}' for file in files]
@ -49,9 +51,11 @@ def color_images_url(default_image, color_name, sku):
any(f.lower().endswith(ext) for ext in supported_extensions)]
if files:
files.sort(key=lambda x: (
1 if '-B_' in x else
2 if '-S_' in x else
0
2 if '-B_' in x else
3 if '-S_' in x else
1 if '-F_' in x else
0 if '-logo' in x else
4
))
return [f'{base_path}{file}' for file in files]

@ -31,6 +31,7 @@ def get_serializer(instance, model_name):
def get_data(model_name, model_id):
model = sync_registry.get_model(model_name)
# print(f'model_name = {model_name}')
# model = apps.get_model(app_label=app_label, model_name=model_name)
return model.objects.get(id=model_id)

@ -106,10 +106,10 @@ class SynchronizationApi(HierarchyApiView):
data = op.get('data')
data_id = data.get('id')
device_registry.register(data_id, device_id)
# print(f'*** 1count = {device_registry.count()}')
# print(f'*** YEAH: {model_operation} : {model_name}')
try:
print(f'{model_operation} : {model_name}, id = {data['id']}')
# print(f'{model_operation} : {model_name}, id = {data['id']}')
models.add(model_name)

@ -13,7 +13,9 @@ class CustomUserAdmin(UserAdmin):
form = CustomUserChangeForm
add_form = CustomUserCreationForm
model = CustomUser
list_display = ['email', 'first_name', 'last_name', 'username', 'date_joined', 'latest_event_club_name', 'is_active', 'event_count', 'origin']
search_fields = ('username', 'email', 'phone', 'first_name', 'last_name', 'licence_id')
list_display = ['email', 'first_name', 'last_name', 'username', 'licence_id', 'date_joined', 'latest_event_club_name', 'is_active', 'event_count', 'origin']
list_filter = ['is_active', 'origin']
ordering = ['-date_joined']
fieldsets = [

@ -1,10 +1,9 @@
from django.contrib import admin
from .models import Club, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, Court, DateInterval, FailedApiCall
from .models import Tournament, Match
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from datetime import timedelta
import uuid
from enum import Enum
class SimpleTournamentListFilter(admin.SimpleListFilter):

@ -5,19 +5,44 @@ import re # Import the re module for regular expressions
from .utils.licence_validator import LicenseValidator
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.http import urlsafe_base64_encode
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.shortcuts import get_current_site
from django.utils.encoding import force_bytes
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth import authenticate # Add this import
import logging
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()
licence_id = licence_id.replace(' ', '').strip().upper()
validator = LicenseValidator(licence_id)
if validator.validate_license():
licence_id = validator.computed_licence_id
else:
raise forms.ValidationError('Le numéro de licence est invalide, la lettre ne correspond pas.')
return licence_id
def clean_email(self):
email = self.cleaned_data.get('email')
if email:
email = email.lower()
if CustomUser.objects.filter(email__iexact=email).exclude(pk=self.instance.pk).exists():
raise forms.ValidationError("Cet email est déjà utilisé. Veuillez en choisir un autre :)")
return email
def clean_username(self):
username = self.cleaned_data.get('username')
if username:
username = username.lower()
if CustomUser.objects.filter(username__iexact=username).exclude(pk=self.instance.pk).exists() | CustomUser.objects.filter(email__iexact=username).exclude(pk=self.instance.pk).exists():
raise forms.ValidationError("Cet identifiant est déjà utilisé. Veuillez en choisir un autre :)")
return username
class Meta:
model = CustomUser
error_messages = {
@ -41,7 +66,12 @@ class SimpleCustomUserCreationForm(UserCreationForm):
def clean_licence_id(self):
licence_id = self.cleaned_data.get('licence_id')
if licence_id:
return licence_id.replace(' ', '').strip().upper()
licence_id = licence_id.replace(' ', '').strip().upper()
validator = LicenseValidator(licence_id)
if validator.validate_license():
licence_id = validator.computed_licence_id
else:
raise forms.ValidationError('Le numéro de licence est invalide, la lettre ne correspond pas.')
return licence_id
def clean_phone(self):
@ -81,16 +111,42 @@ class SimpleCustomUserCreationForm(UserCreationForm):
'password2': 'Confirmer le mot de passe',
}
def clean_email(self):
email = self.cleaned_data.get('email')
if email:
email = email.lower()
if CustomUser.objects.filter(email__iexact=email).exclude(pk=self.instance.pk).exists():
raise forms.ValidationError("Cet email est déjà utilisé. Veuillez en choisir un autre :)")
return email
def clean_username(self):
username = self.cleaned_data.get('username')
if username:
username = username.lower()
if CustomUser.objects.filter(username__iexact=username).exists() | CustomUser.objects.filter(email__iexact=username).exists():
if CustomUser.objects.filter(username__iexact=username).exclude(pk=self.instance.pk).exists() | CustomUser.objects.filter(email__iexact=username).exclude(pk=self.instance.pk).exists():
raise forms.ValidationError("Cet identifiant est déjà utilisé. Veuillez en choisir un autre :)")
return username
class CustomUserChangeForm(UserChangeForm):
def clean_username(self):
username = self.cleaned_data.get('username')
if username:
username = username.lower()
if CustomUser.objects.filter(username__iexact=username).exclude(pk=self.instance.pk).exists() | CustomUser.objects.filter(email__iexact=username).exclude(pk=self.instance.pk).exists():
raise forms.ValidationError("Cet identifiant est déjà utilisé. Veuillez en choisir un autre :)")
return username
def clean_licence_id(self):
licence_id = self.cleaned_data.get('licence_id')
if licence_id:
licence_id = licence_id.replace(' ', '').strip().upper()
validator = LicenseValidator(licence_id)
if validator.validate_license():
licence_id = validator.computed_licence_id
else:
raise forms.ValidationError('Le numéro de licence est invalide, la lettre ne correspond pas.')
return licence_id
class Meta:
model = CustomUser
@ -206,10 +262,31 @@ class ProfileUpdateForm(forms.ModelForm):
# Remove autofocus from the 'username' field
self.fields['username'].widget.attrs.pop("autofocus", None)
def clean_email(self):
email = self.cleaned_data.get('email')
if email:
email = email.lower()
if CustomUser.objects.filter(email__iexact=email).exclude(pk=self.instance.pk).exists():
raise forms.ValidationError("Cet email est déjà utilisé. Veuillez en choisir un autre :)")
return email
def clean_username(self):
username = self.cleaned_data.get('username')
if username:
username = username.lower()
if CustomUser.objects.filter(username__iexact=username).exclude(pk=self.instance.pk).exists() | CustomUser.objects.filter(email__iexact=username).exclude(pk=self.instance.pk).exists():
raise forms.ValidationError("Cet identifiant est déjà utilisé. Veuillez en choisir un autre :)")
return username
def clean_licence_id(self):
licence_id = self.cleaned_data.get('licence_id')
if licence_id:
return licence_id.replace(' ', '').upper()
licence_id = licence_id.replace(' ', '').strip().upper()
validator = LicenseValidator(licence_id)
if validator.validate_license():
licence_id = validator.computed_licence_id
else:
raise forms.ValidationError('Le numéro de licence est invalide, la lettre ne correspond pas.')
return licence_id
def clean_phone(self):
@ -245,8 +322,6 @@ class ProfileUpdateForm(forms.ModelForm):
},
}
from django.contrib.auth.forms import PasswordChangeForm
class CustomPasswordChangeForm(PasswordChangeForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -254,11 +329,6 @@ class CustomPasswordChangeForm(PasswordChangeForm):
for field in self.fields.values():
field.widget.attrs.pop("autofocus", None)
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth import authenticate # Add this import
from django import forms
import logging
logger = logging.getLogger(__name__)
class EmailOrUsernameAuthenticationForm(AuthenticationForm):

@ -0,0 +1,36 @@
# Generated by Django 5.1 on 2025-04-02 14:02
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0113_tournament_team_count_limit'),
]
operations = [
migrations.AddField(
model_name='purchase',
name='creation_date',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
),
migrations.AddField(
model_name='purchase',
name='last_update',
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AddField(
model_name='purchase',
name='last_updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='purchase',
name='related_user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL),
),
]

@ -0,0 +1,45 @@
# Generated by Django 5.1 on 2025-04-03 12:29
from django.db import migrations
from ..utils.licence_validator import LicenseValidator
def clean_license_ids(apps, schema_editor):
# Get the historical model
CustomUser = apps.get_model('tournaments', 'CustomUser')
# Query all users
users = CustomUser.objects.all()
for user in users:
if user.licence_id:
# Clean up logic - examples:
# 1. Strip whitespace
cleaned_id = user.licence_id.strip()
cleaned_id = cleaned_id.replace(' ', '').strip().upper()
last_char = cleaned_id[-1] if cleaned_id else ''
ends_with_non_alpha = not last_char.isalpha()
if ends_with_non_alpha:
key = LicenseValidator.get_computed_license_key(cleaned_id)
if key:
cleaned_id += key.upper()
# Save the cleaned value
if cleaned_id != user.licence_id:
user.licence_id = cleaned_id
user.save()
def reverse_migration(apps, schema_editor):
# Reversing this migration isn't generally needed for data cleanup
pass
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0114_purchase_creation_date_purchase_last_update_and_more'),
]
operations = [
migrations.RunPython(clean_license_ids, reverse_migration),
]

@ -1,5 +1,4 @@
from django.db import models
import uuid
class TournamentPayment(models.IntegerChoices):
FREE = 0, 'Gratuit'
@ -48,6 +47,7 @@ class FederalLevelCategory(models.IntegerChoices):
P1000 = 1000, 'P1000'
P1500 = 1500, 'P1500'
P2000 = 2000, 'P2000'
CHPT = 1, 'Championnat'
@staticmethod
def min_player_rank(level=None, category=None, age_category=None) -> int:
@ -146,6 +146,42 @@ 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 display_register_option(self) -> bool:
status_map = {
OnlineRegistrationStatus.OPEN: True,
OnlineRegistrationStatus.NOT_ENABLED: False,
OnlineRegistrationStatus.NOT_STARTED: False,
OnlineRegistrationStatus.ENDED: False,
OnlineRegistrationStatus.WAITING_LIST_POSSIBLE: True,
OnlineRegistrationStatus.WAITING_LIST_FULL: False,
OnlineRegistrationStatus.IN_PROGRESS: False,
OnlineRegistrationStatus.ENDED_WITH_RESULTS: False,
OnlineRegistrationStatus.CANCELED: False
}
return status_map.get(self, False)
def register_button_text(self) -> str:
if self == OnlineRegistrationStatus.OPEN:
return "S'inscrire"
elif self == OnlineRegistrationStatus.NOT_ENABLED:
return "Inscription désactivée"
elif self == OnlineRegistrationStatus.NOT_STARTED:
return "Ouverture des inscriptions à venir"
elif self == OnlineRegistrationStatus.ENDED:
return "Inscription terminée"
elif self == OnlineRegistrationStatus.WAITING_LIST_POSSIBLE:
return "S'inscrire en liste d'attente"
elif self == OnlineRegistrationStatus.WAITING_LIST_FULL:
return "Liste d'attente complète"
elif self == OnlineRegistrationStatus.IN_PROGRESS:
return "Tournoi en cours"
elif self == OnlineRegistrationStatus.ENDED_WITH_RESULTS:
return "Tournoi terminé"
elif self == OnlineRegistrationStatus.CANCELED:
return "Tournoi annulé"
return ""
def status_localized(self) -> str:
status_map = {
@ -156,10 +192,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'

@ -1,5 +1,5 @@
from django.db import models
from . import BaseModel, CustomUser
from . import CustomUser
import uuid
class FailedApiCall(models.Model):

@ -1,5 +1,5 @@
from django.db import models
from . import BaseModel, CustomUser
from . import CustomUser
import uuid
class Log(models.Model):

@ -1,5 +1,4 @@
from django.db import models
import uuid
class PlayerPaymentType(models.IntegerChoices):
CASH = 0, 'Cash'

@ -1,5 +1,5 @@
from django.db import models
from . import SideStoreModel, TeamRegistration, PlayerSexType, PlayerDataSource, PlayerPaymentType
from . import SideStoreModel, TeamRegistration, PlayerSexType, PlayerDataSource, PlayerPaymentType, OnlineRegistrationStatus
import uuid
from django.utils import timezone
@ -107,3 +107,68 @@ 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
tournament = self.team_registration.tournament
tournament_status_team_count = tournament.get_tournament_status_team_count()
status = {
'header': "Équipes",
'position': tournament_status_team_count,
'display_box': True,
'box_class': 'gray',
'short_label': 'inscrit'
}
team = self.team_registration
# Tournament is ended with results
if tournament.get_online_registration_status() is OnlineRegistrationStatus.ENDED_WITH_RESULTS:
if team.final_ranking:
status['header'] = 'Rang'
status['position'] = f"{team.final_ranking} / {tournament_status_team_count}"
if tournament.display_points_earned and team.points_earned:
if team.final_ranking == 1:
status['box_class'] = 'light-gold'
elif team.final_ranking == 2:
status['box_class'] = 'light-silver'
elif team.final_ranking == 3:
status['box_class'] = 'light-bronze'
else:
status['box_class'] = 'light-beige'
status['short_label'] = f"{team.points_earned} pts"
else:
status['display_box'] = False
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

@ -1,9 +1,8 @@
from django.db import models
import uuid
from . import BaseModel, CustomUser
class Purchase(models.Model):
class Purchase(BaseModel):
id = models.BigIntegerField(primary_key=True, unique=True, editable=True)
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
purchase_date = models.DateTimeField()

@ -238,10 +238,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):

@ -1,6 +1,7 @@
from django.db import models
from . import SideStoreModel, Match, TeamRegistration, PlayerRegistration, FederalMatchCategory
from . import SideStoreModel, Match, TeamRegistration, FederalMatchCategory
import uuid
from .match import Team # Import Team only when needed
class TeamScore(SideStoreModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)
@ -125,7 +126,6 @@ class TeamScore(SideStoreModel):
names = self.shortened_team_names()
scores = self.parsed_scores()
walk_out = self.walk_out
from .match import Team # Import Team only when needed
is_lucky_loser = self.lucky_loser is not None
team = Team(id, image, names, scores, weight, is_winner, walk_out, is_lucky_loser)
return team

@ -1,19 +1,15 @@
from time import daylight
from zoneinfo import ZoneInfo
from django.db import models
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from tournaments.models import group_stage
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
from django.apps import apps
class TeamSortingType(models.IntegerChoices):
RANK = 1, 'Rank'
@ -180,6 +176,8 @@ class Tournament(BaseModel):
def level(self):
if self.federal_level_category == 0:
return "Anim."
if self.federal_level_category == 1:
return "CHPT"
return self.get_federal_level_category_display()
def category(self):
@ -231,36 +229,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
@ -333,9 +307,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
@ -1091,81 +1065,64 @@ class Tournament(BaseModel):
return options
def online_register_is_enabled(self):
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.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 False
return OnlineRegistrationStatus.ENDED
if self.closed_registration_date is not None:
return False
if self.end_date is not None:
return False
return OnlineRegistrationStatus.WAITING_LIST_POSSIBLE
now = timezone.now()
# Check if online registration is enabled
if not self.enable_online_registration:
return False
# Check opening registration date
if self.opening_registration_date is not None:
timezoned_datetime = timezone.localtime(self.opening_registration_date)
if now < timezoned_datetime:
return False
return OnlineRegistrationStatus.NOT_STARTED
# Check registration date limit
if self.registration_date_limit is not None:
timezoned_datetime = timezone.localtime(self.registration_date_limit)
if now > timezoned_datetime:
return False
return OnlineRegistrationStatus.WAITING_LIST_POSSIBLE
if self.team_sorting == TeamSortingType.RANK:
return OnlineRegistrationStatus.OPEN
# Check target team count and waiting list limit
if self.team_count is not None:
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 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.WAITING_LIST_FULL
return OnlineRegistrationStatus.WAITING_LIST_POSSIBLE
if self.end_date is not None:
return OnlineRegistrationStatus.ENDED_WITH_RESULTS
return OnlineRegistrationStatus.OPEN
now = timezone.now()
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()
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_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
@ -1638,6 +1595,86 @@ class Tournament(BaseModel):
@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):
licence_id = user.licence_id
if not licence_id:
return None
validator = LicenseValidator(licence_id)
if validator.validate_license():
stripped_license = validator.stripped_license
# Check if there is a PlayerRegistration for this user in this tournament
PlayerRegistration = apps.get_model('tournaments', 'PlayerRegistration')
user_player = PlayerRegistration.objects.filter(
licence_id__icontains=stripped_license,
team_registration__tournament=self,
).first()
if user_player:
return user_player.get_registration_status()
return None
class MatchGroup:
def __init__(self, name, matches, formatted_schedule, round_id=None):
self.name = name

@ -1,8 +1,6 @@
from django.db import models
from django.db.models.sql.query import Q
from . import Tournament
import uuid
from django.utils import timezone
class UnregisteredTeam(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)

@ -1,6 +1,4 @@
from django.core.mail import EmailMessage
from django.utils import timezone
from django.urls import reverse
from enum import Enum
from ..models.player_registration import RegistrationStatus
from ..models.tournament import TeamSortingType

@ -7,6 +7,9 @@ from ..utils.licence_validator import LicenseValidator
from ..utils.player_search import get_player_name_from_csv
from tournaments.models import PlayerRegistration
from ..utils.extensions import is_not_sqlite_backend
from django.contrib.auth import get_user_model
from django.contrib.messages import get_messages
from django.db import IntegrityError
class TournamentRegistrationService:
def __init__(self, request, tournament):
@ -49,7 +52,6 @@ class TournamentRegistrationService:
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:
@ -70,6 +72,10 @@ class TournamentRegistrationService:
if self._is_already_registered(licence_id):
return
if self.request.user.is_authenticated and self.request.user.licence_id is None:
if self._update_user_license(player_data.get('licence_id')) == False:
return
if self.request.user.licence_id is None and len(self.context['current_players']) == 0:
# if no licence id for authentificated user and trying to add him as first player of the team, we check his federal data
self._handle_invalid_names(licence_id, player_data)
@ -80,9 +86,6 @@ class TournamentRegistrationService:
else:
self._handle_invalid_names(licence_id, player_data)
if self.request.user.is_authenticated and self.request.user.licence_id is None:
self._update_user_license(player_data.get('licence_id'))
def handle_team_registration(self):
if not self.context['team_form'].is_valid():
return
@ -119,7 +122,7 @@ class TournamentRegistrationService:
self.context['registration_successful'] = True
def handle_get_request(self):
from django.contrib.messages import get_messages
print("handle_get_request")
storage = get_messages(self.request)
# Iterate through the storage to clear it
for _ in storage:
@ -180,8 +183,6 @@ class TournamentRegistrationService:
self.request.session.modified = True
def _get_authenticated_user_data(self):
from ..utils.player_search import get_player_name_from_csv
from ..utils.licence_validator import LicenseValidator
user = self.request.user
validator = LicenseValidator(user.licence_id)
@ -209,6 +210,7 @@ class TournamentRegistrationService:
return player_data
def _validate_license(self, licence_id):
print("Validating license...")
validator = LicenseValidator(licence_id)
if validator.validate_license() is False and self.tournament.license_is_required:
@ -221,6 +223,7 @@ class TournamentRegistrationService:
# computed_license_key = validator.computed_license_key
# messages.error(self.request, f"Le numéro de licence est invalide, la lettre ne correspond pas. {computed_license_key}")
messages.error(self.request, "Le numéro de licence est invalide, la lettre ne correspond pas.")
print("License validation failed")
return False
return True
@ -276,16 +279,26 @@ class TournamentRegistrationService:
player_data['is_woman'] = self.request.session.get('is_woman', False)
def _update_user_license(self, licence_id):
if self.request.user.is_authenticated and licence_id:
self.context['add_player_form'].user_without_licence = False
validator = LicenseValidator(licence_id)
self.request.user.licence_id = validator.computed_licence_id
self.request.user.save()
self.request.user.refresh_from_db()
self.request.session.modified = True
# Reset the form state
self.context['add_player_form'] = AddPlayerForm()
self.context['add_player_form'].first_tournament = False
if not self.request.user.is_authenticated or not licence_id:
return False
self.context['add_player_form'].user_without_licence = False
validator = LicenseValidator(licence_id)
if validator.validate_license():
computed_licence_id = validator.computed_licence_id
try:
self.request.user.licence_id = computed_licence_id
self.request.user.save()
self.request.user.refresh_from_db()
self.request.session.modified = True
return True
except IntegrityError:
# Handle the duplicate license error
error_msg = f"Ce numéro de licence ({computed_licence_id}) est déjà utilisé par un autre joueur."
messages.error(self.request, error_msg)
return False
def _update_player_data_from_csv(self, player_data, csv_data):
print("_update_player_data_from_csv", player_data, csv_data)
@ -305,16 +318,15 @@ class TournamentRegistrationService:
'phone': None,
})
from django.contrib.auth import get_user_model
User = get_user_model()
# Get the license ID from player_data
licence_id = player_data.get('licence_id')
validator = LicenseValidator(licence_id)
if validator and validator.stripped_license:
if validator.validate_license():
try:
# Try to find a user with matching license
user_with_same_license = User.objects.get(licence_id__icontains=validator.stripped_license)
user_with_same_license = User.objects.get(licence_id__iexact=validator.computed_licence_id)
# If found, update the email and phone
if user_with_same_license:

@ -1,8 +1,7 @@
from django.contrib import messages
from django.utils import timezone
from ..models import PlayerRegistration, UnregisteredTeam, UnregisteredPlayer
from ..models.player_enums import PlayerDataSource
from ..services.email_service import TournamentEmailService
from ..utils.licence_validator import LicenseValidator
class TournamentUnregistrationService:
def __init__(self, request, tournament):
@ -52,8 +51,16 @@ class TournamentUnregistrationService:
)
def _find_player_registration(self):
if not self.request.user.licence_id:
return False
validator = LicenseValidator(self.request.user.licence_id)
is_license_valid = validator.validate_license()
if not is_license_valid:
return False
self.player_registration = PlayerRegistration.objects.filter(
licence_id__icontains=self.request.user.licence_id,
licence_id__icontains=validator.stripped_license,
team_registration__tournament_id=self.tournament.id,
).first()

@ -1,20 +1,12 @@
import random
import string
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete
from django.db.models.signals import pre_save, post_save, pre_delete
from django.dispatch import receiver
from django.conf import settings
from django.utils import timezone
from .models import Club, Tournament, FailedApiCall, CustomUser, Log, TeamRegistration, PlayerRegistration, UnregisteredTeam, UnregisteredPlayer, TeamSortingType, PlayerDataSource
from .models import Club, Tournament, FailedApiCall, Log, TeamRegistration
from tournaments.services.email_service import TournamentEmailService
from tournaments.services.email_service import TournamentEmailService, TeamEmailType
from tournaments.models import PlayerDataSource
from tournaments.services.email_service import TeamEmailType
from shared.discord import send_discord_log_message, send_discord_failed_calls_message
from datetime import datetime
from .utils.extensions import is_not_sqlite_backend
def generate_unique_code():

File diff suppressed because it is too large Load Diff

@ -6,6 +6,10 @@
/* PADDING */
.padding10 {
padding: 10px;
}
.padding15 {
padding: 15px;
}
@ -14,6 +18,10 @@
padding: 20px;
}
.hpadding10 {
padding: 0px 10px;
}
/* MARGIN */
.margin10 {

@ -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;
}
@ -394,10 +399,6 @@ tr {
margin: 0 auto;
}
.my-block {
padding: 10px 10px;
}
.red {
background-color: #e84038;
}
@ -571,7 +572,7 @@ h-margin {
.table-row-1-colum {
display: grid;
grid-template-columns: 1px auto;
grid-template-columns: 1fr;
/* Vertically center the content within each column */
padding: 5px 0px;
}
@ -608,12 +609,177 @@ 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 95px 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;
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: 64em) {
/* Adjust breakpoint as needed */
.table-row-5-colums-tournament {
grid-template-columns: 80px 100px 1fr 120px;
gap: 4px;
}
.small {
font-size: 1em;
}
.very-large {
font-size: 1.4em;
}
.very-large.club-name {
font-size: 1.2em;
}
}
@media screen and (max-width: 40em) {
/* 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;
}
.light-gold {
background-color: gold !important;
}
.light-silver {
background-color: silver !important;
}
.light-bronze {
background-color: #cd7f32 !important;
color: white !important;
}
.light-beige {
background-color: #fae7ce !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 */
}
.small.event-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.table-row-element.tournament-status {
grid-column: 4;
}
.box {
color: #505050;
border-radius: 12px;
padding: 4px;
}
.table-row-6-colums-club-tournament {
@ -867,7 +1033,7 @@ h-margin {
.match-result a:hover {
background-color: #fae7ce;
color: #707070;
color: #505050;
}
.group-stage-link {

@ -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 */

@ -21,7 +21,7 @@
{% load tz %}
{% if form.errors or password_change_form.errors %}
<div class="cell medium-6 large-6 topblock my-block">
<div class="cell medium-6 large-6 topblock padding10">
<div class="bubble">
<div>
{% for field in form %}
@ -45,7 +45,7 @@
</div>
{% endif %}
<div class="cell medium-6 large-6 topblock my-block">
<div class="cell medium-6 large-6 topblock padding10">
<div class="bubble">
<label class="title">Mes informations</label>
<form method="post">
@ -55,7 +55,7 @@
</form>
</div>
</div>
<div class="cell medium-6 large-6 topblock my-block">
<div class="cell medium-6 large-6 topblock padding10">
<div class="bubble">
<label class="title">Mot de passe</label>
<form method="post" action="{% url 'custom_password_change' %}">

@ -14,8 +14,8 @@
<div class="grid-x">
<div class="cell medium-6 large-6 my-block">
<h1 class="club my-block topmargin20">Inscription : {{ tournament.display_name }} {{ tournament.get_federal_age_category_display}}</h1 >
<div class="cell medium-6 large-6 padding10">
<h1 class="club padding10 topmargin20">Inscription : {{ tournament.display_name }} {{ tournament.get_federal_age_category_display}}</h1 >
<div class="bubble">

@ -9,7 +9,7 @@
<div class="grid-x">
<div class="bubble">
<div class="cell medium-6 large-6 my-block">
<div class="cell medium-6 large-6 padding10">
<label class="title">Lien d'Activation Invalide</label>
<p>Le lien d'activation est invalide ou a expiré.</p>
<div>

@ -9,7 +9,7 @@
<div class="grid-x">
<div class="bubble">
<div class="cell medium-6 large-6 my-block">
<div class="cell medium-6 large-6 padding10">
<label class="title">Compte Activé avec Succès !</label>
<p>Votre compte a été activé et vous êtes maintenant connecté.</p>
<a href="{% url 'index' %}" class="btn styled-link">Aller à la page d'accueil</a>

@ -10,7 +10,7 @@
<div class="grid-x">
<div class="bubble">
<div class="cell medium-6 large-6 my-block">
<div class="cell medium-6 large-6 padding10">
{% if form.non_field_errors %}
<div class="alert alert-error">
{% if form.non_field_errors %}

@ -11,40 +11,64 @@
{% load tz %}
<div class="grid-x">
<div class="cell medium-6 large-6 topblock my-block">
<div class="bubble">
<label class="title">Vos tournois à venir</label>
{% if upcoming_tournaments %}
{% for tournament in upcoming_tournaments %}
{% include 'tournaments/tournament_row.html' %}
{% endfor %}
<div class="cell medium-12 large-6 topblock padding10">
<div>
<div class="table-row-5-colums-tournament header">
<label class="title">Vos tournois en cours</label>
</div>
{% if running_tournaments %}
{% for tournament in running_tournaments %}
{% include 'tournaments/tournament_row.html' %}
{% endfor %}
{% else %}
Aucun tournoi à venir
<div>
Aucun tournoi en cours
</div>
{% endif %}
</div>
</div>
<div class="cell medium-6 large-6 topblock my-block">
<div class="bubble">
<label class="title">Vos tournois en cours</label>
{% if running_tournaments %}
{% for tournament in running_tournaments %}
{% include 'tournaments/tournament_row.html' %}
{% endfor %}
<div>
<div class="table-row-5-colums-tournament header">
<label class="title">Vos tournois à venir</label>
</div>
{% if upcoming_tournaments %}
{% for tournament in upcoming_tournaments %}
{% include 'tournaments/tournament_row.html' %}
{% endfor %}
{% else %}
Aucun tournoi en cours
<div>
Aucun tournoi à venir
</div>
{% endif %}
</div>
</div>
<div class="cell medium-6 large-6 topblock my-block">
<div class="bubble">
<label class="title">Vos tournois terminés</label>
<div class="cell medium-12 large-6 topblock padding10">
<div>
<div class="table-row-5-colums-tournament header">
<label class="title">Vos tournois terminés</label>
</div>
{% if ended_tournaments %}
{% for tournament in ended_tournaments %}
{% include 'tournaments/tournament_row.html' %}
{% endfor %}
{% for tournament in ended_tournaments %}
{% include 'tournaments/tournament_row.html' %}
{% endfor %}
<div class="table-row-5-colums-tournament footer">
{% if ended_tournaments|length >= 12 %}
<div class="small">
<a href="{% url 'all-my-ended-tournaments' %}">voir tous vos tournois terminés</a>
</div>
{% endif %}
</div>
{% else %}
Aucun tournoi terminé
<div>
Aucun tournoi terminé
</div>
{% endif %}
</div>
</div>

@ -6,7 +6,7 @@
{% block content %}
<div class="grid-x">
<div class="bubble">
<div class="cell medium-6 large-6 my-block">
<div class="cell medium-6 large-6 padding10">
<p>
Votre mot de passe a été réinitialisé avec succès. Vous pouvez maintenant vous connecter avec votre nouveau mot de passe.
</p>

@ -6,7 +6,7 @@
{% block content %}
<div class="grid-x">
<div class="bubble">
<div class="cell medium-6 large-6 my-block">
<div class="cell medium-6 large-6 padding10">
{% if form.non_field_errors %}
<div class="alert">
{% for error in form.non_field_errors %}

@ -6,7 +6,7 @@
{% block content %}
<div class="grid-x">
<div class="bubble">
<div class="cell medium-6 large-6 my-block">
<div class="cell medium-6 large-6 padding10">
<p>
Un e-mail contenant un lien pour réinitialiser votre mot de passe a été envoyé à votre adresse.
Veuillez vérifier votre boîte de réception.

@ -6,7 +6,7 @@
{% block content %}
<div class="grid-x">
<div class="bubble">
<div class="cell medium-6 large-6 my-block">
<div class="cell medium-6 large-6 padding10">
<!-- Add non-field errors (if any) -->
{% if form.non_field_errors %}
<div class="alert">

@ -10,7 +10,7 @@
<div class="grid">
{% if form.errors %}
<div class="cell medium-6 large-6 topblock my-block">
<div class="cell medium-6 large-6 topblock padding10">
<div class="bubble">
<div>
{% for field in form %}
@ -25,7 +25,7 @@
</div>
{% endif %}
<div class="cell medium-6 large-6 my-block">
<div class="cell medium-6 large-6 padding10">
<div class="bubble">
{% if form.non_field_errors %}
<div class="alert">

@ -11,7 +11,7 @@
<div class="grid-x">
<div class="bubble">
<label class="title">Bienvenue ! Votre compte a été créé avec succès</label>
<div class="cell medium-6 large-6 my-block">
<div class="cell medium-6 large-6 padding10">
<p>Un e-mail de confirmation a été envoyé à :<br>
<strong>{{ user_email }}</strong></p>

@ -11,7 +11,7 @@
<div class="grid-x">
<div class="cell medium-6 large-6 my-block">
<div class="cell medium-6 large-6 padding10">
<div class="bubble">
<form method="post">

@ -47,7 +47,7 @@
<body class="wrapper">
<header>
<div class="grid-x">
<div class="medium-6 large-9 cell topblock my-block ">
<div class="medium-6 large-9 cell topblock padding10 ">
<a href="{% url 'index' %}">
<img
src="{% static 'tournaments/images/PadelClub_logo_512.png' %}"
@ -73,7 +73,7 @@
{% endblock %}
</main>
<footer/>
<footer></footer>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save