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 @@
"""
Parks test package.
This package contains tests for the parks app including:
- Park workflow tests (test_park_workflows.py)
- Park model tests
"""

View File

@@ -0,0 +1,533 @@
"""
Integration tests for Park lifecycle workflows.
This module tests end-to-end park lifecycle workflows including:
- Park opening workflow
- Park temporary closure workflow
- Park permanent closure workflow
- Park relocation workflow
- Related ride status updates
"""
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.utils import timezone
User = get_user_model()
class ParkOpeningWorkflowTests(TestCase):
"""Tests for park opening workflow."""
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
username='park_user',
email='park_user@example.com',
password='testpass123',
role='USER'
)
cls.moderator = User.objects.create_user(
username='park_mod',
email='park_mod@example.com',
password='testpass123',
role='MODERATOR'
)
def _create_park(self, status='OPERATING', **kwargs):
"""Helper to create a park."""
from apps.parks.models import Park, Company
operator = Company.objects.create(
name=f'Operator {status}',
roles=['OPERATOR']
)
defaults = {
'name': f'Test Park {status}',
'slug': f'test-park-{status.lower()}-{timezone.now().timestamp()}',
'operator': operator,
'status': status,
'timezone': 'America/New_York'
}
defaults.update(kwargs)
return Park.objects.create(**defaults)
def test_park_opens_from_under_construction(self):
"""
Test park opening from under construction state.
Flow: UNDER_CONSTRUCTION → OPERATING
"""
park = self._create_park(status='UNDER_CONSTRUCTION')
self.assertEqual(park.status, 'UNDER_CONSTRUCTION')
# Park opens
park.transition_to_operating(user=self.user)
park.save()
park.refresh_from_db()
self.assertEqual(park.status, 'OPERATING')
class ParkTemporaryClosureWorkflowTests(TestCase):
"""Tests for park temporary closure workflow."""
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
username='temp_closure_user',
email='temp_closure@example.com',
password='testpass123',
role='USER'
)
def _create_park(self, status='OPERATING', **kwargs):
from apps.parks.models import Park, Company
operator = Company.objects.create(
name=f'Operator Temp {timezone.now().timestamp()}',
roles=['OPERATOR']
)
defaults = {
'name': f'Test Park Temp {timezone.now().timestamp()}',
'slug': f'test-park-temp-{timezone.now().timestamp()}',
'operator': operator,
'status': status,
'timezone': 'America/New_York'
}
defaults.update(kwargs)
return Park.objects.create(**defaults)
def test_park_temporary_closure_and_reopen(self):
"""
Test park temporary closure and reopening.
Flow: OPERATING → CLOSED_TEMP → OPERATING
"""
park = self._create_park(status='OPERATING')
self.assertEqual(park.status, 'OPERATING')
# Close temporarily (e.g., off-season)
park.transition_to_closed_temp(user=self.user)
park.save()
park.refresh_from_db()
self.assertEqual(park.status, 'CLOSED_TEMP')
# Reopen
park.transition_to_operating(user=self.user)
park.save()
park.refresh_from_db()
self.assertEqual(park.status, 'OPERATING')
class ParkPermanentClosureWorkflowTests(TestCase):
"""Tests for park permanent closure workflow."""
@classmethod
def setUpTestData(cls):
cls.moderator = User.objects.create_user(
username='perm_mod',
email='perm_mod@example.com',
password='testpass123',
role='MODERATOR'
)
def _create_park(self, status='OPERATING', **kwargs):
from apps.parks.models import Park, Company
operator = Company.objects.create(
name=f'Operator Perm {timezone.now().timestamp()}',
roles=['OPERATOR']
)
defaults = {
'name': f'Test Park Perm {timezone.now().timestamp()}',
'slug': f'test-park-perm-{timezone.now().timestamp()}',
'operator': operator,
'status': status,
'timezone': 'America/New_York'
}
defaults.update(kwargs)
return Park.objects.create(**defaults)
def test_park_permanent_closure(self):
"""
Test park permanent closure from operating state.
Flow: OPERATING → CLOSED_PERM
"""
park = self._create_park(status='OPERATING')
# Close permanently
park.transition_to_closed_perm(user=self.moderator)
park.closing_date = timezone.now().date()
park.save()
park.refresh_from_db()
self.assertEqual(park.status, 'CLOSED_PERM')
self.assertIsNotNone(park.closing_date)
def test_park_permanent_closure_from_temp(self):
"""
Test park permanent closure from temporary closure.
Flow: OPERATING → CLOSED_TEMP → CLOSED_PERM
"""
park = self._create_park(status='OPERATING')
# Temporary closure
park.transition_to_closed_temp(user=self.moderator)
park.save()
# Becomes permanent
park.transition_to_closed_perm(user=self.moderator)
park.closing_date = timezone.now().date()
park.save()
park.refresh_from_db()
self.assertEqual(park.status, 'CLOSED_PERM')
class ParkDemolitionWorkflowTests(TestCase):
"""Tests for park demolition workflow."""
@classmethod
def setUpTestData(cls):
cls.moderator = User.objects.create_user(
username='demo_mod',
email='demo_mod@example.com',
password='testpass123',
role='MODERATOR'
)
def _create_park(self, status='CLOSED_PERM', **kwargs):
from apps.parks.models import Park, Company
operator = Company.objects.create(
name=f'Operator Demo {timezone.now().timestamp()}',
roles=['OPERATOR']
)
defaults = {
'name': f'Test Park Demo {timezone.now().timestamp()}',
'slug': f'test-park-demo-{timezone.now().timestamp()}',
'operator': operator,
'status': status,
'timezone': 'America/New_York'
}
defaults.update(kwargs)
return Park.objects.create(**defaults)
def test_park_demolition_workflow(self):
"""
Test complete park demolition workflow.
Flow: OPERATING → CLOSED_PERM → DEMOLISHED
"""
park = self._create_park(status='CLOSED_PERM')
# Demolish
park.transition_to_demolished(user=self.moderator)
park.save()
park.refresh_from_db()
self.assertEqual(park.status, 'DEMOLISHED')
def test_demolished_is_final_state(self):
"""Test that demolished parks cannot transition further."""
from django_fsm import TransitionNotAllowed
park = self._create_park(status='DEMOLISHED')
# Cannot transition from demolished
with self.assertRaises(TransitionNotAllowed):
park.transition_to_operating(user=self.moderator)
class ParkRelocationWorkflowTests(TestCase):
"""Tests for park relocation workflow."""
@classmethod
def setUpTestData(cls):
cls.moderator = User.objects.create_user(
username='reloc_mod',
email='reloc_mod@example.com',
password='testpass123',
role='MODERATOR'
)
def _create_park(self, status='CLOSED_PERM', **kwargs):
from apps.parks.models import Park, Company
operator = Company.objects.create(
name=f'Operator Reloc {timezone.now().timestamp()}',
roles=['OPERATOR']
)
defaults = {
'name': f'Test Park Reloc {timezone.now().timestamp()}',
'slug': f'test-park-reloc-{timezone.now().timestamp()}',
'operator': operator,
'status': status,
'timezone': 'America/New_York'
}
defaults.update(kwargs)
return Park.objects.create(**defaults)
def test_park_relocation_workflow(self):
"""
Test park relocation workflow.
Flow: OPERATING → CLOSED_PERM → RELOCATED
"""
park = self._create_park(status='CLOSED_PERM')
# Relocate
park.transition_to_relocated(user=self.moderator)
park.save()
park.refresh_from_db()
self.assertEqual(park.status, 'RELOCATED')
def test_relocated_is_final_state(self):
"""Test that relocated parks cannot transition further."""
from django_fsm import TransitionNotAllowed
park = self._create_park(status='RELOCATED')
# Cannot transition from relocated
with self.assertRaises(TransitionNotAllowed):
park.transition_to_operating(user=self.moderator)
class ParkWrapperMethodTests(TestCase):
"""Tests for park wrapper methods."""
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
username='wrapper_user',
email='wrapper@example.com',
password='testpass123',
role='USER'
)
cls.moderator = User.objects.create_user(
username='wrapper_mod',
email='wrapper_mod@example.com',
password='testpass123',
role='MODERATOR'
)
def _create_park(self, status='OPERATING', **kwargs):
from apps.parks.models import Park, Company
operator = Company.objects.create(
name=f'Operator Wrapper {timezone.now().timestamp()}',
roles=['OPERATOR']
)
defaults = {
'name': f'Test Park Wrapper {timezone.now().timestamp()}',
'slug': f'test-park-wrapper-{timezone.now().timestamp()}',
'operator': operator,
'status': status,
'timezone': 'America/New_York'
}
defaults.update(kwargs)
return Park.objects.create(**defaults)
def test_close_temporarily_wrapper(self):
"""Test close_temporarily wrapper method."""
park = self._create_park(status='OPERATING')
# Use wrapper method if it exists
if hasattr(park, 'close_temporarily'):
park.close_temporarily(user=self.user)
else:
park.transition_to_closed_temp(user=self.user)
park.save()
park.refresh_from_db()
self.assertEqual(park.status, 'CLOSED_TEMP')
def test_reopen_wrapper(self):
"""Test reopen wrapper method."""
park = self._create_park(status='CLOSED_TEMP')
# Use wrapper method if it exists
if hasattr(park, 'reopen'):
park.reopen(user=self.user)
else:
park.transition_to_operating(user=self.user)
park.save()
park.refresh_from_db()
self.assertEqual(park.status, 'OPERATING')
def test_close_permanently_wrapper(self):
"""Test close_permanently wrapper method."""
park = self._create_park(status='OPERATING')
closing_date = timezone.now().date()
# Use wrapper method if it exists
if hasattr(park, 'close_permanently'):
park.close_permanently(closing_date=closing_date, user=self.moderator)
else:
park.transition_to_closed_perm(user=self.moderator)
park.closing_date = closing_date
park.save()
park.refresh_from_db()
self.assertEqual(park.status, 'CLOSED_PERM')
def test_demolish_wrapper(self):
"""Test demolish wrapper method."""
park = self._create_park(status='CLOSED_PERM')
# Use wrapper method if it exists
if hasattr(park, 'demolish'):
park.demolish(user=self.moderator)
else:
park.transition_to_demolished(user=self.moderator)
park.save()
park.refresh_from_db()
self.assertEqual(park.status, 'DEMOLISHED')
def test_relocate_wrapper(self):
"""Test relocate wrapper method."""
park = self._create_park(status='CLOSED_PERM')
# Use wrapper method if it exists
if hasattr(park, 'relocate'):
park.relocate(user=self.moderator)
else:
park.transition_to_relocated(user=self.moderator)
park.save()
park.refresh_from_db()
self.assertEqual(park.status, 'RELOCATED')
class ParkStateLogTests(TestCase):
"""Tests for StateLog entries on park transitions."""
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
username='log_user',
email='log_user@example.com',
password='testpass123',
role='USER'
)
cls.moderator = User.objects.create_user(
username='log_mod',
email='log_mod@example.com',
password='testpass123',
role='MODERATOR'
)
def _create_park(self, status='OPERATING', **kwargs):
from apps.parks.models import Park, Company
operator = Company.objects.create(
name=f'Operator Log {timezone.now().timestamp()}',
roles=['OPERATOR']
)
defaults = {
'name': f'Test Park Log {timezone.now().timestamp()}',
'slug': f'test-park-log-{timezone.now().timestamp()}',
'operator': operator,
'status': status,
'timezone': 'America/New_York'
}
defaults.update(kwargs)
return Park.objects.create(**defaults)
def test_transition_creates_state_log(self):
"""Test that park transitions create StateLog entries."""
from django_fsm_log.models import StateLog
from django.contrib.contenttypes.models import ContentType
park = self._create_park(status='OPERATING')
park_ct = ContentType.objects.get_for_model(park)
# Perform transition
park.transition_to_closed_temp(user=self.user)
park.save()
# Check log was created
log = StateLog.objects.filter(
content_type=park_ct,
object_id=park.id
).first()
self.assertIsNotNone(log, "StateLog entry should be created")
self.assertEqual(log.state, 'CLOSED_TEMP')
self.assertEqual(log.by, self.user)
def test_multiple_transitions_logged(self):
"""Test that multiple park transitions are all logged."""
from django_fsm_log.models import StateLog
from django.contrib.contenttypes.models import ContentType
park = self._create_park(status='OPERATING')
park_ct = ContentType.objects.get_for_model(park)
# First transition: OPERATING -> CLOSED_TEMP
park.transition_to_closed_temp(user=self.user)
park.save()
# Second transition: CLOSED_TEMP -> CLOSED_PERM
park.transition_to_closed_perm(user=self.moderator)
park.save()
# Check multiple logs created
logs = StateLog.objects.filter(
content_type=park_ct,
object_id=park.id
).order_by('timestamp')
self.assertEqual(logs.count(), 2, "Should have 2 log entries")
self.assertEqual(logs[0].state, 'CLOSED_TEMP')
self.assertEqual(logs[0].by, self.user)
self.assertEqual(logs[1].state, 'CLOSED_PERM')
self.assertEqual(logs[1].by, self.moderator)
def test_full_lifecycle_logged(self):
"""Test complete park lifecycle is logged."""
from django_fsm_log.models import StateLog
from django.contrib.contenttypes.models import ContentType
park = self._create_park(status='OPERATING')
park_ct = ContentType.objects.get_for_model(park)
# Full lifecycle: OPERATING -> CLOSED_TEMP -> OPERATING -> CLOSED_PERM -> DEMOLISHED
park.transition_to_closed_temp(user=self.user)
park.save()
park.transition_to_operating(user=self.user)
park.save()
park.transition_to_closed_perm(user=self.moderator)
park.save()
park.transition_to_demolished(user=self.moderator)
park.save()
# Check all logs created
logs = StateLog.objects.filter(
content_type=park_ct,
object_id=park.id
).order_by('timestamp')
self.assertEqual(logs.count(), 4, "Should have 4 log entries")
states = [log.state for log in logs]
self.assertEqual(states, ['CLOSED_TEMP', 'OPERATING', 'CLOSED_PERM', 'DEMOLISHED'])