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 = 'F' default_id_homologation = "82546485" default_session_id = "JSESSIONID=CEC70DF4428E76E1FD1BFE5C66904708; AWSALB=omN79AoahQc27iH5vvO14U7ZrjH30faWu5delXAthjiYVq4jzbeXJ0IOmVTGjG6YDoi7Do2uCswhEaO/smz1QG733RpYlsw7ShlFV/X2aLn2L7/DZ5KUBA/8LPNr; AWSALBCORS=omN79AoahQc27iH5vvO14U7ZrjH30faWu5delXAthjiYVq4jzbeXJ0IOmVTGjG6YDoi7Do2uCswhEaO/smz1QG733RpYlsw7ShlFV/X2aLn2L7/DZ5KUBA/8LPNr; incap_ses_2223_2712217=g6xvVwmOBh66wpenPa/ZHpN2ZmgAAAAAcmuXPCKJ1/mEqKuQEXJS2Q==; tc_cj_v2=m_iZZZ%22**%22%27%20ZZZKQNRSMQNLMRQLZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQOKMQKQONJKOZZZ%5D777_rn_lh%5BfyfcheZZZ222H%2B%7B%7E%20%27-%20%21%20-%20%29%7D%20H%7D*%28ZZZKQOKMQKRLNNPMZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQOKNRJOLLQOJZZZ%5D777_rn_lh%5BfyfcheZZZ%2F%20%290%2BH%2C0%200%20G%24%2FH%29%20%2FZZZKQOKOLRLNNQJLZZZ%5D; tc_cj_v2_cmp=; tc_cj_v2_med=; SSESS7ba44afc36c80c3faa2b8fa87e7742c5=4-IzUXNKXq_BQFMLjjivcLW14OXgk3lLPl18WYgSmU0; xtan=-; xtant=1; pa_vid=%22mckhos3iasswydjm%22; datadome=oi7wKIO2uaUDCcpICiRO1_hEYcwyQWDVbXyNCSkAmr315~8pnPcuXWKfvNEEz~jKcoORIOThSRe~AxoRRrPLUsr0miWm7AdAcy~_3hABc1ZWfRt~SKGa_uhyqiE0Hzfj; _pcid=%7B%22browserId%22%3A%22mckhos3iasswydjm%22%2C%22_t%22%3A%22ms8wm9hs%7Cmckhos5s%22%7D; _pctx=%7Bu%7DN4IgrgzgpgThIC4B2YA2qA05owMoBcBDfSREQpAeyRCwgEt8oBJAE0RXSwH18yBbCAA4A7vwCcACwgAffgGMA1pMoQArPAC%2BQA; EA_SESSION_ID=E15E1DD5A23272A1A0CC3B8CEDF56B65; refresh_token=eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIzYjQ2ODk1ZC0zN2EzLTQzM2QtYmQ1My01N2QxZTM1YTI3NzkifQ.eyJleHAiOjE3NTY1NTM5MjgsImlhdCI6MTc1MTM3MjAwNCwianRpIjoiYzJiNzA3N2UtZmQ5MS00ZGM4LWI4ZDEtMzA2MDdkYjk5MTgxIiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5mZnQuZnIvcmVhbG1zL2Nvbm5lY3QiLCJhdWQiOiJodHRwczovL2xvZ2luLmZmdC5mci9yZWFsbXMvY29ubmVjdCIsInN1YiI6IjI3ZDQ5NzRjLTEwZWUtNDNlOC1iOTczLWUyMzc2MDM1ZTE0MSIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJtZWEtc2l0ZSIsInNpZCI6IjM5NTZjMzZlLTczMWItNDJkNy1iNjI2LTE2MGViY2Y2YTY2ZiIsInNjb3BlIjoib3BlbmlkIHJvbGVzIHJlYWQ6bGljZW5jZSByZWFkOmlkZW50aXR5IGVtYWlsIHByb2ZpbGUifQ.e6v5vlen985vSFJhrgMQTTB3fzzsnwugPfXKoyib1QSIBZ9kC47h1cYwcpam0VmZ9vRD_y0hVC14jDvBR6d1dQ; user_login=10000984864; user_token=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJRaTV3bWx2bTNuX2p1YW4tSTl1dHo3UGZRLU1tVVlvektwSExhbm9lTXI4In0.eyJleHAiOjE3NTEzNzIzMDQsImlhdCI6MTc1MTM3MjAwNCwianRpIjoiMzEzMGVhODUtNjFjNC00OGRjLWFlNGMtZTIwZmZhYTU3YTlhIiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5mZnQuZnIvcmVhbG1zL2Nvbm5lY3QiLCJhdWQiOlsiZmVkLWFwaSIsImFjY291bnQiXSwic3ViIjoiMjdkNDk3NGMtMTBlZS00M2U4LWI5NzMtZTIzNzYwMzVlMTQxIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoibWVhLXNpdGUiLCJzaWQiOiIzOTU2YzM2ZS03MzFiLTQyZDctYjYyNi0xNjBlYmNmNmE2NmYiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1jb25uZWN0Il19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCJdfX0sInNjb3BlIjoib3BlbmlkIHJlYWQ6bGljZW5jZSByZWFkOmlkZW50aXR5IGVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaWRDcm0iOiIxMDAwMDk4NDg2NCIsIm5hbWUiOiJSYXptaWcgU0FSS0lTU0lBTiIsInByZWZlcnJlZF91c2VybmFtZSI6InJhem1vZyIsImdpdmVuX25hbWUiOiJSYXptaWciLCJzZXNzaW9uX3N0YXRlIjoiMzk1NmMzNmUtNzMxYi00MmQ3LWI2MjYtMTYwZWJjZjZhNjZmIiwibG9jYWxlIjoiZnIiLCJmYW1pbHlfbmFtZSI6IlNBUktJU1NJQU4iLCJlbWFpbCI6InJhem1pZy5zYXJraXNzaWFuQGdtYWlsLmNvbSJ9.VSjG2htaUMt_acrqL3VcAjVMhAno9q0vdb7LTzw8UVbjIiDLzhR5msRxI8h8gSJ38kFLaa7f_SFGLIsRCSdcmhYRd2zKIrcPE-QFKbsPnH69xN2i3giMMiYEy3hj__IIyijt9z3W4KXeQdwUrlXPxprlXQ2sYTlZG63HlCGq1iI3Go9eXFmNDNM6p1jBypXcHEvJr6HwNcRdn6ZGfZ9LLMZ2aMEJAhDqL2CLrFrOZkGQpFz7ITUi_DVJAqh5DmTK1JqPswcOjhuZhDT7qWNfIleV-L7XCwvofxBwkSX9ve9l_3COZJXbsMiiRdCSTZtewlFRfgo4IuAu3g06fmJw7g; TCID=; nlbi_2712217=Ok4tKplxIEN+k1gmb9lUTgAAAAA70zbGXpiElrV2qkRjBeXO; visid_incap_2712217=LW/brcN4Rwml/7waoG/rloFBYmgAAAAAQUIPAAAAAAAlHbwlYSPbNS2qq3UBZNK8; TCPID=125629554310878226394; xtvrn=$548419$" def calculate_age_from_birth_date(birth_date_str): """ 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' 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
Saves to data/rankings/ directory for later license enrichment

โ† Back to Debug Tools
''') context = Context({ 'csrf_token': csrf_token }) return HttpResponse(html_template.render(context)) @staff_member_required def debug_tools_page(request): """ Simple debug tools page with download button """ html_content = ''' Debug Tools - Padel Club Admin

๐Ÿ“ Debug Tools

French Padel Rankings Download

Download French padel rankings for men from the FFT API for tranches 1001 to 1222.

๐Ÿ“ฅ Download Rankings (1001-1222)

๐Ÿ”— Enhanced Rankings Tools

Advanced tools that combine multiple APIs for enriched data.

Enrich Rankings with License Data

Get rankings from one tranche and automatically lookup license information for each player.

๐Ÿ”— Enrich Rankings with Licenses

๐Ÿ” Player License Lookup Tools

Use the FFT Beach-Padel API to lookup detailed player information.

Search by Name

Search for players by nom (last name) and/or prenom (first name) to find their license information.

๐Ÿ‘ค Search Player by Name

Lookup by License Number

Lookup detailed information for a single player license number.

๐Ÿ” Single License Lookup

Bulk License Lookup

Lookup multiple player licenses at once (use carefully to avoid overloading the API).

๐Ÿ“‹ Bulk License Lookup
๐Ÿ’ก 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.

๐Ÿงช Test Player Detail APIs ๐Ÿ” Explore FFT API Endpoints


โ† Back to Admin Dashboard
''' 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.
Pre-filled with your provided session ID
Competition/event identifier (pre-filled)
Player license number to lookup (pre-filled)

โ† Back to Debug Tools
''') context = Context({ 'csrf_token': csrf_token, 'default_session_id': default_session_id, 'default_id_homologation': default_id_homologation, 'default_license_id': default_license_id }) 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(''' Bulk License Lookup - Debug Tools

๐Ÿ” Bulk License Lookup

โœ… Default values loaded!
ID Homologation and sample license IDs are pre-filled. Add your session ID and modify license list as needed.
โš ๏ธ Warning: This will make multiple API calls. Use responsibly to avoid overwhelming the FFT servers.
Copy the Cookie header from browser dev tools
Competition/event identifier (pre-filled)
Pre-filled with sample license IDs - modify as needed

โ† Back to Debug Tools
''') 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', '') prenom = request.POST.get('prenom', '') 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
Copy the Cookie header from browser dev tools
Competition/event identifier (pre-filled)
Player's last name
Player's first name

๐Ÿ“‹ 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.)


โ† Back to Debug Tools
''') context = Context({ 'csrf_token': csrf_token, 'default_session_id': default_session_id, 'default_id_homologation': default_id_homologation }) 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', {}) 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): 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 # Get player's gender sexe = player.get('sexe', default_sexe) # Default to 'H' for male # 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) # 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(''' Enrich Rankings with Licenses - Debug Tools

Enrich Rankings with Licenses

Optimized for large datasets: This tool now supports concurrent processing to speed up license lookups.

You can adjust the batch size and number of concurrent workers based on your server's capacity.

{% csrf_token %}
How often to save progress to disk. After processing this many players, the file will be updated with results.
Higher values = faster processing but more server load. Adjust based on server capacity.

Available Ranking Files

{% for file in existing_files %}
{{ file.filename }}
Path: {{ file.path }}
Size: {{ file.size|filesizeformat }}
Modified: {{ file.modified }}
{% empty %}

No ranking files found in the data/rankings directory.

{% endfor %}
โ† Back to Debug Tools
''') context = { 'csrf_token': csrf_token, 'existing_files': existing_files, 'default_session_id': default_session_id, 'default_id_homologation': default_id_homologation, } rendered_html = html_template.render(Context(context)) return HttpResponse(rendered_html)