mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 21:11:12 -05:00
773 lines
26 KiB
Python
773 lines
26 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 apps.entities.services.ride_submission import RideSubmissionService
|
|
from apps.users.permissions import jwt_auth, require_auth
|
|
from ..schemas import (
|
|
RideCreate,
|
|
RideUpdate,
|
|
RideOut,
|
|
RideListOut,
|
|
RideNameHistoryOut,
|
|
ErrorResponse,
|
|
HistoryListResponse,
|
|
HistoryEventDetailSchema,
|
|
HistoryComparisonSchema,
|
|
HistoryDiffCurrentSchema,
|
|
FieldHistorySchema,
|
|
HistoryActivitySummarySchema,
|
|
RollbackRequestSchema,
|
|
RollbackResponseSchema,
|
|
ErrorSchema
|
|
)
|
|
from ..services.history_service import HistoryService
|
|
from django.core.exceptions import ValidationError
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
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
|
|
|
|
|
|
# ============================================================================
|
|
# History Endpoints
|
|
# ============================================================================
|
|
|
|
@router.get(
|
|
'/{ride_id}/history/',
|
|
response={200: HistoryListResponse, 404: ErrorSchema},
|
|
summary="Get ride history",
|
|
description="Get historical changes for a ride"
|
|
)
|
|
def get_ride_history(
|
|
request,
|
|
ride_id: UUID,
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(50, ge=1, le=100),
|
|
date_from: Optional[str] = Query(None, description="Filter from date (YYYY-MM-DD)"),
|
|
date_to: Optional[str] = Query(None, description="Filter to date (YYYY-MM-DD)")
|
|
):
|
|
"""Get history for a ride."""
|
|
from datetime import datetime
|
|
|
|
# Verify ride exists
|
|
ride = get_object_or_404(Ride, id=ride_id)
|
|
|
|
# Parse dates if provided
|
|
date_from_obj = datetime.fromisoformat(date_from).date() if date_from else None
|
|
date_to_obj = datetime.fromisoformat(date_to).date() if date_to else None
|
|
|
|
# Get history
|
|
offset = (page - 1) * page_size
|
|
events, accessible_count = HistoryService.get_history(
|
|
'ride', str(ride_id), request.user,
|
|
date_from=date_from_obj, date_to=date_to_obj,
|
|
limit=page_size, offset=offset
|
|
)
|
|
|
|
# Format events
|
|
formatted_events = []
|
|
for event in events:
|
|
formatted_events.append({
|
|
'id': event['id'],
|
|
'timestamp': event['timestamp'],
|
|
'operation': event['operation'],
|
|
'snapshot': event['snapshot'],
|
|
'changed_fields': event.get('changed_fields'),
|
|
'change_summary': event.get('change_summary', ''),
|
|
'can_rollback': HistoryService.can_rollback(request.user)
|
|
})
|
|
|
|
# Calculate pagination
|
|
total_pages = (accessible_count + page_size - 1) // page_size
|
|
|
|
return {
|
|
'entity_id': str(ride_id),
|
|
'entity_type': 'ride',
|
|
'entity_name': ride.name,
|
|
'total_events': accessible_count,
|
|
'accessible_events': accessible_count,
|
|
'access_limited': HistoryService.is_access_limited(request.user),
|
|
'access_reason': HistoryService.get_access_reason(request.user),
|
|
'events': formatted_events,
|
|
'pagination': {
|
|
'page': page,
|
|
'page_size': page_size,
|
|
'total_pages': total_pages,
|
|
'total_items': accessible_count
|
|
}
|
|
}
|
|
|
|
|
|
@router.get(
|
|
'/{ride_id}/history/{event_id}/',
|
|
response={200: HistoryEventDetailSchema, 404: ErrorSchema},
|
|
summary="Get specific ride history event",
|
|
description="Get detailed information about a specific historical event"
|
|
)
|
|
def get_ride_history_event(request, ride_id: UUID, event_id: int):
|
|
"""Get a specific history event for a ride."""
|
|
ride = get_object_or_404(Ride, id=ride_id)
|
|
event = HistoryService.get_event('ride', event_id, request.user)
|
|
|
|
if not event:
|
|
return 404, {"error": "Event not found or not accessible"}
|
|
|
|
return {
|
|
'id': event['id'],
|
|
'timestamp': event['timestamp'],
|
|
'operation': event['operation'],
|
|
'entity_id': str(ride_id),
|
|
'entity_type': 'ride',
|
|
'entity_name': ride.name,
|
|
'snapshot': event['snapshot'],
|
|
'changed_fields': event.get('changed_fields'),
|
|
'metadata': event.get('metadata', {}),
|
|
'can_rollback': HistoryService.can_rollback(request.user),
|
|
'rollback_preview': None
|
|
}
|
|
|
|
|
|
@router.get(
|
|
'/{ride_id}/history/compare/',
|
|
response={200: HistoryComparisonSchema, 400: ErrorSchema, 404: ErrorSchema},
|
|
summary="Compare two ride history events",
|
|
description="Compare two historical events for a ride"
|
|
)
|
|
def compare_ride_history(
|
|
request,
|
|
ride_id: UUID,
|
|
event1: int = Query(..., description="First event ID"),
|
|
event2: int = Query(..., description="Second event ID")
|
|
):
|
|
"""Compare two historical events for a ride."""
|
|
ride = get_object_or_404(Ride, id=ride_id)
|
|
|
|
try:
|
|
comparison = HistoryService.compare_events(
|
|
'ride', event1, event2, request.user
|
|
)
|
|
|
|
if not comparison:
|
|
return 404, {"error": "One or both events not found"}
|
|
|
|
return {
|
|
'entity_id': str(ride_id),
|
|
'entity_type': 'ride',
|
|
'entity_name': ride.name,
|
|
'event1': comparison['event1'],
|
|
'event2': comparison['event2'],
|
|
'differences': comparison['differences'],
|
|
'changed_field_count': comparison['changed_field_count'],
|
|
'unchanged_field_count': comparison['unchanged_field_count'],
|
|
'time_between': comparison['time_between']
|
|
}
|
|
except ValueError as e:
|
|
return 400, {"error": str(e)}
|
|
|
|
|
|
@router.get(
|
|
'/{ride_id}/history/{event_id}/diff-current/',
|
|
response={200: HistoryDiffCurrentSchema, 404: ErrorSchema},
|
|
summary="Compare historical event with current state",
|
|
description="Compare a historical event with the current ride state"
|
|
)
|
|
def diff_ride_history_with_current(request, ride_id: UUID, event_id: int):
|
|
"""Compare historical event with current ride state."""
|
|
ride = get_object_or_404(Ride, id=ride_id)
|
|
|
|
try:
|
|
diff = HistoryService.compare_with_current(
|
|
'ride', event_id, ride, request.user
|
|
)
|
|
|
|
if not diff:
|
|
return 404, {"error": "Event not found"}
|
|
|
|
return {
|
|
'entity_id': str(ride_id),
|
|
'entity_type': 'ride',
|
|
'entity_name': ride.name,
|
|
'event': diff['event'],
|
|
'current_state': diff['current_state'],
|
|
'differences': diff['differences'],
|
|
'changed_field_count': diff['changed_field_count'],
|
|
'time_since': diff['time_since']
|
|
}
|
|
except ValueError as e:
|
|
return 404, {"error": str(e)}
|
|
|
|
|
|
@router.post(
|
|
'/{ride_id}/history/{event_id}/rollback/',
|
|
response={200: RollbackResponseSchema, 400: ErrorSchema, 403: ErrorSchema},
|
|
summary="Rollback ride to historical state",
|
|
description="Rollback ride to a historical state (Moderators/Admins only)"
|
|
)
|
|
def rollback_ride(request, ride_id: UUID, event_id: int, payload: RollbackRequestSchema):
|
|
"""
|
|
Rollback ride to a historical state.
|
|
|
|
**Permission:** Moderators, Admins, Superusers only
|
|
"""
|
|
# Check authentication
|
|
if not request.user or not request.user.is_authenticated:
|
|
return 401, {"error": "Authentication required"}
|
|
|
|
# Check rollback permission
|
|
if not HistoryService.can_rollback(request.user):
|
|
return 403, {"error": "Only moderators and administrators can perform rollbacks"}
|
|
|
|
ride = get_object_or_404(Ride, id=ride_id)
|
|
|
|
try:
|
|
result = HistoryService.rollback_to_event(
|
|
ride, 'ride', event_id, request.user,
|
|
fields=payload.fields,
|
|
comment=payload.comment,
|
|
create_backup=payload.create_backup
|
|
)
|
|
return result
|
|
except (ValueError, PermissionError) as e:
|
|
return 400, {"error": str(e)}
|
|
|
|
|
|
@router.get(
|
|
'/{ride_id}/history/field/{field_name}/',
|
|
response={200: FieldHistorySchema, 404: ErrorSchema},
|
|
summary="Get field-specific history",
|
|
description="Get history of changes to a specific ride field"
|
|
)
|
|
def get_ride_field_history(request, ride_id: UUID, field_name: str):
|
|
"""Get history of changes to a specific ride field."""
|
|
ride = get_object_or_404(Ride, id=ride_id)
|
|
|
|
history = HistoryService.get_field_history(
|
|
'ride', str(ride_id), field_name, request.user
|
|
)
|
|
|
|
return {
|
|
'entity_id': str(ride_id),
|
|
'entity_type': 'ride',
|
|
'entity_name': ride.name,
|
|
'field': field_name,
|
|
'field_type': 'CharField', # Could introspect this
|
|
**history
|
|
}
|
|
|
|
|
|
@router.get(
|
|
'/{ride_id}/history/summary/',
|
|
response={200: HistoryActivitySummarySchema, 404: ErrorSchema},
|
|
summary="Get ride activity summary",
|
|
description="Get activity summary for a ride"
|
|
)
|
|
def get_ride_activity_summary(request, ride_id: UUID):
|
|
"""Get activity summary for a ride."""
|
|
ride = get_object_or_404(Ride, id=ride_id)
|
|
|
|
summary = HistoryService.get_activity_summary(
|
|
'ride', str(ride_id), request.user
|
|
)
|
|
|
|
return {
|
|
'entity_id': str(ride_id),
|
|
'entity_type': 'ride',
|
|
'entity_name': ride.name,
|
|
**summary
|
|
}
|
|
|
|
|
|
@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.get(
|
|
"/{ride_id}/name-history/",
|
|
response={200: List[RideNameHistoryOut], 404: ErrorResponse},
|
|
summary="Get ride name history",
|
|
description="Get historical names for a ride"
|
|
)
|
|
def get_ride_name_history(request, ride_id: UUID):
|
|
"""
|
|
Get historical names for a ride.
|
|
|
|
**Parameters:**
|
|
- ride_id: UUID of the ride
|
|
|
|
**Returns:** List of former ride names with date ranges
|
|
|
|
**Example Response:**
|
|
```json
|
|
[
|
|
{
|
|
"id": "...",
|
|
"former_name": "Original Name",
|
|
"from_year": 2000,
|
|
"to_year": 2010,
|
|
"date_changed": "2010-05-15",
|
|
"date_changed_precision": "day",
|
|
"reason": "Rebranding",
|
|
"order_index": 1,
|
|
"created_at": "...",
|
|
"updated_at": "..."
|
|
}
|
|
]
|
|
```
|
|
"""
|
|
ride = get_object_or_404(Ride, id=ride_id)
|
|
name_history = ride.name_history.all()
|
|
return list(name_history)
|
|
|
|
|
|
@router.post(
|
|
"/",
|
|
response={201: RideOut, 202: dict, 400: ErrorResponse, 401: ErrorResponse, 404: ErrorResponse},
|
|
summary="Create ride",
|
|
description="Create a new ride through the Sacred Pipeline (requires authentication)"
|
|
)
|
|
@require_auth
|
|
def create_ride(request, payload: RideCreate):
|
|
"""
|
|
Create a new ride through the Sacred Pipeline.
|
|
|
|
**Authentication:** Required
|
|
|
|
**Parameters:**
|
|
- payload: Ride data (name, park, ride_category, manufacturer, model, etc.)
|
|
|
|
**Returns:** Created ride (moderators) or submission confirmation (regular users)
|
|
|
|
**Flow:**
|
|
- Moderators: Ride created immediately (bypass moderation)
|
|
- Regular users: Submission created, enters moderation queue
|
|
|
|
**Note:** All rides flow through ContentSubmission pipeline for moderation.
|
|
"""
|
|
try:
|
|
user = request.auth
|
|
|
|
# Create ride through Sacred Pipeline
|
|
submission, ride = RideSubmissionService.create_entity_submission(
|
|
user=user,
|
|
data=payload.dict(),
|
|
source='api',
|
|
ip_address=request.META.get('REMOTE_ADDR'),
|
|
user_agent=request.META.get('HTTP_USER_AGENT', '')
|
|
)
|
|
|
|
# If moderator bypass happened, Ride was created immediately
|
|
if ride:
|
|
logger.info(f"Ride created (moderator): {ride.id} by {user.email}")
|
|
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
|
|
|
|
# Regular user: submission pending moderation
|
|
logger.info(f"Ride submission created: {submission.id} by {user.email}")
|
|
return 202, {
|
|
'submission_id': str(submission.id),
|
|
'status': submission.status,
|
|
'message': 'Ride submission pending moderation. You will be notified when it is approved.',
|
|
}
|
|
|
|
except ValidationError as e:
|
|
return 400, {'detail': str(e)}
|
|
except Exception as e:
|
|
logger.error(f"Error creating ride: {e}")
|
|
return 400, {'detail': str(e)}
|
|
|
|
|
|
@router.put(
|
|
"/{ride_id}",
|
|
response={200: RideOut, 202: dict, 404: ErrorResponse, 400: ErrorResponse, 401: ErrorResponse},
|
|
summary="Update ride",
|
|
description="Update an existing ride through the Sacred Pipeline (requires authentication)"
|
|
)
|
|
@require_auth
|
|
def update_ride(request, ride_id: UUID, payload: RideUpdate):
|
|
"""
|
|
Update a ride through the Sacred Pipeline.
|
|
|
|
**Authentication:** Required
|
|
|
|
**Parameters:**
|
|
- ride_id: UUID of the ride
|
|
- payload: Updated ride data
|
|
|
|
**Returns:** Updated ride (moderators) or submission confirmation (regular users)
|
|
|
|
**Flow:**
|
|
- Moderators: Updates applied immediately (bypass moderation)
|
|
- Regular users: Submission created, enters moderation queue
|
|
|
|
**Note:** All updates flow through ContentSubmission pipeline for moderation.
|
|
"""
|
|
try:
|
|
user = request.auth
|
|
ride = get_object_or_404(
|
|
Ride.objects.select_related('park', 'manufacturer', 'model'),
|
|
id=ride_id
|
|
)
|
|
|
|
data = payload.dict(exclude_unset=True)
|
|
|
|
# Update ride through Sacred Pipeline
|
|
submission, updated_ride = RideSubmissionService.update_entity_submission(
|
|
entity=ride,
|
|
user=user,
|
|
update_data=data,
|
|
source='api',
|
|
ip_address=request.META.get('REMOTE_ADDR'),
|
|
user_agent=request.META.get('HTTP_USER_AGENT', '')
|
|
)
|
|
|
|
# If moderator bypass happened, ride was updated immediately
|
|
if updated_ride:
|
|
logger.info(f"Ride updated (moderator): {updated_ride.id} by {user.email}")
|
|
updated_ride.park_name = updated_ride.park.name if updated_ride.park else None
|
|
updated_ride.manufacturer_name = updated_ride.manufacturer.name if updated_ride.manufacturer else None
|
|
updated_ride.model_name = updated_ride.model.name if updated_ride.model else None
|
|
return 200, updated_ride
|
|
|
|
# Regular user: submission pending moderation
|
|
logger.info(f"Ride update submission created: {submission.id} by {user.email}")
|
|
return 202, {
|
|
'submission_id': str(submission.id),
|
|
'status': submission.status,
|
|
'message': 'Ride update pending moderation. You will be notified when it is approved.',
|
|
}
|
|
|
|
except ValidationError as e:
|
|
return 400, {'detail': str(e)}
|
|
except Exception as e:
|
|
logger.error(f"Error updating ride: {e}")
|
|
return 400, {'detail': str(e)}
|
|
|
|
|
|
@router.patch(
|
|
"/{ride_id}",
|
|
response={200: RideOut, 202: dict, 404: ErrorResponse, 400: ErrorResponse, 401: ErrorResponse},
|
|
summary="Partial update ride",
|
|
description="Partially update an existing ride through the Sacred Pipeline (requires authentication)"
|
|
)
|
|
@require_auth
|
|
def partial_update_ride(request, ride_id: UUID, payload: RideUpdate):
|
|
"""
|
|
Partially update a ride through the Sacred Pipeline.
|
|
|
|
**Authentication:** Required
|
|
|
|
**Parameters:**
|
|
- ride_id: UUID of the ride
|
|
- payload: Fields to update (only provided fields are updated)
|
|
|
|
**Returns:** Updated ride (moderators) or submission confirmation (regular users)
|
|
|
|
**Flow:**
|
|
- Moderators: Updates applied immediately (bypass moderation)
|
|
- Regular users: Submission created, enters moderation queue
|
|
|
|
**Note:** All updates flow through ContentSubmission pipeline for moderation.
|
|
"""
|
|
try:
|
|
user = request.auth
|
|
ride = get_object_or_404(
|
|
Ride.objects.select_related('park', 'manufacturer', 'model'),
|
|
id=ride_id
|
|
)
|
|
|
|
data = payload.dict(exclude_unset=True)
|
|
|
|
# Update ride through Sacred Pipeline
|
|
submission, updated_ride = RideSubmissionService.update_entity_submission(
|
|
entity=ride,
|
|
user=user,
|
|
update_data=data,
|
|
source='api',
|
|
ip_address=request.META.get('REMOTE_ADDR'),
|
|
user_agent=request.META.get('HTTP_USER_AGENT', '')
|
|
)
|
|
|
|
# If moderator bypass happened, ride was updated immediately
|
|
if updated_ride:
|
|
logger.info(f"Ride partially updated (moderator): {updated_ride.id} by {user.email}")
|
|
updated_ride.park_name = updated_ride.park.name if updated_ride.park else None
|
|
updated_ride.manufacturer_name = updated_ride.manufacturer.name if updated_ride.manufacturer else None
|
|
updated_ride.model_name = updated_ride.model.name if updated_ride.model else None
|
|
return 200, updated_ride
|
|
|
|
# Regular user: submission pending moderation
|
|
logger.info(f"Ride partial update submission created: {submission.id} by {user.email}")
|
|
return 202, {
|
|
'submission_id': str(submission.id),
|
|
'status': submission.status,
|
|
'message': 'Ride update pending moderation. You will be notified when it is approved.',
|
|
}
|
|
|
|
except ValidationError as e:
|
|
return 400, {'detail': str(e)}
|
|
except Exception as e:
|
|
logger.error(f"Error partially updating ride: {e}")
|
|
return 400, {'detail': str(e)}
|
|
|
|
|
|
@router.delete(
|
|
"/{ride_id}",
|
|
response={200: dict, 202: dict, 404: ErrorResponse, 400: ErrorResponse, 401: ErrorResponse},
|
|
summary="Delete ride",
|
|
description="Delete a ride through the Sacred Pipeline (requires authentication)"
|
|
)
|
|
@require_auth
|
|
def delete_ride(request, ride_id: UUID):
|
|
"""
|
|
Delete a ride through the Sacred Pipeline.
|
|
|
|
**Authentication:** Required
|
|
|
|
**Parameters:**
|
|
- ride_id: UUID of the ride
|
|
|
|
**Returns:** Deletion confirmation (moderators) or submission confirmation (regular users)
|
|
|
|
**Flow:**
|
|
- Moderators: Ride soft-deleted immediately (status set to 'closed')
|
|
- Regular users: Deletion request created, enters moderation queue
|
|
|
|
**Deletion Strategy:**
|
|
- Soft Delete (default): Sets ride status to 'closed', preserves data
|
|
- Hard Delete: Actually removes from database (moderators only)
|
|
|
|
**Note:** All deletions flow through ContentSubmission pipeline for moderation.
|
|
"""
|
|
try:
|
|
user = request.auth
|
|
ride = get_object_or_404(Ride.objects.select_related('park', 'manufacturer'), id=ride_id)
|
|
|
|
# Delete ride through Sacred Pipeline (soft delete by default)
|
|
submission, deleted = RideSubmissionService.delete_entity_submission(
|
|
entity=ride,
|
|
user=user,
|
|
deletion_type='soft',
|
|
deletion_reason='',
|
|
source='api',
|
|
ip_address=request.META.get('REMOTE_ADDR'),
|
|
user_agent=request.META.get('HTTP_USER_AGENT', '')
|
|
)
|
|
|
|
# If moderator bypass happened, deletion was applied immediately
|
|
if deleted:
|
|
logger.info(f"Ride deleted (moderator): {ride_id} by {user.email}")
|
|
return 200, {
|
|
'message': 'Ride deleted successfully',
|
|
'entity_id': str(ride_id),
|
|
'deletion_type': 'soft'
|
|
}
|
|
|
|
# Regular user: deletion pending moderation
|
|
logger.info(f"Ride deletion submission created: {submission.id} by {user.email}")
|
|
return 202, {
|
|
'submission_id': str(submission.id),
|
|
'status': submission.status,
|
|
'message': 'Ride deletion request pending moderation. You will be notified when it is approved.',
|
|
'entity_id': str(ride_id)
|
|
}
|
|
|
|
except ValidationError as e:
|
|
return 400, {'detail': str(e)}
|
|
except Exception as e:
|
|
logger.error(f"Error deleting ride: {e}")
|
|
return 400, {'detail': str(e)}
|
|
|
|
|
|
@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
|