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.
This commit is contained in:
pacnpal
2025-08-20 10:16:21 -04:00
parent 641fc1a253
commit 78248aa892
11 changed files with 1489 additions and 64 deletions

2
.gitignore vendored
View File

@@ -388,3 +388,5 @@ Temporary Items
.github-token
logs/
profiles
.thrillwiki-github-token
.thrillwiki-template-config

View File

@@ -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
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,10 @@
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):
@@ -10,6 +12,7 @@ class Company(TrackedModel):
from ..managers import CompanyManager
objects = CompanyManager()
class CompanyRole(models.TextChoices):
OPERATOR = 'OPERATOR', 'Park Operator'
PROPERTY_OWNER = 'PROPERTY_OWNER', 'Property Owner'
@@ -29,6 +32,11 @@ class Company(TrackedModel):
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.

View File

@@ -33,12 +33,16 @@ urlpatterns = [
# Road trip planning URLs
path("roadtrip/", RoadTripPlannerView.as_view(), name="roadtrip_planner"),
path("roadtrip/create/", CreateTripView.as_view(), name="roadtrip_create"),
path("roadtrip/<str:trip_id>/", TripDetailView.as_view(), name="roadtrip_detail"),
path("roadtrip/<str:trip_id>/",
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("<slug:slug>/", views.ParkDetailView.as_view(), name="park_detail"),
@@ -46,15 +50,22 @@ urlpatterns = [
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"),
path("<slug:park_slug>/areas/<slug:area_slug>/",
views.ParkAreaDetailView.as_view(), name="area_detail"),
# Park-specific category URLs
path("<slug:park_slug>/roller_coasters/", ParkSingleCategoryListView.as_view(), {'category': 'RC'}, name="park_roller_coasters"),
path("<slug:park_slug>/dark_rides/", ParkSingleCategoryListView.as_view(), {'category': 'DR'}, name="park_dark_rides"),
path("<slug:park_slug>/flat_rides/", ParkSingleCategoryListView.as_view(), {'category': 'FR'}, name="park_flat_rides"),
path("<slug:park_slug>/water_rides/", ParkSingleCategoryListView.as_view(), {'category': 'WR'}, name="park_water_rides"),
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"),
path("<slug:park_slug>/roller_coasters/", ParkSingleCategoryListView.as_view(),
{'category': 'RC'}, name="park_roller_coasters"),
path("<slug:park_slug>/dark_rides/", ParkSingleCategoryListView.as_view(),
{'category': 'DR'}, name="park_dark_rides"),
path("<slug:park_slug>/flat_rides/", ParkSingleCategoryListView.as_view(),
{'category': 'FR'}, name="park_flat_rides"),
path("<slug:park_slug>/water_rides/", ParkSingleCategoryListView.as_view(),
{'category': 'WR'}, name="park_water_rides"),
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 park-specific rides URLs
path("<slug:park_slug>/rides/", include("rides.park_urls", namespace="rides")),

View File

@@ -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;
}

View File

@@ -63,8 +63,7 @@
<div class="text-center">
<dt class="text-sm font-semibold text-gray-900 dark:text-white">Operator</dt>
<dd class="mt-1">
<a href="{% url 'operators:operator_detail' park.operator.slug %}"
class="text-sm font-bold text-sky-900 dark:text-sky-400 hover:text-sky-800 dark:hover:text-sky-300">
<span class="text-sm font-bold text-sky-900 dark:text-sky-400">
{{ park.operator.name }}
</a>
</dd>

View File

@@ -41,10 +41,8 @@
{% endif %}
</div>
{% if park.operator %}
<div class="mt-4 text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
<a href="{% url 'operators:operator_detail' park.operator.slug %}">
{{ park.operator.name }}
</a>
<div class="mt-4 text-sm text-blue-600 dark:text-blue-400">
{{ park.operator.name }}
</div>
{% endif %}
</div>

View File

@@ -119,8 +119,7 @@
{% for operator in operators %}
<div class="p-4 rounded-lg bg-gray-50 dark:bg-gray-700">
<h3 class="mb-2 text-lg font-semibold">
<a href="{% url 'operators:operator_detail' operator.slug %}"
class="text-blue-600 hover:underline dark:text-blue-400">
<span class="text-blue-600 dark:text-blue-400">
{{ operator.name }}
</a>
</h3>

View File

@@ -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",
]

View File

@@ -32,25 +32,34 @@ urlpatterns = [
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,8 +102,10 @@ 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: