You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
padelclub_backend/tournaments/admin.py

493 lines
21 KiB

from django.contrib import admin
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, 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
from .filters import TeamScoreTournamentListFilter, MatchTournamentListFilter, SimpleTournamentListFilter, MatchTypeListFilter, SimpleIndexListFilter, StartDateRangeFilter, UserWithEventsFilter
from sync.admin import SyncedObjectAdmin
class CustomUserAdmin(UserAdmin):
form = CustomUserChangeForm
add_form = CustomUserCreationForm
model = CustomUser
search_fields = ['username', 'email', 'phone', 'first_name', 'last_name', 'licence_id']
filter_horizontal = ('clubs',)
list_display = ['email', 'first_name', 'last_name', 'username', 'date_joined', 'latest_event_club_name', 'is_active', 'event_count', 'origin', 'registration_payment_mode', 'licence_id']
list_filter = ['is_active', 'origin', UserWithEventsFilter]
ordering = ['-date_joined']
raw_id_fields = ['agents']
fieldsets = [
(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', 'origin', 'agents', 'should_synchronize', 'can_synchronize'
]}),
]
add_fieldsets = [
(
None,
{
"classes": ["wide"],
"fields": ['username', 'email', 'password1', 'password2', 'first_name', 'last_name', 'clubs', 'country', 'phone', 'licence_id', 'umpire_code', 'groups'],
},
),
]
def save_model(self, request, obj, form, change):
obj.last_update = timezone.now()
super().save_model(request, obj, form, change)
class EventAdmin(SyncedObjectAdmin):
list_display = ['creation_date', 'name', 'club', 'creator', 'creator_full_name', 'tenup_id', 'display_images']
list_filter = ['creator', 'club', 'tenup_id']
search_fields = ['name', 'club__name', 'creator__email']
raw_id_fields = ['creator', 'club']
ordering = ['-creation_date']
readonly_fields = ['display_images_preview']
fieldsets = [
(None, {'fields': ['last_update', 'related_user', 'name', 'club', 'creator', 'creation_date', 'tenup_id']}),
('Images', {'fields': ['display_images_preview'], 'classes': ['collapse']}),
]
def display_images(self, obj):
count = obj.images.count()
return count if count > 0 else '-'
display_images.short_description = 'Images'
def display_images_preview(self, obj):
html = '<div style="display: flex; flex-wrap: wrap; gap: 10px;">'
for image in obj.images.all():
html += f'''
<div style="text-align: center; margin-bottom: 15px;">
<img src="{image.image.url}" style="max-width: 150px; max-height: 150px; object-fit: contain;" />
<p style="margin: 5px 0 0 0; font-size: 12px;">
<strong>{image.title or "Untitled"}</strong><br>
Type: {image.get_image_type_display()}<br>
</p>
</div>
'''
html += '</div>'
if not obj.images.exists():
html = '<p>No images uploaded for this event.</p>'
return mark_safe(html)
display_images_preview.short_description = 'Images Preview'
class TournamentAdmin(SyncedObjectAdmin):
list_display = ['display_name', 'event', 'is_private', 'start_date', 'payment', 'creator', 'is_canceled']
list_filter = [StartDateRangeFilter, 'is_deleted', 'event__creator']
ordering = ['-start_date']
search_fields = ['id', 'federal_level_category']
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_app_users = CustomUser.objects.filter(origin=2).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_app_users': recent_app_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]
search_fields = ['id']
class TeamScoreAdmin(SyncedObjectAdmin):
list_display = ['team_registration', 'score', 'walk_out', 'match']
list_filter = [TeamScoreTournamentListFilter]
search_fields = ['id', 'team_registration__player_registrations__first_name', 'team_registration__player_registrations__last_name']
raw_id_fields = ['team_registration', 'match'] # Add this line
list_per_page = 50 # Controls pagination on the list view
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('team_registration', 'match')
class RoundAdmin(SyncedObjectAdmin):
list_display = ['tournament', 'name', 'parent', 'index']
list_filter = [SimpleTournamentListFilter, SimpleIndexListFilter]
search_fields = ['id']
ordering = ['parent', 'index']
raw_id_fields = ['parent'] # Add this line
list_per_page = 50 # Controls pagination on the list view
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('parent')
class PlayerRegistrationAdmin(SyncedObjectAdmin):
list_display = ['first_name', 'last_name', 'licence_id', 'rank']
search_fields = ['id', 'first_name', 'last_name', 'licence_id__icontains']
list_filter = ['registered_online', 'payment_id', TeamScoreTournamentListFilter]
ordering = ['last_name', 'first_name']
raw_id_fields = ['team_registration'] # Add this line
list_per_page = 50 # Controls pagination on the list view
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('team_registration')
class MatchAdmin(SyncedObjectAdmin):
list_display = ['__str__', 'round', 'group_stage', 'start_date', 'end_date', 'index']
list_filter = [MatchTypeListFilter, MatchTournamentListFilter, SimpleIndexListFilter]
ordering = ['-group_stage', 'round', 'index']
raw_id_fields = ['round', 'group_stage'] # Add this line
list_per_page = 50 # Controls pagination on the list view
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('round', 'group_stage')
class GroupStageAdmin(SyncedObjectAdmin):
list_display = ['tournament', 'start_date', 'index']
list_filter = [SimpleTournamentListFilter]
ordering = ['-start_date', 'index']
class ClubAdmin(SyncedObjectAdmin):
list_display = ['name', 'acronym', 'city', 'creator', 'events_count', 'broadcast_code']
search_fields = ['name', 'acronym', 'city']
ordering = ['creator']
raw_id_fields = ['creator', 'related_user']
class PurchaseAdmin(SyncedObjectAdmin):
list_display = ['id', 'user', 'product_id', 'quantity', 'purchase_date', 'revocation_date', 'expiration_date']
list_filter = ['user']
ordering = ['-purchase_date']
class CourtAdmin(SyncedObjectAdmin):
list_display = ['index', 'name', 'club']
ordering = ['club']
class DateIntervalAdmin(SyncedObjectAdmin):
list_display = ['court_index', 'event']
class FailedApiCallAdmin(admin.ModelAdmin):
list_display = ['date', 'user', 'type', 'error']
list_filter = ['user']
class LogAdmin(admin.ModelAdmin):
list_display = ['date', 'user', 'message']
list_filter = ['user']
class DeviceTokenAdmin(admin.ModelAdmin):
list_display = ['user', 'value']
list_filter = ['user']
class DrawLogAdmin(SyncedObjectAdmin):
list_display = ['tournament', 'draw_date', 'draw_seed', 'draw_match_index', 'draw_team_position']
list_filter = [SimpleTournamentListFilter]
ordering = ['draw_date']
class UnregisteredTeamAdmin(admin.ModelAdmin):
list_display = ['player_names', 'tournament']
list_filter = [SimpleTournamentListFilter]
class UnregisteredPlayerAdmin(admin.ModelAdmin):
list_display = ['first_name', 'last_name', 'licence_id']
search_fields = ['first_name', 'last_name']
list_filter = []
ordering = ['last_name', 'first_name']
class ImageAdmin(admin.ModelAdmin):
list_display = ['title', 'event', 'image_type', 'order', 'uploaded_at', 'file_size', 'image_preview_small']
list_filter = ['event', 'image_type', 'uploaded_at']
search_fields = ['title', 'description', 'event__name']
ordering = ['order']
readonly_fields = ['id', 'uploaded_at', 'image_preview', 'file_size']
raw_id_fields = ['event']
def image_preview(self, obj):
if obj.image:
return mark_safe(f'<img src="{obj.image.url}" width="150" height="auto" style="max-height: 150px; object-fit: contain;" />')
return "No Image"
image_preview.short_description = 'Preview'
def image_preview_small(self, obj):
if obj.image:
return mark_safe(f'<img src="{obj.image.url}" width="50" height="auto" style="max-height: 50px; object-fit: contain;" />')
return "No Image"
image_preview_small.short_description = 'Preview'
def file_size(self, obj):
if obj.image and hasattr(obj.image, 'size'):
# Convert bytes to KB or MB
size_bytes = obj.image.size
if size_bytes < 1024:
return f"{size_bytes} bytes"
elif size_bytes < 1024 * 1024:
return f"{size_bytes/1024:.1f} KB"
else:
return f"{size_bytes/(1024*1024):.1f} MB"
return "Unknown"
file_size.short_description = 'File Size'
action_flags = {
ADDITION: 'Addition',
CHANGE: 'Change',
DELETION: 'Deletion',
}
@admin.register(LogEntry)
class LogEntryAdmin(admin.ModelAdmin):
date_hierarchy = 'action_time'
list_filter = ['user', 'content_type', 'action_flag']
search_fields = ['object_repr', 'change_message']
list_display = ['action_time', 'user', 'content_type', 'object_link', 'action_flag_display', 'change_message']
readonly_fields = [field.name for field in LogEntry._meta.get_fields()]
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
def object_link(self, obj):
if obj.action_flag == DELETION:
link = escape(obj.object_repr)
else:
ct = obj.content_type
try:
link = '<a href="%s">%s</a>' % (
reverse('admin:%s_%s_change' % (ct.app_label, ct.model),
args=[obj.object_id]),
escape(obj.object_repr),
)
except:
link = escape(obj.object_repr)
return mark_safe(link)
object_link.short_description = 'Object'
def action_flag_display(self, obj):
return action_flags.get(obj.action_flag, '')
action_flag_display.short_description = 'Action'
admin.site.register(CustomUser, CustomUserAdmin)
admin.site.register(Club, ClubAdmin)
admin.site.register(Event, EventAdmin)
admin.site.register(Round, RoundAdmin)
admin.site.register(GroupStage, GroupStageAdmin)
admin.site.register(Match, MatchAdmin)
admin.site.register(TeamScore, TeamScoreAdmin)
admin.site.register(TeamRegistration, TeamRegistrationAdmin)
admin.site.register(Tournament, TournamentAdmin)
admin.site.register(PlayerRegistration, PlayerRegistrationAdmin)
admin.site.register(Purchase, PurchaseAdmin)
admin.site.register(Court, CourtAdmin)
admin.site.register(DateInterval, DateIntervalAdmin)
admin.site.register(FailedApiCall, FailedApiCallAdmin)
admin.site.register(Log, LogAdmin)
admin.site.register(DeviceToken, DeviceTokenAdmin)
admin.site.register(DrawLog, DrawLogAdmin)
admin.site.register(UnregisteredTeam, UnregisteredTeamAdmin)
admin.site.register(UnregisteredPlayer, UnregisteredPlayerAdmin)
admin.site.register(Image, ImageAdmin)