""" 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