Django friendly finite state machine support. RX = Remanufactured.
Project description
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_fsm_rx-5.1.12.tar.gz.
File metadata
- Download URL: django_fsm_rx-5.1.12.tar.gz
- Upload date:
- Size: 57.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ab4fe24b4cf07654137e8cb3183785d68281e9a97046a98a78add81f0026e22e
|
|
| MD5 |
c7bee262c08cbfb05c7b0b280bbc12da
|
|
| BLAKE2b-256 |
1c8632d823aa3ba6b0664f3522b7ef0d4de70c601fc6278a5b7ea9a632759aff
|
Provenance
The following attestation bundles were made for django_fsm_rx-5.1.12.tar.gz:
Publisher:
release.yml on specialorange/django-fsm-rx
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_fsm_rx-5.1.12.tar.gz -
Subject digest:
ab4fe24b4cf07654137e8cb3183785d68281e9a97046a98a78add81f0026e22e - Sigstore transparency entry: 1041913527
- Sigstore integration time:
-
Permalink:
specialorange/django-fsm-rx@f3cf8c5344a6afeaec41bf8e2d94ed9cbf4d0b7e -
Branch / Tag:
refs/tags/5.1.12 - Owner: https://github.com/specialorange
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f3cf8c5344a6afeaec41bf8e2d94ed9cbf4d0b7e -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_fsm_rx-5.1.12-py3-none-any.whl.
File metadata
- Download URL: django_fsm_rx-5.1.12-py3-none-any.whl
- Upload date:
- Size: 69.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7d88dab99f94aa9dcc6b2f7de829dd76f2154e78622bb9ba59581bdf98ea52c1
|
|
| MD5 |
7144dc9fd2ca302e4e373739632f4eb6
|
|
| BLAKE2b-256 |
24dcd894bacd1227375a9dc51c4713b45869dac5baaa0348f89d3ccf6ddd8be7
|
Provenance
The following attestation bundles were made for django_fsm_rx-5.1.12-py3-none-any.whl:
Publisher:
release.yml on specialorange/django-fsm-rx
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_fsm_rx-5.1.12-py3-none-any.whl -
Subject digest:
7d88dab99f94aa9dcc6b2f7de829dd76f2154e78622bb9ba59581bdf98ea52c1 - Sigstore transparency entry: 1041913666
- Sigstore integration time:
-
Permalink:
specialorange/django-fsm-rx@f3cf8c5344a6afeaec41bf8e2d94ed9cbf4d0b7e -
Branch / Tag:
refs/tags/5.1.12 - Owner: https://github.com/specialorange
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f3cf8c5344a6afeaec41bf8e2d94ed9cbf4d0b7e -
Trigger Event:
push
-
Statement type: