Django friendly finite state machine support for models through FSMField and the @transition decorator.
—
Decorator for marking methods as state transitions with comprehensive configuration options and utility functions for checking transition availability and permissions.
The core decorator that marks model methods as state transitions, enabling declarative finite state machine behavior.
def transition(field,
source="*",
target=None,
on_error=None,
conditions=[],
permission=None,
custom={}):
"""
Method decorator to mark allowed state transitions.
Parameters:
- field: FSM field instance or field name string
- source: Source state(s) - string, list, tuple, set, "*" (any), or "+" (any except target)
- target: Target state, State instance (GET_STATE, RETURN_VALUE), or None for validation-only
- on_error: State to transition to if method raises exception
- conditions: List of functions that must return True for transition to proceed
- permission: Permission string or callable for authorization checking
- custom: Dictionary of custom metadata for the transition
Returns:
Decorated method that triggers state transitions when called
"""Simple state transitions with single source and target states:
from django.db import models
from django_fsm import FSMField, transition
class Order(models.Model):
state = FSMField(default='pending')
@transition(field=state, source='pending', target='confirmed')
def confirm(self):
# Business logic here
self.confirmed_at = timezone.now()
@transition(field=state, source='confirmed', target='shipped')
def ship(self):
# Generate tracking number, notify customer, etc.
passTransitions can be triggered from multiple source states:
class BlogPost(models.Model):
state = FSMField(default='draft')
@transition(field=state, source=['draft', 'review'], target='published')
def publish(self):
self.published_at = timezone.now()
@transition(field=state, source=['draft', 'published'], target='archived')
def archive(self):
passUse special source values for flexible transitions:
class Document(models.Model):
state = FSMField(default='new')
# Can be called from any state
@transition(field=state, source='*', target='error')
def mark_error(self):
pass
# Can be called from any state except 'deleted'
@transition(field=state, source='+', target='deleted')
def delete(self):
passAdd conditions that must be met before transition can proceed:
def can_publish(instance):
return instance.content and instance.title
def has_approval(instance):
return instance.approved_by is not None
class Article(models.Model):
state = FSMField(default='draft')
content = models.TextField()
title = models.CharField(max_length=200)
approved_by = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
@transition(
field=state,
source='draft',
target='published',
conditions=[can_publish, has_approval]
)
def publish(self):
self.published_at = timezone.now()Control access to transitions using Django's permission system:
class Document(models.Model):
state = FSMField(default='draft')
# String permission
@transition(
field=state,
source='review',
target='approved',
permission='myapp.can_approve_document'
)
def approve(self):
pass
# Callable permission
def can_reject(instance, user):
return user.is_staff or user == instance.author
@transition(
field=state,
source='review',
target='rejected',
permission=can_reject
)
def reject(self):
passSpecify fallback states when transitions raise exceptions:
class PaymentOrder(models.Model):
state = FSMField(default='pending')
@transition(
field=state,
source='pending',
target='paid',
on_error='failed'
)
def process_payment(self):
# If this raises an exception, state becomes 'failed'
result = payment_gateway.charge(self.amount)
if not result.success:
raise PaymentException("Payment failed")Set target to None to validate state without changing it:
class Contract(models.Model):
state = FSMField(default='draft')
@transition(field=state, source='draft', target=None)
def validate_draft(self):
# Perform validation logic
# State remains 'draft' if successful
if not self.is_valid():
raise ValidationError("Contract is invalid")Store additional metadata with transitions:
class Workflow(models.Model):
state = FSMField(default='new')
@transition(
field=state,
source='new',
target='processing',
custom={
'priority': 'high',
'requires_notification': True,
'estimated_time': 300
}
)
def start_processing(self):
passThe Transition class represents individual state transitions and provides metadata and permission checking capabilities.
class Transition(object):
def __init__(self, method, source, target, on_error, conditions, permission, custom):
"""
Represents a single state machine transition.
Parameters:
- method: The transition method
- source: Source state for the transition
- target: Target state for the transition
- on_error: Error state (optional)
- conditions: List of condition functions
- permission: Permission string or callable
- custom: Custom metadata dictionary
Attributes:
- name: Name of the transition method
- source: Source state
- target: Target state
- on_error: Error fallback state
- conditions: Transition conditions
- permission: Permission configuration
- custom: Custom transition metadata
"""
@property
def name(self):
"""Return the name of the transition method."""
def has_perm(self, instance, user):
"""
Check if user has permission to execute this transition.
Parameters:
- instance: Model instance
- user: User to check permissions for
Returns:
bool: True if user has permission
"""Transition objects are created automatically by the @transition decorator and can be accessed through various utility functions:
from django_fsm import FSMField, transition
class Order(models.Model):
state = FSMField(default='pending')
@transition(field=state, source='pending', target='confirmed')
def confirm(self):
pass
# Access transition objects
order = Order()
transitions = order.get_available_state_transitions()
for transition in transitions:
print(f"Transition: {transition.name}")
print(f"From {transition.source} to {transition.target}")
print(f"Custom metadata: {transition.custom}")The FSMMeta class manages transition metadata for individual methods, storing all configured transitions and their conditions.
class FSMMeta(object):
def __init__(self, field, method):
"""
Initialize FSM metadata for a transition method.
Parameters:
- field: FSM field instance
- method: Transition method
Attributes:
- field: Associated FSM field
- transitions: Dictionary mapping source states to Transition objects
"""
def add_transition(self, method, source, target, on_error=None, conditions=[], permission=None, custom={}):
"""
Add a transition configuration to this method.
Parameters:
- method: Transition method
- source: Source state
- target: Target state
- on_error: Error state (optional)
- conditions: List of condition functions
- permission: Permission string or callable
- custom: Custom metadata dictionary
"""
def has_transition(self, state):
"""
Check if any transition exists from the given state.
Parameters:
- state: Current state to check
Returns:
bool: True if transition is possible
"""
def conditions_met(self, instance, state):
"""
Check if all transition conditions are satisfied.
Parameters:
- instance: Model instance
- state: Current state
Returns:
bool: True if all conditions are met
"""
def has_transition_perm(self, instance, state, user):
"""
Check if user has permission for transition from given state.
Parameters:
- instance: Model instance
- state: Current state
- user: User to check permissions for
Returns:
bool: True if user has permission
"""Access FSMMeta through transition methods:
# Access FSM metadata
order = Order()
meta = order.confirm._django_fsm # FSMMeta instance
# Check transition availability
if meta.has_transition(order.state):
print("Confirm transition is available")
# Check conditions
if meta.conditions_met(order, order.state):
print("All conditions are satisfied")Check if a transition method can be called from the current state:
def can_proceed(bound_method, check_conditions=True):
"""
Returns True if model state allows calling the bound transition method.
Parameters:
- bound_method: Bound method with @transition decorator
- check_conditions: Whether to check transition conditions (default: True)
Returns:
bool: True if transition is allowed
Raises:
TypeError: If method is not a transition
"""Usage example:
from django_fsm import can_proceed
order = Order.objects.get(pk=1)
# Check without conditions
if can_proceed(order.ship, check_conditions=False):
print("Ship transition is possible from current state")
# Check with conditions
if can_proceed(order.ship):
order.ship()
order.save()
else:
print("Cannot ship order yet")Check if a user has permission to execute a transition:
def has_transition_perm(bound_method, user):
"""
Returns True if model state allows calling method and user has permission.
Parameters:
- bound_method: Bound method with @transition decorator
- user: User instance to check permissions for
Returns:
bool: True if user can execute transition
Raises:
TypeError: If method is not a transition
"""Usage example:
from django_fsm import has_transition_perm
def process_approval(request, document_id):
document = Document.objects.get(pk=document_id)
if has_transition_perm(document.approve, request.user):
document.approve()
document.save()
return redirect('success')
else:
return HttpResponseForbidden("You don't have permission to approve")Different fields can have their own independent state machines:
class Order(models.Model):
payment_state = FSMField(default='unpaid')
fulfillment_state = FSMField(default='pending')
@transition(field=payment_state, source='unpaid', target='paid')
def process_payment(self):
pass
@transition(field=fulfillment_state, source='pending', target='shipped')
def ship_order(self):
passTransitions can trigger other transitions:
class Order(models.Model):
state = FSMField(default='new')
@transition(field=state, source='new', target='processing')
def start_processing(self):
pass
@transition(field=state, source='processing', target='completed')
def complete(self):
# Automatically trigger another action
self.notify_customer()
def notify_customer(self):
# Send notification logic
passUse current state to determine method behavior:
class Document(models.Model):
state = FSMField(default='draft')
def save(self, *args, **kwargs):
if self.state == 'published':
# Extra validation for published documents
self.full_clean()
super().save(*args, **kwargs)
@transition(field=state, source='draft', target='published')
def publish(self):
self.published_at = timezone.now()Install with Tessl CLI
npx tessl i tessl/pypi-django-fsm