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

295
parks/api/serializers.py Normal file
View File

@@ -0,0 +1,295 @@
"""
Serializers for Parks API following Django styleguide patterns.
Separates Input and Output serializers for clear boundaries.
"""
from rest_framework import serializers
from django.contrib.gis.geos import Point
from ..models import Park, ParkArea, Company, ParkReview
class ParkLocationOutputSerializer(serializers.Serializer):
"""Output serializer for park location data."""
latitude = serializers.SerializerMethodField()
longitude = serializers.SerializerMethodField()
city = serializers.SerializerMethodField()
state = serializers.SerializerMethodField()
country = serializers.SerializerMethodField()
formatted_address = serializers.SerializerMethodField()
def get_latitude(self, obj):
if hasattr(obj, 'location') and obj.location:
return obj.location.latitude
return None
def get_longitude(self, obj):
if hasattr(obj, 'location') and obj.location:
return obj.location.longitude
return None
def get_city(self, obj):
if hasattr(obj, 'location') and obj.location:
return obj.location.city
return None
def get_state(self, obj):
if hasattr(obj, 'location') and obj.location:
return obj.location.state
return None
def get_country(self, obj):
if hasattr(obj, 'location') and obj.location:
return obj.location.country
return None
def get_formatted_address(self, obj):
if hasattr(obj, 'location') and obj.location:
return obj.location.formatted_address
return ""
class CompanyOutputSerializer(serializers.Serializer):
"""Output serializer for company data."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
roles = serializers.ListField(child=serializers.CharField())
class ParkAreaOutputSerializer(serializers.Serializer):
"""Output serializer for park area data."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
description = serializers.CharField()
class ParkListOutputSerializer(serializers.Serializer):
"""Output serializer for park list view."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
status = serializers.CharField()
description = serializers.CharField()
# Statistics
average_rating = serializers.DecimalField(max_digits=3, decimal_places=2, allow_null=True)
coaster_count = serializers.IntegerField(allow_null=True)
ride_count = serializers.IntegerField(allow_null=True)
# Location (simplified for list view)
location = ParkLocationOutputSerializer(allow_null=True)
# Operator info
operator = CompanyOutputSerializer()
# Metadata
created_at = serializers.DateTimeField()
updated_at = serializers.DateTimeField()
class ParkDetailOutputSerializer(serializers.Serializer):
"""Output serializer for park detail view."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
status = serializers.CharField()
description = serializers.CharField()
# Details
opening_date = serializers.DateField(allow_null=True)
closing_date = serializers.DateField(allow_null=True)
operating_season = serializers.CharField()
size_acres = serializers.DecimalField(max_digits=10, decimal_places=2, allow_null=True)
website = serializers.URLField()
# Statistics
average_rating = serializers.DecimalField(max_digits=3, decimal_places=2, allow_null=True)
coaster_count = serializers.IntegerField(allow_null=True)
ride_count = serializers.IntegerField(allow_null=True)
# Location (full details)
location = ParkLocationOutputSerializer(allow_null=True)
# Companies
operator = CompanyOutputSerializer()
property_owner = CompanyOutputSerializer(allow_null=True)
# Areas
areas = ParkAreaOutputSerializer(many=True)
# Metadata
created_at = serializers.DateTimeField()
updated_at = serializers.DateTimeField()
class ParkCreateInputSerializer(serializers.Serializer):
"""Input serializer for creating parks."""
name = serializers.CharField(max_length=255)
description = serializers.CharField(allow_blank=True, default="")
status = serializers.ChoiceField(
choices=Park.STATUS_CHOICES,
default="OPERATING"
)
# Optional details
opening_date = serializers.DateField(required=False, allow_null=True)
closing_date = serializers.DateField(required=False, allow_null=True)
operating_season = serializers.CharField(max_length=255, required=False, allow_blank=True)
size_acres = serializers.DecimalField(
max_digits=10,
decimal_places=2,
required=False,
allow_null=True
)
website = serializers.URLField(required=False, allow_blank=True)
# Required operator
operator_id = serializers.IntegerField()
# Optional property owner
property_owner_id = serializers.IntegerField(required=False, allow_null=True)
def validate(self, data):
"""Cross-field validation."""
opening_date = data.get('opening_date')
closing_date = data.get('closing_date')
if opening_date and closing_date and closing_date < opening_date:
raise serializers.ValidationError(
"Closing date cannot be before opening date"
)
return data
class ParkUpdateInputSerializer(serializers.Serializer):
"""Input serializer for updating parks."""
name = serializers.CharField(max_length=255, required=False)
description = serializers.CharField(allow_blank=True, required=False)
status = serializers.ChoiceField(
choices=Park.STATUS_CHOICES,
required=False
)
# Optional details
opening_date = serializers.DateField(required=False, allow_null=True)
closing_date = serializers.DateField(required=False, allow_null=True)
operating_season = serializers.CharField(max_length=255, required=False, allow_blank=True)
size_acres = serializers.DecimalField(
max_digits=10,
decimal_places=2,
required=False,
allow_null=True
)
website = serializers.URLField(required=False, allow_blank=True)
# Companies
operator_id = serializers.IntegerField(required=False)
property_owner_id = serializers.IntegerField(required=False, allow_null=True)
def validate(self, data):
"""Cross-field validation."""
opening_date = data.get('opening_date')
closing_date = data.get('closing_date')
if opening_date and closing_date and closing_date < opening_date:
raise serializers.ValidationError(
"Closing date cannot be before opening date"
)
return data
class ParkFilterInputSerializer(serializers.Serializer):
"""Input serializer for park filtering and search."""
# Search
search = serializers.CharField(required=False, allow_blank=True)
# Status filter
status = serializers.MultipleChoiceField(
choices=Park.STATUS_CHOICES,
required=False
)
# Location filters
country = serializers.CharField(required=False, allow_blank=True)
state = serializers.CharField(required=False, allow_blank=True)
city = serializers.CharField(required=False, allow_blank=True)
# Rating filter
min_rating = serializers.DecimalField(
max_digits=3,
decimal_places=2,
required=False,
min_value=1,
max_value=10
)
# Size filter
min_size_acres = serializers.DecimalField(
max_digits=10,
decimal_places=2,
required=False,
min_value=0
)
max_size_acres = serializers.DecimalField(
max_digits=10,
decimal_places=2,
required=False,
min_value=0
)
# Company filters
operator_id = serializers.IntegerField(required=False)
property_owner_id = serializers.IntegerField(required=False)
# Ordering
ordering = serializers.ChoiceField(
choices=[
'name', '-name',
'opening_date', '-opening_date',
'average_rating', '-average_rating',
'coaster_count', '-coaster_count',
'created_at', '-created_at'
],
required=False,
default='name'
)
class ParkReviewOutputSerializer(serializers.Serializer):
"""Output serializer for park reviews."""
id = serializers.IntegerField()
rating = serializers.IntegerField()
title = serializers.CharField()
content = serializers.CharField()
visit_date = serializers.DateField()
created_at = serializers.DateTimeField()
# User info (limited for privacy)
user = serializers.SerializerMethodField()
def get_user(self, obj):
return {
'username': obj.user.username,
'display_name': obj.user.get_full_name() or obj.user.username
}
class ParkStatsOutputSerializer(serializers.Serializer):
"""Output serializer for park statistics."""
total_parks = serializers.IntegerField()
operating_parks = serializers.IntegerField()
closed_parks = serializers.IntegerField()
under_construction = serializers.IntegerField()
# Averages
average_rating = serializers.DecimalField(max_digits=3, decimal_places=2, allow_null=True)
average_coaster_count = serializers.DecimalField(max_digits=5, decimal_places=2, allow_null=True)
# Top countries
top_countries = serializers.ListField(child=serializers.DictField())
# Recently added
recently_added_count = serializers.IntegerField()