Refactor code structure and remove redundant changes

This commit is contained in:
pacnpal
2025-11-09 16:31:34 -05:00
parent 2884bc23ce
commit eb68cf40c6
1080 changed files with 27361 additions and 56687 deletions

View File

View File

@@ -0,0 +1,11 @@
"""
Core app configuration.
"""
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.core'
verbose_name = 'Core'

View File

@@ -0,0 +1,194 @@
# Generated by Django 4.2.8 on 2025-11-08 16:35
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import django_lifecycle.mixins
import model_utils.fields
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Country",
fields=[
(
"created",
model_utils.fields.AutoCreatedField(
default=django.utils.timezone.now,
editable=False,
verbose_name="created",
),
),
(
"modified",
model_utils.fields.AutoLastModifiedField(
default=django.utils.timezone.now,
editable=False,
verbose_name="modified",
),
),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=255, unique=True)),
(
"code",
models.CharField(
help_text="ISO 3166-1 alpha-2 country code",
max_length=2,
unique=True,
),
),
(
"code3",
models.CharField(
blank=True,
help_text="ISO 3166-1 alpha-3 country code",
max_length=3,
),
),
],
options={
"verbose_name_plural": "countries",
"db_table": "countries",
"ordering": ["name"],
},
bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model),
),
migrations.CreateModel(
name="Subdivision",
fields=[
(
"created",
model_utils.fields.AutoCreatedField(
default=django.utils.timezone.now,
editable=False,
verbose_name="created",
),
),
(
"modified",
model_utils.fields.AutoLastModifiedField(
default=django.utils.timezone.now,
editable=False,
verbose_name="modified",
),
),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=255)),
(
"code",
models.CharField(
help_text="ISO 3166-2 subdivision code (without country prefix)",
max_length=10,
),
),
(
"subdivision_type",
models.CharField(
blank=True,
help_text="Type of subdivision (state, province, region, etc.)",
max_length=50,
),
),
(
"country",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="subdivisions",
to="core.country",
),
),
],
options={
"db_table": "subdivisions",
"ordering": ["country", "name"],
"unique_together": {("country", "code")},
},
bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model),
),
migrations.CreateModel(
name="Locality",
fields=[
(
"created",
model_utils.fields.AutoCreatedField(
default=django.utils.timezone.now,
editable=False,
verbose_name="created",
),
),
(
"modified",
model_utils.fields.AutoLastModifiedField(
default=django.utils.timezone.now,
editable=False,
verbose_name="modified",
),
),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=255)),
(
"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
),
),
(
"subdivision",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="localities",
to="core.subdivision",
),
),
],
options={
"verbose_name_plural": "localities",
"db_table": "localities",
"ordering": ["subdivision", "name"],
"indexes": [
models.Index(
fields=["subdivision", "name"],
name="localities_subdivi_675d5a_idx",
)
],
},
bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model),
),
]

View File

@@ -0,0 +1,240 @@
"""
Core base models and utilities for ThrillWiki.
These abstract models provide common functionality for all entities.
"""
import uuid
from django.db import models
from model_utils.models import TimeStampedModel
from django_lifecycle import LifecycleModel, hook, AFTER_CREATE, AFTER_UPDATE
from dirtyfields import DirtyFieldsMixin
class BaseModel(LifecycleModel, TimeStampedModel):
"""
Abstract base model for all entities.
Provides:
- UUID primary key
- created_at and updated_at timestamps (from TimeStampedModel)
- Lifecycle hooks for versioning
"""
id = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
editable=False
)
class Meta:
abstract = True
def __str__(self):
return f"{self.__class__.__name__}({self.id})"
class VersionedModel(DirtyFieldsMixin, BaseModel):
"""
Abstract base model for entities that track field changes.
Uses DirtyFieldsMixin to track which fields changed.
History tracking is now handled automatically by pghistory decorators.
Note: This class is kept for backwards compatibility and the DirtyFieldsMixin
functionality, but no longer triggers custom versioning.
"""
class Meta:
abstract = True
# Location Models
class Country(BaseModel):
"""
Country reference data (ISO 3166-1).
Examples: United States, Canada, United Kingdom, etc.
"""
name = models.CharField(max_length=255, unique=True)
code = models.CharField(
max_length=2,
unique=True,
help_text="ISO 3166-1 alpha-2 country code"
)
code3 = models.CharField(
max_length=3,
blank=True,
help_text="ISO 3166-1 alpha-3 country code"
)
class Meta:
db_table = 'countries'
ordering = ['name']
verbose_name_plural = 'countries'
def __str__(self):
return self.name
class Subdivision(BaseModel):
"""
State/Province/Region reference data (ISO 3166-2).
Examples: California, Ontario, England, etc.
"""
country = models.ForeignKey(
Country,
on_delete=models.CASCADE,
related_name='subdivisions'
)
name = models.CharField(max_length=255)
code = models.CharField(
max_length=10,
help_text="ISO 3166-2 subdivision code (without country prefix)"
)
subdivision_type = models.CharField(
max_length=50,
blank=True,
help_text="Type of subdivision (state, province, region, etc.)"
)
class Meta:
db_table = 'subdivisions'
ordering = ['country', 'name']
unique_together = [['country', 'code']]
def __str__(self):
return f"{self.name}, {self.country.code}"
class Locality(BaseModel):
"""
City/Town reference data.
Examples: Los Angeles, Toronto, London, etc.
"""
subdivision = models.ForeignKey(
Subdivision,
on_delete=models.CASCADE,
related_name='localities'
)
name = models.CharField(max_length=255)
latitude = models.DecimalField(
max_digits=9,
decimal_places=6,
null=True,
blank=True
)
longitude = models.DecimalField(
max_digits=9,
decimal_places=6,
null=True,
blank=True
)
class Meta:
db_table = 'localities'
ordering = ['subdivision', 'name']
verbose_name_plural = 'localities'
indexes = [
models.Index(fields=['subdivision', 'name']),
]
def __str__(self):
return f"{self.name}, {self.subdivision.code}"
@property
def full_location(self):
"""Return full location string: City, State, Country"""
return f"{self.name}, {self.subdivision.name}, {self.subdivision.country.name}"
# Date Precision Tracking
class DatePrecisionMixin(models.Model):
"""
Mixin for models that need to track date precision.
Allows tracking whether a date is known to year, month, or day precision.
This is important for historical records where exact dates may not be known.
"""
DATE_PRECISION_CHOICES = [
('year', 'Year'),
('month', 'Month'),
('day', 'Day'),
]
class Meta:
abstract = True
@classmethod
def add_date_precision_field(cls, field_name):
"""
Helper to add a precision field for a date field.
Usage in subclass:
opening_date = models.DateField(null=True, blank=True)
opening_date_precision = models.CharField(...)
"""
return models.CharField(
max_length=20,
choices=cls.DATE_PRECISION_CHOICES,
default='day',
help_text=f"Precision level for {field_name}"
)
# Soft Delete Mixin
class SoftDeleteMixin(models.Model):
"""
Mixin for soft-deletable models.
Instead of actually deleting records, mark them as deleted.
This preserves data integrity and allows for undelete functionality.
"""
is_deleted = models.BooleanField(default=False, db_index=True)
deleted_at = models.DateTimeField(null=True, blank=True)
deleted_by = models.ForeignKey(
'users.User',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='%(class)s_deletions'
)
class Meta:
abstract = True
def soft_delete(self, user=None):
"""Mark this record as deleted"""
from django.utils import timezone
self.is_deleted = True
self.deleted_at = timezone.now()
if user:
self.deleted_by = user
self.save(update_fields=['is_deleted', 'deleted_at', 'deleted_by'])
def undelete(self):
"""Restore a soft-deleted record"""
self.is_deleted = False
self.deleted_at = None
self.deleted_by = None
self.save(update_fields=['is_deleted', 'deleted_at', 'deleted_by'])
# Model Managers
class ActiveManager(models.Manager):
"""Manager that filters out soft-deleted records by default"""
def get_queryset(self):
return super().get_queryset().filter(is_deleted=False)
class AllObjectsManager(models.Manager):
"""Manager that includes all records, even soft-deleted ones"""
def get_queryset(self):
return super().get_queryset()

View File

@@ -0,0 +1,119 @@
"""
Django Sitemaps for SEO
Generates XML sitemaps for search engine crawlers to discover and index content.
"""
from django.contrib.sitemaps import Sitemap
from django.urls import reverse
from apps.entities.models import Park, Ride, Company, RideModel
class ParkSitemap(Sitemap):
"""Sitemap for theme parks."""
changefreq = "weekly"
priority = 0.9
def items(self):
"""Return all active parks."""
return Park.objects.filter(is_active=True).order_by('-updated')
def lastmod(self, obj):
"""Return last modification date."""
return obj.updated
def location(self, obj):
"""Return URL for park."""
return f'/parks/{obj.slug}/'
class RideSitemap(Sitemap):
"""Sitemap for rides."""
changefreq = "weekly"
priority = 0.8
def items(self):
"""Return all active rides."""
return Ride.objects.filter(
is_active=True
).select_related('park').order_by('-updated')
def lastmod(self, obj):
"""Return last modification date."""
return obj.updated
def location(self, obj):
"""Return URL for ride."""
return f'/parks/{obj.park.slug}/rides/{obj.slug}/'
class CompanySitemap(Sitemap):
"""Sitemap for companies/manufacturers."""
changefreq = "monthly"
priority = 0.6
def items(self):
"""Return all active companies."""
return Company.objects.filter(is_active=True).order_by('-updated')
def lastmod(self, obj):
"""Return last modification date."""
return obj.updated
def location(self, obj):
"""Return URL for company."""
return f'/manufacturers/{obj.slug}/'
class RideModelSitemap(Sitemap):
"""Sitemap for ride models."""
changefreq = "monthly"
priority = 0.7
def items(self):
"""Return all active ride models."""
return RideModel.objects.filter(
is_active=True
).select_related('manufacturer').order_by('-updated')
def lastmod(self, obj):
"""Return last modification date."""
return obj.updated
def location(self, obj):
"""Return URL for ride model."""
return f'/models/{obj.slug}/'
class StaticSitemap(Sitemap):
"""Sitemap for static pages."""
changefreq = "monthly"
priority = 0.5
def items(self):
"""Return list of static pages."""
return ['home', 'about', 'privacy', 'terms']
def location(self, item):
"""Return URL for static page."""
if item == 'home':
return '/'
return f'/{item}/'
def changefreq(self, item):
"""Home page changes more frequently."""
if item == 'home':
return 'daily'
return 'monthly'
def priority(self, item):
"""Home page has higher priority."""
if item == 'home':
return 1.0
return 0.5

View File

@@ -0,0 +1,340 @@
"""
SEO Meta Tag Generation Utilities
Generates comprehensive meta tags for social sharing (OpenGraph, Twitter Cards),
search engines (structured data), and general SEO optimization.
"""
from typing import Dict, Optional
from django.conf import settings
class SEOTags:
"""Generate comprehensive SEO meta tags for any page."""
BASE_URL = getattr(settings, 'SITE_URL', 'https://thrillwiki.com')
DEFAULT_OG_IMAGE = f"{BASE_URL}/static/images/og-default.png"
TWITTER_HANDLE = "@thrillwiki"
SITE_NAME = "ThrillWiki"
@classmethod
def for_park(cls, park) -> Dict[str, str]:
"""
Generate meta tags for a park page.
Args:
park: Park model instance
Returns:
Dictionary of meta tags for HTML head
"""
title = f"{park.name} - Theme Park Database | ThrillWiki"
description = f"Explore {park.name} in {park.locality.name}, {park.country.name}. View rides, reviews, photos, and history on ThrillWiki."
og_image = cls._get_og_image_url('park', str(park.id))
url = f"{cls.BASE_URL}/parks/{park.slug}/"
return {
# Basic Meta
'title': title,
'description': description,
'keywords': f"{park.name}, theme park, amusement park, {park.locality.name}, {park.country.name}",
# OpenGraph (Facebook, LinkedIn, Discord)
'og:title': park.name,
'og:description': description,
'og:type': 'website',
'og:url': url,
'og:image': og_image,
'og:image:width': '1200',
'og:image:height': '630',
'og:site_name': cls.SITE_NAME,
'og:locale': 'en_US',
# Twitter Card
'twitter:card': 'summary_large_image',
'twitter:site': cls.TWITTER_HANDLE,
'twitter:title': park.name,
'twitter:description': description,
'twitter:image': og_image,
# Additional
'canonical': url,
}
@classmethod
def for_ride(cls, ride) -> Dict[str, str]:
"""
Generate meta tags for a ride page.
Args:
ride: Ride model instance
Returns:
Dictionary of meta tags for HTML head
"""
title = f"{ride.name} at {ride.park.name} | ThrillWiki"
# Build description with available details
description_parts = [
f"{ride.name} is a {ride.ride_type.name}",
f"at {ride.park.name}",
]
if ride.opened_year:
description_parts.append(f"Built in {ride.opened_year}")
if ride.manufacturer:
description_parts.append(f"by {ride.manufacturer.name}")
description = ". ".join(description_parts) + ". Read reviews and view photos."
og_image = cls._get_og_image_url('ride', str(ride.id))
url = f"{cls.BASE_URL}/parks/{ride.park.slug}/rides/{ride.slug}/"
keywords_parts = [
ride.name,
ride.ride_type.name,
ride.park.name,
]
if ride.manufacturer:
keywords_parts.append(ride.manufacturer.name)
keywords_parts.extend(['roller coaster', 'theme park ride'])
return {
'title': title,
'description': description,
'keywords': ', '.join(keywords_parts),
# OpenGraph
'og:title': f"{ride.name} at {ride.park.name}",
'og:description': description,
'og:type': 'article',
'og:url': url,
'og:image': og_image,
'og:image:width': '1200',
'og:image:height': '630',
'og:site_name': cls.SITE_NAME,
'og:locale': 'en_US',
# Twitter
'twitter:card': 'summary_large_image',
'twitter:site': cls.TWITTER_HANDLE,
'twitter:title': f"{ride.name} at {ride.park.name}",
'twitter:description': description,
'twitter:image': og_image,
'canonical': url,
}
@classmethod
def for_company(cls, company) -> Dict[str, str]:
"""
Generate meta tags for a manufacturer/company page.
Args:
company: Company model instance
Returns:
Dictionary of meta tags for HTML head
"""
# Get company type name safely
company_type_name = company.company_types.first().name if company.company_types.exists() else "Company"
title = f"{company.name} - {company_type_name} | ThrillWiki"
description = f"{company.name} is a {company_type_name}. View their rides, history, and contributions to the theme park industry."
url = f"{cls.BASE_URL}/manufacturers/{company.slug}/"
return {
'title': title,
'description': description,
'keywords': f"{company.name}, {company_type_name}, theme park manufacturer, ride manufacturer",
# OpenGraph
'og:title': company.name,
'og:description': description,
'og:type': 'website',
'og:url': url,
'og:image': cls.DEFAULT_OG_IMAGE,
'og:image:width': '1200',
'og:image:height': '630',
'og:site_name': cls.SITE_NAME,
'og:locale': 'en_US',
# Twitter
'twitter:card': 'summary',
'twitter:site': cls.TWITTER_HANDLE,
'twitter:title': company.name,
'twitter:description': description,
'twitter:image': cls.DEFAULT_OG_IMAGE,
'canonical': url,
}
@classmethod
def for_ride_model(cls, model) -> Dict[str, str]:
"""
Generate meta tags for a ride model page.
Args:
model: RideModel model instance
Returns:
Dictionary of meta tags for HTML head
"""
title = f"{model.name} by {model.manufacturer.name} | ThrillWiki"
description = f"The {model.name} is a {model.ride_type.name} model manufactured by {model.manufacturer.name}. View installations and specifications."
url = f"{cls.BASE_URL}/models/{model.slug}/"
return {
'title': title,
'description': description,
'keywords': f"{model.name}, {model.manufacturer.name}, {model.ride_type.name}, ride model, theme park",
# OpenGraph
'og:title': f"{model.name} by {model.manufacturer.name}",
'og:description': description,
'og:type': 'website',
'og:url': url,
'og:image': cls.DEFAULT_OG_IMAGE,
'og:image:width': '1200',
'og:image:height': '630',
'og:site_name': cls.SITE_NAME,
'og:locale': 'en_US',
# Twitter
'twitter:card': 'summary',
'twitter:site': cls.TWITTER_HANDLE,
'twitter:title': f"{model.name} by {model.manufacturer.name}",
'twitter:description': description,
'twitter:image': cls.DEFAULT_OG_IMAGE,
'canonical': url,
}
@classmethod
def for_home(cls) -> Dict[str, str]:
"""Generate meta tags for home page."""
title = "ThrillWiki - The Ultimate Theme Park & Roller Coaster Database"
description = "Explore thousands of theme parks and roller coasters worldwide. Read reviews, view photos, track your ride credits, and discover your next adventure."
return {
'title': title,
'description': description,
'keywords': 'theme parks, roller coasters, amusement parks, ride database, coaster enthusiasts, thrillwiki',
'og:title': title,
'og:description': description,
'og:type': 'website',
'og:url': cls.BASE_URL,
'og:image': cls.DEFAULT_OG_IMAGE,
'og:image:width': '1200',
'og:image:height': '630',
'og:site_name': cls.SITE_NAME,
'og:locale': 'en_US',
'twitter:card': 'summary_large_image',
'twitter:site': cls.TWITTER_HANDLE,
'twitter:title': title,
'twitter:description': description,
'twitter:image': cls.DEFAULT_OG_IMAGE,
'canonical': cls.BASE_URL,
}
@staticmethod
def _get_og_image_url(entity_type: str, entity_id: str) -> str:
"""
Generate dynamic OG image URL.
Args:
entity_type: Type of entity (park, ride, company, model)
entity_id: Entity ID
Returns:
URL to dynamic OG image endpoint
"""
# Use existing ssrOG endpoint
return f"{SEOTags.BASE_URL}/api/og?type={entity_type}&id={entity_id}"
@classmethod
def structured_data_for_park(cls, park) -> dict:
"""
Generate JSON-LD structured data for a park.
Args:
park: Park model instance
Returns:
Dictionary for JSON-LD script tag
"""
data = {
"@context": "https://schema.org",
"@type": "TouristAttraction",
"name": park.name,
"description": f"Theme park in {park.locality.name}, {park.country.name}",
"url": f"{cls.BASE_URL}/parks/{park.slug}/",
"image": cls._get_og_image_url('park', str(park.id)),
"address": {
"@type": "PostalAddress",
"addressLocality": park.locality.name,
"addressCountry": park.country.code,
},
}
# Add geo coordinates if available
if hasattr(park, 'latitude') and hasattr(park, 'longitude') and park.latitude and park.longitude:
data["geo"] = {
"@type": "GeoCoordinates",
"latitude": str(park.latitude),
"longitude": str(park.longitude),
}
# Add aggregate rating if available
if hasattr(park, 'review_count') and park.review_count > 0:
data["aggregateRating"] = {
"@type": "AggregateRating",
"ratingValue": str(park.average_rating),
"reviewCount": park.review_count,
}
return data
@classmethod
def structured_data_for_ride(cls, ride) -> dict:
"""
Generate JSON-LD structured data for a ride.
Args:
ride: Ride model instance
Returns:
Dictionary for JSON-LD script tag
"""
data = {
"@context": "https://schema.org",
"@type": "Product",
"name": ride.name,
"description": f"{ride.name} is a {ride.ride_type.name} at {ride.park.name}",
"url": f"{cls.BASE_URL}/parks/{ride.park.slug}/rides/{ride.slug}/",
"image": cls._get_og_image_url('ride', str(ride.id)),
}
# Add manufacturer if available
if ride.manufacturer:
data["manufacturer"] = {
"@type": "Organization",
"name": ride.manufacturer.name,
}
# Add aggregate rating if available
if hasattr(ride, 'review_count') and ride.review_count > 0:
data["aggregateRating"] = {
"@type": "AggregateRating",
"ratingValue": str(ride.average_rating),
"reviewCount": ride.review_count,
}
return data