diff --git a/padelclub_backend/urls.py b/padelclub_backend/urls.py
index abb883a..d56034b 100644
--- a/padelclub_backend/urls.py
+++ b/padelclub_backend/urls.py
@@ -17,6 +17,7 @@ from django.contrib import admin
from django.urls import include, path
from django.conf import settings
from django.conf.urls.static import static
+from tournaments.admin_utils import download_french_padel_rankings, debug_tools_page, test_player_details_apis, explore_fft_api_endpoints, get_player_license_info, bulk_license_lookup, search_player_by_name, enrich_rankings_with_licenses
urlpatterns = [
@@ -24,6 +25,14 @@ urlpatterns = [
path('shop/', include('shop.urls')),
# path("crm/", include("crm.urls")),
path('roads/', include("api.urls")),
+ path('kingdom/debug/', debug_tools_page, name='debug_tools'),
+ path('kingdom/debug/enrich-rankings-with-licenses/', enrich_rankings_with_licenses, name='enrich_rankings_with_licenses'),
+ path('kingdom/debug/search-player-by-name/', search_player_by_name, name='search_player_by_name'),
+ path('kingdom/debug/download-french-padel-rankings/', download_french_padel_rankings, name='download_french_padel_rankings'),
+ path('kingdom/debug/test-player-apis/', test_player_details_apis, name='test_player_apis'),
+ path('kingdom/debug/player-license-lookup/', get_player_license_info, name='player_license_lookup'),
+ path('kingdom/debug/bulk-license-lookup/', bulk_license_lookup, name='bulk_license_lookup'),
+ path('kingdom/debug/explore-api-endpoints/', explore_fft_api_endpoints, name='explore_api_endpoints'),
path('kingdom/', admin.site.urls),
path('api-auth/', include('rest_framework.urls')),
path('dj-auth/', include('django.contrib.auth.urls')),
diff --git a/tournaments/admin_utils.py b/tournaments/admin_utils.py
new file mode 100644
index 0000000..43e8675
--- /dev/null
+++ b/tournaments/admin_utils.py
@@ -0,0 +1,1739 @@
+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
+
+default_sexe = "H"
+default_id_homologation = "82546485"
+default_session_id = "JSESSIONID=CFE4A886CB92764066D1EC920EF9AB1C; AWSALB=c4OHU4Lw6YW6QRsoD1ktcfSgEExZZil/dDetMp3teMKtQ7RlA3VIM8ZHnJH8K3GYMoPu0M61xLjZo64rBNzDEO5tISKEYIX79baengXCKXnaqdqNqHJ7cSPeon+g; AWSALBCORS=c4OHU4Lw6YW6QRsoD1ktcfSgEExZZil/dDetMp3teMKtQ7RlA3VIM8ZHnJH8K3GYMoPu0M61xLjZo64rBNzDEO5tISKEYIX79baengXCKXnaqdqNqHJ7cSPeon+g; tc_cj_v2=m_iZZZ%22**%22%27%20ZZZKQNRSMQNLMRQLZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQOKMQKQONJKOZZZ%5D777_rn_lh%5BfyfcheZZZ222H%2B%7B%7E%20%27-%20%21%20-%20%29%7D%20H%7D*%28ZZZKQOKMQKRLNNPMZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQOKNLQOPMLSMZZZ%5D; tc_cj_v2_cmp=; tc_cj_v2_med=; incap_ses_2223_2712217=/I1fA56LxWI8KbyfPa/ZHpmpZGgAAAAAoadzHEsPdo9W59AkhfXcmQ==; xtan=-; xtant=1; pa_vid=%22mckhos3iasswydjm%22; datadome=oi7wKIO2uaUDCcpICiRO1_hEYcwyQWDVbXyNCSkAmr315~8pnPcuXWKfvNEEz~jKcoORIOThSRe~AxoRRrPLUsr0miWm7AdAcy~_3hABc1ZWfRt~SKGa_uhyqiE0Hzfj; _pcid=%7B%22browserId%22%3A%22mckhos3iasswydjm%22%2C%22_t%22%3A%22ms8wm9hs%7Cmckhos5s%22%7D; _pctx=%7Bu%7DN4IgrgzgpgThIC4B2YA2qA05owMoBcBDfSREQpAeyRCwgEt8oBJAE0RXSwH18yBbCAA4A7vwCcACwgAffgGMA1pMoQArPAC%2BQA; EA_SESSION_ID=E15E1DD5A23272A1A0CC3B8CEDF56B65; refresh_token=eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIzYjQ2ODk1ZC0zN2EzLTQzM2QtYmQ1My01N2QxZTM1YTI3NzkifQ.eyJleHAiOjE3NTY1NTM5MjgsImlhdCI6MTc1MTM3MjAwNCwianRpIjoiYzJiNzA3N2UtZmQ5MS00ZGM4LWI4ZDEtMzA2MDdkYjk5MTgxIiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5mZnQuZnIvcmVhbG1zL2Nvbm5lY3QiLCJhdWQiOiJodHRwczovL2xvZ2luLmZmdC5mci9yZWFsbXMvY29ubmVjdCIsInN1YiI6IjI3ZDQ5NzRjLTEwZWUtNDNlOC1iOTczLWUyMzc2MDM1ZTE0MSIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJtZWEtc2l0ZSIsInNpZCI6IjM5NTZjMzZlLTczMWItNDJkNy1iNjI2LTE2MGViY2Y2YTY2ZiIsInNjb3BlIjoib3BlbmlkIHJvbGVzIHJlYWQ6bGljZW5jZSByZWFkOmlkZW50aXR5IGVtYWlsIHByb2ZpbGUifQ.e6v5vlen985vSFJhrgMQTTB3fzzsnwugPfXKoyib1QSIBZ9kC47h1cYwcpam0VmZ9vRD_y0hVC14jDvBR6d1dQ; user_login=10000984864; user_token=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJRaTV3bWx2bTNuX2p1YW4tSTl1dHo3UGZRLU1tVVlvektwSExhbm9lTXI4In0.eyJleHAiOjE3NTEzNzIzMDQsImlhdCI6MTc1MTM3MjAwNCwianRpIjoiMzEzMGVhODUtNjFjNC00OGRjLWFlNGMtZTIwZmZhYTU3YTlhIiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5mZnQuZnIvcmVhbG1zL2Nvbm5lY3QiLCJhdWQiOlsiZmVkLWFwaSIsImFjY291bnQiXSwic3ViIjoiMjdkNDk3NGMtMTBlZS00M2U4LWI5NzMtZTIzNzYwMzVlMTQxIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoibWVhLXNpdGUiLCJzaWQiOiIzOTU2YzM2ZS03MzFiLTQyZDctYjYyNi0xNjBlYmNmNmE2NmYiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1jb25uZWN0Il19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCJdfX0sInNjb3BlIjoib3BlbmlkIHJlYWQ6bGljZW5jZSByZWFkOmlkZW50aXR5IGVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaWRDcm0iOiIxMDAwMDk4NDg2NCIsIm5hbWUiOiJSYXptaWcgU0FSS0lTU0lBTiIsInByZWZlcnJlZF91c2VybmFtZSI6InJhem1vZyIsImdpdmVuX25hbWUiOiJSYXptaWciLCJzZXNzaW9uX3N0YXRlIjoiMzk1NmMzNmUtNzMxYi00MmQ3LWI2MjYtMTYwZWJjZjZhNjZmIiwibG9jYWxlIjoiZnIiLCJmYW1pbHlfbmFtZSI6IlNBUktJU1NJQU4iLCJlbWFpbCI6InJhem1pZy5zYXJraXNzaWFuQGdtYWlsLmNvbSJ9.VSjG2htaUMt_acrqL3VcAjVMhAno9q0vdb7LTzw8UVbjIiDLzhR5msRxI8h8gSJ38kFLaa7f_SFGLIsRCSdcmhYRd2zKIrcPE-QFKbsPnH69xN2i3giMMiYEy3hj__IIyijt9z3W4KXeQdwUrlXPxprlXQ2sYTlZG63HlCGq1iI3Go9eXFmNDNM6p1jBypXcHEvJr6HwNcRdn6ZGfZ9LLMZ2aMEJAhDqL2CLrFrOZkGQpFz7ITUi_DVJAqh5DmTK1JqPswcOjhuZhDT7qWNfIleV-L7XCwvofxBwkSX9ve9l_3COZJXbsMiiRdCSTZtewlFRfgo4IuAu3g06fmJw7g; TCID=; nlbi_2712217=Ok4tKplxIEN+k1gmb9lUTgAAAAA70zbGXpiElrV2qkRjBeXO; visid_incap_2712217=LW/brcN4Rwml/7waoG/rloFBYmgAAAAAQUIPAAAAAAAlHbwlYSPbNS2qq3UBZNK8; TCPID=125629554310878226394; xtvrn=$548419$"
+
+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_age_sportif):
+ """
+ Find the best matching license from multiple results using ageSportif comparison
+
+ Args:
+ license_results: List of license data from API
+ player_age_sportif: Age from ranking data
+
+ Returns:
+ Tuple of (best_match, match_info)
+ """
+ if not license_results:
+ return None, {"reason": "no_results"}
+
+ if len(license_results) == 1:
+ return license_results[0], {"reason": "single_result", "age_match": "n/a"}
+
+ # If we don't have ageSportif from ranking, take the first match
+ if player_age_sportif is None:
+ return license_results[0], {"reason": "no_age_data", "used_first_result": True}
+
+ best_match = None
+ best_age_diff = float('inf')
+ match_details = []
+
+ for i, license_data in enumerate(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
+ }
+
+ 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:
+ best_age_diff = age_diff
+ best_match = license_data
+
+ match_details.append(match_detail)
+
+ # If no match found with valid age, use first result
+ if best_match is None:
+ best_match = license_results[0]
+ match_info = {
+ "reason": "no_valid_ages",
+ "used_first_result": True,
+ "match_details": match_details
+ }
+ else:
+ match_info = {
+ "reason": "age_matched",
+ "best_age_difference": best_age_diff,
+ "total_candidates": len(license_results),
+ "match_details": match_details
+ }
+
+ return best_match, 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'
+
+ try:
+ # API endpoint and parameters
+ url = "https://tenup.fft.fr/back/public/v1/classements/recherche"
+ headers = {
+ 'Content-Type': 'application/json'
+ }
+
+ all_players = []
+ total_tranches = end_tranche - start_tranche + 1
+ successful_calls = 0
+ failed_calls = 0
+
+ print(f"Starting to fetch tranches {start_tranche} to {end_tranche} ({total_tranches} total)...")
+
+ for tranche in range(start_tranche, end_tranche + 1):
+ try:
+ payload = {
+ "pratique": "padel",
+ "sexe": default_sexe,
+ "tranche": tranche
+ }
+
+ 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
+
+ all_players.extend(json_data['joueurs'])
+ successful_calls += 1
+ print(f"Tranche {tranche}: Found {len(json_data['joueurs'])} players (Total: {len(all_players)})")
+ else:
+ print(f"Tranche {tranche}: No players found")
+
+ else:
+ failed_calls += 1
+ print(f"Tranche {tranche}: HTTP {response.status_code}")
+
+ except requests.exceptions.RequestException as e:
+ failed_calls += 1
+ print(f"Tranche {tranche}: Network error - {str(e)}")
+
+ except Exception as e:
+ failed_calls += 1
+ print(f"Tranche {tranche}: Unexpected error - {str(e)}")
+
+ 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}")
+
+ 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,
+ "total_tranches_requested": total_tranches,
+ "tranche_range": f"{start_tranche}-{end_tranche}",
+ "download_timestamp": datetime.now().isoformat(),
+ "last_enrichment_update": None,
+ "enrichment_progress": {
+ "players_with_licenses": 0,
+ "players_without_licenses": len(all_players),
+ "last_processed_index": -1
+ },
+ "parameters": {
+ "pratique": "padel",
+ "sexe": default_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
+
+ ๐ก 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
+ })
+ 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', '').strip()
+ prenom = request.POST.get('prenom', '').strip()
+
+ 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.)