mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 05:31:12 -05:00
340 lines
11 KiB
Python
340 lines
11 KiB
Python
"""
|
|
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
|
|
}
|