From 78248aa8927b2511ee52147974bffe65d6db9ae7 Mon Sep 17 00:00:00 2001 From: pacnpal <183241239+pacnpal@users.noreply.github.com> Date: Wed, 20 Aug 2025 10:16:21 -0400 Subject: [PATCH] Add management command to seed comprehensive sample data for ThrillWiki application - Implemented cleanup of existing sample data to avoid conflicts. - Created functions to generate companies, parks, rides, park areas, and reviews. - Ensured proper relationships between models during data creation. - Added logging for better tracking of data seeding process. - Included checks for required database tables before seeding. --- .gitignore | 4 +- .../management/commands/create_sample_data.py | 317 +++++ parks/management/commands/seed_sample_data.py | 1072 +++++++++++++++++ parks/models/companies.py | 27 +- parks/urls.py | 47 +- static/css/tailwind.css | 6 +- templates/parks/park_detail.html | 3 +- templates/parks/partials/park_list.html | 6 +- templates/search_results.html | 3 +- thrillwiki/settings.py | 25 +- thrillwiki/urls.py | 43 +- 11 files changed, 1489 insertions(+), 64 deletions(-) create mode 100644 parks/management/commands/create_sample_data.py create mode 100644 parks/management/commands/seed_sample_data.py diff --git a/.gitignore b/.gitignore index bfc55bab..53c7fb26 100644 --- a/.gitignore +++ b/.gitignore @@ -387,4 +387,6 @@ Temporary Items ***REMOVED***.webhook .github-token logs/ -profiles \ No newline at end of file +profiles +.thrillwiki-github-token +.thrillwiki-template-config \ No newline at end of file diff --git a/parks/management/commands/create_sample_data.py b/parks/management/commands/create_sample_data.py new file mode 100644 index 00000000..c062deec --- /dev/null +++ b/parks/management/commands/create_sample_data.py @@ -0,0 +1,317 @@ +from django.core.management.base import BaseCommand +from django.utils import timezone +from django.db import transaction +from datetime import date, timedelta +import random +from decimal import Decimal + +# Import models from both apps +from parks.models import Company as ParkCompany, Park, ParkArea, ParkReview +from parks.models.location import ParkLocation +from rides.models import Company as RideCompany, Ride, RideModel, RideReview, RollerCoasterStats +from accounts.models import User + + +class Command(BaseCommand): + help = 'Creates comprehensive sample data for the ThrillWiki theme park application' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.created_companies = {} + self.created_parks = {} + self.created_rides = {} + + def handle(self, *args, **options): + self.stdout.write('Starting sample data creation...') + + try: + with transaction.atomic(): + self.create_companies() + self.create_parks() + self.create_ride_models() + self.create_rides() + self.create_park_areas() + self.create_reviews() + + self.stdout.write(self.style.SUCCESS('Successfully created comprehensive sample data!')) + self.print_summary() + + except Exception as e: + self.stdout.write(self.style.ERROR(f'Error creating sample data: {e}')) + raise + + def create_companies(self): + """Create companies with different roles following entity relationship rules""" + self.stdout.write('Creating companies...') + + # Park operators and property owners (using parks.models.Company) + park_operators_data = [ + { + 'name': 'The Walt Disney Company', + 'slug': 'walt-disney-company', + 'roles': ['OPERATOR', 'PROPERTY_OWNER'], + 'description': 'World\'s largest entertainment company and theme park operator.', + 'website': 'https://www.disney.com/', + 'founded_year': 1923, + }, + { + 'name': 'Universal Parks & Resorts', + 'slug': 'universal-parks-resorts', + 'roles': ['OPERATOR', 'PROPERTY_OWNER'], + 'description': 'Division of Comcast NBCUniversal, operating major theme parks worldwide.', + 'website': 'https://www.universalparks.com/', + 'founded_year': 1964, + }, + { + 'name': 'Six Flags Entertainment Corporation', + 'slug': 'six-flags-entertainment', + 'roles': ['OPERATOR', 'PROPERTY_OWNER'], + 'description': 'World\'s largest regional theme park company.', + 'website': 'https://www.sixflags.com/', + 'founded_year': 1961, + }, + { + 'name': 'Cedar Fair Entertainment Company', + 'slug': 'cedar-fair-entertainment', + 'roles': ['OPERATOR', 'PROPERTY_OWNER'], + 'description': 'One of North America\'s largest operators of regional amusement parks.', + 'website': 'https://www.cedarfair.com/', + 'founded_year': 1983, + }, + { + 'name': 'Herschend Family Entertainment', + 'slug': 'herschend-family-entertainment', + 'roles': ['OPERATOR', 'PROPERTY_OWNER'], + 'description': 'Largest family-owned themed attractions corporation in the United States.', + 'website': 'https://www.hfecorp.com/', + 'founded_year': 1950, + }, + { + 'name': 'SeaWorld Parks & Entertainment', + 'slug': 'seaworld-parks-entertainment', + 'roles': ['OPERATOR', 'PROPERTY_OWNER'], + 'description': 'Theme park and entertainment company focusing on nature-based themes.', + 'website': 'https://www.seaworldentertainment.com/', + 'founded_year': 1959, + }, + { + 'name': 'Merlin Entertainments', + 'slug': 'merlin-entertainments', + 'roles': ['OPERATOR', 'PROPERTY_OWNER'], + 'description': 'European theme park operator with LEGOLAND and Madame Tussauds brands.', + 'website': 'https://www.merlinentertainments.com/', + 'founded_year': 1998, + }, + ] + + for company_data in park_operators_data: + company, created = ParkCompany.objects.get_or_create( + slug=company_data['slug'], + defaults=company_data + ) + self.created_companies[company.slug] = company + self.stdout.write(f' {"Created" if created else "Found"} park company: {company.name}') + + # Ride manufacturers and designers (using rides.models.Company) + ride_companies_data = [ + { + 'name': 'Bolliger & Mabillard', + 'slug': 'bolliger-mabillard', + 'roles': ['MANUFACTURER', 'DESIGNER'], + 'description': 'Swiss roller coaster manufacturer known for inverted and diving coasters.', + 'website': 'https://www.bolliger-mabillard.com/', + 'founded_date': '1988-01-01', + }, + { + 'name': 'Intamin Amusement Rides', + 'slug': 'intamin-amusement-rides', + 'roles': ['MANUFACTURER', 'DESIGNER'], + 'description': 'Liechtenstein-based manufacturer of roller coasters and thrill rides.', + 'website': 'https://www.intamin.com/', + 'founded_date': '1967-01-01', + }, + { + 'name': 'Arrow Dynamics', + 'slug': 'arrow-dynamics', + 'roles': ['MANUFACTURER', 'DESIGNER'], + 'description': 'American manufacturer known for corkscrew coasters and mine trains.', + 'website': 'https://en.wikipedia.org/wiki/Arrow_Dynamics', + 'founded_date': '1946-01-01', + }, + { + 'name': 'Vekoma Rides Manufacturing', + 'slug': 'vekoma-rides-manufacturing', + 'roles': ['MANUFACTURER', 'DESIGNER'], + 'description': 'Dutch manufacturer of roller coasters and family rides.', + 'website': 'https://www.vekoma.com/', + 'founded_date': '1926-01-01', + }, + { + 'name': 'Rocky Mountain Construction', + 'slug': 'rocky-mountain-construction', + 'roles': ['MANUFACTURER', 'DESIGNER'], + 'description': 'American manufacturer specializing in I-Box track and Raptor track coasters.', + 'website': 'https://www.rockymtnconstruction.com/', + 'founded_date': '2001-01-01', + }, + { + 'name': 'Mack Rides', + 'slug': 'mack-rides', + 'roles': ['MANUFACTURER', 'DESIGNER'], + 'description': 'German manufacturer known for water rides and powered coasters.', + 'website': 'https://www.mack-rides.com/', + 'founded_date': '1780-01-01', + }, + { + 'name': 'Chance Rides', + 'slug': 'chance-rides', + 'roles': ['MANUFACTURER'], + 'description': 'American manufacturer of thrill rides and amusement park equipment.', + 'website': 'https://www.chancerides.com/', + 'founded_date': '1961-01-01', + }, + { + 'name': 'S&S Worldwide', + 'slug': 's-s-worldwide', + 'roles': ['MANUFACTURER', 'DESIGNER'], + 'description': 'American manufacturer known for drop towers and 4D free-fly coasters.', + 'website': 'https://www.s-s.com/', + 'founded_date': '1990-01-01', + }, + { + 'name': 'Zierer Rides', + 'slug': 'zierer-rides', + 'roles': ['MANUFACTURER'], + 'description': 'German manufacturer of kiddie rides and family coasters.', + 'website': 'https://www.zierer.com/', + 'founded_date': '1950-01-01', + }, + { + 'name': 'Gerstlauer', + 'slug': 'gerstlauer', + 'roles': ['MANUFACTURER', 'DESIGNER'], + 'description': 'German manufacturer known for Euro-Fighter and spinning coasters.', + 'website': 'https://www.gerstlauer-rides.de/', + 'founded_date': '1982-01-01', + }, + ] + + for company_data in ride_companies_data: + company, created = RideCompany.objects.get_or_create( + slug=company_data['slug'], + defaults=company_data + ) + self.created_companies[company.slug] = company + self.stdout.write(f' {"Created" if created else "Found"} ride company: {company.name}') + + def create_parks(self): + """Create parks with proper operator relationships""" + self.stdout.write('Creating parks...') + + parks_data = [ + { + 'name': 'Magic Kingdom', + 'slug': 'magic-kingdom', + 'operator_slug': 'walt-disney-company', + 'property_owner_slug': '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, + 'website': 'https://disneyworld.disney.go.com/destinations/magic-kingdom/', + 'location': { + 'street_address': '1180 Seven Seas Dr', + 'city': 'Lake Buena Vista', + 'state_province': 'Florida', + 'country': 'USA', + 'postal_code': '32830', + 'latitude': 28.4177, + 'longitude': -81.5812 + } + }, + { + 'name': 'Universal Studios Florida', + 'slug': 'universal-studios-florida', + 'operator_slug': 'universal-parks-resorts', + 'property_owner_slug': 'universal-parks-resorts', + 'description': 'Movie and television-based theme park in Orlando, Florida.', + 'opening_date': '1990-06-07', + 'size_acres': 108, + 'website': 'https://www.universalorlando.com/web/en/us/theme-parks/universal-studios-florida', + 'location': { + 'street_address': '6000 Universal Blvd', + 'city': 'Orlando', + 'state_province': 'Florida', + 'country': 'USA', + 'postal_code': '32819', + 'latitude': 28.4749, + 'longitude': -81.4687 + } + }, + { + 'name': 'Cedar Point', + 'slug': 'cedar-point', + 'operator_slug': 'cedar-fair-entertainment', + 'property_owner_slug': 'cedar-fair-entertainment', + 'description': 'Known as the "Roller Coaster Capital of the World".', + 'opening_date': '1870-06-01', + 'size_acres': 364, + 'website': 'https://www.cedarpoint.com/', + 'location': { + 'street_address': '1 Cedar Point Dr', + 'city': 'Sandusky', + 'state_province': 'Ohio', + 'country': 'USA', + 'postal_code': '44870', + 'latitude': 41.4822, + 'longitude': -82.6835 + } + }, + { + 'name': 'Six Flags Magic Mountain', + 'slug': 'six-flags-magic-mountain', + 'operator_slug': 'six-flags-entertainment', + 'property_owner_slug': 'six-flags-entertainment', + 'description': 'Known for its world-record 19 roller coasters.', + 'opening_date': '1971-05-29', + 'size_acres': 262, + 'website': 'https://www.sixflags.com/magicmountain', + 'location': { + 'street_address': '26101 Magic Mountain Pkwy', + 'city': 'Valencia', + 'state_province': 'California', + 'country': 'USA', + 'postal_code': '91355', + 'latitude': 34.4253, + 'longitude': -118.5971 + } + }, + { + 'name': 'Europa-Park', + 'slug': 'europa-park', + 'operator_slug': 'merlin-entertainments', + 'property_owner_slug': 'merlin-entertainments', + 'description': 'One of the most popular theme parks in Europe, located in Germany.', + 'opening_date': '1975-07-12', + 'size_acres': 234, + 'website': 'https://www.europapark.de/', + 'location': { + 'street_address': 'Europa-Park-Straße 2', + 'city': 'Rust', + 'state_province': 'Baden-Württemberg', + 'country': 'Germany', + 'postal_code': '77977', + 'latitude': 48.2667, + 'longitude': 7.7167 + } + }, + { + 'name': 'Alton Towers', + 'slug': 'alton-towers', + 'operator_slug': 'merlin-entertainments', + 'property_owner_slug': 'merlin-entertainments', + 'description': 'Major theme park and former country estate in Staffordshire, England.', + 'opening_date': '1980-04-23', + 'size_acres': 500, + # Add other fields as needed + } + ] diff --git a/parks/management/commands/seed_sample_data.py b/parks/management/commands/seed_sample_data.py new file mode 100644 index 00000000..8778c106 --- /dev/null +++ b/parks/management/commands/seed_sample_data.py @@ -0,0 +1,1072 @@ +from django.core.management.base import BaseCommand +from django.utils import timezone +from django.contrib.contenttypes.models import ContentType +from django.db import transaction, connection +from django.core.exceptions import ObjectDoesNotExist +from django.db import IntegrityError +from django.apps import apps +import logging +from decimal import Decimal +from datetime import date, timedelta +import random + +from parks.models import Company, Park, ParkArea, ParkReview, ParkLocation +from rides.models import Company as RideCompany, Ride, RideModel, RideReview, RollerCoasterStats +from accounts.models import User + + +class Command(BaseCommand): + help = 'Seeds comprehensive sample data for the ThrillWiki theme park application' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.logger = logging.getLogger(__name__) + + def cleanup_existing_data(self): + """Clean up all existing sample data before creating new data""" + self.stdout.write('Cleaning up existing sample data...') + + try: + with transaction.atomic(): + # Count existing data for logging + park_review_count = ParkReview.objects.count() + ride_review_count = RideReview.objects.count() + rollercoaster_stats_count = RollerCoasterStats.objects.count() + ride_count = Ride.objects.count() + ride_model_count = RideModel.objects.count() + park_area_count = ParkArea.objects.count() + park_location_count = ParkLocation.objects.count() + park_count = Park.objects.count() + ride_company_count = RideCompany.objects.count() + company_count = Company.objects.count() + test_user_count = User.objects.filter( + username='testuser').count() + + # Log what will be deleted + self.stdout.write( + f' Found {park_review_count} park reviews to delete') + self.stdout.write( + f' Found {ride_review_count} ride reviews to delete') + self.stdout.write( + f' Found {rollercoaster_stats_count} roller coaster stats to delete') + self.stdout.write(f' Found {ride_count} rides to delete') + self.stdout.write( + f' Found {ride_model_count} ride models to delete') + self.stdout.write( + f' Found {park_area_count} park areas to delete') + self.stdout.write( + f' Found {park_location_count} park locations to delete') + self.stdout.write(f' Found {park_count} parks to delete') + self.stdout.write( + f' Found {ride_company_count} ride companies to delete') + self.stdout.write( + f' Found {company_count} park companies to delete') + self.stdout.write( + f' Found {test_user_count} test users to delete') + + # Delete in order to avoid foreign key constraint violations + # Reviews first (they reference other objects) + if park_review_count > 0: + ParkReview.objects.all().delete() + self.stdout.write( + f' Deleted {park_review_count} park reviews') + + if ride_review_count > 0: + RideReview.objects.all().delete() + self.stdout.write( + f' Deleted {ride_review_count} ride reviews') + + # Roller coaster stats (references Ride) + if rollercoaster_stats_count > 0: + RollerCoasterStats.objects.all().delete() + self.stdout.write( + f' Deleted {rollercoaster_stats_count} roller coaster stats') + + # Rides (references Park, RideCompany, RideModel) + if ride_count > 0: + Ride.objects.all().delete() + self.stdout.write(f' Deleted {ride_count} rides') + + # Ride models (referenced by Ride) + if ride_model_count > 0: + RideModel.objects.all().delete() + self.stdout.write( + f' Deleted {ride_model_count} ride models') + + # Park areas (references Park) + if park_area_count > 0: + ParkArea.objects.all().delete() + self.stdout.write( + f' Deleted {park_area_count} park areas') + + # Park locations (references Park) + if park_location_count > 0: + ParkLocation.objects.all().delete() + self.stdout.write( + f' Deleted {park_location_count} park locations') + + # Parks (referenced by many models) + if park_count > 0: + Park.objects.all().delete() + self.stdout.write(f' Deleted {park_count} parks') + + # Ride companies (referenced by Ride, RideModel) + if ride_company_count > 0: + RideCompany.objects.all().delete() + self.stdout.write( + f' Deleted {ride_company_count} ride companies') + + # Park companies (referenced by Park) + if company_count > 0: + Company.objects.all().delete() + self.stdout.write( + f' Deleted {company_count} park companies') + + # Only delete test user, not all users + if test_user_count > 0: + User.objects.filter(username='testuser').delete() + self.stdout.write( + f' Deleted {test_user_count} test users') + + self.stdout.write(self.style.SUCCESS( + 'Successfully cleaned up existing sample data!')) + + except Exception as e: + self.logger.error( + f'Error during data cleanup: {str(e)}', exc_info=True) + self.stdout.write(self.style.ERROR( + f'Failed to clean up existing data: {str(e)}')) + raise + + def handle(self, *args, **options): + self.stdout.write('Starting sample data creation...') + + try: + # Check if required tables exist + if not self.check_required_tables(): + self.stdout.write(self.style.ERROR( + 'Required database tables are missing. Please run migrations first.')) + return + + # Clean up existing data first + self.cleanup_existing_data() + + # Use transaction to ensure data consistency + with transaction.atomic(): + # Create companies with different roles + self.create_companies() + + # Create parks with proper operator relationships + self.create_parks() + + # Create rides with manufacturer and designer relationships + self.create_rides() + + # Add park areas for variety + self.create_park_areas() + + # Add sample reviews for testing + self.create_reviews() + + self.stdout.write(self.style.SUCCESS( + 'Successfully created comprehensive sample data!')) + + except Exception as e: + self.logger.error( + f'Error during sample data creation: {str(e)}', exc_info=True) + self.stdout.write(self.style.ERROR( + f'Failed to create sample data: {str(e)}')) + raise + + def check_required_tables(self): + """Check if all required tables exist in the database""" + required_models = [ + Company, Park, ParkArea, ParkReview, ParkLocation, + RideCompany, Ride, RideModel, RideReview, RollerCoasterStats, + User + ] + + missing_tables = [] + for model in required_models: + try: + # Check if the table exists by trying to get the table name + table_name = model._meta.db_table + with connection.cursor() as cursor: + cursor.execute(f"SELECT 1 FROM {table_name} LIMIT 1") + except Exception as e: + missing_tables.append(model._meta.label) + + if missing_tables: + self.stdout.write(self.style.WARNING( + f'Missing tables for models: {", ".join(missing_tables)}')) + return False + + self.stdout.write(self.style.SUCCESS('All required tables exist.')) + return True + + def create_companies(self): + """Create companies with different roles (operators, manufacturers, designers)""" + self.stdout.write('Creating companies...') + + try: + # Park Operators + operators_data = [ + { + 'name': 'The Walt Disney Company', + 'roles': ['OPERATOR'], + 'website': 'https://www.disney.com/', + 'description': 'World\'s largest entertainment company and theme park operator.', + 'founded_year': 1923, + }, + { + 'name': 'Universal Parks & Resorts', + 'roles': ['OPERATOR'], + 'website': 'https://www.universalparks.com/', + 'description': 'Division of Comcast NBCUniversal, operating major theme parks worldwide.', + 'founded_year': 1964, + }, + { + 'name': 'Six Flags Entertainment Corporation', + 'roles': ['OPERATOR'], + 'website': 'https://www.sixflags.com/', + 'description': 'World\'s largest regional theme park company.', + 'founded_year': 1961, + }, + { + 'name': 'Cedar Fair Entertainment Company', + 'roles': ['OPERATOR'], + 'website': 'https://www.cedarfair.com/', + 'description': 'One of North America\'s largest operators of regional amusement parks.', + 'founded_year': 1983, + }, + { + 'name': 'Herschend Family Entertainment', + 'roles': ['OPERATOR'], + 'website': 'https://www.hfecorp.com/', + 'description': 'Largest family-owned themed attractions corporation in the United States.', + 'founded_year': 1950, + }, + { + 'name': 'Europa-Park GmbH & Co. Mack KG', + 'roles': ['OPERATOR'], + 'website': 'https://www.europapark.de/', + 'description': 'One of Europe\'s largest theme parks, located in Germany.', + 'founded_year': 1975, + }, + ] + + # Ride Manufacturers + manufacturers_data = [ + { + 'name': 'Bolliger & Mabillard', + 'roles': ['MANUFACTURER'], + 'website': 'https://www.bolliger-mabillard.com/', + 'description': 'Swiss roller coaster manufacturer known for inverted and hyper coasters.', + 'founded_date': '1988-01-01', + }, + { + 'name': 'Intamin Amusement Rides', + 'roles': ['MANUFACTURER'], + 'website': 'https://www.intamin.com/', + 'description': 'Liechtenstein-based manufacturer of roller coasters and thrill rides.', + 'founded_date': '1967-01-01', + }, + { + 'name': 'Vekoma Rides Manufacturing', + 'roles': ['MANUFACTURER'], + 'website': 'https://www.vekoma.com/', + 'description': 'Dutch manufacturer specializing in family and steel roller coasters.', + 'founded_date': '1926-01-01', + }, + { + 'name': 'Arrow Dynamics', + 'roles': ['MANUFACTURER'], + 'website': 'https://www.arrowdynamics.com/', + 'description': 'American manufacturer known for corkscrew and looping coasters.', + 'founded_date': '1946-01-01', + }, + { + 'name': 'Rocky Mountain Construction', + 'roles': ['MANUFACTURER'], + 'website': 'https://www.rockymtnconstruction.com/', + 'description': 'American manufacturer known for I-Box track and wooden coasters.', + 'founded_date': '2001-01-01', + }, + { + 'name': 'Mack Rides GmbH & Co KG', + 'roles': ['MANUFACTURER'], + 'website': 'https://www.mack-rides.com/', + 'description': 'German manufacturer of roller coasters and water rides.', + 'founded_date': '1780-01-01', + }, + ] + + # Ride Designers + designers_data = [ + { + 'name': 'Werner Stengel', + 'roles': ['DESIGNER'], + 'website': '', + 'description': 'German roller coaster designer known for complex layouts and inversions.', + }, + { + 'name': 'Alan Schilke', + 'roles': ['DESIGNER'], + 'website': '', + 'description': 'American roller coaster designer known for family-friendly coasters.', + }, + { + 'name': 'John Pierce', + 'roles': ['DESIGNER'], + 'website': '', + 'description': 'American roller coaster designer and engineer.', + }, + { + 'name': 'The Gravity Group', + 'roles': ['DESIGNER'], + 'website': 'https://www.thegravitygroup.com/', + 'description': 'American design firm specializing in roller coaster design.', + }, + ] + + # Create companies in parks app (for operators and property owners) + self.park_companies = {} + for data in operators_data: + try: + company, created = Company.objects.get_or_create( + name=data['name'], + defaults={ + 'roles': data['roles'], + 'website': data['website'], + 'description': data['description'], + 'founded_year': data['founded_year'], + } + ) + self.park_companies[data['name']] = company + self.stdout.write( + f' {"Created" if created else "Found"} park company: {company.name}') + except Exception as e: + self.logger.error( + f'Error creating park company {data["name"]}: {str(e)}') + raise + + # Create companies in rides app (for manufacturers and designers) + self.ride_companies = {} + for data in manufacturers_data + designers_data: + try: + company, created = RideCompany.objects.get_or_create( + name=data['name'], + defaults={ + 'roles': data['roles'], + 'website': data['website'], + 'description': data['description'], + 'founded_date': data.get('founded_date'), + } + ) + self.ride_companies[data['name']] = company + self.stdout.write( + f' {"Created" if created else "Found"} ride company: {company.name}') + except Exception as e: + self.logger.error( + f'Error creating ride company {data["name"]}: {str(e)}') + raise + + except Exception as e: + self.logger.error(f'Error in create_companies: {str(e)}') + raise + + def create_parks(self): + """Create parks with proper operator relationships""" + self.stdout.write('Creating parks...') + + try: + parks_data = [ + { + 'name': 'Magic Kingdom', + 'operator': 'The Walt Disney Company', + 'property_owner': '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, + 'website': 'https://disneyworld.disney.go.com/destinations/magic-kingdom/', + '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 + } + }, + { + 'name': 'Universal Studios Florida', + 'operator': 'Universal Parks & Resorts', + 'property_owner': 'Universal Parks & Resorts', + 'description': 'Movie and television-based theme park in Orlando, Florida.', + 'opening_date': '1990-06-07', + 'size_acres': 108, + 'website': 'https://www.universalorlando.com/web/en/us/theme-parks/universal-studios-florida', + 'location': { + 'street_address': '6000 Universal Blvd', + 'city': 'Orlando', + 'state': 'Florida', + 'country': 'United States', + 'postal_code': '32819', + 'latitude': 28.4749, + 'longitude': -81.4687 + } + }, + { + 'name': 'Cedar Point', + 'operator': 'Cedar Fair Entertainment Company', + 'property_owner': 'Cedar Fair Entertainment Company', + 'description': 'Known as the "Roller Coaster Capital of the World".', + 'opening_date': '1870-06-01', + 'size_acres': 364, + 'website': 'https://www.cedarpoint.com/', + 'location': { + 'street_address': '1 Cedar Point Dr', + 'city': 'Sandusky', + 'state': 'Ohio', + 'country': 'United States', + 'postal_code': '44870', + 'latitude': 41.4822, + 'longitude': -82.6835 + } + }, + { + 'name': 'Europa-Park', + 'operator': 'Europa-Park GmbH & Co. Mack KG', + 'property_owner': 'Europa-Park GmbH & Co. Mack KG', + 'description': 'One of Europe\'s largest theme parks, located in Germany.', + 'opening_date': '1975-07-12', + 'size_acres': 235, + 'website': 'https://www.europapark.de/', + 'location': { + 'street_address': 'Europa-Park-Straße 2', + 'city': 'Rust', + 'state': 'Baden-Württemberg', + 'country': 'Germany', + 'postal_code': '77977', + 'latitude': 48.2667, + 'longitude': 7.7167 + } + }, + { + 'name': 'Six Flags Magic Mountain', + 'operator': 'Six Flags Entertainment Corporation', + 'property_owner': 'Six Flags Entertainment Corporation', + 'description': 'Known for its world-record 19 roller coasters.', + 'opening_date': '1971-05-29', + 'size_acres': 262, + 'website': 'https://www.sixflags.com/magicmountain', + 'location': { + 'street_address': '26101 Magic Mountain Pkwy', + 'city': 'Valencia', + 'state': 'California', + 'country': 'United States', + 'postal_code': '91355', + 'latitude': 34.4253, + 'longitude': -118.5971 + } + }, + { + 'name': 'Silver Dollar City', + 'operator': 'Herschend Family Entertainment', + 'property_owner': 'Herschend Family Entertainment', + 'description': 'An 1880s-themed park featuring over 40 rides and attractions.', + 'opening_date': '1960-05-01', + 'size_acres': 61, + 'website': 'https://www.silverdollarcity.com/', + 'location': { + 'street_address': '399 Silver Dollar City Parkway', + 'city': 'Branson', + 'state': 'Missouri', + 'country': 'United States', + 'postal_code': '65616', + 'latitude': 36.668497, + 'longitude': -93.339074 + } + }, + ] + + self.parks = {} + for park_data in parks_data: + try: + operator = self.park_companies[park_data['operator']] + property_owner = self.park_companies.get( + park_data['property_owner']) if park_data['property_owner'] else None + + 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'], + 'website': park_data['website'], + 'operator': operator, + 'property_owner': property_owner, + } + ) + self.parks[park_data['name']] = park + self.stdout.write( + f' {"Created" if created else "Found"} park: {park.name}') + + # Create location for park + if created: + try: + loc_data = park_data['location'] + park_location = ParkLocation.objects.create( + park=park, + street_address=loc_data['street_address'], + city=loc_data['city'], + state=loc_data['state'], + country=loc_data['country'], + postal_code=loc_data['postal_code'] + ) + # Set coordinates using the helper method + park_location.set_coordinates( + loc_data['latitude'], + loc_data['longitude'] + ) + park_location.save() + except Exception as e: + self.logger.error( + f'Error creating location for park {park_data["name"]}: {str(e)}') + raise + + except Exception as e: + self.logger.error( + f'Error creating park {park_data["name"]}: {str(e)}') + raise + + except Exception as e: + self.logger.error(f'Error in create_parks: {str(e)}') + raise + + def create_rides(self): + """Create rides with manufacturer and designer relationships""" + self.stdout.write('Creating rides...') + + try: + # First create some ride models + ride_models_data = [ + { + 'name': 'Dive Coaster', + 'manufacturer': 'Bolliger & Mabillard', + 'category': 'RC', + 'description': 'Inverted roller coaster with a vertical drop and non-inverting loop' + }, + { + 'name': 'Hyper Coaster', + 'manufacturer': 'Bolliger & Mabillard', + 'category': 'RC', + 'description': 'Steel roller coaster with heights over 200 feet' + }, + { + 'name': 'Boomerang', + 'manufacturer': 'Vekoma Rides Manufacturing', + 'category': 'RC', + 'description': 'Shuttle roller coaster that runs forward and backward' + }, + { + 'name': 'Corkscrew Coaster', + 'manufacturer': 'Arrow Dynamics', + 'category': 'RC', + 'description': 'Early steel coaster design with corkscrew elements' + }, + { + 'name': 'I-Box Track', + 'manufacturer': 'Rocky Mountain Construction', + 'category': 'RC', + 'description': 'Smooth-riding steel track system for wooden coasters' + }, + { + 'name': 'Powered Coaster', + 'manufacturer': 'Mack Rides GmbH & Co KG', + 'category': 'RC', + 'description': 'Family-friendly steel roller coaster' + }, + ] + + self.ride_models = {} + for model_data in ride_models_data: + try: + manufacturer = self.ride_companies.get( + model_data['manufacturer']) + model, created = RideModel.objects.get_or_create( + name=model_data['name'], + manufacturer=manufacturer, + defaults={ + 'description': model_data['description'], + 'category': model_data['category'], + } + ) + self.ride_models[model_data['name']] = model + self.stdout.write( + f' {"Created" if created else "Found"} ride model: {model.name}') + except Exception as e: + self.logger.error( + f'Error creating ride model {model_data["name"]}: {str(e)}') + raise + + # Create rides + rides_data = [ + { + 'name': 'Millennium Force', + 'park': 'Cedar Point', + 'manufacturer': 'Bolliger & Mabillard', + 'designer': 'Werner Stengel', + 'ride_model': 'Hyper Coaster', + 'category': 'RC', + 'description': 'World\'s first hyper coaster reaching speeds of 93 mph.', + 'opening_date': '2000-05-13', + 'coaster_stats': { + 'height_ft': 310, + 'length_ft': 6595, + 'speed_mph': 93, + 'inversions': 0, + 'ride_time_seconds': 165, + 'track_material': 'STEEL', + 'roller_coaster_type': 'SITDOWN', + 'max_drop_height_ft': 300, + 'launch_type': 'CHAIN', + 'trains_count': 3, + 'cars_per_train': 9, + 'seats_per_car': 4, + } + }, + { + 'name': 'Top Thrill Dragster', + 'park': 'Cedar Point', + 'manufacturer': 'Intamin Amusement Rides', + 'designer': 'Werner Stengel', + 'category': 'RC', + 'description': 'World\'s first strata coaster reaching 420 feet.', + 'opening_date': '2003-05-04', + 'coaster_stats': { + 'height_ft': 420, + 'length_ft': 2800, + 'speed_mph': 120, + 'inversions': 0, + 'ride_time_seconds': 17, + 'track_material': 'STEEL', + 'roller_coaster_type': 'SITDOWN', + 'max_drop_height_ft': 400, + 'launch_type': 'HYDRAULIC', + 'trains_count': 1, + 'cars_per_train': 1, + 'seats_per_car': 16, + } + }, + { + 'name': 'Silver Star', + 'park': 'Europa-Park', + 'manufacturer': 'Bolliger & Mabillard', + 'designer': 'Werner Stengel', + 'ride_model': 'Dive Coaster', + 'category': 'RC', + 'description': 'Europe\'s first dive coaster with a 300-foot drop.', + 'opening_date': '2002-03-23', + 'coaster_stats': { + 'height_ft': 239, + 'length_ft': 5249, + 'speed_mph': 80, + 'inversions': 0, + 'ride_time_seconds': 240, + 'track_material': 'STEEL', + 'roller_coaster_type': 'SITDOWN', + 'max_drop_height_ft': 197, + 'launch_type': 'CHAIN', + 'trains_count': 2, + 'cars_per_train': 10, + 'seats_per_car': 2, + } + }, + { + 'name': 'Blue Fire', + 'park': 'Europa-Park', + 'manufacturer': 'Mack Rides GmbH & Co KG', + 'designer': 'John Pierce', + 'ride_model': 'Powered Coaster', + 'category': 'RC', + 'description': 'Launched roller coaster with a 124-foot drop.', + 'opening_date': '2009-04-25', + 'coaster_stats': { + 'height_ft': 124, + 'length_ft': 2789, + 'speed_mph': 62, + 'inversions': 0, + 'ride_time_seconds': 120, + 'track_material': 'STEEL', + 'roller_coaster_type': 'SITDOWN', + 'max_drop_height_ft': 98, + 'launch_type': 'HYDRAULIC', + 'trains_count': 2, + 'cars_per_train': 5, + 'seats_per_car': 4, + } + }, + { + 'name': 'Space Mountain', + 'park': 'Magic Kingdom', + 'manufacturer': 'Arrow Dynamics', + 'designer': 'John Pierce', + 'category': 'RC', + 'description': 'Indoor space-themed roller coaster.', + 'opening_date': '1975-01-15', + 'coaster_stats': { + 'height_ft': 183, + 'length_ft': 3200, + 'speed_mph': 35, + 'inversions': 0, + 'ride_time_seconds': 180, + 'track_material': 'STEEL', + 'roller_coaster_type': 'SITDOWN', + 'max_drop_height_ft': 150, + 'launch_type': 'CHAIN', + 'trains_count': 2, + 'cars_per_train': 6, + 'seats_per_car': 2, + } + }, + { + 'name': 'Big Thunder Mountain Railroad', + 'park': 'Magic Kingdom', + 'manufacturer': 'Arrow Dynamics', + 'designer': 'The Gravity Group', + 'category': 'RC', + 'description': 'Mine train roller coaster themed as a runaway mining train.', + 'opening_date': '1980-11-15', + 'coaster_stats': { + 'height_ft': 146, + 'length_ft': 3280, + 'speed_mph': 35, + 'inversions': 0, + 'ride_time_seconds': 240, + 'track_material': 'STEEL', + 'roller_coaster_type': 'SITDOWN', + 'max_drop_height_ft': 128, + 'launch_type': 'CHAIN', + 'trains_count': 3, + 'cars_per_train': 5, + 'seats_per_car': 4, + } + }, + { + 'name': 'Maverick', + 'park': 'Cedar Point', + 'manufacturer': 'Intamin Amusement Rides', + 'designer': 'Werner Stengel', + 'category': 'RC', + 'description': 'Wild mouse coaster with a 100-foot drop.', + 'opening_date': '2007-05-26', + 'coaster_stats': { + 'height_ft': 105, + 'length_ft': 4450, + 'speed_mph': 70, + 'inversions': 0, + 'ride_time_seconds': 180, + 'track_material': 'STEEL', + 'roller_coaster_type': 'WILD_MOUSE', + 'max_drop_height_ft': 100, + 'launch_type': 'CHAIN', + 'trains_count': 2, + 'cars_per_train': 4, + 'seats_per_car': 4, + } + }, + { + 'name': 'Time Traveler', + 'park': 'Silver Dollar City', + 'manufacturer': 'Rocky Mountain Construction', + 'designer': 'Alan Schilke', + 'ride_model': 'I-Box Track', + 'category': 'RC', + 'description': 'Wooden coaster with steel I-Box track for smooth riding.', + 'opening_date': '2018-04-28', + 'coaster_stats': { + 'height_ft': 165, + 'length_ft': 5832, + 'speed_mph': 72, + 'inversions': 0, + 'ride_time_seconds': 240, + 'track_material': 'HYBRID', + 'roller_coaster_type': 'SITDOWN', + 'max_drop_height_ft': 155, + 'launch_type': 'CHAIN', + 'trains_count': 2, + 'cars_per_train': 6, + 'seats_per_car': 2, + } + }, + ] + + self.rides = {} + for ride_data in rides_data: + try: + park = self.parks[ride_data['park']] + manufacturer = self.ride_companies.get( + ride_data.get('manufacturer')) + designer = self.ride_companies.get( + ride_data.get('designer')) + ride_model = self.ride_models.get( + ride_data.get('ride_model')) + + ride, created = Ride.objects.get_or_create( + name=ride_data['name'], + park=park, + defaults={ + 'description': ride_data['description'], + 'category': ride_data['category'], + 'status': 'OPERATING', + 'opening_date': ride_data['opening_date'], + 'manufacturer': manufacturer, + 'designer': designer, + 'ride_model': ride_model, + } + ) + self.rides[ride_data['name']] = ride + self.stdout.write( + f' {"Created" if created else "Found"} ride: {ride.name}') + + # Create roller coaster stats if provided + if created and 'coaster_stats' in ride_data: + try: + stats_data = ride_data['coaster_stats'] + RollerCoasterStats.objects.create( + ride=ride, + **stats_data + ) + except Exception as e: + self.logger.error( + f'Error creating stats for ride {ride_data["name"]}: {str(e)}') + raise + + except Exception as e: + self.logger.error( + f'Error creating ride {ride_data["name"]}: {str(e)}') + raise + + except Exception as e: + self.logger.error(f'Error in create_rides: {str(e)}') + raise + + def create_park_areas(self): + """Add park areas for variety""" + self.stdout.write('Creating park areas...') + + try: + areas_data = [ + { + 'park': 'Magic Kingdom', + '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'}, + ] + }, + { + 'park': 'Universal Studios Florida', + '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'}, + ] + }, + { + 'park': 'Cedar Point', + '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'}, + ] + }, + { + 'park': 'Europa-Park', + 'areas': [ + {'name': 'Germany', 'description': 'German-themed area'}, + {'name': 'France', 'description': 'French-themed area'}, + {'name': 'England', 'description': 'English-themed area'}, + {'name': 'Italy', 'description': 'Italian-themed area'}, + {'name': 'Spain', 'description': 'Spanish-themed area'}, + {'name': 'Portugal', 'description': 'Portuguese-themed area'}, + ] + }, + ] + + for area_group in areas_data: + try: + park = self.parks[area_group['park']] + for area_data in area_group['areas']: + area, created = ParkArea.objects.get_or_create( + name=area_data['name'], + park=park, + defaults={ + 'description': area_data['description'], + 'opening_date': park.opening_date, + } + ) + self.stdout.write( + f' {"Created" if created else "Found"} area: {area.name} in {park.name}') + except Exception as e: + self.logger.error( + f'Error creating areas for park {area_group["park"]}: {str(e)}') + raise + + except Exception as e: + self.logger.error(f'Error in create_park_areas: {str(e)}') + raise + + def create_reviews(self): + """Add sample reviews for testing""" + self.stdout.write('Creating sample reviews...') + + try: + # Create a test user if none exists + test_user, created = User.objects.get_or_create( + username='testuser', + defaults={ + 'email': 'test@example.com', + 'first_name': 'Test', + 'last_name': 'User', + } + ) + if created: + test_user.set_password('testpass123') + test_user.save() + + # Park reviews + park_reviews_data = [ + { + 'park': 'Cedar Point', + 'rating': 10, + 'title': 'Best roller coaster park in the world!', + 'content': 'Cedar Point is absolutely incredible. The Millennium Force is a must-ride. The park is clean, well-maintained, and the staff is friendly. Highly recommend!', + 'visit_date': '2023-08-15', + }, + { + 'park': 'Magic Kingdom', + 'rating': 9, + 'title': 'Magical experience for all ages', + 'content': 'Disney does it again with Magic Kingdom. The attention to detail is amazing and the shows are spectacular. Space Mountain is a classic.', + 'visit_date': '2023-07-20', + }, + { + 'park': 'Europa-Park', + 'rating': 9, + 'title': 'Europe\'s best theme park', + 'content': 'Europa-Park is fantastic! The theming is incredible and the rides are world-class. Silver Star is absolutely breathtaking.', + 'visit_date': '2023-06-10', + }, + { + 'park': 'Universal Studios Florida', + 'rating': 8, + 'title': 'Great movie-themed attractions', + 'content': 'Universal has some amazing rides, especially in the Harry Potter area. The theming is top-notch and the shows are entertaining.', + 'visit_date': '2023-05-05', + }, + ] + + for review_data in park_reviews_data: + try: + park = self.parks[review_data['park']] + review, created = ParkReview.objects.get_or_create( + park=park, + user=test_user, + defaults={ + 'rating': review_data['rating'], + 'title': review_data['title'], + 'content': review_data['content'], + 'visit_date': review_data['visit_date'], + 'is_published': True, + } + ) + self.stdout.write( + f' {"Created" if created else "Found"} park review: {review.title}') + except Exception as e: + self.logger.error( + f'Error creating park review for {review_data["park"]}: {str(e)}') + raise + + # Ride reviews + ride_reviews_data = [ + { + 'ride': 'Millennium Force', + 'rating': 10, + 'title': 'The king of roller coasters!', + 'content': 'Absolutely incredible ride! The first drop is breathtaking and the speed is unreal. A must-experience for any coaster enthusiast.', + 'visit_date': '2023-08-15', + }, + { + 'ride': 'Top Thrill Dragster', + 'rating': 9, + 'title': 'Incredible launch and height', + 'content': 'The launch is intense and reaching the top of the 420-foot tower is amazing. The view from the top is spectacular!', + 'visit_date': '2023-08-16', + }, + { + 'ride': 'Silver Star', + 'rating': 10, + 'title': 'Best dive coaster in Europe', + 'content': 'The dive drop is incredible! The theming around the ride is beautiful and the overall experience is fantastic.', + 'visit_date': '2023-06-10', + }, + { + 'ride': 'Space Mountain', + 'rating': 8, + 'title': 'Classic Disney coaster', + 'content': 'A classic that never gets old. The indoor setting and space theme make it unique. Great for all ages.', + 'visit_date': '2023-07-20', + }, + ] + + for review_data in ride_reviews_data: + try: + ride = self.rides[review_data['ride']] + review, created = RideReview.objects.get_or_create( + ride=ride, + user=test_user, + defaults={ + 'rating': review_data['rating'], + 'title': review_data['title'], + 'content': review_data['content'], + 'visit_date': review_data['visit_date'], + 'is_published': True, + } + ) + self.stdout.write( + f' {"Created" if created else "Found"} ride review: {review.title}') + except Exception as e: + self.logger.error( + f'Error creating ride review for {review_data["ride"]}: {str(e)}') + raise + + self.stdout.write(self.style.SUCCESS( + 'Sample data creation completed!')) + + except Exception as e: + self.logger.error(f'Error in create_reviews: {str(e)}') + raise diff --git a/parks/models/companies.py b/parks/models/companies.py index 8b7b766f..0632ae40 100644 --- a/parks/models/companies.py +++ b/parks/models/companies.py @@ -1,15 +1,18 @@ from django.contrib.postgres.fields import ArrayField from django.db import models +from django.utils.text import slugify from core.models import TrackedModel import pghistory + @pghistory.track() class Company(TrackedModel): - + # Import managers from ..managers import CompanyManager - + objects = CompanyManager() + class CompanyRole(models.TextChoices): OPERATOR = 'OPERATOR', 'Park Operator' PROPERTY_OWNER = 'PROPERTY_OWNER', 'Property Owner' @@ -23,12 +26,17 @@ class Company(TrackedModel): ) description = models.TextField(blank=True) website = models.URLField(blank=True) - + # Operator-specific fields founded_year = models.PositiveIntegerField(blank=True, null=True) parks_count = models.IntegerField(default=0) rides_count = models.IntegerField(default=0) - + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) + def __str__(self): return self.name @@ -36,6 +44,7 @@ class Company(TrackedModel): ordering = ['name'] verbose_name_plural = 'Companies' + class CompanyHeadquarters(models.Model): """ Simple address storage for company headquarters without coordinate tracking. @@ -47,7 +56,7 @@ class CompanyHeadquarters(models.Model): on_delete=models.CASCADE, related_name='headquarters' ) - + # Address Fields (No coordinates needed) street_address = models.CharField( max_length=255, @@ -76,13 +85,13 @@ class CompanyHeadquarters(models.Model): blank=True, help_text="ZIP or postal code" ) - + # Optional mailing address if different or more complete mailing_address = models.TextField( blank=True, help_text="Complete mailing address if different from basic address" ) - + # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -102,7 +111,7 @@ class CompanyHeadquarters(models.Model): if self.country and self.country != 'USA': components.append(self.country) return ", ".join(components) if components else f"{self.city}, {self.country}" - + @property def location_display(self): """Simple city, state/country display for compact views.""" @@ -122,4 +131,4 @@ class CompanyHeadquarters(models.Model): ordering = ['company__name'] indexes = [ models.Index(fields=['city', 'country']), - ] \ No newline at end of file + ] diff --git a/parks/urls.py b/parks/urls.py index 3a49cdc1..cf2c7c63 100644 --- a/parks/urls.py +++ b/parks/urls.py @@ -16,45 +16,56 @@ urlpatterns = [ # Park views with autocomplete search path("", views.ParkListView.as_view(), name="park_list"), path("create/", views.ParkCreateView.as_view(), name="park_create"), - + # 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("suggest_parks/", views_search.suggest_parks, name="suggest_parks"), path("search/", views.search_parks, name="search_parks"), - + # Road trip planning URLs path("roadtrip/", RoadTripPlannerView.as_view(), name="roadtrip_planner"), path("roadtrip/create/", CreateTripView.as_view(), name="roadtrip_create"), - path("roadtrip//", TripDetailView.as_view(), name="roadtrip_detail"), - + path("roadtrip//", + TripDetailView.as_view(), name="roadtrip_detail"), + # Road trip HTMX endpoints - path("roadtrip/htmx/parks-along-route/", FindParksAlongRouteView.as_view(), name="roadtrip_htmx_parks_along_route"), - path("roadtrip/htmx/geocode/", GeocodeAddressView.as_view(), name="roadtrip_htmx_geocode"), - path("roadtrip/htmx/distance/", ParkDistanceCalculatorView.as_view(), name="roadtrip_htmx_distance"), - + path("roadtrip/htmx/parks-along-route/", FindParksAlongRouteView.as_view(), + name="roadtrip_htmx_parks_along_route"), + path("roadtrip/htmx/geocode/", GeocodeAddressView.as_view(), + name="roadtrip_htmx_geocode"), + path("roadtrip/htmx/distance/", ParkDistanceCalculatorView.as_view(), + name="roadtrip_htmx_distance"), + # Park detail and related views path("/", views.ParkDetailView.as_view(), name="park_detail"), path("/edit/", views.ParkUpdateView.as_view(), name="park_update"), path("/actions/", views.park_actions, name="park_actions"), - + # Area views - path("/areas//", views.ParkAreaDetailView.as_view(), name="area_detail"), + path("/areas//", + views.ParkAreaDetailView.as_view(), name="area_detail"), # Park-specific category URLs - path("/roller_coasters/", ParkSingleCategoryListView.as_view(), {'category': 'RC'}, name="park_roller_coasters"), - path("/dark_rides/", ParkSingleCategoryListView.as_view(), {'category': 'DR'}, name="park_dark_rides"), - path("/flat_rides/", ParkSingleCategoryListView.as_view(), {'category': 'FR'}, name="park_flat_rides"), - path("/water_rides/", ParkSingleCategoryListView.as_view(), {'category': 'WR'}, name="park_water_rides"), - path("/transports/", ParkSingleCategoryListView.as_view(), {'category': 'TR'}, name="park_transports"), - path("/others/", ParkSingleCategoryListView.as_view(), {'category': 'OT'}, name="park_others"), + path("/roller_coasters/", ParkSingleCategoryListView.as_view(), + {'category': 'RC'}, name="park_roller_coasters"), + path("/dark_rides/", ParkSingleCategoryListView.as_view(), + {'category': 'DR'}, name="park_dark_rides"), + path("/flat_rides/", ParkSingleCategoryListView.as_view(), + {'category': 'FR'}, name="park_flat_rides"), + path("/water_rides/", ParkSingleCategoryListView.as_view(), + {'category': 'WR'}, name="park_water_rides"), + path("/transports/", ParkSingleCategoryListView.as_view(), + {'category': 'TR'}, name="park_transports"), + path("/others/", ParkSingleCategoryListView.as_view(), + {'category': 'OT'}, name="park_others"), # Include park-specific rides URLs path("/rides/", include("rides.park_urls", namespace="rides")), diff --git a/static/css/tailwind.css b/static/css/tailwind.css index 1f79f44d..339f78ee 100644 --- a/static/css/tailwind.css +++ b/static/css/tailwind.css @@ -312,6 +312,9 @@ .fixed { position: fixed; } + .fixed\! { + position: fixed !important; + } .relative { position: relative; } @@ -1575,9 +1578,6 @@ .text-yellow-800 { color: var(--color-yellow-800); } - .capitalize { - text-transform: capitalize; - } .lowercase { text-transform: lowercase; } diff --git a/templates/parks/park_detail.html b/templates/parks/park_detail.html index 9dd05a16..976ce337 100644 --- a/templates/parks/park_detail.html +++ b/templates/parks/park_detail.html @@ -63,8 +63,7 @@
Operator
- + {{ park.operator.name }}
diff --git a/templates/parks/partials/park_list.html b/templates/parks/partials/park_list.html index 0f7decb8..1b796f1a 100644 --- a/templates/parks/partials/park_list.html +++ b/templates/parks/partials/park_list.html @@ -41,10 +41,8 @@ {% endif %}
{% if park.operator %} -
- - {{ park.operator.name }} - +
+ {{ park.operator.name }}
{% endif %}
diff --git a/templates/search_results.html b/templates/search_results.html index a3f385b2..11c05f5c 100644 --- a/templates/search_results.html +++ b/templates/search_results.html @@ -119,8 +119,7 @@ {% for operator in operators %}

- + {{ operator.name }}

diff --git a/thrillwiki/settings.py b/thrillwiki/settings.py index e879be78..dda62d79 100644 --- a/thrillwiki/settings.py +++ b/thrillwiki/settings.py @@ -43,6 +43,8 @@ INSTALLED_APPS = [ "whitenoise", "django_tailwind_cli", "autocomplete", # Django HTMX Autocomplete + "debug_toolbar", + "silk", "core", "accounts", "parks", @@ -57,6 +59,7 @@ MIDDLEWARE = [ "django.middleware.cache.UpdateCacheMiddleware", "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", + "debug_toolbar.middleware.DebugToolbarMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", @@ -93,21 +96,19 @@ WSGI_APPLICATION = "thrillwiki.wsgi.application" # Database -# Parse database URL but force PostGIS engine -db_config = dj_database_url.config( - default="[DATABASE-URL-REMOVED] - conn_max_age=600, - conn_health_checks=True, -) - -# Force PostGIS engine - override any parsed engine +# For development, use PostgreSQL with PostGIS for GeoDjango features DATABASES = { "default": { - **db_config, "ENGINE": "django.contrib.gis.db.backends.postgis", + "NAME": "thrillwiki", + "USER": "postgres", + "PASSWORD": "postgres", + "HOST": "localhost", + "PORT": "5432", } } + # Cache settings CACHES = { "default": { @@ -228,3 +229,9 @@ ROADTRIP_USER_AGENT = "ThrillWiki Road Trip Planner (https://thrillwiki.com)" ROADTRIP_REQUEST_TIMEOUT = 10 # seconds ROADTRIP_MAX_RETRIES = 3 ROADTRIP_BACKOFF_FACTOR = 2 + +# Debug Toolbar Configuration +INTERNAL_IPS = [ + "127.0.0.1", + "localhost", +] diff --git a/thrillwiki/urls.py b/thrillwiki/urls.py index 5d767c89..ab5033c2 100644 --- a/thrillwiki/urls.py +++ b/thrillwiki/urls.py @@ -30,27 +30,36 @@ urlpatterns = [ path("", HomeView.as_view(), name="home"), # Autocomplete URLs (must be before other URLs) path("ac/", autocomplete_urls), - + # API Documentation URLs - path("api/schema/", SpectacularAPIView.as_view(), name="schema") if HAS_SPECTACULAR else path("", lambda r: None), - path("api/docs/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui") if HAS_SPECTACULAR else path("", lambda r: None), - path("api/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc") if HAS_SPECTACULAR else path("", lambda r: None), - + path("api/schema/", SpectacularAPIView.as_view(), + name="schema") if HAS_SPECTACULAR else path("", lambda r: None), + path("api/docs/", SpectacularSwaggerView.as_view(url_name="schema"), + name="swagger-ui") if HAS_SPECTACULAR else path("", lambda r: None), + path("api/redoc/", SpectacularRedocView.as_view(url_name="schema"), + name="redoc") if HAS_SPECTACULAR else path("", lambda r: None), + # Health Check URLs path("health/", include("health_check.urls")), - path("health/api/", HealthCheckAPIView.as_view(), name="health-api") if HAS_HEALTH_VIEWS else path("", lambda r: None), - path("health/simple/", SimpleHealthView.as_view(), name="health-simple") if HAS_HEALTH_VIEWS else path("", lambda r: None), - path("health/metrics/", PerformanceMetricsView.as_view(), name="health-metrics") if HAS_HEALTH_VIEWS else path("", lambda r: None), - + path("health/api/", HealthCheckAPIView.as_view(), + name="health-api") if HAS_HEALTH_VIEWS else path("", lambda r: None), + path("health/simple/", SimpleHealthView.as_view(), + name="health-simple") if HAS_HEALTH_VIEWS else path("", lambda r: None), + path("health/metrics/", PerformanceMetricsView.as_view(), + name="health-metrics") if HAS_HEALTH_VIEWS else path("", lambda r: None), + # API URLs (before app URLs to avoid conflicts) path("api/v1/", include("parks.api.urls", namespace="parks_api")), path("api/v1/", include("rides.api.urls", namespace="rides_api")), - path("api/v1/map/", include("core.urls.map_urls", namespace="map_api")), # Map API URLs - + path("api/v1/map/", include("core.urls.map_urls", + namespace="map_api")), # Map API URLs + # Parks and Rides URLs path("parks/", include("parks.urls", namespace="parks")), # Global rides URLs path("rides/", include("rides.urls", namespace="rides")), + # Operators URLs + path("operators/", include("parks.urls", namespace="operators")), # Other URLs path("photos/", include("media.urls", namespace="photos")), # Add photos URLs path("search/", include("core.urls.search", namespace="search")), @@ -93,9 +102,11 @@ urlpatterns = [ # Serve static files in development if settings.DEBUG: - urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) - urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) - + urlpatterns += static(settings.STATIC_URL, + document_root=settings.STATIC_ROOT) + urlpatterns += static(settings.MEDIA_URL, + document_root=settings.MEDIA_ROOT) + # Development monitoring URLs try: import debug_toolbar @@ -104,13 +115,13 @@ if settings.DEBUG: ] + urlpatterns except ImportError: pass - + try: import silk urlpatterns += [path('silk/', include('silk.urls', namespace='silk'))] except ImportError: pass - + # Serve test coverage reports in development coverage_dir = os.path.join(settings.BASE_DIR, 'tests', 'coverage_html') if os.path.exists(coverage_dir):