From 03cab14cf237473bf91aa3a78ae3e369616b447a Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Thu, 5 Jun 2025 07:22:51 +0200 Subject: [PATCH 01/31] fix crash with get_tournament missing in place of tournament() in match.py --- tournaments/models/match.py | 2 +- tournaments/models/tournament.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tournaments/models/match.py b/tournaments/models/match.py index 044bba1..25f0a15 100644 --- a/tournaments/models/match.py +++ b/tournaments/models/match.py @@ -307,7 +307,7 @@ class Match(TournamentSubModel): return self.start_date.astimezone(timezone) def local_planned_start_date(self): - timezone = self.tournament().timezone() + timezone = self.get_tournament().timezone() return self.planned_start_date.astimezone(timezone) def formatted_start_date(self): diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index c9868ac..86c380d 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -594,7 +594,7 @@ class Tournament(BaseModel): first_match = min(valid_matches, key=lambda match: match.start_date) # Format the date - timezone = first_match.tournament().timezone() + timezone = first_match.get_tournament().timezone() local_start = first_match.start_date.astimezone(timezone) time_format = 'l d M' formatted_schedule = f" - {formats.date_format(local_start, format=time_format)}" From 2fa01108d838288a33f5b25b12a1db658a33e0d4 Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 5 Jun 2025 10:19:57 +0200 Subject: [PATCH 02/31] fix crash --- tournaments/models/tournament.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index c9868ac..86c380d 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -594,7 +594,7 @@ class Tournament(BaseModel): first_match = min(valid_matches, key=lambda match: match.start_date) # Format the date - timezone = first_match.tournament().timezone() + timezone = first_match.get_tournament().timezone() local_start = first_match.start_date.astimezone(timezone) time_format = 'l d M' formatted_schedule = f" - {formats.date_format(local_start, format=time_format)}" From 26ee2e49d08b10d93f312c7a5d56c4c29d9af612 Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 5 Jun 2025 15:38:58 +0200 Subject: [PATCH 03/31] sets daily rotating log files --- padelclub_backend/settings.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/padelclub_backend/settings.py b/padelclub_backend/settings.py index 31e51fb..0cd7d5a 100644 --- a/padelclub_backend/settings.py +++ b/padelclub_backend/settings.py @@ -189,8 +189,11 @@ LOGGING = { }, 'file': { 'level': 'DEBUG', - 'class': 'logging.FileHandler', - 'filename': os.path.join(BASE_DIR, 'django.log'), + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': os.path.join(BASE_DIR, 'logs', 'django.log'), + 'when': 'midnight', + 'interval': 1, + 'backupCount': 30, 'formatter': 'verbose', }, 'rotating_file': { From 99be99019bd7606d78720a886731e10f6b97f00a Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 5 Jun 2025 16:00:16 +0200 Subject: [PATCH 04/31] fix log --- padelclub_backend/settings.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/padelclub_backend/settings.py b/padelclub_backend/settings.py index 0cd7d5a..31e51fb 100644 --- a/padelclub_backend/settings.py +++ b/padelclub_backend/settings.py @@ -189,11 +189,8 @@ LOGGING = { }, 'file': { 'level': 'DEBUG', - 'class': 'logging.handlers.TimedRotatingFileHandler', - 'filename': os.path.join(BASE_DIR, 'logs', 'django.log'), - 'when': 'midnight', - 'interval': 1, - 'backupCount': 30, + 'class': 'logging.FileHandler', + 'filename': os.path.join(BASE_DIR, 'django.log'), 'formatter': 'verbose', }, 'rotating_file': { From 70f4f343aa96cafd95e4522123dc7fb0e9862979 Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 5 Jun 2025 16:13:31 +0200 Subject: [PATCH 05/31] fix logs --- padelclub_backend/settings.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/padelclub_backend/settings.py b/padelclub_backend/settings.py index 31e51fb..4859194 100644 --- a/padelclub_backend/settings.py +++ b/padelclub_backend/settings.py @@ -168,6 +168,9 @@ AUTHENTICATION_BACKENDS = [ CSRF_COOKIE_SECURE = True # if using HTTPS SESSION_COOKIE_SECURE = True +LOGS_DIR = os.path.join(BASE_DIR, 'logs') +os.makedirs(LOGS_DIR, exist_ok=True) + LOGGING = { 'version': 1, 'disable_existing_loggers': False, @@ -196,7 +199,7 @@ LOGGING = { 'rotating_file': { 'level': 'INFO', 'class': 'logging.handlers.RotatingFileHandler', - 'filename': os.path.join(BASE_DIR, 'django.log'), + 'filename': os.path.join(LOGS_DIR, 'django.log'), 'maxBytes': 10 * 1024 * 1024, 'backupCount': 10, 'formatter': 'verbose', From 28f89d3ca8054aabb701b54541b083a9ec669700 Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 5 Jun 2025 17:54:36 +0200 Subject: [PATCH 06/31] remove data_access_ids from the admin --- sync/admin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sync/admin.py b/sync/admin.py index 16d3c77..cb97d80 100644 --- a/sync/admin.py +++ b/sync/admin.py @@ -4,6 +4,9 @@ from django.utils import timezone from .models import BaseModel, ModelLog, DataAccess class SyncedObjectAdmin(admin.ModelAdmin): + + exclude = ('data_access_ids',) + def save_model(self, request, obj, form, change): if isinstance(obj, BaseModel): obj.last_updated_by = request.user From 38843a996a45f5524087020d6b6d056719d46ed7 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 6 Jun 2025 10:47:11 +0200 Subject: [PATCH 07/31] 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 08/31] 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 09/31] 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 10/31] 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 11/31] 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 12/31] 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 13/31] 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 14/31] 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 15/31] 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 16/31] 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 17/31] 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 18/31] 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 19/31] 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 = [] From 72b0281a07ef387caf571fee14e9c19cf32ba9d5 Mon Sep 17 00:00:00 2001 From: Laurent Date: Sun, 8 Jun 2025 10:38:56 +0200 Subject: [PATCH 20/31] add raw id field for agents --- tournaments/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tournaments/admin.py b/tournaments/admin.py index 6be27c2..d6f6561 100644 --- a/tournaments/admin.py +++ b/tournaments/admin.py @@ -21,6 +21,7 @@ class CustomUserAdmin(UserAdmin): list_display = ['email', 'first_name', 'last_name', 'username', 'date_joined', 'latest_event_club_name', 'is_active', 'event_count', 'origin', 'registration_payment_mode', 'licence_id'] list_filter = ['is_active', 'origin'] ordering = ['-date_joined'] + raw_id_fields = ['agents'] fieldsets = [ (None, {'fields': ['id', 'username', 'email', 'password', 'first_name', 'last_name', 'is_active', 'registration_payment_mode', 'clubs', 'country', 'phone', 'licence_id', 'umpire_code', From 48df72d2c3a47b5e8ddc1b06c260f90111ea77e8 Mon Sep 17 00:00:00 2001 From: Laurent Date: Sun, 8 Jun 2025 16:18:07 +0200 Subject: [PATCH 21/31] adds can_sync in the user admin --- tournaments/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tournaments/admin.py b/tournaments/admin.py index 0ca5ef2..f1b0435 100644 --- a/tournaments/admin.py +++ b/tournaments/admin.py @@ -33,7 +33,7 @@ class CustomUserAdmin(UserAdmin): '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', 'origin', 'agents', 'should_synchronize' + 'loser_bracket_match_format_preference', 'device_id', 'loser_bracket_mode', 'origin', 'agents', 'should_synchronize', 'can_synchronize' ]}), ] From e7979427c312199a97d90d95b02ab15f73b8c9b2 Mon Sep 17 00:00:00 2001 From: Laurent Date: Tue, 10 Jun 2025 10:58:47 +0200 Subject: [PATCH 22/31] adds TeamScore search by name --- tournaments/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tournaments/admin.py b/tournaments/admin.py index f1b0435..3b67f56 100644 --- a/tournaments/admin.py +++ b/tournaments/admin.py @@ -299,7 +299,7 @@ class TeamRegistrationAdmin(SyncedObjectAdmin): class TeamScoreAdmin(SyncedObjectAdmin): list_display = ['team_registration', 'score', 'walk_out', 'match'] list_filter = [TeamScoreTournamentListFilter] - search_fields = ['id'] + search_fields = ['id', 'team_registration__player_registrations__first_name', 'team_registration__player_registrations__last_name'] raw_id_fields = ['team_registration', 'match'] # Add this line list_per_page = 50 # Controls pagination on the list view From e3a7096216a03b2250f260230b1f2e837ab63cf9 Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 12 Jun 2025 14:57:21 +0200 Subject: [PATCH 23/31] add signals log --- sync/signals.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/sync/signals.py b/sync/signals.py index 9ce0b59..5574a36 100644 --- a/sync/signals.py +++ b/sync/signals.py @@ -6,11 +6,14 @@ from .models import DataAccess, ModelLog, ModelOperation, BaseModel, SideStoreMo from authentication.models import Device from django.contrib.auth import get_user_model -from django.utils import timezone from .ws_sender import websocket_sender from .registry import device_registry, related_users_registry +import logging + +logger = logging.getLogger(__name__) + User = get_user_model() ### Sync @@ -28,7 +31,9 @@ def presave_handler(sender, instance, **kwargs): return users = related_users(instance) - # print(f'* impacted users = {users}') + + logger.info(f'* {signal} : {instance.__class__.__name__} > impacted users = {users}') + related_users_registry.register(instance.id, users) # user_ids = [user.id for user in users] @@ -129,7 +134,7 @@ def save_model_log(users, model_operation, model_name, model_id, store_id): device_id=device_id ) for user in users - if user.can_synchronize + # if user.can_synchronize ] with transaction.atomic(): @@ -302,18 +307,8 @@ def related_users(instance): users.add(instance) elif isinstance(instance, BaseModel): users.add(instance.related_user) - # users.add(instance.last_updated_by) - - # look in hierarchy - # related_instances = instance.related_instances() - # print(f'related_instances = {related_instances}') - # related_users = [ri.related_user for ri in related_instances if isinstance(ri, BaseModel)] - # users.update(related_users) - data_access_list = DataAccess.objects.filter(id__in=instance.data_access_ids) - # look in related DataAccess - # data_access_list = instances_related_data_access(instance, related_instances) # print(f'instance = {instance.__class__.__name__}, data access count = {len(data_access_list)}') for data_access in data_access_list: users.add(data_access.related_user) From b64a0fb6b63194149d0fbc73cdfdffc1ad4af126 Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 12 Jun 2025 14:58:36 +0200 Subject: [PATCH 24/31] add signals log --- sync/signals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sync/signals.py b/sync/signals.py index 5574a36..0b3fde6 100644 --- a/sync/signals.py +++ b/sync/signals.py @@ -116,7 +116,7 @@ def save_model_log_if_possible(instance, signal, created): save_model_log(users, operation, model_name, instance.id, store_id) else: - print(f'>>> Model Log could not be created because no linked user could be found: {instance.__class__.__name__} {instance}, {signal}') + logger.info(f'>>> Model Log could not be created because no linked user could be found: {instance.__class__.__name__} {instance}, {signal}') def save_model_log(users, model_operation, model_name, model_id, store_id): From de1bcb1c710e75d6fb3262fa9d13f5418155b665 Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 12 Jun 2025 15:05:07 +0200 Subject: [PATCH 25/31] improve log --- sync/signals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sync/signals.py b/sync/signals.py index 0b3fde6..cf042ca 100644 --- a/sync/signals.py +++ b/sync/signals.py @@ -32,7 +32,7 @@ def presave_handler(sender, instance, **kwargs): users = related_users(instance) - logger.info(f'* {signal} : {instance.__class__.__name__} > impacted users = {users}') + logger.info(f'* {signal.__class__.__name__} : {instance.__class__.__name__} > impacted users = {users}') related_users_registry.register(instance.id, users) # user_ids = [user.id for user in users] From 484c3560bc0e8d571b46cf68ded27847121ed861 Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 12 Jun 2025 15:08:19 +0200 Subject: [PATCH 26/31] improve loggs --- sync/signals.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sync/signals.py b/sync/signals.py index cf042ca..807ccc8 100644 --- a/sync/signals.py +++ b/sync/signals.py @@ -32,13 +32,17 @@ def presave_handler(sender, instance, **kwargs): users = related_users(instance) - logger.info(f'* {signal.__class__.__name__} : {instance.__class__.__name__} > impacted users = {users}') - related_users_registry.register(instance.id, users) # user_ids = [user.id for user in users] + if signal == pre_save: detect_foreign_key_changes_for_shared_instances(sender, instance) + sig_type = 'pre_save' + else: + sig_type = 'pre_delete' + logger.info(f'* {sig_type} : {instance.__class__.__name__} > impacted users = {users}') + @receiver([post_save, post_delete]) def synchronization_notifications(sender, instance, created=False, **kwargs): From 83ec420c60d3132d8194f04e5a22a153458add2f Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 12 Jun 2025 15:12:39 +0200 Subject: [PATCH 27/31] logs again --- sync/signals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sync/signals.py b/sync/signals.py index 807ccc8..c067d03 100644 --- a/sync/signals.py +++ b/sync/signals.py @@ -120,13 +120,13 @@ def save_model_log_if_possible(instance, signal, created): save_model_log(users, operation, model_name, instance.id, store_id) else: - logger.info(f'>>> Model Log could not be created because no linked user could be found: {instance.__class__.__name__} {instance}, {signal}') + logger.info(f'!!! Model Log could not be created because no linked user could be found: {instance.__class__.__name__} {instance}, {signal}') def save_model_log(users, model_operation, model_name, model_id, store_id): device_id = device_registry.get_device_id(model_id) - # print(f'>> creating Model Log for: {model_operation} {model_name}') + logger.info(f'*** creating ModelLogs for: {model_operation} {model_name} : {users}') logs_to_create = [ ModelLog( From 156a4ff0ef3793c9df61cf481bc633b63d4d8aba Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 12 Jun 2025 15:21:33 +0200 Subject: [PATCH 28/31] remove bulk_creation of ModelLog - test --- sync/signals.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/sync/signals.py b/sync/signals.py index c067d03..e1eeb21 100644 --- a/sync/signals.py +++ b/sync/signals.py @@ -128,8 +128,9 @@ def save_model_log(users, model_operation, model_name, model_id, store_id): logger.info(f'*** creating ModelLogs for: {model_operation} {model_name} : {users}') - logs_to_create = [ - ModelLog( + with transaction.atomic(): + for user in users: + model_log = ModelLog( user=user, operation=model_operation, model_name=model_name, @@ -137,12 +138,7 @@ def save_model_log(users, model_operation, model_name, model_id, store_id): store_id=store_id, device_id=device_id ) - for user in users - # if user.can_synchronize - ] - - with transaction.atomic(): - ModelLog.objects.bulk_create(logs_to_create) + model_log.save() # with transaction.atomic(): # for user in users: From 2f34664f2d4a29301c07985bb47365cbef6108ff Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 12 Jun 2025 15:29:41 +0200 Subject: [PATCH 29/31] more logs ! --- sync/models/data_access.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sync/models/data_access.py b/sync/models/data_access.py index e4ba81d..0f33c25 100644 --- a/sync/models/data_access.py +++ b/sync/models/data_access.py @@ -8,7 +8,9 @@ from ..registry import model_registry import uuid from . import ModelLog, SideStoreModel, BaseModel +import logging +logger = logging.getLogger(__name__) class DataAccess(BaseModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4) @@ -40,6 +42,9 @@ class DataAccess(BaseModel): store_id = obj.store_id for user in users: + + logger.info(f'=== create ModelLog for: {operation} > {users}') + existing_log = ModelLog.objects.filter(user=user, model_id=self.model_id, operation=operation).first() if existing_log: existing_log.date = timezone.now() @@ -55,9 +60,10 @@ class DataAccess(BaseModel): store_id=store_id ) except ObjectDoesNotExist: + logger.info('object does not exists any more: {self.model_name} : {self.model_id}') pass else: - print(f'model not found: {self.model_name}') + logger.info(f'model not found: {self.model_name}') def add_references(self): model_class = model_registry.get_model(self.model_name) From efbe72d67547af709947f1d5a0cb287b28b23e32 Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 12 Jun 2025 15:34:30 +0200 Subject: [PATCH 30/31] adds atomic transaction for sharing ModelLog --- sync/models/data_access.py | 4 ++-- sync/signals.py | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/sync/models/data_access.py b/sync/models/data_access.py index 0f33c25..5a38ae3 100644 --- a/sync/models/data_access.py +++ b/sync/models/data_access.py @@ -60,10 +60,10 @@ class DataAccess(BaseModel): store_id=store_id ) except ObjectDoesNotExist: - logger.info('object does not exists any more: {self.model_name} : {self.model_id}') + logger.warn(f'!!! object does not exists any more: {self.model_name} : {self.model_id} : {operation}') pass else: - logger.info(f'model not found: {self.model_name}') + logger.warn(f'!!!model not found: {self.model_name}') def add_references(self): model_class = model_registry.get_model(self.model_name) diff --git a/sync/signals.py b/sync/signals.py index e1eeb21..ed7c2fe 100644 --- a/sync/signals.py +++ b/sync/signals.py @@ -263,10 +263,11 @@ def handle_shared_with_changes(sender, instance, action, pk_set, **kwargs): # print(f'm2m changed = {pk_set}') users = User.objects.filter(id__in=pk_set) - if action == "post_add": - instance.create_access_log(users, 'SHARED_ACCESS') - elif action == "post_remove": - instance.create_access_log(users, 'REVOKED_ACCESS') + with transaction.atomic(): + if action == "post_add": + instance.create_access_log(users, 'SHARED_ACCESS') + elif action == "post_remove": + instance.create_access_log(users, 'REVOKED_ACCESS') device_id = device_registry.get_device_id(instance.id) websocket_sender.send_message(pk_set, device_id) From 8d1b3dbdc963934f59d0595a9a0e23da6258ee7c Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 12 Jun 2025 15:44:21 +0200 Subject: [PATCH 31/31] add logs for investigation --- sync/signals.py | 49 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/sync/signals.py b/sync/signals.py index ed7c2fe..e0c0fba 100644 --- a/sync/signals.py +++ b/sync/signals.py @@ -96,7 +96,14 @@ def notify_impacted_users(instance): def save_model_log_if_possible(instance, signal, created): users = related_users_registry.get_users(instance.id) - # print(f'*** save_model_log >>> users = {users}, instance = {instance}') + logger.debug(f'*** save_model_log_if_possible >>> users from registry = {users}, instance = {instance}') + + if not users: + logger.warning(f'!!! Registry returned empty users for instance {instance.id} ({instance.__class__.__name__})') + # Try to recalculate users as fallback + users = related_users(instance) + logger.info(f'!!! Recalculated users for fallback: {users}') + if users: if signal == post_save or signal == pre_save: if created: @@ -128,17 +135,35 @@ def save_model_log(users, model_operation, model_name, model_id, store_id): logger.info(f'*** creating ModelLogs for: {model_operation} {model_name} : {users}') - with transaction.atomic(): - for user in users: - model_log = ModelLog( - user=user, - operation=model_operation, - model_name=model_name, - model_id=model_id, - store_id=store_id, - device_id=device_id - ) - model_log.save() + try: + with transaction.atomic(): + created_logs = [] + for user in users: + logger.debug(f'Creating ModelLog for user {user.id} ({user.username})') + model_log = ModelLog( + user=user, + operation=model_operation, + model_name=model_name, + model_id=model_id, + store_id=store_id, + device_id=device_id + ) + model_log.save() + created_logs.append(model_log.id) + logger.debug(f'Successfully created ModelLog {model_log.id}') + + logger.info(f'*** Successfully created {len(created_logs)} ModelLogs: {created_logs}') + + # Verify ModelLogs were actually persisted + persisted_count = ModelLog.objects.filter(id__in=created_logs).count() + if persisted_count != len(created_logs): + logger.error(f'*** PERSISTENCE VERIFICATION FAILED! Created {len(created_logs)} ModelLogs but only {persisted_count} were persisted to database') + else: + logger.debug(f'*** PERSISTENCE VERIFIED: All {persisted_count} ModelLogs successfully persisted') + + except Exception as e: + logger.error(f'*** FAILED to create ModelLogs for: {model_operation} {model_name}, users: {[u.id for u in users]}, error: {e}', exc_info=True) + raise # with transaction.atomic(): # for user in users: