okay fine

This commit is contained in:
pacnpal
2024-11-03 17:47:26 +00:00
parent 01c6004a79
commit 27eb239e97
10020 changed files with 1935769 additions and 2364 deletions

View File

@@ -4,15 +4,20 @@ from simple_history.admin import SimpleHistoryAdmin
from .models import Park, ParkArea
class ParkAdmin(SimpleHistoryAdmin):
list_display = ('name', 'location', 'status', 'owner', 'created_at', 'updated_at')
list_filter = ('status', 'country', 'region', 'city')
search_fields = ('name', 'location', 'description')
list_display = ('name', 'formatted_location', 'status', 'owner', 'created_at', 'updated_at')
list_filter = ('status',)
search_fields = ('name', 'description', 'location__name', 'location__city', 'location__country')
readonly_fields = ('created_at', 'updated_at')
prepopulated_fields = {'slug': ('name',)}
def formatted_location(self, obj):
"""Display formatted location string"""
return obj.formatted_location
formatted_location.short_description = 'Location'
def get_history_list_display(self, request):
"""Customize the list display for history records"""
return ('name', 'location', 'status', 'history_date', 'history_user')
return ('name', 'formatted_location', 'status', 'history_date', 'history_user')
class ParkAreaAdmin(SimpleHistoryAdmin):
list_display = ('name', 'park', 'created_at', 'updated_at')
@@ -34,11 +39,16 @@ 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')
list_display = ('name', 'formatted_location', 'status', 'history_date', 'history_user', 'history_type')
list_filter = ('status', 'history_type')
search_fields = ('name', 'description')
readonly_fields = ('history_date', 'history_type', 'history_id')
def formatted_location(self, obj):
"""Display formatted location string"""
return obj.instance.formatted_location
formatted_location.short_description = 'Location'
def has_add_permission(self, request):
return False # Prevent adding new historical records directly

View File

@@ -1,191 +1,148 @@
from django import forms
from django.urls import reverse_lazy
from decimal import Decimal, InvalidOperation, ROUND_DOWN
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 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': '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': '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': 'Select or type a city',
'data-autocomplete': 'true'
})
)
"""Form for creating and updating Park objects with location support"""
class Meta:
model = Park
fields = ['name', 'description', 'owner', 'status', 'opening_date', 'closing_date',
'operating_season', 'size_acres', 'website', 'country', 'region', 'city']
fields = [
"name",
"description",
"owner",
"status",
"opening_date",
"closing_date",
"operating_season",
"size_acres",
"website",
"latitude",
"longitude",
"street_address",
"city",
"state",
"country",
"postal_code",
]
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={
'class': 'w-full border-gray-300 rounded-lg form-textarea dark:border-gray-600 dark:bg-gray-700 dark:text-white',
'rows': 2
}),
'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'
}),
'status': forms.Select(attrs={
'class': 'w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white'
}),
'opening_date': forms.DateInput(attrs={
'type': 'date',
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white'
}),
'closing_date': forms.DateInput(attrs={
'type': 'date',
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white'
}),
'operating_season': 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': 'e.g., Year-round, Summer only, etc.'
}),
'size_acres': forms.NumberInput(attrs={
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white',
'step': '0.01',
'min': '0'
}),
'website': forms.URLInput(attrs={
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white',
'placeholder': 'https://example.com'
}),
"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={
"class": "w-full border-gray-300 rounded-lg form-textarea dark:border-gray-600 dark:bg-gray-700 dark:text-white",
"rows": 2,
}
),
"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"
}
),
"status": forms.Select(
attrs={
"class": "w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white"
}
),
"opening_date": forms.DateInput(
attrs={
"type": "date",
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white",
}
),
"closing_date": forms.DateInput(
attrs={
"type": "date",
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white",
}
),
"operating_season": 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": "e.g., Year-round, Summer only, etc.",
}
),
"size_acres": forms.NumberInput(
attrs={
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white",
"step": "0.01",
"min": "0",
}
),
"website": forms.URLInput(
attrs={
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white",
"placeholder": "https://example.com",
}
),
# Location fields
"latitude": forms.HiddenInput(),
"longitude": forms.HiddenInput(),
"street_address": forms.TextInput(
attrs={
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
}
),
"city": forms.TextInput(
attrs={
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
}
),
"state": forms.TextInput(
attrs={
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
}
),
"country": forms.TextInput(
attrs={
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
}
),
"postal_code": forms.TextInput(
attrs={
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
}
),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
instance = kwargs.get('instance')
if instance:
def clean_latitude(self):
latitude = self.cleaned_data.get('latitude')
if latitude is not None:
try:
if instance.country:
self.fields['country_name'].initial = instance.country.name
self.fields['country'].initial = instance.country
except Country.DoesNotExist:
pass
# Convert to Decimal for precise handling
latitude = Decimal(str(latitude))
# Round to exactly 6 decimal places
latitude = latitude.quantize(Decimal('0.000001'), rounding=ROUND_DOWN)
# Validate range
if latitude < -90 or latitude > 90:
raise forms.ValidationError("Latitude must be between -90 and 90 degrees.")
# Convert to string to preserve exact decimal places
return str(latitude)
except (InvalidOperation, TypeError):
raise forms.ValidationError("Invalid latitude value.")
return latitude
def clean_longitude(self):
longitude = self.cleaned_data.get('longitude')
if longitude is not None:
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()
country_name = cleaned_data.get('country_name')
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:
cleaned_data['country'] = default_country
else:
raise forms.ValidationError("Country is required")
if region_name and cleaned_data.get('country'):
try:
region = Region.objects.get(
name__iexact=region_name,
country=cleaned_data['country']
)
cleaned_data['region'] = region
except Region.DoesNotExist:
cleaned_data['region'] = None
if city_name and cleaned_data.get('region'):
try:
city = City.objects.get(
name__iexact=city_name,
region=cleaned_data['region']
)
cleaned_data['city'] = city
except City.DoesNotExist:
cleaned_data['city'] = None
return cleaned_data
# Convert to Decimal for precise handling
longitude = Decimal(str(longitude))
# Round to exactly 6 decimal places
longitude = longitude.quantize(Decimal('0.000001'), rounding=ROUND_DOWN)
# Validate range
if longitude < -180 or longitude > 180:
raise forms.ValidationError("Longitude must be between -180 and 180 degrees.")
# Convert to string to preserve exact decimal places
return str(longitude)
except (InvalidOperation, TypeError):
raise forms.ValidationError("Invalid longitude value.")
return longitude

55
parks/location_utils.py Normal file
View File

@@ -0,0 +1,55 @@
from decimal import Decimal, ROUND_DOWN, InvalidOperation
def normalize_coordinate(value, max_digits, decimal_places):
"""Normalize coordinate to have exactly 6 decimal places"""
try:
if value is None:
return None
# Convert to Decimal for precise handling
value = Decimal(str(value))
# Round to exactly 6 decimal places
value = value.quantize(Decimal('0.000001'), rounding=ROUND_DOWN)
return float(value)
except (TypeError, ValueError, InvalidOperation):
return None
def get_english_name(tags):
"""Extract English name from OSM tags, falling back to default name"""
# Try name:en first
if 'name:en' in tags:
return tags['name:en']
# Then try int_name (international name)
if 'int_name' in tags:
return tags['int_name']
# Fall back to default name
return tags.get('name')
def normalize_osm_result(result):
"""Normalize OpenStreetMap result to use English names and normalized coordinates"""
# Normalize coordinates
result['lat'] = normalize_coordinate(float(result['lat']), 9, 6)
result['lon'] = normalize_coordinate(float(result['lon']), 10, 6)
# Get address details
address = result.get('address', {})
# Normalize place names to English where possible
if 'namedetails' in result:
# For main display name
result['display_name'] = get_english_name(result['namedetails'])
# For address components
if 'city' in address and 'city_tags' in result:
address['city'] = get_english_name(result['city_tags'])
if 'state' in address and 'state_tags' in result:
address['state'] = get_english_name(result['state_tags'])
if 'country' in address and 'country_tags' in result:
address['country'] = get_english_name(result['country_tags'])
result['address'] = address
return result

View File

@@ -1,99 +0,0 @@
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

@@ -1,90 +0,0 @@
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

@@ -0,0 +1,28 @@
from django.core.management.base import BaseCommand
from django.db import connection
class Command(BaseCommand):
help = 'Fix migration history'
def handle(self, *args, **options):
with connection.cursor() as cursor:
# Drop existing historical tables
cursor.execute("""
DROP TABLE IF EXISTS parks_historicalpark CASCADE;
DROP TABLE IF EXISTS parks_historicalparkarea CASCADE;
""")
# Delete all existing parks migrations
cursor.execute("""
DELETE FROM django_migrations
WHERE app = 'parks';
""")
# Insert the new initial migration
cursor.execute("""
INSERT INTO django_migrations (app, name, applied)
VALUES ('parks', '0001_initial', NOW());
""")
self.stdout.write(self.style.SUCCESS('Successfully fixed migration history'))

View File

@@ -1,5 +1,6 @@
import json
import os
import shutil
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
@@ -11,128 +12,180 @@ from rides.models import Ride, RollerCoasterStats
from companies.models import Company, Manufacturer
from reviews.models import Review
from media.models import Photo
from cities_light.models import Country, Region, City
from django.contrib.auth.models import Permission
import random
from datetime import datetime, timedelta
User = get_user_model()
# Park coordinates mapping
PARK_COORDINATES = {
"Walt Disney World Magic Kingdom": {
"latitude": "28.418778",
"longitude": "-81.581212",
"street_address": "1180 Seven Seas Dr",
"city": "Orlando",
"state": "Florida",
"postal_code": "32836"
},
"Cedar Point": {
"latitude": "41.482207",
"longitude": "-82.683523",
"street_address": "1 Cedar Point Dr",
"city": "Sandusky",
"state": "Ohio",
"postal_code": "44870"
},
"Universal's Islands of Adventure": {
"latitude": "28.470891",
"longitude": "-81.471756",
"street_address": "6000 Universal Blvd",
"city": "Orlando",
"state": "Florida",
"postal_code": "32819"
},
"Alton Towers": {
"latitude": "52.988889",
"longitude": "-1.892778",
"street_address": "Farley Ln",
"city": "Alton",
"state": "Staffordshire",
"postal_code": "ST10 4DB"
},
"Europa-Park": {
"latitude": "48.266031",
"longitude": "7.722044",
"street_address": "Europa-Park-Straße 2",
"city": "Rust",
"state": "Baden-Württemberg",
"postal_code": "77977"
}
}
class Command(BaseCommand):
help = 'Seeds the database with initial data'
help = "Seeds the database with initial data"
def handle(self, *args, **kwargs):
self.stdout.write('Starting database seed...')
self.stdout.write("Starting database seed...")
# Clean up media directory
self.stdout.write("Cleaning up media directory...")
media_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'media')
if os.path.exists(media_dir):
for item in os.listdir(media_dir):
if item != '__init__.py': # Preserve __init__.py
item_path = os.path.join(media_dir, item)
if os.path.isdir(item_path):
shutil.rmtree(item_path)
else:
os.remove(item_path)
# Delete all existing data
self.stdout.write("Deleting existing data...")
User.objects.exclude(username='admin').delete() # Delete all users except admin
Park.objects.all().delete()
Ride.objects.all().delete()
Company.objects.all().delete()
Manufacturer.objects.all().delete()
Review.objects.all().delete()
Photo.objects.all().delete()
# Create users and set permissions
self.create_users()
self.setup_permissions()
# Create parks and rides
self.stdout.write('Creating parks and rides from seed data...')
self.stdout.write("Creating parks and rides from seed data...")
self.create_companies()
self.create_manufacturers()
self.create_parks_and_rides()
# Create reviews
self.stdout.write('Creating reviews...')
self.stdout.write("Creating reviews...")
self.create_reviews()
# Create top lists
self.stdout.write('Creating top lists...')
self.create_top_lists()
self.stdout.write('Successfully seeded database')
self.stdout.write("Successfully seeded database")
def setup_permissions(self):
"""Set up photo permissions for all users"""
self.stdout.write('Setting up photo permissions...')
self.stdout.write("Setting up photo permissions...")
# Get photo permissions
photo_content_type = ContentType.objects.get_for_model(Photo)
photo_permissions = Permission.objects.filter(content_type=photo_content_type)
# Update all users
users = User.objects.all()
for user in users:
for perm in photo_permissions:
user.user_permissions.add(perm)
user.save()
self.stdout.write(f'Updated permissions for user: {user.username}')
self.stdout.write(f"Updated permissions for user: {user.username}")
def create_users(self):
self.stdout.write('Creating users...')
self.stdout.write("Creating users...")
# Try to get admin user
try:
admin = User.objects.get(username='admin')
self.stdout.write('Admin user exists, updating permissions...')
admin = User.objects.get(username="admin")
self.stdout.write("Admin user exists, updating permissions...")
except User.DoesNotExist:
admin = User.objects.create_superuser('admin', 'admin@example.com', 'admin')
self.stdout.write('Created admin user')
# Create regular users
admin = User.objects.create_superuser("admin", "admin@example.com", "admin")
self.stdout.write("Created admin user")
# Create 10 regular users
usernames = [
'destiny89', 'destiny97', 'thompsonchris', 'chriscohen', 'littlesharon',
'wrichardson', 'christophermiles', 'jacksonangela', 'jennifer71', 'smithemily',
'brandylong', 'milleranna', 'tlopez', 'fgriffith', 'mariah80',
'kendradavis', 'rosarioashley', 'camposkaitlyn', 'lisaherrera', 'riveratiffany',
'codytucker', 'cheyenne78', 'christinagreen', 'eric57', 'steinsuzanne',
'david95', 'rstewart', 'josephhaynes', 'umedina', 'tylerbryant',
'lcampos', 'shellyford', 'ksmith', 'qeverett', 'waguilar',
'zbrowning', 'yalexander', 'wallacewilliam', 'bsuarez', 'ismith',
'joyceosborne', 'garythomas', 'tlewis', 'robertgonzales', 'medinashannon',
'yhanson', 'howellmorgan', 'taylorsusan', 'barnold', 'bryan20'
"thrillseeker1",
"coasterrider2",
"parkfan3",
"adventurer4",
"funseeker5",
"parkexplorer6",
"ridetester7",
"themepark8",
"coaster9",
"parkvisitor10"
]
for username in usernames:
if not User.objects.filter(username=username).exists():
User.objects.create_user(
username=username,
email=f'{username}@example.com',
password='password123'
)
self.stdout.write(f'Created user: {username}')
User.objects.create_user(
username=username,
email=f"{username}@example.com",
[PASSWORD-REMOVED]",
)
self.stdout.write(f"Created user: {username}")
def create_companies(self):
self.stdout.write('Creating companies...')
# Delete existing companies
Company.objects.all().delete()
self.stdout.write('Deleted existing companies')
self.stdout.write("Creating companies...")
companies = [
'The Walt Disney Company',
'Cedar Fair',
'NBCUniversal',
'Merlin Entertainments',
'Mack Rides'
"The Walt Disney Company",
"Cedar Fair",
"NBCUniversal",
"Merlin Entertainments",
"Mack Rides",
]
for name in companies:
Company.objects.create(name=name)
self.stdout.write(f'Created company: {name}')
self.stdout.write(f"Created company: {name}")
def create_manufacturers(self):
self.stdout.write('Creating manufacturers...')
# Delete existing manufacturers
Manufacturer.objects.all().delete()
self.stdout.write('Deleted existing manufacturers')
self.stdout.write("Creating manufacturers...")
manufacturers = [
'Walt Disney Imagineering',
'Bolliger & Mabillard',
'Intamin',
'Rocky Mountain Construction',
'Vekoma',
'Mack Rides',
'Oceaneering International'
"Walt Disney Imagineering",
"Bolliger & Mabillard",
"Intamin",
"Rocky Mountain Construction",
"Vekoma",
"Mack Rides",
"Oceaneering International",
]
for name in manufacturers:
Manufacturer.objects.create(name=name)
self.stdout.write(f'Created manufacturer: {name}')
self.stdout.write(f"Created manufacturer: {name}")
def download_image(self, url):
"""Download image from URL and return as Django File object"""
@@ -145,98 +198,93 @@ class Command(BaseCommand):
return None
def create_parks_and_rides(self):
# Delete existing parks and rides
Park.objects.all().delete()
self.stdout.write('Deleted existing parks and rides')
# Load seed data
with open(os.path.join(os.path.dirname(__file__), 'seed_data.json')) as f:
with open(os.path.join(os.path.dirname(__file__), "seed_data.json")) as f:
data = json.load(f)
country_map = {
'US': 'United States',
'GB': 'United Kingdom',
'DE': 'Germany'
}
for park_data in data['parks']:
for park_data in data["parks"]:
try:
country = Country.objects.get(code2=park_data['country'])
# Create park
# Create park with location data
park_coords = PARK_COORDINATES[park_data["name"]]
park = Park.objects.create(
name=park_data['name'],
country=country,
opening_date=park_data['opening_date'],
status=park_data['status'],
description=park_data['description'],
website=park_data['website'],
owner=Company.objects.get(name=park_data['owner']),
size_acres=park_data['size_acres']
name=park_data["name"],
country=park_data["country"],
opening_date=park_data["opening_date"],
status=park_data["status"],
description=park_data["description"],
website=park_data["website"],
owner=Company.objects.get(name=park_data["owner"]),
size_acres=park_data["size_acres"],
# Add location fields
latitude=park_coords["latitude"],
longitude=park_coords["longitude"],
street_address=park_coords["street_address"],
city=park_coords["city"],
state=park_coords["state"],
postal_code=park_coords["postal_code"]
)
# Add park photos
for photo_url in park_data['photos']:
for photo_url in park_data["photos"]:
img_file = self.download_image(photo_url)
if img_file:
Photo.objects.create(
image=img_file,
content_type=ContentType.objects.get_for_model(park),
object_id=park.id,
is_primary=True # First photo is primary
is_primary=True, # First photo is primary
)
# Create rides
for ride_data in park_data['rides']:
for ride_data in park_data["rides"]:
ride = Ride.objects.create(
name=ride_data['name'],
name=ride_data["name"],
park=park,
category=ride_data['category'],
opening_date=ride_data['opening_date'],
status=ride_data['status'],
manufacturer=Manufacturer.objects.get(name=ride_data['manufacturer']),
description=ride_data['description']
category=ride_data["category"],
opening_date=ride_data["opening_date"],
status=ride_data["status"],
manufacturer=Manufacturer.objects.get(
name=ride_data["manufacturer"]
),
description=ride_data["description"],
)
# Add ride photos
for photo_url in ride_data['photos']:
for photo_url in ride_data["photos"]:
img_file = self.download_image(photo_url)
if img_file:
Photo.objects.create(
image=img_file,
content_type=ContentType.objects.get_for_model(ride),
object_id=ride.id,
is_primary=True # First photo is primary
is_primary=True, # First photo is primary
)
# Add coaster stats if present
if 'stats' in ride_data:
if "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']
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"],
)
self.stdout.write(f'Created park and rides: {park.name}')
except Country.DoesNotExist:
self.stdout.write(f'Country not found: {park_data["country"]}')
self.stdout.write(f"Created park and rides: {park.name}")
except Exception as e:
self.stdout.write(self.style.ERROR(f"Error creating park {park_data['name']}: {str(e)}"))
continue
def create_reviews(self):
# Delete existing reviews
Review.objects.all().delete()
self.stdout.write('Deleted existing reviews')
users = list(User.objects.exclude(username='admin'))
users = list(User.objects.exclude(username="admin"))
parks = list(Park.objects.all())
# Generate random dates within the last year
today = datetime.now().date()
one_year_ago = today - timedelta(days=365)
for park in parks:
# Create 3-5 reviews per park
num_reviews = random.randint(3, 5)
@@ -244,42 +292,14 @@ class Command(BaseCommand):
# Generate random visit date
days_offset = random.randint(0, 365)
visit_date = one_year_ago + timedelta(days=days_offset)
Review.objects.create(
user=random.choice(users),
content_type=ContentType.objects.get_for_model(park),
object_id=park.id,
title=f'Great experience at {park.name}',
content='Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
title=f"Great experience at {park.name}",
content="Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
rating=random.randint(7, 10),
visit_date=visit_date
visit_date=visit_date,
)
self.stdout.write(f'Created reviews for {park.name}')
def create_top_lists(self):
# Delete existing top lists
# TopList.objects.all().delete()
self.stdout.write('Deleted existing top lists')
users = list(User.objects.exclude(username='admin'))
parks = list(Park.objects.all())
for i, user in enumerate(users, 1):
# Create top list for every 10th user
if i % 10 == 0:
# top_list = TopList.objects.create(
# user=user,
# name=f"{user.username}'s Top Parks",
# description='My favorite theme parks'
# )
# Add 3-5 random parks
# selected_parks = random.sample(parks, random.randint(3, 5))
# for j, park in enumerate(selected_parks, 1):
# TopListItem.objects.create(
# top_list=top_list,
# content_type=ContentType.objects.get_for_model(park),
# object_id=park.id,
# rank=j
# )
self.stdout.write(f'Created top lists for {i}/50 users')
self.stdout.write(f"Created reviews for {park.name}")

View File

@@ -1,9 +1,5 @@
# Generated by Django 5.1.2 on 2024-10-28 20:17
import django.db.models.deletion
import simple_history.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
@@ -12,57 +8,32 @@ class Migration(migrations.Migration):
dependencies = [
('companies', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='HistoricalPark',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('slug', models.SlugField(max_length=255)),
('location', models.CharField(max_length=255)),
('description', models.TextField(blank=True)),
('status', models.CharField(choices=[('OPERATING', 'Operating'), ('CLOSED_TEMP', 'Temporarily Closed'), ('CLOSED_PERM', 'Permanently Closed'), ('UNDER_CONSTRUCTION', 'Under Construction'), ('DEMOLISHED', 'Demolished')], default='OPERATING', max_length=20)),
('opening_date', models.DateField(blank=True, null=True)),
('closing_date', models.DateField(blank=True, null=True)),
('operating_season', models.CharField(blank=True, max_length=255)),
('size_acres', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('website', models.URLField(blank=True)),
('average_rating', models.DecimalField(blank=True, decimal_places=2, max_digits=3, null=True)),
('created_at', models.DateTimeField(blank=True, editable=False, null=True)),
('updated_at', models.DateTimeField(blank=True, editable=False)),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('owner', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='companies.company')),
],
options={
'verbose_name': 'historical park',
'verbose_name_plural': 'historical parks',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='Park',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('slug', models.SlugField(max_length=255, unique=True)),
('location', models.CharField(max_length=255)),
('description', models.TextField(blank=True)),
('status', models.CharField(choices=[('OPERATING', 'Operating'), ('CLOSED_TEMP', 'Temporarily Closed'), ('CLOSED_PERM', 'Permanently Closed'), ('UNDER_CONSTRUCTION', 'Under Construction'), ('DEMOLISHED', 'Demolished')], default='OPERATING', max_length=20)),
('status', models.CharField(choices=[('OPERATING', 'Operating'), ('CLOSED_TEMP', 'Temporarily Closed'), ('CLOSED_PERM', 'Permanently Closed'), ('UNDER_CONSTRUCTION', 'Under Construction'), ('DEMOLISHED', 'Demolished'), ('RELOCATED', 'Relocated')], default='OPERATING', max_length=20)),
('opening_date', models.DateField(blank=True, null=True)),
('closing_date', models.DateField(blank=True, null=True)),
('operating_season', models.CharField(blank=True, max_length=255)),
('size_acres', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('website', models.URLField(blank=True)),
('average_rating', models.DecimalField(blank=True, decimal_places=2, max_digits=3, null=True)),
('total_rides', models.IntegerField(blank=True, null=True)),
('total_roller_coasters', models.IntegerField(blank=True, null=True)),
('latitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
('longitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
('street_address', models.CharField(blank=True, max_length=255)),
('city', models.CharField(blank=True, max_length=255)),
('state', models.CharField(blank=True, max_length=255)),
('country', models.CharField(blank=True, max_length=255)),
('postal_code', models.CharField(blank=True, max_length=20)),
('created_at', models.DateTimeField(auto_now_add=True, null=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parks', to='companies.company')),
@@ -71,32 +42,6 @@ class Migration(migrations.Migration):
'ordering': ['name'],
},
),
migrations.CreateModel(
name='HistoricalParkArea',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('slug', models.SlugField(max_length=255)),
('description', models.TextField(blank=True)),
('opening_date', models.DateField(blank=True, null=True)),
('closing_date', models.DateField(blank=True, null=True)),
('created_at', models.DateTimeField(blank=True, editable=False, null=True)),
('updated_at', models.DateTimeField(blank=True, editable=False)),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('park', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='parks.park')),
],
options={
'verbose_name': 'historical park area',
'verbose_name_plural': 'historical park areas',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='ParkArea',
fields=[

View File

@@ -1,25 +0,0 @@
# Generated manually
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('parks', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='park',
name='country',
field=models.CharField(max_length=2, default='US', help_text='Two-letter country code (ISO 3166-1 alpha-2)'),
preserve_default=False,
),
migrations.AddField(
model_name='historicalpark',
name='country',
field=models.CharField(max_length=2, default='US', help_text='Two-letter country code (ISO 3166-1 alpha-2)'),
preserve_default=False,
),
]

View File

@@ -0,0 +1,84 @@
# Generated by Django 5.1.2 on 2024-11-03 03:44
import django.db.models.deletion
import simple_history.models
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('companies', '0004_add_total_parks'),
('parks', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='HistoricalPark',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('slug', models.SlugField(max_length=255)),
('description', models.TextField(blank=True)),
('status', models.CharField(choices=[('OPERATING', 'Operating'), ('CLOSED_TEMP', 'Temporarily Closed'), ('CLOSED_PERM', 'Permanently Closed'), ('UNDER_CONSTRUCTION', 'Under Construction'), ('DEMOLISHED', 'Demolished'), ('RELOCATED', 'Relocated')], default='OPERATING', max_length=20)),
('opening_date', models.DateField(blank=True, null=True)),
('closing_date', models.DateField(blank=True, null=True)),
('operating_season', models.CharField(blank=True, max_length=255)),
('size_acres', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('website', models.URLField(blank=True)),
('average_rating', models.DecimalField(blank=True, decimal_places=2, max_digits=3, null=True)),
('total_rides', models.IntegerField(blank=True, null=True)),
('total_roller_coasters', models.IntegerField(blank=True, null=True)),
('latitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
('longitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
('street_address', models.CharField(blank=True, max_length=255)),
('city', models.CharField(blank=True, max_length=255)),
('state', models.CharField(blank=True, max_length=255)),
('country', models.CharField(blank=True, max_length=255)),
('postal_code', models.CharField(blank=True, max_length=20)),
('created_at', models.DateTimeField(blank=True, editable=False, null=True)),
('updated_at', models.DateTimeField(blank=True, editable=False)),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('owner', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='companies.company')),
],
options={
'verbose_name': 'historical park',
'verbose_name_plural': 'historical parks',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='HistoricalParkArea',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('slug', models.SlugField(max_length=255)),
('description', models.TextField(blank=True)),
('opening_date', models.DateField(blank=True, null=True)),
('closing_date', models.DateField(blank=True, null=True)),
('created_at', models.DateTimeField(blank=True, editable=False, null=True)),
('updated_at', models.DateTimeField(blank=True, editable=False)),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('park', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='parks.park')),
],
options={
'verbose_name': 'historical park area',
'verbose_name_plural': 'historical park areas',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
]

View File

@@ -1,45 +0,0 @@
# Generated by Django 5.1.2 on 2024-10-30 01:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("parks", "0002_add_country_field"),
]
operations = [
migrations.AlterField(
model_name="historicalpark",
name="status",
field=models.CharField(
choices=[
("OPERATING", "Operating"),
("CLOSED_TEMP", "Temporarily Closed"),
("CLOSED_PERM", "Permanently Closed"),
("UNDER_CONSTRUCTION", "Under Construction"),
("DEMOLISHED", "Demolished"),
("RELOCATED", "Relocated"),
],
default="OPERATING",
max_length=20,
),
),
migrations.AlterField(
model_name="park",
name="status",
field=models.CharField(
choices=[
("OPERATING", "Operating"),
("CLOSED_TEMP", "Temporarily Closed"),
("CLOSED_PERM", "Permanently Closed"),
("UNDER_CONSTRUCTION", "Under Construction"),
("DEMOLISHED", "Demolished"),
("RELOCATED", "Relocated"),
],
default="OPERATING",
max_length=20,
),
),
]

View File

@@ -0,0 +1,55 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('parks', '0002_historicalpark_historicalparkarea'),
]
operations = [
migrations.AlterField(
model_name='park',
name='latitude',
field=models.DecimalField(
blank=True,
decimal_places=6,
max_digits=9, # Changed to 9 to handle -90.000000 to 90.000000
null=True,
help_text='Latitude coordinate (-90 to 90)',
),
),
migrations.AlterField(
model_name='park',
name='longitude',
field=models.DecimalField(
blank=True,
decimal_places=6,
max_digits=10, # Changed to 10 to handle -180.000000 to 180.000000
null=True,
help_text='Longitude coordinate (-180 to 180)',
),
),
migrations.AlterField(
model_name='historicalpark',
name='latitude',
field=models.DecimalField(
blank=True,
decimal_places=6,
max_digits=9, # Changed to 9 to handle -90.000000 to 90.000000
null=True,
help_text='Latitude coordinate (-90 to 90)',
),
),
migrations.AlterField(
model_name='historicalpark',
name='longitude',
field=models.DecimalField(
blank=True,
decimal_places=6,
max_digits=10, # Changed to 10 to handle -180.000000 to 180.000000
null=True,
help_text='Longitude coordinate (-180 to 180)',
),
),
]

View File

@@ -0,0 +1,101 @@
from django.db import migrations, models
from django.core.validators import MinValueValidator, MaxValueValidator
from decimal import Decimal
from django.core.exceptions import ValidationError
def validate_coordinate_digits(value, max_digits):
"""Validate total number of digits in a coordinate value"""
if value is not None:
# Convert to string and remove decimal point and sign
str_val = str(abs(value)).replace('.', '')
# Remove trailing zeros after decimal point
str_val = str_val.rstrip('0')
if len(str_val) > max_digits:
raise ValidationError(
f'Ensure that there are no more than {max_digits} digits in total.'
)
def validate_latitude_digits(value):
"""Validate total number of digits in latitude"""
validate_coordinate_digits(value, 9)
def validate_longitude_digits(value):
"""Validate total number of digits in longitude"""
validate_coordinate_digits(value, 10)
class Migration(migrations.Migration):
dependencies = [
('parks', '0003_update_coordinate_fields'),
]
operations = [
migrations.AlterField(
model_name='park',
name='latitude',
field=models.DecimalField(
blank=True,
decimal_places=6,
help_text='Latitude coordinate (-90 to 90)',
max_digits=9,
null=True,
validators=[
MinValueValidator(Decimal('-90')),
MaxValueValidator(Decimal('90')),
validate_latitude_digits,
],
),
),
migrations.AlterField(
model_name='park',
name='longitude',
field=models.DecimalField(
blank=True,
decimal_places=6,
help_text='Longitude coordinate (-180 to 180)',
max_digits=10,
null=True,
validators=[
MinValueValidator(Decimal('-180')),
MaxValueValidator(Decimal('180')),
validate_longitude_digits,
],
),
),
migrations.AlterField(
model_name='historicalpark',
name='latitude',
field=models.DecimalField(
blank=True,
decimal_places=6,
help_text='Latitude coordinate (-90 to 90)',
max_digits=9,
null=True,
validators=[
MinValueValidator(Decimal('-90')),
MaxValueValidator(Decimal('90')),
validate_latitude_digits,
],
),
),
migrations.AlterField(
model_name='historicalpark',
name='longitude',
field=models.DecimalField(
blank=True,
decimal_places=6,
help_text='Longitude coordinate (-180 to 180)',
max_digits=10,
null=True,
validators=[
MinValueValidator(Decimal('-180')),
MaxValueValidator(Decimal('180')),
validate_longitude_digits,
],
),
),
]

View File

@@ -1,37 +0,0 @@
# Generated by Django 5.1.2 on 2024-10-30 23:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("parks", "0003_alter_historicalpark_status_alter_park_status"),
]
operations = [
migrations.AddField(
model_name="historicalpark",
name="city",
field=models.CharField(blank=True, max_length=255),
),
migrations.AddField(
model_name="historicalpark",
name="state",
field=models.CharField(
blank=True, help_text="State/Province/Region", max_length=255
),
),
migrations.AddField(
model_name="park",
name="city",
field=models.CharField(blank=True, max_length=255),
),
migrations.AddField(
model_name="park",
name="state",
field=models.CharField(
blank=True, help_text="State/Province/Region", max_length=255
),
),
]

View File

@@ -0,0 +1,58 @@
from django.db import migrations
from decimal import Decimal, ROUND_DOWN
def normalize_coordinate(value, max_digits, decimal_places):
"""Normalize coordinate to have exactly 6 decimal places"""
try:
if value is None:
return None
# Convert to Decimal for precise handling
value = Decimal(str(value))
# Round to exactly 6 decimal places
value = value.quantize(Decimal('0.000001'), rounding=ROUND_DOWN)
return value
except (TypeError, ValueError):
return None
def normalize_existing_coordinates(apps, schema_editor):
Park = apps.get_model('parks', 'Park')
HistoricalPark = apps.get_model('parks', 'HistoricalPark')
# Normalize coordinates in current parks
for park in Park.objects.all():
if park.latitude is not None:
park.latitude = normalize_coordinate(park.latitude, 9, 6)
if park.longitude is not None:
park.longitude = normalize_coordinate(park.longitude, 10, 6)
park.save()
# Normalize coordinates in historical records
for record in HistoricalPark.objects.all():
if record.latitude is not None:
record.latitude = normalize_coordinate(record.latitude, 9, 6)
if record.longitude is not None:
record.longitude = normalize_coordinate(record.longitude, 10, 6)
record.save()
def reverse_normalize_coordinates(apps, schema_editor):
# No need to reverse normalization as it would only reduce precision
pass
class Migration(migrations.Migration):
dependencies = [
('parks', '0004_add_coordinate_validators'),
]
operations = [
migrations.RunPython(
normalize_existing_coordinates,
reverse_normalize_coordinates
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 5.1.2 on 2024-10-30 23:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("parks", "0004_historicalpark_city_historicalpark_state_park_city_and_more"),
]
operations = [
migrations.AlterField(
model_name="historicalpark",
name="country",
field=models.CharField(help_text="Country name", max_length=255),
),
migrations.AlterField(
model_name="park",
name="country",
field=models.CharField(help_text="Country name", max_length=255),
),
]

View File

@@ -1,125 +0,0 @@
from django.db import migrations, models
import django.db.models.deletion
def forwards_func(apps, schema_editor):
# Get the historical models
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")
# Create default country for existing parks
default_country, _ = Country.objects.get_or_create(
name='Unknown',
name_ascii='Unknown',
slug='unknown',
code2='XX'
)
# Store old values
parks_data = []
for park in Park.objects.all():
parks_data.append({
'id': park.id,
'old_country': park.country,
'old_state': park.state,
'location': park.location
})
# Remove old fields first
Park._meta.get_field('country').null = True
Park._meta.get_field('state').null = True
Park.objects.all().update(country=None, state=None)
# Now update with new values
for data in parks_data:
park = Park.objects.get(id=data['id'])
park.country_id = default_country.id
park.save()
def reverse_func(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('cities_light', '0011_alter_city_country_alter_city_region_and_more'),
('parks', '0005_update_country_field_length'),
]
operations = [
# First make the fields nullable
migrations.AlterField(
model_name='park',
name='country',
field=models.CharField(max_length=255, null=True),
),
migrations.AlterField(
model_name='historicalpark',
name='country',
field=models.CharField(max_length=255, null=True),
),
migrations.AlterField(
model_name='park',
name='state',
field=models.CharField(max_length=255, null=True),
),
migrations.AlterField(
model_name='historicalpark',
name='state',
field=models.CharField(max_length=255, null=True),
),
# Run the data migration
migrations.RunPython(forwards_func, reverse_func),
# Remove old fields
migrations.RemoveField(
model_name='park',
name='state',
),
migrations.RemoveField(
model_name='historicalpark',
name='state',
),
migrations.RemoveField(
model_name='park',
name='country',
),
migrations.RemoveField(
model_name='historicalpark',
name='country',
),
# Add new fields
migrations.AddField(
model_name='park',
name='country',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='cities_light.country'),
),
migrations.AddField(
model_name='park',
name='region',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='cities_light.region'),
),
migrations.AddField(
model_name='park',
name='city',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='cities_light.city'),
),
migrations.AddField(
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.AddField(
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.AddField(
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

@@ -1,17 +0,0 @@
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

@@ -1,43 +0,0 @@
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

@@ -1,33 +0,0 @@
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

@@ -1,34 +0,0 @@
# 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

@@ -1,72 +0,0 @@
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

@@ -1,52 +0,0 @@
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

@@ -1,13 +0,0 @@
# 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

@@ -1,13 +0,0 @@
# 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

@@ -1,67 +0,0 @@
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

@@ -1,52 +0,0 @@
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

@@ -1,16 +0,0 @@
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

@@ -1,22 +0,0 @@
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

@@ -1,22 +0,0 @@
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

@@ -1,48 +0,0 @@
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

@@ -1,15 +0,0 @@
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

@@ -1,16 +0,0 @@
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

@@ -1,32 +0,0 @@
# Generated by Django 5.1.2 on 2024-11-01 00:24
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", "0020_remove_historicalpark_city_text"),
]
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="location",
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

@@ -1,8 +1,55 @@
from django.db import models
from django.contrib.contenttypes.fields import GenericRelation
from django.urls import reverse
from django.utils.text import slugify
from django.contrib.contenttypes.fields import GenericRelation
from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.exceptions import ValidationError
from decimal import Decimal, ROUND_DOWN, InvalidOperation
from simple_history.models import HistoricalRecords
from cities_light.models import Country, Region, City
from companies.models import Company
from media.models import Photo
def normalize_coordinate(value, max_digits, decimal_places):
"""Normalize coordinate to have at most max_digits total digits and decimal_places decimal places"""
try:
if value is None:
return None
# Convert to Decimal for precise handling
value = Decimal(str(value))
# Round to specified decimal places
value = Decimal(value.quantize(Decimal('0.' + '0' * decimal_places), rounding=ROUND_DOWN))
return value
except (TypeError, ValueError, InvalidOperation):
return None
def validate_coordinate_digits(value, max_digits, decimal_places):
"""Validate total number of digits in a coordinate value"""
if value is not None:
try:
# Convert to Decimal for precise handling
value = Decimal(str(value))
# Round to exactly 6 decimal places
value = value.quantize(Decimal('0.000001'), rounding=ROUND_DOWN)
return value
except (InvalidOperation, TypeError):
raise ValidationError('Invalid coordinate value.')
return value
def validate_latitude_digits(value):
"""Validate total number of digits in latitude"""
return validate_coordinate_digits(value, 9, 6)
def validate_longitude_digits(value):
"""Validate total number of digits in longitude"""
return validate_coordinate_digits(value, 10, 6)
class Park(models.Model):
STATUS_CHOICES = [
@@ -16,23 +63,45 @@ class Park(models.Model):
name = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True)
location = models.CharField(max_length=255, blank=True, null=True) # Made nullable
country = models.ForeignKey(Country, on_delete=models.PROTECT)
region = models.ForeignKey(Region, on_delete=models.PROTECT, null=True, blank=True)
city = models.ForeignKey(City, on_delete=models.PROTECT, null=True, blank=True)
description = models.TextField(blank=True)
owner = models.ForeignKey(
'companies.Company',
on_delete=models.SET_NULL,
related_name='parks',
null=True,
blank=True
)
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='OPERATING'
)
# Location fields
latitude = models.DecimalField(
max_digits=9,
decimal_places=6,
null=True,
blank=True,
help_text='Latitude coordinate (-90 to 90)',
validators=[
MinValueValidator(Decimal('-90')),
MaxValueValidator(Decimal('90')),
validate_latitude_digits,
]
)
longitude = models.DecimalField(
max_digits=10,
decimal_places=6,
null=True,
blank=True,
help_text='Longitude coordinate (-180 to 180)',
validators=[
MinValueValidator(Decimal('-180')),
MaxValueValidator(Decimal('180')),
validate_longitude_digits,
]
)
street_address = models.CharField(max_length=255, blank=True)
city = models.CharField(max_length=255, blank=True)
state = models.CharField(max_length=255, blank=True)
country = models.CharField(max_length=255, blank=True)
postal_code = models.CharField(max_length=20, blank=True)
# Details
opening_date = models.DateField(null=True, blank=True)
closing_date = models.DateField(null=True, blank=True)
operating_season = models.CharField(max_length=255, blank=True)
@@ -43,16 +112,30 @@ class Park(models.Model):
blank=True
)
website = models.URLField(blank=True)
# Statistics
average_rating = models.DecimalField(
max_digits=3,
decimal_places=2,
null=True,
blank=True
)
total_rides = models.IntegerField(null=True, blank=True)
total_roller_coasters = models.IntegerField(null=True, blank=True)
# Relationships
owner = models.ForeignKey(
Company,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='parks'
)
photos = GenericRelation(Photo, related_query_name='park')
# Metadata
created_at = models.DateTimeField(auto_now_add=True, null=True)
updated_at = models.DateTimeField(auto_now=True)
photos = GenericRelation('media.Photo')
reviews = GenericRelation('reviews.Review')
history = HistoricalRecords()
class Meta:
@@ -65,11 +148,17 @@ class Park(models.Model):
if not self.slug:
self.slug = slugify(self.name)
# Update the location field to combine country, region, and city
self.location = self.get_formatted_location()
# Normalize coordinates before saving
if self.latitude is not None:
self.latitude = normalize_coordinate(self.latitude, 9, 6)
if self.longitude is not None:
self.longitude = normalize_coordinate(self.longitude, 10, 6)
super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse('parks:park_detail', kwargs={'slug': self.slug})
@classmethod
def get_by_slug(cls, slug):
"""Get park by current or historical slug"""
@@ -79,37 +168,28 @@ class Park(models.Model):
# Check historical slugs
history = cls.history.filter(slug=slug).order_by('-history_date').first()
if history:
return cls.objects.get(id=history.id), True
raise cls.DoesNotExist("No park found with this slug")
try:
return cls.objects.get(id=history.id), True
except cls.DoesNotExist:
pass
raise cls.DoesNotExist()
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:
location += f", {self.region.name}, {self.city.name}"
elif self.region:
location += f", {self.region.name}"
return location
class ParkArea(models.Model):
name = models.CharField(max_length=255)
slug = models.SlugField(max_length=255)
description = models.TextField(blank=True)
park = models.ForeignKey(
Park,
on_delete=models.CASCADE,
related_name='areas'
)
name = models.CharField(max_length=255)
slug = models.SlugField(max_length=255)
description = models.TextField(blank=True)
opening_date = models.DateField(null=True, blank=True)
closing_date = models.DateField(null=True, blank=True)
# Metadata
created_at = models.DateTimeField(auto_now_add=True, null=True)
updated_at = models.DateTimeField(auto_now=True)
photos = GenericRelation('media.Photo')
history = HistoricalRecords()
class Meta:
@@ -117,13 +197,19 @@ class ParkArea(models.Model):
unique_together = ['park', 'slug']
def __str__(self):
return f"{self.park.name} - {self.name}"
return f"{self.name} at {self.park.name}"
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse('parks:area_detail', kwargs={
'park_slug': self.park.slug,
'area_slug': self.slug
})
@classmethod
def get_by_slug(cls, slug):
"""Get area by current or historical slug"""
@@ -133,5 +219,8 @@ class ParkArea(models.Model):
# Check historical slugs
history = cls.history.filter(slug=slug).order_by('-history_date').first()
if history:
return cls.objects.get(id=history.id), True
raise cls.DoesNotExist("No area found with this slug")
try:
return cls.objects.get(id=history.id), True
except cls.DoesNotExist:
pass
raise cls.DoesNotExist()

View File

@@ -1,17 +1,22 @@
from django.urls import path, include
from . import views
from rides.views import RideListView
app_name = 'parks'
app_name = "parks"
urlpatterns = [
path('', views.ParkListView.as_view(), name='park_list'),
path('create/', views.ParkCreateView.as_view(), name='park_create'),
path('rides/', RideListView.as_view(), name='all_rides'), # Global rides list
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')),
# Park views
path("", views.ParkListView.as_view(), name="park_list"),
path("create/", views.ParkCreateView.as_view(), name="park_create"),
path("<slug:slug>/", views.ParkDetailView.as_view(), name="park_detail"),
path("<slug:slug>/edit/", views.ParkUpdateView.as_view(), name="park_update"),
# Location search endpoints
path("search/location/", views.location_search, name="location_search"),
path("search/reverse-geocode/", views.reverse_geocode, name="reverse_geocode"),
# Area views
path("<slug:park_slug>/areas/<slug:area_slug>/", views.ParkAreaDetailView.as_view(), name="area_detail"),
# Include rides URLs
path("<slug:park_slug>/rides/", include("rides.urls", namespace="rides")),
]

View File

@@ -1,3 +1,4 @@
from decimal import Decimal, ROUND_DOWN
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
@@ -7,190 +8,146 @@ 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
import requests
from .models import Park, ParkArea
from .forms import ParkForm
from rides.models import Ride
from .location_utils import normalize_coordinate, normalize_osm_result
from core.views import SlugRedirectMixin
from moderation.mixins import EditSubmissionMixin, PhotoSubmissionMixin, HistoryMixin
from moderation.models import EditSubmission
from cities_light.models import Country, Region, City
from media.models import Photo
def get_countries(request):
def location_search(request):
"""Search for locations using OpenStreetMap Nominatim API"""
query = request.GET.get("q", "")
filter_parks = request.GET.get("filter_parks", "false") == "true"
if not query:
return JsonResponse({"results": []})
# 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,
# Call Nominatim API
response = requests.get(
"https://nominatim.openstreetmap.org/search",
params={
"q": query,
"format": "json",
"addressdetails": 1,
"namedetails": 1, # Include name tags
"accept-language": "en", # Prefer English results
"limit": 10,
},
headers={"User-Agent": "ThrillWiki/1.0"},
)
# Only filter by parks if explicitly requested
if filter_parks:
regions = regions.filter(park__isnull=False)
if response.status_code == 200:
results = response.json()
# Normalize each result
normalized_results = [normalize_osm_result(result) for result in results]
# Filter out any results with invalid coordinates
valid_results = [r for r in normalized_results if r['lat'] is not None and r['lon'] is not None]
return JsonResponse({"results": valid_results})
regions = regions.distinct().values("id", "name")[:10]
return JsonResponse(list(regions), safe=False)
return JsonResponse({"results": []})
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"
def reverse_geocode(request):
"""Reverse geocode coordinates using OpenStreetMap Nominatim API"""
try:
lat = Decimal(request.GET.get("lat", ""))
lon = Decimal(request.GET.get("lon", ""))
except (TypeError, ValueError, InvalidOperation):
return JsonResponse({"error": "Invalid coordinates"}, status=400)
if not region or not country:
return JsonResponse([], safe=False)
if not lat or not lon:
return JsonResponse({"error": "Missing coordinates"}, status=400)
# Base query
cities = City.objects.filter(
Q(name__icontains=query) | Q(alternate_names__icontains=query),
region__name__iexact=region,
region__country__name__iexact=country,
# Normalize coordinates before geocoding
lat = lat.quantize(Decimal('0.000001'), rounding=ROUND_DOWN)
lon = lon.quantize(Decimal('0.000001'), rounding=ROUND_DOWN)
# Validate ranges
if lat < -90 or lat > 90:
return JsonResponse({"error": "Latitude must be between -90 and 90"}, status=400)
if lon < -180 or lon > 180:
return JsonResponse({"error": "Longitude must be between -180 and 180"}, status=400)
# Call Nominatim API
response = requests.get(
"https://nominatim.openstreetmap.org/reverse",
params={
"lat": str(lat),
"lon": str(lon),
"format": "json",
"addressdetails": 1,
"namedetails": 1, # Include name tags
"accept-language": "en", # Prefer English results
},
headers={"User-Agent": "ThrillWiki/1.0"},
)
# Only filter by parks if explicitly requested
if filter_parks:
cities = cities.filter(park__isnull=False)
if response.status_code == 200:
result = response.json()
# Normalize the result
normalized_result = normalize_osm_result(result)
if normalized_result['lat'] is None or normalized_result['lon'] is None:
return JsonResponse({"error": "Invalid coordinates"}, status=400)
return JsonResponse(normalized_result)
cities = cities.distinct().values("id", "name")[:10]
return JsonResponse(list(cities), safe=False)
return JsonResponse({"error": "Geocoding failed"}, status=500)
class ParkCreateView(LoginRequiredMixin, CreateView):
class ParkListView(ListView):
model = Park
form_class = ParkForm
template_name = "parks/park_form.html"
template_name = "parks/park_list.html"
context_object_name = "parks"
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 get_queryset(self):
queryset = Park.objects.select_related("owner").prefetch_related("photos")
def form_valid(self, form):
changes = self.prepare_changes_data(form.cleaned_data)
search = self.request.GET.get("search", "").strip()
country = self.request.GET.get("country", "").strip()
region = self.request.GET.get("region", "").strip()
city = self.request.GET.get("city", "").strip()
statuses = self.request.GET.getlist("status")
# Create submission record
submission = EditSubmission.objects.create(
user=self.request.user,
content_type=ContentType.objects.get_for_model(Park),
submission_type="CREATE",
changes=changes,
reason=self.request.POST.get("reason", ""),
source=self.request.POST.get("source", ""),
)
if search:
queryset = queryset.filter(
Q(name__icontains=search)
| Q(city__icontains=search)
| Q(state__icontains=search)
| Q(country__icontains=search)
)
# 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())
if country:
queryset = queryset.filter(country__icontains=country)
messages.success(self.request, "Your park submission has been sent for review")
return HttpResponseRedirect(reverse("parks:park_list"))
if region:
queryset = queryset.filter(state__icontains=region)
def get_success_url(self):
return reverse("parks:park_detail", kwargs={"slug": self.object.slug})
if city:
queryset = queryset.filter(city__icontains=city)
if statuses:
queryset = queryset.filter(status__in=statuses)
class ParkUpdateView(LoginRequiredMixin, UpdateView):
model = Park
form_class = ParkForm
template_name = "parks/park_form.html"
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["is_edit"] = True
context["current_filters"] = {
"search": self.request.GET.get("search", ""),
"country": self.request.GET.get("country", ""),
"region": self.request.GET.get("region", ""),
"city": self.request.GET.get("city", ""),
"statuses": self.request.GET.getlist("status"),
}
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})
def get(self, request, *args, **kwargs):
# Check if this is an HTMX request
if request.htmx:
# If it is, return just the parks list partial
self.template_name = "parks/partials/park_list.html"
return super().get(request, *args, **kwargs)
class ParkDetailView(
@@ -211,18 +168,241 @@ class ParkDetailView(
# Try to get by current or historical slug
return self.model.get_by_slug(slug)[0]
def get_queryset(self):
return super().get_queryset().prefetch_related(
'rides',
'rides__manufacturer',
'photos',
'areas'
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["rides"] = Ride.objects.filter(park=self.object).select_related(
"coaster_stats"
context["areas"] = self.object.areas.all()
# Get rides ordered by status (operating first) and name
context["rides"] = self.object.rides.all().order_by(
'-status', # OPERATING will come before others
'name'
)
context["areas"] = ParkArea.objects.filter(park=self.object)
return context
def get_redirect_url_pattern(self):
return "parks:park_detail"
class ParkCreateView(LoginRequiredMixin, CreateView):
model = Park
form_class = ParkForm
template_name = "parks/park_form.html"
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
# 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()
# Convert Decimal fields to strings
decimal_fields = ["latitude", "longitude", "size_acres", "average_rating"]
for field in decimal_fields:
if data.get(field):
data[field] = str(data[field])
return data
def form_valid(self, form):
# Normalize coordinates before saving
if form.cleaned_data.get("latitude"):
lat = Decimal(str(form.cleaned_data["latitude"]))
form.cleaned_data["latitude"] = lat.quantize(Decimal('0.000001'), rounding=ROUND_DOWN)
if form.cleaned_data.get("longitude"):
lon = Decimal(str(form.cleaned_data["longitude"]))
form.cleaned_data["longitude"] = lon.quantize(Decimal('0.000001'), rounding=ROUND_DOWN)
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=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"]:
try:
self.object = form.save()
submission.object_id = self.object.id
submission.status = "APPROVED"
submission.handled_by = self.request.user
submission.save()
# Handle photo uploads
photos = self.request.FILES.getlist("photos")
for photo_file in photos:
try:
Photo.objects.create(
image=photo_file,
uploaded_by=self.request.user,
content_type=ContentType.objects.get_for_model(Park),
object_id=self.object.id,
)
except Exception as e:
messages.error(
self.request,
f"Error uploading photo {photo_file.name}: {str(e)}",
)
messages.success(
self.request,
f"Successfully created {self.object.name}. "
f"Added {len(photos)} photo(s).",
)
return HttpResponseRedirect(self.get_success_url())
except Exception as e:
messages.error(
self.request,
f"Error creating park: {str(e)}. Please check your input and try again.",
)
return self.form_invalid(form)
messages.success(
self.request,
"Your park submission has been sent for review. "
"You will be notified when it is approved.",
)
return HttpResponseRedirect(reverse("parks:park_list"))
def form_invalid(self, form):
messages.error(
self.request,
"Please correct the errors below. Required fields are marked with an asterisk (*).",
)
for field, errors in form.errors.items():
for error in errors:
messages.error(self.request, f"{field}: {error}")
return super().form_invalid(form)
def get_success_url(self):
return reverse("parks:park_detail", kwargs={"slug": self.object.slug})
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
# 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()
# Convert Decimal fields to strings
decimal_fields = ["latitude", "longitude", "size_acres", "average_rating"]
for field in decimal_fields:
if data.get(field):
data[field] = str(data[field])
return data
def form_valid(self, form):
# Normalize coordinates before saving
if form.cleaned_data.get("latitude"):
lat = Decimal(str(form.cleaned_data["latitude"]))
form.cleaned_data["latitude"] = lat.quantize(Decimal('0.000001'), rounding=ROUND_DOWN)
if form.cleaned_data.get("longitude"):
lon = Decimal(str(form.cleaned_data["longitude"]))
form.cleaned_data["longitude"] = lon.quantize(Decimal('0.000001'), rounding=ROUND_DOWN)
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"]:
try:
self.object = form.save()
submission.status = "APPROVED"
submission.handled_by = self.request.user
submission.save()
# Handle photo uploads
photos = self.request.FILES.getlist("photos")
uploaded_count = 0
for photo_file in photos:
try:
Photo.objects.create(
image=photo_file,
uploaded_by=self.request.user,
content_type=ContentType.objects.get_for_model(Park),
object_id=self.object.id,
)
uploaded_count += 1
except Exception as e:
messages.error(
self.request,
f"Error uploading photo {photo_file.name}: {str(e)}",
)
messages.success(
self.request,
f"Successfully updated {self.object.name}. "
f"Added {uploaded_count} new photo(s).",
)
return HttpResponseRedirect(self.get_success_url())
except Exception as e:
messages.error(
self.request,
f"Error updating park: {str(e)}. Please check your input and try again.",
)
return self.form_invalid(form)
messages.success(
self.request,
f"Your changes to {self.object.name} have been sent for review. "
"You will be notified when they are approved.",
)
return HttpResponseRedirect(
reverse("parks:park_detail", kwargs={"slug": self.object.slug})
)
def form_invalid(self, form):
messages.error(
self.request,
"Please correct the errors below. Required fields are marked with an asterisk (*).",
)
for field, errors in form.errors.items():
for error in errors:
messages.error(self.request, f"{field}: {error}")
return super().form_invalid(form)
def get_success_url(self):
return reverse("parks:park_detail", kwargs={"slug": self.object.slug})
class ParkAreaDetailView(
SlugRedirectMixin,
EditSubmissionMixin,
@@ -248,9 +428,6 @@ class ParkAreaDetailView(
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["rides"] = Ride.objects.filter(area=self.object).select_related(
"coaster_stats"
)
return context
def get_redirect_url_pattern(self):
@@ -258,57 +435,3 @@ class ParkAreaDetailView(
def get_redirect_url_kwargs(self):
return {"park_slug": self.object.park.slug, "area_slug": self.object.slug}
class ParkListView(ListView):
model = Park
template_name = "parks/park_list.html"
context_object_name = "parks"
def get_queryset(self):
queryset = Park.objects.select_related(
"owner", "country", "region", "city"
).prefetch_related("photos", "rides")
search = self.request.GET.get("search", "").strip()
country = self.request.GET.get("country", "").strip()
region = self.request.GET.get("region", "").strip()
city = self.request.GET.get("city", "").strip()
statuses = self.request.GET.getlist("status")
if search:
queryset = queryset.filter(
Q(name__icontains=search) | Q(location__icontains=search)
)
if country:
queryset = queryset.filter(country__name__icontains=country)
if region:
queryset = queryset.filter(region__name__icontains=region)
if city:
queryset = queryset.filter(city__name__icontains=city)
if statuses:
queryset = queryset.filter(status__in=statuses)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["current_filters"] = {
"search": self.request.GET.get("search", ""),
"country": self.request.GET.get("country", ""),
"region": self.request.GET.get("region", ""),
"city": self.request.GET.get("city", ""),
"statuses": self.request.GET.getlist("status"),
}
return context
def get(self, request, *args, **kwargs):
# Check if this is an HTMX request
if request.htmx:
# If it is, return just the parks list partial
self.template_name = "parks/partials/park_list.html"
return super().get(request, *args, **kwargs)

16
parks/views_update.py Normal file
View File

@@ -0,0 +1,16 @@
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
# 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()
# Convert Decimal fields to strings
decimal_fields = ['latitude', 'longitude', 'size_acres', 'average_rating']
for field in decimal_fields:
if data.get(field):
data[field] = str(data[field])
return data