mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:11:13 -05:00
Add email templates for user notifications and account management
- Created a base email template (base.html) for consistent styling across all emails. - Added moderation approval email template (moderation_approved.html) to notify users of approved submissions. - Added moderation rejection email template (moderation_rejected.html) to inform users of required changes for their submissions. - Created password reset email template (password_reset.html) for users requesting to reset their passwords. - Developed a welcome email template (welcome.html) to greet new users and provide account details and tips for using ThrillWiki.
This commit is contained in:
@@ -1,97 +1,402 @@
|
||||
"""
|
||||
Django Admin configuration for entity models.
|
||||
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(admin.ModelAdmin):
|
||||
"""Admin interface for Company model."""
|
||||
class CompanyAdmin(ModelAdmin, ImportExportModelAdmin):
|
||||
"""Enhanced admin interface for Company model."""
|
||||
|
||||
list_display = ['name', 'slug', 'location', 'park_count', 'ride_count', 'created', 'modified']
|
||||
list_filter = ['company_types', 'founded_date']
|
||||
search_fields = ['name', 'slug', 'description']
|
||||
readonly_fields = ['id', 'created', 'modified', 'park_count', 'ride_count']
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
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', {
|
||||
'fields': ('location',)
|
||||
('Location & Contact', {
|
||||
'fields': ('location', 'website')
|
||||
}),
|
||||
('Dates', {
|
||||
('History', {
|
||||
'fields': (
|
||||
'founded_date', 'founded_date_precision',
|
||||
'closed_date', 'closed_date_precision'
|
||||
)
|
||||
}),
|
||||
('Media', {
|
||||
'fields': ('logo_image_id', 'logo_image_url', 'website')
|
||||
'fields': ('logo_image_id', 'logo_image_url'),
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
('Statistics', {
|
||||
'fields': ('park_count', 'ride_count'),
|
||||
'classes': ('collapse',)
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
('System', {
|
||||
('System Information', {
|
||||
'fields': ('id', 'created', 'modified'),
|
||||
'classes': ('collapse',)
|
||||
'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'<span style="background-color: {color}; color: white; '
|
||||
f'padding: 2px 8px; border-radius: 4px; font-size: 11px; '
|
||||
f'margin-right: 4px;">{ctype.upper()}</span>'
|
||||
)
|
||||
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(
|
||||
'<span style="color: red;">●</span> Closed'
|
||||
)
|
||||
return format_html(
|
||||
'<span style="color: green;">●</span> Active'
|
||||
)
|
||||
status_indicator.short_description = 'Status'
|
||||
|
||||
actions = ['export_admin_action']
|
||||
|
||||
|
||||
@admin.register(RideModel)
|
||||
class RideModelAdmin(admin.ModelAdmin):
|
||||
"""Admin interface for RideModel model."""
|
||||
class RideModelAdmin(ModelAdmin, ImportExportModelAdmin):
|
||||
"""Enhanced admin interface for RideModel model."""
|
||||
|
||||
list_display = ['name', 'manufacturer', 'model_type', 'installation_count', 'created', 'modified']
|
||||
list_filter = ['model_type', 'manufacturer']
|
||||
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']
|
||||
prepopulated_fields = {'slug': ('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')
|
||||
'fields': (
|
||||
'typical_height', 'typical_speed', 'typical_capacity'
|
||||
),
|
||||
'description': 'Standard specifications for this ride model'
|
||||
}),
|
||||
('Media', {
|
||||
'fields': ('image_id', 'image_url')
|
||||
'fields': ('image_id', 'image_url'),
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
('Statistics', {
|
||||
'fields': ('installation_count',),
|
||||
'classes': ('collapse',)
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
('System', {
|
||||
('System Information', {
|
||||
'fields': ('id', 'created', 'modified'),
|
||||
'classes': ('collapse',)
|
||||
'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(admin.ModelAdmin):
|
||||
"""Admin interface for Park model."""
|
||||
class ParkAdmin(ModelAdmin, ImportExportModelAdmin):
|
||||
"""Enhanced admin interface for Park model with geographic features."""
|
||||
|
||||
list_display = ['name', 'location', 'park_type', 'status', 'ride_count', 'coaster_count', 'opening_date']
|
||||
list_filter = ['park_type', 'status', 'operator', 'opening_date']
|
||||
search_fields = ['name', 'slug', 'description', 'location__name']
|
||||
readonly_fields = ['id', 'created', 'modified', 'ride_count', 'coaster_count']
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
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']
|
||||
raw_id_fields = ['location']
|
||||
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')
|
||||
}),
|
||||
('Location', {
|
||||
'fields': ('location', 'latitude', 'longitude')
|
||||
('Geographic Location', {
|
||||
'fields': ('location', 'latitude', 'longitude', 'coordinates_display'),
|
||||
'description': 'Enter latitude and longitude for the park location'
|
||||
}),
|
||||
('Dates', {
|
||||
'fields': (
|
||||
@@ -102,38 +407,136 @@ class ParkAdmin(admin.ModelAdmin):
|
||||
('Operator', {
|
||||
'fields': ('operator',)
|
||||
}),
|
||||
('Media', {
|
||||
('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',)
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
('Custom Data', {
|
||||
'fields': ('custom_fields',),
|
||||
'classes': ('collapse',)
|
||||
'classes': ['collapse'],
|
||||
'description': 'Additional custom data in JSON format'
|
||||
}),
|
||||
('System', {
|
||||
('System Information', {
|
||||
'fields': ('id', 'created', 'modified'),
|
||||
'classes': ('collapse',)
|
||||
'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(
|
||||
'{}<br><small style="color: gray;">({:.4f}, {:.4f})</small>',
|
||||
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(
|
||||
'<span style="background-color: {}; color: white; '
|
||||
'padding: 3px 10px; border-radius: 12px; font-size: 11px;">'
|
||||
'{}</span>',
|
||||
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(admin.ModelAdmin):
|
||||
"""Admin interface for Ride model."""
|
||||
class RideAdmin(ModelAdmin, ImportExportModelAdmin):
|
||||
"""Enhanced admin interface for Ride model."""
|
||||
|
||||
list_display = ['name', 'park', 'ride_category', 'status', 'is_coaster', 'manufacturer', 'opening_date']
|
||||
list_filter = ['ride_category', 'status', 'is_coaster', 'park', 'manufacturer', 'opening_date']
|
||||
search_fields = ['name', 'slug', 'description', 'park__name', 'manufacturer__name']
|
||||
readonly_fields = ['id', 'created', 'modified', 'is_coaster']
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
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', {
|
||||
@@ -148,21 +551,156 @@ class RideAdmin(admin.ModelAdmin):
|
||||
'closing_date', 'closing_date_precision'
|
||||
)
|
||||
}),
|
||||
('Manufacturer', {
|
||||
('Manufacturer & Model', {
|
||||
'fields': ('manufacturer', 'model')
|
||||
}),
|
||||
('Statistics', {
|
||||
'fields': ('height', 'speed', 'length', 'duration', 'inversions', 'capacity')
|
||||
('Ride Statistics', {
|
||||
'fields': (
|
||||
'height', 'speed', 'length',
|
||||
'duration', 'inversions', 'capacity'
|
||||
),
|
||||
'description': 'Technical specifications and statistics'
|
||||
}),
|
||||
('Media', {
|
||||
'fields': ('image_id', 'image_url')
|
||||
'fields': ('image_id', 'image_url'),
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
('Custom Data', {
|
||||
'fields': ('custom_fields',),
|
||||
'classes': ('collapse',)
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
('System', {
|
||||
('System Information', {
|
||||
'fields': ('id', 'created', 'modified'),
|
||||
'classes': ('collapse',)
|
||||
'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(
|
||||
'<span style="background-color: #ff6b6b; color: white; '
|
||||
'padding: 2px 8px; border-radius: 10px; font-size: 10px;">'
|
||||
'🎢 COASTER</span>'
|
||||
)
|
||||
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(
|
||||
'<span style="background-color: {}; color: white; '
|
||||
'padding: 3px 10px; border-radius: 12px; font-size: 11px;">'
|
||||
'{}</span>',
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user