diff --git a/shop/admin.py b/shop/admin.py index bce87cf..a3febdc 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,10 @@ 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'), + 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 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 }}) 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/admin.py b/tournaments/admin.py index d6f6561..0ca5ef2 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 @@ -23,14 +26,17 @@ class CustomUserAdmin(UserAdmin): 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', + (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, @@ -86,6 +92,205 @@ 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_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 - tournaments starting TODAY + tournaments_today = Tournament.objects.filter( + 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 - tournaments starting THIS WEEK + tournaments_this_week = Tournament.objects.filter( + 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 - tournaments starting THIS MONTH + tournaments_this_month = Tournament.objects.filter( + 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() + 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 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_start, + end_date__date__lte=week_end + ).exclude(is_deleted=True) + + tournaments_ended_month = Tournament.objects.filter( + 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__date__lt=today + ).exclude(is_deleted=True) + + # 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() + 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 + + # 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', + 'opts': Tournament._meta, + + # 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 (tournaments STARTING this week) + 'tournaments_week_total': tournaments_week_total, + 'tournaments_week_private': tournaments_week_private, + 'tournaments_week_public': tournaments_week_public, + + # Month statistics (tournaments STARTING this month) + '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 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(), + '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), + + # 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) + + 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/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/admin/tournaments/dashboard.html b/tournaments/templates/admin/tournaments/dashboard.html new file mode 100644 index 0000000..b6f6fb1 --- /dev/null +++ b/tournaments/templates/admin/tournaments/dashboard.html @@ -0,0 +1,446 @@ +{% extends "admin/base_site.html" %} +{% load admin_urls %} + +{% block title %}Tournament Dashboard{% endblock %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} +
    +

    🏆 Tournament Dashboard

    + + +
    + + +
    +

    + 🎾 Starting 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
    +
    +
    +
    +
    + + +
    +

    + 👤 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 + + View All 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.
    +
    +
    +
    +
    + + +
    +

    + 📊 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 %} 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/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 diff --git a/tournaments/templates/tournaments/navigation_base.html b/tournaments/templates/tournaments/navigation_base.html index b1ef0ea..7e07f5e 100644 --- a/tournaments/templates/tournaments/navigation_base.html +++ b/tournaments/templates/tournaments/navigation_base.html @@ -1,14 +1,18 @@ - - + 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 6867a7c..7b400c3 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'), @@ -80,4 +80,7 @@ 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'), + path("private-tournaments/", views.private_tournaments, name="private-tournaments"), + ] diff --git a/tournaments/views.py b/tournaments/views.py index 0db52eb..c9ff7e9 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 @@ -1622,6 +1659,122 @@ 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) + +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) + + # 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 tournaments that should be displayed + display_tournament = tournaments + + # 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) + + # 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()] + + # Combine both lists + finished = clean_ended_tournaments + ended_tournaments + + # Sort the combined list by start_date in descending order + finished.sort(key=lambda t: t.sorting_finished_date(), reverse=True) + + return render( + request, + "tournaments/tournaments.html", + { + 'future': future[:10], + 'live': live[: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 + } + ) class UserListExportView(LoginRequiredMixin, View): def get(self, request, *args, **kwargs):