""" Django admin configuration for the Rides application. This module provides comprehensive admin interfaces for managing rides, ride models, roller coaster stats, reviews, and rankings. All admin classes use optimized querysets and follow the standardized admin patterns defined in apps.core.admin. Performance targets: - List views: < 15 queries - Change views: < 20 queries - Page load time: < 500ms for 100 records """ from django.contrib import admin from django.contrib.gis.admin import GISModelAdmin from django.db.models import Avg, Count from django.utils import timezone from django.utils.html import format_html from apps.core.admin import ( BaseModelAdmin, ExportActionMixin, QueryOptimizationMixin, ReadOnlyAdminMixin, SlugFieldMixin, TimestampFieldsMixin, ) from .models.company import Company from .models.location import RideLocation from .models.rankings import RankingSnapshot, RidePairComparison, RideRanking from .models.reviews import RideReview from .models.rides import Ride, RideModel, RollerCoasterStats class RideLocationInline(admin.StackedInline): """ Inline admin for RideLocation within Ride admin. Displays location and accessibility information in a collapsible section. """ model = RideLocation extra = 0 classes = ("collapse",) fields = ( "park_area", "point", "entrance_notes", "accessibility_notes", ) class RollerCoasterStatsInline(admin.StackedInline): """ Inline admin for RollerCoasterStats within Ride admin. Shows roller coaster-specific statistics in a collapsible section. Only relevant for roller coaster category rides. """ model = RollerCoasterStats extra = 0 classes = ("collapse",) fields = ( ("height_ft", "length_ft", "speed_mph"), ("track_material", "roller_coaster_type"), ("propulsion_system", "inversions"), ("max_drop_height_ft", "ride_time_seconds"), ("train_style", "trains_count"), ("cars_per_train", "seats_per_car"), ) class RideLocationAdmin(QueryOptimizationMixin, GISModelAdmin): """ Admin interface for standalone RideLocation management. Provides GIS functionality with map widgets for coordinate entry. Optimized with select_related for ride and park data. """ list_display = ( "ride_link", "park_name", "park_area", "has_coordinates", "created_at", ) list_filter = ("park_area", "created_at") list_select_related = ["ride", "ride__park"] search_fields = ( "ride__name", "ride__park__name", "park_area", "entrance_notes", ) readonly_fields = ( "latitude", "longitude", "coordinates", "created_at", "updated_at", ) autocomplete_fields = ["ride"] list_per_page = 50 show_full_result_count = False fieldsets = ( ( "Ride", { "fields": ("ride",), "description": "Select the ride for this location.", }, ), ( "Location Information", { "fields": ( "park_area", "point", "latitude", "longitude", "coordinates", ), "description": "Optional coordinates - not all rides need precise location tracking.", }, ), ( "Navigation Notes", { "fields": ("entrance_notes", "accessibility_notes"), "description": "Helpful information for visitors navigating to this ride.", }, ), ( "Metadata", { "fields": ("created_at", "updated_at"), "classes": ("collapse",), }, ), ) @admin.display(description="Ride") def ride_link(self, obj): """Display ride name as a clickable link.""" if obj.ride: from django.urls import reverse url = reverse("admin:rides_ride_change", args=[obj.ride.pk]) return format_html('{}', url, obj.ride.name) return "-" @admin.display(description="Park") def park_name(self, obj): """Display park name.""" if obj.ride and obj.ride.park: return obj.ride.park.name return "-" @admin.display(description="Latitude") def latitude(self, obj): """Display latitude coordinate.""" return obj.latitude @admin.display(description="Longitude") def longitude(self, obj): """Display longitude coordinate.""" return obj.longitude @admin.display(description="Has Coords", boolean=True) def has_coordinates(self, obj): """Indicate whether location has valid coordinates.""" return obj.point is not None @admin.register(Ride) class RideAdmin( QueryOptimizationMixin, ExportActionMixin, SlugFieldMixin, TimestampFieldsMixin, BaseModelAdmin, ): """ Admin interface for Ride management. Provides comprehensive ride administration with: - Optimized queries using select_related/prefetch_related - Bulk actions for status changes and exports - Inline editing of locations and coaster stats - Enhanced filtering and search capabilities - FSM-aware status handling Query optimizations: - select_related: park, park_area, manufacturer, designer, ride_model, banner_image, card_image - prefetch_related: coaster_stats, location, reviews """ list_display = ( "name", "park_link", "category_badge", "manufacturer_link", "status_badge", "opening_date", "average_rating_display", "review_count", ) list_filter = ( "category", "status", "park", "manufacturer", "designer", "opening_date", ) list_select_related = [ "park", "park_area", "manufacturer", "designer", "ride_model", "banner_image", "card_image", ] list_prefetch_related = ["reviews", "coaster_stats", "location"] search_fields = ( "name", "slug", "description", "park__name", "manufacturer__name", "designer__name", ) readonly_fields = ("created_at", "updated_at", "average_rating", "review_count") autocomplete_fields = [ "park", "park_area", "manufacturer", "designer", "ride_model", "banner_image", "card_image", ] inlines = [RideLocationInline, RollerCoasterStatsInline] date_hierarchy = "opening_date" ordering = ("park", "name") export_fields = [ "id", "name", "slug", "category", "status", "park", "manufacturer", "opening_date", "created_at", ] export_filename_prefix = "rides" fieldsets = ( ( "Basic Information", { "fields": ( "name", "slug", "description", "park", "park_area", "category", ), "description": "Core ride identification and categorization.", }, ), ( "Companies", { "fields": ( "manufacturer", "designer", "ride_model", ), "description": "Companies responsible for manufacturing and designing the ride.", }, ), ( "Status & Dates", { "fields": ( "status", "post_closing_status", "opening_date", "closing_date", "status_since", ), "description": "Operational status and historical dates.", }, ), ( "Ride Specifications", { "fields": ( "min_height_in", "max_height_in", "capacity_per_hour", "ride_duration_seconds", ), "classes": ("collapse",), "description": "Technical specifications and requirements.", }, ), ( "Media", { "fields": ("banner_image", "card_image"), "classes": ("collapse",), "description": "Images displayed in banners and card views.", }, ), ( "Statistics", { "fields": ("average_rating", "review_count"), "classes": ("collapse",), "description": "Auto-calculated statistics (read-only).", }, ), ( "Metadata", { "fields": ("created_at", "updated_at"), "classes": ("collapse",), }, ), ) @admin.display(description="Park") def park_link(self, obj): """Display park as clickable link.""" if obj.park: from django.urls import reverse url = reverse("admin:parks_park_change", args=[obj.park.pk]) return format_html('{}', url, obj.park.name) return "-" @admin.display(description="Manufacturer") def manufacturer_link(self, obj): """Display manufacturer as clickable link.""" if obj.manufacturer: from django.urls import reverse url = reverse("admin:rides_company_change", args=[obj.manufacturer.pk]) return format_html('{}', url, obj.manufacturer.name) return "-" @admin.display(description="Category") def category_badge(self, obj): """Display category with color-coded badge.""" colors = { "roller_coaster": "#e74c3c", "water_ride": "#3498db", "dark_ride": "#9b59b6", "flat_ride": "#2ecc71", "transport": "#f39c12", "show": "#1abc9c", } color = colors.get(obj.category, "#95a5a6") choices_dict = dict(obj._meta.get_field("category").choices) label = choices_dict.get(obj.category, obj.category) return format_html( '{}', color, label, ) @admin.display(description="Status") def status_badge(self, obj): """Display status with color-coded badge.""" colors = { "operating": "green", "closed": "red", "sbno": "orange", "under_construction": "blue", "announced": "purple", } color = colors.get(obj.status, "gray") return format_html( '{}', color, obj.get_status_display(), ) @admin.display(description="Rating") def average_rating_display(self, obj): """Display average rating as stars.""" if hasattr(obj, "_avg_rating") and obj._avg_rating: rating = obj._avg_rating elif obj.average_rating: rating = float(obj.average_rating) else: return "-" full_stars = int(rating) stars = "★" * full_stars + "☆" * (5 - full_stars) return format_html( '{} {:.1f}', stars, rating, ) @admin.display(description="Reviews") def review_count(self, obj): """Display count of reviews.""" if hasattr(obj, "_review_count"): return obj._review_count return obj.reviews.count() def get_queryset(self, request): """Optimize queryset with annotations for list display.""" qs = super().get_queryset(request) qs = qs.annotate( _review_count=Count("reviews", distinct=True), _avg_rating=Avg("reviews__rating"), ) return qs @admin.action(description="Set status to Operating") def bulk_set_operating(self, request, queryset): """Set status to operating for selected rides.""" updated = queryset.update(status="operating", status_since=timezone.now().date()) self.message_user(request, f"Successfully set {updated} rides to Operating.") @admin.action(description="Set status to Closed") def bulk_set_closed(self, request, queryset): """Set status to closed for selected rides.""" updated = queryset.update(status="closed", status_since=timezone.now().date()) self.message_user(request, f"Successfully set {updated} rides to Closed.") @admin.action(description="Set status to SBNO") def bulk_set_sbno(self, request, queryset): """Set status to SBNO for selected rides.""" updated = queryset.update(status="sbno", status_since=timezone.now().date()) self.message_user(request, f"Successfully set {updated} rides to SBNO.") @admin.action(description="Recalculate ride ratings") def recalculate_ratings(self, request, queryset): """Recalculate average ratings for selected rides.""" count = 0 for ride in queryset: avg = ride.reviews.aggregate(avg=Avg("rating"))["avg"] if avg: ride.average_rating = avg ride.save(update_fields=["average_rating", "updated_at"]) count += 1 self.message_user(request, f"Successfully recalculated ratings for {count} rides.") def get_actions(self, request): """Add custom actions to the admin.""" actions = super().get_actions(request) actions["bulk_set_operating"] = ( self.bulk_set_operating, "bulk_set_operating", "Set status to Operating", ) actions["bulk_set_closed"] = ( self.bulk_set_closed, "bulk_set_closed", "Set status to Closed", ) actions["bulk_set_sbno"] = ( self.bulk_set_sbno, "bulk_set_sbno", "Set status to SBNO", ) actions["recalculate_ratings"] = ( self.recalculate_ratings, "recalculate_ratings", "Recalculate ride ratings", ) return actions @admin.register(RideModel) class RideModelAdmin( QueryOptimizationMixin, ExportActionMixin, TimestampFieldsMixin, BaseModelAdmin, ): """ Admin interface for ride models. Manages ride model/type information with manufacturer linking and installation counts. """ list_display = ( "name", "manufacturer_link", "category_badge", "installation_count", ) list_filter = ("manufacturer", "category") list_select_related = ["manufacturer"] list_prefetch_related = ["rides"] search_fields = ("name", "description", "manufacturer__name") ordering = ("manufacturer", "name") autocomplete_fields = ["manufacturer"] export_fields = ["id", "name", "manufacturer", "category", "description"] export_filename_prefix = "ride_models" fieldsets = ( ( "Model Information", { "fields": ( "name", "manufacturer", "category", "description", ), "description": "Core model identification and categorization.", }, ), ( "Statistics", { "fields": ("installation_count",), "classes": ("collapse",), "description": "Auto-calculated statistics.", }, ), ) readonly_fields = ("installation_count",) @admin.display(description="Manufacturer") def manufacturer_link(self, obj): """Display manufacturer as clickable link.""" if obj.manufacturer: from django.urls import reverse url = reverse("admin:rides_company_change", args=[obj.manufacturer.pk]) return format_html('{}', url, obj.manufacturer.name) return "-" @admin.display(description="Category") def category_badge(self, obj): """Display category with color-coded badge.""" colors = { "roller_coaster": "#e74c3c", "water_ride": "#3498db", "dark_ride": "#9b59b6", "flat_ride": "#2ecc71", } color = colors.get(obj.category, "#95a5a6") choices_dict = dict(obj._meta.get_field("category").choices) label = choices_dict.get(obj.category, obj.category) return format_html( '{}', color, label, ) @admin.display(description="Installations") def installation_count(self, obj): """Display number of ride installations.""" if hasattr(obj, "_installation_count"): return obj._installation_count return obj.rides.count() def get_queryset(self, request): """Optimize queryset with installation count annotation.""" qs = super().get_queryset(request) qs = qs.annotate(_installation_count=Count("rides", distinct=True)) return qs @admin.register(RollerCoasterStats) class RollerCoasterStatsAdmin(QueryOptimizationMixin, ExportActionMixin, BaseModelAdmin): """ Admin interface for roller coaster statistics. Manages detailed roller coaster specifications with calculated capacity and record indicators. """ list_display = ( "ride_link", "park_name", "height_ft", "speed_mph", "length_ft", "inversions", "track_material", "roller_coaster_type", ) list_filter = ( "track_material", "roller_coaster_type", "propulsion_system", "inversions", ) list_select_related = ["ride", "ride__park", "ride__manufacturer"] search_fields = ( "ride__name", "ride__park__name", "track_type", "train_style", ) readonly_fields = ("calculated_capacity",) autocomplete_fields = ["ride"] export_fields = [ "id", "ride", "height_ft", "speed_mph", "length_ft", "inversions", "track_material", "roller_coaster_type", ] export_filename_prefix = "coaster_stats" fieldsets = ( ( "Ride", { "fields": ("ride",), "description": "The roller coaster these stats belong to.", }, ), ( "Basic Stats", { "fields": ( "height_ft", "length_ft", "speed_mph", "max_drop_height_ft", ), "description": "Primary statistics for the roller coaster.", }, ), ( "Track & Design", { "fields": ( "track_material", "track_type", "roller_coaster_type", "propulsion_system", "inversions", ), "description": "Track construction and design details.", }, ), ( "Operation Details", { "fields": ( "ride_time_seconds", "train_style", "trains_count", "cars_per_train", "seats_per_car", "calculated_capacity", ), "classes": ("collapse",), "description": "Operational specifications and capacity.", }, ), ) @admin.display(description="Ride") def ride_link(self, obj): """Display ride as clickable link.""" if obj.ride: from django.urls import reverse url = reverse("admin:rides_ride_change", args=[obj.ride.pk]) return format_html('{}', url, obj.ride.name) return "-" @admin.display(description="Park") def park_name(self, obj): """Display park name.""" if obj.ride and obj.ride.park: return obj.ride.park.name return "-" @admin.display(description="Calculated Capacity") def calculated_capacity(self, obj): """Calculate theoretical hourly capacity.""" if all( [ obj.trains_count, obj.cars_per_train, obj.seats_per_car, obj.ride_time_seconds, ] ): total_seats = obj.trains_count * obj.cars_per_train * obj.seats_per_car cycles_per_hour = 3600 / (obj.ride_time_seconds + 120) # 2 min loading return f"{int(total_seats * cycles_per_hour)} riders/hour" return "N/A" @admin.action(description="Bulk update from ride model") def sync_with_model(self, request, queryset): """Sync stats with ride model defaults where available.""" count = 0 for stats in queryset.select_related("ride__ride_model"): if stats.ride and stats.ride.ride_model: # Sync would happen here if model had default stats count += 1 self.message_user(request, f"Synced {count} coaster stats with ride models.") def get_actions(self, request): """Add custom actions to the admin.""" actions = super().get_actions(request) actions["sync_with_model"] = ( self.sync_with_model, "sync_with_model", "Bulk update from ride model", ) return actions @admin.register(RideReview) class RideReviewAdmin(QueryOptimizationMixin, ExportActionMixin, BaseModelAdmin): """ Admin interface for ride reviews. Provides review moderation with: - Bulk approve/reject actions - Moderation status filtering - User and ride linking - Automatic moderation tracking """ list_display = ( "ride_link", "park_name", "user_link", "rating_display", "title", "visit_date", "is_published", "moderation_status", "created_at", ) list_filter = ( "is_published", "rating", "visit_date", "created_at", "ride__park", "ride__category", ) list_select_related = ["ride", "ride__park", "user", "moderated_by"] search_fields = ( "title", "content", "user__username", "ride__name", "ride__park__name", ) readonly_fields = ("created_at", "updated_at", "moderated_by", "moderated_at") autocomplete_fields = ["ride", "user"] date_hierarchy = "created_at" ordering = ("-created_at",) export_fields = [ "id", "ride", "user", "rating", "title", "visit_date", "is_published", "created_at", ] export_filename_prefix = "ride_reviews" fieldsets = ( ( "Review Details", { "fields": ( "user", "ride", "rating", "title", "content", "visit_date", ), "description": "Core review information.", }, ), ( "Publication Status", { "fields": ("is_published",), "description": "Toggle to publish or unpublish this review.", }, ), ( "Moderation", { "fields": ( "moderated_by", "moderated_at", "moderation_notes", ), "classes": ("collapse",), "description": "Moderation tracking (auto-populated on status change).", }, ), ( "Metadata", { "fields": ("created_at", "updated_at"), "classes": ("collapse",), }, ), ) @admin.display(description="Ride") def ride_link(self, obj): """Display ride as clickable link.""" if obj.ride: from django.urls import reverse url = reverse("admin:rides_ride_change", args=[obj.ride.pk]) return format_html('{}', url, obj.ride.name) return "-" @admin.display(description="Park") def park_name(self, obj): """Display park name.""" if obj.ride and obj.ride.park: return obj.ride.park.name return "-" @admin.display(description="User") def user_link(self, obj): """Display user as clickable link.""" if obj.user: from django.urls import reverse url = reverse("admin:accounts_customuser_change", args=[obj.user.pk]) return format_html('{}', url, obj.user.username) return "-" @admin.display(description="Rating") def rating_display(self, obj): """Display rating as stars.""" if obj.rating: stars = "★" * obj.rating + "☆" * (5 - obj.rating) return format_html('{}', stars) return "-" @admin.display(description="Moderation") def moderation_status(self, obj): """Display moderation status with color coding.""" if obj.moderated_by: if obj.is_published: return format_html( 'Approved' ) return format_html( 'Rejected' ) return format_html('Pending') def save_model(self, request, obj, form, change): """Auto-set moderation info when publication status changes.""" if change and "is_published" in form.changed_data: obj.moderated_by = request.user obj.moderated_at = timezone.now() super().save_model(request, obj, form, change) @admin.action(description="Approve and publish selected reviews") def bulk_approve(self, request, queryset): """Approve and publish all selected reviews.""" updated = queryset.update( is_published=True, moderated_by=request.user, moderated_at=timezone.now(), ) self.message_user(request, f"Successfully approved {updated} reviews.") @admin.action(description="Reject selected reviews") def bulk_reject(self, request, queryset): """Reject and unpublish all selected reviews.""" updated = queryset.update( is_published=False, moderated_by=request.user, moderated_at=timezone.now(), ) self.message_user(request, f"Successfully rejected {updated} reviews.") @admin.action(description="Flag for review") def flag_for_review(self, request, queryset): """Flag suspicious reviews for closer review.""" updated = queryset.update(is_published=False) self.message_user(request, f"Flagged {updated} reviews for review.") def get_actions(self, request): """Add moderation actions to the admin.""" actions = super().get_actions(request) actions["bulk_approve"] = ( self.bulk_approve, "bulk_approve", "Approve and publish selected reviews", ) actions["bulk_reject"] = ( self.bulk_reject, "bulk_reject", "Reject selected reviews", ) actions["flag_for_review"] = ( self.flag_for_review, "flag_for_review", "Flag for review", ) return actions @admin.register(Company) class CompanyAdmin( QueryOptimizationMixin, ExportActionMixin, SlugFieldMixin, TimestampFieldsMixin, BaseModelAdmin, ): """ Admin interface for Company (manufacturers/designers) management. Manages ride manufacturers and designers with: - Role-based filtering - Ride and coaster count annotations - Enhanced search capabilities """ list_display = ( "name", "roles_display", "website", "founded_date", "rides_count_display", "coasters_count_display", ) list_filter = ("roles", "founded_date") list_prefetch_related = ["manufactured_rides", "designed_rides", "ride_models"] search_fields = ("name", "slug", "description") readonly_fields = ("created_at", "updated_at", "rides_count", "coasters_count") export_fields = ["id", "name", "slug", "roles", "website", "founded_date", "created_at"] export_filename_prefix = "ride_companies" fieldsets = ( ( "Basic Information", { "fields": ( "name", "slug", "roles", "description", "website", ), "description": "Company identification and role.", }, ), ( "Company Details", { "fields": ( "founded_date", ), "classes": ("collapse",), "description": "Historical information about the company.", }, ), ( "Statistics", { "fields": ("rides_count", "coasters_count"), "classes": ("collapse",), "description": "Auto-calculated ride counts.", }, ), ( "Metadata", { "fields": ("created_at", "updated_at"), "classes": ("collapse",), }, ), ) @admin.display(description="Roles") def roles_display(self, obj): """Display roles as formatted badges.""" if obj.roles: badges = [] colors = { "MANUFACTURER": "#007bff", "DESIGNER": "#28a745", } for role in obj.roles: color = colors.get(role, "#6c757d") badges.append( f'{role}' ) return format_html("".join(badges)) return "-" @admin.display(description="Rides") def rides_count_display(self, obj): """Display total ride count.""" if hasattr(obj, "_rides_count"): return obj._rides_count return obj.rides_count if hasattr(obj, "rides_count") else "-" @admin.display(description="Coasters") def coasters_count_display(self, obj): """Display coaster count.""" if hasattr(obj, "_coasters_count"): return obj._coasters_count return obj.coasters_count if hasattr(obj, "coasters_count") else "-" def get_queryset(self, request): """Optimize queryset with ride count annotations.""" qs = super().get_queryset(request) qs = qs.annotate( _rides_count=Count("manufactured_rides", distinct=True), _coasters_count=Count( "manufactured_rides", filter=admin.models.Q(manufactured_rides__category="roller_coaster"), distinct=True, ), ) return qs @admin.register(RideRanking) class RideRankingAdmin(ReadOnlyAdminMixin, ExportActionMixin, BaseModelAdmin): """ Admin interface for ride rankings. Read-only admin for viewing calculated ride rankings. Rankings are automatically calculated and should not be manually modified. """ list_display = ( "rank", "ride_link", "park_name", "winning_percentage_display", "wins", "losses", "ties", "average_rating", "mutual_riders_count", "last_calculated", ) list_filter = ( "ride__category", "last_calculated", "calculation_version", ) list_select_related = ["ride", "ride__park"] search_fields = ( "ride__name", "ride__park__name", ) readonly_fields = ( "ride", "rank", "wins", "losses", "ties", "winning_percentage", "mutual_riders_count", "comparison_count", "average_rating", "last_calculated", "calculation_version", "total_comparisons", ) ordering = ["rank"] export_fields = [ "rank", "ride", "winning_percentage", "wins", "losses", "ties", "average_rating", "last_calculated", ] export_filename_prefix = "ride_rankings" fieldsets = ( ( "Ride Information", { "fields": ("ride",), "description": "The ride this ranking belongs to.", }, ), ( "Ranking Metrics", { "fields": ( "rank", "winning_percentage", "wins", "losses", "ties", "total_comparisons", ), "description": "Head-to-head comparison results.", }, ), ( "Additional Metrics", { "fields": ( "average_rating", "mutual_riders_count", "comparison_count", ), "description": "Supporting metrics for ranking calculation.", }, ), ( "Calculation Info", { "fields": ( "last_calculated", "calculation_version", ), "classes": ("collapse",), "description": "When and how this ranking was calculated.", }, ), ) @admin.display(description="Ride") def ride_link(self, obj): """Display ride as clickable link.""" if obj.ride: from django.urls import reverse url = reverse("admin:rides_ride_change", args=[obj.ride.pk]) return format_html('{}', url, obj.ride.name) return "-" @admin.display(description="Park") def park_name(self, obj): """Display park name.""" if obj.ride and obj.ride.park: return obj.ride.park.name return "-" @admin.display(description="Win %") def winning_percentage_display(self, obj): """Display winning percentage formatted.""" return f"{obj.winning_percentage:.1%}" @admin.register(RidePairComparison) class RidePairComparisonAdmin(ReadOnlyAdminMixin, ExportActionMixin, BaseModelAdmin): """ Admin interface for ride pair comparisons. Read-only admin for viewing head-to-head ride comparison data. Comparisons are automatically calculated. """ list_display = ( "comparison_summary", "ride_a_link", "ride_b_link", "winner_display", "ride_a_wins", "ride_b_wins", "ties", "mutual_riders_count", "last_calculated", ) list_filter = ("last_calculated",) list_select_related = ["ride_a", "ride_a__park", "ride_b", "ride_b__park"] search_fields = ( "ride_a__name", "ride_b__name", "ride_a__park__name", "ride_b__park__name", ) readonly_fields = ( "ride_a", "ride_b", "ride_a_wins", "ride_b_wins", "ties", "mutual_riders_count", "ride_a_avg_rating", "ride_b_avg_rating", "last_calculated", "winner", "is_tie", ) ordering = ["-mutual_riders_count"] export_fields = [ "ride_a", "ride_b", "ride_a_wins", "ride_b_wins", "ties", "mutual_riders_count", "last_calculated", ] export_filename_prefix = "ride_comparisons" fieldsets = ( ( "Rides Being Compared", { "fields": ("ride_a", "ride_b"), "description": "The two rides in this comparison.", }, ), ( "Comparison Results", { "fields": ( "ride_a_wins", "ride_b_wins", "ties", "winner", "is_tie", ), "description": "Head-to-head comparison results.", }, ), ( "Rating Data", { "fields": ( "ride_a_avg_rating", "ride_b_avg_rating", "mutual_riders_count", ), "description": "Rating metrics for comparison.", }, ), ( "Calculation Info", { "fields": ("last_calculated",), "classes": ("collapse",), }, ), ) @admin.display(description="Comparison") def comparison_summary(self, obj): """Display comparison summary.""" return f"{obj.ride_a.name} vs {obj.ride_b.name}" @admin.display(description="Ride A") def ride_a_link(self, obj): """Display ride A as clickable link.""" if obj.ride_a: from django.urls import reverse url = reverse("admin:rides_ride_change", args=[obj.ride_a.pk]) return format_html('{}', url, obj.ride_a.name) return "-" @admin.display(description="Ride B") def ride_b_link(self, obj): """Display ride B as clickable link.""" if obj.ride_b: from django.urls import reverse url = reverse("admin:rides_ride_change", args=[obj.ride_b.pk]) return format_html('{}', url, obj.ride_b.name) return "-" @admin.display(description="Winner") def winner_display(self, obj): """Display winner or tie status.""" if obj.is_tie: return format_html('TIE') winner = obj.winner if winner: return format_html('{}', winner.name) return "-" @admin.register(RankingSnapshot) class RankingSnapshotAdmin(ReadOnlyAdminMixin, ExportActionMixin, BaseModelAdmin): """ Admin interface for ranking history snapshots. Read-only admin for viewing historical ranking data. Snapshots are automatically created during ranking calculations. """ list_display = ( "ride_link", "park_name", "rank", "winning_percentage_display", "snapshot_date", ) list_filter = ( "snapshot_date", "ride__category", ) list_select_related = ["ride", "ride__park"] search_fields = ( "ride__name", "ride__park__name", ) readonly_fields = ( "ride", "rank", "winning_percentage", "snapshot_date", ) date_hierarchy = "snapshot_date" ordering = ["-snapshot_date", "rank"] export_fields = ["ride", "rank", "winning_percentage", "snapshot_date"] export_filename_prefix = "ranking_snapshots" fieldsets = ( ( "Ride Information", { "fields": ("ride",), "description": "The ride this snapshot belongs to.", }, ), ( "Ranking Data", { "fields": ( "rank", "winning_percentage", "snapshot_date", ), "description": "Historical ranking data at this point in time.", }, ), ) @admin.display(description="Ride") def ride_link(self, obj): """Display ride as clickable link.""" if obj.ride: from django.urls import reverse url = reverse("admin:rides_ride_change", args=[obj.ride.pk]) return format_html('{}', url, obj.ride.name) return "-" @admin.display(description="Park") def park_name(self, obj): """Display park name.""" if obj.ride and obj.ride.park: return obj.ride.park.name return "-" @admin.display(description="Win %") def winning_percentage_display(self, obj): """Display winning percentage formatted.""" return f"{obj.winning_percentage:.1%}" # Register standalone admin admin.site.register(RideLocation, RideLocationAdmin)