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 |
|---|---|---|
ATOMIC |
True |
Wrap transitions in transaction.atomic(). Ensures state changes and related DB operations roll back together on failure. |
AUDIT_LOG |
True |
Automatically log all state transitions to FSMTransitionLog. |
AUDIT_LOG_MODE |
'transaction' |
'transaction': Log inside atomic block (rolls back with transition). 'signal': Log via post_transition signal (persists even if later code fails). |
AUDIT_LOG_MODEL |
None |
Use a custom model for audit logs (e.g., 'myapp.TransitionLog'). Must have compatible fields. |
PROTECTED_FIELDS |
False |
Default value for protected parameter on FSMField. When True, direct field assignment raises an exception. |
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 |
|---|---|---|
FSMField, FSMIntegerField, FSMKeyField |
Identical | |
@transition decorator |
Compatible | New optional params: on_success, on_commit, atomic |
can_proceed(), has_transition_perm() |
Identical | |
ConcurrentTransitionMixin, FSMModelMixin |
Identical | |
RETURN_VALUE, GET_STATE |
Identical | |
pre_transition, post_transition signals |
Identical | |
Wildcard sources (*, +) |
Identical | |
Prefix wildcards (WRK-*) |
New | Matches WRK-REP-PRG, WRK-INS-PRG, etc. |
New Features
django-fsm-rx adds these optional features:
- Automatic audit logging - All transitions logged to
FSMTransitionLog on_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
FSMTransitionLogtable - Automatically copies all data from
django_fsm_log_statelogto the new table - Does 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 models - Basic Usage - Transitions, conditions, protected fields
- Source State Options - Wildcards (
*,+), multiple sources - Dynamic Targets -
RETURN_VALUEandGET_STATE - Permissions - String-based and callable permissions
- Signals -
pre_transitionandpost_transition - Optimistic Locking -
ConcurrentTransitionMixin - Field Types -
FSMField,FSMIntegerField,FSMKeyField - Admin Integration -
FSMAdminMixin,FSMTransitionLogInline,FSMCascadeWidget - Contributing - 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.
Metadata
Release files for django-fsm-rx 5.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| django_fsm_rx-5.3.0.tar.gz | 58.0 kB | With provenance | Details for django_fsm_rx-5.3.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| django_fsm_rx-5.3.0-py3-none-any.whl | Python 3 | none | any | Details for django_fsm_rx-5.3.0-py3-none-any.whl |
Total release size: 128.1 kB
Release files / django_fsm_rx-5.3.0.tar.gz
| Download URL | django_fsm_rx-5.3.0.tar.gz |
|---|---|
| Size | 58.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5a10429507d25099584a1cfb59f17c4fad110fc9b325f87415477c228b3ca1e0
|
|
BLAKE2b-256 checksum How to use checksums |
844c37b4c035e47d950db1c77eb66a076abe9ed0ca9088b8f114467d901c73cd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.
Transparency logRelease files / django_fsm_rx-5.3.0-py3-none-any.whl
| Download URL | django_fsm_rx-5.3.0-py3-none-any.whl |
|---|---|
| Size | 70.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7aaff81fdf90135d4176857778cbc3e9c78211c44cc6cbea4e11a20e02d6d567
|
|
BLAKE2b-256 checksum How to use checksums |
2ade739842d850f863ed9a9d92811f525c0c641afd3d3651801eba0a1a971ec5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.
Transparency logSecurity for django-fsm-rx 5.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for django_fsm_rx-5.3.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.