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. 191
      tournaments/models/tournament.py
  77. 2
      tournaments/models/unregistered_team.py
  78. 2
      tournaments/services/email_service.py
  79. 42
      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. 54
      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 %} {% block content %}
<div class="container padding-bottom"> <div class="container padding-bottom">
<div class="grid-x 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> <h1 class="title">Add New Prospect</h1>
<form method="post"> <form method="post">

@ -44,7 +44,7 @@
<body class="wrapper"> <body class="wrapper">
<header> <header>
<div class="grid-x"> <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' %}"> <a href="{% url 'index' %}">
<img <img
src="{% static 'tournaments/images/PadelClub_logo_512.png' %}" src="{% static 'tournaments/images/PadelClub_logo_512.png' %}"

@ -1,7 +1,7 @@
{% extends "crm/base.html" %} {% block content %} {% extends "crm/base.html" %} {% block content %}
<div class="container"> <div class="container">
<div class="grid-x 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"> <h1 class="title">
{% if form.instance.pk %}Edit{% else %}Add{% endif %} Event {% if form.instance.pk %}Edit{% else %}Add{% endif %} Event
</h1> </h1>

@ -23,7 +23,7 @@
</div> </div>
<div class="container grid-x padding-bottom"> <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"> <div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="title">Completed Events</h1> <h1 class="title">Completed Events</h1>
@ -39,7 +39,7 @@
</div> </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"> <div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="title">Planned Events</h1> <h1 class="title">Planned Events</h1>

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

@ -95,7 +95,7 @@
.add-to-cart-button, .add-to-cart-button,
.checkout-button { .checkout-button {
background-color: #90ee90; background-color: #90ee90;
color: #707070; color: #505050;
border: none; border: none;
border-radius: 12px; border-radius: 12px;
font-size: 12px; font-size: 12px;
@ -120,7 +120,7 @@
} }
.coupon-section { .coupon-section {
color: #707070; color: #505050;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
text-decoration: none; text-decoration: none;
@ -129,7 +129,7 @@
.confirm-nav-button { .confirm-nav-button {
background-color: #90ee90; background-color: #90ee90;
color: #707070; color: #505050;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
text-decoration: none; text-decoration: none;
@ -541,3 +541,8 @@ v .cart-table {
.next:hover { .next:hover {
opacity: 1; 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 %} {% endif %}
<div class="grid-x"> <div class="grid-x">
<div class="small-12 medium-9 large-6 my-block"> <div class="small-12 medium-9 large-6 padding10">
<h1 class="club my-block topmargin20">Votre panier</h1> <h1 class="club padding10 topmargin20">Votre panier</h1>
<div class="bubble"> <div class="bubble">
{% if display_data.items %} {% 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;"> <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> <a href="{% url 'login' %}">Se connecter</a>
{% endif %} {% endif %}
</nav> </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="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"> <div class="bubble checkout-container">
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<div class="checkout-section"> <div class="checkout-section">

@ -25,8 +25,8 @@
</nav> </nav>
<div class="grid-x"> <div class="grid-x">
<div class="cell medium-6 large-6 my-block"> <div class="cell medium-6 large-6 padding10">
<h1 class="club my-block topmargin20">Résumé de votre commande</h1> <h1 class="club padding10 topmargin20">Résumé de votre commande</h1>
<div class="bubble"> <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 %} {% 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> </nav>
<div class="grid-x"> <div class="grid-x">
<div class="cell medium-6 large-6 my-block"> <div class="cell medium-6 large-6 padding10">
<h1 class="club my-block topmargin20">Paiement</h1> <h1 class="club padding10 topmargin20">Paiement</h1>
<div class="bubble"> <div class="bubble">
<h2>Le paiement a été annulé</h2> <h2>Le paiement a été annulé</h2>
<p>Votre commande n'a pas été finalisée car le paiement a été annulé.</p> <p>Votre commande n'a pas été finalisée car le paiement a été annulé.</p>

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

@ -1,5 +1,5 @@
{% load shop_extras %} {% 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"> <div class="bubble">
{% if product.image %} {% if product.image %}
<div class="slider-container" id="slider-{{ product.id }}"> <div class="slider-container" id="slider-{{ product.id }}">
@ -137,7 +137,9 @@ function selectColor(productId, colorId, colorName, element) {
// Remove selected class from all colors // Remove selected class from all colors
const colorSamples = element.parentElement.querySelectorAll('.color-sample'); 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 // Add selected class to clicked color
element.classList.add('selected'); element.classList.add('selected');
@ -185,7 +187,7 @@ function addToCartAjax(productId) {
notification.style.right = '20px'; notification.style.right = '20px';
notification.style.padding = '20px'; notification.style.padding = '20px';
notification.style.backgroundColor = '#90ee90'; notification.style.backgroundColor = '#90ee90';
notification.style.color = '#707070'; notification.style.color = '#505050';
notification.style.borderRadius = '12px'; notification.style.borderRadius = '12px';
notification.style.zIndex = '9999'; notification.style.zIndex = '9999';
notification.style.opacity = '0'; notification.style.opacity = '0';

@ -18,6 +18,12 @@
{% endif %} {% endif %}
</nav> </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"> <nav class="margin10">
<a class="confirm-nav-button" href="{% url 'shop:view_cart' %}">Voir mon panier ({{ total }} €)</a> <a class="confirm-nav-button" href="{% url 'shop:view_cart' %}">Voir mon panier ({{ total }} €)</a>
{% if cart_items %} {% if cart_items %}

@ -36,9 +36,11 @@ def color_images_url(default_image, color_name, sku):
if files: if files:
# Sort files by specific prefix rules # Sort files by specific prefix rules
files.sort(key=lambda x: ( files.sort(key=lambda x: (
1 if '-B_' in x else 2 if '-B_' in x else
2 if '-S_' in x else 3 if '-S_' in x else
0 1 if '-F_' in x else
0 if '-logo' in x else
4
)) ))
return [f'{base_path}{color_folder}/{file}' for file in files] 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)] any(f.lower().endswith(ext) for ext in supported_extensions)]
if files: if files:
files.sort(key=lambda x: ( files.sort(key=lambda x: (
1 if '-B_' in x else 2 if '-B_' in x else
2 if '-S_' in x else 3 if '-S_' in x else
0 1 if '-F_' in x else
0 if '-logo' in x else
4
)) ))
return [f'{base_path}{file}' for file in files] 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): def get_data(model_name, model_id):
model = sync_registry.get_model(model_name) 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) # model = apps.get_model(app_label=app_label, model_name=model_name)
return model.objects.get(id=model_id) return model.objects.get(id=model_id)

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

@ -13,7 +13,9 @@ class CustomUserAdmin(UserAdmin):
form = CustomUserChangeForm form = CustomUserChangeForm
add_form = CustomUserCreationForm add_form = CustomUserCreationForm
model = CustomUser 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'] list_filter = ['is_active', 'origin']
ordering = ['-date_joined'] ordering = ['-date_joined']
fieldsets = [ fieldsets = [

@ -1,10 +1,9 @@
from django.contrib import admin 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.db.models import Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils import timezone from django.utils import timezone
from datetime import timedelta from datetime import timedelta
import uuid
from enum import Enum from enum import Enum
class SimpleTournamentListFilter(admin.SimpleListFilter): class SimpleTournamentListFilter(admin.SimpleListFilter):

@ -5,19 +5,44 @@ import re # Import the re module for regular expressions
from .utils.licence_validator import LicenseValidator from .utils.licence_validator import LicenseValidator
from django.core.mail import send_mail from django.core.mail import send_mail
from django.template.loader import render_to_string 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.auth.tokens import default_token_generator
from django.contrib.sites.shortcuts import get_current_site from django.contrib.sites.shortcuts import get_current_site
from django.utils.encoding import force_bytes 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): class CustomUserCreationForm(UserCreationForm):
usable_password = None usable_password = None
def clean_licence_id(self): def clean_licence_id(self):
licence_id = self.cleaned_data.get('licence_id') licence_id = self.cleaned_data.get('licence_id')
if licence_id: 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 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: class Meta:
model = CustomUser model = CustomUser
error_messages = { error_messages = {
@ -41,7 +66,12 @@ class SimpleCustomUserCreationForm(UserCreationForm):
def clean_licence_id(self): def clean_licence_id(self):
licence_id = self.cleaned_data.get('licence_id') licence_id = self.cleaned_data.get('licence_id')
if licence_id: 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 return licence_id
def clean_phone(self): def clean_phone(self):
@ -81,16 +111,42 @@ class SimpleCustomUserCreationForm(UserCreationForm):
'password2': 'Confirmer le mot de passe', '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): def clean_username(self):
username = self.cleaned_data.get('username') username = self.cleaned_data.get('username')
if username: if username:
username = username.lower() 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 :)") raise forms.ValidationError("Cet identifiant est déjà utilisé. Veuillez en choisir un autre :)")
return username return username
class CustomUserChangeForm(UserChangeForm): 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: class Meta:
model = CustomUser model = CustomUser
@ -206,10 +262,31 @@ class ProfileUpdateForm(forms.ModelForm):
# Remove autofocus from the 'username' field # Remove autofocus from the 'username' field
self.fields['username'].widget.attrs.pop("autofocus", None) self.fields['username'].widget.attrs.pop("autofocus", None)
def clean_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): def clean_licence_id(self):
licence_id = self.cleaned_data.get('licence_id') licence_id = self.cleaned_data.get('licence_id')
if 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 return licence_id
def clean_phone(self): def clean_phone(self):
@ -245,8 +322,6 @@ class ProfileUpdateForm(forms.ModelForm):
}, },
} }
from django.contrib.auth.forms import PasswordChangeForm
class CustomPasswordChangeForm(PasswordChangeForm): class CustomPasswordChangeForm(PasswordChangeForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -254,11 +329,6 @@ class CustomPasswordChangeForm(PasswordChangeForm):
for field in self.fields.values(): for field in self.fields.values():
field.widget.attrs.pop("autofocus", None) 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__) logger = logging.getLogger(__name__)
class EmailOrUsernameAuthenticationForm(AuthenticationForm): 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 from django.db import models
import uuid
class TournamentPayment(models.IntegerChoices): class TournamentPayment(models.IntegerChoices):
FREE = 0, 'Gratuit' FREE = 0, 'Gratuit'
@ -48,6 +47,7 @@ class FederalLevelCategory(models.IntegerChoices):
P1000 = 1000, 'P1000' P1000 = 1000, 'P1000'
P1500 = 1500, 'P1500' P1500 = 1500, 'P1500'
P2000 = 2000, 'P2000' P2000 = 2000, 'P2000'
CHPT = 1, 'Championnat'
@staticmethod @staticmethod
def min_player_rank(level=None, category=None, age_category=None) -> int: 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' WAITING_LIST_FULL = 6, 'Waiting List Full'
IN_PROGRESS = 7, 'In Progress' IN_PROGRESS = 7, 'In Progress'
ENDED_WITH_RESULTS = 8, 'Ended with Results' ENDED_WITH_RESULTS = 8, 'Ended with Results'
CANCELED = 9, 'Canceled'
def 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: def status_localized(self) -> str:
status_map = { status_map = {
@ -156,10 +192,58 @@ class OnlineRegistrationStatus(models.IntegerChoices):
OnlineRegistrationStatus.WAITING_LIST_POSSIBLE: "Liste d'attente ouverte", OnlineRegistrationStatus.WAITING_LIST_POSSIBLE: "Liste d'attente ouverte",
OnlineRegistrationStatus.WAITING_LIST_FULL: "Liste d'attente complète", OnlineRegistrationStatus.WAITING_LIST_FULL: "Liste d'attente complète",
OnlineRegistrationStatus.IN_PROGRESS: "Tournoi en cours", OnlineRegistrationStatus.IN_PROGRESS: "Tournoi en cours",
OnlineRegistrationStatus.ENDED_WITH_RESULTS: "Tournoi terminé" OnlineRegistrationStatus.ENDED_WITH_RESULTS: "Tournoi terminé",
OnlineRegistrationStatus.CANCELED: "Tournoi annulé"
} }
return status_map.get(self, "") return status_map.get(self, "")
def short_label(self) -> str:
"""Returns a short, concise label for the status box"""
label_map = {
OnlineRegistrationStatus.OPEN: "ouvert",
OnlineRegistrationStatus.NOT_ENABLED: "désactivé",
OnlineRegistrationStatus.NOT_STARTED: "à venir",
OnlineRegistrationStatus.ENDED: "clôturé",
OnlineRegistrationStatus.WAITING_LIST_POSSIBLE: "ouvert",
OnlineRegistrationStatus.WAITING_LIST_FULL: "complet",
OnlineRegistrationStatus.IN_PROGRESS: "en cours",
OnlineRegistrationStatus.ENDED_WITH_RESULTS: "résultats",
OnlineRegistrationStatus.CANCELED: "annulé"
}
return label_map.get(self, "")
def box_class(self) -> str:
"""Returns the CSS class for the status box"""
class_map = {
OnlineRegistrationStatus.OPEN: "light-green",
OnlineRegistrationStatus.NOT_ENABLED: "gray",
OnlineRegistrationStatus.NOT_STARTED: "light-green",
OnlineRegistrationStatus.ENDED: "gray",
OnlineRegistrationStatus.WAITING_LIST_POSSIBLE: "light-orange",
OnlineRegistrationStatus.WAITING_LIST_FULL: "light-red",
OnlineRegistrationStatus.IN_PROGRESS: "blue",
OnlineRegistrationStatus.ENDED_WITH_RESULTS: "dark-gray",
OnlineRegistrationStatus.CANCELED: "light-red",
}
return class_map.get(self, "gray")
def display_box(self) -> bool:
"""
Determines whether this status should display a status box
Returns True if the status should be displayed, False otherwise
"""
# List the statuses that should display a box
display_statuses = [
OnlineRegistrationStatus.OPEN,
OnlineRegistrationStatus.NOT_STARTED,
OnlineRegistrationStatus.WAITING_LIST_POSSIBLE,
OnlineRegistrationStatus.WAITING_LIST_FULL,
OnlineRegistrationStatus.CANCELED,
# You can add or remove statuses as needed
]
return self in display_statuses
class UserOrigin(models.IntegerChoices): class UserOrigin(models.IntegerChoices):
ADMIN = 0, 'Admin' ADMIN = 0, 'Admin'
SITE = 1, 'Site' SITE = 1, 'Site'

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

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

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

@ -1,5 +1,5 @@
from django.db import models from django.db import models
from . import SideStoreModel, TeamRegistration, PlayerSexType, PlayerDataSource, PlayerPaymentType from . import SideStoreModel, TeamRegistration, PlayerSexType, PlayerDataSource, PlayerPaymentType, OnlineRegistrationStatus
import uuid import uuid
from django.utils import timezone from django.utils import timezone
@ -107,3 +107,68 @@ class PlayerRegistration(SideStoreModel):
return "1ère" return "1ère"
return "1er" return "1er"
return f"{self.rank}ème" return f"{self.rank}ème"
def get_registration_status(self):
"""
Returns a status object with information about the player's registration status.
This object contains display_box, box_class, and short_label properties
used in the tournament row template.
Returns None if no relevant status can be determined.
"""
# If no team registration exists, return None
if not self.team_registration:
return None
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 from django.db import models
import uuid
from . import BaseModel, CustomUser from . import BaseModel, CustomUser
class Purchase(models.Model): class Purchase(BaseModel):
id = models.BigIntegerField(primary_key=True, unique=True, editable=True) id = models.BigIntegerField(primary_key=True, unique=True, editable=True)
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE) user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
purchase_date = models.DateTimeField() purchase_date = models.DateTimeField()

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

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

@ -1,19 +1,15 @@
from time import daylight
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from django.db import models from django.db import models
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from tournaments.models import group_stage
from . import BaseModel, Event, TournamentPayment, FederalMatchCategory, FederalCategory, FederalLevelCategory, FederalAgeCategory, OnlineRegistrationStatus from . import BaseModel, Event, TournamentPayment, FederalMatchCategory, FederalCategory, FederalLevelCategory, FederalAgeCategory, OnlineRegistrationStatus
import uuid import uuid
from django.utils import timezone, formats from django.utils import timezone, formats
from datetime import datetime, timedelta from datetime import datetime, timedelta, time
from zoneinfo import ZoneInfo
from tournaments.utils.player_search import get_player_name_from_csv from tournaments.utils.player_search import get_player_name_from_csv
from shared.cryptography import encryption_util from shared.cryptography import encryption_util
from ..utils.extensions import plural_format from ..utils.extensions import plural_format
from django.utils.formats import date_format
from ..utils.licence_validator import LicenseValidator
from django.apps import apps
class TeamSortingType(models.IntegerChoices): class TeamSortingType(models.IntegerChoices):
RANK = 1, 'Rank' RANK = 1, 'Rank'
@ -180,6 +176,8 @@ class Tournament(BaseModel):
def level(self): def level(self):
if self.federal_level_category == 0: if self.federal_level_category == 0:
return "Anim." return "Anim."
if self.federal_level_category == 1:
return "CHPT"
return self.get_federal_level_category_display() return self.get_federal_level_category_display()
def category(self): def category(self):
@ -231,36 +229,12 @@ class Tournament(BaseModel):
else: else:
return None return None
def tournament_status_display(self): def get_tournament_status(self):
if self.is_canceled() is True: return self.get_online_registration_status().status_localized()
return "Annulé"
teams = self.teams(True) def get_tournament_status_team_count(self):
if self.supposedly_in_progress() or self.end_date is not None or self.should_be_over(): active_teams_count = self.team_registrations.filter(walk_out=False).count()
teams = [t for t in teams if t.stage != "Attente"] return min(active_teams_count, self.team_count)
if teams is not None and len(teams) > 0:
word = "équipe"
if len(teams) > 1:
word = word + "s"
return f"{len(teams)} {word}"
else:
return None
registration_status = None
if self.enable_online_registration == True:
registration_status = self.get_online_registration_status().status_localized()
if teams is not None and len(teams) > 0:
word = "inscription"
if len(teams) > 1:
word = word + "s"
if registration_status is not None:
return f"{registration_status}\n{len(teams)} {word}"
else:
return f"{len(teams)} {word}"
else:
if registration_status is not None:
return f"{registration_status}"
return None
def name_and_event(self): def name_and_event(self):
event_name = None event_name = None
@ -333,9 +307,9 @@ class Tournament(BaseModel):
index = i index = i
# Check if team_count exists # Check if team_count exists
if self.team_count: if self.team_count_limit == True:
# Team is not in list # Team is not in list
if index < self.team_count: if index < 0:
print("Team is not in list", index, self.team_count) print("Team is not in list", index, self.team_count)
return -1 return -1
# Return position in waiting list relative to target count # Return position in waiting list relative to target count
@ -1091,42 +1065,6 @@ class Tournament(BaseModel):
return options return options
def online_register_is_enabled(self):
if self.supposedly_in_progress():
return False
if self.closed_registration_date is not None:
return False
if self.end_date is not None:
return False
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
# 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
# Check target team count and waiting list limit
if self.team_count is not None:
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): def get_selection_status_localized(self):
if self.team_sorting == TeamSortingType.RANK: if self.team_sorting == TeamSortingType.RANK:
return "La sélection se fait par le poids de l'équipe" return "La sélection se fait par le poids de l'équipe"
@ -1134,12 +1072,16 @@ class Tournament(BaseModel):
return "La sélection se fait par date d'inscription" return "La sélection se fait par date d'inscription"
def get_online_registration_status(self): def get_online_registration_status(self):
if self.is_canceled():
return OnlineRegistrationStatus.CANCELED
if self.end_date is not None:
return OnlineRegistrationStatus.ENDED_WITH_RESULTS
if self.enable_online_registration is False:
return OnlineRegistrationStatus.NOT_ENABLED
if self.supposedly_in_progress(): if self.supposedly_in_progress():
return OnlineRegistrationStatus.ENDED return OnlineRegistrationStatus.ENDED
if self.closed_registration_date is not None: if self.closed_registration_date is not None:
return OnlineRegistrationStatus.WAITING_LIST_POSSIBLE return OnlineRegistrationStatus.WAITING_LIST_POSSIBLE
if self.end_date is not None:
return OnlineRegistrationStatus.ENDED_WITH_RESULTS
now = timezone.now() now = timezone.now()
@ -1167,6 +1109,21 @@ class Tournament(BaseModel):
return OnlineRegistrationStatus.WAITING_LIST_POSSIBLE return OnlineRegistrationStatus.WAITING_LIST_POSSIBLE
return OnlineRegistrationStatus.OPEN return OnlineRegistrationStatus.OPEN
def get_registration_status_short_label(self):
"""Returns a short label for the registration status"""
status = self.get_online_registration_status()
return status.short_label()
def get_registration_status_class(self):
"""Returns the CSS class for the registration status box"""
status = self.get_online_registration_status()
return status.box_class()
def should_display_status_box(self):
"""Returns whether the registration status box should be displayed"""
status = self.get_online_registration_status()
return status.display_box()
def is_unregistration_possible(self): def is_unregistration_possible(self):
# Check if tournament has started # Check if tournament has started
if self.supposedly_in_progress(): if self.supposedly_in_progress():
@ -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: class MatchGroup:
def __init__(self, name, matches, formatted_schedule, round_id=None): def __init__(self, name, matches, formatted_schedule, round_id=None):
self.name = name self.name = name

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

@ -1,6 +1,4 @@
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from django.utils import timezone
from django.urls import reverse
from enum import Enum from enum import Enum
from ..models.player_registration import RegistrationStatus from ..models.player_registration import RegistrationStatus
from ..models.tournament import TeamSortingType 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 ..utils.player_search import get_player_name_from_csv
from tournaments.models import PlayerRegistration from tournaments.models import PlayerRegistration
from ..utils.extensions import is_not_sqlite_backend 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: class TournamentRegistrationService:
def __init__(self, request, tournament): def __init__(self, request, tournament):
@ -49,7 +52,6 @@ class TournamentRegistrationService:
return return
# Clear existing messages if the form is valid # Clear existing messages if the form is valid
from django.contrib.messages import get_messages
storage = get_messages(self.request) storage = get_messages(self.request)
# Iterate through the storage to clear it # Iterate through the storage to clear it
for _ in storage: for _ in storage:
@ -70,6 +72,10 @@ class TournamentRegistrationService:
if self._is_already_registered(licence_id): if self._is_already_registered(licence_id):
return 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 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 # 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) self._handle_invalid_names(licence_id, player_data)
@ -80,9 +86,6 @@ class TournamentRegistrationService:
else: else:
self._handle_invalid_names(licence_id, player_data) 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): def handle_team_registration(self):
if not self.context['team_form'].is_valid(): if not self.context['team_form'].is_valid():
return return
@ -119,7 +122,7 @@ class TournamentRegistrationService:
self.context['registration_successful'] = True self.context['registration_successful'] = True
def handle_get_request(self): def handle_get_request(self):
from django.contrib.messages import get_messages print("handle_get_request")
storage = get_messages(self.request) storage = get_messages(self.request)
# Iterate through the storage to clear it # Iterate through the storage to clear it
for _ in storage: for _ in storage:
@ -180,8 +183,6 @@ class TournamentRegistrationService:
self.request.session.modified = True self.request.session.modified = True
def _get_authenticated_user_data(self): 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 user = self.request.user
validator = LicenseValidator(user.licence_id) validator = LicenseValidator(user.licence_id)
@ -209,6 +210,7 @@ class TournamentRegistrationService:
return player_data return player_data
def _validate_license(self, licence_id): def _validate_license(self, licence_id):
print("Validating license...")
validator = LicenseValidator(licence_id) validator = LicenseValidator(licence_id)
if validator.validate_license() is False and self.tournament.license_is_required: 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 # 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, 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.") messages.error(self.request, "Le numéro de licence est invalide, la lettre ne correspond pas.")
print("License validation failed")
return False return False
return True return True
@ -276,16 +279,26 @@ class TournamentRegistrationService:
player_data['is_woman'] = self.request.session.get('is_woman', False) player_data['is_woman'] = self.request.session.get('is_woman', False)
def _update_user_license(self, licence_id): def _update_user_license(self, licence_id):
if self.request.user.is_authenticated and licence_id: if not self.request.user.is_authenticated or not licence_id:
return False
self.context['add_player_form'].user_without_licence = False self.context['add_player_form'].user_without_licence = False
validator = LicenseValidator(licence_id) validator = LicenseValidator(licence_id)
self.request.user.licence_id = validator.computed_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.save()
self.request.user.refresh_from_db() self.request.user.refresh_from_db()
self.request.session.modified = True self.request.session.modified = True
# Reset the form state return True
self.context['add_player_form'] = AddPlayerForm()
self.context['add_player_form'].first_tournament = False 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): def _update_player_data_from_csv(self, player_data, csv_data):
print("_update_player_data_from_csv", player_data, csv_data) print("_update_player_data_from_csv", player_data, csv_data)
@ -305,16 +318,15 @@ class TournamentRegistrationService:
'phone': None, 'phone': None,
}) })
from django.contrib.auth import get_user_model
User = get_user_model() User = get_user_model()
# Get the license ID from player_data # Get the license ID from player_data
licence_id = player_data.get('licence_id') licence_id = player_data.get('licence_id')
validator = LicenseValidator(licence_id) validator = LicenseValidator(licence_id)
if validator and validator.stripped_license: if validator.validate_license():
try: try:
# Try to find a user with matching license # 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 found, update the email and phone
if user_with_same_license: if user_with_same_license:

@ -1,8 +1,7 @@
from django.contrib import messages from django.contrib import messages
from django.utils import timezone from django.utils import timezone
from ..models import PlayerRegistration, UnregisteredTeam, UnregisteredPlayer from ..models import PlayerRegistration, UnregisteredTeam, UnregisteredPlayer
from ..models.player_enums import PlayerDataSource from ..utils.licence_validator import LicenseValidator
from ..services.email_service import TournamentEmailService
class TournamentUnregistrationService: class TournamentUnregistrationService:
def __init__(self, request, tournament): def __init__(self, request, tournament):
@ -52,8 +51,16 @@ class TournamentUnregistrationService:
) )
def _find_player_registration(self): 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( 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, team_registration__tournament_id=self.tournament.id,
).first() ).first()

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

File diff suppressed because it is too large Load Diff

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

@ -35,7 +35,7 @@ body {
} }
label { label {
color: #707070; color: #505050;
font-size: 1.1em; font-size: 1.1em;
} }
@ -55,7 +55,7 @@ footer {
} }
a { a {
color: #707070; color: #505050;
} }
a:hover { a:hover {
@ -73,7 +73,7 @@ nav {
} }
nav a { nav a {
color: #707070; color: #505050;
padding: 8px 12px; padding: 8px 12px;
background-color: #fae7ce; background-color: #fae7ce;
border-radius: 12px; border-radius: 12px;
@ -161,7 +161,7 @@ tr {
.rounded-button { .rounded-button {
background-color: #fae7ce; /* Green background */ background-color: #fae7ce; /* Green background */
color: #707070; /* White text */ color: #505050; /* White text */
padding: 15px 32px; /* Some padding */ padding: 15px 32px; /* Some padding */
font-size: 1em; font-size: 1em;
font-weight: 800; font-weight: 800;
@ -193,7 +193,7 @@ tr {
} }
.mybox { .mybox {
color: #707070; color: #505050;
padding: 8px 12px; padding: 8px 12px;
background-color: #fae7ce; background-color: #fae7ce;
border-radius: 12px; border-radius: 12px;
@ -260,6 +260,11 @@ tr {
font-size: 1.2em; font-size: 1.2em;
} }
.very-large {
font-family: "Montserrat-SemiBold";
font-size: 1.4em;
}
@media screen and (max-width: 40em) { @media screen and (max-width: 40em) {
.large { .large {
font-size: 0.9em; font-size: 0.9em;
@ -278,7 +283,7 @@ tr {
.info { .info {
font-family: "Montserrat-SemiBold"; font-family: "Montserrat-SemiBold";
font-size: 0.9em; font-size: 0.9em;
color: #707070; color: #505050;
} }
.small { .small {
@ -286,7 +291,7 @@ tr {
} }
.minor-info { .minor-info {
color: #707070; color: #505050;
font-size: 0.85em; font-size: 0.85em;
} }
@ -362,7 +367,7 @@ tr {
.separator { .separator {
height: 1px; height: 1px;
background-color: #707070; background-color: #505050;
margin: 5px 0px; margin: 5px 0px;
} }
@ -394,10 +399,6 @@ tr {
margin: 0 auto; margin: 0 auto;
} }
.my-block {
padding: 10px 10px;
}
.red { .red {
background-color: #e84038; background-color: #e84038;
} }
@ -571,7 +572,7 @@ h-margin {
.table-row-1-colum { .table-row-1-colum {
display: grid; display: grid;
grid-template-columns: 1px auto; grid-template-columns: 1fr;
/* Vertically center the content within each column */ /* Vertically center the content within each column */
padding: 5px 0px; padding: 5px 0px;
} }
@ -608,12 +609,177 @@ h-margin {
padding: 5px 0px; padding: 5px 0px;
} }
.table-row-4-colums-tournament { .table-row-5-colums-tournament {
display: grid; display: grid;
grid-template-columns: auto 1fr auto auto; grid-template-columns: 75px 95px 1fr 120px;
align-items: center; align-items: center;
/* Vertically center the content within each column */ gap: 4px;
padding: 5px 0px; }
.very-large.club-name {
font-size: 1.2em;
}
.table-row-5-colums-tournament.header {
grid-template-columns: 1fr;
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 { .table-row-6-colums-club-tournament {
@ -867,7 +1033,7 @@ h-margin {
.match-result a:hover { .match-result a:hover {
background-color: #fae7ce; background-color: #fae7ce;
color: #707070; color: #505050;
} }
.group-stage-link { .group-stage-link {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -47,7 +47,7 @@
<body class="wrapper"> <body class="wrapper">
<header> <header>
<div class="grid-x"> <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' %}"> <a href="{% url 'index' %}">
<img <img
src="{% static 'tournaments/images/PadelClub_logo_512.png' %}" src="{% static 'tournaments/images/PadelClub_logo_512.png' %}"
@ -73,7 +73,7 @@
{% endblock %} {% endblock %}
</main> </main>
<footer/> <footer></footer>
</body> </body>
</html> </html>

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

Loading…
Cancel
Save