|
|
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
|
|
|
|