""" Django admin configuration for the Parks application. This module provides comprehensive admin interfaces for managing parks, park areas, companies, locations, and reviews. All admin classes use optimized querysets and follow the standardized admin patterns defined in apps.core.admin. Performance targets: - List views: < 10 queries - Change views: < 15 queries - Page load time: < 500ms for 100 records """ import pghistory.models 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 import ( Company, CompanyHeadquarters, Park, ParkArea, ParkLocation, ParkReview, ) class ParkLocationInline(admin.StackedInline): """ Inline admin for ParkLocation within Park admin. Displays location information in a collapsible section with fields organized for efficient data entry. """ model = ParkLocation extra = 0 classes = ("collapse",) fields = ( ("city", "state", "country"), "street_address", "postal_code", "point", ("highway_exit", "best_arrival_time"), "parking_notes", "seasonal_notes", ("osm_id", "osm_type"), ) class ParkAreaInline(admin.TabularInline): """ Inline admin for ParkArea within Park admin. Shows areas as a collapsed tabular list for quick overview. """ model = ParkArea extra = 0 classes = ("collapse",) fields = ("name", "slug", "description") prepopulated_fields = {"slug": ("name",)} show_change_link = True class ParkLocationAdmin(QueryOptimizationMixin, GISModelAdmin): """ Admin interface for standalone ParkLocation management. Provides full GIS functionality with map widgets for geographic coordinate entry. Optimized with select_related for park data. """ list_display = ( "park_link", "city", "state", "country", "latitude", "longitude", "has_coordinates", ) list_filter = ("country", "state") list_select_related = ["park"] search_fields = ( "park__name", "city", "state", "country", "street_address", ) readonly_fields = ("latitude", "longitude", "coordinates", "created_at", "updated_at") autocomplete_fields = ["park"] list_per_page = 50 show_full_result_count = False fieldsets = ( ( "Park", { "fields": ("park",), "description": "Select the park for this location.", }, ), ( "Address", { "fields": ( "street_address", "city", "state", "country", "postal_code", ), "description": "Enter the physical address of the park.", }, ), ( "Geographic Coordinates", { "fields": ("point", "latitude", "longitude", "coordinates"), "description": "Set coordinates by clicking on the map or entering latitude/longitude. " "Coordinates are required for map display.", }, ), ( "Travel Information", { "fields": ( "highway_exit", "best_arrival_time", "parking_notes", "seasonal_notes", ), "classes": ("collapse",), "description": "Optional travel tips and parking information for visitors.", }, ), ( "OpenStreetMap Integration", { "fields": ("osm_id", "osm_type"), "classes": ("collapse",), "description": "OpenStreetMap identifiers for data synchronization.", }, ), ( "Metadata", { "fields": ("created_at", "updated_at"), "classes": ("collapse",), }, ), ) @admin.display(description="Park") def park_link(self, obj): """Display park name as a clickable link to the park admin.""" 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="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 class ParkAdmin( QueryOptimizationMixin, ExportActionMixin, SlugFieldMixin, TimestampFieldsMixin, BaseModelAdmin, ): """ Admin interface for Park management. Provides comprehensive park administration with: - Optimized queries using select_related/prefetch_related - Bulk actions for status changes and exports - Inline editing of locations and areas - Enhanced filtering and search capabilities Query optimizations: - select_related: operator, property_owner, location, banner_image, card_image - prefetch_related: areas, rides """ list_display = ( "name", "formatted_location", "status_badge", "operator_link", "ride_count", "average_rating", "created_at", ) list_filter = ("status", "location__country", "location__state", "operator") list_select_related = ["operator", "property_owner", "location", "banner_image", "card_image"] list_prefetch_related = ["areas", "rides"] search_fields = ( "name", "slug", "description", "location__city", "location__state", "location__country", "operator__name", ) readonly_fields = ("created_at", "updated_at", "ride_count", "average_rating") autocomplete_fields = ["operator", "property_owner", "banner_image", "card_image"] date_hierarchy = "created_at" ordering = ("-created_at",) inlines = [ParkLocationInline, ParkAreaInline] export_fields = ["id", "name", "slug", "status", "created_at", "updated_at"] export_filename_prefix = "parks" fieldsets = ( ( "Basic Information", { "fields": ("name", "slug", "description"), "description": "Core park identification and description.", }, ), ( "Status", { "fields": ("status",), "description": "Current operational status of the park.", }, ), ( "Ownership & Operation", { "fields": ("operator", "property_owner"), "description": "Companies responsible for operating and owning the park.", }, ), ( "Media", { "fields": ("banner_image", "card_image"), "classes": ("collapse",), "description": "Images displayed in banners and card views.", }, ), ( "Statistics", { "fields": ("ride_count", "average_rating"), "classes": ("collapse",), "description": "Auto-calculated statistics (read-only).", }, ), ( "Metadata", { "fields": ("created_at", "updated_at"), "classes": ("collapse",), }, ), ) @admin.display(description="Location") def formatted_location(self, obj): """Display formatted location string.""" return obj.formatted_location or "-" @admin.display(description="Status") def status_badge(self, obj): """Display status with color-coded badge.""" colors = { "operating": "green", "closed": "red", "seasonal": "orange", "under_construction": "blue", } color = colors.get(obj.status, "gray") return format_html( '{}', color, obj.get_status_display(), ) @admin.display(description="Operator") def operator_link(self, obj): """Display operator as clickable link.""" if obj.operator: from django.urls import reverse url = reverse("admin:parks_company_change", args=[obj.operator.pk]) return format_html('{}', url, obj.operator.name) return "-" @admin.display(description="Rides") def ride_count(self, obj): """Display count of rides at this park.""" if hasattr(obj, "_ride_count"): return obj._ride_count return obj.rides.count() @admin.display(description="Avg Rating") def average_rating(self, obj): """Display average park review rating.""" if hasattr(obj, "_avg_rating"): rating = obj._avg_rating else: rating = obj.reviews.aggregate(avg=Avg("rating"))["avg"] if rating: stars = "★" * int(rating) + "☆" * (5 - int(rating)) return format_html('{} {:.1f}', stars, rating) return "-" def get_queryset(self, request): """Optimize queryset with annotations for list display.""" qs = super().get_queryset(request) qs = qs.annotate( _ride_count=Count("rides", distinct=True), _avg_rating=Avg("reviews__rating"), ) return qs @admin.action(description="Activate selected parks") def bulk_activate(self, request, queryset): """Set status to operating for selected parks.""" updated = queryset.update(status="operating") self.message_user(request, f"Successfully activated {updated} parks.") @admin.action(description="Deactivate selected parks") def bulk_deactivate(self, request, queryset): """Set status to closed for selected parks.""" updated = queryset.update(status="closed") self.message_user(request, f"Successfully deactivated {updated} parks.") @admin.action(description="Recalculate park statistics") def recalculate_stats(self, request, queryset): """Recalculate ride counts and ratings for selected parks.""" for park in queryset: # Statistics are auto-calculated, so just touch the record park.save(update_fields=["updated_at"]) self.message_user( request, f"Successfully recalculated statistics for {queryset.count()} parks." ) def get_actions(self, request): """Add custom actions to the admin.""" actions = super().get_actions(request) actions["bulk_activate"] = ( self.bulk_activate, "bulk_activate", "Activate selected parks", ) actions["bulk_deactivate"] = ( self.bulk_deactivate, "bulk_deactivate", "Deactivate selected parks", ) actions["recalculate_stats"] = ( self.recalculate_stats, "recalculate_stats", "Recalculate park statistics", ) return actions class ParkAreaAdmin( QueryOptimizationMixin, ExportActionMixin, SlugFieldMixin, TimestampFieldsMixin, BaseModelAdmin, ): """ Admin interface for ParkArea management. Manages themed areas within parks with optimized queries and ride count annotations. """ list_display = ("name", "park_link", "ride_count", "created_at", "updated_at") list_filter = ("park",) list_select_related = ["park"] list_prefetch_related = ["rides"] search_fields = ("name", "slug", "description", "park__name") readonly_fields = ("created_at", "updated_at", "ride_count") autocomplete_fields = ["park"] export_fields = ["id", "name", "slug", "park", "created_at"] export_filename_prefix = "park_areas" fieldsets = ( ( "Basic Information", { "fields": ("name", "slug", "description"), "description": "Area identification within the park.", }, ), ( "Park", { "fields": ("park",), "description": "The park this area belongs to.", }, ), ( "Statistics", { "fields": ("ride_count",), "classes": ("collapse",), "description": "Auto-calculated statistics.", }, ), ( "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="Rides") def ride_count(self, obj): """Display count of rides in this area.""" if hasattr(obj, "_ride_count"): return obj._ride_count return obj.rides.count() def get_queryset(self, request): """Optimize queryset with ride count annotation.""" qs = super().get_queryset(request) qs = qs.annotate(_ride_count=Count("rides", distinct=True)) return qs class CompanyHeadquartersInline(admin.StackedInline): """ Inline admin for CompanyHeadquarters within Company admin. Displays headquarters address information in a collapsible section. """ model = CompanyHeadquarters extra = 0 classes = ("collapse",) fields = ( ("city", "state_province", "country"), "street_address", "postal_code", "mailing_address", ) class CompanyHeadquartersAdmin( QueryOptimizationMixin, TimestampFieldsMixin, BaseModelAdmin ): """ Admin interface for standalone CompanyHeadquarters management. Provides headquarters address management with company linking and location-based filtering. """ list_display = ( "company_link", "location_display", "city", "country", "created_at", ) list_filter = ("country", "state_province") list_select_related = ["company"] search_fields = ( "company__name", "city", "state_province", "country", "street_address", ) readonly_fields = ("created_at", "updated_at") autocomplete_fields = ["company"] fieldsets = ( ( "Company", { "fields": ("company",), "description": "The company this headquarters belongs to.", }, ), ( "Address", { "fields": ( "street_address", "city", "state_province", "country", "postal_code", ), "description": "Physical address of the company headquarters.", }, ), ( "Additional Information", { "fields": ("mailing_address",), "classes": ("collapse",), "description": "Mailing address if different from physical address.", }, ), ( "Metadata", { "fields": ("created_at", "updated_at"), "classes": ("collapse",), }, ), ) @admin.display(description="Company") def company_link(self, obj): """Display company as clickable link.""" if obj.company: from django.urls import reverse url = reverse("admin:parks_company_change", args=[obj.company.pk]) return format_html('{}', url, obj.company.name) return "-" class CompanyAdmin( QueryOptimizationMixin, ExportActionMixin, SlugFieldMixin, TimestampFieldsMixin, BaseModelAdmin, ): """ Admin interface for Company management. Manages park operators and property owners with: - Role-based filtering - Park count annotations - Headquarters inline editing - Search by headquarters location Query optimizations: - prefetch_related: operated_parks, owned_parks, headquarters """ list_display = ( "name", "roles_display", "headquarters_location", "operated_parks_count", "owned_parks_count", "website", "founded_year", ) list_filter = ("roles", "founded_year") list_prefetch_related = ["operated_parks", "owned_parks"] search_fields = ( "name", "slug", "description", "headquarters__city", "headquarters__country", ) readonly_fields = ("created_at", "updated_at", "operated_parks_count", "owned_parks_count") inlines = [CompanyHeadquartersInline] export_fields = ["id", "name", "slug", "roles", "website", "founded_year", "created_at"] export_filename_prefix = "companies" fieldsets = ( ( "Basic Information", { "fields": ("name", "slug", "description"), "description": "Company identification and description.", }, ), ( "Roles", { "fields": ("roles",), "description": "Select all roles this company performs (operator, owner, etc.).", }, ), ( "Contact & Links", { "fields": ("website",), "description": "Company website and contact information.", }, ), ( "History", { "fields": ("founded_year",), "classes": ("collapse",), "description": "Historical information about the company.", }, ), ( "Statistics", { "fields": ("operated_parks_count", "owned_parks_count"), "classes": ("collapse",), "description": "Auto-calculated park 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 = {"operator": "#007bff", "owner": "#28a745", "manufacturer": "#6c757d"} for role in obj.roles: color = colors.get(role, "#6c757d") badges.append( f'{role}' ) return format_html("".join(badges)) return "-" @admin.display(description="Headquarters") def headquarters_location(self, obj): """Display headquarters location if available.""" if hasattr(obj, "headquarters"): return obj.headquarters.location_display return "-" @admin.display(description="Operated") def operated_parks_count(self, obj): """Display count of parks operated by this company.""" if hasattr(obj, "_operated_count"): return obj._operated_count return obj.operated_parks.count() @admin.display(description="Owned") def owned_parks_count(self, obj): """Display count of parks owned by this company.""" if hasattr(obj, "_owned_count"): return obj._owned_count return obj.owned_parks.count() def get_queryset(self, request): """Optimize queryset with park count annotations.""" qs = super().get_queryset(request) qs = qs.annotate( _operated_count=Count("operated_parks", distinct=True), _owned_count=Count("owned_parks", distinct=True), ) return qs @admin.action(description="Update park counts") def update_park_counts(self, request, queryset): """Refresh park count statistics for selected companies.""" for company in queryset: company.save(update_fields=["updated_at"]) self.message_user( request, f"Successfully updated counts for {queryset.count()} companies." ) def get_actions(self, request): """Add custom actions to the admin.""" actions = super().get_actions(request) actions["update_park_counts"] = ( self.update_park_counts, "update_park_counts", "Update park counts", ) return actions @admin.register(ParkReview) class ParkReviewAdmin(QueryOptimizationMixin, ExportActionMixin, BaseModelAdmin): """ Admin interface for park reviews. Provides review moderation with: - Bulk approve/reject actions - Moderation status filtering - User and park linking - Automatic moderation tracking Query optimizations: - select_related: park, user, moderated_by """ list_display = ( "park_link", "user_link", "rating_display", "title", "visit_date", "is_published", "moderation_status", "created_at", ) list_filter = ( "is_published", "rating", "visit_date", "created_at", "park", ) list_select_related = ["park", "user", "moderated_by"] search_fields = ( "title", "content", "user__username", "park__name", ) readonly_fields = ("created_at", "updated_at", "moderated_by", "moderated_at") autocomplete_fields = ["park", "user"] date_hierarchy = "created_at" ordering = ("-created_at",) export_fields = ["id", "park", "user", "rating", "title", "visit_date", "is_published", "created_at"] export_filename_prefix = "park_reviews" fieldsets = ( ( "Review Details", { "fields": ( "user", "park", "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="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="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.") 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", ) return actions @admin.register(pghistory.models.Events) class PgHistoryEventsAdmin(ReadOnlyAdminMixin, ExportActionMixin, BaseModelAdmin): """ Admin interface for pghistory Events. Provides read-only access to change history events for auditing. Superusers can delete records for maintenance purposes. Note: This admin is read-only because history events are auto-generated and should not be manually modified. """ list_display = ( "pgh_id", "pgh_created_at", "pgh_label", "pgh_model", "pgh_obj_id", "pgh_context_display", ) list_filter = ( "pgh_label", "pgh_model", "pgh_created_at", ) search_fields = ( "pgh_obj_id", "pgh_context", "pgh_model", ) readonly_fields = ( "pgh_id", "pgh_created_at", "pgh_label", "pgh_model", "pgh_obj_id", "pgh_context", "pgh_data", ) date_hierarchy = "pgh_created_at" ordering = ("-pgh_created_at",) export_fields = ["pgh_id", "pgh_created_at", "pgh_label", "pgh_model", "pgh_obj_id"] export_filename_prefix = "history_events" fieldsets = ( ( "Event Information", { "fields": ( "pgh_id", "pgh_created_at", "pgh_label", "pgh_model", "pgh_obj_id", ), "description": "Core event identification and timing.", }, ), ( "Context & Data", { "fields": ( "pgh_context", "pgh_data", ), "classes": ("collapse",), "description": "Detailed context and data snapshot at time of event.", }, ), ) @admin.display(description="Context") def pgh_context_display(self, obj): """Display context information in a readable format.""" if obj.pgh_context: if isinstance(obj.pgh_context, dict): context_items = [] for key, value in list(obj.pgh_context.items())[:3]: context_items.append(f"{key}: {value}") result = ", ".join(context_items) if len(obj.pgh_context) > 3: result += "..." return result return str(obj.pgh_context)[:100] return "-" @admin.action(description="Export audit trail to CSV") def export_audit_trail(self, request, queryset): """Export selected history events for audit reporting.""" return self.export_to_csv(request, queryset) def get_actions(self, request): """Add export actions to the admin.""" actions = super().get_actions(request) actions["export_audit_trail"] = ( self.export_audit_trail, "export_audit_trail", "Export audit trail to CSV", ) return actions # Register the models with their admin classes admin.site.register(Park, ParkAdmin) admin.site.register(ParkArea, ParkAreaAdmin) admin.site.register(ParkLocation, ParkLocationAdmin) admin.site.register(Company, CompanyAdmin) admin.site.register(CompanyHeadquarters, CompanyHeadquartersAdmin)