from rest_framework import viewsets from rest_framework.response import Response from rest_framework.decorators import api_view, permission_classes from rest_framework import status from rest_framework.exceptions import MethodNotAllowed from rest_framework.permissions import IsAuthenticated from django.conf import settings from django.http import Http404, HttpResponse, JsonResponse from django.db.models import Q from django.core.files.storage import default_storage from django.core.files.base import ContentFile from django.shortcuts import get_object_or_404 from .serializers import ClubSerializer, CourtSerializer, DateIntervalSerializer, DrawLogSerializer, TournamentSerializer, UserSerializer, EventSerializer, RoundSerializer, GroupStageSerializer, MatchSerializer, TeamScoreSerializer, TeamRegistrationSerializer, PlayerRegistrationSerializer, PurchaseSerializer, ShortUserSerializer, FailedApiCallSerializer, LogSerializer, DeviceTokenSerializer, CustomUserSerializer, UnregisteredTeamSerializer, UnregisteredPlayerSerializer, ImageSerializer from tournaments.models import Club, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamScore, TeamRegistration, PlayerRegistration, Court, DateInterval, Purchase, FailedApiCall, Log, DeviceToken, DrawLog, UnregisteredTeam, UnregisteredPlayer, Image from crm.serializers import CRMActivitySerializer, CRMProspectSerializer, CRMEntitySerializer from crm.models import Activity, Prospect, Entity from rest_framework import viewsets from rest_framework.response import Response from rest_framework.decorators import api_view from rest_framework import status from rest_framework.exceptions import MethodNotAllowed from django.http import Http404 from django.db.models import Q from .permissions import IsClubOwner from .utils import check_version_smaller_than_1_1_12, scrape_fft_club_tournaments, scrape_fft_club_tournaments_all_pages, get_umpire_data, scrape_fft_all_tournaments from shared.discord import send_discord_log_message from tournaments.services.payment_service import PaymentService from tournaments.utils.extensions import create_random_filename import stripe import json import pandas as pd import os import logging logger = logging.getLogger(__name__) @api_view(['GET']) def user_by_token(request): serializer = UserSerializer(request.user) return Response(serializer.data, status=status.HTTP_200_OK) class SoftDeleteViewSet(viewsets.ModelViewSet): def destroy(self, request, *args, **kwargs): try: return super().destroy(request, *args, **kwargs) except Http404: return Response(status=status.HTTP_204_NO_CONTENT) class UserViewSet(SoftDeleteViewSet): queryset = CustomUser.objects.all() serializer_class = CustomUserSerializer permission_classes = [] # Users are public whereas the other requests are only for logged users def get_serializer_class(self): # Use UserSerializer for other actions (e.g., create, retrieve) if self.action in ['create', 'retrieve']: return UserSerializer return self.serializer_class class ClubViewSet(SoftDeleteViewSet): queryset = Club.objects.all() serializer_class = ClubSerializer permission_classes = [IsClubOwner] # Clubs are public whereas the other requests are only for logged users def perform_create(self, serializer): serializer.save(creator=self.request.user) class TournamentViewSet(SoftDeleteViewSet): queryset = Tournament.objects.all() serializer_class = TournamentSerializer def get_queryset(self): if self.request.user.is_anonymous: return [] return self.queryset.filter(event__creator=self.request.user) def perform_create(self, serializer): serializer.save() # version check app_version = self.request.headers.get('App-Version') self.warn_if_version_is_too_small(app_version) def warn_if_version_is_too_small(self, version): if check_version_smaller_than_1_1_12(version): message = f'{self.request.user.username} app version is {version}' send_discord_log_message(message) class PurchaseViewSet(SoftDeleteViewSet): queryset = Purchase.objects.all() serializer_class = PurchaseSerializer def get_queryset(self): if self.request.user: return self.queryset.filter(user=self.request.user) return [] def create(self, request, *args, **kwargs): id = request.data.get('id') if Purchase.objects.filter(id=id).exists(): return Response({"detail": "This transaction id is already registered."}, status=status.HTTP_208_ALREADY_REPORTED) serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) def patch(self, request, pk): raise MethodNotAllowed('PATCH') def delete(self, request, pk): raise MethodNotAllowed('DELETE') class EventViewSet(SoftDeleteViewSet): queryset = Event.objects.all() serializer_class = EventSerializer def get_queryset(self): if self.request.user.is_anonymous: return [] # return self.queryset.filter(creator=self.request.user) return self.queryset.filter( Q(creator=self.request.user) ) class RoundViewSet(SoftDeleteViewSet): queryset = Round.objects.all() serializer_class = RoundSerializer def get_queryset(self): tournament_id = self.request.query_params.get('store_id') or self.request.query_params.get('tournament') if tournament_id: return self.queryset.filter(tournament=tournament_id) if self.request.user: return self.queryset.filter(tournament__event__creator=self.request.user) return [] class GroupStageViewSet(SoftDeleteViewSet): queryset = GroupStage.objects.all() serializer_class = GroupStageSerializer def get_queryset(self): tournament_id = self.request.query_params.get('store_id') or self.request.query_params.get('tournament') if tournament_id: return self.queryset.filter(tournament=tournament_id) if self.request.user: return self.queryset.filter(tournament__event__creator=self.request.user) return [] class MatchViewSet(SoftDeleteViewSet): queryset = Match.objects.all() serializer_class = MatchSerializer def get_queryset(self): tournament_id = self.request.query_params.get('store_id') or self.request.query_params.get('tournament') if tournament_id: return self.queryset.filter(Q(group_stage__tournament=tournament_id) | Q(round__tournament=tournament_id)) if self.request.user: return self.queryset.filter(Q(group_stage__tournament__event__creator=self.request.user) | Q(round__tournament__event__creator=self.request.user)) return [] class TeamScoreViewSet(SoftDeleteViewSet): queryset = TeamScore.objects.all() serializer_class = TeamScoreSerializer def get_queryset(self): tournament_id = self.request.query_params.get('store_id') or self.request.query_params.get('tournament') if tournament_id: q = Q(team_registration__tournament=tournament_id) | Q(match__group_stage__tournament=tournament_id) | Q(match__round__tournament=tournament_id) return self.queryset.filter(q) if self.request.user: return self.queryset.filter(team_registration__tournament__event__creator=self.request.user) return [] class TeamRegistrationViewSet(SoftDeleteViewSet): queryset = TeamRegistration.objects.all() serializer_class = TeamRegistrationSerializer def get_queryset(self): tournament_id = self.request.query_params.get('store_id') or self.request.query_params.get('tournament') if tournament_id: return self.queryset.filter(tournament=tournament_id) if self.request.user: return self.queryset.filter(tournament__event__creator=self.request.user) return [] class PlayerRegistrationViewSet(SoftDeleteViewSet): queryset = PlayerRegistration.objects.all() serializer_class = PlayerRegistrationSerializer def get_queryset(self): tournament_id = self.request.query_params.get('store_id') or self.request.query_params.get('tournament') if tournament_id: return self.queryset.filter(team_registration__tournament=tournament_id) if self.request.user: return self.queryset.filter(team_registration__tournament__event__creator=self.request.user) return [] class CourtViewSet(SoftDeleteViewSet): queryset = Court.objects.all() serializer_class = CourtSerializer class DateIntervalViewSet(SoftDeleteViewSet): queryset = DateInterval.objects.all() serializer_class = DateIntervalSerializer def get_queryset(self): if self.request.user.is_anonymous: return [] return self.queryset.filter( Q(event__creator=self.request.user) ) class FailedApiCallViewSet(viewsets.ModelViewSet): queryset = FailedApiCall.objects.all() serializer_class = FailedApiCallSerializer permission_classes = [] # FailedApiCall are public whereas the other requests are only for logged users def get_queryset(self): return [] def perform_create(self, serializer): if self.request.user.is_anonymous == False: serializer.save(user=self.request.user) else: serializer.save() class LogViewSet(viewsets.ModelViewSet): queryset = Log.objects.all() serializer_class = LogSerializer permission_classes = [] # Log are public whereas the other requests are only for logged users def get_queryset(self): return [] def perform_create(self, serializer): if self.request.user.is_anonymous == False: serializer.save(user=self.request.user) else: serializer.save() class DeviceTokenViewSet(viewsets.ModelViewSet): queryset = DeviceToken.objects.all() serializer_class = DeviceTokenSerializer def get_queryset(self): if self.request.user: return self.queryset.filter(user=self.request.user) return [] def create(self, request, *args, **kwargs): value = request.data.get('value') if DeviceToken.objects.filter(value=value).exists(): return Response({"detail": "This device token is already registered."}, status=208) serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) def perform_create(self, serializer): serializer.save(user=self.request.user) class DrawLogViewSet(SoftDeleteViewSet): queryset = DrawLog.objects.all() serializer_class = DrawLogSerializer def get_queryset(self): tournament_id = self.request.query_params.get('tournament') or self.request.query_params.get('tournament') if tournament_id: return self.queryset.filter(tournament=tournament_id) if self.request.user: return self.queryset.filter(tournament__event__creator=self.request.user) return [] class UnregisteredTeamViewSet(SoftDeleteViewSet): queryset = UnregisteredTeam.objects.all() serializer_class = UnregisteredTeamSerializer def get_queryset(self): tournament_id = self.request.query_params.get('tournament') if tournament_id: return self.queryset.filter(tournament=tournament_id) if self.request.user: return self.queryset.filter(tournament__event__creator=self.request.user) return [] class UnregisteredPlayerViewSet(SoftDeleteViewSet): queryset = UnregisteredPlayer.objects.all() serializer_class = UnregisteredPlayerSerializer def get_queryset(self): tournament_id = self.request.query_params.get('tournament') if tournament_id: return self.queryset.filter(unregistered_team__tournament=tournament_id) if self.request.user: return self.queryset.filter(unregistered_team__tournament__event__creator=self.request.user) return [] class ShortUserViewSet(viewsets.ModelViewSet): queryset = CustomUser.objects.all() serializer_class = ShortUserSerializer permission_classes = [] # Users are public whereas the other requests are only for logged users def get_queryset(self): return self.request.user.agents class ImageViewSet(viewsets.ModelViewSet): """ Viewset for handling event image uploads and retrieval. This allows umpires/organizers to upload images for events from the iOS app, which can then be displayed on the event pages. """ serializer_class = ImageSerializer queryset = Image.objects.all() def get_queryset(self): queryset = Image.objects.all() # Filter by event event_id = self.request.query_params.get('event_id') image_type = self.request.query_params.get('image_type') if event_id: queryset = queryset.filter(event_id=event_id) if image_type: queryset = queryset.filter(image_type=image_type) return queryset def perform_create(self, serializer): serializer.save() @api_view(['POST']) @permission_classes([IsAuthenticated]) def process_refund(request, team_registration_id): try: # Verify the user is the tournament umpire team_registration = get_object_or_404(TeamRegistration, id=team_registration_id) if request.user != team_registration.tournament.event.creator: return Response({ 'success': False, 'message': "Vous n'êtes pas autorisé à effectuer ce remboursement" }, status=403) payment_service = PaymentService(request) players_serializer = PlayerRegistrationSerializer(team_registration.players_sorted_by_rank, many=True) success, message, refund = payment_service.process_refund(team_registration_id, force_refund=True) return Response({ 'success': success, 'message': message, 'players': players_serializer.data }) except Exception as e: return Response({ 'success': False, 'message': str(e) }, status=400) @api_view(['POST']) @permission_classes([IsAuthenticated]) def xls_to_csv(request): # Check if the request has a file if 'file' in request.FILES: uploaded_file = request.FILES['file'] # Save the uploaded file directory = 'tmp/csv/' file_path = os.path.join(directory, uploaded_file.name) file_name = default_storage.save(file_path, ContentFile(uploaded_file.read())) logger.info(f'file saved at {file_name}') full_path = default_storage.path(file_name) logger.info(f'full_path = {full_path}') # Check available sheets and look for 'inscriptions' xls = pd.ExcelFile(full_path) sheet_names = xls.sheet_names # Determine which sheet to use target_sheet = 0 # Default to first sheet if 'inscriptions' in [name.lower() for name in sheet_names]: for i, name in enumerate(sheet_names): if name.lower() == 'inscriptions': target_sheet = i # or use the name directly: target_sheet = name break # Convert to csv and save data_xls = pd.read_excel(full_path, sheet_name=target_sheet, index_col=None) csv_file_name = create_random_filename('players', 'csv') output_path = os.path.join(directory, csv_file_name) full_output_path = default_storage.path(output_path) data_xls.to_csv(full_output_path, sep=';', index=False, encoding='utf-8') # Send the processed file back with default_storage.open(full_output_path, 'rb') as file: response = HttpResponse(file.read(), content_type='application/octet-stream') response['Content-Disposition'] = f'attachment; filename="players.csv"' # Clean up: delete both files default_storage.delete(file_path) default_storage.delete(output_path) return response else: return HttpResponse("No file was uploaded", status=400) @api_view(['GET']) @permission_classes([IsAuthenticated]) def get_tournament_config(request): """Return tournament-related configuration settings""" config = settings.TOURNAMENT_SETTINGS return Response({ 'time_proximity_rules': config['TIME_PROXIMITY_RULES'], 'waiting_list_rules': config['WAITING_LIST_RULES'], 'business_rules': config['BUSINESS_RULES'], 'minimum_response_time': config['MINIMUM_RESPONSE_TIME'] }) @api_view(['GET']) @permission_classes([IsAuthenticated]) def get_payment_config(request): """Return payment-related configuration settings""" return Response({ 'stripe_fee': getattr(settings, 'STRIPE_FEE', 0) }) @api_view(['POST']) @permission_classes([IsAuthenticated]) def create_stripe_connect_account(request): stripe.api_key = settings.STRIPE_SECRET_KEY user = request.user try: # Create a new Standard account account = stripe.Account.create( type='standard', metadata={ 'padelclub_email': user.email, 'platform': 'padelclub' } ) return Response({ 'success': True, 'account_id': account.id, }) except Exception as e: return Response({ 'success': False, 'error': str(e) }, status=400) @api_view(['POST']) @permission_classes([IsAuthenticated]) def create_stripe_account_link(request): """ Create an account link for a Stripe account. Uses HTTPS URLs only - no custom URL schemes. """ stripe.api_key = settings.STRIPE_SECRET_KEY # Parse request data data = json.loads(request.body) account_id = data.get('account_id') if not account_id: return Response({ 'success': False, 'error': 'No Stripe account ID found' }, status=400) try: base_path = f"{request.scheme}://{request.get_host()}" refresh_url = f"{base_path}/stripe-refresh-account-link/" return_url = f"{base_path}/stripe-onboarding-complete/" # Generate the account link URL account_link = stripe.AccountLink.create( account=account_id, refresh_url=refresh_url, return_url=return_url, type='account_onboarding', ) return Response({ 'success': True, 'url': account_link.url, 'account_id': account_id }) except Exception as e: return Response({ 'success': False, 'error': str(e) }, status=400) @api_view(['POST']) @permission_classes([IsAuthenticated]) def validate_stripe_account(request): """ Validate a Stripe account for a tournament. Returns validation status and onboarding URL if needed. """ stripe.api_key = settings.STRIPE_SECRET_KEY # Parse the request body data = json.loads(request.body) account_id = data.get('account_id') if not account_id: return Response({ 'valid': False, 'error': 'No account ID found to validate', 'needs_onboarding': True }, status=200) try: # Validate the account with Stripe account = stripe.Account.retrieve(account_id) # Check account capabilities charges_enabled = account.get('charges_enabled', False) payouts_enabled = account.get('payouts_enabled', False) details_submitted = account.get('details_submitted', False) # Determine if the account is valid and ready is_valid = account.id is not None can_process_payments = charges_enabled and payouts_enabled onboarding_complete = details_submitted needs_onboarding = not (can_process_payments and onboarding_complete) return Response({ 'valid': is_valid, 'can_process_payments': can_process_payments, 'onboarding_complete': onboarding_complete, 'needs_onboarding': needs_onboarding, 'account': { 'id': account.id, 'charges_enabled': charges_enabled, 'payouts_enabled': payouts_enabled, 'details_submitted': details_submitted } }) except stripe.error.PermissionError: # Account doesn't exist or isn't connected to your platform return Response({ 'valid': False, 'error': 'This Stripe account is not connected to your platform or does not exist.', 'needs_onboarding': True, }, status=200) except stripe.error.InvalidRequestError: return Response({ 'valid': False, 'error': 'Invalid account ID format', 'needs_onboarding': True, }, status=200) except Exception as e: return Response({ 'valid': False, 'error': f'Unexpected error: {str(e)}', 'needs_onboarding': True, }, status=200) @api_view(['GET']) @permission_classes([IsAuthenticated]) def is_granted_unlimited_access(request): can_create = False if request.user and request.user.is_anonymous == False and request.user.owners: for owner in request.user.owners.all(): purchases = Purchase.objects.filter(user=owner,product_id='app.padelclub.tournament.subscription.unlimited') for purchase in purchases: if purchase.is_active(): can_create = True return JsonResponse({'can_create': can_create}, status=status.HTTP_200_OK) @api_view(['GET', 'POST']) @permission_classes([]) def get_fft_club_tournaments(request): """ API endpoint to get tournaments for a specific club Handles pagination automatically to get all results """ try: if request.method == 'POST': data = request.data else: data = request.GET club_code = data.get('club_code', '62130180') club_name = data.get('club_name', 'TENNIS SPORTING CLUB DE CASSIS') start_date = data.get('start_date') end_date = data.get('end_date') paginate = data.get('paginate', 'true').lower() == 'true' if paginate: # Get all pages automatically (matching Swift behavior) result = scrape_fft_club_tournaments_all_pages( club_code=club_code, club_name=club_name, start_date=start_date, end_date=end_date ) else: # Get single page page = int(data.get('page', 0)) result = scrape_fft_club_tournaments( club_code=club_code, club_name=club_name, start_date=start_date, end_date=end_date, page=page ) if result: return JsonResponse({ 'success': True, 'tournaments': result.get('tournaments', []), 'total_results': result.get('total_results', 0), 'current_count': result.get('current_count', 0), 'pages_scraped': result.get('pages_scraped', 1), 'message': f'Successfully scraped {len(result.get("tournaments", []))} tournaments' }, status=status.HTTP_200_OK) else: return JsonResponse({ 'success': False, 'tournaments': [], 'total_results': 0, 'current_count': 0, 'message': 'Failed to scrape club tournaments' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) except Exception as e: logger.error(f"Error in get_fft_club_tournaments endpoint: {e}") return JsonResponse({ 'success': False, 'tournaments': [], 'message': f'Unexpected error: {str(e)}' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @api_view(['GET']) @permission_classes([]) def get_fft_umpire_data(request, tournament_id): """ API endpoint to get umpire data for a specific tournament Returns data that can be used to populate japPhoneNumber field """ try: name, email, phone = get_umpire_data(tournament_id) return JsonResponse({ 'success': True, 'umpire': { 'name': name, 'email': email, 'phone': phone }, 'japPhoneNumber': phone, # Direct field for updating FederalTournament 'message': 'Umpire data retrieved successfully' }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"Error in get_fft_umpire_data endpoint: {e}") return JsonResponse({ 'success': False, 'umpire': None, 'japPhoneNumber': None, 'message': f'Unexpected error: {str(e)}' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @api_view(['GET', 'POST']) @permission_classes([]) def get_fft_all_tournaments(request): """ API endpoint to get all tournaments with filters """ try: if request.method == 'POST': data = request.data else: data = request.GET # Extract parameters sorting_option = data.get('sorting_option', 'dateDebut+asc') page = int(data.get('page', 0)) start_date = data.get('start_date') end_date = data.get('end_date') city = data.get('city', '') distance = float(data.get('distance', 15)) categories = data.getlist('categories') if hasattr(data, 'getlist') else data.get('categories', []) levels = data.getlist('levels') if hasattr(data, 'getlist') else data.get('levels', []) lat = data.get('lat') lng = data.get('lng') ages = data.getlist('ages') if hasattr(data, 'getlist') else data.get('ages', []) tournament_types = data.getlist('types') if hasattr(data, 'getlist') else data.get('types', []) national_cup = data.get('national_cup', 'false').lower() == 'true' result = scrape_fft_all_tournaments( sorting_option=sorting_option, page=page, start_date=start_date, end_date=end_date, city=city, distance=distance, categories=categories, levels=levels, lat=lat, lng=lng, ages=ages, tournament_types=tournament_types, national_cup=national_cup ) if result: return JsonResponse({ 'success': True, 'tournaments': result.get('tournaments', []), 'total_results': result.get('total_results', 0), 'current_count': result.get('current_count', 0), 'page': page, 'message': f'Successfully scraped {len(result.get("tournaments", []))} tournaments' }, status=status.HTTP_200_OK) else: return JsonResponse({ 'success': False, 'tournaments': [], 'total_results': 0, 'current_count': 0, 'message': 'Failed to scrape all tournaments' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) except Exception as e: logger.error(f"Error in get_fft_all_tournaments endpoint: {e}") return JsonResponse({ 'success': False, 'tournaments': [], 'message': f'Unexpected error: {str(e)}' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @api_view(['GET', 'POST']) @permission_classes([]) def get_fft_federal_clubs(request): """ API endpoint to get federal clubs """ try: if request.method == 'POST': data = request.data else: data = request.GET country = data.get('country', 'fr') city = data.get('city', '') radius = float(data.get('radius', 15)) latitude = data.get('latitude') longitude = data.get('longitude') if latitude: latitude = float(latitude) if longitude: longitude = float(longitude) result = scrape_federal_clubs( country=country, city=city, radius=radius, latitude=latitude, longitude=longitude ) if result: return JsonResponse({ 'success': True, 'clubs': result, 'message': 'Federal clubs retrieved successfully' }, status=status.HTTP_200_OK) else: return JsonResponse({ 'success': False, 'clubs': [], 'message': 'Failed to retrieve federal clubs' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) except Exception as e: logger.error(f"Error in get_fft_federal_clubs endpoint: {e}") return JsonResponse({ 'success': False, 'clubs': [], 'message': f'Unexpected error: {str(e)}' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @api_view(['GET', 'POST']) @permission_classes([]) def get_fft_club_tournaments_with_umpire_data(request): """ Combined endpoint that gets club tournaments and enriches them with umpire data This matches the complete workflow from your Swift code """ try: if request.method == 'POST': data = request.data else: data = request.GET club_code = data.get('club_code', '62130180') club_name = data.get('club_name', 'TENNIS SPORTING CLUB DE CASSIS') start_date = data.get('start_date') end_date = data.get('end_date') include_umpire_data = data.get('include_umpire_data', 'false').lower() == 'true' # Get all tournaments for the club result = scrape_fft_club_tournaments_all_pages( club_code=club_code, club_name=club_name, start_date=start_date, end_date=end_date ) if not result: return JsonResponse({ 'success': False, 'tournaments': [], 'message': 'Failed to scrape club tournaments' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) tournaments = result.get('tournaments', []) # Enrich with umpire data if requested if include_umpire_data: logger.info(f"Enriching {len(tournaments)} tournaments with umpire data...") for tournament in tournaments: try: tournament_id = tournament.get('id') if tournament_id: name, email, phone = get_umpire_data(tournament_id) tournament['japPhoneNumber'] = phone # Also update jugeArbitre if we got more data if name and not tournament.get('jugeArbitre'): tournament['jugeArbitre'] = { 'nom': name.split(' ')[-1] if name else None, 'prenom': ' '.join(name.split(' ')[:-1]) if name and ' ' in name else name } # Small delay to avoid rate limiting time.sleep(0.5) except Exception as e: logger.warning(f"Failed to get umpire data for tournament {tournament_id}: {e}") continue return JsonResponse({ 'success': True, 'tournaments': tournaments, 'total_results': result.get('total_results', 0), 'current_count': len(tournaments), 'pages_scraped': result.get('pages_scraped', 1), 'umpire_data_included': include_umpire_data, 'message': f'Successfully scraped {len(tournaments)} tournaments' + (' with umpire data' if include_umpire_data else '') }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"Error in get_fft_club_tournaments_with_umpire_data endpoint: {e}") return JsonResponse({ 'success': False, 'tournaments': [], 'message': f'Unexpected error: {str(e)}' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) ### CRM class CRMActivityViewSet(SoftDeleteViewSet): queryset = Activity.objects.all() serializer_class = CRMActivitySerializer class CRMProspectViewSet(SoftDeleteViewSet): queryset = Prospect.objects.all() serializer_class = CRMProspectSerializer class CRMEntityViewSet(SoftDeleteViewSet): queryset = Entity.objects.all() serializer_class = CRMEntitySerializer