""" Timeline API endpoints. Handles entity timeline events for tracking significant lifecycle events like openings, closings, relocations, etc. """ from typing import List from uuid import UUID from ninja import Router, Query from django.shortcuts import get_object_or_404 from django.db.models import Q, Count, Min, Max from apps.timeline.models import EntityTimelineEvent from apps.entities.models import Park, Ride, Company, RideModel from apps.users.permissions import require_role from api.v1.schemas import ( EntityTimelineEventOut, EntityTimelineEventCreate, EntityTimelineEventUpdate, EntityTimelineEventListOut, TimelineStatsOut, MessageSchema, ErrorResponse, ) router = Router(tags=["Timeline"]) def get_entity_model(entity_type: str): """Get the Django model class for an entity type.""" models = { 'park': Park, 'ride': Ride, 'company': Company, 'ridemodel': RideModel, } return models.get(entity_type.lower()) def serialize_timeline_event(event: EntityTimelineEvent) -> dict: """Serialize a timeline event to dict for output.""" return { 'id': event.id, 'entity_id': event.entity_id, 'entity_type': event.entity_type, 'event_type': event.event_type, 'event_date': event.event_date, 'event_date_precision': event.event_date_precision, 'title': event.title, 'description': event.description, 'from_entity_id': event.from_entity_id, 'to_entity_id': event.to_entity_id, 'from_location_id': event.from_location_id, 'from_location_name': event.from_location.name if event.from_location else None, 'to_location_id': event.to_location_id, 'to_location_name': event.to_location.name if event.to_location else None, 'from_value': event.from_value, 'to_value': event.to_value, 'is_public': event.is_public, 'display_order': event.display_order, 'created_by_id': event.created_by_id, 'created_by_email': event.created_by.email if event.created_by else None, 'approved_by_id': event.approved_by_id, 'approved_by_email': event.approved_by.email if event.approved_by else None, 'submission_id': event.submission_id, 'created_at': event.created_at, 'updated_at': event.updated_at, } @router.get("/{entity_type}/{entity_id}/", response={200: List[EntityTimelineEventOut], 404: ErrorResponse}) def get_entity_timeline( request, entity_type: str, entity_id: UUID, event_type: str = Query(None, description="Filter by event type"), is_public: bool = Query(None, description="Filter by public/private"), page: int = Query(1, ge=1), page_size: int = Query(50, ge=1, le=100), ): """ Get timeline events for a specific entity. Returns a paginated list of timeline events for the specified entity. Regular users only see public events; moderators see all events. """ # Validate entity type model = get_entity_model(entity_type) if not model: return 404, {'detail': f'Invalid entity type: {entity_type}'} # Verify entity exists entity = get_object_or_404(model, id=entity_id) # Build query queryset = EntityTimelineEvent.objects.filter( entity_type=entity_type.lower(), entity_id=entity_id ).select_related('from_location', 'to_location', 'created_by', 'approved_by') # Filter by public status (non-moderators only see public events) is_moderator = hasattr(request.user, 'role') and request.user.role in ['moderator', 'admin'] if not is_moderator: queryset = queryset.filter(is_public=True) # Apply filters if event_type: queryset = queryset.filter(event_type=event_type) if is_public is not None: queryset = queryset.filter(is_public=is_public) # Order by date (newest first) and display order queryset = queryset.order_by('-event_date', 'display_order', '-created_at') # Pagination total = queryset.count() start = (page - 1) * page_size end = start + page_size events = queryset[start:end] return 200, [serialize_timeline_event(event) for event in events] @router.get("/recent/", response={200: List[EntityTimelineEventOut]}) def get_recent_timeline_events( request, entity_type: str = Query(None, description="Filter by entity type"), event_type: str = Query(None, description="Filter by event type"), limit: int = Query(20, ge=1, le=100), ): """ Get recent timeline events across all entities. Returns the most recent timeline events. Only public events are returned for regular users; moderators see all events. """ # Build query queryset = EntityTimelineEvent.objects.all().select_related( 'from_location', 'to_location', 'created_by', 'approved_by' ) # Filter by public status is_moderator = hasattr(request.user, 'role') and request.user.role in ['moderator', 'admin'] if not is_moderator: queryset = queryset.filter(is_public=True) # Apply filters if entity_type: queryset = queryset.filter(entity_type=entity_type.lower()) if event_type: queryset = queryset.filter(event_type=event_type) # Order by date and limit queryset = queryset.order_by('-event_date', '-created_at')[:limit] return 200, [serialize_timeline_event(event) for event in queryset] @router.get("/stats/{entity_type}/{entity_id}/", response={200: TimelineStatsOut, 404: ErrorResponse}) def get_timeline_stats(request, entity_type: str, entity_id: UUID): """ Get statistics about timeline events for an entity. """ # Validate entity type model = get_entity_model(entity_type) if not model: return 404, {'detail': f'Invalid entity type: {entity_type}'} # Verify entity exists entity = get_object_or_404(model, id=entity_id) # Build query queryset = EntityTimelineEvent.objects.filter( entity_type=entity_type.lower(), entity_id=entity_id ) # Filter by public status if not moderator is_moderator = hasattr(request.user, 'role') and request.user.role in ['moderator', 'admin'] if not is_moderator: queryset = queryset.filter(is_public=True) # Get stats total_events = queryset.count() public_events = queryset.filter(is_public=True).count() # Event type distribution event_types = dict(queryset.values('event_type').annotate(count=Count('id')).values_list('event_type', 'count')) # Date range date_stats = queryset.aggregate( earliest=Min('event_date'), latest=Max('event_date') ) return 200, { 'total_events': total_events, 'public_events': public_events, 'event_types': event_types, 'earliest_event': date_stats['earliest'], 'latest_event': date_stats['latest'], } @router.post("/", response={201: EntityTimelineEventOut, 400: ErrorResponse, 403: ErrorResponse}) @require_role(['moderator', 'admin']) def create_timeline_event(request, data: EntityTimelineEventCreate): """ Create a new timeline event (moderators only). Allows moderators to manually create timeline events for entities. """ # Validate entity exists model = get_entity_model(data.entity_type) if not model: return 400, {'detail': f'Invalid entity type: {data.entity_type}'} entity = get_object_or_404(model, id=data.entity_id) # Validate locations if provided if data.from_location_id: get_object_or_404(Park, id=data.from_location_id) if data.to_location_id: get_object_or_404(Park, id=data.to_location_id) # Create event event = EntityTimelineEvent.objects.create( entity_id=data.entity_id, entity_type=data.entity_type.lower(), event_type=data.event_type, event_date=data.event_date, event_date_precision=data.event_date_precision or 'day', title=data.title, description=data.description, from_entity_id=data.from_entity_id, to_entity_id=data.to_entity_id, from_location_id=data.from_location_id, to_location_id=data.to_location_id, from_value=data.from_value, to_value=data.to_value, is_public=data.is_public, display_order=data.display_order, created_by=request.user, approved_by=request.user, # Moderator-created events are auto-approved ) return 201, serialize_timeline_event(event) @router.patch("/{event_id}/", response={200: EntityTimelineEventOut, 404: ErrorResponse, 403: ErrorResponse}) @require_role(['moderator', 'admin']) def update_timeline_event(request, event_id: UUID, data: EntityTimelineEventUpdate): """ Update a timeline event (moderators only). """ event = get_object_or_404(EntityTimelineEvent, id=event_id) # Update fields if provided update_fields = [] if data.event_type is not None: event.event_type = data.event_type update_fields.append('event_type') if data.event_date is not None: event.event_date = data.event_date update_fields.append('event_date') if data.event_date_precision is not None: event.event_date_precision = data.event_date_precision update_fields.append('event_date_precision') if data.title is not None: event.title = data.title update_fields.append('title') if data.description is not None: event.description = data.description update_fields.append('description') if data.from_entity_id is not None: event.from_entity_id = data.from_entity_id update_fields.append('from_entity_id') if data.to_entity_id is not None: event.to_entity_id = data.to_entity_id update_fields.append('to_entity_id') if data.from_location_id is not None: # Validate park exists if data.from_location_id: get_object_or_404(Park, id=data.from_location_id) event.from_location_id = data.from_location_id update_fields.append('from_location_id') if data.to_location_id is not None: # Validate park exists if data.to_location_id: get_object_or_404(Park, id=data.to_location_id) event.to_location_id = data.to_location_id update_fields.append('to_location_id') if data.from_value is not None: event.from_value = data.from_value update_fields.append('from_value') if data.to_value is not None: event.to_value = data.to_value update_fields.append('to_value') if data.is_public is not None: event.is_public = data.is_public update_fields.append('is_public') if data.display_order is not None: event.display_order = data.display_order update_fields.append('display_order') if update_fields: update_fields.append('updated_at') event.save(update_fields=update_fields) return 200, serialize_timeline_event(event) @router.delete("/{event_id}/", response={200: MessageSchema, 404: ErrorResponse, 403: ErrorResponse}) @require_role(['moderator', 'admin']) def delete_timeline_event(request, event_id: UUID): """ Delete a timeline event (moderators only). """ event = get_object_or_404(EntityTimelineEvent, id=event_id) event.delete() return 200, { 'message': 'Timeline event deleted successfully', 'success': True }