Add state machine diagrams and code examples for ThrillWiki

- Created a comprehensive documentation file for state machine diagrams, detailing various states and transitions for models such as EditSubmission, ModerationReport, and Park Status.
- Included transition matrices for each state machine to clarify role requirements and guards.
- Developed a new document providing code examples for implementing state machines, including adding new state machines to models, defining custom guards, implementing callbacks, and testing state machines.
- Added examples for document approval workflows, custom guards, email notifications, and cache invalidation callbacks.
- Implemented a test suite for document workflows, covering various scenarios including approval, rejection, and transition logging.
This commit is contained in:
pacnpal
2025-12-21 20:21:54 -05:00
parent 8f6acbdc23
commit b508434574
24 changed files with 9979 additions and 360 deletions

View File

@@ -0,0 +1,7 @@
"""
Moderation test package.
This package contains tests for the moderation app including:
- Workflow tests (test_workflows.py)
- Permission tests (test_permissions.py - planned)
"""

View File

@@ -0,0 +1,532 @@
"""
Integration tests for complete moderation workflows.
This module tests end-to-end moderation workflows including:
- Submission approval workflow
- Submission rejection workflow
- Submission escalation workflow
- Report handling workflow
- Bulk operation workflow
"""
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from unittest.mock import patch, Mock
User = get_user_model()
class SubmissionApprovalWorkflowTests(TestCase):
"""Tests for the complete submission approval workflow."""
@classmethod
def setUpTestData(cls):
"""Set up test data for all tests."""
cls.regular_user = User.objects.create_user(
username='regular_user',
email='user@example.com',
password='testpass123',
role='USER'
)
cls.moderator = User.objects.create_user(
username='moderator',
email='mod@example.com',
password='testpass123',
role='MODERATOR'
)
cls.admin = User.objects.create_user(
username='admin',
email='admin@example.com',
password='testpass123',
role='ADMIN'
)
def test_edit_submission_approval_workflow(self):
"""
Test complete edit submission approval workflow.
Flow: User submits → Moderator reviews → Moderator approves → Changes applied
"""
from apps.moderation.models import EditSubmission
from apps.parks.models import Company
# Create target object
company = Company.objects.create(
name='Test Company',
description='Original description'
)
# User submits an edit
content_type = ContentType.objects.get_for_model(company)
submission = EditSubmission.objects.create(
user=self.regular_user,
content_type=content_type,
object_id=company.id,
submission_type='EDIT',
changes={'description': 'Updated description'},
status='PENDING',
reason='Fixing typo'
)
self.assertEqual(submission.status, 'PENDING')
self.assertIsNone(submission.handled_by)
self.assertIsNone(submission.handled_at)
# Moderator approves
submission.transition_to_approved(user=self.moderator)
submission.handled_by = self.moderator
submission.handled_at = timezone.now()
submission.save()
submission.refresh_from_db()
self.assertEqual(submission.status, 'APPROVED')
self.assertEqual(submission.handled_by, self.moderator)
self.assertIsNotNone(submission.handled_at)
def test_photo_submission_approval_workflow(self):
"""
Test complete photo submission approval workflow.
Flow: User submits photo → Moderator reviews → Moderator approves → Photo created
"""
from apps.moderation.models import PhotoSubmission
from apps.parks.models import Park, Company
# Create target park
operator = Company.objects.create(
name='Test Operator',
roles=['OPERATOR']
)
park = Park.objects.create(
name='Test Park',
slug='test-park',
operator=operator,
status='OPERATING',
timezone='America/New_York'
)
# User submits a photo
content_type = ContentType.objects.get_for_model(park)
submission = PhotoSubmission.objects.create(
user=self.regular_user,
content_type=content_type,
object_id=park.id,
status='PENDING',
photo_type='GENERAL',
description='Beautiful park entrance'
)
self.assertEqual(submission.status, 'PENDING')
# Moderator approves
submission.transition_to_approved(user=self.moderator)
submission.handled_by = self.moderator
submission.handled_at = timezone.now()
submission.save()
submission.refresh_from_db()
self.assertEqual(submission.status, 'APPROVED')
class SubmissionRejectionWorkflowTests(TestCase):
"""Tests for the submission rejection workflow."""
@classmethod
def setUpTestData(cls):
cls.regular_user = User.objects.create_user(
username='user_rej',
email='user_rej@example.com',
password='testpass123',
role='USER'
)
cls.moderator = User.objects.create_user(
username='mod_rej',
email='mod_rej@example.com',
password='testpass123',
role='MODERATOR'
)
def test_edit_submission_rejection_with_reason(self):
"""
Test rejection workflow with reason.
Flow: User submits → Moderator rejects with reason → User notified
"""
from apps.moderation.models import EditSubmission
from apps.parks.models import Company
company = Company.objects.create(
name='Test Company',
description='Original'
)
content_type = ContentType.objects.get_for_model(company)
submission = EditSubmission.objects.create(
user=self.regular_user,
content_type=content_type,
object_id=company.id,
submission_type='EDIT',
changes={'name': 'Spam Content'},
status='PENDING',
reason='Name change request'
)
# Moderator rejects
submission.transition_to_rejected(user=self.moderator)
submission.handled_by = self.moderator
submission.handled_at = timezone.now()
submission.notes = 'Rejected: Content appears to be spam'
submission.save()
submission.refresh_from_db()
self.assertEqual(submission.status, 'REJECTED')
self.assertIn('spam', submission.notes.lower())
class SubmissionEscalationWorkflowTests(TestCase):
"""Tests for the submission escalation workflow."""
@classmethod
def setUpTestData(cls):
cls.regular_user = User.objects.create_user(
username='user_esc',
email='user_esc@example.com',
password='testpass123',
role='USER'
)
cls.moderator = User.objects.create_user(
username='mod_esc',
email='mod_esc@example.com',
password='testpass123',
role='MODERATOR'
)
cls.admin = User.objects.create_user(
username='admin_esc',
email='admin_esc@example.com',
password='testpass123',
role='ADMIN'
)
def test_escalation_workflow(self):
"""
Test complete escalation workflow.
Flow: User submits → Moderator escalates → Admin reviews → Admin approves
"""
from apps.moderation.models import EditSubmission
from apps.parks.models import Company
company = Company.objects.create(
name='Sensitive Company',
description='Original'
)
content_type = ContentType.objects.get_for_model(company)
submission = EditSubmission.objects.create(
user=self.regular_user,
content_type=content_type,
object_id=company.id,
submission_type='EDIT',
changes={'name': 'New Sensitive Name'},
status='PENDING',
reason='Major name change'
)
# Moderator escalates
submission.transition_to_escalated(user=self.moderator)
submission.notes = 'Escalated: Major change needs admin review'
submission.save()
self.assertEqual(submission.status, 'ESCALATED')
# Admin approves
submission.transition_to_approved(user=self.admin)
submission.handled_by = self.admin
submission.handled_at = timezone.now()
submission.save()
submission.refresh_from_db()
self.assertEqual(submission.status, 'APPROVED')
self.assertEqual(submission.handled_by, self.admin)
class ReportHandlingWorkflowTests(TestCase):
"""Tests for the moderation report handling workflow."""
@classmethod
def setUpTestData(cls):
cls.reporter = User.objects.create_user(
username='reporter',
email='reporter@example.com',
password='testpass123',
role='USER'
)
cls.moderator = User.objects.create_user(
username='mod_report',
email='mod_report@example.com',
password='testpass123',
role='MODERATOR'
)
def test_report_resolution_workflow(self):
"""
Test complete report resolution workflow.
Flow: User reports → Moderator assigned → Moderator investigates → Resolved
"""
from apps.moderation.models import ModerationReport
from apps.parks.models import Company
reported_company = Company.objects.create(
name='Problematic Company',
description='Some inappropriate content'
)
content_type = ContentType.objects.get_for_model(reported_company)
# User reports content
report = ModerationReport.objects.create(
report_type='CONTENT',
status='PENDING',
priority='HIGH',
reported_entity_type='company',
reported_entity_id=reported_company.id,
content_type=content_type,
reason='INAPPROPRIATE',
description='This content is inappropriate',
reported_by=self.reporter
)
self.assertEqual(report.status, 'PENDING')
# Moderator claims and starts review
report.transition_to_under_review(user=self.moderator)
report.assigned_moderator = self.moderator
report.save()
self.assertEqual(report.status, 'UNDER_REVIEW')
self.assertEqual(report.assigned_moderator, self.moderator)
# Moderator resolves
report.transition_to_resolved(user=self.moderator)
report.resolution_action = 'CONTENT_REMOVED'
report.resolution_notes = 'Content was removed'
report.resolved_at = timezone.now()
report.save()
report.refresh_from_db()
self.assertEqual(report.status, 'RESOLVED')
self.assertIsNotNone(report.resolved_at)
def test_report_dismissal_workflow(self):
"""
Test report dismissal workflow for invalid reports.
Flow: User reports → Moderator reviews → Moderator dismisses
"""
from apps.moderation.models import ModerationReport
from apps.parks.models import Company
company = Company.objects.create(
name='Valid Company',
description='Normal content'
)
content_type = ContentType.objects.get_for_model(company)
report = ModerationReport.objects.create(
report_type='CONTENT',
status='PENDING',
priority='LOW',
reported_entity_type='company',
reported_entity_id=company.id,
content_type=content_type,
reason='OTHER',
description='I just do not like this',
reported_by=self.reporter
)
# Moderator claims
report.transition_to_under_review(user=self.moderator)
report.assigned_moderator = self.moderator
report.save()
# Moderator dismisses as invalid
report.transition_to_dismissed(user=self.moderator)
report.resolution_notes = 'Report does not violate any guidelines'
report.resolved_at = timezone.now()
report.save()
report.refresh_from_db()
self.assertEqual(report.status, 'DISMISSED')
class BulkOperationWorkflowTests(TestCase):
"""Tests for bulk operation workflows."""
@classmethod
def setUpTestData(cls):
cls.admin = User.objects.create_user(
username='admin_bulk',
email='admin_bulk@example.com',
password='testpass123',
role='ADMIN'
)
def test_bulk_operation_success_workflow(self):
"""
Test successful bulk operation workflow.
Flow: Admin creates → Operation runs → Progress tracked → Completed
"""
from apps.moderation.models import BulkOperation
operation = BulkOperation.objects.create(
operation_type='APPROVE_SUBMISSIONS',
status='PENDING',
total_items=10,
processed_items=0,
created_by=self.admin,
parameters={'submission_ids': list(range(1, 11))}
)
self.assertEqual(operation.status, 'PENDING')
# Start operation
operation.transition_to_running(user=self.admin)
operation.started_at = timezone.now()
operation.save()
self.assertEqual(operation.status, 'RUNNING')
# Simulate progress
for i in range(1, 11):
operation.processed_items = i
operation.save()
# Complete operation
operation.transition_to_completed(user=self.admin)
operation.completed_at = timezone.now()
operation.results = {'approved': 10, 'failed': 0}
operation.save()
operation.refresh_from_db()
self.assertEqual(operation.status, 'COMPLETED')
self.assertEqual(operation.processed_items, 10)
def test_bulk_operation_failure_workflow(self):
"""
Test bulk operation failure workflow.
Flow: Admin creates → Operation runs → Error occurs → Failed
"""
from apps.moderation.models import BulkOperation
operation = BulkOperation.objects.create(
operation_type='DELETE_CONTENT',
status='PENDING',
total_items=5,
processed_items=0,
created_by=self.admin,
parameters={'content_ids': list(range(1, 6))}
)
operation.transition_to_running(user=self.admin)
operation.started_at = timezone.now()
operation.save()
# Simulate partial progress then failure
operation.processed_items = 2
operation.failed_items = 3
operation.transition_to_failed(user=self.admin)
operation.completed_at = timezone.now()
operation.results = {'error': 'Database connection lost', 'processed': 2}
operation.save()
operation.refresh_from_db()
self.assertEqual(operation.status, 'FAILED')
self.assertEqual(operation.failed_items, 3)
def test_bulk_operation_cancellation_workflow(self):
"""
Test bulk operation cancellation workflow.
Flow: Admin creates → Operation runs → Admin cancels
"""
from apps.moderation.models import BulkOperation
operation = BulkOperation.objects.create(
operation_type='BATCH_UPDATE',
status='PENDING',
total_items=100,
processed_items=0,
created_by=self.admin,
parameters={'update_field': 'status'},
can_cancel=True
)
operation.transition_to_running(user=self.admin)
operation.save()
# Partial progress
operation.processed_items = 30
operation.save()
# Admin cancels
operation.transition_to_cancelled(user=self.admin)
operation.completed_at = timezone.now()
operation.results = {'cancelled_at': 30, 'reason': 'User requested cancellation'}
operation.save()
operation.refresh_from_db()
self.assertEqual(operation.status, 'CANCELLED')
self.assertEqual(operation.processed_items, 30)
class ModerationQueueWorkflowTests(TestCase):
"""Tests for moderation queue workflows."""
@classmethod
def setUpTestData(cls):
cls.moderator = User.objects.create_user(
username='mod_queue',
email='mod_queue@example.com',
password='testpass123',
role='MODERATOR'
)
def test_queue_completion_workflow(self):
"""
Test queue item completion workflow.
Flow: Item created → Moderator claims → Work done → Completed
"""
from apps.moderation.models import ModerationQueue
queue_item = ModerationQueue.objects.create(
queue_type='SUBMISSION_REVIEW',
status='PENDING',
priority='MEDIUM',
item_type='edit_submission',
item_id=123
)
self.assertEqual(queue_item.status, 'PENDING')
# Moderator claims
queue_item.transition_to_in_progress(user=self.moderator)
queue_item.assigned_to = self.moderator
queue_item.assigned_at = timezone.now()
queue_item.save()
self.assertEqual(queue_item.status, 'IN_PROGRESS')
# Work completed
queue_item.transition_to_completed(user=self.moderator)
queue_item.completed_at = timezone.now()
queue_item.save()
queue_item.refresh_from_db()
self.assertEqual(queue_item.status, 'COMPLETED')