# State Machine System Documentation ## Overview ThrillWiki uses a sophisticated state machine system built on django-fsm integrated with the RichChoice system. This provides: - **Type-safe state transitions** with validation - **Guard-based access control** for transitions - **Callback system** for side effects - **Automatic logging** via django-fsm-log - **RichChoice metadata** for transition rules ## Architecture ``` ┌─────────────────────────────────────────────────────────────┐ │ State Machine Layer │ ├─────────────────────────────────────────────────────────────┤ │ RichFSMField │ StateMachineMixin │ │ - Choice validation │ - Transition methods │ │ - Metadata access │ - State field management │ ├─────────────────────────────────────────────────────────────┤ │ Guards │ Callbacks │ │ - PermissionGuard │ - Pre-transition │ │ - OwnershipGuard │ - Post-transition │ │ - AssignmentGuard │ - Error handlers │ │ - StateGuard │ - Notifications │ │ - MetadataGuard │ - Cache invalidation │ │ - CompositeGuard │ - Related model updates │ ├─────────────────────────────────────────────────────────────┤ │ django-fsm │ django-fsm-log │ │ - @transition │ - StateLog model │ │ - can_proceed() │ - Automatic logging │ └─────────────────────────────────────────────────────────────┘ ``` ## Key Components ### RichFSMField A custom FSM field that integrates with the RichChoice system: ```python from apps.core.state_machine import RichFSMField class Ride(StateMachineMixin, TrackedModel): status = RichFSMField( choice_group="statuses", domain="rides", max_length=20, default="OPERATING" ) ``` ### StateMachineMixin Provides the base functionality for models with state machines: ```python from apps.core.state_machine import StateMachineMixin class EditSubmission(StateMachineMixin, models.Model): state_field_name = "status" # Required attribute # Transition methods are auto-generated from metadata # e.g., transition_to_approved(user=None) ``` ### Guards Guards control who can perform transitions: | Guard | Purpose | |-------|---------| | `PermissionGuard` | Role and permission checks | | `OwnershipGuard` | Verify user owns the object | | `AssignmentGuard` | Verify user is assigned | | `StateGuard` | Validate current state | | `MetadataGuard` | Check required fields | | `CompositeGuard` | Combine guards with AND/OR | ### Callbacks Callbacks execute side effects during transitions: | Callback Type | When Executed | |--------------|---------------| | Pre-transition | Before state change | | Post-transition | After state change | | Error | On transition failure | | Notification | Send emails/alerts | | Cache | Invalidate caches | | Related | Update related models | ## Models with State Machines ### Moderation Domain | Model | States | Description | |-------|--------|-------------| | `EditSubmission` | PENDING → APPROVED/REJECTED/ESCALATED | User edit submissions | | `PhotoSubmission` | PENDING → APPROVED/REJECTED/ESCALATED | Photo submissions | | `ModerationReport` | PENDING → UNDER_REVIEW → RESOLVED/DISMISSED | Content reports | | `ModerationQueue` | PENDING → IN_PROGRESS → COMPLETED/CANCELLED | Queue items | | `BulkOperation` | PENDING → RUNNING → COMPLETED/FAILED/CANCELLED | Bulk actions | ### Parks Domain | Model | States | Description | |-------|--------|-------------| | `Park` | OPERATING/CLOSED_TEMP/CLOSED_PERM/DEMOLISHED/RELOCATED | Park lifecycle | ### Rides Domain | Model | States | Description | |-------|--------|-------------| | `Ride` | OPERATING/CLOSED_TEMP/SBNO/CLOSING/CLOSED_PERM/DEMOLISHED/RELOCATED | Ride lifecycle | ## Transition Metadata RichChoice metadata defines transition behavior: ```python RichChoice( value="PENDING", label="Pending Review", metadata={ 'can_transition_to': ['APPROVED', 'REJECTED', 'ESCALATED'], 'requires_moderator': False, 'requires_admin_approval': False, 'requires_assignment': False, 'is_final': False, 'color': 'yellow', 'icon': 'clock' } ) ``` ### Metadata Fields | Field | Type | Description | |-------|------|-------------| | `can_transition_to` | List[str] | Allowed target states | | `requires_moderator` | bool | Requires MODERATOR role or higher | | `requires_admin_approval` | bool | Requires ADMIN role or higher | | `requires_assignment` | bool | Requires user to be assigned | | `is_final` | bool | Terminal state (no transitions out) | | `zero_tolerance` | bool | Requires SUPERUSER role | | `escalation_level` | str | 'moderator', 'admin', or 'superuser' | ## Adding a New State Machine ### Step 1: Define RichChoice Statuses ```python # apps/myapp/choices.py from apps.core.choices import RichChoice, register_choices WORKFLOW_STATUSES = [ RichChoice( value="DRAFT", label="Draft", metadata={ 'can_transition_to': ['REVIEW', 'CANCELLED'], 'requires_moderator': False, 'is_final': False, } ), RichChoice( value="REVIEW", label="Under Review", metadata={ 'can_transition_to': ['APPROVED', 'REJECTED'], 'requires_moderator': True, } ), RichChoice( value="APPROVED", label="Approved", metadata={ 'can_transition_to': [], 'is_final': True, } ), RichChoice( value="REJECTED", label="Rejected", metadata={ 'can_transition_to': ['DRAFT'], # Can resubmit 'is_final': False, } ), RichChoice( value="CANCELLED", label="Cancelled", metadata={ 'can_transition_to': [], 'is_final': True, } ), ] register_choices('workflow_statuses', 'myapp', WORKFLOW_STATUSES) ``` ### Step 2: Add RichFSMField to Model ```python # apps/myapp/models.py from apps.core.state_machine import RichFSMField, StateMachineMixin from apps.core.models import TrackedModel class Document(StateMachineMixin, TrackedModel): state_field_name = "status" title = models.CharField(max_length=255) content = models.TextField() status = RichFSMField( choice_group="workflow_statuses", domain="myapp", max_length=20, default="DRAFT" ) # Transition methods are auto-generated: # - transition_to_review(user=None) # - transition_to_approved(user=None) # - transition_to_rejected(user=None) # - transition_to_cancelled(user=None) ``` ### Step 3: Add Wrapper Methods (Optional) ```python class Document(StateMachineMixin, TrackedModel): # ... fields ... def submit_for_review(self, user=None): """Submit document for review.""" self.transition_to_review(user=user) self.save() def approve(self, user=None, notes=None): """Approve the document.""" self.transition_to_approved(user=user) if notes: self.approval_notes = notes self.approved_at = timezone.now() self.approved_by = user self.save() ``` ### Step 4: Create Migration ```bash uv run manage.py makemigrations myapp uv run manage.py migrate ``` ### Step 5: Write Tests ```python # apps/myapp/tests.py from django.test import TestCase from django_fsm import TransitionNotAllowed class DocumentTransitionTests(TestCase): def test_draft_to_review_transition(self): doc = Document.objects.create(title='Test', status='DRAFT') doc.transition_to_review(user=self.user) doc.save() self.assertEqual(doc.status, 'REVIEW') def test_approved_cannot_transition(self): doc = Document.objects.create(title='Test', status='APPROVED') with self.assertRaises(TransitionNotAllowed): doc.transition_to_rejected(user=self.moderator) ``` ## Guards Usage ### PermissionGuard ```python from apps.core.state_machine.guards import PermissionGuard # Require moderator role guard = PermissionGuard(requires_moderator=True) # Require admin role guard = PermissionGuard(requires_admin=True) # Require specific roles guard = PermissionGuard(required_roles=['ADMIN', 'SUPERUSER']) # Custom check guard = PermissionGuard( custom_check=lambda instance, user: instance.department == user.department ) ``` ### OwnershipGuard ```python from apps.core.state_machine.guards import OwnershipGuard # Default ownership check (created_by, user, submitted_by) guard = OwnershipGuard() # With moderator override guard = OwnershipGuard(allow_moderator_override=True) # Custom owner field guard = OwnershipGuard(owner_fields=['author']) ``` ### CompositeGuard ```python from apps.core.state_machine.guards import CompositeGuard, PermissionGuard, OwnershipGuard # Require moderator OR owner guard = CompositeGuard([ PermissionGuard(requires_moderator=True), OwnershipGuard() ], operator='OR') # Require moderator AND assigned guard = CompositeGuard([ PermissionGuard(requires_moderator=True), AssignmentGuard() ], operator='AND') ``` ## Callbacks Usage ### Registering Callbacks ```python from apps.core.state_machine.registry import state_machine_registry # Register post-transition callback @state_machine_registry.register_callback('myapp.Document', 'post_transition') def on_document_approved(instance, from_state, to_state, user): if to_state == 'APPROVED': send_approval_notification(instance, user) invalidate_document_cache(instance) ``` ### Notification Callbacks ```python from apps.core.state_machine.callbacks import NotificationCallback class ApprovalNotification(NotificationCallback): def execute(self, context): if context['to_state'] == 'APPROVED': send_email( to=context['instance'].author.email, template='document_approved', context={'document': context['instance']} ) ``` ## Transition Logging All transitions are automatically logged via django-fsm-log: ```python from django_fsm_log.models import StateLog # Get transition history for an instance logs = StateLog.objects.for_instance(document).order_by('timestamp') for log in logs: print(f"{log.timestamp}: {log.source_state} → {log.state} by {log.by}") ``` ## Testing State Machines Use the provided test helpers: ```python from apps.core.state_machine.tests.helpers import ( assert_transition_allowed, assert_transition_denied, assert_state_log_created, transition_and_save ) def test_moderator_can_approve(self): submission = self._create_submission() # Assert transition is allowed assert_transition_allowed(submission, 'transition_to_approved', self.moderator) # Execute and verify transition_and_save(submission, 'transition_to_approved', self.moderator) # Verify log was created assert_state_log_created(submission, 'APPROVED', self.moderator) ``` ## Best Practices 1. **Use wrapper methods** for complex transitions that need additional logic 2. **Define clear metadata** in RichChoice for each state 3. **Test all transition paths** including invalid ones 4. **Use CompositeGuard** for complex permission requirements 5. **Log transitions** for audit trails 6. **Handle callbacks atomically** with the transition ## Related Documentation - [State Diagrams](./diagrams.md) - Visual state diagrams for each model - [Code Examples](./examples.md) - Detailed implementation examples - [API Documentation](../api/state_transitions.md) - API endpoints for transitions