CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-django-fsm

Django friendly finite state machine support for models through FSMField and the @transition decorator.

Pending
Overview
Eval results
Files

model-mixins.mddocs/

Model Mixins and Advanced Features

Model mixins for enhanced FSM functionality including refresh_from_db support for protected fields and optimistic locking protection against concurrent transitions.

Capabilities

FSMModelMixin

Mixin that allows refresh_from_db for models with fsm protected fields. This mixin ensures that protected FSM fields are properly handled during database refresh operations.

class FSMModelMixin(object):
    def refresh_from_db(self, *args, **kwargs):
        """
        Refresh model instance from database while respecting protected FSM fields.
        
        Protected FSM fields are excluded from refresh to prevent corruption
        of the finite state machine's integrity.
        """
    
    def _get_protected_fsm_fields(self):
        """
        Get set of protected FSM field attribute names.
        
        Returns:
        set: Attribute names of protected FSM fields
        """

Usage example:

from django.db import models
from django_fsm import FSMField, FSMModelMixin, transition

class Order(FSMModelMixin, models.Model):
    state = FSMField(default='pending', protected=True)
    amount = models.DecimalField(max_digits=10, decimal_places=2)
    
    @transition(field=state, source='pending', target='confirmed')
    def confirm(self):
        pass

# Protected fields are automatically excluded during refresh
order = Order.objects.get(pk=1)
order.refresh_from_db()  # state field is preserved if protected

ConcurrentTransitionMixin

Protects models from undesirable effects caused by concurrently executed transitions using optimistic locking. This mixin prevents race conditions where multiple processes try to modify the same object's state simultaneously.

class ConcurrentTransitionMixin(object):
    def __init__(self, *args, **kwargs):
        """Initialize concurrent transition protection."""
    
    def save(self, *args, **kwargs):
        """
        Save model with concurrent transition protection.
        
        Raises:
        ConcurrentTransition: If state has changed since object was fetched
        """
    
    def refresh_from_db(self, *args, **kwargs):
        """Refresh from database and update internal state tracking."""
    
    @property
    def state_fields(self):
        """
        Get all FSM fields in the model.
        
        Returns:
        filter: FSM field instances
        """
    
    def _update_initial_state(self):
        """Update internal tracking of initial state values."""
    
    def _do_update(self, base_qs, using, pk_val, values, update_fields, forced_update):
        """
        Internal method that performs the actual database update with state validation.
        
        This method is called by Django's save mechanism and adds concurrent
        transition protection by filtering the update query on the original
        state values.
        
        Parameters:
        - base_qs: Base queryset for the update
        - using: Database alias to use
        - pk_val: Primary key value
        - values: Values to update
        - update_fields: Fields to update
        - forced_update: Whether update is forced
        
        Returns:
        bool: True if update was successful
        
        Raises:
        ConcurrentTransition: If state has changed since object was fetched
        """

Usage example:

from django.db import transaction
from django_fsm import FSMField, ConcurrentTransitionMixin, transition, ConcurrentTransition

class BankAccount(ConcurrentTransitionMixin, models.Model):
    state = FSMField(default='active')
    balance = models.DecimalField(max_digits=10, decimal_places=2)
    
    @transition(field=state, source='active', target='frozen')
    def freeze_account(self):
        pass

# Safe concurrent usage pattern
def transfer_money(account_id, amount):
    try:
        with transaction.atomic():
            account = BankAccount.objects.get(pk=account_id)
            if account.balance >= amount:
                account.balance -= amount
                account.save()  # Will raise ConcurrentTransition if state changed
    except ConcurrentTransition:
        # Handle concurrent modification
        raise ValueError("Account was modified by another process")

Advanced Integration Patterns

Combining Both Mixins

You can use both mixins together for maximum protection:

class SafeDocument(FSMModelMixin, ConcurrentTransitionMixin, models.Model):
    state = FSMField(default='draft', protected=True)
    workflow_state = FSMField(default='new')
    content = models.TextField()
    
    @transition(field=state, source='draft', target='published')
    def publish(self):
        pass
    
    @transition(field=workflow_state, source='new', target='processing')
    def start_workflow(self):
        pass

Custom Concurrent Protection

Customize which fields are used for concurrent protection:

class CustomProtectedModel(ConcurrentTransitionMixin, models.Model):
    primary_state = FSMField(default='new')
    secondary_state = FSMField(default='inactive')
    version = models.PositiveIntegerField(default=1)
    
    def save(self, *args, **kwargs):
        # Custom logic before concurrent protection
        if self.primary_state == 'published':
            self.version += 1
        
        # Call parent save with concurrent protection
        super().save(*args, **kwargs)

Handling Concurrent Exceptions

Proper exception handling for concurrent modifications:

from django_fsm import ConcurrentTransition
import time
import random

def safe_state_change(model_instance, transition_method, max_retries=3):
    """
    Safely execute state transition with retry logic for concurrent conflicts.
    """
    for attempt in range(max_retries):
        try:
            with transaction.atomic():
                # Refresh to get latest state
                model_instance.refresh_from_db()
                
                # Execute transition
                transition_method()
                model_instance.save()
                return True
                
        except ConcurrentTransition:
            if attempt == max_retries - 1:
                raise
            
            # Wait before retry with exponential backoff
            time.sleep(0.1 * (2 ** attempt) + random.uniform(0, 0.1))
    
    return False

# Usage
try:
    safe_state_change(order, order.confirm)
except ConcurrentTransition:
    # Final failure after retries
    logger.error(f"Failed to confirm order {order.id} after retries")

Integration with Django Signals

Combining mixins with Django FSM signals for comprehensive state management:

from django_fsm.signals import post_transition
from django.dispatch import receiver

class AuditedOrder(FSMModelMixin, ConcurrentTransitionMixin, models.Model):
    state = FSMField(default='pending', protected=True)
    audit_trail = models.JSONField(default=list)
    
    @transition(field=state, source='pending', target='confirmed')
    def confirm(self):
        pass

@receiver(post_transition, sender=AuditedOrder)
def log_state_change(sender, instance, name, source, target, **kwargs):
    """Log all state changes for audit purposes."""
    instance.audit_trail.append({
        'timestamp': timezone.now().isoformat(),
        'transition': name,
        'from_state': source,
        'to_state': target,
        'user_id': getattr(kwargs.get('user'), 'id', None)
    })
    
    # Save without triggering state machine (direct field update)
    sender.objects.filter(pk=instance.pk).update(
        audit_trail=instance.audit_trail
    )

Performance Considerations

Optimizing Protected Field Queries

When using FSMModelMixin with many protected fields:

class OptimizedModel(FSMModelMixin, models.Model):
    state = FSMField(default='new', protected=True)
    workflow = FSMField(default='pending', protected=True)
    
    # Use select_related/prefetch_related for efficient queries
    @classmethod
    def get_with_relations(cls, pk):
        return cls.objects.select_related('related_field').get(pk=pk)

Batch Operations with Concurrent Protection

Handle bulk operations safely:

def bulk_state_update(queryset, transition_method_name):
    """
    Safely update multiple objects with concurrent protection.
    """
    updated_count = 0
    
    for obj in queryset:
        try:
            with transaction.atomic():
                obj.refresh_from_db()
                method = getattr(obj, transition_method_name)
                method()
                obj.save()
                updated_count += 1
        except ConcurrentTransition:
            # Log failure but continue with other objects
            logger.warning(f"Concurrent update conflict for {obj.pk}")
    
    return updated_count

Database Constraints and State Consistency

Ensure database-level consistency:

class ConstrainedOrder(ConcurrentTransitionMixin, models.Model):
    state = FSMField(default='pending')
    payment_confirmed = models.BooleanField(default=False)
    
    class Meta:
        constraints = [
            models.CheckConstraint(
                check=~(models.Q(state='shipped') & models.Q(payment_confirmed=False)),
                name='shipped_orders_must_be_paid'
            )
        ]
    
    @transition(field=state, source='confirmed', target='shipped')
    def ship(self):
        if not self.payment_confirmed:
            raise ValueError("Cannot ship unpaid order")

Install with Tessl CLI

npx tessl i tessl/pypi-django-fsm

docs

dynamic-states.md

exceptions.md

field-types.md

index.md

model-mixins.md

signals.md

transitions.md

visualization.md

tile.json