Files
thrillwiki_django_no_react/backend/apps/parks/services/park_management.py
pacnpal 2e35f8c5d9 feat: Refactor rides app with unique constraints, mixins, and enhanced documentation
- 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.
2025-12-22 11:17:31 -05:00

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