From 38843a996a45f5524087020d6b6d056719d46ed7 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 6 Jun 2025 10:47:11 +0200 Subject: [PATCH 01/13] add shop dashboard --- shop/admin.py | 115 +++++++++++- shop/templates/admin/shop/dashboard.html | 163 ++++++++++++++++++ .../admin/shop/order/change_list.html | 5 + 3 files changed, 280 insertions(+), 3 deletions(-) create mode 100644 shop/templates/admin/shop/dashboard.html diff --git a/shop/admin.py b/shop/admin.py index bce87cf..f1f968f 100644 --- a/shop/admin.py +++ b/shop/admin.py @@ -4,12 +4,72 @@ from django.utils.html import format_html from django.urls import path from django.http import HttpResponseRedirect from django import forms +from django.db.models import Sum, Count, Avg +from datetime import datetime, timedelta +from django.utils import timezone from .models import ( Product, Color, Size, Order, OrderItem, GuestUser, Coupon, CouponUsage, OrderStatus, ShippingAddress ) +class ShopAdminSite(admin.AdminSite): + site_header = "Shop Administration" + site_title = "Shop Admin Portal" + index_title = "Welcome to Shop Administration" + + def index(self, request, extra_context=None): + """Custom admin index view with dashboard""" + # Calculate order statistics + order_status_data = [] + total_orders = Order.objects.count() + total_revenue = Order.objects.aggregate(Sum('total_price'))['total_price__sum'] or 0 + + # Get data for each status + for status_choice in OrderStatus.choices: + status_code, status_label = status_choice + orders_for_status = Order.objects.filter(status=status_code) + count = orders_for_status.count() + total_amount = orders_for_status.aggregate(Sum('total_price'))['total_price__sum'] or 0 + avg_order_value = orders_for_status.aggregate(Avg('total_price'))['total_price__avg'] or 0 + percentage = (count / total_orders * 100) if total_orders > 0 else 0 + + order_status_data.append({ + 'status': status_code, + 'label': status_label, + 'count': count, + 'total_amount': total_amount, + 'avg_order_value': avg_order_value, + 'percentage': percentage + }) + + # Recent activity calculations + now = timezone.now() + today = now.date() + week_ago = today - timedelta(days=7) + month_ago = today - timedelta(days=30) + + orders_today = Order.objects.filter(date_ordered__date=today).count() + orders_this_week = Order.objects.filter(date_ordered__date__gte=week_ago).count() + orders_this_month = Order.objects.filter(date_ordered__date__gte=month_ago).count() + orders_to_prepare = Order.objects.filter(status=OrderStatus.PAID).count() + + extra_context = extra_context or {} + extra_context.update({ + 'order_status_data': order_status_data, + 'total_orders': total_orders, + 'total_revenue': total_revenue, + 'orders_today': orders_today, + 'orders_this_week': orders_this_week, + 'orders_this_month': orders_this_month, + 'orders_to_prepare': orders_to_prepare, + }) + + return render(request, 'admin/shop/dashboard.html', extra_context) + +# Create an instance of the custom admin site +shop_admin_site = ShopAdminSite(name='shop_admin') + @admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display = ("title", "ordering_value", "price", "cut") @@ -131,6 +191,57 @@ class OrderAdmin(admin.ModelAdmin): }), ) + def dashboard_view(self, request): + """Dashboard view with order statistics""" + # Calculate order statistics + order_status_data = [] + total_orders = Order.objects.count() + total_revenue = Order.objects.aggregate(Sum('total_price'))['total_price__sum'] or 0 + + # Get data for each status + for status_choice in OrderStatus.choices: + status_code, status_label = status_choice + orders_for_status = Order.objects.filter(status=status_code) + count = orders_for_status.count() + total_amount = orders_for_status.aggregate(Sum('total_price'))['total_price__sum'] or 0 + avg_order_value = orders_for_status.aggregate(Avg('total_price'))['total_price__avg'] or 0 + percentage = (count / total_orders * 100) if total_orders > 0 else 0 + + order_status_data.append({ + 'status': status_code, + 'label': status_label, + 'count': count, + 'total_amount': total_amount, + 'avg_order_value': avg_order_value, + 'percentage': percentage + }) + + # Recent activity calculations + now = timezone.now() + today = now.date() + week_ago = today - timedelta(days=7) + month_ago = today - timedelta(days=30) + + orders_today = Order.objects.filter(date_ordered__date=today).count() + orders_this_week = Order.objects.filter(date_ordered__date__gte=week_ago).count() + orders_this_month = Order.objects.filter(date_ordered__date__gte=month_ago).count() + orders_to_prepare = Order.objects.filter(status=OrderStatus.PAID).count() + + context = { + 'title': 'Shop Dashboard', + 'app_label': 'shop', + 'opts': Order._meta, + 'order_status_data': order_status_data, + 'total_orders': total_orders, + 'total_revenue': total_revenue, + 'orders_today': orders_today, + 'orders_this_week': orders_this_week, + 'orders_this_month': orders_this_month, + 'orders_to_prepare': orders_to_prepare, + } + + return render(request, 'admin/shop/dashboard.html', context) + def changelist_view(self, request, extra_context=None): # If 'show_preparation' parameter is in the request, show the preparation view if 'show_preparation' in request.GET: @@ -194,9 +305,7 @@ class OrderAdmin(admin.ModelAdmin): def get_urls(self): urls = super().get_urls() custom_urls = [ - path('prepare-all-orders/', self.admin_site.admin_view(self.prepare_all_orders), name='prepare_all_orders'), - path('prepare-order//', self.admin_site.admin_view(self.prepare_order), name='prepare_order'), - path('cancel-and-refund-order//', self.admin_site.admin_view(self.cancel_and_refund_order), name='cancel_and_refund_order'), + path('dashboard/', self.admin_site.admin_view(self.dashboard_view), name='shop_order_dashboard'), ] return custom_urls + urls diff --git a/shop/templates/admin/shop/dashboard.html b/shop/templates/admin/shop/dashboard.html new file mode 100644 index 0000000..b8b3a5d --- /dev/null +++ b/shop/templates/admin/shop/dashboard.html @@ -0,0 +1,163 @@ +{% extends "admin/base_site.html" %} +{% load admin_urls %} + +{% block title %}Shop Dashboard{% endblock %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} +
+
+ + +
+

Orders by Status

+
+ {% for status_data in order_status_data %} +
+ {{ status_data.label }} +
+ + {{ status_data.count }} + + + €{{ status_data.total_amount|floatformat:2 }} + +
+
+ {% endfor %} +
+
+ + +
+

Total Summary

+
+
+
{{ total_orders }}
+
Total Orders
+
+
+
€{{ total_revenue|floatformat:2 }}
+
Total Revenue
+
+
+
+ + +
+

Recent Activity

+
+
+
{{ orders_today }}
+
Orders Today
+
+
+
{{ orders_this_week }}
+
Orders This Week
+
+
+
{{ orders_this_month }}
+
Orders This Month
+
+
+
+ + + +
+ + +
+

Status Breakdown

+
+ + + + + + + + + + + + {% for status_data in order_status_data %} + + + + + + + + {% endfor %} + +
StatusCountPercentageTotal ValueAvg Order Value
{{ status_data.label }} + + {{ status_data.count }} + + {{ status_data.percentage|floatformat:1 }}% + €{{ status_data.total_amount|floatformat:2 }} + + {% if status_data.count > 0 %} + €{{ status_data.avg_order_value|floatformat:2 }} + {% else %} + €0.00 + {% endif %} +
+
+
+
+ + +{% endblock %} diff --git a/shop/templates/admin/shop/order/change_list.html b/shop/templates/admin/shop/order/change_list.html index e3e7fdc..f5ccfbc 100644 --- a/shop/templates/admin/shop/order/change_list.html +++ b/shop/templates/admin/shop/order/change_list.html @@ -2,6 +2,11 @@ {% block object-tools %}
    +
  • + + 📊 Dashboard + +
  • Orders to Prepare ({{ paid_orders_count }}) From d541205f223e9ba293669e705cdcda81b7f2a97c Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 6 Jun 2025 15:37:08 +0200 Subject: [PATCH 02/13] add tournaments dashboard --- tournaments/admin.py | 56 +++- .../admin/tournaments/dashboard.html | 286 ++++++++++++++++++ .../tournaments/tournament/change_list.html | 18 ++ 3 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 tournaments/templates/admin/tournaments/dashboard.html create mode 100644 tournaments/templates/admin/tournaments/tournament/change_list.html diff --git a/tournaments/admin.py b/tournaments/admin.py index 6be27c2..9575633 100644 --- a/tournaments/admin.py +++ b/tournaments/admin.py @@ -3,8 +3,11 @@ from django.contrib.auth.admin import UserAdmin from django.utils import timezone from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION from django.utils.html import escape -from django.urls import reverse +from django.urls import reverse, path # Add path import from django.utils.safestring import mark_safe +from django.shortcuts import render # Add this import +from django.db.models import Sum, Count, Avg, Q # Add these imports +from datetime import datetime, timedelta # Add this import from .models import Club, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, Court, DateInterval, FailedApiCall, Log, DeviceToken, DrawLog, UnregisteredTeam, UnregisteredPlayer, Image from .forms import CustomUserCreationForm, CustomUserChangeForm @@ -85,6 +88,57 @@ class TournamentAdmin(SyncedObjectAdmin): ordering = ['-start_date'] search_fields = ['id'] + def dashboard_view(self, request): + """Tournament dashboard view with comprehensive statistics""" + + # Calculate date ranges + now = timezone.now() + today = now.date() + week_ago = today - timedelta(days=7) + month_ago = today - timedelta(days=30) + + # Tournament statistics - running today + tournaments_today = Tournament.objects.filter( + start_date__date__lte=today, + end_date__date__gte=today + ).exclude(is_deleted=True) + + tournaments_today_private = tournaments_today.filter(is_private=True).count() + tournaments_today_public = tournaments_today.filter(is_private=False).count() + tournaments_today_total = tournaments_today.count() + + # All time tournament statistics + all_tournaments = Tournament.objects.exclude(is_deleted=True) + tournaments_all_total = all_tournaments.count() + + # Team and player statistics + total_teams = TeamRegistration.objects.count() + total_players = PlayerRegistration.objects.count() + + # Match statistics + total_matches = Match.objects.count() + matches_played = Match.objects.filter(end_date__isnull=False).count() + + context = { + 'tournaments_today_total': tournaments_today_total, + 'tournaments_today_private': tournaments_today_private, + 'tournaments_today_public': tournaments_today_public, + 'tournaments_all_total': tournaments_all_total, + 'total_teams': total_teams, + 'total_players': total_players, + 'total_matches': total_matches, + 'matches_played': matches_played, + } + + return render(request, 'admin/tournaments/dashboard.html', context) + + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path('dashboard/', self.admin_site.admin_view(self.dashboard_view), name='tournaments_tournament_dashboard'), + ] + return custom_urls + urls + class TeamRegistrationAdmin(SyncedObjectAdmin): list_display = ['player_names', 'group_stage', 'name', 'tournament', 'registration_date'] list_filter = [SimpleTournamentListFilter] diff --git a/tournaments/templates/admin/tournaments/dashboard.html b/tournaments/templates/admin/tournaments/dashboard.html new file mode 100644 index 0000000..e1269b3 --- /dev/null +++ b/tournaments/templates/admin/tournaments/dashboard.html @@ -0,0 +1,286 @@ +{% extends "admin/base_site.html" %} +{% load admin_urls %} + +{% block title %}Tournament Dashboard{% endblock %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} +
    +

    🏆 Tournament Dashboard

    + + +
    + + +
    +

    + 🎾 Running Tournaments +

    +
    +
    +
    +
    {{ tournaments_today_total }}
    +
    Today
    +
    +
    +
    {{ tournaments_today_private }}/{{ tournaments_today_public }}
    +
    Private/Public
    +
    +
    +
    +
    +
    {{ tournaments_week_total }}
    +
    This Week
    +
    +
    +
    {{ tournaments_week_private }}/{{ tournaments_week_public }}
    +
    Private/Public
    +
    +
    +
    +
    +
    {{ tournaments_month_total }}
    +
    This Month
    +
    +
    +
    {{ tournaments_month_private }}/{{ tournaments_month_public }}
    +
    Private/Public
    +
    +
    +
    +
    + + +
    +

    + 🏁 Ended Tournaments +

    +
    +
    +
    {{ tournaments_ended_today }}
    +
    Today
    +
    +
    +
    {{ tournaments_ended_week }}
    +
    This Week
    +
    +
    +
    {{ tournaments_ended_month }}
    +
    This Month
    +
    +
    +
    {{ tournaments_ended_all }}
    +
    All Time
    +
    +
    +
    + + +
    +

    + 👥 Participants +

    +
    +
    +
    {{ total_teams }}
    +
    Total Teams
    +
    +
    +
    {{ total_players }}
    +
    Total Players
    +
    +
    +
    {{ avg_teams_per_tournament }}
    +
    Avg Teams/Tournament
    +
    +
    +
    + + +
    +

    + 🏓 Matches +

    +
    +
    +
    {{ total_matches }}
    +
    Total Matches
    +
    +
    +
    {{ matches_played }}
    +
    Played
    +
    +
    +
    {{ matches_pending }}
    +
    Pending
    +
    +
    +
    +
    + + +
    +

    + 📊 All Time Overview +

    +
    +
    +
    {{ tournaments_all_total }}
    +
    Total Tournaments
    +
    + {{ tournaments_all_private }} Private | {{ tournaments_all_public }} Public +
    +
    +
    +
    {{ tournaments_with_online_reg }}
    +
    Online Registration
    +
    +
    +
    {{ tournaments_with_payment }}
    +
    Online Payment
    +
    +
    +
    €{{ avg_entry_fee }}
    +
    Avg Entry Fee
    +
    +
    +
    + + +
    +

    📈 Tournament Breakdown

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PeriodRunningPrivatePublicEnded
    Today + + {{ tournaments_today_total }} + + {{ tournaments_today_private }}{{ tournaments_today_public }}{{ tournaments_ended_today }}
    This Week + + {{ tournaments_week_total }} + + {{ tournaments_week_private }}{{ tournaments_week_public }}{{ tournaments_ended_week }}
    This Month + + {{ tournaments_month_total }} + + {{ tournaments_month_private }}{{ tournaments_month_public }}{{ tournaments_ended_month }}
    All Time + + {{ tournaments_all_total }} + + {{ tournaments_all_private }}{{ tournaments_all_public }}{{ tournaments_ended_all }}
    +
    +
    + + + +
    + + +{% endblock %} diff --git a/tournaments/templates/admin/tournaments/tournament/change_list.html b/tournaments/templates/admin/tournaments/tournament/change_list.html new file mode 100644 index 0000000..28ea9b6 --- /dev/null +++ b/tournaments/templates/admin/tournaments/tournament/change_list.html @@ -0,0 +1,18 @@ +{% extends "admin/change_list.html" %} + +{% block object-tools %} + +{% endblock %} From 621639f30e8c7f447006302593fde7b563004623 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 6 Jun 2025 15:47:59 +0200 Subject: [PATCH 03/13] Update admin.py --- tournaments/admin.py | 96 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/tournaments/admin.py b/tournaments/admin.py index 9575633..a628225 100644 --- a/tournaments/admin.py +++ b/tournaments/admin.py @@ -107,10 +107,51 @@ class TournamentAdmin(SyncedObjectAdmin): tournaments_today_public = tournaments_today.filter(is_private=False).count() tournaments_today_total = tournaments_today.count() + # Tournament statistics - running this week + tournaments_this_week = Tournament.objects.filter( + Q(start_date__date__gte=week_ago) | + Q(end_date__date__gte=week_ago, start_date__date__lte=today) + ).exclude(is_deleted=True) + + tournaments_week_private = tournaments_this_week.filter(is_private=True).count() + tournaments_week_public = tournaments_this_week.filter(is_private=False).count() + tournaments_week_total = tournaments_this_week.count() + + # Tournament statistics - running this month + tournaments_this_month = Tournament.objects.filter( + Q(start_date__date__gte=month_ago) | + Q(end_date__date__gte=month_ago, start_date__date__lte=today) + ).exclude(is_deleted=True) + + tournaments_month_private = tournaments_this_month.filter(is_private=True).count() + tournaments_month_public = tournaments_this_month.filter(is_private=False).count() + tournaments_month_total = tournaments_this_month.count() + # All time tournament statistics all_tournaments = Tournament.objects.exclude(is_deleted=True) + tournaments_all_private = all_tournaments.filter(is_private=True).count() + tournaments_all_public = all_tournaments.filter(is_private=False).count() tournaments_all_total = all_tournaments.count() + # Ended tournaments + tournaments_ended_today = Tournament.objects.filter( + end_date__date=today + ).exclude(is_deleted=True) + + tournaments_ended_week = Tournament.objects.filter( + end_date__date__gte=week_ago, + end_date__date__lte=today + ).exclude(is_deleted=True) + + tournaments_ended_month = Tournament.objects.filter( + end_date__date__gte=month_ago, + end_date__date__lte=today + ).exclude(is_deleted=True) + + tournaments_ended_all = Tournament.objects.filter( + end_date__lt=now + ).exclude(is_deleted=True) + # Team and player statistics total_teams = TeamRegistration.objects.count() total_players = PlayerRegistration.objects.count() @@ -118,16 +159,71 @@ class TournamentAdmin(SyncedObjectAdmin): # Match statistics total_matches = Match.objects.count() matches_played = Match.objects.filter(end_date__isnull=False).count() + matches_pending = Match.objects.filter(end_date__isnull=True).count() + + # Additional statistics + tournaments_with_online_reg = Tournament.objects.filter( + enable_online_registration=True + ).exclude(is_deleted=True).count() + + tournaments_with_payment = Tournament.objects.filter( + enable_online_payment=True + ).exclude(is_deleted=True).count() + + # Average statistics + avg_teams_per_tournament = TeamRegistration.objects.aggregate( + avg_teams=Avg('tournament__team_count') + )['avg_teams'] or 0 + + avg_entry_fee = Tournament.objects.exclude(is_deleted=True).aggregate( + avg_fee=Avg('entry_fee') + )['avg_fee'] or 0 context = { + 'title': 'Tournament Dashboard', + 'app_label': 'tournaments', + 'opts': Tournament._meta, + + # Today statistics 'tournaments_today_total': tournaments_today_total, 'tournaments_today_private': tournaments_today_private, 'tournaments_today_public': tournaments_today_public, + + # Week statistics + 'tournaments_week_total': tournaments_week_total, + 'tournaments_week_private': tournaments_week_private, + 'tournaments_week_public': tournaments_week_public, + + # Month statistics + 'tournaments_month_total': tournaments_month_total, + 'tournaments_month_private': tournaments_month_private, + 'tournaments_month_public': tournaments_month_public, + + # All time statistics 'tournaments_all_total': tournaments_all_total, + 'tournaments_all_private': tournaments_all_private, + 'tournaments_all_public': tournaments_all_public, + + # Ended tournaments + 'tournaments_ended_today': tournaments_ended_today.count(), + 'tournaments_ended_week': tournaments_ended_week.count(), + 'tournaments_ended_month': tournaments_ended_month.count(), + 'tournaments_ended_all': tournaments_ended_all.count(), + + # Teams and players 'total_teams': total_teams, 'total_players': total_players, + + # Matches 'total_matches': total_matches, 'matches_played': matches_played, + 'matches_pending': matches_pending, + + # Additional stats + 'tournaments_with_online_reg': tournaments_with_online_reg, + 'tournaments_with_payment': tournaments_with_payment, + 'avg_teams_per_tournament': round(avg_teams_per_tournament, 1), + 'avg_entry_fee': round(avg_entry_fee, 2), } return render(request, 'admin/tournaments/dashboard.html', context) From 621f37791cf8a11631aa1ae696ff173374085442 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 6 Jun 2025 16:00:48 +0200 Subject: [PATCH 04/13] fix dashboard --- tournaments/admin.py | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/tournaments/admin.py b/tournaments/admin.py index a628225..2779dfb 100644 --- a/tournaments/admin.py +++ b/tournaments/admin.py @@ -94,33 +94,33 @@ class TournamentAdmin(SyncedObjectAdmin): # Calculate date ranges now = timezone.now() today = now.date() - week_ago = today - timedelta(days=7) - month_ago = today - timedelta(days=30) + week_start = today - timedelta(days=today.weekday()) # Monday of this week + week_end = week_start + timedelta(days=6) # Sunday of this week + month_start = today.replace(day=1) # First day of current month - # Tournament statistics - running today + # Tournament statistics - tournaments starting TODAY tournaments_today = Tournament.objects.filter( - start_date__date__lte=today, - end_date__date__gte=today + start_date__date=today ).exclude(is_deleted=True) tournaments_today_private = tournaments_today.filter(is_private=True).count() tournaments_today_public = tournaments_today.filter(is_private=False).count() tournaments_today_total = tournaments_today.count() - # Tournament statistics - running this week + # Tournament statistics - tournaments starting THIS WEEK tournaments_this_week = Tournament.objects.filter( - Q(start_date__date__gte=week_ago) | - Q(end_date__date__gte=week_ago, start_date__date__lte=today) + start_date__date__gte=week_start, + start_date__date__lte=week_end ).exclude(is_deleted=True) tournaments_week_private = tournaments_this_week.filter(is_private=True).count() tournaments_week_public = tournaments_this_week.filter(is_private=False).count() tournaments_week_total = tournaments_this_week.count() - # Tournament statistics - running this month + # Tournament statistics - tournaments starting THIS MONTH tournaments_this_month = Tournament.objects.filter( - Q(start_date__date__gte=month_ago) | - Q(end_date__date__gte=month_ago, start_date__date__lte=today) + start_date__date__gte=month_start, + start_date__date__lte=today + timedelta(days=31 - today.day) # End of current month ).exclude(is_deleted=True) tournaments_month_private = tournaments_this_month.filter(is_private=True).count() @@ -133,23 +133,23 @@ class TournamentAdmin(SyncedObjectAdmin): tournaments_all_public = all_tournaments.filter(is_private=False).count() tournaments_all_total = all_tournaments.count() - # Ended tournaments + # Ended tournaments (tournaments that have an end_date in the past) tournaments_ended_today = Tournament.objects.filter( end_date__date=today ).exclude(is_deleted=True) tournaments_ended_week = Tournament.objects.filter( - end_date__date__gte=week_ago, - end_date__date__lte=today + end_date__date__gte=week_start, + end_date__date__lte=week_end ).exclude(is_deleted=True) tournaments_ended_month = Tournament.objects.filter( - end_date__date__gte=month_ago, - end_date__date__lte=today + end_date__date__gte=month_start, + end_date__date__lte=today + timedelta(days=31 - today.day) ).exclude(is_deleted=True) tournaments_ended_all = Tournament.objects.filter( - end_date__lt=now + end_date__date__lt=today ).exclude(is_deleted=True) # Team and player statistics @@ -184,17 +184,17 @@ class TournamentAdmin(SyncedObjectAdmin): 'app_label': 'tournaments', 'opts': Tournament._meta, - # Today statistics + # Today statistics (tournaments STARTING today) 'tournaments_today_total': tournaments_today_total, 'tournaments_today_private': tournaments_today_private, 'tournaments_today_public': tournaments_today_public, - # Week statistics + # Week statistics (tournaments STARTING this week) 'tournaments_week_total': tournaments_week_total, 'tournaments_week_private': tournaments_week_private, 'tournaments_week_public': tournaments_week_public, - # Month statistics + # Month statistics (tournaments STARTING this month) 'tournaments_month_total': tournaments_month_total, 'tournaments_month_private': tournaments_month_private, 'tournaments_month_public': tournaments_month_public, @@ -204,7 +204,7 @@ class TournamentAdmin(SyncedObjectAdmin): 'tournaments_all_private': tournaments_all_private, 'tournaments_all_public': tournaments_all_public, - # Ended tournaments + # Ended tournaments (tournaments ENDING in the respective periods) 'tournaments_ended_today': tournaments_ended_today.count(), 'tournaments_ended_week': tournaments_ended_week.count(), 'tournaments_ended_month': tournaments_ended_month.count(), From acdc1a270c3215c2fc8017487902db8edb3cadb4 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 6 Jun 2025 18:22:23 +0200 Subject: [PATCH 05/13] add some data in dashboards --- tournaments/admin.py | 52 ++++++ .../admin/tournaments/dashboard.html | 157 +++++++++++++++++- 2 files changed, 208 insertions(+), 1 deletion(-) diff --git a/tournaments/admin.py b/tournaments/admin.py index 2779dfb..006ca06 100644 --- a/tournaments/admin.py +++ b/tournaments/admin.py @@ -179,6 +179,41 @@ class TournamentAdmin(SyncedObjectAdmin): avg_fee=Avg('entry_fee') )['avg_fee'] or 0 + # User Account Statistics + total_users = CustomUser.objects.count() + users_admin = CustomUser.objects.filter(origin=0).count() # ADMIN + users_site = CustomUser.objects.filter(origin=1).count() # SITE + users_app = CustomUser.objects.filter(origin=2).count() # APP + + # Recent User Registrations + recent_users = CustomUser.objects.all().order_by('-date_joined')[:10] + + # New users by period + users_today = CustomUser.objects.filter(date_joined__date=today).count() + users_this_week = CustomUser.objects.filter( + date_joined__date__gte=week_start, + date_joined__date__lte=week_end + ).count() + users_this_month = CustomUser.objects.filter( + date_joined__date__gte=month_start, + date_joined__date__lte=today + timedelta(days=31 - today.day) + ).count() + + # Purchase Statistics + total_purchases = Purchase.objects.count() + recent_purchases = Purchase.objects.all().order_by('-purchase_date')[:10] + + # Purchases by period + purchases_today = Purchase.objects.filter(purchase_date__date=today).count() + purchases_this_week = Purchase.objects.filter( + purchase_date__date__gte=week_start, + purchase_date__date__lte=week_end + ).count() + purchases_this_month = Purchase.objects.filter( + purchase_date__date__gte=month_start, + purchase_date__date__lte=today + timedelta(days=31 - today.day) + ).count() + context = { 'title': 'Tournament Dashboard', 'app_label': 'tournaments', @@ -224,6 +259,23 @@ class TournamentAdmin(SyncedObjectAdmin): 'tournaments_with_payment': tournaments_with_payment, 'avg_teams_per_tournament': round(avg_teams_per_tournament, 1), 'avg_entry_fee': round(avg_entry_fee, 2), + + # User statistics + 'total_users': total_users, + 'users_admin': users_admin, + 'users_site': users_site, + 'users_app': users_app, + 'users_today': users_today, + 'users_this_week': users_this_week, + 'users_this_month': users_this_month, + 'recent_users': recent_users, + + # Purchase statistics + 'total_purchases': total_purchases, + 'recent_purchases': recent_purchases, + 'purchases_today': purchases_today, + 'purchases_this_week': purchases_this_week, + 'purchases_this_month': purchases_this_month, } return render(request, 'admin/tournaments/dashboard.html', context) diff --git a/tournaments/templates/admin/tournaments/dashboard.html b/tournaments/templates/admin/tournaments/dashboard.html index e1269b3..3986625 100644 --- a/tournaments/templates/admin/tournaments/dashboard.html +++ b/tournaments/templates/admin/tournaments/dashboard.html @@ -21,7 +21,7 @@

    - 🎾 Running Tournaments + 🎾 Starting Tournaments

    @@ -125,6 +125,161 @@
    + +
    +

    + 👤 User Statistics +

    +
    + +
    +

    Total Users: {{ total_users }}

    +
    + Admin: + {{ users_admin }} +
    +
    + Site: + {{ users_site }} +
    +
    + App: + {{ users_app }} +
    +
    +
    + {% if total_users > 0 %} +
    +
    +
    + {% endif %} +
    +
    + Admin + Site + App +
    +
    +
    + + +
    +

    New Registrations

    +
    + Today: +
    {{ users_today }}
    +
    +
    + This Week: +
    {{ users_this_week }}
    +
    +
    + This Month: +
    {{ users_this_month }}
    +
    +
    + + +
    +

    Recently Registered Users

    +
    + + + + + + + + + + + {% for user in recent_users %} + + + + + + + {% empty %} + + + + {% endfor %} + +
    NameEmailOriginDate Joined
    {{ user.first_name }} {{ user.last_name }}{{ user.email }} + {% if user.origin == 0 %} + Admin + {% elif user.origin == 1 %} + Site + {% elif user.origin == 2 %} + App + {% else %} + Unknown + {% endif %} + {{ user.date_joined|date:"M d, Y H:i" }}
    No recent users found.
    +
    +
    +
    +
    + + +
    +

    + 💰 Purchase Statistics +

    +
    + +
    +

    Total Purchases: {{ total_purchases }}

    +
    + Today: +
    {{ purchases_today }}
    +
    +
    + This Week: +
    {{ purchases_this_week }}
    +
    +
    + This Month: +
    {{ purchases_this_month }}
    +
    +
    + + +
    +

    Recent Purchases

    +
    + + + + + + + + + + + + {% for purchase in recent_purchases %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
    IDUserProductQuantityDate
    {{ purchase.id }}{{ purchase.user.email }}{{ purchase.product_id }}{{ purchase.quantity }}{{ purchase.purchase_date|date:"M d, Y H:i" }}
    No recent purchases found.
    +
    +
    +
    +
    +

    From e43e69fa62c3760c3e9fad5288eb6bf9e5c649b1 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 6 Jun 2025 18:44:47 +0200 Subject: [PATCH 06/13] add user staff in custom user admin --- tournaments/admin.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tournaments/admin.py b/tournaments/admin.py index 006ca06..f8f7b60 100644 --- a/tournaments/admin.py +++ b/tournaments/admin.py @@ -25,14 +25,17 @@ class CustomUserAdmin(UserAdmin): list_filter = ['is_active', 'origin'] ordering = ['-date_joined'] fieldsets = [ - (None, {'fields': ['id', 'username', 'email', 'password', 'first_name', 'last_name', 'is_active', 'registration_payment_mode', - 'clubs', 'country', 'phone', 'licence_id', 'umpire_code', + (None, {'fields': ['id', 'username', 'email', 'password', 'first_name', 'last_name', 'is_active']}), + ('Permissions', {'fields': ['is_staff', 'is_superuser', 'groups', 'user_permissions']}), + ('Personal Info', {'fields': ['registration_payment_mode', 'clubs', 'country', 'phone', 'licence_id', 'umpire_code']}), + ('Tournament Settings', {'fields': [ 'summons_message_body', 'summons_message_signature', 'summons_available_payment_methods', 'summons_display_format', 'summons_display_entry_fee', 'summons_use_full_custom_message', 'match_formats_default_duration', 'bracket_match_format_preference', 'group_stage_match_format_preference', - 'loser_bracket_match_format_preference', 'device_id', 'loser_bracket_mode', 'groups', 'origin', 'agents', 'should_synchronize' + 'loser_bracket_match_format_preference', 'device_id', 'loser_bracket_mode', 'origin', 'agents', 'should_synchronize' ]}), - ] + ] + add_fieldsets = [ ( None, From aaf4bad035db0fbcaa1c4995593372b08b5bc56c Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 6 Jun 2025 18:51:20 +0200 Subject: [PATCH 07/13] fix 500 in shop prepare dashboard --- shop/admin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shop/admin.py b/shop/admin.py index f1f968f..a3febdc 100644 --- a/shop/admin.py +++ b/shop/admin.py @@ -306,6 +306,9 @@ class OrderAdmin(admin.ModelAdmin): urls = super().get_urls() custom_urls = [ path('dashboard/', self.admin_site.admin_view(self.dashboard_view), name='shop_order_dashboard'), + path('prepare-all/', self.admin_site.admin_view(self.prepare_all_orders), name='prepare_all_orders'), + path('/prepare/', self.admin_site.admin_view(self.prepare_order), name='prepare_order'), + path('/cancel-refund/', self.admin_site.admin_view(self.cancel_and_refund_order), name='cancel_and_refund_order'), ] return custom_urls + urls From 12aa84ebdb074c28dc1a0cc5e573413d736fccb7 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Sun, 8 Jun 2025 09:23:11 +0200 Subject: [PATCH 08/13] add resend verification link system --- tournaments/custom_views.py | 15 ++++++++ tournaments/forms.py | 35 +++++++++++++++--- tournaments/templates/registration/login.html | 32 +++++++++++++--- tournaments/urls.py | 2 +- tournaments/views.py | 37 +++++++++++++++++++ 5 files changed, 109 insertions(+), 12 deletions(-) diff --git a/tournaments/custom_views.py b/tournaments/custom_views.py index 6469c21..d3503dc 100644 --- a/tournaments/custom_views.py +++ b/tournaments/custom_views.py @@ -31,6 +31,17 @@ class CustomLoginView(auth_views.LoginView): # Fall back to default return reverse('index') + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + # Check for inactive user in session + inactive_user_email = self.request.session.get('inactive_user_email') + if inactive_user_email: + context['inactive_user_email'] = inactive_user_email + context['show_resend_activation'] = True + + return context + def get(self, request, *args, **kwargs): # Clear any potential password reset session data keys_to_clear = [key for key in request.session.keys() @@ -38,6 +49,10 @@ class CustomLoginView(auth_views.LoginView): for key in keys_to_clear: del request.session[key] + # Clear inactive user session data on GET request (fresh login page) + request.session.pop('inactive_user_email', None) + request.session.pop('inactive_user_id', None) + storage = messages.get_messages(request) for _ in storage: pass diff --git a/tournaments/forms.py b/tournaments/forms.py index 94578d2..2ca881d 100644 --- a/tournaments/forms.py +++ b/tournaments/forms.py @@ -338,29 +338,52 @@ class EmailOrUsernameAuthenticationForm(AuthenticationForm): username = self.cleaned_data.get('username') password = self.cleaned_data.get('password') - print(f"Login attempt with username/email: {username}") # Debug print logger.info(f"Login attempt with username/email: {username}") if username and password: + # Check if user exists first (either by username or email) + user_exists = None + try: + # Try to find user by username first + user_exists = CustomUser.objects.get(username__iexact=username) + except CustomUser.DoesNotExist: + # Try to find user by email + try: + user_exists = CustomUser.objects.get(email__iexact=username) + except CustomUser.DoesNotExist: + pass + + # If user exists but is inactive, provide specific feedback + if user_exists and not user_exists.is_active: + # Store the inactive user in session for template use + if hasattr(self, 'request') and self.request.session: + self.request.session['inactive_user_email'] = user_exists.email + self.request.session['inactive_user_id'] = str(user_exists.id) + + raise forms.ValidationError( + "Votre compte n'est pas encore activé. Veuillez cliquer sur le lien d'activation envoyé à votre adresse e-mail.", + code='inactive_account' + ) + + # Try regular authentication self.user_cache = authenticate( self.request, username=username, password=password ) - print(f"Authentication result: {self.user_cache}") # Debug print - logger.info(f"Authentication result: {self.user_cache}") - if self.user_cache is None: - print("Authentication failed") # Debug print logger.warning("Authentication failed") raise forms.ValidationError( "Identifiant/E-mail ou mot de passe incorrect. Les champs sont sensibles à la casse.", code='invalid_login' ) else: - print(f"Authentication successful for user: {self.user_cache}") # Debug print logger.info(f"Authentication successful for user: {self.user_cache}") + # Clear any inactive user session data on successful login + if hasattr(self, 'request') and self.request.session: + self.request.session.pop('inactive_user_email', None) + self.request.session.pop('inactive_user_id', None) self.confirm_login_allowed(self.user_cache) return self.cleaned_data diff --git a/tournaments/templates/registration/login.html b/tournaments/templates/registration/login.html index d4ac8e7..6bde7f3 100644 --- a/tournaments/templates/registration/login.html +++ b/tournaments/templates/registration/login.html @@ -13,11 +13,9 @@
    {% if form.non_field_errors %}
    - {% if form.non_field_errors %} - {% for error in form.non_field_errors %} -

    {{ error }}

    - {% endfor %} - {% endif %} + {% for error in form.non_field_errors %} +

    {{ error }}

    + {% endfor %} {% for field in form %} {% for error in field.errors %} @@ -26,6 +24,30 @@ {% endfor %}
    {% endif %} + + + {% if inactive_user_email %} +
    +

    Besoin d'aide ?

    +

    Si vous n'avez pas reçu l'e-mail d'activation ou si le lien a expiré, vous pouvez en demander un nouveau :

    + +
    + {% csrf_token %} + + {% if request.GET.next %} + + {% endif %} + +
    + +

    + Le lien sera envoyé à : {{ inactive_user_email }} +

    +
    + {% endif %} +
    {% csrf_token %} {% if request.GET.next and 'reset' not in request.GET.next and 'password_reset' not in request.GET.next %} diff --git a/tournaments/urls.py b/tournaments/urls.py index 6867a7c..5ea47c7 100644 --- a/tournaments/urls.py +++ b/tournaments/urls.py @@ -60,7 +60,7 @@ urlpatterns = [ path('logout/', views.custom_logout, name='custom_logout'), path('utils/xls-to-csv/', views.xls_to_csv, name='xls-to-csv'), path('signup/', views.signup, name='signup'), # URL pattern for signup -# path('profile/', views.profile, name='profile'), # URL pattern for signup + path('resend-activation/', views.resend_activation_email, name='resend-activation'), path('my-tournaments/', views.my_tournaments, name='my-tournaments'), # URL pattern for signup path('all_my_ended_tournaments/', views.all_my_ended_tournaments, name='all-my-ended-tournaments'), # URL pattern for signup path('tournaments//cancel-registration/', views.cancel_registration, name='cancel_registration'), diff --git a/tournaments/views.py b/tournaments/views.py index 0db52eb..4d606a6 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -730,6 +730,43 @@ def send_verification_email(request, user, next_url): email.content_subtype = "html" email.send() +def resend_activation_email(request): + """View to resend activation email for inactive users.""" + if request.method == 'POST': + username_or_email = request.POST.get('username_or_email', '').strip() + + if not username_or_email: + messages.error(request, 'Veuillez fournir un nom d\'utilisateur ou un e-mail.') + return redirect('custom-login') + + # Try to find the user + user = None + try: + # Try by username first + user = CustomUser.objects.get(username__iexact=username_or_email) + except CustomUser.DoesNotExist: + try: + # Try by email + user = CustomUser.objects.get(email__iexact=username_or_email) + except CustomUser.DoesNotExist: + messages.error(request, 'Aucun compte trouvé avec cet identifiant.') + return redirect('custom-login') + + # Check if user is already active + if user.is_active: + messages.info(request, 'Votre compte est déjà activé. Vous pouvez vous connecter.') + return redirect('custom-login') + + # Send the activation email + next_url = request.POST.get('next', '') + send_verification_email(request, user, next_url) + + messages.success(request, f'Un nouveau lien d\'activation a été envoyé à {user.email}.') + return redirect('custom-login') + + # If GET request, redirect to login + return redirect('custom-login') + @login_required def profile(request): user = request.user # Get the currently authenticated user From 2a61240e0eeda363aa06e488582141713791ab65 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Sun, 8 Jun 2025 09:26:54 +0200 Subject: [PATCH 09/13] Update signup_success.html --- .../registration/signup_success.html | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tournaments/templates/registration/signup_success.html b/tournaments/templates/registration/signup_success.html index 1357f90..5c4fad0 100644 --- a/tournaments/templates/registration/signup_success.html +++ b/tournaments/templates/registration/signup_success.html @@ -20,6 +20,27 @@
  • Vérifier votre boîte de réception (et vos spams si nécessaire)
  • Cliquer sur le lien de confirmation dans l'e-mail
  • + + +
    +

    Vous n'avez pas reçu l'e-mail ?

    +

    Si l'e-mail de confirmation n'arrive pas dans les prochaines minutes, vous pouvez en demander un nouveau :

    + + + {% csrf_token %} + + {% if next_url %} + + {% endif %} + + + +

    + Le lien sera envoyé à : {{ user_email }} +

    +
    Continuer From ae1a24a083390080cc2ce8f793c046d83f41d9e4 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Sun, 8 Jun 2025 10:06:12 +0200 Subject: [PATCH 10/13] add view all users and access to dashboard and private toggle for superuser / staff --- .../shop/partials/navigation_base.html | 4 ++ .../admin/tournaments/dashboard.html | 7 +++- .../tournaments/navigation_base.html | 6 ++- .../tournaments/navigation_tournament.html | 31 ++++++++++++++ tournaments/urls.py | 2 + tournaments/views.py | 42 +++++++++++++++++++ 6 files changed, 90 insertions(+), 2 deletions(-) diff --git a/shop/templates/shop/partials/navigation_base.html b/shop/templates/shop/partials/navigation_base.html index f5dc311..351b98f 100644 --- a/shop/templates/shop/partials/navigation_base.html +++ b/shop/templates/shop/partials/navigation_base.html @@ -9,4 +9,8 @@ Se connecter {% endif %} La boutique + {% if user.is_authenticated and user.is_staff %} + Tableau de bord boutique + Préparer commandes + {% endif %} diff --git a/tournaments/templates/admin/tournaments/dashboard.html b/tournaments/templates/admin/tournaments/dashboard.html index 3986625..b6f6fb1 100644 --- a/tournaments/templates/admin/tournaments/dashboard.html +++ b/tournaments/templates/admin/tournaments/dashboard.html @@ -181,7 +181,12 @@
    -

    Recently Registered Users

    +

    + Recently Registered Users + + View All Users + +

    diff --git a/tournaments/templates/tournaments/navigation_base.html b/tournaments/templates/tournaments/navigation_base.html index b1ef0ea..91c9752 100644 --- a/tournaments/templates/tournaments/navigation_base.html +++ b/tournaments/templates/tournaments/navigation_base.html @@ -10,5 +10,9 @@ Se connecter {% endif %} La boutique - Ajouter vos tournois + {% if user.is_authenticated and user.is_staff %} + Tableau de bord + {% else %} + Ajouter vos tournois + {% endif %} diff --git a/tournaments/templates/tournaments/navigation_tournament.html b/tournaments/templates/tournaments/navigation_tournament.html index 7f74a36..4af9993 100644 --- a/tournaments/templates/tournaments/navigation_tournament.html +++ b/tournaments/templates/tournaments/navigation_tournament.html @@ -37,4 +37,35 @@ {% else %} Se connecter {% endif %} + + + {% if user.is_superuser %} + + {% csrf_token %} + + + {% endif %} diff --git a/tournaments/urls.py b/tournaments/urls.py index 5ea47c7..3d14290 100644 --- a/tournaments/urls.py +++ b/tournaments/urls.py @@ -80,4 +80,6 @@ urlpatterns = [ path('tournaments//confirm/', views.confirm_tournament_registration, name='confirm_tournament_registration'), path('stripe-onboarding-complete/', views.stripe_onboarding_complete, name='stripe-onboarding-complete'), path('stripe-refresh-account-link/', views.stripe_refresh_account_link, name='stripe-refresh-account-link'), + path('tournaments//toggle-private/', views.toggle_tournament_private, name='toggle_tournament_private'), + ] diff --git a/tournaments/views.py b/tournaments/views.py index 4d606a6..6d98079 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -1659,6 +1659,48 @@ def stripe_onboarding_complete(request): def stripe_refresh_account_link(request): return render(request, 'stripe/refresh_account_link.html') +def toggle_tournament_private(request, tournament_id): + """Toggle tournament privacy status (for superusers only)""" + + # Check if user is superuser + if not request.user.is_superuser: + if request.headers.get('Content-Type') == 'application/json': + return JsonResponse({'error': 'Accès non autorisé'}, status=403) + messages.error(request, 'Accès non autorisé') + return redirect('tournament-info', tournament_id=tournament_id) + + # Only allow POST requests + if request.method != 'POST': + if request.headers.get('Content-Type') == 'application/json': + return JsonResponse({'error': 'Méthode non autorisée'}, status=405) + messages.error(request, 'Méthode non autorisée') + return redirect('tournament-info', tournament_id=tournament_id) + + try: + tournament = get_object_or_404(Tournament, pk=tournament_id) + + # Toggle the private status + tournament.is_private = not tournament.is_private + tournament.save() + + # Check if this is an AJAX request + if request.headers.get('Content-Type') == 'application/json': + return JsonResponse({ + 'success': True, + 'is_private': tournament.is_private, + 'message': f'Tournoi défini comme {"privé" if tournament.is_private else "public"}' + }) + else: + # Regular form submission - add success message and redirect + status = "privé" if tournament.is_private else "public" + messages.success(request, f'Tournoi défini comme {status}') + return redirect('tournament-info', tournament_id=tournament_id) + + except Exception as e: + if request.headers.get('Content-Type') == 'application/json': + return JsonResponse({'error': f'Erreur: {str(e)}'}, status=500) + messages.error(request, f'Erreur: {str(e)}') + return redirect('tournament-info', tournament_id=tournament_id) class UserListExportView(LoginRequiredMixin, View): def get(self, request, *args, **kwargs): From ace8801eccbb68f2424e9f8ccf38d1ccedeb07fc Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Sun, 8 Jun 2025 10:12:45 +0200 Subject: [PATCH 11/13] add a section for private tournaments for staff members --- .../tournaments/navigation_base.html | 36 ++++++------ tournaments/urls.py | 1 + tournaments/views.py | 57 +++++++++++++++++++ 3 files changed, 76 insertions(+), 18 deletions(-) diff --git a/tournaments/templates/tournaments/navigation_base.html b/tournaments/templates/tournaments/navigation_base.html index 91c9752..7e07f5e 100644 --- a/tournaments/templates/tournaments/navigation_base.html +++ b/tournaments/templates/tournaments/navigation_base.html @@ -1,18 +1,18 @@ - - + diff --git a/tournaments/urls.py b/tournaments/urls.py index 3d14290..7b400c3 100644 --- a/tournaments/urls.py +++ b/tournaments/urls.py @@ -81,5 +81,6 @@ urlpatterns = [ path('stripe-onboarding-complete/', views.stripe_onboarding_complete, name='stripe-onboarding-complete'), path('stripe-refresh-account-link/', views.stripe_refresh_account_link, name='stripe-refresh-account-link'), path('tournaments//toggle-private/', views.toggle_tournament_private, name='toggle_tournament_private'), + path("private-tournaments/", views.private_tournaments, name="private-tournaments"), ] diff --git a/tournaments/views.py b/tournaments/views.py index 6d98079..816c2ce 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -1702,6 +1702,63 @@ def toggle_tournament_private(request, tournament_id): messages.error(request, f'Erreur: {str(e)}') return redirect('tournament-info', tournament_id=tournament_id) +def private_tournaments(request): + """ + View for displaying private tournaments (staff-only). + Similar to index, but shows private tournaments instead. + """ + if not request.user.is_staff: + messages.error(request, 'Accès non autorisé') + return redirect('index') + + now = timezone.now() + thirty_days_ago = now - timedelta(days=30) + thirty_days_future = now + timedelta(days=30) + + # Custom query for private tournaments + queries = [ + Q(is_private=True, is_deleted=False, event__club__isnull=False) + ] + + # Filter by date range for future tournaments + future_query = queries.copy() + future_query.append(Q(start_date__gt=now, start_date__lte=thirty_days_future)) + + # Filter for live tournaments + live_query = queries.copy() + live_query.append(Q(start_date__lte=now, end_date__isnull=True) | Q(start_date__lte=now, end_date__gt=now)) + + # Filter for ended tournaments + ended_query = queries.copy() + ended_query.append(Q(end_date__lte=now, end_date__gte=thirty_days_ago)) + + # Get the tournaments from the database + future_tournaments = Tournament.objects.filter(*future_query).prefetch_related( + 'group_stages', 'rounds', 'team_registrations').order_by('start_date') + + live_tournaments = Tournament.objects.filter(*live_query).prefetch_related( + 'group_stages', 'rounds', 'team_registrations').order_by('start_date') + + ended_tournaments = Tournament.objects.filter(*ended_query).prefetch_related( + 'group_stages', 'rounds', 'team_registrations').order_by('-end_date') + + # Filter tournaments that should be displayed + future = [t for t in future_tournaments if t.display_tournament()] + live = [t for t in live_tournaments if t.display_tournament()] + ended = [t for t in ended_tournaments if t.display_tournament()] + + return render( + request, + "tournaments/tournaments.html", + { + 'future': future[:10], + 'live': live[:10], + 'ended': ended[:10], + 'is_private_section': True, # Flag to indicate we're in the private tournaments section + 'section_title': 'Tournois privés', # Title for the private tournaments page + } + ) + class UserListExportView(LoginRequiredMixin, View): def get(self, request, *args, **kwargs): users = CustomUser.objects.order_by('date_joined') From 138509447420a916d2f3b9017b756616cdfb6b9b Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Sun, 8 Jun 2025 10:19:50 +0200 Subject: [PATCH 12/13] fix live private section --- tournaments/views.py | 71 +++++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/tournaments/views.py b/tournaments/views.py index 816c2ce..a2f1e0e 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -1715,37 +1715,54 @@ def private_tournaments(request): thirty_days_ago = now - timedelta(days=30) thirty_days_future = now + timedelta(days=30) - # Custom query for private tournaments - queries = [ - Q(is_private=True, is_deleted=False, event__club__isnull=False) - ] - - # Filter by date range for future tournaments - future_query = queries.copy() - future_query.append(Q(start_date__gt=now, start_date__lte=thirty_days_future)) - - # Filter for live tournaments - live_query = queries.copy() - live_query.append(Q(start_date__lte=now, end_date__isnull=True) | Q(start_date__lte=now, end_date__gt=now)) + # Define a custom private tournaments query function + def private_tournaments_query(query, ascending, limit=None): + queries = [query, Q(is_private=True, is_deleted=False, event__club__isnull=False)] + + sortkey = 'start_date' + if not ascending: + sortkey = '-start_date' + + queryset = Tournament.objects.filter(*queries).prefetch_related( + 'group_stages', + 'rounds', + 'team_registrations', + ).order_by(sortkey) + + # Apply limit directly in the database query + if limit is not None and isinstance(limit, int) and limit > 0: + queryset = queryset[:limit] + + return queryset + + # Get all tournaments matching our criteria (similar to index but for private tournaments) + tournaments = private_tournaments_query( + Q(end_date__isnull=True, start_date__gte=thirty_days_ago, start_date__lte=thirty_days_future), + True, 50 + ) - # Filter for ended tournaments - ended_query = queries.copy() - ended_query.append(Q(end_date__lte=now, end_date__gte=thirty_days_ago)) + # Filter tournaments that should be displayed + display_tournament = [t for t in tournaments if t.display_tournament()] - # Get the tournaments from the database - future_tournaments = Tournament.objects.filter(*future_query).prefetch_related( - 'group_stages', 'rounds', 'team_registrations').order_by('start_date') + # Categorize tournaments by status + live = [] + future = [] + for t in display_tournament: + if t.supposedly_in_progress(): + live.append(t) + elif t.starts_in_the_future(): + future.append(t) - live_tournaments = Tournament.objects.filter(*live_query).prefetch_related( - 'group_stages', 'rounds', 'team_registrations').order_by('start_date') + # Get ended tournaments + clean_ended_tournaments = private_tournaments_query(Q(end_date__isnull=False), False, 50) + clean_ended_tournaments = [t for t in clean_ended_tournaments if t.display_tournament()] + ended_tournaments = [t for t in display_tournament if t.should_be_over()] - ended_tournaments = Tournament.objects.filter(*ended_query).prefetch_related( - 'group_stages', 'rounds', 'team_registrations').order_by('-end_date') + # Combine both lists + finished = clean_ended_tournaments + ended_tournaments - # Filter tournaments that should be displayed - future = [t for t in future_tournaments if t.display_tournament()] - live = [t for t in live_tournaments if t.display_tournament()] - ended = [t for t in ended_tournaments if t.display_tournament()] + # Sort the combined list by start_date in descending order + finished.sort(key=lambda t: t.sorting_finished_date(), reverse=True) return render( request, @@ -1753,7 +1770,7 @@ def private_tournaments(request): { 'future': future[:10], 'live': live[:10], - 'ended': ended[:10], + 'ended': finished[:10], 'is_private_section': True, # Flag to indicate we're in the private tournaments section 'section_title': 'Tournois privés', # Title for the private tournaments page } From 8d814b49c4003b9fde47e5a4a9766a4de2f85457 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Sun, 8 Jun 2025 10:25:27 +0200 Subject: [PATCH 13/13] Update views.py --- tournaments/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tournaments/views.py b/tournaments/views.py index a2f1e0e..c9ff7e9 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -1742,7 +1742,7 @@ def private_tournaments(request): ) # Filter tournaments that should be displayed - display_tournament = [t for t in tournaments if t.display_tournament()] + display_tournament = tournaments # Categorize tournaments by status live = []