mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 10:31:09 -05:00
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:
660
docs/api/state_transitions.md
Normal file
660
docs/api/state_transitions.md
Normal file
@@ -0,0 +1,660 @@
|
||||
# State Transition API Endpoints
|
||||
|
||||
This document describes the API endpoints for performing state transitions on various models in ThrillWiki.
|
||||
|
||||
## Overview
|
||||
|
||||
State transitions are performed via POST requests to specific action endpoints. All transition endpoints:
|
||||
|
||||
- Require authentication
|
||||
- Return the updated object on success
|
||||
- Return appropriate error codes on failure
|
||||
- Log the transition in the StateLog
|
||||
|
||||
## Authentication
|
||||
|
||||
All endpoints require a valid JWT token in the Authorization header:
|
||||
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
## Error Responses
|
||||
|
||||
| Status Code | Meaning |
|
||||
|-------------|---------|
|
||||
| 400 | Invalid transition (state not allowed) |
|
||||
| 401 | Not authenticated |
|
||||
| 403 | Permission denied (insufficient role) |
|
||||
| 404 | Object not found |
|
||||
| 422 | Validation error (missing required fields) |
|
||||
|
||||
---
|
||||
|
||||
## EditSubmission Transitions
|
||||
|
||||
### Approve Submission
|
||||
|
||||
Approve an edit submission and apply changes to the target object.
|
||||
|
||||
```http
|
||||
POST /api/moderation/submissions/{id}/approve/
|
||||
```
|
||||
|
||||
**Permissions**: MODERATOR or above
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"notes": "Optional approval notes"
|
||||
}
|
||||
```
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"status": "APPROVED",
|
||||
"handled_by": {
|
||||
"id": 456,
|
||||
"username": "moderator"
|
||||
},
|
||||
"handled_at": "2025-01-15T10:30:00Z",
|
||||
"notes": "Looks good, approved"
|
||||
}
|
||||
```
|
||||
|
||||
### Reject Submission
|
||||
|
||||
Reject an edit submission with a reason.
|
||||
|
||||
```http
|
||||
POST /api/moderation/submissions/{id}/reject/
|
||||
```
|
||||
|
||||
**Permissions**: MODERATOR or above
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"reason": "Required rejection reason"
|
||||
}
|
||||
```
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"status": "REJECTED",
|
||||
"handled_by": {
|
||||
"id": 456,
|
||||
"username": "moderator"
|
||||
},
|
||||
"handled_at": "2025-01-15T10:30:00Z",
|
||||
"notes": "Rejected: Insufficient evidence"
|
||||
}
|
||||
```
|
||||
|
||||
### Escalate Submission
|
||||
|
||||
Escalate a submission to admin review.
|
||||
|
||||
```http
|
||||
POST /api/moderation/submissions/{id}/escalate/
|
||||
```
|
||||
|
||||
**Permissions**: MODERATOR or above
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"reason": "Reason for escalation"
|
||||
}
|
||||
```
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"status": "ESCALATED",
|
||||
"handled_by": {
|
||||
"id": 456,
|
||||
"username": "moderator"
|
||||
},
|
||||
"handled_at": "2025-01-15T10:30:00Z",
|
||||
"notes": "Escalated: Needs admin approval for sensitive change"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ModerationReport Transitions
|
||||
|
||||
### Start Review
|
||||
|
||||
Claim a report and start reviewing it.
|
||||
|
||||
```http
|
||||
POST /api/moderation/reports/{id}/start_review/
|
||||
```
|
||||
|
||||
**Permissions**: MODERATOR or above
|
||||
|
||||
**Request Body**: None
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"status": "UNDER_REVIEW",
|
||||
"assigned_moderator": {
|
||||
"id": 456,
|
||||
"username": "moderator"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Resolve Report
|
||||
|
||||
Mark a report as resolved.
|
||||
|
||||
```http
|
||||
POST /api/moderation/reports/{id}/resolve/
|
||||
```
|
||||
|
||||
**Permissions**: MODERATOR or above (must be assigned)
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"resolution_action": "CONTENT_REMOVED",
|
||||
"resolution_notes": "Description of action taken"
|
||||
}
|
||||
```
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"status": "RESOLVED",
|
||||
"resolution_action": "CONTENT_REMOVED",
|
||||
"resolution_notes": "Description of action taken",
|
||||
"resolved_at": "2025-01-15T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Dismiss Report
|
||||
|
||||
Dismiss a report as invalid.
|
||||
|
||||
```http
|
||||
POST /api/moderation/reports/{id}/dismiss/
|
||||
```
|
||||
|
||||
**Permissions**: MODERATOR or above (must be assigned)
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"resolution_notes": "Reason for dismissal"
|
||||
}
|
||||
```
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"status": "DISMISSED",
|
||||
"resolution_notes": "Report did not violate guidelines",
|
||||
"resolved_at": "2025-01-15T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Park Transitions
|
||||
|
||||
### Close Temporarily
|
||||
|
||||
Temporarily close a park.
|
||||
|
||||
```http
|
||||
POST /api/parks/{slug}/close_temporarily/
|
||||
```
|
||||
|
||||
**Permissions**: Authenticated user
|
||||
|
||||
**Request Body**: None
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"slug": "example-park",
|
||||
"name": "Example Park",
|
||||
"status": "CLOSED_TEMP"
|
||||
}
|
||||
```
|
||||
|
||||
### Reopen
|
||||
|
||||
Reopen a temporarily closed park.
|
||||
|
||||
```http
|
||||
POST /api/parks/{slug}/reopen/
|
||||
```
|
||||
|
||||
**Permissions**: Authenticated user
|
||||
|
||||
**Request Body**: None
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"slug": "example-park",
|
||||
"name": "Example Park",
|
||||
"status": "OPERATING"
|
||||
}
|
||||
```
|
||||
|
||||
### Close Permanently
|
||||
|
||||
Permanently close a park.
|
||||
|
||||
```http
|
||||
POST /api/parks/{slug}/close_permanently/
|
||||
```
|
||||
|
||||
**Permissions**: MODERATOR or above
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"closing_date": "2025-12-31"
|
||||
}
|
||||
```
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"slug": "example-park",
|
||||
"name": "Example Park",
|
||||
"status": "CLOSED_PERM",
|
||||
"closing_date": "2025-12-31"
|
||||
}
|
||||
```
|
||||
|
||||
### Demolish
|
||||
|
||||
Mark a park as demolished.
|
||||
|
||||
```http
|
||||
POST /api/parks/{slug}/demolish/
|
||||
```
|
||||
|
||||
**Permissions**: MODERATOR or above
|
||||
|
||||
**Request Body**: None
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"slug": "example-park",
|
||||
"name": "Example Park",
|
||||
"status": "DEMOLISHED"
|
||||
}
|
||||
```
|
||||
|
||||
### Relocate
|
||||
|
||||
Mark a park as relocated.
|
||||
|
||||
```http
|
||||
POST /api/parks/{slug}/relocate/
|
||||
```
|
||||
|
||||
**Permissions**: MODERATOR or above
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"new_location_notes": "Optional notes about new location"
|
||||
}
|
||||
```
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"slug": "example-park",
|
||||
"name": "Example Park",
|
||||
"status": "RELOCATED"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ride Transitions
|
||||
|
||||
### Open
|
||||
|
||||
Open a ride (from CLOSED_TEMP, SBNO, or UNDER_CONSTRUCTION).
|
||||
|
||||
```http
|
||||
POST /api/parks/{park_slug}/rides/{ride_slug}/open/
|
||||
```
|
||||
|
||||
**Permissions**: Authenticated user (CLOSED_TEMP), MODERATOR+ (SBNO)
|
||||
|
||||
**Request Body**: None
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"slug": "example-ride",
|
||||
"name": "Example Coaster",
|
||||
"status": "OPERATING"
|
||||
}
|
||||
```
|
||||
|
||||
### Close Temporarily
|
||||
|
||||
Temporarily close a ride.
|
||||
|
||||
```http
|
||||
POST /api/parks/{park_slug}/rides/{ride_slug}/close_temporarily/
|
||||
```
|
||||
|
||||
**Permissions**: Authenticated user
|
||||
|
||||
**Request Body**: None
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"slug": "example-ride",
|
||||
"name": "Example Coaster",
|
||||
"status": "CLOSED_TEMP"
|
||||
}
|
||||
```
|
||||
|
||||
### Mark SBNO
|
||||
|
||||
Mark a ride as Standing But Not Operating.
|
||||
|
||||
```http
|
||||
POST /api/parks/{park_slug}/rides/{ride_slug}/mark_sbno/
|
||||
```
|
||||
|
||||
**Permissions**: MODERATOR or above
|
||||
|
||||
**Request Body**: None
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"slug": "example-ride",
|
||||
"name": "Example Coaster",
|
||||
"status": "SBNO"
|
||||
}
|
||||
```
|
||||
|
||||
### Mark Closing
|
||||
|
||||
Mark a ride as scheduled for closure.
|
||||
|
||||
```http
|
||||
POST /api/parks/{park_slug}/rides/{ride_slug}/mark_closing/
|
||||
```
|
||||
|
||||
**Permissions**: MODERATOR or above
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"closing_date": "2025-12-31",
|
||||
"post_closing_status": "DEMOLISHED"
|
||||
}
|
||||
```
|
||||
|
||||
**Valid post_closing_status values**:
|
||||
- `SBNO` - Will become Standing But Not Operating
|
||||
- `CLOSED_PERM` - Will be permanently closed
|
||||
- `DEMOLISHED` - Will be demolished
|
||||
- `RELOCATED` - Will be relocated
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"slug": "example-ride",
|
||||
"name": "Example Coaster",
|
||||
"status": "CLOSING",
|
||||
"closing_date": "2025-12-31",
|
||||
"post_closing_status": "DEMOLISHED"
|
||||
}
|
||||
```
|
||||
|
||||
### Close Permanently
|
||||
|
||||
Permanently close a ride.
|
||||
|
||||
```http
|
||||
POST /api/parks/{park_slug}/rides/{ride_slug}/close_permanently/
|
||||
```
|
||||
|
||||
**Permissions**: MODERATOR or above
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"closing_date": "2025-12-31"
|
||||
}
|
||||
```
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"slug": "example-ride",
|
||||
"name": "Example Coaster",
|
||||
"status": "CLOSED_PERM",
|
||||
"closing_date": "2025-12-31"
|
||||
}
|
||||
```
|
||||
|
||||
### Demolish
|
||||
|
||||
Mark a ride as demolished.
|
||||
|
||||
```http
|
||||
POST /api/parks/{park_slug}/rides/{ride_slug}/demolish/
|
||||
```
|
||||
|
||||
**Permissions**: MODERATOR or above
|
||||
|
||||
**Request Body**: None
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"slug": "example-ride",
|
||||
"name": "Example Coaster",
|
||||
"status": "DEMOLISHED"
|
||||
}
|
||||
```
|
||||
|
||||
### Relocate
|
||||
|
||||
Mark a ride as relocated.
|
||||
|
||||
```http
|
||||
POST /api/parks/{park_slug}/rides/{ride_slug}/relocate/
|
||||
```
|
||||
|
||||
**Permissions**: MODERATOR or above
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"new_park_slug": "destination-park",
|
||||
"notes": "Optional relocation notes"
|
||||
}
|
||||
```
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"slug": "example-ride",
|
||||
"name": "Example Coaster",
|
||||
"status": "RELOCATED"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Transition History
|
||||
|
||||
### Get Object History
|
||||
|
||||
Get the state transition history for any object.
|
||||
|
||||
```http
|
||||
GET /api/moderation/history/{content_type}/{object_id}/
|
||||
```
|
||||
|
||||
**Permissions**: MODERATOR or above
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"count": 3,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"timestamp": "2025-01-10T08:00:00Z",
|
||||
"source_state": "PENDING",
|
||||
"state": "ESCALATED",
|
||||
"transition": "transition_to_escalated",
|
||||
"by": {
|
||||
"id": 456,
|
||||
"username": "moderator"
|
||||
},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"timestamp": "2025-01-15T10:30:00Z",
|
||||
"source_state": "ESCALATED",
|
||||
"state": "APPROVED",
|
||||
"transition": "transition_to_approved",
|
||||
"by": {
|
||||
"id": 789,
|
||||
"username": "admin"
|
||||
},
|
||||
"description": "Approved after escalation review"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Get All History (Admin)
|
||||
|
||||
Get all recent transition history across all models.
|
||||
|
||||
```http
|
||||
GET /api/moderation/reports/all_history/
|
||||
```
|
||||
|
||||
**Permissions**: ADMIN or above
|
||||
|
||||
**Query Parameters**:
|
||||
- `page` - Page number (default: 1)
|
||||
- `page_size` - Items per page (default: 20)
|
||||
- `model` - Filter by model name (optional)
|
||||
- `user` - Filter by user ID (optional)
|
||||
- `from_date` - Filter from date (optional)
|
||||
- `to_date` - Filter to date (optional)
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"count": 100,
|
||||
"next": "/api/moderation/reports/all_history/?page=2",
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"content_type": "moderation.editsubmission",
|
||||
"object_id": 123,
|
||||
"timestamp": "2025-01-15T10:30:00Z",
|
||||
"source_state": "PENDING",
|
||||
"state": "APPROVED",
|
||||
"transition": "transition_to_approved",
|
||||
"by": {
|
||||
"id": 456,
|
||||
"username": "moderator"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mandatory API Rules
|
||||
|
||||
1. **Trailing Slashes**: All endpoints MUST include trailing forward slashes
|
||||
2. **HTTP Methods**: All transitions use POST
|
||||
3. **Authentication**: All endpoints require valid JWT token
|
||||
4. **Content-Type**: Request bodies must be `application/json`
|
||||
|
||||
## Error Response Format
|
||||
|
||||
All error responses follow this format:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Error code",
|
||||
"message": "Human-readable error message",
|
||||
"details": {
|
||||
"field_name": ["Specific field errors"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Error Responses
|
||||
|
||||
**Invalid Transition (400)**:
|
||||
```json
|
||||
{
|
||||
"error": "TRANSITION_NOT_ALLOWED",
|
||||
"message": "Cannot transition from APPROVED to REJECTED"
|
||||
}
|
||||
```
|
||||
|
||||
**Permission Denied (403)**:
|
||||
```json
|
||||
{
|
||||
"error": "PERMISSION_DENIED",
|
||||
"message": "This action requires moderator privileges"
|
||||
}
|
||||
```
|
||||
|
||||
**Validation Error (422)**:
|
||||
```json
|
||||
{
|
||||
"error": "VALIDATION_ERROR",
|
||||
"message": "Missing required fields",
|
||||
"details": {
|
||||
"post_closing_status": ["This field is required when marking as CLOSING"]
|
||||
}
|
||||
}
|
||||
418
docs/state_machines/README.md
Normal file
418
docs/state_machines/README.md
Normal file
@@ -0,0 +1,418 @@
|
||||
# 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
|
||||
383
docs/state_machines/diagrams.md
Normal file
383
docs/state_machines/diagrams.md
Normal file
@@ -0,0 +1,383 @@
|
||||
# State Machine Diagrams
|
||||
|
||||
This document contains Mermaid state diagrams for all models with state machines in ThrillWiki.
|
||||
|
||||
## EditSubmission / PhotoSubmission States
|
||||
|
||||
User submissions for edits and photos follow the same state flow.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> PENDING: User submits
|
||||
|
||||
PENDING --> APPROVED: Moderator approves
|
||||
PENDING --> REJECTED: Moderator rejects
|
||||
PENDING --> ESCALATED: Moderator escalates
|
||||
|
||||
ESCALATED --> APPROVED: Admin approves
|
||||
ESCALATED --> REJECTED: Admin rejects
|
||||
|
||||
APPROVED --> [*]
|
||||
REJECTED --> [*]
|
||||
|
||||
note right of PENDING
|
||||
Guards: None (any authenticated user)
|
||||
Callbacks: None
|
||||
end note
|
||||
|
||||
note right of APPROVED
|
||||
Guards: PermissionGuard (moderator+)
|
||||
Callbacks:
|
||||
- Apply changes to target object
|
||||
- Send approval notification
|
||||
- Invalidate cache
|
||||
end note
|
||||
|
||||
note right of REJECTED
|
||||
Guards: PermissionGuard (moderator+)
|
||||
Callbacks:
|
||||
- Send rejection notification with reason
|
||||
end note
|
||||
|
||||
note right of ESCALATED
|
||||
Guards: PermissionGuard (moderator+)
|
||||
Callbacks:
|
||||
- Notify admins of escalation
|
||||
- Update escalation_level
|
||||
end note
|
||||
```
|
||||
|
||||
### Transition Matrix
|
||||
|
||||
| From State | To State | Required Role | Guard |
|
||||
|------------|----------|---------------|-------|
|
||||
| PENDING | APPROVED | MODERATOR+ | PermissionGuard |
|
||||
| PENDING | REJECTED | MODERATOR+ | PermissionGuard |
|
||||
| PENDING | ESCALATED | MODERATOR+ | PermissionGuard |
|
||||
| ESCALATED | APPROVED | ADMIN+ | PermissionGuard |
|
||||
| ESCALATED | REJECTED | ADMIN+ | PermissionGuard |
|
||||
|
||||
## ModerationReport States
|
||||
|
||||
Content reports from users follow a review workflow.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> PENDING: User reports content
|
||||
|
||||
PENDING --> UNDER_REVIEW: Moderator claims report
|
||||
|
||||
UNDER_REVIEW --> RESOLVED: Issue fixed
|
||||
UNDER_REVIEW --> DISMISSED: Invalid report
|
||||
|
||||
RESOLVED --> [*]
|
||||
DISMISSED --> [*]
|
||||
|
||||
note right of PENDING
|
||||
Priority: LOW, MEDIUM, HIGH, CRITICAL
|
||||
Auto-assigned based on report type
|
||||
end note
|
||||
|
||||
note right of UNDER_REVIEW
|
||||
Guards: PermissionGuard (moderator+)
|
||||
Sets: assigned_moderator
|
||||
Callbacks: Update queue counts
|
||||
end note
|
||||
|
||||
note right of RESOLVED
|
||||
Guards: AssignmentGuard (assigned moderator)
|
||||
Sets: resolved_at, resolution_action
|
||||
Callbacks: Notify reporter
|
||||
end note
|
||||
|
||||
note right of DISMISSED
|
||||
Guards: AssignmentGuard (assigned moderator)
|
||||
Sets: resolved_at, resolution_notes
|
||||
Callbacks: Notify reporter
|
||||
end note
|
||||
```
|
||||
|
||||
### Transition Matrix
|
||||
|
||||
| From State | To State | Required Role | Additional Guard |
|
||||
|------------|----------|---------------|------------------|
|
||||
| PENDING | UNDER_REVIEW | MODERATOR+ | None |
|
||||
| UNDER_REVIEW | RESOLVED | MODERATOR+ | AssignmentGuard |
|
||||
| UNDER_REVIEW | DISMISSED | MODERATOR+ | AssignmentGuard |
|
||||
|
||||
## ModerationQueue States
|
||||
|
||||
Queue items for moderator work.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> PENDING: Item created
|
||||
|
||||
PENDING --> IN_PROGRESS: Moderator claims
|
||||
PENDING --> CANCELLED: Cancelled
|
||||
|
||||
IN_PROGRESS --> COMPLETED: Work finished
|
||||
IN_PROGRESS --> CANCELLED: Cancelled
|
||||
|
||||
COMPLETED --> [*]
|
||||
CANCELLED --> [*]
|
||||
|
||||
note right of IN_PROGRESS
|
||||
Guards: PermissionGuard (moderator+)
|
||||
Sets: assigned_to, assigned_at
|
||||
end note
|
||||
|
||||
note right of COMPLETED
|
||||
Guards: AssignmentGuard
|
||||
Sets: completed_at
|
||||
end note
|
||||
```
|
||||
|
||||
## BulkOperation States
|
||||
|
||||
Admin bulk operations with progress tracking.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> PENDING: Operation created
|
||||
|
||||
PENDING --> RUNNING: Operation starts
|
||||
PENDING --> CANCELLED: Admin cancels
|
||||
|
||||
RUNNING --> COMPLETED: All items processed
|
||||
RUNNING --> FAILED: Fatal error
|
||||
RUNNING --> CANCELLED: Admin cancels (if cancellable)
|
||||
|
||||
COMPLETED --> [*]
|
||||
FAILED --> [*]
|
||||
CANCELLED --> [*]
|
||||
|
||||
note right of PENDING
|
||||
Guards: PermissionGuard (admin+)
|
||||
Fields: total_items, parameters
|
||||
end note
|
||||
|
||||
note right of RUNNING
|
||||
Guards: PermissionGuard (admin+)
|
||||
Sets: started_at
|
||||
Progress: processed_items / total_items
|
||||
end note
|
||||
|
||||
note right of COMPLETED
|
||||
Sets: completed_at
|
||||
Fields: processed_items, results
|
||||
end note
|
||||
|
||||
note right of FAILED
|
||||
Sets: completed_at
|
||||
Fields: failed_items, results (error)
|
||||
end note
|
||||
```
|
||||
|
||||
### Transition Matrix
|
||||
|
||||
| From State | To State | Required Role | Condition |
|
||||
|------------|----------|---------------|-----------|
|
||||
| PENDING | RUNNING | ADMIN+ | None |
|
||||
| PENDING | CANCELLED | ADMIN+ | None |
|
||||
| RUNNING | COMPLETED | ADMIN+ | None |
|
||||
| RUNNING | FAILED | ADMIN+ | None |
|
||||
| RUNNING | CANCELLED | ADMIN+ | can_cancel=True |
|
||||
|
||||
## Park Status States
|
||||
|
||||
Park lifecycle management.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> UNDER_CONSTRUCTION: New park announced
|
||||
[*] --> OPERATING: Existing park
|
||||
|
||||
UNDER_CONSTRUCTION --> OPERATING: Grand opening
|
||||
|
||||
OPERATING --> CLOSED_TEMP: Seasonal/temporary closure
|
||||
OPERATING --> CLOSED_PERM: Permanent closure
|
||||
|
||||
CLOSED_TEMP --> OPERATING: Reopens
|
||||
CLOSED_TEMP --> CLOSED_PERM: Becomes permanent
|
||||
|
||||
CLOSED_PERM --> DEMOLISHED: Site cleared
|
||||
CLOSED_PERM --> RELOCATED: Moved to new location
|
||||
|
||||
DEMOLISHED --> [*]
|
||||
RELOCATED --> [*]
|
||||
|
||||
note right of OPERATING
|
||||
Default state for active parks
|
||||
Guards: Any authenticated user
|
||||
end note
|
||||
|
||||
note right of CLOSED_TEMP
|
||||
Seasonal closures, maintenance
|
||||
Guards: Any authenticated user
|
||||
end note
|
||||
|
||||
note right of CLOSED_PERM
|
||||
Guards: PermissionGuard (moderator+)
|
||||
Sets: closing_date
|
||||
Callbacks: Update ride statuses
|
||||
end note
|
||||
|
||||
note right of DEMOLISHED
|
||||
Guards: PermissionGuard (moderator+)
|
||||
Final state - no transitions out
|
||||
end note
|
||||
|
||||
note right of RELOCATED
|
||||
Guards: PermissionGuard (moderator+)
|
||||
Final state - link to new location
|
||||
end note
|
||||
```
|
||||
|
||||
### Transition Matrix
|
||||
|
||||
| From State | To State | Required Role | Sets |
|
||||
|------------|----------|---------------|------|
|
||||
| UNDER_CONSTRUCTION | OPERATING | USER+ | None |
|
||||
| OPERATING | CLOSED_TEMP | USER+ | None |
|
||||
| OPERATING | CLOSED_PERM | MODERATOR+ | closing_date |
|
||||
| CLOSED_TEMP | OPERATING | USER+ | None |
|
||||
| CLOSED_TEMP | CLOSED_PERM | MODERATOR+ | closing_date |
|
||||
| CLOSED_PERM | DEMOLISHED | MODERATOR+ | None |
|
||||
| CLOSED_PERM | RELOCATED | MODERATOR+ | None |
|
||||
|
||||
## Ride Status States
|
||||
|
||||
Ride lifecycle with scheduled closures.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> UNDER_CONSTRUCTION: New ride announced
|
||||
[*] --> OPERATING: Existing ride
|
||||
|
||||
UNDER_CONSTRUCTION --> OPERATING: Grand opening
|
||||
|
||||
OPERATING --> CLOSED_TEMP: Maintenance/refurb
|
||||
OPERATING --> SBNO: Extended closure
|
||||
OPERATING --> CLOSING: Scheduled closure
|
||||
|
||||
CLOSED_TEMP --> OPERATING: Reopens
|
||||
CLOSED_TEMP --> SBNO: Extended to SBNO
|
||||
CLOSED_TEMP --> CLOSED_PERM: Permanent closure
|
||||
|
||||
SBNO --> OPERATING: Revival
|
||||
SBNO --> CLOSED_PERM: Confirmed closure
|
||||
|
||||
CLOSING --> SBNO: Becomes SBNO
|
||||
CLOSING --> CLOSED_PERM: Closure date reached
|
||||
|
||||
CLOSED_PERM --> DEMOLISHED: Removed
|
||||
CLOSED_PERM --> RELOCATED: Moved
|
||||
|
||||
DEMOLISHED --> [*]
|
||||
RELOCATED --> [*]
|
||||
|
||||
note right of OPERATING
|
||||
Active ride
|
||||
Guards: Any authenticated user
|
||||
end note
|
||||
|
||||
note right of CLOSED_TEMP
|
||||
Short-term closure
|
||||
Guards: Any authenticated user
|
||||
end note
|
||||
|
||||
note right of SBNO
|
||||
Standing But Not Operating
|
||||
Guards: PermissionGuard (moderator+)
|
||||
Long-term uncertainty
|
||||
end note
|
||||
|
||||
note right of CLOSING
|
||||
Scheduled to close
|
||||
Guards: PermissionGuard (moderator+)
|
||||
Requires: closing_date, post_closing_status
|
||||
Automated transition on date
|
||||
end note
|
||||
|
||||
note right of CLOSED_PERM
|
||||
Guards: PermissionGuard (moderator+)
|
||||
Sets: closing_date
|
||||
end note
|
||||
|
||||
note right of DEMOLISHED
|
||||
Guards: PermissionGuard (moderator+)
|
||||
Final state
|
||||
end note
|
||||
|
||||
note right of RELOCATED
|
||||
Guards: PermissionGuard (moderator+)
|
||||
Final state - link to new installation
|
||||
end note
|
||||
```
|
||||
|
||||
### CLOSING State Automation
|
||||
|
||||
The CLOSING state is special - it represents a ride that has been announced to close on a specific date. When the `closing_date` is reached, the ride automatically transitions to the `post_closing_status` (SBNO, CLOSED_PERM, DEMOLISHED, or RELOCATED).
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant Ride
|
||||
participant Scheduler
|
||||
|
||||
User->>Ride: mark_closing(closing_date, post_closing_status)
|
||||
Ride->>Ride: transition_to_closing()
|
||||
Ride->>Ride: Set closing_date, post_closing_status
|
||||
Ride->>Ride: Save
|
||||
|
||||
Note over Scheduler: Daily job runs
|
||||
|
||||
Scheduler->>Ride: Check closing_date <= today
|
||||
alt Date reached
|
||||
Scheduler->>Ride: apply_post_closing_status()
|
||||
Ride->>Ride: Transition to post_closing_status
|
||||
Ride->>Ride: Save
|
||||
end
|
||||
```
|
||||
|
||||
### Transition Matrix
|
||||
|
||||
| From State | To State | Required Role | Sets |
|
||||
|------------|----------|---------------|------|
|
||||
| UNDER_CONSTRUCTION | OPERATING | USER+ | None |
|
||||
| OPERATING | CLOSED_TEMP | USER+ | None |
|
||||
| OPERATING | SBNO | MODERATOR+ | None |
|
||||
| OPERATING | CLOSING | MODERATOR+ | closing_date, post_closing_status |
|
||||
| CLOSED_TEMP | OPERATING | USER+ | None |
|
||||
| CLOSED_TEMP | SBNO | MODERATOR+ | None |
|
||||
| CLOSED_TEMP | CLOSED_PERM | MODERATOR+ | closing_date |
|
||||
| SBNO | OPERATING | MODERATOR+ | None |
|
||||
| SBNO | CLOSED_PERM | MODERATOR+ | None |
|
||||
| CLOSING | SBNO | MODERATOR+ | None |
|
||||
| CLOSING | CLOSED_PERM | MODERATOR+ | None |
|
||||
| CLOSED_PERM | DEMOLISHED | MODERATOR+ | None |
|
||||
| CLOSED_PERM | RELOCATED | MODERATOR+ | None |
|
||||
|
||||
## State Color Legend
|
||||
|
||||
All state machines use consistent colors for states:
|
||||
|
||||
| Color | Meaning | Example States |
|
||||
|-------|---------|----------------|
|
||||
| 🟡 Yellow | Pending/Waiting | PENDING, UNDER_REVIEW, CLOSING |
|
||||
| 🟢 Green | Active/Approved | OPERATING, APPROVED, COMPLETED |
|
||||
| 🔴 Red | Closed/Rejected | REJECTED, FAILED, CLOSED_PERM |
|
||||
| 🟠 Orange | Warning/SBNO | SBNO, ESCALATED, IN_PROGRESS |
|
||||
| ⚫ Gray | Final/Terminal | DEMOLISHED, RELOCATED, CANCELLED |
|
||||
| 🔵 Blue | Temporary | CLOSED_TEMP, UNDER_CONSTRUCTION |
|
||||
|
||||
## Guard Icons
|
||||
|
||||
| Icon | Guard Type | Description |
|
||||
|------|-----------|-------------|
|
||||
| 🔐 | PermissionGuard | Role-based access |
|
||||
| 👤 | OwnershipGuard | Owner verification |
|
||||
| 📋 | AssignmentGuard | Assigned user check |
|
||||
| 📊 | StateGuard | State validation |
|
||||
| 📝 | MetadataGuard | Required fields |
|
||||
1060
docs/state_machines/examples.md
Normal file
1060
docs/state_machines/examples.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user