""" Ride Model endpoints for API v1. Provides CRUD operations for RideModel 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 RideModel, Company from apps.entities.services.ride_model_submission import RideModelSubmissionService from apps.users.permissions import jwt_auth, require_auth from ..schemas import ( RideModelCreate, RideModelUpdate, RideModelOut, RideModelListOut, 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=["Ride Models"]) class RideModelPagination(PageNumberPagination): """Custom pagination for ride models.""" page_size = 50 @router.get( "/", response={200: List[RideModelOut]}, summary="List ride models", description="Get a paginated list of ride models with optional filtering" ) @paginate(RideModelPagination) def list_ride_models( request, search: Optional[str] = Query(None, description="Search by model name"), manufacturer_id: Optional[UUID] = Query(None, description="Filter by manufacturer"), model_type: Optional[str] = Query(None, description="Filter by model type"), ordering: Optional[str] = Query("-created", description="Sort by field (prefix with - for descending)") ): """ List all ride models with optional filters. **Filters:** - search: Search model names (case-insensitive partial match) - manufacturer_id: Filter by manufacturer - model_type: Filter by model type - ordering: Sort results (default: -created) **Returns:** Paginated list of ride models """ queryset = RideModel.objects.select_related('manufacturer').all() # Apply search filter if search: queryset = queryset.filter( Q(name__icontains=search) | Q(description__icontains=search) ) # Apply manufacturer filter if manufacturer_id: queryset = queryset.filter(manufacturer_id=manufacturer_id) # Apply model type filter if model_type: queryset = queryset.filter(model_type=model_type) # Apply ordering valid_order_fields = ['name', 'created', 'modified', 'installation_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 manufacturer name for model in queryset: model.manufacturer_name = model.manufacturer.name if model.manufacturer else None return queryset @router.get( "/{model_id}", response={200: RideModelOut, 404: ErrorResponse}, summary="Get ride model", description="Retrieve a single ride model by ID" ) def get_ride_model(request, model_id: UUID): """ Get a ride model by ID. **Parameters:** - model_id: UUID of the ride model **Returns:** Ride model details """ model = get_object_or_404(RideModel.objects.select_related('manufacturer'), id=model_id) model.manufacturer_name = model.manufacturer.name if model.manufacturer else None return model @router.post( "/", response={201: RideModelOut, 202: dict, 400: ErrorResponse, 401: ErrorResponse, 404: ErrorResponse}, summary="Create ride model", description="Create a new ride model through the Sacred Pipeline (requires authentication)" ) @require_auth def create_ride_model(request, payload: RideModelCreate): """ Create a new ride model through the Sacred Pipeline. **Authentication:** Required **Parameters:** - payload: Ride model data (name, manufacturer, model_type, specifications, etc.) **Returns:** Created ride model (moderators) or submission confirmation (regular users) **Flow:** - Moderators: Ride model created immediately (bypass moderation) - Regular users: Submission created, enters moderation queue **Note:** All ride models flow through ContentSubmission pipeline for moderation. """ try: user = request.auth # Create ride model through Sacred Pipeline submission, ride_model = RideModelSubmissionService.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, RideModel was created immediately if ride_model: logger.info(f"RideModel created (moderator): {ride_model.id} by {user.email}") ride_model.manufacturer_name = ride_model.manufacturer.name if ride_model.manufacturer else None return 201, ride_model # Regular user: submission pending moderation logger.info(f"RideModel submission created: {submission.id} by {user.email}") return 202, { 'submission_id': str(submission.id), 'status': submission.status, 'message': 'Ride model 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 model: {e}") return 400, {'detail': str(e)} @router.put( "/{model_id}", response={200: RideModelOut, 202: dict, 404: ErrorResponse, 400: ErrorResponse, 401: ErrorResponse}, summary="Update ride model", description="Update an existing ride model through the Sacred Pipeline (requires authentication)" ) @require_auth def update_ride_model(request, model_id: UUID, payload: RideModelUpdate): """ Update a ride model through the Sacred Pipeline. **Authentication:** Required **Parameters:** - model_id: UUID of the ride model - payload: Updated ride model data **Returns:** Updated ride model (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 model = get_object_or_404(RideModel.objects.select_related('manufacturer'), id=model_id) data = payload.dict(exclude_unset=True) # Update ride model through Sacred Pipeline submission, updated_model = RideModelSubmissionService.update_entity_submission( entity=model, 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 model was updated immediately if updated_model: logger.info(f"RideModel updated (moderator): {updated_model.id} by {user.email}") updated_model.manufacturer_name = updated_model.manufacturer.name if updated_model.manufacturer else None return 200, updated_model # Regular user: submission pending moderation logger.info(f"RideModel update submission created: {submission.id} by {user.email}") return 202, { 'submission_id': str(submission.id), 'status': submission.status, 'message': 'Ride model 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 model: {e}") return 400, {'detail': str(e)} @router.patch( "/{model_id}", response={200: RideModelOut, 202: dict, 404: ErrorResponse, 400: ErrorResponse, 401: ErrorResponse}, summary="Partial update ride model", description="Partially update an existing ride model through the Sacred Pipeline (requires authentication)" ) @require_auth def partial_update_ride_model(request, model_id: UUID, payload: RideModelUpdate): """ Partially update a ride model through the Sacred Pipeline. **Authentication:** Required **Parameters:** - model_id: UUID of the ride model - payload: Fields to update (only provided fields are updated) **Returns:** Updated ride model (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 model = get_object_or_404(RideModel.objects.select_related('manufacturer'), id=model_id) data = payload.dict(exclude_unset=True) # Update ride model through Sacred Pipeline submission, updated_model = RideModelSubmissionService.update_entity_submission( entity=model, 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 model was updated immediately if updated_model: logger.info(f"RideModel partially updated (moderator): {updated_model.id} by {user.email}") updated_model.manufacturer_name = updated_model.manufacturer.name if updated_model.manufacturer else None return 200, updated_model # Regular user: submission pending moderation logger.info(f"RideModel partial update submission created: {submission.id} by {user.email}") return 202, { 'submission_id': str(submission.id), 'status': submission.status, 'message': 'Ride model 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 model: {e}") return 400, {'detail': str(e)} @router.delete( "/{model_id}", response={200: dict, 202: dict, 404: ErrorResponse, 400: ErrorResponse, 401: ErrorResponse}, summary="Delete ride model", description="Delete a ride model through the Sacred Pipeline (requires authentication)" ) @require_auth def delete_ride_model(request, model_id: UUID): """ Delete a ride model through the Sacred Pipeline. **Authentication:** Required **Parameters:** - model_id: UUID of the ride model **Returns:** Deletion confirmation (moderators) or submission confirmation (regular users) **Flow:** - Moderators: RideModel hard-deleted immediately (removed from database) - Regular users: Deletion request created, enters moderation queue **Deletion Strategy:** - Hard Delete: Removes ride model from database (RideModel has no status field for soft delete) **Note:** All deletions flow through ContentSubmission pipeline for moderation. **Warning:** Deleting a ride model may affect related rides. """ try: user = request.auth model = get_object_or_404(RideModel.objects.select_related('manufacturer'), id=model_id) # Delete ride model through Sacred Pipeline (hard delete - no status field) submission, deleted = RideModelSubmissionService.delete_entity_submission( entity=model, user=user, deletion_type='hard', # RideModel has no status field 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"RideModel deleted (moderator): {model_id} by {user.email}") return 200, { 'message': 'Ride model deleted successfully', 'entity_id': str(model_id), 'deletion_type': 'hard' } # Regular user: deletion pending moderation logger.info(f"RideModel deletion submission created: {submission.id} by {user.email}") return 202, { 'submission_id': str(submission.id), 'status': submission.status, 'message': 'Ride model deletion request pending moderation. You will be notified when it is approved.', 'entity_id': str(model_id) } except ValidationError as e: return 400, {'detail': str(e)} except Exception as e: logger.error(f"Error deleting ride model: {e}") return 400, {'detail': str(e)} @router.get( "/{model_id}/installations", response={200: List[dict], 404: ErrorResponse}, summary="Get ride model installations", description="Get all ride installations of this model" ) def get_ride_model_installations(request, model_id: UUID): """ Get all installations of a ride model. **Parameters:** - model_id: UUID of the ride model **Returns:** List of rides using this model """ model = get_object_or_404(RideModel, id=model_id) rides = model.rides.select_related('park').all().values( 'id', 'name', 'slug', 'status', 'park__name', 'park__id' ) return list(rides) # ============================================================================ # History Endpoints # ============================================================================ @router.get( '/{model_id}/history/', response={200: HistoryListResponse, 404: ErrorSchema}, summary="Get ride model history", description="Get historical changes for a ride model" ) def get_ride_model_history( request, model_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 model.""" from datetime import datetime # Verify ride model exists ride_model = get_object_or_404(RideModel, id=model_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( 'ridemodel', str(model_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(model_id), 'entity_type': 'ridemodel', 'entity_name': ride_model.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( '/{model_id}/history/{event_id}/', response={200: HistoryEventDetailSchema, 404: ErrorSchema}, summary="Get specific ride model history event", description="Get detailed information about a specific historical event" ) def get_ride_model_history_event(request, model_id: UUID, event_id: int): """Get a specific history event for a ride model.""" ride_model = get_object_or_404(RideModel, id=model_id) event = HistoryService.get_event('ridemodel', 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(model_id), 'entity_type': 'ridemodel', 'entity_name': ride_model.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( '/{model_id}/history/compare/', response={200: HistoryComparisonSchema, 400: ErrorSchema, 404: ErrorSchema}, summary="Compare two ride model history events", description="Compare two historical events for a ride model" ) def compare_ride_model_history( request, model_id: UUID, event1: int = Query(..., description="First event ID"), event2: int = Query(..., description="Second event ID") ): """Compare two historical events for a ride model.""" ride_model = get_object_or_404(RideModel, id=model_id) try: comparison = HistoryService.compare_events( 'ridemodel', event1, event2, request.user ) if not comparison: return 404, {"error": "One or both events not found"} return { 'entity_id': str(model_id), 'entity_type': 'ridemodel', 'entity_name': ride_model.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( '/{model_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 model state" ) def diff_ride_model_history_with_current(request, model_id: UUID, event_id: int): """Compare historical event with current ride model state.""" ride_model = get_object_or_404(RideModel, id=model_id) try: diff = HistoryService.compare_with_current( 'ridemodel', event_id, ride_model, request.user ) if not diff: return 404, {"error": "Event not found"} return { 'entity_id': str(model_id), 'entity_type': 'ridemodel', 'entity_name': ride_model.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( '/{model_id}/history/{event_id}/rollback/', response={200: RollbackResponseSchema, 400: ErrorSchema, 403: ErrorSchema}, summary="Rollback ride model to historical state", description="Rollback ride model to a historical state (Moderators/Admins only)" ) def rollback_ride_model(request, model_id: UUID, event_id: int, payload: RollbackRequestSchema): """ Rollback ride model 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_model = get_object_or_404(RideModel, id=model_id) try: result = HistoryService.rollback_to_event( ride_model, 'ridemodel', 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( '/{model_id}/history/field/{field_name}/', response={200: FieldHistorySchema, 404: ErrorSchema}, summary="Get field-specific history", description="Get history of changes to a specific ride model field" ) def get_ride_model_field_history(request, model_id: UUID, field_name: str): """Get history of changes to a specific ride model field.""" ride_model = get_object_or_404(RideModel, id=model_id) history = HistoryService.get_field_history( 'ridemodel', str(model_id), field_name, request.user ) return { 'entity_id': str(model_id), 'entity_type': 'ridemodel', 'entity_name': ride_model.name, 'field': field_name, 'field_type': 'CharField', # Could introspect this **history } @router.get( '/{model_id}/history/summary/', response={200: HistoryActivitySummarySchema, 404: ErrorSchema}, summary="Get ride model activity summary", description="Get activity summary for a ride model" ) def get_ride_model_activity_summary(request, model_id: UUID): """Get activity summary for a ride model.""" ride_model = get_object_or_404(RideModel, id=model_id) summary = HistoryService.get_activity_summary( 'ridemodel', str(model_id), request.user ) return { 'entity_id': str(model_id), 'entity_type': 'ridemodel', 'entity_name': ride_model.name, **summary }