Laurent 5 months ago
commit e230a00d46
  1. 118
      shop/admin.py
  2. 163
      shop/templates/admin/shop/dashboard.html
  3. 5
      shop/templates/admin/shop/order/change_list.html
  4. 4
      shop/templates/shop/partials/navigation_base.html
  5. 213
      tournaments/admin.py
  6. 15
      tournaments/custom_views.py
  7. 35
      tournaments/forms.py
  8. 446
      tournaments/templates/admin/tournaments/dashboard.html
  9. 18
      tournaments/templates/admin/tournaments/tournament/change_list.html
  10. 26
      tournaments/templates/registration/login.html
  11. 21
      tournaments/templates/registration/signup_success.html
  12. 10
      tournaments/templates/tournaments/navigation_base.html
  13. 31
      tournaments/templates/tournaments/navigation_tournament.html
  14. 5
      tournaments/urls.py
  15. 153
      tournaments/views.py

@ -4,12 +4,72 @@ from django.utils.html import format_html
from django.urls import path from django.urls import path
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django import forms 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 ( from .models import (
Product, Color, Size, Order, OrderItem, GuestUser, Coupon, CouponUsage, Product, Color, Size, Order, OrderItem, GuestUser, Coupon, CouponUsage,
OrderStatus, ShippingAddress 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) @admin.register(Product)
class ProductAdmin(admin.ModelAdmin): class ProductAdmin(admin.ModelAdmin):
list_display = ("title", "ordering_value", "price", "cut") 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): def changelist_view(self, request, extra_context=None):
# If 'show_preparation' parameter is in the request, show the preparation view # If 'show_preparation' parameter is in the request, show the preparation view
if 'show_preparation' in request.GET: if 'show_preparation' in request.GET:
@ -194,9 +305,10 @@ class OrderAdmin(admin.ModelAdmin):
def get_urls(self): def get_urls(self):
urls = super().get_urls() urls = super().get_urls()
custom_urls = [ custom_urls = [
path('prepare-all-orders/', self.admin_site.admin_view(self.prepare_all_orders), name='prepare_all_orders'), path('dashboard/', self.admin_site.admin_view(self.dashboard_view), name='shop_order_dashboard'),
path('prepare-order/<int:order_id>/', self.admin_site.admin_view(self.prepare_order), name='prepare_order'), path('prepare-all/', self.admin_site.admin_view(self.prepare_all_orders), name='prepare_all_orders'),
path('cancel-and-refund-order/<int:order_id>/', self.admin_site.admin_view(self.cancel_and_refund_order), name='cancel_and_refund_order'), path('<int:order_id>/prepare/', self.admin_site.admin_view(self.prepare_order), name='prepare_order'),
path('<int:order_id>/cancel-refund/', self.admin_site.admin_view(self.cancel_and_refund_order), name='cancel_and_refund_order'),
] ]
return custom_urls + urls return custom_urls + urls

@ -0,0 +1,163 @@
{% extends "admin/base_site.html" %}
{% load admin_urls %}
{% block title %}Shop Dashboard{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">Home</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label='shop' %}">Shop</a>
&rsaquo; Dashboard
</div>
{% endblock %}
{% block content %}
<div class="dashboard">
<div class="dashboard-stats" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin: 20px 0;">
<!-- Order Status Cards -->
<div class="stat-card" style="background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 8px; padding: 20px;">
<h3 style="margin: 0 0 15px 0; color: #495057;">Orders by Status</h3>
<div class="status-list">
{% for status_data in order_status_data %}
<div class="status-item" style="display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #e9ecef;">
<span style="font-weight: 500; color: #495057;">{{ status_data.label }}</span>
<div style="display: flex; align-items: center; gap: 10px;">
<span class="count" style="background: #007bff; color: white; padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: bold;">
{{ status_data.count }}
</span>
<span class="total" style="color: #28a745; font-weight: 500;">
€{{ status_data.total_amount|floatformat:2 }}
</span>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Total Summary Card -->
<div class="stat-card" style="background: linear-gradient(135deg, #007bff, #0056b3); color: white; border-radius: 8px; padding: 20px;">
<h3 style="margin: 0 0 15px 0;">Total Summary</h3>
<div class="summary-stats">
<div style="margin-bottom: 10px;">
<div style="font-size: 24px; font-weight: bold;">{{ total_orders }}</div>
<div style="opacity: 0.9;">Total Orders</div>
</div>
<div>
<div style="font-size: 24px; font-weight: bold;">€{{ total_revenue|floatformat:2 }}</div>
<div style="opacity: 0.9;">Total Revenue</div>
</div>
</div>
</div>
<!-- Recent Activity Card -->
<div class="stat-card" style="background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 8px; padding: 20px;">
<h3 style="margin: 0 0 15px 0; color: #495057;">Recent Activity</h3>
<div class="recent-stats">
<div style="margin-bottom: 10px;">
<div style="font-size: 18px; font-weight: bold; color: #28a745;">{{ orders_today }}</div>
<div style="color: #6c757d; font-size: 14px;">Orders Today</div>
</div>
<div style="margin-bottom: 10px;">
<div style="font-size: 18px; font-weight: bold; color: #ffc107;">{{ orders_this_week }}</div>
<div style="color: #6c757d; font-size: 14px;">Orders This Week</div>
</div>
<div>
<div style="font-size: 18px; font-weight: bold; color: #17a2b8;">{{ orders_this_month }}</div>
<div style="color: #6c757d; font-size: 14px;">Orders This Month</div>
</div>
</div>
</div>
<!-- Quick Actions Card -->
<div class="stat-card" style="background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 8px; padding: 20px;">
<h3 style="margin: 0 0 15px 0; color: #495057;">Quick Actions</h3>
<div class="quick-actions">
<a href="{% url 'admin:shop_order_changelist' %}"
style="display: block; padding: 8px 12px; margin-bottom: 8px; background: #007bff; color: white; text-decoration: none; border-radius: 4px; text-align: center;">
View All Orders
</a>
<a href="{% url 'admin:shop_order_changelist' %}?show_preparation=1"
style="display: block; padding: 8px 12px; margin-bottom: 8px; background: #28a745; color: white; text-decoration: none; border-radius: 4px; text-align: center;">
Orders to Prepare ({{ orders_to_prepare }})
</a>
<a href="{% url 'admin:shop_product_changelist' %}"
style="display: block; padding: 8px 12px; margin-bottom: 8px; background: #6c757d; color: white; text-decoration: none; border-radius: 4px; text-align: center;">
Manage Products
</a>
<a href="{% url 'admin:shop_coupon_changelist' %}"
style="display: block; padding: 8px 12px; background: #ffc107; color: #212529; text-decoration: none; border-radius: 4px; text-align: center;">
Manage Coupons
</a>
</div>
</div>
</div>
<!-- Detailed Status Breakdown -->
<div class="detailed-breakdown" style="background: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 20px; margin-top: 20px;">
<h3 style="margin: 0 0 20px 0; color: #495057;">Status Breakdown</h3>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="background: #f8f9fa;">
<th style="padding: 12px; text-align: left; border-bottom: 2px solid #dee2e6;">Status</th>
<th style="padding: 12px; text-align: center; border-bottom: 2px solid #dee2e6;">Count</th>
<th style="padding: 12px; text-align: center; border-bottom: 2px solid #dee2e6;">Percentage</th>
<th style="padding: 12px; text-align: right; border-bottom: 2px solid #dee2e6;">Total Value</th>
<th style="padding: 12px; text-align: right; border-bottom: 2px solid #dee2e6;">Avg Order Value</th>
</tr>
</thead>
<tbody>
{% for status_data in order_status_data %}
<tr style="border-bottom: 1px solid #dee2e6;">
<td style="padding: 12px; font-weight: 500;">{{ status_data.label }}</td>
<td style="padding: 12px; text-align: center;">
<span style="background: #007bff; color: white; padding: 4px 8px; border-radius: 12px; font-size: 12px;">
{{ status_data.count }}
</span>
</td>
<td style="padding: 12px; text-align: center;">{{ status_data.percentage|floatformat:1 }}%</td>
<td style="padding: 12px; text-align: right; color: #28a745; font-weight: 500;">
€{{ status_data.total_amount|floatformat:2 }}
</td>
<td style="padding: 12px; text-align: right;">
{% if status_data.count > 0 %}
€{{ status_data.avg_order_value|floatformat:2 }}
{% else %}
€0.00
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<style>
.dashboard {
max-width: 1200px;
margin: 0 auto;
}
.stat-card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.quick-actions a:hover {
opacity: 0.9;
}
@media (max-width: 768px) {
.dashboard-stats {
grid-template-columns: 1fr !important;
}
}
</style>
{% endblock %}

@ -2,6 +2,11 @@
{% block object-tools %} {% block object-tools %}
<ul class="object-tools"> <ul class="object-tools">
<li>
<a href="dashboard/" class="viewlink" style="background: #28a745; color: white;">
📊 Dashboard
</a>
</li>
<li> <li>
<a href="?show_preparation=1" class="viewlink"> <a href="?show_preparation=1" class="viewlink">
Orders to Prepare ({{ paid_orders_count }}) Orders to Prepare ({{ paid_orders_count }})

@ -9,4 +9,8 @@
<a href="{% url 'custom-login' %}">Se connecter</a> <a href="{% url 'custom-login' %}">Se connecter</a>
{% endif %} {% endif %}
<a href="{% url 'shop:product_list' %}">La boutique</a> <a href="{% url 'shop:product_list' %}">La boutique</a>
{% if user.is_authenticated and user.is_staff %}
<a href="{% url 'admin:shop_order_dashboard' %}" class="download-button">Tableau de bord boutique</a>
<a href="{% url 'admin:prepare_all_orders' %}" class="download-button">Préparer commandes</a>
{% endif %}
</nav> </nav>

@ -3,8 +3,11 @@ from django.contrib.auth.admin import UserAdmin
from django.utils import timezone from django.utils import timezone
from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION
from django.utils.html import escape 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.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 .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 from .forms import CustomUserCreationForm, CustomUserChangeForm
@ -23,14 +26,17 @@ class CustomUserAdmin(UserAdmin):
ordering = ['-date_joined'] ordering = ['-date_joined']
raw_id_fields = ['agents'] raw_id_fields = ['agents']
fieldsets = [ fieldsets = [
(None, {'fields': ['id', 'username', 'email', 'password', 'first_name', 'last_name', 'is_active', 'registration_payment_mode', (None, {'fields': ['id', 'username', 'email', 'password', 'first_name', 'last_name', 'is_active']}),
'clubs', 'country', 'phone', 'licence_id', 'umpire_code', ('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_message_body', 'summons_message_signature', 'summons_available_payment_methods',
'summons_display_format', 'summons_display_entry_fee', 'summons_use_full_custom_message', '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', '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 = [ add_fieldsets = [
( (
None, None,
@ -86,6 +92,205 @@ class TournamentAdmin(SyncedObjectAdmin):
ordering = ['-start_date'] ordering = ['-start_date']
search_fields = ['id'] 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): class TeamRegistrationAdmin(SyncedObjectAdmin):
list_display = ['player_names', 'group_stage', 'name', 'tournament', 'registration_date'] list_display = ['player_names', 'group_stage', 'name', 'tournament', 'registration_date']
list_filter = [SimpleTournamentListFilter] list_filter = [SimpleTournamentListFilter]

@ -31,6 +31,17 @@ class CustomLoginView(auth_views.LoginView):
# Fall back to default # Fall back to default
return reverse('index') 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): def get(self, request, *args, **kwargs):
# Clear any potential password reset session data # Clear any potential password reset session data
keys_to_clear = [key for key in request.session.keys() 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: for key in keys_to_clear:
del request.session[key] 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) storage = messages.get_messages(request)
for _ in storage: for _ in storage:
pass pass

@ -338,29 +338,52 @@ class EmailOrUsernameAuthenticationForm(AuthenticationForm):
username = self.cleaned_data.get('username') username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password') 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}") logger.info(f"Login attempt with username/email: {username}")
if username and password: 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.user_cache = authenticate(
self.request, self.request,
username=username, username=username,
password=password 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: if self.user_cache is None:
print("Authentication failed") # Debug print
logger.warning("Authentication failed") logger.warning("Authentication failed")
raise forms.ValidationError( raise forms.ValidationError(
"Identifiant/E-mail ou mot de passe incorrect. Les champs sont sensibles à la casse.", "Identifiant/E-mail ou mot de passe incorrect. Les champs sont sensibles à la casse.",
code='invalid_login' code='invalid_login'
) )
else: else:
print(f"Authentication successful for user: {self.user_cache}") # Debug print
logger.info(f"Authentication successful for user: {self.user_cache}") 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) self.confirm_login_allowed(self.user_cache)
return self.cleaned_data return self.cleaned_data

@ -0,0 +1,446 @@
{% extends "admin/base_site.html" %}
{% load admin_urls %}
{% block title %}Tournament Dashboard{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">Home</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label='tournaments' %}">Tournaments</a>
&rsaquo; Dashboard
</div>
{% endblock %}
{% block content %}
<div class="tournament-dashboard">
<h1>🏆 Tournament Dashboard</h1>
<!-- Summary Statistics Cards -->
<div class="dashboard-stats" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin: 20px 0;">
<!-- Running Tournaments Card -->
<div class="stat-card" style="background: linear-gradient(135deg, #28a745, #20c997); color: white; border-radius: 12px; padding: 25px; box-shadow: 0 4px 15px rgba(0,0,0,0.1);">
<h3 style="margin: 0 0 20px 0; display: flex; align-items: center; gap: 10px;">
🎾 Starting Tournaments
</h3>
<div class="tournament-stats">
<div style="display: flex; justify-content: space-between; margin-bottom: 15px;">
<div>
<div style="font-size: 28px; font-weight: bold;">{{ tournaments_today_total }}</div>
<div style="opacity: 0.9; font-size: 14px;">Today</div>
</div>
<div style="text-align: right;">
<div style="font-size: 16px;">{{ tournaments_today_private }}/{{ tournaments_today_public }}</div>
<div style="opacity: 0.9; font-size: 12px;">Private/Public</div>
</div>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 15px;">
<div>
<div style="font-size: 24px; font-weight: bold;">{{ tournaments_week_total }}</div>
<div style="opacity: 0.9; font-size: 14px;">This Week</div>
</div>
<div style="text-align: right;">
<div style="font-size: 16px;">{{ tournaments_week_private }}/{{ tournaments_week_public }}</div>
<div style="opacity: 0.9; font-size: 12px;">Private/Public</div>
</div>
</div>
<div style="display: flex; justify-content: space-between;">
<div>
<div style="font-size: 24px; font-weight: bold;">{{ tournaments_month_total }}</div>
<div style="opacity: 0.9; font-size: 14px;">This Month</div>
</div>
<div style="text-align: right;">
<div style="font-size: 16px;">{{ tournaments_month_private }}/{{ tournaments_month_public }}</div>
<div style="opacity: 0.9; font-size: 12px;">Private/Public</div>
</div>
</div>
</div>
</div>
<!-- Ended Tournaments Card -->
<div class="stat-card" style="background: linear-gradient(135deg, #dc3545, #e74c3c); color: white; border-radius: 12px; padding: 25px; box-shadow: 0 4px 15px rgba(0,0,0,0.1);">
<h3 style="margin: 0 0 20px 0; display: flex; align-items: center; gap: 10px;">
🏁 Ended Tournaments
</h3>
<div class="tournament-stats">
<div style="margin-bottom: 15px;">
<div style="font-size: 28px; font-weight: bold;">{{ tournaments_ended_today }}</div>
<div style="opacity: 0.9; font-size: 14px;">Today</div>
</div>
<div style="margin-bottom: 15px;">
<div style="font-size: 24px; font-weight: bold;">{{ tournaments_ended_week }}</div>
<div style="opacity: 0.9; font-size: 14px;">This Week</div>
</div>
<div style="margin-bottom: 15px;">
<div style="font-size: 24px; font-weight: bold;">{{ tournaments_ended_month }}</div>
<div style="opacity: 0.9; font-size: 14px;">This Month</div>
</div>
<div>
<div style="font-size: 20px; font-weight: bold;">{{ tournaments_ended_all }}</div>
<div style="opacity: 0.9; font-size: 14px;">All Time</div>
</div>
</div>
</div>
<!-- Participants Card -->
<div class="stat-card" style="background: linear-gradient(135deg, #007bff, #0056b3); color: white; border-radius: 12px; padding: 25px; box-shadow: 0 4px 15px rgba(0,0,0,0.1);">
<h3 style="margin: 0 0 20px 0; display: flex; align-items: center; gap: 10px;">
👥 Participants
</h3>
<div class="participant-stats">
<div style="margin-bottom: 20px;">
<div style="font-size: 32px; font-weight: bold;">{{ total_teams }}</div>
<div style="opacity: 0.9; font-size: 16px;">Total Teams</div>
</div>
<div style="margin-bottom: 20px;">
<div style="font-size: 32px; font-weight: bold;">{{ total_players }}</div>
<div style="opacity: 0.9; font-size: 16px;">Total Players</div>
</div>
<div>
<div style="font-size: 20px; font-weight: bold;">{{ avg_teams_per_tournament }}</div>
<div style="opacity: 0.9; font-size: 14px;">Avg Teams/Tournament</div>
</div>
</div>
</div>
<!-- Matches Card -->
<div class="stat-card" style="background: linear-gradient(135deg, #fd7e14, #e55e2b); color: white; border-radius: 12px; padding: 25px; box-shadow: 0 4px 15px rgba(0,0,0,0.1);">
<h3 style="margin: 0 0 20px 0; display: flex; align-items: center; gap: 10px;">
🏓 Matches
</h3>
<div class="match-stats">
<div style="margin-bottom: 20px;">
<div style="font-size: 32px; font-weight: bold;">{{ total_matches }}</div>
<div style="opacity: 0.9; font-size: 16px;">Total Matches</div>
</div>
<div style="margin-bottom: 20px;">
<div style="font-size: 28px; font-weight: bold; color: #90EE90;">{{ matches_played }}</div>
<div style="opacity: 0.9; font-size: 14px;">Played</div>
</div>
<div>
<div style="font-size: 24px; font-weight: bold; color: #FFB6C1;">{{ matches_pending }}</div>
<div style="opacity: 0.9; font-size: 14px;">Pending</div>
</div>
</div>
</div>
</div>
<!-- New User Statistics -->
<div class="user-section" style="background: white; border: 1px solid #dee2e6; border-radius: 12px; padding: 25px; margin: 20px 0;">
<h3 style="margin: 0 0 20px 0; color: #495057; display: flex; align-items: center; gap: 10px;">
👤 User Statistics
</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px;">
<!-- User Counts by Origin -->
<div style="background: #f8f9fa; border-radius: 8px; padding: 20px;">
<h4 style="margin: 0 0 15px 0;">Total Users: {{ total_users }}</h4>
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
<span>Admin:</span>
<strong>{{ users_admin }}</strong>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
<span>Site:</span>
<strong>{{ users_site }}</strong>
</div>
<div style="display: flex; justify-content: space-between;">
<span>App:</span>
<strong>{{ users_app }}</strong>
</div>
<div style="margin-top: 15px;">
<div style="height: 20px; background: #e9ecef; border-radius: 10px; overflow: hidden; display: flex;">
{% if total_users > 0 %}
<div style="width: {{ users_admin|floatformat:0 }}%; height: 100%; background: #6c757d;" title="Admin: {{ users_admin }}"></div>
<div style="width: {{ users_site|floatformat:0 }}%; height: 100%; background: #28a745;" title="Site: {{ users_site }}"></div>
<div style="width: {{ users_app|floatformat:0 }}%; height: 100%; background: #007bff;" title="App: {{ users_app }}"></div>
{% endif %}
</div>
<div style="display: flex; justify-content: space-between; margin-top: 5px; font-size: 12px;">
<span>Admin</span>
<span>Site</span>
<span>App</span>
</div>
</div>
</div>
<!-- New User Registrations -->
<div style="background: #f8f9fa; border-radius: 8px; padding: 20px;">
<h4 style="margin: 0 0 15px 0;">New Registrations</h4>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<span>Today:</span>
<div style="padding: 4px 12px; background: #007bff; color: white; border-radius: 15px; font-weight: bold;">{{ users_today }}</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<span>This Week:</span>
<div style="padding: 4px 12px; background: #28a745; color: white; border-radius: 15px; font-weight: bold;">{{ users_this_week }}</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>This Month:</span>
<div style="padding: 4px 12px; background: #fd7e14; color: white; border-radius: 15px; font-weight: bold;">{{ users_this_month }}</div>
</div>
</div>
<!-- Recent Users -->
<div style="background: #f8f9fa; border-radius: 8px; padding: 20px; grid-column: span 2;">
<h4 style="margin: 0 0 15px 0; display: flex; justify-content: space-between; align-items: center;">
<span>Recently Registered Users</span>
<a href="{% url 'admin:tournaments_customuser_changelist' %}" style="font-size: 14px; text-decoration: none; color: #007bff; display: flex; align-items: center;">
View All Users <span style="margin-left: 5px; font-size: 16px;"></span>
</a>
</h4>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="border-bottom: 2px solid #dee2e6;">
<th style="padding: 8px 12px; text-align: left;">Name</th>
<th style="padding: 8px 12px; text-align: left;">Email</th>
<th style="padding: 8px 12px; text-align: left;">Origin</th>
<th style="padding: 8px 12px; text-align: left;">Date Joined</th>
</tr>
</thead>
<tbody>
{% for user in recent_users %}
<tr style="border-bottom: 1px solid #dee2e6;">
<td style="padding: 8px 12px;">{{ user.first_name }} {{ user.last_name }}</td>
<td style="padding: 8px 12px;">{{ user.email }}</td>
<td style="padding: 8px 12px;">
{% if user.origin == 0 %}
<span style="padding: 2px 6px; background: #6c757d; color: white; border-radius: 4px; font-size: 12px;">Admin</span>
{% elif user.origin == 1 %}
<span style="padding: 2px 6px; background: #28a745; color: white; border-radius: 4px; font-size: 12px;">Site</span>
{% elif user.origin == 2 %}
<span style="padding: 2px 6px; background: #007bff; color: white; border-radius: 4px; font-size: 12px;">App</span>
{% else %}
<span style="padding: 2px 6px; background: #6c757d; color: white; border-radius: 4px; font-size: 12px;">Unknown</span>
{% endif %}
</td>
<td style="padding: 8px 12px;">{{ user.date_joined|date:"M d, Y H:i" }}</td>
</tr>
{% empty %}
<tr>
<td colspan="4" style="padding: 12px; text-align: center;">No recent users found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Purchase Statistics -->
<div class="purchase-section" style="background: white; border: 1px solid #dee2e6; border-radius: 12px; padding: 25px; margin: 20px 0;">
<h3 style="margin: 0 0 20px 0; color: #495057; display: flex; align-items: center; gap: 10px;">
💰 Purchase Statistics
</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px;">
<!-- Purchase Counts -->
<div style="background: #f8f9fa; border-radius: 8px; padding: 20px;">
<h4 style="margin: 0 0 15px 0;">Total Purchases: {{ total_purchases }}</h4>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<span>Today:</span>
<div style="padding: 4px 12px; background: #007bff; color: white; border-radius: 15px; font-weight: bold;">{{ purchases_today }}</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<span>This Week:</span>
<div style="padding: 4px 12px; background: #28a745; color: white; border-radius: 15px; font-weight: bold;">{{ purchases_this_week }}</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>This Month:</span>
<div style="padding: 4px 12px; background: #fd7e14; color: white; border-radius: 15px; font-weight: bold;">{{ purchases_this_month }}</div>
</div>
</div>
<!-- Recent Purchases -->
<div style="background: #f8f9fa; border-radius: 8px; padding: 20px; grid-column: span 3;">
<h4 style="margin: 0 0 15px 0;">Recent Purchases</h4>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="border-bottom: 2px solid #dee2e6;">
<th style="padding: 8px 12px; text-align: left;">ID</th>
<th style="padding: 8px 12px; text-align: left;">User</th>
<th style="padding: 8px 12px; text-align: left;">Product</th>
<th style="padding: 8px 12px; text-align: left;">Quantity</th>
<th style="padding: 8px 12px; text-align: left;">Date</th>
</tr>
</thead>
<tbody>
{% for purchase in recent_purchases %}
<tr style="border-bottom: 1px solid #dee2e6;">
<td style="padding: 8px 12px;">{{ purchase.id }}</td>
<td style="padding: 8px 12px;">{{ purchase.user.email }}</td>
<td style="padding: 8px 12px;">{{ purchase.product_id }}</td>
<td style="padding: 8px 12px;">{{ purchase.quantity }}</td>
<td style="padding: 8px 12px;">{{ purchase.purchase_date|date:"M d, Y H:i" }}</td>
</tr>
{% empty %}
<tr>
<td colspan="5" style="padding: 12px; text-align: center;">No recent purchases found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- All Time Overview -->
<div class="all-time-section" style="background: white; border: 1px solid #dee2e6; border-radius: 12px; padding: 25px; margin: 20px 0;">
<h3 style="margin: 0 0 20px 0; color: #495057; display: flex; align-items: center; gap: 10px;">
📊 All Time Overview
</h3>
<div class="overview-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px;">
<div class="overview-item" style="text-align: center; padding: 15px; background: #f8f9fa; border-radius: 8px;">
<div style="font-size: 24px; font-weight: bold; color: #28a745;">{{ tournaments_all_total }}</div>
<div style="color: #6c757d; margin-top: 5px;">Total Tournaments</div>
<div style="font-size: 12px; color: #6c757d; margin-top: 5px;">
{{ tournaments_all_private }} Private | {{ tournaments_all_public }} Public
</div>
</div>
<div class="overview-item" style="text-align: center; padding: 15px; background: #f8f9fa; border-radius: 8px;">
<div style="font-size: 24px; font-weight: bold; color: #007bff;">{{ tournaments_with_online_reg }}</div>
<div style="color: #6c757d; margin-top: 5px;">Online Registration</div>
</div>
<div class="overview-item" style="text-align: center; padding: 15px; background: #f8f9fa; border-radius: 8px;">
<div style="font-size: 24px; font-weight: bold; color: #ffc107;">{{ tournaments_with_payment }}</div>
<div style="color: #6c757d; margin-top: 5px;">Online Payment</div>
</div>
<div class="overview-item" style="text-align: center; padding: 15px; background: #f8f9fa; border-radius: 8px;">
<div style="font-size: 24px; font-weight: bold; color: #17a2b8;">€{{ avg_entry_fee }}</div>
<div style="color: #6c757d; margin-top: 5px;">Avg Entry Fee</div>
</div>
</div>
</div>
<!-- Detailed Breakdown Table -->
<div class="detailed-breakdown" style="background: white; border: 1px solid #dee2e6; border-radius: 12px; padding: 25px; margin-top: 20px;">
<h3 style="margin: 0 0 20px 0; color: #495057;">📈 Tournament Breakdown</h3>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="background: #f8f9fa;">
<th style="padding: 15px; text-align: left; border-bottom: 2px solid #dee2e6;">Period</th>
<th style="padding: 15px; text-align: center; border-bottom: 2px solid #dee2e6;">Running</th>
<th style="padding: 15px; text-align: center; border-bottom: 2px solid #dee2e6;">Private</th>
<th style="padding: 15px; text-align: center; border-bottom: 2px solid #dee2e6;">Public</th>
<th style="padding: 15px; text-align: center; border-bottom: 2px solid #dee2e6;">Ended</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom: 1px solid #dee2e6;">
<td style="padding: 15px; font-weight: 500;">Today</td>
<td style="padding: 15px; text-align: center;">
<span style="background: #28a745; color: white; padding: 6px 12px; border-radius: 15px; font-weight: bold;">
{{ tournaments_today_total }}
</span>
</td>
<td style="padding: 15px; text-align: center; color: #6c757d;">{{ tournaments_today_private }}</td>
<td style="padding: 15px; text-align: center; color: #6c757d;">{{ tournaments_today_public }}</td>
<td style="padding: 15px; text-align: center; color: #dc3545; font-weight: 500;">{{ tournaments_ended_today }}</td>
</tr>
<tr style="border-bottom: 1px solid #dee2e6;">
<td style="padding: 15px; font-weight: 500;">This Week</td>
<td style="padding: 15px; text-align: center;">
<span style="background: #28a745; color: white; padding: 6px 12px; border-radius: 15px; font-weight: bold;">
{{ tournaments_week_total }}
</span>
</td>
<td style="padding: 15px; text-align: center; color: #6c757d;">{{ tournaments_week_private }}</td>
<td style="padding: 15px; text-align: center; color: #6c757d;">{{ tournaments_week_public }}</td>
<td style="padding: 15px; text-align: center; color: #dc3545; font-weight: 500;">{{ tournaments_ended_week }}</td>
</tr>
<tr style="border-bottom: 1px solid #dee2e6;">
<td style="padding: 15px; font-weight: 500;">This Month</td>
<td style="padding: 15px; text-align: center;">
<span style="background: #28a745; color: white; padding: 6px 12px; border-radius: 15px; font-weight: bold;">
{{ tournaments_month_total }}
</span>
</td>
<td style="padding: 15px; text-align: center; color: #6c757d;">{{ tournaments_month_private }}</td>
<td style="padding: 15px; text-align: center; color: #6c757d;">{{ tournaments_month_public }}</td>
<td style="padding: 15px; text-align: center; color: #dc3545; font-weight: 500;">{{ tournaments_ended_month }}</td>
</tr>
<tr>
<td style="padding: 15px; font-weight: 500;">All Time</td>
<td style="padding: 15px; text-align: center;">
<span style="background: #007bff; color: white; padding: 6px 12px; border-radius: 15px; font-weight: bold;">
{{ tournaments_all_total }}
</span>
</td>
<td style="padding: 15px; text-align: center; color: #6c757d;">{{ tournaments_all_private }}</td>
<td style="padding: 15px; text-align: center; color: #6c757d;">{{ tournaments_all_public }}</td>
<td style="padding: 15px; text-align: center; color: #dc3545; font-weight: 500;">{{ tournaments_ended_all }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Quick Actions -->
<div class="quick-actions" style="background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 12px; padding: 25px; margin-top: 20px;">
<h3 style="margin: 0 0 20px 0; color: #495057;">🚀 Quick Actions</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
<a href="{% url 'admin:tournaments_tournament_changelist' %}"
style="display: block; padding: 12px 15px; background: #007bff; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
View All Tournaments
</a>
<a href="{% url 'admin:tournaments_teamregistration_changelist' %}"
style="display: block; padding: 12px 15px; background: #28a745; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
Manage Teams
</a>
<a href="{% url 'admin:tournaments_playerregistration_changelist' %}"
style="display: block; padding: 12px 15px; background: #6c757d; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
Manage Players
</a>
<a href="{% url 'admin:tournaments_match_changelist' %}"
style="display: block; padding: 12px 15px; background: #fd7e14; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
View Matches
</a>
<a href="{% url 'admin:tournaments_event_changelist' %}"
style="display: block; padding: 12px 15px; background: #17a2b8; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
Manage Events
</a>
<a href="{% url 'admin:tournaments_club_changelist' %}"
style="display: block; padding: 12px 15px; background: #ffc107; color: #212529; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;">
Manage Clubs
</a>
</div>
</div>
</div>
<style>
.tournament-dashboard {
max-width: 1400px;
margin: 0 auto;
}
.stat-card {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.stat-card:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}
.quick-actions a:hover {
opacity: 0.9;
transform: translateY(-1px);
}
@media (max-width: 768px) {
.dashboard-stats {
grid-template-columns: 1fr !important;
}
.overview-grid {
grid-template-columns: repeat(2, 1fr) !important;
}
.quick-actions > div {
grid-template-columns: 1fr !important;
}
}
</style>
{% endblock %}

@ -0,0 +1,18 @@
{% extends "admin/change_list.html" %}
{% block object-tools %}
<ul class="object-tools">
<li>
<a href="dashboard/" class="viewlink" style="background: #007bff; color: white;">
📊 Dashboard
</a>
</li>
{% if has_add_permission %}
<li>
<a href="{% url 'admin:tournaments_tournament_add' %}" class="addlink">
Add Tournament
</a>
</li>
{% endif %}
</ul>
{% endblock %}

@ -13,11 +13,9 @@
<div class="cell medium-6 large-6 padding10"> <div class="cell medium-6 large-6 padding10">
{% if form.non_field_errors %} {% if form.non_field_errors %}
<div class="alert alert-error"> <div class="alert alert-error">
{% if form.non_field_errors %}
{% for error in form.non_field_errors %} {% for error in form.non_field_errors %}
<p>{{ error }}</p> <p>{{ error }}</p>
{% endfor %} {% endfor %}
{% endif %}
{% for field in form %} {% for field in form %}
{% for error in field.errors %} {% for error in field.errors %}
@ -26,6 +24,30 @@
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
<!-- Check if there's an inactive user email in session -->
{% if inactive_user_email %}
<div style="margin-top: 15px; padding: 15px; background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 5px;">
<p><strong>Besoin d'aide ?</strong></p>
<p>Si vous n'avez pas reçu l'e-mail d'activation ou si le lien a expiré, vous pouvez en demander un nouveau :</p>
<form method="post" action="{% url 'resend-activation' %}" style="margin-top: 10px;">
{% csrf_token %}
<input type="hidden" name="username_or_email" value="{{ inactive_user_email }}">
{% if request.GET.next %}
<input type="hidden" name="next" value="{{ request.GET.next }}">
{% endif %}
<button type="submit" class="rounded-button" style="background-color: #28a745; margin-top: 10px;">
Renvoyer le lien d'activation
</button>
</form>
<p style="margin-top: 10px; font-size: 14px; color: #6c757d;">
Le lien sera envoyé à : <strong>{{ inactive_user_email }}</strong>
</p>
</div>
{% endif %}
<form method="post" action="{% url 'custom-login' %}"> <form method="post" action="{% url 'custom-login' %}">
{% csrf_token %} {% csrf_token %}
{% if request.GET.next and 'reset' not in request.GET.next and 'password_reset' not in request.GET.next %} {% if request.GET.next and 'reset' not in request.GET.next and 'password_reset' not in request.GET.next %}

@ -20,6 +20,27 @@
<li>Vérifier votre boîte de réception (et vos spams si nécessaire)</li> <li>Vérifier votre boîte de réception (et vos spams si nécessaire)</li>
<li>Cliquer sur le lien de confirmation dans l'e-mail</li> <li>Cliquer sur le lien de confirmation dans l'e-mail</li>
</ol> </ol>
<!-- Resend activation section -->
<div style="margin-top: 20px; padding: 15px; background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 5px;">
<p><strong>Vous n'avez pas reçu l'e-mail ?</strong></p>
<p>Si l'e-mail de confirmation n'arrive pas dans les prochaines minutes, vous pouvez en demander un nouveau :</p>
<form method="post" action="{% url 'resend-activation' %}" style="margin-top: 10px;">
{% csrf_token %}
<input type="hidden" name="username_or_email" value="{{ user_email }}">
{% if next_url %}
<input type="hidden" name="next" value="{{ next_url }}">
{% endif %}
<button type="submit" class="rounded-button" style="background-color: #28a745; margin-top: 10px;">
Renvoyer l'e-mail d'activation
</button>
</form>
<p style="margin-top: 10px; font-size: 14px; color: #6c757d;">
Le lien sera envoyé à : <strong>{{ user_email }}</strong>
</p>
</div>
</div> </div>
<div class="button-container"> <div class="button-container">
<a href="{{ next_url }}" class="rounded-button">Continuer</a> <a href="{{ next_url }}" class="rounded-button">Continuer</a>

@ -1,5 +1,4 @@
<nav class="margin10">
<nav class="margin10">
<a href="{% url 'index' %}" class="orange">Accueil</a> <a href="{% url 'index' %}" class="orange">Accueil</a>
<a href="{% url 'clubs' %}" class="orange">Clubs</a> <a href="{% url 'clubs' %}" class="orange">Clubs</a>
{% if user.is_authenticated %} {% if user.is_authenticated %}
@ -10,5 +9,10 @@
<a href="{% url 'custom-login' %}">Se connecter</a> <a href="{% url 'custom-login' %}">Se connecter</a>
{% endif %} {% endif %}
<a href="{% url 'shop:product_list' %}">La boutique</a> <a href="{% url 'shop:product_list' %}">La boutique</a>
{% if user.is_authenticated and user.is_staff %}
<a href="{% url 'private-tournaments' %}" class="orange">Tournois privés</a>
<a href="{% url 'admin:tournaments_tournament_dashboard' %}" class="download-button">Tableau de bord</a>
{% else %}
<a href="{% url 'download' %}" class="download-button">Ajouter vos tournois</a> <a href="{% url 'download' %}" class="download-button">Ajouter vos tournois</a>
</nav> {% endif %}
</nav>

@ -37,4 +37,35 @@
{% else %} {% else %}
<a href="{% url 'login' %}" class="topmargin5">Se connecter</a> <a href="{% url 'login' %}" class="topmargin5">Se connecter</a>
{% endif %} {% endif %}
<!-- Superuser private toggle form -->
{% if user.is_superuser %}
<form method="post" action="{% url 'toggle_tournament_private' tournament.id %}" style="display: inline;">
{% csrf_token %}
<button
type="submit"
class="topmargin5"
title="Changer la visibilité du tournoi"
onclick="return confirm('Êtes-vous sûr de vouloir changer la visibilité de ce tournoi ?{% if tournament.is_private %}\n\nCe tournoi deviendra public et sera visible par tous.{% else %}\n\nCe tournoi deviendra privé et ne sera visible que par les administrateurs.{% endif %}')"
style="
padding: 8px 12px;
font-size: 0.9em;
border-radius: 4px;
background: {% if tournament.is_private %}#dc3545{% else %}#28a745{% endif %};
color: white;
cursor: pointer;
font-weight: bold;
margin-top: 5px;
display: block;
width: auto;
font-family: inherit;
">
{% if tournament.is_private %}
🔒 Tournoi Privé
{% else %}
🌐 Tournoi Public
{% endif %}
</button>
</form>
{% endif %}
</nav> </nav>

@ -60,7 +60,7 @@ urlpatterns = [
path('logout/', views.custom_logout, name='custom_logout'), path('logout/', views.custom_logout, name='custom_logout'),
path('utils/xls-to-csv/', views.xls_to_csv, name='xls-to-csv'), path('utils/xls-to-csv/', views.xls_to_csv, name='xls-to-csv'),
path('signup/', views.signup, name='signup'), # URL pattern for signup 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('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('all_my_ended_tournaments/', views.all_my_ended_tournaments, name='all-my-ended-tournaments'), # URL pattern for signup
path('tournaments/<str:tournament_id>/cancel-registration/', views.cancel_registration, name='cancel_registration'), path('tournaments/<str:tournament_id>/cancel-registration/', views.cancel_registration, name='cancel_registration'),
@ -80,4 +80,7 @@ urlpatterns = [
path('tournaments/<str:tournament_id>/confirm/', views.confirm_tournament_registration, name='confirm_tournament_registration'), path('tournaments/<str:tournament_id>/confirm/', views.confirm_tournament_registration, name='confirm_tournament_registration'),
path('stripe-onboarding-complete/', views.stripe_onboarding_complete, name='stripe-onboarding-complete'), 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('stripe-refresh-account-link/', views.stripe_refresh_account_link, name='stripe-refresh-account-link'),
path('tournaments/<str:tournament_id>/toggle-private/', views.toggle_tournament_private, name='toggle_tournament_private'),
path("private-tournaments/", views.private_tournaments, name="private-tournaments"),
] ]

@ -730,6 +730,43 @@ def send_verification_email(request, user, next_url):
email.content_subtype = "html" email.content_subtype = "html"
email.send() 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 @login_required
def profile(request): def profile(request):
user = request.user # Get the currently authenticated user user = request.user # Get the currently authenticated user
@ -1622,6 +1659,122 @@ def stripe_onboarding_complete(request):
def stripe_refresh_account_link(request): def stripe_refresh_account_link(request):
return render(request, 'stripe/refresh_account_link.html') 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): class UserListExportView(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):

Loading…
Cancel
Save