""" Moderation API endpoints. Provides REST API for content submission and moderation workflow. """ from typing import List, Optional from uuid import UUID from ninja import Router from django.shortcuts import get_object_or_404 from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError, PermissionDenied from apps.moderation.models import ContentSubmission, SubmissionItem from apps.moderation.services import ModerationService from api.v1.schemas import ( ContentSubmissionCreate, ContentSubmissionOut, ContentSubmissionDetail, SubmissionListOut, StartReviewRequest, ApproveRequest, ApproveSelectiveRequest, RejectRequest, RejectSelectiveRequest, ApprovalResponse, SelectiveApprovalResponse, SelectiveRejectionResponse, ErrorResponse, ) router = Router(tags=['Moderation']) # ============================================================================ # Helper Functions # ============================================================================ def _submission_to_dict(submission: ContentSubmission) -> dict: """Convert submission model to dict for schema.""" return { 'id': submission.id, 'status': submission.status, 'submission_type': submission.submission_type, 'title': submission.title, 'description': submission.description or '', 'entity_type': submission.entity_type.model, 'entity_id': submission.entity_id, 'user_id': submission.user.id, 'user_email': submission.user.email, 'locked_by_id': submission.locked_by.id if submission.locked_by else None, 'locked_by_email': submission.locked_by.email if submission.locked_by else None, 'locked_at': submission.locked_at, 'reviewed_by_id': submission.reviewed_by.id if submission.reviewed_by else None, 'reviewed_by_email': submission.reviewed_by.email if submission.reviewed_by else None, 'reviewed_at': submission.reviewed_at, 'rejection_reason': submission.rejection_reason or '', 'source': submission.source, 'metadata': submission.metadata, 'items_count': submission.get_items_count(), 'approved_items_count': submission.get_approved_items_count(), 'rejected_items_count': submission.get_rejected_items_count(), 'created': submission.created, 'modified': submission.modified, } def _item_to_dict(item: SubmissionItem) -> dict: """Convert submission item model to dict for schema.""" return { 'id': item.id, 'submission_id': item.submission.id, 'field_name': item.field_name, 'field_label': item.field_label or item.field_name, 'old_value': item.old_value, 'new_value': item.new_value, 'change_type': item.change_type, 'is_required': item.is_required, 'order': item.order, 'status': item.status, 'reviewed_by_id': item.reviewed_by.id if item.reviewed_by else None, 'reviewed_by_email': item.reviewed_by.email if item.reviewed_by else None, 'reviewed_at': item.reviewed_at, 'rejection_reason': item.rejection_reason or '', 'old_value_display': item.old_value_display, 'new_value_display': item.new_value_display, 'created': item.created, 'modified': item.modified, } def _get_entity(entity_type: str, entity_id: UUID): """Get entity instance from type string and ID.""" # Map entity type strings to models type_map = { 'park': 'entities.Park', 'ride': 'entities.Ride', 'company': 'entities.Company', 'ridemodel': 'entities.RideModel', } app_label, model = type_map.get(entity_type.lower(), '').split('.') content_type = ContentType.objects.get(app_label=app_label, model=model.lower()) model_class = content_type.model_class() return get_object_or_404(model_class, id=entity_id) # ============================================================================ # Submission Endpoints # ============================================================================ @router.post('/submissions', response={201: ContentSubmissionOut, 400: ErrorResponse, 401: ErrorResponse}) def create_submission(request, data: ContentSubmissionCreate): """ Create a new content submission. Creates a submission with multiple items representing field changes. If auto_submit is True, the submission is immediately moved to pending state. """ # TODO: Require authentication # For now, use a test user or get from request from apps.users.models import User user = User.objects.first() # TEMP: Get first user for testing if not user: return 401, {'detail': 'Authentication required'} try: # Get entity entity = _get_entity(data.entity_type, data.entity_id) # Prepare items data items_data = [ { 'field_name': item.field_name, 'field_label': item.field_label, 'old_value': item.old_value, 'new_value': item.new_value, 'change_type': item.change_type, 'is_required': item.is_required, 'order': item.order, } for item in data.items ] # Create submission submission = ModerationService.create_submission( user=user, entity=entity, submission_type=data.submission_type, title=data.title, description=data.description or '', items_data=items_data, metadata=data.metadata, auto_submit=data.auto_submit, source='api' ) return 201, _submission_to_dict(submission) except Exception as e: return 400, {'detail': str(e)} @router.get('/submissions', response=SubmissionListOut) def list_submissions( request, status: Optional[str] = None, page: int = 1, page_size: int = 50 ): """ List content submissions with optional filtering. Query Parameters: - status: Filter by status (draft, pending, reviewing, approved, rejected) - page: Page number (default: 1) - page_size: Items per page (default: 50, max: 100) """ # Validate page_size page_size = min(page_size, 100) offset = (page - 1) * page_size # Get submissions submissions = ModerationService.get_queue( status=status, limit=page_size, offset=offset ) # Get total count total_queryset = ContentSubmission.objects.all() if status: total_queryset = total_queryset.filter(status=status) total = total_queryset.count() # Calculate total pages total_pages = (total + page_size - 1) // page_size # Convert to dicts items = [_submission_to_dict(sub) for sub in submissions] return { 'items': items, 'total': total, 'page': page, 'page_size': page_size, 'total_pages': total_pages, } @router.get('/submissions/{submission_id}', response={200: ContentSubmissionDetail, 404: ErrorResponse}) def get_submission(request, submission_id: UUID): """ Get detailed submission information with all items. """ try: submission = ModerationService.get_submission_details(submission_id) # Convert to dict with items data = _submission_to_dict(submission) data['items'] = [_item_to_dict(item) for item in submission.items.all()] return 200, data except ContentSubmission.DoesNotExist: return 404, {'detail': 'Submission not found'} @router.delete('/submissions/{submission_id}', response={204: None, 403: ErrorResponse, 404: ErrorResponse}) def delete_submission(request, submission_id: UUID): """ Delete a submission (only if draft/pending and owned by user). """ # TODO: Get current user from request from apps.users.models import User user = User.objects.first() # TEMP try: ModerationService.delete_submission(submission_id, user) return 204, None except ContentSubmission.DoesNotExist: return 404, {'detail': 'Submission not found'} except PermissionDenied as e: return 403, {'detail': str(e)} except ValidationError as e: return 400, {'detail': str(e)} # ============================================================================ # Review Endpoints # ============================================================================ @router.post( '/submissions/{submission_id}/start-review', response={200: ContentSubmissionOut, 400: ErrorResponse, 403: ErrorResponse, 404: ErrorResponse} ) def start_review(request, submission_id: UUID, data: StartReviewRequest): """ Start reviewing a submission (lock it for 15 minutes). Only moderators can start reviews. """ # TODO: Get current user (moderator) from request from apps.users.models import User user = User.objects.first() # TEMP try: submission = ModerationService.start_review(submission_id, user) return 200, _submission_to_dict(submission) except ContentSubmission.DoesNotExist: return 404, {'detail': 'Submission not found'} except PermissionDenied as e: return 403, {'detail': str(e)} except ValidationError as e: return 400, {'detail': str(e)} @router.post( '/submissions/{submission_id}/approve', response={200: ApprovalResponse, 400: ErrorResponse, 403: ErrorResponse, 404: ErrorResponse} ) def approve_submission(request, submission_id: UUID, data: ApproveRequest): """ Approve an entire submission and apply all changes. Uses atomic transactions - all changes are applied or none are. Only moderators can approve submissions. """ # TODO: Get current user (moderator) from request from apps.users.models import User user = User.objects.first() # TEMP try: submission = ModerationService.approve_submission(submission_id, user) return 200, { 'success': True, 'message': 'Submission approved successfully', 'submission': _submission_to_dict(submission) } except ContentSubmission.DoesNotExist: return 404, {'detail': 'Submission not found'} except PermissionDenied as e: return 403, {'detail': str(e)} except ValidationError as e: return 400, {'detail': str(e)} @router.post( '/submissions/{submission_id}/approve-selective', response={200: SelectiveApprovalResponse, 400: ErrorResponse, 403: ErrorResponse, 404: ErrorResponse} ) def approve_selective(request, submission_id: UUID, data: ApproveSelectiveRequest): """ Approve only specific items in a submission. Allows moderators to approve some changes while leaving others pending or rejected. Uses atomic transactions for data integrity. """ # TODO: Get current user (moderator) from request from apps.users.models import User user = User.objects.first() # TEMP try: result = ModerationService.approve_selective( submission_id, user, [str(item_id) for item_id in data.item_ids] ) return 200, { 'success': True, 'message': f"Approved {result['approved']} of {result['total']} items", **result } except ContentSubmission.DoesNotExist: return 404, {'detail': 'Submission not found'} except PermissionDenied as e: return 403, {'detail': str(e)} except ValidationError as e: return 400, {'detail': str(e)} @router.post( '/submissions/{submission_id}/reject', response={200: ApprovalResponse, 400: ErrorResponse, 403: ErrorResponse, 404: ErrorResponse} ) def reject_submission(request, submission_id: UUID, data: RejectRequest): """ Reject an entire submission. All pending items are rejected with the provided reason. Only moderators can reject submissions. """ # TODO: Get current user (moderator) from request from apps.users.models import User user = User.objects.first() # TEMP try: submission = ModerationService.reject_submission(submission_id, user, data.reason) return 200, { 'success': True, 'message': 'Submission rejected', 'submission': _submission_to_dict(submission) } except ContentSubmission.DoesNotExist: return 404, {'detail': 'Submission not found'} except PermissionDenied as e: return 403, {'detail': str(e)} except ValidationError as e: return 400, {'detail': str(e)} @router.post( '/submissions/{submission_id}/reject-selective', response={200: SelectiveRejectionResponse, 400: ErrorResponse, 403: ErrorResponse, 404: ErrorResponse} ) def reject_selective(request, submission_id: UUID, data: RejectSelectiveRequest): """ Reject only specific items in a submission. Allows moderators to reject some changes while leaving others pending or approved. """ # TODO: Get current user (moderator) from request from apps.users.models import User user = User.objects.first() # TEMP try: result = ModerationService.reject_selective( submission_id, user, [str(item_id) for item_id in data.item_ids], data.reason or '' ) return 200, { 'success': True, 'message': f"Rejected {result['rejected']} of {result['total']} items", **result } except ContentSubmission.DoesNotExist: return 404, {'detail': 'Submission not found'} except PermissionDenied as e: return 403, {'detail': str(e)} except ValidationError as e: return 400, {'detail': str(e)} @router.post( '/submissions/{submission_id}/unlock', response={200: ContentSubmissionOut, 404: ErrorResponse} ) def unlock_submission(request, submission_id: UUID): """ Manually unlock a submission. Removes the review lock. Can be used by moderators or automatically by cleanup tasks. """ try: submission = ModerationService.unlock_submission(submission_id) return 200, _submission_to_dict(submission) except ContentSubmission.DoesNotExist: return 404, {'detail': 'Submission not found'} # ============================================================================ # Queue Endpoints # ============================================================================ @router.get('/queue/pending', response=SubmissionListOut) def get_pending_queue(request, page: int = 1, page_size: int = 50): """ Get pending submissions queue. Returns all submissions awaiting review. """ return list_submissions(request, status='pending', page=page, page_size=page_size) @router.get('/queue/reviewing', response=SubmissionListOut) def get_reviewing_queue(request, page: int = 1, page_size: int = 50): """ Get submissions currently under review. Returns all submissions being reviewed by moderators. """ return list_submissions(request, status='reviewing', page=page, page_size=page_size) @router.get('/queue/my-submissions', response=SubmissionListOut) def get_my_submissions(request, page: int = 1, page_size: int = 50): """ Get current user's submissions. Returns all submissions created by the authenticated user. """ # TODO: Get current user from request from apps.users.models import User user = User.objects.first() # TEMP # Validate page_size page_size = min(page_size, 100) offset = (page - 1) * page_size # Get user's submissions submissions = ModerationService.get_queue( user=user, limit=page_size, offset=offset ) # Get total count total = ContentSubmission.objects.filter(user=user).count() # Calculate total pages total_pages = (total + page_size - 1) // page_size # Convert to dicts items = [_submission_to_dict(sub) for sub in submissions] return { 'items': items, 'total': total, 'page': page, 'page_size': page_size, 'total_pages': total_pages, }