Skip to main content

Django FSM RX - Remanufactured Finite State Machine

PyPI version Documentation CI tests codecov MIT License

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:

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:

  1. Combining features - Admin, logging, and core FSM in one package
  2. Independent governance - Not tied to any organization's processes
  3. 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.

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_success callback - Runs inside transaction, rolls back together
  • on_commit callback - Runs after commit (for emails, external APIs)
  • atomic=True default - Transitions wrapped in transaction.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:

  1. Creates the new FSMTransitionLog table
  2. Automatically copies all data from django_fsm_log_statelog to the new table
  3. Does NOT delete the old table - your original data remains safe

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:

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)

Source distribution for django-fsm-rx 5.3.0
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)

Table of built distributions (wheels) for django-fsm-rx 5.3.0
File Interpreter ABI Platform Actions
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page