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:
pacnpal
2025-08-17 19:36:20 -04:00
parent 17228e9935
commit c26414ff74
210 changed files with 24155 additions and 833 deletions

244
parks/selectors.py Normal file
View File

@@ -0,0 +1,244 @@
"""
Selectors for park-related data retrieval.
Following Django styleguide pattern for separating data access from business logic.
"""
from typing import Optional, Dict, Any, List
from django.db.models import QuerySet, Q, F, Count, Avg, Prefetch
from django.contrib.gis.geos import Point
from django.contrib.gis.measure import Distance
from .models import Park, ParkArea, ParkReview
from rides.models import Ride
def park_list_with_stats(*, filters: Optional[Dict[str, Any]] = None) -> QuerySet[Park]:
"""
Get parks optimized for list display with basic stats.
Args:
filters: Optional dictionary of filter parameters
Returns:
QuerySet of parks with optimized queries
"""
queryset = Park.objects.select_related(
'operator',
'property_owner'
).prefetch_related(
'location'
).annotate(
ride_count_calculated=Count('rides', distinct=True),
coaster_count_calculated=Count(
'rides',
filter=Q(rides__category__in=['RC', 'WC']),
distinct=True
),
average_rating_calculated=Avg('reviews__rating')
)
if filters:
if 'status' in filters:
queryset = queryset.filter(status=filters['status'])
if 'operator' in filters:
queryset = queryset.filter(operator=filters['operator'])
if 'country' in filters:
queryset = queryset.filter(location__country=filters['country'])
if 'search' in filters:
search_term = filters['search']
queryset = queryset.filter(
Q(name__icontains=search_term) |
Q(description__icontains=search_term)
)
return queryset.order_by('name')
def park_detail_optimized(*, slug: str) -> Park:
"""
Get a single park with all related data optimized for detail view.
Args:
slug: Park slug identifier
Returns:
Park instance with optimized prefetches
Raises:
Park.DoesNotExist: If park with slug doesn't exist
"""
return Park.objects.select_related(
'operator',
'property_owner'
).prefetch_related(
'location',
'areas',
Prefetch(
'rides',
queryset=Ride.objects.select_related('manufacturer', 'designer', 'ride_model')
),
Prefetch(
'reviews',
queryset=ParkReview.objects.select_related('user').filter(is_published=True)
),
'photos'
).get(slug=slug)
def parks_near_location(
*,
point: Point,
distance_km: float = 50,
limit: int = 10
) -> QuerySet[Park]:
"""
Get parks near a specific geographic location.
Args:
point: Geographic point (longitude, latitude)
distance_km: Maximum distance in kilometers
limit: Maximum number of results
Returns:
QuerySet of nearby parks ordered by distance
"""
return Park.objects.filter(
location__coordinates__distance_lte=(point, Distance(km=distance_km))
).select_related(
'operator'
).prefetch_related(
'location'
).distance(point).order_by('distance')[:limit]
def park_statistics() -> Dict[str, Any]:
"""
Get overall park statistics for dashboard/analytics.
Returns:
Dictionary containing park statistics
"""
total_parks = Park.objects.count()
operating_parks = Park.objects.filter(status='OPERATING').count()
total_rides = Ride.objects.count()
total_coasters = Ride.objects.filter(category__in=['RC', 'WC']).count()
return {
'total_parks': total_parks,
'operating_parks': operating_parks,
'closed_parks': total_parks - operating_parks,
'total_rides': total_rides,
'total_coasters': total_coasters,
'average_rides_per_park': total_rides / total_parks if total_parks > 0 else 0
}
def parks_by_operator(*, operator_id: int) -> QuerySet[Park]:
"""
Get all parks operated by a specific company.
Args:
operator_id: Company ID of the operator
Returns:
QuerySet of parks operated by the company
"""
return Park.objects.filter(
operator_id=operator_id
).select_related(
'operator'
).prefetch_related(
'location'
).annotate(
ride_count_calculated=Count('rides')
).order_by('name')
def parks_with_recent_reviews(*, days: int = 30) -> QuerySet[Park]:
"""
Get parks that have received reviews in the last N days.
Args:
days: Number of days to look back for reviews
Returns:
QuerySet of parks with recent reviews
"""
from django.utils import timezone
from datetime import timedelta
cutoff_date = timezone.now() - timedelta(days=days)
return Park.objects.filter(
reviews__created_at__gte=cutoff_date,
reviews__is_published=True
).select_related(
'operator'
).prefetch_related(
'location'
).annotate(
recent_review_count=Count('reviews', filter=Q(reviews__created_at__gte=cutoff_date))
).order_by('-recent_review_count').distinct()
def park_search_autocomplete(*, query: str, limit: int = 10) -> QuerySet[Park]:
"""
Get parks matching a search query for autocomplete functionality.
Args:
query: Search string
limit: Maximum number of results
Returns:
QuerySet of matching parks for autocomplete
"""
return Park.objects.filter(
Q(name__icontains=query) |
Q(location__city__icontains=query) |
Q(location__region__icontains=query)
).select_related(
'operator'
).prefetch_related(
'location'
).order_by('name')[:limit]
def park_areas_for_park(*, park_slug: str) -> QuerySet[ParkArea]:
"""
Get all areas for a specific park.
Args:
park_slug: Slug of the park
Returns:
QuerySet of park areas with related data
"""
return ParkArea.objects.filter(
park__slug=park_slug
).select_related(
'park'
).prefetch_related(
'rides'
).annotate(
ride_count=Count('rides')
).order_by('name')
def park_reviews_for_park(*, park_id: int, limit: int = 20) -> QuerySet[ParkReview]:
"""
Get reviews for a specific park.
Args:
park_id: Park ID
limit: Maximum number of reviews to return
Returns:
QuerySet of park reviews
"""
return ParkReview.objects.filter(
park_id=park_id,
is_published=True
).select_related(
'user',
'park'
).order_by('-created_at')[:limit]