mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 08:31:08 -05:00
Add comprehensive tests for Parks API and models
- Implemented extensive test cases for the Parks API, covering endpoints for listing, retrieving, creating, updating, and deleting parks. - Added tests for filtering, searching, and ordering parks in the API. - Created tests for error handling in the API, including malformed JSON and unsupported methods. - Developed model tests for Park, ParkArea, Company, and ParkReview models, ensuring validation and constraints are enforced. - Introduced utility mixins for API and model testing to streamline assertions and enhance test readability. - Included integration tests to validate complete workflows involving park creation, retrieval, updating, and deletion.
This commit is contained in:
330
parks/services/park_management.py
Normal file
330
parks/services/park_management.py
Normal file
@@ -0,0 +1,330 @@
|
||||
"""
|
||||
Services for park-related business logic.
|
||||
Following Django styleguide pattern for business logic encapsulation.
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict, Any, TYPE_CHECKING
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
|
||||
from ..models import Park, ParkArea
|
||||
from location.models import Location
|
||||
|
||||
|
||||
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 parks.models import Company
|
||||
park.operator = Company.objects.get(id=operator_id)
|
||||
|
||||
if property_owner_id:
|
||||
from 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:
|
||||
LocationService.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 rides.models import Ride
|
||||
from 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
|
||||
|
||||
|
||||
class LocationService:
|
||||
"""Service for managing location operations."""
|
||||
|
||||
@staticmethod
|
||||
def create_park_location(
|
||||
*,
|
||||
park: Park,
|
||||
latitude: Optional[float] = None,
|
||||
longitude: Optional[float] = None,
|
||||
street_address: str = "",
|
||||
city: str = "",
|
||||
state: str = "",
|
||||
country: str = "",
|
||||
postal_code: str = ""
|
||||
) -> Location:
|
||||
"""
|
||||
Create a location for a park.
|
||||
|
||||
Args:
|
||||
park: Park instance
|
||||
latitude: Latitude coordinate
|
||||
longitude: Longitude coordinate
|
||||
street_address: Street address
|
||||
city: City name
|
||||
state: State/region name
|
||||
country: Country name
|
||||
postal_code: Postal/ZIP code
|
||||
|
||||
Returns:
|
||||
Created Location instance
|
||||
|
||||
Raises:
|
||||
ValidationError: If location data is invalid
|
||||
"""
|
||||
location = Location(
|
||||
content_object=park,
|
||||
name=park.name,
|
||||
location_type='park',
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
street_address=street_address,
|
||||
city=city,
|
||||
state=state,
|
||||
country=country,
|
||||
postal_code=postal_code
|
||||
)
|
||||
|
||||
# CRITICAL STYLEGUIDE FIX: Call full_clean before save
|
||||
location.full_clean()
|
||||
location.save()
|
||||
|
||||
return location
|
||||
|
||||
@staticmethod
|
||||
def update_park_location(
|
||||
*,
|
||||
park_id: int,
|
||||
location_updates: Dict[str, Any]
|
||||
) -> Location:
|
||||
"""
|
||||
Update location information for a park.
|
||||
|
||||
Args:
|
||||
park_id: ID of the park
|
||||
location_updates: Dictionary of location field updates
|
||||
|
||||
Returns:
|
||||
Updated Location instance
|
||||
|
||||
Raises:
|
||||
Location.DoesNotExist: If location doesn't exist
|
||||
ValidationError: If location data is invalid
|
||||
"""
|
||||
with transaction.atomic():
|
||||
park = Park.objects.get(id=park_id)
|
||||
|
||||
try:
|
||||
location = park.location
|
||||
except Location.DoesNotExist:
|
||||
# Create location if it doesn't exist
|
||||
return LocationService.create_park_location(
|
||||
park=park,
|
||||
**location_updates
|
||||
)
|
||||
|
||||
# Apply updates
|
||||
for field, value in location_updates.items():
|
||||
if hasattr(location, field):
|
||||
setattr(location, field, value)
|
||||
|
||||
# CRITICAL STYLEGUIDE FIX: Call full_clean before save
|
||||
location.full_clean()
|
||||
location.save()
|
||||
|
||||
return location
|
||||
Reference in New Issue
Block a user