from django.contrib import admin, messages 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 from django.utils.safestring import mark_safe from django.shortcuts import render from django.db.models import Avg, Count from datetime import timedelta, datetime from biz.models import Prospect, ProspectGroup 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, UserWithPurchasesFilter, UserWithProspectFilter, TeamScoreRoundIndexFilter from sync.admin import SyncedObjectAdmin import logging logger = logging.getLogger(__name__) class CustomUserAdmin(UserAdmin): form = CustomUserChangeForm add_form = CustomUserCreationForm model = CustomUser search_fields = ['username', 'email', 'phone', 'first_name', 'last_name', 'licence_id'] filter_horizontal = ('clubs',) actions = ['convert_to_prospect', 'create_group'] 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, UserWithPurchasesFilter, UserWithProspectFilter] ordering = ['-date_joined'] autocomplete_fields = ['supervisors', 'organizers'] fieldsets = [ (None, {'fields': ['id', 'username', 'email', 'password', 'first_name', 'last_name', 'is_active', 'date_joined']}), ('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', 'supervisors', 'organizers', '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) def create_group(self, request, queryset): prospects = [] source_value = f"auto_created_{datetime.now().strftime('%Y-%m-%d_%H:%M')}" for user in queryset: prospect = Prospect.objects.filter(email=user.email).first() if prospect: prospects.append(prospect) else: prospect = Prospect.objects.create( first_name=user.first_name, last_name=user.last_name, email=user.email, phone=user.phone, official_user=user, source=source_value ) prospects.append(prospect) prospect_group = ProspectGroup.objects.create( name=f"{datetime.now().strftime('%Y-%m-%d_%H:%M')}", ) prospect_group.prospects.add(*prospects) messages.success(request, f'Created prospect group {prospect_group.name} with {queryset.count()} prospects') create_group.short_description = "Create group with selection" def convert_to_prospect(self, request, queryset): created_count = 0 skipped_count = 0 source_value = f"user_conversion_{datetime.now().strftime('%Y-%m-%d_%H:%M')}" for user in queryset: if user.email and Prospect.objects.filter(email=user.email).exists(): skipped_count += 1 continue prospect = Prospect.objects.create( first_name=user.first_name, last_name=user.last_name, email=user.email, phone=user.phone, official_user=user, source=source_value ) created_count += 1 if created_count > 0: messages.success(request, f'{created_count} prospect(s) successfully created.') if skipped_count > 0: messages.warning(request, f'{skipped_count} user(s) skipped (prospect with same email already exists).') convert_to_prospect.short_description = "Convert selected users to prospects" class EventAdmin(SyncedObjectAdmin): list_display = ['creation_date', 'name', 'club', 'creator', 'tenup_id', 'display_images'] list_filter = ['creator', 'club', 'tenup_id'] search_fields = ['name', 'club__name', 'creator__email'] raw_id_fields = ['related_user', 'creator', 'club'] ordering = ['-creation_date'] readonly_fields = ['display_images_preview'] actions = ['set_club_action'] 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 = '
' for image in obj.images.all(): html += f'''

{image.title or "Untitled"}
Type: {image.get_image_type_display()}

''' html += '
' if not obj.images.exists(): html = '

No images uploaded for this event.

' return mark_safe(html) display_images_preview.short_description = 'Images Preview' def set_club_action(self, request, queryset): """Action to set club for selected events""" from django import forms from django.contrib.admin.widgets import ForeignKeyRawIdWidget from django.contrib.admin import helpers from django.core.exceptions import ValidationError class ClubSelectionForm(forms.Form): _selected_action = forms.CharField(widget=forms.MultipleHiddenInput) action = forms.CharField(widget=forms.HiddenInput) club_id = forms.CharField( label='Club', required=True, help_text='Enter Club ID or use the search icon to find a club', widget=ForeignKeyRawIdWidget( Event._meta.get_field('club').remote_field, self.admin_site ) ) def clean_club_id(self): club_id = self.cleaned_data['club_id'] try: club = Club.objects.get(pk=club_id) return club except Club.DoesNotExist: raise ValidationError(f'Club with ID {club_id} does not exist.') except (ValueError, TypeError) as e: raise ValidationError(f'Invalid Club ID format: {club_id}') if 'apply' in request.POST: form = ClubSelectionForm(request.POST) if form.is_valid(): club = form.cleaned_data['club_id'] # This is now a Club instance updated_count = queryset.update(club=club) self.message_user( request, f'Successfully updated {updated_count} event(s) with club: {club.name}', messages.SUCCESS ) return None else: # Show form errors self.message_user( request, f'Form validation failed. Errors: {form.errors}', messages.ERROR ) # Initial form display form = ClubSelectionForm(initial={ '_selected_action': request.POST.getlist(helpers.ACTION_CHECKBOX_NAME), 'action': 'set_club_action', }) context = { 'form': form, 'events': queryset, 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME, 'action_name': 'set_club_action', 'title': 'Set Club for Events', 'media': form.media, 'has_change_permission': True, } return render(request, 'admin/tournaments/set_club_action.html', context) set_club_action.short_description = "Set club for selected events" 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'] raw_id_fields = ['last_updated_by', 'event'] 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 email_count = PlayerRegistration.objects.aggregate( total=Count('email', distinct=True) )['total'] 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), 'email_count': email_count, # 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'] raw_id_fields = ['related_user', 'tournament'] class TeamScoreAdmin(SyncedObjectAdmin): list_display = ['team_registration', 'score', 'walk_out', 'match'] list_filter = [TeamScoreRoundIndexFilter, TeamScoreTournamentListFilter] search_fields = ['id', 'team_registration__player_registrations__first_name', 'team_registration__player_registrations__last_name'] raw_id_fields = ['team_registration', 'match'] 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'] raw_id_fields = ['user'] 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'') return "No Image" image_preview.short_description = 'Preview' def image_preview_small(self, obj): if obj.image: return mark_safe(f'') 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 = '%s' % ( 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)