""" Django Admin configuration for entity models with Unfold theme. """ from django.contrib import admin from django.contrib.gis import admin as gis_admin from django.db.models import Count, Q from django.utils.html import format_html from django.urls import reverse from django.conf import settings from unfold.admin import ModelAdmin, TabularInline from unfold.contrib.filters.admin import RangeDateFilter, RangeNumericFilter, RelatedDropdownFilter, ChoicesDropdownFilter from unfold.contrib.import_export.forms import ImportForm, ExportForm from import_export.admin import ImportExportModelAdmin from import_export import resources, fields from import_export.widgets import ForeignKeyWidget from .models import Company, RideModel, Park, Ride from apps.media.admin import PhotoInline # ============================================================================ # IMPORT/EXPORT RESOURCES # ============================================================================ class CompanyResource(resources.ModelResource): """Import/Export resource for Company model.""" class Meta: model = Company fields = ( 'id', 'name', 'slug', 'description', 'location', 'company_types', 'founded_date', 'founded_date_precision', 'closed_date', 'closed_date_precision', 'website', 'logo_image_url', 'created', 'modified' ) export_order = fields class RideModelResource(resources.ModelResource): """Import/Export resource for RideModel model.""" manufacturer = fields.Field( column_name='manufacturer', attribute='manufacturer', widget=ForeignKeyWidget(Company, 'name') ) class Meta: model = RideModel fields = ( 'id', 'name', 'slug', 'description', 'manufacturer', 'model_type', 'typical_height', 'typical_speed', 'typical_capacity', 'image_url', 'created', 'modified' ) export_order = fields class ParkResource(resources.ModelResource): """Import/Export resource for Park model.""" operator = fields.Field( column_name='operator', attribute='operator', widget=ForeignKeyWidget(Company, 'name') ) class Meta: model = Park fields = ( 'id', 'name', 'slug', 'description', 'park_type', 'status', 'latitude', 'longitude', 'operator', 'opening_date', 'opening_date_precision', 'closing_date', 'closing_date_precision', 'website', 'banner_image_url', 'logo_image_url', 'created', 'modified' ) export_order = fields class RideResource(resources.ModelResource): """Import/Export resource for Ride model.""" park = fields.Field( column_name='park', attribute='park', widget=ForeignKeyWidget(Park, 'name') ) manufacturer = fields.Field( column_name='manufacturer', attribute='manufacturer', widget=ForeignKeyWidget(Company, 'name') ) model = fields.Field( column_name='model', attribute='model', widget=ForeignKeyWidget(RideModel, 'name') ) class Meta: model = Ride fields = ( 'id', 'name', 'slug', 'description', 'park', 'ride_category', 'ride_type', 'status', 'manufacturer', 'model', 'height', 'speed', 'length', 'duration', 'inversions', 'capacity', 'opening_date', 'opening_date_precision', 'closing_date', 'closing_date_precision', 'image_url', 'created', 'modified' ) export_order = fields # ============================================================================ # INLINE ADMIN CLASSES # ============================================================================ class RideInline(TabularInline): """Inline for Rides within a Park.""" model = Ride extra = 0 fields = ['name', 'ride_category', 'status', 'manufacturer', 'opening_date'] readonly_fields = ['name'] show_change_link = True classes = ['collapse'] def has_add_permission(self, request, obj=None): return False class CompanyParksInline(TabularInline): """Inline for Parks operated by a Company.""" model = Park fk_name = 'operator' extra = 0 fields = ['name', 'park_type', 'status', 'ride_count', 'opening_date'] readonly_fields = ['name', 'ride_count'] show_change_link = True classes = ['collapse'] def has_add_permission(self, request, obj=None): return False class RideModelInstallationsInline(TabularInline): """Inline for Ride installations of a RideModel.""" model = Ride fk_name = 'model' extra = 0 fields = ['name', 'park', 'status', 'opening_date'] readonly_fields = ['name', 'park'] show_change_link = True classes = ['collapse'] def has_add_permission(self, request, obj=None): return False # ============================================================================ # MAIN ADMIN CLASSES # ============================================================================ @admin.register(Company) class CompanyAdmin(ModelAdmin, ImportExportModelAdmin): """Enhanced admin interface for Company model.""" resource_class = CompanyResource import_form_class = ImportForm export_form_class = ExportForm list_display = [ 'name_with_icon', 'location', 'company_types_display', 'park_count', 'ride_count', 'founded_date', 'status_indicator', 'created' ] list_filter = [ ('company_types', ChoicesDropdownFilter), ('founded_date', RangeDateFilter), ('closed_date', RangeDateFilter), ] search_fields = ['name', 'slug', 'description', 'location'] readonly_fields = ['id', 'created', 'modified', 'park_count', 'ride_count', 'slug'] prepopulated_fields = {} # Slug is auto-generated via lifecycle hook autocomplete_fields = [] inlines = [CompanyParksInline, PhotoInline] list_per_page = 50 list_max_show_all = 200 fieldsets = ( ('Basic Information', { 'fields': ('name', 'slug', 'description', 'company_types') }), ('Location & Contact', { 'fields': ('location', 'website') }), ('History', { 'fields': ( 'founded_date', 'founded_date_precision', 'closed_date', 'closed_date_precision' ) }), ('Media', { 'fields': ('logo_image_id', 'logo_image_url'), 'classes': ['collapse'] }), ('Statistics', { 'fields': ('park_count', 'ride_count'), 'classes': ['collapse'] }), ('System Information', { 'fields': ('id', 'created', 'modified'), 'classes': ['collapse'] }), ) def name_with_icon(self, obj): """Display name with company type icon.""" icons = { 'manufacturer': '🏭', 'operator': '🎡', 'designer': '✏️', } icon = '🏢' # Default company icon if obj.company_types: for ctype in obj.company_types: if ctype in icons: icon = icons[ctype] break return format_html('{} {}', icon, obj.name) name_with_icon.short_description = 'Company' name_with_icon.admin_order_field = 'name' def company_types_display(self, obj): """Display company types as badges.""" if not obj.company_types: return '-' badges = [] for ctype in obj.company_types: color = { 'manufacturer': 'blue', 'operator': 'green', 'designer': 'purple', }.get(ctype, 'gray') badges.append( f'{ctype.upper()}' ) return format_html(' '.join(badges)) company_types_display.short_description = 'Types' def status_indicator(self, obj): """Visual status indicator.""" if obj.closed_date: return format_html( ' Closed' ) return format_html( ' Active' ) status_indicator.short_description = 'Status' actions = ['export_admin_action'] @admin.register(RideModel) class RideModelAdmin(ModelAdmin, ImportExportModelAdmin): """Enhanced admin interface for RideModel model.""" resource_class = RideModelResource import_form_class = ImportForm export_form_class = ExportForm list_display = [ 'name_with_type', 'manufacturer', 'model_type', 'typical_specs', 'installation_count', 'created' ] list_filter = [ ('model_type', ChoicesDropdownFilter), ('manufacturer', RelatedDropdownFilter), ('typical_height', RangeNumericFilter), ('typical_speed', RangeNumericFilter), ] search_fields = ['name', 'slug', 'description', 'manufacturer__name'] readonly_fields = ['id', 'created', 'modified', 'installation_count', 'slug'] prepopulated_fields = {} autocomplete_fields = ['manufacturer'] inlines = [RideModelInstallationsInline, PhotoInline] list_per_page = 50 fieldsets = ( ('Basic Information', { 'fields': ('name', 'slug', 'description', 'manufacturer', 'model_type') }), ('Typical Specifications', { 'fields': ( 'typical_height', 'typical_speed', 'typical_capacity' ), 'description': 'Standard specifications for this ride model' }), ('Media', { 'fields': ('image_id', 'image_url'), 'classes': ['collapse'] }), ('Statistics', { 'fields': ('installation_count',), 'classes': ['collapse'] }), ('System Information', { 'fields': ('id', 'created', 'modified'), 'classes': ['collapse'] }), ) def name_with_type(self, obj): """Display name with model type icon.""" icons = { 'roller_coaster': '🎢', 'water_ride': '🌊', 'flat_ride': '🎡', 'dark_ride': '🎭', 'transport': '🚂', } icon = icons.get(obj.model_type, '🎪') return format_html('{} {}', icon, obj.name) name_with_type.short_description = 'Model Name' name_with_type.admin_order_field = 'name' def typical_specs(self, obj): """Display typical specifications.""" specs = [] if obj.typical_height: specs.append(f'H: {obj.typical_height}m') if obj.typical_speed: specs.append(f'S: {obj.typical_speed}km/h') if obj.typical_capacity: specs.append(f'C: {obj.typical_capacity}') return ' | '.join(specs) if specs else '-' typical_specs.short_description = 'Typical Specs' actions = ['export_admin_action'] @admin.register(Park) class ParkAdmin(ModelAdmin, ImportExportModelAdmin): """Enhanced admin interface for Park model with geographic features.""" resource_class = ParkResource import_form_class = ImportForm export_form_class = ExportForm list_display = [ 'name_with_icon', 'location_display', 'park_type', 'status_badge', 'ride_count', 'coaster_count', 'opening_date', 'operator' ] list_filter = [ ('park_type', ChoicesDropdownFilter), ('status', ChoicesDropdownFilter), ('operator', RelatedDropdownFilter), ('opening_date', RangeDateFilter), ('closing_date', RangeDateFilter), ] search_fields = ['name', 'slug', 'description', 'location'] readonly_fields = [ 'id', 'created', 'modified', 'ride_count', 'coaster_count', 'slug', 'coordinates_display' ] prepopulated_fields = {} autocomplete_fields = ['operator'] inlines = [RideInline, PhotoInline] list_per_page = 50 # Use GeoDjango admin for PostGIS mode if hasattr(settings, 'DATABASES') and 'postgis' in settings.DATABASES['default'].get('ENGINE', ''): change_form_template = 'gis/admin/change_form.html' fieldsets = ( ('Basic Information', { 'fields': ('name', 'slug', 'description', 'park_type', 'status') }), ('Geographic Location', { 'fields': ('location', 'latitude', 'longitude', 'coordinates_display'), 'description': 'Enter latitude and longitude for the park location' }), ('Dates', { 'fields': ( 'opening_date', 'opening_date_precision', 'closing_date', 'closing_date_precision' ) }), ('Operator', { 'fields': ('operator',) }), ('Media & Web', { 'fields': ( 'banner_image_id', 'banner_image_url', 'logo_image_id', 'logo_image_url', 'website' ), 'classes': ['collapse'] }), ('Statistics', { 'fields': ('ride_count', 'coaster_count'), 'classes': ['collapse'] }), ('Custom Data', { 'fields': ('custom_fields',), 'classes': ['collapse'], 'description': 'Additional custom data in JSON format' }), ('System Information', { 'fields': ('id', 'created', 'modified'), 'classes': ['collapse'] }), ) def name_with_icon(self, obj): """Display name with park type icon.""" icons = { 'theme_park': '🎡', 'amusement_park': '🎢', 'water_park': '🌊', 'indoor_park': '🏢', 'fairground': '🎪', } icon = icons.get(obj.park_type, '🎠') return format_html('{} {}', icon, obj.name) name_with_icon.short_description = 'Park Name' name_with_icon.admin_order_field = 'name' def location_display(self, obj): """Display location with coordinates.""" if obj.location: coords = obj.coordinates if coords: return format_html( '{}
({:.4f}, {:.4f})', obj.location, coords[0], coords[1] ) return obj.location return '-' location_display.short_description = 'Location' def coordinates_display(self, obj): """Read-only display of coordinates.""" coords = obj.coordinates if coords: return f"Longitude: {coords[0]:.6f}, Latitude: {coords[1]:.6f}" return "No coordinates set" coordinates_display.short_description = 'Current Coordinates' def status_badge(self, obj): """Display status as colored badge.""" colors = { 'operating': 'green', 'closed_temporarily': 'orange', 'closed_permanently': 'red', 'under_construction': 'blue', 'planned': 'purple', } color = colors.get(obj.status, 'gray') return format_html( '' '{}', color, obj.get_status_display() ) status_badge.short_description = 'Status' status_badge.admin_order_field = 'status' actions = ['export_admin_action', 'activate_parks', 'close_parks'] def activate_parks(self, request, queryset): """Bulk action to activate parks.""" updated = queryset.update(status='operating') self.message_user(request, f'{updated} park(s) marked as operating.') activate_parks.short_description = 'Mark selected parks as operating' def close_parks(self, request, queryset): """Bulk action to close parks temporarily.""" updated = queryset.update(status='closed_temporarily') self.message_user(request, f'{updated} park(s) marked as temporarily closed.') close_parks.short_description = 'Mark selected parks as temporarily closed' @admin.register(Ride) class RideAdmin(ModelAdmin, ImportExportModelAdmin): """Enhanced admin interface for Ride model.""" resource_class = RideResource import_form_class = ImportForm export_form_class = ExportForm list_display = [ 'name_with_icon', 'park', 'ride_category', 'status_badge', 'manufacturer', 'stats_display', 'opening_date', 'coaster_badge' ] list_filter = [ ('ride_category', ChoicesDropdownFilter), ('status', ChoicesDropdownFilter), ('is_coaster', admin.BooleanFieldListFilter), ('park', RelatedDropdownFilter), ('manufacturer', RelatedDropdownFilter), ('opening_date', RangeDateFilter), ('height', RangeNumericFilter), ('speed', RangeNumericFilter), ] search_fields = [ 'name', 'slug', 'description', 'park__name', 'manufacturer__name' ] readonly_fields = ['id', 'created', 'modified', 'is_coaster', 'slug'] prepopulated_fields = {} autocomplete_fields = ['park', 'manufacturer', 'model'] inlines = [PhotoInline] list_per_page = 50 fieldsets = ( ('Basic Information', { 'fields': ('name', 'slug', 'description', 'park') }), ('Classification', { 'fields': ('ride_category', 'ride_type', 'is_coaster', 'status') }), ('Dates', { 'fields': ( 'opening_date', 'opening_date_precision', 'closing_date', 'closing_date_precision' ) }), ('Manufacturer & Model', { 'fields': ('manufacturer', 'model') }), ('Ride Statistics', { 'fields': ( 'height', 'speed', 'length', 'duration', 'inversions', 'capacity' ), 'description': 'Technical specifications and statistics' }), ('Media', { 'fields': ('image_id', 'image_url'), 'classes': ['collapse'] }), ('Custom Data', { 'fields': ('custom_fields',), 'classes': ['collapse'] }), ('System Information', { 'fields': ('id', 'created', 'modified'), 'classes': ['collapse'] }), ) def name_with_icon(self, obj): """Display name with category icon.""" icons = { 'roller_coaster': '🎢', 'water_ride': '🌊', 'dark_ride': '🎭', 'flat_ride': '🎡', 'transport': '🚂', 'show': '🎪', } icon = icons.get(obj.ride_category, '🎠') return format_html('{} {}', icon, obj.name) name_with_icon.short_description = 'Ride Name' name_with_icon.admin_order_field = 'name' def stats_display(self, obj): """Display key statistics.""" stats = [] if obj.height: stats.append(f'H: {obj.height}m') if obj.speed: stats.append(f'S: {obj.speed}km/h') if obj.inversions: stats.append(f'🔄 {obj.inversions}') return ' | '.join(stats) if stats else '-' stats_display.short_description = 'Key Stats' def coaster_badge(self, obj): """Display coaster indicator.""" if obj.is_coaster: return format_html( '' '🎢 COASTER' ) return '' coaster_badge.short_description = 'Type' def status_badge(self, obj): """Display status as colored badge.""" colors = { 'operating': 'green', 'closed_temporarily': 'orange', 'closed_permanently': 'red', 'under_construction': 'blue', 'sbno': 'gray', } color = colors.get(obj.status, 'gray') return format_html( '' '{}', color, obj.get_status_display() ) status_badge.short_description = 'Status' status_badge.admin_order_field = 'status' actions = ['export_admin_action', 'activate_rides', 'close_rides'] def activate_rides(self, request, queryset): """Bulk action to activate rides.""" updated = queryset.update(status='operating') self.message_user(request, f'{updated} ride(s) marked as operating.') activate_rides.short_description = 'Mark selected rides as operating' def close_rides(self, request, queryset): """Bulk action to close rides temporarily.""" updated = queryset.update(status='closed_temporarily') self.message_user(request, f'{updated} ride(s) marked as temporarily closed.') close_rides.short_description = 'Mark selected rides as temporarily closed' # ============================================================================ # DASHBOARD CALLBACK # ============================================================================ def dashboard_callback(request, context): """ Callback function for Unfold dashboard. Provides statistics and overview data. """ # Entity counts total_parks = Park.objects.count() total_rides = Ride.objects.count() total_companies = Company.objects.count() total_models = RideModel.objects.count() # Operating counts operating_parks = Park.objects.filter(status='operating').count() operating_rides = Ride.objects.filter(status='operating').count() # Coaster count total_coasters = Ride.objects.filter(is_coaster=True).count() # Recent additions (last 30 days) from django.utils import timezone from datetime import timedelta thirty_days_ago = timezone.now() - timedelta(days=30) recent_parks = Park.objects.filter(created__gte=thirty_days_ago).count() recent_rides = Ride.objects.filter(created__gte=thirty_days_ago).count() # Top manufacturers by ride count top_manufacturers = Company.objects.filter( company_types__contains=['manufacturer'] ).annotate( ride_count_actual=Count('manufactured_rides') ).order_by('-ride_count_actual')[:5] # Parks by type parks_by_type = Park.objects.values('park_type').annotate( count=Count('id') ).order_by('-count') context.update({ 'total_parks': total_parks, 'total_rides': total_rides, 'total_companies': total_companies, 'total_models': total_models, 'operating_parks': operating_parks, 'operating_rides': operating_rides, 'total_coasters': total_coasters, 'recent_parks': recent_parks, 'recent_rides': recent_rides, 'top_manufacturers': top_manufacturers, 'parks_by_type': parks_by_type, }) return context