From 966beb659940e730de945bc58402cb0cd5804ebc Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 28 Jul 2025 16:25:17 +0200 Subject: [PATCH] adds month/year filtering for tournament lists --- tournaments/static/tournaments/css/basics.css | 4 + tournaments/static/tournaments/css/style.css | 1 - .../tournaments/tournaments_list.html | 49 +++++ tournaments/templatetags/tournament_tags.py | 16 ++ tournaments/views.py | 191 ++++++++++++++---- 5 files changed, 217 insertions(+), 44 deletions(-) diff --git a/tournaments/static/tournaments/css/basics.css b/tournaments/static/tournaments/css/basics.css index 19dee98..e7371bf 100644 --- a/tournaments/static/tournaments/css/basics.css +++ b/tournaments/static/tournaments/css/basics.css @@ -60,6 +60,10 @@ margin: 20px 0px; } +.bmargin10 { + margin-bottom: 10px; +} + /* WIDTH PERCENTAGE */ .w15 { diff --git a/tournaments/static/tournaments/css/style.css b/tournaments/static/tournaments/css/style.css index ccd3358..8ab3594 100644 --- a/tournaments/static/tournaments/css/style.css +++ b/tournaments/static/tournaments/css/style.css @@ -1015,7 +1015,6 @@ h-margin { color: white; /* White text on hover */ } .download-button { - margin-right: 6px; color: #fff7ed; padding: 8px 12px; background-color: #1a223a; diff --git a/tournaments/templates/tournaments/tournaments_list.html b/tournaments/templates/tournaments/tournaments_list.html index 8c34d99..7ec61bf 100644 --- a/tournaments/templates/tournaments/tournaments_list.html +++ b/tournaments/templates/tournaments/tournaments_list.html @@ -1,4 +1,5 @@ {% extends 'tournaments/base.html' %} +{% load tournament_tags %} {% block head_title %}{{ head_title }}{% endblock %} {% block first_title %}{{ first_title }}{% endblock %} @@ -7,6 +8,42 @@ {% block content %} {% include 'tournaments/navigation_base.html' %} + + {% if filter == 0 or filter == 2 %} + + {% if available_years %} + +
+ + {% endif %} + + + {% if available_months and selected_year %} + +
+ + {% endif %} + {% endif %} +
{% if tournaments %}
@@ -18,6 +55,18 @@
+ {% elif selected_month and selected_year %} +
+
+

Aucun tournoi trouvé pour {{ month_names|array_lookup:selected_month }} {{ selected_year }}.

+
+
+ {% elif selected_year %} +
+
+

Aucun tournoi trouvé pour l'année {{ selected_year }}.

+
+
{% endif %} {% if first_tournament_prog_url and tournaments %} diff --git a/tournaments/templatetags/tournament_tags.py b/tournaments/templatetags/tournament_tags.py index 072fd07..b3104a7 100644 --- a/tournaments/templatetags/tournament_tags.py +++ b/tournaments/templatetags/tournament_tags.py @@ -5,3 +5,19 @@ register = template.Library() @register.filter def get_player_status(tournament, user): return tournament.get_player_registration_status_by_licence(user) + +@register.filter +def lookup(dictionary, key): + """Template filter to lookup dictionary values by key""" + return dictionary.get(key, '') + +@register.filter +def array_lookup(array, index): + """Template filter to lookup array values by index""" + try: + index = int(index) + if 0 <= index < len(array): + return array[index] + return '' + except (ValueError, TypeError, IndexError): + return '' diff --git a/tournaments/views.py b/tournaments/views.py index bdcb002..e15b6b2 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -1,58 +1,53 @@ # Standard library imports + +from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth import login, get_user_model, logout, update_session_auth_hash +from django.contrib.auth.views import PasswordResetCompleteView, PasswordResetConfirmView +from django.contrib.sites.shortcuts import get_current_site +from django.contrib.auth.decorators import login_required +from django.contrib.admin.views.decorators import staff_member_required +from django.shortcuts import redirect, render, get_object_or_404 +from django.utils import timezone, formats +from django.utils.encoding import force_str, force_bytes +from django.utils.http import urlsafe_base64_decode +from django.http import JsonResponse, HttpResponse, Http404 +from django.urls import reverse, reverse_lazy +from django.conf import settings +from django.db.models import Q +from django.views.generic import View +from django.views.generic.edit import UpdateView +from django.views.decorators.csrf import csrf_protect, csrf_exempt +from django.template.loader import render_to_string +from django.core.mail import EmailMessage +from django.core.exceptions import ValidationError +from django.core.files.storage import default_storage +from django.core.files.base import ContentFile + import os import csv +import locale +import calendar + from api.serializers import GroupStageSerializer, MatchSerializer, PlayerRegistrationSerializer, TeamRegistrationSerializer, TeamScoreSerializer -from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.auth import logout from .utils.extensions import is_not_sqlite_backend import stripe -from django.contrib.auth import update_session_auth_hash -from django.contrib.auth.views import PasswordResetCompleteView -from django.shortcuts import redirect -from django.contrib.auth import login -from django.contrib.auth import get_user_model -from django.shortcuts import render, get_object_or_404 -from django.http import JsonResponse, HttpResponse -from django.utils.encoding import force_str -from django.utils.http import urlsafe_base64_decode -from django.urls import reverse -from django.conf import settings -from django.contrib.admin.views.decorators import staff_member_required -from django.views.generic import View -from django.db.models import Q -from .models import Club, Tournament, CustomUser, Event, Round, Match, TeamScore, TeamRegistration, PlayerRegistration, UserOrigin, Purchase +from .models import Club, Tournament, CustomUser, Event, Round, Match, TeamScore, TeamRegistration, PlayerRegistration, UserOrigin from datetime import datetime, timedelta import time import json import asyncio import zipfile 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 -from django.utils import formats +from tournaments.utils.extensions import create_random_filename from api.tokens import account_activation_token -# Third-party imports from qr_code.qrcode.utils import QRCodeOptions -# Django imports -from django.http import Http404 -from django.urls import reverse_lazy -from django.utils import timezone -from django.utils.encoding import force_bytes -from django.utils.http import urlsafe_base64_encode -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.decorators import login_required -from django.contrib.auth.views import PasswordResetConfirmView -from django.core.mail import EmailMessage -from django.views.decorators.csrf import csrf_protect from .services.tournament_unregistration import TournamentUnregistrationService -from django.core.exceptions import ValidationError + from .forms import ( ProfileUpdateForm, SimpleCustomUserCreationForm, @@ -62,12 +57,11 @@ from .forms import ( ) from .utils.apns import send_push_notification from .utils.licence_validator import LicenseValidator -from django.views.generic.edit import UpdateView + from .forms import CustomPasswordChangeForm from .services.email_service import TournamentEmailService from .services.tournament_registration import RegistrationCartManager from .services.payment_service import PaymentService -from django.views.decorators.csrf import csrf_exempt import logging @@ -171,6 +165,60 @@ def future_tournaments(club_id, limit=None): tournaments = tournaments_query(Q(end_date__isnull=True), club_id, True, limit) return [t for t in tournaments if t.display_tournament() and t.starts_in_the_future()] +def future_tournaments_by_month(club_id, year, month): + """Get future tournaments filtered by year and month""" + + logger.info(f'filter {year} {month}') + tournaments = tournaments_query(Q(end_date__isnull=True), club_id, True) + filtered_tournaments = [] + for t in tournaments: + if (t.display_tournament() and t.starts_in_the_future() and + t.start_date.year == year and t.start_date.month == month + 1): + filtered_tournaments.append(t) + return filtered_tournaments + +def finished_tournaments_by_month(club_id, year, month): + """Get finished tournaments filtered by year and month""" + clean_ended_tournaments = tournaments_query(Q(end_date__isnull=False), club_id, False) + clean_ended_tournaments = [t for t in clean_ended_tournaments if t.display_tournament()] + + one_day_ago = timezone.now() - timedelta(days=1) + ended_tournaments = tournaments_query( + Q(end_date__isnull=True, start_date__lt=one_day_ago), + club_id, + False + ) + ended_tournaments = [t for t in ended_tournaments if t.display_tournament() and t.should_be_over()] + + # Combine both lists + all_tournaments = clean_ended_tournaments + ended_tournaments + + # Filter by year and month + filtered_tournaments = [] + for t in all_tournaments: + if t.start_date.year == year and t.start_date.month == month: + filtered_tournaments.append(t) + + # Sort by start_date descending + filtered_tournaments.sort(key=lambda t: t.start_date, reverse=True) + return filtered_tournaments + +def get_available_years_for_tournaments(club_id, filter_type): + """Get available years for tournaments based on filter type""" + if filter_type == 0: # Future tournaments + tournaments = tournaments_query(Q(end_date__isnull=True), club_id, True) + tournaments = [t for t in tournaments if t.display_tournament() and t.starts_in_the_future()] + elif filter_type == 2: # Finished tournaments + tournaments = finished_tournaments(club_id) + else: + return [] + + years = set() + for t in tournaments: + years.add(t.start_date.year) + + return sorted(years, reverse=(filter_type == 2)) + def tournament_info(request, tournament_id): tournament = get_object_or_404(Tournament, pk=tournament_id) registered_user = None @@ -217,18 +265,45 @@ def tournaments(request): return redirect('/') club_id = request.GET.get('club') + year_param = request.GET.get('year') + month_param = request.GET.get('month') title = '' tournaments = [] - if filter==0: + available_years = [] + selected_year = None + if month_param: + selected_month = int(month_param) + else: + selected_month = timezone.now().month - 1 + + if filter == 0: title = 'À venir' - tournaments = future_tournaments(club_id) - elif filter==1: + available_years, selected_year = handle_year_month_filtering(year_param, club_id, filter, is_future=True) + + if selected_year and selected_month: + tournaments = future_tournaments_by_month(club_id, selected_year, selected_month) + elif selected_year: + tournaments = future_tournaments(club_id) + else: + tournaments = future_tournaments(club_id) + + elif filter == 1: title = 'En cours' tournaments = live_tournaments(club_id) - elif filter==2: + + elif filter == 2: title = 'Terminés' - tournaments = finished_tournaments(club_id) + available_years, selected_year = handle_year_month_filtering(year_param, club_id, filter, is_future=False) + + if selected_year and selected_month: + tournaments = finished_tournaments_by_month(club_id, selected_year, selected_month) + elif selected_year: + tournaments = finished_tournaments(club_id) + else: + tournaments = finished_tournaments(club_id) + + locale.setlocale(locale.LC_TIME, 'fr_FR.UTF-8') # Define month names in french return render( request, @@ -239,9 +314,39 @@ def tournaments(request): 'second_title': title, 'head_title': title, 'club': club_id, + 'filter': filter, + 'available_years': available_years, + 'available_months': range(0, 12), + 'selected_year': selected_year, + 'selected_month': selected_month, + 'month_names': list(calendar.month_name)[1:], + 'year': year_param, + 'month': month_param, } ) +def handle_year_month_filtering(year_param, club_id, filter_type, is_future=True): + """Helper function to handle year/month filtering for future and finished tournaments""" + available_years = get_available_years_for_tournaments(club_id, filter_type) + selected_year = None + + # Handle year selection + current_year = timezone.now().year + if not year_param and available_years: + if is_future: + selected_year = current_year if current_year in available_years else available_years[0] + else: + selected_year = available_years[0] # Most recent year for finished tournaments + elif year_param: + try: + selected_year = int(year_param) + if selected_year not in available_years: + selected_year = available_years[0] if available_years else None + except: + selected_year = available_years[0] if available_years else None + + return available_years, selected_year + def clubs(request): all_clubs = Club.objects.all().order_by('name') clubs = []