feat: Implement MFA authentication, add ride statistics model, and update various services, APIs, and tests across the application.

This commit is contained in:
pacnpal
2025-12-28 17:32:53 -05:00
parent aa56c46c27
commit c95f99ca10
452 changed files with 7948 additions and 6073 deletions

View File

@@ -8,15 +8,14 @@ The Company model is aliased as Operator to clarify its role as park operators,
while maintaining backward compatibility through the Company alias.
"""
from .parks import Park
from .areas import ParkArea
from .location import ParkLocation
from .reviews import ParkReview
from .companies import Company, CompanyHeadquarters
from .media import ParkPhoto
# Import choices to trigger registration
from ..choices import *
from .areas import ParkArea
from .companies import Company, CompanyHeadquarters
from .location import ParkLocation
from .media import ParkPhoto
from .parks import Park
from .reviews import ParkReview
# Alias Company as Operator for clarity
Operator = Company

View File

@@ -1,8 +1,9 @@
import pghistory
from django.db import models
from django.utils.text import slugify
import pghistory
from apps.core.history import TrackedModel
from .parks import Park

View File

@@ -1,9 +1,10 @@
import pghistory
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.utils.text import slugify
from apps.core.models import TrackedModel
from apps.core.choices.fields import RichChoiceField
import pghistory
from apps.core.models import TrackedModel
@pghistory.track()

View File

@@ -1,6 +1,6 @@
import pghistory
from django.contrib.gis.db import models
from django.contrib.gis.geos import Point
import pghistory
@pghistory.track()

View File

@@ -4,12 +4,14 @@ Park-specific media models for ThrillWiki.
This module contains media models specific to parks domain.
"""
from typing import Any, List, Optional, cast
from django.db import models
from typing import Any, cast
import pghistory
from django.conf import settings
from django.db import models
from apps.core.history import TrackedModel
from apps.core.services.media_service import MediaService
import pghistory
def park_photo_upload_path(instance: models.Model, filename: str) -> str:
@@ -114,7 +116,7 @@ class ParkPhoto(TrackedModel):
super().save(*args, **kwargs)
@property
def file_size(self) -> Optional[int]:
def file_size(self) -> int | None:
"""Get file size in bytes."""
try:
return self.image.size
@@ -122,7 +124,7 @@ class ParkPhoto(TrackedModel):
return None
@property
def dimensions(self) -> Optional[List[int]]:
def dimensions(self) -> list[int] | None:
"""Get image dimensions as [width, height]."""
try:
return [self.image.width, self.image.height]

View File

@@ -1,20 +1,23 @@
from typing import TYPE_CHECKING, Any, Optional
import pghistory
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.text import slugify
from django.core.exceptions import ValidationError
from config.django import base as settings
from typing import Optional, Any, TYPE_CHECKING, List
import pghistory
from apps.core.history import TrackedModel
from apps.core.choices import RichChoiceField
from apps.core.state_machine import RichFSMField, StateMachineMixin
from apps.core.choices import RichChoiceField
from apps.core.history import TrackedModel
from apps.core.state_machine import RichFSMField, StateMachineMixin
from config.django import base as settings
if TYPE_CHECKING:
from apps.rides.models import Ride
from . import ParkArea
from django.contrib.auth.models import AbstractBaseUser
from apps.rides.models import Ride
from . import ParkArea
@pghistory.track()
class Park(StateMachineMixin, TrackedModel):
@@ -57,6 +60,8 @@ class Park(StateMachineMixin, TrackedModel):
max_digits=10, decimal_places=2, null=True, blank=True, help_text="Park size in acres"
)
website = models.URLField(blank=True, help_text="Official website URL")
phone = models.CharField(max_length=30, blank=True, help_text="Contact phone number")
email = models.EmailField(blank=True, help_text="Contact email address")
# Statistics
average_rating = models.DecimalField(
@@ -113,17 +118,17 @@ class Park(StateMachineMixin, TrackedModel):
# Computed fields for hybrid filtering
opening_year = models.IntegerField(
null=True,
blank=True,
null=True,
blank=True,
db_index=True,
help_text="Year the park opened (computed from opening_date)"
)
search_text = models.TextField(
blank=True,
blank=True,
db_index=True,
help_text="Searchable text combining name, description, location, and operator"
)
# Timezone for park operations
timezone = models.CharField(
max_length=50,
@@ -220,6 +225,7 @@ class Park(StateMachineMixin, TrackedModel):
def save(self, *args: Any, **kwargs: Any) -> None:
from django.contrib.contenttypes.models import ContentType
from apps.core.history import HistoricalSlug
# Get old instance if it exists
@@ -264,13 +270,13 @@ class Park(StateMachineMixin, TrackedModel):
self.opening_year = self.opening_date.year
else:
self.opening_year = None
# Populate search_text for client-side filtering
search_parts = [self.name]
if self.description:
search_parts.append(self.description)
# Add location information if available
try:
if hasattr(self, 'location') and self.location:
@@ -283,15 +289,15 @@ class Park(StateMachineMixin, TrackedModel):
except Exception:
# Handle case where location relationship doesn't exist yet
pass
# Add operator information
if self.operator:
search_parts.append(self.operator.name)
# Add property owner information if different
if self.property_owner and self.property_owner != self.operator:
search_parts.append(self.property_owner.name)
# Combine all parts into searchable text
self.search_text = ' '.join(filter(None, search_parts)).lower()
@@ -315,7 +321,7 @@ class Park(StateMachineMixin, TrackedModel):
return ""
@property
def coordinates(self) -> Optional[List[float]]:
def coordinates(self) -> list[float] | None:
"""Returns coordinates as a list [latitude, longitude]"""
if hasattr(self, "location") and self.location:
coords = self.location.coordinates
@@ -327,6 +333,7 @@ class Park(StateMachineMixin, TrackedModel):
def get_by_slug(cls, slug: str) -> tuple["Park", bool]:
"""Get park by current or historical slug"""
from django.contrib.contenttypes.models import ContentType
from apps.core.history import HistoricalSlug
print(f"\nLooking up slug: {slug}")

View File

@@ -1,8 +1,9 @@
import pghistory
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import functions
from django.core.validators import MinValueValidator, MaxValueValidator
from apps.core.history import TrackedModel
import pghistory
@pghistory.track()