Django FSM-2
Django FSM-2 is an active, maintained fork of the original `django-fsm` library, providing declarative finite state machine support for Django models. It enables developers to define state fields and transitions using decorators, centralizing an object's lifecycle logic. Currently at version 4.2.4, it aims for regular updates and planned typing support.
Common errors
-
AttributeError: can't set attribute 'state' (or similar with your FSMField name)
cause Attempting to directly assign a new value to an FSMField, which is protected against direct modification.fixUse a method decorated with `@transition` to change the state. For example, if your field is `state` and you have a `publish()` method: `my_model_instance.publish()`. -
TransitionNotAllowed
cause An attempt was made to execute a transition that is not permitted by the defined state machine rules (e.g., source state does not match, or target state is unreachable from the current state).fixEnsure the current state of the model instance is a valid `source` for the desired transition. Check the `@transition` decorator's `source` and `target` parameters. Use `can_proceed(instance.transition_method)` to check validity before calling. -
NameError: name 'my_state_constant' is not defined
cause This usually happens when state names in `source` or `target` parameters of the `@transition` decorator are used as unquoted variables instead of string literals or properly imported constants (e.g., from `models.TextChoices`).fixEnsure state names are either enclosed in quotes (e.g., `source='new'`) or correctly reference an imported/defined constant (e.g., `source=BlogPost.State.NEW`).
Warnings
- breaking Version 4.0.0 of `django-fsm-2` removed support for Django 3.2, 4.0, and 4.1. It added support for Django 5.1. Projects on these older Django versions should stick to `django-fsm-2 < 4.0.0` or upgrade their Django version.
- gotcha Attempting to directly assign a new value to an `FSMField` (e.g., `instance.state = 'new_state'`) will often fail with an `AttributeError` if the field is `protected=True` (which is often the default or desired behavior). State changes *must* occur via methods decorated with `@transition`.
- gotcha After a successful transition method call, the model's state is updated in memory, but it is *not* automatically persisted to the database. You must explicitly call `instance.save()` to commit the state change.
- breaking The original `django-fsm` project was archived and later revived as `viewflow.fsm` (version 3.0.0+), introducing an entirely new and incompatible API. While `django-fsm-2` aims to be a drop-in replacement for *older* `django-fsm` versions (pre-archival), migrating from `viewflow.fsm` (i.e., `django-fsm >= 3.0.0`) to `django-fsm-2` is not a simple switch.
Install
-
pip install django-fsm-2
Imports
- FSMField
from django_fsm.db.fields import FSMField
from django_fsm import FSMField
- FSMModelMixin
from django_fsm import FSMModelMixin
- transition
from django_fsm import transition
- can_proceed
from django_fsm import can_proceed
- FSMAdminMixin
from fsm_admin.mixins import FSMTransitionMixin
from django_fsm.admin import FSMAdminMixin
Quickstart
from django.db import models
from django_fsm import FSMField, transition, FSMModelMixin
class BlogPost(FSMModelMixin, models.Model):
class State(models.TextChoices):
NEW = "new", "New"
DRAFT = "draft", "Draft"
PUBLISHED = "published", "Published"
ARCHIVED = "archived", "Archived"
title = models.CharField(max_length=255)
state = FSMField(default=State.NEW, protected=True)
@transition(field=state, source=State.NEW, target=State.DRAFT)
def create_draft(self):
print(f"Transitioning from {self.state} to {self.State.DRAFT}")
@transition(field=state, source=State.DRAFT, target=State.PUBLISHED)
def publish(self):
print(f"Transitioning from {self.state} to {self.State.PUBLISHED}")
@transition(field=state, source='*', target=State.ARCHIVED)
def archive(self):
print(f"Transitioning from {self.state} to {self.State.ARCHIVED}")
def __str__(self):
return f"{self.title} ({self.state})"
# Example Usage (assuming a Django environment and database):
# from .models import BlogPost, can_proceed # assuming this is in app.models
#
# post = BlogPost.objects.create(title='My First Post')
# print(post) # My First Post (new)
#
# if can_proceed(post.create_draft):
# post.create_draft()
# post.save()
# print(post) # My First Post (draft)
#
# if can_proceed(post.publish):
# post.publish()
# post.save()
# print(post) # My First Post (published)
#
# # Attempting an invalid transition
# if can_proceed(post.create_draft):
# print("Should not be able to draft from published state")
# else:
# print(f"Cannot transition from {post.state} to draft")
#
# if can_proceed(post.archive):
# post.archive()
# post.save()
# print(post) # My First Post (archived)