here we go

This commit is contained in:
pacnpal
2024-10-31 22:32:01 +00:00
parent c52f14e700
commit 80a9d61ca2
68 changed files with 3114 additions and 1485 deletions

View File

@@ -1,19 +1,49 @@
from django.contrib import admin
from django.utils.html import format_html
from simple_history.admin import SimpleHistoryAdmin
from .models import Park, ParkArea
@admin.register(Park)
class ParkAdmin(SimpleHistoryAdmin):
list_display = ('name', 'location', 'owner', 'status', 'opening_date')
list_filter = ('status', 'owner')
list_display = ('name', 'location', 'status', 'owner', 'created_at', 'updated_at')
list_filter = ('status', 'country', 'region', 'city')
search_fields = ('name', 'location', 'description')
readonly_fields = ('created_at', 'updated_at')
prepopulated_fields = {'slug': ('name',)}
readonly_fields = ('id', 'created_at', 'updated_at')
@admin.register(ParkArea)
def get_history_list_display(self, request):
"""Customize the list display for history records"""
return ('name', 'location', 'status', 'history_date', 'history_user')
class ParkAreaAdmin(SimpleHistoryAdmin):
list_display = ('name', 'park', 'opening_date')
list_display = ('name', 'park', 'created_at', 'updated_at')
list_filter = ('park',)
search_fields = ('name', 'description')
search_fields = ('name', 'description', 'park__name')
readonly_fields = ('created_at', 'updated_at')
prepopulated_fields = {'slug': ('name',)}
readonly_fields = ('id', 'created_at', 'updated_at')
def get_history_list_display(self, request):
"""Customize the list display for history records"""
return ('name', 'park', 'history_date', 'history_user')
# Register the models with their admin classes
admin.site.register(Park, ParkAdmin)
admin.site.register(ParkArea, ParkAreaAdmin)
# Register the historical records for direct editing
from simple_history.models import HistoricalRecords
from .models import Park
class HistoricalParkAdmin(admin.ModelAdmin):
list_display = ('name', 'location', 'status', 'history_date', 'history_user', 'history_type')
list_filter = ('status', 'country', 'region', 'city', 'history_type')
search_fields = ('name', 'location', 'description')
readonly_fields = ('history_date', 'history_type', 'history_id')
def has_add_permission(self, request):
return False # Prevent adding new historical records directly
def has_delete_permission(self, request, obj=None):
return False # Prevent deleting historical records
# Register the historical model
admin.site.register(Park.history.model, HistoricalParkAdmin)

View File

@@ -3,48 +3,89 @@ from django.urls import reverse_lazy
from .models import Park
from cities_light.models import Country, Region, City
class LocationFilterForm(forms.Form):
country = forms.CharField(
required=False,
widget=forms.TextInput(attrs={
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white',
'placeholder': 'Select or type a country',
'hx-get': reverse_lazy('parks:country_search'),
'hx-trigger': 'focus, input',
'hx-target': '#country-results',
'autocomplete': 'off'
})
)
region = forms.CharField(
required=False,
widget=forms.TextInput(attrs={
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white',
'placeholder': 'Select or type a region/state',
'hx-get': reverse_lazy('parks:region_search'),
'hx-trigger': 'focus, input',
'hx-target': '#region-results',
'autocomplete': 'off'
})
)
city = forms.CharField(
required=False,
widget=forms.TextInput(attrs={
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white',
'placeholder': 'Select or type a city',
'hx-get': reverse_lazy('parks:city_search'),
'hx-trigger': 'focus, input',
'hx-target': '#city-results',
'autocomplete': 'off'
})
)
class ParkForm(forms.ModelForm):
# Hidden fields for actual model relations
country = forms.ModelChoiceField(queryset=Country.objects.all(), required=True, widget=forms.HiddenInput())
region = forms.ModelChoiceField(queryset=Region.objects.all(), required=False, widget=forms.HiddenInput())
city = forms.ModelChoiceField(queryset=City.objects.all(), required=False, widget=forms.HiddenInput())
# Visible fields for Awesomplete
# Visible fields for Alpine.js
country_name = forms.CharField(
label="Country",
required=True,
widget=forms.TextInput(attrs={
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white',
'placeholder': 'Start typing a country name...',
'placeholder': 'Select or type a country',
'data-autocomplete': 'true'
})
)
region_name = forms.CharField(
label="Region/State",
required=False,
widget=forms.TextInput(attrs={
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white',
'placeholder': 'Start typing a region/state name...',
'placeholder': 'Select or type a region/state',
'data-autocomplete': 'true'
})
)
city_name = forms.CharField(
label="City",
required=False,
widget=forms.TextInput(attrs={
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white',
'placeholder': 'Start typing a city name...',
'placeholder': 'Select or type a city',
'data-autocomplete': 'true'
})
)
class Meta:
model = Park
fields = ['name', 'country', 'region', 'city', 'description', 'owner', 'status',
'opening_date', 'closing_date', 'operating_season', 'size_acres', 'website']
fields = ['name', 'description', 'owner', 'status', 'opening_date', 'closing_date',
'operating_season', 'size_acres', 'website', 'country', 'region', 'city']
widgets = {
'name': forms.TextInput(attrs={
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white'
}),
'description': forms.Textarea(attrs={
'rows': 4,
'class': 'w-full border-gray-300 rounded-lg form-textarea dark:border-gray-600 dark:bg-gray-700 dark:text-white'
'class': 'w-full border-gray-300 rounded-lg form-textarea dark:border-gray-600 dark:bg-gray-700 dark:text-white',
'rows': 4
}),
'owner': forms.Select(attrs={
'class': 'w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white'
@@ -79,15 +120,26 @@ class ParkForm(forms.ModelForm):
super().__init__(*args, **kwargs)
instance = kwargs.get('instance')
if instance:
if instance.country:
self.fields['country_name'].initial = instance.country.name
self.fields['country'].initial = instance.country
if instance.region:
self.fields['region_name'].initial = instance.region.name
self.fields['region'].initial = instance.region
if instance.city:
self.fields['city_name'].initial = instance.city.name
self.fields['city'].initial = instance.city
try:
if instance.country:
self.fields['country_name'].initial = instance.country.name
self.fields['country'].initial = instance.country
except Country.DoesNotExist:
pass
try:
if instance.region:
self.fields['region_name'].initial = instance.region.name
self.fields['region'].initial = instance.region
except Region.DoesNotExist:
pass
try:
if instance.city:
self.fields['city_name'].initial = instance.city.name
self.fields['city'].initial = instance.city
except City.DoesNotExist:
pass
def clean(self):
cleaned_data = super().clean()
@@ -95,12 +147,26 @@ class ParkForm(forms.ModelForm):
region_name = cleaned_data.get('region_name')
city_name = cleaned_data.get('city_name')
# Get or create default country if needed
default_country = Country.objects.first()
if not default_country:
default_country = Country.objects.create(
name='Unknown',
name_ascii='Unknown',
slug='unknown',
geoname_id=0,
alternate_names='',
search_names='Unknown'
)
if country_name:
try:
country = Country.objects.get(name__iexact=country_name)
cleaned_data['country'] = country
except Country.DoesNotExist:
self.add_error('country_name', 'Invalid country name')
cleaned_data['country'] = default_country
else:
raise forms.ValidationError("Country is required")
if region_name and cleaned_data.get('country'):
try:
@@ -110,7 +176,7 @@ class ParkForm(forms.ModelForm):
)
cleaned_data['region'] = region
except Region.DoesNotExist:
self.add_error('region_name', 'Invalid region name for selected country')
cleaned_data['region'] = None
if city_name and cleaned_data.get('region'):
try:
@@ -120,6 +186,6 @@ class ParkForm(forms.ModelForm):
)
cleaned_data['city'] = city
except City.DoesNotExist:
self.add_error('city_name', 'Invalid city name for selected region')
cleaned_data['city'] = None
return cleaned_data

View File

@@ -0,0 +1,99 @@
from django.core.management.base import BaseCommand
from django.db import connection
class Command(BaseCommand):
help = 'Fix historical park records with null location values'
def handle(self, *args, **options):
with connection.cursor() as cursor:
# Make fields nullable temporarily
self.stdout.write('Making fields nullable...')
cursor.execute("""
ALTER TABLE parks_historicalpark
ALTER COLUMN city_id DROP NOT NULL,
ALTER COLUMN region_id DROP NOT NULL,
ALTER COLUMN country_id DROP NOT NULL,
ALTER COLUMN location DROP NOT NULL;
""")
# Get or create default locations
self.stdout.write('Creating default locations if needed...')
# Check if Unknown country exists
cursor.execute("SELECT id FROM cities_light_country WHERE name = 'Unknown' LIMIT 1;")
result = cursor.fetchone()
if not result:
cursor.execute("""
INSERT INTO cities_light_country (name, name_ascii, slug, geoname_id, alternate_names, display_name, search_names)
VALUES ('Unknown', 'Unknown', 'unknown', 0, '', 'Unknown', 'Unknown')
RETURNING id;
""")
default_country_id = cursor.fetchone()[0]
else:
default_country_id = result[0]
# Check if Unknown region exists
cursor.execute("""
SELECT id FROM cities_light_region
WHERE name = 'Unknown' AND country_id = %s LIMIT 1;
""", [default_country_id])
result = cursor.fetchone()
if not result:
cursor.execute("""
INSERT INTO cities_light_region (name, name_ascii, slug, geoname_id, alternate_names, country_id, display_name, search_names)
VALUES ('Unknown', 'Unknown', 'unknown', 0, '', %s, 'Unknown', 'Unknown')
RETURNING id;
""", [default_country_id])
default_region_id = cursor.fetchone()[0]
else:
default_region_id = result[0]
# Check if Unknown city exists
cursor.execute("""
SELECT id FROM cities_light_city
WHERE name = 'Unknown' AND region_id = %s LIMIT 1;
""", [default_region_id])
result = cursor.fetchone()
if not result:
cursor.execute("""
INSERT INTO cities_light_city (
name, name_ascii, slug, geoname_id, alternate_names,
region_id, country_id, display_name, search_names,
latitude, longitude, population
)
VALUES (
'Unknown', 'Unknown', 'unknown', 0, '',
%s, %s, 'Unknown', 'Unknown',
0, 0, 0
)
RETURNING id;
""", [default_region_id, default_country_id])
default_city_id = cursor.fetchone()[0]
else:
default_city_id = result[0]
# Update historical records with null values
self.stdout.write('Updating historical records...')
cursor.execute("""
UPDATE parks_historicalpark
SET country_id = %s,
region_id = %s,
city_id = %s,
location = 'Unknown, Unknown, Unknown'
WHERE country_id IS NULL
OR region_id IS NULL
OR city_id IS NULL
OR location IS NULL;
""", [default_country_id, default_region_id, default_city_id])
# Make fields non-nullable again
self.stdout.write('Making fields non-nullable...')
cursor.execute("""
ALTER TABLE parks_historicalpark
ALTER COLUMN city_id SET NOT NULL,
ALTER COLUMN region_id SET NOT NULL,
ALTER COLUMN country_id SET NOT NULL,
ALTER COLUMN location SET NOT NULL;
""")
self.stdout.write(self.style.SUCCESS('Successfully fixed historical records'))

View File

@@ -0,0 +1,90 @@
from django.core.management.base import BaseCommand
from django.db import connection
class Command(BaseCommand):
help = 'Fix location fields in parks and historical records'
def handle(self, *args, **options):
with connection.cursor() as cursor:
# Check if Unknown country exists
cursor.execute("""
SELECT id FROM cities_light_country WHERE name = 'Unknown' LIMIT 1;
""")
result = cursor.fetchone()
if result:
default_country_id = result[0]
else:
cursor.execute("""
INSERT INTO cities_light_country (name, name_ascii, slug, geoname_id, alternate_names)
VALUES ('Unknown', 'Unknown', 'unknown', 0, '')
RETURNING id;
""")
default_country_id = cursor.fetchone()[0]
# Check if Unknown region exists
cursor.execute("""
SELECT id FROM cities_light_region
WHERE name = 'Unknown' AND country_id = %s LIMIT 1;
""", [default_country_id])
result = cursor.fetchone()
if result:
default_region_id = result[0]
else:
cursor.execute("""
INSERT INTO cities_light_region (name, name_ascii, slug, geoname_id, alternate_names, country_id, display_name)
VALUES ('Unknown', 'Unknown', 'unknown', 0, '', %s, 'Unknown')
RETURNING id;
""", [default_country_id])
default_region_id = cursor.fetchone()[0]
# Check if Unknown city exists
cursor.execute("""
SELECT id FROM cities_light_city
WHERE name = 'Unknown' AND region_id = %s LIMIT 1;
""", [default_region_id])
result = cursor.fetchone()
if result:
default_city_id = result[0]
else:
cursor.execute("""
INSERT INTO cities_light_city (
name, name_ascii, slug, geoname_id, alternate_names,
region_id, country_id, display_name,
latitude, longitude, population
)
VALUES (
'Unknown', 'Unknown', 'unknown', 0, '',
%s, %s, 'Unknown',
0, 0, 0
)
RETURNING id;
""", [default_region_id, default_country_id])
default_city_id = cursor.fetchone()[0]
# Update parks with null locations
cursor.execute("""
UPDATE parks_park
SET country_id = %s,
region_id = %s,
city_id = %s,
location = 'Unknown, Unknown, Unknown'
WHERE country_id IS NULL
OR region_id IS NULL
OR city_id IS NULL
OR location IS NULL;
""", [default_country_id, default_region_id, default_city_id])
# Update historical records with null locations
cursor.execute("""
UPDATE parks_historicalpark
SET country_id = %s,
region_id = %s,
city_id = %s,
location = 'Unknown, Unknown, Unknown'
WHERE country_id IS NULL
OR region_id IS NULL
OR city_id IS NULL
OR location IS NULL;
""", [default_country_id, default_region_id, default_city_id])
self.stdout.write(self.style.SUCCESS('Successfully fixed location fields'))

View File

@@ -10,6 +10,11 @@
"website": "https://disneyworld.disney.go.com/destinations/magic-kingdom/",
"owner": "The Walt Disney Company",
"size_acres": 142,
"photos": [
"https://upload.wikimedia.org/wikipedia/commons/thumb/8/8d/Walt_Disney_World_Magic_Kingdom_Cinderella_Castle.jpg/1280px-Walt_Disney_World_Magic_Kingdom_Cinderella_Castle.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/9/96/Magic_Kingdom_Main_Street_USA_Panorama.jpg/1280px-Magic_Kingdom_Main_Street_USA_Panorama.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/5/5d/Magic_Kingdom_-_Cinderella_Castle_at_Night.jpg/1280px-Magic_Kingdom_-_Cinderella_Castle_at_Night.jpg"
],
"rides": [
{
"name": "Space Mountain",
@@ -18,6 +23,10 @@
"status": "OPERATING",
"manufacturer": "Walt Disney Imagineering",
"description": "A high-speed roller coaster in the dark through space.",
"photos": [
"https://upload.wikimedia.org/wikipedia/commons/thumb/1/1b/Magic_Kingdom_Space_Mountain.jpg/1280px-Magic_Kingdom_Space_Mountain.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/Space_Mountain_%28Magic_Kingdom%29_entrance.jpg/1280px-Space_Mountain_%28Magic_Kingdom%29_entrance.jpg"
],
"stats": {
"height_ft": 183,
"length_ft": 3196,
@@ -33,6 +42,10 @@
"status": "OPERATING",
"manufacturer": "Walt Disney Imagineering",
"description": "A mine train roller coaster through the Old West.",
"photos": [
"https://upload.wikimedia.org/wikipedia/commons/thumb/9/9c/Big_Thunder_Mountain_Railroad_at_Magic_Kingdom.jpg/1280px-Big_Thunder_Mountain_Railroad_at_Magic_Kingdom.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/5/5d/Big_Thunder_Mountain_Railroad_%28Magic_Kingdom%29.jpg/1280px-Big_Thunder_Mountain_Railroad_%28Magic_Kingdom%29.jpg"
],
"stats": {
"height_ft": 104,
"length_ft": 2671,
@@ -48,6 +61,10 @@
"status": "OPERATING",
"manufacturer": "Vekoma",
"description": "A family roller coaster featuring unique swinging cars.",
"photos": [
"https://upload.wikimedia.org/wikipedia/commons/thumb/d/d6/Seven_Dwarfs_Mine_Train_at_Magic_Kingdom.jpg/1280px-Seven_Dwarfs_Mine_Train_at_Magic_Kingdom.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/f/f5/Seven_Dwarfs_Mine_Train_drop.jpg/1280px-Seven_Dwarfs_Mine_Train_drop.jpg"
],
"stats": {
"height_ft": 112,
"length_ft": 2000,
@@ -62,7 +79,11 @@
"opening_date": "1971-10-01",
"status": "OPERATING",
"manufacturer": "Walt Disney Imagineering",
"description": "A dark ride through a haunted estate."
"description": "A dark ride through a haunted estate.",
"photos": [
"https://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Haunted_Mansion_at_Magic_Kingdom.jpg/1280px-Haunted_Mansion_at_Magic_Kingdom.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/2/2b/Haunted_Mansion_entrance.jpg/1280px-Haunted_Mansion_entrance.jpg"
]
},
{
"name": "Pirates of the Caribbean",
@@ -70,7 +91,11 @@
"opening_date": "1973-12-15",
"status": "OPERATING",
"manufacturer": "Walt Disney Imagineering",
"description": "A boat ride through pirate-filled Caribbean waters."
"description": "A boat ride through pirate-filled Caribbean waters.",
"photos": [
"https://upload.wikimedia.org/wikipedia/commons/thumb/2/2f/Pirates_of_the_Caribbean_%28Magic_Kingdom%29.jpg/1280px-Pirates_of_the_Caribbean_%28Magic_Kingdom%29.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/8/89/Pirates_of_the_Caribbean_entrance.jpg/1280px-Pirates_of_the_Caribbean_entrance.jpg"
]
}
]
},
@@ -84,6 +109,11 @@
"website": "https://www.cedarpoint.com",
"owner": "Cedar Fair",
"size_acres": 364,
"photos": [
"https://upload.wikimedia.org/wikipedia/commons/thumb/7/7c/Cedar_Point_aerial_view.jpg/1280px-Cedar_Point_aerial_view.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/8/8d/Cedar_Point_Beach.jpg/1280px-Cedar_Point_Beach.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cedar_Point_at_dusk.jpg/1280px-Cedar_Point_at_dusk.jpg"
],
"rides": [
{
"name": "Steel Vengeance",
@@ -92,6 +122,10 @@
"status": "OPERATING",
"manufacturer": "Rocky Mountain Construction",
"description": "A hybrid roller coaster featuring multiple inversions.",
"photos": [
"https://upload.wikimedia.org/wikipedia/commons/thumb/8/8c/Steel_Vengeance_at_Cedar_Point.jpg/1280px-Steel_Vengeance_at_Cedar_Point.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/f/f2/Steel_Vengeance_first_drop.jpg/1280px-Steel_Vengeance_first_drop.jpg"
],
"stats": {
"height_ft": 205,
"length_ft": 5740,
@@ -107,6 +141,10 @@
"status": "OPERATING",
"manufacturer": "Intamin",
"description": "A giga coaster with stunning views of Lake Erie.",
"photos": [
"https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Millennium_Force_at_Cedar_Point.jpg/1280px-Millennium_Force_at_Cedar_Point.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/Millennium_Force_lift_hill.jpg/1280px-Millennium_Force_lift_hill.jpg"
],
"stats": {
"height_ft": 310,
"length_ft": 6595,
@@ -122,6 +160,10 @@
"status": "SBNO",
"manufacturer": "Intamin",
"description": "A strata coaster featuring a 420-foot top hat element.",
"photos": [
"https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/Top_Thrill_Dragster.jpg/1280px-Top_Thrill_Dragster.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/8/8f/Top_Thrill_Dragster_launch.jpg/1280px-Top_Thrill_Dragster_launch.jpg"
],
"stats": {
"height_ft": 420,
"length_ft": 2800,
@@ -137,6 +179,10 @@
"status": "OPERATING",
"manufacturer": "Intamin",
"description": "A launched roller coaster with multiple inversions.",
"photos": [
"https://upload.wikimedia.org/wikipedia/commons/thumb/3/39/Maverick_at_Cedar_Point.jpg/1280px-Maverick_at_Cedar_Point.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/b/b5/Maverick_first_drop.jpg/1280px-Maverick_first_drop.jpg"
],
"stats": {
"height_ft": 105,
"length_ft": 4450,
@@ -157,6 +203,11 @@
"website": "https://www.universalorlando.com/web/en/us/theme-parks/islands-of-adventure",
"owner": "NBCUniversal",
"size_acres": 110,
"photos": [
"https://upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Islands_of_Adventure_entrance.jpg/1280px-Islands_of_Adventure_entrance.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/c/c5/Hogwarts_Castle_at_Universal%27s_Islands_of_Adventure.jpg/1280px-Hogwarts_Castle_at_Universal%27s_Islands_of_Adventure.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/2/2f/Port_of_Entry_at_Islands_of_Adventure.jpg/1280px-Port_of_Entry_at_Islands_of_Adventure.jpg"
],
"rides": [
{
"name": "Jurassic World VelociCoaster",
@@ -165,6 +216,10 @@
"status": "OPERATING",
"manufacturer": "Intamin",
"description": "A high-speed launch coaster featuring velociraptors.",
"photos": [
"https://upload.wikimedia.org/wikipedia/commons/thumb/f/f8/Jurassic_World_VelociCoaster.jpg/1280px-Jurassic_World_VelociCoaster.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/e/e5/VelociCoaster_top_hat.jpg/1280px-VelociCoaster_top_hat.jpg"
],
"stats": {
"height_ft": 155,
"length_ft": 4700,
@@ -180,6 +235,10 @@
"status": "OPERATING",
"manufacturer": "Intamin",
"description": "A story coaster through the Forbidden Forest.",
"photos": [
"https://upload.wikimedia.org/wikipedia/commons/thumb/d/d7/Hagrid%27s_Magical_Creatures_Motorbike_Adventure.jpg/1280px-Hagrid%27s_Magical_Creatures_Motorbike_Adventure.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/c/c8/Hagrid%27s_entrance.jpg/1280px-Hagrid%27s_entrance.jpg"
],
"stats": {
"height_ft": 65,
"length_ft": 5053,
@@ -194,7 +253,11 @@
"opening_date": "1999-05-28",
"status": "OPERATING",
"manufacturer": "Oceaneering International",
"description": "A 3D dark ride featuring Spider-Man."
"description": "A 3D dark ride featuring Spider-Man.",
"photos": [
"https://upload.wikimedia.org/wikipedia/commons/thumb/5/5e/The_Amazing_Adventures_of_Spider-Man.jpg/1280px-The_Amazing_Adventures_of_Spider-Man.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/b/b2/Spider-Man_ride_entrance.jpg/1280px-Spider-Man_ride_entrance.jpg"
]
}
]
},
@@ -208,6 +271,11 @@
"website": "https://www.altontowers.com",
"owner": "Merlin Entertainments",
"size_acres": 910,
"photos": [
"https://upload.wikimedia.org/wikipedia/commons/thumb/5/5c/Alton_Towers_aerial_view.jpg/1280px-Alton_Towers_aerial_view.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/3/37/Alton_Towers_mansion.jpg/1280px-Alton_Towers_mansion.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/f/f8/Alton_Towers_gardens.jpg/1280px-Alton_Towers_gardens.jpg"
],
"rides": [
{
"name": "Nemesis",
@@ -216,6 +284,10 @@
"status": "CLOSED",
"manufacturer": "Bolliger & Mabillard",
"description": "An inverted roller coaster through ravines.",
"photos": [
"https://upload.wikimedia.org/wikipedia/commons/thumb/9/98/Nemesis_at_Alton_Towers.jpg/1280px-Nemesis_at_Alton_Towers.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/2/29/Nemesis_loop.jpg/1280px-Nemesis_loop.jpg"
],
"stats": {
"height_ft": 43,
"length_ft": 2349,
@@ -231,6 +303,10 @@
"status": "OPERATING",
"manufacturer": "Bolliger & Mabillard",
"description": "The world's first vertical drop roller coaster.",
"photos": [
"https://upload.wikimedia.org/wikipedia/commons/thumb/6/67/Oblivion_at_Alton_Towers.jpg/1280px-Oblivion_at_Alton_Towers.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/d/d4/Oblivion_vertical_drop.jpg/1280px-Oblivion_vertical_drop.jpg"
],
"stats": {
"height_ft": 65,
"length_ft": 1804,
@@ -251,6 +327,11 @@
"website": "https://www.europapark.de",
"owner": "Mack Rides",
"size_acres": 235,
"photos": [
"https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Europa-Park_entrance.jpg/1280px-Europa-Park_entrance.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/f/f2/Europa-Park_aerial_view.jpg/1280px-Europa-Park_aerial_view.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Europa-Park_at_night.jpg/1280px-Europa-Park_at_night.jpg"
],
"rides": [
{
"name": "Silver Star",
@@ -259,6 +340,10 @@
"status": "OPERATING",
"manufacturer": "Bolliger & Mabillard",
"description": "A hypercoaster with stunning views.",
"photos": [
"https://upload.wikimedia.org/wikipedia/commons/thumb/2/24/Silver_Star_at_Europa-Park.jpg/1280px-Silver_Star_at_Europa-Park.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/7/75/Silver_Star_first_drop.jpg/1280px-Silver_Star_first_drop.jpg"
],
"stats": {
"height_ft": 239,
"length_ft": 4003,
@@ -274,6 +359,10 @@
"status": "OPERATING",
"manufacturer": "Mack Rides",
"description": "A launched roller coaster with multiple inversions.",
"photos": [
"https://upload.wikimedia.org/wikipedia/commons/thumb/1/1c/Blue_Fire_at_Europa-Park.jpg/1280px-Blue_Fire_at_Europa-Park.jpg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Blue_Fire_launch.jpg/1280px-Blue_Fire_launch.jpg"
],
"stats": {
"height_ft": 125,
"length_ft": 3465,
@@ -285,4 +374,4 @@
]
}
]
}
}

View File

@@ -13,6 +13,7 @@ from faker import Faker
import requests
from io import BytesIO
from PIL import Image
from cities_light.models import City, Country
from parks.models import Park
from rides.models import Ride, RollerCoasterStats
@@ -30,25 +31,30 @@ class Command(BaseCommand):
parser.add_argument('--users', type=int, default=50)
parser.add_argument('--reviews-per-item', type=int, default=10)
def download_and_save_image(self, url, prefix):
def download_and_save_image(self, url):
try:
response = requests.get(url)
img = Image.open(BytesIO(response.content))
img_io = BytesIO()
img.save(img_io, format='JPEG')
img_io.seek(0)
return f'{prefix}_{fake.uuid4()}.jpg', File(img_io)
except:
filename = url.split('/')[-1]
return filename, File(img_io)
except Exception as e:
self.stdout.write(self.style.WARNING(f'Failed to download image {url}: {str(e)}'))
return None, None
def create_users(self, count):
self.stdout.write('Creating users...')
users = []
# Get existing admin user
admin_user = User.objects.get(username='admin')
users.append(admin_user)
self.stdout.write('Added existing admin user')
try:
# Get existing admin user
admin_user = User.objects.get(username='admin')
users.append(admin_user)
self.stdout.write('Added existing admin user')
except User.DoesNotExist:
self.stdout.write(self.style.WARNING('Admin user not found, skipping...'))
# Create regular users using raw SQL
roles = ['USER'] * 20 + ['MODERATOR'] * 3 + ['ADMIN'] * 2
@@ -232,72 +238,87 @@ class Command(BaseCommand):
parks = []
for park_data in seed_data['parks']:
# Create park with company instance
park = Park.objects.create(
name=park_data['name'],
slug=slugify(park_data['name']),
location=park_data['location'],
country=park_data['country'],
opening_date=datetime.strptime(park_data['opening_date'], '%Y-%m-%d').date(),
status=park_data['status'],
description=park_data['description'],
website=park_data['website'],
owner=companies[park_data['owner']],
size_acres=park_data['size_acres']
)
# Add park photos
for _ in range(random.randint(2, 5)):
img_url = f'https://picsum.photos/800/600?random={fake.random_number(5)}'
filename, file = self.download_and_save_image(img_url, 'park')
if filename and file:
Photo.objects.create(
content_object=park,
image=file,
uploaded_by=random.choice(users),
caption=fake.sentence(),
is_approved=True
)
# Create rides for this park
for ride_data in park_data['rides']:
ride = Ride.objects.create(
name=ride_data['name'],
slug=slugify(ride_data['name']),
category=ride_data['category'],
park=park,
status=ride_data['status'],
opening_date=datetime.strptime(ride_data['opening_date'], '%Y-%m-%d').date(),
manufacturer=manufacturers[ride_data['manufacturer']],
description=ride_data['description']
try:
# Get country from cities_light
country = Country.objects.get(code2=park_data['country'])
# Try to find city, but don't require it
city = None
try:
city_name = park_data['location'].split(',')[0].strip()
city = City.objects.filter(name__iexact=city_name, country=country).first()
except:
self.stdout.write(self.style.WARNING(f'City not found for {park_data["name"]}, using location text'))
# Create park
park = Park.objects.create(
name=park_data['name'],
slug=slugify(park_data['name']),
location=park_data['location'],
country=country,
city=city,
opening_date=datetime.strptime(park_data['opening_date'], '%Y-%m-%d').date(),
status=park_data['status'],
description=park_data['description'],
website=park_data['website'],
owner=companies[park_data['owner']],
size_acres=park_data['size_acres']
)
# Add roller coaster stats if applicable
if ride_data['category'] == 'RC' and 'stats' in ride_data:
RollerCoasterStats.objects.create(
ride=ride,
height_ft=ride_data['stats']['height_ft'],
length_ft=ride_data['stats']['length_ft'],
speed_mph=ride_data['stats']['speed_mph'],
inversions=ride_data['stats']['inversions'],
ride_time_seconds=ride_data['stats']['ride_time_seconds']
)
# Add ride photos
for _ in range(random.randint(2, 5)):
img_url = f'https://picsum.photos/800/600?random={fake.random_number(5)}'
filename, file = self.download_and_save_image(img_url, 'ride')
# Add park photos
for photo_url in park_data.get('photos', []):
filename, file = self.download_and_save_image(photo_url)
if filename and file:
Photo.objects.create(
content_object=ride,
content_object=park,
image=file,
uploaded_by=random.choice(users),
caption=fake.sentence(),
caption=f"Photo of {park.name}",
is_approved=True
)
# Create rides for this park
for ride_data in park_data['rides']:
ride = Ride.objects.create(
name=ride_data['name'],
slug=slugify(ride_data['name']),
category=ride_data['category'],
park=park,
status=ride_data['status'],
opening_date=datetime.strptime(ride_data['opening_date'], '%Y-%m-%d').date(),
manufacturer=manufacturers[ride_data['manufacturer']],
description=ride_data['description']
)
# Add roller coaster stats if applicable
if ride_data['category'] == 'RC' and 'stats' in ride_data:
RollerCoasterStats.objects.create(
ride=ride,
height_ft=ride_data['stats']['height_ft'],
length_ft=ride_data['stats']['length_ft'],
speed_mph=ride_data['stats']['speed_mph'],
inversions=ride_data['stats']['inversions'],
ride_time_seconds=ride_data['stats']['ride_time_seconds']
)
# Add ride photos
for photo_url in ride_data.get('photos', []):
filename, file = self.download_and_save_image(photo_url)
if filename and file:
Photo.objects.create(
content_object=ride,
image=file,
uploaded_by=random.choice(users),
caption=f"Photo of {ride.name}",
is_approved=True
)
parks.append(park)
self.stdout.write(f'Created park and rides: {park.name}')
parks.append(park)
self.stdout.write(f'Created park and rides: {park.name}')
except Exception as e:
self.stdout.write(self.style.ERROR(f'Failed to create park {park_data["name"]}: {str(e)}'))
continue
return parks

View File

@@ -0,0 +1,17 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cities_light', '0001_initial'),
('parks', '0006_update_location_fields_to_cities_light'),
]
operations = [
migrations.AlterField(
model_name='historicalpark',
name='city',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='cities_light.city'),
),
]

View File

@@ -0,0 +1,43 @@
from django.db import migrations
from django.db.models import Q
def fix_historical_park_data(apps, schema_editor):
HistoricalPark = apps.get_model('parks', 'HistoricalPark')
Park = apps.get_model('parks', 'Park')
Country = apps.get_model('cities_light', 'Country')
# Get a default country (create one if none exists)
default_country = Country.objects.first()
if not default_country:
default_country = Country.objects.create(name='Unknown')
# Fix all historical records with null country
historical_records = HistoricalPark.objects.filter(
Q(country__isnull=True) | Q(location__isnull=True)
)
for record in historical_records:
try:
# Try to get the current park's country
park = Park.objects.get(id=record.id)
record.country = park.country
record.location = park.location or f"{park.country.name}"
except Park.DoesNotExist:
# If park doesn't exist, use default country
record.country = default_country
record.location = default_country.name
record.save()
def reverse_func(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('parks', '0007_fix_historical_park_city_null'),
('cities_light', '0001_initial'),
]
operations = [
migrations.RunPython(fix_historical_park_data, reverse_func),
]

View File

@@ -0,0 +1,33 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cities_light', '0001_initial'),
('parks', '0008_fix_historical_park_data'),
]
operations = [
migrations.AlterField(
model_name='historicalpark',
name='city',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='cities_light.city'),
),
migrations.AlterField(
model_name='historicalpark',
name='region',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='cities_light.region'),
),
migrations.AlterField(
model_name='historicalpark',
name='location',
field=models.CharField(max_length=255, default=''),
preserve_default=False,
),
migrations.AlterField(
model_name='historicalpark',
name='country',
field=models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='cities_light.country'),
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 5.1.2 on 2024-10-31 20:15
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("cities_light", "0011_alter_city_country_alter_city_region_and_more"),
("parks", "0009_fix_historical_park_fields"),
]
operations = [
migrations.AlterField(
model_name="historicalpark",
name="country",
field=models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
to="cities_light.country",
),
),
migrations.AlterField(
model_name="park",
name="country",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to="cities_light.country"
),
),
]

View File

@@ -0,0 +1,72 @@
from django.db import migrations, models
from django.db.models import Q
import django.db.models.deletion
def fix_historical_records(apps, schema_editor):
HistoricalPark = apps.get_model('parks', 'HistoricalPark')
Park = apps.get_model('parks', 'Park')
Country = apps.get_model('cities_light', 'Country')
# Get or create a default country
default_country = Country.objects.first()
if not default_country:
default_country = Country.objects.create(name='Unknown')
# Update all historical records with null values
for record in HistoricalPark.objects.filter(Q(country__isnull=True) | Q(location__isnull=True)):
try:
park = Park.objects.get(id=record.id)
record.country = park.country
record.location = park.location
except Park.DoesNotExist:
record.country = default_country
record.location = default_country.name
record.save()
class Migration(migrations.Migration):
atomic = False # Allow long-running operations
dependencies = [
('parks', '0009_fix_historical_park_fields'),
]
operations = [
# First, make sure all fields allow null temporarily
migrations.AlterField(
model_name='historicalpark',
name='country',
field=models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name='+',
to='cities_light.country'
),
),
migrations.AlterField(
model_name='historicalpark',
name='location',
field=models.CharField(max_length=255, null=True, blank=True),
),
# Fix the data
migrations.RunPython(fix_historical_records),
# Now make the fields non-nullable
migrations.AlterField(
model_name='historicalpark',
name='country',
field=models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name='+',
to='cities_light.country'
),
),
migrations.AlterField(
model_name='historicalpark',
name='location',
field=models.CharField(max_length=255),
),
]

View File

@@ -0,0 +1,52 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cities_light', '0001_initial'),
('parks', '0010_fix_historical_records'),
]
operations = [
migrations.AlterField(
model_name='historicalpark',
name='city',
field=models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name='+',
to='cities_light.city'
),
),
migrations.AlterField(
model_name='historicalpark',
name='region',
field=models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name='+',
to='cities_light.region'
),
),
migrations.AlterField(
model_name='historicalpark',
name='country',
field=models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name='+',
to='cities_light.country'
),
),
migrations.AlterField(
model_name='historicalpark',
name='location',
field=models.CharField(max_length=255, blank=True, null=True),
),
]

View File

@@ -0,0 +1,13 @@
# Generated by Django 5.1.2 on 2024-10-31 20:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("parks", "0010_alter_historicalpark_country_alter_park_country"),
("parks", "0010_fix_historical_records"),
]
operations = []

View File

@@ -0,0 +1,13 @@
# Generated by Django 5.1.2 on 2024-10-31 20:35
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("parks", "0011_alter_historicalpark_fields"),
("parks", "0011_merge_20241031_1617"),
]
operations = []

View File

@@ -0,0 +1,67 @@
from django.db import migrations
def fix_null_locations(apps, schema_editor):
Park = apps.get_model('parks', 'Park')
Country = apps.get_model('cities_light', 'Country')
Region = apps.get_model('cities_light', 'Region')
City = apps.get_model('cities_light', 'City')
# Get or create default locations
default_country = Country.objects.first()
if not default_country:
default_country = Country.objects.create(
name='Unknown',
name_ascii='Unknown',
slug='unknown',
geoname_id=0,
alternate_names='',
search_names='Unknown'
)
default_region = Region.objects.filter(country=default_country).first()
if not default_region:
default_region = Region.objects.create(
name='Unknown',
name_ascii='Unknown',
slug='unknown',
geoname_id=0,
alternate_names='',
country=default_country,
display_name='Unknown',
search_names='Unknown'
)
default_city = City.objects.filter(region=default_region).first()
if not default_city:
default_city = City.objects.create(
name='Unknown',
name_ascii='Unknown',
slug='unknown',
geoname_id=0,
alternate_names='',
region=default_region,
country=default_country,
display_name='Unknown',
search_names='Unknown',
latitude=0,
longitude=0,
population=0
)
# Update parks with null locations
for park in Park.objects.filter(country__isnull=True):
park.country = default_country
park.region = default_region
park.city = default_city
park.location = 'Unknown, Unknown, Unknown'
park.save()
class Migration(migrations.Migration):
dependencies = [
('parks', '0012_merge_20241031_1635'),
('cities_light', '0001_initial'),
]
operations = [
migrations.RunPython(fix_null_locations, reverse_code=migrations.RunPython.noop),
]

View File

@@ -0,0 +1,52 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cities_light', '0001_initial'),
('parks', '0013_fix_null_locations'),
]
operations = [
migrations.AlterField(
model_name='historicalpark',
name='city',
field=models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name='+',
to='cities_light.city'
),
),
migrations.AlterField(
model_name='historicalpark',
name='region',
field=models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name='+',
to='cities_light.region'
),
),
migrations.AlterField(
model_name='historicalpark',
name='country',
field=models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name='+',
to='cities_light.country'
),
),
migrations.AlterField(
model_name='historicalpark',
name='location',
field=models.CharField(max_length=255, blank=True, null=True),
),
]

View File

@@ -0,0 +1,16 @@
from django.db import migrations
def fix_historical_records(apps, schema_editor):
HistoricalPark = apps.get_model('parks', 'HistoricalPark')
# Update any historical records that might have issues
HistoricalPark.objects.filter(city__isnull=True).update(city=None)
class Migration(migrations.Migration):
dependencies = [
('parks', '0014_alter_location_fields'),
]
operations = [
migrations.RunPython(fix_historical_records, migrations.RunPython.noop),
]

View File

@@ -0,0 +1,22 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cities_light', '0011_alter_city_country_alter_city_region_and_more'),
('parks', '0015_fix_historical_park_city_constraint'),
]
operations = [
migrations.AlterField(
model_name='historicalpark',
name='city',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='cities_light.city'),
),
migrations.AlterField(
model_name='historicalpark',
name='region',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='cities_light.region'),
),
]

View File

@@ -0,0 +1,22 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('parks', '0016_alter_historicalpark_city_nullable'),
]
operations = [
migrations.RunSQL(
# Make the city column nullable
sql='ALTER TABLE parks_historicalpark ALTER COLUMN city DROP NOT NULL;',
# Reverse operation if needed
reverse_sql='ALTER TABLE parks_historicalpark ALTER COLUMN city SET NOT NULL;'
),
migrations.RunSQL(
# Make the city_id column nullable
sql='ALTER TABLE parks_historicalpark ALTER COLUMN city_id DROP NOT NULL;',
# Reverse operation if needed
reverse_sql='ALTER TABLE parks_historicalpark ALTER COLUMN city_id SET NOT NULL;'
),
]

View File

@@ -0,0 +1,48 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cities_light', '0011_alter_city_country_alter_city_region_and_more'),
('parks', '0017_fix_historicalpark_city_column'),
]
operations = [
migrations.AlterField(
model_name='historicalpark',
name='country',
field=models.ForeignKey(
blank=False,
db_constraint=False,
null=False,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name='+',
to='cities_light.country'
),
),
migrations.AlterField(
model_name='historicalpark',
name='region',
field=models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name='+',
to='cities_light.region'
),
),
migrations.AlterField(
model_name='historicalpark',
name='city',
field=models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name='+',
to='cities_light.city'
),
),
]

View File

@@ -0,0 +1,15 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('parks', '0018_fix_historicalpark_location_fields'),
]
operations = [
migrations.RunSQL(
# Make region_id nullable
sql='ALTER TABLE parks_historicalpark ALTER COLUMN region_id DROP NOT NULL;',
reverse_sql='ALTER TABLE parks_historicalpark ALTER COLUMN region_id SET NOT NULL;'
),
]

View File

@@ -0,0 +1,16 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('parks', '0019_fix_historicalpark_region_constraint'),
]
operations = [
migrations.RunSQL(
# Remove the redundant city text column
sql='ALTER TABLE parks_historicalpark DROP COLUMN IF EXISTS city;',
# Recreate the column if needed (reverse migration)
reverse_sql='ALTER TABLE parks_historicalpark ADD COLUMN city character varying(255);'
),
]

View File

@@ -11,7 +11,7 @@ class Park(models.Model):
('CLOSED_PERM', 'Permanently Closed'),
('UNDER_CONSTRUCTION', 'Under Construction'),
('DEMOLISHED', 'Demolished'),
('RELOCATED', 'Relocated'), # Added to match Ride model
('RELOCATED', 'Relocated'),
]
name = models.CharField(max_length=255)
@@ -84,6 +84,9 @@ class Park(models.Model):
def get_formatted_location(self):
"""Get a formatted location string: $COUNTRY, $REGION, $CITY"""
if not self.country:
return ""
location = self.country.name
if self.region and self.city:

View File

@@ -11,6 +11,7 @@ urlpatterns = [
path('ajax/countries/', views.get_countries, name='get_countries'),
path('ajax/regions/', views.get_regions, name='get_regions'),
path('ajax/cities/', views.get_cities, name='get_cities'),
path('<slug:slug>/edit/', views.ParkUpdateView.as_view(), name='park_edit'),
path('<slug:slug>/', views.ParkDetailView.as_view(), name='park_detail'),
path('<slug:park_slug>/rides/', include('rides.urls', namespace='rides')),
]

View File

@@ -1,48 +1,76 @@
from django.views.generic import DetailView, ListView, CreateView
from django.views.generic import DetailView, ListView, CreateView, UpdateView
from django.shortcuts import get_object_or_404, render
from django.core.serializers.json import DjangoJSONEncoder
from django.urls import reverse
from django.db.models import Q
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType
from django.contrib import messages
from django.http import JsonResponse, HttpResponseRedirect, HttpResponse
from .models import Park, ParkArea
from .forms import ParkForm
from rides.models import Ride
from core.views import SlugRedirectMixin
from moderation.mixins import EditSubmissionMixin, PhotoSubmissionMixin, InlineEditMixin, HistoryMixin
from moderation.mixins import EditSubmissionMixin, PhotoSubmissionMixin, HistoryMixin
from moderation.models import EditSubmission
from cities_light.models import Country, Region, City
def get_countries(request):
query = request.GET.get('q', '')
countries = Country.objects.filter(name__icontains=query).values_list('name', flat=True)[:10]
filter_parks = request.GET.get('filter_parks', 'false') == 'true'
# Base query
countries = Country.objects.filter(name__icontains=query)
# Only filter by parks if explicitly requested
if filter_parks:
countries = countries.filter(park__isnull=False)
countries = countries.distinct().values('id', 'name')[:10]
return JsonResponse(list(countries), safe=False)
def get_regions(request):
query = request.GET.get('q', '')
country = request.GET.get('country', '')
filter_parks = request.GET.get('filter_parks', 'false') == 'true'
if not country:
return JsonResponse([], safe=False)
# Base query
regions = Region.objects.filter(
Q(name__icontains=query) | Q(alternate_names__icontains=query),
country__name__iexact=country
).values_list('name', flat=True)[:10]
)
# Only filter by parks if explicitly requested
if filter_parks:
regions = regions.filter(park__isnull=False)
regions = regions.distinct().values('id', 'name')[:10]
return JsonResponse(list(regions), safe=False)
def get_cities(request):
query = request.GET.get('q', '')
region = request.GET.get('region', '')
country = request.GET.get('country', '')
filter_parks = request.GET.get('filter_parks', 'false') == 'true'
if not region or not country:
return JsonResponse([], safe=False)
# Base query
cities = City.objects.filter(
Q(name__icontains=query) | Q(alternate_names__icontains=query),
region__name__iexact=region,
region__country__name__iexact=country
).values_list('name', flat=True)[:10]
)
# Only filter by parks if explicitly requested
if filter_parks:
cities = cities.filter(park__isnull=False)
cities = cities.distinct().values('id', 'name')[:10]
return JsonResponse(list(cities), safe=False)
class ParkCreateView(LoginRequiredMixin, CreateView):
@@ -50,38 +78,111 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
form_class = ParkForm
template_name = 'parks/park_form.html'
def form_valid(self, form):
# If user is moderator or above, save directly
if self.request.user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']:
self.object = form.save()
return HttpResponseRedirect(self.get_success_url())
# Otherwise, create a submission
cleaned_data = form.cleaned_data.copy()
def prepare_changes_data(self, cleaned_data):
data = cleaned_data.copy()
# Convert model instances to IDs for JSON serialization
if cleaned_data.get('owner'):
cleaned_data['owner'] = cleaned_data['owner'].id
if cleaned_data.get('country'):
cleaned_data['country'] = cleaned_data['country'].id
if cleaned_data.get('region'):
cleaned_data['region'] = cleaned_data['region'].id
if cleaned_data.get('city'):
cleaned_data['city'] = cleaned_data['city'].id
if data.get('owner'):
data['owner'] = data['owner'].id
if data.get('country'):
data['country'] = data['country'].id
if data.get('region'):
data['region'] = data['region'].id
if data.get('city'):
data['city'] = data['city'].id
# Convert dates to ISO format strings
if data.get('opening_date'):
data['opening_date'] = data['opening_date'].isoformat()
if data.get('closing_date'):
data['closing_date'] = data['closing_date'].isoformat()
return data
def form_valid(self, form):
changes = self.prepare_changes_data(form.cleaned_data)
# Create submission record
submission = EditSubmission.objects.create(
user=self.request.user,
content_type=ContentType.objects.get_for_model(Park),
submission_type='CREATE',
changes=cleaned_data,
changes=changes,
reason=self.request.POST.get('reason', ''),
source=self.request.POST.get('source', '')
)
# If user is moderator or above, auto-approve
if self.request.user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']:
self.object = form.save()
submission.object_id = self.object.id
submission.status = 'APPROVED'
submission.handled_by = self.request.user
submission.save()
messages.success(self.request, f'Successfully created {self.object.name}')
return HttpResponseRedirect(self.get_success_url())
messages.success(self.request, 'Your park submission has been sent for review')
return HttpResponseRedirect(reverse('parks:park_list'))
def get_success_url(self):
return reverse('parks:park_detail', kwargs={'slug': self.object.slug})
class ParkDetailView(SlugRedirectMixin, EditSubmissionMixin, PhotoSubmissionMixin, InlineEditMixin, HistoryMixin, DetailView):
class ParkUpdateView(LoginRequiredMixin, UpdateView):
model = Park
form_class = ParkForm
template_name = 'parks/park_form.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['is_edit'] = True
return context
def prepare_changes_data(self, cleaned_data):
data = cleaned_data.copy()
# Convert model instances to IDs for JSON serialization
if data.get('owner'):
data['owner'] = data['owner'].id
if data.get('country'):
data['country'] = data['country'].id
if data.get('region'):
data['region'] = data['region'].id
if data.get('city'):
data['city'] = data['city'].id
# Convert dates to ISO format strings
if data.get('opening_date'):
data['opening_date'] = data['opening_date'].isoformat()
if data.get('closing_date'):
data['closing_date'] = data['closing_date'].isoformat()
return data
def form_valid(self, form):
changes = self.prepare_changes_data(form.cleaned_data)
# Create submission record
submission = EditSubmission.objects.create(
user=self.request.user,
content_type=ContentType.objects.get_for_model(Park),
object_id=self.object.id,
submission_type='EDIT',
changes=changes,
reason=self.request.POST.get('reason', ''),
source=self.request.POST.get('source', '')
)
# If user is moderator or above, auto-approve
if self.request.user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']:
self.object = form.save()
submission.status = 'APPROVED'
submission.handled_by = self.request.user
submission.save()
messages.success(self.request, f'Successfully updated {self.object.name}')
return HttpResponseRedirect(self.get_success_url())
messages.success(self.request, f'Your changes to {self.object.name} have been sent for review')
return HttpResponseRedirect(reverse('parks:park_detail', kwargs={'slug': self.object.slug}))
def get_success_url(self):
return reverse('parks:park_detail', kwargs={'slug': self.object.slug})
class ParkDetailView(SlugRedirectMixin, EditSubmissionMixin, PhotoSubmissionMixin, HistoryMixin, DetailView):
model = Park
template_name = 'parks/park_detail.html'
context_object_name = 'park'
@@ -104,7 +205,7 @@ class ParkDetailView(SlugRedirectMixin, EditSubmissionMixin, PhotoSubmissionMixi
def get_redirect_url_pattern(self):
return 'parks:park_detail'
class ParkAreaDetailView(SlugRedirectMixin, EditSubmissionMixin, PhotoSubmissionMixin, InlineEditMixin, HistoryMixin, DetailView):
class ParkAreaDetailView(SlugRedirectMixin, EditSubmissionMixin, PhotoSubmissionMixin, HistoryMixin, DetailView):
model = ParkArea
template_name = 'parks/area_detail.html'
context_object_name = 'area'
@@ -149,7 +250,7 @@ class ParkListView(ListView):
country = self.request.GET.get('country', '').strip()
region = self.request.GET.get('region', '').strip()
city = self.request.GET.get('city', '').strip()
status = self.request.GET.get('status', '').strip()
statuses = self.request.GET.getlist('status')
if search:
queryset = queryset.filter(
@@ -166,8 +267,8 @@ class ParkListView(ListView):
if city:
queryset = queryset.filter(city__name__icontains=city)
if status:
queryset = queryset.filter(status=status)
if statuses:
queryset = queryset.filter(status__in=statuses)
return queryset
@@ -178,7 +279,7 @@ class ParkListView(ListView):
'country': self.request.GET.get('country', ''),
'region': self.request.GET.get('region', ''),
'city': self.request.GET.get('city', ''),
'status': self.request.GET.get('status', '')
'statuses': self.request.GET.getlist('status')
}
return context