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

601 lines
25 KiB

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 = '<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'
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
class ClubSelectionForm(forms.Form):
club = forms.ModelChoiceField(
queryset=Club.objects.all(),
required=True,
label='Club',
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
)
)
if 'apply' in request.POST:
form = ClubSelectionForm(request.POST)
if form.is_valid():
club = form.cleaned_data['club']
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:
form = ClubSelectionForm()
context = {
'form': form,
'events': queryset,
'action_name': 'set_club_action',
'title': 'Set Club for Events',
}
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'<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)