apikeys
Laurent 4 months ago
commit 285292ac55
  1. 6
      api/urls.py
  2. 548
      api/utils.py
  3. 337
      api/views.py
  4. 9
      padelclub_backend/settings.py
  5. 1
      requirements.txt
  6. 4
      tournaments/admin.py
  7. 152
      tournaments/admin_utils.py
  8. 523
      tournaments/management/commands/analyze_rankings.py
  9. 222
      tournaments/management/commands/test_fft_all_tournaments.py
  10. 103
      tournaments/management/commands/test_fft_scraper.py
  11. 6
      tournaments/models/purchase.py
  12. 37
      tournaments/models/tournament.py
  13. 1622
      tournaments/static/rankings/CLASSEMENT-PADEL-DAMES-07-2025.csv
  14. 8368
      tournaments/static/rankings/CLASSEMENT-PADEL-MESSIEURS-07-2025.csv
  15. 1
      tournaments/static/tournaments/css/style.css
  16. 36
      tournaments/templates/admin/tournaments/dashboard.html
  17. 1
      tournaments/templates/tournaments/broadcast/broadcast.html
  18. 1
      tournaments/templates/tournaments/broadcast/broadcast_club.html
  19. 9
      tournaments/templates/tournaments/download.html
  20. 3
      tournaments/views.py

@ -39,6 +39,7 @@ urlpatterns = [
path('sync-data/', SynchronizationApi.as_view(), name="data"), path('sync-data/', SynchronizationApi.as_view(), name="data"),
path('data-access-content/', UserDataAccessApi.as_view(), name="data-access-content"), path('data-access-content/', UserDataAccessApi.as_view(), name="data-access-content"),
path("is_granted_unlimited_access/", views.is_granted_unlimited_access, name="is-granted-unlimited-access"),
path('api-token-auth/', obtain_auth_token, name='api_token_auth'), path('api-token-auth/', obtain_auth_token, name='api_token_auth'),
path("user-by-token/", views.user_by_token, name="user_by_token"), path("user-by-token/", views.user_by_token, name="user_by_token"),
@ -47,6 +48,11 @@ urlpatterns = [
path('xls-to-csv/', views.xls_to_csv, name='xls-to-csv'), path('xls-to-csv/', views.xls_to_csv, name='xls-to-csv'),
path('config/tournament/', views.get_tournament_config, name='tournament-config'), path('config/tournament/', views.get_tournament_config, name='tournament-config'),
path('config/payment/', views.get_payment_config, name='payment-config'), path('config/payment/', views.get_payment_config, name='payment-config'),
path('fft/club-tournaments/', views.get_fft_club_tournaments, name='get-fft-club-tournaments'),
path('fft/club-tournaments-complete/', views.get_fft_club_tournaments_with_umpire_data, name='get-fft-club-tournaments-complete'),
path('fft/all-tournaments/', views.get_fft_all_tournaments, name='get-fft-all-tournaments'),
path('fft/umpire/<str:tournament_id>/', views.get_fft_umpire_data, name='get-fft-umpire-data'),
path('fft/federal-clubs/', views.get_fft_federal_clubs, name='get-fft-federal-clubs'),
# authentication # authentication
path("change-password/", ChangePasswordView.as_view(), name="change_password"), path("change-password/", ChangePasswordView.as_view(), name="change_password"),

@ -1,13 +1,557 @@
import time
import logging
import requests
import re
from playwright.sync_api import sync_playwright
from datetime import datetime, timedelta
import json
import traceback
logger = logging.getLogger(__name__)
def check_version_smaller_than_1_1_12(version_str): def check_version_smaller_than_1_1_12(version_str):
# Remove the parentheses part if it exists, example of version: 1.1.12 (2) # Remove the parentheses part if it exists, example of version: 1.1.12 (2)
version_str = version_str.split()[0] version_str = version_str.split()[0]
if version_str: if version_str:
# Split version into components # Split version into components
version_parts = [int(x) for x in version_str.split('.')] version_parts = [int(x) for x in version_str.split('.')]
target_parts = [1, 1, 12] target_parts = [1, 1, 12]
# Compare version components # Compare version components
return version_parts < target_parts return version_parts < target_parts
else: else:
return False return False
def scrape_fft_club_tournaments(club_code, club_name, start_date=None, end_date=None, page=0):
"""
Scrapes FFT tournaments using Playwright with detailed debugging
"""
logger.info(f"Starting Playwright scraping for {club_name}")
try:
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page_obj = browser.new_page()
page_obj.set_extra_http_headers({
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Safari/605.1.15"
})
# Navigate to FFT
target_url = "https://tenup.fft.fr/recherche/tournois"
logger.info(f"Navigating to: {target_url}")
page_obj.goto(target_url)
# page_obj.wait_for_timeout(7000)
current_url = page_obj.url
logger.info(f"Current URL: {current_url}")
if "queue-it.net" in current_url.lower():
logger.warning("Still in Queue-It")
browser.close()
return None
# Extract form_build_id
form_input = page_obj.query_selector('input[name="form_build_id"]')
if not form_input:
logger.error("Could not find form_build_id")
browser.close()
return None
form_build_id = form_input.get_attribute('value')
logger.info(f"Extracted form_build_id: {form_build_id}")
# Build parameters
date_component = ""
if start_date and end_date:
date_component = f"&date[start]={start_date}&date[end]={end_date}"
elif start_date:
try:
start_dt = datetime.strptime(start_date, "%d/%m/%y")
end_dt = start_dt + timedelta(days=90)
date_component = f"&date[start]={start_date}&date[end]={end_dt.strftime('%d/%m/%y')}"
except ValueError:
logger.warning(f"Invalid date format: {start_date}")
club_name_encoded = club_name.replace(" ", "+")
club_code_clean = club_code.replace(" ", "")
params = f"recherche_type=club&club[autocomplete][value_container][value_field]={club_code_clean}&club[autocomplete][value_container][label_field]={club_name_encoded}&pratique=PADEL{date_component}&page={page}&sort=dateDebut+asc&form_build_id={form_build_id}&form_id=recherche_tournois_form&_triggering_element_name=submit_page&_triggering_element_value=Submit+page"
logger.info(f"AJAX Parameters: {params}")
# Make AJAX request and capture the full response
ajax_script = f"""
async () => {{
try {{
const response = await fetch('https://tenup.fft.fr/system/ajax', {{
method: 'POST',
headers: {{
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest',
'Origin': 'https://tenup.fft.fr',
'Referer': 'https://tenup.fft.fr/recherche/tournois'
}},
body: `{params}`
}});
const status = response.status;
const responseText = await response.text();
return {{
success: response.ok,
status: status,
responseText: responseText
}};
}} catch (error) {{
return {{
success: false,
error: error.message
}};
}}
}}
"""
logger.info("Making AJAX request...")
result = page_obj.evaluate(ajax_script)
browser.close()
# Print the full response for debugging
logger.info(f"AJAX Response Status: {result.get('status')}")
logger.info(f"AJAX Response Success: {result.get('success')}")
if result.get('success'):
response_text = result.get('responseText', '')
logger.info(f"Raw Response Length: {len(response_text)}")
# logger.info(f"Raw Response (first 500 chars): {response_text[:500]}")
try:
# Try to parse as JSON
json_data = json.loads(response_text)
logger.info(f"JSON Response Type: {type(json_data)}")
# logger.info(f"JSON Response: {json.dumps(json_data, indent=2, default=str)}")
# Now try to parse it
parsed_result = _parse_ajax_response(json_data)
# logger.info(f"Parsed Result: {parsed_result}")
return parsed_result
except json.JSONDecodeError as json_error:
logger.error(f"JSON Parse Error: {json_error}")
logger.error(f"Response text: {response_text}")
return None
else:
logger.error(f"AJAX request failed: {result.get('error')}")
return None
except Exception as e:
logger.error(f"Error in Playwright scraping: {e}")
logger.error(f"Traceback: {traceback.format_exc()}")
return None
def scrape_fft_club_tournaments_all_pages(club_code, club_name, start_date=None, end_date=None):
"""
Scrapes all pages of FFT tournaments for a specific club
"""
logger.info(f"Starting complete tournament scraping for {club_name}")
all_tournaments = []
page = 0
while True:
try:
# Call the working single-page function
result = scrape_fft_club_tournaments(
club_code=club_code,
club_name=club_name,
start_date=start_date,
end_date=end_date,
page=page
)
# Debug: Log what we got
logger.info(f"Page {page} result: {result}")
if not result:
logger.warning(f"No result for page {page}")
break
tournaments = result.get('tournaments', [])
logger.info(f"Page {page} returned {len(tournaments)} tournaments")
if not tournaments:
logger.info(f"No tournaments on page {page}, stopping")
break
all_tournaments.extend(tournaments)
# Check if we have all results
total_results = result.get('total_results', 0)
logger.info(f"Total so far: {len(all_tournaments)}, Target: {total_results}")
if len(all_tournaments) >= total_results:
logger.info("Got all tournaments, stopping")
break
page += 1
logger.info(f"Moving to page {page}")
# time.sleep(1) # Rate limiting
except Exception as e:
logger.error(f"Error on page {page}: {e}")
break
logger.info(f"Completed scraping: {len(all_tournaments)} tournaments across {page + 1} pages")
return {
'tournaments': all_tournaments,
'total_results': len(all_tournaments),
'current_count': len(all_tournaments),
'pages_scraped': page + 1
}
def get_umpire_data(tournament_id):
"""
Scrapes umpire data for a specific tournament
"""
logger.info(f"Getting umpire data for tournament {tournament_id}")
try:
url = f"https://tenup.fft.fr/tournoi/{tournament_id}"
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Safari/605.1.15'
}
response = requests.get(url, headers=headers, timeout=30)
if response.status_code != 200:
logger.error(f"Failed to fetch tournament page: {response.status_code}")
return None, None, None
html_content = response.text
# Extract name
name_pattern = r'tournoi-detail-page-inscription-responsable-title">\s*([^<]+)\s*<'
name_match = re.search(name_pattern, html_content)
name = name_match.group(1).strip() if name_match else None
# Extract email
email_pattern = r'mailto:([^"]+)"'
email_match = re.search(email_pattern, html_content)
email = email_match.group(1) if email_match else None
# Extract phone
phone_pattern = r'<div class="details-bloc">\s*(\d{2}\s+\d{2}\s+\d{2}\s+\d{2}\s+\d{2})\s*</div>'
phone_match = re.search(phone_pattern, html_content)
phone = phone_match.group(1).strip() if phone_match else None
logger.info(f"Extracted umpire data: name={name}, email={email}, phone={phone}")
return name, email, phone
except Exception as e:
logger.error(f"Error getting umpire data: {e}")
return None, None, None
def _parse_ajax_response(commands):
"""
Parse the AJAX response commands to extract tournament data
Returns data in the exact format expected by Swift FederalTournament struct
"""
tournaments = []
try:
# Check for alert commands (maintenance mode)
for command in commands:
if command.get('command') == 'alert':
logger.warning("Maintenance mode detected")
return None
# Find the command with results
result_command = None
for command in commands:
if command.get('command') == 'recherche_tournois_update':
result_command = command
logger.info("Found recherche_tournois_update command!")
break
if result_command and result_command.get('results'):
results = result_command['results']
items = results.get('items', [])
total_results = results.get('nb_results', 0)
logger.info(f"Processing {len(items)} tournaments from results")
for item in items:
# Parse dates - they're already in the correct format
date_debut = item.get('dateDebut')
date_fin = item.get('dateFin')
date_validation = item.get('dateValidation')
# Build the tournament object to match Swift FederalTournament structure
tournament = {
"id": str(item.get('id', '')),
"millesime": item.get('millesime'),
"libelle": item.get('libelle'),
"tmc": item.get('tmc'),
"tarifAdulteChampionnat": item.get('tarifAdulteChampionnat'),
"type": item.get('type'),
"ageReel": item.get('ageReel'),
"naturesTerrains": item.get('naturesTerrains', []),
"idsArbitres": item.get('idsArbitres', []),
"tarifJeuneChampionnat": item.get('tarifJeuneChampionnat'),
"international": item.get('international'),
"inscriptionEnLigne": item.get('inscriptionEnLigne'),
"categorieTournoi": item.get('categorieTournoi'),
"prixLot": item.get('prixLot'),
"paiementEnLigne": item.get('paiementEnLigne'),
"reductionAdherentJeune": item.get('reductionAdherentJeune'),
"reductionAdherentAdulte": item.get('reductionAdherentAdulte'),
"paiementEnLigneObligatoire": item.get('paiementEnLigneObligatoire'),
"villeEngagement": item.get('villeEngagement'),
"senior": item.get('senior'),
"veteran": item.get('veteran'),
"inscriptionEnLigneEnCours": item.get('inscriptionEnLigneEnCours'),
"avecResultatPublie": item.get('avecResultatPublie'),
"code": item.get('code'),
"categorieAge": item.get('categorieAge'),
"codeComite": item.get('codeComite'),
"installations": item.get('installations', []),
"reductionEpreuveSupplementaireJeune": item.get('reductionEpreuveSupplementaireJeune'),
"reductionEpreuveSupplementaireAdulte": item.get('reductionEpreuveSupplementaireAdulte'),
"nomComite": item.get('nomComite'),
"naturesEpreuves": item.get('naturesEpreuves'),
"jeune": item.get('jeune'),
"courrielEngagement": item.get('courrielEngagement'),
"nomClub": item.get('nomClub'),
"installation": item.get('installation'),
"categorieAgeMax": item.get('categorieAgeMax'),
"tournoiInterne": item.get('tournoiInterne'),
"nomLigue": item.get('nomLigue'),
"nomEngagement": item.get('nomEngagement'),
"codeLigue": item.get('codeLigue'),
"modeleDeBalle": item.get('modeleDeBalle'),
"jugeArbitre": item.get('jugeArbitre'),
"adresse2Engagement": item.get('adresse2Engagement'),
"epreuves": item.get('epreuves'),
"dateDebut": date_debut,
"serie": item.get('serie'),
"dateFin": date_fin,
"dateValidation": date_validation,
"codePostalEngagement": item.get('codePostalEngagement'),
"codeClub": item.get('codeClub'),
"prixEspece": item.get('prixEspece'),
"japPhoneNumber": None, # Will be populated by separate umpire call
# Additional fields from the response
"adresse1Engagement": item.get('adresse1Engagement'),
"originalId": item.get('originalId'),
"familleTournoi": item.get('familleTournoi', []),
"isTournoi": item.get('isTournoi'),
"natureWithCatAge": item.get('natureWithCatAge')
}
tournaments.append(tournament)
logger.info(f"Successfully parsed {len(tournaments)} tournaments from response")
return {
'tournaments': tournaments,
'total_results': total_results,
'current_count': len(tournaments)
}
else:
logger.error("No recherche_tournois_update command found in AJAX response")
return {'tournaments': [], 'total_results': 0, 'current_count': 0}
except Exception as e:
logger.error(f"Error parsing AJAX response: {e}")
return None
def scrape_fft_all_tournaments(sorting_option=None, page=0, start_date=None, end_date=None,
city='', distance=15, categories=None, levels=None,
lat=None, lng=None, ages=None, tournament_types=None,
national_cup=False):
"""
Scrapes FFT tournaments using Playwright with detailed debugging
Based exactly on the working scrape_fft_club_tournaments function
"""
logger.info(f"Starting Playwright scraping for city: {city}")
try:
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page_obj = browser.new_page()
page_obj.set_extra_http_headers({
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Safari/605.1.15"
})
# Navigate to FFT
target_url = "https://tenup.fft.fr/recherche/tournois"
logger.info(f"Navigating to: {target_url}")
page_obj.goto(target_url)
# page_obj.wait_for_timeout(7000)
current_url = page_obj.url
logger.info(f"Current URL: {current_url}")
if "queue-it.net" in current_url.lower():
logger.warning("Still in Queue-It")
browser.close()
return None
# Extract form_build_id
form_input = page_obj.query_selector('input[name="form_build_id"]')
if not form_input:
logger.error("Could not find form_build_id")
browser.close()
return None
form_build_id = form_input.get_attribute('value')
logger.info(f"Extracted form_build_id: {form_build_id}")
# Build parameters - EXACT same pattern as club function
date_component = ""
if start_date and end_date:
date_component = f"&date[start]={start_date}&date[end]={end_date}"
elif start_date:
try:
start_dt = datetime.strptime(start_date, "%d/%m/%y")
end_dt = start_dt + timedelta(days=90)
date_component = f"&date[start]={start_date}&date[end]={end_dt.strftime('%d/%m/%y')}"
except ValueError:
logger.warning(f"Invalid date format: {start_date}")
# Build filter parameters
filter_params = ""
# Add categories filter
if categories:
logger.info(f"Adding categories filter: {categories}")
for category in categories:
filter_params += f"&epreuve[{category}]={category}"
# Add levels filter
if levels:
logger.info(f"Adding levels filter: {levels}")
for level in levels:
filter_params += f"&categorie_tournoi[{level}]={level}"
# Add ages filter
if ages:
logger.info(f"Adding ages filter: {ages}")
for age in ages:
filter_params += f"&categorie_age[{age}]={age}"
# Add types filter
if tournament_types:
logger.info(f"Adding types filter: {tournament_types}")
for t_type in tournament_types:
capitalized_type = t_type.capitalize()
filter_params += f"&type[{capitalized_type}]={capitalized_type}"
# Add national cup filter
if national_cup:
logger.info("Adding national cup filter")
filter_params += "&tournoi_npc=1"
# Fix the sorting parameter
if sorting_option:
sort_param = f"&sort={sorting_option}"
else:
sort_param = "&sort=dateDebut+asc"
# Build city parameters with distance and location
if city and city.strip():
city_name_encoded = city.strip().replace(" ", "+")
# Start with the working base parameters
base_params = f"recherche_type=ville&ville[autocomplete][value_container][value_field]={city_name_encoded}&ville[autocomplete][value_container][label_field]={city_name_encoded}"
# Add distance parameter
distance_param = f"&ville[distance][value_field]={int(distance)}"
# Add lat/lng if provided
location_params = ""
if lat and lng:
location_params = f"&ville[autocomplete][value_container][lat_field]={lat}&ville[autocomplete][value_container][lng_field]={lng}"
# Combine all parameters including filters
params = f"{base_params}{location_params}{distance_param}&pratique=PADEL{date_component}&page={page}{sort_param}&form_build_id={form_build_id}&form_id=recherche_tournois_form&_triggering_element_name=submit_page&_triggering_element_value=Submit+page"
else:
# Default to ligue search if no city provided
params = f"recherche_type=ligue&pratique=PADEL{date_component}{filter_params}&page={page}&sort={sorting_option or 'dateDebut+asc'}&form_build_id={form_build_id}&form_id=recherche_tournois_form&_triggering_element_name=submit_page&_triggering_element_value=Submit+page"
logger.info(f"AJAX Parameters: {params}")
# Make AJAX request and capture the full response - EXACT same as club function
ajax_script = f"""
async () => {{
try {{
const response = await fetch('https://tenup.fft.fr/system/ajax', {{
method: 'POST',
headers: {{
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest',
'Origin': 'https://tenup.fft.fr',
'Referer': 'https://tenup.fft.fr/recherche/tournois'
}},
body: `{params}`
}});
const status = response.status;
const responseText = await response.text();
return {{
success: response.ok,
status: status,
responseText: responseText
}};
}} catch (error) {{
return {{
success: false,
error: error.message
}};
}}
}}
"""
logger.info("Making AJAX request...")
result = page_obj.evaluate(ajax_script)
browser.close()
# Print the full response for debugging - EXACT same as club function
logger.info(f"AJAX Response Status: {result.get('status')}")
logger.info(f"AJAX Response Success: {result.get('success')}")
if result.get('success'):
response_text = result.get('responseText', '')
logger.info(f"Raw Response Length: {len(response_text)}")
try:
# Try to parse as JSON
json_data = json.loads(response_text)
logger.info(f"JSON Response Type: {type(json_data)}")
# Now try to parse it - EXACT same as club function
parsed_result = _parse_ajax_response(json_data)
return parsed_result
except json.JSONDecodeError as json_error:
logger.error(f"JSON Parse Error: {json_error}")
logger.error(f"Response text: {response_text}")
return None
else:
logger.error(f"AJAX request failed: {result.get('error')}")
return None
except Exception as e:
logger.error(f"Error in Playwright scraping: {e}")
logger.error(f"Traceback: {traceback.format_exc()}")
return None

@ -1,6 +1,21 @@
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework.decorators import api_view, permission_classes
from rest_framework import status
from rest_framework.exceptions import MethodNotAllowed
from rest_framework.permissions import IsAuthenticated
from django.conf import settings
from django.http import Http404, HttpResponse, JsonResponse
from django.db.models import Q
from django.core.files.storage import default_storage
from django.core.files.base import ContentFile
from django.shortcuts import get_object_or_404
from .serializers import ClubSerializer, CourtSerializer, DateIntervalSerializer, DrawLogSerializer, TournamentSerializer, UserSerializer, EventSerializer, RoundSerializer, GroupStageSerializer, MatchSerializer, TeamScoreSerializer, TeamRegistrationSerializer, PlayerRegistrationSerializer, PurchaseSerializer, ShortUserSerializer, FailedApiCallSerializer, LogSerializer, DeviceTokenSerializer, CustomUserSerializer, UnregisteredTeamSerializer, UnregisteredPlayerSerializer, ImageSerializer from .serializers import ClubSerializer, CourtSerializer, DateIntervalSerializer, DrawLogSerializer, TournamentSerializer, UserSerializer, EventSerializer, RoundSerializer, GroupStageSerializer, MatchSerializer, TeamScoreSerializer, TeamRegistrationSerializer, PlayerRegistrationSerializer, PurchaseSerializer, ShortUserSerializer, FailedApiCallSerializer, LogSerializer, DeviceTokenSerializer, CustomUserSerializer, UnregisteredTeamSerializer, UnregisteredPlayerSerializer, ImageSerializer
from tournaments.models import Club, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamScore, TeamRegistration, PlayerRegistration, Court, DateInterval, Purchase, FailedApiCall, Log, DeviceToken, DrawLog, UnregisteredTeam, UnregisteredPlayer, Image from tournaments.models import Club, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamScore, TeamRegistration, PlayerRegistration, Court, DateInterval, Purchase, FailedApiCall, Log, DeviceToken, DrawLog, UnregisteredTeam, UnregisteredPlayer, Image
<<<<<<< HEAD
from crm.serializers import CRMActivitySerializer, CRMProspectSerializer, CRMEntitySerializer from crm.serializers import CRMActivitySerializer, CRMProspectSerializer, CRMEntitySerializer
from crm.models import Activity, Prospect, Entity from crm.models import Activity, Prospect, Entity
@ -13,25 +28,21 @@ from rest_framework.exceptions import MethodNotAllowed
from django.http import Http404 from django.http import Http404
from django.db.models import Q from django.db.models import Q
=======
>>>>>>> f988fc1c06ccb26ea16383304e65161733a71fca
from .permissions import IsClubOwner from .permissions import IsClubOwner
from .utils import check_version_smaller_than_1_1_12 from .utils import check_version_smaller_than_1_1_12, scrape_fft_club_tournaments, scrape_fft_club_tournaments_all_pages, get_umpire_data, scrape_fft_all_tournaments
from shared.discord import send_discord_log_message from shared.discord import send_discord_log_message
from rest_framework.decorators import permission_classes
from rest_framework.permissions import IsAuthenticated
from django.shortcuts import get_object_or_404
from tournaments.services.payment_service import PaymentService from tournaments.services.payment_service import PaymentService
from django.conf import settings from tournaments.utils.extensions import create_random_filename
import stripe import stripe
import json import json
import pandas as pd import pandas as pd
from tournaments.utils.extensions import create_random_filename
from django.core.files.storage import default_storage
from django.core.files.base import ContentFile
import os import os
from django.http import HttpResponse
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -572,6 +583,312 @@ def validate_stripe_account(request):
'needs_onboarding': True, 'needs_onboarding': True,
}, status=200) }, status=200)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def is_granted_unlimited_access(request):
can_create = False
if request.user and request.user.is_anonymous == False and request.user.owners:
for owner in request.user.owners.all():
purchases = Purchase.objects.filter(user=owner,product_id='app.padelclub.tournament.subscription.unlimited')
for purchase in purchases:
if purchase.is_active():
can_create = True
return JsonResponse({'can_create': can_create}, status=status.HTTP_200_OK)
@api_view(['GET', 'POST'])
@permission_classes([])
def get_fft_club_tournaments(request):
"""
API endpoint to get tournaments for a specific club
Handles pagination automatically to get all results
"""
try:
if request.method == 'POST':
data = request.data
else:
data = request.GET
club_code = data.get('club_code', '62130180')
club_name = data.get('club_name', 'TENNIS SPORTING CLUB DE CASSIS')
start_date = data.get('start_date')
end_date = data.get('end_date')
paginate = data.get('paginate', 'true').lower() == 'true'
if paginate:
# Get all pages automatically (matching Swift behavior)
result = scrape_fft_club_tournaments_all_pages(
club_code=club_code,
club_name=club_name,
start_date=start_date,
end_date=end_date
)
else:
# Get single page
page = int(data.get('page', 0))
result = scrape_fft_club_tournaments(
club_code=club_code,
club_name=club_name,
start_date=start_date,
end_date=end_date,
page=page
)
if result:
return JsonResponse({
'success': True,
'tournaments': result.get('tournaments', []),
'total_results': result.get('total_results', 0),
'current_count': result.get('current_count', 0),
'pages_scraped': result.get('pages_scraped', 1),
'message': f'Successfully scraped {len(result.get("tournaments", []))} tournaments'
}, status=status.HTTP_200_OK)
else:
return JsonResponse({
'success': False,
'tournaments': [],
'total_results': 0,
'current_count': 0,
'message': 'Failed to scrape club tournaments'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except Exception as e:
logger.error(f"Error in get_fft_club_tournaments endpoint: {e}")
return JsonResponse({
'success': False,
'tournaments': [],
'message': f'Unexpected error: {str(e)}'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
@permission_classes([])
def get_fft_umpire_data(request, tournament_id):
"""
API endpoint to get umpire data for a specific tournament
Returns data that can be used to populate japPhoneNumber field
"""
try:
name, email, phone = get_umpire_data(tournament_id)
return JsonResponse({
'success': True,
'umpire': {
'name': name,
'email': email,
'phone': phone
},
'japPhoneNumber': phone, # Direct field for updating FederalTournament
'message': 'Umpire data retrieved successfully'
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Error in get_fft_umpire_data endpoint: {e}")
return JsonResponse({
'success': False,
'umpire': None,
'japPhoneNumber': None,
'message': f'Unexpected error: {str(e)}'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET', 'POST'])
@permission_classes([])
def get_fft_all_tournaments(request):
"""
API endpoint to get all tournaments with filters
"""
try:
if request.method == 'POST':
data = request.data
else:
data = request.GET
# Extract parameters
sorting_option = data.get('sorting_option', 'dateDebut+asc')
page = int(data.get('page', 0))
start_date = data.get('start_date')
end_date = data.get('end_date')
city = data.get('city', '')
distance = float(data.get('distance', 15))
categories = data.getlist('categories') if hasattr(data, 'getlist') else data.get('categories', [])
levels = data.getlist('levels') if hasattr(data, 'getlist') else data.get('levels', [])
lat = data.get('lat')
lng = data.get('lng')
ages = data.getlist('ages') if hasattr(data, 'getlist') else data.get('ages', [])
tournament_types = data.getlist('types') if hasattr(data, 'getlist') else data.get('types', [])
national_cup = data.get('national_cup', 'false').lower() == 'true'
result = scrape_fft_all_tournaments(
sorting_option=sorting_option,
page=page,
start_date=start_date,
end_date=end_date,
city=city,
distance=distance,
categories=categories,
levels=levels,
lat=lat,
lng=lng,
ages=ages,
tournament_types=tournament_types,
national_cup=national_cup
)
if result:
return JsonResponse({
'success': True,
'tournaments': result.get('tournaments', []),
'total_results': result.get('total_results', 0),
'current_count': result.get('current_count', 0),
'page': page,
'message': f'Successfully scraped {len(result.get("tournaments", []))} tournaments'
}, status=status.HTTP_200_OK)
else:
return JsonResponse({
'success': False,
'tournaments': [],
'total_results': 0,
'current_count': 0,
'message': 'Failed to scrape all tournaments'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except Exception as e:
logger.error(f"Error in get_fft_all_tournaments endpoint: {e}")
return JsonResponse({
'success': False,
'tournaments': [],
'message': f'Unexpected error: {str(e)}'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET', 'POST'])
@permission_classes([])
def get_fft_federal_clubs(request):
"""
API endpoint to get federal clubs
"""
try:
if request.method == 'POST':
data = request.data
else:
data = request.GET
country = data.get('country', 'fr')
city = data.get('city', '')
radius = float(data.get('radius', 15))
latitude = data.get('latitude')
longitude = data.get('longitude')
if latitude:
latitude = float(latitude)
if longitude:
longitude = float(longitude)
result = scrape_federal_clubs(
country=country,
city=city,
radius=radius,
latitude=latitude,
longitude=longitude
)
if result:
return JsonResponse({
'success': True,
'clubs': result,
'message': 'Federal clubs retrieved successfully'
}, status=status.HTTP_200_OK)
else:
return JsonResponse({
'success': False,
'clubs': [],
'message': 'Failed to retrieve federal clubs'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except Exception as e:
logger.error(f"Error in get_fft_federal_clubs endpoint: {e}")
return JsonResponse({
'success': False,
'clubs': [],
'message': f'Unexpected error: {str(e)}'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET', 'POST'])
@permission_classes([])
def get_fft_club_tournaments_with_umpire_data(request):
"""
Combined endpoint that gets club tournaments and enriches them with umpire data
This matches the complete workflow from your Swift code
"""
try:
if request.method == 'POST':
data = request.data
else:
data = request.GET
club_code = data.get('club_code', '62130180')
club_name = data.get('club_name', 'TENNIS SPORTING CLUB DE CASSIS')
start_date = data.get('start_date')
end_date = data.get('end_date')
include_umpire_data = data.get('include_umpire_data', 'false').lower() == 'true'
# Get all tournaments for the club
result = scrape_fft_club_tournaments_all_pages(
club_code=club_code,
club_name=club_name,
start_date=start_date,
end_date=end_date
)
if not result:
return JsonResponse({
'success': False,
'tournaments': [],
'message': 'Failed to scrape club tournaments'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
tournaments = result.get('tournaments', [])
# Enrich with umpire data if requested
if include_umpire_data:
logger.info(f"Enriching {len(tournaments)} tournaments with umpire data...")
for tournament in tournaments:
try:
tournament_id = tournament.get('id')
if tournament_id:
name, email, phone = get_umpire_data(tournament_id)
tournament['japPhoneNumber'] = phone
# Also update jugeArbitre if we got more data
if name and not tournament.get('jugeArbitre'):
tournament['jugeArbitre'] = {
'nom': name.split(' ')[-1] if name else None,
'prenom': ' '.join(name.split(' ')[:-1]) if name and ' ' in name else name
}
# Small delay to avoid rate limiting
time.sleep(0.5)
except Exception as e:
logger.warning(f"Failed to get umpire data for tournament {tournament_id}: {e}")
continue
return JsonResponse({
'success': True,
'tournaments': tournaments,
'total_results': result.get('total_results', 0),
'current_count': len(tournaments),
'pages_scraped': result.get('pages_scraped', 1),
'umpire_data_included': include_umpire_data,
'message': f'Successfully scraped {len(tournaments)} tournaments' +
(' with umpire data' if include_umpire_data else '')
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Error in get_fft_club_tournaments_with_umpire_data endpoint: {e}")
return JsonResponse({
'success': False,
'tournaments': [],
'message': f'Unexpected error: {str(e)}'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
### CRM ### CRM
class CRMActivityViewSet(SoftDeleteViewSet): class CRMActivityViewSet(SoftDeleteViewSet):

@ -204,8 +204,17 @@ LOGGING = {
'backupCount': 10, 'backupCount': 10,
'formatter': 'verbose', 'formatter': 'verbose',
}, },
'mail_admins': {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler'
},
}, },
'loggers': { 'loggers': {
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': True,
},
'django': { 'django': {
'handlers': ['console', 'file'], 'handlers': ['console', 'file'],
'level': 'INFO', 'level': 'INFO',

@ -18,3 +18,4 @@ cryptography==41.0.7
stripe==11.6.0 stripe==11.6.0
django-background-tasks==1.2.8 django-background-tasks==1.2.8
Pillow==10.2.0 Pillow==10.2.0
playwright==1.40.0

@ -90,7 +90,7 @@ class TournamentAdmin(SyncedObjectAdmin):
list_display = ['display_name', 'event', 'is_private', 'start_date', 'payment', 'creator', 'is_canceled'] list_display = ['display_name', 'event', 'is_private', 'start_date', 'payment', 'creator', 'is_canceled']
list_filter = [StartDateRangeFilter, 'is_deleted', 'event__creator'] list_filter = [StartDateRangeFilter, 'is_deleted', 'event__creator']
ordering = ['-start_date'] ordering = ['-start_date']
search_fields = ['id', 'display_name', 'federal_level_category'] search_fields = ['id', 'federal_level_category']
def dashboard_view(self, request): def dashboard_view(self, request):
"""Tournament dashboard view with comprehensive statistics""" """Tournament dashboard view with comprehensive statistics"""
@ -322,7 +322,7 @@ class RoundAdmin(SyncedObjectAdmin):
class PlayerRegistrationAdmin(SyncedObjectAdmin): class PlayerRegistrationAdmin(SyncedObjectAdmin):
list_display = ['first_name', 'last_name', 'licence_id', 'rank'] list_display = ['first_name', 'last_name', 'licence_id', 'rank']
search_fields = ['id', 'first_name', 'last_name', 'licence_id__icontains'] search_fields = ['id', 'first_name', 'last_name', 'licence_id__icontains']
list_filter = ['registered_online', TeamScoreTournamentListFilter] list_filter = ['registered_online', 'payment_id', TeamScoreTournamentListFilter]
ordering = ['last_name', 'first_name'] ordering = ['last_name', 'first_name']
raw_id_fields = ['team_registration'] # Add this line raw_id_fields = ['team_registration'] # Add this line
list_per_page = 50 # Controls pagination on the list view list_per_page = 50 # Controls pagination on the list view

@ -15,9 +15,9 @@ from django.middleware.csrf import get_token
import concurrent.futures import concurrent.futures
from functools import partial from functools import partial
default_sexe = "H" default_sexe = 'F'
default_id_homologation = "82546485" default_id_homologation = "82546485"
default_session_id = "JSESSIONID=CFE4A886CB92764066D1EC920EF9AB1C; AWSALB=c4OHU4Lw6YW6QRsoD1ktcfSgEExZZil/dDetMp3teMKtQ7RlA3VIM8ZHnJH8K3GYMoPu0M61xLjZo64rBNzDEO5tISKEYIX79baengXCKXnaqdqNqHJ7cSPeon+g; AWSALBCORS=c4OHU4Lw6YW6QRsoD1ktcfSgEExZZil/dDetMp3teMKtQ7RlA3VIM8ZHnJH8K3GYMoPu0M61xLjZo64rBNzDEO5tISKEYIX79baengXCKXnaqdqNqHJ7cSPeon+g; tc_cj_v2=m_iZZZ%22**%22%27%20ZZZKQNRSMQNLMRQLZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQOKMQKQONJKOZZZ%5D777_rn_lh%5BfyfcheZZZ222H%2B%7B%7E%20%27-%20%21%20-%20%29%7D%20H%7D*%28ZZZKQOKMQKRLNNPMZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQOKNLQOPMLSMZZZ%5D; tc_cj_v2_cmp=; tc_cj_v2_med=; incap_ses_2223_2712217=/I1fA56LxWI8KbyfPa/ZHpmpZGgAAAAAoadzHEsPdo9W59AkhfXcmQ==; xtan=-; xtant=1; pa_vid=%22mckhos3iasswydjm%22; datadome=oi7wKIO2uaUDCcpICiRO1_hEYcwyQWDVbXyNCSkAmr315~8pnPcuXWKfvNEEz~jKcoORIOThSRe~AxoRRrPLUsr0miWm7AdAcy~_3hABc1ZWfRt~SKGa_uhyqiE0Hzfj; _pcid=%7B%22browserId%22%3A%22mckhos3iasswydjm%22%2C%22_t%22%3A%22ms8wm9hs%7Cmckhos5s%22%7D; _pctx=%7Bu%7DN4IgrgzgpgThIC4B2YA2qA05owMoBcBDfSREQpAeyRCwgEt8oBJAE0RXSwH18yBbCAA4A7vwCcACwgAffgGMA1pMoQArPAC%2BQA; EA_SESSION_ID=E15E1DD5A23272A1A0CC3B8CEDF56B65; refresh_token=eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIzYjQ2ODk1ZC0zN2EzLTQzM2QtYmQ1My01N2QxZTM1YTI3NzkifQ.eyJleHAiOjE3NTY1NTM5MjgsImlhdCI6MTc1MTM3MjAwNCwianRpIjoiYzJiNzA3N2UtZmQ5MS00ZGM4LWI4ZDEtMzA2MDdkYjk5MTgxIiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5mZnQuZnIvcmVhbG1zL2Nvbm5lY3QiLCJhdWQiOiJodHRwczovL2xvZ2luLmZmdC5mci9yZWFsbXMvY29ubmVjdCIsInN1YiI6IjI3ZDQ5NzRjLTEwZWUtNDNlOC1iOTczLWUyMzc2MDM1ZTE0MSIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJtZWEtc2l0ZSIsInNpZCI6IjM5NTZjMzZlLTczMWItNDJkNy1iNjI2LTE2MGViY2Y2YTY2ZiIsInNjb3BlIjoib3BlbmlkIHJvbGVzIHJlYWQ6bGljZW5jZSByZWFkOmlkZW50aXR5IGVtYWlsIHByb2ZpbGUifQ.e6v5vlen985vSFJhrgMQTTB3fzzsnwugPfXKoyib1QSIBZ9kC47h1cYwcpam0VmZ9vRD_y0hVC14jDvBR6d1dQ; user_login=10000984864; user_token=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJRaTV3bWx2bTNuX2p1YW4tSTl1dHo3UGZRLU1tVVlvektwSExhbm9lTXI4In0.eyJleHAiOjE3NTEzNzIzMDQsImlhdCI6MTc1MTM3MjAwNCwianRpIjoiMzEzMGVhODUtNjFjNC00OGRjLWFlNGMtZTIwZmZhYTU3YTlhIiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5mZnQuZnIvcmVhbG1zL2Nvbm5lY3QiLCJhdWQiOlsiZmVkLWFwaSIsImFjY291bnQiXSwic3ViIjoiMjdkNDk3NGMtMTBlZS00M2U4LWI5NzMtZTIzNzYwMzVlMTQxIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoibWVhLXNpdGUiLCJzaWQiOiIzOTU2YzM2ZS03MzFiLTQyZDctYjYyNi0xNjBlYmNmNmE2NmYiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1jb25uZWN0Il19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCJdfX0sInNjb3BlIjoib3BlbmlkIHJlYWQ6bGljZW5jZSByZWFkOmlkZW50aXR5IGVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaWRDcm0iOiIxMDAwMDk4NDg2NCIsIm5hbWUiOiJSYXptaWcgU0FSS0lTU0lBTiIsInByZWZlcnJlZF91c2VybmFtZSI6InJhem1vZyIsImdpdmVuX25hbWUiOiJSYXptaWciLCJzZXNzaW9uX3N0YXRlIjoiMzk1NmMzNmUtNzMxYi00MmQ3LWI2MjYtMTYwZWJjZjZhNjZmIiwibG9jYWxlIjoiZnIiLCJmYW1pbHlfbmFtZSI6IlNBUktJU1NJQU4iLCJlbWFpbCI6InJhem1pZy5zYXJraXNzaWFuQGdtYWlsLmNvbSJ9.VSjG2htaUMt_acrqL3VcAjVMhAno9q0vdb7LTzw8UVbjIiDLzhR5msRxI8h8gSJ38kFLaa7f_SFGLIsRCSdcmhYRd2zKIrcPE-QFKbsPnH69xN2i3giMMiYEy3hj__IIyijt9z3W4KXeQdwUrlXPxprlXQ2sYTlZG63HlCGq1iI3Go9eXFmNDNM6p1jBypXcHEvJr6HwNcRdn6ZGfZ9LLMZ2aMEJAhDqL2CLrFrOZkGQpFz7ITUi_DVJAqh5DmTK1JqPswcOjhuZhDT7qWNfIleV-L7XCwvofxBwkSX9ve9l_3COZJXbsMiiRdCSTZtewlFRfgo4IuAu3g06fmJw7g; TCID=; nlbi_2712217=Ok4tKplxIEN+k1gmb9lUTgAAAAA70zbGXpiElrV2qkRjBeXO; visid_incap_2712217=LW/brcN4Rwml/7waoG/rloFBYmgAAAAAQUIPAAAAAAAlHbwlYSPbNS2qq3UBZNK8; TCPID=125629554310878226394; xtvrn=$548419$" default_session_id = "JSESSIONID=CEC70DF4428E76E1FD1BFE5C66904708; AWSALB=omN79AoahQc27iH5vvO14U7ZrjH30faWu5delXAthjiYVq4jzbeXJ0IOmVTGjG6YDoi7Do2uCswhEaO/smz1QG733RpYlsw7ShlFV/X2aLn2L7/DZ5KUBA/8LPNr; AWSALBCORS=omN79AoahQc27iH5vvO14U7ZrjH30faWu5delXAthjiYVq4jzbeXJ0IOmVTGjG6YDoi7Do2uCswhEaO/smz1QG733RpYlsw7ShlFV/X2aLn2L7/DZ5KUBA/8LPNr; incap_ses_2223_2712217=g6xvVwmOBh66wpenPa/ZHpN2ZmgAAAAAcmuXPCKJ1/mEqKuQEXJS2Q==; tc_cj_v2=m_iZZZ%22**%22%27%20ZZZKQNRSMQNLMRQLZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQOKMQKQONJKOZZZ%5D777_rn_lh%5BfyfcheZZZ222H%2B%7B%7E%20%27-%20%21%20-%20%29%7D%20H%7D*%28ZZZKQOKMQKRLNNPMZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQOKNRJOLLQOJZZZ%5D777_rn_lh%5BfyfcheZZZ%2F%20%290%2BH%2C0%200%20G%24%2FH%29%20%2FZZZKQOKOLRLNNQJLZZZ%5D; tc_cj_v2_cmp=; tc_cj_v2_med=; SSESS7ba44afc36c80c3faa2b8fa87e7742c5=4-IzUXNKXq_BQFMLjjivcLW14OXgk3lLPl18WYgSmU0; xtan=-; xtant=1; pa_vid=%22mckhos3iasswydjm%22; datadome=oi7wKIO2uaUDCcpICiRO1_hEYcwyQWDVbXyNCSkAmr315~8pnPcuXWKfvNEEz~jKcoORIOThSRe~AxoRRrPLUsr0miWm7AdAcy~_3hABc1ZWfRt~SKGa_uhyqiE0Hzfj; _pcid=%7B%22browserId%22%3A%22mckhos3iasswydjm%22%2C%22_t%22%3A%22ms8wm9hs%7Cmckhos5s%22%7D; _pctx=%7Bu%7DN4IgrgzgpgThIC4B2YA2qA05owMoBcBDfSREQpAeyRCwgEt8oBJAE0RXSwH18yBbCAA4A7vwCcACwgAffgGMA1pMoQArPAC%2BQA; EA_SESSION_ID=E15E1DD5A23272A1A0CC3B8CEDF56B65; refresh_token=eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIzYjQ2ODk1ZC0zN2EzLTQzM2QtYmQ1My01N2QxZTM1YTI3NzkifQ.eyJleHAiOjE3NTY1NTM5MjgsImlhdCI6MTc1MTM3MjAwNCwianRpIjoiYzJiNzA3N2UtZmQ5MS00ZGM4LWI4ZDEtMzA2MDdkYjk5MTgxIiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5mZnQuZnIvcmVhbG1zL2Nvbm5lY3QiLCJhdWQiOiJodHRwczovL2xvZ2luLmZmdC5mci9yZWFsbXMvY29ubmVjdCIsInN1YiI6IjI3ZDQ5NzRjLTEwZWUtNDNlOC1iOTczLWUyMzc2MDM1ZTE0MSIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJtZWEtc2l0ZSIsInNpZCI6IjM5NTZjMzZlLTczMWItNDJkNy1iNjI2LTE2MGViY2Y2YTY2ZiIsInNjb3BlIjoib3BlbmlkIHJvbGVzIHJlYWQ6bGljZW5jZSByZWFkOmlkZW50aXR5IGVtYWlsIHByb2ZpbGUifQ.e6v5vlen985vSFJhrgMQTTB3fzzsnwugPfXKoyib1QSIBZ9kC47h1cYwcpam0VmZ9vRD_y0hVC14jDvBR6d1dQ; user_login=10000984864; user_token=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJRaTV3bWx2bTNuX2p1YW4tSTl1dHo3UGZRLU1tVVlvektwSExhbm9lTXI4In0.eyJleHAiOjE3NTEzNzIzMDQsImlhdCI6MTc1MTM3MjAwNCwianRpIjoiMzEzMGVhODUtNjFjNC00OGRjLWFlNGMtZTIwZmZhYTU3YTlhIiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5mZnQuZnIvcmVhbG1zL2Nvbm5lY3QiLCJhdWQiOlsiZmVkLWFwaSIsImFjY291bnQiXSwic3ViIjoiMjdkNDk3NGMtMTBlZS00M2U4LWI5NzMtZTIzNzYwMzVlMTQxIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoibWVhLXNpdGUiLCJzaWQiOiIzOTU2YzM2ZS03MzFiLTQyZDctYjYyNi0xNjBlYmNmNmE2NmYiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1jb25uZWN0Il19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCJdfX0sInNjb3BlIjoib3BlbmlkIHJlYWQ6bGljZW5jZSByZWFkOmlkZW50aXR5IGVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaWRDcm0iOiIxMDAwMDk4NDg2NCIsIm5hbWUiOiJSYXptaWcgU0FSS0lTU0lBTiIsInByZWZlcnJlZF91c2VybmFtZSI6InJhem1vZyIsImdpdmVuX25hbWUiOiJSYXptaWciLCJzZXNzaW9uX3N0YXRlIjoiMzk1NmMzNmUtNzMxYi00MmQ3LWI2MjYtMTYwZWJjZjZhNjZmIiwibG9jYWxlIjoiZnIiLCJmYW1pbHlfbmFtZSI6IlNBUktJU1NJQU4iLCJlbWFpbCI6InJhem1pZy5zYXJraXNzaWFuQGdtYWlsLmNvbSJ9.VSjG2htaUMt_acrqL3VcAjVMhAno9q0vdb7LTzw8UVbjIiDLzhR5msRxI8h8gSJ38kFLaa7f_SFGLIsRCSdcmhYRd2zKIrcPE-QFKbsPnH69xN2i3giMMiYEy3hj__IIyijt9z3W4KXeQdwUrlXPxprlXQ2sYTlZG63HlCGq1iI3Go9eXFmNDNM6p1jBypXcHEvJr6HwNcRdn6ZGfZ9LLMZ2aMEJAhDqL2CLrFrOZkGQpFz7ITUi_DVJAqh5DmTK1JqPswcOjhuZhDT7qWNfIleV-L7XCwvofxBwkSX9ve9l_3COZJXbsMiiRdCSTZtewlFRfgo4IuAu3g06fmJw7g; TCID=; nlbi_2712217=Ok4tKplxIEN+k1gmb9lUTgAAAAA70zbGXpiElrV2qkRjBeXO; visid_incap_2712217=LW/brcN4Rwml/7waoG/rloFBYmgAAAAAQUIPAAAAAAAlHbwlYSPbNS2qq3UBZNK8; TCPID=125629554310878226394; xtvrn=$548419$"
def calculate_age_from_birth_date(birth_date_str): def calculate_age_from_birth_date(birth_date_str):
""" """
@ -36,9 +36,10 @@ def calculate_age_from_birth_date(birth_date_str):
except (ValueError, TypeError): except (ValueError, TypeError):
return None return None
def find_best_license_match(license_results, player_age_sportif): def find_best_license_match(license_results, player):
""" """
Find the best matching license from multiple results using ageSportif comparison Find the best matching license from multiple results using ageSportif comparison
Also filters out players without valid classement data
Args: Args:
license_results: List of license data from API license_results: List of license data from API
@ -47,21 +48,90 @@ def find_best_license_match(license_results, player_age_sportif):
Returns: Returns:
Tuple of (best_match, match_info) Tuple of (best_match, match_info)
""" """
# Get player's age from ranking data for duplicate matching
player_age_sportif = player.get('ageSportif')
rank = player.get('classement')
lastname = player.get('nom')
firstname = player.get('prenom')
if not license_results: if not license_results:
return None, {"reason": "no_results"} return None, {"reason": "no_results"}
if len(license_results) == 1: # First, filter out players without valid classement data
return license_results[0], {"reason": "single_result", "age_match": "n/a"} def has_valid_classement(license_data, rank):
"""Check if a license has valid classement data"""
classement = license_data.get('classement', {})
if not classement:
return False
# Check if any of the key classement fields have meaningful data
date_fr = classement.get('dateFr', '').strip()
rang = classement.get('rang')
points = classement.get('points')
date = classement.get('date')
# Consider it valid if at least one of these conditions is met:
# - dateFr is not empty
# - rang is not None
# - points is not None (and > 0)
# - date is not None
return (
rang is not None and rang == rank
)
# First, filter out players without valid classement data
def has_valid_name(license_data, firstname, lastname):
lk_firstname = license_data.get('prenom', '')
lk_lastname = license_data.get('nom', '')
if not lk_firstname and not lk_lastname:
return False
return (
lk_firstname == firstname and lk_lastname == lastname
)
# Filter license results to only include those with valid classement
valid_license_results = [
license_data for license_data in license_results
if has_valid_name(license_data, firstname, lastname)
if has_valid_classement(license_data, rank)
]
# If no valid results after filtering, return None
if not valid_license_results:
return None, {
"reason": "no_valid_classement",
"original_count": len(license_results),
"filtered_count": 0
}
# If only one valid result, return it
if len(valid_license_results) == 1:
return valid_license_results[0], {
"reason": "single_valid_result",
"original_count": len(license_results),
"filtered_count": 1,
"age_match": "n/a"
}
# If we don't have ageSportif from ranking, take the first match # If we don't have ageSportif from ranking, take the first valid match
if player_age_sportif is None: if player_age_sportif is None:
return license_results[0], {"reason": "no_age_data", "used_first_result": True} return valid_license_results[0], {
"reason": "no_age_data_used_first_valid",
"original_count": len(license_results),
"filtered_count": len(valid_license_results),
"used_first_result": True
}
best_match = None best_match = None
best_age_diff = float('inf') best_age_diff = float('inf')
match_details = [] match_details = []
best_match_count = 0
for i, license_data in enumerate(license_results): for i, license_data in enumerate(valid_license_results):
birth_date_fr = license_data.get('dateNaissanceFr') birth_date_fr = license_data.get('dateNaissanceFr')
calculated_age = calculate_age_from_birth_date(birth_date_fr) calculated_age = calculate_age_from_birth_date(birth_date_fr)
@ -70,36 +140,53 @@ def find_best_license_match(license_results, player_age_sportif):
"dateNaissanceFr": birth_date_fr, "dateNaissanceFr": birth_date_fr,
"calculated_age": calculated_age, "calculated_age": calculated_age,
"player_age_sportif": player_age_sportif, "player_age_sportif": player_age_sportif,
"age_difference": None "age_difference": None,
"license": license_data.get('licence'),
"classement": license_data.get('classement', {})
} }
if calculated_age is not None: if calculated_age is not None:
age_diff = abs(calculated_age - player_age_sportif) age_diff = abs(calculated_age - player_age_sportif)
match_detail["age_difference"] = age_diff match_detail["age_difference"] = age_diff
if age_diff < best_age_diff: if age_diff < best_age_diff and best_age_diff > 1 and age_diff < 2:
best_age_diff = age_diff best_age_diff = age_diff
best_match = license_data best_match = license_data
best_match_count = 1
elif age_diff <= best_age_diff:
best_match_count += 1
match_details.append(match_detail) match_details.append(match_detail)
# If no match found with valid age, use first result # If no match found with valid age, use first valid result
if best_match is None: if best_match is None:
best_match = license_results[0]
match_info = { match_info = {
"reason": "no_valid_ages", "reason": "no_valid_ages_used_first_valid",
"original_count": len(license_results),
"filtered_count": len(valid_license_results),
"used_first_result": True, "used_first_result": True,
"match_details": match_details "match_details": match_details
} }
return valid_license_results[0], match_info
else: else:
match_info = { if best_match_count == 1:
"reason": "age_matched", match_info = {
"best_age_difference": best_age_diff, "reason": "age_matched",
"total_candidates": len(license_results), "best_age_difference": best_age_diff,
"match_details": match_details "total_candidates": len(license_results),
} "valid_candidates": len(valid_license_results),
"match_details": match_details
return best_match, match_info }
return best_match, match_info
else:
match_info = {
"reason": "multiple_matches",
"best_age_difference": best_age_diff,
"total_candidates": len(license_results),
"valid_candidates": len(valid_license_results),
"match_details": match_details
}
return None, match_info
@staff_member_required @staff_member_required
def test_player_details_apis(request): def test_player_details_apis(request):
@ -1089,8 +1176,8 @@ def search_player_by_name(request):
if request.method == 'POST': if request.method == 'POST':
session_id = request.POST.get('sessionId', '').strip() session_id = request.POST.get('sessionId', '').strip()
id_homologation = request.POST.get('idHomologation', '').strip() id_homologation = request.POST.get('idHomologation', '').strip()
nom = request.POST.get('nom', '').strip() nom = request.POST.get('nom', '')
prenom = request.POST.get('prenom', '').strip() prenom = request.POST.get('prenom', '')
if not session_id or not id_homologation: if not session_id or not id_homologation:
messages.error(request, "sessionId and idHomologation are required.") messages.error(request, "sessionId and idHomologation are required.")
@ -1396,11 +1483,11 @@ def enrich_rankings_with_licenses(request):
return player, False, None return player, False, None
# Keep original case and accents, just clean up any extra whitespace # Keep original case and accents, just clean up any extra whitespace
nom = raw_nom.strip() nom = raw_nom
prenom = raw_prenom.strip() prenom = raw_prenom
# Get player's gender # Get player's gender
sexe = player.get('sexe', 'H') # Default to 'H' for male sexe = player.get('sexe', default_sexe) # Default to 'H' for male
# Setup license lookup headers # Setup license lookup headers
license_headers = { license_headers = {
@ -1440,11 +1527,12 @@ def enrich_rankings_with_licenses(request):
presence_doublon = license_data['object'].get('presenceDoublon', False) presence_doublon = license_data['object'].get('presenceDoublon', False)
if liste_joueurs: if liste_joueurs:
# Get player's age from ranking data for duplicate matching
player_age_sportif = player.get('ageSportif')
# Find the best matching license using age comparison # Find the best matching license using age comparison
license_info, match_info = find_best_license_match(liste_joueurs, player_age_sportif) license_info, match_info = find_best_license_match(liste_joueurs, player)
if license_info is None:
player['license_lookup_status'] = 'too_many_results'
player['presenceDoublon'] = presence_doublon
return player, False, f"Failed {nom} {prenom} {player['idCrm']} -> Too many results"
# Add all license data to player # Add all license data to player
player['licence'] = license_info.get('licence') player['licence'] = license_info.get('licence')
@ -1476,7 +1564,7 @@ def enrich_rankings_with_licenses(request):
else: else:
player['license_lookup_status'] = 'no_results' player['license_lookup_status'] = 'no_results'
player['presenceDoublon'] = presence_doublon player['presenceDoublon'] = presence_doublon
return player, False, f"Failed {nom} {prenom} {player['idCrm']} -> No results" return player, False, f"Failed {nom} {prenom} {player['idCrm']} {presence_doublon} -> No results"
else: else:
player['license_lookup_status'] = 'no_data' player['license_lookup_status'] = 'no_data'
return player, False, f"Failed {nom} {prenom} {player['idCrm']} -> No data" return player, False, f"Failed {nom} {prenom} {player['idCrm']} -> No data"
@ -1690,7 +1778,7 @@ def enrich_rankings_with_licenses(request):
<div class="form-group"> <div class="form-group">
<label for="max_workers">Concurrent Workers (1-50):</label> <label for="max_workers">Concurrent Workers (1-50):</label>
<input type="number" id="max_workers" name="max_workers" class="form-control" value="10" min="1" max="200"> <input type="number" id="max_workers" name="max_workers" class="form-control" value="100" min="1" max="200">
<small>Higher values = faster processing but more server load. Adjust based on server capacity.</small> <small>Higher values = faster processing but more server load. Adjust based on server capacity.</small>
</div> </div>

@ -20,10 +20,12 @@ class Command(BaseCommand):
parser.add_argument('--clubs', type=int, default=10, help='Number of top clubs to display') parser.add_argument('--clubs', type=int, default=10, help='Number of top clubs to display')
parser.add_argument('--leagues', type=int, default=10, help='Number of top leagues to display') parser.add_argument('--leagues', type=int, default=10, help='Number of top leagues to display')
parser.add_argument('--find-anonymous', action='store_true', help='Find and match anonymous players with previous month rankings') parser.add_argument('--find-anonymous', action='store_true', help='Find and match anonymous players with previous month rankings')
parser.add_argument('--confidence', type=float, default=0.7, help='Confidence threshold for automatic matching (0-1)') parser.add_argument('--confidence', type=int, default=7, help='Confidence threshold for automatic matching (0-1)')
parser.add_argument('--auto-match', action='store_true', help='Automatically match anonymous players when confidence is high') parser.add_argument('--auto-match', action='store_true', help='Automatically match anonymous players when confidence is high')
parser.add_argument('--output', type=str, help='Save results to output file') parser.add_argument('--output', type=str, help='Save results to output file')
parser.add_argument('--verbose', action='store_true', help='Show detailed matching information') parser.add_argument('--verbose', action='store_true', help='Show detailed matching information')
parser.add_argument('--named-only', action='store_true', help='Process only anonymous players WITH names (missing license)')
parser.add_argument('--unnamed-only', action='store_true', help='Process only anonymous players WITHOUT names')
def handle(self, *args, **options): def handle(self, *args, **options):
# Base directory for rankings files # Base directory for rankings files
@ -53,7 +55,7 @@ class Command(BaseCommand):
# Generate statistics # Generate statistics
if players: if players:
self.generate_statistics(players, options) # self.generate_statistics(players, options)
# Find anonymous players if requested # Find anonymous players if requested
if options['find_anonymous']: if options['find_anonymous']:
@ -243,10 +245,17 @@ class Command(BaseCommand):
self.stdout.write(self.style.SUCCESS(f'\nAnonymous players: {len(anonymous_players)} ({(len(anonymous_players) / total_players) * 100:.1f}%)')) self.stdout.write(self.style.SUCCESS(f'\nAnonymous players: {len(anonymous_players)} ({(len(anonymous_players) / total_players) * 100:.1f}%)'))
def is_anonymous_player(self, player): def is_anonymous_player(self, player):
"""Check if a player is anonymous (missing name data)""" """Check if a player is anonymous (missing name data or license)"""
# Define criteria for anonymous players - adjust as needed # Player is anonymous if they have no name data
return (player['name'] == 'N/A' or player['name'] == '' or if (player['name'] == 'N/A' or player['name'] == '' or
player['first_name'] == 'N/A' or player['first_name'] == '') player['first_name'] == 'N/A' or player['first_name'] == ''):
return True
# Player is also anonymous if they have name but no license
if (player['license'] == 'N/A' or player['license'] == ''):
return True
return False
def player_exists_in_current_month(self, prev_player, current_players_indexes): def player_exists_in_current_month(self, prev_player, current_players_indexes):
""" """
@ -276,8 +285,11 @@ class Command(BaseCommand):
self.stdout.write("Building player indexes for fast lookup...") self.stdout.write("Building player indexes for fast lookup...")
start_time = datetime.now() start_time = datetime.now()
# Players to index (only non-anonymous) # Only index players that have BOTH name AND license
players_to_index = [p for p in current_players if not self.is_anonymous_player(p)] players_to_index = [p for p in current_players
if (p['license'] != 'N/A' and p['license'] != '') and
(p['name'] != 'N/A' and p['name'] != '') and
(p['first_name'] != 'N/A' and p['first_name'] != '')]
# Create license index # Create license index
license_index = {} license_index = {}
@ -376,14 +388,59 @@ class Command(BaseCommand):
matched_count = 0 matched_count = 0
# Identify anonymous players # Identify anonymous players
anonymous_players = [p for p in current_players if self.is_anonymous_player(p)] all_anonymous_players = [p for p in current_players if self.is_anonymous_player(p)]
if not anonymous_players: if not all_anonymous_players:
self.stdout.write(self.style.SUCCESS('No anonymous players found!')) self.stdout.write(self.style.SUCCESS('No anonymous players found!'))
if return_count: if return_count:
return 0 return 0
return return
self.stdout.write(self.style.SUCCESS(f'\nFound {len(anonymous_players)} anonymous players. Looking for matches...')) # Check for conflicting options
if options['named_only'] and options['unnamed_only']:
self.stderr.write(self.style.ERROR('Cannot use both --named-only and --unnamed-only options together'))
if return_count:
return 0
return
# Sort anonymous players by type
anonymous_players_with_names = []
anonymous_players_without_names = []
for player in all_anonymous_players:
if (player['name'] != 'N/A' and player['name'] != '' and
player['first_name'] != 'N/A' and player['first_name'] != ''):
anonymous_players_with_names.append(player)
else:
anonymous_players_without_names.append(player)
# Select which players to process based on options
if options['named_only']:
anonymous_players = anonymous_players_with_names
processing_type = "named anonymous players (with names but missing license)"
elif options['unnamed_only']:
anonymous_players = anonymous_players_without_names
processing_type = "unnamed anonymous players (missing names)"
else:
# Default behavior: process named players first, then unnamed
anonymous_players = anonymous_players_with_names + anonymous_players_without_names
processing_type = "all anonymous players (named first, then unnamed)"
if not anonymous_players:
if options['named_only']:
self.stdout.write(self.style.SUCCESS('No anonymous players with names found!'))
elif options['unnamed_only']:
self.stdout.write(self.style.SUCCESS('No anonymous players without names found!'))
if return_count:
return 0
return
# Display summary
self.stdout.write(self.style.SUCCESS(f'\nProcessing {processing_type}'))
self.stdout.write(f'Anonymous players breakdown:')
self.stdout.write(f' Total found: {len(all_anonymous_players)}')
self.stdout.write(f' With names: {len(anonymous_players_with_names)}')
self.stdout.write(f' Without names: {len(anonymous_players_without_names)}')
self.stdout.write(f' Selected for processing: {len(anonymous_players)}')
# Find previous month file # Find previous month file
prev_month_file = self.find_previous_month_file(current_metadata, rankings_dir) prev_month_file = self.find_previous_month_file(current_metadata, rankings_dir)
@ -422,7 +479,19 @@ class Command(BaseCommand):
# Show progress # Show progress
progress_counter += 1 progress_counter += 1
if progress_counter % progress_interval == 0 or progress_counter == 1: if progress_counter % progress_interval == 0 or progress_counter == 1:
self.stdout.write(f' Processing anonymous player {progress_counter}/{len(anonymous_players)} ({(progress_counter/len(anonymous_players))*100:.1f}%)') # Determine which type of player we're processing
if options['named_only']:
player_type = "named"
elif options['unnamed_only']:
player_type = "unnamed"
else:
# Default behavior: check if we're still processing named players
if progress_counter <= len(anonymous_players_with_names):
player_type = "named"
else:
player_type = "unnamed"
self.stdout.write(f' Processing {player_type} anonymous player {progress_counter}/{len(anonymous_players)} ({(progress_counter/len(anonymous_players))*100:.1f}%)')
potential_matches = self.find_potential_matches(anon_player, prev_players, current_players_indexes, options) potential_matches = self.find_potential_matches(anon_player, prev_players, current_players_indexes, options)
@ -442,7 +511,12 @@ class Command(BaseCommand):
progression = f", Progression: {anon_player['progression']}" if anon_player['progression'] != 'N/A' else "" progression = f", Progression: {anon_player['progression']}" if anon_player['progression'] != 'N/A' else ""
assimilation = f", Assimilation: {anon_player['assimilation']}" if anon_player['assimilation'] != 'N/A' else "" assimilation = f", Assimilation: {anon_player['assimilation']}" if anon_player['assimilation'] != 'N/A' else ""
self.stdout.write(f"\nAnonymous player: Rank {anon_player['rank']}, League: {anon_player['league']}{progression}{assimilation}") # Show if this is a named or unnamed anonymous player
if (anon_player['name'] != 'N/A' and anon_player['name'] != '' and
anon_player['first_name'] != 'N/A' and anon_player['first_name'] != ''):
self.stdout.write(f"\nNamed anonymous player: {anon_player['name']} {anon_player['first_name']} - Rank {anon_player['rank']}, League: {anon_player['league']}{progression}{assimilation}")
else:
self.stdout.write(f"\nUnnamed anonymous player: Rank {anon_player['rank']}, League: {anon_player['league']}{progression}{assimilation}")
for i, match in enumerate(potential_matches[:3]): # Show top 3 matches for i, match in enumerate(potential_matches[:3]): # Show top 3 matches
player = match['player'] player = match['player']
@ -456,7 +530,11 @@ class Command(BaseCommand):
high_confidence_matches += 1 high_confidence_matches += 1
else: else:
if options['verbose']: if options['verbose']:
self.stdout.write(f"\nNo matches found for anonymous player: Rank {anon_player['rank']}, League: {anon_player['league']}") if (anon_player['name'] != 'N/A' and anon_player['name'] != '' and
anon_player['first_name'] != 'N/A' and anon_player['first_name'] != ''):
self.stdout.write(f"\nNo matches found for named anonymous player: {anon_player['name']} {anon_player['first_name']} - Rank {anon_player['rank']}, League: {anon_player['league']}")
else:
self.stdout.write(f"\nNo matches found for unnamed anonymous player: Rank {anon_player['rank']}, League: {anon_player['league']}")
# Batch processing status update # Batch processing status update
if progress_counter % 100 == 0 and progress_counter > 0: if progress_counter % 100 == 0 and progress_counter > 0:
@ -472,7 +550,11 @@ class Command(BaseCommand):
# Summary # Summary
self.stdout.write(self.style.SUCCESS(f'\nMatching summary:')) self.stdout.write(self.style.SUCCESS(f'\nMatching summary:'))
self.stdout.write(f'Total anonymous players: {len(anonymous_players)}') self.stdout.write(f'Processing mode: {processing_type}')
self.stdout.write(f'Anonymous players processed: {len(anonymous_players)}')
if not options['named_only'] and not options['unnamed_only']:
self.stdout.write(f' Named: {len(anonymous_players_with_names)}')
self.stdout.write(f' Unnamed: {len(anonymous_players_without_names)}')
self.stdout.write(f'Players with potential matches: {matches_found}') self.stdout.write(f'Players with potential matches: {matches_found}')
self.stdout.write(f'High confidence matches (≥{options["confidence"]}): {high_confidence_matches}') self.stdout.write(f'High confidence matches (≥{options["confidence"]}): {high_confidence_matches}')
self.stdout.write(f'Skipped players already in current month: {skipped_existing_players}') self.stdout.write(f'Skipped players already in current month: {skipped_existing_players}')
@ -484,6 +566,7 @@ class Command(BaseCommand):
# Auto-match players if requested # Auto-match players if requested
if options['auto_match'] and matches_found > 0 and file_path: if options['auto_match'] and matches_found > 0 and file_path:
# Note: We pass the selected anonymous_players for matching
matched_count = self.update_rankings_with_matches(file_path, anonymous_players, results, matched_count = self.update_rankings_with_matches(file_path, anonymous_players, results,
options['confidence'], options) options['confidence'], options)
elif options['auto_match'] and file_path is None: elif options['auto_match'] and file_path is None:
@ -546,32 +629,40 @@ class Command(BaseCommand):
if options['verbose']: if options['verbose']:
self.stdout.write(f" Target previous rank: {prev_rank_from_progression} (current rank {anon_rank} + progression {prog_value})") self.stdout.write(f" Target previous rank: {prev_rank_from_progression} (current rank {anon_rank} + progression {prog_value})")
# Show anonymous player details
self.stdout.write("\n" + "="*80)
self.stdout.write(f"Looking for matches for anonymous player at rank {anon_player['rank']}:")
self.stdout.write(f" Points: {anon_player['points']}")
self.stdout.write(f" Assimilation: {anon_player['assimilation']}")
self.stdout.write(f" Tournaments: {anon_player['tournaments_played']}")
self.stdout.write(f" League: {anon_player['league']}")
if anon_player['name'] != 'N/A' and anon_player['first_name'] != 'N/A':
self.stdout.write(f" Name: {anon_player['name']} {anon_player['first_name']}")
self.stdout.write("-"*80)
for prev_player in prev_players: for prev_player in prev_players:
# Skip anonymous players in previous month # Skip anonymous players in previous month
if self.is_anonymous_player(prev_player): if self.is_anonymous_player(prev_player):
continue continue
# Check if this player exists in current month with the same license
exists, existing_player = self.player_exists_in_current_month(prev_player, current_players_indexes)
if exists:
# If we found the exact same player (same license), skip them
if existing_player['license'] == prev_player['license']:
skipped_players += 1
continue
# If we found someone with the same name but different license, we can still consider this player
# Initialize match data # Initialize match data
match_data = { match_data = {
'player': prev_player, 'player': prev_player,
'rank_match_type': None,
'rank_diff': None,
'has_league_match': False,
'has_assimilation_match': False,
'points_similarity': 0.0,
'match_reasons': [], 'match_reasons': [],
'confidence': 0.0 'confidence': 0
} }
# Print candidate details
self.stdout.write(f"\nChecking candidate: {prev_player['name']} {prev_player['first_name']}")
self.stdout.write(f" Rank: {prev_player['rank']}")
self.stdout.write(f" Points: {prev_player['points']}")
self.stdout.write(f" Assimilation: {prev_player['assimilation']}")
self.stdout.write(f" Tournaments: {prev_player['tournaments_played']}")
self.stdout.write(f" League: {prev_player['league']}")
# Start building confidence score
confidence_details = []
# 1. PRIMARY MATCHER: Previous rank match # 1. PRIMARY MATCHER: Previous rank match
if prev_rank_from_progression is not None: if prev_rank_from_progression is not None:
try: try:
@ -583,68 +674,142 @@ class Command(BaseCommand):
if rank_diff == 0: if rank_diff == 0:
match_data['rank_match_type'] = 'exact' match_data['rank_match_type'] = 'exact'
match_data['match_reasons'].append(f"exact previous rank match ({prev_rank_value})") match_data['match_reasons'].append(f"exact previous rank match ({prev_rank_value})")
match_data['confidence'] = 0.7 match_data['confidence'] = 7
# Assimilation match
if anon_player['assimilation'] == prev_player['assimilation']:
match_data['confidence'] += 3
confidence_details.append(f"Assimilation match (+0.3)")
match_data['match_reasons'].append(f"same assimilation ({anon_player['assimilation']})")
# League match
if (anon_player['league'] == prev_player['league'] and
anon_player['league'] != 'N/A' and anon_player['league'] != ''):
match_data['confidence'] += 7
confidence_details.append(f"League match (+0.5)")
match_data['match_reasons'].append(f"same league ({anon_player['league']})")
# Tournament count comparison
try:
anon_tournaments = int(anon_player['tournaments_played'])
prev_tournaments = int(prev_player['tournaments_played'])
tournaments_diff = abs(anon_tournaments - prev_tournaments)
if tournaments_diff == 0:
match_data['confidence'] += 4
confidence_details.append(f"Tournaments unchanged (+0.2)")
match_data['match_reasons'].append(f"same tournaments played ({anon_tournaments})")
else:
# Calculate percentage difference
max_tournaments = max(anon_tournaments, prev_tournaments)
if max_tournaments > 0:
percentage_diff = (tournaments_diff / max_tournaments) * 100
if percentage_diff <= 10:
match_data['confidence'] += 3
confidence_details.append(f"Tournaments within 10% range (+0.15, diff: {percentage_diff:.1f}%)")
match_data['match_reasons'].append(f"tournaments played: prev={prev_tournaments}, current={anon_tournaments}")
elif percentage_diff <= 20:
match_data['confidence'] += 2
confidence_details.append(f"Tournaments within 20% range (+0.1, diff: {percentage_diff:.1f}%)")
match_data['match_reasons'].append(f"tournaments played: prev={prev_tournaments}, current={anon_tournaments}")
else:
confidence_details.append(f"Tournaments too different (diff: {percentage_diff:.1f}%)")
match_data['match_reasons'].append(f"tournaments played: prev={prev_tournaments}, current={anon_tournaments}")
else:
# Handle edge case where both values are 0
match_data['confidence'] += 4
confidence_details.append(f"Both have 0 tournaments (+0.2)")
match_data['match_reasons'].append(f"both have 0 tournaments played")
except ValueError:
confidence_details.append("Could not compare tournaments played")
# Points comparison
try:
anon_points = float(anon_player['points'])
prev_points = float(prev_player['points'])
points_diff = abs(anon_points - prev_points)
match_data['match_reasons'].append(f"points: prev={prev_points}, current={anon_points}, diff={points_diff}")
if points_diff == 0:
match_data['confidence'] += 4
confidence_details.append(f"Points unchanged (+0.3)")
else:
# Calculate percentage difference
max_points = max(anon_points, prev_points)
if max_points > 0:
percentage_diff = (points_diff / max_points) * 100
if percentage_diff <= 10:
match_data['confidence'] += 3
confidence_details.append(f"Points within 10% range (+0.25, diff: {percentage_diff:.1f}%)")
elif percentage_diff <= 20:
match_data['confidence'] += 2
confidence_details.append(f"Points within 20% range (+0.15, diff: {percentage_diff:.1f}%)")
elif percentage_diff <= 30:
match_data['confidence'] += 1
confidence_details.append(f"Points within 30% range (+0.1, diff: {percentage_diff:.1f}%)")
else:
confidence_details.append(f"Points too different (diff: {percentage_diff:.1f}%)")
except ValueError:
confidence_details.append("Could not compare points")
elif rank_diff <= 3: elif rank_diff <= 3:
match_data['rank_match_type'] = 'close' match_data['rank_match_type'] = 'close'
match_data['match_reasons'].append(f"close previous rank match ({prev_rank_value} vs {prev_rank_from_progression})") match_data['match_reasons'].append(f"close previous rank match ({prev_rank_value} vs {prev_rank_from_progression})")
match_data['confidence'] = 0.4 match_data['confidence'] = 4
elif rank_diff <= 10: elif rank_diff <= 10:
match_data['rank_match_type'] = 'approximate' match_data['rank_match_type'] = 'approximate'
match_data['match_reasons'].append(f"approximate previous rank match ({prev_rank_value} vs {prev_rank_from_progression})") match_data['match_reasons'].append(f"approximate previous rank match ({prev_rank_value} vs {prev_rank_from_progression})")
match_data['confidence'] = 0.2 match_data['confidence'] = 2
except ValueError: except ValueError:
pass pass
# 2. Points similarity (new check) # Name match check
try: if (anon_player['name'] != 'N/A' and anon_player['name'] != '' and
if anon_player['points'] != 'N/A' and prev_player['points'] != 'N/A': anon_player['first_name'] != 'N/A' and anon_player['first_name'] != ''):
anon_points = float(anon_player['points']) if (anon_player['name'].lower() == prev_player['name'].lower() and
prev_points = float(prev_player['points']) anon_player['first_name'].lower() == prev_player['first_name'].lower()):
points_diff = abs(anon_points - prev_points) match_data['confidence'] += 25
points_similarity = max(0, 1 - (points_diff / max(anon_points, prev_points))) confidence_details.append(f"Exact name match (+0.4)")
match_data['match_reasons'].append("exact name match")
if points_similarity > 0.9:
match_data['points_similarity'] = points_similarity # Birth year match
match_data['match_reasons'].append(f"similar points ({prev_points} vs {anon_points})") if (anon_player['birth_year'] != 'N/A' and anon_player['birth_year'] != '' and
match_data['confidence'] += 0.2 prev_player['birth_year'] != 'N/A' and prev_player['birth_year'] != '' and
except ValueError: anon_player['birth_year'] == prev_player['birth_year']):
pass match_data['confidence'] += 1
confidence_details.append(f"Birth year match (+0.2)")
# 3. League match match_data['match_reasons'].append(f"same birth year ({anon_player['birth_year']})")
if anon_player['league'] != 'N/A' and prev_player['league'] != 'N/A':
if anon_player['league'] == prev_player['league']: # Only consider matches with reasonable confidence
match_data['has_league_match'] = True if match_data['confidence'] >= 10:
match_data['match_reasons'].append("league match") # Print confidence calculation details
match_data['confidence'] += 0.25 self.stdout.write("\n Confidence calculation:")
for detail in confidence_details:
# 4. Assimilation match self.stdout.write(f" {detail}")
if anon_player['assimilation'] != 'N/A' and prev_player['assimilation'] != 'N/A': self.stdout.write(f" Total confidence: {match_data['confidence']:.2f}")
if anon_player['assimilation'] == prev_player['assimilation']:
match_data['has_assimilation_match'] = True
match_data['match_reasons'].append("assimilation match")
match_data['confidence'] += 0.1
# Only consider matches with minimum confidence
if match_data['confidence'] >= 0.1:
match_data['match_reasons'] = ", ".join(match_data['match_reasons']) match_data['match_reasons'] = ", ".join(match_data['match_reasons'])
potential_matches.append(match_data) potential_matches.append(match_data)
self.stdout.write(" → Considered as potential match")
# else:
# self.stdout.write(" → Rejected (confidence too low)")
# self.stdout.write("-"*40)
# Sort matches by confidence
potential_matches.sort(key=lambda x: x['confidence'], reverse=True)
# Summary of best matches
if potential_matches:
self.stdout.write("\nTop matches found:")
for i, match in enumerate(potential_matches[:3]): # Show top 3
self.stdout.write(f"\n{i+1}. {match['player']['name']} {match['player']['first_name']}")
self.stdout.write(f" Confidence: {match['confidence']:.2f}")
self.stdout.write(f" Reasons: {match['match_reasons']}")
else:
self.stdout.write("\nNo matches found with sufficient confidence.")
# Sort matches with updated criteria
def match_sort_key(match):
rank_score = {
'exact': 1000,
'close': 100,
'approximate': 10,
None: 1
}.get(match['rank_match_type'], 0)
points_score = int(match.get('points_similarity', 0) * 100)
league_value = 2 if match['has_league_match'] else 1
assimilation_value = 2 if match['has_assimilation_match'] else 1
return (rank_score, points_score, league_value, assimilation_value, match['confidence'])
potential_matches.sort(key=match_sort_key, reverse=True)
return potential_matches return potential_matches
def save_results(self, results, output_path): def save_results(self, results, output_path):
@ -679,7 +844,7 @@ class Command(BaseCommand):
Args: Args:
file_path: Path to the current month's rankings file file_path: Path to the current month's rankings file
anonymous_players: List of anonymous players anonymous_players: List of anonymous players (filtered based on command options)
matches: List of match info dictionaries matches: List of match info dictionaries
confidence_threshold: Minimum confidence to apply auto-matching confidence_threshold: Minimum confidence to apply auto-matching
options: Command options options: Command options
@ -698,39 +863,42 @@ class Command(BaseCommand):
with open(file_path, 'r', encoding='utf-8') as f: with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines() lines = f.readlines()
# Create a map of anonymous players by rank for faster lookup # Create a set of players that should be updated
anon_by_rank = {} # Only include players that were in our filtered anonymous_players list AND have high confidence matches
for player in anonymous_players: players_to_update = set()
if player['rank'] != 'N/A': update_info = {}
anon_by_rank[player['rank']] = player
# Track which players will be updated (use a dictionary to ensure only one update per anonymous player)
players_to_update = {}
for match_info in matches: for match_info in matches:
anon_player = match_info['anonymous_player'] anon_player = match_info['anonymous_player']
best_match = match_info['best_match'] best_match = match_info['best_match']
rank = anon_player['rank']
if best_match['confidence'] >= confidence_threshold and rank not in players_to_update: # Only update if this player was in our filtered list AND meets confidence threshold
# This match has high enough confidence to auto-apply if anon_player in anonymous_players and best_match['confidence'] >= confidence_threshold:
# Only add if we haven't already found a match for this rank # Create a unique identifier for this player
players_to_update[rank] = { player_id = f"{anon_player['rank']}_{anon_player['points']}_{anon_player['assimilation']}_{anon_player['tournaments_played']}_{anon_player['league']}"
# Add additional uniqueness based on name status
if (anon_player['name'] != 'N/A' and anon_player['name'] != '' and
anon_player['first_name'] != 'N/A' and anon_player['first_name'] != ''):
player_id += f"_{anon_player['name']}_{anon_player['first_name']}"
players_to_update.add(player_id)
update_info[player_id] = {
'anonymous_player': anon_player, 'anonymous_player': anon_player,
'match': best_match 'match': best_match
} }
if not players_to_update: if not players_to_update:
self.stdout.write("No players met the confidence threshold for auto-matching.") self.stdout.write("No players met the confidence threshold for auto-matching.")
return 0 # Return 0 because no players were updated return 0
self.stdout.write(f"Found {len(players_to_update)} players to update.") self.stdout.write(f"Found {len(players_to_update)} players to update.")
# Process the file line by line # Process the file line by line
updated_count = 0 updated_count = 0
updated_lines = [] updated_lines = []
already_updated_ranks = set() # Track which ranks we've already updated
# First, we need to find the data start line # First, find the data start line
data_start_line = 0 data_start_line = 0
for i, line in enumerate(lines): for i, line in enumerate(lines):
if ';RANG;NOM;PRENOM;' in line: if ';RANG;NOM;PRENOM;' in line:
@ -752,84 +920,55 @@ class Command(BaseCommand):
updated_lines.append(line) updated_lines.append(line)
continue continue
# Check if this is an anonymous player line # Extract player data from the line
rank = values[1].strip() if len(values) > 1 else '' rank = values[1].strip() if len(values) > 1 else ''
name = values[2].strip() if len(values) > 2 else '' name = values[2].strip() if len(values) > 2 else ''
first_name = values[3].strip() if len(values) > 3 else '' first_name = values[3].strip() if len(values) > 3 else ''
license_num = values[5].strip() if len(values) > 5 else ''
# Skip if we've already updated this rank (prevent duplicates) points = values[6].strip() if len(values) > 6 else ''
if rank in already_updated_ranks: assimilation = values[7].strip() if len(values) > 7 else ''
updated_lines.append(line) tournaments = values[8].strip() if len(values) > 8 else ''
continue league = values[9].strip() if len(values) > 9 else ''
# CRITICAL CHECK: Only update if this is actually an anonymous player # Create player identifier for this line
# Check if player is anonymous (empty or missing name fields) line_player_id = f"{rank}_{points}_{assimilation}_{tournaments}_{league}"
is_anonymous = not name or not first_name
# Add name info if present
if rank in players_to_update and is_anonymous: if name and first_name and name != 'N/A' and first_name != 'N/A':
# This is an anonymous player line with a match to apply line_player_id += f"_{name}_{first_name}"
update_info = players_to_update[rank]
matched_player = update_info['match']['player'] # Check if this player should be updated
if line_player_id in players_to_update:
# Log the current values for debugging # This player should be updated
self.stdout.write(f"Updating anonymous player at rank {rank}. Current values: Name='{name}', First name='{first_name}'") match_info = update_info[line_player_id]
matched_player = match_info['match']['player']
# Update this line with matched player info
# Update the line with matched player information
# Basic information: name and first name # Keep the existing rank and points, but update name and license
values[2] = matched_player['name'] # Last name new_values = values.copy()
values[3] = matched_player['first_name'] # First name new_values[2] = matched_player['name'] # Name
new_values[3] = matched_player['first_name'] # First name
# Update nationality if available new_values[4] = matched_player['nationality']
if matched_player['nationality'] != 'N/A' and len(values) > 4: new_values[5] = matched_player['license']
values[4] = matched_player['nationality'] new_values[10] = matched_player['club_code']
new_values[11] = matched_player['club']
# Update license if available new_values[14] = matched_player['birth_year']
if matched_player['license'] != 'N/A' and len(values) > 5:
values[5] = matched_player['license'] new_line = ';'.join(new_values) + '\n'
updated_lines.append(new_line)
# Additional fields:
# Club code (position 10)
if matched_player['club_code'] != 'N/A' and len(values) > 10:
values[10] = matched_player['club_code']
# Club name (position 11)
if matched_player['club'] != 'N/A' and len(values) > 11:
values[11] = matched_player['club']
# Birth year (position 14)
if matched_player['birth_year'] != 'N/A' and len(values) > 14:
values[14] = matched_player['birth_year']
# Reconstruct the line
updated_line = ';'.join(values) + '\n'
updated_lines.append(updated_line)
updated_count += 1 updated_count += 1
# Mark this rank as updated to prevent duplicates self.stdout.write(f"Updated player: {matched_player['name']} {matched_player['first_name']} (Rank: {rank})")
already_updated_ranks.add(rank)
self.stdout.write(f"Updated player rank {rank}: {matched_player['name']} {matched_player['first_name']}")
else: else:
# Not an anonymous player or no match to apply - keep the line unchanged # This player should NOT be updated - keep the line exactly as is
updated_lines.append(line) updated_lines.append(line)
# If this is a non-anonymous player with a rank that was in our update list,
# log a warning that we skipped it
if rank in players_to_update and not is_anonymous:
self.stdout.write(self.style.WARNING(
f"WARNING: Skipped rank {rank} because it already contains a non-anonymous player: {name} {first_name}"
))
# Write the updated file # Write the updated file
with open(file_path, 'w', encoding='utf-8') as f: with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(updated_lines) f.writelines(updated_lines)
self.stdout.write(self.style.SUCCESS(f"\nUpdated {updated_count} players in the rankings file.")) self.stdout.write(self.style.SUCCESS(f"Successfully updated {updated_count} players in {file_path}"))
self.stdout.write(f"Original file backed up to: {backup_path}") return updated_count
return updated_count # Return the count of updated players
def iterative_match_anonymous_players(self, file_path, rankings_dir, options): def iterative_match_anonymous_players(self, file_path, rankings_dir, options):
""" """
@ -875,10 +1014,13 @@ class Command(BaseCommand):
anon_file = os.path.join(temp_dir, "anonymous_players.json") anon_file = os.path.join(temp_dir, "anonymous_players.json")
prev_players_file = os.path.join(temp_dir, "prev_month_players.json") prev_players_file = os.path.join(temp_dir, "prev_month_players.json")
matches_file = os.path.join(temp_dir, "matches.json") matches_file = os.path.join(temp_dir, "matches.json")
print(os.path.join(temp_dir))
# Extract anonymous players and filter previous month players # Extract anonymous players and filter previous month players
self.stdout.write("Creating initial working files...") self.stdout.write("Creating initial working files...")
filtered_data = self.create_filtered_working_files(current_players, prev_players, anon_file, prev_players_file) filtered_data = self.create_filtered_working_files(
current_players, prev_players, anon_file, prev_players_file, options
)
anon_count = filtered_data['anon_count'] anon_count = filtered_data['anon_count']
prev_count = filtered_data['prev_count'] prev_count = filtered_data['prev_count']
@ -907,13 +1049,25 @@ class Command(BaseCommand):
# Update current players from the main file # Update current players from the main file
current_players, _ = self.parse_rankings_file(file_path) current_players, _ = self.parse_rankings_file(file_path)
# Remove matched players from prev_players for next iteration
# Load the matches from the temp file to identify which prev players were used
if os.path.exists(matches_file):
with open(matches_file, 'r', encoding='utf-8') as f:
matches = json.load(f)
# Create a set of licenses that were matched
matched_licenses = set()
for match in matches:
matched_player = match['best_match']['player']
if matched_player['license'] != 'N/A' and matched_player['license']:
matched_licenses.add(matched_player['license'])
# Remove matched players from prev_players
prev_players = [p for p in prev_players if p['license'] not in matched_licenses]
# Update temp files for next iteration # Update temp files for next iteration
filtered_data = self.create_filtered_working_files(current_players, prev_players, anon_file, prev_players_file) filtered_data = self.create_filtered_working_files(current_players, prev_players, anon_file, prev_players_file, options)
anon_count = filtered_data['anon_count']
prev_count = filtered_data['prev_count']
self.stdout.write(f"Updated working files: {anon_count} anonymous players and {prev_count} eligible previous month players")
else:
self.stdout.write(self.style.SUCCESS(f"Iteration {iteration} complete: No new matches found")) self.stdout.write(self.style.SUCCESS(f"Iteration {iteration} complete: No new matches found"))
changes_made = False changes_made = False
@ -921,7 +1075,7 @@ class Command(BaseCommand):
iteration += 1 iteration += 1
# Prevent infinite loops (optional safety check) # Prevent infinite loops (optional safety check)
if iteration > 10: # Cap at 10 iterations maximum if iteration > 1: # Cap at 10 iterations maximum
self.stdout.write(self.style.WARNING("Maximum iterations reached (10). Stopping process.")) self.stdout.write(self.style.WARNING("Maximum iterations reached (10). Stopping process."))
break break
@ -940,7 +1094,7 @@ class Command(BaseCommand):
improvement = ((initial_anonymous_count - final_anonymous_count) / initial_anonymous_count) * 100 improvement = ((initial_anonymous_count - final_anonymous_count) / initial_anonymous_count) * 100
self.stdout.write(f"Data completeness improved by {improvement:.1f}%") self.stdout.write(f"Data completeness improved by {improvement:.1f}%")
def create_filtered_working_files(self, current_players, prev_players, anon_file, prev_players_file): def create_filtered_working_files(self, current_players, prev_players, anon_file, prev_players_file, options):
""" """
Create filtered working files: Create filtered working files:
1. anonymous_players.json - Contains only anonymous players from current month 1. anonymous_players.json - Contains only anonymous players from current month
@ -950,7 +1104,23 @@ class Command(BaseCommand):
""" """
# Extract anonymous players from current month # Extract anonymous players from current month
anonymous_players = [p for p in current_players if self.is_anonymous_player(p)] all_anonymous_players = [p for p in current_players if self.is_anonymous_player(p)]
# Filter based on named/unnamed options
if options['named_only']:
anonymous_players = [p for p in all_anonymous_players if (
p['name'] != 'N/A' and p['name'] != '' and
p['first_name'] != 'N/A' and p['first_name'] != ''
)]
self.stdout.write(self.style.SUCCESS(f"Filtering to only process named anonymous players ({len(anonymous_players)}/{len(all_anonymous_players)})"))
elif options['unnamed_only']:
anonymous_players = [p for p in all_anonymous_players if (
p['name'] == 'N/A' or p['name'] == '' or
p['first_name'] == 'N/A' or p['first_name'] == ''
)]
self.stdout.write(self.style.SUCCESS(f"Filtering to only process unnamed anonymous players ({len(anonymous_players)}/{len(all_anonymous_players)})"))
else:
anonymous_players = all_anonymous_players
# Create lookup for current non-anonymous players # Create lookup for current non-anonymous players
current_players_lookup = {} current_players_lookup = {}
@ -1022,8 +1192,15 @@ class Command(BaseCommand):
for anon_player in anonymous_players: for anon_player in anonymous_players:
potential_matches = self.find_potential_matches(anon_player, prev_players, current_players_indexes, options) potential_matches = self.find_potential_matches(anon_player, prev_players, current_players_indexes, options)
if potential_matches: if potential_matches:
best_match = potential_matches[0] # Highest confidence match if len(potential_matches) == 1:
best_match = potential_matches[0] # Highest confidence match
elif len(potential_matches) > 1 and potential_matches[0]['confidence'] - potential_matches[1]['confidence'] > 2:
# print(potential_matches[0]['confidence'], potential_matches[1]['match_reasons'])
best_match = potential_matches[0] # Highest confidence match
else:
# for match in potential_matches:
# print(match['player']['name'], match['confidence'], match['match_reasons'])
continue
# Record the match info # Record the match info
match_info = { match_info = {
'anonymous_player': anon_player, 'anonymous_player': anon_player,

@ -0,0 +1,222 @@
from django.core.management.base import BaseCommand
from datetime import datetime, timedelta
import logging
class Command(BaseCommand):
help = 'Test FFT all tournaments scraping with various filters'
def add_arguments(self, parser):
parser.add_argument(
'--sorting',
type=str,
default='dateDebut+asc',
choices=['dateDebut+asc', 'dateDebut+desc', '_DISTANCE_'],
help='Sorting option (default: dateDebut+asc)'
)
parser.add_argument(
'--page',
type=int,
default=0,
help='Page number to scrape (default: 0)'
)
parser.add_argument(
'--city',
type=str,
default='',
help='City to search around'
)
parser.add_argument(
'--distance',
type=float,
default=15.0,
help='Distance in km (default: 15)'
)
parser.add_argument(
'--categories',
nargs='*',
default=[],
help='Tournament categories to filter by'
)
parser.add_argument(
'--levels',
nargs='*',
default=[],
help='Tournament levels to filter by'
)
parser.add_argument(
'--ages',
nargs='*',
default=[],
help='Age categories to filter by'
)
parser.add_argument(
'--types',
nargs='*',
default=[],
help='Tournament types to filter by'
)
parser.add_argument(
'--national-cup',
action='store_true',
help='Filter for national cup tournaments only'
)
parser.add_argument(
'--lat',
type=float,
help='Latitude for location-based search'
)
parser.add_argument(
'--lng',
type=float,
help='Longitude for location-based search'
)
parser.add_argument(
'--days-ahead',
type=int,
default=90,
help='How many days ahead to search (default: 90)'
)
parser.add_argument(
'--start-date',
type=str,
help='Start date in DD/MM/YY format (overrides --days-ahead)'
)
parser.add_argument(
'--end-date',
type=str,
help='End date in DD/MM/YY format (overrides --days-ahead)'
)
parser.add_argument(
'--verbose',
action='store_true',
help='Enable verbose logging'
)
def handle(self, *args, **options):
if options['verbose']:
logging.basicConfig(level=logging.INFO)
# Extract options
sorting_option = options['sorting']
page = options['page']
city = options['city']
distance = options['distance']
categories = options['categories']
levels = options['levels']
ages = options['ages']
tournament_types = options['types']
national_cup = options['national_cup']
lat = options['lat']
lng = options['lng']
verbose = options['verbose']
# Calculate date range
if options['start_date'] and options['end_date']:
start_date_str = options['start_date']
end_date_str = options['end_date']
else:
start_date = datetime.now()
end_date = start_date + timedelta(days=options['days_ahead'])
start_date_str = start_date.strftime('%d/%m/%y')
end_date_str = end_date.strftime('%d/%m/%y')
self.stdout.write(self.style.SUCCESS("=== FFT All Tournaments Scraper ==="))
self.stdout.write(f"Sorting: {sorting_option}")
self.stdout.write(f"Page: {page}")
self.stdout.write(f"Date range: {start_date_str} to {end_date_str}")
self.stdout.write(f"City: {city if city else 'Not specified'}")
self.stdout.write(f"Distance: {distance} km")
self.stdout.write(f"Categories: {categories if categories else 'All'}")
self.stdout.write(f"Levels: {levels if levels else 'All'}")
self.stdout.write(f"Ages: {ages if ages else 'All'}")
self.stdout.write(f"Types: {tournament_types if tournament_types else 'All'}")
self.stdout.write(f"National Cup: {'Yes' if national_cup else 'No'}")
if lat and lng:
self.stdout.write(f"Location: {lat}, {lng}")
self.stdout.write(f"Method: Playwright (Chrome-free)")
self.stdout.write("")
try:
from api.utils import scrape_fft_all_tournaments
self.stdout.write("🚀 Testing general tournament scraping...")
result = scrape_fft_all_tournaments(
sorting_option=sorting_option,
page=page,
start_date=start_date_str,
end_date=end_date_str,
city=city,
distance=distance,
categories=categories,
levels=levels,
lat=lat,
lng=lng,
ages=ages,
tournament_types=tournament_types,
national_cup=national_cup
)
# Debug: Show what we got (only in verbose mode)
if verbose:
self.stdout.write(f"🔍 Raw result: {result}")
if result:
tournaments = result.get('tournaments', [])
self.stdout.write(self.style.SUCCESS(f"✅ SUCCESS: {len(tournaments)} tournaments found"))
if tournaments:
self.stdout.write("\n📝 Sample tournaments:")
# Show first 3 tournaments
for i, tournament in enumerate(tournaments[:3]):
self.stdout.write(f"\n Tournament {i+1}:")
self.stdout.write(f" ID: {tournament.get('id')}")
self.stdout.write(f" Name: {tournament.get('libelle')}")
self.stdout.write(f" Date: {tournament.get('dateDebut', {}).get('date', 'N/A')}")
self.stdout.write(f" Club: {tournament.get('nomClub', 'N/A')}")
self.stdout.write(f" City: {tournament.get('villeEngagement', 'N/A')}")
self.stdout.write(f" Category: {tournament.get('categorieTournoi', 'N/A')}")
self.stdout.write(f" Type: {tournament.get('type', 'N/A')}")
if tournament.get('jugeArbitre'):
self.stdout.write(f" Judge: {tournament.get('jugeArbitre', {}).get('nom', 'N/A')}")
self.stdout.write(f"\n📊 Summary:")
self.stdout.write(f" Total tournaments: {len(tournaments)}")
self.stdout.write(f" Current page: {page}")
self.stdout.write(f" Total results available: {result.get('total_results', 'Unknown')}")
# Analysis of results
if tournaments:
cities = set()
clubs = set()
categories = set()
types = set()
for tournament in tournaments:
if tournament.get('villeEngagement'):
cities.add(tournament['villeEngagement'])
if tournament.get('nomClub'):
clubs.add(tournament['nomClub'])
if tournament.get('categorieTournoi'):
categories.add(tournament['categorieTournoi'])
if tournament.get('type'):
types.add(tournament['type'])
self.stdout.write(f"\n🔍 Analysis:")
self.stdout.write(f" Unique cities: {len(cities)}")
self.stdout.write(f" Unique clubs: {len(clubs)}")
self.stdout.write(f" Unique categories: {len(categories)}")
self.stdout.write(f" Unique types: {len(types)}")
if verbose:
self.stdout.write(f"\n Cities: {sorted(list(cities))[:10]}") # Show first 10
self.stdout.write(f" Categories: {sorted(list(categories))}")
self.stdout.write(f" Types: {sorted(list(types))}")
else:
self.stdout.write(self.style.ERROR("❌ FAILED: No tournaments found"))
except Exception as e:
self.stdout.write(self.style.ERROR(f"❌ ERROR: {e}"))
import traceback
if verbose:
self.stdout.write(traceback.format_exc())

@ -0,0 +1,103 @@
from django.core.management.base import BaseCommand
from datetime import datetime, timedelta
import logging
class Command(BaseCommand):
help = 'Test FFT tournament scraping with Playwright'
def add_arguments(self, parser):
parser.add_argument(
'--club-code',
type=str,
default='62130180',
help='Club code for testing (default: 62130180)'
)
parser.add_argument(
'--club-name',
type=str,
default='TENNIS SPORTING CLUB DE CASSIS',
help='Club name for testing'
)
parser.add_argument(
'--all-pages',
action='store_true',
help='Test all pages scraping'
)
parser.add_argument(
'--verbose',
action='store_true',
help='Enable verbose logging'
)
def handle(self, *args, **options):
if options['verbose']:
logging.basicConfig(level=logging.INFO)
club_code = options['club_code']
club_name = options['club_name']
all_pages = options['all_pages']
verbose = options['verbose']
# Calculate date range
start_date = datetime.now()
end_date = start_date + timedelta(days=90)
start_date_str = start_date.strftime('%d/%m/%y')
end_date_str = end_date.strftime('%d/%m/%y')
self.stdout.write(self.style.SUCCESS("=== FFT Tournament Scraper ==="))
self.stdout.write(f"Club: {club_name} ({club_code})")
self.stdout.write(f"Date range: {start_date_str} to {end_date_str}")
self.stdout.write(f"Method: Playwright (Chrome-free)")
self.stdout.write("")
try:
if all_pages:
from api.utils import scrape_fft_club_tournaments_all_pages
self.stdout.write("🚀 Testing complete tournament scraping...")
result = scrape_fft_club_tournaments_all_pages(
club_code=club_code,
club_name=club_name,
start_date=start_date_str,
end_date=end_date_str
)
else:
from api.utils import scrape_fft_club_tournaments
self.stdout.write("🚀 Testing single page scraping...")
result = scrape_fft_club_tournaments(
club_code=club_code,
club_name=club_name,
start_date=start_date_str,
end_date=end_date_str,
page=0
)
# Debug: Show what we got (only in verbose mode)
if verbose:
self.stdout.write(f"🔍 Raw result: {result}")
if result:
tournaments = result.get('tournaments', [])
self.stdout.write(self.style.SUCCESS(f"✅ SUCCESS: {len(tournaments)} tournaments found"))
if tournaments:
self.stdout.write("\n📝 Sample tournament:")
sample = tournaments[0]
self.stdout.write(f" ID: {sample.get('id')}")
self.stdout.write(f" Name: {sample.get('libelle')}")
self.stdout.write(f" Date: {sample.get('dateDebut', {}).get('date', 'N/A')}")
self.stdout.write(f" Judge: {sample.get('jugeArbitre', {}).get('nom', 'N/A')}")
self.stdout.write(f"\n📊 Summary:")
self.stdout.write(f" Total tournaments: {len(tournaments)}")
self.stdout.write(f" Pages scraped: {result.get('pages_scraped', 1)}")
else:
self.stdout.write(self.style.ERROR("❌ FAILED: No tournaments found"))
except Exception as e:
self.stdout.write(self.style.ERROR(f"❌ ERROR: {e}"))
import traceback
if verbose:
self.stdout.write(traceback.format_exc())

@ -1,6 +1,7 @@
from django.db import models from django.db import models
from . import BaseModel, CustomUser from django.utils import timezone
from . import BaseModel, CustomUser
class Purchase(BaseModel): class Purchase(BaseModel):
id = models.BigIntegerField(primary_key=True, unique=True, editable=True) id = models.BigIntegerField(primary_key=True, unique=True, editable=True)
@ -16,3 +17,6 @@ class Purchase(BaseModel):
def __str__(self): def __str__(self):
return f"{self.id} > {self.product_id} - {self.purchase_date} - {self.user.username}" return f"{self.id} > {self.product_id} - {self.purchase_date} - {self.user.username}"
def is_active(self):
return self.expiration_date > timezone.now()

@ -1014,30 +1014,35 @@ class Tournament(BaseModel):
return self.has_started(hour_delta=hour_delta) return self.has_started(hour_delta=hour_delta)
def supposedly_in_progress(self): def supposedly_in_progress(self):
start = self.start_date - timedelta(hours=1)
end = self.start_date + timedelta(days=self.day_duration + 1)
return start <= timezone.now() <= end
# end = self.start_date + timedelta(days=self.day_duration + 1) # end = self.start_date + timedelta(days=self.day_duration + 1)
# return self.start_date.replace(hour=0, minute=0) <= timezone.now() <= end # return self.start_date.replace(hour=0, minute=0) <= timezone.now() <= end
timezoned_datetime = self.local_start_date() # timezoned_datetime = self.local_start_date()
end = timezoned_datetime + timedelta(days=self.day_duration + 1) # end = timezoned_datetime + timedelta(days=self.day_duration + 1)
now_utc = timezone.now() # now_utc = timezone.now()
now = now_utc.astimezone(self.timezone()) # now = now_utc.astimezone(self.timezone())
start = timezoned_datetime.replace(hour=0, minute=0) # start = timezoned_datetime.replace(hour=0, minute=0)
# print(f"timezoned_datetime: {timezoned_datetime}") # # print(f"timezoned_datetime: {timezoned_datetime}")
# print(f"tournament end date: {end}") # # print(f"tournament end date: {end}")
# print(f"current time: {now}") # # print(f"current time: {now}")
# print(f"tournament start: {start}") # # print(f"tournament start: {start}")
# print(f"start <= now <= end: {start <= now <= end}") # # print(f"start <= now <= end: {start <= now <= end}")
return start <= now <= end # return start <= now <= end
def starts_in_the_future(self): def starts_in_the_future(self):
# tomorrow = datetime.now().date() + timedelta(days=1) # tomorrow = datetime.now().date() + timedelta(days=1)
timezoned_datetime = self.local_start_date() # timezoned_datetime = self.local_start_date()
start = timezoned_datetime.replace(hour=0, minute=0) # start = timezoned_datetime.replace(hour=0, minute=0)
now_utc = timezone.now() # now_utc = timezone.now()
now = now_utc.astimezone(self.timezone()) # now = now_utc.astimezone(self.timezone())
return start >= now start = self.start_date - timedelta(hours=1)
return start >= timezone.now()
def has_ended(self): def has_ended(self):
return self.end_date is not None return self.end_date is not None

@ -302,6 +302,7 @@ tr {
.logo { .logo {
height: 80px; height: 80px;
margin-bottom: 15px;
} }
.padding-bottom-small { .padding-bottom-small {

@ -15,25 +15,33 @@
<div class="tournament-dashboard"> <div class="tournament-dashboard">
<!-- Quick Actions - Déplacé en haut --> <!-- Quick Actions - Déplacé en haut -->
<div class="quick-actions" style="background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 12px; padding: 25px; margin-bottom: 20px;"> <div class="quick-actions" style="background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 12px; padding: 25px; margin-bottom: 20px;">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;"> <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 15px;">
<a href="{% url 'admin:tournaments_customuser_changelist' %}"
style="display: block; padding: 12px 15px; background: #17a2b8; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
Users
</a>
<a href="{% url 'admin:tournaments_purchase_changelist' %}"
style="display: block; padding: 12px 15px; background: #17a2b8; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
Purchases
</a>
<a href="{% url 'admin:tournaments_tournament_changelist' %}" <a href="{% url 'admin:tournaments_tournament_changelist' %}"
style="display: block; padding: 12px 15px; background: #007bff; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;"> style="display: block; padding: 12px 15px; background: #ffc107; color: #212529; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
Tournaments Tournaments
</a> </a>
<a href="{% url 'admin:tournaments_teamregistration_changelist' %}" <a href="{% url 'admin:tournaments_teamregistration_changelist' %}"
style="display: block; padding: 12px 15px; background: #28a745; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;"> style="display: block; padding: 12px 15px; background: #ffc107; color: #212529; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
Teams Teams
</a> </a>
<a href="{% url 'admin:tournaments_playerregistration_changelist' %}" <a href="{% url 'admin:tournaments_playerregistration_changelist' %}"
style="display: block; padding: 12px 15px; background: #6c757d; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;"> style="display: block; padding: 12px 15px; background: #ffc107; color: #212529; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
Players Players
</a> </a>
<a href="{% url 'admin:tournaments_match_changelist' %}" <a href="{% url 'admin:tournaments_match_changelist' %}"
style="display: block; padding: 12px 15px; background: #fd7e14; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;"> style="display: block; padding: 12px 15px; background: #ffc107; color: #212529; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
Matches Matches
</a> </a>
<a href="{% url 'admin:tournaments_event_changelist' %}" <a href="{% url 'admin:tournaments_event_changelist' %}"
style="display: block; padding: 12px 15px; background: #17a2b8; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;"> style="display: block; padding: 12px 15px; background: #ffc107; color: #212529; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
Events Events
</a> </a>
<a href="{% url 'admin:tournaments_club_changelist' %}" <a href="{% url 'admin:tournaments_club_changelist' %}"
@ -47,8 +55,8 @@
<div class="dashboard-stats" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin: 20px 0;"> <div class="dashboard-stats" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin: 20px 0;">
<!-- Running Tournaments Card --> <!-- Running Tournaments Card -->
<div class="stat-card" style="background: linear-gradient(135deg, #28a745, #20c997); color: white; border-radius: 12px; padding: 25px; box-shadow: 0 4px 15px rgba(0,0,0,0.1);"> <div class="stat-card" style="background: #17a2b8; color: white; border-radius: 12px; padding: 25px; box-shadow: 0 4px 15px rgba(0,0,0,0.1);">
<h3 style="margin: 0 0 20px 0; display: flex; align-items: center; gap: 10px;"> <h3 style="margin: 0 0 20px 0; display: flex; align-items: center; gap: 10px; color: white;">
🎾 Starting Tournaments 🎾 Starting Tournaments
</h3> </h3>
<div class="tournament-stats"> <div class="tournament-stats">
@ -86,8 +94,8 @@
</div> </div>
<!-- Ended Tournaments Card --> <!-- Ended Tournaments Card -->
<div class="stat-card" style="background: linear-gradient(135deg, #dc3545, #e74c3c); color: white; border-radius: 12px; padding: 25px; box-shadow: 0 4px 15px rgba(0,0,0,0.1);"> <div class="stat-card" style="background: #17a2b8; color: white; border-radius: 12px; padding: 25px; box-shadow: 0 4px 15px rgba(0,0,0,0.1);">
<h3 style="margin: 0 0 20px 0; display: flex; align-items: center; gap: 10px;"> <h3 style="margin: 0 0 20px 0; display: flex; align-items: center; gap: 10px; color: white;">
🏁 Ended Tournaments 🏁 Ended Tournaments
</h3> </h3>
<div class="tournament-stats"> <div class="tournament-stats">
@ -111,8 +119,8 @@
</div> </div>
<!-- Participants Card --> <!-- Participants Card -->
<div class="stat-card" style="background: linear-gradient(135deg, #007bff, #0056b3); color: white; border-radius: 12px; padding: 25px; box-shadow: 0 4px 15px rgba(0,0,0,0.1);"> <div class="stat-card" style="background: #17a2b8; color: white; border-radius: 12px; padding: 25px; box-shadow: 0 4px 15px rgba(0,0,0,0.1);">
<h3 style="margin: 0 0 20px 0; display: flex; align-items: center; gap: 10px;"> <h3 style="margin: 0 0 20px 0; display: flex; align-items: center; gap: 10px; color: white;">
👥 Participants 👥 Participants
</h3> </h3>
<div class="participant-stats"> <div class="participant-stats">
@ -132,8 +140,8 @@
</div> </div>
<!-- Matches Card --> <!-- Matches Card -->
<div class="stat-card" style="background: linear-gradient(135deg, #fd7e14, #e55e2b); color: white; border-radius: 12px; padding: 25px; box-shadow: 0 4px 15px rgba(0,0,0,0.1);"> <div class="stat-card" style="background: #17a2b8; color: white; border-radius: 12px; padding: 25px; box-shadow: 0 4px 15px rgba(0,0,0,0.1);">
<h3 style="margin: 0 0 20px 0; display: flex; align-items: center; gap: 10px;"> <h3 style="margin: 0 0 20px 0; display: flex; align-items: center; gap: 10px; color: white;">
🏓 Matches 🏓 Matches
</h3> </h3>
<div class="match-stats"> <div class="match-stats">

@ -29,7 +29,6 @@
<div><a href="{% url 'broadcasted-group-stages' tournament.id %}">Poules</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> <div><a href="{% url 'broadcasted-summons' tournament.id %}">Convocations</a></div>
<div><a href="{% url 'broadcasted-rankings' tournament.id %}">Classement</a></div> <div><a href="{% url 'broadcasted-rankings' tournament.id %}">Classement</a></div>
<div><a href="{% url 'broadcasted-prog' tournament.id %}">(beta) Programmation</a></div>
<div><a href="{% url 'broadcasted-planning' tournament.id %}">(beta) Planning</a></div> <div><a href="{% url 'broadcasted-planning' tournament.id %}">(beta) Planning</a></div>
<div><a href="{% url 'broadcasted-bracket' tournament.id %}">(beta) Tableau</a></div> <div><a href="{% url 'broadcasted-bracket' tournament.id %}">(beta) Tableau</a></div>
</div> </div>

@ -33,7 +33,6 @@
<span><a href="{% url 'automatic-broadcast' tournament.id %}">Automatic</a></span> | <span><a href="{% url 'automatic-broadcast' tournament.id %}">Automatic</a></span> |
<span><a href="{% url 'broadcasted-bracket' tournament.id %}">(beta) Tableau</a></span> | <span><a href="{% url 'broadcasted-bracket' tournament.id %}">(beta) Tableau</a></span> |
<span><a href="{% url 'broadcasted-planning' tournament.id %}">(beta) Planning</a></span> | <span><a href="{% url 'broadcasted-planning' tournament.id %}">(beta) Planning</a></span> |
<span><a href="{% url 'broadcasted-prog' tournament.id %}">(beta) Programmation</a></span> |
<span><a href="{% url 'broadcasted-matches' tournament.id %}">Matchs</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-group-stages' tournament.id %}">Poules</a></span> |
<span><a href="{% url 'broadcasted-summons' tournament.id %}">Convocations</a></span> | <span><a href="{% url 'broadcasted-summons' tournament.id %}">Convocations</a></span> |

@ -115,15 +115,16 @@
<div class="bubble"> <div class="bubble">
<h4 class="orange">Avant le tournoi</h4> <h4 class="orange">Avant le tournoi</h4>
<ul> <ul>
<li>Inscriptions <span class="bold">en ligne</span></li>
<li><span class="bold">Paiement en ligne</span> via Stripe</li>
<li>Gestion <span class="bold">automatique des listes d'attente</span></li>
<li><span class="bold">Recherche et importation</span> facile des joueurs licenciés</li> <li><span class="bold">Recherche et importation</span> facile des joueurs licenciés</li>
<li>Gestion de la <span class="bold">liste d'attente</span></li>
<li>Interface avec <span class="bold">Beach-Padel</span></li> <li>Interface avec <span class="bold">Beach-Padel</span></li>
<li>Simulation des <span class="bold">structures de tournoi</span></li> <li>Simulation des <span class="bold">Structures de tournoi</span></li>
<li><span class="bold">Planification instantanée</span></li> <li><span class="bold">Planification instantanée</span></li>
<li><span class="bold">Convocations groupées</span> des joueurs</li> <li><span class="bold">Convocations groupées</span> des joueurs</li>
<li><span class="bold">Création de vos paires</span> rapide</li> <li><span class="bold">Impression papier</span> des tableaux et poules</li>
<li><span class="bold">Calcul du poids des équipes</span> automatique</li> <li><span class="bold">Calcul du poids des équipes</span> automatique</li>
<li><span class="bold">Convocation de tous les joueurs</span> en un clic</li>
<li><span class="bold">Traitement des forfaits</span> de dernière minute</li> <li><span class="bold">Traitement des forfaits</span> de dernière minute</li>
</ul> </ul>
</div> </div>

@ -21,7 +21,7 @@ from django.conf import settings
from django.contrib.admin.views.decorators import staff_member_required from django.contrib.admin.views.decorators import staff_member_required
from django.views.generic import View from django.views.generic import View
from django.db.models import Q from django.db.models import Q
from .models import Club, Tournament, CustomUser, Event, Round, Match, TeamScore, TeamRegistration, PlayerRegistration, UserOrigin from .models import Club, Tournament, CustomUser, Event, Round, Match, TeamScore, TeamRegistration, PlayerRegistration, UserOrigin, Purchase
from datetime import datetime, timedelta from datetime import datetime, timedelta
import time import time
import json import json
@ -1678,7 +1678,6 @@ def get_user_tournaments(user, tournaments):
def stripe_onboarding_complete(request): def stripe_onboarding_complete(request):
return render(request, 'stripe/onboarding_complete.html') return render(request, 'stripe/onboarding_complete.html')
def stripe_refresh_account_link(request): def stripe_refresh_account_link(request):
return render(request, 'stripe/refresh_account_link.html') return render(request, 'stripe/refresh_account_link.html')

Loading…
Cancel
Save