Laurent 9 months ago
commit b983b933e7
  1. 22
      api/serializers.py
  2. 2
      api/urls.py
  3. 170
      api/views.py
  4. 12
      padelclub_backend/settings.py
  5. 12
      padelclub_backend/settings_app.py
  6. 2
      padelclub_backend/urls.py
  7. 17
      sync/migrations/0002_alter_modellog_options.py
  8. 3
      sync/views.py
  9. 21
      tournaments/admin.py
  10. 34
      tournaments/backends.py
  11. 240
      tournaments/forms.py
  12. 18
      tournaments/migrations/0094_playerregistration_captain.py
  13. 14
      tournaments/migrations/0095_merge_20241129_1243.py
  14. 41
      tournaments/migrations/0096_drawlog_creation_date_drawlog_last_update_and_more.py
  15. 38
      tournaments/migrations/0096_tournament_account_is_required_and_more.py
  16. 23
      tournaments/migrations/0097_tournament_display_entry_fee_information_and_more.py
  17. 19
      tournaments/migrations/0098_alter_drawlog_tournament.py
  18. 38
      tournaments/migrations/0098_tournament_enable_online_registration_and_more.py
  19. 79
      tournaments/migrations/0099_alter_court_club_alter_dateinterval_event_and_more.py
  20. 17
      tournaments/migrations/0099_remove_tournament_display_entry_fee_information.py
  21. 28
      tournaments/migrations/0100_playerregistration_coach_and_more.py
  22. 38
      tournaments/migrations/0101_unregisteredteam_unregisteredplayer.py
  23. 21
      tournaments/migrations/0102_remove_teamregistration_unregistered_and_more.py
  24. 33
      tournaments/migrations/0103_remove_unregisteredplayer_reason_and_more.py
  25. 17
      tournaments/migrations/0104_remove_tournament_target_team_count.py
  26. 23
      tournaments/migrations/0105_playerregistration_registered_online_and_more.py
  27. 18
      tournaments/migrations/0106_alter_customuser_licence_id.py
  28. 18
      tournaments/migrations/0107_customuser_origin.py
  29. 64
      tournaments/migrations/0108_club_creation_date_club_last_update_and_more.py
  30. 2
      tournaments/migrations/0109_special_store_id.py
  31. 4
      tournaments/models/__init__.py
  32. 13
      tournaments/models/custom_user.py
  33. 74
      tournaments/models/enums.py
  34. 2
      tournaments/models/group_stage.py
  35. 16
      tournaments/models/match.py
  36. 33
      tournaments/models/player_registration.py
  37. 129
      tournaments/models/team_registration.py
  38. 22
      tournaments/models/team_score.py
  39. 452
      tournaments/models/tournament.py
  40. 25
      tournaments/models/unregistered_player.py
  41. 39
      tournaments/models/unregistered_team.py
  42. 79
      tournaments/repositories.py
  43. 259
      tournaments/services/email_service.py
  44. 311
      tournaments/services/tournament_registration.py
  45. 72
      tournaments/services/tournament_unregistration.py
  46. 92
      tournaments/signals.py
  47. 12511
      tournaments/static/rankings/CLASSEMENT-PADEL-DAMES-01-2025.csv
  48. 12934
      tournaments/static/rankings/CLASSEMENT-PADEL-DAMES-02-2025.csv
  49. 252
      tournaments/static/rankings/CLASSEMENT-PADEL-DAMES-12-2024.csv
  50. 80000
      tournaments/static/rankings/CLASSEMENT-PADEL-MESSIEURS-01-2025.csv
  51. 80002
      tournaments/static/rankings/CLASSEMENT-PADEL-MESSIEURS-02-2025.csv
  52. 1210
      tournaments/static/rankings/CLASSEMENT-PADEL-MESSIEURS-12-2024.csv
  53. 107
      tournaments/static/tournaments/css/style.css
  54. BIN
      tournaments/static/tournaments/images/main_background@2x.jpg
  55. 79
      tournaments/templates/profile.html
  56. 140
      tournaments/templates/register_tournament.html
  57. 52
      tournaments/templates/registration/login.html
  58. 51
      tournaments/templates/registration/my_tournaments.html
  59. 19
      tournaments/templates/registration/password_reset_complete.html
  60. 47
      tournaments/templates/registration/password_reset_confirm.html
  61. 20
      tournaments/templates/registration/password_reset_done.html
  62. 11
      tournaments/templates/registration/password_reset_email.html
  63. 43
      tournaments/templates/registration/password_reset_form.html
  64. 46
      tournaments/templates/registration/signup.html
  65. 38
      tournaments/templates/registration/signup_success.html
  66. 15
      tournaments/templates/tournaments/acc_active_email.html
  67. 5
      tournaments/templates/tournaments/base.html
  68. 1
      tournaments/templates/tournaments/broadcast/broadcast.html
  69. 1
      tournaments/templates/tournaments/broadcast/broadcast_club.html
  70. 5
      tournaments/templates/tournaments/broadcast/broadcasted_group_stage.html
  71. 19
      tournaments/templates/tournaments/broadcast/broadcasted_match.html
  72. 100
      tournaments/templates/tournaments/broadcast/broadcasted_prog.html
  73. 2
      tournaments/templates/tournaments/download.html
  74. 13
      tournaments/templates/tournaments/group_stage_cell.html
  75. 14
      tournaments/templates/tournaments/match_cell.html
  76. 11
      tournaments/templates/tournaments/navigation_base.html
  77. 17
      tournaments/templates/tournaments/navigation_tournament.html
  78. 66
      tournaments/templates/tournaments/player_row.html
  79. 10
      tournaments/templates/tournaments/ranking_row.html
  80. 29
      tournaments/templates/tournaments/rankings.html
  81. 10
      tournaments/templates/tournaments/summon_row.html
  82. 54
      tournaments/templates/tournaments/team_details.html
  83. 13
      tournaments/templates/tournaments/team_row.html
  84. 84
      tournaments/templates/tournaments/team_stats.html
  85. 138
      tournaments/templates/tournaments/tournament_info.html
  86. 6
      tournaments/templates/tournaments/tournament_row.html
  87. 5
      tournaments/templates/tournaments/tournaments.html
  88. 28
      tournaments/urls.py
  89. 48
      tournaments/utils/licence_validator.py
  90. 115
      tournaments/utils/player_search.py
  91. 364
      tournaments/views.py

@ -1,6 +1,6 @@
from rest_framework import serializers
from tournaments.models.court import Court
from tournaments.models import Club, LiveMatch, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, FailedApiCall, DateInterval, Log, DeviceToken
from tournaments.models import Club, LiveMatch, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, FailedApiCall, DateInterval, Log, DeviceToken, UnregisteredTeam, UnregisteredPlayer
from django.contrib.auth import password_validation
from django.utils.translation import gettext_lazy as _
# email
@ -14,6 +14,7 @@ from api.tokens import account_activation_token
from shared.cryptography import encryption_util
from tournaments.models.draw_log import DrawLog
from tournaments.models.enums import UserOrigin
class EncryptedUserField(serializers.Field):
def to_representation(self, value):
@ -71,6 +72,7 @@ class UserSerializer(serializers.ModelSerializer):
group_stage_match_format_preference=validated_data.get('group_stage_match_format_preference'),
loser_bracket_match_format_preference=validated_data.get('loser_bracket_match_format_preference'),
loser_bracket_mode=validated_data.get('loser_bracket_mode'),
origin=UserOrigin.APP,
)
self.send_email(self.context['request'], user)
@ -89,6 +91,7 @@ class UserSerializer(serializers.ModelSerializer):
'token': account_activation_token.make_token(user),
})
email = EmailMessage(mail_subject, message, to=[user.email])
email.content_subtype = "html"
email.send()
class Meta:
@ -231,3 +234,20 @@ class DrawLogSerializer(serializers.ModelSerializer):
class Meta:
model = DrawLog
fields = '__all__'
class UnregisteredTeamSerializer(serializers.ModelSerializer):
class Meta:
# match_id = serializers.PrimaryKeyRelatedField(queryset=Match.objects.all())
# group_stage_id = serializers.PrimaryKeyRelatedField(queryset=GroupStage.objects.all())
model = UnregisteredTeam
fields = '__all__'
# ['id', 'group_stage_id', 'registration_date', 'call_date', 'bracket_position',
# 'group_stage_position', 'logo']
class UnregisteredPlayerSerializer(serializers.ModelSerializer):
class Meta:
# team_registration_id = serializers.PrimaryKeyRelatedField(queryset=TeamRegistration.objects.all())
# team_state_id = serializers.PrimaryKeyRelatedField(queryset=TeamState.objects.all())
model = UnregisteredPlayer
fields = '__all__'
# ['id', 'team_registration_id', 'first_name', 'last_name', 'licence_id', 'rank', 'has_paid']

@ -25,6 +25,8 @@ router.register(r'failed-api-calls', views.FailedApiCallViewSet)
router.register(r'logs', views.LogViewSet)
router.register(r'device-token', views.DeviceTokenViewSet)
router.register(r'data-access', DataAccessViewSet)
router.register(r'unregistered-teams', views.UnregisteredTeamViewSet)
router.register(r'unregistered-players', views.UnregisteredPlayerViewSet)
urlpatterns = [
path('', include(router.urls)),

@ -1,6 +1,6 @@
from pandas.io.feather_format import pd
from .serializers import ClubSerializer, CourtSerializer, DateIntervalSerializer, DrawLogSerializer, TournamentSerializer, UserSerializer, ChangePasswordSerializer, EventSerializer, RoundSerializer, GroupStageSerializer, MatchSerializer, TeamScoreSerializer, TeamRegistrationSerializer, PlayerRegistrationSerializer, LiveMatchSerializer, PurchaseSerializer, ShortUserSerializer, FailedApiCallSerializer, LogSerializer, DeviceTokenSerializer, CustomUserSerializer
from tournaments.models import Club, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamScore, TeamRegistration, PlayerRegistration, Court, DateInterval, Purchase, FailedApiCall, Log, DeviceToken, DrawLog
from .serializers import ClubSerializer, CourtSerializer, DateIntervalSerializer, DrawLogSerializer, TournamentSerializer, UserSerializer, ChangePasswordSerializer, EventSerializer, RoundSerializer, GroupStageSerializer, MatchSerializer, TeamScoreSerializer, TeamRegistrationSerializer, PlayerRegistrationSerializer, LiveMatchSerializer, PurchaseSerializer, ShortUserSerializer, FailedApiCallSerializer, LogSerializer, DeviceTokenSerializer, CustomUserSerializer, UnregisteredTeamSerializer, UnregisteredPlayerSerializer
from tournaments.models import Club, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamScore, TeamRegistration, PlayerRegistration, Court, DateInterval, Purchase, FailedApiCall, Log, DeviceToken, DrawLog, UnregisteredTeam, UnregisteredPlayer
from rest_framework import viewsets, permissions
from rest_framework.authtoken.models import Token
@ -12,17 +12,21 @@ from rest_framework.exceptions import MethodNotAllowed
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView
from django.http import Http404
from django.contrib.auth import authenticate
from django.db.models import Q
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
from django.apps import apps
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from collections import defaultdict
from .permissions import IsClubOwner
from .utils import is_valid_email
@method_decorator(csrf_exempt, name='dispatch')
class CustomAuthToken(APIView):
permission_classes = []
@ -38,11 +42,21 @@ class CustomAuthToken(APIView):
user = authenticate(username=true_username, password=password)
if user:
user.device_id = device_id
user.save()
# user.device_id = device_id
# user.save()
# token, created = Token.objects.get_or_create(user=user)
# return Response({'token': token.key})
if user.device_id is None or user.device_id == device_id or user.username == 'apple-test':
user.device_id = device_id
user.save()
token, created = Token.objects.get_or_create(user=user)
return Response({'token': token.key})
else:
return Response({'error': 'Vous ne pouvez pour l\'instant vous connecter sur plusieurs appareils en même temps. Veuillez vous déconnecter du précédent appareil. Autrement, veuillez contacter le support.'}, status=status.HTTP_403_FORBIDDEN)
token, created = Token.objects.get_or_create(user=user)
return Response({'token': token.key})
else:
return Response({'error': 'L\'utilisateur et le mot de passe de correspondent pas'}, status=status.HTTP_401_UNAUTHORIZED)
@ -66,26 +80,19 @@ class Logout(APIView):
return Response(status=status.HTTP_200_OK)
class ChangePasswordView(UpdateAPIView):
serializer_class = ChangePasswordSerializer
def update(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
# if using drf authtoken, create a new token
if hasattr(user, 'auth_token'):
user.auth_token.delete()
token, created = Token.objects.get_or_create(user=user)
# return new token
return Response({'token': token.key}, status=status.HTTP_200_OK)
@api_view(['GET'])
def user_by_token(request):
serializer = UserSerializer(request.user)
return Response(serializer.data, status=status.HTTP_200_OK)
class UserViewSet(viewsets.ModelViewSet):
class SoftDeleteViewSet(viewsets.ModelViewSet):
def destroy(self, request, *args, **kwargs):
try:
return super().destroy(request, *args, **kwargs)
except Http404:
return Response(status=status.HTTP_204_NO_CONTENT)
class UserViewSet(SoftDeleteViewSet):
queryset = CustomUser.objects.all()
serializer_class = CustomUserSerializer
permission_classes = [] # Users are public whereas the other requests are only for logged users
@ -96,56 +103,42 @@ class UserViewSet(viewsets.ModelViewSet):
return UserSerializer
return self.serializer_class
class ShortUserViewSet(viewsets.ModelViewSet):
queryset = CustomUser.objects.all()
serializer_class = ShortUserSerializer
permission_classes = [] # Users are public whereas the other requests are only for logged users
class ClubViewSet(viewsets.ModelViewSet):
class ClubViewSet(SoftDeleteViewSet):
queryset = Club.objects.all()
serializer_class = ClubSerializer
permission_classes = [IsClubOwner] # Clubs are public whereas the other requests are only for logged users
def perform_create(self, serializer):
# super.perform_create()
serializer.save(creator=self.request.user)
class EventViewSet(viewsets.ModelViewSet):
queryset = Event.objects.all()
serializer_class = EventSerializer
def get_queryset(self):
if self.request.user.is_anonymous:
return []
# return self.queryset.filter(creator=self.request.user)
return self.queryset.filter(
Q(creator=self.request.user)
)
class TournamentViewSet(viewsets.ModelViewSet):
class TournamentViewSet(SoftDeleteViewSet):
queryset = Tournament.objects.all()
serializer_class = TournamentSerializer
def get_queryset(self):
if self.request.user.is_anonymous:
return []
return self.queryset.filter(event__creator=self.request.user)
return self.queryset.filter(
Q(event__creator=self.request.user)
)
class PurchaseViewSet(viewsets.ModelViewSet):
class PurchaseViewSet(SoftDeleteViewSet):
queryset = Purchase.objects.all()
serializer_class = PurchaseSerializer
def get_queryset(self):
if self.request.user.is_anonymous:
return []
if self.request.user:
return self.queryset.filter(user=self.request.user)
return []
# return self.queryset.filter(user=self.request.user)
return self.queryset.filter(
Q(user=self.request.user)
)
def create(self, request, *args, **kwargs):
id = request.data.get('id')
if Purchase.objects.filter(id=id).exists():
return Response({"detail": "This transaction id is already registered."}, status=status.HTTP_208_ALREADY_REPORTED)
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def patch(self, request, pk):
raise MethodNotAllowed('PATCH')
@ -153,7 +146,33 @@ class PurchaseViewSet(viewsets.ModelViewSet):
def delete(self, request, pk):
raise MethodNotAllowed('DELETE')
class RoundViewSet(viewsets.ModelViewSet):
class ChangePasswordView(UpdateAPIView):
serializer_class = ChangePasswordSerializer
def update(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
# if using drf authtoken, create a new token
if hasattr(user, 'auth_token'):
user.auth_token.delete()
token, created = Token.objects.get_or_create(user=user)
# return new token
return Response({'token': token.key}, status=status.HTTP_200_OK)
class EventViewSet(SoftDeleteViewSet):
queryset = Event.objects.all()
serializer_class = EventSerializer
def get_queryset(self):
if self.request.user.is_anonymous:
return []
# return self.queryset.filter(creator=self.request.user)
return self.queryset.filter(
Q(creator=self.request.user)
)
class RoundViewSet(SoftDeleteViewSet):
queryset = Round.objects.all()
serializer_class = RoundSerializer
@ -165,7 +184,7 @@ class RoundViewSet(viewsets.ModelViewSet):
return self.queryset.filter(tournament__event__creator=self.request.user)
return []
class GroupStageViewSet(viewsets.ModelViewSet):
class GroupStageViewSet(SoftDeleteViewSet):
queryset = GroupStage.objects.all()
serializer_class = GroupStageSerializer
@ -177,7 +196,7 @@ class GroupStageViewSet(viewsets.ModelViewSet):
return self.queryset.filter(tournament__event__creator=self.request.user)
return []
class MatchViewSet(viewsets.ModelViewSet):
class MatchViewSet(SoftDeleteViewSet):
queryset = Match.objects.all()
serializer_class = MatchSerializer
@ -189,7 +208,7 @@ class MatchViewSet(viewsets.ModelViewSet):
return self.queryset.filter(Q(group_stage__tournament__event__creator=self.request.user) | Q(round__tournament__event__creator=self.request.user))
return []
class TeamScoreViewSet(viewsets.ModelViewSet):
class TeamScoreViewSet(SoftDeleteViewSet):
queryset = TeamScore.objects.all()
serializer_class = TeamScoreSerializer
@ -202,7 +221,7 @@ class TeamScoreViewSet(viewsets.ModelViewSet):
return self.queryset.filter(team_registration__tournament__event__creator=self.request.user)
return []
class TeamRegistrationViewSet(viewsets.ModelViewSet):
class TeamRegistrationViewSet(SoftDeleteViewSet):
queryset = TeamRegistration.objects.all()
serializer_class = TeamRegistrationSerializer
@ -214,7 +233,7 @@ class TeamRegistrationViewSet(viewsets.ModelViewSet):
return self.queryset.filter(tournament__event__creator=self.request.user)
return []
class PlayerRegistrationViewSet(viewsets.ModelViewSet):
class PlayerRegistrationViewSet(SoftDeleteViewSet):
queryset = PlayerRegistration.objects.all()
serializer_class = PlayerRegistrationSerializer
@ -226,11 +245,11 @@ class PlayerRegistrationViewSet(viewsets.ModelViewSet):
return self.queryset.filter(team_registration__tournament__event__creator=self.request.user)
return []
class CourtViewSet(viewsets.ModelViewSet):
class CourtViewSet(SoftDeleteViewSet):
queryset = Court.objects.all()
serializer_class = CourtSerializer
class DateIntervalViewSet(viewsets.ModelViewSet):
class DateIntervalViewSet(SoftDeleteViewSet):
queryset = DateInterval.objects.all()
serializer_class = DateIntervalSerializer
@ -293,7 +312,7 @@ class DeviceTokenViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class DrawLogViewSet(viewsets.ModelViewSet):
class DrawLogViewSet(SoftDeleteViewSet):
queryset = DrawLog.objects.all()
serializer_class = DrawLogSerializer
@ -304,3 +323,32 @@ class DrawLogViewSet(viewsets.ModelViewSet):
if self.request.user:
return self.queryset.filter(tournament__event__creator=self.request.user)
return []
class UnregisteredTeamViewSet(SoftDeleteViewSet):
queryset = UnregisteredTeam.objects.all()
serializer_class = UnregisteredTeamSerializer
def get_queryset(self):
tournament_id = self.request.query_params.get('tournament')
if tournament_id:
return self.queryset.filter(tournament=tournament_id)
if self.request.user:
return self.queryset.filter(tournament__event__creator=self.request.user)
return []
class UnregisteredPlayerViewSet(SoftDeleteViewSet):
queryset = UnregisteredPlayer.objects.all()
serializer_class = UnregisteredPlayerSerializer
def get_queryset(self):
tournament_id = self.request.query_params.get('tournament')
if tournament_id:
return self.queryset.filter(unregistered_team__tournament=tournament_id)
if self.request.user:
return self.queryset.filter(unregistered_team__tournament__event__creator=self.request.user)
return []
class ShortUserViewSet(viewsets.ModelViewSet):
queryset = CustomUser.objects.all()
serializer_class = ShortUserSerializer
permission_classes = [] # Users are public whereas the other requests are only for logged users

@ -35,7 +35,7 @@ INSTALLED_APPS = [
'daphne',
'sync',
'tournaments',
'crm',
# 'crm',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
@ -144,6 +144,16 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# settings.py
LOGIN_REDIRECT_URL = '/' # Redirect to the homepage after login
LOGOUT_REDIRECT_URL = '/' # Redirect to the homepage after logout
AUTHENTICATION_BACKENDS = [
'tournaments.backends.EmailOrUsernameModelBackend', # replace 'yourapp' with your actual app name
'django.contrib.auth.backends.ModelBackend',
]
CSRF_COOKIE_SECURE = True # if using HTTPS
from .settings_local import *
from .settings_app import *

@ -18,18 +18,18 @@ REST_FRAMEWORK = {
# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
# EMAIL_HOST = 'smtp-xlr.alwaysdata.net'
# EMAIL_PORT = 587
# EMAIL_HOST_USER = 'automatic@padelclub.app'
# EMAIL_HOST_PASSWORD = 'XLRSport$2024'
EMAIL_HOST_USER = 'automatic@padelclub.app'
EMAIL_HOST_PASSWORD = 'XLR@Sport@2024'
# EMAIL_USE_TLS = True
# DEFAULT_FROM_EMAIL = 'Padel Club <automatic@padelclub.app>'
DEFAULT_FROM_EMAIL = 'Padel Club <automatic@padelclub.app>'
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp-xlr.alwaysdata.net'
EMAIL_PORT = 587
EMAIL_HOST_USER = 'xlr@alwaysdata.net'
EMAIL_HOST_PASSWORD = 'XLRSport$2024'
#EMAIL_HOST_USER = 'xlr@alwaysdata.net'
#EMAIL_HOST_PASSWORD = 'XLRSport$2024'
EMAIL_USE_TLS = True
DEFAULT_FROM_EMAIL = 'Padel Club <xlr@alwaysdata.net>'
#DEFAULT_FROM_EMAIL = 'Padel Club <xlr@alwaysdata.net>'
CACHES = {
'default': {

@ -19,7 +19,7 @@ from django.urls import include, path
urlpatterns = [
path("", include("tournaments.urls")),
path("crm/", include("crm.urls")),
# path("crm/", include("crm.urls")),
path('roads/', include("api.urls")),
path('kingdom/', admin.site.urls),
path('api-auth/', include('rest_framework.urls')),

@ -0,0 +1,17 @@
# Generated by Django 5.1 on 2025-02-12 13:36
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('sync', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='modellog',
options={'ordering': ['date']},
),
]

@ -106,6 +106,8 @@ class SynchronizationApi(HierarchyApiView):
model_name = op.get('model_name')
data = op.get('data')
print(f'{model_operation} : {model_name}')
models.add(model_name)
serializer_class = build_serializer_class(model_name)
@ -158,6 +160,7 @@ class SynchronizationApi(HierarchyApiView):
except Exception as e:
response_status = status.HTTP_400_BAD_REQUEST
print(f'other exception: {str(e)}')
message = str(e)
results.append({

@ -3,7 +3,7 @@ from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.utils import timezone
from .models import Club, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, Court, DateInterval, FailedApiCall, Log, DeviceToken, DrawLog
from .models import Club, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, Court, DateInterval, FailedApiCall, Log, DeviceToken, DrawLog, UnregisteredTeam, UnregisteredPlayer
from .forms import CustomUserCreationForm, CustomUserChangeForm
from .filters import TeamScoreTournamentListFilter, MatchTournamentListFilter, SimpleTournamentListFilter, MatchTypeListFilter, SimpleIndexListFilter
@ -13,7 +13,8 @@ class CustomUserAdmin(UserAdmin):
form = CustomUserChangeForm
add_form = CustomUserCreationForm
model = CustomUser
list_display = ['email', 'username', 'is_active', 'is_staff', 'first_name', 'last_name', 'date_joined', 'event_count']
list_display = ['first_name', 'last_name', 'email', 'latest_event_club_name', 'username', 'is_active', 'date_joined', 'event_count', 'origin']
list_filter = ['is_active', 'origin']
ordering = ['-date_joined']
fieldsets = [
(None, {'fields': ['id', 'username', 'email', 'password', 'first_name', 'last_name', 'is_active',
@ -21,7 +22,7 @@ class CustomUserAdmin(UserAdmin):
'summons_message_body', 'summons_message_signature', 'summons_available_payment_methods',
'summons_display_format', 'summons_display_entry_fee', 'summons_use_full_custom_message',
'match_formats_default_duration', 'bracket_match_format_preference', 'group_stage_match_format_preference',
'loser_bracket_match_format_preference', 'device_id', 'loser_bracket_mode', 'groups', 'agents'
'loser_bracket_match_format_preference', 'device_id', 'loser_bracket_mode', 'groups', 'origin', 'agents'
]}),
]
add_fieldsets = [
@ -47,6 +48,7 @@ class TournamentAdmin(SyncedObjectAdmin):
list_display = ['display_name', 'event', 'is_private', 'start_date', 'payment', 'creator']
list_filter = ['is_deleted', 'event__creator']
ordering = ['-start_date']
search_fields = ['id']
class TeamRegistrationAdmin(SyncedObjectAdmin):
list_display = ['player_names', 'group_stage_position', 'name', 'tournament']
@ -56,6 +58,7 @@ class TeamRegistrationAdmin(SyncedObjectAdmin):
class TeamScoreAdmin(SyncedObjectAdmin):
list_display = ['team_registration', 'score', 'walk_out', 'match']
list_filter = [TeamScoreTournamentListFilter]
search_fields = ['id']
class RoundAdmin(SyncedObjectAdmin):
list_display = ['tournament', 'name', 'index', 'parent']
@ -112,6 +115,16 @@ class DrawLogAdmin(SyncedObjectAdmin):
list_filter = [SimpleTournamentListFilter]
ordering = ['draw_date']
class UnregisteredTeamAdmin(admin.ModelAdmin):
list_display = ['player_names', 'tournament']
list_filter = [SimpleTournamentListFilter]
class UnregisteredPlayerAdmin(admin.ModelAdmin):
list_display = ['first_name', 'last_name', 'licence_id']
search_fields = ('first_name', 'last_name')
list_filter = []
ordering = ['last_name', 'first_name']
admin.site.register(CustomUser, CustomUserAdmin)
admin.site.register(Club, ClubAdmin)
admin.site.register(Event, EventAdmin)
@ -129,3 +142,5 @@ admin.site.register(FailedApiCall, FailedApiCallAdmin)
admin.site.register(Log, LogAdmin)
admin.site.register(DeviceToken, DeviceTokenAdmin)
admin.site.register(DrawLog, DrawLogAdmin)
admin.site.register(UnregisteredTeam, UnregisteredTeamAdmin)
admin.site.register(UnregisteredPlayer, UnregisteredPlayerAdmin)

@ -0,0 +1,34 @@
# backends.py
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q
import logging
logger = logging.getLogger(__name__)
class EmailOrUsernameModelBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
UserModel = get_user_model()
print(f"Backend attempting authentication for: {username}") # Debug print
logger.info(f"Backend attempting authentication for: {username}")
try:
user = UserModel.objects.get(
Q(username__iexact=username) | Q(email__iexact=username)
)
print(f"User found: {user}") # Debug print
logger.info(f"User found: {user}")
if user.check_password(password):
print("Password check successful") # Debug print
logger.info("Password check successful")
return user
print("Password check failed") # Debug print
logger.warning("Password check failed")
return None
except UserModel.DoesNotExist:
print("User not found") # Debug print
logger.warning("User not found")
return None

@ -1,19 +1,257 @@
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, PasswordResetForm
from django import forms
from .models import CustomUser
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.contrib.auth.tokens import default_token_generator
from django.contrib.sites.shortcuts import get_current_site
from django.utils.encoding import force_bytes
class CustomUserCreationForm(UserCreationForm):
class Meta:
model = CustomUser
error_messages = {
'email': {
'unique': "Cette adresse email est déjà utilisée.",
},
'username': {
'unique': "Ce nom d'utilisateur est déjà pris.",
},
'licence_id': {
'unique': "Cette licence est déjà utilisée par un autre compte.",
},
}
fields = UserCreationForm.Meta.fields + ('umpire_code', 'clubs', 'phone', 'first_name', 'last_name', 'licence_id', 'country')
class SimpleCustomUserCreationForm(UserCreationForm):
class Meta:
model = CustomUser
fields = UserCreationForm.Meta.fields + ('email', 'phone', 'first_name', 'last_name', 'licence_id', 'country')
error_messages = {
'email': {
'unique': "Cette adresse email est déjà utilisée.",
},
'username': {
'unique': "Ce nom d'utilisateur est déjà pris.",
},
'licence_id': {
'unique': "Cette licence est déjà utilisée par un autre compte.",
},
}
labels = {
'username': 'Nom d’utilisateur',
'email': 'E-mail',
'phone': 'Numéro de téléphone',
'first_name': 'Prénom',
'last_name': 'Nom de famille',
'licence_id': 'Numéro de licence',
'country': 'Pays',
'password1': 'Mot de passe',
'password2': 'Confirmer le mot de passe',
}
class CustomUserChangeForm(UserChangeForm):
class Meta:
model = CustomUser
error_messages = {
'email': {
'unique': "Cette adresse email est déjà utilisée.",
},
'username': {
'unique': "Ce nom d'utilisateur est déjà pris.",
},
'licence_id': {
'unique': "Cette licence est déjà utilisée par un autre compte.",
},
}
fields = UserCreationForm.Meta.fields + ('umpire_code', 'clubs', 'phone', 'first_name', 'last_name', 'licence_id', 'country')
class SimpleForm(forms.Form):
# A single field for user input (e.g., a text field)
name = forms.CharField(label='Enter your name!', max_length=100)
class TournamentRegistrationForm(forms.Form):
#first_name = forms.CharField(label='Prénom', max_length=50)
#last_name = forms.CharField(label='Nom', max_length=50)
email = forms.EmailField(label='E-mail', widget=forms.EmailInput(attrs={'readonly': 'readonly'}))
mobile_number = forms.CharField(
label='Téléphone',
max_length=15,
required=True
)
def clean_mobile_number(self):
mobile_number = self.cleaned_data.get('mobile_number')
if mobile_number:
# Basic regex for mobile numbers, matching common formats
# Remove spaces from the number first
mobile_number = mobile_number.replace(' ', '')
if not re.match(r"^\+?\d{10,15}$", mobile_number):
raise forms.ValidationError("Entrer un numéro de téléphone valide.")
return mobile_number
class AddPlayerForm(forms.Form):
licence_id = forms.CharField(label='Numéro de licence (avec la lettre)', max_length=20, required=False)
first_name = forms.CharField(label='Prénom', min_length=2, max_length=50, required=False)
last_name = forms.CharField(label='Nom', min_length=2, max_length=50, required=False)
first_tournament = False
user_without_licence = False
def names_is_valid(self):
first_name = self.cleaned_data.get('first_name')
last_name = self.cleaned_data.get('last_name')
return len(first_name) >= 2 and len(last_name) >= 2
def clean_licence_id(self):
licence_id = self.cleaned_data.get('licence_id')
# Convert to uppercase
licence_id = licence_id.upper()
# Update the cleaned_data with the modified licence_id
self.cleaned_data['licence_id'] = licence_id
# Optionally, print the cleaned license ID for debugging
print(f"Cleaned Licence ID (inside clean_licence_id): {licence_id}")
return licence_id
def clean_last_name(self):
last_name = self.cleaned_data.get('last_name')
# Convert to uppercase
last_name = last_name.upper()
# Update the cleaned_data with the modified licence_id
self.cleaned_data['last_name'] = last_name
return last_name
def clean_first_name(self):
first_name = self.cleaned_data.get('first_name')
# Convert to capitalize
first_name = first_name.capitalize()
# Update the cleaned_data with the modified licence_id
self.cleaned_data['first_name'] = first_name
return first_name
class CustomPasswordResetForm(PasswordResetForm):
def save(self, *args, **kwargs):
"""
Override the save method to send a custom email.
"""
email = self.cleaned_data["email"]
users = self.get_users(email)
for user in users:
# Generate the token for password reset
token = default_token_generator.make_token(user)
uid = urlsafe_base64_encode(force_bytes(user.pk))
# Prepare the context for the email template
context = {
"email": user.email,
"domain": get_current_site(self.request).domain,
"site_name": "Padel Club",
"uid": uid,
"token": token,
"protocol": "http", # Use 'https' in production
}
# Render the email content from the template
subject = "Réinitialisation du mot de passe"
message = render_to_string("registration/password_reset_email.html", context)
# Send the email
send_mail(subject, message, None, [user.email])
class ProfileUpdateForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Remove autofocus from the 'username' field
self.fields['username'].widget.attrs.pop("autofocus", None)
class Meta:
model = CustomUser
fields = ['first_name', 'last_name', 'licence_id', 'username', 'email', 'phone']
labels = {
'username': 'Nom d’utilisateur',
'email': 'E-mail',
'phone': 'Numéro de téléphone',
'first_name': 'Prénom',
'last_name': 'Nom de famille',
'licence_id': 'Numéro de licence',
}
error_messages = {
'email': {
'unique': "Cette adresse email est déjà utilisée.",
},
'username': {
'unique': "Ce nom d'utilisateur est déjà pris.",
},
'licence_id': {
'unique': "Cette licence est déjà utilisée par un autre compte.",
},
}
from django.contrib.auth.forms import PasswordChangeForm
class CustomPasswordChangeForm(PasswordChangeForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Remove autofocus from all fields in the 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):
username = forms.CharField(label='Username or Email')
def clean(self):
username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password')
print(f"Login attempt with username/email: {username}") # Debug print
logger.info(f"Login attempt with username/email: {username}")
if username and password:
self.user_cache = authenticate(
self.request,
username=username,
password=password
)
print(f"Authentication result: {self.user_cache}") # Debug print
logger.info(f"Authentication result: {self.user_cache}")
if self.user_cache is None:
print("Authentication failed") # Debug print
logger.warning("Authentication failed")
raise forms.ValidationError(
"Identifiant/E-mail ou mot de passe incorrect. Les champs sont sensibles à la casse.",
code='invalid_login'
)
else:
print(f"Authentication successful for user: {self.user_cache}") # Debug print
logger.info(f"Authentication successful for user: {self.user_cache}")
self.confirm_login_allowed(self.user_cache)
return self.cleaned_data

@ -0,0 +1,18 @@
# Generated by Django 4.2.11 on 2024-11-15 07:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0093_drawlog_draw_type_and_more'),
]
operations = [
migrations.AddField(
model_name='playerregistration',
name='captain',
field=models.BooleanField(default=False),
),
]

@ -0,0 +1,14 @@
# Generated by Django 4.2.11 on 2024-11-29 11:43
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0094_alter_customuser_bracket_match_format_preference_and_more'),
('tournaments', '0094_playerregistration_captain'),
]
operations = [
]

@ -1,41 +0,0 @@
# Generated by Django 5.1 on 2024-12-13 14:18
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', '0095_club_creation_date_club_last_update_and_more'),
]
operations = [
migrations.AddField(
model_name='drawlog',
name='creation_date',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
),
migrations.AddField(
model_name='drawlog',
name='last_update',
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AddField(
model_name='drawlog',
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='drawlog',
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),
),
migrations.AddField(
model_name='drawlog',
name='store_id',
field=models.CharField(default='', max_length=100),
),
]

@ -0,0 +1,38 @@
# Generated by Django 4.2.11 on 2024-11-29 11:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0095_merge_20241129_1243'),
]
operations = [
migrations.AddField(
model_name='tournament',
name='account_is_required',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='tournament',
name='license_is_required',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='tournament',
name='maximum_player_per_team',
field=models.IntegerField(default=2),
),
migrations.AddField(
model_name='tournament',
name='minimum_player_per_team',
field=models.IntegerField(default=2),
),
migrations.AlterField(
model_name='playerregistration',
name='source',
field=models.IntegerField(blank=True, choices=[(0, 'French Federation'), (1, 'Beach Padel'), (2, 'Online Registration')], null=True),
),
]

@ -0,0 +1,23 @@
# Generated by Django 4.2.11 on 2024-11-29 13:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0096_tournament_account_is_required_and_more'),
]
operations = [
migrations.AddField(
model_name='tournament',
name='display_entry_fee_information',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='tournament',
name='information',
field=models.CharField(blank=True, max_length=4000, null=True),
),
]

@ -1,19 +0,0 @@
# Generated by Django 5.1 on 2024-12-16 09:02
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0097_special_store_id'),
]
operations = [
migrations.AlterField(
model_name='drawlog',
name='tournament',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='draw_logs', to='tournaments.tournament'),
),
]

@ -0,0 +1,38 @@
# Generated by Django 4.2.11 on 2024-11-30 09:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0097_tournament_display_entry_fee_information_and_more'),
]
operations = [
migrations.AddField(
model_name='tournament',
name='enable_online_registration',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='tournament',
name='opening_registration_date',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='tournament',
name='registration_date_limit',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='tournament',
name='target_team_count',
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='tournament',
name='waiting_list_limit',
field=models.IntegerField(blank=True, null=True),
),
]

@ -1,79 +0,0 @@
# Generated by Django 5.1 on 2025-01-28 14:54
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0098_alter_drawlog_tournament'),
]
operations = [
migrations.AlterField(
model_name='court',
name='club',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='courts', to='tournaments.club'),
),
migrations.AlterField(
model_name='dateinterval',
name='event',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='date_intervals', to='tournaments.event'),
),
migrations.AlterField(
model_name='drawlog',
name='tournament',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='draw_logs', to='tournaments.tournament'),
),
migrations.AlterField(
model_name='groupstage',
name='tournament',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='group_stages', to='tournaments.tournament'),
),
migrations.AlterField(
model_name='match',
name='group_stage',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='matches', to='tournaments.groupstage'),
),
migrations.AlterField(
model_name='match',
name='round',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='matches', to='tournaments.round'),
),
migrations.AlterField(
model_name='playerregistration',
name='team_registration',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='player_registrations', to='tournaments.teamregistration'),
),
migrations.AlterField(
model_name='round',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='tournaments.round'),
),
migrations.AlterField(
model_name='round',
name='tournament',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='rounds', to='tournaments.tournament'),
),
migrations.AlterField(
model_name='teamregistration',
name='tournament',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='team_registrations', to='tournaments.tournament'),
),
migrations.AlterField(
model_name='teamscore',
name='match',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='team_scores', to='tournaments.match'),
),
migrations.AlterField(
model_name='teamscore',
name='team_registration',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='team_scores', to='tournaments.teamregistration'),
),
migrations.AlterField(
model_name='tournament',
name='event',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tournaments', to='tournaments.event'),
),
]

@ -0,0 +1,17 @@
# Generated by Django 4.2.11 on 2024-11-30 10:08
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0098_tournament_enable_online_registration_and_more'),
]
operations = [
migrations.RemoveField(
model_name='tournament',
name='display_entry_fee_information',
),
]

@ -0,0 +1,28 @@
# Generated by Django 4.2.11 on 2024-12-10 08:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0099_remove_tournament_display_entry_fee_information'),
]
operations = [
migrations.AddField(
model_name='playerregistration',
name='coach',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='teamregistration',
name='unregistered',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='teamregistration',
name='unregistration_date',
field=models.DateTimeField(blank=True, null=True),
),
]

@ -0,0 +1,38 @@
# Generated by Django 4.2.11 on 2024-12-16 08:57
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0100_playerregistration_coach_and_more'),
]
operations = [
migrations.CreateModel(
name='UnregisteredTeam',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('unregistration_date', models.DateTimeField(blank=True, null=True)),
('comment', models.CharField(blank=True, max_length=200, null=True)),
('source', models.CharField(blank=True, max_length=20, null=True)),
('source_value', models.CharField(blank=True, max_length=200, null=True)),
('reason', models.IntegerField(blank=True, null=True)),
('tournament', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tournaments.tournament')),
],
),
migrations.CreateModel(
name='UnregisteredPlayer',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('first_name', models.CharField(blank=True, max_length=50)),
('last_name', models.CharField(blank=True, max_length=50)),
('licence_id', models.CharField(blank=True, max_length=50, null=True)),
('reason', models.IntegerField(blank=True, null=True)),
('unregistered_team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tournaments.unregisteredteam')),
],
),
]

@ -0,0 +1,21 @@
# Generated by Django 4.2.11 on 2024-12-16 13:10
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0101_unregisteredteam_unregisteredplayer'),
]
operations = [
migrations.RemoveField(
model_name='teamregistration',
name='unregistered',
),
migrations.RemoveField(
model_name='teamregistration',
name='unregistration_date',
),
]

@ -0,0 +1,33 @@
# Generated by Django 4.2.11 on 2024-12-17 14:14
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0102_remove_teamregistration_unregistered_and_more'),
]
operations = [
migrations.RemoveField(
model_name='unregisteredplayer',
name='reason',
),
migrations.RemoveField(
model_name='unregisteredteam',
name='comment',
),
migrations.RemoveField(
model_name='unregisteredteam',
name='reason',
),
migrations.RemoveField(
model_name='unregisteredteam',
name='source',
),
migrations.RemoveField(
model_name='unregisteredteam',
name='source_value',
),
]

@ -0,0 +1,17 @@
# Generated by Django 4.2.11 on 2024-12-20 13:33
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0103_remove_unregisteredplayer_reason_and_more'),
]
operations = [
migrations.RemoveField(
model_name='tournament',
name='target_team_count',
),
]

@ -0,0 +1,23 @@
# Generated by Django 4.2.11 on 2025-01-15 07:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0104_remove_tournament_target_team_count'),
]
operations = [
migrations.AddField(
model_name='playerregistration',
name='registered_online',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='playerregistration',
name='source',
field=models.IntegerField(blank=True, choices=[(0, 'French Federation'), (1, 'Beach Padel')], null=True),
),
]

@ -0,0 +1,18 @@
# Generated by Django 4.2.11 on 2025-01-15 07:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0105_playerregistration_registered_online_and_more'),
]
operations = [
migrations.AlterField(
model_name='customuser',
name='licence_id',
field=models.CharField(blank=True, max_length=10, null=True, unique=True),
),
]

@ -0,0 +1,18 @@
# Generated by Django 5.1 on 2025-01-28 07:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0106_alter_customuser_licence_id'),
]
operations = [
migrations.AddField(
model_name='customuser',
name='origin',
field=models.IntegerField(blank=True, choices=[(0, 'Admin'), (1, 'Site'), (2, 'App')], default=0, null=True),
),
]

@ -1,4 +1,4 @@
# Generated by Django 5.1 on 2024-12-13 13:13
# Generated by Django 5.1 on 2025-02-12 13:36
import django.db.models.deletion
import django.utils.timezone
@ -9,7 +9,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0094_alter_customuser_bracket_match_format_preference_and_more'),
('tournaments', '0107_customuser_origin'),
]
operations = [
@ -83,6 +83,31 @@ class Migration(migrations.Migration):
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),
),
migrations.AddField(
model_name='drawlog',
name='creation_date',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
),
migrations.AddField(
model_name='drawlog',
name='last_update',
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AddField(
model_name='drawlog',
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='drawlog',
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),
),
migrations.AddField(
model_name='drawlog',
name='store_id',
field=models.CharField(default='', max_length=100),
),
migrations.AddField(
model_name='event',
name='last_update',
@ -311,7 +336,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='court',
name='club',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='courts', to='tournaments.club'),
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='courts', to='tournaments.club'),
),
migrations.AlterField(
model_name='customuser',
@ -321,13 +346,18 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='dateinterval',
name='event',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='date_intervals', to='tournaments.event'),
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='date_intervals', to='tournaments.event'),
),
migrations.AlterField(
model_name='devicetoken',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='device_tokens', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='drawlog',
name='tournament',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='draw_logs', to='tournaments.tournament'),
),
migrations.AlterField(
model_name='event',
name='club',
@ -346,7 +376,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='groupstage',
name='tournament',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='group_stages', to='tournaments.tournament'),
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='group_stages', to='tournaments.tournament'),
),
migrations.AlterField(
model_name='log',
@ -356,22 +386,27 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='match',
name='group_stage',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='matches', to='tournaments.groupstage'),
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='matches', to='tournaments.groupstage'),
),
migrations.AlterField(
model_name='match',
name='round',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='matches', to='tournaments.round'),
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='matches', to='tournaments.round'),
),
migrations.AlterField(
model_name='playerregistration',
name='team_registration',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='player_registrations', to='tournaments.teamregistration'),
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='player_registrations', to='tournaments.teamregistration'),
),
migrations.AlterField(
model_name='round',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='tournaments.round'),
),
migrations.AlterField(
model_name='round',
name='tournament',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rounds', to='tournaments.tournament'),
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='rounds', to='tournaments.tournament'),
),
migrations.AlterField(
model_name='teamregistration',
@ -381,16 +416,21 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='teamregistration',
name='tournament',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team_registrations', to='tournaments.tournament'),
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='team_registrations', to='tournaments.tournament'),
),
migrations.AlterField(
model_name='teamscore',
name='match',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='team_scores', to='tournaments.match'),
),
migrations.AlterField(
model_name='teamscore',
name='team_registration',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='team_scores', to='tournaments.teamregistration'),
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='team_scores', to='tournaments.teamregistration'),
),
migrations.AlterField(
model_name='tournament',
name='event',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tournaments', to='tournaments.event'),
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tournaments', to='tournaments.event'),
),
]

@ -60,7 +60,7 @@ def init_related_names(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0096_drawlog_creation_date_drawlog_last_update_and_more'),
('tournaments', '0108_club_creation_date_club_last_update_and_more'),
]
operations = [

@ -4,7 +4,7 @@ from .custom_user import CustomUser
from .club import Club
from .court import Court
from .date_interval import DateInterval
from .enums import TournamentPayment, FederalCategory, FederalLevelCategory, FederalAgeCategory, FederalMatchCategory, ModelOperation
from .enums import UserOrigin, TournamentPayment, FederalCategory, FederalLevelCategory, FederalAgeCategory, FederalMatchCategory, OnlineRegistrationStatus, ModelOperation
from .player_enums import PlayerSexType, PlayerDataSource, PlayerPaymentType
from .event import Event
from .tournament import Tournament, TeamSummon, TeamSortingType, TeamItem
@ -19,3 +19,5 @@ from .failed_api_call import FailedApiCall
from .log import Log
from .device_token import DeviceToken
from .draw_log import DrawLog
from .unregistered_team import UnregisteredTeam
from .unregistered_player import UnregisteredPlayer

@ -15,7 +15,7 @@ class CustomUser(AbstractUser):
phone = models.CharField(max_length=15, null=True, blank=True)
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
licence_id = models.CharField(max_length=10, null=True, blank=True)
licence_id = models.CharField(max_length=10, unique=True, null=True, blank=True)
country = models.CharField(max_length=40, null=True, blank=True)
summons_message_body = models.TextField(blank=True, null=True)
@ -36,6 +36,8 @@ class CustomUser(AbstractUser):
agents = models.ManyToManyField('CustomUser', blank=True, related_name='owners')
loser_bracket_mode = models.IntegerField(default=0)
origin = models.IntegerField(default=enums.UserOrigin.ADMIN, choices=enums.UserOrigin.choices, null=True, blank=True)
### ### ### ### ### ### ### ### ### ### ### WARNING ### ### ### ### ### ### ### ### ### ###
### WARNING : Any added field MUST be inserted in the method below: fields_for_update() ###
### ### ### ### ### ### ### ### ### ### ### WARNING ### ### ### ### ### ### ### ### ### ###
@ -47,8 +49,7 @@ class CustomUser(AbstractUser):
'summons_message_body', 'summons_message_signature', 'summons_available_payment_methods',
'summons_display_format', 'summons_display_entry_fee',
'summons_use_full_custom_message', 'match_formats_default_duration', 'bracket_match_format_preference',
'group_stage_match_format_preference', 'loser_bracket_match_format_preference', 'device_id', 'loser_bracket_mode', 'agents']
'group_stage_match_format_preference', 'loser_bracket_match_format_preference', 'device_id', 'loser_bracket_mode', 'origin', 'agents']
def __str__(self):
return self.username
@ -61,3 +62,9 @@ class CustomUser(AbstractUser):
def full_name(self):
return f"{self.first_name} {self.last_name}"
def latest_event_club_name(self):
latest_event = self.events.order_by('-creation_date').first()
if latest_event and latest_event.club:
return latest_event.club.name
return None

@ -13,6 +13,30 @@ class FederalCategory(models.IntegerChoices):
MIXED = 2, 'Mixte'
UNLISTED = 3, ''
@staticmethod
def female_in_male_assimilation_addition(rank: int) -> int:
if 1 <= rank <= 10:
return 400
elif 11 <= rank <= 30:
return 1000
elif 31 <= rank <= 60:
return 2000
elif 61 <= rank <= 100:
return 3500
elif 101 <= rank <= 200:
return 10000
elif 201 <= rank <= 500:
return 15000
elif 501 <= rank <= 1000:
return 25000
elif 1001 <= rank <= 2000:
return 35000
elif 2001 <= rank <= 3000:
return 45000
else:
return 50000
class FederalLevelCategory(models.IntegerChoices):
UNLISTED = 0, 'Animation'
P25 = 25, 'P25'
@ -23,6 +47,28 @@ class FederalLevelCategory(models.IntegerChoices):
P1500 = 1500, 'P1500'
P2000 = 2000, 'P2000'
@staticmethod
def min_player_rank(level=None, category=None, age_category=None) -> int:
if level == FederalLevelCategory.P25:
if age_category in [FederalAgeCategory.SENIOR, FederalAgeCategory.A45, FederalAgeCategory.A55]:
return 20000 if category == FederalCategory.MEN else 1000
return 0
elif level == FederalLevelCategory.P100:
if age_category in [FederalAgeCategory.SENIOR, FederalAgeCategory.A45, FederalAgeCategory.A55]:
return 2000 if category == FederalCategory.MEN else 300
return 0
elif level == FederalLevelCategory.P250:
if age_category in [FederalAgeCategory.SENIOR, FederalAgeCategory.A45, FederalAgeCategory.A55]:
if category == FederalCategory.MIXED:
return 0
return 500 if category == FederalCategory.MEN else 100
return 0
return 0
class FederalAgeCategory(models.IntegerChoices):
UNLISTED = 0, ''
A11_12 = 120, 'U12'
@ -68,3 +114,31 @@ class ModelOperation(models.TextChoices):
DELETE = 'DELETE', 'DELETE'
GRANT_ACCESS = 'GRANT_ACCESS', 'GRANT_ACCESS'
REVOKE_ACCESS = 'REVOKE_ACCESS', 'REVOKE_ACCESS'
class OnlineRegistrationStatus(models.IntegerChoices):
OPEN = 1, 'Open'
NOT_ENABLED = 2, 'Not Enabled'
NOT_STARTED = 3, 'Not Started'
ENDED = 4, 'Ended'
WAITING_LIST_POSSIBLE = 5, 'Waiting List Possible'
WAITING_LIST_FULL = 6, 'Waiting List Full'
IN_PROGRESS = 7, 'In Progress'
ENDED_WITH_RESULTS = 8, 'Ended with Results'
def status_localized(self) -> str:
status_map = {
OnlineRegistrationStatus.OPEN: "Inscription ouverte",
OnlineRegistrationStatus.NOT_ENABLED: "Inscription désactivée",
OnlineRegistrationStatus.NOT_STARTED: "Ouverture des inscriptions à venir",
OnlineRegistrationStatus.ENDED: "Inscription terminée",
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é"
}
return status_map.get(self, "")
class UserOrigin(models.IntegerChoices):
ADMIN = 0, 'Admin'
SITE = 1, 'Site'
APP = 2, 'App'

@ -213,6 +213,7 @@ class GroupStageTeam:
self.display_set_difference = False
self.weight = team_registration.weight
self.team_registration = team_registration
self.qualified = team_registration.qualified
def wins_losses(self):
return f"{self.wins}/{self.losses}"
@ -257,4 +258,5 @@ class GroupStageTeam:
"diff": self.formatted_diff(),
"weight": self.weight,
"match_count": self.match_count(),
"qualified": self.qualified,
}

@ -64,7 +64,8 @@ class Match(SideStoreModel):
items = []
if self.round:
items.append(self.round.name())
items.append(f" #{self.index_in_round() + 1}")
if self.round.index > 0:
items.append(f" #{self.index_in_round() + 1}")
elif self.group_stage:
items.append(self.group_stage.name())
items.append(f"Match #{self.index + 1}")
@ -145,7 +146,7 @@ class Match(SideStoreModel):
is_winner = False
scores = []
walk_out = None
team = Team(image, names, scores, weight, is_winner, walk_out)
team = Team(None, image, names, scores, weight, is_winner, walk_out)
return team
def live_teams(self):
@ -169,11 +170,11 @@ class Match(SideStoreModel):
# No team scores at all
if previous_top_match:
names = [f"Gagnants du {previous_top_match.computed_name()}", '']
names = [f"Gagnants {previous_top_match.computed_name()}", '']
team = self.default_live_team(names)
teams.append(team)
if previous_bottom_match:
names = [f"Gagnants du {previous_bottom_match.computed_name()}", '']
names = [f"Gagnants {previous_bottom_match.computed_name()}", '']
team = self.default_live_team(names)
teams.append(team)
elif len(team_scores) == 1:
@ -185,12 +186,12 @@ class Match(SideStoreModel):
teams.append(existing_team)
else:
if previous_top_match and previous_top_match.disabled == False and previous_top_match.end_date is None:
names = [f"Gagnants du {previous_top_match.computed_name()}", '']
names = [f"Gagnants {previous_top_match.computed_name()}", '']
team = self.default_live_team(names)
teams.append(team)
teams.append(existing_team)
elif previous_bottom_match:
names = [f"Gagnants du {previous_bottom_match.computed_name()}", '']
names = [f"Gagnants {previous_bottom_match.computed_name()}", '']
team = self.default_live_team(names)
teams.append(existing_team)
teams.append(team)
@ -359,8 +360,9 @@ class Match(SideStoreModel):
# return sort_score
class Team:
def __init__(self, image, names, scores, weight, is_winner, walk_out):
def __init__(self, id, image, names, scores, weight, is_winner, walk_out):
# print(f"image = {image}, names= {names}, scores ={scores}, weight={weight}, win={is_winner}")
self.id = str(id)
self.image = image
self.names = names
self.scores = scores

@ -1,6 +1,7 @@
from django.db import models
from . import SideStoreModel, TeamRegistration, PlayerSexType, PlayerDataSource, PlayerPaymentType
from . import SideStoreModel, TeamRegistration, PlayerSexType, PlayerDataSource, PlayerPaymentType, FederalCategory
import uuid
from django.utils import timezone
class PlayerRegistration(SideStoreModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)
@ -33,6 +34,9 @@ class PlayerRegistration(SideStoreModel):
source = models.IntegerField(choices=PlayerDataSource.choices, null=True, blank=True)
has_arrived = models.BooleanField(default=False)
captain = models.BooleanField(default=False)
coach = models.BooleanField(default=False)
registered_online = models.BooleanField(default=False)
def delete_dependencies(self):
pass
@ -58,3 +62,30 @@ class PlayerRegistration(SideStoreModel):
name_parts = self.last_name.split(" ")
name = f"{self.first_name[0]}. {name_parts[0]}"
return name
def clean_club_name(self):
if self.club_name:
return self.club_name.split(' (')[0]
return "Non renseigné"
def calculate_age(self):
if self.birthdate:
try:
birth_year = int(self.birthdate[-4:]) # Assumes birthdate ends with YYYY
current_year = timezone.now().year
return current_year - birth_year
except (ValueError, TypeError, IndexError):
return None
return None
def format_ordinal(self):
if self.rank is None:
if self.sex == PlayerSexType.FEMALE:
return "Non Classée"
return "Non Classé"
if self.rank == 1:
if self.sex == PlayerSexType.FEMALE:
return "1ère"
return "1er"
return f"{self.rank}ème"

@ -3,6 +3,7 @@ from django.db.models.sql.query import Q
from . import SideStoreModel, Tournament, GroupStage, Match
import uuid
from django.utils import timezone
from django.db.models import Count
class TeamRegistration(SideStoreModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)
@ -51,11 +52,14 @@ class TeamRegistration(SideStoreModel):
def get_tournament_id(self):
return self.tournament.id
def player_names_as_list(self):
return [pr.name() for pr in self.player_registrations.all()]
def team_names(self):
if self.name:
return [self.name]
else:
return [pr.name() for pr in self.player_registrations.all()]
return self.player_names_as_list()
def shortened_team_names(self):
if self.name:
@ -67,16 +71,30 @@ class TeamRegistration(SideStoreModel):
else:
return [pr.shortened_name() for pr in players]
@property
def players(self):
# Fetch related PlayerRegistration objects
return self.player_registrations.all().order_by('rank')
def player_names(self):
names = [pr.name() for pr in self.player_registrations.all()]
names = self.player_names_as_list()
str = " - ".join(names)
if len(str) > 0:
return str
else:
return "no players"
def formatted_team_names(self):
if self.name:
return self.name
names = [pr.last_name for pr in self.player_registrations.all()][:2] # Take max first 2
joined_names = " / ".join(names)
if joined_names:
return f"Paire {joined_names}"
return "Détail de l'équipe"
def next_match(self):
all_matches = [ts.match for ts in self.teamscore_set.all()]
all_matches = [ts.match for ts in self.team_scores.all()]
now = timezone.now()
all_matches = sorted(all_matches, key=lambda m: m.start_date if m.start_date is not None else now)
matches = [m for m in all_matches if m.end_date is None]
@ -86,7 +104,7 @@ class TeamRegistration(SideStoreModel):
return None
def next_stage(self):
matches = map(lambda ts: ts.match, self.teamscore_set.all())
matches = map(lambda ts: ts.match, self.team_scores.all())
matches = [m for m in matches if m.group_stage is None]
matches = sorted(matches, key=lambda m: m.round.index)
@ -100,6 +118,10 @@ class TeamRegistration(SideStoreModel):
else:
return "--"
def set_weight(self):
self.weight = self.player_registrations.aggregate(total_weight=models.Sum('computed_rank'))['total_weight'] or 0
self.save() # Save the updated weight if necessary
def is_valid_for_summon(self):
return len(self.player_registrations.all()) > 0
@ -116,3 +138,102 @@ class TeamRegistration(SideStoreModel):
else:
# print("no date")
return None
def local_registration_date(self):
timezone = self.tournament.timezone()
if self.registration_date:
return self.registration_date.astimezone(timezone)
else:
# print("no date")
return None
def out_of_tournament(self):
return self.walk_out
def get_other_player(self, player):
for p in self.player_registrations.all():
if p != player:
return p
return None
def is_in_waiting_list(self):
return self.tournament.get_team_waiting_list_position(self)
def get_matches(self):
matches = Match.objects.filter(team_scores__team_registration=self).distinct()
print(f"All matches for team {self.id}: {matches.count()}")
for match in matches:
print(f"Match {match.id}: start_date={match.start_date}, end_date={match.end_date}")
return matches
def get_upcoming_matches(self):
matches = self.get_matches()
upcoming = matches.filter(end_date__isnull=True).order_by('start_date')
print(f"Upcoming matches count: {upcoming.count()}")
return [match.live_match() for match in upcoming]
def get_completed_matches(self):
matches = self.get_matches()
completed = matches.filter(end_date__isnull=False).order_by('-end_date')
print(f"Completed matches count: {completed.count()}")
return [match.live_match() for match in completed]
def get_statistics(self):
stats = {
'final_ranking': self.get_final_ranking(),
'weight': self.weight,
'points_earned': self.get_points_earned(),
'initial_stage': self.get_initial_stage(),
'matches_played': self.count_matches_played(),
'victory_ratio': self.calculate_victory_ratio()
}
return stats
def get_initial_stage(self):
matches = self.get_matches().order_by('start_date')
first_match = matches.first()
if first_match:
if first_match.group_stage:
return "Poule"
elif first_match.round:
return first_match.round.name()
return None
def get_final_ranking(self):
if self.final_ranking:
if self.final_ranking == 1:
return "1er"
return f"{self.final_ranking}e"
return None
def get_points_earned(self):
return self.points_earned
def calculate_total_duration(self):
total_seconds = 0
for match in self.get_matches().filter(end_date__isnull=False):
if match.start_date and match.end_date:
duration = (match.end_date - match.start_date).total_seconds()
total_seconds += duration
if total_seconds > 0:
hours = int(total_seconds // 3600)
minutes = int((total_seconds % 3600) // 60)
if hours > 0:
return f"{hours}h{minutes:02d}"
return f"{minutes}min"
return None
def count_matches_played(self):
return self.get_matches().filter(end_date__isnull=False).count()
def calculate_victory_ratio(self):
matches = self.get_matches().filter(end_date__isnull=False)
total_matches = matches.count()
if total_matches > 0:
wins = matches.filter(winning_team_id=self.id).count()
ratio = (wins / total_matches) * 100
return f"{wins}/{total_matches}"
return None

@ -58,10 +58,24 @@ class TeamScore(SideStoreModel):
def scores(self):
if self.score:
return [int(x) for x in self.score.split(',')]
return [
int(x.split('-')[0]) # Extract the integer part before the hyphen
for x in self.score.split(',') # Split by commas for multiple scores
]
else:
return []
def parsed_scores(self):
if self.score:
return [
{
'main': int(x.split('-')[0]), # Main score
'tiebreak': x.split('-')[1] if '-' in x else None # Tiebreak
}
for x in self.score.split(',')
]
return []
def scores_array(self):
if self.score:
return [x for x in self.score.split(',')]
@ -89,16 +103,18 @@ class TeamScore(SideStoreModel):
def live_team(self, match):
if self.team_registration:
id = self.team_registration.id
image = self.team_registration.logo
weight = self.team_registration.weight
is_winner = self.team_registration.id == match.winning_team_id
else:
id = None
image = None
weight= None
is_winner = False
names = self.shortened_team_names()
scores = self.scores_array()
scores = self.parsed_scores()
walk_out = self.walk_out
from .match import Team # Import Team only when needed
team = Team(image, names, scores, weight, is_winner, walk_out)
team = Team(id, image, names, scores, weight, is_winner, walk_out)
return team

@ -5,12 +5,13 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
from tournaments.models import group_stage
from . import BaseModel, Event, TournamentPayment, FederalMatchCategory, FederalCategory, FederalLevelCategory, FederalAgeCategory
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 tournaments.utils.player_search import get_player_name_from_csv
from shared.cryptography import encryption_util
from ..utils.extensions import plural_format
@ -20,7 +21,7 @@ class TeamSortingType(models.IntegerChoices):
class Tournament(BaseModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)
event = models.ForeignKey(Event, blank=True, null=True, on_delete=models.SET_NULL, related_name="tournaments")
event = models.ForeignKey(Event, blank=True, null=True, on_delete=models.SET_NULL, related_name='tournaments')
name = models.CharField(max_length=200, null=True, blank=True)
start_date = models.DateTimeField()
end_date = models.DateTimeField(null=True, blank=True)
@ -65,6 +66,15 @@ class Tournament(BaseModel):
loser_bracket_mode = models.IntegerField(default=0)
initial_seed_round = models.IntegerField(default=0)
initial_seed_count = models.IntegerField(default=0)
enable_online_registration = models.BooleanField(default=False) # Equivalent to Bool = false
registration_date_limit = models.DateTimeField(null=True, blank=True) # Equivalent to Date? = nil
opening_registration_date = models.DateTimeField(null=True, blank=True) # Equivalent to Date? = nil
waiting_list_limit = models.IntegerField(null=True, blank=True) # Equivalent to Int? = nil
account_is_required = models.BooleanField(default=True)
license_is_required = models.BooleanField(default=True)
minimum_player_per_team = models.IntegerField(default=2)
maximum_player_per_team = models.IntegerField(default=2)
information = models.CharField(max_length=4000, null=True, blank=True)
def delete_dependencies(self):
for team_registration in self.team_registrations.all():
@ -183,7 +193,7 @@ class Tournament(BaseModel):
if self.event and self.event.creator:
return self.event.creator.username
else:
return "--"
return None
def private_label(self):
if self.is_private:
@ -207,7 +217,9 @@ class Tournament(BaseModel):
def registration_count_display(self):
teams = self.teams(True)
if teams is not None and len(teams) > 0:
if teams is not None and len(teams) == 1:
return f"{len(teams)} équipe inscrite"
elif teams is not None and len(teams) > 1:
return f"{len(teams)} équipes inscrites"
else:
return None
@ -226,12 +238,21 @@ class Tournament(BaseModel):
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"
return f"{len(teams)} {word}"
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):
@ -256,7 +277,7 @@ class Tournament(BaseModel):
names = team.names
stage = team.stage
weight = team.weight
summon = TeamSummon(names, team.date, weight, stage, "", team.image, self.day_duration)
summon = TeamSummon(team.team_registration.id, names, team.date, weight, stage, "", team.image, self.day_duration)
summons.append(summon)
else:
print('>>> team_summons')
@ -267,7 +288,7 @@ class Tournament(BaseModel):
names = team_registration.team_names()
stage = next_match.summon_stage_name()
weight = team_registration.weight
summon = TeamSummon(names, next_match.local_start_date(), weight, stage, next_match.court_name(next_match.court_index), team_registration.logo, self.day_duration)
summon = TeamSummon(team_registration.id, names, next_match.local_start_date(), weight, stage, next_match.court_name(next_match.court_index), team_registration.logo, self.day_duration)
summons.append(summon)
summons.sort(key=lambda s: (s.date is None, s.date or datetime.min))
@ -288,12 +309,34 @@ class Tournament(BaseModel):
names = team_registration.team_names()
ranking = team_registration.final_ranking
points = team_registration.points_earned
team = TeamRanking(names, ranking, points, team_registration.logo)
team = TeamRanking(team_registration.id, names, ranking, points, team_registration.logo)
rankings.append(team)
rankings.sort(key=lambda r: r.ranking)
return rankings
def get_team_waiting_list_position(self, team_registration):
# Use the teams method to get sorted list of teams
all_teams = self.teams(True)
index = -1
# Find position of team in all teams list
for i, team in enumerate(all_teams):
if team.team_registration.id == team_registration.id:
index = i
# Check if team_count exists
if self.team_count:
# Team is not in list
if index < self.team_count:
print("Team is not in list", index, self.team_count)
return -1
# Return position in waiting list relative to target count
print("Return position in waiting list relative to target count", index, self.team_count)
return index - self.team_count
else:
print("else", index, self.team_count)
return -1
def teams(self, includeWaitingList):
# print("Starting teams method")
bracket_teams = []
@ -316,8 +359,7 @@ class Tournament(BaseModel):
if team_registration.registration_date is None:
is_valid = True
# print(f"Is valid: {is_valid}")
if team_registration.walk_out is False:
if team_registration.out_of_tournament() is False:
team = TeamItem(team_registration)
# print(f"Created team: {team}")
if team_registration.group_stage_position is not None:
@ -358,7 +400,7 @@ class Tournament(BaseModel):
# print(f"Group stage members count: {group_stage_members_count}")
if self.team_sorting == TeamSortingType.INSCRIPTION_DATE:
complete_teams.sort(key=lambda s: (s.date is None, s.date or datetime.min, s.initial_weight, s.team_registration.id))
complete_teams.sort(key=lambda s: (s.registration_date is None, s.registration_date or datetime.min, s.initial_weight, s.team_registration.id))
else:
complete_teams.sort(key=lambda s: (s.initial_weight, s.team_registration.id))
@ -387,7 +429,7 @@ class Tournament(BaseModel):
if waiting_list_count > 0:
waiting_teams = waiting_teams + complete_teams[-waiting_list_count:]
if self.team_sorting == TeamSortingType.INSCRIPTION_DATE:
waiting_teams.sort(key=lambda s: (s.date is None, s.date or datetime.min, s.initial_weight, s.team_registration.id))
waiting_teams.sort(key=lambda s: (s.registration_date is None, s.registration_date or datetime.min, s.initial_weight, s.team_registration.id))
else:
waiting_teams.sort(key=lambda s: (s.initial_weight, s.team_registration.id))
else:
@ -442,11 +484,11 @@ class Tournament(BaseModel):
groups.extend(self.round_match_groups(round, broadcasted, hide_empty_matches=True))
if self.display_group_stages():
for round in self.group_stages.all().order_by('index'):
for round in self.rounds.all().order_by('index'):
groups.extend(self.round_match_groups(round, broadcasted, hide_empty_matches=True))
ordered = sorted(self.get_computed_group_stage(), key=lambda s: (-s.step, s.index))
for group_stage in ordered:
group_stages = sorted(self.sorted_group_stages(), key=lambda s: (-s.step, s.index))
for group_stage in group_stages:
group = self.group_stage_match_group(group_stage, broadcasted, hide_empty_matches=True)
if group:
groups.append(group)
@ -454,6 +496,9 @@ class Tournament(BaseModel):
return groups
def group_stage_match_group(self, group_stage, broadcasted, hide_empty_matches):
if group_stage is None:
return None
matches = group_stage.matches.all()
if hide_empty_matches:
matches = [m for m in matches if m.should_appear()]
@ -476,7 +521,7 @@ class Tournament(BaseModel):
else:
matches = [m for m in matches if m.disabled is False]
if matches:
if round and matches:
matches.sort(key=lambda m: m.index)
group = self.create_match_group(round.name(), matches)
groups.append(group)
@ -492,7 +537,7 @@ class Tournament(BaseModel):
key=lambda m: (
m.round.index, # Sort by Round index first
m.round.get_depth(),
m.name, # Then by Round depth
m.name or '', # Then by Round depth, using empty string if name is None
)
)
group = self.create_match_group('Matchs de classement', ranking_matches)
@ -520,10 +565,10 @@ class Tournament(BaseModel):
return MatchGroup(name, live_matches, formatted_schedule)
def live_group_stages(self):
group_stages = self.get_computed_group_stage()
group_stages = self.sorted_group_stages()
return [gs.live_group_stages() for gs in group_stages]
def get_computed_group_stage(self):
def sorted_group_stages(self):
# Get all group stages and sort by step (descending) and index (ascending)
group_stages = self.group_stages.all().order_by('-step', 'index')
@ -549,7 +594,7 @@ class Tournament(BaseModel):
return [gs.live_group_stages() for gs in previous_step_group_stages]
def last_group_stage_step(self):
live_group_stages = self.get_computed_group_stage()
live_group_stages = self.sorted_group_stages()
# Filter to find the last running step
last_running_step = max(gs.step for gs in live_group_stages) if live_group_stages else None
@ -696,7 +741,7 @@ class Tournament(BaseModel):
def round_to_show(self):
# print('===== round_to_show')
last_started_match = self.first_unfinished_match()
if last_started_match:
if last_started_match and last_started_match.round:
# print(f'last_started_match = {last_started_match.name}')
current_round = last_started_match.round.root_round()
# print(f'round_to_show > current_round: {current_round.name()}')
@ -705,17 +750,24 @@ class Tournament(BaseModel):
# all started matches have ended, possibly
last_finished_match = self.last_finished_match()
# print(f'last_finished_match = {last_finished_match.name}')
round_root_index = last_finished_match.round.root_round().index
# print(f'round_index = {round_root_index}')
if round_root_index == 0:
return last_finished_match.round
else:
round = self.rounds.filter(parent=None,index=round_root_index-1).first()
if round:
round = last_finished_match.round
if round is None: # when the last finished match is in the group stage
round = self.rounds.filter(parent__isnull=True).order_by('-index').first()
if round:
# print(f'last_finished_match = {last_finished_match.name}')
round_root_index = round.root_round().index
# print(f'round_index = {round_root_index}')
if round_root_index == 0:
return round
else:
return None
round = self.round_set.filter(parent=None,index=round_root_index-1).first()
if round:
return round
else:
return None
else:
return None
def last_started_match(self):
matches = [m for m in self.all_matches(False) if m.start_date]
@ -762,7 +814,9 @@ class Tournament(BaseModel):
return group_stages
def display_rankings(self):
if self.publish_rankings is True and self.end_date is not None:
if self.supposedly_in_progress():
return True
if self.end_date is not None:
return True
return False
@ -774,10 +828,10 @@ class Tournament(BaseModel):
if self.end_date is not None:
return is_build_and_not_empty
if datetime.now().date() >= self.start_date.date():
if timezone.now() >= timezone.localtime(self.start_date):
return is_build_and_not_empty
minimum_publish_date = self.creation_date.replace(hour=9, minute=0) + timedelta(days=1)
return timezone.now() >= minimum_publish_date
return timezone.now() >= timezone.localtime(minimum_publish_date)
def display_teams(self):
if self.end_date is not None:
@ -801,10 +855,10 @@ class Tournament(BaseModel):
return False
def display_group_stages(self):
if self.end_date is not None:
return True
if len(self.group_stages.all()) == 0:
return False
if self.end_date is not None:
return True
if self.publish_group_stages:
return True
@ -909,6 +963,325 @@ class Tournament(BaseModel):
return False
return True
def options_online_registration(self):
options = []
# Date d'ouverture
if self.opening_registration_date:
date = formats.date_format(timezone.localtime(self.opening_registration_date), format='j F Y H:i')
options.append(f"Ouverture des inscriptions le {date}")
# Date limite
if self.registration_date_limit:
date = formats.date_format(timezone.localtime(self.registration_date_limit), format='j F Y H:i')
options.append(f"Clôture des inscriptions le {date}")
# Cible d'équipes
if self.team_count:
options.append(f"Maximum {self.team_count} équipes")
# Liste d'attente
if self.waiting_list_limit:
options.append(f"Liste d'attente limitée à {self.waiting_list_limit} équipes")
# Options d'inscription
if self.account_is_required:
options.append("Compte requis")
if self.license_is_required:
options.append("Licence requise")
# Joueurs par équipe
min_players = self.minimum_player_per_team
max_players = self.maximum_player_per_team
if min_players == max_players:
options.append(f"{min_players} joueurs par équipe")
else:
options.append(f"Entre {min_players} et {max_players} joueurs par équipe")
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 = len([tr for tr in self.team_registrations.all() if tr.out_of_tournament() is False])
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_online_registration_status(self):
if self.supposedly_in_progress():
return OnlineRegistrationStatus.ENDED
if self.closed_registration_date is not None:
return OnlineRegistrationStatus.ENDED
if self.end_date is not None:
return OnlineRegistrationStatus.ENDED_WITH_RESULTS
now = timezone.now()
if self.opening_registration_date is not None:
timezoned_datetime = timezone.localtime(self.opening_registration_date)
if now < timezoned_datetime:
return OnlineRegistrationStatus.NOT_STARTED
if self.registration_date_limit is not None:
timezoned_datetime = timezone.localtime(self.registration_date_limit)
if now > timezoned_datetime:
return OnlineRegistrationStatus.ENDED
if self.team_count is not None:
# Get all team registrations excluding walk_outs
current_team_count = len([tr for tr in self.team_registrations.all() if tr.out_of_tournament() is False])
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 is_unregistration_possible(self):
# Check if tournament has started
if self.supposedly_in_progress():
return False
if self.closed_registration_date is not None:
return False
# Check if tournament is finished
if self.end_date is not None:
return False
# Check if registration is closed
if self.registration_date_limit is not None:
if timezone.now() > timezone.localtime(self.registration_date_limit):
return False
# Otherwise unregistration is allowed
return True
def get_waiting_list_position(self):
# If no target team count exists, no one goes to waiting list
if self.team_count is None:
return -1
# Get count of active teams (not walked out)
current_team_count = len([tr for tr in self.team_registrations.all() if tr.out_of_tournament() is False])
# If current count is less than target count, next team is not in waiting list
if current_team_count < self.team_count:
return -1
# If we have a waiting list limit
if self.waiting_list_limit is not None:
waiting_list_count = current_team_count - self.team_count
# If waiting list is full
if waiting_list_count >= self.waiting_list_limit:
return -1
# Return waiting list position
return waiting_list_count
# In waiting list with no limit
return current_team_count - self.team_count
def build_tournament_type_array(self):
tournament_details = []
if self.federal_level_category > 0:
tournament_details.append(self.level())
if self.category():
tournament_details.append(self.category())
if self.age() and self.federal_age_category != FederalAgeCategory.SENIOR:
tournament_details.append(self.age())
return tournament_details
def build_tournament_type_str(self):
tournament_details = self.build_tournament_type_array()
return " ".join(filter(None, tournament_details))
def build_tournament_details_str(self):
tournament_details = self.build_tournament_type_array()
name_str = self.build_name_details_str()
if len(name_str) > 0:
tournament_details.append(name_str)
return " ".join(filter(None, tournament_details))
def build_name_details_str(self):
name_details = []
if self.name:
name_details.append(self.name)
if self.event.name:
name_details.append(self.event.name)
name_str = " - ".join(filter(None, name_details))
if name_str:
name_str = f"{name_str}"
return name_str
def player_register_check(self, licence_id):
reasons = []
if not licence_id:
return None
data, found = get_player_name_from_csv(self.federal_category, licence_id)
if not found or not data:
print("not found or not data")
return None
birth_year = data.get('birth_year', None)
is_woman = data.get('is_woman', None)
# Check gender category restrictions
if is_woman is not None and self.federal_category == FederalCategory.WOMEN and is_woman is False:
reasons.append("Ce tournoi est réservé aux femmes")
if birth_year is None:
return reasons if reasons else None
current_year = timezone.now().year
if timezone.now().month >= 9: # Check if current month is September or later
current_year += 1
user_age = current_year - int(birth_year)
print(user_age)
# Check age category restrictions
if self.federal_age_category == FederalAgeCategory.A11_12 and user_age > 12:
reasons.append("Ce tournoi est réservé aux -12 ans")
if self.federal_age_category == FederalAgeCategory.A13_14 and user_age > 14:
reasons.append("Ce tournoi est réservé aux -14 ans")
if self.federal_age_category == FederalAgeCategory.A15_16 and user_age > 16:
reasons.append("Ce tournoi est réservé aux -16 ans")
if self.federal_age_category == FederalAgeCategory.A17_18 and user_age > 18:
reasons.append("Ce tournoi est réservé aux -18 ans")
if self.federal_age_category == FederalAgeCategory.SENIOR and user_age < 11:
reasons.append("Ce tournoi est réservé aux 11 ans et plus")
if self.federal_age_category == FederalAgeCategory.A45 and user_age < 45:
reasons.append("Ce tournoi est réservé aux +45 ans")
if self.federal_age_category == FederalAgeCategory.A55 and user_age < 55:
reasons.append("Ce tournoi est réservé aux +55 ans")
addon = 0
computedRank = int(data.get("rank", 0))
if is_woman and self.federal_category == FederalCategory.MEN:
addon = FederalCategory.female_in_male_assimilation_addition(computedRank)
computedRank = computedRank + addon
if computedRank <= self.min_player_rank():
name = data['first_name'] + " " + data['last_name'].upper()
reasons.append(f"{name} ({licence_id}): trop bien classé pour ce tournoi")
return reasons if reasons else None
def min_player_rank(self):
return FederalLevelCategory.min_player_rank(self.federal_level_category, self.federal_category, self.federal_age_category)
def first_waiting_list_team(self):
teams = self.teams(True)
if len(teams)<=self.team_count:
return None
waiting_teams = [team for team in teams if team.stage == "Attente"]
if waiting_teams:
return waiting_teams[0].team_registration
return None
def broadcasted_prog(self):
# Get matches from broadcasted_matches_and_group_stages
matches, _ = self.broadcasted_matches_and_group_stages()
if not matches:
return []
# Get all unfinished matches for courts
active_matches = [
m for m in matches
if m.end_date is None # Not finished
and m.court_index is not None
]
# Group matches by court
matches_by_court = {}
courts = set()
for match in active_matches:
if match.court_index not in matches_by_court:
matches_by_court[match.court_index] = []
courts.add(match.court_index)
matches_by_court[match.court_index].append(match)
# Sort matches within each court by start time
for court in matches_by_court:
matches_by_court[court].sort(key=lambda m: (
m.start_date is None, # None dates come last
m.start_date if m.start_date else timezone.now()
))
# Sort courts and organize them into groups of 4
sorted_courts = sorted(list(courts))
court_groups = [sorted_courts[i:i+4] for i in range(0, len(sorted_courts), 4)]
ordered_matches = []
# For each group of up to 4 courts
for court_group in court_groups:
# First row: earliest match for each court
for court in court_group:
if court in matches_by_court and matches_by_court[court]:
ordered_matches.append(matches_by_court[court][0])
else:
ordered_matches.append({"empty": True})
# Pad to 4 courts if needed
while len(ordered_matches) % 4 != 0:
ordered_matches.append({"empty": True})
# Second row: next match for each court
for court in court_group:
if court in matches_by_court and len(matches_by_court[court]) > 1:
ordered_matches.append(matches_by_court[court][1])
else:
ordered_matches.append({"empty": True})
# Pad to 4 courts if needed
while len(ordered_matches) % 4 != 0:
ordered_matches.append({"empty": True})
# Add unassigned matches at the end if needed
unassigned_matches = [
m for m in matches
if m.end_date is None and m.court_index is None
]
if unassigned_matches:
ordered_matches.extend(unassigned_matches)
return ordered_matches
class MatchGroup:
def __init__(self, name, matches, formatted_schedule):
self.name = name
@ -922,7 +1295,8 @@ class MatchGroup:
self.matches = matches
class TeamSummon:
def __init__(self, names, date, weight, stage, court, image, day_duration):
def __init__(self, id, names, date, weight, stage, court, image, day_duration):
self.id = str(id)
self.names = names
self.date = date
self.weight = weight
@ -954,6 +1328,7 @@ class TeamItem:
def __init__(self, team_registration):
self.names = team_registration.team_names()
self.date = team_registration.local_call_date()
self.registration_date = team_registration.registration_date
self.weight = team_registration.weight
self.initial_weight = team_registration.initial_weight()
self.image = team_registration.logo
@ -969,6 +1344,7 @@ class TeamItem:
return {
"names": self.names,
"date": self.date,
"registration_date": self.registration_date,
"weight": self.weight,
"initial_weight": self.initial_weight,
"image": self.image,
@ -978,7 +1354,8 @@ class TeamItem:
}
class TeamRanking:
def __init__(self, names, ranking, points, image):
def __init__(self, id, names, ranking, points, image):
self.id = str(id)
self.names = names
self.ranking = ranking
self.formatted_ranking = self.ordinal(ranking)
@ -1000,7 +1377,6 @@ class TeamRanking:
return ''
def ordinal(self, n):
suffixes = {1: 'er', 2: 'ème', 3: 'rd'}
if n == 1:
suffix = 'er'
else:

@ -0,0 +1,25 @@
from django.db import models
from . import UnregisteredTeam
import uuid
class UnregisteredPlayer(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)
unregistered_team = models.ForeignKey(UnregisteredTeam, on_delete=models.SET_NULL, related_name='unregistered_players', null=True, blank=True)
first_name = models.CharField(max_length=50, blank=True)
last_name = models.CharField(max_length=50, blank=True)
licence_id = models.CharField(max_length=50, null=True, blank=True)
def __str__(self):
return self.name()
def name(self):
return f"{self.first_name} {self.last_name}"
def shortened_name(self):
name = self.name()
if len(name) > 20 and self.first_name:
name = f"{self.first_name[0]}. {self.last_name}"
if len(name) > 20:
name_parts = self.last_name.split(" ")
name = f"{self.first_name[0]}. {name_parts[0]}"
return name

@ -0,0 +1,39 @@
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)
tournament = models.ForeignKey(Tournament, on_delete=models.SET_NULL, related_name='unregistered_teams', null=True, blank=True)
unregistration_date = models.DateTimeField(null=True, blank=True)
def __str__(self):
return self.player_names()
def player_names_as_list(self):
return [pr.name() for pr in self.unregistered_players.all()]
def team_names(self):
return self.player_names_as_list()
def shortened_team_names(self):
players = list(self.unregistered_players.all())
if len(players) == 1:
return [players[0].shortened_name(), '']
else:
return [pr.shortened_name() for pr in players]
@property
def players(self):
# Fetch related PlayerRegistration objects
return self.unregistered_players.all().order_by('last_name')
def player_names(self):
names = self.player_names_as_list()
str = " - ".join(names)
if len(str) > 0:
return str
else:
return "no players"

@ -0,0 +1,79 @@
from django.utils import timezone
from .models import TeamRegistration, PlayerRegistration
from .models.player_enums import PlayerSexType, PlayerDataSource
from .models.enums import FederalCategory
from tournaments.utils.licence_validator import LicenseValidator
class TournamentRegistrationRepository:
@staticmethod
def create_team_registration(tournament, registration_date):
team_registration = TeamRegistration.objects.create(
tournament=tournament,
registration_date=registration_date
)
return team_registration
@staticmethod
def create_player_registrations(request, team_registration, players_data, team_form_data):
stripped_license = None
if request.user.is_authenticated and request.user.licence_id:
stripped_license = LicenseValidator(request.user.licence_id).stripped_license
for player_data in players_data:
is_captain = False
player_licence_id = player_data['licence_id']
if player_licence_id and stripped_license:
if player_licence_id.startswith(stripped_license):
is_captain = True
sex, rank, computed_rank = TournamentRegistrationRepository._compute_rank_and_sex(
team_registration.tournament,
player_data
)
data_source = None
if player_data.get('found_in_french_federation', False) == True:
data_source = PlayerDataSource.FRENCH_FEDERATION
player_registration = PlayerRegistration.objects.create(
team_registration=team_registration,
captain=is_captain,
source=data_source,
registered_online=True,
first_name=player_data.get('first_name'),
last_name=player_data.get('last_name'),
points=player_data.get('points'),
assimilation=player_data.get('assimilation'),
tournament_played=player_data.get('tournament_count'),
ligue_name=player_data.get('ligue_name'),
club_name=player_data.get('club_name'),
birthdate=player_data.get('birth_year'),
sex=sex,
rank=rank,
computed_rank=computed_rank,
licence_id=player_data['licence_id'],
)
if is_captain is True:
player_registration.email=team_form_data['email']
player_registration.phone_number=team_form_data['mobile_number']
player_registration.save()
team_registration.set_weight()
team_registration.save()
@staticmethod
def _compute_rank_and_sex(tournament, player_data):
is_woman = player_data.get('is_woman', False)
rank = player_data.get('rank', 0)
computed_rank = rank
sex = PlayerSexType.MALE
if is_woman:
sex = PlayerSexType.FEMALE
if tournament.federal_category == FederalCategory.MEN:
computed_rank = str(int(computed_rank) +
FederalCategory.female_in_male_assimilation_addition(int(rank)))
return sex, rank, computed_rank

@ -0,0 +1,259 @@
from django.core.mail import EmailMessage
from django.utils import timezone
from django.urls import reverse
class TournamentEmailService:
@staticmethod
def _convert_newlines_to_html(text):
html_content = text.replace('\n', '<br>')
return f"""
<html>
<body>
{html_content}
</body>
</html>
"""
@staticmethod
def email_subject(tournament, topic):
base_subject = f"[{tournament.build_tournament_type_str()}] [{tournament.formatted_start_date()}] " + topic
return base_subject
@staticmethod
def send_registration_confirmation(request, tournament, team_registration, waiting_list_position):
tournament_details_str = tournament.build_tournament_details_str()
email_subject = TournamentEmailService._build_email_subject(
tournament,
tournament_details_str,
waiting_list_position
)
email_body = TournamentEmailService._build_email_body(
request,
tournament,
team_registration,
tournament_details_str,
waiting_list_position
)
email = EmailMessage(
subject=email_subject,
body=TournamentEmailService._convert_newlines_to_html(email_body),
to=[request.user.email]
)
email.content_subtype = "html"
email.send()
@staticmethod
def _build_email_subject(tournament, tournament_details_str, waiting_list_position):
if waiting_list_position >= 0:
base_subject = "En liste d'attente du tournoi"
else:
base_subject = "Confirmation d'inscription au tournoi"
return TournamentEmailService.email_subject(tournament, base_subject)
@staticmethod
def _build_email_body(request, tournament, team_registration, tournament_details_str, waiting_list_position):
inscription_date = team_registration.local_registration_date().strftime("%d/%m/%Y à %H:%M")
team_members = [player.name() for player in team_registration.player_registrations.all()]
team_members_str = " et ".join(team_members)
body_parts = []
body_parts.append("Bonjour,\n")
if waiting_list_position >= 0:
body_parts.append(f"Votre inscription en liste d'attente du tournoi {tournament_details_str} est confirmée.")
else:
body_parts.append(f"Votre inscription au tournoi {tournament_details_str} est confirmée.")
absolute_url = f"{request.build_absolute_uri(f'/tournament/{tournament.id}/')}"
link_text = "informations sur le tournoi"
absolute_url = f'<a href="{absolute_url}">{link_text}</a>'
body_parts.extend([
f"\nDate d'inscription: {inscription_date}",
f"\nÉquipe inscrite: {team_members_str}",
f"\nLe tournoi commencera le {tournament.formatted_start_date()} au club {tournament.event.club.name}",
f"\nVoir les {absolute_url}",
"\nPour toute question, veuillez contacter votre juge-arbitre. Si vous n'êtes pas à l'origine de cette inscription, merci de le contacter rapidement.",
f"\n{tournament.event.creator.full_name()}\n{tournament.event.creator.email}",
"\nCeci est un e-mail automatique, veuillez ne pas y répondre.",
"\nCordialement,\n\nPadel Club"
])
return "\n".join(body_parts)
@staticmethod
def send_unregistration_confirmation(captain, tournament, other_player):
tournament_details_str = tournament.build_tournament_details_str()
email_subject = TournamentEmailService.email_subject(tournament, "Désistement du tournoi")
email_body = TournamentEmailService._build_unregistration_email_body(
tournament,
captain,
tournament_details_str,
other_player
)
email = EmailMessage(
subject=email_subject,
body=TournamentEmailService._convert_newlines_to_html(email_body),
to=[captain.email]
)
email.content_subtype = "html"
email.send()
if other_player.email is not None:
email_body = TournamentEmailService._build_unregistration_email_body(
tournament,
other_player,
tournament_details_str,
captain
)
email = EmailMessage(
subject=email_subject,
body=TournamentEmailService._convert_newlines_to_html(email_body),
to=[other_player.email]
)
email.content_subtype = "html"
email.send()
@staticmethod
def send_out_of_waiting_list_confirmation(captain, tournament, other_player):
tournament_details_str = tournament.build_tournament_details_str()
email_subject = TournamentEmailService.email_subject(tournament, "Participation au tournoi")
email_body = TournamentEmailService._build_out_of_waiting_list_email_body(
tournament,
captain,
tournament_details_str,
other_player
)
email = EmailMessage(
subject=email_subject,
body=TournamentEmailService._convert_newlines_to_html(email_body),
to=[captain.email]
)
email.content_subtype = "html"
email.send()
if other_player.email is not None:
email_body = TournamentEmailService._build_out_of_waiting_list_email_body(
tournament,
other_player,
tournament_details_str,
captain
)
email = EmailMessage(
subject=email_subject,
body=TournamentEmailService._convert_newlines_to_html(email_body),
to=[other_player.email]
)
email.content_subtype = "html"
email.send()
@staticmethod
def _build_unregistration_email_body(tournament, captain, tournament_details_str, other_player):
body_parts = [
"Bonjour,\n\n",
f"Votre inscription au tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name} a été annulée"
]
if other_player is not None:
body_parts.append(
f"\n\nVous étiez inscrit avec {other_player.name()}, n'oubliez pas de prévenir votre partenaire."
)
absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info"
link_text = "informations sur le tournoi"
absolute_url = f'<a href="{absolute_url}">{link_text}</a>'
body_parts.append(
f"\n\nVoir les {absolute_url}",
)
body_parts.extend([
"\n\nPour toute question, veuillez contacter votre juge-arbitre. "
"Si vous n'êtes pas à l'origine de cette désinscription, merci de le contacter rapidement.",
f"\n{tournament.event.creator.full_name()}\n{tournament.event.creator.email}",
"\n\nCeci est un e-mail automatique, veuillez ne pas y répondre."
])
return "".join(body_parts)
@staticmethod
def _build_out_of_waiting_list_email_body(tournament, captain, tournament_details_str, other_player):
body_parts = [
"Bonjour,\n\n",
f"Suite au désistement d'une paire, vous êtes maintenant inscrit au tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name}"
]
absolute_url = f"https://padelclub.app/tournament/{tournament.id}/info"
link_text = "accéder au tournoi"
absolute_url = f'<a href="{absolute_url}">{link_text}</a>'
if other_player is not None:
body_parts.append(
f"\nVoici le partenaire indiqué dans l'inscription : {other_player.name()}, n'oubliez pas de le prévenir."
)
body_parts.append(
"\n\nSi vous n'êtes plus disponible pour participer à ce tournoi, cliquez sur ce lien ou contactez rapidement le juge-arbitre."
f"\n{absolute_url}"
"\nPour vous désinscrire en ligne vous devez avoir un compte Padel Club."
)
body_parts.extend([
f"\n\n{tournament.event.creator.full_name()}\n{tournament.event.creator.email}",
"\n\nCeci est un e-mail automatique, veuillez ne pas y répondre."
])
return "".join(body_parts)
@staticmethod
def send_tournament_cancellation_notification(player, tournament, other_player):
tournament_details_str = tournament.build_tournament_details_str()
email_subject = TournamentEmailService.email_subject(tournament, "Annulation du tournoi")
email_body = TournamentEmailService._build_tournament_cancellation_email_body(
tournament,
player,
tournament_details_str,
other_player
)
email = EmailMessage(
subject=email_subject,
body=TournamentEmailService._convert_newlines_to_html(email_body),
to=[player.email]
)
email.content_subtype = "html"
email.send()
@staticmethod
def _build_tournament_cancellation_email_body(tournament, player, tournament_details_str, other_player):
body_parts = [
"Bonjour,\n\n",
f"Le tournoi {tournament_details_str}, prévu le {tournament.formatted_start_date()} au club {tournament.event.club.name} a été annulé par le juge-arbitre."
]
if other_player is not None:
body_parts.append(
f"\nVous étiez inscrit avec {other_player.name()}, n'oubliez pas de prévenir votre partenaire."
)
body_parts.extend([
"\n\nPour toute question, veuillez contacter votre juge-arbitre:",
f"\n{tournament.event.creator.full_name()}\n{tournament.event.creator.email}",
"\n\nCeci est un e-mail automatique, veuillez ne pas y répondre."
])
return "".join(body_parts)

@ -0,0 +1,311 @@
from django.utils import timezone
from ..forms import TournamentRegistrationForm, AddPlayerForm
from ..repositories import TournamentRegistrationRepository
from .email_service import TournamentEmailService
from django.contrib import messages
from ..utils.licence_validator import LicenseValidator
from ..utils.player_search import get_player_name_from_csv
from tournaments.models import PlayerRegistration
class TournamentRegistrationService:
def __init__(self, request, tournament):
self.request = request
self.tournament = tournament
self.context = {}
self.repository = TournamentRegistrationRepository()
self.email_service = TournamentEmailService()
def initialize_context(self):
self.context = {
'tournament': self.tournament,
'registration_successful': False,
'team_form': None,
'add_player_form': None,
'current_players': self.request.session.get('team_registration', []),
}
return self.context
def handle_post_request(self):
self.context['team_form'] = TournamentRegistrationForm(self.request.POST)
self.context['add_player_form'] = AddPlayerForm(self.request.POST)
if 'add_player' in self.request.POST:
self.handle_add_player()
elif 'register_team' in self.request.POST:
self.handle_team_registration()
def handle_add_player(self):
if not self.context['add_player_form'].is_valid():
return
player_data = self.context['add_player_form'].cleaned_data
licence_id = player_data.get('licence_id', '').upper()
# Validate license
if not self._validate_license(licence_id):
return
# Check for duplicate players
if self._is_duplicate_player(licence_id):
return
# Check if player is already registered in tournament
if self._is_already_registered(licence_id):
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)
else:
# Handle player data
if self.context['add_player_form'].names_is_valid():
self._handle_valid_names(player_data)
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
if self.request.user.is_authenticated:
cleaned_data = self.context['team_form'].cleaned_data
mobile_number = cleaned_data.get('mobile_number')
self.request.user.phone = mobile_number
self.request.user.save()
waiting_list_position = self.tournament.get_waiting_list_position()
team_registration = self.repository.create_team_registration(
self.tournament,
timezone.now().replace(microsecond=0)
)
self.repository.create_player_registrations(
self.request,
team_registration,
self.request.session['team_registration'],
self.context['team_form'].cleaned_data
)
self.email_service.send_registration_confirmation(
self.request,
self.tournament,
team_registration,
waiting_list_position
)
self.clear_session_data()
self.context['registration_successful'] = True
def handle_get_request(self):
self.context['add_player_form'] = AddPlayerForm()
self.context['team_form'] = self.initialize_team_form()
self.initialize_session_data()
def add_player_to_session(self, player_data):
if not self.request.session.get('team_registration'):
self.request.session['team_registration'] = []
self.request.session['team_registration'].append(player_data)
self.context['current_players'] = self.request.session.get('team_registration', [])
self.context['add_player_form'].first_tournament = False
self.context['add_player_form'].user_without_licence = False
self.request.session.modified = True
def clear_session_data(self):
self.request.session['team_registration'] = []
self.request.session.modified = True
def initialize_team_form(self):
initial_data = {}
if self.request.user.is_authenticated:
initial_data = {
'email': self.request.user.email,
'mobile_number': self.request.user.phone,
}
return TournamentRegistrationForm(initial=initial_data)
def initialize_session_data(self):
print("initialize_session_data")
self.request.session['team_registration'] = []
if self.request.user.is_authenticated:
self._add_authenticated_user_to_session()
def _add_authenticated_user_to_session(self):
if not self.request.user.licence_id:
self._handle_user_without_license()
return
player_data = self._get_authenticated_user_data()
if player_data:
self.request.session['team_registration'].insert(0, player_data)
self.context['current_players'] = self.request.session.get('team_registration', [])
self.request.session.modified = True
def _handle_user_without_license(self):
player_data = {
'first_name': self.request.user.first_name,
'last_name': self.request.user.last_name.upper(),
}
self.context['add_player_form'] = AddPlayerForm(initial=player_data)
self.context['add_player_form'].user_without_licence = True
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)
player_data = {
'first_name': user.first_name,
'last_name': user.last_name.upper(),
'email': user.email,
'phone': user.phone,
'licence_id': validator.computed_licence_id
}
data, found = get_player_name_from_csv(self.tournament.federal_category, user.licence_id)
if found and data:
player_data.update({
'rank': data['rank'],
'points': data.get('points'),
'assimilation': data.get('assimilation'),
'tournament_count': data.get('tournament_count'),
'ligue_name': data.get('ligue_name'),
'club_name': data.get('club_name'),
'birth_year': data.get('birth_year'),
'found_in_french_federation': True,
})
return player_data
def _validate_license(self, licence_id):
validator = LicenseValidator(licence_id)
if validator.validate_license() is False and self.tournament.license_is_required:
if not licence_id:
message = ("Le numéro de licence est obligatoire."
if not self.request.session.get('team_registration', [])
else "Le numéro de licence de votre partenaire est obligatoire.")
messages.error(self.request, message)
else:
# 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.")
return False
return True
def _is_duplicate_player(self, licence_id):
existing_players = [player['licence_id'] for player in self.request.session.get('team_registration', [])]
if licence_id in existing_players:
messages.error(self.request, "Ce joueur est déjà dans l'équipe.")
return True
return False
def _is_already_registered(self, licence_id):
validator = LicenseValidator(licence_id)
if (validator.validate_license() and
self._license_already_registered(validator.stripped_license) and
self.tournament.license_is_required):
messages.error(self.request, "Un joueur avec ce numéro de licence est déjà inscrit dans une équipe.")
return True
return False
def _handle_valid_names(self, player_data):
if player_data.get('rank') is None:
self._set_default_rank(player_data)
self.add_player_to_session(player_data)
self.context['add_player_form'] = AddPlayerForm()
self.context['add_player_form'].first_tournament = False
def _handle_invalid_names(self, licence_id, player_data):
if not self.context['add_player_form'].first_tournament:
data, found = get_player_name_from_csv(self.tournament.federal_category, licence_id)
if found and data:
self._update_player_data_from_csv(player_data, data)
player_check = self._player_check(player_data)
if player_check == True:
self.add_player_to_session(player_data)
self.context['add_player_form'] = AddPlayerForm()
else:
return
else:
self._handle_first_tournament_case(data)
def _set_default_rank(self, player_data):
if self.request.session.get('last_rank') is None:
data, found = get_player_name_from_csv(self.tournament.federal_category, None)
if data:
self.request.session['last_rank'] = data['rank']
self.request.session['is_woman'] = data['is_woman']
self.request.session.modified = True
player_data['rank'] = self.request.session.get('last_rank', 0)
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
def _update_player_data_from_csv(self, player_data, csv_data):
player_data.update({
'first_name': csv_data['first_name'],
'last_name': csv_data['last_name'],
'rank': csv_data['rank'],
'is_woman': csv_data['is_woman'],
'points': csv_data.get('points'),
'assimilation': csv_data.get('assimilation'),
'tournament_count': csv_data.get('tournament_count'),
'ligue_name': csv_data.get('ligue_name'),
'club_name': csv_data.get('club_name'),
'birth_year': csv_data.get('birth_year'),
'found_in_french_federation': True,
'email': None,
'phone': None,
})
def _handle_first_tournament_case(self, data):
if data:
self.request.session['last_rank'] = data['rank']
self.request.session['is_woman'] = data['is_woman']
self.request.session.modified = True
self.context['add_player_form'].first_tournament = True
if not self.context['add_player_form'].names_is_valid():
message = ("Pour confirmer votre inscription votre prénom et votre nom sont obligatoires."
if not self.request.session.get('team_registration', [])
else "Pour rajouter un partenaire, son prénom et son nom sont obligatoires.")
messages.error(self.request, message)
def _player_check(self, player_data):
licence_id = player_data['licence_id'].upper()
validator = LicenseValidator(licence_id)
is_license_valid = validator.validate_license()
player_register_check = self.tournament.player_register_check(licence_id)
if is_license_valid and player_register_check is not None:
for message in player_register_check:
messages.error(self.request, message)
return False
return True
def _license_already_registered(self, stripped_license):
return PlayerRegistration.objects.filter(
team_registration__tournament=self.tournament,
licence_id__startswith=stripped_license
).exists()

@ -0,0 +1,72 @@
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
class TournamentUnregistrationService:
def __init__(self, request, tournament):
self.request = request
self.tournament = tournament
self.player_registration = None
self.other_player = None
def can_unregister(self):
if not self.tournament.is_unregistration_possible():
messages.error(self.request, "Le désistement n'est plus possible pour ce tournoi. Si vous souhaitez vous désinscrire, veuillez contacter le juge-arbitre.")
return False
if not self.request.user.licence_id:
messages.error(self.request,
"Vous ne pouvez pas vous désinscrire car vous n'avez pas de numéro de licence associé.")
return False
return True
def process_unregistration(self):
if not self._find_player_registration():
messages.error(self.request,
"La désincription a échouée. Veuillez contacter le juge-arbitre.")
return False
self._unregister_team()
self._delete_registered_team()
self._cleanup_session()
return True
def _unregister_team(self):
# Create unregistered team record
team_registration = self.player_registration.team_registration
unregistered_team = UnregisteredTeam.objects.create(
tournament=team_registration.tournament,
unregistration_date=timezone.now(),
)
for player in team_registration.player_registrations.all():
UnregisteredPlayer.objects.create(
unregistered_team=unregistered_team,
first_name=player.first_name,
last_name=player.last_name,
licence_id=player.licence_id,
)
def _find_player_registration(self):
self.player_registration = PlayerRegistration.objects.filter(
licence_id__startswith=self.request.user.licence_id,
team_registration__tournament_id=self.tournament.id,
).first()
if self.player_registration:
team_registration = self.player_registration.team_registration
self.other_player = team_registration.get_other_player(self.player_registration)
return True
return False
def _delete_registered_team(self):
team_registration = self.player_registration.team_registration
team_registration.delete()
def _cleanup_session(self):
self.request.session['team_registration'] = []
self.request.session.modified = True

@ -1,14 +1,14 @@
import random
import string
from django.db.models.signals import post_save
# from django.db.transaction import DatabaseError
from django.db.models.signals import post_save, pre_delete, post_delete
from django.dispatch import receiver
from django.conf import settings
# from django.apps import apps
# from django.db.models import Q
from django.utils import timezone
from .models import Club, FailedApiCall, Log
from .models import Club, Tournament, FailedApiCall, CustomUser, Log, TeamRegistration, PlayerRegistration, UnregisteredTeam, UnregisteredPlayer, PlayerDataSource
import requests
from tournaments.services.email_service import TournamentEmailService
# Others
@ -57,3 +57,85 @@ def send_discord_message(webhook_url, content):
raise ValueError(
f'Error sending message to Discord webhook: {response.status_code}, {response.text}'
)
@receiver(pre_delete, sender=TeamRegistration)
def unregister_team(sender, instance, **kwargs):
team_registration = instance
tournament = instance.tournament
if tournament.is_deleted is True:
return
# Create unregistered player records and track captain/other player
captain = None
other_player = None
for player in team_registration.player_registrations.all():
if player.captain is True:
captain = player
else:
other_player = player
# Send unregistration confirmation
if captain and captain.registered_online and captain.email:
TournamentEmailService.send_unregistration_confirmation(
captain,
tournament,
other_player
)
first_waiting_list_team = tournament.first_waiting_list_team()
print("first_waiting_list_team", first_waiting_list_team)
# Handle waiting list notifications
if first_waiting_list_team:
waiting_captain = None
waiting_other_player = None
for player in first_waiting_list_team.player_registrations.all():
if player.captain is True:
waiting_captain = player
else:
waiting_other_player = player
if waiting_captain and waiting_captain.registered_online and waiting_captain.email:
TournamentEmailService.send_out_of_waiting_list_confirmation(
waiting_captain,
tournament,
waiting_other_player
)
@receiver(post_save, sender=Tournament)
def notify_players_of_tournament_cancellation(sender, instance, **kwargs):
tournament = instance
if tournament.is_deleted is False:
return
# Get all team registrations
team_registrations = tournament.team_registrations.all()
for team_registration in team_registrations:
captain = None
other_player = None
# Get players who registered online and have email
for player in team_registration.player_registrations.all():
print(player, player.registered_online)
if player.captain:
captain = player
else:
other_player = player
# Send email to captain
if captain and captain.registered_online and captain.email:
TournamentEmailService.send_tournament_cancellation_notification(
captain,
tournament,
other_player
)
# Send email to other player if they exist and registered online
if other_player and other_player.registered_online and other_player.email:
TournamentEmailService.send_tournament_cancellation_notification(
other_player,
tournament,
captain
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -11341,7 +11341,7 @@
;250;CHAUMEIL;Méline;FRA;5731847;125;Oui;1;AUVERGNE RHONE-ALPES;50 69 0065;SAINT PRIEST (TENNIS CLUB DE);0;250;2004;
;250;SAPIN;Jeanne;FRA;8456751;100;Oui;1;NOUVELLE AQUITAINE;59 17 0609;LOIX TENNIS COUARDAIS;0;150;1998;
;250;BUSSON;Elodie;FRA;4134839;50;Oui;1;NORMANDIE;58 14 0253;MONDEVILLE USO;0;250;1994;
;250;;;FRA;5505555;;Oui;;ILE DE FRANCE;57 92 0048;NANTERRE (ES);0;250;2004;
;250;CLAUSSMANN;Emma;FRA;5505555;;Oui;;ILE DE FRANCE;57 92 0048;NANTERRE (ES);0;250;2004;
;274;MOUSTAHSINE;Sylvie;FRA;2688771;2594;Non;43;ILE DE FRANCE;57 92 0014;Association Sportive du Stade de la Marche;-10;228;1977;
;285;VILA;Emilie;FRA;6710080;2538;Non;23;OCCITANIE;60 66 0357;PADEL CLUB DU MAS;-12;167;1981;
;300;LEVENEZ;Armelle;FRA;7024816;1543;Oui;13;BRETAGNE;52 35 0073;RENNES GARDEN TENNIS CLUB;0;94;1968;
@ -11362,7 +11362,7 @@
;500;WIECZOREK;Nel;FRA;1134278;140;Oui;2;ILE DE FRANCE;57 78 0265;U.S. VESINET;0;300;2004;
;500;BANCE;Delphine;FRA;7650702;113;Oui;2;NORMANDIE;58 76 0126;ROUEN TENNIS CLUB;0;150;1985;
;500;PENINQUE;Pauline;FRA;9488382;113;Oui;1;ILE DE FRANCE;57 75 0234;TENNIS CLUB DU 16EME;0;300;1989;
;500;;;FRA;1683542;100;Oui;1;ILE DE FRANCE;57 93 0034;AS BONDY;0;500;1999;
;500;CAKAREVIC;Tania;FRA;1683542;100;Oui;1;ILE DE FRANCE;57 93 0034;AS BONDY;0;500;1999;
;500;VEDEL;Léa;FRA;2651073;83;Oui;2;NOUVELLE AQUITAINE;59 33 0087;GRADIGNAN TC;0;500;2000;
;500;MINC;Natacha;FRA;5033619;70;Oui;2;ILE DE FRANCE;57 L0 0920;CD HAUTS DE SEINE;0;500;1996;
;500;LANG COHEN;Victoria;FRA;3627583;63;Oui;1;OCCITANIE;60 34 0391;TCM JUVIGNAC;0;500;1996;
@ -11372,8 +11372,8 @@
;500;PIQUET;Noémie;FRA;5564375;15;Oui;1;PAYS DE LA LOIRE;61 85 0142;GARDEN TC ST JEAN DE MONTS;0;300;2005;
;500;MEGDICHE;Jihene;TUN;896156;5;Oui;1;OCCITANIE;60 81 0333;CASTRES TENNIS CLUB;0;500;2002;
;500;ANDRIEU;Margo;FRA;3883123;3;Oui;1;OCCITANIE;60 31 0304;BLAGNAC TENNIS CLUB;0;154;2006;
;500;;;FRA;6790394;;Oui;;PROVENCE ALPES COTE D'AZUR;62 83 0130;SAINT RAPHAEL CC;0;500;2006;
;500;;;UKR;2184371;;Oui;;BOURGOGNE FRANCHE COMTE;51 25 0356;TENNIS PADEL GRAND BESANCON;0;500;2005;
;500;DADOUN;Noémie;FRA;6790394;;Oui;;PROVENCE ALPES COTE D'AZUR;62 83 0130;SAINT RAPHAEL CC;0;500;2006;
;500;DIATLOVA;Daria;UKR;2184371;;Oui;;BOURGOGNE FRANCHE COMTE;51 25 0356;TENNIS PADEL GRAND BESANCON;0;500;2005;
;543;VOYER;Marine;FRA;9039791;1647;Non;13;NOUVELLE AQUITAINE;59 33 0117;CENON US;130;300;1990;
;558;LACOSTE;Isabelle;FRA;4795121;1609;Non;19;PROVENCE ALPES COTE D'AZUR;62 06 0305;TC CARROS;16;449;1964;
;561;BLARY;Séverine;FRA;3882462;1604;Non;17;ILE DE FRANCE;57 78 0218;T.C. DES LOGES ST GERMAIN;59;373;1974;
@ -11447,14 +11447,14 @@
;1500;CHAPUIS;Solène;FRA;5980710;264;Oui;4;ILE DE FRANCE;57 75 0525;SPORT OPTION TENNIS;0;1500;2002;
;1500;LAIRIE;Rose;FRA;1560004;264;Oui;3;NORMANDIE;58 14 0006;CAEN TC;0;1500;1994;
;1500;MALLET;Louise;FRA;5658448;251;Oui;5;CENTRE VAL DE LOIRE;53 28 0025;ES NOGENT LE ROI TENNIS;0;1500;2002;
;1500;;;FRA;9323441;170;Oui;3;ILE DE FRANCE;57 95 0511;SARCELLOIS TC - A.S.S;0;1500;2008;
;1500;AUDRY;Azelice;FRA;9323441;170;Oui;3;ILE DE FRANCE;57 95 0511;SARCELLOIS TC - A.S.S;0;1500;2008;
;1500;LE GUENIC;Manon;FRA;3329936;170;Oui;2;BRETAGNE;52 L0 0350;COMITE ILLE ET VILAINE TENNIS;0;1500;2000;
;1500;DUBOIS;Agathe;FRA;1855023;165;Oui;4;NORMANDIE;58 27 0032;EVREUX AC;0;1500;1993;
;1500;MATHIEU;Christelle;FRA;5798832;150;Oui;1;GRAND EST;55 55 0076;GRAND VERDUN TC;0;1500;1978;
;1500;VALY;Solène;FRA;8192250;148;Oui;3;ILE DE FRANCE;57 91 0013;U.S.O. ATHIS MONS;0;1500;2001;
;1500;BAUMES;Nathalie;FRA;3289685;143;Oui;5;AUVERGNE RHONE-ALPES;50 38 0085;TC ECHIROLLES;0;1500;1979;
;1500;MICHEL;Audrey;FRA;7139400;131;Oui;4;OCCITANIE;60 34 0713;TC DE LA VIERE;0;1500;1982;
;1500;;;FRA;6251016;125;Oui;1;NOUVELLE AQUITAINE;59 64 8002;CENTRAL SPORT CLUB;0;1500;1987;
;1500;CHALEARD;Marine;FRA;6251016;125;Oui;1;NOUVELLE AQUITAINE;59 64 8002;CENTRAL SPORT CLUB;0;1500;1987;
;1500;PERONI;Liloo;FRA;3757789;115;Oui;1;PAYS DE LA LOIRE;61 44 0288;TC DE CARQUEFOU;0;1500;2009;
;1500;MARIAT;Jeanne;FRA;9681825;111;Oui;3;AUVERGNE RHONE-ALPES;50 69 0326;FIDESIEN (TENNIS CLUB);0;1500;2000;
;1500;MOUSTAHSINE;Houda;FRA;7236386;100;Oui;1;ILE DE FRANCE;57 92 0014;Association Sportive du Stade de la Marche;0;500;1977;
@ -11471,7 +11471,7 @@
;1500;KLEIN;Amélie;FRA;6105598;25;Oui;2;GRAND EST;55 57 0259;FLEURY TC;0;1500;1981;
;1500;LOISEL;Stephanie;FRA;5699945;25;Oui;1;ILE DE FRANCE;57 92 0066;CLICHY (CS);0;1500;1974;
;1500;LORMEAU;Clara;FRA;1349086;25;Oui;1;OCCITANIE;60 34 8007;SUD PADEL Vailhauquès;0;500;1993;
;1500;;;FRA;645601;25;Oui;1;HAUTS DE FRANCE;56 80 0058;ALBERT TENNIS STADE;0;1500;1991;
;1500;RAMECKI;Céline;FRA;645601;25;Oui;1;HAUTS DE FRANCE;56 80 0058;ALBERT TENNIS STADE;0;1500;1991;
;1500;GODHBANE;Neirouz;FRA;9906235;23;Oui;1;ILE DE FRANCE;57 75 0507;LAGARDERE PARIS RACING;0;1500;2012;
;1500;STARES;Kellie;AUS;3311569;23;Oui;2;OCCITANIE;60 34 0724;TENNIS PADEL CAP D'AGDE;0;1500;1983;
;1500;GUYOMARC'H;Justine;FRA;8643017;22;Oui;2;CENTRE VAL DE LOIRE;53 37 0001;SKIN UP ACADEMY;0;1500;2003;
@ -11479,7 +11479,7 @@
;1500;SIMON;Zoé;FRA;7704626;15;Oui;1;CENTRE VAL DE LOIRE;53 37 0055;TENNIS CLUB JOCONDIEN;0;1500;1999;
;1500;VALETTE;Anne;FRA;5594051;13;Oui;1;OCCITANIE;60 34 0141;TC LA GRANDE MOTTE;0;1500;1964;
;1500;BENYAHIA;Nadia;FRA;2964341;10;Oui;1;CENTRE VAL DE LOIRE;53 28 0020;DREUX ATHLETIC CLUB;0;1500;2011;
;1500;;;FRA;8582732;10;Oui;1;NORMANDIE;58 76 0304;ROUEN UC AS;0;1500;2007;
;1500;LETELLIER;Juliette;FRA;8582732;10;Oui;1;NORMANDIE;58 76 0304;ROUEN UC AS;0;1500;2007;
;1500;POMADE;Carla;FRA;7534592;8;Oui;1;AUVERGNE RHONE-ALPES;50 69 0174;DARDILLY CHAMPAGNE (TENNIS CLUB);0;1500;2006;
;1500;BALDINO;Christine;FRA;4140948;6;Oui;2;AUVERGNE RHONE-ALPES;50 42 0161;UNIEUX (TENNIS CLUB);0;1500;1970;
;1500;BENMEZIANE;Mélissa;ALG;5979349;5;Oui;1;OCCITANIE;60 31 0803;STADE TOULOUSAIN TENNIS PADEL;0;1500;2011;
@ -11491,11 +11491,11 @@
;1500;HUMBERT;Marie Laure;FRA;5983828;1;Oui;1;AUVERGNE RHONE-ALPES;50 01 0511;JASSANS (TENNIS CLUB DE);0;1500;1975;
;1500;BILLON;Chloé;FRA;461358;;Oui;;ILE DE FRANCE;57 75 0507;LAGARDERE PARIS RACING;0;1500;2003;
;1500;BRINGAUD;Janice;FRA;3333496;;Oui;;AUVERGNE RHONE-ALPES;50 69 0040;DECINES (TENNIS CLUB DE);0;1500;1996;
;1500;;;FRA;350430;;Oui;;AUVERGNE RHONE-ALPES;50 01 0052;VALSERHONE TENNIS PADEL;0;1500;1983;
;1500;BRULLEBAUT;Solene;FRA;350430;;Oui;;AUVERGNE RHONE-ALPES;50 01 0052;VALSERHONE TENNIS PADEL;0;1500;1983;
;1500;CAUMETTE;Louisa;FRA;1174870;;Oui;;OCCITANIE;60 34 0036;TC BITERROIS;0;1500;1988;
;1500;COLAS DES FRANCS;Candice;FRA;2442215;;Oui;;AUVERGNE RHONE-ALPES;50 38 0344;PONT EVEQUE TC;0;1500;1990;
;1500;RAPOPORT;Margaux;FRA;1828930;;Oui;;PROVENCE ALPES COTE D'AZUR;62 13 0008;SMUC;0;1500;1994;
;1500;;;FRA;5892409;;Oui;;AUVERGNE RHONE-ALPES;50 69 0089;CALUIRE (TENNIS CLUB AS);0;1500;1981;
;1500;TRIQUIGNEAUX;Celine;FRA;5892409;;Oui;;AUVERGNE RHONE-ALPES;50 69 0089;CALUIRE (TENNIS CLUB AS);0;1500;1981;
;1506;RICHARD;Laura;FRA;5571011;578;Non;8;CENTRE VAL DE LOIRE;53 36 0314;CHATEAUROUX TENNIS CLUB;124;734;2005;
;1515;TRESALLET;Carole;FRA;2693611;574;Non;10;OCCITANIE;60 34 0270;TC BAILLARGUES;16;1515;1977;
;1536;GUERIN;Audrey;FRA;651271;564;Non;10;AUVERGNE RHONE-ALPES;50 63 0402;ACCESS PADEL CLUB;-85;1451;1979;
@ -11588,7 +11588,7 @@
;3000;WISNIEWSKI;Sophie;FRA;6985289;103;Oui;3;ILE DE FRANCE;57 94 0510;SPORT HORIZON;0;2938;1985;
;3000;BRONSSARD;Julie;FRA;416495;82;Oui;4;NOUVELLE AQUITAINE;59 64 0006;HENDAYE TC;0;3000;1977;
;3000;JAILLET-LAVERGNE;Marine;FRA;6245048;38;Oui;2;NOUVELLE AQUITAINE;59 33 8003;SQUASH BAD 33;0;1500;1980;
;3000;;;FRA;5805524;30;Oui;1;ILE DE FRANCE;57 93 0505;AS PADEL AFICIONADOS;0;3000;1980;
;3000;GIRET;Marion;FRA;5805524;30;Oui;1;ILE DE FRANCE;57 93 0505;AS PADEL AFICIONADOS;0;3000;1980;
;3000;PAILLERE;Sophie;FRA;5962840;15;Oui;1;ILE DE FRANCE;57 92 0046;SAINT CLOUD (UAS);0;3000;1956;
;3000;LENZINI;Marie Cécile;FRA;6164421;14;Oui;3;CORSE;54 2B 0732;AS C5 SPORTS;0;3000;1984;
;3000;LABORDE;Christel;FRA;5655937;2;Oui;1;NOUVELLE AQUITAINE;59 64 0001;AVIRON BAYONNAIS;0;3000;1970;
@ -11641,7 +11641,7 @@
;3730;LACOUR;Veronique;FRA;9468512;118;Non;5;NOUVELLE AQUITAINE;59 L0 0330;COMITE GIRONDE TENNIS;-730;720;1969;
;3752;CHAPUIS;Eléonore;FRA;3075784;117;Non;9;REUNION - MAYOTTE;67 97 0001;T.C. DIONYSIEN STE CLOTILDE;-144;3359;1979;
;3769;THEVENET;Sophia;FRA;277144;115;Non;3;AUVERGNE RHONE-ALPES;50 69 0185;ECULLY (TENNIS CLUB);-141;3377;1975;
;3792;;;FRA;3428662;114;Non;22;PROVENCE ALPES COTE D'AZUR;62 06 0107;US CAGNES TENNIS;205;3792;1985;
;3792;KREBS;cynthia;FRA;3428662;114;Non;22;PROVENCE ALPES COTE D'AZUR;62 06 0107;US CAGNES TENNIS;205;3792;1985;
;3819;AVERCHENKO;Kataryna;FRA;3385159;112;Non;7;PROVENCE ALPES COTE D'AZUR;62 13 8009;WIN WIN PADEL;94;3591;1994;
;3844;GRIVELET;Martine;FRA;3321690;110;Non;5;ILE DE FRANCE;57 94 0510;SPORT HORIZON;-131;3438;1972;
;3873;ESCOUFFIER;Julie;FRA;6251674;108;Non;2;BOURGOGNE FRANCHE COMTE;51 58 0032;VAUZELLES A.S.A.;-129;1062;2007;
@ -11705,10 +11705,10 @@
;4802;OUAHLIMA;Laurine;FRA;2824887;61;Non;5;HAUTS DE FRANCE;56 59 0037;TC DOUAI;711;4802;1992;
;4832;BAKEKOLO;Eléonore;FRA;4047680;60;Non;2;NOUVELLE AQUITAINE;59 79 0067;NIORT ECOLE DE TENNIS;-203;4629;1997;
;4832;CARACENA;Laura;FRA;3319775;60;Non;7;PROVENCE ALPES COTE D'AZUR;62 13 8009;WIN WIN PADEL;-203;4629;1999;
;4832;;;POR;3948328;60;Non;1;AUVERGNE RHONE-ALPES;50 69 0613;ALL IN COUNTRY CLUB DECINES;0;4832;2006;
;4832;COUCEIRO DA COSTA OLIVEIRA SA;Maria;POR;3948328;60;Non;1;AUVERGNE RHONE-ALPES;50 69 0613;ALL IN COUNTRY CLUB DECINES;0;4832;2006;
;4832;ERBEIA;Flavie;FRA;6344793;60;Non;4;MARTINIQUE;65 97 0047;A.S. COUNTRY CLUB DE SCHOELCHER;376;4382;1972;
;4832;GRANDEL;Alexandra;FRA;9202161;60;Non;2;PROVENCE ALPES COTE D'AZUR;62 83 0456;TENNIS PADEL ST CHARLES;-203;2830;1974;
;4832;;;FRA;3956034;60;Non;1;HAUTS DE FRANCE;56 59 8013;Padel Break Bondues;0;4832;1978;
;4832;MAILER;Sabrina;FRA;3956034;60;Non;1;HAUTS DE FRANCE;56 59 8013;Padel Break Bondues;0;4832;1978;
;4832;MOREIRA;Camille;FRA;3401686;60;Non;6;PROVENCE ALPES COTE D'AZUR;62 83 0115;TM OLLIOULAIS;1152;4832;2000;
;4832;PINAZZA;Ombretta;ITA;1499469;60;Non;4;AUVERGNE RHONE-ALPES;50 01 0086;GEX (TENNIS CLUB DE);2038;4832;1968;
;4832;ROMEDENNE;Annabel;BEL;3525268;60;Non;1;OCCITANIE;60 34 8009;MY CENTER LA GRANDE MOTTE;-203;4327;1972;
@ -11725,8 +11725,8 @@
;4965;OSMANI;Inès;FRA;3427529;55;Non;2;ILE DE FRANCE;57 93 0505;AS PADEL AFICIONADOS;1073;4965;1995;
;4965;ZIMMERMANN;Lisa;FRA;6187664;55;Non;2;GRAND EST;55 57 0178;MARLY TC;-194;4456;2005;
;5037;CULOSSE;Stéphanie;FRA;3226753;53;Non;3;PROVENCE ALPES COTE D'AZUR;62 13 8014;MONKEY PADEL;1001;5037;1974;
;5123;;;FRA;5681266;50;Non;1;BOURGOGNE FRANCHE COMTE;51 25 0008;ASCAP TENNIS MONTBELIARD;0;5123;1971;
;5123;;;FRA;3392524;50;Non;2;PROVENCE ALPES COTE D'AZUR;62 13 8017;LE COMPLEXE PADEL;0;5123;1986;
;5123;BONIFAY;Dominique;FRA;5681266;50;Non;1;BOURGOGNE FRANCHE COMTE;51 25 0008;ASCAP TENNIS MONTBELIARD;0;5123;1971;
;5123;DEL RIO;Aurore;FRA;3392524;50;Non;2;PROVENCE ALPES COTE D'AZUR;62 13 8017;LE COMPLEXE PADEL;0;5123;1986;
;5123;DUJARDIN;Pia;FRA;8298503;50;Non;5;HAUTS DE FRANCE;56 59 0179;FOS TENNIS VILLENEUVE D'ASCQ;-200;4549;2009;
;5123;FREBOEUF;Elodie;FRA;409724;50;Non;1;AUVERGNE RHONE-ALPES;50 26 0615;MIRABEL PIEGON TENNIS CLUB;-200;3930;1982;
;5123;GAYE-METOU;Dominique;FRA;3315858;50;Non;2;NOUVELLE AQUITAINE;59 64 0039;GUETHARY TC;-200;4139;1978;
@ -11749,13 +11749,13 @@
;5318;DURGUEIL;Cécile;FRA;1786251;46;Non;2;NOUVELLE AQUITAINE;59 47 0594;PUJOLAIS TC;-21;5099;1957;
;5342;CHOMETON;Lea;FRA;4341635;45;Non;2;AUVERGNE RHONE-ALPES;50 42 0161;UNIEUX (TENNIS CLUB);-199;3195;2001;
;5342;CLEMENT;Laura;FRA;1332296;45;Non;1;BOURGOGNE FRANCHE COMTE;51 21 0151;NORGES LES BOIS T.C.;-199;5075;1991;
;5342;;;FRA;2151070;45;Non;1;OCCITANIE;60 30 0415;TENNIS CLUB CARREAU DE LANES;0;5342;1999;
;5342;GARCIA;Laurie;FRA;2151070;45;Non;1;OCCITANIE;60 30 0415;TENNIS CLUB CARREAU DE LANES;0;5342;1999;
;5342;ROY;Marie;FRA;3303622;45;Non;7;NOUVELLE AQUITAINE;59 64 0002;BIARRITZ OLYMPIQUE;-747;2111;1959;
;5389;BALAGUER;Francoise;FRA;2977780;44;Non;5;OCCITANIE;60 L0 0660;Comité Départemental de Tennis des Pyrénées-Orient;-198;4656;1965;
;5409;CIRILLO;Aurore;FRA;8614477;43;Non;2;OCCITANIE;60 34 0270;TC BAILLARGUES;-201;4795;2006;
;5409;;;FRA;2780881;43;Non;1;HAUTS DE FRANCE;56 59 8012;PADEL FOOTBALL CLUB;0;5409;1991;
;5409;MILIA;Laura;FRA;2780881;43;Non;1;HAUTS DE FRANCE;56 59 8012;PADEL FOOTBALL CLUB;0;5409;1991;
;5409;SALINAS MARTINEZ;Barbara;FRA;3253895;43;Non;3;OCCITANIE;60 31 0785;TOULOUSE PADEL CLUB;-705;4400;1995;
;5409;;;FRA;3489959;43;Non;5;PROVENCE ALPES COTE D'AZUR;62 13 8026;ENJOY PADEL;-201;5196;1998;
;5409;WILTZ;Charlotte;FRA;3489959;43;Non;5;PROVENCE ALPES COTE D'AZUR;62 13 8026;ENJOY PADEL;-201;5196;1998;
;5442;L'HERMENIER;Virginie;FRA;3400370;42;Non;8;OCCITANIE;60 66 0120;TC GRAND STADE ST CYPRIEN;259;5332;1973;
;5442;MISIASZEK;Tamara;FRA;3338098;42;Non;5;NOUVELLE AQUITAINE;59 33 0723;BIG PADEL;44;4988;1985;
;5471;RICHARD;Caroline;FRA;3316036;41;Non;2;OCCITANIE;60 31 0785;TOULOUSE PADEL CLUB;-209;3933;1973;
@ -11766,9 +11766,9 @@
;5503;GENCO;Emma;FRA;3748580;40;Non;1;NOUVELLE AQUITAINE;59 24 0573;BOULAZAC ISLE MANOIRE TC;-206;4219;1993;
;5503;GIRAUDEAU;Jane;FRA;3838366;40;Non;1;NOUVELLE AQUITAINE;59 33 8000;4PADEL BORDEAUX;-206;5297;1989;
;5503;LOMBARD;Manon;FRA;1584850;40;Non;4;REUNION - MAYOTTE;67 97 0010;TENNIS CLUB DE L OASIS;-206;4842;1993;
;5503;;;FRA;3976887;40;Non;1;ILE DE FRANCE;57 L0 0750;TC COMITE DE PARIS;0;5503;1982;
;5503;Malone;Maria Julia;FRA;3976887;40;Non;1;ILE DE FRANCE;57 L0 0750;TC COMITE DE PARIS;0;5503;1982;
;5503;PASTOR;Cristina;MON;1233969;40;Non;1;PROVENCE ALPES COTE D'AZUR;62 06 0433;TC BEAUSOLEIL;-206;2404;1973;
;5503;;;FRA;5695502;40;Non;1;HAUTS DE FRANCE;56 59 8013;Padel Break Bondues;0;5503;1977;
;5503;POTTIER;Alexandra;FRA;5695502;40;Non;1;HAUTS DE FRANCE;56 59 8013;Padel Break Bondues;0;5503;1977;
;5503;REBIN;Laura;FRA;3669994;40;Non;1;NOUVELLE AQUITAINE;59 33 0723;BIG PADEL;-206;5099;1984;
;5503;ROUMIER;Adeline;FRA;2757368;40;Non;1;AUVERGNE RHONE-ALPES;50 03 0004;GANNAT (TENNIS CLUB);-206;3933;1979;
;5503;RUSE;Alison;FRA;4190754;40;Non;2;PROVENCE ALPES COTE D'AZUR;62 83 0081;TC TOULONNAIS;257;1868;1993;
@ -11789,8 +11789,8 @@
;5761;MALAVIALLE BARRE;Véronique;FRA;3172678;35;Non;4;OCCITANIE;60 34 0390;VALRAS TENNIS/PADEL;588;5761;1979;
;5761;MIRALLES;Cécile;FRA;8542059;35;Non;2;PROVENCE ALPES COTE D'AZUR;62 83 0352;TC NANS LES PINS;-212;4398;1978;
;5761;MOUTOUH;Caroline;FRA;3192646;35;Non;8;PROVENCE ALPES COTE D'AZUR;62 13 0603;ALL IN PADEL ASSOCIATION;-1;5379;1981;
;5761;;;FRA;9275259;35;Non;1;GRAND EST;55 10 8001;PADEL 3;0;5761;1994;
;5761;;;FRA;6016404;35;Non;2;PROVENCE ALPES COTE D'AZUR;62 83 0074;TC LE LAVANDOU;0;5761;2000;
;5761;NHAM;Hélène;FRA;9275259;35;Non;1;GRAND EST;55 10 8001;PADEL 3;0;5761;1994;
;5761;PIERSANTI;Lorena;FRA;6016404;35;Non;2;PROVENCE ALPES COTE D'AZUR;62 83 0074;TC LE LAVANDOU;0;5761;2000;
;5761;RICHARD;Laure;FRA;8779227;35;Non;1;GRAND EST;55 51 0362;INSIDE CLUB;-212;5350;2000;
;5761;ROYER;Aurélie;FRA;1578200;35;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 8023;CLUB 45;-212;5549;1980;
;5761;VASSEUR;Elodie;FRA;2848012;35;Non;2;HAUTS DE FRANCE;56 59 8013;Padel Break Bondues;1855;5075;1975;
@ -11798,7 +11798,7 @@
;5844;PROVENCAL;Stephanie;FRA;3405656;34;Non;3;AUVERGNE RHONE-ALPES;50 L0 0260;COMITE DROME TENNIS;-214;5129;1972;
;5844;TARDIEUX;Marie Christine;FRA;3751302;34;Non;2;PROVENCE ALPES COTE D'AZUR;62 13 8004;COUNTRY CLUB PADEL;-214;4692;1964;
;5880;MILLAN;Laurence;FRA;3438527;33;Non;3;PROVENCE ALPES COTE D'AZUR;62 13 8009;WIN WIN PADEL;-217;5224;1969;
;5880;;;FRA;3311244;33;Non;4;AUVERGNE RHONE-ALPES;50 69 0609;ESPRIT PADEL CLUB;158;5805;1965;
;5880;GEONI;Marie-Claire;FRA;3311244;33;Non;4;AUVERGNE RHONE-ALPES;50 69 0609;ESPRIT PADEL CLUB;158;5805;1965;
;5880;MENANT;Julie;FRA;1892053;33;Non;3;NOUVELLE AQUITAINE;59 17 0017;ROCHEFORT SA;-217;5663;1986;
;5880;VIVES;Lola;FRA;9688788;33;Non;2;GUADELOUPE ST MARTIN ST BARTH;63 97 0087;SXM PADEL LOVER;-217;4341;1996;
;5949;BESSOUD;Solene;FRA;2719713;31;Non;2;OCCITANIE;60 34 0056;TC SETE;-226;1741;1987;
@ -11812,25 +11812,25 @@
;5992;BAUDU;Héléna;FRA;5284168;30;Non;1;NOUVELLE AQUITAINE;59 64 0150;BRUYERES (LES) TC;-232;612;1999;
;5992;BENIGAUD;Laury;FRA;3243038;30;Non;2;OCCITANIE;60 30 0415;TENNIS CLUB CARREAU DE LANES;-443;4116;1992;
;5992;BERNARD;Stéphanie;FRA;1480065;30;Non;4;PROVENCE ALPES COTE D'AZUR;62 83 0050;TC SAINT CYRIEN;357;5929;1987;
;5992;;;FRA;4798486;30;Non;2;PROVENCE ALPES COTE D'AZUR;62 83 0460;SOUTH PADEL PARK;0;5992;1992;
;5992;DESVIGNES;Solenne;FRA;4798486;30;Non;2;PROVENCE ALPES COTE D'AZUR;62 83 0460;SOUTH PADEL PARK;0;5992;1992;
;5992;GREFF;Margaux;FRA;5556981;30;Non;2;PROVENCE ALPES COTE D'AZUR;62 13 0054;TENNIS CLUB PORT-DE-BOUC;878;5348;2007;
;5992;;;FRA;5204597;30;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 8004;COUNTRY CLUB PADEL;0;5992;1986;
;5992;JACQUELINET;Mathilde;FRA;5204597;30;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 8004;COUNTRY CLUB PADEL;0;5992;1986;
;5992;JACQUETIN;Isabelle;FRA;3420320;30;Non;3;PROVENCE ALPES COTE D'AZUR;62 05 0257;TC BATIE-NEUVE;878;5992;1976;
;5992;;;FRA;3375734;30;Non;2;HAUTS DE FRANCE;56 59 8013;Padel Break Bondues;0;5992;1988;
;5992;JON;Anissa;FRA;3375734;30;Non;2;HAUTS DE FRANCE;56 59 8013;Padel Break Bondues;0;5992;1988;
;5992;LUCIANI;Christelle;FRA;3390402;30;Non;2;CORSE;54 2A 0602;MEZZAVIA TENNIS CLUB;1346;5992;1992;
;5992;;;FRA;1737305;30;Non;2;PROVENCE ALPES COTE D'AZUR;62 13 0559;TRETS TENNIS CLUB;0;5992;1982;
;6130;;;FRA;3513809;28;Non;2;PROVENCE ALPES COTE D'AZUR;62 13 8023;CLUB 45;-235;5522;1992;
;5992;MINVIELLE;Carine;FRA;1737305;30;Non;2;PROVENCE ALPES COTE D'AZUR;62 13 0559;TRETS TENNIS CLUB;0;5992;1982;
;6130;FERNANDEZ;melanie;FRA;3513809;28;Non;2;PROVENCE ALPES COTE D'AZUR;62 13 8023;CLUB 45;-235;5522;1992;
;6130;GRUDZIEN;Julie;FRA;2847999;28;Non;3;HAUTS DE FRANCE;56 62 8005;PADEL CAMPUS ARENA;-500;4539;1974;
;6130;LINIER;Beatrice;FRA;2711724;28;Non;3;NOUVELLE AQUITAINE;59 64 8001;RUGBY PARK 64;1039;4003;1973;
;6130;OSMANI;Miriam;FRA;3427530;28;Non;2;ILE DE FRANCE;57 92 0014;Association Sportive du Stade de la Marche;-235;5164;1993;
;6184;;;FRA;2768780;27;Non;2;PROVENCE ALPES COTE D'AZUR;62 13 8017;LE COMPLEXE PADEL;-6184;4612;1988;
;6184;BONINO;Aurore;FRA;2768780;27;Non;2;PROVENCE ALPES COTE D'AZUR;62 13 8017;LE COMPLEXE PADEL;-6184;4612;1988;
;6231;COUSTY;Elisa;FRA;3198777;26;Non;2;OCCITANIE;60 30 0415;TENNIS CLUB CARREAU DE LANES;3350;6231;2001;
;6231;VON HATTEN;Isabelle;FRA;6200032;26;Non;2;GRAND EST;55 67 0050;TC PADEL REICHSTETT;-247;4304;1983;
;6284;;;FRA;3891115;25;Non;1;PROVENCE ALPES COTE D'AZUR;62 L0 0060;COMITE ALPES MARITIMES TENNIS;0;6284;2000;
;6284;;;FRA;8715604;25;Non;1;NOUVELLE AQUITAINE;59 33 0073;ARCACHON TC;-246;5522;1967;
;6284;bartoli;pierre-louis;FRA;3891115;25;Non;1;PROVENCE ALPES COTE D'AZUR;62 L0 0060;COMITE ALPES MARITIMES TENNIS;0;6284;2000;
;6284;BETTS;Allie;FRA;8715604;25;Non;1;NOUVELLE AQUITAINE;59 33 0073;ARCACHON TC;-246;5522;1967;
;6284;CROMBEZ;Charlotte;FRA;9810347;25;Non;1;HAUTS DE FRANCE;56 62 0057;TC BERCK;-246;6038;1995;
;6284;DAUBENFELD;Claire;FRA;8293243;25;Non;1;NOUVELLE AQUITAINE;59 19 0089;SAINTE FEREOLE SS;-246;1210;1991;
;6284;;;FRA;2912707;25;Non;1;PROVENCE ALPES COTE D'AZUR;62 06 0303;TC ROQUETTAN;-6284;2493;1962;
;6284;FORQUET;Sylvie;FRA;2912707;25;Non;1;PROVENCE ALPES COTE D'AZUR;62 06 0303;TC ROQUETTAN;-6284;2493;1962;
;6284;HOOGSTOEL;Caroline;FRA;4220797;25;Non;1;HAUTS DE FRANCE;56 59 0284;TC LOON-PLAGE;-246;4382;1977;
;6284;HORDEL;Jeanne;FRA;4325671;25;Non;1;NORMANDIE;58 50 0053;CHERBOURG AS-BR TENNIS;-246;5017;1995;
;6284;LASNE;Sara;FRA;3862346;25;Non;1;CENTRE VAL DE LOIRE;53 45 8000;4PADEL ORLEANS;-246;6038;2001;
@ -11838,57 +11838,57 @@
;6284;LUBRANO;Magalie;FRA;3198369;25;Non;2;NOUVELLE AQUITAINE;59 17 0178;LAGORD TENNIS SQUASH;-246;3255;1976;
;6284;MASSON;Sophie;FRA;2758966;25;Non;2;ILE DE FRANCE;57 78 0094;A.S. LES PYRAMIDES;1560;4173;1965;
;6284;PEDUZZI;Corinne;FRA;1776133;25;Non;1;NOUVELLE AQUITAINE;59 24 0339;COULOUNIEIX CHAMIERS;-246;5522;1962;
;6284;;;FRA;5692825;25;Non;1;OCCITANIE;60 30 0016;TC BESSEGES;0;6284;2006;
;6284;;;FRA;3844886;25;Non;1;ILE DE FRANCE;57 78 0072;T.C. CONFLANS STE HONORINE;0;6284;1991;
;6284;PELLIER;Lola;FRA;5692825;25;Non;1;OCCITANIE;60 30 0016;TC BESSEGES;0;6284;2006;
;6284;PEREIRA;LAETITIA;FRA;3844886;25;Non;1;ILE DE FRANCE;57 78 0072;T.C. CONFLANS STE HONORINE;0;6284;1991;
;6284;PHILIPPON;Laure;FRA;3742971;25;Non;2;OCCITANIE;60 30 8001;LE HANGAR;-246;6038;1971;
;6284;QUEGUINER;Sonia;FRA;3147173;25;Non;1;PROVENCE ALPES COTE D'AZUR;62 83 0050;TC SAINT CYRIEN;-246;6038;1971;
;6284;;;FRA;2721994;25;Non;1;OCCITANIE;60 30 8004;SOCCER TEAM Alès;0;6284;1991;
;6284;SAVANIER;Amandine;FRA;2721994;25;Non;1;OCCITANIE;60 30 8004;SOCCER TEAM Alès;0;6284;1991;
;6284;TITA;Amal;FRA;4907353;25;Non;1;GRAND EST;55 54 0334;VILLERS LES NANCY VNTC;-867;2047;1982;
;6519;HUGUES;Valerie;FRA;3288598;24;Non;3;REUNION - MAYOTTE;67 97 8001;ENDEMIK CLUB;-269;5927;1975;
;6519;TURUANI GUBERMAN;Béatrice;FRA;2551540;24;Non;3;OCCITANIE;60 31 0785;TOULOUSE PADEL CLUB;-269;609;1958;
;6575;DUCOS;Charlotte;FRA;590437;23;Non;2;AUVERGNE RHONE-ALPES;50 69 0613;ALL IN COUNTRY CLUB DECINES;-276;5467;1985;
;6575;SARRIQUET;Mathilde;FRA;2687027;23;Non;3;NOUVELLE AQUITAINE;59 33 8003;SQUASH BAD 33;-173;2783;1983;
;6632;;;FRA;7476439;22;Non;2;AUVERGNE RHONE-ALPES;50 38 0145;TRONCHE (LA) TENNIS CLUB;-283;2434;1984;
;6632;FAURE;Monique;FRA;7476439;22;Non;2;AUVERGNE RHONE-ALPES;50 38 0145;TRONCHE (LA) TENNIS CLUB;-283;2434;1984;
;6632;GONZALEZ;Elodie;FRA;3827093;22;Non;2;PROVENCE ALPES COTE D'AZUR;62 13 8011;CAMARG'ANIM;-283;6349;1988;
;6681;BONNO;Sidney;FRA;3217130;21;Non;1;PROVENCE ALPES COTE D'AZUR;62 06 0144;TCM MOUANS SARTOUX;-279;6402;1977;
;6681;;;FRA;8650977;21;Non;1;OCCITANIE;60 31 0785;TOULOUSE PADEL CLUB;0;6681;1990;
;6681;CAUMETTE;Marine;FRA;8650977;21;Non;1;OCCITANIE;60 31 0785;TOULOUSE PADEL CLUB;0;6681;1990;
;6681;DOMINGUEZ;Melissa;FRA;3433566;21;Non;3;OCCITANIE;60 34 0184;ASCH MONTPELLIER;112;6178;1985;
;6681;EL KHOUMISTI;Sabah;FRA;3254981;21;Non;1;AUVERGNE RHONE-ALPES;50 38 0591;GRENOBLE TENNIS;-279;4666;1985;
;6681;GUIMARD;Marie;FRA;5416714;21;Non;2;ILE DE FRANCE;57 94 0040;TC DE LA FERME DE MANDRES;-279;6160;2003;
;6681;MAITROT;Sylvie;FRA;8568738;21;Non;4;CENTRE VAL DE LOIRE;53 37 8002;PADEL SHOT TOURS SAINT PIERRE DES CORPS;-70;6611;1969;
;6681;;;FRA;3846525;21;Non;3;PROVENCE ALPES COTE D'AZUR;62 83 0456;TENNIS PADEL ST CHARLES;0;6681;1971;
;6681;MATTIO;patricia;FRA;3846525;21;Non;3;PROVENCE ALPES COTE D'AZUR;62 83 0456;TENNIS PADEL ST CHARLES;0;6681;1971;
;6753;ASSARAF;Virna;FRA;608093;20;Non;1;GUADELOUPE ST MARTIN ST BARTH;63 L0 1000;LIGUE GUADELOUPE TENNIS;-283;6470;1974;
;6753;DRUAIS;Emilie;FRA;1002225;20;Non;1;ILE DE FRANCE;57 94 0518;COUNTRY SPORTS CLUB ASSOCIATION;-283;6470;1978;
;6753;;;FRA;3965853;20;Non;1;ILE DE FRANCE;57 91 8001;PARTOUT - B14;0;6753;1981;
;6753;;;FRA;2197832;20;Non;1;GUADELOUPE ST MARTIN ST BARTH;63 97 0087;SXM PADEL LOVER;0;6753;1983;
;6753;DURAND;Aline;FRA;3965853;20;Non;1;ILE DE FRANCE;57 91 8001;PARTOUT - B14;0;6753;1981;
;6753;EVEN;Delphine;FRA;2197832;20;Non;1;GUADELOUPE ST MARTIN ST BARTH;63 97 0087;SXM PADEL LOVER;0;6753;1983;
;6753;LAB;Ingrid;FRA;3238734;20;Non;1;BOURGOGNE FRANCHE COMTE;51 25 0078;EXINCOURT T.C.;-283;6228;1978;
;6753;LOMBARDO;Wendy;FRA;1805920;20;Non;1;OCCITANIE;60 66 8003;IPC SPORT;-283;833;1996;
;6753;SFRECOLA;Elena;FRA;3233547;20;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 0167;TENNIS CLUB MARTIGUES;-283;4804;2015;
;6753;YUNGMANN;Mélanie;FRA;9627836;20;Non;3;OCCITANIE;60 66 0120;TC GRAND STADE ST CYPRIEN;731;581;1982;
;6856;;;FRA;1803496;19;Non;2;PROVENCE ALPES COTE D'AZUR;62 83 0460;SOUTH PADEL PARK;0;6856;1997;
;6856;DE STEFANO;pauline;FRA;1803496;19;Non;2;PROVENCE ALPES COTE D'AZUR;62 83 0460;SOUTH PADEL PARK;0;6856;1997;
;6856;EZZAIM;Nawelle;FRA;3331515;19;Non;3;OCCITANIE;60 31 0785;TOULOUSE PADEL CLUB;-285;5996;1995;
;6856;GIPOULOU PAUMÉ;Véronique;FRA;3178586;19;Non;3;NOUVELLE AQUITAINE;59 64 0149;PAU TC;-285;5279;1960;
;6856;LABOUS;Dolores;FRA;1735784;19;Non;1;PROVENCE ALPES COTE D'AZUR;62 83 0115;TM OLLIOULAIS;-285;4778;1969;
;6856;LEGER;Marie;FRA;3853155;19;Non;3;NORMANDIE;58 61 8001;BAZSPORTS;231;6856;1998;
;6856;;;FRA;3475440;19;Non;2;PROVENCE ALPES COTE D'AZUR;62 83 0460;SOUTH PADEL PARK;482;6680;1994;
;6856;PAVELIN;Maria;FRA;3475440;19;Non;2;PROVENCE ALPES COTE D'AZUR;62 83 0460;SOUTH PADEL PARK;482;6680;1994;
;6856;SCHOLLHAMMER;Alice;FRA;492084;19;Non;2;GRAND EST;55 54 0513;JARVILLE TSB;1967;6856;1997;
;6906;BERGERON;Valerie;FRA;1618578;18;Non;3;OCCITANIE;60 34 0713;TC DE LA VIERE;-295;1074;1967;
;6906;COIFFIER;Anne Sophie;FRA;5990876;18;Non;1;HAUTS DE FRANCE;56 59 0264;HEM TC;-295;4814;1968;
;6906;COLIN;Violaine;FRA;3183371;18;Non;2;OCCITANIE;60 11 8004;SET PADEL NARBONNE;-295;5863;1979;
;6906;GALLO;Clara;FRA;3399406;18;Non;3;OCCITANIE;60 34 0713;TC DE LA VIERE;-295;6611;1981;
;6906;;;FRA;3401577;18;Non;2;MARTINIQUE;65 97 8002;RC SPORT CENTER - ARENA;0;6906;1982;
;6906;Gouyer;Coralie;FRA;3401577;18;Non;2;MARTINIQUE;65 97 8002;RC SPORT CENTER - ARENA;0;6906;1982;
;6906;KLEVGE;Celine;FRA;3879050;18;Non;2;OCCITANIE;60 34 8003;BEZIERS PADEL CLUB;578;6906;1981;
;6906;LECONTE;Julie;FRA;3245154;18;Non;3;HAUTS DE FRANCE;56 80 0039;TENNIS CLUB AMIENS METROPOLE;-295;6277;1997;
;6906;VERMEERSCH;Sandra;FRA;3245326;18;Non;2;HAUTS DE FRANCE;56 59 8002;URBAN SOCCER LILLE LEZENNES;-295;4899;1983;
;7016;CAPERAA-BOURDA;Cécile;FRA;3312152;17;Non;2;NOUVELLE AQUITAINE;59 64 8001;RUGBY PARK 64;-306;5070;1981;
;7016;DEIAS;Dorothée;FRA;3184107;17;Non;4;PROVENCE ALPES COTE D'AZUR;62 83 0032;TC DE BANDOL;-445;5777;1983;
;7016;;;FRA;3942309;17;Non;1;BOURGOGNE FRANCHE COMTE;51 21 8001;URBAN PADEL DIJON;0;7016;1988;
;7016;GUEUGNON;Mathieu;FRA;3942309;17;Non;1;BOURGOGNE FRANCHE COMTE;51 21 8001;URBAN PADEL DIJON;0;7016;1988;
;7016;GUILLOU;Virginie;FRA;3349179;17;Non;2;NOUVELLE AQUITAINE;59 87 0007;RED STAR PADEL TENNIS CLUB;-306;5550;1978;
;7016;SAVES;Anaïs;FRA;2771059;17;Non;3;OCCITANIE;60 65 0405;TENNIS SQUASH LES PEUPLIERS;-306;4354;1999;
;7016;VIALLARD;Emilie;FRA;3351322;17;Non;2;PROVENCE ALPES COTE D'AZUR;62 06 0444;AZUR TENNIS SPORTS VALROSE;322;7016;1986;
;7016;VOISIN;Valerie;FRA;6311814;17;Non;7;OCCITANIE;60 34 0270;TC BAILLARGUES;-306;6315;1971;
;7078;BOURDENET;Karine;FRA;2258715;16;Non;3;REUNION - MAYOTTE;67 97 0009;TENNIS CLUB DE LA BAIE DE ST PAUL;-368;5299;1972;
;7078;;;FRA;5381504;16;Non;2;OCCITANIE;60 34 0435;TC BASSAN;-285;6793;1981;
;7078;JOLIVET;Carole;FRA;5381504;16;Non;2;OCCITANIE;60 34 0435;TC BASSAN;-285;6793;1981;
;7078;PORTET RODRIGUEZ;Aurélie;FRA;3331964;16;Non;1;AUVERGNE RHONE-ALPES;50 L0 0380;COMITE ISERE TENNIS;-285;5616;1984;
;7078;TIRAT;Brigitte;FRA;8332880;16;Non;2;GUADELOUPE ST MARTIN ST BARTH;63 97 0017;T.C. MONTAUBAN GOSIER;-1680;2709;1958;
;7078;VALIN;Laury;FRA;3349539;16;Non;2;CENTRE VAL DE LOIRE;53 45 8000;4PADEL ORLEANS;-285;6315;1994;
@ -11902,13 +11902,13 @@
;7168;LANNELONGUE;Hélène;FRA;3340618;15;Non;3;NOUVELLE AQUITAINE;59 64 0259;LUY DU BEARN TC;1655;6833;1985;
;7168;MATHIEU;Cloé;FRA;2764126;15;Non;5;REUNION - MAYOTTE;67 97 8005;KAZ A PADEL;-298;4456;1995;
;7168;MIEDZYRZECKI;Camille;FRA;5204708;15;Non;3;OCCITANIE;60 31 0785;TOULOUSE PADEL CLUB;-298;5854;2001;
;7168;;;FRA;3488814;15;Non;1;BRETAGNE;52 35 0073;RENNES GARDEN TENNIS CLUB;-298;6262;2002;
;7168;MOREL;Capucine;FRA;3488814;15;Non;1;BRETAGNE;52 35 0073;RENNES GARDEN TENNIS CLUB;-298;6262;2002;
;7168;PLATRE-BROSSETTE;Félie;FRA;8307435;15;Non;1;AUVERGNE RHONE-ALPES;50 69 0033;MUNICIPAL 5EME (TENNIS CLUB);-298;5671;1969;
;7168;;;FRA;2497281;15;Non;1;ILE DE FRANCE;57 94 0510;SPORT HORIZON;0;7168;1979;
;7168;POUJARDIEU;Cecile;FRA;2497281;15;Non;1;ILE DE FRANCE;57 94 0510;SPORT HORIZON;0;7168;1979;
;7168;PY;Anne Laure;FRA;3217918;15;Non;1;OCCITANIE;60 34 0632;TC MARAUSSAN-CAZOULS;-298;6596;1982;
;7168;SOTO;Marion;FRA;3920493;15;Non;1;NOUVELLE AQUITAINE;59 33 0723;BIG PADEL;-298;6870;1987;
;7168;TOBI;Karine;FRA;3135606;15;Non;1;NOUVELLE AQUITAINE;59 17 0372;SAUJON TC;-298;5382;1984;
;7395;;;FRA;3420494;14;Non;4;AUVERGNE RHONE-ALPES;50 03 0390;PADEL DES ANCISES;221;7120;1977;
;7395;COGNEIN;Erika;FRA;3420494;14;Non;4;AUVERGNE RHONE-ALPES;50 03 0390;PADEL DES ANCISES;221;7120;1977;
;7395;DY;Sophie;FRA;3619263;14;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 8009;WIN WIN PADEL;-308;6803;1998;
;7395;FRELICOT;Marine;FRA;3229424;14;Non;4;NOUVELLE AQUITAINE;59 64 8003;PADEL FACTORY;-525;5600;1993;
;7395;LOHR;Agnes;FRA;6773019;14;Non;1;GRAND EST;55 67 0109;TC VENDENHEIM;-308;7087;1971;
@ -11928,11 +11928,11 @@
;7478;ROTT;Johanna;FRA;1168353;13;Non;2;NOUVELLE AQUITAINE;59 24 0525;BERGERAC TC;-309;6099;1993;
;7478;TAILLEFER;Sophie;FRA;2773270;13;Non;1;OCCITANIE;60 34 0725;SETE PADEL;-309;3147;1983;
;7478;TOUBOUL;Camille;FRA;2776283;13;Non;2;REUNION - MAYOTTE;67 97 0046;HERMITAGE ACADEMIE TENNIS;-309;5907;2009;
;7649;;;FRA;3956109;12;Non;1;AUVERGNE RHONE-ALPES;50 73 0044;AIX LES BAINS TC;0;7649;1990;
;7649;BERGER-SABBATEL;marielle;FRA;3956109;12;Non;1;AUVERGNE RHONE-ALPES;50 73 0044;AIX LES BAINS TC;0;7649;1990;
;7649;COUPET BAILLARGUET;Géraldine;FRA;3491546;12;Non;1;PAYS DE LA LOIRE;61 L0 1000;LIGUE PAYS DE LA LOIRE TENNIS;-311;7338;1973;
;7649;HEMON;Alexandra;FRA;3368010;12;Non;4;NOUVELLE AQUITAINE;59 40 8004;BLUE PADEL;-311;6917;1977;
;7649;;;FRA;3897919;12;Non;1;AUVERGNE RHONE-ALPES;50 42 8002;TERRAPADEL;0;7649;1978;
;7649;;;FRA;3649088;12;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 8014;MONKEY PADEL;0;7649;1989;
;7649;JEANETTI;AUDE;FRA;3897919;12;Non;1;AUVERGNE RHONE-ALPES;50 42 8002;TERRAPADEL;0;7649;1978;
;7649;LEONARDO;Stessy;FRA;3649088;12;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 8014;MONKEY PADEL;0;7649;1989;
;7649;LEONET;Valérie;FRA;5557693;12;Non;1;OCCITANIE;60 34 0724;TENNIS PADEL CAP D'AGDE;-311;1063;1968;
;7649;RICHARD;Sandra;FRA;3882474;12;Non;1;PAYS DE LA LOIRE;61 44 0029;TC CASTELBRIANTAIS;-311;7338;1974;
;7797;DAGNAC;Lou;FRA;491797;11;Non;1;OCCITANIE;60 12 0749;PADEL TOLOSA RODEZ ACADEMY;-313;5515;2001;
@ -11956,35 +11956,35 @@
;7931;DO CARMO;Katia;FRA;5161758;10;Non;1;ILE DE FRANCE;57 78 0094;A.S. LES PYRAMIDES;-315;5467;1977;
;7931;ERRECART;Isabel;ESP;6968004;10;Non;1;NOUVELLE AQUITAINE;59 47 0265;BOE;-315;5600;1964;
;7931;FABERT;Sandra;FRA;3380704;10;Non;2;NOUVELLE AQUITAINE;59 64 0150;BRUYERES (LES) TC;-315;7046;1973;
;7931;;;FRA;3491370;10;Non;1;OCCITANIE;60 66 8006;CAN PADEL;-315;6905;1979;
;7931;GENIS;NATHALIE;FRA;3491370;10;Non;1;OCCITANIE;60 66 8006;CAN PADEL;-315;6905;1979;
;7931;GOBERT;Océane;FRA;4199580;10;Non;1;ILE DE FRANCE;57 75 0141; U.S. BRETONS DE PARIS;-315;6905;1998;
;7931;;;FRA;6756547;10;Non;1;CENTRE VAL DE LOIRE;53 45 0775;SAINT PRYVE TENNIS CLUB;0;7931;1983;
;7931;JOUSSE;Anne Laure;FRA;6756547;10;Non;1;CENTRE VAL DE LOIRE;53 45 0775;SAINT PRYVE TENNIS CLUB;0;7931;1983;
;7931;LACOSTE;Emmanuelle;FRA;3877689;10;Non;1;GUADELOUPE ST MARTIN ST BARTH;63 97 0004;DUGAZON SPORTING CLUB;-315;7616;1980;
;7931;LE MOIGNE;Laurence;FRA;3404151;10;Non;2;OCCITANIE;60 31 8001;4PADEL TOULOUSE COLOMIERS;-315;7046;1964;
;7931;;;FRA;1855027;10;Non;1;HAUTS DE FRANCE;56 59 8013;Padel Break Bondues;0;7931;1994;
;7931;;;FRA;3862374;10;Non;2;OCCITANIE;60 L0 0310;COMITE HAUTE GARONNE TENNIS;0;7931;1992;
;7931;;;FRA;8713551;10;Non;1;PROVENCE ALPES COTE D'AZUR;62 06 0433;TC BEAUSOLEIL;0;7931;1995;
;7931;LECOMTE;Caroline;FRA;1855027;10;Non;1;HAUTS DE FRANCE;56 59 8013;Padel Break Bondues;0;7931;1994;
;7931;mairesse;olivia;FRA;3862374;10;Non;2;OCCITANIE;60 L0 0310;COMITE HAUTE GARONNE TENNIS;0;7931;1992;
;7931;MALGHERINI;Malory;FRA;8713551;10;Non;1;PROVENCE ALPES COTE D'AZUR;62 06 0433;TC BEAUSOLEIL;0;7931;1995;
;7931;MAUPAS;Julie;FRA;5015480;10;Non;1;BOURGOGNE FRANCHE COMTE;51 71 0204;CHARNAY LES MACON T.C.;-315;1704;1997;
;7931;MERAH;Malika;FRA;6723557;10;Non;1;CENTRE VAL DE LOIRE;53 37 0003;ASSOCIATION PADEL DE LA BULLE;-315;5600;1975;
;7931;MORIN PARRA;Julie;FRA;3831051;10;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 8023;CLUB 45;-315;7616;1989;
;7931;OLIVIER;Anne;FRA;3349590;10;Non;2;ILE DE FRANCE;57 94 0510;SPORT HORIZON;-315;6458;1972;
;7931;;;FRA;3658533;10;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 8014;MONKEY PADEL;0;7931;1982;
;7931;;;FRA;658927;10;Non;1;REUNION - MAYOTTE;67 97 8005;KAZ A PADEL;0;7931;1986;
;7931;;;FRA;3977620;10;Non;1;ILE DE FRANCE;57 91 8001;PARTOUT - B14;0;7931;1985;
;7931;;;FRA;3973377;10;Non;1;HAUTS DE FRANCE;56 59 8004;4PADEL VALENCIENNES;0;7931;1994;
;7931;PAPE;justine;FRA;3658533;10;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 8014;MONKEY PADEL;0;7931;1982;
;7931;PELLEGRINO;Magali;FRA;658927;10;Non;1;REUNION - MAYOTTE;67 97 8005;KAZ A PADEL;0;7931;1986;
;7931;POQUET;deborah;FRA;3977620;10;Non;1;ILE DE FRANCE;57 91 8001;PARTOUT - B14;0;7931;1985;
;7931;ROMBEAUX;Amandine;FRA;3973377;10;Non;1;HAUTS DE FRANCE;56 59 8004;4PADEL VALENCIENNES;0;7931;1994;
;7931;ROUSSEAU;Sophie;FRA;3323841;10;Non;1;Z-DIVERS FFT;00 F0 0100;TC FEDERATION;-315;5907;1975;
;7931;ROUSSEAU;Jade;FRA;3769042;10;Non;2;OCCITANIE;60 66 0357;PADEL CLUB DU MAS;892;7931;2005;
;7931;;;FRA;8345773;10;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 0613;PADEL TENNIS CLUB ST VICTORET;0;7931;1963;
;7931;;;FRA;2609856;10;Non;1;NOUVELLE AQUITAINE;59 16 8001;PADEL SPOT;0;7931;1993;
;7931;SATGE;Fabienne;FRA;8345773;10;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 0613;PADEL TENNIS CLUB ST VICTORET;0;7931;1963;
;7931;SORE;Celine;FRA;2609856;10;Non;1;NOUVELLE AQUITAINE;59 16 8001;PADEL SPOT;0;7931;1993;
;7931;TALON;Carole;FRA;3351010;10;Non;1;GRAND EST;55 67 8000;4PADEL STRASBOURG;-315;5907;1993;
;7931;;;FRA;3945925;10;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 0613;PADEL TENNIS CLUB ST VICTORET;0;7931;1973;
;7931;ZEMOURI;Linda;FRA;3945925;10;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 0613;PADEL TENNIS CLUB ST VICTORET;0;7931;1973;
;8197;BAROCHE;Sandrine;FRA;2939428;9;Non;1;REUNION - MAYOTTE;67 97 0046;HERMITAGE ACADEMIE TENNIS;-353;5149;1973;
;8197;BEYLOT;Anne Sophie;FRA;1160804;9;Non;1;NOUVELLE AQUITAINE;59 33 8003;SQUASH BAD 33;-353;2473;1985;
;8197;BOILEAU;Diana;FRA;3294891;9;Non;1;GRAND EST;55 54 0513;JARVILLE TSB;-353;7544;1986;
;8197;BOURGOIN;Catherine;FRA;3362855;9;Non;3;NOUVELLE AQUITAINE;59 16 8001;PADEL SPOT;-353;7326;1959;
;8197;CAHUZAC;Sandrine;FRA;966832;9;Non;2;NOUVELLE AQUITAINE;59 33 0723;BIG PADEL;-353;6029;1971;
;8197;FURIO;Charlene;FRA;3828858;9;Non;1;OCCITANIE;60 31 0799;CLUB ON PADEL Escalquens;-353;7844;2002;
;8197;;;FRA;3634870;9;Non;1;PROVENCE ALPES COTE D'AZUR;62 83 0050;TC SAINT CYRIEN;0;8197;1989;
;8197;GALA;justine;FRA;3634870;9;Non;1;PROVENCE ALPES COTE D'AZUR;62 83 0050;TC SAINT CYRIEN;0;8197;1989;
;8197;GANDOIS;Helene;FRA;536126;9;Non;1;OCCITANIE;60 31 0785;TOULOUSE PADEL CLUB;-353;6445;1967;
;8197;GIMENEZ;Romina;ARG;6746900;9;Non;1;OCCITANIE;60 82 0733;PADEL TOLOSA MONTAUBAN ACADEMIE;-353;7544;1986;
;8197;GUERIN;Louanne;FRA;985949;9;Non;1;NORMANDIE;58 14 0515;RANVILLE TC;-353;7544;2013;
@ -11996,17 +11996,17 @@
;8197;MEKKI;Apolline;FRA;3700941;9;Non;1;ILE DE FRANCE;57 92 0169;MONTROUGE (CA);-353;4722;2012;
;8197;MEYSONNIER;Pascale;FRA;5000351;9;Non;1;OCCITANIE;60 82 0058;ASSOCIATION TENNIS MONTAUBAN A.T.M;-353;6922;1967;
;8197;PANTAROTTO;Sylvie;FRA;3190388;9;Non;1;OCCITANIE;60 82 0058;ASSOCIATION TENNIS MONTAUBAN A.T.M;-353;6922;1963;
;8197;;;FRA;393058;9;Non;1;ILE DE FRANCE;57 78 0094;A.S. LES PYRAMIDES;0;8197;1999;
;8197;PARBOT;Emma;FRA;393058;9;Non;1;ILE DE FRANCE;57 78 0094;A.S. LES PYRAMIDES;0;8197;1999;
;8197;POISSON;Sophie;FRA;197129;9;Non;1;PROVENCE ALPES COTE D'AZUR;62 83 0035;TC DRACENOIS;-353;5640;1963;
;8197;PREUILH;Mélanie;FRA;1933697;9;Non;1;NOUVELLE AQUITAINE;59 40 0034;POUILLON US;-353;6445;1992;
;8197;;;FRA;112781;9;Non;1;REUNION - MAYOTTE;67 97 8006;SARL REUNION PADEL CLUB;-8197;2559;1984;
;8197;QUENTIN;Aurélie;FRA;112781;9;Non;1;REUNION - MAYOTTE;67 97 8006;SARL REUNION PADEL CLUB;-8197;2559;1984;
;8197;ROUSSEAUX;Magali;FRA;3635036;9;Non;1;OCCITANIE;60 30 0415;TENNIS CLUB CARREAU DE LANES;-353;7544;1974;
;8197;STIPCEVIC;Nadia;FRA;2637930;9;Non;1;GRAND EST;55 67 0109;TC VENDENHEIM;-353;7844;1966;
;8197;;;FRA;1703190;9;Non;1;OCCITANIE;60 31 0801;TOP PADEL PLAISANCE;0;8197;1994;
;8197;TAILLEFER;Camille;FRA;1703190;9;Non;1;OCCITANIE;60 31 0801;TOP PADEL PLAISANCE;0;8197;1994;
;8197;VALSOT;Mallaurie;FRA;3428567;9;Non;1;PROVENCE ALPES COTE D'AZUR;62 06 8005;V.INDOMITUS PADEL;-353;7844;1993;
;8439;BONNIER;Julie;FRA;8281083;8;Non;1;NOUVELLE AQUITAINE;59 16 8001;PADEL SPOT;-358;8081;1999;
;8439;CAILLET;Maelys;FRA;6030021;8;Non;2;PROVENCE ALPES COTE D'AZUR;62 06 0456;ULTRA COUNTRY CLUB;384;8439;2002;
;8439;;;FRA;3961375;8;Non;1;ILE DE FRANCE;57 78 0094;A.S. LES PYRAMIDES;0;8439;1989;
;8439;DUGRAINDELORGE;JULIE;FRA;3961375;8;Non;1;ILE DE FRANCE;57 78 0094;A.S. LES PYRAMIDES;0;8439;1989;
;8439;FASSIER;Virginie;FRA;3347377;8;Non;3;OCCITANIE;60 31 0800;TOAC PADEL TOULOUSE;-358;7412;1972;
;8439;GIRAULT;Charlotte;FRA;895566;8;Non;2;BOURGOGNE FRANCHE COMTE;51 21 0151;NORGES LES BOIS T.C.;-358;2654;1979;
;8439;GRIMALDI;Laurence;FRA;7226735;8;Non;4;OCCITANIE;60 11 0694;PADEL CLUB CARCASSONNE;-256;7196;1968;
@ -12015,35 +12015,35 @@
;8439;STOEHR;Isabelle;FRA;3408648;8;Non;2;AUVERGNE RHONE-ALPES;50 73 0046;CHAMBERY TC;-358;7040;1979;
;8539;BARTH;Capucine;FRA;5766545;7;Non;1;CENTRE VAL DE LOIRE;53 45 0775;SAINT PRYVE TENNIS CLUB;-356;5845;2007;
;8539;BEGUET;Geraldine;FRA;3320832;7;Non;1;AUVERGNE RHONE-ALPES;50 69 0612;PADEL SHOT LYON;-356;6026;1971;
;8539;;;FRA;3886474;7;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 8009;WIN WIN PADEL;0;8539;1998;
;8539;BERTHIER;Garance;FRA;3886474;7;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 8009;WIN WIN PADEL;0;8539;1998;
;8539;BESSON MONCET;Julie;FRA;1492175;7;Non;3;ILE DE FRANCE;57 92 0045;PARIS COUNTRY CLUB;-356;1752;1991;
;8539;BOURGET;Camille;FRA;1477410;7;Non;2;PROVENCE ALPES COTE D'AZUR;62 83 0032;TC DE BANDOL;-356;5384;1964;
;8539;CASSAM CHENAI;Tatiana;FRA;5270624;7;Non;1;REUNION - MAYOTTE;67 97 8001;ENDEMIK CLUB;-356;8183;1981;
;8539;CATHELOT;Chloe;FRA;3704535;7;Non;3;OCCITANIE;60 30 8003;MAM'SOCCER FIVE PADEL;541;8539;1991;
;8539;GALESTE;Malorie;FRA;3349930;7;Non;2;PROVENCE ALPES COTE D'AZUR;62 13 8009;WIN WIN PADEL;-204;6115;1994;
;8539;GOMES;Marine;FRA;2269988;7;Non;4;PROVENCE ALPES COTE D'AZUR;62 13 8014;MONKEY PADEL;284;2625;1999;
;8539;;;FRA;3963463;7;Non;1;OCCITANIE;60 34 0184;ASCH MONTPELLIER;0;8539;1968;
;8539;GOMEZ;pilar;FRA;3963463;7;Non;1;OCCITANIE;60 34 0184;ASCH MONTPELLIER;0;8539;1968;
;8539;LAFFORGUE;Clemence;FRA;8111996;7;Non;3;OCCITANIE;60 L0 0340;COMITE HERAULT TENNIS;-356;6932;1998;
;8539;LOTA;Stephanie;FRA;3661506;7;Non;1;CORSE;54 2B 0732;AS C5 SPORTS;-356;7848;1980;
;8539;MARION;Laurie;FRA;3284219;7;Non;1;PROVENCE ALPES COTE D'AZUR;62 83 0115;TM OLLIOULAIS;-356;5845;1990;
;8539;;;FRA;3886479;7;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 8009;WIN WIN PADEL;0;8539;1997;
;8539;MOLUS;Julie;FRA;3886479;7;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 8009;WIN WIN PADEL;0;8539;1997;
;8539;NAVARRO;Herma;FRA;1907338;7;Non;1;OCCITANIE;60 09 0210;FOIX TENNIS;-356;6687;1958;
;8539;PALOMO;Céline;FRA;3234592;7;Non;5;OCCITANIE;60 81 0751;PADEL CLUB DE L'ALBIGEOIS;-695;6016;1982;
;8539;PILTE;Laurene;FRA;4774464;7;Non;1;CENTRE VAL DE LOIRE;53 45 0456;T.C. DE SAINT DENIS EN VAL;-356;6343;1995;
;8539;;;FRA;6439095;7;Non;1;HAUTS DE FRANCE;56 L0 0590;COMITE NORD TENNIS;0;8539;2005;
;8539;PYTA;Isaline;FRA;6439095;7;Non;1;HAUTS DE FRANCE;56 L0 0590;COMITE NORD TENNIS;0;8539;2005;
;8539;SALVADOR;Margot;FRA;3274749;7;Non;2;OCCITANIE;60 31 8001;4PADEL TOULOUSE COLOMIERS;-356;6563;1993;
;8691;;;FRA;3962729;6;Non;1;PROVENCE ALPES COTE D'AZUR;62 06 0315;TC ACACIAS;0;8691;1964;
;8691;ADJADJ;Sylvie;FRA;3962729;6;Non;1;PROVENCE ALPES COTE D'AZUR;62 06 0315;TC ACACIAS;0;8691;1964;
;8691;CHATELET;Marina;FRA;3399601;6;Non;1;REUNION - MAYOTTE;67 97 8003;HANGAR;-356;7040;1991;
;8691;GELIN;Armonie;FRA;3141551;6;Non;2;NOUVELLE AQUITAINE;59 17 0372;SAUJON TC;-508;5926;1996;
;8691;GOUBAULT;Sylvie;FRA;5904521;6;Non;1;NOUVELLE CALEDONIE;66 97 0001;T.C. DU MONT COFFYN;-356;2154;1965;
;8691;GROLEAZ;Lisa;FRA;3839028;6;Non;1;AUVERGNE RHONE-ALPES;50 38 8002;OLYMPIA SPORTS;-356;8000;2001;
;8691;LANNES;Muriel;FRA;3900919;6;Non;2;NOUVELLE AQUITAINE;59 64 8006;GRAINS DE SPORT;-356;8335;1984;
;8691;LARZUL;Justine;FRA;2980167;6;Non;1;NOUVELLE AQUITAINE;59 17 0017;ROCHEFORT SA;-2968;3757;1991;
;8691;;;FRA;3527348;6;Non;2;OCCITANIE;60 31 0329;L'HERS TENNIS CLUB;-356;7977;1970;
;8691;LE DANTEC;Valerie;FRA;3527348;6;Non;2;OCCITANIE;60 31 0329;L'HERS TENNIS CLUB;-356;7977;1970;
;8691;LESAFFRE;Aurelie;FRA;719698;6;Non;1;PROVENCE ALPES COTE D'AZUR;62 83 0081;TC TOULONNAIS;-1604;3206;1984;
;8691;PELTIER;Charlotte;FRA;3142635;6;Non;1;GRAND EST;55 10 0085;TC RIVIERE DE CORPS;-356;6115;2001;
;8691;PIERMONT;Anne;FRA;4666307;6;Non;2;OCCITANIE;60 30 0337;TC MANDUEL;-356;8000;1974;
;8691;;;FRA;324861;6;Non;1;OCCITANIE;60 30 8004;SOCCER TEAM Alès;0;8691;1970;
;8691;RANCHOUX;christel;FRA;324861;6;Non;1;OCCITANIE;60 30 8004;SOCCER TEAM Alès;0;8691;1970;
;8691;SALQUES;Alice;FRA;5657193;6;Non;2;AUVERGNE RHONE-ALPES;50 42 0130;VILLARS (TENNIS CLUB DE);890;7053;1998;
;8877;BALESTE;Nadia;FRA;3323302;5;Non;2;NOUVELLE AQUITAINE;59 33 8002;PADEL TOUCH BASSIN D'ARCACHON;-368;6932;1967;
;8877;BAYEUX;Lucie;FRA;639923;5;Non;1;OCCITANIE;60 30 8001;LE HANGAR;-368;8509;1999;
@ -12051,10 +12051,10 @@
;8877;CASSET;Adele;FRA;2674394;5;Non;1;OCCITANIE;60 30 8001;LE HANGAR;-368;2541;1977;
;8877;DA SILVA;Julie;FRA;2040335;5;Non;1;OCCITANIE;60 66 8002;SQUASH CLUB PARC DUCUP;-368;8509;1980;
;8877;DELARBRE;Léa;FRA;6877098;5;Non;1;GRAND EST;55 67 0109;TC VENDENHEIM;-368;8509;1976;
;8877;;;FRA;3872068;5;Non;1;NOUVELLE AQUITAINE;59 33 0723;BIG PADEL;0;8877;1995;
;8877;;;FRA;3896803;5;Non;1;OCCITANIE;60 31 8001;4PADEL TOULOUSE COLOMIERS;0;8877;1977;
;8877;DELAS;Marie;FRA;3872068;5;Non;1;NOUVELLE AQUITAINE;59 33 0723;BIG PADEL;0;8877;1995;
;8877;Dias;Eloy;FRA;3896803;5;Non;1;OCCITANIE;60 31 8001;4PADEL TOULOUSE COLOMIERS;0;8877;1977;
;8877;GARNIER;Berenice;FRA;1238877;5;Non;1;CENTRE VAL DE LOIRE;53 L0 0370;COMITE INDRE ET LOIRE TENNIS;-368;7170;2001;
;8877;;;FRA;3229581;5;Non;1;ILE DE FRANCE;57 78 0072;T.C. CONFLANS STE HONORINE;-368;7692;1971;
;8877;GAUVAIN;Geraldine;FRA;3229581;5;Non;1;ILE DE FRANCE;57 78 0072;T.C. CONFLANS STE HONORINE;-368;7692;1971;
;8877;IONESCU;Raluca;FRA;3164059;5;Non;2;OCCITANIE;60 34 0009;ASPTT MONTPELLIER;-368;6246;1982;
;8877;LEURENT-GUILLOU;Annabelle;FRA;3406028;5;Non;1;PAYS DE LA LOIRE;61 72 8002;URBAN PADEL LE MANS;-368;7472;1980;
;8877;LOPEZ;Ethan;FRA;3449334;5;Non;1;PROVENCE ALPES COTE D'AZUR;62 83 0032;TC DE BANDOL;-368;7472;2014;
@ -12062,27 +12062,27 @@
;8877;MEYNADIER;Victoire;FRA;3403852;5;Non;1;HAUTS DE FRANCE;56 L0 0590;COMITE NORD TENNIS;-368;7170;2002;
;8877;MORIS;Sandrine;FRA;2634483;5;Non;1;OCCITANIE;60 34 0023;TENNIS PADEL MAUGUIO;-368;5350;1975;
;8877;MORLANS;Daphné;FRA;1917608;5;Non;1;NOUVELLE AQUITAINE;59 33 8002;PADEL TOUCH BASSIN D'ARCACHON;-1033;2148;1975;
;8877;;;FRA;3980313;5;Non;2;OCCITANIE;60 81 0514;CASTRES T.C DES CEDRES;0;8877;1989;
;8877;NEGRE;Mathilde;FRA;3980313;5;Non;2;OCCITANIE;60 81 0514;CASTRES T.C DES CEDRES;0;8877;1989;
;8877;NEJJARI;Beaja;FRA;3254965;5;Non;1;OCCITANIE;60 81 0042;TENNIS CLUB ALBI PADEL;-796;5777;1980;
;8877;;;FRA;9222497;5;Non;1;HAUTS DE FRANCE;56 80 0039;TENNIS CLUB AMIENS METROPOLE;0;8877;1997;
;8877;PINCHON;Camille;FRA;9222497;5;Non;1;HAUTS DE FRANCE;56 80 0039;TENNIS CLUB AMIENS METROPOLE;0;8877;1997;
;8877;PITCHER;Michele;FRA;1177974;5;Non;1;OCCITANIE;60 34 0463;TENNIS & PADEL CLUB DE CASTRIES;-368;5740;1959;
;8877;POISSE;Garance;FRA;3427959;5;Non;1;AUVERGNE RHONE-ALPES;50 38 8004;SAS KYPH MOOVING ARENA;-368;7472;2003;
;8877;;;FRA;3486732;5;Non;1;BRETAGNE;52 56 8002;ARENA 18;-368;7692;1997;
;8877;;;FRA;3941826;5;Non;1;PROVENCE ALPES COTE D'AZUR;62 L0 0050;COMITE HAUTES ALPES TENNIS;0;8877;1992;
;8877;QUÉRO;Soazig;FRA;3486732;5;Non;1;BRETAGNE;52 56 8002;ARENA 18;-368;7692;1997;
;8877;ROLLAND;sarah;FRA;3941826;5;Non;1;PROVENCE ALPES COTE D'AZUR;62 L0 0050;COMITE HAUTES ALPES TENNIS;0;8877;1992;
;8877;SEMEL;Christel;FRA;3768685;5;Non;1;REUNION - MAYOTTE;67 97 0020;TENNIS CLUB SAINT-PIERRE;-368;5846;1969;
;8877;SHEVEREVA;Tania;UKR;3409947;5;Non;1;ILE DE FRANCE;57 L0 0750;TC COMITE DE PARIS;-368;7170;1991;
;8877;SUPERSAC;Beatrice;FRA;2966019;5;Non;1;NOUVELLE AQUITAINE;59 64 8003;PADEL FACTORY;-796;5467;1969;
;8877;VETTER;Lina;AUT;3409333;5;Non;1;NOUVELLE AQUITAINE;59 24 0037;CLUB ATHLETIQUE RIBERACOIS SECTION TENNIS;-368;7170;2004;
;9229;BARWICK;Nikki;FRA;3600505;4;Non;1;PROVENCE ALPES COTE D'AZUR;62 83 0111;TC PUGETOIS;-406;8458;1987;
;9229;;;FRA;3927883;4;Non;1;PROVENCE ALPES COTE D'AZUR;62 83 0081;TC TOULONNAIS;0;9229;2000;
;9229;BOURRILLON;Lou-anne;FRA;3927883;4;Non;1;PROVENCE ALPES COTE D'AZUR;62 83 0081;TC TOULONNAIS;0;9229;2000;
;9229;CAPIA;Lisandra;FRA;3337255;4;Non;2;CORSE;54 2A 0602;MEZZAVIA TENNIS CLUB;-406;6682;1991;
;9229;COIFFET;Rejane;FRA;4390642;4;Non;1;REUNION - MAYOTTE;67 97 8001;ENDEMIK CLUB;-406;8823;1966;
;9229;COUARCH;Eleonore;FRA;3212189;4;Non;2;GRAND EST;55 54 0513;JARVILLE TSB;-1745;5397;1981;
;9229;;;FRA;3997050;4;Non;1;OCCITANIE;60 34 0184;ASCH MONTPELLIER;0;9229;1983;
;9229;DARRENOUGUE;Laura;FRA;3997050;4;Non;1;OCCITANIE;60 34 0184;ASCH MONTPELLIER;0;9229;1983;
;9229;LOURDIN;Emma;FRA;699013;4;Non;2;GRAND EST;55 54 0513;JARVILLE TSB;-149;9080;1982;
;9229;NIELSEN;Marine;FRA;2516194;4;Non;1;BOURGOGNE FRANCHE COMTE;51 89 0048;SAINT FLORENTIN E.S.;-720;2169;1988;
;9229;;;FRA;3494488;4;Non;1;PROVENCE ALPES COTE D'AZUR;62 83 8002;PAD AND BALL FREJUS;0;9229;1986;
;9229;;;FRA;2560181;4;Non;1;AUVERGNE RHONE-ALPES;50 69 0033;MUNICIPAL 5EME (TENNIS CLUB);-406;7977;1989;
;9229;PANICUCCI;Damia;FRA;3494488;4;Non;1;PROVENCE ALPES COTE D'AZUR;62 83 8002;PAD AND BALL FREJUS;0;9229;1986;
;9229;PHAM;EVY;FRA;2560181;4;Non;1;AUVERGNE RHONE-ALPES;50 69 0033;MUNICIPAL 5EME (TENNIS CLUB);-406;7977;1989;
;9229;ROCHETTE;Aurelie;FRA;2649674;4;Non;1;BOURGOGNE FRANCHE COMTE;51 21 0151;NORGES LES BOIS T.C.;-406;2789;1985;
;9229;SAINTE CLUQUE;Cécile;FRA;5153827;4;Non;1;OCCITANIE;60 11 0002;ASS NARBONNAISE DE TENNIS;-406;7764;1972;
;9229;VACHOT-INUKAI;Reiko;FRA;3557858;4;Non;1;AUVERGNE RHONE-ALPES;50 69 0612;PADEL SHOT LYON;-406;8133;1965;
@ -12090,65 +12090,65 @@
;9229;VANNUCCHI;Barbara;FRA;5217481;4;Non;1;NOUVELLE AQUITAINE;59 64 8001;RUGBY PARK 64;-406;5956;1975;
;9229;VERNHES;Virginie;FRA;3285208;4;Non;1;OCCITANIE;60 12 0749;PADEL TOLOSA RODEZ ACADEMY;-406;6510;1970;
;9229;VIARD;Gwenaelle;FRA;3515178;4;Non;1;OCCITANIE;60 12 0012;RODEZ TENNIS PADEL;-406;8133;1994;
;9229;;;FRA;3592408;4;Non;1;AUVERGNE RHONE-ALPES;50 38 0591;GRENOBLE TENNIS;0;9229;1994;
;9229;VINCENT;justine;FRA;3592408;4;Non;1;AUVERGNE RHONE-ALPES;50 38 0591;GRENOBLE TENNIS;0;9229;1994;
;9510;ARGENCE;Nais;FRA;291221;3;Non;1;ILE DE FRANCE;57 75 0046;CHAMPIONNET SPORTS;-430;8377;2007;
;9510;BAUDY;Christina;FRA;2675225;3;Non;1;NOUVELLE AQUITAINE;59 40 8004;BLUE PADEL;-430;7420;1983;
;9510;;;FRA;3923907;3;Non;1;NOUVELLE AQUITAINE;59 33 8010;UCPA SPORT STATION BORDEAUX;0;9510;1991;
;9510;BONNEMENT;Swann;FRA;3923907;3;Non;1;NOUVELLE AQUITAINE;59 33 8010;UCPA SPORT STATION BORDEAUX;0;9510;1991;
;9510;CALASNIVES;Marie;FRA;2597989;3;Non;1;NOUVELLE AQUITAINE;59 64 0149;PAU TC;-430;1606;1998;
;9510;;;FRA;572190;3;Non;1;PROVENCE ALPES COTE D'AZUR;62 83 0035;TC DRACENOIS;0;9510;1964;
;9510;;;FRA;3945178;3;Non;1;PROVENCE ALPES COTE D'AZUR;62 L0 0050;COMITE HAUTES ALPES TENNIS;0;9510;1993;
;9510;CARPENTIER;Jackie;FRA;572190;3;Non;1;PROVENCE ALPES COTE D'AZUR;62 83 0035;TC DRACENOIS;0;9510;1964;
;9510;CHAPPA;Julie;FRA;3945178;3;Non;1;PROVENCE ALPES COTE D'AZUR;62 L0 0050;COMITE HAUTES ALPES TENNIS;0;9510;1993;
;9510;CRAYSSAC;Anne;FRA;3323194;3;Non;1;PROVENCE ALPES COTE D'AZUR;62 06 0023;ALL IN TENNIS PADEL COUNTRY CLUB DE GRASSE;-430;7420;1979;
;9510;DEL SOL;Marion;FRA;2325677;3;Non;1;BRETAGNE;52 35 0073;RENNES GARDEN TENNIS CLUB;-2717;1498;1965;
;9510;DUBOIS;Julie;FRA;3302130;3;Non;1;NOUVELLE AQUITAINE;59 17 8001;ATLANTIC STADIUM MULTISPORTS;-430;6428;1999;
;9510;;;FRA;3949005;3;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 8011;CAMARG'ANIM;0;9510;1978;
;9510;EZZOUAOUI;Sema;FRA;3949005;3;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 8011;CAMARG'ANIM;0;9510;1978;
;9510;FAISANT;Clara;FRA;288942;3;Non;1;ILE DE FRANCE;57 94 0510;SPORT HORIZON;-430;6682;1997;
;9510;FARGANEL;Rachel;FRA;3125396;3;Non;1;PROVENCE ALPES COTE D'AZUR;62 06 0023;ALL IN TENNIS PADEL COUNTRY CLUB DE GRASSE;-430;6185;1970;
;9510;FROUNTIL;Isabelle;FRA;3370113;3;Non;1;NOUVELLE AQUITAINE;59 33 0723;BIG PADEL;-430;7420;1964;
;9510;FURRER;Erika;FRA;3595638;3;Non;1;PROVENCE ALPES COTE D'AZUR;62 06 0195;AGASC TC MONTALEIGNE;-430;9080;1983;
;9510;;;FRA;3865559;3;Non;1;ILE DE FRANCE;57 78 0424;T.C. VOISINS LE BRETONNEUX;0;9510;1980;
;9510;GALLAS;Maelle;FRA;3865559;3;Non;1;ILE DE FRANCE;57 78 0424;T.C. VOISINS LE BRETONNEUX;0;9510;1980;
;9510;HANNIER;Camille;FRA;5466137;3;Non;1;GRAND EST;55 57 8001;4PADEL METZ SUD;-430;6682;1999;
;9510;HEUSS;Mélissa;FRA;6210461;3;Non;1;GRAND EST;55 51 0078;TC GEO ANDRE;-430;8377;1994;
;9510;HORNOY;Alizee;FRA;529750;3;Non;1;REUNION - MAYOTTE;67 97 0086;BOURBON BRISANTS BEACH TENNIS;-430;6384;1988;
;9510;;;FRA;191283;3;Non;1;REUNION - MAYOTTE;67 97 8001;ENDEMIK CLUB;0;9510;1978;
;9510;;;FRA;3624408;3;Non;1;GRAND EST;55 67 8000;4PADEL STRASBOURG;0;9510;1994;
;9510;JULIEN;Jessica;FRA;191283;3;Non;1;REUNION - MAYOTTE;67 97 8001;ENDEMIK CLUB;0;9510;1978;
;9510;LAURENT;Chloée;FRA;3624408;3;Non;1;GRAND EST;55 67 8000;4PADEL STRASBOURG;0;9510;1994;
;9510;LESCURE;Maryline;FRA;3862482;3;Non;1;REUNION - MAYOTTE;67 97 8001;ENDEMIK CLUB;-430;9080;1983;
;9510;LONGUET GRAS;Laure;FRA;802035;3;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 8004;COUNTRY CLUB PADEL;-430;2949;1975;
;9510;MORTIER;Veronique;FRA;2771847;3;Non;1;PAYS DE LA LOIRE;61 44 8005;URBAN SOCCER CARQUEFOU;-430;7420;1974;
;9510;;;FRA;3945917;3;Non;1;PROVENCE ALPES COTE D'AZUR;62 L0 0050;COMITE HAUTES ALPES TENNIS;0;9510;1987;
;9510;;;FRA;4734288;3;Non;1;MARTINIQUE;65 97 0047;A.S. COUNTRY CLUB DE SCHOELCHER;0;9510;1994;
;9510;SOUPENE;Anne camille;FRA;3945917;3;Non;1;PROVENCE ALPES COTE D'AZUR;62 L0 0050;COMITE HAUTES ALPES TENNIS;0;9510;1987;
;9510;TASSIN;Claire;FRA;4734288;3;Non;1;MARTINIQUE;65 97 0047;A.S. COUNTRY CLUB DE SCHOELCHER;0;9510;1994;
;9510;TORREGROSSA;Prescillia;FRA;3928573;3;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 8019;LB13 PADEL TENNIS CLUB;-430;9080;1989;
;9510;VANEXEM;Carolina;FRA;3243609;3;Non;1;PROVENCE ALPES COTE D'AZUR;62 06 0372;TC EZE;-687;6490;1976;
;9510;VIGNERES;Emilie;FRA;3243786;3;Non;1;OCCITANIE;60 65 0743;LEGEND PADEL;-430;6682;1999;
;9510;;;FRA;3166365;3;Non;1;OCCITANIE;60 81 0752;PADEL TOLOSA ALBI ACADEMY;-430;8220;1977;
;9510;;;FRA;7323158;3;Non;1;OCCITANIE;60 34 0301;TC VAILHAUQUES;0;9510;1977;
;9824;;;ESP;3514668;2;Non;1;OCCITANIE;60 31 0801;TOP PADEL PLAISANCE;-459;8472;1980;
;9824;;;FRA;3238523;2;Non;1;HAUTS DE FRANCE;56 80 0039;TENNIS CLUB AMIENS METROPOLE;0;9824;1997;
;9510;VIRAZELS;Béatrice;FRA;3166365;3;Non;1;OCCITANIE;60 81 0752;PADEL TOLOSA ALBI ACADEMY;-430;8220;1977;
;9510;WAGNER;Ban;FRA;7323158;3;Non;1;OCCITANIE;60 34 0301;TC VAILHAUQUES;0;9510;1977;
;9824;ALONSO;Raquel;ESP;3514668;2;Non;1;OCCITANIE;60 31 0801;TOP PADEL PLAISANCE;-459;8472;1980;
;9824;ATEK;melissa;FRA;3238523;2;Non;1;HAUTS DE FRANCE;56 80 0039;TENNIS CLUB AMIENS METROPOLE;0;9824;1997;
;9824;BARIL;Emilie;FRA;9216413;2;Non;1;NOUVELLE AQUITAINE;59 17 0017;ROCHEFORT SA;-459;9365;1992;
;9824;;;FRA;3973387;2;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 8017;LE COMPLEXE PADEL;0;9824;1986;
;9824;BELLONE;LAURE;FRA;3973387;2;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 8017;LE COMPLEXE PADEL;0;9824;1986;
;9824;BERTRAND;Marielle;FRA;3584540;2;Non;1;GRAND EST;55 67 8000;4PADEL STRASBOURG;-459;8631;1969;
;9824;;;FRA;461579;2;Non;1;OCCITANIE;60 81 0751;PADEL CLUB DE L'ALBIGEOIS;-459;8472;1997;
;9824;CHRISTINA;Mélissa;FRA;461579;2;Non;1;OCCITANIE;60 81 0751;PADEL CLUB DE L'ALBIGEOIS;-459;8472;1997;
;9824;CRAVINO;Catherine;FRA;7807769;2;Non;1;PROVENCE ALPES COTE D'AZUR;62 06 0023;ALL IN TENNIS PADEL COUNTRY CLUB DE GRASSE;-459;8631;1969;
;9824;;;FRA;3975430;2;Non;1;HAUTS DE FRANCE;56 59 8004;4PADEL VALENCIENNES;0;9824;2000;
;9824;;;FRA;5668275;2;Non;1;OCCITANIE;60 30 8003;MAM'SOCCER FIVE PADEL;0;9824;1999;
;9824;DEHAINAUT;juliette;FRA;3975430;2;Non;1;HAUTS DE FRANCE;56 59 8004;4PADEL VALENCIENNES;0;9824;2000;
;9824;FERRAZ;Laura;FRA;5668275;2;Non;1;OCCITANIE;60 30 8003;MAM'SOCCER FIVE PADEL;0;9824;1999;
;9824;GRONDIN;Vanessa;FRA;2228959;2;Non;1;REUNION - MAYOTTE;67 97 0009;TENNIS CLUB DE LA BAIE DE ST PAUL;-459;6563;1970;
;9824;;;FRA;3635800;2;Non;1;OCCITANIE;60 34 0184;ASCH MONTPELLIER;0;9824;1987;
;9824;HENNION;Faustine;FRA;3635800;2;Non;1;OCCITANIE;60 34 0184;ASCH MONTPELLIER;0;9824;1987;
;9824;HENRY;Charlotte;FRA;3544521;2;Non;2;NOUVELLE AQUITAINE;59 17 8001;ATLANTIC STADIUM MULTISPORTS;-243;9581;1981;
;9824;HUARD;Auxanne;FRA;6317510;2;Non;1;NORMANDIE;58 50 0787;PADEL MAX ST LO;-459;9365;2000;
;9824;JEZEQUEL;Emmy;FRA;5570555;2;Non;1;BRETAGNE;52 22 8003;ARMORIC SPORTS;-459;7659;2001;
;9824;;;FRA;3797678;2;Non;1;OCCITANIE;60 31 8001;4PADEL TOULOUSE COLOMIERS;0;9824;1988;
;9824;;;FRA;3338846;2;Non;1;NOUVELLE AQUITAINE;59 64 8002;CENTRAL SPORT CLUB;-459;8472;1972;
;9824;manzoni;chloe;FRA;3797678;2;Non;1;OCCITANIE;60 31 8001;4PADEL TOULOUSE COLOMIERS;0;9824;1988;
;9824;MONGIS;laetitia;FRA;3338846;2;Non;1;NOUVELLE AQUITAINE;59 64 8002;CENTRAL SPORT CLUB;-459;8472;1972;
;9824;PEREIRA;Sandy;FRA;1557553;2;Non;1;OCCITANIE;60 34 0724;TENNIS PADEL CAP D'AGDE;-459;1816;1979;
;9824;POUGET;Melanie;FRA;2586096;2;Non;1;NOUVELLE AQUITAINE;59 33 8003;SQUASH BAD 33;-459;7245;1990;
;9824;ROBERT;Céline;FRA;3894463;2;Non;1;REUNION - MAYOTTE;67 97 8001;ENDEMIK CLUB;-459;9365;1982;
;9824;;;FRA;3649104;2;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 8014;MONKEY PADEL;0;9824;1975;
;9824;ROUBAUD;Anne;FRA;3649104;2;Non;1;PROVENCE ALPES COTE D'AZUR;62 13 8014;MONKEY PADEL;0;9824;1975;
;9824;RUBAT DU MERAC;Coralie;FRA;3248293;2;Non;2;OCCITANIE;60 65 0744;TARBES PADEL CLUB;-459;7850;1990;
;9824;;;FRA;3976482;2;Non;1;OCCITANIE;60 30 8006;SAS CITY BALL Sernhac;0;9824;1993;
;9824;TAFROUTE;ibtissame;FRA;3976482;2;Non;1;OCCITANIE;60 30 8006;SAS CITY BALL Sernhac;0;9824;1993;
;9824;UBEDA;Sabrina;FRA;3862408;2;Non;1;OCCITANIE;60 11 8001;AREA PADEL CLUB;-459;9365;1984;
;9824;VAUZELLE;Stéphanie;FRA;2729213;2;Non;1;OCCITANIE;60 66 0052;CANET 66 TENNIS;-459;4193;1976;
;9824;;;FRA;5486735;2;Non;1;NOUVELLE AQUITAINE;59 79 8001;ARENA PADEL;0;9824;1992;
;9824;VICTOR;Rebecca;FRA;5486735;2;Non;1;NOUVELLE AQUITAINE;59 79 8001;ARENA PADEL;0;9824;1992;
;10065;ALESSANDRO;Dina;FRA;3900199;1;Non;1;AUVERGNE RHONE-ALPES;50 74 8001;4PADEL ANNEMASSE;-484;9581;1984;
;10065;;;FRA;5808554;1;Non;1;OCCITANIE;60 30 0415;TENNIS CLUB CARREAU DE LANES;0;10065;2001;
;10065;;;FRA;3925576;1;Non;1;PAYS DE LA LOIRE;61 L0 0440;COMITE LOIRE ATLANTIQUE TENNIS;0;10065;1998;
;10065;ANDRIO;PERRINE;FRA;5808554;1;Non;1;OCCITANIE;60 30 0415;TENNIS CLUB CARREAU DE LANES;0;10065;2001;
;10065;Archambaud;Camille;FRA;3925576;1;Non;1;PAYS DE LA LOIRE;61 L0 0440;COMITE LOIRE ATLANTIQUE TENNIS;0;10065;1998;
;10065;AUBIA;Celine;FRA;3360428;1;Non;1;OCCITANIE;60 34 0729;TENNIS PADEL CLUB PALAVAS;-484;7412;1981;
;10065;BANCEL;Manon;FRA;1510851;1;Non;1;AUVERGNE RHONE-ALPES;50 63 0028;CLERMONT U.C.;-484;4879;2002;
;10065;BECERRA;Marta;ESP;3812289;1;Non;1;GRAND EST;55 L0 0670;COMITE BAS RHIN TENNIS;-484;9581;1979;
@ -12159,37 +12159,37 @@
;10065;CASIN;Agnès;FRA;3921276;1;Non;1;NOUVELLE AQUITAINE;59 17 0609;LOIX TENNIS COUARDAIS;-484;9581;1974;
;10065;CHABAUD;Alexandra;FRA;3712983;1;Non;1;PROVENCE ALPES COTE D'AZUR;62 83 0032;TC DE BANDOL;-484;9204;1971;
;10065;CHAPIN;Anthony;FRA;3663312;1;Non;1;PROVENCE ALPES COTE D'AZUR;62 L0 0840;COMITE VAUCLUSE TENNIS;-484;9581;1977;
;10065;;;SUI;3026467;1;Non;1;AUVERGNE RHONE-ALPES;50 01 0086;GEX (TENNIS CLUB DE);-484;8679;1982;
;10065;CHENAB;Leila;SUI;3026467;1;Non;1;AUVERGNE RHONE-ALPES;50 01 0086;GEX (TENNIS CLUB DE);-484;8679;1982;
;10065;COLLET;Caroline;FRA;3212758;1;Non;1;CENTRE VAL DE LOIRE;53 45 0775;SAINT PRYVE TENNIS CLUB;-484;7850;1972;
;10065;;;FRA;2515292;1;Non;1;OCCITANIE;60 L0 0006;CENTRE DE LIGUE PYRENEES;-484;8679;1986;
;10065;;;FRA;2623862;1;Non;1;NOUVELLE AQUITAINE;59 33 8011;PADEL HOUSE;-10065;3237;1978;
;10065;COMBES;Jennifer;FRA;2515292;1;Non;1;OCCITANIE;60 L0 0006;CENTRE DE LIGUE PYRENEES;-484;8679;1986;
;10065;DE BAILLIENCOURT;Segolene;FRA;2623862;1;Non;1;NOUVELLE AQUITAINE;59 33 8011;PADEL HOUSE;-10065;3237;1978;
;10065;DELARGE;Romy;FRA;1737790;1;Non;1;CENTRE VAL DE LOIRE;53 37 0001;SKIN UP ACADEMY;-484;9204;2010;
;10065;DEMAISON;Elina;FRA;3610231;1;Non;1;NOUVELLE AQUITAINE;59 24 0525;BERGERAC TC;-484;4802;1991;
;10065;DEMEURANT;Doriane;FRA;3356225;1;Non;1;BRETAGNE;52 22 0713;TENPAD SAINT-CAST;-484;7412;1998;
;10065;DO PINHAL;Laetitia;FRA;3301287;1;Non;1;OCCITANIE;60 81 0514;CASTRES T.C DES CEDRES;-484;6767;1998;
;10065;;;FRA;7316215;1;Non;1;OCCITANIE;60 34 8010;PADEL SAUVIAN SUD MEDITERRANEE;0;10065;2000;
;10065;DUPRE;Mathilde;FRA;7316215;1;Non;1;OCCITANIE;60 34 8010;PADEL SAUVIAN SUD MEDITERRANEE;0;10065;2000;
;10065;FASSI;Vanessa;FRA;3163119;1;Non;1;PROVENCE ALPES COTE D'AZUR;62 83 0035;TC DRACENOIS;-484;8840;1985;
;10065;FERRANDO;Laurie;FRA;2829301;1;Non;1;OCCITANIE;60 30 8001;LE HANGAR;-484;3757;1988;
;10065;GAILLARD;Céline;FRA;3209186;1;Non;1;OCCITANIE;60 31 8001;4PADEL TOULOUSE COLOMIERS;-484;4300;1979;
;10065;GERAUDEL;Camille;FRA;5774453;1;Non;1;GRAND EST;55 10 8002;PALATIUM;-484;9581;1994;
;10065;;;FRA;229840;1;Non;1;NOUVELLE AQUITAINE;59 64 8003;PADEL FACTORY;0;10065;1997;
;10065;GOUVENOU;Clemence;FRA;229840;1;Non;1;NOUVELLE AQUITAINE;59 64 8003;PADEL FACTORY;0;10065;1997;
;10065;JAOUEN;Dorian;FRA;3861979;1;Non;1;GRAND EST;55 68 8001;4PADEL MULHOUSE;-484;9581;1998;
;10065;JAUME;Nathalie;FRA;121507;1;Non;1;HAUTS DE FRANCE;56 60 0266;AGNETZ ASSOCIATION SPORTIVE;-2581;5397;1973;
;10065;;;FRA;3974889;1;Non;1;CENTRE VAL DE LOIRE;53 28 0772;RAZE ACADEMY;0;10065;1991;
;10065;;;FRA;3486462;1;Non;1;OCCITANIE;60 65 8001;CENTRE DEPARTEMENTAL DE PADEL DE HORGUES;-484;8679;1993;
;10065;KESSAS;Hayat;FRA;3974889;1;Non;1;CENTRE VAL DE LOIRE;53 28 0772;RAZE ACADEMY;0;10065;1991;
;10065;KILBURG;Sarah;FRA;3486462;1;Non;1;OCCITANIE;60 65 8001;CENTRE DEPARTEMENTAL DE PADEL DE HORGUES;-484;8679;1993;
;10065;LALANNE;Sandrine;FRA;1263318;1;Non;1;OCCITANIE;60 11 0694;PADEL CLUB CARCASSONNE;-484;7053;1970;
;10065;;;FRA;3843252;1;Non;1;ILE DE FRANCE;57 78 0218;T.C. DES LOGES ST GERMAIN;0;10065;1981;
;10065;LAM;MARILYNE;FRA;3843252;1;Non;1;ILE DE FRANCE;57 78 0218;T.C. DES LOGES ST GERMAIN;0;10065;1981;
;10065;LAPERE;Charlotte;FRA;3301293;1;Non;1;NOUVELLE CALEDONIE;66 97 0001;T.C. DU MONT COFFYN;-484;6767;1989;
;10065;LAVOCAT;Elise;FRA;2983234;1;Non;1;BOURGOGNE FRANCHE COMTE;51 89 0048;SAINT FLORENTIN E.S.;-484;7412;1993;
;10065;;;FRA;3761640;1;Non;1;ILE DE FRANCE;57 77 0353;LESIGNY USC;-484;8679;2012;
;10065;;;FRA;3957566;1;Non;1;OCCITANIE;60 82 0733;PADEL TOLOSA MONTAUBAN ACADEMIE;0;10065;1977;
;10065;LE JEUNE;Chloé;FRA;3761640;1;Non;1;ILE DE FRANCE;57 77 0353;LESIGNY USC;-484;8679;2012;
;10065;LECHAT;NATACHA;FRA;3957566;1;Non;1;OCCITANIE;60 82 0733;PADEL TOLOSA MONTAUBAN ACADEMIE;0;10065;1977;
;10065;LEDRU D'ORAZIO;Frédérique;FRA;3898118;1;Non;1;AUVERGNE RHONE-ALPES;50 69 0613;ALL IN COUNTRY CLUB DECINES;-484;9581;1976;
;10065;MALCHEVA;Marina;FRA;3201914;1;Non;1;PROVENCE ALPES COTE D'AZUR;62 83 0081;TC TOULONNAIS;-484;6681;1996;
;10065;;;FRA;376370;1;Non;1;OCCITANIE;60 30 8006;SAS CITY BALL Sernhac;0;10065;2000;
;10065;MARTIN;Lisa;FRA;376370;1;Non;1;OCCITANIE;60 30 8006;SAS CITY BALL Sernhac;0;10065;2000;
;10065;PITOUN;Estelle;FRA;238864;1;Non;1;ILE DE FRANCE;57 93 8001;4PADEL MARVILLE;-484;8453;1999;
;10065;PROUVOST;Sophie;FRA;1253806;1;Non;1;OCCITANIE;60 11 0694;PADEL CLUB CARCASSONNE;-484;6681;2007;
;10065;;;FRA;3254687;1;Non;1;OCCITANIE;60 66 0357;PADEL CLUB DU MAS;0;10065;1973;
;10065;;;FRA;3884457;1;Non;1;PROVENCE ALPES COTE D'AZUR;62 06 0372;TC EZE;0;10065;1979;
;10065;RIERA;Sandrine;FRA;3254687;1;Non;1;OCCITANIE;60 66 0357;PADEL CLUB DU MAS;0;10065;1973;
;10065;SALTI;Marjorie;FRA;3884457;1;Non;1;PROVENCE ALPES COTE D'AZUR;62 06 0372;TC EZE;0;10065;1979;
;10065;SPINELLI;Carole;FRA;2087548;1;Non;1;PROVENCE ALPES COTE D'AZUR;62 06 0305;TC CARROS;-484;4543;1985;
;10065;URRUTY;Camille;FRA;3421709;1;Non;1;OCCITANIE;60 65 0743;LEGEND PADEL;-484;8453;2001;
;10065;VILLAIN;Linda;FRA;2923392;1;Non;1;GRAND EST;55 10 0009;ESPERANCE NOGENT SUR SEINE;-484;8453;1982;

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

@ -82,6 +82,26 @@ nav a {
font-weight: 600;
}
nav a.orange {
color: white;
background-color: #f39200;
}
nav a.orange:hover {
color: #1b223a; /* Same text color on hover */
text-decoration: none; /* Prevent underline on <a> hover */
}
nav a.red {
color: white;
background-color: #e84038;
}
nav a.red:hover {
color: white; /* Same text color on hover */
background-color: #1b223a;
}
hr {
margin: 2px 0px;
}
@ -140,14 +160,19 @@ tr {
}
.rounded-button {
background-color: #f39200; /* Green background */
color: white; /* White text */
background-color: #fae7ce; /* Green background */
color: #707070; /* White text */
padding: 15px 32px; /* Some padding */
font-size: 1em;
font-weight: 800;
cursor: pointer; /* Add a mouse pointer on hover */
border-radius: 16px; /* Rounded corners */
}
.rounded-button:hover {
background-color: #f39200; /* Same background color on hover */
color: white; /* Same text color on hover */
text-decoration: none; /* Prevent underline on <a> hover */
}
.numbers {
font-feature-settings: "tnum";
@ -327,6 +352,10 @@ tr {
color: #f39200;
}
.qualified {
color: #f9d348;
}
.ws {
font-family: "Montserrat-SemiBold";
/* text-align: right; */
@ -343,7 +372,7 @@ tr {
}
.red {
background-color: red;
background-color: #e84038;
}
svg {
@ -755,3 +784,75 @@ h-margin {
background-color: #6c757d;
color: white;
}
.styled-link {
text-decoration: underline; /* Ensures the link is underlined */
color: #f39200; /* Use your main color variable if defined */
font-weight: bold; /* Optional: To make the link more prominent */
}
.styled-link:hover {
color: #f39200; /* Optional: Define a hover color */
text-decoration: none; /* Optional: Remove underline on hover */
}
.sup {
font-size: x-small;
vertical-align: super;
}
.alert {
color: #e84038; /* Make the text red */
font-weight: bold; /* Optional: Make the text bold */
}
.destructive-button {
background-color: #ff4d4d; /* Red background */
color: white; /* White text */
}
.destructive-button:hover {
background-color: #cc0000; /* Darker red on hover */
color: white; /* White text on hover */
}
.download-button {
margin-right: 6px;
color: #1a223a;
padding: 8px 12px;
background-color: white;
border-radius: 12px;
text-decoration: none;
font-size: 12px;
font-weight: 600;
}
.download-button:hover {
color: orange;
}
.match-result a {
text-decoration: none;
color: inherit;
display: block;
padding: 2px 8px;
border-radius: 6px;
}
.match-result a:hover {
background-color: #fae7ce;
color: #707070;
}
.single-line {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.group-stage-link {
text-decoration: none;
color: inherit;
}
.group-stage-link:hover {
color: #f39200; /* Or whatever hover color you prefer */
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 758 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

@ -0,0 +1,79 @@
{% extends 'tournaments/base.html' %}
{% block head_title %} Mon Compte {% endblock %}
{% block first_title %} Mon Compte Padel Club {% endblock %}
{% block second_title %} {{ user.first_name }} {{ user.last_name }} {% endblock %}
{% block content %}
<nav class="margin10">
<a href="{% url 'index' %}" class="orange">Accueil</a>
<a href="{% url 'clubs' %}" class="orange">Clubs</a>
{% if user.is_authenticated %}
<a href="{% url 'my-tournaments' %}" class="orange">Mes tournois</a>
<a href="{% url 'profile' %}">Mon compte</a>
<a href="{% url 'logout' %}" class="red">Se déconnecter</a>
{% else %}
<a href="{% url 'login' %}">Se connecter</a>
{% endif %}
</nav>
{% load static %}
{% load tz %}
<div class="cell medium-6 large-6 topblock my-block">
<div class="bubble">
<label class="title">Mes informations</label>
{% if form.errors %}
<div class="alert">
{% for field, errors in form.errors.items %}
{% for error in errors %}
<p>{{ error }}</p>
{% endfor %}
{% endfor %}
</div>
{% endif %}
<!-- Add non-field errors (if any) -->
{% if form.non_field_errors %}
<div class="alert">
{% for error in form.non_field_errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="rounded-button">Sauver les changements</button>
</form>
</div>
</div>
<div class="cell medium-6 large-6 topblock my-block">
<div class="bubble">
<label class="title">Mot de passe</label>
{% if password_change_form.errors %}
<div class="alert">
{% for field, errors in password_change_form.errors.items %}
{% for error in errors %}
<p>{{ error }}</p>
{% endfor %}
{% endfor %}
</div>
{% endif %}
{% if password_change_form.non_field_errors %}
<div class="alert">
{% for error in password_change_form.non_field_errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<form method="post" action="{% url 'password_change' %}">
{% csrf_token %}
{{ password_change_form.as_p }}
<button type="submit" class="rounded-button">Modifier le mot de passe</button>
</form>
</div>
</div>
{% endblock %}

@ -0,0 +1,140 @@
{% extends 'tournaments/base.html' %}
{% block head_title %}{{ tournament.display_name }} : Informations{% endblock %}
{% block first_title %}{{ tournament.event.display_name }}{% endblock %}
{% block second_title %}{{ tournament.display_name }}{% endblock %}
{% block content %}
{% load static %}
{% load tz %}
<nav class="margin10">
<a href="{% url 'tournament-info' tournament.id %}" class="topmargin5">Informations</a>
</nav>
<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="bubble">
{% if registration_successful %}
<p>Merci, l'inscription a bien été envoyée au juge-arbitre.</p>
<p style="text-align: justify;">
Un email de confirmation a été envoyé à {{ user.email }} pour confirmer votre inscription. Pensez à vérifier vos spams si vous ne recevez pas l'email. En cas de problème, contactez le juge-arbitre.
</p>
{% else %}
<form method="post">
{% csrf_token %}
<!-- Team Registration Form -->
<div>
<p>
<div class="semibold">
Informations de contact
</div>
</p>
{{ team_form.as_p }} <!-- Render team registration form fields here -->
</div>
<!-- Show players added to the team only if there are players added -->
{% if current_players %}
<p>
<div class="semibold">
Constitution de votre équipe
</div>
</p>
<ul>
{% for player in current_players %}
<li>{{ player.first_name }} {{ player.last_name }}{% if player.licence_id %} ({{ player.licence_id }}){% endif %}</li>
{% endfor %}
</ul>
{% endif %}
<!-- Add Player Form -->
{% if current_players|length < 2 %}
<div>
{% if current_players|length == 1 %}
<div class="semibold">
Inscrivez votre partenaire
</div>
{% endif %}
{% if current_players|length == 0 and add_player_form.user_without_licence and tournament.license_is_required %}
<div class="semibold">
Une licence est obligatoire pour vous inscrire :
</div>
{% endif %}
{% if tournament.license_is_required %}
{{ add_player_form.licence_id.label_tag }}
{{ add_player_form.licence_id }}
{% endif %}
{% if add_player_form.first_tournament or add_player_form.user_without_licence or tournament.license_is_required is False %}
{% if not add_player_form.user_without_licence and tournament.license_is_required is True %}
<div class="semibold">
Précisez les informations du joueur :
</div>
{% endif %}
{{ add_player_form.first_name.label_tag }}
{{ add_player_form.first_name }}
{{ add_player_form.last_name.label_tag }}
{{ add_player_form.last_name }}
{% if tournament.license_is_required is False %}
{{ add_player_form.licence_id.label_tag }}
{% if tournament.license_is_required is False %}(facultatif){% endif %}
{{ add_player_form.licence_id }}
{% endif %}
{% endif %}
<div class="margin10">
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
</div>
<button type="submit" name="add_player" class="rounded-button">
{% if add_player_form.user_without_licence %}
Confirmer
{% else %}
{% if current_players|length == 0 %}
Confirmer
{% else %}
Ajouter un partenaire
{% endif %}
{% endif %}
</button>
</div>
{% endif %}
<!-- Show players added to the team only if there are players added -->
{% if current_players|length >= tournament.minimum_player_per_team %}
<div class="margin10">
</div>
<div class="semibold margin10">
{% if tournament.get_waiting_list_position == 1 %}
Tournoi complet, {{ tournament.get_waiting_list_position }} équipe en liste d'attente actuellement.
{% elif tournament.get_waiting_list_position > 1 %}
Tournoi complet, {{ tournament.get_waiting_list_position }} équipes en liste d'attente actuellement.
{% elif tournament.get_waiting_list_position == 0 %}
Tournoi complet, vous seriez la première équipe en liste d'attente.
{% endif %}
</div>
<div>
<button type="submit" name="register_team" class="rounded-button">
{% if tournament.get_waiting_list_position < 0 %}
Confirmer l'inscription
{% else %}
Se mettre en liste d'attente
{% endif %}
</button>
</div>
{% endif %}
</form>
{% endif %}
</div>
</div>
</div>
{% endblock %}

@ -0,0 +1,52 @@
<!-- templates/registration/login.html -->
{% extends 'tournaments/base.html' %}
{% block head_title %} Connexion {% endblock %}
{% block first_title %} Padel Club {% endblock %}
{% block second_title %} Connexion {% endblock %}
{% block content %}
{% load static %}
{% load tz %}
<div class="grid-x">
<div class="bubble">
<div class="cell medium-6 large-6 my-block">
{% if form.errors %}
<div class="alert alert-error">
{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
<p>{{ error }}</p>
{% endfor %}
{% endif %}
{% for field in form %}
{% for error in field.errors %}
<p>{{ error }}</p>
{% endfor %}
{% endfor %}
</div>
{% endif %}
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.GET.next }}">
<label for="username">Identifiant ou e-mail </label>
<input type="text" name="username" id="username" required>
<label for="password">Mot de passe :</label>
<input type="password" name="password" id="password" required>
<button type="submit" class="rounded-button">Se connecter</button>
<p>
<a href="{% url 'password_reset' %}" class="styled-link">Mot de passe oublié ?</a>
</p>
</form>
<p>Pas encore de compte ? <a href="{% url 'signup' %}" class="styled-link">Créer le tout de suite !</a></p>
</div>
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
</div>
</div>
{% endblock %}

@ -0,0 +1,51 @@
{% extends 'tournaments/base.html' %}
{% block head_title %} Mes tournois {% endblock %}
{% block first_title %} Mes tournois Padel Club {% endblock %}
{% block second_title %} {{ user.first_name }} {{ user.last_name }} {% endblock %}
{% block content %}
{% include 'tournaments/navigation_base.html' %}
{% load static %}
{% 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 %}
{% else %}
Aucun tournoi à venir
{% 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 %}
{% else %}
Aucun tournoi en cours
{% endif %}
</div>
</div>
<div class="cell medium-6 large-6 topblock my-block">
<div class="bubble">
<label class="title">Vos tournois terminés</label>
{% if ended_tournaments %}
{% for tournament in ended_tournaments %}
{% include 'tournaments/tournament_row.html' %}
{% endfor %}
{% else %}
Aucun tournoi terminé
{% endif %}
</div>
</div>
{% endblock %}

@ -0,0 +1,19 @@
{% extends 'tournaments/base.html' %}
{% block head_title %} Réinitialisation terminée {% endblock %}
{% block first_title %} Padel Club {% endblock %}
{% block second_title %} Réinitialisation terminée {% endblock %}
{% block content %}
<div class="grid-x">
<div class="bubble">
<div class="cell medium-6 large-6 my-block">
<p>
Votre mot de passe a été réinitialisé avec succès. Vous pouvez maintenant vous connecter avec votre nouveau mot de passe.
</p>
<p>
<a href="{% url 'login' %}" class="rounded-button">Se connecter</a>
</p>
</div>
</div>
</div>
{% endblock %}

@ -0,0 +1,47 @@
{% extends 'tournaments/base.html' %}
{% block head_title %} Nouveau mot de passe {% endblock %}
{% block first_title %} Padel Club {% endblock %}
{% block second_title %} Nouveau mot de passe {% endblock %}
{% block content %}
<div class="grid-x">
<div class="bubble">
<div class="cell medium-6 large-6 my-block">
{% if form.errors %}
<div class="alert">
{% for field, errors in form.errors.items %}
{% for error in errors %}
<p>{{ error }}</p>
{% endfor %}
{% endfor %}
</div>
{% endif %}
<!-- Add non-field errors (if any) -->
{% if form.non_field_errors %}
<div class="alert">
{% for error in form.non_field_errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<form method="post">
{% csrf_token %}
<label for="new_password1">Nouveau mot de passe :</label>
<input type="password" name="new_password1" id="new_password1" required>
<label for="new_password2">Confirmer le nouveau mot de passe :</label>
<input type="password" name="new_password2" id="new_password2" required>
<button type="submit" class="rounded-button">Réinitialiser le mot de passe</button>
</form>
<p>
<a href="{% url 'login' %}" class="styled-link">Retour à la connexion</a>
</p>
</div>
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
</div>
</div>
{% endblock %}

@ -0,0 +1,20 @@
{% extends 'tournaments/base.html' %}
{% block head_title %} Demande envoyée {% endblock %}
{% block first_title %} Padel Club {% endblock %}
{% block second_title %} Demande envoyée {% endblock %}
{% block content %}
<div class="grid-x">
<div class="bubble">
<div class="cell medium-6 large-6 my-block">
<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.
</p>
<p>
<a href="{% url 'login' %}" class="styled-link">Retour à la connexion</a>
</p>
</div>
</div>
</div>
{% endblock %}

@ -0,0 +1,11 @@
{% autoescape off %}
Bonjour,
Vous avez demandé une réinitialisation de votre mot de passe. Veuillez cliquer sur le lien suivant pour en choisir un nouveau :
http://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
Si vous n'avez pas fait cette demande, vous pouvez ignorer ce message.
A tout de suite sur Padel Club !
{% endautoescape %}

@ -0,0 +1,43 @@
{% extends 'tournaments/base.html' %}
{% block head_title %} Réinitialisation du mot de passe {% endblock %}
{% block first_title %} Padel Club {% endblock %}
{% block second_title %} Réinitialisation du mot de passe {% endblock %}
{% block content %}
<div class="grid-x">
<div class="bubble">
<div class="cell medium-6 large-6 my-block">
{% if form.errors %}
<div class="alert">
{% for field, errors in form.errors.items %}
{% for error in errors %}
<p>{{ error }}</p>
{% endfor %}
{% endfor %}
</div>
{% endif %}
<!-- Add non-field errors (if any) -->
{% if form.non_field_errors %}
<div class="alert">
{% for error in form.non_field_errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<form method="post" action="{% url 'password_reset' %}">
{% csrf_token %}
<label for="email">Adresse e-mail :</label>
<input type="email" name="email" id="email" required>
<button type="submit" class="rounded-button">Envoyer le lien de réinitialisation</button>
</form>
<p>
<a href="{% url 'login' %}" class="styled-link">Retour à la connexion</a>
</p>
</div>
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
</div>
</div>
{% endblock %}

@ -0,0 +1,46 @@
<!-- templates/registration/signup.html -->
{% extends 'tournaments/base.html' %}
{% block head_title %} Création de compte {% endblock %}
{% block first_title %} Padel Club {% endblock %}
{% block second_title %} Création de compte {% endblock %}
{% block content %}
{% load static %}
{% load tz %}
<div class="grid-x">
<div class="bubble">
<div class="cell medium-6 large-6 my-block">
{% if form.errors %}
<div class="alert">
{% for field, errors in form.errors.items %}
{% for error in errors %}
<p>{{ error }}</p>
{% endfor %}
{% endfor %}
</div>
{% endif %}
<!-- Add non-field errors (if any) -->
{% if form.non_field_errors %}
<div class="alert">
{% for error in form.non_field_errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="rounded-button">Créer votre compte</button>
</form>
</div>
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
</div>
</div>
{% endblock %}

@ -0,0 +1,38 @@
{% extends 'tournaments/base.html' %}
{% block head_title %} Création de compte {% endblock %}
{% block first_title %} Padel Club {% endblock %}
{% block second_title %} Création de compte {% endblock %}
{% block content %}
{% load static %}
{% load tz %}
<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">
<p>Un e-mail de confirmation a été envoyé à :<br>
<strong>{{ user_email }}</strong></p>
<p>Pour finaliser votre inscription, merci de :</p>
<ol>
<li>Vérifier votre boîte de réception (et vos spams si nécessaire)</li>
<li>Cliquer sur le lien de confirmation dans l'e-mail</li>
</ol>
</div>
<div class="button-container">
<a href="{{ next_url }}" class="rounded-button">Continuer</a>
</div>
<div class="support-link">
<p>Un problème ? <a href="mailto:support@padelclub.app" class="styled-link">Contactez-nous</a></p>
</div>
</div>
</div>
<style>
.button-container {
margin-bottom: 20px;
}
</style>
{% endblock %}

@ -1,13 +1,12 @@
<html>
<body>
{% autoescape off %}
Bienvenue {{ user.username }} !
Veuillez cliquer sur le lien suivant pour activer votre compte:
padelclub://{{ domain }}{% url 'activate' uidb64=uid token=token %}
<br><br>Veuillez cliquer sur le lien suivant pour activer votre compte :<br>
<a href="https://{{ domain }}{% url 'activate' uidb64=uid token=token %}">Activer mon compte</a>
Si le lien ne marche pas, cliquer ici :
http://{{ domain }}{% url 'activate' uidb64=uid token=token %}
Une fois votre compte activé, connectez-vous dans l'app.
A tout de suite sur Padel Club !
<br><br>À tout de suite sur <a href="https://padelclub.app">Padel Club</a> !
{% endautoescape %}
</body>
</html>

@ -1,5 +1,7 @@
<!DOCTYPE html>
<html>
<!-- <a href="{% url 'download' %}" class="top-link">Vous êtes juge-arbitre ? Téléchargez l'app pour organiser vos tournois !</a> -->
{% load static %}
<head>
@ -54,7 +56,8 @@
</div>
</a>
</div>
{% block right_header %}{% endblock %}
{% block right_header %}
{% endblock %}
</div>
</header>

@ -12,6 +12,7 @@
<div class="cell medium-6 large-6 topblock my-block">
<div class="bubble">
<div><a href="{% url 'automatic-broadcast' tournament.id %}">Automatique</a></div>
<div><a href="{% url 'broadcasted-prog' tournament.id %}">Programmation</a></div>
<div><a href="{% url 'broadcasted-matches' tournament.id %}">Matchs</a></div>
<div><a href="{% url 'broadcasted-group-stages' tournament.id %}">Poules</a></div>
<div><a href="{% url 'broadcasted-summons' tournament.id %}">Convocations</a></div>

@ -31,6 +31,7 @@
</div>
<div class="table-cell">
<span><a href="{% url 'automatic-broadcast' tournament.id %}">Automatic</a></span> |
<span><a href="{% url 'broadcasted-prog' tournament.id %}">Programmation</a></span> |
<span><a href="{% url 'broadcasted-matches' tournament.id %}">Matchs</a></span> |
<span><a href="{% url 'broadcasted-group-stages' tournament.id %}">Poules</a></span> |
<span><a href="{% url 'broadcasted-summons' tournament.id %}">Convocations</a></span> |

@ -7,12 +7,11 @@
<template x-for="i in group_stage.teams.length">
<div>
<div class="flex">
<div class="flex" :class="group_stage.teams[i-1].qualified ? 'qualified' : ''">
<div class="flex-left">
<template x-for="name in group_stage.teams[i-1].names">
<div class="bold" :class="group_stage.teams[i-1].is_winner ? 'winner' : ''" x-text="name"></div>
<div class="bold" x-text="name"></div>
<!-- <div class="semibold" x-data="{

@ -21,24 +21,31 @@
</div>
<div class="scores">
<template x-for="score in match.teams[i-1].scores">
<span class="score bold w30px" :class="match.teams[i-1].is_winner ? 'winner' : ''" x-text="score"></span>
<span class="score ws"
:class="{
'w35px': score.tiebreak,
'w30px': !score.tiebreak,
'winner': match.teams[i-1].is_winner
}"
>
<span x-text="score.main"></span>
<template x-if="score.tiebreak">
<sup x-text="score.tiebreak"></sup>
</template>
</span>
</template>
<span x-data="{
showWalkOut(match, team) {
let html = ``
if (match.has_walk_out) {
html += `<span class='score bold w60px'>`
if (team.walk_out) html += `WO`
html += `</span>`
}
return html
},
}" x-html="showWalkOut(match, match.teams[i-1])">
}" x-html="showWalkOut(match, match.teams[i-1])">
</span>
</div>
</div>

@ -0,0 +1,100 @@
<!DOCTYPE html>
{% load static %}
{% load qr_code %}
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" href="{% static 'tournaments/css/foundation.min.css' %}" />
<link rel="stylesheet" href="{% static 'tournaments/css/basics.css' %}" />
<link rel="stylesheet" href="{% static 'tournaments/css/style.css' %}" />
<link rel="stylesheet" href="{% static 'tournaments/css/broadcast.css' %}" />
<link rel="icon" type="image/png" href="{% static 'tournaments/images/favicon.png' %}" />
<title>Matchs</title>
<script src="{% static 'tournaments/js/alpine.min.js' %}"></script>
<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["setDocumentTitle", document.domain + "/" + document.title]);
_paq.push(["setDoNotTrack", true]);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//matomo.padelclub.app/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '1']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<!-- End Matomo Code -->
</head>
<body x-data="{
paginatedMatches: null,
active: 1,
retrieveMatches() {
fetch('/tournament/{{ tournament.id }}/prog/json/')
.then(res => res.json())
.then((data) => {
this.paginatedMatches = this.paginate(data, 8)
})
},
paginate(array, pageSize) {
let paginatedArray = [];
for (let i = 0; i < array.length; i += pageSize) {
paginatedArray.push(array.slice(i, i + pageSize));
}
return paginatedArray;
},
loop() {
this.retrieveMatches()
setInterval(() => {
this.retrieveMatches()
this.active = this.active === this.paginatedMatches.length ? 1 : this.active+1
}, 15000)
}
}" x-init="loop()">
<header>
<div id="header">
<div class="left-content bubble">
<img src="{% static 'tournaments/images/PadelClub_logo_512.png' %}" alt="logo" class="logo">
<div class="left-margin">
<h1 class="club">{{ tournament.broadcast_event_display_name }}</h1>
<h1 class="event">Programmation {{ tournament.broadcast_display_name }}</h1>
</div>
</div>
<div class="right-content">{% qr_from_text qr_code_url options=qr_code_options %}</div>
</div>
</header>
<div class="wrapper">
<main>
<div class="grid-x padding-bottom">
<template x-for="i in paginatedMatches.length" >
<template x-for="match in paginatedMatches[i-1]" >
<div class="cell medium-6 large-3 my-block" x-show="active === i">
<template x-if="!match.empty">
{% include 'tournaments/broadcast/broadcasted_match.html' %}
</template>
</div>
</template>
</template>
</div>
</main>
</div>
</body>
</html>

@ -253,7 +253,7 @@
<section class="center topmargin40">
<h4 class="">
<span class="orange">Essai gratuit</span> pour un tournoi
<span class="orange">Essai gratuit</span> pour 3 tournois
</h4>
<span>
et des tarifs adaptés à vos besoins

@ -12,13 +12,22 @@
{% for team in group_stage.teams %}
<div class="flex">
<div class="flex {% if team.qualified %}qualified{% endif %}">
<div class="flex-left">
{% if team.team_registration.id %}
<a href="{% url 'team-details' tournament.id team.team_registration.id %}" class="group-stage-link"> <!-- Add this anchor tag -->
{% endif %}
{% for name in team.names %}
<div class="semibold {% if team.is_winner %}winner{% endif %}">
<div class="semibold">
{{ name }}
</div>
{% endfor %}
{% if team.team_registration.id %}
</a>
{% endif %}
</div>
<div class="flex-right">
{% if group_stage.started %}

@ -11,9 +11,11 @@
<div>
{% for team in match.teams %}
<div class="match-result {% cycle 'bottom-border' '' %}">
<div class="player">
{% if team.id %}
<a href="{% url 'team-details' tournament.id team.id %}" class="player-link"> <!-- Add this anchor tag -->
{% endif %}
{% for name in team.names %}
<div class="semibold {% if team.is_winner %}winner{% endif %}">
{% if name|length > 0 %}
@ -23,12 +25,20 @@
{% endif %}
</div>
{% endfor %}
{% if team.id %}
</a>
{% endif %}
</div>
{% if match.should_show_scores %}
<div class="scores">
{% for score in team.scores %}
<span class="score ws w30px{% if team.is_winner %} winner{% endif %}">{{ score }}</span>
<span class="score ws {% if score.tiebreak %}w35px{% else %}w30px{% endif %}{% if team.is_winner %} winner{% endif %}">
{{ score.main }}
{% if score.tiebreak %}
<sup>{{ score.tiebreak }}</sup>
{% endif %}
</span>
{% endfor %}
</div>
{% elif match.has_walk_out %}

@ -1,5 +1,12 @@
<nav class="margin10">
<a href="{% url 'index' %}">Accueil</a>
<a href="{% url 'clubs' %}">Clubs</a>
<a href="{% url 'index' %}" class="orange">Accueil</a>
<a href="{% url 'clubs' %}" class="orange">Clubs</a>
{% if user.is_authenticated %}
<a href="{% url 'my-tournaments' %}" class="orange">Mes tournois</a>
<a href="{% url 'profile' %}">Mon compte</a>
{% else %}
<a href="{% url 'login' %}">Se connecter</a>
{% endif %}
<a href="{% url 'download' %}" class="download-button">Ajouter vos tournois</a>
</nav>

@ -1,25 +1,30 @@
<nav class="margin10">
<a href="{% url 'tournament-info' tournament.id %}" class="topmargin5">Informations</a>
<a href="{% url 'tournament-info' tournament.id %}" class="topmargin5 orange">Informations</a>
{% if tournament.display_matches or tournament.display_group_stages %}
<a href="{% url 'tournament' tournament.id %}" class="topmargin5">Matches</a>
<a href="{% url 'tournament' tournament.id %}" class="topmargin5 orange">Matches</a>
{% endif %}
{% if tournament.display_group_stages %}
<a href="{% url 'group-stages' tournament.id %}" class="topmargin5">Poules</a>
<a href="{% url 'group-stages' tournament.id %}" class="topmargin5 orange">Poules</a>
{% endif %}
{% if tournament.display_summons %}
<a href="{% url 'tournament-summons' tournament.id %}" class="topmargin5">Convocations</a>
<a href="{% url 'tournament-summons' tournament.id %}" class="topmargin5 orange">Convocations</a>
{% endif %}
{% if tournament.display_teams %}
<a href="{% url 'tournament-teams' tournament.id %}" class="topmargin5">Équipes</a>
<a href="{% url 'tournament-teams' tournament.id %}" class="topmargin5 orange">Équipes</a>
{% endif %}
{% if tournament.display_rankings %}
<a href="{% url 'tournament-rankings' tournament.id %}" class="topmargin5">Classement</a>
<a href="{% url 'tournament-rankings' tournament.id %}" class="topmargin5 orange">Classement</a>
{% endif %}
{% if user.is_authenticated %}
<a href="{% url 'profile' %}" class="topmargin5">Mon compte</a>
{% else %}
<a href="{% url 'login' %}" class="topmargin5">Se connecter</a>
{% endif %}
</nav>

@ -0,0 +1,66 @@
<div class="cell medium-12 large-3 my-block">
<div class="bubble">
<label class="matchtitle">{{ player.name }}</label>
<div>
<div class="match-result bottom-border" style="padding-right: 10px;">
<div class="player">
<div class="single-line">
<strong>{{ player.clean_club_name }}</strong>
</div>
</div>
</div>
<div class="match-result bottom-border">
<div class="player">
<div class="semibold">
<strong>Classement</strong>
</div>
</div>
<div class="scores">
<span class="score ws numbers">{{ player.format_ordinal }}</span>
</div>
</div>
<div class="match-result bottom-border">
<div class="player">
<div class="semibold">
<strong>Age</strong>
</div>
</div>
<div class="scores">
<span class="score ws numbers">
{{ player.calculate_age|default:"?" }} ans
</span>
</div>
</div>
{% if player.points %}
<div class="match-result bottom-border">
<div class="player">
<div class="semibold">
<strong>Points</strong>
</div>
</div>
<div class="scores">
<span class="score ws numbers">{{ player.points|default:"0" }} pts</span>
</div>
</div>
{% endif %}
{% if player.tournament_played %}
<div class="match-result">
<div class="player">
<div class="semibold">
<strong>Tournois joués</strong>
</div>
</div>
<div class="scores">
<span class="score ws numbers">{{ player.tournament_played|default:"0" }}</span>
</div>
</div>
{% endif %}
</div>
</div>
</div>

@ -4,9 +4,19 @@
<div class="table-cell"><div class="mybox center">{{ ranking.formatted_ranking }}</div></div>
<div class="table-cell table-cell-large padding-left semibold">
{% if ranking.id %}
<a href="{% url 'team-details' tournament.id ranking.id %}" class="player-link"> <!-- Add this anchor tag -->
{% endif %}
{% for name in ranking.names %}
<div>{{ name }}</div>
{% endfor %}
{% if ranking.id %}
</a>
{% endif %}
</div>
{% if tournament.display_points_earned %}

@ -12,26 +12,29 @@
{% include 'tournaments/navigation_tournament.html' %}
{% if rankings %}
<div class="grid-x padding-bottom">
<div class="cell medium-6 large-6 my-block">
<div class="bubble">
<div class="grid-x padding-bottom">
<div class="cell medium-6 large-6 my-block">
<div class="bubble">
<label class="title">{{ tournament.ranking_count_display }}</label>
{% if rankings %}
<label class="title">{{ tournament.ranking_count_display }}</label>
{% for ranking in rankings %}
{% include 'tournaments/ranking_row.html' %}
{% include 'tournaments/ranking_row.html' %}
{% endfor %}
{% else %}
<div class="ranking">
<div class="cell medium-12">
<div class="semibold">
Aucun classement disponible
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% endif %}

@ -4,9 +4,19 @@
<div class="summons-left">
<div class="semibold">
{% if summon.id %}
<a href="{% url 'team-details' tournament.id summon.id %}" class="player-link"> <!-- Add this anchor tag -->
{% endif %}
{% for name in summon.names %}
<div>{{ name }}</div>
{% endfor %}
{% if summon.id %}
</a>
{% endif %}
</div>
</div>
<div class="summons-right">

@ -0,0 +1,54 @@
{% extends 'tournaments/base.html' %}
{% block head_title %}Équipes du {{ tournament.display_name }}{% endblock %}
{% block first_title %}{{ tournament.event.display_name }}{% endblock %}
{% block second_title %}{{ tournament.display_name }}{% endblock %}
{% block content %}
<div class="grid-x grid-margin-x">
<style>
.bubble {
height: 100%;
}
</style>
<div class="cell medium-12">
<h1 class="club my-block topmargin20">{{ team.formatted_team_names }}</h1>
<div class="grid-x">
{% for player in team.player_registrations.all %}
{% include 'tournaments/player_row.html' with player=player %}
{% endfor %}
{% include 'tournaments/team_stats.html' %}
</div>
</div>
</div>
<div class="grid-x grid-margin-x">
{% with upcoming_matches=team.get_upcoming_matches %}
{% if upcoming_matches %}
<!-- Upcoming Matches -->
<div class="cell medium-12">
<h1 class="club my-block topmargin20">Prochains matchs</h1>
<div class="grid-x">
{% for match in upcoming_matches %}
{% include 'tournaments/match_cell.html' %}
{% endfor %}
</div>
</div>
{% endif %}
{% endwith %}
{% with completed_matches=team.get_completed_matches %}
{% if completed_matches %}
<!-- Completed Matches -->
<div class="cell medium-12">
<h1 class="club my-block topmargin20">Matchs terminés</h1>
<div class="grid-x">
{% for match in completed_matches %}
{% include 'tournaments/match_cell.html' %}
{% endfor %}
</div>
</div>
{% endif %}
{% endwith %}
</div>
{% endblock %}

@ -8,9 +8,22 @@
{% endif %}
{% if team.names %}
<div class="table-cell table-cell-large semibold">
{% if team.team_registration.id %}
<a href="{% url 'team-details' tournament.id team.team_registration.id %}" class="player-link"> <!-- Add this anchor tag -->
{% endif %}
{% for name in team.names %}
<div>{{ name }}</div>
{% endfor %}
{% if team.team_registration.id %}
</a>
{% endif %}
</div>
{% else %}
<div class="table-cell table-cell-large semibold">
<div>Bloqué</div>
</div>
{% endif %}
{% if tournament.hide_teams_weight %}

@ -0,0 +1,84 @@
<div class="cell medium-12 large-3 my-block">
<div class="dark_bubble">
<label class="matchtitle winner">Statistiques du tournoi</label>
<div>
{% with stats=team.get_statistics %}
<div class="match-result bottom-border">
<div class="player">
<div class="semibold">
<strong>Poids de la paire</strong>
</div>
</div>
<div class="scores">
<span class="score ws numbers">{{ stats.weight }}</span>
</div>
</div>
{% if stats.final_ranking %}
<div class="match-result bottom-border">
<div class="player">
<div class="semibold">
<strong>Classement final</strong>
</div>
</div>
<div class="scores">
<span class="score ws numbers">{{ stats.final_ranking }}</span>
</div>
</div>
{% endif %}
{% if stats.points_earned %}
<div class="match-result bottom-border">
<div class="player">
<div class="semibold">
<strong>Points gagnés</strong>
</div>
</div>
<div class="scores">
<span class="score ws numbers">{{ stats.points_earned }} pts</span>
</div>
</div>
{% endif %}
{% if stats.initial_stage %}
<div class="match-result bottom-border">
<div class="player">
<div class="semibold">
<strong>Départ</strong>
</div>
</div>
<div class="scores">
<span class="score ws numbers">{{ stats.initial_stage }}</span>
</div>
</div>
{% endif %}
<div class="match-result bottom-border">
<div class="player">
<div class="semibold">
<strong>Matchs joués</strong>
</div>
</div>
<div class="scores">
<span class="score ws numbers">{{ stats.matches_played }}</span>
</div>
</div>
{% if stats.victory_ratio %}
<div class="match-result">
<div class="player">
<div class="semibold">
<strong>Ratio victoires</strong>
</div>
</div>
<div class="scores">
<span class="score ws numbers">{{ stats.victory_ratio }}</span>
</div>
</div>
{% endif %}
{% endwith %}
</div>
</div>
</div>

@ -14,8 +14,66 @@
<div class="grid-x">
<div class="cell medium-6 large-6 my-block">
<h1 class="club my-block topmargin20">{{ tournament.display_name }} {{ tournament.get_federal_age_category_display}}</h1 >
{% if tournament.enable_online_registration and team %}
<h1 class="club my-block topmargin20">Votre équipe</h1 >
<div class="bubble">
<div class="alert">
{% if team.is_in_waiting_list >= 0 %}
Tournoi complet, vous êtes en liste d'attente.
<div>
{% if team.is_in_waiting_list == 1 %}
{{ team.is_in_waiting_list }} équipe en liste d'attente devant vous
{% elif team.is_in_waiting_list > 1 %}
{{ team.is_in_waiting_list }} équipes en liste d'attente devant vous
{% elif team.is_in_waiting_list == 0 %}
vous êtes la première équipe en liste d'attente.
{% endif %}
</div>
{% endif %}
</div>
<div class="semibold topmargin20">
{% for player in team.players %}
<div>{{ player.name }}</div>
{% endfor %}
</div>
</p>
<p>
<div>Inscrits le {{ team.local_registration_date }}</div>
</p>
{% if tournament.is_unregistration_possible %}
<p>
<div class="margin10">
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
</div>
<a href="{% url 'unregister_tournament' tournament.id %}"
class="rounded-button destructive-button"
onclick="return confirm('Êtes-vous sûr de vouloir vous désinscrire ?');">
{% if team.is_in_waiting_list >= 0 %}
Se retirer de la liste d'attente
{% else %}
Se désinscrire
{% endif %}
</a>
</p>
<!-- {% if is_captain %}
{% else %}
<p>
<div>Vous n'êtes pas le capitaine de l'équipe, la désinscription en ligne n'est pas disponible. Veuillez contacter le JAP ou votre partenaire.</div>
</p>
{% endif %}
-->
{% endif %}
</div>
{% endif %}
<h1 class="club my-block topmargin20">{{ tournament.display_name }} {{ tournament.get_federal_age_category_display}}</h1>
<div class="bubble">
<p>
<div class="semibold">{{ tournament.local_start_date_formatted }}</div>
@ -38,9 +96,85 @@
</p>
{% endif %}
{% if tournament.information %}
<p>
<div class="semibold">Infos</div>
<div>{{ tournament.information|linebreaksbr }}</div>
</p>
{% endif %}
{% if tournament.enable_online_registration %}
<p>
<div class="semibold">Inscription en ligne</div>
{% if tournament.options_online_registration %}
<ul>
{% for option in tournament.options_online_registration %}
<li>{{ option }}</li>
{% endfor %}
</ul>
{% endif %}
</p>
<p>
<div class="semibold">
{% if tournament.registration_count_display %}
<div>{{ tournament.registration_count_display }}</div>
{% else %}
<div>Aucune équipe inscrite</div>
{% endif %}
</div>
</p>
<p>
<div class="semibold">
{{ tournament.get_online_registration_status.status_localized }}
</div>
</p>
{% endif %}
{% if tournament.online_register_is_enabled and team is None %}
{% if tournament.account_is_required is False or user.is_authenticated and user.is_active %}
{% if player_register_check is None %}
<p>
<div>
<a href="{% url 'register_tournament' tournament.id %}" class="rounded-button">S'inscrire</a>
</div>
</p>
{% else %}
<p>
<div class="semibold">
Vous ne pouvez pas vous inscrire à ce tournoi.
</div>
<div class="alert">
{% for reason in player_register_check %}
<div>{{ reason }}</div>
{% endfor %}
</div>
</p>
{% endif %}
{% else %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
<p>
<div>
<a href="{% url 'login' %}?next={{ request.path }}" class="styled-link">Connectez-vous !</a>
</div>
</p>
<p>
<div>
Vous avez besoin d'un compte Padel Club pour pouvoir vous inscrire en ligne.
<a href="{% url 'signup' %}?next={{ request.path }}" class="styled-link">Créer le tout de suite !</a>
</div>
</p>
{% endif %}
{% endif %}
</div>
</div>
</div>
{% endblock %}

@ -26,12 +26,14 @@
</div>
{% if tournament.tournament_status_display %}
<div class="table-cell-responsive-large center horizontal-padding">{{ tournament.tournament_status_display }}</div>
<div class="table-cell-responsive-large right horizontal-padding">
{{ tournament.tournament_status_display|linebreaksbr }}
</div>
{% endif %}
<div class="table-cell">
<div class="mybox center">{{ tournament.formatted_start_date }}</div>
{% if tournament.tournament_status_display %}
<div class="table-cell-responsive-short small center">{{ tournament.tournament_status_display }}</div>
<div class="table-cell-responsive-short small center">{{ tournament.tournament_status_display|linebreaksbr }}</div>
{% endif %}
</div>
</div>

@ -6,11 +6,6 @@
{% block right_header %}
<div class="medium-6 large-3 cell topblock my-block flex">
<div class="right-content w300px">
<a href="{% url 'download' %}" class="large_button">Téléchargez l'app pour organiser vos tournois !</a>
</div>
</div>
{% endblock %}
{% block content %}

@ -1,8 +1,11 @@
from django.contrib.auth import views as auth_views
from django.urls import include, path
from .forms import EmailOrUsernameAuthenticationForm, CustomPasswordChangeForm
from . import views
urlpatterns = [
path('reset/<uidb64>/<token>/', views.CustomPasswordResetConfirmView.as_view(), name='password_reset_confirm'),
path("", views.index, name="index"),
path("tournaments/", views.tournaments, name="tournaments"),
path("clubs/", views.clubs, name="clubs"),
@ -21,13 +24,16 @@ urlpatterns = [
path('broadcast/', views.tournament_broadcast_home, name='broadcast'),
path('broadcast/auto/', views.automatic_broadcast, name='automatic-broadcast'),
path('matches/json/', views.tournament_matches_json, name='tournament-matches-json'),
path('prog/json/', views.tournament_prog_json, name='tournament-prog-json'),
path('broadcast/json/', views.broadcast_json, name='broadcast-json'),
path('broadcast/group-stages/', views.tournament_broadcasted_group_stages, name='broadcasted-group-stages'),
path('broadcast/prog/', views.tournament_broadcasted_prog, name='broadcasted-prog'),
path('group-stages/', views.tournament_group_stages, name='group-stages'),
path('group-stages/json/', views.tournament_live_group_stage_json, name='group-stages-json'),
path('rankings/', views.tournament_rankings, name='tournament-rankings'),
path('rankings/json/', views.tournament_rankings_json, name='tournament-rankings-json'),
path('broadcast/rankings/', views.tournament_broadcast_rankings, name='broadcasted-rankings'),
path('team/<str:team_id>/', views.team_details, name='team-details'),
])
),
path("event/<str:event_id>/broadcast/auto/", views.automatic_broadcast_event, name='automatic-broadcast-event'),
@ -38,6 +44,28 @@ urlpatterns = [
path('terms-of-use/', views.terms_of_use, name='terms-of-use'),
path('utils/xls-to-csv/', views.xls_to_csv, name='xls-to-csv'),
path('mail-test/', views.simple_form_view, name='mail-test'),
path('login/', auth_views.LoginView.as_view(
template_name='registration/login.html',
authentication_form=EmailOrUsernameAuthenticationForm
), name='login'),
path('password_change/',
auth_views.PasswordChangeView.as_view(
success_url='/profile/', # Redirect back to profile after success
form_class=CustomPasswordChangeForm
),
name='password_change'
),
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
path('signup/', views.signup, name='signup'), # URL pattern for signup
# path('profile/', views.profile, name='profile'), # URL pattern for signup
path('my-tournaments/', views.my_tournaments, name='my-tournaments'), # URL pattern for signup
path('tournaments/<str:tournament_id>/register/', views.register_tournament, name='register_tournament'),
path('tournaments/<str:tournament_id>/unregister/', views.unregister_tournament, name='unregister_tournament'),
path('password_reset/', auth_views.PasswordResetView.as_view(), name='password_reset'),
path('password_reset_done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'),
path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
path('profile/', views.ProfileUpdateView.as_view(), name='profile'),
path('admin/tournament-import/', views.tournament_import_view, name='tournament_import'),
path('admin/status/', views.status_page, name='status_page'),
path('admin/users-export/', views.UserListExportView.as_view(), name='users_export'),
]

@ -0,0 +1,48 @@
import re
class LicenseValidator:
def __init__(self, license_id: str):
self.license_id = license_id.upper() # Ensure uppercase for consistency
@property
def stripped_license(self) -> str:
# Remove leading zero if present and match only the numeric part
license_without_leading_zero = self.license_id.lstrip("0")
match = re.match(r"^[0-9]{6,8}", license_without_leading_zero)
if match:
return match.group(0)
return 'licence invalide'
@property
def computed_licence_id(self) -> str:
return f"{self.stripped_license}{self.computed_license_key}"
@property
def computed_license_key(self) -> str:
stripped = self.stripped_license
if stripped and stripped.isdigit():
int_value = int(stripped)
value = (int_value - 1) % 23
char_code = ord('A') + value
# Adjust for letters to skip: I, O, Q
if char_code >= ord('I'):
char_code += 1
if char_code >= ord('O'):
char_code += 1
if char_code >= ord('Q'):
char_code += 1
return chr(char_code)
return 'aucune clé de licence'
def validate_license(self) -> bool:
if not self.license_id or len(self.license_id) < 7:
return False # Invalid length for a license ID
# Separate the numeric part and the letter
numeric_part = self.license_id[:-1]
given_letter = self.license_id[-1]
# Verify that the last character matches the computed license key
return self.computed_license_key == given_letter

@ -0,0 +1,115 @@
import csv
import os
import re
from datetime import datetime
from django.conf import settings
from tournaments.models.enums import FederalCategory
def clean_licence_id(licence_id):
# This regex matches the trailing letters (non-digits) and removes them
cleaned_licence_id = re.sub(r'\D+$', '', str(licence_id)) # \D+ matches non-digits at the end
return cleaned_licence_id
def get_player_name_from_csv(category, licence_id, base_folder=None):
"""
Search for a player's first name, last name, and rank in the most recent rankings file.
:param licence_id: The licence ID to search for.
:param base_folder: Base folder containing the rankings folder.
:return: A tuple (first_name, last_name, rank) or (None, None, None) if not found.
"""
if base_folder is None:
base_folder = settings.STATIC_ROOT
folder_path = os.path.join(base_folder, "rankings")
if licence_id:
cleaned_licence_id = clean_licence_id(licence_id)
else:
cleaned_licence_id = None
def extract_date(file_name):
"""
Extract the date (MM-YYYY) from the file name and return it as a datetime object.
"""
match = re.search(r"(\d{2}-\d{4})", file_name)
if match:
return datetime.strptime(match.group(1), "%m-%Y")
return None
def find_most_recent_file(file_type):
"""
Find the most recent file for the given type (-MESSIEURS- or -DAMES-).
"""
files = [
f for f in os.listdir(folder_path)
if file_type in f and re.search(r"\d{2}-\d{4}", f)
]
files_with_dates = [(f, extract_date(f) or datetime.min) for f in files]
if not files_with_dates:
return None
# Sort by date in descending order
files_with_dates.sort(key=lambda x: x[1], reverse=True)
return os.path.join(folder_path, files_with_dates[0][0])
def search_file(file_path, is_woman):
if not file_path or not os.path.exists(file_path):
print("no file found")
return None, False
last_rank = None
last_rank_count = 0
with open(file_path, newline='', encoding='utf-8') as file:
reader = csv.reader(file, delimiter=';')
rows = list(reader) # Read all rows at once to process later
if cleaned_licence_id:
for row in rows:
if len(row) >= 15: # Ensure row has enough columns
current_licence_id = row[5]
if current_licence_id == str(cleaned_licence_id):
data = {
"first_name": row[3], # 4th column: first name
"last_name": row[2], # 3rd column: last name
"rank": row[1],
"points": row[6],
"assimilation": row[7],
"tournament_count": row[8],
"ligue_name": row[9],
"club_name": row[11],
"birth_year": row[14],
"is_woman": is_woman,
}
return data, True
# Determine the rank for an unranked player
if rows:
# Find the last rank in the file
last_row = rows[-1]
if len(last_row) >= 2:
last_rank = last_row[1]
# Count how many times the last rank appears
last_rank_count = sum(1 for row in rows if len(row) >= 2 and row[1] == last_rank)
if last_rank is not None:
unranked_rank = int(last_rank) + last_rank_count + 1
data = {
"rank": str(unranked_rank),
"is_woman": is_woman
}
return data, False
return None, False
dames_file = find_most_recent_file("CLASSEMENT-PADEL-DAMES-")
result, found = search_file(dames_file, True)
if found or category is FederalCategory.WOMEN:
return result, found
messieurs_file = find_most_recent_file("CLASSEMENT-PADEL-MESSIEURS-")
result, found = search_file(messieurs_file, False)
return result, found

@ -1,3 +1,6 @@
# Standard library imports
import os
import csv
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse
from django.utils.encoding import force_str
@ -8,10 +11,11 @@ from django.views.decorators.csrf import csrf_exempt
from django.contrib.admin.views.decorators import staff_member_required
from django.core.files.storage import default_storage
from django.core.files.base import ContentFile
from django.views.generic import View
from tournaments.models.device_token import DeviceToken
from .models import Court, DateInterval, Club, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamScore, TeamRegistration, PlayerRegistration, Purchase, FailedApiCall
from .models import Court, DateInterval, Club, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamScore, TeamRegistration, PlayerRegistration, Purchase, FailedApiCall, UserOrigin
from .models import TeamSummon
from datetime import datetime, timedelta
import time
@ -21,20 +25,84 @@ from datetime import date
from django.http import JsonResponse, HttpResponse
from django.db.models import Q
import json
import time
import asyncio
from datetime import date, datetime, timedelta
import csv
import zipfile
from api.tokens import account_activation_token
# Third-party imports
from qr_code.qrcode.utils import QRCodeOptions
from .utils.apns import send_push_notification
import os
from .forms import SimpleForm
from django.core.mail import EmailMessage
from datetime import timedelta
# Django imports
from django.shortcuts import render, redirect, get_object_or_404
from django.http import HttpResponse, JsonResponse, Http404
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.utils.encoding import force_str, force_bytes
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.template import loader
from django.template.loader import render_to_string
from django.contrib import messages
from django.contrib.sites.shortcuts import get_current_site
from django.contrib.auth import login
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import (
UserCreationForm,
SetPasswordForm,
)
from django.contrib.auth.views import PasswordResetConfirmView
from django.contrib.auth import get_user_model
from django.contrib.auth.tokens import default_token_generator
from django.db.models import Q
from django.views.decorators.csrf import csrf_exempt
from django.core.files.storage import default_storage
from django.core.files.base import ContentFile
from django.core.mail import EmailMessage
from django.views.decorators.csrf import csrf_protect
from .services.tournament_registration import TournamentRegistrationService
from .services.tournament_unregistration import TournamentUnregistrationService
from django.core.exceptions import ValidationError
# Local application imports
from .models import (
Court,
DateInterval,
Club,
Tournament,
CustomUser,
Event,
Round,
GroupStage,
Match,
TeamScore,
TeamRegistration,
PlayerRegistration,
Purchase,
FailedApiCall,
TeamSummon,
FederalCategory,
UnregisteredTeam,
UnregisteredPlayer
)
from .forms import (
SimpleForm,
SimpleCustomUserCreationForm,
TournamentRegistrationForm,
AddPlayerForm,
ProfileUpdateForm,
)
from .utils.apns import send_push_notification
from .utils.licence_validator import LicenseValidator
from .utils.player_search import get_player_name_from_csv
from api.tokens import account_activation_token
from tournaments.models.device_token import DeviceToken
from tournaments.models.player_enums import PlayerDataSource, PlayerSexType
from django.views.generic.edit import UpdateView
from .forms import CustomPasswordChangeForm
def index(request):
@ -87,13 +155,51 @@ def future_tournaments(club_id):
def tournament_info(request, tournament_id):
tournament = get_object_or_404(Tournament, pk=tournament_id)
registered_user = None
team_registration = None
is_captain = False
player_register_check = None
if request.user.is_authenticated:
# Assuming user's licence_id is stored in the user profile (e.g., request.user.licence_id)
user_licence_id = request.user.licence_id
player_register_check = tournament.player_register_check(user_licence_id)
if user_licence_id is not None and player_register_check is None:
validator = LicenseValidator(user_licence_id)
stripped_license = validator.stripped_license
# Check if there is a PlayerRegistration for this user in this tournament
registered_user = PlayerRegistration.objects.filter(
licence_id__startswith=stripped_license,
team_registration__tournament=tournament,
team_registration__walk_out=False,
).first()
# If the user is registered, retrieve their team registration
if registered_user:
is_captain = registered_user.captain
team_registration = registered_user.team_registration
return render(request, 'tournaments/tournament_info.html', {
'tournament': tournament,
'team': team_registration,
'is_captain': is_captain,
'player_register_check': player_register_check
})
def tournaments(request):
filter_param = request.GET.get('filter')
filter = None
if filter_param:
try:
filter = int(filter_param)
if filter not in [0, 1, 2]: # Valid filter values
return redirect('/')
except:
return redirect('/')
filter = int(request.GET.get('filter'))
club_id = request.GET.get('club')
title = ''
@ -148,7 +254,7 @@ def tournament(request, tournament_id):
rounds = list(tournament.rounds.filter(group_stage_loser_bracket=True))
rounds.extend(bracket_rounds)
group_stages = sorted(tournament.get_computed_group_stage(), key=lambda s: (s.step, s.index))
group_stages = sorted(tournament.sorted_group_stages(), key=lambda s: (s.step, s.index))
if tournament.display_matches() or tournament.display_group_stages():
return render(request, 'tournaments/matches.html', {
@ -251,6 +357,21 @@ def tournament_matches_json(request, tournament_id):
data = json.dumps(live_matches, default=vars)
return HttpResponse(data, content_type='application/json')
def tournament_prog_json(request, tournament_id):
tournament = get_object_or_404(Tournament, pk=tournament_id)
matches = tournament.broadcasted_prog()
# Convert matches to JSON-serializable format, handling None/empty slots
live_matches = []
for match in matches:
if match is None or isinstance(match, dict) and match.get('empty'):
live_matches.append({"empty": True})
else:
live_matches.append(match.live_match())
data = json.dumps(live_matches, default=vars)
return HttpResponse(data, content_type='application/json')
def tournament_group_stages(request, tournament_id):
tournament = get_object_or_404(Tournament, pk=tournament_id)
live_group_stages = list(tournament.live_group_stages())
@ -270,6 +391,14 @@ def tournament_broadcasted_group_stages(request, tournament_id):
'qr_code_options': qr_code_options(),
})
def tournament_broadcasted_prog(request, tournament_id):
tournament = get_object_or_404(Tournament, pk=tournament_id)
return render(request, 'tournaments/broadcast/broadcasted_prog.html', {
'tournament': tournament,
'qr_code_url': qr_code_url(request, tournament_id),
'qr_code_options': qr_code_options(),
})
def tournament_live_group_stage_json(request, tournament_id):
tournament = get_object_or_404(Tournament, pk=tournament_id)
@ -316,7 +445,13 @@ def activate(request, uidb64, token):
if user is not None and account_activation_token.check_token(user, token):
user.is_active = True
user.save()
return HttpResponse('Votre email est confirmé. Vous pouvez maintenant vous connecter.')
# Specify the authentication backend when logging in
from django.contrib.auth import login
login(request, user, backend='django.contrib.auth.backends.ModelBackend')
next_url = request.GET.get('next', '/')
return redirect(next_url)
else:
return HttpResponse('Le lien est invalide.')
@ -485,6 +620,158 @@ def send_email(mail, name):
email = EmailMessage(subject, body, to=[mail])
email.send()
def signup(request):
next_url = request.GET.get('next', '/')
if request.method == 'POST':
form = SimpleCustomUserCreationForm(request.POST)
if form.is_valid():
user = form.save(commit=False)
user.is_active = False
user.origin = UserOrigin.SITE
user.save()
send_verification_email(request, user, next_url)
return render(request, 'registration/signup_success.html', {
'next_url': next_url,
'user_email': user.email # Add the user's email to the context
})
else:
form = SimpleCustomUserCreationForm()
response = render(request, 'registration/signup.html', {'form': form})
return response
def send_verification_email(request, user, next_url):
print('next_url', next_url)
current_site = get_current_site(request)
mail_subject = 'Activez votre compte Padel Club !'
# Prepare the email subject and message
message = render_to_string('tournaments/acc_active_email.html', {
'user': user,
'domain': current_site.domain,
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
'token': account_activation_token.make_token(user),
'next': next_url, # Pass next URL to the template
})
email = EmailMessage(mail_subject, message, to=[user.email])
email.content_subtype = "html"
email.send()
@login_required
def profile(request):
user = request.user # Get the currently authenticated user
return render(request, 'registration/profile.html', {
'user_name': user.username
})
@csrf_protect
def register_tournament(request, tournament_id):
tournament = get_object_or_404(Tournament, id=tournament_id)
service = TournamentRegistrationService(request, tournament)
service.initialize_context()
print("initialize_context")
if request.method == 'POST':
service.handle_post_request()
else:
service.handle_get_request()
return render(request, 'register_tournament.html', service.context)
@login_required
def unregister_tournament(request, tournament_id):
tournament = get_object_or_404(Tournament, id=tournament_id)
service = TournamentUnregistrationService(request, tournament)
if not service.can_unregister():
return redirect('tournament-info', tournament_id=tournament_id)
service.process_unregistration()
return redirect('tournament-info', tournament_id=tournament_id)
class CustomPasswordResetConfirmView(PasswordResetConfirmView):
template_name = 'registration/password_reset_confirm.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['custom_message'] = "Veuillez entrer un nouveau mot de passe."
return context
def form_valid(self, form):
response = super().form_valid(form)
return response
def get_user(self, uidb64):
"""
Override this method to decode the uid and return the corresponding user.
"""
try:
# Use get_user_model() instead of direct User import
User = get_user_model()
uid = urlsafe_base64_decode(uidb64).decode()
user = User.objects.get(pk=uid)
return user
except (TypeError, ValueError, User.DoesNotExist):
raise Http404("User not found")
@login_required
def my_tournaments(request):
user = request.user
user_licence_id = request.user.licence_id
# If no licence_id, return empty lists
if user_licence_id is None:
return render(request, 'registration/my_tournaments.html', {
'upcoming_tournaments': [],
'running_tournaments': [],
'ended_tournaments': [],
'user_name': user.username
})
# Get all tournaments for the user based on their licence
validator = LicenseValidator(user_licence_id)
stripped_license = validator.stripped_license
def filter_user_tournaments(tournaments):
return [t for t in tournaments if t.teamregistration_set.filter(
playerregistration__licence_id__startswith=stripped_license,
walk_out=False
).exists()]
# Get filtered tournaments using the helper function
upcoming_tournaments = filter_user_tournaments(future_tournaments(None))
running_tournaments = filter_user_tournaments(live_tournaments(None))
ended_tournaments = filter_user_tournaments(finished_tournaments(None))
return render(request, 'registration/my_tournaments.html', {
'upcoming_tournaments': upcoming_tournaments,
'running_tournaments': running_tournaments,
'ended_tournaments': ended_tournaments,
'user_name': user.username
})
from django.contrib.auth.mixins import LoginRequiredMixin
class ProfileUpdateView(LoginRequiredMixin, UpdateView):
model = CustomUser
form_class = ProfileUpdateForm
template_name = 'profile.html'
success_url = reverse_lazy('profile')
login_url = '/login/' # Specify where to redirect if user is not logged in
def get_object(self, queryset=None):
return self.request.user
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['password_change_form'] = CustomPasswordChangeForm(user=self.request.user)
return context
from api.serializers import GroupStageSerializer, MatchSerializer, PlayerRegistrationSerializer, RoundSerializer, TeamRegistrationSerializer, TeamScoreSerializer
@staff_member_required
@ -578,3 +865,62 @@ def get_file_data(zip_file, file_path):
def status_page(request):
return render(request, 'tournaments/admin/status.html')
def team_details(request, tournament_id, team_id):
# First check if team_id is None or invalid
if team_id is None or team_id == 'None':
# Redirect to tournament page or show an error
return redirect('tournament-info', tournament_id=tournament_id)
tournament = get_object_or_404(Tournament, id=tournament_id)
try:
team = get_object_or_404(TeamRegistration, id=team_id)
except (ValueError, ValidationError):
# Handle invalid UUID
return redirect('tournament-info', tournament_id=tournament_id)
# Get all matches for this team
all_matches = team.get_matches()
return render(request, 'tournaments/team_details.html', {
'tournament': tournament,
'team': team,
'matches': all_matches,
'debug': False # Set to False in production
})
class UserListExportView(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs):
# Get users, excluding those with origin=UserOrigin.SITE, ordered by date_joined
users = CustomUser.objects.exclude(
origin=UserOrigin.SITE
).order_by('date_joined')
# Prepare the response
response = HttpResponse(content_type='text/plain; charset=utf-8')
# Write header
headers = [
'Prenom', 'Nom', 'Club', 'Email', 'Telephone',
'Login', 'Actif', 'Inscription', 'Tournois'
]
response.write('\t'.join(headers) + '\n')
# Write data rows
for user in users:
row = [
str(user.first_name or ''),
str(user.last_name or ''),
str(user.latest_event_club_name() or ''),
str(user.email or ''),
str(user.phone or ''),
str(user.username or ''),
'Oui' if user.is_active else 'Non',
user.date_joined.strftime('%Y-%m-%d %H:%M:%S'),
str(user.event_count())
]
# Replace any tabs or newlines in the data to prevent formatting issues
row = [field.replace('\t', ' ') for field in row]
response.write('\t'.join(row) + '\r\n')
return response

Loading…
Cancel
Save