mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 15:11:09 -05:00
- 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.
315 lines
9.4 KiB
Python
315 lines
9.4 KiB
Python
"""
|
|
Parks API views following Django styleguide patterns.
|
|
Uses ClassNameApi naming convention and proper Input/Output serializers.
|
|
"""
|
|
|
|
from typing import Any, Dict
|
|
|
|
from rest_framework import status
|
|
from rest_framework.decorators import action
|
|
from rest_framework.request import Request
|
|
from rest_framework.response import Response
|
|
from rest_framework.viewsets import GenericViewSet
|
|
from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
|
|
from django_filters.rest_framework import DjangoFilterBackend
|
|
from rest_framework.filters import SearchFilter, OrderingFilter
|
|
|
|
from core.api.mixins import (
|
|
ApiMixin,
|
|
CreateApiMixin,
|
|
UpdateApiMixin,
|
|
ListApiMixin,
|
|
RetrieveApiMixin,
|
|
DestroyApiMixin
|
|
)
|
|
from ..selectors import (
|
|
park_list_with_stats,
|
|
park_detail_optimized,
|
|
park_reviews_for_park,
|
|
park_statistics
|
|
)
|
|
from ..services import ParkService
|
|
from .serializers import (
|
|
ParkListOutputSerializer,
|
|
ParkDetailOutputSerializer,
|
|
ParkCreateInputSerializer,
|
|
ParkUpdateInputSerializer,
|
|
ParkFilterInputSerializer,
|
|
ParkReviewOutputSerializer,
|
|
ParkStatsOutputSerializer
|
|
)
|
|
|
|
|
|
class ParkListApi(
|
|
ListApiMixin,
|
|
GenericViewSet
|
|
):
|
|
"""
|
|
API endpoint for listing parks with filtering and search.
|
|
|
|
GET /api/v1/parks/
|
|
"""
|
|
|
|
permission_classes = [IsAuthenticatedOrReadOnly]
|
|
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
|
search_fields = ['name', 'description']
|
|
ordering_fields = ['name', 'opening_date', 'average_rating', 'coaster_count', 'created_at']
|
|
ordering = ['name']
|
|
|
|
OutputSerializer = ParkListOutputSerializer
|
|
FilterSerializer = ParkFilterInputSerializer
|
|
|
|
def get_queryset(self):
|
|
"""Use selector to get optimized queryset."""
|
|
# Parse filter parameters
|
|
filter_serializer = self.FilterSerializer(data=self.request.query_params)
|
|
filter_serializer.is_valid(raise_exception=True)
|
|
filters = filter_serializer.validated_data
|
|
|
|
return park_list_with_stats(filters=filters)
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def stats(self, request: Request) -> Response:
|
|
"""
|
|
Get park statistics.
|
|
|
|
GET /api/v1/parks/stats/
|
|
"""
|
|
stats = park_statistics()
|
|
serializer = ParkStatsOutputSerializer(stats)
|
|
|
|
return self.create_response(
|
|
data=serializer.data,
|
|
metadata={'cache_duration': 3600} # 1 hour cache hint
|
|
)
|
|
|
|
|
|
class ParkDetailApi(
|
|
RetrieveApiMixin,
|
|
GenericViewSet
|
|
):
|
|
"""
|
|
API endpoint for retrieving individual park details.
|
|
|
|
GET /api/v1/parks/{id}/
|
|
"""
|
|
|
|
permission_classes = [IsAuthenticatedOrReadOnly]
|
|
lookup_field = 'slug'
|
|
|
|
OutputSerializer = ParkDetailOutputSerializer
|
|
|
|
def get_object(self):
|
|
"""Use selector for optimized detail query."""
|
|
slug = self.kwargs.get('slug')
|
|
return park_detail_optimized(slug=slug)
|
|
|
|
@action(detail=True, methods=['get'])
|
|
def reviews(self, request: Request, slug: str = None) -> Response:
|
|
"""
|
|
Get reviews for a specific park.
|
|
|
|
GET /api/v1/parks/{slug}/reviews/
|
|
"""
|
|
park = self.get_object()
|
|
reviews = park_reviews_for_park(park_id=park.id, limit=50)
|
|
|
|
serializer = ParkReviewOutputSerializer(reviews, many=True)
|
|
|
|
return self.create_response(
|
|
data=serializer.data,
|
|
metadata={
|
|
'total_reviews': len(reviews),
|
|
'park_name': park.name
|
|
}
|
|
)
|
|
|
|
|
|
class ParkCreateApi(
|
|
CreateApiMixin,
|
|
GenericViewSet
|
|
):
|
|
"""
|
|
API endpoint for creating parks.
|
|
|
|
POST /api/v1/parks/create/
|
|
"""
|
|
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
InputSerializer = ParkCreateInputSerializer
|
|
OutputSerializer = ParkDetailOutputSerializer
|
|
|
|
def perform_create(self, **validated_data):
|
|
"""Create park using service layer."""
|
|
return ParkService.create_park(**validated_data)
|
|
|
|
|
|
class ParkUpdateApi(
|
|
UpdateApiMixin,
|
|
RetrieveApiMixin,
|
|
GenericViewSet
|
|
):
|
|
"""
|
|
API endpoint for updating parks.
|
|
|
|
PUT /api/v1/parks/{slug}/update/
|
|
PATCH /api/v1/parks/{slug}/update/
|
|
"""
|
|
|
|
permission_classes = [IsAuthenticated]
|
|
lookup_field = 'slug'
|
|
|
|
InputSerializer = ParkUpdateInputSerializer
|
|
OutputSerializer = ParkDetailOutputSerializer
|
|
|
|
def get_object(self):
|
|
"""Use selector for optimized detail query."""
|
|
slug = self.kwargs.get('slug')
|
|
return park_detail_optimized(slug=slug)
|
|
|
|
def perform_update(self, instance, **validated_data):
|
|
"""Update park using service layer."""
|
|
return ParkService.update_park(
|
|
park_id=instance.id,
|
|
**validated_data
|
|
)
|
|
|
|
|
|
class ParkDeleteApi(
|
|
DestroyApiMixin,
|
|
RetrieveApiMixin,
|
|
GenericViewSet
|
|
):
|
|
"""
|
|
API endpoint for deleting parks.
|
|
|
|
DELETE /api/v1/parks/{slug}/delete/
|
|
"""
|
|
|
|
permission_classes = [IsAuthenticated] # TODO: Add staff/admin permission
|
|
lookup_field = 'slug'
|
|
|
|
def get_object(self):
|
|
"""Use selector for optimized detail query."""
|
|
slug = self.kwargs.get('slug')
|
|
return park_detail_optimized(slug=slug)
|
|
|
|
def perform_destroy(self, instance):
|
|
"""Delete park using service layer."""
|
|
ParkService.delete_park(park_id=instance.id)
|
|
|
|
|
|
# Unified API ViewSet (alternative approach)
|
|
class ParkApi(
|
|
CreateApiMixin,
|
|
UpdateApiMixin,
|
|
ListApiMixin,
|
|
RetrieveApiMixin,
|
|
DestroyApiMixin,
|
|
GenericViewSet
|
|
):
|
|
"""
|
|
Unified API endpoint for parks with all CRUD operations.
|
|
|
|
GET /api/v1/parks/ - List parks
|
|
POST /api/v1/parks/ - Create park
|
|
GET /api/v1/parks/{slug}/ - Get park detail
|
|
PUT /api/v1/parks/{slug}/ - Update park
|
|
PATCH /api/v1/parks/{slug}/ - Partial update park
|
|
DELETE /api/v1/parks/{slug}/ - Delete park
|
|
"""
|
|
|
|
permission_classes = [IsAuthenticatedOrReadOnly]
|
|
lookup_field = 'slug'
|
|
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
|
search_fields = ['name', 'description']
|
|
ordering_fields = ['name', 'opening_date', 'average_rating', 'coaster_count', 'created_at']
|
|
ordering = ['name']
|
|
|
|
# Serializers for different operations
|
|
InputSerializer = ParkCreateInputSerializer # Used for create
|
|
UpdateInputSerializer = ParkUpdateInputSerializer # Used for update
|
|
OutputSerializer = ParkDetailOutputSerializer # Used for retrieve
|
|
ListOutputSerializer = ParkListOutputSerializer # Used for list
|
|
FilterSerializer = ParkFilterInputSerializer
|
|
|
|
def get_queryset(self):
|
|
"""Use selector to get optimized queryset."""
|
|
if self.action == 'list':
|
|
# Parse filter parameters for list view
|
|
filter_serializer = self.FilterSerializer(data=self.request.query_params)
|
|
filter_serializer.is_valid(raise_exception=True)
|
|
filters = filter_serializer.validated_data
|
|
return park_list_with_stats(**filters)
|
|
|
|
# For detail views, this won't be used since we override get_object
|
|
return []
|
|
|
|
def get_object(self):
|
|
"""Use selector for optimized detail query."""
|
|
slug = self.kwargs.get('slug')
|
|
return park_detail_optimized(slug=slug)
|
|
|
|
def get_output_serializer(self, *args, **kwargs):
|
|
"""Return appropriate output serializer based on action."""
|
|
if self.action == 'list':
|
|
return self.ListOutputSerializer(*args, **kwargs)
|
|
return self.OutputSerializer(*args, **kwargs)
|
|
|
|
def get_input_serializer(self, *args, **kwargs):
|
|
"""Return appropriate input serializer based on action."""
|
|
if self.action in ['update', 'partial_update']:
|
|
return self.UpdateInputSerializer(*args, **kwargs)
|
|
return self.InputSerializer(*args, **kwargs)
|
|
|
|
def perform_create(self, **validated_data):
|
|
"""Create park using service layer."""
|
|
return ParkService.create_park(**validated_data)
|
|
|
|
def perform_update(self, instance, **validated_data):
|
|
"""Update park using service layer."""
|
|
return ParkService.update_park(
|
|
park_id=instance.id,
|
|
**validated_data
|
|
)
|
|
|
|
def perform_destroy(self, instance):
|
|
"""Delete park using service layer."""
|
|
ParkService.delete_park(park_id=instance.id)
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def stats(self, request: Request) -> Response:
|
|
"""
|
|
Get park statistics.
|
|
|
|
GET /api/v1/parks/stats/
|
|
"""
|
|
stats = park_statistics()
|
|
serializer = ParkStatsOutputSerializer(stats)
|
|
|
|
return self.create_response(
|
|
data=serializer.data,
|
|
metadata={'cache_duration': 3600}
|
|
)
|
|
|
|
@action(detail=True, methods=['get'])
|
|
def reviews(self, request: Request, slug: str = None) -> Response:
|
|
"""
|
|
Get reviews for a specific park.
|
|
|
|
GET /api/v1/parks/{slug}/reviews/
|
|
"""
|
|
park = self.get_object()
|
|
reviews = park_reviews_for_park(park_id=park.id, limit=50)
|
|
|
|
serializer = ParkReviewOutputSerializer(reviews, many=True)
|
|
|
|
return self.create_response(
|
|
data=serializer.data,
|
|
metadata={
|
|
'total_reviews': len(reviews),
|
|
'park_name': park.name
|
|
}
|
|
)
|