mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 15:51:08 -05:00
Enhance moderation dashboard UI and UX:
- Add HTMX-powered filtering with instant updates - Add smooth transitions and loading states - Improve visual hierarchy and styling - Add review notes functionality - Add confirmation dialogs for actions - Make navigation sticky - Add hover effects and visual feedback - Improve dark mode support
This commit is contained in:
245
parks/management/commands/seed_initial_data.py
Normal file
245
parks/management/commands/seed_initial_data.py
Normal file
@@ -0,0 +1,245 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone
|
||||
from companies.models import Company
|
||||
from parks.models import Park, ParkArea
|
||||
from location.models import Location
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Seeds initial park data with major theme parks worldwide'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Create major theme park companies
|
||||
companies_data = [
|
||||
{
|
||||
'name': 'The Walt Disney Company',
|
||||
'website': 'https://www.disney.com/',
|
||||
'headquarters': 'Burbank, California',
|
||||
'description': 'The world\'s largest entertainment company and theme park operator.'
|
||||
},
|
||||
{
|
||||
'name': 'Universal Parks & Resorts',
|
||||
'website': 'https://www.universalparks.com/',
|
||||
'headquarters': 'Orlando, Florida',
|
||||
'description': 'A division of Comcast NBCUniversal, operating major theme parks worldwide.'
|
||||
},
|
||||
{
|
||||
'name': 'Six Flags Entertainment Corporation',
|
||||
'website': 'https://www.sixflags.com/',
|
||||
'headquarters': 'Arlington, Texas',
|
||||
'description': 'The world\'s largest regional theme park company.'
|
||||
},
|
||||
{
|
||||
'name': 'Cedar Fair Entertainment Company',
|
||||
'website': 'https://www.cedarfair.com/',
|
||||
'headquarters': 'Sandusky, Ohio',
|
||||
'description': 'One of North America\'s largest operators of regional amusement parks.'
|
||||
},
|
||||
{
|
||||
'name': 'Herschend Family Entertainment',
|
||||
'website': 'https://www.hfecorp.com/',
|
||||
'headquarters': 'Atlanta, Georgia',
|
||||
'description': 'The largest family-owned themed attractions corporation in the United States.'
|
||||
},
|
||||
{
|
||||
'name': 'SeaWorld Parks & Entertainment',
|
||||
'website': 'https://www.seaworldentertainment.com/',
|
||||
'headquarters': 'Orlando, Florida',
|
||||
'description': 'Theme park and entertainment company focusing on nature-based themes.'
|
||||
}
|
||||
]
|
||||
|
||||
companies = {}
|
||||
for company_data in companies_data:
|
||||
company, created = Company.objects.get_or_create(
|
||||
name=company_data['name'],
|
||||
defaults=company_data
|
||||
)
|
||||
companies[company.name] = company
|
||||
self.stdout.write(f'{"Created" if created else "Found"} company: {company.name}')
|
||||
|
||||
# Create parks with their locations
|
||||
parks_data = [
|
||||
{
|
||||
'name': 'Magic Kingdom',
|
||||
'company': 'The Walt Disney Company',
|
||||
'description': 'The first theme park at Walt Disney World Resort in Florida, opened in 1971.',
|
||||
'opening_date': '1971-10-01',
|
||||
'size_acres': 142,
|
||||
'location': {
|
||||
'street_address': '1180 Seven Seas Dr',
|
||||
'city': 'Lake Buena Vista',
|
||||
'state': 'Florida',
|
||||
'country': 'United States',
|
||||
'postal_code': '32830',
|
||||
'latitude': 28.4177,
|
||||
'longitude': -81.5812
|
||||
},
|
||||
'areas': [
|
||||
{'name': 'Main Street, U.S.A.', 'description': 'Victorian-era themed entrance corridor'},
|
||||
{'name': 'Adventureland', 'description': 'Exotic tropical places themed area'},
|
||||
{'name': 'Frontierland', 'description': 'American Old West themed area'},
|
||||
{'name': 'Liberty Square', 'description': 'Colonial America themed area'},
|
||||
{'name': 'Fantasyland', 'description': 'Fairy tale themed area'},
|
||||
{'name': 'Tomorrowland', 'description': 'Future themed area'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'name': 'Universal Studios Florida',
|
||||
'company': 'Universal Parks & Resorts',
|
||||
'description': 'Movie and television-based theme park in Orlando, Florida.',
|
||||
'opening_date': '1990-06-07',
|
||||
'size_acres': 108,
|
||||
'location': {
|
||||
'street_address': '6000 Universal Blvd',
|
||||
'city': 'Orlando',
|
||||
'state': 'Florida',
|
||||
'country': 'United States',
|
||||
'postal_code': '32819',
|
||||
'latitude': 28.4749,
|
||||
'longitude': -81.4687
|
||||
},
|
||||
'areas': [
|
||||
{'name': 'Production Central', 'description': 'Main entrance area with movie-themed attractions'},
|
||||
{'name': 'New York', 'description': 'Themed after New York City streets'},
|
||||
{'name': 'San Francisco', 'description': 'Themed after San Francisco\'s waterfront'},
|
||||
{'name': 'The Wizarding World of Harry Potter - Diagon Alley', 'description': 'Themed after the Harry Potter series'},
|
||||
{'name': 'Springfield', 'description': 'Themed after The Simpsons hometown'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'name': 'Cedar Point',
|
||||
'company': 'Cedar Fair Entertainment Company',
|
||||
'description': 'Known as the "Roller Coaster Capital of the World".',
|
||||
'opening_date': '1870-06-01',
|
||||
'size_acres': 364,
|
||||
'location': {
|
||||
'street_address': '1 Cedar Point Dr',
|
||||
'city': 'Sandusky',
|
||||
'state': 'Ohio',
|
||||
'country': 'United States',
|
||||
'postal_code': '44870',
|
||||
'latitude': 41.4822,
|
||||
'longitude': -82.6835
|
||||
},
|
||||
'areas': [
|
||||
{'name': 'Frontiertown', 'description': 'Western-themed area with multiple roller coasters'},
|
||||
{'name': 'Millennium Island', 'description': 'Home to the Millennium Force roller coaster'},
|
||||
{'name': 'Cedar Point Shores', 'description': 'Waterpark area'},
|
||||
{'name': 'Top Thrill Dragster', 'description': 'Area surrounding the iconic launched coaster'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'name': 'Silver Dollar City',
|
||||
'company': 'Herschend Family Entertainment',
|
||||
'description': 'An 1880s-themed park featuring over 40 rides and attractions.',
|
||||
'opening_date': '1960-05-01',
|
||||
'size_acres': 61,
|
||||
'location': {
|
||||
'street_address': '399 Silver Dollar City Parkway',
|
||||
'city': 'Branson',
|
||||
'state': 'Missouri',
|
||||
'country': 'United States',
|
||||
'postal_code': '65616',
|
||||
'latitude': 36.668497,
|
||||
'longitude': -93.339074
|
||||
},
|
||||
'areas': [
|
||||
{'name': 'Grand Exposition', 'description': 'Home to many family rides and attractions'},
|
||||
{'name': 'Wildfire', 'description': 'Named after the famous B&M coaster'},
|
||||
{'name': 'Wilson\'s Farm', 'description': 'Farm-themed attractions and dining'},
|
||||
{'name': 'Riverfront', 'description': 'Water-themed attractions area'},
|
||||
{'name': 'The Valley', 'description': 'Home to Time Traveler and other major attractions'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'name': 'Six Flags Magic Mountain',
|
||||
'company': 'Six Flags Entertainment Corporation',
|
||||
'description': 'Known for its world-record 19 roller coasters.',
|
||||
'opening_date': '1971-05-29',
|
||||
'size_acres': 262,
|
||||
'location': {
|
||||
'street_address': '26101 Magic Mountain Pkwy',
|
||||
'city': 'Valencia',
|
||||
'state': 'California',
|
||||
'country': 'United States',
|
||||
'postal_code': '91355',
|
||||
'latitude': 34.4253,
|
||||
'longitude': -118.5971
|
||||
},
|
||||
'areas': [
|
||||
{'name': 'Six Flags Plaza', 'description': 'Main entrance area'},
|
||||
{'name': 'DC Universe', 'description': 'DC Comics themed area'},
|
||||
{'name': 'Screampunk District', 'description': 'Steampunk themed area'},
|
||||
{'name': 'The Underground', 'description': 'Urban themed area'},
|
||||
{'name': 'Goliath Territory', 'description': 'Area surrounding the Goliath hypercoaster'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'name': 'SeaWorld Orlando',
|
||||
'company': 'SeaWorld Parks & Entertainment',
|
||||
'description': 'Marine zoological park combined with thrill rides and shows.',
|
||||
'opening_date': '1973-12-15',
|
||||
'size_acres': 200,
|
||||
'location': {
|
||||
'street_address': '7007 Sea World Dr',
|
||||
'city': 'Orlando',
|
||||
'state': 'Florida',
|
||||
'country': 'United States',
|
||||
'postal_code': '32821',
|
||||
'latitude': 28.4115,
|
||||
'longitude': -81.4617
|
||||
},
|
||||
'areas': [
|
||||
{'name': 'Sea Harbor', 'description': 'Main entrance and shopping area'},
|
||||
{'name': 'Shark Encounter', 'description': 'Shark exhibit and themed area'},
|
||||
{'name': 'Antarctica: Empire of the Penguin', 'description': 'Penguin-themed area'},
|
||||
{'name': 'Manta', 'description': 'Area themed around the Manta flying roller coaster'},
|
||||
{'name': 'Sesame Street Land', 'description': 'Kid-friendly area based on Sesame Street'}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
# Create parks and their areas
|
||||
for park_data in parks_data:
|
||||
company = companies[park_data['company']]
|
||||
park, created = Park.objects.get_or_create(
|
||||
name=park_data['name'],
|
||||
defaults={
|
||||
'description': park_data['description'],
|
||||
'status': 'OPERATING',
|
||||
'opening_date': park_data['opening_date'],
|
||||
'size_acres': park_data['size_acres'],
|
||||
'owner': company
|
||||
}
|
||||
)
|
||||
self.stdout.write(f'{"Created" if created else "Found"} park: {park.name}')
|
||||
|
||||
# Create location for park
|
||||
if created:
|
||||
loc_data = park_data['location']
|
||||
park_content_type = ContentType.objects.get_for_model(Park)
|
||||
Location.objects.create(
|
||||
content_type=park_content_type,
|
||||
object_id=park.id,
|
||||
street_address=loc_data['street_address'],
|
||||
city=loc_data['city'],
|
||||
state=loc_data['state'],
|
||||
country=loc_data['country'],
|
||||
postal_code=loc_data['postal_code'],
|
||||
latitude=loc_data['latitude'],
|
||||
longitude=loc_data['longitude']
|
||||
)
|
||||
|
||||
# Create areas for park
|
||||
for area_data in park_data['areas']:
|
||||
area, created = ParkArea.objects.get_or_create(
|
||||
name=area_data['name'],
|
||||
park=park,
|
||||
defaults={
|
||||
'description': area_data['description']
|
||||
}
|
||||
)
|
||||
self.stdout.write(f'{"Created" if created else "Found"} area: {area.name} in {park.name}')
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('Successfully seeded initial park data'))
|
||||
321
parks/management/commands/seed_ride_data.py
Normal file
321
parks/management/commands/seed_ride_data.py
Normal file
@@ -0,0 +1,321 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone
|
||||
from companies.models import Manufacturer
|
||||
from parks.models import Park
|
||||
from rides.models import Ride, RollerCoasterStats
|
||||
from decimal import Decimal
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Seeds ride data for parks'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Create major ride manufacturers
|
||||
manufacturers_data = [
|
||||
{
|
||||
'name': 'Bolliger & Mabillard',
|
||||
'website': 'https://www.bolligermabillard.com/',
|
||||
'headquarters': 'Monthey, Switzerland',
|
||||
'description': 'Known for their smooth steel roller coasters.'
|
||||
},
|
||||
{
|
||||
'name': 'Rocky Mountain Construction',
|
||||
'website': 'https://www.rockymtnconstruction.com/',
|
||||
'headquarters': 'Hayden, Idaho, USA',
|
||||
'description': 'Specialists in hybrid and steel roller coasters.'
|
||||
},
|
||||
{
|
||||
'name': 'Intamin',
|
||||
'website': 'https://www.intamin.com/',
|
||||
'headquarters': 'Schaan, Liechtenstein',
|
||||
'description': 'Creators of record-breaking roller coasters and rides.'
|
||||
},
|
||||
{
|
||||
'name': 'Vekoma',
|
||||
'website': 'https://www.vekoma.com/',
|
||||
'headquarters': 'Vlodrop, Netherlands',
|
||||
'description': 'Manufacturers of various roller coaster types.'
|
||||
},
|
||||
{
|
||||
'name': 'Mack Rides',
|
||||
'website': 'https://mack-rides.com/',
|
||||
'headquarters': 'Waldkirch, Germany',
|
||||
'description': 'Family-owned manufacturer of roller coasters and attractions.'
|
||||
},
|
||||
{
|
||||
'name': 'Sally Dark Rides',
|
||||
'website': 'https://sallydarkrides.com/',
|
||||
'headquarters': 'Jacksonville, Florida, USA',
|
||||
'description': 'Specialists in dark rides and interactive attractions.'
|
||||
},
|
||||
{
|
||||
'name': 'Zamperla',
|
||||
'website': 'https://www.zamperla.com/',
|
||||
'headquarters': 'Vicenza, Italy',
|
||||
'description': 'Manufacturer of family rides and thrill attractions.'
|
||||
}
|
||||
]
|
||||
|
||||
manufacturers = {}
|
||||
for mfg_data in manufacturers_data:
|
||||
manufacturer, created = Manufacturer.objects.get_or_create(
|
||||
name=mfg_data['name'],
|
||||
defaults=mfg_data
|
||||
)
|
||||
manufacturers[manufacturer.name] = manufacturer
|
||||
self.stdout.write(f'{"Created" if created else "Found"} manufacturer: {manufacturer.name}')
|
||||
|
||||
# Create rides for each park
|
||||
rides_data = [
|
||||
# Silver Dollar City Rides
|
||||
{
|
||||
'park_name': 'Silver Dollar City',
|
||||
'rides': [
|
||||
{
|
||||
'name': 'Time Traveler',
|
||||
'manufacturer': 'Mack Rides',
|
||||
'description': 'The world\'s fastest, steepest, and tallest spinning roller coaster.',
|
||||
'category': 'RC',
|
||||
'opening_date': '2018-03-14',
|
||||
'stats': {
|
||||
'height_ft': 100,
|
||||
'length_ft': 3020,
|
||||
'speed_mph': 50.3,
|
||||
'inversions': 3,
|
||||
'track_material': 'STEEL',
|
||||
'roller_coaster_type': 'SPINNING',
|
||||
'launch_type': 'LSM'
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Wildfire',
|
||||
'manufacturer': 'Bolliger & Mabillard',
|
||||
'description': 'A multi-looping roller coaster with a 155-foot drop.',
|
||||
'category': 'RC',
|
||||
'opening_date': '2001-04-01',
|
||||
'stats': {
|
||||
'height_ft': 155,
|
||||
'length_ft': 3073,
|
||||
'speed_mph': 66,
|
||||
'inversions': 5,
|
||||
'track_material': 'STEEL',
|
||||
'roller_coaster_type': 'SITDOWN',
|
||||
'launch_type': 'CHAIN'
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Fire In The Hole',
|
||||
'manufacturer': 'Sally Dark Rides',
|
||||
'description': 'Indoor coaster featuring special effects and storytelling.',
|
||||
'category': 'DR',
|
||||
'opening_date': '1972-01-01'
|
||||
},
|
||||
{
|
||||
'name': 'American Plunge',
|
||||
'manufacturer': 'Intamin',
|
||||
'description': 'Log flume ride with a 50-foot splashdown.',
|
||||
'category': 'WR',
|
||||
'opening_date': '1981-01-01'
|
||||
}
|
||||
]
|
||||
},
|
||||
# Magic Kingdom Rides
|
||||
{
|
||||
'park_name': 'Magic Kingdom',
|
||||
'rides': [
|
||||
{
|
||||
'name': 'Space Mountain',
|
||||
'manufacturer': 'Vekoma',
|
||||
'description': 'An indoor roller coaster through space.',
|
||||
'category': 'RC',
|
||||
'opening_date': '1975-01-15',
|
||||
'stats': {
|
||||
'height_ft': 180,
|
||||
'length_ft': 3196,
|
||||
'speed_mph': 27,
|
||||
'inversions': 0,
|
||||
'track_material': 'STEEL',
|
||||
'roller_coaster_type': 'SITDOWN',
|
||||
'launch_type': 'CHAIN'
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Haunted Mansion',
|
||||
'manufacturer': 'Sally Dark Rides',
|
||||
'description': 'Classic dark ride through a haunted estate.',
|
||||
'category': 'DR',
|
||||
'opening_date': '1971-10-01'
|
||||
},
|
||||
{
|
||||
'name': 'Mad Tea Party',
|
||||
'manufacturer': 'Zamperla',
|
||||
'description': 'Spinning teacup ride based on Alice in Wonderland.',
|
||||
'category': 'FR',
|
||||
'opening_date': '1971-10-01'
|
||||
},
|
||||
{
|
||||
'name': 'Splash Mountain',
|
||||
'manufacturer': 'Intamin',
|
||||
'description': 'Log flume ride with multiple drops and animatronics.',
|
||||
'category': 'WR',
|
||||
'opening_date': '1992-10-02'
|
||||
}
|
||||
]
|
||||
},
|
||||
# Cedar Point Rides
|
||||
{
|
||||
'park_name': 'Cedar Point',
|
||||
'rides': [
|
||||
{
|
||||
'name': 'Millennium Force',
|
||||
'manufacturer': 'Intamin',
|
||||
'description': 'Former world\'s tallest and fastest complete-circuit roller coaster.',
|
||||
'category': 'RC',
|
||||
'opening_date': '2000-05-13',
|
||||
'stats': {
|
||||
'height_ft': 310,
|
||||
'length_ft': 6595,
|
||||
'speed_mph': 93,
|
||||
'inversions': 0,
|
||||
'track_material': 'STEEL',
|
||||
'roller_coaster_type': 'SITDOWN',
|
||||
'launch_type': 'CABLE'
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Cedar Downs Racing Derby',
|
||||
'manufacturer': 'Zamperla',
|
||||
'description': 'High-speed carousel with racing horses.',
|
||||
'category': 'FR',
|
||||
'opening_date': '1967-01-01'
|
||||
},
|
||||
{
|
||||
'name': 'Snake River Falls',
|
||||
'manufacturer': 'Intamin',
|
||||
'description': 'Shoot-the-Chutes water ride with an 82-foot drop.',
|
||||
'category': 'WR',
|
||||
'opening_date': '1993-05-01'
|
||||
}
|
||||
]
|
||||
},
|
||||
# Universal Studios Florida Rides
|
||||
{
|
||||
'park_name': 'Universal Studios Florida',
|
||||
'rides': [
|
||||
{
|
||||
'name': 'Harry Potter and the Escape from Gringotts',
|
||||
'manufacturer': 'Intamin',
|
||||
'description': 'Indoor steel roller coaster with 3D effects.',
|
||||
'category': 'RC',
|
||||
'opening_date': '2014-07-08',
|
||||
'stats': {
|
||||
'height_ft': 65,
|
||||
'length_ft': 2000,
|
||||
'speed_mph': 50,
|
||||
'inversions': 0,
|
||||
'track_material': 'STEEL',
|
||||
'roller_coaster_type': 'SITDOWN',
|
||||
'launch_type': 'LSM'
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'The Amazing Adventures of Spider-Man',
|
||||
'manufacturer': 'Sally Dark Rides',
|
||||
'description': 'groundbreaking 3D dark ride.',
|
||||
'category': 'DR',
|
||||
'opening_date': '1999-05-28'
|
||||
},
|
||||
{
|
||||
'name': 'Jurassic World VelociCoaster',
|
||||
'manufacturer': 'Intamin',
|
||||
'description': 'Florida\'s fastest and tallest launch coaster.',
|
||||
'category': 'RC',
|
||||
'opening_date': '2021-06-10',
|
||||
'stats': {
|
||||
'height_ft': 155,
|
||||
'length_ft': 4700,
|
||||
'speed_mph': 70,
|
||||
'inversions': 4,
|
||||
'track_material': 'STEEL',
|
||||
'roller_coaster_type': 'SITDOWN',
|
||||
'launch_type': 'LSM'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
# SeaWorld Orlando Rides
|
||||
{
|
||||
'park_name': 'SeaWorld Orlando',
|
||||
'rides': [
|
||||
{
|
||||
'name': 'Mako',
|
||||
'manufacturer': 'Bolliger & Mabillard',
|
||||
'description': 'Orlando\'s tallest, fastest and longest roller coaster.',
|
||||
'category': 'RC',
|
||||
'opening_date': '2016-06-10',
|
||||
'stats': {
|
||||
'height_ft': 200,
|
||||
'length_ft': 4760,
|
||||
'speed_mph': 73,
|
||||
'inversions': 0,
|
||||
'track_material': 'STEEL',
|
||||
'roller_coaster_type': 'SITDOWN',
|
||||
'launch_type': 'CHAIN'
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Journey to Atlantis',
|
||||
'manufacturer': 'Mack Rides',
|
||||
'description': 'Water coaster combining dark ride elements with splashes.',
|
||||
'category': 'WR',
|
||||
'opening_date': '1998-03-01'
|
||||
},
|
||||
{
|
||||
'name': 'Sky Tower',
|
||||
'manufacturer': 'Intamin',
|
||||
'description': 'Rotating observation tower providing views of Orlando.',
|
||||
'category': 'TR',
|
||||
'opening_date': '1973-12-15'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
# Create rides and their stats
|
||||
for park_data in rides_data:
|
||||
try:
|
||||
park = Park.objects.get(name=park_data['park_name'])
|
||||
|
||||
for ride_data in park_data['rides']:
|
||||
manufacturer = manufacturers[ride_data['manufacturer']]
|
||||
|
||||
ride, created = Ride.objects.get_or_create(
|
||||
name=ride_data['name'],
|
||||
park=park,
|
||||
defaults={
|
||||
'description': ride_data['description'],
|
||||
'category': ride_data['category'],
|
||||
'manufacturer': manufacturer,
|
||||
'opening_date': ride_data['opening_date'],
|
||||
'status': 'OPERATING'
|
||||
}
|
||||
)
|
||||
self.stdout.write(f'{"Created" if created else "Found"} ride: {ride.name}')
|
||||
|
||||
if created and ride_data.get('stats'):
|
||||
stats = ride_data['stats']
|
||||
RollerCoasterStats.objects.create(
|
||||
ride=ride,
|
||||
height_ft=stats['height_ft'],
|
||||
length_ft=stats['length_ft'],
|
||||
speed_mph=stats['speed_mph'],
|
||||
inversions=stats['inversions'],
|
||||
track_material=stats['track_material'],
|
||||
roller_coaster_type=stats['roller_coaster_type'],
|
||||
launch_type=stats['launch_type']
|
||||
)
|
||||
self.stdout.write(f'Created stats for: {ride.name}')
|
||||
|
||||
except Park.DoesNotExist:
|
||||
self.stdout.write(self.style.WARNING(f'Park not found: {park_data["park_name"]}'))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('Successfully seeded ride data'))
|
||||
@@ -1,5 +1,9 @@
|
||||
from django.db import migrations, models
|
||||
# Generated by Django 5.1.3 on 2024-11-12 18:07
|
||||
|
||||
import django.db.models.deletion
|
||||
import simple_history.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -7,57 +11,228 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('companies', '0001_initial'),
|
||||
("companies", "0001_initial"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Park',
|
||||
name="HistoricalPark",
|
||||
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)),
|
||||
('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(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')),
|
||||
("id", models.BigIntegerField(blank=True, db_index=True)),
|
||||
("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
|
||||
),
|
||||
),
|
||||
("ride_count", models.IntegerField(blank=True, null=True)),
|
||||
("coaster_count", models.IntegerField(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,
|
||||
),
|
||||
),
|
||||
(
|
||||
"owner",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
to="companies.company",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
"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(primary_key=True, serialize=False)),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("slug", models.SlugField(max_length=255, unique=True)),
|
||||
("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
|
||||
),
|
||||
),
|
||||
("ride_count", models.IntegerField(blank=True, null=True)),
|
||||
("coaster_count", models.IntegerField(blank=True, null=True)),
|
||||
("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",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["name"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ParkArea',
|
||||
name="HistoricalParkArea",
|
||||
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)),
|
||||
('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(auto_now_add=True, null=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('park', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='areas', to='parks.park')),
|
||||
("id", models.BigIntegerField(blank=True, db_index=True)),
|
||||
("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={
|
||||
'ordering': ['name'],
|
||||
'unique_together': {('park', 'slug')},
|
||||
"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=[
|
||||
("id", models.BigAutoField(primary_key=True, serialize=False)),
|
||||
("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(auto_now_add=True, null=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"park",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="areas",
|
||||
to="parks.park",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["name"],
|
||||
"unique_together": {("park", "slug")},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@@ -1,55 +0,0 @@
|
||||
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)',
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,101 +0,0 @@
|
||||
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,
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,58 +0,0 @@
|
||||
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
|
||||
),
|
||||
]
|
||||
@@ -1,75 +0,0 @@
|
||||
# Generated by Django 5.1.2 on 2024-11-03 19:59
|
||||
|
||||
import django.core.validators
|
||||
from decimal import Decimal
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("parks", "0005_normalize_coordinates"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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=[
|
||||
django.core.validators.MinValueValidator(Decimal("-90")),
|
||||
django.core.validators.MaxValueValidator(Decimal("90")),
|
||||
],
|
||||
),
|
||||
),
|
||||
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=[
|
||||
django.core.validators.MinValueValidator(Decimal("-180")),
|
||||
django.core.validators.MaxValueValidator(Decimal("180")),
|
||||
],
|
||||
),
|
||||
),
|
||||
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=[
|
||||
django.core.validators.MinValueValidator(Decimal("-90")),
|
||||
django.core.validators.MaxValueValidator(Decimal("90")),
|
||||
],
|
||||
),
|
||||
),
|
||||
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=[
|
||||
django.core.validators.MinValueValidator(Decimal("-180")),
|
||||
django.core.validators.MaxValueValidator(Decimal("180")),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
# Generated by Django 5.1.2 on 2024-11-03 20:26
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("parks", "0006_alter_historicalpark_latitude_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="historicalparkarea",
|
||||
name="history_user",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="historicalparkarea",
|
||||
name="park",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="HistoricalPark",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="HistoricalParkArea",
|
||||
),
|
||||
]
|
||||
@@ -1,209 +0,0 @@
|
||||
# Generated by Django 5.1.2 on 2024-11-03 20:38
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import history_tracking.mixins
|
||||
import simple_history.models
|
||||
from decimal import Decimal
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("companies", "0004_add_total_parks"),
|
||||
("parks", "0007_remove_historicalparkarea_history_user_and_more"),
|
||||
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,
|
||||
),
|
||||
),
|
||||
(
|
||||
"latitude",
|
||||
models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=6,
|
||||
help_text="Latitude coordinate (-90 to 90)",
|
||||
max_digits=9,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(Decimal("-90")),
|
||||
django.core.validators.MaxValueValidator(Decimal("90")),
|
||||
],
|
||||
),
|
||||
),
|
||||
(
|
||||
"longitude",
|
||||
models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=6,
|
||||
help_text="Longitude coordinate (-180 to 180)",
|
||||
max_digits=10,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(Decimal("-180")),
|
||||
django.core.validators.MaxValueValidator(Decimal("180")),
|
||||
],
|
||||
),
|
||||
),
|
||||
("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)),
|
||||
("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)),
|
||||
(
|
||||
"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=(
|
||||
history_tracking.mixins.HistoricalChangeMixin,
|
||||
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=(
|
||||
history_tracking.mixins.HistoricalChangeMixin,
|
||||
simple_history.models.HistoricalChanges,
|
||||
models.Model,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,83 +0,0 @@
|
||||
# Generated by Django 5.1.2 on 2024-11-04 22:21
|
||||
|
||||
from django.db import migrations, transaction
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
def forwards_func(apps, schema_editor):
|
||||
"""Move park location data to Location model"""
|
||||
Park = apps.get_model("parks", "Park")
|
||||
Location = apps.get_model("location", "Location")
|
||||
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
# Get or create content type for Park model
|
||||
park_content_type, created = ContentType.objects.db_manager(db_alias).get_or_create(
|
||||
app_label='parks',
|
||||
model='park'
|
||||
)
|
||||
|
||||
# Move location data for each park
|
||||
with transaction.atomic():
|
||||
for park in Park.objects.using(db_alias).all():
|
||||
# Only create Location if park has coordinate data
|
||||
if park.latitude is not None and park.longitude is not None:
|
||||
Location.objects.using(db_alias).create(
|
||||
content_type=park_content_type,
|
||||
object_id=park.id,
|
||||
name=park.name,
|
||||
location_type='park',
|
||||
latitude=park.latitude,
|
||||
longitude=park.longitude,
|
||||
street_address=park.street_address,
|
||||
city=park.city,
|
||||
state=park.state,
|
||||
country=park.country,
|
||||
postal_code=park.postal_code
|
||||
)
|
||||
|
||||
def reverse_func(apps, schema_editor):
|
||||
"""Move location data back to Park model"""
|
||||
Park = apps.get_model("parks", "Park")
|
||||
Location = apps.get_model("location", "Location")
|
||||
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
# Get or create content type for Park model
|
||||
park_content_type, created = ContentType.objects.db_manager(db_alias).get_or_create(
|
||||
app_label='parks',
|
||||
model='park'
|
||||
)
|
||||
|
||||
# Move location data back to each park
|
||||
with transaction.atomic():
|
||||
locations = Location.objects.using(db_alias).filter(
|
||||
content_type=park_content_type
|
||||
)
|
||||
for location in locations:
|
||||
try:
|
||||
park = Park.objects.using(db_alias).get(id=location.object_id)
|
||||
park.latitude = location.latitude
|
||||
park.longitude = location.longitude
|
||||
park.street_address = location.street_address
|
||||
park.city = location.city
|
||||
park.state = location.state
|
||||
park.country = location.country
|
||||
park.postal_code = location.postal_code
|
||||
park.save()
|
||||
except Park.DoesNotExist:
|
||||
continue
|
||||
|
||||
# Delete all park locations
|
||||
locations.delete()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('parks', '0008_historicalpark_historicalparkarea'),
|
||||
('location', '0005_convert_coordinates_to_points'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(forwards_func, reverse_func, atomic=True),
|
||||
]
|
||||
@@ -1,69 +0,0 @@
|
||||
# Generated by Django 5.1.2 on 2024-11-04 22:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("parks", "0009_migrate_to_location_model"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="historicalpark",
|
||||
name="latitude",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="historicalpark",
|
||||
name="longitude",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="historicalpark",
|
||||
name="street_address",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="historicalpark",
|
||||
name="city",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="historicalpark",
|
||||
name="state",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="historicalpark",
|
||||
name="country",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="historicalpark",
|
||||
name="postal_code",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="park",
|
||||
name="latitude",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="park",
|
||||
name="longitude",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="park",
|
||||
name="street_address",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="park",
|
||||
name="city",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="park",
|
||||
name="state",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="park",
|
||||
name="country",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="park",
|
||||
name="postal_code",
|
||||
),
|
||||
]
|
||||
@@ -4,14 +4,16 @@ from django.utils.text import slugify
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from decimal import Decimal, ROUND_DOWN, InvalidOperation
|
||||
from typing import Tuple, Optional, Any
|
||||
from simple_history.models import HistoricalRecords
|
||||
from typing import Tuple, Optional, Any, TYPE_CHECKING
|
||||
|
||||
from companies.models import Company
|
||||
from media.models import Photo
|
||||
from history_tracking.models import HistoricalModel
|
||||
from location.models import Location
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from rides.models import Ride
|
||||
|
||||
|
||||
class Park(HistoricalModel):
|
||||
id: int # Type hint for Django's automatic id field
|
||||
@@ -55,6 +57,8 @@ class Park(HistoricalModel):
|
||||
Company, on_delete=models.SET_NULL, null=True, blank=True, related_name="parks"
|
||||
)
|
||||
photos = GenericRelation(Photo, related_query_name="park")
|
||||
areas: models.Manager['ParkArea'] # Type hint for reverse relation
|
||||
rides: models.Manager['Ride'] # Type hint for reverse relation from rides app
|
||||
|
||||
# Metadata
|
||||
created_at = models.DateTimeField(auto_now_add=True, null=True)
|
||||
|
||||
@@ -8,13 +8,23 @@ urlpatterns = [
|
||||
# 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"),
|
||||
|
||||
# Add park button endpoint (moved before park detail pattern)
|
||||
path("add-park-button/", views.add_park_button, name="add_park_button"),
|
||||
|
||||
# Location search endpoints
|
||||
path("search/location/", views.location_search, name="location_search"),
|
||||
path("search/reverse-geocode/", views.reverse_geocode, name="reverse_geocode"),
|
||||
|
||||
# Areas and search endpoints for HTMX
|
||||
path("areas/", views.get_park_areas, name="get_park_areas"),
|
||||
path("search/", views.search_parks, name="search_parks"),
|
||||
|
||||
# Park detail and related views
|
||||
path("<slug:slug>/", views.ParkDetailView.as_view(), name="park_detail"),
|
||||
path("<slug:slug>/edit/", views.ParkUpdateView.as_view(), name="park_update"),
|
||||
path("<slug:slug>/actions/", views.park_actions, name="park_actions"),
|
||||
|
||||
# Area views
|
||||
path("<slug:park_slug>/areas/<slug:area_slug>/", views.ParkAreaDetailView.as_view(), name="area_detail"),
|
||||
|
||||
@@ -26,6 +36,6 @@ urlpatterns = [
|
||||
path("<slug:park_slug>/transports/", ParkSingleCategoryListView.as_view(), {'category': 'TR'}, name="park_transports"),
|
||||
path("<slug:park_slug>/others/", ParkSingleCategoryListView.as_view(), {'category': 'OT'}, name="park_others"),
|
||||
|
||||
# Include rides URLs
|
||||
# Include rides URLs with park_slug
|
||||
path("<slug:park_slug>/rides/", include("rides.urls", namespace="rides")),
|
||||
]
|
||||
|
||||
274
parks/views.py
274
parks/views.py
@@ -1,13 +1,15 @@
|
||||
from decimal import Decimal, ROUND_DOWN, InvalidOperation
|
||||
from typing import Any, Optional, cast, Type
|
||||
from django.views.generic import DetailView, ListView, CreateView, UpdateView
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.urls import reverse
|
||||
from django.db.models import Q, Avg, Count
|
||||
from django.db.models import Q, Avg, Count, QuerySet, Model
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib import messages
|
||||
from django.http import JsonResponse, HttpResponseRedirect, HttpResponse
|
||||
from django.http import JsonResponse, HttpResponseRedirect, HttpResponse, HttpRequest
|
||||
import requests
|
||||
from .models import Park, ParkArea
|
||||
from .forms import ParkForm
|
||||
@@ -17,11 +19,49 @@ from moderation.mixins import EditSubmissionMixin, PhotoSubmissionMixin, History
|
||||
from moderation.models import EditSubmission
|
||||
from media.models import Photo
|
||||
from location.models import Location
|
||||
from reviews.models import Review # Import the Review model
|
||||
from analytics.models import PageView # Import PageView for tracking views
|
||||
from reviews.models import Review
|
||||
from analytics.models import PageView
|
||||
|
||||
|
||||
def location_search(request):
|
||||
def park_actions(request: HttpRequest, slug: str) -> HttpResponse:
|
||||
"""Return the park actions partial template"""
|
||||
park = get_object_or_404(Park, slug=slug)
|
||||
return render(request, "parks/partials/park_actions.html", {"park": park})
|
||||
|
||||
|
||||
def get_park_areas(request: HttpRequest) -> HttpResponse:
|
||||
"""Return park areas as options for a select element"""
|
||||
park_id = request.GET.get('park')
|
||||
if not park_id:
|
||||
return HttpResponse('<option value="">Select a park first</option>')
|
||||
|
||||
try:
|
||||
park = Park.objects.get(id=park_id)
|
||||
areas = park.areas.all()
|
||||
options = ['<option value="">No specific area</option>']
|
||||
options.extend([
|
||||
f'<option value="{area.id}">{area.name}</option>'
|
||||
for area in areas
|
||||
])
|
||||
return HttpResponse('\n'.join(options))
|
||||
except Park.DoesNotExist:
|
||||
return HttpResponse('<option value="">Invalid park selected</option>')
|
||||
|
||||
|
||||
def search_parks(request: HttpRequest) -> HttpResponse:
|
||||
"""Search parks and return results for HTMX"""
|
||||
query = request.GET.get('q', '').strip()
|
||||
|
||||
# If no query, show first 10 parks
|
||||
if not query:
|
||||
parks = Park.objects.all().order_by('name')[:10]
|
||||
else:
|
||||
parks = Park.objects.filter(name__icontains=query).order_by('name')[:10]
|
||||
|
||||
return render(request, "parks/partials/park_search_results.html", {"parks": parks})
|
||||
|
||||
|
||||
def location_search(request: HttpRequest) -> JsonResponse:
|
||||
"""Search for locations using OpenStreetMap Nominatim API"""
|
||||
query = request.GET.get("q", "")
|
||||
if not query:
|
||||
@@ -34,8 +74,8 @@ def location_search(request):
|
||||
"q": query,
|
||||
"format": "json",
|
||||
"addressdetails": 1,
|
||||
"namedetails": 1, # Include name tags
|
||||
"accept-language": "en", # Prefer English results
|
||||
"namedetails": 1,
|
||||
"accept-language": "en",
|
||||
"limit": 10,
|
||||
},
|
||||
headers={"User-Agent": "ThrillWiki/1.0"},
|
||||
@@ -43,16 +83,18 @@ def location_search(request):
|
||||
|
||||
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]
|
||||
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})
|
||||
|
||||
return JsonResponse({"results": []})
|
||||
|
||||
|
||||
def reverse_geocode(request):
|
||||
def reverse_geocode(request: HttpRequest) -> JsonResponse:
|
||||
"""Reverse geocode coordinates using OpenStreetMap Nominatim API"""
|
||||
try:
|
||||
lat = Decimal(request.GET.get("lat", ""))
|
||||
@@ -63,17 +105,18 @@ def reverse_geocode(request):
|
||||
if not lat or not lon:
|
||||
return JsonResponse({"error": "Missing coordinates"}, status=400)
|
||||
|
||||
# Normalize coordinates before geocoding
|
||||
lat = lat.quantize(Decimal('0.000001'), rounding=ROUND_DOWN)
|
||||
lon = lon.quantize(Decimal('0.000001'), rounding=ROUND_DOWN)
|
||||
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)
|
||||
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)
|
||||
return JsonResponse(
|
||||
{"error": "Longitude must be between -180 and 180"}, status=400
|
||||
)
|
||||
|
||||
# Call Nominatim API
|
||||
response = requests.get(
|
||||
"https://nominatim.openstreetmap.org/reverse",
|
||||
params={
|
||||
@@ -81,30 +124,36 @@ def reverse_geocode(request):
|
||||
"lon": str(lon),
|
||||
"format": "json",
|
||||
"addressdetails": 1,
|
||||
"namedetails": 1, # Include name tags
|
||||
"accept-language": "en", # Prefer English results
|
||||
"namedetails": 1,
|
||||
"accept-language": "en",
|
||||
},
|
||||
headers={"User-Agent": "ThrillWiki/1.0"},
|
||||
)
|
||||
|
||||
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:
|
||||
if normalized_result["lat"] is None or normalized_result["lon"] is None:
|
||||
return JsonResponse({"error": "Invalid coordinates"}, status=400)
|
||||
return JsonResponse(normalized_result)
|
||||
|
||||
return JsonResponse({"error": "Geocoding failed"}, status=500)
|
||||
|
||||
|
||||
def add_park_button(request: HttpRequest) -> HttpResponse:
|
||||
"""Return the add park button partial template"""
|
||||
return render(request, "parks/partials/add_park_button.html")
|
||||
|
||||
|
||||
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").prefetch_related("photos", "location")
|
||||
def get_queryset(self) -> QuerySet[Park]:
|
||||
queryset = Park.objects.select_related("owner").prefetch_related(
|
||||
"photos", "location"
|
||||
)
|
||||
|
||||
search = self.request.GET.get("search", "").strip()
|
||||
country = self.request.GET.get("country", "").strip()
|
||||
@@ -114,10 +163,10 @@ class ParkListView(ListView):
|
||||
|
||||
if search:
|
||||
queryset = queryset.filter(
|
||||
Q(name__icontains=search) |
|
||||
Q(location__city__icontains=search) |
|
||||
Q(location__state__icontains=search) |
|
||||
Q(location__country__icontains=search)
|
||||
Q(name__icontains=search)
|
||||
| Q(location__city__icontains=search)
|
||||
| Q(location__state__icontains=search)
|
||||
| Q(location__country__icontains=search)
|
||||
)
|
||||
|
||||
if country:
|
||||
@@ -132,16 +181,14 @@ class ParkListView(ListView):
|
||||
if statuses:
|
||||
queryset = queryset.filter(status__in=statuses)
|
||||
|
||||
# Annotate with ride count, coaster count, and average review rating
|
||||
queryset = queryset.annotate(
|
||||
ride_count=Count('rides'),
|
||||
coaster_count=Count('rides', filter=Q(rides__type='coaster')),
|
||||
average_rating=Avg('reviews__rating')
|
||||
total_rides=Count("rides"),
|
||||
total_coasters=Count("rides", filter=Q(rides__category="RC")),
|
||||
)
|
||||
|
||||
return queryset.distinct()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["current_filters"] = {
|
||||
"search": self.request.GET.get("search", ""),
|
||||
@@ -152,10 +199,8 @@ class ParkListView(ListView):
|
||||
}
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Check if this is an HTMX request
|
||||
if hasattr(request, 'htmx') and getattr(request, 'htmx', False):
|
||||
# If it is, return just the parks list partial
|
||||
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
if hasattr(request, "htmx") and getattr(request, "htmx", False):
|
||||
self.template_name = "parks/partials/park_list.html"
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
@@ -171,44 +216,43 @@ class ParkDetailView(
|
||||
template_name = "parks/park_detail.html"
|
||||
context_object_name = "park"
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
def get_object(self, queryset: Optional[QuerySet[Park]] = None) -> Park:
|
||||
if queryset is None:
|
||||
queryset = self.get_queryset()
|
||||
slug = self.kwargs.get(self.slug_url_kwarg)
|
||||
# Try to get by current or historical slug
|
||||
return Park.get_by_slug(slug)[0]
|
||||
if slug is None:
|
||||
raise ObjectDoesNotExist("No slug provided")
|
||||
park, _ = Park.get_by_slug(slug)
|
||||
return park
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().prefetch_related(
|
||||
'rides',
|
||||
'rides__manufacturer',
|
||||
'photos',
|
||||
'areas',
|
||||
'location'
|
||||
def get_queryset(self) -> QuerySet[Park]:
|
||||
return cast(
|
||||
QuerySet[Park],
|
||||
super()
|
||||
.get_queryset()
|
||||
.prefetch_related(
|
||||
"rides", "rides__manufacturer", "photos", "areas", "location"
|
||||
),
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs)
|
||||
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'
|
||||
)
|
||||
|
||||
# Check if the user has reviewed the park
|
||||
park = cast(Park, self.object)
|
||||
context["areas"] = park.areas.all()
|
||||
context["rides"] = park.rides.all().order_by("-status", "name")
|
||||
|
||||
if self.request.user.is_authenticated:
|
||||
context["has_reviewed"] = Review.objects.filter(
|
||||
user=self.request.user,
|
||||
content_type=ContentType.objects.get_for_model(Park),
|
||||
object_id=self.object.id
|
||||
object_id=park.id,
|
||||
).exists()
|
||||
else:
|
||||
context["has_reviewed"] = False
|
||||
|
||||
return context
|
||||
|
||||
def get_redirect_url_pattern(self):
|
||||
def get_redirect_url_pattern(self) -> str:
|
||||
return "parks:park_detail"
|
||||
|
||||
|
||||
@@ -217,38 +261,36 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
|
||||
form_class = ParkForm
|
||||
template_name = "parks/park_form.html"
|
||||
|
||||
def prepare_changes_data(self, cleaned_data):
|
||||
def prepare_changes_data(self, cleaned_data: dict[str, Any]) -> dict[str, Any]:
|
||||
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 normalize_coordinates(self, form):
|
||||
def normalize_coordinates(self, form: ParkForm) -> None:
|
||||
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)
|
||||
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)
|
||||
form.cleaned_data["longitude"] = lon.quantize(
|
||||
Decimal("0.000001"), rounding=ROUND_DOWN
|
||||
)
|
||||
|
||||
def form_valid(self, form):
|
||||
# Normalize coordinates before saving
|
||||
def form_valid(self, form: ParkForm) -> HttpResponse:
|
||||
self.normalize_coordinates(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),
|
||||
@@ -258,8 +300,9 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
|
||||
source=self.request.POST.get("source", ""),
|
||||
)
|
||||
|
||||
# If user is moderator or above, auto-approve
|
||||
if hasattr(self.request.user, 'role') and getattr(self.request.user, 'role', None) in ["MODERATOR", "ADMIN", "SUPERUSER"]:
|
||||
if hasattr(self.request.user, "role") and getattr(
|
||||
self.request.user, "role", None
|
||||
) in ["MODERATOR", "ADMIN", "SUPERUSER"]:
|
||||
try:
|
||||
self.object = form.save()
|
||||
submission.object_id = self.object.id
|
||||
@@ -267,23 +310,23 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
|
||||
submission.handled_by = self.request.user
|
||||
submission.save()
|
||||
|
||||
# Create Location record
|
||||
if form.cleaned_data.get("latitude") and form.cleaned_data.get("longitude"):
|
||||
if form.cleaned_data.get("latitude") and form.cleaned_data.get(
|
||||
"longitude"
|
||||
):
|
||||
Location.objects.create(
|
||||
content_type=ContentType.objects.get_for_model(Park),
|
||||
object_id=self.object.id,
|
||||
name=self.object.name,
|
||||
location_type='park',
|
||||
location_type="park",
|
||||
latitude=form.cleaned_data["latitude"],
|
||||
longitude=form.cleaned_data["longitude"],
|
||||
street_address=form.cleaned_data.get("street_address", ""),
|
||||
city=form.cleaned_data.get("city", ""),
|
||||
state=form.cleaned_data.get("state", ""),
|
||||
country=form.cleaned_data.get("country", ""),
|
||||
postal_code=form.cleaned_data.get("postal_code", "")
|
||||
postal_code=form.cleaned_data.get("postal_code", ""),
|
||||
)
|
||||
|
||||
# Handle photo uploads
|
||||
photos = self.request.FILES.getlist("photos")
|
||||
for photo_file in photos:
|
||||
try:
|
||||
@@ -319,7 +362,7 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
|
||||
)
|
||||
return HttpResponseRedirect(reverse("parks:park_list"))
|
||||
|
||||
def form_invalid(self, form):
|
||||
def form_invalid(self, form: ParkForm) -> HttpResponse:
|
||||
messages.error(
|
||||
self.request,
|
||||
"Please correct the errors below. Required fields are marked with an asterisk (*).",
|
||||
@@ -329,7 +372,7 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
|
||||
messages.error(self.request, f"{field}: {error}")
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
def get_success_url(self) -> str:
|
||||
return reverse("parks:park_detail", kwargs={"slug": self.object.slug})
|
||||
|
||||
|
||||
@@ -338,43 +381,41 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
|
||||
form_class = ParkForm
|
||||
template_name = "parks/park_form.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["is_edit"] = True
|
||||
return context
|
||||
|
||||
def prepare_changes_data(self, cleaned_data):
|
||||
def prepare_changes_data(self, cleaned_data: dict[str, Any]) -> dict[str, Any]:
|
||||
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 normalize_coordinates(self, form):
|
||||
def normalize_coordinates(self, form: ParkForm) -> None:
|
||||
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)
|
||||
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)
|
||||
form.cleaned_data["longitude"] = lon.quantize(
|
||||
Decimal("0.000001"), rounding=ROUND_DOWN
|
||||
)
|
||||
|
||||
def form_valid(self, form):
|
||||
# Normalize coordinates before saving
|
||||
def form_valid(self, form: ParkForm) -> HttpResponse:
|
||||
self.normalize_coordinates(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),
|
||||
@@ -385,25 +426,25 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
|
||||
source=self.request.POST.get("source", ""),
|
||||
)
|
||||
|
||||
# If user is moderator or above, auto-approve
|
||||
if hasattr(self.request.user, 'role') and getattr(self.request.user, 'role', None) in ["MODERATOR", "ADMIN", "SUPERUSER"]:
|
||||
if hasattr(self.request.user, "role") and getattr(
|
||||
self.request.user, "role", None
|
||||
) in ["MODERATOR", "ADMIN", "SUPERUSER"]:
|
||||
try:
|
||||
self.object = form.save()
|
||||
submission.status = "APPROVED"
|
||||
submission.handled_by = self.request.user
|
||||
submission.save()
|
||||
|
||||
# Update or create Location record
|
||||
location_data = {
|
||||
'name': self.object.name,
|
||||
'location_type': 'park',
|
||||
'latitude': form.cleaned_data.get("latitude"),
|
||||
'longitude': form.cleaned_data.get("longitude"),
|
||||
'street_address': form.cleaned_data.get("street_address", ""),
|
||||
'city': form.cleaned_data.get("city", ""),
|
||||
'state': form.cleaned_data.get("state", ""),
|
||||
'country': form.cleaned_data.get("country", ""),
|
||||
'postal_code': form.cleaned_data.get("postal_code", "")
|
||||
"name": self.object.name,
|
||||
"location_type": "park",
|
||||
"latitude": form.cleaned_data.get("latitude"),
|
||||
"longitude": form.cleaned_data.get("longitude"),
|
||||
"street_address": form.cleaned_data.get("street_address", ""),
|
||||
"city": form.cleaned_data.get("city", ""),
|
||||
"state": form.cleaned_data.get("state", ""),
|
||||
"country": form.cleaned_data.get("country", ""),
|
||||
"postal_code": form.cleaned_data.get("postal_code", ""),
|
||||
}
|
||||
|
||||
if self.object.location.exists():
|
||||
@@ -415,10 +456,9 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
|
||||
Location.objects.create(
|
||||
content_type=ContentType.objects.get_for_model(Park),
|
||||
object_id=self.object.id,
|
||||
**location_data
|
||||
**location_data,
|
||||
)
|
||||
|
||||
# Handle photo uploads
|
||||
photos = self.request.FILES.getlist("photos")
|
||||
uploaded_count = 0
|
||||
for photo_file in photos:
|
||||
@@ -458,7 +498,7 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
|
||||
reverse("parks:park_detail", kwargs={"slug": self.object.slug})
|
||||
)
|
||||
|
||||
def form_invalid(self, form):
|
||||
def form_invalid(self, form: ParkForm) -> HttpResponse:
|
||||
messages.error(
|
||||
self.request,
|
||||
"Please correct the errors below. Required fields are marked with an asterisk (*).",
|
||||
@@ -468,7 +508,7 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
|
||||
messages.error(self.request, f"{field}: {error}")
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
def get_success_url(self) -> str:
|
||||
return reverse("parks:park_detail", kwargs={"slug": self.object.slug})
|
||||
|
||||
|
||||
@@ -484,23 +524,25 @@ class ParkAreaDetailView(
|
||||
context_object_name = "area"
|
||||
slug_url_kwarg = "area_slug"
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
def get_object(self, queryset: Optional[QuerySet[ParkArea]] = None) -> ParkArea:
|
||||
if queryset is None:
|
||||
queryset = self.get_queryset()
|
||||
park_slug = self.kwargs.get("park_slug")
|
||||
area_slug = self.kwargs.get("area_slug")
|
||||
# Try to get by current or historical slug
|
||||
obj, is_old_slug = ParkArea.get_by_slug(area_slug)
|
||||
if obj.park.slug != park_slug:
|
||||
raise self.model.DoesNotExist("Park slug doesn't match")
|
||||
return obj
|
||||
if park_slug is None or area_slug is None:
|
||||
raise ObjectDoesNotExist("Missing slug")
|
||||
area, _ = ParkArea.get_by_slug(area_slug)
|
||||
if area.park.slug != park_slug:
|
||||
raise ObjectDoesNotExist("Park slug doesn't match")
|
||||
return area
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs)
|
||||
return context
|
||||
|
||||
def get_redirect_url_pattern(self):
|
||||
def get_redirect_url_pattern(self) -> str:
|
||||
return "parks:park_detail"
|
||||
|
||||
def get_redirect_url_kwargs(self):
|
||||
return {"park_slug": self.object.park.slug, "area_slug": self.object.slug}
|
||||
def get_redirect_url_kwargs(self) -> dict[str, str]:
|
||||
area = cast(ParkArea, self.object)
|
||||
return {"park_slug": area.park.slug, "area_slug": area.slug}
|
||||
|
||||
Reference in New Issue
Block a user