# Standard library imports import os import csv from django.shortcuts import render, get_object_or_404 from django.http import HttpResponse from django.utils.encoding import force_str from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode from django.urls import reverse from django.views.decorators.csrf import csrf_exempt from django.contrib.admin.views.decorators import staff_member_required from django.core.files.storage import default_storage from django.core.files.base import ContentFile from tournaments.models.device_token import DeviceToken from .models import Court, DateInterval, Club, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamScore, TeamRegistration, PlayerRegistration, Purchase, FailedApiCall from .models import TeamSummon from datetime import datetime, timedelta import time from django.template import loader from datetime import date from django.http import JsonResponse, HttpResponse from django.db.models import Q import json import time import asyncio from datetime import date, datetime, timedelta import csv import zipfile from api.tokens import account_activation_token # Third-party imports from qr_code.qrcode.utils import QRCodeOptions # Django imports from django.shortcuts import render, redirect, get_object_or_404 from django.http import HttpResponse, JsonResponse, Http404 from django.urls import reverse, reverse_lazy from django.utils import timezone from django.utils.encoding import force_str, force_bytes from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode from django.template import loader from django.template.loader import render_to_string from django.contrib import messages from django.contrib.sites.shortcuts import get_current_site from django.contrib.auth import login from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import ( UserCreationForm, SetPasswordForm, ) from django.contrib.auth.views import PasswordResetConfirmView from django.contrib.auth.models import User from django.contrib.auth.tokens import default_token_generator from django.db.models import Q from django.views.decorators.csrf import csrf_exempt from django.core.files.storage import default_storage from django.core.files.base import ContentFile from django.core.mail import EmailMessage from django.views.decorators.csrf import csrf_protect from .services.tournament_registration import TournamentRegistrationService from .services.tournament_unregistration import TournamentUnregistrationService # Local application imports from .models import ( Court, DateInterval, Club, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamScore, TeamRegistration, PlayerRegistration, Purchase, FailedApiCall, TeamSummon, FederalCategory, UnregisteredTeam, UnregisteredPlayer ) from .forms import ( SimpleForm, SimpleCustomUserCreationForm, TournamentRegistrationForm, AddPlayerForm, ProfileUpdateForm, ) from .utils.apns import send_push_notification from .utils.licence_validator import LicenseValidator from .utils.player_search import get_player_name_from_csv from api.tokens import account_activation_token from tournaments.models.device_token import DeviceToken from tournaments.models.player_enums import PlayerDataSource, PlayerSexType from django.views.generic.edit import UpdateView from .forms import CustomPasswordChangeForm def index(request): club_id = request.GET.get('club') future = future_tournaments(club_id) live = live_tournaments(club_id) finished = finished_tournaments(club_id) club = None if club_id: club = get_object_or_404(Club, pk=club_id) return render( request, "tournaments/tournaments.html", { 'future': future[:10], 'live': live[:10], 'ended': finished[:10], 'club': club, } ) def tournaments_query(query, club_id, ascending): queries = [query, Q(is_private=False, is_deleted=False, event__club__isnull=False)] club = None if club_id: club = get_object_or_404(Club, pk=club_id) q_club = Q(event__club=club) queries.append(q_club) sortkey = 'start_date' if not ascending: sortkey = '-start_date' return Tournament.objects.filter(*queries).order_by(sortkey) def finished_tournaments(club_id): ended_tournaments = tournaments_query(Q(end_date__isnull=False), club_id, False) return [t for t in ended_tournaments if t.display_tournament()] def live_tournaments(club_id): tournaments = tournaments_query(Q(end_date__isnull=True), club_id, True) return [t for t in tournaments if t.display_tournament() and t.supposedly_in_progress()] def future_tournaments(club_id): tomorrow = datetime.now().date() + timedelta(days=1) tournaments = tournaments_query(Q(end_date__isnull=True, start_date__gte=tomorrow), club_id, True) return [t for t in tournaments if t.display_tournament()] def tournament_info(request, tournament_id): tournament = get_object_or_404(Tournament, pk=tournament_id) registered_user = None team_registration = None is_captain = False player_register_check = None if request.user.is_authenticated: # Assuming user's licence_id is stored in the user profile (e.g., request.user.licence_id) user_licence_id = request.user.licence_id player_register_check = tournament.player_register_check(user_licence_id) if user_licence_id is not None and player_register_check is None: validator = LicenseValidator(user_licence_id) stripped_license = validator.stripped_license # Check if there is a PlayerRegistration for this user in this tournament registered_user = PlayerRegistration.objects.filter( licence_id__startswith=stripped_license, team_registration__tournament=tournament, team_registration__walk_out=False, ).first() # If the user is registered, retrieve their team registration if registered_user: is_captain = registered_user.captain team_registration = registered_user.team_registration return render(request, 'tournaments/tournament_info.html', { 'tournament': tournament, 'team': team_registration, 'is_captain': is_captain, 'player_register_check': player_register_check }) def tournaments(request): filter = int(request.GET.get('filter')) club_id = request.GET.get('club') title = '' tournaments = [] if filter==0: title = 'À venir' tournaments = future_tournaments(club_id) elif filter==1: title = 'En cours' tournaments = live_tournaments(club_id) elif filter==2: title = 'Terminés' tournaments = finished_tournaments(club_id) return render( request, "tournaments/tournaments_list.html", { 'tournaments': tournaments, 'title': title, 'club': club_id, } ) def clubs(request): all_clubs = Club.objects.all().order_by('name') clubs = [] for club in all_clubs: if club.events_count() > 0: clubs.append(club) return render(request, 'tournaments/clubs.html', { 'clubs': clubs, }) def club(request, club_id): club = get_object_or_404(Club, pk=club_id) return render(request, 'tournaments/summons.html', { 'tournament': tournament, 'team_summons': team_summons, }) def tournament(request, tournament_id): tournament = get_object_or_404(Tournament, pk=tournament_id) round_id = request.GET.get('round') group_stage_id = request.GET.get('group_stage') match_groups = tournament.match_groups(False, group_stage_id, round_id) bracket_rounds = tournament.round_set.filter(parent=None, group_stage_loser_bracket=False).order_by('-index') rounds = list(tournament.round_set.filter(group_stage_loser_bracket=True)) rounds.extend(bracket_rounds) group_stages = sorted(tournament.get_computed_group_stage(), key=lambda s: (s.step, s.index)) if tournament.display_matches() or tournament.display_group_stages(): return render(request, 'tournaments/matches.html', { 'tournament': tournament, 'rounds': rounds, 'group_stages': group_stages, 'match_groups': match_groups, }) else: return tournament_info(request, tournament_id) def tournament_teams(request, tournament_id): tournament = get_object_or_404(Tournament, pk=tournament_id) teams = tournament.teams(True) return render(request, 'tournaments/teams.html', { 'tournament': tournament, 'teams': teams, }) def tournament_summons(request, tournament_id): tournament = get_object_or_404(Tournament, pk=tournament_id) team_summons = tournament.team_summons() return render(request, 'tournaments/summons.html', { 'tournament': tournament, 'team_summons': team_summons, }) def tournament_broadcasted_summons(request, tournament_id): tournament = get_object_or_404(Tournament, pk=tournament_id) return render(request, 'tournaments/broadcast/broadcasted_summons.html', { 'tournament': tournament, 'qr_code_url': qr_code_url(request, tournament_id), 'qr_code_options': qr_code_options(), }) def tournament_summons_json(request, tournament_id): tournament = get_object_or_404(Tournament, pk=tournament_id) team_summons = [summon.to_dict() for summon in tournament.team_summons()] data = json.dumps(team_summons) return HttpResponse(data, content_type='application/json') def tournament_broadcast_home(request, tournament_id): tournament = get_object_or_404(Tournament, pk=tournament_id) return render(request, 'tournaments/broadcast/broadcast.html', { 'tournament': tournament, 'qr_code_url': qr_code_url(request, tournament_id), 'qr_code_options': qr_code_options(), }) def automatic_broadcast(request, tournament_id): tournament = get_object_or_404(Tournament, pk=tournament_id) return render(request, 'tournaments/broadcast/broadcasted_auto.html', { 'tournament': tournament, 'qr_code_url': qr_code_url(request, tournament_id), 'qr_code_options': qr_code_options(), }) def automatic_broadcast_event(request, event_id): event = get_object_or_404(Event, pk=event_id) tournaments = Tournament.objects.filter(event=event) tournament_ids = [str(tournament.id) for tournament in tournaments] return render(request, 'tournaments/broadcast/broadcasted_auto_event.html', { 'tournament_ids': tournament_ids, }) def qr_code_url(request, tournament_id): qr_code_url = reverse('tournament', args=[tournament_id]) return request.build_absolute_uri(qr_code_url) def qr_code_url_with_query(request, club_id): url_with_query = f"{request.build_absolute_uri('/')}?club={str(club_id)}" return url_with_query def qr_code_options(): return QRCodeOptions(size=10, border=4, error_correction='L') def tournament_matches(request, tournament_id): tournament = get_object_or_404(Tournament, pk=tournament_id) return render(request, 'tournaments/broadcast/broadcasted_matches.html', { 'tournament': tournament, 'qr_code_url': qr_code_url(request, tournament_id), 'qr_code_options': qr_code_options(), }) def broadcast_json(request, tournament_id): tournament = get_object_or_404(Tournament, pk=tournament_id) broadcast = tournament.broadcast_content() data = json.dumps(broadcast) return HttpResponse(data, content_type='application/json') def tournament_matches_json(request, tournament_id): tournament = get_object_or_404(Tournament, pk=tournament_id) matches, group_stages = tournament.broadcasted_matches_and_group_stages() live_matches = [match.live_match() for match in matches] data = json.dumps(live_matches, default=vars) return HttpResponse(data, content_type='application/json') def tournament_group_stages(request, tournament_id): tournament = get_object_or_404(Tournament, pk=tournament_id) live_group_stages = list(tournament.live_group_stages()) unique_steps = sorted(set(gs.step for gs in live_group_stages), reverse=True) return render(request, 'tournaments/group_stages.html', { 'tournament': tournament, 'group_stages': live_group_stages, 'unique_steps': unique_steps, }) def tournament_broadcasted_group_stages(request, tournament_id): tournament = get_object_or_404(Tournament, pk=tournament_id) return render(request, 'tournaments/broadcast/broadcasted_group_stages.html', { 'tournament': tournament, 'qr_code_url': qr_code_url(request, tournament_id), 'qr_code_options': qr_code_options(), }) def tournament_live_group_stage_json(request, tournament_id): tournament = get_object_or_404(Tournament, pk=tournament_id) # Get all live group stages and filter for the last running step live_group_stages = [gs.live_group_stages() for gs in tournament.last_group_stage_step()] gs_dicts = [gs.to_dict() for gs in live_group_stages] data = json.dumps(gs_dicts) return HttpResponse(data, content_type='application/json') def tournament_rankings(request, tournament_id): tournament = get_object_or_404(Tournament, pk=tournament_id) rankings = tournament.rankings() return render(request, 'tournaments/rankings.html', { 'tournament': tournament, 'rankings': rankings, }) def tournament_rankings_json(request, tournament_id): tournament = get_object_or_404(Tournament, pk=tournament_id) rankings = tournament.rankings() data = json.dumps(rankings, default=vars) return HttpResponse(data, content_type='application/json') def tournament_broadcast_rankings(request, tournament_id): tournament = get_object_or_404(Tournament, pk=tournament_id) return render(request, 'tournaments/broadcast/broadcasted_rankings.html', { 'tournament': tournament, 'qr_code_url': qr_code_url(request, tournament_id), 'qr_code_options': qr_code_options(), }) def players(request): return render(request, 'tournaments/test.html') def activate(request, uidb64, token): try: uid = force_str(urlsafe_base64_decode(uidb64)) user = CustomUser.objects.get(pk=uid) except(TypeError, ValueError, OverflowError, CustomUser.DoesNotExist): user = None if user is not None and account_activation_token.check_token(user, token): user.is_active = True user.save() login(request, user) next_url = request.GET.get('next', '/') return redirect(next_url) else: return HttpResponse('Le lien est invalide.') def club_broadcast(request, broadcast_code): club = get_object_or_404(Club, broadcast_code=broadcast_code) q_not_deleted = Q(is_deleted=False, event__club=club) tournaments = Tournament.objects.filter(q_not_deleted).order_by('-start_date') return render(request, 'tournaments/broadcast/broadcast_club.html', { 'club': club, 'tournaments': tournaments, }) def club_broadcast_auto(request, broadcast_code): club = get_object_or_404(Club, broadcast_code=broadcast_code) q_not_deleted = Q(is_deleted=False, event__club=club) now = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) recent_time_window = now - timedelta(hours=4) tournaments = Tournament.objects.filter( q_not_deleted, Q(is_private=False), # Exclude private tournaments ( ( Q(end_date__isnull=True) & # Include ongoing tournaments (Q(start_date__gte=now - timedelta(days=3)) & Q(start_date__lt=now + timedelta(days=3))) # Include tournaments that are ongoing and start within 3 days ) | (Q(end_date__isnull=False) & Q(end_date__gte=recent_time_window)) # Include tournaments finished within the last 4 hours ) ) tournament_ids = [str(tournament.id) for tournament in tournaments] #print(tournament_ids) return render(request, 'tournaments/broadcast/broadcasted_auto_event.html', { 'tournament_ids': tournament_ids, 'club_name': club.name, 'qr_code_url': qr_code_url_with_query(request, club.id), 'qr_code_options': qr_code_options(), }) def download(request): return render(request, 'tournaments/download.html') def test_apns(request): token = DeviceToken.objects.first() asyncio.run(send_push_notification(token.value, 'hello!')) return HttpResponse('OK!') def test_websocket(request): return render(request, 'tournaments/test_websocket.html') def terms_of_use(request): return render(request, 'terms_of_use.html') import pandas as pd from .utils.extensions import create_random_filename @csrf_exempt def xls_to_csv(request): if request.method == 'POST': # 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())) # convert to csv and save data_xls = pd.read_excel(file_name, sheet_name=0, index_col=None) csv_file_name = create_random_filename('players', 'csv') output_path = os.path.join(directory, csv_file_name) data_xls.to_csv(output_path, sep=';', index=False, encoding='utf-8') # Send the processed file back with default_storage.open(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) else: return HttpResponse("Only POST requests are allowed", status=405) def simple_form_view(request): if request.method == 'POST': # If this is a POST request, we need to process the form data form = SimpleForm(request.POST) if form.is_valid(): # Process the data in form.cleaned_data name = form.cleaned_data['name'] # Do something with the data (e.g., save to the database) send_email_to_jap_list() # send_email('laurent@staxriver.com', 'Laurent') return HttpResponse(f"Hello, {name}!") # Simple response with name else: form = SimpleForm() # print(request.method) # If this is a GET request, we display an empty form return render(request, 'tournaments/admin/mail_test.html', {'form': form}) def send_email_to_jap_list(): # Open the CSV file with open('tournaments/static/misc/jap-test.csv', 'r') as file: csv_reader = csv.reader(file) def log_error(message): with open('email_errors.log', 'a') as log_file: timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') log_file.write(f"{timestamp}: {message}\n") batch = [] for row in csv_reader: if len(row) >= 4: name = row[2] mail = row[3] batch.append((mail, name)) if len(batch) == 10: for email, recipient_name in batch: try: send_email(email, recipient_name) print(f"Name: {recipient_name}, mail: {email}") except Exception as e: error_message = f"Error sending email to {email}: {str(e)}" log_error(error_message) print(error_message) batch = [] time.sleep(2) else: print(f"Row doesn't have enough elements: {row}") # Send any remaining emails in the last batch for email, recipient_name in batch: try: send_email(email, recipient_name) print(f"Name: {recipient_name}, mail: {email}") except Exception as e: error_message = f"Error sending email to {email}: {str(e)}" log_error(error_message) print(error_message) def send_email(mail, name): if name.lower() not in mail.lower(): name = "" subject = "Tes tournois en toute simplicité avec Padel Club" body = f"Salut {name} !\n\nJe me permets de t'écrire car je suis JAP2 en région PACA et développeur, et je viens de lancer Padel Club, une app iOS qui facilite enfin l'organisation des tournois.\n\nAvec elle, tu peux convoquer rapidement, simuler et programmer tes structures, diffuser tous les résultats à tous les joueurs, et ce depuis un iPhone.\n\nTu peux l'essayer gratuitement pour découvrir tout son potentiel ! Télécharge l'app ici et teste la dès ton prochain tournoi: https://padelclub.app/download/\n\nJe suis disponible pour échanger avec toi par mail ou téléphone au 06 81 59 81 93 et voir ce que tu en penses.\nÀ bientôt j'espère !\n\nRazmig" email = EmailMessage(subject, body, to=[mail]) email.send() def signup(request): next_url = request.GET.get('next', '/') if request.method == 'POST': form = SimpleCustomUserCreationForm(request.POST) if form.is_valid(): user = form.save(commit=False) user.is_active = False user.save() send_verification_email(request, user, next_url) messages.success(request, "Votre compte a été créé ! Veuillez vérifier votre e-mail pour confirmer votre compte.") return redirect(next_url) else: form = SimpleCustomUserCreationForm() response = render(request, 'registration/signup.html', {'form': form}) return response def send_verification_email(request, user, next_url): print('next_url', next_url) current_site = get_current_site(request) mail_subject = 'Activez votre compte Padel Club !' # Prepare the email subject and message message = render_to_string('tournaments/acc_active_email.html', { 'user': user, 'domain': current_site.domain, 'uid': urlsafe_base64_encode(force_bytes(user.pk)), 'token': account_activation_token.make_token(user), 'next': next_url, # Pass next URL to the template }) email = EmailMessage(mail_subject, message, to=[user.email]) email.send() @login_required def profile(request): user = request.user # Get the currently authenticated user # Assuming the authenticated user has a `licence_id` attribute user_licence_id = request.user.licence_id return render(request, 'registration/profile.html', { 'user_name': user.username }) @csrf_protect def register_tournament(request, tournament_id): tournament = get_object_or_404(Tournament, id=tournament_id) service = TournamentRegistrationService(request, tournament) service.initialize_context() print("initialize_context") if request.method == 'POST': service.handle_post_request() else: service.handle_get_request() return render(request, 'register_tournament.html', service.context) @login_required def unregister_tournament(request, tournament_id): tournament = get_object_or_404(Tournament, id=tournament_id) service = TournamentUnregistrationService(request, tournament) if not service.can_unregister(): return redirect('tournament-info', tournament_id=tournament_id) service.process_unregistration() return redirect('tournament-info', tournament_id=tournament_id) class CustomPasswordResetConfirmView(PasswordResetConfirmView): template_name = 'registration/password_reset_confirm.html' # Custom template def get_context_data(self, **kwargs): """ Modify the context to provide custom data to the template. """ context = super().get_context_data(**kwargs) context['custom_message'] = "Veuillez entrer un nouveau mot de passe." return context def form_valid(self, form): """ Custom behavior when the password reset form is valid. """ # Call the parent method to save the new password response = super().form_valid(form) # Additional actions (e.g., logging or sending notifications) can be added here # You can add custom logic after password reset return response def get_user(self, uidb64): """ Override this method to decode the uid and return the corresponding user. """ try: uid = urlsafe_base64_decode(uidb64).decode() user = User.objects.get(pk=uid) return user except (TypeError, ValueError, User.DoesNotExist): raise Http404("User not found") @login_required def my_tournaments(request): user = request.user user_licence_id = request.user.licence_id # If no licence_id, return empty lists if user_licence_id is None: return render(request, 'registration/my_tournaments.html', { 'upcoming_tournaments': [], 'running_tournaments': [], 'ended_tournaments': [], 'user_name': user.username }) # Get all tournaments for the user based on their licence validator = LicenseValidator(user_licence_id) stripped_license = validator.stripped_license def filter_user_tournaments(tournaments): return [t for t in tournaments if t.teamregistration_set.filter( playerregistration__licence_id__startswith=stripped_license, walk_out=False ).exists()] # Get filtered tournaments using the helper function upcoming_tournaments = filter_user_tournaments(future_tournaments(None)) running_tournaments = filter_user_tournaments(live_tournaments(None)) ended_tournaments = filter_user_tournaments(finished_tournaments(None)) return render(request, 'registration/my_tournaments.html', { 'upcoming_tournaments': upcoming_tournaments, 'running_tournaments': running_tournaments, 'ended_tournaments': ended_tournaments, 'user_name': user.username }) class ProfileUpdateView(UpdateView): model = CustomUser form_class = ProfileUpdateForm template_name = 'profile.html' success_url = reverse_lazy('profile') def get_object(self, queryset=None): return self.request.user def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['password_change_form'] = CustomPasswordChangeForm(user=self.request.user) return context from api.serializers import GroupStageSerializer, MatchSerializer, PlayerRegistrationSerializer, RoundSerializer, TeamRegistrationSerializer, TeamScoreSerializer @staff_member_required def tournament_import_view(request): if request.method == 'POST': zip_file = request.FILES.get('tournament_zip') if zip_file: try: tournament_id = os.path.splitext(zip_file.name)[0] tournament = Tournament.objects.get(id=tournament_id) # Delete existing relationships tournament.round_set.all().delete() tournament.groupstage_set.all().delete() tournament.teamregistration_set.all().delete() with zipfile.ZipFile(zip_file) as z: # First, process rounds rounds_data = get_file_data(z, f"{tournament_id}/rounds.json") if rounds_data: # First pass: Create rounds without parent relationships rounds_without_parent = [] for item in rounds_data: item['tournament'] = tournament.id # Temporarily remove parent field parent = item.pop('parent', None) rounds_without_parent.append({'data': item, 'parent': parent}) serializer = RoundSerializer(data=[item['data'] for item in rounds_without_parent], many=True) serializer.is_valid(raise_exception=True) created_rounds = serializer.save() # Create a mapping of round IDs round_mapping = {round_obj.id: round_obj for round_obj in created_rounds} # Second pass: Update parent relationships for round_obj, original_data in zip(created_rounds, rounds_without_parent): if original_data['parent']: round_obj.parent = round_mapping.get(original_data['parent']) round_obj.save() # Then process all other files serializer_mapping = { 'group-stages.json': GroupStageSerializer, 'team-registrations.json': TeamRegistrationSerializer, 'matches.json': MatchSerializer, 'player-registrations.json': PlayerRegistrationSerializer, 'team-scores.json': TeamScoreSerializer } # Process each remaining file for filename, serializer_class in serializer_mapping.items(): process_file(z, filename, tournament_id, tournament, serializer_class) return JsonResponse({'status': 'success'}) except Exception as e: return JsonResponse({'status': 'error', 'message': str(e)}) else: return render(request, 'tournaments/admin/tournament_cleaner.html') def process_file(zip_file, filename, tournament_id, tournament, serializer_class): """Helper function to process individual files""" try: file_path = f"{tournament_id}/{filename}" json_data = get_file_data(zip_file, file_path) if json_data: # Add tournament to each item for item in json_data: item['tournament'] = tournament.id serializer = serializer_class(data=json_data, many=True) serializer.is_valid(raise_exception=True) serializer.save() except Exception as e: print(f"Error processing {filename}: {str(e)}") def get_file_data(zip_file, file_path): """Helper function to read and parse JSON data from zip file""" try: file_content = zip_file.read(file_path) return json.loads(file_content) except KeyError: print(f"File not found: {file_path}") return None except json.JSONDecodeError as e: print(f"JSON Error for {file_path}: {str(e)}") raise Exception(f"Invalid JSON in file {file_path}") def team_details(request, tournament_id, team_id): # First check if team_id is None or invalid if team_id is None or team_id == 'None': # Redirect to tournament page or show an error return redirect('tournament-info', tournament_id=tournament_id) tournament = get_object_or_404(Tournament, id=tournament_id) try: team = get_object_or_404(TeamRegistration, id=team_id) except (ValueError, ValidationError): # Handle invalid UUID return redirect('tournament-info', tournament_id=tournament_id) # Get all matches for this team all_matches = team.get_matches() return render(request, 'tournaments/team_details.html', { 'tournament': tournament, 'team': team, 'matches': all_matches, 'debug': False # Set to False in production })