Files
thrillwiki_django_no_react/backend/apps/parks/admin.py
pacnpal edcd8f2076 Add secret management guide, client-side performance monitoring, and search accessibility enhancements
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols.
- Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage.
- Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
2025-12-23 16:41:42 -05:00

1012 lines
30 KiB
Python

"""
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('<a href="{}">{}</a>', 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(
'<span style="background-color: {}; color: white; padding: 2px 8px; '
'border-radius: 4px; font-size: 11px;">{}</span>',
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('<a href="{}">{}</a>', 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('<span style="color: gold;">{}</span> {:.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('<a href="{}">{}</a>', 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('<a href="{}">{}</a>', 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'<span style="background-color: {color}; color: white; '
f'padding: 2px 6px; border-radius: 3px; font-size: 10px; '
f'margin-right: 4px;">{role}</span>'
)
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('<a href="{}">{}</a>', 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('<a href="{}">{}</a>', 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('<span style="color: gold;">{}</span>', 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(
'<span style="color: green; font-weight: bold;">Approved</span>'
)
return format_html(
'<span style="color: red; font-weight: bold;">Rejected</span>'
)
return format_html('<span style="color: orange;">Pending</span>')
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)