You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
940 lines
34 KiB
940 lines
34 KiB
from pandas.core.groupby import base
|
|
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 .authentication import HasAPIKey
|
|
|
|
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, ActivitySerializer, ProspectSerializer, EntitySerializer, TournamentSummarySerializer
|
|
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 tournaments.services.email_service import TournamentEmailService
|
|
|
|
from biz.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, scrape_fft_all_tournaments_concurrent, scrape_federal_clubs
|
|
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
|
|
from datetime import datetime
|
|
|
|
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 TournamentSummaryViewSet(SoftDeleteViewSet):
|
|
queryset = Tournament.objects.all()
|
|
serializer_class = TournamentSummarySerializer
|
|
permission_classes = [HasAPIKey]
|
|
|
|
def get_queryset(self):
|
|
if self.request.user.is_anonymous:
|
|
return Tournament.objects.none()
|
|
|
|
queryset = self.queryset.filter(
|
|
Q(event__creator=self.request.user) | Q(related_user=self.request.user)
|
|
).distinct()
|
|
|
|
# Add min_start_date filtering
|
|
min_start_date = self.request.query_params.get('min_start_date')
|
|
if min_start_date:
|
|
try:
|
|
# Parse the date string (assumes ISO format: YYYY-MM-DD)
|
|
min_date = datetime.fromisoformat(min_start_date).date()
|
|
queryset = queryset.filter(start_date__gte=min_date)
|
|
except (ValueError, TypeError):
|
|
# If date parsing fails, ignore the filter
|
|
pass
|
|
|
|
return queryset
|
|
|
|
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(
|
|
Q(event__creator=self.request.user))
|
|
|
|
return self.queryset.filter(
|
|
Q(event__creator=self.request.user) | Q(related_user=self.request.user)
|
|
).distinct()
|
|
|
|
|
|
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 SupervisorViewSet(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.supervisors
|
|
|
|
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:
|
|
# Force HTTPS for production Stripe calls
|
|
if hasattr(settings, 'STRIPE_MODE') and settings.STRIPE_MODE == 'live':
|
|
base_path = f"https://{request.get_host()}"
|
|
else:
|
|
base_path = f"{request.scheme}://{request.get_host()}"
|
|
|
|
# print("create_stripe_account_link", base_path)
|
|
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(['POST'])
|
|
@permission_classes([IsAuthenticated])
|
|
def resend_payment_email(request, team_registration_id):
|
|
"""
|
|
Resend the registration confirmation email (which includes payment info/link)
|
|
"""
|
|
try:
|
|
team_registration = TeamRegistration.objects.get(id=team_registration_id)
|
|
tournament = team_registration.tournament
|
|
|
|
TournamentEmailService.send_registration_confirmation(
|
|
request,
|
|
tournament,
|
|
team_registration,
|
|
waiting_list_position=-1,
|
|
force_send=True
|
|
)
|
|
|
|
return Response({
|
|
'success': True,
|
|
'message': 'Email de paiement renvoyé'
|
|
})
|
|
|
|
except TeamRegistration.DoesNotExist:
|
|
return Response({'error': 'Team not found'}, status=status.HTTP_404_NOT_FOUND)
|
|
except Exception as e:
|
|
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
|
|
@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.organising_for:
|
|
for owner in request.user.organising_for.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({
|
|
'name': name,
|
|
'email': email,
|
|
'phone': phone
|
|
}, 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 tournaments with smart pagination:
|
|
- page=0: Returns first page + metadata about total pages
|
|
- page>0: Returns all remaining pages concurrently
|
|
"""
|
|
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 = int(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'
|
|
max_workers = int(data.get('max_workers', 5))
|
|
|
|
if page == 0:
|
|
# Handle first page individually
|
|
result = scrape_fft_all_tournaments(
|
|
sorting_option=sorting_option,
|
|
page=0,
|
|
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:
|
|
tournaments = result.get('tournaments', [])
|
|
total_results = result.get('total_results', 0)
|
|
results_per_page = len(tournaments)
|
|
|
|
# Calculate total pages
|
|
if results_per_page > 0:
|
|
total_pages = (total_results + results_per_page - 1) // results_per_page
|
|
else:
|
|
total_pages = 1
|
|
|
|
return JsonResponse({
|
|
'success': True,
|
|
'tournaments': tournaments,
|
|
'total_results': total_results,
|
|
'current_count': len(tournaments),
|
|
'page': 0,
|
|
'total_pages': total_pages,
|
|
'has_more_pages': total_pages > 1,
|
|
'message': f'Successfully scraped page 0: {len(tournaments)} tournaments. Total: {total_results} across {total_pages} pages.'
|
|
}, status=status.HTTP_200_OK)
|
|
else:
|
|
return JsonResponse({
|
|
'success': False,
|
|
'tournaments': [],
|
|
'total_results': 0,
|
|
'current_count': 0,
|
|
'page': 0,
|
|
'total_pages': 0,
|
|
'has_more_pages': False,
|
|
'message': 'Failed to scrape first page'
|
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
|
|
else:
|
|
# Handle all remaining pages concurrently
|
|
result = scrape_fft_all_tournaments_concurrent(
|
|
sorting_option=sorting_option,
|
|
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,
|
|
max_workers=max_workers
|
|
)
|
|
|
|
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', 0),
|
|
'message': f'Successfully scraped {result.get("pages_scraped", 0)} remaining pages concurrently: {len(result.get("tournaments", []))} tournaments'
|
|
}, status=status.HTTP_200_OK)
|
|
else:
|
|
return JsonResponse({
|
|
'success': False,
|
|
'tournaments': [],
|
|
'total_results': 0,
|
|
'current_count': 0,
|
|
'pages_scraped': 0,
|
|
'message': 'Failed to scrape remaining pages'
|
|
}, 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 with filters
|
|
"""
|
|
try:
|
|
if request.method == 'POST':
|
|
data = request.data
|
|
else:
|
|
data = request.GET
|
|
|
|
# Extract parameters - matching the Swift query parameters
|
|
country = data.get('country', 'fr')
|
|
city = data.get('city', '')
|
|
radius = float(data.get('radius', 15))
|
|
latitude = data.get('lat')
|
|
longitude = data.get('lng')
|
|
|
|
# Convert latitude and longitude to float if provided
|
|
if latitude:
|
|
latitude = float(latitude)
|
|
if longitude:
|
|
longitude = float(longitude)
|
|
|
|
result = scrape_federal_clubs(
|
|
country=country,
|
|
city=city,
|
|
latitude=latitude,
|
|
longitude=longitude,
|
|
radius=radius
|
|
)
|
|
|
|
if result:
|
|
# Return the result directly as JSON (already in correct format)
|
|
return JsonResponse(result, status=status.HTTP_200_OK)
|
|
else:
|
|
# Return error in expected format
|
|
return JsonResponse({
|
|
"typeRecherche": "clubs",
|
|
"nombreResultat": 0,
|
|
"club_markers": []
|
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in get_fft_federal_clubs endpoint: {e}")
|
|
return JsonResponse({
|
|
"typeRecherche": "clubs",
|
|
"nombreResultat": 0,
|
|
"club_markers": []
|
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
|
|
### biz
|
|
class CRMActivityViewSet(SoftDeleteViewSet):
|
|
queryset = Activity.objects.all()
|
|
serializer_class = ActivitySerializer
|
|
|
|
class CRMProspectViewSet(SoftDeleteViewSet):
|
|
queryset = Prospect.objects.all()
|
|
serializer_class = ProspectSerializer
|
|
|
|
class CRMEntityViewSet(SoftDeleteViewSet):
|
|
queryset = Entity.objects.all()
|
|
serializer_class = EntitySerializer
|
|
|