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

@@ -1,10 +1,32 @@
"""
Comprehensive tests for the moderation app.
This module contains tests for:
- EditSubmission state machine transitions
- PhotoSubmission state machine transitions
- ModerationReport state machine transitions
- ModerationQueue state machine transitions
- BulkOperation state machine transitions
- FSM transition logging with django-fsm-log
- Mixin functionality tests
"""
from django.test import TestCase, Client
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.contrib.contenttypes.models import ContentType
from django.core.files.uploadedfile import SimpleUploadedFile
from django.http import JsonResponse, HttpRequest
from .models import EditSubmission
from django.utils import timezone
from django_fsm import TransitionNotAllowed
from .models import (
EditSubmission,
PhotoSubmission,
ModerationReport,
ModerationQueue,
BulkOperation,
ModerationAction,
)
from .mixins import (
EditSubmissionMixin,
PhotoSubmissionMixin,
@@ -349,6 +371,480 @@ class ModerationMixinsTests(TestCase):
self.assertEqual(len(context["edit_submissions"]), 1)
# ============================================================================
# EditSubmission FSM Transition Tests
# ============================================================================
class EditSubmissionTransitionTests(TestCase):
"""Comprehensive tests for EditSubmission FSM transitions."""
def setUp(self):
"""Set up test fixtures."""
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass123',
role='USER'
)
self.moderator = User.objects.create_user(
username='moderator',
email='moderator@example.com',
password='testpass123',
role='MODERATOR'
)
self.admin = User.objects.create_user(
username='admin',
email='admin@example.com',
password='testpass123',
role='ADMIN'
)
self.operator = Operator.objects.create(
name='Test Operator',
description='Test Description'
)
self.content_type = ContentType.objects.get_for_model(Operator)
def _create_submission(self, status='PENDING'):
"""Helper to create an EditSubmission."""
return EditSubmission.objects.create(
user=self.user,
content_type=self.content_type,
object_id=self.operator.id,
submission_type='EDIT',
changes={'name': 'Updated Name'},
status=status,
reason='Test reason'
)
def test_pending_to_approved_transition(self):
"""Test transition from PENDING to APPROVED."""
submission = self._create_submission()
self.assertEqual(submission.status, 'PENDING')
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_pending_to_rejected_transition(self):
"""Test transition from PENDING to REJECTED."""
submission = self._create_submission()
self.assertEqual(submission.status, 'PENDING')
submission.transition_to_rejected(user=self.moderator)
submission.handled_by = self.moderator
submission.handled_at = timezone.now()
submission.notes = 'Rejected: Insufficient evidence'
submission.save()
submission.refresh_from_db()
self.assertEqual(submission.status, 'REJECTED')
self.assertEqual(submission.handled_by, self.moderator)
self.assertIn('Rejected', submission.notes)
def test_pending_to_escalated_transition(self):
"""Test transition from PENDING to ESCALATED."""
submission = self._create_submission()
self.assertEqual(submission.status, 'PENDING')
submission.transition_to_escalated(user=self.moderator)
submission.handled_by = self.moderator
submission.handled_at = timezone.now()
submission.notes = 'Escalated: Needs admin review'
submission.save()
submission.refresh_from_db()
self.assertEqual(submission.status, 'ESCALATED')
def test_escalated_to_approved_transition(self):
"""Test transition from ESCALATED to APPROVED."""
submission = self._create_submission(status='ESCALATED')
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)
def test_escalated_to_rejected_transition(self):
"""Test transition from ESCALATED to REJECTED."""
submission = self._create_submission(status='ESCALATED')
submission.transition_to_rejected(user=self.admin)
submission.handled_by = self.admin
submission.handled_at = timezone.now()
submission.notes = 'Rejected by admin'
submission.save()
submission.refresh_from_db()
self.assertEqual(submission.status, 'REJECTED')
def test_invalid_transition_from_approved(self):
"""Test that transitions from APPROVED state fail."""
submission = self._create_submission(status='APPROVED')
# Attempting to transition from APPROVED should raise TransitionNotAllowed
with self.assertRaises(TransitionNotAllowed):
submission.transition_to_rejected(user=self.moderator)
def test_invalid_transition_from_rejected(self):
"""Test that transitions from REJECTED state fail."""
submission = self._create_submission(status='REJECTED')
# Attempting to transition from REJECTED should raise TransitionNotAllowed
with self.assertRaises(TransitionNotAllowed):
submission.transition_to_approved(user=self.moderator)
def test_approve_wrapper_method(self):
"""Test the approve() wrapper method."""
submission = self._create_submission()
result = submission.approve(self.moderator)
submission.refresh_from_db()
self.assertEqual(submission.status, 'APPROVED')
self.assertEqual(submission.handled_by, self.moderator)
self.assertIsNotNone(submission.handled_at)
def test_reject_wrapper_method(self):
"""Test the reject() wrapper method."""
submission = self._create_submission()
submission.reject(self.moderator, reason='Not enough evidence')
submission.refresh_from_db()
self.assertEqual(submission.status, 'REJECTED')
self.assertIn('Not enough evidence', submission.notes)
def test_escalate_wrapper_method(self):
"""Test the escalate() wrapper method."""
submission = self._create_submission()
submission.escalate(self.moderator, reason='Needs admin approval')
submission.refresh_from_db()
self.assertEqual(submission.status, 'ESCALATED')
self.assertIn('Needs admin approval', submission.notes)
# ============================================================================
# ModerationReport FSM Transition Tests
# ============================================================================
class ModerationReportTransitionTests(TestCase):
"""Comprehensive tests for ModerationReport FSM transitions."""
def setUp(self):
"""Set up test fixtures."""
self.user = User.objects.create_user(
username='reporter',
email='reporter@example.com',
password='testpass123',
role='USER'
)
self.moderator = User.objects.create_user(
username='moderator',
email='moderator@example.com',
password='testpass123',
role='MODERATOR'
)
self.operator = Operator.objects.create(
name='Test Operator',
description='Test Description'
)
self.content_type = ContentType.objects.get_for_model(Operator)
def _create_report(self, status='PENDING'):
"""Helper to create a ModerationReport."""
return ModerationReport.objects.create(
report_type='CONTENT',
status=status,
priority='MEDIUM',
reported_entity_type='company',
reported_entity_id=self.operator.id,
content_type=self.content_type,
reason='Inaccurate information',
description='The company information is incorrect',
reported_by=self.user
)
def test_pending_to_under_review_transition(self):
"""Test transition from PENDING to UNDER_REVIEW."""
report = self._create_report()
self.assertEqual(report.status, 'PENDING')
report.transition_to_under_review(user=self.moderator)
report.assigned_moderator = self.moderator
report.save()
report.refresh_from_db()
self.assertEqual(report.status, 'UNDER_REVIEW')
self.assertEqual(report.assigned_moderator, self.moderator)
def test_under_review_to_resolved_transition(self):
"""Test transition from UNDER_REVIEW to RESOLVED."""
report = self._create_report(status='UNDER_REVIEW')
report.assigned_moderator = self.moderator
report.save()
report.transition_to_resolved(user=self.moderator)
report.resolution_action = 'Content updated'
report.resolution_notes = 'Fixed the incorrect information'
report.resolved_at = timezone.now()
report.save()
report.refresh_from_db()
self.assertEqual(report.status, 'RESOLVED')
self.assertIsNotNone(report.resolved_at)
def test_under_review_to_dismissed_transition(self):
"""Test transition from UNDER_REVIEW to DISMISSED."""
report = self._create_report(status='UNDER_REVIEW')
report.assigned_moderator = self.moderator
report.save()
report.transition_to_dismissed(user=self.moderator)
report.resolution_notes = 'Report is not valid'
report.resolved_at = timezone.now()
report.save()
report.refresh_from_db()
self.assertEqual(report.status, 'DISMISSED')
def test_invalid_transition_from_resolved(self):
"""Test that transitions from RESOLVED state fail."""
report = self._create_report(status='RESOLVED')
with self.assertRaises(TransitionNotAllowed):
report.transition_to_dismissed(user=self.moderator)
def test_invalid_transition_from_dismissed(self):
"""Test that transitions from DISMISSED state fail."""
report = self._create_report(status='DISMISSED')
with self.assertRaises(TransitionNotAllowed):
report.transition_to_resolved(user=self.moderator)
# ============================================================================
# ModerationQueue FSM Transition Tests
# ============================================================================
class ModerationQueueTransitionTests(TestCase):
"""Comprehensive tests for ModerationQueue FSM transitions."""
def setUp(self):
"""Set up test fixtures."""
self.moderator = User.objects.create_user(
username='moderator',
email='moderator@example.com',
password='testpass123',
role='MODERATOR'
)
def _create_queue_item(self, status='PENDING'):
"""Helper to create a ModerationQueue item."""
return ModerationQueue.objects.create(
item_type='EDIT_SUBMISSION',
status=status,
priority='MEDIUM',
title='Review edit submission',
description='User submitted an edit that needs review',
flagged_by=self.moderator
)
def test_pending_to_in_progress_transition(self):
"""Test transition from PENDING to IN_PROGRESS."""
item = self._create_queue_item()
self.assertEqual(item.status, 'PENDING')
item.transition_to_in_progress(user=self.moderator)
item.assigned_to = self.moderator
item.assigned_at = timezone.now()
item.save()
item.refresh_from_db()
self.assertEqual(item.status, 'IN_PROGRESS')
self.assertEqual(item.assigned_to, self.moderator)
def test_in_progress_to_completed_transition(self):
"""Test transition from IN_PROGRESS to COMPLETED."""
item = self._create_queue_item(status='IN_PROGRESS')
item.assigned_to = self.moderator
item.save()
item.transition_to_completed(user=self.moderator)
item.save()
item.refresh_from_db()
self.assertEqual(item.status, 'COMPLETED')
def test_in_progress_to_cancelled_transition(self):
"""Test transition from IN_PROGRESS to CANCELLED."""
item = self._create_queue_item(status='IN_PROGRESS')
item.assigned_to = self.moderator
item.save()
item.transition_to_cancelled(user=self.moderator)
item.save()
item.refresh_from_db()
self.assertEqual(item.status, 'CANCELLED')
def test_pending_to_cancelled_transition(self):
"""Test transition from PENDING to CANCELLED."""
item = self._create_queue_item()
item.transition_to_cancelled(user=self.moderator)
item.save()
item.refresh_from_db()
self.assertEqual(item.status, 'CANCELLED')
def test_invalid_transition_from_completed(self):
"""Test that transitions from COMPLETED state fail."""
item = self._create_queue_item(status='COMPLETED')
with self.assertRaises(TransitionNotAllowed):
item.transition_to_in_progress(user=self.moderator)
# ============================================================================
# BulkOperation FSM Transition Tests
# ============================================================================
class BulkOperationTransitionTests(TestCase):
"""Comprehensive tests for BulkOperation FSM transitions."""
def setUp(self):
"""Set up test fixtures."""
self.admin = User.objects.create_user(
username='admin',
email='admin@example.com',
password='testpass123',
role='ADMIN'
)
def _create_bulk_operation(self, status='PENDING'):
"""Helper to create a BulkOperation."""
return BulkOperation.objects.create(
operation_type='BULK_UPDATE',
status=status,
priority='MEDIUM',
description='Bulk update park statuses',
parameters={'target': 'parks', 'action': 'update_status'},
created_by=self.admin,
total_items=100
)
def test_pending_to_running_transition(self):
"""Test transition from PENDING to RUNNING."""
operation = self._create_bulk_operation()
self.assertEqual(operation.status, 'PENDING')
operation.transition_to_running(user=self.admin)
operation.started_at = timezone.now()
operation.save()
operation.refresh_from_db()
self.assertEqual(operation.status, 'RUNNING')
self.assertIsNotNone(operation.started_at)
def test_running_to_completed_transition(self):
"""Test transition from RUNNING to COMPLETED."""
operation = self._create_bulk_operation(status='RUNNING')
operation.started_at = timezone.now()
operation.save()
operation.transition_to_completed(user=self.admin)
operation.completed_at = timezone.now()
operation.processed_items = 100
operation.save()
operation.refresh_from_db()
self.assertEqual(operation.status, 'COMPLETED')
self.assertIsNotNone(operation.completed_at)
self.assertEqual(operation.processed_items, 100)
def test_running_to_failed_transition(self):
"""Test transition from RUNNING to FAILED."""
operation = self._create_bulk_operation(status='RUNNING')
operation.started_at = timezone.now()
operation.save()
operation.transition_to_failed(user=self.admin)
operation.completed_at = timezone.now()
operation.results = {'error': 'Database connection failed'}
operation.failed_items = 50
operation.save()
operation.refresh_from_db()
self.assertEqual(operation.status, 'FAILED')
self.assertEqual(operation.failed_items, 50)
def test_pending_to_cancelled_transition(self):
"""Test transition from PENDING to CANCELLED."""
operation = self._create_bulk_operation()
operation.transition_to_cancelled(user=self.admin)
operation.save()
operation.refresh_from_db()
self.assertEqual(operation.status, 'CANCELLED')
def test_running_to_cancelled_transition(self):
"""Test transition from RUNNING to CANCELLED when cancellable."""
operation = self._create_bulk_operation(status='RUNNING')
operation.can_cancel = True
operation.save()
operation.transition_to_cancelled(user=self.admin)
operation.save()
operation.refresh_from_db()
self.assertEqual(operation.status, 'CANCELLED')
def test_invalid_transition_from_completed(self):
"""Test that transitions from COMPLETED state fail."""
operation = self._create_bulk_operation(status='COMPLETED')
with self.assertRaises(TransitionNotAllowed):
operation.transition_to_running(user=self.admin)
def test_invalid_transition_from_failed(self):
"""Test that transitions from FAILED state fail."""
operation = self._create_bulk_operation(status='FAILED')
with self.assertRaises(TransitionNotAllowed):
operation.transition_to_completed(user=self.admin)
def test_progress_percentage_calculation(self):
"""Test progress percentage property."""
operation = self._create_bulk_operation()
operation.total_items = 100
operation.processed_items = 50
self.assertEqual(operation.progress_percentage, 50.0)
operation.processed_items = 0
self.assertEqual(operation.progress_percentage, 0.0)
operation.total_items = 0
self.assertEqual(operation.progress_percentage, 0.0)
# ============================================================================
# FSM Transition Logging Tests
# ============================================================================
@@ -388,7 +884,8 @@ class TransitionLoggingTestCase(TestCase):
object_id=self.operator.id,
submission_type='EDIT',
changes={'name': 'Updated Name'},
status='PENDING'
status='PENDING',
reason='Test reason'
)
# Perform transition
@@ -417,7 +914,8 @@ class TransitionLoggingTestCase(TestCase):
object_id=self.operator.id,
submission_type='EDIT',
changes={'name': 'Updated Name'},
status='PENDING'
status='PENDING',
reason='Test reason'
)
submission_ct = ContentType.objects.get_for_model(submission)
@@ -443,7 +941,6 @@ class TransitionLoggingTestCase(TestCase):
def test_history_endpoint_returns_logs(self):
"""Test history API endpoint returns transition logs."""
from rest_framework.test import APIClient
from django_fsm_log.models import StateLog
api_client = APIClient()
api_client.force_authenticate(user=self.moderator)
@@ -454,7 +951,8 @@ class TransitionLoggingTestCase(TestCase):
object_id=self.operator.id,
submission_type='EDIT',
changes={'name': 'Updated Name'},
status='PENDING'
status='PENDING',
reason='Test reason'
)
# Perform transition to create log
@@ -463,11 +961,9 @@ class TransitionLoggingTestCase(TestCase):
# Note: This assumes EditSubmission has a history endpoint
# Adjust URL pattern based on actual implementation
response = api_client.get(f'/api/moderation/reports/all_history/')
response = api_client.get('/api/moderation/reports/all_history/')
self.assertEqual(response.status_code, 200)
# Response should contain history data
# Actual assertions depend on response format
def test_system_transitions_without_user(self):
"""Test that system transitions work without a user."""
@@ -479,7 +975,8 @@ class TransitionLoggingTestCase(TestCase):
object_id=self.operator.id,
submission_type='EDIT',
changes={'name': 'Updated Name'},
status='PENDING'
status='PENDING',
reason='Test reason'
)
# Perform transition without user
@@ -507,7 +1004,8 @@ class TransitionLoggingTestCase(TestCase):
object_id=self.operator.id,
submission_type='EDIT',
changes={'name': 'Updated Name'},
status='PENDING'
status='PENDING',
reason='Test reason'
)
# Perform transition
@@ -525,3 +1023,367 @@ class TransitionLoggingTestCase(TestCase):
# Description field exists and can be used for audit trails
self.assertTrue(hasattr(log, 'description'))
def test_log_ordering_by_timestamp(self):
"""Test that logs are properly ordered by timestamp."""
from django_fsm_log.models import StateLog
import time
submission = EditSubmission.objects.create(
user=self.user,
content_type=self.content_type,
object_id=self.operator.id,
submission_type='EDIT',
changes={'name': 'Updated Name'},
status='PENDING',
reason='Test reason'
)
submission_ct = ContentType.objects.get_for_model(submission)
# Create multiple transitions
submission.transition_to_escalated(user=self.moderator)
submission.save()
submission.transition_to_approved(user=self.moderator)
submission.save()
# Get logs ordered by timestamp
logs = list(StateLog.objects.filter(
content_type=submission_ct,
object_id=submission.id
).order_by('timestamp'))
# Verify ordering
self.assertEqual(len(logs), 2)
self.assertTrue(logs[0].timestamp <= logs[1].timestamp)
# ============================================================================
# ModerationAction Model Tests
# ============================================================================
class ModerationActionTests(TestCase):
"""Tests for ModerationAction model."""
def setUp(self):
"""Set up test fixtures."""
self.moderator = User.objects.create_user(
username='moderator',
email='moderator@example.com',
password='testpass123',
role='MODERATOR'
)
self.target_user = User.objects.create_user(
username='target',
email='target@example.com',
password='testpass123',
role='USER'
)
def test_create_action_with_duration(self):
"""Test creating an action with duration sets expires_at."""
action = ModerationAction.objects.create(
action_type='TEMPORARY_BAN',
reason='Spam',
details='User was spamming the forums',
duration_hours=24,
moderator=self.moderator,
target_user=self.target_user
)
self.assertIsNotNone(action.expires_at)
# expires_at should be approximately 24 hours from now
time_diff = action.expires_at - timezone.now()
self.assertAlmostEqual(time_diff.total_seconds(), 24 * 3600, delta=60)
def test_create_action_without_duration(self):
"""Test creating an action without duration has no expires_at."""
action = ModerationAction.objects.create(
action_type='WARNING',
reason='First offense',
details='Warning issued for minor violation',
moderator=self.moderator,
target_user=self.target_user
)
self.assertIsNone(action.expires_at)
def test_action_is_active_by_default(self):
"""Test that new actions are active by default."""
action = ModerationAction.objects.create(
action_type='WARNING',
reason='Test',
details='Test warning',
moderator=self.moderator,
target_user=self.target_user
)
self.assertTrue(action.is_active)
# ============================================================================
# PhotoSubmission FSM Transition Tests
# ============================================================================
class PhotoSubmissionTransitionTests(TestCase):
"""Comprehensive tests for PhotoSubmission FSM transitions."""
def setUp(self):
"""Set up test fixtures."""
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass123',
role='USER'
)
self.moderator = User.objects.create_user(
username='moderator',
email='moderator@example.com',
password='testpass123',
role='MODERATOR'
)
self.admin = User.objects.create_user(
username='admin',
email='admin@example.com',
password='testpass123',
role='ADMIN'
)
self.operator = Operator.objects.create(
name='Test Operator',
description='Test Description',
roles=['OPERATOR']
)
self.content_type = ContentType.objects.get_for_model(Operator)
def _create_mock_photo(self):
"""Create a mock CloudflareImage for testing."""
from unittest.mock import Mock
mock_photo = Mock()
mock_photo.pk = 1
mock_photo.id = 1
return mock_photo
def _create_submission(self, status='PENDING'):
"""Helper to create a PhotoSubmission."""
# Create using direct database creation to bypass FK validation
from unittest.mock import patch, Mock
with patch.object(PhotoSubmission, 'photo', Mock()):
submission = PhotoSubmission(
user=self.user,
content_type=self.content_type,
object_id=self.operator.id,
caption='Test Photo',
status=status,
)
# Bypass model save to avoid FK constraint on photo
submission.photo_id = 1
submission.save(update_fields=None)
# Force status after creation for non-PENDING states
if status != 'PENDING':
PhotoSubmission.objects.filter(pk=submission.pk).update(status=status)
submission.refresh_from_db()
return submission
def test_pending_to_approved_transition(self):
"""Test transition from PENDING to APPROVED."""
submission = self._create_submission()
self.assertEqual(submission.status, 'PENDING')
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_pending_to_rejected_transition(self):
"""Test transition from PENDING to REJECTED."""
submission = self._create_submission()
self.assertEqual(submission.status, 'PENDING')
submission.transition_to_rejected(user=self.moderator)
submission.handled_by = self.moderator
submission.handled_at = timezone.now()
submission.notes = 'Rejected: Image quality too low'
submission.save()
submission.refresh_from_db()
self.assertEqual(submission.status, 'REJECTED')
self.assertEqual(submission.handled_by, self.moderator)
self.assertIn('Rejected', submission.notes)
def test_pending_to_escalated_transition(self):
"""Test transition from PENDING to ESCALATED."""
submission = self._create_submission()
self.assertEqual(submission.status, 'PENDING')
submission.transition_to_escalated(user=self.moderator)
submission.handled_by = self.moderator
submission.handled_at = timezone.now()
submission.notes = 'Escalated: Copyright concerns'
submission.save()
submission.refresh_from_db()
self.assertEqual(submission.status, 'ESCALATED')
def test_escalated_to_approved_transition(self):
"""Test transition from ESCALATED to APPROVED."""
submission = self._create_submission(status='ESCALATED')
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)
def test_escalated_to_rejected_transition(self):
"""Test transition from ESCALATED to REJECTED."""
submission = self._create_submission(status='ESCALATED')
submission.transition_to_rejected(user=self.admin)
submission.handled_by = self.admin
submission.handled_at = timezone.now()
submission.notes = 'Rejected by admin after review'
submission.save()
submission.refresh_from_db()
self.assertEqual(submission.status, 'REJECTED')
def test_invalid_transition_from_approved(self):
"""Test that transitions from APPROVED state fail."""
submission = self._create_submission(status='APPROVED')
with self.assertRaises(TransitionNotAllowed):
submission.transition_to_rejected(user=self.moderator)
def test_invalid_transition_from_rejected(self):
"""Test that transitions from REJECTED state fail."""
submission = self._create_submission(status='REJECTED')
with self.assertRaises(TransitionNotAllowed):
submission.transition_to_approved(user=self.moderator)
def test_reject_wrapper_method(self):
"""Test the reject() wrapper method."""
from unittest.mock import patch
submission = self._create_submission()
# Mock the photo creation part since we don't have actual photos
with patch.object(submission, 'transition_to_rejected'):
submission.reject(self.moderator, notes='Not suitable')
submission.refresh_from_db()
self.assertEqual(submission.status, 'REJECTED')
self.assertIn('Not suitable', submission.notes)
def test_escalate_wrapper_method(self):
"""Test the escalate() wrapper method."""
from unittest.mock import patch
submission = self._create_submission()
with patch.object(submission, 'transition_to_escalated'):
submission.escalate(self.moderator, notes='Needs admin review')
submission.refresh_from_db()
self.assertEqual(submission.status, 'ESCALATED')
self.assertIn('Needs admin review', submission.notes)
def test_transition_creates_state_log(self):
"""Test that transitions create StateLog entries."""
from django_fsm_log.models import StateLog
submission = self._create_submission()
# Perform transition
submission.transition_to_approved(user=self.moderator)
submission.save()
# Check log was created
submission_ct = ContentType.objects.get_for_model(submission)
log = StateLog.objects.filter(
content_type=submission_ct,
object_id=submission.id
).first()
self.assertIsNotNone(log, "StateLog entry should be created")
self.assertEqual(log.state, 'APPROVED')
self.assertEqual(log.by, self.moderator)
def test_multiple_transitions_logged(self):
"""Test that multiple transitions are all logged."""
from django_fsm_log.models import StateLog
submission = self._create_submission()
submission_ct = ContentType.objects.get_for_model(submission)
# First transition: PENDING -> ESCALATED
submission.transition_to_escalated(user=self.moderator)
submission.save()
# Second transition: ESCALATED -> APPROVED
submission.transition_to_approved(user=self.admin)
submission.save()
# Check multiple logs created
logs = StateLog.objects.filter(
content_type=submission_ct,
object_id=submission.id
).order_by('timestamp')
self.assertEqual(logs.count(), 2, "Should have 2 log entries")
self.assertEqual(logs[0].state, 'ESCALATED')
self.assertEqual(logs[1].state, 'APPROVED')
def test_handled_by_and_handled_at_updated(self):
"""Test that handled_by and handled_at are properly updated."""
submission = self._create_submission()
self.assertIsNone(submission.handled_by)
self.assertIsNone(submission.handled_at)
before_time = timezone.now()
submission.transition_to_approved(user=self.moderator)
submission.handled_by = self.moderator
submission.handled_at = timezone.now()
submission.save()
after_time = timezone.now()
submission.refresh_from_db()
self.assertEqual(submission.handled_by, self.moderator)
self.assertIsNotNone(submission.handled_at)
self.assertTrue(before_time <= submission.handled_at <= after_time)
def test_notes_field_updated_on_rejection(self):
"""Test that notes field is updated with rejection reason."""
submission = self._create_submission()
rejection_reason = 'Image contains watermarks'
submission.transition_to_rejected(user=self.moderator)
submission.notes = rejection_reason
submission.save()
submission.refresh_from_db()
self.assertEqual(submission.notes, rejection_reason)
def test_notes_field_updated_on_escalation(self):
"""Test that notes field is updated with escalation reason."""
submission = self._create_submission()
escalation_reason = 'Potentially copyrighted content'
submission.transition_to_escalated(user=self.moderator)
submission.notes = escalation_reason
submission.save()
submission.refresh_from_db()
self.assertEqual(submission.notes, escalation_reason)