Django FSM
Django FSM (Finite State Machine) provides declarative state management for Django models. It allows defining states and transitions using decorators, ensuring state changes adhere to predefined rules. The standalone `django-fsm` library, currently at version 3.0.1, is deprecated; its functionality has been integrated into `viewflow.fsm` since version 3.0.0. The original library will no longer receive updates.
Warnings
- breaking The standalone `django-fsm` library (version 3.0.0+) is no longer maintained. Its functionality has been fully integrated into `viewflow` as the `viewflow.fsm` package. Users are strongly encouraged to migrate to `viewflow.fsm` for new features and ongoing maintenance.
- gotcha Calling a transition method (e.g., `model_instance.transition_method()`) changes the state only in memory. You *must* call `model_instance.save()` afterward to persist the state change to the database.
- gotcha If `FSMField(protected=True)` is used, direct assignment to the state field (e.g., `model.state = 'new_state'`) will raise an `AttributeError`. State changes must occur via decorated transition methods. This is often desired for strict state machine enforcement.
- breaking The default `db_index=True` for `FSMIntegerField` was removed in version 2.2.0. This could subtly affect database performance if you relied on it for indexing.
Install
-
pip install django-fsm -
pip install django-viewflow
Imports
- FSMField
from django_fsm import FSMField
- transition
from django_fsm import transition
- FSMIntegerField
from django_fsm import FSMIntegerField
- FSMKeyField
from django_fsm import FSMKeyField
- can_proceed
from django_fsm import can_proceed
- FSMAdminMixin
from django_fsm.admin import FSMAdminMixin
- State
from viewflow.fsm import State
Quickstart
import os
import django
from enum import Enum
from django.db import models
from django_fsm import FSMField, transition
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'your_project_name.settings') # Replace with actual project settings if needed
django.setup()
class BlogPost(models.Model):
class BlogState(models.TextChoices):
DRAFT = "draft", "Draft"
REVIEW = "review", "Under Review"
PUBLISHED = "published", "Published"
ARCHIVED = "archived", "Archived"
state = FSMField(choices=BlogState.choices, default=BlogState.DRAFT, protected=True)
title = models.CharField(max_length=255)
content = models.TextField()
@transition(field=state, source=BlogState.DRAFT, target=BlogState.REVIEW)
def submit_for_review(self):
print(f"Blog post '{self.title}' submitted for review.")
@transition(field=state, source=BlogState.REVIEW, target=BlogState.PUBLISHED)
def publish(self):
print(f"Blog post '{self.title}' published!")
@transition(field=state, source=[BlogState.DRAFT, BlogState.REVIEW, BlogState.PUBLISHED], target=BlogState.ARCHIVED)
def archive(self):
print(f"Blog post '{self.title}' archived.")
def __str__(self):
return f"{self.title} ({self.get_state_display()})"
# Example Usage (assuming a Django environment is set up and models are synced)
# For a real application, replace this with actual model creation/retrieval.
# try:
# blog_post = BlogPost.objects.create(title="My First Post", content="Lorem ipsum...")
# print(blog_post)
# if can_proceed(blog_post.submit_for_review):
# blog_post.submit_for_review()
# blog_post.save()
# print(blog_post)
# if can_proceed(blog_post.publish):
# blog_post.publish()
# blog_post.save()
# print(blog_post)
# if can_proceed(blog_post.archive):
# blog_post.archive()
# blog_post.save()
# print(blog_post)
# # This would fail if protected=True and not using a transition method
# # blog_post.state = BlogPost.BlogState.DRAFT
# # blog_post.save()
# except Exception as e:
# print(f"An error occurred: {e}")