Files
thrillwiki_django_no_react/backend/apps/api/v1/schema.py
pacnpal e4e36c7899 Add migrations for ParkPhoto and RidePhoto models with associated events
- Created ParkPhoto and ParkPhotoEvent models in the parks app, including fields for image, caption, alt text, and relationships to the Park model.
- Implemented triggers for insert and update operations on ParkPhoto to log changes in ParkPhotoEvent.
- Created RidePhoto and RidePhotoEvent models in the rides app, with similar structure and functionality as ParkPhoto.
- Added fields for photo type in RidePhoto and implemented corresponding triggers for logging changes.
- Established necessary indexes and unique constraints for both models to ensure data integrity and optimize queries.
2025-08-26 14:40:46 -04:00

333 lines
10 KiB
Python

"""
Schema extensions and customizations for drf-spectacular.
This module provides custom extensions to improve OpenAPI schema generation
for the ThrillWiki API, including better documentation and examples.
"""
from drf_spectacular.openapi import AutoSchema
# Custom examples for common serializers
PARK_EXAMPLE = {
"id": 1,
"name": "Cedar Point",
"slug": "cedar-point",
"description": "The Roller Coaster Capital of the World",
"status": "OPERATING",
"opening_date": "1870-07-04",
"closing_date": None,
"location": {
"latitude": 41.4793,
"longitude": -82.6833,
"city": "Sandusky",
"state": "Ohio",
"country": "United States",
"formatted_address": "Sandusky, OH, United States",
},
"operator": {
"id": 1,
"name": "Cedar Fair",
"slug": "cedar-fair",
"roles": ["OPERATOR", "PROPERTY_OWNER"],
},
"property_owner": {
"id": 1,
"name": "Cedar Fair",
"slug": "cedar-fair",
"roles": ["OPERATOR", "PROPERTY_OWNER"],
},
"area_count": 15,
"ride_count": 70,
"operating_rides_count": 68,
"roller_coaster_count": 17,
}
RIDE_EXAMPLE = {
"id": 1,
"name": "Steel Vengeance",
"slug": "steel-vengeance",
"description": "A hybrid wooden/steel roller coaster",
"category": "ROLLER_COASTER",
"status": "OPERATING",
"opening_date": "2018-05-05",
"closing_date": None,
"park": {"id": 1, "name": "Cedar Point", "slug": "cedar-point"},
"manufacturer": {
"id": 1,
"name": "Rocky Mountain Construction",
"slug": "rmc",
"roles": ["MANUFACTURER"],
},
"designer": {
"id": 1,
"name": "Rocky Mountain Construction",
"slug": "rmc",
"roles": ["DESIGNER"],
},
"height_feet": 205,
"length_feet": 5740,
"speed_mph": 74,
"inversions": 4,
"duration_seconds": 150,
"capacity_per_hour": 1200,
"minimum_height_inches": 48,
"maximum_height_inches": None,
}
COMPANY_EXAMPLE = {
"id": 1,
"name": "Cedar Fair",
"slug": "cedar-fair",
"roles": ["OPERATOR", "PROPERTY_OWNER"],
}
LOCATION_EXAMPLE = {
"latitude": 41.4793,
"longitude": -82.6833,
"city": "Sandusky",
"state": "Ohio",
"country": "United States",
"formatted_address": "Sandusky, OH, United States",
}
HISTORY_EVENT_EXAMPLE = {
"id": "12345678-1234-5678-9012-123456789012",
"pgh_created_at": "2024-01-15T14:30:00Z",
"pgh_label": "updated",
"pgh_model": "parks.park",
"pgh_obj_id": 1,
"pgh_context": {
"user_id": 42,
"request_id": "req_abc123",
"ip_address": "192.168.1.100",
},
"changed_fields": ["name", "description"],
"field_changes": {
"name": {"old_value": "Cedar Point Amusement Park", "new_value": "Cedar Point"},
"description": {
"old_value": "America's Roller Coast",
"new_value": "The Roller Coaster Capital of the World",
},
},
}
PARK_HISTORY_EXAMPLE = {
"park": PARK_EXAMPLE,
"current_state": PARK_EXAMPLE,
"summary": {
"total_events": 25,
"first_recorded": "2023-01-01T00:00:00Z",
"last_modified": "2024-01-15T14:30:00Z",
"significant_changes": [
{
"date": "2024-01-15T14:30:00Z",
"event_type": "updated",
"description": "Name and description updated",
},
{
"date": "2023-06-01T10:00:00Z",
"event_type": "updated",
"description": "Operating status changed",
},
],
},
"events": [HISTORY_EVENT_EXAMPLE],
}
UNIFIED_HISTORY_TIMELINE_EXAMPLE = {
"summary": {
"total_events": 1250,
"events_returned": 100,
"event_type_breakdown": {"created": 45, "updated": 180, "deleted": 5},
"model_type_breakdown": {
"parks.park": 75,
"rides.ride": 120,
"companies.operator": 15,
"companies.manufacturer": 25,
"accounts.user": 30,
},
"time_range": {
"earliest": "2023-01-01T00:00:00Z",
"latest": "2024-01-15T14:30:00Z",
},
},
"events": [
{
"id": "event_001",
"pgh_created_at": "2024-01-15T14:30:00Z",
"pgh_label": "updated",
"pgh_model": "parks.park",
"pgh_obj_id": 1,
"entity_name": "Cedar Point",
"entity_slug": "cedar-point",
"change_significance": "minor",
"change_summary": "Park description updated",
},
{
"id": "event_002",
"pgh_created_at": "2024-01-15T12:00:00Z",
"pgh_label": "created",
"pgh_model": "rides.ride",
"pgh_obj_id": 100,
"entity_name": "New Roller Coaster",
"entity_slug": "new-roller-coaster",
"change_significance": "major",
"change_summary": "New ride added to park",
},
],
}
# OpenAPI schema customizations
def custom_preprocessing_hook(endpoints):
"""
Custom preprocessing hook to modify endpoints before schema generation.
This can be used to filter out certain endpoints, modify their metadata,
or add custom documentation.
"""
# Filter out any endpoints we don't want in the public API
filtered = []
for path, path_regex, method, callback in endpoints:
# Skip internal or debug endpoints
if "/debug/" not in path and "/internal/" not in path:
filtered.append((path, path_regex, method, callback))
return filtered
def custom_postprocessing_hook(result, generator, request, public):
"""
Custom postprocessing hook to modify the generated schema.
This can be used to add custom metadata, modify response schemas,
or enhance the overall API documentation.
"""
# Add custom info to the schema
if "info" in result:
result["info"]["contact"] = {
"name": "ThrillWiki API Support",
"email": "api@thrillwiki.com",
"url": "https://thrillwiki.com/support",
}
result["info"]["license"] = {
"name": "MIT",
"url": "https://opensource.org/licenses/MIT",
}
# Add custom tags with descriptions
if "tags" not in result:
result["tags"] = []
result["tags"].extend(
[
{
"name": "Parks",
"description": "Operations related to theme parks, including CRUD operations and statistics",
},
{
"name": "Rides",
"description": "Operations related to rides and attractions within theme parks",
},
{
"name": "History",
"description": "Historical change tracking for all entities, providing complete audit trails and version history",
"externalDocs": {
"description": "Learn more about pghistory",
"url": "https://django-pghistory.readthedocs.io/",
},
},
{
"name": "Statistics",
"description": "Statistical endpoints providing aggregated data and insights",
},
{
"name": "Reviews",
"description": "User reviews and ratings for parks and rides",
},
{
"name": "Authentication",
"description": "User authentication and account management endpoints",
},
{
"name": "Health",
"description": "System health checks and monitoring endpoints",
},
{
"name": "Recent Changes",
"description": "Endpoints for accessing recently changed entities by type and change category",
},
]
)
# Add custom servers if not present
if "servers" not in result:
result["servers"] = [
{
"url": "https://api.thrillwiki.com/v1",
"description": "Production server",
},
{
"url": "https://staging-api.thrillwiki.com/v1",
"description": "Staging server",
},
{
"url": "http://localhost:8000/api/v1",
"description": "Development server",
},
]
return result
# Custom AutoSchema class for enhanced documentation
class ThrillWikiAutoSchema(AutoSchema):
"""
Custom AutoSchema class that provides enhanced documentation
for ThrillWiki API endpoints.
"""
def get_operation_id(self):
"""Generate meaningful operation IDs."""
if hasattr(self.view, "basename"):
basename = self.view.basename
else:
basename = getattr(self.view, "__class__", self.view).__name__.lower()
if basename.endswith("viewset"):
basename = basename[:-7] # Remove 'viewset' suffix
action = self.method_mapping.get(self.method.lower(), self.method.lower())
return f"{basename}_{action}"
def get_tags(self):
"""Generate tags based on the viewset."""
if hasattr(self.view, "basename"):
return [self.view.basename.title()]
return super().get_tags()
def get_summary(self):
"""Generate summary from docstring or method name."""
summary = super().get_summary()
if summary:
return summary
# Generate from method and model
action = self.method_mapping.get(self.method.lower(), self.method.lower())
model_name = getattr(self.view, "basename", "resource")
action_map = {
"list": f"List {model_name}",
"create": f"Create {model_name}",
"retrieve": f"Get {model_name} details",
"update": f"Update {model_name}",
"partial_update": f"Partially update {model_name}",
"destroy": f"Delete {model_name}",
}
return action_map.get(action, f"{action.title()} {model_name}")