mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-31 05:07:02 -05:00
feat: Implement MFA authentication, add ride statistics model, and update various services, APIs, and tests across the application.
This commit is contained in:
@@ -1,14 +1,18 @@
|
||||
import contextlib
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pghistory
|
||||
from django.contrib.auth.models import AbstractBaseUser
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.text import slugify
|
||||
from django.core.exceptions import ValidationError
|
||||
from config.django import base as settings
|
||||
from apps.core.models import TrackedModel
|
||||
|
||||
from apps.core.choices import RichChoiceField
|
||||
from apps.core.models import TrackedModel
|
||||
from apps.core.state_machine import RichFSMField, StateMachineMixin
|
||||
from config.django import base as settings
|
||||
|
||||
from .company import Company
|
||||
import pghistory
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from django.contrib.auth.models import AbstractBaseUser
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .rides import RollerCoasterStats
|
||||
@@ -498,7 +502,7 @@ class Ride(StateMachineMixin, TrackedModel):
|
||||
Note: The average_rating field is denormalized and refreshed by background
|
||||
jobs. Use selectors or annotations for real-time calculations if needed.
|
||||
"""
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
coaster_stats: 'RollerCoasterStats'
|
||||
|
||||
@@ -669,17 +673,17 @@ class Ride(StateMachineMixin, TrackedModel):
|
||||
return f"{self.name} at {self.park.name}"
|
||||
|
||||
# FSM Transition Wrapper Methods
|
||||
def open(self, *, user: Optional[AbstractBaseUser] = None) -> None:
|
||||
def open(self, *, user: AbstractBaseUser | None = None) -> None:
|
||||
"""Transition ride to OPERATING status."""
|
||||
self.transition_to_operating(user=user)
|
||||
self.save()
|
||||
|
||||
def close_temporarily(self, *, user: Optional[AbstractBaseUser] = None) -> None:
|
||||
def close_temporarily(self, *, user: AbstractBaseUser | None = None) -> None:
|
||||
"""Transition ride to CLOSED_TEMP status."""
|
||||
self.transition_to_closed_temp(user=user)
|
||||
self.save()
|
||||
|
||||
def mark_sbno(self, *, user: Optional[AbstractBaseUser] = None) -> None:
|
||||
def mark_sbno(self, *, user: AbstractBaseUser | None = None) -> None:
|
||||
"""Transition ride to SBNO (Standing But Not Operating) status."""
|
||||
self.transition_to_sbno(user=user)
|
||||
self.save()
|
||||
@@ -689,7 +693,7 @@ class Ride(StateMachineMixin, TrackedModel):
|
||||
*,
|
||||
closing_date,
|
||||
post_closing_status: str,
|
||||
user: Optional[AbstractBaseUser] = None,
|
||||
user: AbstractBaseUser | None = None,
|
||||
) -> None:
|
||||
"""Transition ride to CLOSING status with closing date and target status."""
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -703,25 +707,25 @@ class Ride(StateMachineMixin, TrackedModel):
|
||||
self.post_closing_status = post_closing_status
|
||||
self.save()
|
||||
|
||||
def close_permanently(self, *, user: Optional[AbstractBaseUser] = None) -> None:
|
||||
def close_permanently(self, *, user: AbstractBaseUser | None = None) -> None:
|
||||
"""Transition ride to CLOSED_PERM status."""
|
||||
self.transition_to_closed_perm(user=user)
|
||||
self.save()
|
||||
|
||||
def demolish(self, *, user: Optional[AbstractBaseUser] = None) -> None:
|
||||
def demolish(self, *, user: AbstractBaseUser | None = None) -> None:
|
||||
"""Transition ride to DEMOLISHED status."""
|
||||
self.transition_to_demolished(user=user)
|
||||
self.save()
|
||||
|
||||
def relocate(self, *, user: Optional[AbstractBaseUser] = None) -> None:
|
||||
def relocate(self, *, user: AbstractBaseUser | None = None) -> None:
|
||||
"""Transition ride to RELOCATED status."""
|
||||
self.transition_to_relocated(user=user)
|
||||
self.save()
|
||||
|
||||
def apply_post_closing_status(self, *, user: Optional[AbstractBaseUser] = None) -> None:
|
||||
def apply_post_closing_status(self, *, user: AbstractBaseUser | None = None) -> None:
|
||||
"""Apply post_closing_status if closing_date has been reached."""
|
||||
from django.utils import timezone
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils import timezone
|
||||
|
||||
if self.status != "CLOSING":
|
||||
raise ValidationError("Ride must be in CLOSING status")
|
||||
@@ -757,10 +761,8 @@ class Ride(StateMachineMixin, TrackedModel):
|
||||
# Check for slug conflicts when park changes or slug is new
|
||||
original_ride = None
|
||||
if self.pk:
|
||||
try:
|
||||
with contextlib.suppress(Ride.DoesNotExist):
|
||||
original_ride = Ride.objects.get(pk=self.pk)
|
||||
except Ride.DoesNotExist:
|
||||
pass
|
||||
|
||||
# If park changed or this is a new ride, ensure slug uniqueness within the park
|
||||
park_changed = original_ride and original_ride.park.id != self.park.id
|
||||
@@ -805,13 +807,13 @@ class Ride(StateMachineMixin, TrackedModel):
|
||||
|
||||
# Build comprehensive search text
|
||||
search_parts = []
|
||||
|
||||
|
||||
# Basic ride info
|
||||
if self.name:
|
||||
search_parts.append(self.name)
|
||||
if self.description:
|
||||
search_parts.append(self.description)
|
||||
|
||||
|
||||
# Park info
|
||||
if self.park:
|
||||
search_parts.append(self.park.name)
|
||||
@@ -822,39 +824,39 @@ class Ride(StateMachineMixin, TrackedModel):
|
||||
search_parts.append(self.park.location.state)
|
||||
if self.park.location.country:
|
||||
search_parts.append(self.park.location.country)
|
||||
|
||||
|
||||
# Park area
|
||||
if self.park_area:
|
||||
search_parts.append(self.park_area.name)
|
||||
|
||||
|
||||
# Category
|
||||
if self.category:
|
||||
category_choice = self.get_category_rich_choice()
|
||||
if category_choice:
|
||||
search_parts.append(category_choice.label)
|
||||
|
||||
|
||||
# Status
|
||||
if self.status:
|
||||
status_choice = self.get_status_rich_choice()
|
||||
if status_choice:
|
||||
search_parts.append(status_choice.label)
|
||||
|
||||
|
||||
# Companies
|
||||
if self.manufacturer:
|
||||
search_parts.append(self.manufacturer.name)
|
||||
if self.designer:
|
||||
search_parts.append(self.designer.name)
|
||||
|
||||
|
||||
# Ride model
|
||||
if self.ride_model:
|
||||
search_parts.append(self.ride_model.name)
|
||||
if self.ride_model.manufacturer:
|
||||
search_parts.append(self.ride_model.manufacturer.name)
|
||||
|
||||
|
||||
# Roller coaster stats if available
|
||||
try:
|
||||
if hasattr(self, 'coaster_stats') and self.coaster_stats:
|
||||
stats = self.coaster_stats
|
||||
if hasattr(self, 'coaster_stats') and self.coaster_stats:
|
||||
stats = self.coaster_stats
|
||||
if stats.track_type:
|
||||
search_parts.append(stats.track_type)
|
||||
if stats.track_material:
|
||||
@@ -874,7 +876,7 @@ class Ride(StateMachineMixin, TrackedModel):
|
||||
except Exception:
|
||||
# Ignore if coaster_stats doesn't exist or has issues
|
||||
pass
|
||||
|
||||
|
||||
self.search_text = ' '.join(filter(None, search_parts)).lower()
|
||||
|
||||
def _ensure_unique_slug_in_park(self) -> None:
|
||||
@@ -947,6 +949,7 @@ class Ride(StateMachineMixin, TrackedModel):
|
||||
def get_by_slug(cls, slug: str, park=None) -> tuple["Ride", bool]:
|
||||
"""Get ride by current or historical slug, optionally within a specific park"""
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from apps.core.history import HistoricalSlug
|
||||
|
||||
# Build base query
|
||||
@@ -975,7 +978,7 @@ class Ride(StateMachineMixin, TrackedModel):
|
||||
event_model = getattr(cls, "event_model", None)
|
||||
if event_model:
|
||||
historical_events = event_model.objects.filter(slug=slug).order_by("-pgh_created_at")
|
||||
|
||||
|
||||
for historical_event in historical_events:
|
||||
try:
|
||||
ride = base_query.get(pk=historical_event.pgh_obj_id)
|
||||
|
||||
Reference in New Issue
Block a user