Django FSM RX Documentation¶
Django FSM RX - Remanufactured Finite State Machine¶
Django-fsm-rx adds simple declarative state management for Django models.
Full Documentation | PyPI | GitHub
What does RX mean?¶
RX = Remanufactured
In the automotive and mechanic shop world, “RX” commonly denotes a remanufactured part - rebuilt to meet or exceed original specifications, often with improvements. This project follows that philosophy: taking the battle-tested django-fsm codebase and remanufacturing it with modern enhancements.
About This Project¶
Django FSM RX is an independent fork that combines the best features from the django-fsm ecosystem:
Core FSM functionality from the original django-fsm by Mikhail Podgurskiy
Admin integration inspired by django-fsm-admin and django-fsm-2-admin
Transition logging inspired by django-fsm-log
Full type hints for modern Python development
This is a new independent branch, separate from both Django Commons and Jazzband. The goal is to provide a unified, actively maintained package that combines all essential FSM features in one place.
Why a new fork?¶
The original django-fsm was archived after 2 years without releases. While django-fsm-2 under Django Commons continued maintenance, this project takes a different approach by:
Combining features - Admin, logging, and core FSM in one package
Independent governance - Not tied to any organization’s processes
Opinionated defaults - Built for mechanic shop / automotive industry workflows
Installation¶
pip install django-fsm-rx
Add to your Django settings:
INSTALLED_APPS = [
...,
'django_fsm_rx',
...,
]
Then run migrations to create the audit log table:
python manage.py migrate django_fsm_rx
Configuration¶
Django FSM RX works out of the box with sensible defaults. All settings are optional:
# settings.py
DJANGO_FSM_RX = {
'ATOMIC': True, # Wrap transitions in database transactions
'AUDIT_LOG': True, # Enable automatic audit logging
'AUDIT_LOG_MODE': 'transaction', # 'transaction' or 'signal'
'AUDIT_LOG_MODEL': None, # Custom audit log model (dotted path)
'PROTECTED_FIELDS': False, # Default for FSMField protected parameter
}
Settings Reference¶
Setting |
Default |
Description |
|---|---|---|
|
|
Wrap transitions in |
|
|
Automatically log all state transitions to |
|
|
|
|
|
Use a custom model for audit logs (e.g., |
|
|
Default value for |
Audit Log Modes¶
Transaction mode (default, recommended):
DJANGO_FSM_RX = {
'AUDIT_LOG_MODE': 'transaction', # Audit log rolls back if transition fails
}
The audit log is created inside the atomic transaction. If anything fails after the transition, both the state change and the audit log roll back together.
Signal mode:
DJANGO_FSM_RX = {
'AUDIT_LOG_MODE': 'signal', # Audit log persists even if later code fails
}
The audit log is created via the post_transition signal, after the transition completes. Use this if you want audit logs even when subsequent operations fail.
Disabling Features¶
# Disable audit logging entirely
DJANGO_FSM_RX = {
'AUDIT_LOG': False,
}
# Disable atomic transactions (not recommended)
DJANGO_FSM_RX = {
'ATOMIC': False,
}
Custom Audit Log Model¶
To use your own audit log model:
# settings.py
DJANGO_FSM_RX = {
'AUDIT_LOG_MODEL': 'myapp.TransitionLog',
}
# myapp/models.py
from django.db import models
from django.contrib.contenttypes.models import ContentType
class TransitionLog(models.Model):
# Required fields
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.TextField()
transition_name = models.CharField(max_length=255)
source_state = models.CharField(max_length=255)
target_state = models.CharField(max_length=255)
timestamp = models.DateTimeField(auto_now_add=True)
# Add your custom fields
user = models.ForeignKey('auth.User', null=True, on_delete=models.SET_NULL)
notes = models.TextField(blank=True)
Quick Start¶
Option 1: Method on Model¶
Define transitions as methods on your model:
from django.db import models
from django_fsm_rx import FSMField, transition
class RepairOrder(models.Model):
state = FSMField(default='intake')
@transition(field=state, source='intake', target='diagnosis')
def begin_diagnosis(self):
"""Vehicle moved to diagnostic bay."""
pass
@transition(field=state, source='diagnosis', target='awaiting_approval')
def submit_estimate(self):
"""Estimate ready for customer approval."""
pass
@transition(field=state, source='awaiting_approval', target='in_progress')
def approve_repair(self):
"""Customer approved the repair."""
pass
@transition(field=state, source='in_progress', target='complete')
def complete_repair(self):
"""Repair finished, ready for pickup."""
pass
order = RepairOrder()
order.begin_diagnosis()
order.save() # State change is not persisted until save()
Option 2: Decorator with Callbacks¶
Use optional callbacks for side effects like audit logging and notifications:
from django.db import models
from django_fsm_rx import FSMField, transition
def example_log_transition(instance, source, target, **kwargs):
"""Runs immediately - part of the atomic transaction."""
AuditLog.objects.create(
order=instance,
from_state=source,
to_state=target,
)
def example_notify_customer(instance, source, target, **kwargs):
"""Runs after commit - safe for external side effects."""
from django.core.mail import send_mail
send_mail(
subject=f"Your repair order status: {target}",
message=f"Order #{instance.id} is now {target}.",
from_email="shop@example.com",
recipient_list=[instance.customer_email],
)
class RepairOrder(models.Model):
state = FSMField(default='intake')
customer_email = models.EmailField()
@transition(
field=state,
source='in_progress',
target='complete',
on_success=example_log_transition, # default: None
on_commit=example_notify_customer, # default: None
# atomic=True is the default
)
def complete_repair(self):
"""Repair finished, ready for pickup."""
self.completed_at = timezone.now()
self.save()
order = RepairOrder.objects.get(id=1)
order.complete_repair() # Logs audit, then emails customer after commit
All callback parameters are optional - use only what you need:
# Just on_success (for DB operations that should roll back together)
@transition(field=state, source='new', target='done', on_success=example_log_transition)
# Just on_commit (for external notifications after commit)
@transition(field=state, source='new', target='done', on_commit=example_notify_customer)
# Neither (simple state change, still atomic by default)
@transition(field=state, source='new', target='done')
Migration Guide¶
django-fsm-rx provides full backwards compatibility with django-fsm, django-fsm-2, django-fsm-admin, and django-fsm-log. Your existing code will work with deprecation warnings guiding you to update imports.
Quick Migration Check¶
Run the built-in migration check command to find deprecated imports in your project:
python manage.py check_fsm_migration
This scans your codebase and shows exactly what imports need updating:
Files affected: 3
Deprecated imports found: 5
myapp/models.py:
Line 1:
- from django_fsm import FSMField, transition
+ from django_fsm_rx import FSMField, transition
myapp/admin.py:
Line 2:
- from django_fsm_admin.mixins import FSMTransitionMixin
+ from django_fsm_rx.admin import FSMAdminMixin
Additional options:
--path /path/to/scan- Scan a specific directory--exclude migrations,tests- Exclude directories--verbose- Show migration notes--json- Output as JSON
From django-fsm-2 or django-fsm¶
Step 1: Install the new package¶
# Uninstall old package
# django-fsm
pip uninstall django-fsm
# or django-fsm-2
pip uninstall django-fsm-2
# Install new package
pip install django-fsm-rx
Step 2: Update INSTALLED_APPS¶
# settings.py
INSTALLED_APPS = [
...,
'django_fsm_rx',
...,
]
Step 3: Run migrations¶
python manage.py migrate django_fsm_rx
This creates the FSMTransitionLog table for audit logging.
Step 4: Update imports (recommended)¶
Your existing imports will continue to work with a deprecation warning:
# Old (still works, shows deprecation warning)
from django_fsm_2 import FSMField, transition
# New (recommended)
from django_fsm_rx import FSMField, transition
API Compatibility¶
All core APIs from django-fsm-2 are fully compatible:
Feature |
Status |
Notes |
|---|---|---|
|
Identical |
|
|
Compatible |
New optional params: |
|
Identical |
|
|
Identical |
|
|
Identical |
|
|
Identical |
|
Wildcard sources ( |
Identical |
|
Prefix wildcards ( |
New |
Matches |
New Features¶
django-fsm-rx adds these optional features:
Automatic audit logging - All transitions logged to
FSMTransitionLogon_successcallback - Runs inside transaction, rolls back togetheron_commitcallback - Runs after commit (for emails, external APIs)atomic=Truedefault - Transitions wrapped intransaction.atomic()
Opting out of new defaults¶
To get behavior identical to django-fsm-2:
# settings.py
DJANGO_FSM_RX = {
'AUDIT_LOG': False, # Disable audit logging (skip Step 3)
'ATOMIC': False, # Disable transaction wrapping (not recommended)
}
From django-fsm¶
pip uninstall django-fsm
pip install django-fsm-rx
Follow the same steps as “From django-fsm-2” above. Your from django_fsm import ... imports will also continue to work with a deprecation warning.
From django-fsm-log¶
django-fsm-rx includes built-in audit logging that replaces django-fsm-log. Your existing data is automatically migrated when you run migrations.
Step 1: Install django-fsm-rx¶
pip uninstall django-fsm-log
pip install django-fsm-rx
Step 2: Update INSTALLED_APPS¶
# settings.py
INSTALLED_APPS = [
...,
'django_fsm_rx', # This is all you need
# 'django_fsm_log', # Remove - no longer needed
...,
]
Note: You do NOT need to add django_fsm_log to INSTALLED_APPS. The compatibility shim is built into django-fsm-rx.
Step 3: Run migrations¶
python manage.py migrate django_fsm_rx
This:
Creates the new
FSMTransitionLogtableAutomatically copies all data from
django_fsm_log_statelogto the new tableDoes NOT delete the old table - your original data remains safe
Step 4: Update imports (recommended)¶
Your existing imports will continue to work:
# Old (still works via compatibility shim)
from django_fsm_log.models import StateLog
# New (recommended)
from django_fsm_rx import FSMTransitionLog
StateLog is an alias to FSMTransitionLog - they are the same model.
Step 5: Clean up (optional)¶
After verifying migration, you can delete the old table:
-- Only after verifying data migrated correctly!
DROP TABLE IF EXISTS django_fsm_log_statelog;
Decorators¶
The @fsm_log_by and @fsm_log_description decorators are available:
from django_fsm_rx.log import fsm_log_by, fsm_log_description
@fsm_log_by
@fsm_log_description
@transition(field=state, source='draft', target='published')
def publish(self, by=None, description=None):
pass
However, with audit logging enabled (default), you may not need these decorators - transitions are automatically logged.
From django-fsm-admin¶
Step 1: Install django-fsm-rx¶
pip uninstall django-fsm-admin
pip install django-fsm-rx
Step 2: Update imports¶
# Old (still works via compatibility shim with deprecation warning)
from django_fsm_admin.mixins import FSMTransitionMixin
# New (recommended)
from django_fsm_rx.admin import FSMAdminMixin
Note: FSMTransitionMixin is aliased to FSMAdminMixin for backwards compatibility.
Programmatic Migration Utilities¶
For automated migration or CI integration, use the migration utilities programmatically:
from django_fsm_rx.migration import (
scan_imports_in_directory,
validate_model_fsm_compatibility,
get_import_replacements,
)
# Scan a directory for deprecated imports
report = scan_imports_in_directory('/path/to/project')
print(f"Files to update: {len(report.files_affected)}")
for item in report.deprecated_imports:
print(f"{item['file']}:{item['line']}: {item['old']} -> {item['new']}")
# Validate a model's FSM configuration
from myapp.models import Order
warnings = validate_model_fsm_compatibility(Order)
for warning in warnings:
print(f"Warning: {warning}")
# Get all import replacements as a dict
replacements = get_import_replacements()
# {'from django_fsm import FSMField': 'from django_fsm_rx import FSMField', ...}
Documentation¶
For complete documentation, visit django-fsm-rx.readthedocs.io
Topics covered in the full documentation:¶
Configuration - Settings for
ATOMIC,AUDIT_LOG,AUDIT_LOG_MODE, custom modelsBasic Usage - Transitions, conditions, protected fields
Source State Options - Wildcards (
*,+), multiple sourcesDynamic Targets -
RETURN_VALUEandGET_STATEPermissions - String-based and callable permissions
Signals -
pre_transitionandpost_transitionOptimistic Locking -
ConcurrentTransitionMixinField Types -
FSMField,FSMIntegerField,FSMKeyFieldAdmin Integration -
FSMAdminMixin,FSMTransitionLogInline,FSMCascadeWidgetContributing - Development setup, testing, code style
Contributing¶
We welcome contributions! See CONTRIBUTING.md for detailed instructions on:
Development setup with uv or pip
Code style and linting
Type checking with mypy
Pre-commit hooks
Pull request guidelines
Credits¶
Mikhail Podgurskiy - Original django-fsm creator
Django Commons - django-fsm-2 maintenance
Jazzband - Original community support
All contributors to the django-fsm ecosystem
License¶
MIT License - see LICENSE for details.