--- description: Add moderation support to a content type in ThrillWiki --- # Moderation Workflow Add moderation (submission queue, version history, approval flow) to a content type. ## Overview ThrillWiki's moderation system ensures quality by: 1. User submits new/edited content → Creates `Submission` record 2. Content enters moderation queue with `pending` status 3. Moderator reviews and approves/rejects 4. On approval → Content is published, version record created ## Implementation Steps ### Step 1: Ensure Model Supports Versioning The content model needs to track its current state and history: ```python # backend/apps/[app]/models.py class Park(BaseModel): """Main park model - always shows current approved data""" name = models.CharField(max_length=255) slug = models.SlugField(unique=True) description = models.TextField(blank=True) # ... other fields # Track the current approved version current_version = models.ForeignKey( 'ParkVersion', null=True, blank=True, on_delete=models.SET_NULL, related_name='current_for' ) class ParkVersion(BaseModel): """Immutable snapshot of park data at a point in time""" park = models.ForeignKey( Park, on_delete=models.CASCADE, related_name='versions' ) # Store complete snapshot of editable fields data = models.JSONField() # Metadata changed_by = models.ForeignKey( 'users.User', on_delete=models.SET_NULL, null=True ) change_summary = models.CharField(max_length=255, blank=True) submission = models.ForeignKey( 'submissions.Submission', on_delete=models.SET_NULL, null=True, related_name='versions' ) class Meta: ordering = ['-created_at'] def apply_to_park(self): """Apply this version's data to the parent park""" for field, value in self.data.items(): if hasattr(self.park, field): setattr(self.park, field, value) self.park.current_version = self self.park.save() ``` ### Step 2: Create Submission Serializers ```python # backend/apps/submissions/serializers.py class ParkSubmissionSerializer(serializers.Serializer): """Serializer for park submission data""" name = serializers.CharField(max_length=255) description = serializers.CharField(required=False, allow_blank=True) city = serializers.CharField(max_length=100) country = serializers.CharField(max_length=100) status = serializers.ChoiceField(choices=Park.Status.choices) # ... other editable fields def validate_name(self, value): # Custom validation if needed return value class SubmissionCreateSerializer(serializers.ModelSerializer): """Create a new submission""" data = serializers.JSONField() class Meta: model = Submission fields = ['content_type', 'object_id', 'data', 'change_summary'] def validate(self, attrs): content_type = attrs['content_type'] # Get the appropriate serializer for this content type serializer_map = { 'park': ParkSubmissionSerializer, 'ride': RideSubmissionSerializer, # ... other content types } serializer_class = serializer_map.get(content_type) if not serializer_class: raise serializers.ValidationError( {'content_type': 'Unsupported content type'} ) # Validate the data field data_serializer = serializer_class(data=attrs['data']) data_serializer.is_valid(raise_exception=True) attrs['data'] = data_serializer.validated_data return attrs def create(self, validated_data): validated_data['submitted_by'] = self.context['request'].user validated_data['status'] = Submission.Status.PENDING return super().create(validated_data) ``` ### Step 3: Create Submission ViewSet ```python # backend/apps/submissions/views.py class SubmissionViewSet(viewsets.ModelViewSet): """API for content submissions""" serializer_class = SubmissionSerializer permission_classes = [IsAuthenticated] def get_queryset(self): user = self.request.user # Users see their own submissions # Moderators see all pending submissions if user.is_moderator: return Submission.objects.all() return Submission.objects.filter(submitted_by=user) def get_serializer_class(self): if self.action == 'create': return SubmissionCreateSerializer return SubmissionSerializer @action(detail=True, methods=['get']) def diff(self, request, pk=None): """Get diff between submission and current version""" submission = self.get_object() if submission.object_id: # Edit submission - compare to current current = self.get_current_data(submission) return Response({ 'before': current, 'after': submission.data, 'changes': self.compute_diff(current, submission.data) }) else: # New submission - no comparison return Response({ 'before': None, 'after': submission.data, 'changes': None }) ``` ### Step 4: Create Moderation ViewSet ```python # backend/apps/moderation/views.py class ModerationViewSet(viewsets.ViewSet): """Moderation queue and actions""" permission_classes = [IsModerator] def list(self, request): """Get moderation queue""" queryset = Submission.objects.filter( status=Submission.Status.PENDING ).select_related( 'submitted_by' ).order_by('created_at') # Filter by content type content_type = request.query_params.get('type') if content_type: queryset = queryset.filter(content_type=content_type) serializer = SubmissionSerializer(queryset, many=True) return Response(serializer.data) @action(detail=True, methods=['post']) def approve(self, request, pk=None): """Approve a submission""" submission = get_object_or_404(Submission, pk=pk) if submission.status != Submission.Status.PENDING: return Response( {'error': 'Submission is not pending'}, status=status.HTTP_400_BAD_REQUEST ) # Apply the submission with transaction.atomic(): if submission.object_id: # Edit existing content content = self.get_content_object(submission) version = self.create_version(content, submission) version.apply_to_park() else: # Create new content content = self.create_content(submission) version = self.create_version(content, submission) content.current_version = version content.save() submission.status = Submission.Status.APPROVED submission.reviewed_by = request.user submission.reviewed_at = timezone.now() submission.save() # Notify user notify_user( submission.submitted_by, 'submission_approved', {'submission': submission} ) return Response({'status': 'approved'}) @action(detail=True, methods=['post']) def reject(self, request, pk=None): """Reject a submission""" submission = get_object_or_404(Submission, pk=pk) submission.status = Submission.Status.REJECTED submission.reviewed_by = request.user submission.reviewed_at = timezone.now() submission.review_notes = request.data.get('notes', '') submission.save() # Notify user notify_user( submission.submitted_by, 'submission_rejected', {'submission': submission, 'reason': submission.review_notes} ) return Response({'status': 'rejected'}) @action(detail=True, methods=['post']) def request_changes(self, request, pk=None): """Request changes to a submission""" submission = get_object_or_404(Submission, pk=pk) submission.status = Submission.Status.CHANGES_REQUESTED submission.reviewed_by = request.user submission.review_notes = request.data.get('notes', '') submission.save() # Notify user notify_user( submission.submitted_by, 'submission_changes_requested', {'submission': submission, 'notes': submission.review_notes} ) return Response({'status': 'changes_requested'}) ``` ### Step 5: Frontend - Submission Form ```vue