mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 05:51:12 -05:00
- Created a base email template (base.html) for consistent styling across all emails. - Added moderation approval email template (moderation_approved.html) to notify users of approved submissions. - Added moderation rejection email template (moderation_rejected.html) to inform users of required changes for their submissions. - Created password reset email template (password_reset.html) for users requesting to reset their passwords. - Developed a welcome email template (welcome.html) to greet new users and provide account details and tips for using ThrillWiki.
361 lines
11 KiB
Python
361 lines
11 KiB
Python
"""
|
|
Ride endpoints for API v1.
|
|
|
|
Provides CRUD operations for Ride entities with filtering and search.
|
|
"""
|
|
from typing import List, Optional
|
|
from uuid import UUID
|
|
from django.shortcuts import get_object_or_404
|
|
from django.db.models import Q
|
|
from ninja import Router, Query
|
|
from ninja.pagination import paginate, PageNumberPagination
|
|
|
|
from apps.entities.models import Ride, Park, Company, RideModel
|
|
from ..schemas import (
|
|
RideCreate,
|
|
RideUpdate,
|
|
RideOut,
|
|
RideListOut,
|
|
ErrorResponse
|
|
)
|
|
|
|
|
|
router = Router(tags=["Rides"])
|
|
|
|
|
|
class RidePagination(PageNumberPagination):
|
|
"""Custom pagination for rides."""
|
|
page_size = 50
|
|
|
|
|
|
@router.get(
|
|
"/",
|
|
response={200: List[RideOut]},
|
|
summary="List rides",
|
|
description="Get a paginated list of rides with optional filtering"
|
|
)
|
|
@paginate(RidePagination)
|
|
def list_rides(
|
|
request,
|
|
search: Optional[str] = Query(None, description="Search by ride name"),
|
|
park_id: Optional[UUID] = Query(None, description="Filter by park"),
|
|
ride_category: Optional[str] = Query(None, description="Filter by ride category"),
|
|
status: Optional[str] = Query(None, description="Filter by status"),
|
|
is_coaster: Optional[bool] = Query(None, description="Filter for roller coasters only"),
|
|
manufacturer_id: Optional[UUID] = Query(None, description="Filter by manufacturer"),
|
|
ordering: Optional[str] = Query("-created", description="Sort by field (prefix with - for descending)")
|
|
):
|
|
"""
|
|
List all rides with optional filters.
|
|
|
|
**Filters:**
|
|
- search: Search ride names (case-insensitive partial match)
|
|
- park_id: Filter by park
|
|
- ride_category: Filter by ride category
|
|
- status: Filter by operational status
|
|
- is_coaster: Filter for roller coasters (true/false)
|
|
- manufacturer_id: Filter by manufacturer
|
|
- ordering: Sort results (default: -created)
|
|
|
|
**Returns:** Paginated list of rides
|
|
"""
|
|
queryset = Ride.objects.select_related('park', 'manufacturer', 'model').all()
|
|
|
|
# Apply search filter
|
|
if search:
|
|
queryset = queryset.filter(
|
|
Q(name__icontains=search) | Q(description__icontains=search)
|
|
)
|
|
|
|
# Apply park filter
|
|
if park_id:
|
|
queryset = queryset.filter(park_id=park_id)
|
|
|
|
# Apply ride category filter
|
|
if ride_category:
|
|
queryset = queryset.filter(ride_category=ride_category)
|
|
|
|
# Apply status filter
|
|
if status:
|
|
queryset = queryset.filter(status=status)
|
|
|
|
# Apply coaster filter
|
|
if is_coaster is not None:
|
|
queryset = queryset.filter(is_coaster=is_coaster)
|
|
|
|
# Apply manufacturer filter
|
|
if manufacturer_id:
|
|
queryset = queryset.filter(manufacturer_id=manufacturer_id)
|
|
|
|
# Apply ordering
|
|
valid_order_fields = ['name', 'created', 'modified', 'opening_date', 'height', 'speed', 'length']
|
|
order_field = ordering.lstrip('-')
|
|
if order_field in valid_order_fields:
|
|
queryset = queryset.order_by(ordering)
|
|
else:
|
|
queryset = queryset.order_by('-created')
|
|
|
|
# Annotate with related names
|
|
for ride in queryset:
|
|
ride.park_name = ride.park.name if ride.park else None
|
|
ride.manufacturer_name = ride.manufacturer.name if ride.manufacturer else None
|
|
ride.model_name = ride.model.name if ride.model else None
|
|
|
|
return queryset
|
|
|
|
|
|
@router.get(
|
|
"/{ride_id}",
|
|
response={200: RideOut, 404: ErrorResponse},
|
|
summary="Get ride",
|
|
description="Retrieve a single ride by ID"
|
|
)
|
|
def get_ride(request, ride_id: UUID):
|
|
"""
|
|
Get a ride by ID.
|
|
|
|
**Parameters:**
|
|
- ride_id: UUID of the ride
|
|
|
|
**Returns:** Ride details
|
|
"""
|
|
ride = get_object_or_404(
|
|
Ride.objects.select_related('park', 'manufacturer', 'model'),
|
|
id=ride_id
|
|
)
|
|
ride.park_name = ride.park.name if ride.park else None
|
|
ride.manufacturer_name = ride.manufacturer.name if ride.manufacturer else None
|
|
ride.model_name = ride.model.name if ride.model else None
|
|
return ride
|
|
|
|
|
|
@router.post(
|
|
"/",
|
|
response={201: RideOut, 400: ErrorResponse, 404: ErrorResponse},
|
|
summary="Create ride",
|
|
description="Create a new ride (requires authentication)"
|
|
)
|
|
def create_ride(request, payload: RideCreate):
|
|
"""
|
|
Create a new ride.
|
|
|
|
**Authentication:** Required
|
|
|
|
**Parameters:**
|
|
- payload: Ride data
|
|
|
|
**Returns:** Created ride
|
|
"""
|
|
# TODO: Add authentication check
|
|
# if not request.auth:
|
|
# return 401, {"detail": "Authentication required"}
|
|
|
|
# Verify park exists
|
|
park = get_object_or_404(Park, id=payload.park_id)
|
|
|
|
# Verify manufacturer if provided
|
|
if payload.manufacturer_id:
|
|
get_object_or_404(Company, id=payload.manufacturer_id)
|
|
|
|
# Verify model if provided
|
|
if payload.model_id:
|
|
get_object_or_404(RideModel, id=payload.model_id)
|
|
|
|
ride = Ride.objects.create(**payload.dict())
|
|
|
|
# Reload with related objects
|
|
ride = Ride.objects.select_related('park', 'manufacturer', 'model').get(id=ride.id)
|
|
ride.park_name = ride.park.name if ride.park else None
|
|
ride.manufacturer_name = ride.manufacturer.name if ride.manufacturer else None
|
|
ride.model_name = ride.model.name if ride.model else None
|
|
|
|
return 201, ride
|
|
|
|
|
|
@router.put(
|
|
"/{ride_id}",
|
|
response={200: RideOut, 404: ErrorResponse, 400: ErrorResponse},
|
|
summary="Update ride",
|
|
description="Update an existing ride (requires authentication)"
|
|
)
|
|
def update_ride(request, ride_id: UUID, payload: RideUpdate):
|
|
"""
|
|
Update a ride.
|
|
|
|
**Authentication:** Required
|
|
|
|
**Parameters:**
|
|
- ride_id: UUID of the ride
|
|
- payload: Updated ride data
|
|
|
|
**Returns:** Updated ride
|
|
"""
|
|
# TODO: Add authentication check
|
|
# if not request.auth:
|
|
# return 401, {"detail": "Authentication required"}
|
|
|
|
ride = get_object_or_404(
|
|
Ride.objects.select_related('park', 'manufacturer', 'model'),
|
|
id=ride_id
|
|
)
|
|
|
|
# Update only provided fields
|
|
for key, value in payload.dict(exclude_unset=True).items():
|
|
setattr(ride, key, value)
|
|
|
|
ride.save()
|
|
|
|
# Reload to get updated relationships
|
|
ride = Ride.objects.select_related('park', 'manufacturer', 'model').get(id=ride.id)
|
|
ride.park_name = ride.park.name if ride.park else None
|
|
ride.manufacturer_name = ride.manufacturer.name if ride.manufacturer else None
|
|
ride.model_name = ride.model.name if ride.model else None
|
|
|
|
return ride
|
|
|
|
|
|
@router.patch(
|
|
"/{ride_id}",
|
|
response={200: RideOut, 404: ErrorResponse, 400: ErrorResponse},
|
|
summary="Partial update ride",
|
|
description="Partially update an existing ride (requires authentication)"
|
|
)
|
|
def partial_update_ride(request, ride_id: UUID, payload: RideUpdate):
|
|
"""
|
|
Partially update a ride.
|
|
|
|
**Authentication:** Required
|
|
|
|
**Parameters:**
|
|
- ride_id: UUID of the ride
|
|
- payload: Fields to update
|
|
|
|
**Returns:** Updated ride
|
|
"""
|
|
# TODO: Add authentication check
|
|
# if not request.auth:
|
|
# return 401, {"detail": "Authentication required"}
|
|
|
|
ride = get_object_or_404(
|
|
Ride.objects.select_related('park', 'manufacturer', 'model'),
|
|
id=ride_id
|
|
)
|
|
|
|
# Update only provided fields
|
|
for key, value in payload.dict(exclude_unset=True).items():
|
|
setattr(ride, key, value)
|
|
|
|
ride.save()
|
|
|
|
# Reload to get updated relationships
|
|
ride = Ride.objects.select_related('park', 'manufacturer', 'model').get(id=ride.id)
|
|
ride.park_name = ride.park.name if ride.park else None
|
|
ride.manufacturer_name = ride.manufacturer.name if ride.manufacturer else None
|
|
ride.model_name = ride.model.name if ride.model else None
|
|
|
|
return ride
|
|
|
|
|
|
@router.delete(
|
|
"/{ride_id}",
|
|
response={204: None, 404: ErrorResponse},
|
|
summary="Delete ride",
|
|
description="Delete a ride (requires authentication)"
|
|
)
|
|
def delete_ride(request, ride_id: UUID):
|
|
"""
|
|
Delete a ride.
|
|
|
|
**Authentication:** Required
|
|
|
|
**Parameters:**
|
|
- ride_id: UUID of the ride
|
|
|
|
**Returns:** No content (204)
|
|
"""
|
|
# TODO: Add authentication check
|
|
# if not request.auth:
|
|
# return 401, {"detail": "Authentication required"}
|
|
|
|
ride = get_object_or_404(Ride, id=ride_id)
|
|
ride.delete()
|
|
return 204, None
|
|
|
|
|
|
@router.get(
|
|
"/coasters/",
|
|
response={200: List[RideOut]},
|
|
summary="List roller coasters",
|
|
description="Get a paginated list of roller coasters only"
|
|
)
|
|
@paginate(RidePagination)
|
|
def list_coasters(
|
|
request,
|
|
search: Optional[str] = Query(None, description="Search by ride name"),
|
|
park_id: Optional[UUID] = Query(None, description="Filter by park"),
|
|
status: Optional[str] = Query(None, description="Filter by status"),
|
|
manufacturer_id: Optional[UUID] = Query(None, description="Filter by manufacturer"),
|
|
min_height: Optional[float] = Query(None, description="Minimum height in feet"),
|
|
min_speed: Optional[float] = Query(None, description="Minimum speed in mph"),
|
|
ordering: Optional[str] = Query("-height", description="Sort by field (prefix with - for descending)")
|
|
):
|
|
"""
|
|
List only roller coasters with optional filters.
|
|
|
|
**Filters:**
|
|
- search: Search coaster names
|
|
- park_id: Filter by park
|
|
- status: Filter by operational status
|
|
- manufacturer_id: Filter by manufacturer
|
|
- min_height: Minimum height filter
|
|
- min_speed: Minimum speed filter
|
|
- ordering: Sort results (default: -height)
|
|
|
|
**Returns:** Paginated list of roller coasters
|
|
"""
|
|
queryset = Ride.objects.filter(is_coaster=True).select_related(
|
|
'park', 'manufacturer', 'model'
|
|
)
|
|
|
|
# Apply search filter
|
|
if search:
|
|
queryset = queryset.filter(
|
|
Q(name__icontains=search) | Q(description__icontains=search)
|
|
)
|
|
|
|
# Apply park filter
|
|
if park_id:
|
|
queryset = queryset.filter(park_id=park_id)
|
|
|
|
# Apply status filter
|
|
if status:
|
|
queryset = queryset.filter(status=status)
|
|
|
|
# Apply manufacturer filter
|
|
if manufacturer_id:
|
|
queryset = queryset.filter(manufacturer_id=manufacturer_id)
|
|
|
|
# Apply height filter
|
|
if min_height is not None:
|
|
queryset = queryset.filter(height__gte=min_height)
|
|
|
|
# Apply speed filter
|
|
if min_speed is not None:
|
|
queryset = queryset.filter(speed__gte=min_speed)
|
|
|
|
# Apply ordering
|
|
valid_order_fields = ['name', 'height', 'speed', 'length', 'opening_date', 'inversions']
|
|
order_field = ordering.lstrip('-')
|
|
if order_field in valid_order_fields:
|
|
queryset = queryset.order_by(ordering)
|
|
else:
|
|
queryset = queryset.order_by('-height')
|
|
|
|
# Annotate with related names
|
|
for ride in queryset:
|
|
ride.park_name = ride.park.name if ride.park else None
|
|
ride.manufacturer_name = ride.manufacturer.name if ride.manufacturer else None
|
|
ride.model_name = ride.model.name if ride.model else None
|
|
|
|
return queryset
|