mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 17:51:08 -05:00
- Added migration to convert unique_together constraints to UniqueConstraint for RideModel. - Introduced RideFormMixin for handling entity suggestions in ride forms. - Created comprehensive code standards documentation outlining formatting, docstring requirements, complexity guidelines, and testing requirements. - Established error handling guidelines with a structured exception hierarchy and best practices for API and view error handling. - Documented view pattern guidelines, emphasizing the use of CBVs, FBVs, and ViewSets with examples. - Implemented a benchmarking script for query performance analysis and optimization. - Developed security documentation detailing measures, configurations, and a security checklist. - Compiled a database optimization guide covering indexing strategies, query optimization patterns, and computed fields.
513 lines
16 KiB
Python
513 lines
16 KiB
Python
"""
|
|
Services for park-related business logic.
|
|
Following Django styleguide pattern for business logic encapsulation.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional, Dict, Any, List, TYPE_CHECKING
|
|
from django.db import transaction
|
|
from django.db.models import Q
|
|
from django.core.files.uploadedfile import UploadedFile
|
|
|
|
if TYPE_CHECKING:
|
|
from django.contrib.auth.models import AbstractUser
|
|
|
|
from ..models import Park, ParkArea, ParkPhoto
|
|
from ..models.location import ParkLocation
|
|
from .location_service import ParkLocationService
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ParkService:
|
|
"""Service for managing park operations."""
|
|
|
|
@staticmethod
|
|
def create_park(
|
|
*,
|
|
name: str,
|
|
description: str = "",
|
|
status: str = "OPERATING",
|
|
operator_id: Optional[int] = None,
|
|
property_owner_id: Optional[int] = None,
|
|
opening_date: Optional[str] = None,
|
|
closing_date: Optional[str] = None,
|
|
operating_season: str = "",
|
|
size_acres: Optional[float] = None,
|
|
website: str = "",
|
|
location_data: Optional[Dict[str, Any]] = None,
|
|
created_by: Optional["AbstractUser"] = None,
|
|
) -> Park:
|
|
"""
|
|
Create a new park with validation and location handling.
|
|
|
|
Args:
|
|
name: Park name
|
|
description: Park description
|
|
status: Operating status
|
|
operator_id: ID of operating company
|
|
property_owner_id: ID of property owner company
|
|
opening_date: Opening date
|
|
closing_date: Closing date
|
|
operating_season: Operating season description
|
|
size_acres: Park size in acres
|
|
website: Park website URL
|
|
location_data: Dictionary containing location information
|
|
created_by: User creating the park
|
|
|
|
Returns:
|
|
Created Park instance
|
|
|
|
Raises:
|
|
ValidationError: If park data is invalid
|
|
"""
|
|
with transaction.atomic():
|
|
# Create park instance
|
|
park = Park(
|
|
name=name,
|
|
description=description,
|
|
status=status,
|
|
opening_date=opening_date,
|
|
closing_date=closing_date,
|
|
operating_season=operating_season,
|
|
size_acres=size_acres,
|
|
website=website,
|
|
)
|
|
|
|
# Set foreign key relationships if provided
|
|
if operator_id:
|
|
from apps.parks.models import Company
|
|
|
|
park.operator = Company.objects.get(id=operator_id)
|
|
|
|
if property_owner_id:
|
|
from apps.parks.models import Company
|
|
|
|
park.property_owner = Company.objects.get(id=property_owner_id)
|
|
|
|
# CRITICAL STYLEGUIDE FIX: Call full_clean before save
|
|
park.full_clean()
|
|
park.save()
|
|
|
|
# Handle location if provided
|
|
if location_data:
|
|
ParkLocationService.create_park_location(park=park, **location_data)
|
|
|
|
return park
|
|
|
|
@staticmethod
|
|
def update_park(
|
|
*,
|
|
park_id: int,
|
|
updates: Dict[str, Any],
|
|
updated_by: Optional["AbstractUser"] = None,
|
|
) -> Park:
|
|
"""
|
|
Update an existing park with validation.
|
|
|
|
Args:
|
|
park_id: ID of park to update
|
|
updates: Dictionary of field updates
|
|
updated_by: User performing the update
|
|
|
|
Returns:
|
|
Updated Park instance
|
|
|
|
Raises:
|
|
Park.DoesNotExist: If park doesn't exist
|
|
ValidationError: If update data is invalid
|
|
"""
|
|
with transaction.atomic():
|
|
park = Park.objects.select_for_update().get(id=park_id)
|
|
|
|
# Apply updates
|
|
for field, value in updates.items():
|
|
if hasattr(park, field):
|
|
setattr(park, field, value)
|
|
|
|
# CRITICAL STYLEGUIDE FIX: Call full_clean before save
|
|
park.full_clean()
|
|
park.save()
|
|
|
|
return park
|
|
|
|
@staticmethod
|
|
def delete_park(
|
|
*, park_id: int, deleted_by: Optional["AbstractUser"] = None
|
|
) -> bool:
|
|
"""
|
|
Soft delete a park by setting status to DEMOLISHED.
|
|
|
|
Args:
|
|
park_id: ID of park to delete
|
|
deleted_by: User performing the deletion
|
|
|
|
Returns:
|
|
True if successfully deleted
|
|
|
|
Raises:
|
|
Park.DoesNotExist: If park doesn't exist
|
|
"""
|
|
with transaction.atomic():
|
|
park = Park.objects.select_for_update().get(id=park_id)
|
|
park.status = "DEMOLISHED"
|
|
|
|
# CRITICAL STYLEGUIDE FIX: Call full_clean before save
|
|
park.full_clean()
|
|
park.save()
|
|
|
|
return True
|
|
|
|
@staticmethod
|
|
def create_park_area(
|
|
*,
|
|
park_id: int,
|
|
name: str,
|
|
description: str = "",
|
|
created_by: Optional["AbstractUser"] = None,
|
|
) -> ParkArea:
|
|
"""
|
|
Create a new area within a park.
|
|
|
|
Args:
|
|
park_id: ID of the parent park
|
|
name: Area name
|
|
description: Area description
|
|
created_by: User creating the area
|
|
|
|
Returns:
|
|
Created ParkArea instance
|
|
|
|
Raises:
|
|
Park.DoesNotExist: If park doesn't exist
|
|
ValidationError: If area data is invalid
|
|
"""
|
|
park = Park.objects.get(id=park_id)
|
|
|
|
area = ParkArea(park=park, name=name, description=description)
|
|
|
|
# CRITICAL STYLEGUIDE FIX: Call full_clean before save
|
|
area.full_clean()
|
|
area.save()
|
|
|
|
return area
|
|
|
|
@staticmethod
|
|
def update_park_statistics(*, park_id: int) -> Park:
|
|
"""
|
|
Recalculate and update park statistics (ride counts, ratings).
|
|
|
|
Args:
|
|
park_id: ID of park to update statistics for
|
|
|
|
Returns:
|
|
Updated Park instance with fresh statistics
|
|
"""
|
|
from apps.rides.models import Ride
|
|
from apps.parks.models import ParkReview
|
|
from django.db.models import Count, Avg
|
|
|
|
with transaction.atomic():
|
|
park = Park.objects.select_for_update().get(id=park_id)
|
|
|
|
# Calculate ride counts
|
|
ride_stats = Ride.objects.filter(park=park).aggregate(
|
|
total_rides=Count("id"),
|
|
coaster_count=Count("id", filter=Q(category__in=["RC", "WC"])),
|
|
)
|
|
|
|
# Calculate average rating
|
|
avg_rating = ParkReview.objects.filter(
|
|
park=park, is_published=True
|
|
).aggregate(avg_rating=Avg("rating"))["avg_rating"]
|
|
|
|
# Update park fields
|
|
park.ride_count = ride_stats["total_rides"] or 0
|
|
park.coaster_count = ride_stats["coaster_count"] or 0
|
|
park.average_rating = avg_rating
|
|
|
|
# CRITICAL STYLEGUIDE FIX: Call full_clean before save
|
|
park.full_clean()
|
|
park.save()
|
|
|
|
return park
|
|
|
|
@staticmethod
|
|
def create_park_with_moderation(
|
|
*,
|
|
changes: Dict[str, Any],
|
|
submitter: "AbstractUser",
|
|
reason: str = "",
|
|
source: str = "",
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Create a park through the moderation system.
|
|
|
|
Args:
|
|
changes: Dictionary of park data
|
|
submitter: User submitting the park
|
|
reason: Reason for submission
|
|
source: Source of information
|
|
|
|
Returns:
|
|
Dictionary with status and created object (if auto-approved)
|
|
"""
|
|
from apps.moderation.services import ModerationService
|
|
|
|
return ModerationService.create_edit_submission_with_queue(
|
|
content_object=None,
|
|
changes=changes,
|
|
submitter=submitter,
|
|
submission_type="CREATE",
|
|
reason=reason,
|
|
source=source,
|
|
)
|
|
|
|
@staticmethod
|
|
def update_park_with_moderation(
|
|
*,
|
|
park: Park,
|
|
changes: Dict[str, Any],
|
|
submitter: "AbstractUser",
|
|
reason: str = "",
|
|
source: str = "",
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Update a park through the moderation system.
|
|
|
|
Args:
|
|
park: Park instance to update
|
|
changes: Dictionary of changes
|
|
submitter: User submitting the update
|
|
reason: Reason for submission
|
|
source: Source of information
|
|
|
|
Returns:
|
|
Dictionary with status and updated object (if auto-approved)
|
|
"""
|
|
from apps.moderation.services import ModerationService
|
|
|
|
return ModerationService.create_edit_submission_with_queue(
|
|
content_object=park,
|
|
changes=changes,
|
|
submitter=submitter,
|
|
submission_type="EDIT",
|
|
reason=reason,
|
|
source=source,
|
|
)
|
|
|
|
@staticmethod
|
|
def create_or_update_location(
|
|
*,
|
|
park: Park,
|
|
latitude: Optional[float],
|
|
longitude: Optional[float],
|
|
street_address: str = "",
|
|
city: str = "",
|
|
state: str = "",
|
|
country: str = "USA",
|
|
postal_code: str = "",
|
|
) -> Optional[ParkLocation]:
|
|
"""
|
|
Create or update a park's location.
|
|
|
|
Args:
|
|
park: Park instance
|
|
latitude: Latitude coordinate
|
|
longitude: Longitude coordinate
|
|
street_address: Street address
|
|
city: City name
|
|
state: State/region
|
|
country: Country (default: USA)
|
|
postal_code: Postal/ZIP code
|
|
|
|
Returns:
|
|
ParkLocation instance or None if no coordinates provided
|
|
"""
|
|
if not latitude or not longitude:
|
|
return None
|
|
|
|
try:
|
|
park_location = park.location
|
|
# Update existing location
|
|
park_location.street_address = street_address
|
|
park_location.city = city
|
|
park_location.state = state
|
|
park_location.country = country or "USA"
|
|
park_location.postal_code = postal_code
|
|
park_location.set_coordinates(float(latitude), float(longitude))
|
|
park_location.save()
|
|
return park_location
|
|
except ParkLocation.DoesNotExist:
|
|
# Create new location
|
|
park_location = ParkLocation.objects.create(
|
|
park=park,
|
|
street_address=street_address,
|
|
city=city,
|
|
state=state,
|
|
country=country or "USA",
|
|
postal_code=postal_code,
|
|
)
|
|
park_location.set_coordinates(float(latitude), float(longitude))
|
|
park_location.save()
|
|
return park_location
|
|
|
|
@staticmethod
|
|
def upload_photos(
|
|
*,
|
|
park: Park,
|
|
photos: List[UploadedFile],
|
|
uploaded_by: "AbstractUser",
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Upload multiple photos for a park.
|
|
|
|
Args:
|
|
park: Park instance
|
|
photos: List of uploaded photo files
|
|
uploaded_by: User uploading the photos
|
|
|
|
Returns:
|
|
Dictionary with uploaded_count and errors list
|
|
"""
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
uploaded_count = 0
|
|
errors: List[str] = []
|
|
|
|
for photo_file in photos:
|
|
try:
|
|
ParkPhoto.objects.create(
|
|
image=photo_file,
|
|
uploaded_by=uploaded_by,
|
|
park=park,
|
|
)
|
|
uploaded_count += 1
|
|
except Exception as e:
|
|
error_msg = f"Error uploading photo {photo_file.name}: {str(e)}"
|
|
errors.append(error_msg)
|
|
logger.warning(error_msg)
|
|
|
|
return {
|
|
"uploaded_count": uploaded_count,
|
|
"errors": errors,
|
|
}
|
|
|
|
@staticmethod
|
|
def handle_park_creation_result(
|
|
*,
|
|
result: Dict[str, Any],
|
|
form_data: Dict[str, Any],
|
|
photos: List[UploadedFile],
|
|
user: "AbstractUser",
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Handle the result of park creation through moderation.
|
|
|
|
Args:
|
|
result: Result from create_park_with_moderation
|
|
form_data: Cleaned form data containing location info
|
|
photos: List of uploaded photo files
|
|
user: User who submitted
|
|
|
|
Returns:
|
|
Dictionary with status, park (if created), uploaded_count, and errors
|
|
"""
|
|
response: Dict[str, Any] = {
|
|
"status": result["status"],
|
|
"park": None,
|
|
"uploaded_count": 0,
|
|
"errors": [],
|
|
}
|
|
|
|
if result["status"] == "auto_approved":
|
|
park = result["created_object"]
|
|
response["park"] = park
|
|
|
|
# Create location
|
|
ParkService.create_or_update_location(
|
|
park=park,
|
|
latitude=form_data.get("latitude"),
|
|
longitude=form_data.get("longitude"),
|
|
street_address=form_data.get("street_address", ""),
|
|
city=form_data.get("city", ""),
|
|
state=form_data.get("state", ""),
|
|
country=form_data.get("country", "USA"),
|
|
postal_code=form_data.get("postal_code", ""),
|
|
)
|
|
|
|
# Upload photos
|
|
if photos:
|
|
photo_result = ParkService.upload_photos(
|
|
park=park,
|
|
photos=photos,
|
|
uploaded_by=user,
|
|
)
|
|
response["uploaded_count"] = photo_result["uploaded_count"]
|
|
response["errors"] = photo_result["errors"]
|
|
|
|
elif result["status"] == "failed":
|
|
response["message"] = result.get("message", "Creation failed")
|
|
|
|
return response
|
|
|
|
@staticmethod
|
|
def handle_park_update_result(
|
|
*,
|
|
result: Dict[str, Any],
|
|
park: Park,
|
|
form_data: Dict[str, Any],
|
|
photos: List[UploadedFile],
|
|
user: "AbstractUser",
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Handle the result of park update through moderation.
|
|
|
|
Args:
|
|
result: Result from update_park_with_moderation
|
|
park: Original park instance (for queued submissions)
|
|
form_data: Cleaned form data containing location info
|
|
photos: List of uploaded photo files
|
|
user: User who submitted
|
|
|
|
Returns:
|
|
Dictionary with status, park, uploaded_count, and errors
|
|
"""
|
|
response: Dict[str, Any] = {
|
|
"status": result["status"],
|
|
"park": park,
|
|
"uploaded_count": 0,
|
|
"errors": [],
|
|
}
|
|
|
|
if result["status"] == "auto_approved":
|
|
updated_park = result["created_object"]
|
|
response["park"] = updated_park
|
|
|
|
# Update location
|
|
ParkService.create_or_update_location(
|
|
park=updated_park,
|
|
latitude=form_data.get("latitude"),
|
|
longitude=form_data.get("longitude"),
|
|
street_address=form_data.get("street_address", ""),
|
|
city=form_data.get("city", ""),
|
|
state=form_data.get("state", ""),
|
|
country=form_data.get("country", ""),
|
|
postal_code=form_data.get("postal_code", ""),
|
|
)
|
|
|
|
# Upload photos
|
|
if photos:
|
|
photo_result = ParkService.upload_photos(
|
|
park=updated_park,
|
|
photos=photos,
|
|
uploaded_by=user,
|
|
)
|
|
response["uploaded_count"] = photo_result["uploaded_count"]
|
|
response["errors"] = photo_result["errors"]
|
|
|
|
elif result["status"] == "failed":
|
|
response["message"] = result.get("message", "Update failed")
|
|
|
|
return response
|