mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:11:13 -05:00
- 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.
707 lines
23 KiB
Python
707 lines
23 KiB
Python
"""
|
|
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'<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(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(
|
|
'{}<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(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(
|
|
'<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
|