mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 06:11:11 -05:00
Add email templates for user notifications and account management
- 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.
This commit is contained in:
362
django/api/v1/endpoints/parks.py
Normal file
362
django/api/v1/endpoints/parks.py
Normal file
@@ -0,0 +1,362 @@
|
||||
"""
|
||||
Park endpoints for API v1.
|
||||
|
||||
Provides CRUD operations for Park entities with filtering, search, and geographic queries.
|
||||
Supports both SQLite (lat/lng) and PostGIS (location_point) modes.
|
||||
"""
|
||||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
from decimal import Decimal
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.db.models import Q
|
||||
from django.conf import settings
|
||||
from ninja import Router, Query
|
||||
from ninja.pagination import paginate, PageNumberPagination
|
||||
import math
|
||||
|
||||
from apps.entities.models import Park, Company, _using_postgis
|
||||
from ..schemas import (
|
||||
ParkCreate,
|
||||
ParkUpdate,
|
||||
ParkOut,
|
||||
ParkListOut,
|
||||
ErrorResponse
|
||||
)
|
||||
|
||||
|
||||
router = Router(tags=["Parks"])
|
||||
|
||||
|
||||
class ParkPagination(PageNumberPagination):
|
||||
"""Custom pagination for parks."""
|
||||
page_size = 50
|
||||
|
||||
|
||||
@router.get(
|
||||
"/",
|
||||
response={200: List[ParkOut]},
|
||||
summary="List parks",
|
||||
description="Get a paginated list of parks with optional filtering"
|
||||
)
|
||||
@paginate(ParkPagination)
|
||||
def list_parks(
|
||||
request,
|
||||
search: Optional[str] = Query(None, description="Search by park name"),
|
||||
park_type: Optional[str] = Query(None, description="Filter by park type"),
|
||||
status: Optional[str] = Query(None, description="Filter by status"),
|
||||
operator_id: Optional[UUID] = Query(None, description="Filter by operator"),
|
||||
ordering: Optional[str] = Query("-created", description="Sort by field (prefix with - for descending)")
|
||||
):
|
||||
"""
|
||||
List all parks with optional filters.
|
||||
|
||||
**Filters:**
|
||||
- search: Search park names (case-insensitive partial match)
|
||||
- park_type: Filter by park type
|
||||
- status: Filter by operational status
|
||||
- operator_id: Filter by operator company
|
||||
- ordering: Sort results (default: -created)
|
||||
|
||||
**Returns:** Paginated list of parks
|
||||
"""
|
||||
queryset = Park.objects.select_related('operator').all()
|
||||
|
||||
# Apply search filter
|
||||
if search:
|
||||
queryset = queryset.filter(
|
||||
Q(name__icontains=search) | Q(description__icontains=search)
|
||||
)
|
||||
|
||||
# Apply park type filter
|
||||
if park_type:
|
||||
queryset = queryset.filter(park_type=park_type)
|
||||
|
||||
# Apply status filter
|
||||
if status:
|
||||
queryset = queryset.filter(status=status)
|
||||
|
||||
# Apply operator filter
|
||||
if operator_id:
|
||||
queryset = queryset.filter(operator_id=operator_id)
|
||||
|
||||
# Apply ordering
|
||||
valid_order_fields = ['name', 'created', 'modified', 'opening_date', 'ride_count', 'coaster_count']
|
||||
order_field = ordering.lstrip('-')
|
||||
if order_field in valid_order_fields:
|
||||
queryset = queryset.order_by(ordering)
|
||||
else:
|
||||
queryset = queryset.order_by('-created')
|
||||
|
||||
# Annotate with operator name
|
||||
for park in queryset:
|
||||
park.operator_name = park.operator.name if park.operator else None
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{park_id}",
|
||||
response={200: ParkOut, 404: ErrorResponse},
|
||||
summary="Get park",
|
||||
description="Retrieve a single park by ID"
|
||||
)
|
||||
def get_park(request, park_id: UUID):
|
||||
"""
|
||||
Get a park by ID.
|
||||
|
||||
**Parameters:**
|
||||
- park_id: UUID of the park
|
||||
|
||||
**Returns:** Park details
|
||||
"""
|
||||
park = get_object_or_404(Park.objects.select_related('operator'), id=park_id)
|
||||
park.operator_name = park.operator.name if park.operator else None
|
||||
park.coordinates = park.coordinates
|
||||
return park
|
||||
|
||||
|
||||
@router.get(
|
||||
"/nearby/",
|
||||
response={200: List[ParkOut]},
|
||||
summary="Find nearby parks",
|
||||
description="Find parks within a radius of given coordinates. Uses PostGIS in production, bounding box in SQLite."
|
||||
)
|
||||
def find_nearby_parks(
|
||||
request,
|
||||
latitude: float = Query(..., description="Latitude coordinate"),
|
||||
longitude: float = Query(..., description="Longitude coordinate"),
|
||||
radius: float = Query(50, description="Search radius in kilometers"),
|
||||
limit: int = Query(50, description="Maximum number of results")
|
||||
):
|
||||
"""
|
||||
Find parks near a geographic point.
|
||||
|
||||
**Geographic Search Modes:**
|
||||
- **PostGIS (Production)**: Uses accurate distance-based search with location_point field
|
||||
- **SQLite (Local Dev)**: Uses bounding box approximation with latitude/longitude fields
|
||||
|
||||
**Parameters:**
|
||||
- latitude: Center point latitude
|
||||
- longitude: Center point longitude
|
||||
- radius: Search radius in kilometers (default: 50)
|
||||
- limit: Maximum results to return (default: 50)
|
||||
|
||||
**Returns:** List of nearby parks
|
||||
"""
|
||||
if _using_postgis:
|
||||
# Use PostGIS for accurate distance-based search
|
||||
try:
|
||||
from django.contrib.gis.measure import D
|
||||
from django.contrib.gis.geos import Point
|
||||
|
||||
user_point = Point(longitude, latitude, srid=4326)
|
||||
nearby_parks = Park.objects.filter(
|
||||
location_point__distance_lte=(user_point, D(km=radius))
|
||||
).select_related('operator')[:limit]
|
||||
except Exception as e:
|
||||
return {"detail": f"Geographic search error: {str(e)}"}, 500
|
||||
else:
|
||||
# Use bounding box approximation for SQLite
|
||||
# Calculate rough bounding box (1 degree ≈ 111 km at equator)
|
||||
lat_offset = radius / 111.0
|
||||
lng_offset = radius / (111.0 * math.cos(math.radians(latitude)))
|
||||
|
||||
min_lat = latitude - lat_offset
|
||||
max_lat = latitude + lat_offset
|
||||
min_lng = longitude - lng_offset
|
||||
max_lng = longitude + lng_offset
|
||||
|
||||
nearby_parks = Park.objects.filter(
|
||||
latitude__gte=Decimal(str(min_lat)),
|
||||
latitude__lte=Decimal(str(max_lat)),
|
||||
longitude__gte=Decimal(str(min_lng)),
|
||||
longitude__lte=Decimal(str(max_lng))
|
||||
).select_related('operator')[:limit]
|
||||
|
||||
# Annotate results
|
||||
results = []
|
||||
for park in nearby_parks:
|
||||
park.operator_name = park.operator.name if park.operator else None
|
||||
park.coordinates = park.coordinates
|
||||
results.append(park)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
@router.post(
|
||||
"/",
|
||||
response={201: ParkOut, 400: ErrorResponse},
|
||||
summary="Create park",
|
||||
description="Create a new park (requires authentication)"
|
||||
)
|
||||
def create_park(request, payload: ParkCreate):
|
||||
"""
|
||||
Create a new park.
|
||||
|
||||
**Authentication:** Required
|
||||
|
||||
**Parameters:**
|
||||
- payload: Park data
|
||||
|
||||
**Returns:** Created park
|
||||
"""
|
||||
# TODO: Add authentication check
|
||||
# if not request.auth:
|
||||
# return 401, {"detail": "Authentication required"}
|
||||
|
||||
data = payload.dict()
|
||||
|
||||
# Extract coordinates to use set_location method
|
||||
latitude = data.pop('latitude', None)
|
||||
longitude = data.pop('longitude', None)
|
||||
|
||||
park = Park.objects.create(**data)
|
||||
|
||||
# Set location using helper method (handles both SQLite and PostGIS)
|
||||
if latitude is not None and longitude is not None:
|
||||
park.set_location(longitude, latitude)
|
||||
park.save()
|
||||
|
||||
park.coordinates = park.coordinates
|
||||
if park.operator:
|
||||
park.operator_name = park.operator.name
|
||||
|
||||
return 201, park
|
||||
|
||||
|
||||
@router.put(
|
||||
"/{park_id}",
|
||||
response={200: ParkOut, 404: ErrorResponse, 400: ErrorResponse},
|
||||
summary="Update park",
|
||||
description="Update an existing park (requires authentication)"
|
||||
)
|
||||
def update_park(request, park_id: UUID, payload: ParkUpdate):
|
||||
"""
|
||||
Update a park.
|
||||
|
||||
**Authentication:** Required
|
||||
|
||||
**Parameters:**
|
||||
- park_id: UUID of the park
|
||||
- payload: Updated park data
|
||||
|
||||
**Returns:** Updated park
|
||||
"""
|
||||
# TODO: Add authentication check
|
||||
# if not request.auth:
|
||||
# return 401, {"detail": "Authentication required"}
|
||||
|
||||
park = get_object_or_404(Park.objects.select_related('operator'), id=park_id)
|
||||
|
||||
data = payload.dict(exclude_unset=True)
|
||||
|
||||
# Handle coordinates separately
|
||||
latitude = data.pop('latitude', None)
|
||||
longitude = data.pop('longitude', None)
|
||||
|
||||
# Update other fields
|
||||
for key, value in data.items():
|
||||
setattr(park, key, value)
|
||||
|
||||
# Update location if coordinates provided
|
||||
if latitude is not None and longitude is not None:
|
||||
park.set_location(longitude, latitude)
|
||||
|
||||
park.save()
|
||||
park.operator_name = park.operator.name if park.operator else None
|
||||
park.coordinates = park.coordinates
|
||||
|
||||
return park
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/{park_id}",
|
||||
response={200: ParkOut, 404: ErrorResponse, 400: ErrorResponse},
|
||||
summary="Partial update park",
|
||||
description="Partially update an existing park (requires authentication)"
|
||||
)
|
||||
def partial_update_park(request, park_id: UUID, payload: ParkUpdate):
|
||||
"""
|
||||
Partially update a park.
|
||||
|
||||
**Authentication:** Required
|
||||
|
||||
**Parameters:**
|
||||
- park_id: UUID of the park
|
||||
- payload: Fields to update
|
||||
|
||||
**Returns:** Updated park
|
||||
"""
|
||||
# TODO: Add authentication check
|
||||
# if not request.auth:
|
||||
# return 401, {"detail": "Authentication required"}
|
||||
|
||||
park = get_object_or_404(Park.objects.select_related('operator'), id=park_id)
|
||||
|
||||
data = payload.dict(exclude_unset=True)
|
||||
|
||||
# Handle coordinates separately
|
||||
latitude = data.pop('latitude', None)
|
||||
longitude = data.pop('longitude', None)
|
||||
|
||||
# Update other fields
|
||||
for key, value in data.items():
|
||||
setattr(park, key, value)
|
||||
|
||||
# Update location if coordinates provided
|
||||
if latitude is not None and longitude is not None:
|
||||
park.set_location(longitude, latitude)
|
||||
|
||||
park.save()
|
||||
park.operator_name = park.operator.name if park.operator else None
|
||||
park.coordinates = park.coordinates
|
||||
|
||||
return park
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{park_id}",
|
||||
response={204: None, 404: ErrorResponse},
|
||||
summary="Delete park",
|
||||
description="Delete a park (requires authentication)"
|
||||
)
|
||||
def delete_park(request, park_id: UUID):
|
||||
"""
|
||||
Delete a park.
|
||||
|
||||
**Authentication:** Required
|
||||
|
||||
**Parameters:**
|
||||
- park_id: UUID of the park
|
||||
|
||||
**Returns:** No content (204)
|
||||
"""
|
||||
# TODO: Add authentication check
|
||||
# if not request.auth:
|
||||
# return 401, {"detail": "Authentication required"}
|
||||
|
||||
park = get_object_or_404(Park, id=park_id)
|
||||
park.delete()
|
||||
return 204, None
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{park_id}/rides",
|
||||
response={200: List[dict], 404: ErrorResponse},
|
||||
summary="Get park rides",
|
||||
description="Get all rides at a park"
|
||||
)
|
||||
def get_park_rides(request, park_id: UUID):
|
||||
"""
|
||||
Get all rides at a park.
|
||||
|
||||
**Parameters:**
|
||||
- park_id: UUID of the park
|
||||
|
||||
**Returns:** List of rides
|
||||
"""
|
||||
park = get_object_or_404(Park, id=park_id)
|
||||
rides = park.rides.select_related('manufacturer').all().values(
|
||||
'id', 'name', 'slug', 'status', 'ride_category', 'is_coaster', 'manufacturer__name'
|
||||
)
|
||||
return list(rides)
|
||||
Reference in New Issue
Block a user