""" Moderation API Views This module contains DRF viewsets for the moderation system, including: - ModerationReport views for content reporting - ModerationQueue views for moderation workflow - ModerationAction views for tracking moderation actions - BulkOperation views for administrative bulk operations All views include comprehensive permissions, filtering, and pagination. """ from rest_framework import viewsets, status, permissions from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.filters import SearchFilter, OrderingFilter from django_filters.rest_framework import DjangoFilterBackend from django.contrib.auth import get_user_model from django.utils import timezone from django.db.models import Q, Count from datetime import timedelta from django_fsm import can_proceed, TransitionNotAllowed from apps.core.state_machine.exceptions import ( TransitionPermissionDenied, TransitionValidationError, format_transition_error, ) from .models import ( ModerationReport, ModerationQueue, ModerationAction, BulkOperation, ) from .serializers import ( ModerationReportSerializer, CreateModerationReportSerializer, UpdateModerationReportSerializer, ModerationQueueSerializer, AssignQueueItemSerializer, CompleteQueueItemSerializer, ModerationActionSerializer, CreateModerationActionSerializer, BulkOperationSerializer, CreateBulkOperationSerializer, UserModerationProfileSerializer, ) from .filters import ( ModerationReportFilter, ModerationQueueFilter, ModerationActionFilter, BulkOperationFilter, ) from .permissions import ( IsModeratorOrAdmin, IsAdminOrSuperuser, CanViewModerationData, ) User = get_user_model() # ============================================================================ # Moderation Report ViewSet # ============================================================================ class ModerationReportViewSet(viewsets.ModelViewSet): """ ViewSet for managing moderation reports. Provides CRUD operations for moderation reports with comprehensive filtering, search, and permission controls. """ queryset = ModerationReport.objects.select_related( "reported_by", "assigned_moderator", "content_type" ).all() filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] filterset_class = ModerationReportFilter search_fields = ["reason", "description", "resolution_notes"] ordering_fields = ["created_at", "updated_at", "priority", "status"] ordering = ["-created_at"] def get_serializer_class(self): """Return appropriate serializer based on action.""" if self.action == "create": return CreateModerationReportSerializer elif self.action in ["update", "partial_update"]: return UpdateModerationReportSerializer return ModerationReportSerializer def get_permissions(self): """Return appropriate permissions based on action.""" if self.action == "create": # Any authenticated user can create reports permission_classes = [permissions.IsAuthenticated] elif self.action in ["list", "retrieve"]: # Moderators and above can view reports permission_classes = [CanViewModerationData] else: # Only moderators and above can modify reports permission_classes = [IsModeratorOrAdmin] return [permission() for permission in permission_classes] def get_queryset(self): """Filter queryset based on user permissions.""" queryset = super().get_queryset() # Regular users can only see their own reports if not self.request.user.is_authenticated: return queryset.none() user_role = getattr(self.request.user, "role", "USER") if user_role == "USER": queryset = queryset.filter(reported_by=self.request.user) return queryset @action(detail=True, methods=["post"], permission_classes=[IsModeratorOrAdmin]) def assign(self, request, pk=None): """Assign a report to a moderator.""" report = self.get_object() moderator_id = request.data.get("moderator_id") try: moderator = User.objects.get(id=moderator_id) moderator_role = getattr(moderator, "role", "USER") if moderator_role not in ["MODERATOR", "ADMIN", "SUPERUSER"]: return Response( {"error": "User must be a moderator, admin, or superuser"}, status=status.HTTP_400_BAD_REQUEST, ) # Check if transition method exists transition_method = getattr(report, "transition_to_under_review", None) if transition_method is None: return Response( {"error": "Transition method not available"}, status=status.HTTP_400_BAD_REQUEST, ) # Check if transition can proceed before attempting if not can_proceed(transition_method, moderator): return Response( format_transition_error( TransitionPermissionDenied( message="Cannot transition to UNDER_REVIEW", user_message="You don't have permission to assign this report or it cannot be transitioned from the current state.", ) ), status=status.HTTP_403_FORBIDDEN, ) report.assigned_moderator = moderator try: transition_method(user=moderator) report.save() except TransitionPermissionDenied as e: return Response( format_transition_error(e), status=status.HTTP_403_FORBIDDEN, ) except TransitionValidationError as e: return Response( format_transition_error(e), status=status.HTTP_400_BAD_REQUEST, ) except TransitionNotAllowed as e: return Response( format_transition_error(e), status=status.HTTP_400_BAD_REQUEST, ) serializer = self.get_serializer(report) return Response(serializer.data) except User.DoesNotExist: return Response( {"error": "Moderator not found"}, status=status.HTTP_404_NOT_FOUND ) @action(detail=True, methods=["post"], permission_classes=[IsModeratorOrAdmin]) def resolve(self, request, pk=None): """Resolve a moderation report.""" report = self.get_object() resolution_action = request.data.get("resolution_action") resolution_notes = request.data.get("resolution_notes", "") if not resolution_action: return Response( {"error": "resolution_action is required"}, status=status.HTTP_400_BAD_REQUEST, ) # Check if transition method exists transition_method = getattr(report, "transition_to_resolved", None) if transition_method is None: return Response( {"error": "Transition method not available"}, status=status.HTTP_400_BAD_REQUEST, ) # Check if transition can proceed before attempting if not can_proceed(transition_method, request.user): return Response( format_transition_error( TransitionPermissionDenied( message="Cannot transition to RESOLVED", user_message="You don't have permission to resolve this report or it cannot be resolved from the current state.", ) ), status=status.HTTP_403_FORBIDDEN, ) try: transition_method(user=request.user) except TransitionPermissionDenied as e: return Response( format_transition_error(e), status=status.HTTP_403_FORBIDDEN, ) except TransitionValidationError as e: return Response( format_transition_error(e), status=status.HTTP_400_BAD_REQUEST, ) except TransitionNotAllowed as e: return Response( format_transition_error(e), status=status.HTTP_400_BAD_REQUEST, ) report.resolution_action = resolution_action report.resolution_notes = resolution_notes report.resolved_at = timezone.now() report.save() serializer = self.get_serializer(report) return Response(serializer.data) @action(detail=False, methods=["get"], permission_classes=[CanViewModerationData]) def stats(self, request): """Get moderation report statistics.""" queryset = self.get_queryset() # Basic counts total_reports = queryset.count() pending_reports = queryset.filter(status="PENDING").count() resolved_reports = queryset.filter(status="RESOLVED").count() # Overdue reports (based on priority SLA) now = timezone.now() overdue_reports = 0 for report in queryset.filter(status__in=["PENDING", "UNDER_REVIEW"]): sla_hours = {"URGENT": 2, "HIGH": 8, "MEDIUM": 24, "LOW": 72} hours_since_created = (now - report.created_at).total_seconds() / 3600 if report.priority in sla_hours: threshold = sla_hours[report.priority] else: raise ValueError(f"Unknown priority level: {report.priority}") if hours_since_created > threshold: overdue_reports += 1 # Reports by priority and type reports_by_priority = dict( queryset.values_list("priority").annotate(count=Count("id")) ) reports_by_type = dict( queryset.values_list("report_type").annotate(count=Count("id")) ) # Average resolution time resolved_queryset = queryset.filter( status="RESOLVED", resolved_at__isnull=False ) avg_resolution_time = 0 if resolved_queryset.exists(): total_time = sum( [ (report.resolved_at - report.created_at).total_seconds() / 3600 for report in resolved_queryset if report.resolved_at ] ) avg_resolution_time = total_time / resolved_queryset.count() stats_data = { "total_reports": total_reports, "pending_reports": pending_reports, "resolved_reports": resolved_reports, "overdue_reports": overdue_reports, "reports_by_priority": reports_by_priority, "reports_by_type": reports_by_type, "average_resolution_time_hours": round(avg_resolution_time, 2), } return Response(stats_data) @action(detail=True, methods=['get'], permission_classes=[CanViewModerationData]) def history(self, request, pk=None): """Get transition history for this report.""" from django_fsm_log.models import StateLog from django.contrib.contenttypes.models import ContentType report = self.get_object() content_type = ContentType.objects.get_for_model(report) logs = StateLog.objects.filter( content_type=content_type, object_id=report.id ).select_related('by').order_by('-timestamp') history_data = [{ 'id': log.id, 'timestamp': log.timestamp, 'state': log.state, 'from_state': log.source_state, 'to_state': log.state, 'transition': log.transition, 'user': log.by.username if log.by else None, 'description': log.description, 'reason': log.description, } for log in logs] return Response(history_data) @action(detail=False, methods=['get'], permission_classes=[CanViewModerationData]) def all_history(self, request): """Get all transition history with filtering.""" from django_fsm_log.models import StateLog from django.contrib.contenttypes.models import ContentType queryset = StateLog.objects.select_related('by', 'content_type').all() # Filter by id (for detail view) log_id = request.query_params.get('id') if log_id: queryset = queryset.filter(id=log_id) # Filter by model type model_type = request.query_params.get('model_type') if model_type: try: content_type = ContentType.objects.get(model=model_type) queryset = queryset.filter(content_type=content_type) except ContentType.DoesNotExist: pass # Filter by user user_id = request.query_params.get('user_id') if user_id: queryset = queryset.filter(by_id=user_id) # Filter by date range start_date = request.query_params.get('start_date') end_date = request.query_params.get('end_date') if start_date: queryset = queryset.filter(timestamp__gte=start_date) if end_date: queryset = queryset.filter(timestamp__lte=end_date) # Filter by state state = request.query_params.get('state') if state: queryset = queryset.filter(state=state) # Order queryset queryset = queryset.order_by('-timestamp') # Paginate page = self.paginate_queryset(queryset) if page is not None: history_data = [{ 'id': log.id, 'timestamp': log.timestamp, 'model': log.content_type.model, 'object_id': log.object_id, 'state': log.state, 'from_state': log.source_state, 'to_state': log.state, 'transition': log.transition, 'user': log.by.username if log.by else None, 'description': log.description, 'reason': log.description, } for log in page] return self.get_paginated_response(history_data) # Return all history data when pagination is not triggered history_data = [{ 'id': log.id, 'timestamp': log.timestamp, 'model': log.content_type.model, 'object_id': log.object_id, 'state': log.state, 'from_state': log.source_state, 'to_state': log.state, 'transition': log.transition, 'user': log.by.username if log.by else None, 'description': log.description, 'reason': log.description, } for log in queryset] return Response(history_data) # ============================================================================ # Moderation Queue ViewSet # ============================================================================ class ModerationQueueViewSet(viewsets.ModelViewSet): """ ViewSet for managing moderation queue items. Provides workflow management for moderation tasks with assignment, completion, and progress tracking. """ queryset = ModerationQueue.objects.select_related( "assigned_to", "related_report", "content_type" ).all() serializer_class = ModerationQueueSerializer permission_classes = [CanViewModerationData] filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] filterset_class = ModerationQueueFilter search_fields = ["title", "description"] ordering_fields = ["created_at", "updated_at", "priority", "status"] ordering = ["-created_at"] @action(detail=True, methods=["post"], permission_classes=[IsModeratorOrAdmin]) def assign(self, request, pk=None): """Assign a queue item to a moderator.""" queue_item = self.get_object() serializer = AssignQueueItemSerializer(data=request.data) if serializer.is_valid(): moderator_id = serializer.validated_data["moderator_id"] moderator = User.objects.get(id=moderator_id) # Check if transition method exists transition_method = getattr(queue_item, "transition_to_in_progress", None) if transition_method is None: return Response( {"error": "Transition method not available"}, status=status.HTTP_400_BAD_REQUEST, ) # Check if transition can proceed before attempting if not can_proceed(transition_method, moderator): return Response( format_transition_error( TransitionPermissionDenied( message="Cannot transition to IN_PROGRESS", user_message="You don't have permission to assign this queue item or it cannot be transitioned from the current state.", ) ), status=status.HTTP_403_FORBIDDEN, ) queue_item.assigned_to = moderator queue_item.assigned_at = timezone.now() try: transition_method(user=moderator) except TransitionPermissionDenied as e: return Response( format_transition_error(e), status=status.HTTP_403_FORBIDDEN, ) except TransitionValidationError as e: return Response( format_transition_error(e), status=status.HTTP_400_BAD_REQUEST, ) except TransitionNotAllowed as e: return Response( format_transition_error(e), status=status.HTTP_400_BAD_REQUEST, ) queue_item.save() response_serializer = self.get_serializer(queue_item) return Response(response_serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @action(detail=True, methods=["post"], permission_classes=[IsModeratorOrAdmin]) def unassign(self, request, pk=None): """Unassign a queue item.""" queue_item = self.get_object() # Check if transition method exists transition_method = getattr(queue_item, "transition_to_pending", None) if transition_method is None: return Response( {"error": "Transition method not available"}, status=status.HTTP_400_BAD_REQUEST, ) # Check if transition can proceed before attempting if not can_proceed(transition_method, request.user): return Response( format_transition_error( TransitionPermissionDenied( message="Cannot transition to PENDING", user_message="You don't have permission to unassign this queue item or it cannot be transitioned from the current state.", ) ), status=status.HTTP_403_FORBIDDEN, ) queue_item.assigned_to = None queue_item.assigned_at = None try: transition_method(user=request.user) except TransitionPermissionDenied as e: return Response( format_transition_error(e), status=status.HTTP_403_FORBIDDEN, ) except TransitionValidationError as e: return Response( format_transition_error(e), status=status.HTTP_400_BAD_REQUEST, ) except TransitionNotAllowed as e: return Response( format_transition_error(e), status=status.HTTP_400_BAD_REQUEST, ) queue_item.save() serializer = self.get_serializer(queue_item) return Response(serializer.data) @action(detail=True, methods=["post"], permission_classes=[IsModeratorOrAdmin]) def complete(self, request, pk=None): """Complete a queue item.""" queue_item = self.get_object() serializer = CompleteQueueItemSerializer(data=request.data) if serializer.is_valid(): action_taken = serializer.validated_data["action"] notes = serializer.validated_data.get("notes", "") # Check if transition method exists transition_method = getattr(queue_item, "transition_to_completed", None) if transition_method is None: return Response( {"error": "Transition method not available"}, status=status.HTTP_400_BAD_REQUEST, ) # Check if transition can proceed before attempting if not can_proceed(transition_method, request.user): return Response( format_transition_error( TransitionPermissionDenied( message="Cannot transition to COMPLETED", user_message="You don't have permission to complete this queue item or it cannot be transitioned from the current state.", ) ), status=status.HTTP_403_FORBIDDEN, ) try: transition_method(user=request.user) except TransitionPermissionDenied as e: return Response( format_transition_error(e), status=status.HTTP_403_FORBIDDEN, ) except TransitionValidationError as e: return Response( format_transition_error(e), status=status.HTTP_400_BAD_REQUEST, ) except TransitionNotAllowed as e: return Response( format_transition_error(e), status=status.HTTP_400_BAD_REQUEST, ) queue_item.save() # Create moderation action if needed if action_taken != "NO_ACTION" and queue_item.related_report: ModerationAction.objects.create( action_type=action_taken, reason=f"Queue item completion: {action_taken}", details=notes, moderator=request.user, target_user=queue_item.related_report.reported_by, related_report=queue_item.related_report, is_active=True, ) response_serializer = self.get_serializer(queue_item) return Response(response_serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @action(detail=False, methods=["get"], permission_classes=[CanViewModerationData]) def my_queue(self, request): """Get queue items assigned to the current user.""" queryset = self.get_queryset().filter(assigned_to=request.user) page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) @action(detail=True, methods=['get'], permission_classes=[CanViewModerationData]) def history(self, request, pk=None): """Get transition history for this queue item.""" from django_fsm_log.models import StateLog from django.contrib.contenttypes.models import ContentType queue_item = self.get_object() content_type = ContentType.objects.get_for_model(queue_item) logs = StateLog.objects.filter( content_type=content_type, object_id=queue_item.id ).select_related('by').order_by('-timestamp') history_data = [{ 'id': log.id, 'timestamp': log.timestamp, 'state': log.state, 'from_state': log.source_state, 'to_state': log.state, 'transition': log.transition, 'user': log.by.username if log.by else None, 'description': log.description, 'reason': log.description, } for log in logs] return Response(history_data) # ============================================================================ # Moderation Action ViewSet # ============================================================================ class ModerationActionViewSet(viewsets.ModelViewSet): """ ViewSet for managing moderation actions. Tracks actions taken against users and content with expiration and status management. """ queryset = ModerationAction.objects.select_related( "moderator", "target_user", "related_report" ).all() filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] filterset_class = ModerationActionFilter search_fields = ["reason", "details"] ordering_fields = ["created_at", "expires_at", "action_type"] ordering = ["-created_at"] def get_serializer_class(self): """Return appropriate serializer based on action.""" if self.action == "create": return CreateModerationActionSerializer return ModerationActionSerializer def get_permissions(self): """Return appropriate permissions based on action.""" if self.action == "create": permission_classes = [IsModeratorOrAdmin] else: permission_classes = [CanViewModerationData] return [permission() for permission in permission_classes] @action(detail=True, methods=["post"], permission_classes=[IsModeratorOrAdmin]) def deactivate(self, request, pk=None): """Deactivate a moderation action.""" action_obj = self.get_object() action_obj.is_active = False action_obj.save() serializer = self.get_serializer(action_obj) return Response(serializer.data) @action(detail=False, methods=["get"], permission_classes=[CanViewModerationData]) def active(self, request): """Get all active moderation actions.""" queryset = self.get_queryset().filter( is_active=True, expires_at__gt=timezone.now() ) page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) @action(detail=False, methods=["get"], permission_classes=[CanViewModerationData]) def expired(self, request): """Get all expired moderation actions.""" queryset = self.get_queryset().filter( expires_at__lte=timezone.now(), is_active=True ) page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) # ============================================================================ # Bulk Operation ViewSet # ============================================================================ class BulkOperationViewSet(viewsets.ModelViewSet): """ ViewSet for managing bulk operations. Provides administrative bulk operations with progress tracking and cancellation support. """ queryset = BulkOperation.objects.select_related("created_by").all() permission_classes = [IsAdminOrSuperuser] filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] filterset_class = BulkOperationFilter search_fields = ["description"] ordering_fields = ["created_at", "started_at", "completed_at", "priority"] ordering = ["-created_at"] def get_serializer_class(self): """Return appropriate serializer based on action.""" if self.action == "create": return CreateBulkOperationSerializer return BulkOperationSerializer @action(detail=True, methods=["post"]) def cancel(self, request, pk=None): """Cancel a bulk operation.""" operation = self.get_object() if operation.status not in ["PENDING", "RUNNING"]: return Response( {"error": "Operation cannot be cancelled"}, status=status.HTTP_400_BAD_REQUEST, ) if not operation.can_cancel: return Response( {"error": "Operation is not cancellable"}, status=status.HTTP_400_BAD_REQUEST, ) # Check if transition method exists transition_method = getattr(operation, "transition_to_cancelled", None) if transition_method is None: return Response( {"error": "Transition method not available"}, status=status.HTTP_400_BAD_REQUEST, ) # Check if transition can proceed before attempting if not can_proceed(transition_method, request.user): return Response( format_transition_error( TransitionPermissionDenied( message="Cannot transition to CANCELLED", user_message="You don't have permission to cancel this operation or it cannot be cancelled from the current state.", ) ), status=status.HTTP_403_FORBIDDEN, ) try: transition_method(user=request.user) except TransitionPermissionDenied as e: return Response( format_transition_error(e), status=status.HTTP_403_FORBIDDEN, ) except TransitionValidationError as e: return Response( format_transition_error(e), status=status.HTTP_400_BAD_REQUEST, ) except TransitionNotAllowed as e: return Response( format_transition_error(e), status=status.HTTP_400_BAD_REQUEST, ) operation.completed_at = timezone.now() operation.save() serializer = self.get_serializer(operation) return Response(serializer.data) @action(detail=True, methods=["post"]) def retry(self, request, pk=None): """Retry a failed bulk operation.""" operation = self.get_object() if operation.status != "FAILED": return Response( {"error": "Only failed operations can be retried"}, status=status.HTTP_400_BAD_REQUEST, ) # Check if transition method exists transition_method = getattr(operation, "transition_to_pending", None) if transition_method is None: return Response( {"error": "Transition method not available"}, status=status.HTTP_400_BAD_REQUEST, ) # Check if transition can proceed before attempting if not can_proceed(transition_method, request.user): return Response( format_transition_error( TransitionPermissionDenied( message="Cannot transition to PENDING", user_message="You don't have permission to retry this operation or it cannot be retried from the current state.", ) ), status=status.HTTP_403_FORBIDDEN, ) # Reset operation status try: transition_method(user=request.user) except TransitionPermissionDenied as e: return Response( format_transition_error(e), status=status.HTTP_403_FORBIDDEN, ) except TransitionValidationError as e: return Response( format_transition_error(e), status=status.HTTP_400_BAD_REQUEST, ) except TransitionNotAllowed as e: return Response( format_transition_error(e), status=status.HTTP_400_BAD_REQUEST, ) operation.started_at = None operation.completed_at = None operation.processed_items = 0 operation.failed_items = 0 operation.results = {} operation.save() serializer = self.get_serializer(operation) return Response(serializer.data) @action(detail=True, methods=["get"]) def logs(self, request, pk=None): """Get logs for a bulk operation.""" operation = self.get_object() # This would typically fetch logs from a logging system # For now, return a placeholder response logs = { "logs": [ { "timestamp": operation.created_at.isoformat(), "level": "INFO", "message": f"Operation {operation.id} created", "details": operation.parameters, } ], "count": 1, } return Response(logs) @action(detail=False, methods=["get"]) def running(self, request): """Get all running bulk operations.""" queryset = self.get_queryset().filter(status="RUNNING") page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) @action(detail=True, methods=['get']) def history(self, request, pk=None): """Get transition history for this bulk operation.""" from django_fsm_log.models import StateLog from django.contrib.contenttypes.models import ContentType operation = self.get_object() content_type = ContentType.objects.get_for_model(operation) logs = StateLog.objects.filter( content_type=content_type, object_id=operation.id ).select_related('by').order_by('-timestamp') history_data = [{ 'id': log.id, 'timestamp': log.timestamp, 'state': log.state, 'from_state': log.source_state, 'to_state': log.state, 'transition': log.transition, 'user': log.by.username if log.by else None, 'description': log.description, 'reason': log.description, } for log in logs] return Response(history_data) # ============================================================================ # User Moderation ViewSet # ============================================================================ class UserModerationViewSet(viewsets.ViewSet): """ ViewSet for user moderation operations. Provides user-specific moderation data, statistics, and actions. """ permission_classes = [IsModeratorOrAdmin] # Default serializer for schema generation serializer_class = UserModerationProfileSerializer def retrieve(self, request, pk=None): """Get moderation profile for a specific user.""" try: user = User.objects.get(pk=pk) except User.DoesNotExist: return Response( {"error": "User not found"}, status=status.HTTP_404_NOT_FOUND ) # Gather user moderation data reports_made = ModerationReport.objects.filter(reported_by=user).count() reports_against = ModerationReport.objects.filter( reported_entity_type="user", reported_entity_id=user.id ).count() actions_against = ModerationAction.objects.filter(target_user=user) warnings_received = actions_against.filter(action_type="WARNING").count() suspensions_received = actions_against.filter( action_type="USER_SUSPENSION" ).count() active_restrictions = actions_against.filter( is_active=True, expires_at__gt=timezone.now() ).count() # Risk assessment (simplified) risk_factors = [] risk_level = "LOW" if reports_against > 5: risk_factors.append("Multiple reports against user") risk_level = "MEDIUM" if suspensions_received > 0: risk_factors.append("Previous suspensions") risk_level = "HIGH" if active_restrictions > 0: risk_factors.append("Active restrictions") risk_level = "HIGH" # Recent activity recent_reports = ModerationReport.objects.filter(reported_by=user).order_by( "-created_at" )[:5] recent_actions = actions_against.order_by("-created_at")[:5] # Account status account_status = "ACTIVE" if getattr(user, "is_banned", False): account_status = "BANNED" elif active_restrictions > 0: account_status = "RESTRICTED" last_violation = ( actions_against.filter( action_type__in=["WARNING", "USER_SUSPENSION", "USER_BAN"] ) .order_by("-created_at") .first() ) profile_data = { "user": { "id": user.id, "username": user.username, "display_name": user.get_display_name(), "email": user.email, "role": getattr(user, "role", "USER"), }, "reports_made": reports_made, "reports_against": reports_against, "warnings_received": warnings_received, "suspensions_received": suspensions_received, "active_restrictions": active_restrictions, "risk_level": risk_level, "risk_factors": risk_factors, "recent_reports": ModerationReportSerializer( recent_reports, many=True ).data, "recent_actions": ModerationActionSerializer( recent_actions, many=True ).data, "account_status": account_status, "last_violation_date": ( last_violation.created_at if last_violation else None ), "next_review_date": None, # Would be calculated based on business rules } return Response(profile_data) @action(detail=True, methods=["post"]) def moderate(self, request, pk=None): """Take moderation action against a user.""" try: user = User.objects.get(pk=pk) except User.DoesNotExist: return Response( {"error": "User not found"}, status=status.HTTP_404_NOT_FOUND ) serializer = CreateModerationActionSerializer( data=request.data, context={"request": request} ) if serializer.is_valid(): # Override target_user_id with the user from URL validated_data = serializer.validated_data.copy() validated_data["target_user_id"] = user.id action = ModerationAction.objects.create( action_type=validated_data["action_type"], reason=validated_data["reason"], details=validated_data["details"], duration_hours=validated_data.get("duration_hours"), moderator=request.user, target_user=user, related_report_id=validated_data.get("related_report_id"), is_active=True, expires_at=( timezone.now() + timedelta(hours=validated_data["duration_hours"]) if validated_data.get("duration_hours") else None ), ) response_serializer = ModerationActionSerializer(action) return Response(response_serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @action(detail=False, methods=["get"]) def search(self, request): """Search users for moderation purposes.""" query = request.query_params.get("query", "") role = request.query_params.get("role") has_restrictions = request.query_params.get("has_restrictions") queryset = User.objects.all() if query: queryset = queryset.filter( Q(username__icontains=query) | Q(email__icontains=query) ) if role: queryset = queryset.filter(role=role) if has_restrictions == "true": active_action_users = ModerationAction.objects.filter( is_active=True, expires_at__gt=timezone.now() ).values_list("target_user_id", flat=True) queryset = queryset.filter(id__in=active_action_users) # Paginate results page = self.paginate_queryset(queryset) if page is not None: users_data = [] for user in page: restriction_count = ModerationAction.objects.filter( target_user=user, is_active=True, expires_at__gt=timezone.now() ).count() users_data.append( { "id": user.id, "username": user.username, "display_name": user.get_display_name(), "email": user.email, "role": getattr(user, "role", "USER"), "date_joined": user.date_joined, "last_login": user.last_login, "is_active": user.is_active, "restriction_count": restriction_count, "risk_level": "HIGH" if restriction_count > 0 else "LOW", } ) return self.get_paginated_response(users_data) return Response([]) @action(detail=False, methods=["get"]) def stats(self, request): """Get overall user moderation statistics.""" total_actions = ModerationAction.objects.count() active_actions = ModerationAction.objects.filter( is_active=True, expires_at__gt=timezone.now() ).count() expired_actions = ModerationAction.objects.filter( expires_at__lte=timezone.now() ).count() stats_data = { "total_actions": total_actions, "active_actions": active_actions, "expired_actions": expired_actions, } return Response(stats_data)