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.
587 lines
21 KiB
587 lines
21 KiB
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
|
|
|
|
from shared.discord import send_discord_log_message
|
|
|
|
from rest_framework.decorators import permission_classes
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from django.shortcuts import get_object_or_404
|
|
|
|
from tournaments.services.payment_service import PaymentService
|
|
from django.conf import settings
|
|
import stripe
|
|
import json
|
|
import pandas as pd
|
|
from tournaments.utils.extensions import create_random_filename
|
|
from django.core.files.storage import default_storage
|
|
from django.core.files.base import ContentFile
|
|
import os
|
|
from django.http import HttpResponse
|
|
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)
|
|
|
|
|
|
### 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
|
|
|