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 = "82553537"
default_session_id = "JSESSIONID=D4827C95015A626E3875F0B6F7595118; AWSALB=6MbqGI4p8pOK+7Z1dhU+rBcE2ahiNvCRAaHB+GPKS9y/G7LYVt/d/4ArQMqTmWSUvQNzNZNj8fu02oU2YMC5N0aag93ZlMMdMUvyiPrmrNPX8pg5jRnKrI2t5V3R; AWSALBCORS=6MbqGI4p8pOK+7Z1dhU+rBcE2ahiNvCRAaHB+GPKS9y/G7LYVt/d/4ArQMqTmWSUvQNzNZNj8fu02oU2YMC5N0aag93ZlMMdMUvyiPrmrNPX8pg5jRnKrI2t5V3R; _pcid=%7B%22browserId%22%3A%22mckhos3iasswydjm%22%2C%22_t%22%3A%22mx8qjlef%7Cmhkbm42f%22%7D; tc_cj_v2=%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQPLLNOQRKONOZZZ%5D; tc_cj_v2_cmp=; tc_cj_v2_med=; TCID=; datadome=uGOumlzX7RG4xt8Z53eYpnhe~YuSyRRgqnChCcy2Xx~fQecmm4XptKYUuJgRdpaBrGv4SsFor~jwm2rk9p37Ok6k9tGc~1~ntNWx4fI7WlbPxEfLBkEKOm4Y7WdWyuK5; xtan=-; xtant=1; SSESS7ba44afc36c80c3faa2b8fa87e7742c5=LAtWVNYPtWmJdMeJQ96O5KvPvdevS3dgvj_ipz1ykvQ; pa_vid=%22mckhos3iasswydjm%22; 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("""
Download French Padel Rankings - Debug Tools
๐ Download French Padel Rankings
๐ New Workflow:
1. Use this tool to download and save rankings locally
2. Then use "Enrich Rankings with Licenses" to add license data
3. Process is resumable - you can stop and restart enrichment
๐ Export Format: The CSV will contain: CLUB_NAME;LAST_NAME;FIRST_NAME;EMAIL;PHONE_NUMBER โฑ๏ธ Processing Time: This may take several minutes for large date ranges as it processes tournaments in batches of 100.
๐ Enhanced Rankings Tools
Advanced tools that combine multiple APIs for enriched data.
Enrich Rankings with License Data
Get rankings from one tranche and automatically lookup license information for each player.
๐ก How to use: 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.
๐ฌ API Research Tools
Tools to help discover and explore the FFT API endpoints.
"""
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("""
Player License Lookup - Debug Tools
๐ Player License Lookup
โ Default values loaded!
The form is pre-filled with your provided sessionId, idHomologation, and license ID.
You can modify any field if needed before submitting.
""")
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("""
Search Player by Name - Debug Tools
๐ Search Player by Name
โ Default values loaded!
ID Homologation is pre-filled. Add your session ID and enter the player's name to search.
โน๏ธ How it works:
โข You can search by nom (last name), prenom (first name), or both
โข Partial names should work (e.g., "DUPON" might find "DUPONT")
โข Leave one field empty to search by the other
โข Results will include license numbers and detailed player information
๐ Example Searches
Search by last name only: nom = "MARTIN", prenom = empty
Search by first name only: nom = empty, prenom = "Jean"
Search by both: nom = "DUPONT", prenom = "Pierre"
Partial search: nom = "DUPON" (might find "DUPONT", "DUPONTEL", etc.)
""")
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 = """
Monthly Tournament & Umpire Export - Padel Club Admin
๐ Monthly Tournament & Umpire Export
This tool will gather all tournaments within a specified date range and export umpire contact information to CSV format.
"""
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"
No tournaments found for the period {start_date_formatted} to {end_date_formatted}
"
f"โ Back to form",
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(
"
No umpire contact information found for the tournaments in this period
"
"โ Back to form",
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"
Error processing request
"
f"
Error: {str(e)}
"
f"
{error_details}
"
f"โ Back to form",
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