Skip to main content

Django friendly finite state machine support. RX = Remanufactured.

Project description

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.

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_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

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:

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_fsm_rx-5.1.8.tar.gz (56.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_fsm_rx-5.1.8-py3-none-any.whl (68.8 kB view details)

Uploaded Python 3

File details

Details for the file django_fsm_rx-5.1.8.tar.gz.

File metadata

  • Download URL: django_fsm_rx-5.1.8.tar.gz
  • Upload date:
  • Size: 56.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for django_fsm_rx-5.1.8.tar.gz
Algorithm Hash digest
SHA256 c3ddf84476982b75f17042e45554a740ef723861ffc520ddb8faaed4817f4e1e
MD5 e1a3981c58bbfc71a126f7012e32673e
BLAKE2b-256 9b4417e17e6e71d46c400277680ad85921050b3fde4e2a3a77ddc14ddac372d5

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_fsm_rx-5.1.8.tar.gz:

Publisher: release.yml on specialorange/django-fsm-rx

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_fsm_rx-5.1.8-py3-none-any.whl.

File metadata

  • Download URL: django_fsm_rx-5.1.8-py3-none-any.whl
  • Upload date:
  • Size: 68.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for django_fsm_rx-5.1.8-py3-none-any.whl
Algorithm Hash digest
SHA256 9737ef3bf47f235f7400402e7f9fcab9ad9ac647d72d5e4c874393a999416340
MD5 8cd6c0b97a011ecac20abca6196846c3
BLAKE2b-256 530d24182434014a91cd4562a2ff8b7eeb412c6bfae06f780e75d13d0a00a014

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_fsm_rx-5.1.8-py3-none-any.whl:

Publisher: release.yml on specialorange/django-fsm-rx

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page