You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
padelclub_backend/tournaments/admin_utils.py

2530 lines
105 KiB

from django.http import HttpResponse, JsonResponse
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib import messages
from django.shortcuts import redirect
from datetime import datetime
import requests
import json
import time
import unicodedata
import urllib.parse # Add this import
import os
from django.conf import settings
from django.template import Template, Context
from django.middleware.csrf import get_token
import concurrent.futures
from functools import partial
import csv
import io
from api.utils import scrape_fft_all_tournaments, get_umpire_data
default_sexe = "H"
default_id_homologation = "82546485"
default_session_id = "JSESSIONID=E3DE6A54D5367D48B0CFA970E09EB422; AWSALB=UlkEmLYVxfS3RNwiNeNygqdqjroNzOZF3D9k6nR+NP6YPG3r6JLIzOqtw3nV1aVKsyNMldzeFOmVy/V1OPf7LNVW/sckdD1EprkGtgqjX8N8DpihxhTGtTm+0sX1; AWSALBCORS=UlkEmLYVxfS3RNwiNeNygqdqjroNzOZF3D9k6nR+NP6YPG3r6JLIzOqtw3nV1aVKsyNMldzeFOmVy/V1OPf7LNVW/sckdD1EprkGtgqjX8N8DpihxhTGtTm+0sX1; datadome=K3v~wZc~sLs5C7D4p0OoS3jOXGpeDfai9vk~TDPw2mSFbxqpfjUcR68wvPaYXHYqXgAHOrFnrBGpoyNepJ6bXfncdSmYOUfMNPbAtvBBo67zZTxxSeogLiLu1U1_5Txo; TCID=; tc_cj_v2=%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQOKSRRSNRRNQZZZ%5D777_rn_lh%5BfyfcheZZZ%2F%20%290%2BH%2C0%200%20G%24%2FH%29%20%2FZZZKQOLJNPJONLMPZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQOLJNPJSSQMNZZZ%5D777_rn_lh%5BfyfcheZZZ%2F%20%290%2BH%2C0%200%20G%24%2FH%29%20%2FZZZKQOLJNPRRONPSZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQOLJNQLKOMSOZZZ%5D777_rn_lh%5BfyfcheZZZ%2F%20%290%2BH%2C0%200%20G%24%2FH%29%20%2FZZZKQOLJPMNSNOJKZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQOLJPMRPKRKMZZZ%5D777_rn_lh%5BfyfcheZZZ%2F%20%290%2BH%2C0%200%20G%24%2FH%29%20%2FZZZKQOLJQSONNLNQZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQOLKMOPOJKSLZZZ%5D777_rn_lh%5BfyfcheZZZ%2F%20%290%2BH%2C0%200%20G%24%2FH%29%20%2FZZZKQONMQSSNRPKQZZZ%5D; tc_cj_v2_cmp=; tc_cj_v2_med=; xtan=-; xtant=1; pa_vid=%22mckhos3iasswydjm%22; _pcid=%7B%22browserId%22%3A%22mckhos3iasswydjm%22%2C%22_t%22%3A%22ms8wm9hs%7Cmckhos5s%22%7D; _pctx=%7Bu%7DN4IgrgzgpgThIC4B2YA2qA05owMoBcBDfSREQpAeyRCwgEt8oBJAE0RXSwH18yBbCAA4A7vwCcACwgAffgGMA1pMoQArPAC%2BQA; TCPID=125629554310878226394; xtvrn=$548419$"
def calculate_age_from_birth_date(birth_date_str):
"""
Calculate age from birth date string (format: DD/MM/YYYY)
Returns age as integer or None if parsing fails
"""
if not birth_date_str:
return None
try:
# Parse French date format DD/MM/YYYY
birth_date = datetime.strptime(birth_date_str, "%d/%m/%Y")
today = datetime.now()
age = (
today.year
- birth_date.year
- ((today.month, today.day) < (birth_date.month, birth_date.day))
)
return age
except (ValueError, TypeError):
return None
def find_best_license_match(license_results, player):
"""
Find the best matching license from multiple results using ageSportif comparison
Also filters out players without valid classement data
Args:
license_results: List of license data from API
player_age_sportif: Age from ranking data
Returns:
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:
return None, {"reason": "no_results"}
# First, filter out players without valid classement data
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 valid match
if player_age_sportif is None:
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_age_diff = float("inf")
match_details = []
best_match_count = 0
for i, license_data in enumerate(valid_license_results):
birth_date_fr = license_data.get("dateNaissanceFr")
calculated_age = calculate_age_from_birth_date(birth_date_fr)
match_detail = {
"index": i,
"dateNaissanceFr": birth_date_fr,
"calculated_age": calculated_age,
"player_age_sportif": player_age_sportif,
"age_difference": None,
"license": license_data.get("licence"),
"classement": license_data.get("classement", {}),
}
if calculated_age is not None:
age_diff = abs(calculated_age - player_age_sportif)
match_detail["age_difference"] = age_diff
if age_diff < best_age_diff and best_age_diff > 1 and age_diff < 2:
best_age_diff = age_diff
best_match = license_data
best_match_count = 1
elif age_diff <= best_age_diff:
best_match_count += 1
match_details.append(match_detail)
# If no match found with valid age, use first valid result
if best_match is None:
match_info = {
"reason": "no_valid_ages_used_first_valid",
"original_count": len(license_results),
"filtered_count": len(valid_license_results),
"used_first_result": True,
"match_details": match_details,
}
return valid_license_results[0], match_info
else:
if best_match_count == 1:
match_info = {
"reason": "age_matched",
"best_age_difference": best_age_diff,
"total_candidates": len(license_results),
"valid_candidates": len(valid_license_results),
"match_details": match_details,
}
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
def test_player_details_apis(request):
"""
Test different API endpoints to find player license information
"""
# Sample idCrm values from your data
test_ids = [5417299111, 9721526122]
results = {"timestamp": datetime.now().isoformat(), "test_results": []}
for idCrm in test_ids:
player_results = {"idCrm": idCrm, "tests": []}
# Test 1: Try player detail endpoint
try:
url = f"https://tenup.fft.fr/back/public/v1/joueurs/{idCrm}"
response = requests.get(url, timeout=10)
player_results["tests"].append(
{
"method": "GET",
"url": url,
"status_code": response.status_code,
"response_preview": response.text[:500]
if response.status_code == 200
else response.text,
}
)
except Exception as e:
player_results["tests"].append(
{"method": "GET", "url": url, "error": str(e)}
)
# Test 2: Try with different endpoint structure
try:
url = f"https://tenup.fft.fr/back/public/v1/players/{idCrm}"
response = requests.get(url, timeout=10)
player_results["tests"].append(
{
"method": "GET",
"url": url,
"status_code": response.status_code,
"response_preview": response.text[:500]
if response.status_code == 200
else response.text,
}
)
except Exception as e:
player_results["tests"].append(
{"method": "GET", "url": url, "error": str(e)}
)
# Test 3: Try POST with idCrm in body
try:
url = "https://tenup.fft.fr/back/public/v1/joueurs/detail"
payload = {"idCrm": idCrm}
response = requests.post(
url,
json=payload,
headers={"Content-Type": "application/json"},
timeout=10,
)
player_results["tests"].append(
{
"method": "POST",
"url": url,
"payload": payload,
"status_code": response.status_code,
"response_preview": response.text[:500]
if response.status_code == 200
else response.text,
}
)
except Exception as e:
player_results["tests"].append(
{"method": "POST", "url": url, "payload": payload, "error": str(e)}
)
# Test 4: Try the classements endpoint with more parameters
try:
url = "https://tenup.fft.fr/back/public/v1/classements/recherche"
payload = {"pratique": "padel", "sexe": default_sexe, "idCrm": idCrm}
response = requests.post(
url,
json=payload,
headers={"Content-Type": "application/json"},
timeout=10,
)
player_results["tests"].append(
{
"method": "POST",
"url": url,
"payload": payload,
"status_code": response.status_code,
"response_preview": response.text[:500]
if response.status_code == 200
else response.text,
}
)
except Exception as e:
player_results["tests"].append(
{"method": "POST", "url": url, "payload": payload, "error": str(e)}
)
results["test_results"].append(player_results)
time.sleep(0.5) # Small delay between tests
# Create filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"fft_api_tests_{timestamp}.json"
# Return results as downloadable JSON
http_response = HttpResponse(
json.dumps(results, indent=2, ensure_ascii=False),
content_type="application/json; charset=utf-8",
)
http_response["Content-Disposition"] = f'attachment; filename="{filename}"'
return http_response
@staff_member_required
def explore_fft_api_endpoints(request):
"""
Try to discover FFT API endpoints
"""
base_url = "https://tenup.fft.fr/back/public/v1"
# Common API endpoint patterns to try
endpoints_to_test = [
"/joueurs",
"/players",
"/licences",
"/licenses",
"/classements",
"/rankings",
"/pratiques",
"/sports",
"/search",
"/recherche",
"/tournaments",
"/tournois",
]
results = {
"base_url": base_url,
"timestamp": datetime.now().isoformat(),
"endpoint_tests": [],
}
for endpoint in endpoints_to_test:
try:
url = base_url + endpoint
response = requests.get(url, timeout=10)
results["endpoint_tests"].append(
{
"endpoint": endpoint,
"url": url,
"status_code": response.status_code,
"content_type": response.headers.get("content-type", ""),
"response_preview": response.text[:300]
if len(response.text) < 1000
else response.text[:300] + "...",
}
)
except Exception as e:
results["endpoint_tests"].append(
{"endpoint": endpoint, "url": url, "error": str(e)}
)
time.sleep(0.2) # Small delay
# Create filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"fft_api_discovery_{timestamp}.json"
# Return results as downloadable JSON
http_response = HttpResponse(
json.dumps(results, indent=2, ensure_ascii=False),
content_type="application/json; charset=utf-8",
)
http_response["Content-Disposition"] = f'attachment; filename="{filename}"'
return http_response
@staff_member_required
def download_french_padel_rankings(request):
"""
Download French padel rankings from FFT API and save locally for later enrichment
"""
if request.method == "POST":
start_tranche = int(request.POST.get("start_tranche", 1001))
end_tranche = int(request.POST.get("end_tranche", 1222))
save_locally = request.POST.get("save_locally", "true") == "true"
sexe = request.POST.get("sexe", default_sexe)
try:
# API endpoint and parameters
url = "https://tenup.fft.fr/back/public/v1/classements/recherche"
headers = {
'Accept': 'application/json',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'fr-FR,fr;q=0.9',
'Content-Type': 'application/json',
'Cookie': 'QueueITAccepted-SDFrts345E-V3_tenupprod=EventId%3Dtenupprod%26RedirectType%3Dsafetynet%26IssueTime%3D1760201365%26Hash%3D9df48ec92a3fda96d0f1f66b4b17602f43a294c4c847e57248401fd8ae3e21c0; _pcid=%7B%22browserId%22%3A%22mckhos3iasswydjm%22%2C%22_t%22%3A%22mwas74z5%7Cmgmb4hv5%22%7D; tc_cj_v2=%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQPJLJKMPMQMLZZZ%5D; tc_cj_v2_cmp=; tc_cj_v2_med=; i18n_redirected=fr; SHARED_SESSION_JAVA=085692ea-c23f-4812-ae86-6cc0f3467a69; userStore=%7B%22isLogged%22%3Atrue%2C%22user%22%3A%7B%22id%22%3A10000984864%2C%22idPersonne%22%3A109001023%2C%22nom%22%3A%22SARKISSIAN%22%2C%22prenom%22%3A%22Razmig%22%2C%22civilite%22%3A%22M%22%2C%22classement%22%3A%22NC%22%2C%22pratiquePrincipale%22%3A%22PADEL%22%2C%22pratiqueSecondaire%22%3A%5B%5D%2C%22licencie%22%3Atrue%2C%22licencieMillesimePrecedent%22%3Atrue%2C%22licencieMillesimeSuivant%22%3Afalse%2C%22codeTypeLicence%22%3A%22PAD%22%2C%22groupes%22%3A%5B%22Licenci%C3%A9%20Padel%22%2C%22Adherent%22%2C%22Juge%20arbitre%22%2C%22Loisir%22%2C%22Licenci%C3%A9%22%5D%2C%22clubs%22%3A%5B%7B%22code%22%3A%2262130180%22%2C%22nom%22%3A%22TENNIS%20SPORTING%20CLUB%20DE%20CASSIS%22%7D%5D%2C%22codeClubActif%22%3A%2262130180%22%2C%22nbPaiementsEnAttente%22%3A0%2C%22completionConnexionRequise%22%3Afalse%2C%22completionDonneesRequise%22%3Afalse%2C%22completionCGARequise%22%3Afalse%2C%22completionHonorabilite%22%3Afalse%2C%22millesimeSuivant%22%3Afalse%2C%22dirigeant%22%3Afalse%7D%7D; datadome=eOhBW_xLJNlEXp_0ffYtq~AAuDrpoQZ38mo9fuxTeCItoQl_g0Tr1YfswxzFATknzJ2Er4j_dOLzqfN0iO~2gsEES5GMdrNiat6SFAy7_sRFRs0nFykcBf_wrSCt360D; xtan=-; xtant=1; SSESS7ba44afc36c80c3faa2b8fa87e7742c5=t7J6ikkE34JlGEgFUittt_nQWNiNtHySzLCpRXYnn64; TCID=; pa_vid=%22mckhos3iasswydjm%22; TC_PRIVACY=1%40033%7C64%7C3288%40%405%401748938434779%2C1748938434779%2C1782634434779%40; TC_PRIVACY_CENTER=; pa_privacy=%22optin%22; TCPID=125629554310878226394; xtvrn=$548419$',
'Origin': 'https://tenup.fft.fr',
'Referer': 'https://tenup.fft.fr/classement-padel',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0.1 Safari/605.1.15'
}
all_players = []
total_tranches = end_tranche - start_tranche + 1
successful_calls = 0
failed_calls = 0
failed_tranches_list = []
retry_stats = {} # Track retry attempts per tranche
print(
f"Starting to fetch tranches {start_tranche} to {end_tranche} ({total_tranches} total)..."
)
def fetch_tranche_with_retry(tranche, max_retries=10):
"""
Fetch a single tranche with retry logic
Returns: (success, players_data, retry_count)
"""
payload = {"pratique": "padel", "sexe": sexe, "tranche": tranche}
for attempt in range(max_retries + 1): # +1 for initial attempt
try:
response = requests.post(
url, json=payload, headers=headers, timeout=30
)
if response.status_code == 200:
json_data = response.json()
if "joueurs" in json_data and json_data["joueurs"]:
# Add metadata to each player for enrichment tracking
for player in json_data["joueurs"]:
player["source_tranche"] = tranche
player["license_lookup_status"] = "not_attempted"
player["license_data"] = None
if attempt > 0:
print(
f"Tranche {tranche}: SUCCESS after {attempt} retries - Found {len(json_data['joueurs'])} players"
)
else:
print(
f"Tranche {tranche}: Found {len(json_data['joueurs'])} players"
)
return True, json_data["joueurs"], attempt
else:
if attempt > 0:
print(
f"Tranche {tranche}: SUCCESS after {attempt} retries - No players found"
)
else:
print(f"Tranche {tranche}: No players found")
return True, [], attempt
else:
if attempt < max_retries:
print(
f"Tranche {tranche}: HTTP {response.status_code} - Retry {attempt + 1}/{max_retries}"
)
time.sleep(
min(2**attempt, 10)
) # Exponential backoff, max 10 seconds
else:
print(
f"Tranche {tranche}: FAILED after {max_retries} retries - HTTP {response.status_code}"
)
except requests.exceptions.RequestException as e:
if attempt < max_retries:
print(
f"Tranche {tranche}: Network error - {str(e)} - Retry {attempt + 1}/{max_retries}"
)
time.sleep(
min(2**attempt, 10)
) # Exponential backoff, max 10 seconds
else:
print(
f"Tranche {tranche}: FAILED after {max_retries} retries - Network error: {str(e)}"
)
except Exception as e:
if attempt < max_retries:
print(
f"Tranche {tranche}: Unexpected error - {str(e)} - Retry {attempt + 1}/{max_retries}"
)
time.sleep(
min(2**attempt, 10)
) # Exponential backoff, max 10 seconds
else:
print(
f"Tranche {tranche}: FAILED after {max_retries} retries - Unexpected error: {str(e)}"
)
return False, [], max_retries
# Process all tranches with retry logic
for tranche in range(start_tranche, end_tranche + 1):
success, players_data, retry_count = fetch_tranche_with_retry(tranche)
if success:
all_players.extend(players_data)
successful_calls += 1
if retry_count > 0:
retry_stats[tranche] = retry_count
else:
failed_calls += 1
failed_tranches_list.append(tranche)
retry_stats[tranche] = retry_count
# Progress update and small delay
if tranche % 10 == 0:
time.sleep(0.1)
current_progress = tranche - start_tranche + 1
print(
f"Progress: {current_progress}/{total_tranches} tranches processed..."
)
print(f"Completed! Total players found: {len(all_players)}")
print(f"Successful calls: {successful_calls}, Failed calls: {failed_calls}")
# Enhanced retry statistics logging
retry_summary = {}
tranches_with_retries = [
t
for t, c in retry_stats.items()
if c > 0 and t not in failed_tranches_list
]
if tranches_with_retries:
print(f"Tranches that required retries: {len(tranches_with_retries)}")
for tranche in sorted(tranches_with_retries):
retry_count = retry_stats[tranche]
print(f" Tranche {tranche}: {retry_count} retries")
if retry_count not in retry_summary:
retry_summary[retry_count] = 0
retry_summary[retry_count] += 1
print("Retry distribution:")
for retry_count in sorted(retry_summary.keys()):
print(
f" {retry_summary[retry_count]} tranches needed {retry_count} retries"
)
else:
print("No retries were needed!")
if failed_tranches_list:
print(f"Failed tranches: {failed_tranches_list}")
failed_retry_counts = [
retry_stats.get(t, 0) for t in failed_tranches_list
]
print(f"All failed tranches attempted maximum retries (10)")
else:
print("No failed tranches - all requests successful!")
if all_players:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
final_data = {
"metadata": {
"total_players": len(all_players),
"successful_tranches": successful_calls,
"failed_tranches": failed_calls,
"failed_tranches_list": failed_tranches_list,
"total_tranches_requested": total_tranches,
"tranche_range": f"{start_tranche}-{end_tranche}",
"download_timestamp": datetime.now().isoformat(),
"retry_statistics": {
"tranches_with_retries": len(tranches_with_retries),
"retry_stats_per_tranche": retry_stats,
"retry_distribution": retry_summary,
"max_retries_attempted": 10,
},
"last_enrichment_update": None,
"enrichment_progress": {
"players_with_licenses": 0,
"players_without_licenses": len(all_players),
"last_processed_index": -1,
},
"parameters": {
"pratique": "padel",
"sexe": sexe,
"tranche_start": start_tranche,
"tranche_end": end_tranche,
},
},
"joueurs": all_players,
}
# Save locally if requested
if save_locally:
rankings_dir = os.path.join(settings.BASE_DIR, "data", "rankings")
os.makedirs(rankings_dir, exist_ok=True)
filename = f"french_padel_rankings_{start_tranche}-{end_tranche}_{timestamp}.json"
local_file_path = os.path.join(rankings_dir, filename)
with open(local_file_path, "w", encoding="utf-8") as f:
json.dump(final_data, f, indent=2, ensure_ascii=False)
print(f"Rankings saved locally to: {local_file_path}")
messages.success(
request, f"Rankings saved locally to: {local_file_path}"
)
# Create download response
download_filename = f"french_padel_rankings_{start_tranche}-{end_tranche}_{timestamp}.json"
http_response = HttpResponse(
json.dumps(final_data, indent=2, ensure_ascii=False),
content_type="application/json; charset=utf-8",
)
http_response["Content-Disposition"] = (
f'attachment; filename="{download_filename}"'
)
return http_response
else:
messages.error(
request,
f"No players found in tranches {start_tranche}-{end_tranche}.",
)
except Exception as e:
messages.error(request, f"Unexpected error: {str(e)}")
csrf_token = get_token(request)
html_template = Template("""
<!DOCTYPE html>
<html>
<head>
<title>Download French Padel Rankings - Debug Tools</title>
<link rel="stylesheet" type="text/css" href="/static/admin/css/base.css">
<style>
body { font-family: "Roboto", "Lucida Grande", Verdana, Arial, sans-serif; margin: 40px; background: #f8f8f8; }
.debug-container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-width: 800px;
}
.form-group { margin: 20px 0; }
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
}
.form-group input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-row {
display: flex;
gap: 15px;
}
.form-row .form-group {
flex: 1;
}
.button {
background-color: #79aec8;
border: none;
color: white;
padding: 12px 24px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 14px;
margin: 8px 4px;
cursor: pointer;
border-radius: 4px;
font-weight: bold;
}
.button:hover { background-color: #6ba6cd; }
.info {
background-color: #d1ecf1;
color: #0c5460;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
border-left: 4px solid #17a2b8;
}
.back-link {
color: #79aec8;
text-decoration: none;
font-weight: bold;
margin-top: 20px;
display: inline-block;
}
.back-link:hover { text-decoration: underline; }
h1 { color: #333; margin-bottom: 30px; }
</style>
</head>
<body>
<div class="debug-container">
<h1>📊 Download French Padel Rankings</h1>
<div class="info">
<strong>🔄 New Workflow:</strong><br>
1. Use this tool to download and save rankings locally<br>
2. Then use "Enrich Rankings with Licenses" to add license data<br>
3. Process is resumable - you can stop and restart enrichment
</div>
<form method="post" action="">
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
<div class="form-row">
<div class="form-group">
<label for="start_tranche">Start Tranche:</label>
<input type="number" name="start_tranche" id="start_tranche" value="1001" min="1" max="2000" required>
</div>
<div class="form-group">
<label for="end_tranche">End Tranche:</label>
<input type="number" name="end_tranche" id="end_tranche" value="1222" min="1" max="2000" required>
</div>
</div>
<div class="form-group">
<label for="sexe">Sexe:</label>
<select name="sexe" id="sexe" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;" required>
<option value="H" {% if default_sexe == 'H' %}selected{% endif %}>Homme (H)</option>
<option value="F" {% if default_sexe == 'F' %}selected{% endif %}>Femme (F)</option>
</select>
<small style="color: #666;">Gender for rankings search</small>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="save_locally" value="true" checked> Save locally for enrichment
</label>
<small style="color: #666;">Saves to data/rankings/ directory for later license enrichment</small>
</div>
<button type="submit" class="button">📊 Download Rankings</button>
</form>
<br>
<a href="/kingdom/debug/" class="back-link">← Back to Debug Tools</a>
</div>
</body>
</html>
""")
context = Context({"csrf_token": csrf_token, "default_sexe": default_sexe})
return HttpResponse(html_template.render(context))
@staff_member_required
def debug_tools_page(request):
"""
Simple debug tools page with download button
"""
html_content = """
<!DOCTYPE html>
<html>
<head>
<title>Debug Tools - Padel Club Admin</title>
<link rel="stylesheet" type="text/css" href="/static/admin/css/base.css">
<style>
body { font-family: "Roboto", "Lucida Grande", Verdana, Arial, sans-serif; margin: 40px; background: #f8f8f8; }
.debug-container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-width: 900px;
}
.button {
background-color: #79aec8;
border: none;
color: white;
padding: 12px 24px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 14px;
margin: 8px 4px;
cursor: pointer;
border-radius: 4px;
font-weight: bold;
}
.button:hover { background-color: #6ba6cd; }
.button.secondary { background-color: #6c757d; }
.button.secondary:hover { background-color: #5a6268; }
.button.success { background-color: #28a745; }
.button.success:hover { background-color: #218838; }
.button.info { background-color: #17a2b8; }
.button.info:hover { background-color: #138496; }
.button.warning { background-color: #ffc107; color: #212529; }
.button.warning:hover { background-color: #e0a800; }
.info {
background-color: #d1ecf1;
color: #0c5460;
padding: 12px;
border-radius: 4px;
margin: 15px 0;
border-left: 4px solid #17a2b8;
}
.section {
margin: 30px 0;
padding: 20px 0;
border-top: 1px solid #eee;
}
.back-link {
color: #79aec8;
text-decoration: none;
font-weight: bold;
margin-top: 20px;
display: inline-block;
}
.back-link:hover { text-decoration: underline; }
h1 { color: #333; margin-bottom: 30px; }
h2 { color: #666; margin-top: 30px; margin-bottom: 15px; }
h3 { color: #777; margin-top: 20px; margin-bottom: 10px; }
p { color: #666; line-height: 1.5; }
ul { color: #666; line-height: 1.5; }
</style>
</head>
<body>
<div class="debug-container">
<h1>🏓 Debug Tools</h1>
<div class="section">
<h2>French Padel Rankings Download</h2>
<p>Download French padel rankings for men from the FFT API for tranches 1001 to 1222.</p>
<a href="/kingdom/debug/download-french-padel-rankings/" class="button">📥 Download Rankings (1001-1222)</a>
</div>
<div class="section">
<h2>🎯 Tournament & Umpire Management</h2>
<p>Tools for gathering tournament data and umpire contact information from FFT.</p>
<h3>Monthly Umpire Export</h3>
<p>Gather all tournaments within a specified month and export umpire contact information to CSV format.</p>
<a href="/kingdom/debug/gather-monthly-umpires/" class="button warning">📊 Export Monthly Umpires</a>
<div class="info">
<strong>📋 Export Format:</strong> The CSV will contain: CLUB_NAME;LAST_NAME;FIRST_NAME;EMAIL;PHONE_NUMBER<br>
<strong>⏱ Processing Time:</strong> This may take several minutes for large date ranges as it processes tournaments in batches of 100.
</div>
</div>
<div class="section">
<h2>🔗 Enhanced Rankings Tools</h2>
<p>Advanced tools that combine multiple APIs for enriched data.</p>
<h3>Enrich Rankings with License Data</h3>
<p>Get rankings from one tranche and automatically lookup license information for each player.</p>
<a href="/kingdom/debug/enrich-rankings-with-licenses/" class="button info">🔗 Enrich Rankings with Licenses</a>
</div>
<div class="section">
<h2>🔍 Player License Lookup Tools</h2>
<p>Use the FFT Beach-Padel API to lookup detailed player information.</p>
<h3>Search by Name</h3>
<p>Search for players by nom (last name) and/or prenom (first name) to find their license information.</p>
<a href="/kingdom/debug/search-player-by-name/" class="button info">👤 Search Player by Name</a>
<h3>Lookup by License Number</h3>
<p>Lookup detailed information for a single player license number.</p>
<a href="/kingdom/debug/player-license-lookup/" class="button success">🔍 Single License Lookup</a>
<h3>Bulk License Lookup</h3>
<p>Lookup multiple player licenses at once (use carefully to avoid overloading the API).</p>
<a href="/kingdom/debug/bulk-license-lookup/" class="button success">📋 Bulk License Lookup</a>
<div class="info">
<strong>💡 How to use:</strong> You'll need a valid sessionId (from Cookie header) and idHomologation (competition ID) from the FFT Beach-Padel website. Use browser dev tools to get these values.
</div>
</div>
<div class="section">
<h2>🔬 API Research Tools</h2>
<p>Tools to help discover and explore the FFT API endpoints.</p>
<a href="/kingdom/debug/test-player-apis/" class="button secondary">🧪 Test Player Detail APIs</a>
<a href="/kingdom/debug/explore-api-endpoints/" class="button secondary">🔍 Explore FFT API Endpoints</a>
</div>
<br><br>
<a href="/kingdom/" class="back-link">← Back to Admin Dashboard</a>
</div>
</body>
</html>
"""
return HttpResponse(html_content)
@staff_member_required
def get_player_license_info(request):
"""
Get player license information using sessionId and idHomologation
"""
if request.method == "POST":
session_id = request.POST.get("sessionId", "").strip()
id_homologation = request.POST.get("idHomologation", "").strip()
license_id = request.POST.get("licenseId", "").strip()
if not session_id or not id_homologation or not license_id:
messages.error(
request, "sessionId, idHomologation, and licenseId are all required."
)
return redirect(request.path)
try:
# Construct the URL (equivalent to Swift's URL construction)
url = f"https://beach-padel.app.fft.fr/beachja/rechercheJoueur/licencies?idHomologation={id_homologation}&numeroLicence={license_id}"
# Set up headers (equivalent to Swift's URLRequest headers)
headers = {
"Accept": "application/json, text/javascript, */*; q=0.01",
"Sec-Fetch-Site": "same-origin",
"Accept-Language": "fr-FR,fr;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Sec-Fetch-Mode": "cors",
"Host": "beach-padel.app.fft.fr",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15",
"Connection": "keep-alive",
"Referer": f"https://beach-padel.app.fft.fr/beachja/competitionFiche/inscrireEquipe?identifiantHomologation={id_homologation}",
"X-Requested-With": "XMLHttpRequest",
"Cookie": session_id,
}
print(f"Making request to: {url}")
print(f"Headers: {headers}")
# Make the GET request (equivalent to Swift's URLSession.shared.data)
response = requests.get(url, headers=headers, timeout=30)
print(f"Response status: {response.status_code}")
print(f"Raw JSON response: {response.text}")
if response.status_code == 200:
try:
json_data = response.json()
# Create result structure
result = {
"request_info": {
"url": url,
"license_id": license_id,
"id_homologation": id_homologation,
"timestamp": datetime.now().isoformat(),
},
"response_info": {
"status_code": response.status_code,
"headers": dict(response.headers),
"raw_response": response.text,
},
"parsed_data": json_data,
}
# Create filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"player_license_info_{license_id}_{timestamp}.json"
# Return as downloadable JSON
http_response = HttpResponse(
json.dumps(result, indent=2, ensure_ascii=False),
content_type="application/json; charset=utf-8",
)
http_response["Content-Disposition"] = (
f'attachment; filename="{filename}"'
)
return http_response
except json.JSONDecodeError as e:
messages.error(request, f"Failed to parse JSON response: {str(e)}")
else:
messages.error(
request,
f"Request failed with status {response.status_code}: {response.text}",
)
except requests.exceptions.RequestException as e:
messages.error(request, f"Network error: {str(e)}")
except Exception as e:
messages.error(request, f"Unexpected error: {str(e)}")
csrf_token = get_token(request)
# Default values
default_license_id = "5186803"
html_template = Template("""
<!DOCTYPE html>
<html>
<head>
<title>Player License Lookup - Debug Tools</title>
<link rel="stylesheet" type="text/css" href="/static/admin/css/base.css">
<style>
body { font-family: "Roboto", "Lucida Grande", Verdana, Arial, sans-serif; margin: 40px; background: #f8f8f8; }
.debug-container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-width: 800px;
}
.form-group { margin: 20px 0; }
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
}
.form-group input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-group textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 12px;
height: 120px;
font-family: monospace;
}
.button {
background-color: #79aec8;
border: none;
color: white;
padding: 12px 24px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 14px;
margin: 8px 4px;
cursor: pointer;
border-radius: 4px;
font-weight: bold;
}
.button:hover { background-color: #6ba6cd; }
.success {
background-color: #d4edda;
color: #155724;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
border-left: 4px solid #28a745;
}
.back-link {
color: #79aec8;
text-decoration: none;
font-weight: bold;
margin-top: 20px;
display: inline-block;
}
.back-link:hover { text-decoration: underline; }
h1 { color: #333; margin-bottom: 30px; }
h2 { color: #666; margin-top: 30px; margin-bottom: 15px; }
</style>
</head>
<body>
<div class="debug-container">
<h1>🔍 Player License Lookup</h1>
<div class="success">
<strong>✅ Default values loaded!</strong><br>
The form is pre-filled with your provided sessionId, idHomologation, and license ID.
You can modify any field if needed before submitting.
</div>
<form method="post" action="">
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
<div class="form-group">
<label for="sessionId">Session ID (from Cookie header):</label>
<textarea name="sessionId" id="sessionId" required>{{ default_session_id }}</textarea>
<small style="color: #666;">Pre-filled with your provided session ID</small>
</div>
<div class="form-group">
<label for="idHomologation">ID Homologation:</label>
<input type="text" name="idHomologation" id="idHomologation" value="{{ default_id_homologation }}" required>
<small style="color: #666;">Competition/event identifier (pre-filled)</small>
</div>
<div class="form-group">
<label for="licenseId">License ID:</label>
<input type="text" name="licenseId" id="licenseId" value="{{ default_license_id }}" required>
<small style="color: #666;">Player license number to lookup (pre-filled)</small>
</div>
<button type="submit" class="button">🔍 Lookup Player License Info</button>
</form>
<br>
<a href="/kingdom/debug/" class="back-link">← Back to Debug Tools</a>
</div>
</body>
</html>
""")
context = Context(
{
"csrf_token": csrf_token,
"default_session_id": default_session_id,
"default_id_homologation": default_id_homologation,
"default_license_id": default_license_id,
"default_sexe": default_sexe,
}
)
return HttpResponse(html_template.render(context))
@staff_member_required
def bulk_license_lookup(request):
"""
Lookup multiple license IDs at once
"""
if request.method == "POST":
session_id = request.POST.get("sessionId", "").strip()
id_homologation = request.POST.get("idHomologation", "").strip()
license_ids_text = request.POST.get("licenseIds", "").strip()
if not session_id or not id_homologation or not license_ids_text:
messages.error(request, "All fields are required.")
return redirect(request.path)
# Parse license IDs (one per line or comma-separated)
license_ids = []
for line in license_ids_text.replace(",", "\n").split("\n"):
license_id = line.strip()
if license_id:
license_ids.append(license_id)
if not license_ids:
messages.error(request, "No valid license IDs provided.")
return redirect(request.path)
results = {
"bulk_lookup_info": {
"total_licenses": len(license_ids),
"id_homologation": id_homologation,
"timestamp": datetime.now().isoformat(),
},
"results": [],
}
# Headers setup (same as single lookup)
headers = {
"Accept": "application/json, text/javascript, */*; q=0.01",
"Sec-Fetch-Site": "same-origin",
"Accept-Language": "fr-FR,fr;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Sec-Fetch-Mode": "cors",
"Host": "beach-padel.app.fft.fr",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15",
"Connection": "keep-alive",
"Referer": f"https://beach-padel.app.fft.fr/beachja/competitionFiche/inscrireEquipe?identifiantHomologation={id_homologation}",
"X-Requested-With": "XMLHttpRequest",
"Cookie": session_id,
}
for i, license_id in enumerate(license_ids):
try:
url = f"https://beach-padel.app.fft.fr/beachja/rechercheJoueur/licencies?idHomologation={id_homologation}&numeroLicence={license_id}"
print(f"Looking up license {i + 1}/{len(license_ids)}: {license_id}")
response = requests.get(url, headers=headers, timeout=30)
result_item = {
"license_id": license_id,
"status_code": response.status_code,
"success": response.status_code == 200,
}
if response.status_code == 200:
try:
json_data = response.json()
result_item["data"] = json_data
except json.JSONDecodeError:
result_item["error"] = "Failed to parse JSON"
result_item["raw_response"] = response.text[:500]
else:
result_item["error"] = f"HTTP {response.status_code}"
result_item["raw_response"] = response.text[:500]
results["results"].append(result_item)
# Small delay to avoid overwhelming the API
if i < len(license_ids) - 1:
time.sleep(0.5)
except Exception as e:
results["results"].append(
{"license_id": license_id, "success": False, "error": str(e)}
)
# Create filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"bulk_license_lookup_{len(license_ids)}_licenses_{timestamp}.json"
# Return as downloadable JSON
http_response = HttpResponse(
json.dumps(results, indent=2, ensure_ascii=False),
content_type="application/json; charset=utf-8",
)
http_response["Content-Disposition"] = f'attachment; filename="{filename}"'
return http_response
csrf_token = get_token(request)
# Default values
default_license_ids = "5186803\n1234567\n2345678"
html_template = Template("""
<!DOCTYPE html>
<html>
<head>
<title>Bulk License Lookup - Debug Tools</title>
<link rel="stylesheet" type="text/css" href="/static/admin/css/base.css">
<style>
body { font-family: "Roboto", "Lucida Grande", Verdana, Arial, sans-serif; margin: 40px; background: #f8f8f8; }
.debug-container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-width: 800px;
}
.form-group { margin: 20px 0; }
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
}
.form-group input, .form-group textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-group textarea {
font-family: monospace;
height: 120px;
}
.button {
background-color: #79aec8;
border: none;
color: white;
padding: 12px 24px;
font-size: 14px;
cursor: pointer;
border-radius: 4px;
font-weight: bold;
}
.button:hover { background-color: #6ba6cd; }
.warning {
background-color: #fff3cd;
color: #856404;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
border-left: 4px solid #ffc107;
}
.success {
background-color: #d4edda;
color: #155724;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
border-left: 4px solid #28a745;
}
.back-link {
color: #79aec8;
text-decoration: none;
font-weight: bold;
margin-top: 20px;
display: inline-block;
}
h1 { color: #333; margin-bottom: 30px; }
</style>
</head>
<body>
<div class="debug-container">
<h1>🔍 Bulk License Lookup</h1>
<div class="success">
<strong>✅ Default values loaded!</strong><br>
ID Homologation and sample license IDs are pre-filled. Add your session ID and modify license list as needed.
</div>
<div class="warning">
<strong>⚠ Warning:</strong> This will make multiple API calls. Use responsibly to avoid overwhelming the FFT servers.
</div>
<form method="post" action="">
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
<div class="form-group">
<label for="sessionId">Session ID (Cookie header):</label>
<textarea name="sessionId" id="sessionId" placeholder="Paste your session ID here..." required>{{ default_session_id }}</textarea>
<small style="color: #666;">Copy the Cookie header from browser dev tools</small>
</div>
<div class="form-group">
<label for="idHomologation">ID Homologation:</label>
<input type="text" name="idHomologation" id="idHomologation" value="{{ default_id_homologation }}" required>
<small style="color: #666;">Competition/event identifier (pre-filled)</small>
</div>
<div class="form-group">
<label for="licenseIds">License IDs (one per line or comma-separated):</label>
<textarea name="licenseIds" id="licenseIds" required>{{ default_license_ids }}</textarea>
<small style="color: #666;">Pre-filled with sample license IDs - modify as needed</small>
</div>
<button type="submit" class="button">🔍 Lookup All Licenses</button>
</form>
<br>
<a href="/kingdom/debug/" class="back-link">← Back to Debug Tools</a>
</div>
</body>
</html>
""")
context = Context(
{
"csrf_token": csrf_token,
"default_session_id": default_session_id,
"default_id_homologation": default_id_homologation,
"default_license_ids": default_license_ids,
"default_sexe": default_sexe,
}
)
return HttpResponse(html_template.render(context))
@staff_member_required
def search_player_by_name(request):
"""
Search for players by nom and prenom
"""
if request.method == "POST":
session_id = request.POST.get("sessionId", "").strip()
id_homologation = request.POST.get("idHomologation", "").strip()
nom = request.POST.get("nom", "")
prenom = request.POST.get("prenom", "")
sexe = request.POST.get("sexe", default_sexe)
if not session_id or not id_homologation:
messages.error(request, "sessionId and idHomologation are required.")
return redirect(request.path)
if not nom and not prenom:
messages.error(request, "At least nom or prenom is required.")
return redirect(request.path)
try:
# Construct the URL for name search
base_url = (
"https://beach-padel.app.fft.fr/beachja/rechercheJoueur/licencies"
)
params = {"idHomologation": id_homologation}
# Add name parameters if provided
if nom:
params["nom"] = nom
if prenom:
params["prenom"] = prenom
# Build query string
query_string = "&".join([f"{k}={v}" for k, v in params.items()])
url = f"{base_url}?{query_string}"
# Set up headers
headers = {
"Accept": "application/json, text/javascript, */*; q=0.01",
"Sec-Fetch-Site": "same-origin",
"Accept-Language": "fr-FR,fr;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Sec-Fetch-Mode": "cors",
"Host": "beach-padel.app.fft.fr",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15",
"Connection": "keep-alive",
"Referer": f"https://beach-padel.app.fft.fr/beachja/competitionFiche/inscrireEquipe?identifiantHomologation={id_homologation}",
"X-Requested-With": "XMLHttpRequest",
"Cookie": session_id,
}
print(f"Making request to: {url}")
print(f"Headers: {headers}")
# Make the GET request
response = requests.get(url, headers=headers, timeout=30)
print(f"Response status: {response.status_code}")
print(f"Raw JSON response: {response.text}")
if response.status_code == 200:
try:
json_data = response.json()
# Create result structure
result = {
"search_info": {
"url": url,
"nom": nom,
"prenom": prenom,
"id_homologation": id_homologation,
"timestamp": datetime.now().isoformat(),
},
"response_info": {
"status_code": response.status_code,
"headers": dict(response.headers),
"raw_response": response.text,
},
"parsed_data": json_data,
}
# Create filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
search_term = f"{nom}_{prenom}".replace(" ", "_")
filename = f"player_search_{search_term}_{timestamp}.json"
# Return as downloadable JSON
http_response = HttpResponse(
json.dumps(result, indent=2, ensure_ascii=False),
content_type="application/json; charset=utf-8",
)
http_response["Content-Disposition"] = (
f'attachment; filename="{filename}"'
)
return http_response
except json.JSONDecodeError as e:
messages.error(request, f"Failed to parse JSON response: {str(e)}")
else:
messages.error(
request,
f"Request failed with status {response.status_code}: {response.text}",
)
except requests.exceptions.RequestException as e:
messages.error(request, f"Network error: {str(e)}")
except Exception as e:
messages.error(request, f"Unexpected error: {str(e)}")
csrf_token = get_token(request)
# Default values
html_template = Template("""
<!DOCTYPE html>
<html>
<head>
<title>Search Player by Name - Debug Tools</title>
<link rel="stylesheet" type="text/css" href="/static/admin/css/base.css">
<style>
body { font-family: "Roboto", "Lucida Grande", Verdana, Arial, sans-serif; margin: 40px; background: #f8f8f8; }
.debug-container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-width: 800px;
}
.form-group { margin: 20px 0; }
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
}
.form-group input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-group textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 12px;
height: 100px;
font-family: monospace;
}
.form-row {
display: flex;
gap: 15px;
}
.form-row .form-group {
flex: 1;
}
.button {
background-color: #79aec8;
border: none;
color: white;
padding: 12px 24px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 14px;
margin: 8px 4px;
cursor: pointer;
border-radius: 4px;
font-weight: bold;
}
.button:hover { background-color: #6ba6cd; }
.info {
background-color: #d1ecf1;
color: #0c5460;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
border-left: 4px solid #17a2b8;
}
.success {
background-color: #d4edda;
color: #155724;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
border-left: 4px solid #28a745;
}
.back-link {
color: #79aec8;
text-decoration: none;
font-weight: bold;
margin-top: 20px;
display: inline-block;
}
.back-link:hover { text-decoration: underline; }
h1 { color: #333; margin-bottom: 30px; }
h2 { color: #666; margin-top: 30px; margin-bottom: 15px; }
</style>
</head>
<body>
<div class="debug-container">
<h1>🔍 Search Player by Name</h1>
<div class="success">
<strong>✅ Default values loaded!</strong><br>
ID Homologation is pre-filled. Add your session ID and enter the player's name to search.
</div>
<div class="info">
<strong>ℹ How it works:</strong><br>
• You can search by nom (last name), prenom (first name), or both<br>
• Partial names should work (e.g., "DUPON" might find "DUPONT")<br>
• Leave one field empty to search by the other<br>
• Results will include license numbers and detailed player information
</div>
<form method="post" action="">
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
<div class="form-group">
<label for="sessionId">Session ID (Cookie header):</label>
<textarea name="sessionId" id="sessionId" placeholder="Paste your session ID here..." required>{{ default_session_id }}</textarea>
<small style="color: #666;">Copy the Cookie header from browser dev tools</small>
</div>
<div class="form-group">
<label for="idHomologation">ID Homologation:</label>
<input type="text" name="idHomologation" id="idHomologation" value="{{ default_id_homologation }}" required>
<small style="color: #666;">Competition/event identifier (pre-filled)</small>
</div>
<div class="form-row">
<div class="form-group">
<label for="nom">Nom (Last Name):</label>
<input type="text" name="nom" id="nom" placeholder="DUPONT">
<small style="color: #666;">Player's last name</small>
</div>
<div class="form-group">
<label for="prenom">Prénom (First Name):</label>
<input type="text" name="prenom" id="prenom" placeholder="Jean">
<small style="color: #666;">Player's first name</small>
</div>
</div>
<button type="submit" class="button">🔍 Search Player</button>
</form>
<h2>📋 Example Searches</h2>
<p><strong>Search by last name only:</strong> nom = "MARTIN", prenom = empty</p>
<p><strong>Search by first name only:</strong> nom = empty, prenom = "Jean"</p>
<p><strong>Search by both:</strong> nom = "DUPONT", prenom = "Pierre"</p>
<p><strong>Partial search:</strong> nom = "DUPON" (might find "DUPONT", "DUPONTEL", etc.)</p>
<br>
<a href="/kingdom/debug/" class="back-link">← Back to Debug Tools</a>
</div>
</body>
</html>
""")
context = Context(
{
"csrf_token": csrf_token,
"default_session_id": default_session_id,
"default_id_homologation": default_id_homologation,
"default_sexe": default_sexe,
}
)
return HttpResponse(html_template.render(context))
@staff_member_required
def enrich_rankings_with_licenses(request):
"""
Load a local rankings file and enrich players with license data (resumable)
Uses concurrent processing to speed up lookups for large files.
"""
if request.method == "POST":
file_path = request.POST.get("file_path", "").strip()
session_id = request.POST.get("sessionId", default_session_id).strip()
id_homologation = request.POST.get(
"idHomologation", default_id_homologation
).strip()
save_batch_size = int(
request.POST.get("batch_size", 1000)
) # How often to save progress
max_workers = int(
request.POST.get("max_workers", 10)
) # New parameter for controlling concurrency
if not file_path or not session_id or not id_homologation:
messages.error(
request, "File path, session ID, and ID homologation are required."
)
return redirect(request.path)
try:
# Load the rankings file
if not os.path.exists(file_path):
messages.error(request, f"File not found: {file_path}")
return redirect(request.path)
print(f"Loading rankings from: {file_path}")
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
players = data.get("joueurs", [])
metadata = data.get("metadata", {})
# Extract sex from metadata (it's the same for all players in the ranking)
sexe = metadata.get("parameters", {}).get("sexe", default_sexe)
print(f"Using sex from metadata: {sexe}")
if not players:
messages.error(request, "No players found in the file.")
return redirect(request.path)
print(f"Loaded {len(players)} players from file")
# Define a worker function that will process a single player
def lookup_player_license(player_tuple, session_id, id_homologation, sexe):
original_index, player = player_tuple
# Extract and normalize names
raw_nom = player.get("nom")
raw_prenom = player.get("prenom")
if not raw_nom or not raw_prenom:
player["license_lookup_status"] = "missing_name_data"
return player, False, None
# Keep original case and accents, just clean up any extra whitespace
nom = raw_nom
prenom = raw_prenom
# Setup license lookup headers
license_headers = {
"Accept": "application/json, text/javascript, */*; q=0.01",
"Sec-Fetch-Site": "same-origin",
"Accept-Language": "fr-FR,fr;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Sec-Fetch-Mode": "cors",
"Host": "beach-padel.app.fft.fr",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15",
"Connection": "keep-alive",
"Referer": f"https://beach-padel.app.fft.fr/beachja/competitionFiche/inscrireEquipe?identifiantHomologation={id_homologation}",
"X-Requested-With": "XMLHttpRequest",
"Cookie": session_id,
}
def sanitize_for_latin1(text):
# Replace specific problematic characters
text = text.replace(
"\u2019", "'"
) # Replace right single quotation mark with regular apostrophe
# Add more replacements as needed
return text
try:
# Build license lookup URL with proper URL encoding
license_url = f"https://beach-padel.app.fft.fr/beachja/rechercheJoueur/licencies?idHomologation={id_homologation}&nom={urllib.parse.quote(sanitize_for_latin1(nom), encoding='latin-1')}&prenom={urllib.parse.quote(sanitize_for_latin1(prenom), encoding='latin-1')}&sexe={sexe}"
# Make license lookup request
license_response = requests.get(
license_url, headers=license_headers, timeout=30
)
license_response.encoding = "utf-8" # Ensure consistent encoding
if license_response.status_code == 200:
try:
license_data = license_response.json()
if (
license_data
and "object" in license_data
and "listeJoueurs" in license_data["object"]
):
liste_joueurs = license_data["object"]["listeJoueurs"]
presence_doublon = license_data["object"].get(
"presenceDoublon", False
)
if liste_joueurs:
# Find the best matching license using age comparison
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
player["licence"] = license_info.get("licence")
player["codeClub"] = license_info.get("codeClub")
player["nomClub"] = license_info.get("nomClub")
# Track duplicates and matching info
player["presenceDoublon"] = presence_doublon
player["nombreResultatsLicence"] = len(
liste_joueurs
)
player["license_match_info"] = match_info
if len(liste_joueurs) > 1:
player["tousResultatsLicence"] = liste_joueurs
player["license_lookup_status"] = "success"
# Enhanced logging with age matching info
doublon_status = ""
if presence_doublon:
if match_info["reason"] == "age_matched":
age_diff = match_info.get(
"best_age_difference", "?"
)
doublon_status = f" (DOUBLON - matched by age, diff: {age_diff})"
elif match_info["reason"] == "no_age_data":
doublon_status = (
" (DOUBLON - no age data, used first)"
)
else:
doublon_status = (
f" (DOUBLON - {match_info['reason']})"
)
return (
player,
True,
f"{nom} {prenom} -> {license_info.get('licence')}{doublon_status}",
)
else:
player["license_lookup_status"] = "no_results"
player["presenceDoublon"] = presence_doublon
return (
player,
False,
f"Failed {nom} {prenom} {player['idCrm']} {presence_doublon} -> No results",
)
else:
player["license_lookup_status"] = "no_data"
return (
player,
False,
f"Failed {nom} {prenom} {player['idCrm']} -> No data",
)
except json.JSONDecodeError as json_err:
player["license_lookup_status"] = "json_decode_error"
player["license_lookup_error"] = str(json_err)
player["raw_response"] = license_response.text
# Debug info for JSON decode errors
error_position = json_err.pos
return (
player,
False,
f"Failed {nom} {prenom} {player['idCrm']} -> JSON decode error: {str(json_err)} at pos {error_position}",
)
else:
player["license_lookup_status"] = (
f"http_error_{license_response.status_code}"
)
return (
player,
False,
f"Failed {nom} {prenom} {player['idCrm']} -> HTTP {license_response.status_code}",
)
except requests.exceptions.RequestException as e:
player["license_lookup_status"] = (
f"network_error: {type(e).__name__}"
)
player["license_lookup_error"] = str(e)
return (
player,
False,
f"Failed {nom} {prenom} {player['idCrm']} -> Network error: {type(e).__name__}: {str(e)}",
)
# Find players that need license enrichment
players_needing_licenses = []
players_with_licenses = 0
for i, player in enumerate(players):
if player is None:
continue
if player.get("license_lookup_status") not in ["success"]:
players_needing_licenses.append((i, player))
else:
players_with_licenses += 1
print(f"Players with licenses: {players_with_licenses}")
print(f"Players needing licenses: {len(players_needing_licenses)}")
if not players_needing_licenses:
messages.success(request, "All players already have license data!")
return redirect(request.path)
# Process players concurrently in batches
total_processed = 0
successful_lookups = 0
failed_lookups = 0
print(
f"Starting concurrent enrichment of {len(players_needing_licenses)} players with {max_workers} workers"
)
# Prepare the partial function with fixed parameters
worker_fn = partial(
lookup_player_license,
session_id=session_id,
id_homologation=id_homologation,
sexe=sexe,
)
# Process in batches for saving progress, using the value from the form
for batch_start in range(0, len(players_needing_licenses), save_batch_size):
batch_end = min(
batch_start + save_batch_size, len(players_needing_licenses)
)
current_batch = players_needing_licenses[batch_start:batch_end]
print(
f"\n=== Processing batch {batch_start // save_batch_size + 1}: players {batch_start + 1} to {batch_end} ==="
)
batch_results = []
# Process the current batch with concurrent workers
with concurrent.futures.ThreadPoolExecutor(
max_workers=max_workers
) as executor:
future_to_player = {
executor.submit(worker_fn, player_tuple): player_tuple
for player_tuple in current_batch
}
for future in concurrent.futures.as_completed(future_to_player):
player_tuple = future_to_player[future]
original_index = player_tuple[0]
try:
updated_player, success, message = future.result()
players[original_index] = (
updated_player # Update the player in the main list
)
if success:
successful_lookups += 1
else:
failed_lookups += 1
total_processed += 1
# Print progress every 10 players
if total_processed % 10 == 0 or total_processed == len(
players_needing_licenses
):
print(
f" Progress: {total_processed}/{len(players_needing_licenses)} ({(total_processed / len(players_needing_licenses) * 100):.1f}%)"
)
if message:
batch_results.append(message)
except Exception as e:
print(
f" ERROR processing player {original_index} ({player_tuple}'): {str(e)}"
)
failed_lookups += 1
total_processed += 1
# Print batch results
for msg in batch_results:
print(f" {msg}")
# Save progress after each batch
metadata["last_enrichment_update"] = datetime.now().isoformat()
metadata["enrichment_progress"] = {
"players_with_licenses": successful_lookups + players_with_licenses,
"players_without_licenses": len(players)
- (successful_lookups + players_with_licenses),
"last_processed_index": batch_end - 1,
"total_processed_this_run": total_processed,
"successful_this_run": successful_lookups,
"failed_this_run": failed_lookups,
}
# Save the updated file
with open(file_path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
print(
f"Progress saved: {successful_lookups + players_with_licenses}/{len(players)} players have licenses"
)
print(f"\n=== ENRICHMENT COMPLETE ===")
print(f"Total processed this run: {total_processed}")
print(f"Successful lookups this run: {successful_lookups}")
print(f"Failed lookups this run: {failed_lookups}")
print(
f"Total players with licenses: {successful_lookups + players_with_licenses}/{len(players)}"
)
messages.success(
request,
f"Enrichment complete! Processed {total_processed} players. {successful_lookups + players_with_licenses}/{len(players)} players now have license data.",
)
except Exception as e:
import traceback
error_traceback = traceback.format_exc()
print(f"CRITICAL ERROR: {type(e).__name__}: {str(e)}")
print(f"Traceback: {error_traceback}")
messages.error(
request, f"Error during enrichment: {type(e).__name__}: {str(e)}"
)
return redirect(request.path)
# Show the form
from django.template import Template, Context
from django.middleware.csrf import get_token
csrf_token = get_token(request)
# Try to find existing ranking files
rankings_dir = os.path.join(settings.BASE_DIR, "data", "rankings")
existing_files = []
if os.path.exists(rankings_dir):
for filename in os.listdir(rankings_dir):
if filename.endswith(".json") and "french_padel_rankings" in filename:
file_path = os.path.join(rankings_dir, filename)
existing_files.append(
{
"filename": filename,
"path": file_path,
"size": os.path.getsize(file_path),
"modified": datetime.fromtimestamp(
os.path.getmtime(file_path)
).strftime("%Y-%m-%d %H:%M:%S"),
}
)
existing_files.sort(key=lambda x: x["modified"], reverse=True)
html_template = Template("""
<!DOCTYPE html>
<html>
<head>
<title>Enrich Rankings with Licenses - Debug Tools</title>
<link rel="stylesheet" type="text/css" href="/static/admin/css/base.css">
<style>
body { font-family: "Roboto", "Lucida Grande", Verdana, Arial, sans-serif; margin: 40px; background: #f8f8f8; }
.debug-container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-width: 800px;
}
.form-group { margin: 20px 0; }
.form-group label {
display: block;
font-weight: bold;
margin-bottom: 5px;
}
.form-control { width: 100%; padding: 8px; box-sizing: border-box; }
.file-list { margin: 20px 0; }
.file-list div { padding: 10px; border-bottom: 1px solid #eee; }
.file-list div:hover { background: #f5f5f5; }
input[type="submit"] { margin-top: 20px; padding: 10px 20px; background: #79aec8; border: none; color: white; cursor: pointer; }
input[type="submit"]:hover { background: #609ab6; }
.back-link { margin-top: 20px; display: block; }
.info-box { background: #f8f8f8; padding: 15px; border-left: 4px solid #79aec8; margin-bottom: 20px; }
</style>
</head>
<body>
<div class="debug-container">
<h1>Enrich Rankings with Licenses</h1>
<div class="info-box">
<p><strong>Optimized for large datasets:</strong> This tool now supports concurrent processing to speed up license lookups.</p>
<p>You can adjust the batch size and number of concurrent workers based on your server's capacity.</p>
</div>
<form method="post">
{% csrf_token %}
<div class="form-group">
<label for="file_path">Rankings File Path:</label>
<input type="text" id="file_path" name="file_path" class="form-control" placeholder="/path/to/rankings.json" required>
</div>
<div class="form-group">
<label for="sessionId">Session ID (Cookie value):</label>
<input type="text" id="sessionId" name="sessionId" class="form-control" value="{{ default_session_id }}" placeholder="JSESSIONID=..." required>
</div>
<div class="form-group">
<label for="idHomologation">ID Homologation:</label>
<input type="text" id="idHomologation" name="idHomologation" class="form-control" value="{{ default_id_homologation }}" placeholder="e.g., 123456" required>
</div>
<div class="form-group">
<label for="batch_size">Save Progress Every N Players:</label>
<input type="number" id="batch_size" name="batch_size" class="form-control" value="1000" min="100" max="10000">
<small>How often to save progress to disk. After processing this many players, the file will be updated with results.</small>
</div>
<div class="form-group">
<label for="max_workers">Concurrent Workers (1-50):</label>
<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>
</div>
<input type="submit" value="Start Enrichment">
</form>
<h2>Available Ranking Files</h2>
<div class="file-list">
{% for file in existing_files %}
<div>
<strong>{{ file.filename }}</strong><br>
Path: {{ file.path }}<br>
Size: {{ file.size|filesizeformat }}<br>
Modified: {{ file.modified }}
<button onclick="document.getElementById('file_path').value = '{{ file.path }}';" style="float: right;">Use This File</button>
</div>
{% empty %}
<p>No ranking files found in the data/rankings directory.</p>
{% endfor %}
</div>
<a href="/kingdom/debug/" class="back-link">← Back to Debug Tools</a>
</div>
<script>
// Auto-select the file path when clicking on a file
document.querySelectorAll('.file-list div').forEach(function(el) {
el.addEventListener('click', function() {
var path = this.querySelector('strong').nextSibling.nextSibling.nextSibling.nodeValue.replace('Path: ', '').trim();
document.getElementById('file_path').value = path;
});
});
</script>
</body>
</html>
""")
context = {
"csrf_token": csrf_token,
"existing_files": existing_files,
"default_session_id": default_session_id,
"default_id_homologation": default_id_homologation,
"default_sexe": default_sexe,
}
rendered_html = html_template.render(Context(context))
return HttpResponse(rendered_html)
@staff_member_required
def gather_monthly_tournaments_and_umpires(request):
"""
Gather tournaments from current month and export umpire data to CSV
"""
if request.method == "GET":
# Display the form
html_content = """
<!DOCTYPE html>
<html>
<head>
<title>Monthly Tournament & Umpire Export - Padel Club Admin</title>
<link rel="stylesheet" type="text/css" href="/static/admin/css/base.css">
<style>
body { font-family: "Roboto", "Lucida Grande", Verdana, Arial, sans-serif; margin: 40px; background: #f8f8f8; }
.form-container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-width: 600px;
}
.button {
background-color: #79aec8;
border: none;
color: white;
padding: 12px 24px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 14px;
margin: 8px 4px;
cursor: pointer;
border-radius: 4px;
font-weight: bold;
}
.button:hover { background-color: #6ba6cd; }
.form-group {
margin: 20px 0;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="date"], input[type="text"], input[type="number"] {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
width: 200px;
}
.info {
background-color: #d1ecf1;
color: #0c5460;
padding: 12px;
border-radius: 4px;
margin: 15px 0;
border-left: 4px solid #17a2b8;
}
.back-link {
color: #79aec8;
text-decoration: none;
font-weight: bold;
margin-top: 20px;
display: inline-block;
}
.back-link:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="form-container">
<h1>🏓 Monthly Tournament & Umpire Export</h1>
<div class="info">
This tool will gather all tournaments within a specified date range and export umpire contact information to CSV format.
</div>
<form method="post">
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
<div class="form-group">
<label for="start_date">Start Date:</label>
<input type="date" id="start_date" name="start_date" required>
</div>
<div class="form-group">
<label for="end_date">End Date:</label>
<input type="date" id="end_date" name="end_date" required>
</div>
<div class="form-group">
<label for="city">City (optional):</label>
<input type="text" id="city" name="city" placeholder="Paris" value="Paris">
</div>
<div class="form-group">
<label for="distance">Distance (km):</label>
<input type="number" id="distance" name="distance" value="15" min="1" max="5000">
</div>
<button type="submit" class="button">🚀 Start Export</button>
<a href="/kingdom/debug/" class="back-link">← Back to Debug Tools</a>
</form>
<script>
// Set default dates to current month
const now = new Date();
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0);
document.getElementById('start_date').value = firstDay.toISOString().split('T')[0];
document.getElementById('end_date').value = lastDay.toISOString().split('T')[0];
</script>
</div>
</body>
</html>
"""
from django.middleware.csrf import get_token
from django.template import Template, Context
csrf_token = get_token(request)
template = Template(html_content)
context = Context({"csrf_token": csrf_token})
return HttpResponse(template.render(context))
elif request.method == "POST":
start_date = request.POST.get("start_date")
end_date = request.POST.get("end_date")
city = request.POST.get("city", "Paris")
distance = int(request.POST.get("distance", 3000))
if not start_date or not end_date:
return HttpResponse("Missing start_date or end_date", status=400)
try:
# Convert to datetime objects
start_datetime = datetime.strptime(start_date, "%Y-%m-%d")
end_datetime = datetime.strptime(end_date, "%Y-%m-%d")
# Format for FFT API (DD/MM/YY format like iOS)
start_date_formatted = start_datetime.strftime("%d/%m/%y")
end_date_formatted = end_datetime.strftime("%d/%m/%y")
# Step 1: Gather all tournaments using the same API endpoint as iOS
print(
f"🔍 Gathering tournaments from {start_date_formatted} to {end_date_formatted}"
)
tournaments = []
page = 0 # Start from page 0
# Default coordinates for Paris (same as iOS request)
lat = 48.856788
lng = 2.351077
base_url = f"http://127.0.0.1:8000/roads/fft/all-tournaments"
while True:
# Build URL exactly like iOS request
params = {
"sort": "dateDebut+asc",
"page": page,
"start_date": start_date_formatted,
"end_date": end_date_formatted,
"city": city,
"distance": distance,
"national_cup": "false",
"lat": lat,
"lng": lng,
}
# Make API request (internal call)
try:
import urllib.parse
query_string = urllib.parse.urlencode(params)
full_url = f"{base_url}?{query_string}"
print(f"📄 Requesting page {page}: {full_url}")
# Make internal API call using requests
response = requests.get(full_url)
if response.status_code != 200:
print(
f"❌ API request failed with status {response.status_code}"
)
break
result = response.json()
if not result.get("success") or not result.get("tournaments"):
print(f"📄 No more tournaments found on page {page}")
break
page_tournaments = result["tournaments"]
tournaments.extend(page_tournaments)
current_count = len(page_tournaments)
print(
f"📄 Page {page}: found {current_count} tournaments (total: {len(tournaments)})"
)
# Check if we have more pages based on total results
total_results = result.get("total_results", 0)
if len(tournaments) >= total_results:
print(f"✅ Reached all {total_results} tournaments")
break
page += 1
# Safety limit
if page > 50:
print(" Reached page limit, stopping")
break
except Exception as e:
print(f"❌ Error making API request for page {page}: {str(e)}")
break
print(f"📊 Found {len(tournaments)} tournaments total")
if not tournaments:
return HttpResponse(
f"<h2>No tournaments found for the period {start_date_formatted} to {end_date_formatted}</h2>"
f"<a href='/kingdom/debug/gather-monthly-umpires/'>← Back to form</a>",
content_type="text/html",
)
# Step 2: Gather umpire data for each tournament (with batching)
umpire_data = []
batch_size = 100
total_tournaments = len(tournaments)
print(
f"🎯 Starting umpire data collection for {total_tournaments} tournaments"
)
# Process tournaments in batches
for i in range(0, len(tournaments), batch_size):
batch = tournaments[i : i + batch_size]
batch_number = (i // batch_size) + 1
total_batches = (len(tournaments) + batch_size - 1) // batch_size
print(
f"\n🔄 Processing batch {batch_number}/{total_batches} ({len(batch)} tournaments)"
)
batch_processed = 0
# Use ThreadPoolExecutor for concurrent requests within each batch
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
# Submit all tasks for this batch
future_to_tournament = {
executor.submit(
get_tournament_umpire_data_api, tournament
): tournament
for tournament in batch
}
# Process results as they complete
for future in concurrent.futures.as_completed(future_to_tournament):
tournament = future_to_tournament[future]
batch_processed += 1
processed_total = i + batch_processed
try:
umpire_info = future.result()
if umpire_info:
umpire_data.append(umpire_info)
# Progress update every 10 tournaments
if batch_processed % 10 == 0 or batch_processed == len(
batch
):
batch_progress = int(
(batch_processed / len(batch)) * 100
)
total_progress = int(
(processed_total / total_tournaments) * 100
)
remaining = total_tournaments - processed_total
print(
f" ⚡ Batch {batch_number}: {batch_processed}/{len(batch)} ({batch_progress}%) | Total: {processed_total}/{total_tournaments} ({total_progress}%) | {remaining} left"
)
except Exception as e:
print(
f"❌ Error processing tournament {tournament.get('id', 'unknown')}: {str(e)}"
)
continue
print(f"✅ Completed batch {batch_number}/{total_batches}")
print(
f"\n🎉 Umpire data collection complete! Found {len(umpire_data)} umpires with contact info"
)
# Step 3: Generate CSV
if not umpire_data:
return HttpResponse(
"<h2>No umpire contact information found for the tournaments in this period</h2>"
"<a href='/kingdom/debug/gather-monthly-umpires/'>← Back to form</a>",
content_type="text/html",
)
# Create CSV content
output = io.StringIO()
for umpire in umpire_data:
# Format: CLUB_NAME;LAST_NAME;FIRST_NAME;EMAIL;PHONE
club_name = umpire.get("club_name", "").replace(
";", ","
) # Remove semicolons to avoid CSV issues
# Try to split name into first and last name
full_name = umpire.get("name", "")
name_parts = full_name.split(" ", 1)
if len(name_parts) >= 2:
first_name = name_parts[0]
last_name = " ".join(name_parts[1:])
else:
first_name = full_name
last_name = ""
email = umpire.get("email", "")
phone = umpire.get("phone", "")
# Write line in the specified format
output.write(f"{club_name};{last_name};{first_name};{email};{phone}\n")
csv_content = output.getvalue()
output.close()
# Generate filename with date range
filename = f"umpires_{start_date}_{end_date}.csv"
# Return CSV as download
response = HttpResponse(csv_content, content_type="text/csv; charset=utf-8")
response["Content-Disposition"] = f'attachment; filename="{filename}"'
return response
except Exception as e:
import traceback
error_details = traceback.format_exc()
return HttpResponse(
f"<h2>Error processing request</h2>"
f"<p>Error: {str(e)}</p>"
f"<pre>{error_details}</pre>"
f"<a href='/kingdom/debug/gather-monthly-umpires/'>← Back to form</a>",
content_type="text/html",
)
def get_tournament_umpire_data_api(tournament):
"""
Helper function to get umpire data for a single tournament using the same API endpoint as iOS
Returns dict with umpire info or None if not found/error
"""
try:
tournament_id = tournament.get("id")
if not tournament_id:
return None
# Use the same API endpoint as iOS: /roads/fft/umpire/{tournament_id}/
api_url = f"http://127.0.0.1:8000/roads/fft/umpire/{tournament_id}/"
response = requests.get(api_url, timeout=30)
if response.status_code != 200:
print(
f"❌ Umpire API request failed for tournament {tournament_id}: {response.status_code}"
)
return None
data = response.json()
name = data.get("name", "")
email = data.get("email", "")
phone = data.get("phone", "")
# Skip if no contact info
if not name and not email and not phone:
return None
# Extract club name from tournament data
club_name = (
tournament.get("organisateur", {}).get("nom", "")
if tournament.get("organisateur")
else ""
)
return {
"tournament_id": tournament_id,
"tournament_name": tournament.get("intitule", ""),
"club_name": club_name,
"name": name or "",
"email": email or "",
"phone": phone or "",
}
except Exception as e:
print(
f"Error getting umpire data for tournament {tournament.get('id', 'unknown')}: {str(e)}"
)
return None