Django friendly finite state machine support

Overview

Django friendly finite state machine support

Build Status

django-fsm adds simple declarative state management for django models.

If you need parallel task execution, view and background task code reuse over different flows - check my new project django-viewflow:

https://github.com/viewflow/viewflow

Instead of adding a state field to a django model and managing its values by hand, you use FSMField and mark model methods with the transition decorator. These methods could contain side-effects of the state change.

Nice introduction is available here: https://gist.github.com/Nagyman/9502133

You may also take a look at django-fsm-admin project containing a mixin and template tags to integrate django-fsm state transitions into the django admin.

https://github.com/gadventures/django-fsm-admin

Transition logging support could be achieved with help of django-fsm-log package

https://github.com/gizmag/django-fsm-log

FSM really helps to structure the code, especially when a new developer comes to the project. FSM is most effective when you use it for some sequential steps.

Installation

$ pip install django-fsm

Or, for the latest git version

$ pip install -e git://github.com/kmmbvnr/django-fsm.git#egg=django-fsm

The library has full Python 3 support

Usage

Add FSMState field to your model

from django_fsm import FSMField, transition

class BlogPost(models.Model):
    state = FSMField(default='new')

Use the transition decorator to annotate model methods

@transition(field=state, source='new', target='published')
def publish(self):
    """
    This function may contain side-effects,
    like updating caches, notifying users, etc.
    The return value will be discarded.
    """

The field parameter accepts both a string attribute name or an actual field instance.

If calling publish() succeeds without raising an exception, the state field will be changed, but not written to the database.

from django_fsm import can_proceed

def publish_view(request, post_id):
    post = get_object__or_404(BlogPost, pk=post_id)
    if not can_proceed(post.publish):
        raise PermissionDenied

    post.publish()
    post.save()
    return redirect('/')

If some conditions are required to be met before changing the state, use the conditions argument to transition. conditions must be a list of functions taking one argument, the model instance. The function must return either True or False or a value that evaluates to True or False. If all functions return True, all conditions are considered to be met and the transition is allowed to happen. If one of the functions returns False, the transition will not happen. These functions should not have any side effects.

You can use ordinary functions

def can_publish(instance):
    # No publishing after 17 hours
    if datetime.datetime.now().hour > 17:
        return False
    return True

Or model methods

def can_destroy(self):
    return self.is_under_investigation()

Use the conditions like this:

@transition(field=state, source='new', target='published', conditions=[can_publish])
def publish(self):
    """
    Side effects galore
    """

@transition(field=state, source='*', target='destroyed', conditions=[can_destroy])
def destroy(self):
    """
    Side effects galore
    """

You can instantiate a field with protected=True option to prevent direct state field modification.

class BlogPost(models.Model):
    state = FSMField(default='new', protected=True)

model = BlogPost()
model.state = 'invalid' # Raises AttributeError

Note that calling refresh_from_db on a model instance with a protected FSMField will cause an exception.

source state

source parameter accepts a list of states, or an individual state or django_fsm.State implementation.

You can use * for source to allow switching to target from any state.

You can use + for source to allow switching to target from any state exluding target state.

target state

target state parameter could point to a specific state or django_fsm.State implementation

from django_fsm import FSMField, transition, RETURN_VALUE, GET_STATE
@transition(field=state,
            source='*',
            target=RETURN_VALUE('for_moderators', 'published'))
def publish(self, is_public=False):
    return 'for_moderators' if is_public else 'published'

@transition(
    field=state,
    source='for_moderators',
    target=GET_STATE(
        lambda self, allowed: 'published' if allowed else 'rejected',
        states=['published', 'rejected']))
def moderate(self, allowed):
    pass

@transition(
    field=state,
    source='for_moderators',
    target=GET_STATE(
        lambda self, **kwargs: 'published' if kwargs.get("allowed", True) else 'rejected',
        states=['published', 'rejected']))
def moderate(self, allowed=True):
    pass

custom properties

Custom properties can be added by providing a dictionary to the custom keyword on the transition decorator.

@transition(field=state,
            source='*',
            target='onhold',
            custom=dict(verbose='Hold for legal reasons'))
def legal_hold(self):
    """
    Side effects galore
    """

on_error state

If the transition method raises an exception, you can provide a specific target state

@transition(field=state, source='new', target='published', on_error='failed')
def publish(self):
   """
   Some exception could happen here
   """

state_choices

Instead of passing a two-item iterable choices you can instead use the three-element state_choices, the last element being a string reference to a model proxy class.

The base class instance would be dynamically changed to the corresponding Proxy class instance, depending on the state. Even for queryset results, you will get Proxy class instances, even if the QuerySet is executed on the base class.

Check the test case for example usage. Or read about implementation internals

Permissions

It is common to have permissions attached to each model transition. django-fsm handles this with permission keyword on the transition decorator. permission accepts a permission string, or callable that expects instance and user arguments and returns True if the user can perform the transition.

@transition(field=state, source='*', target='published',
            permission=lambda instance, user: not user.has_perm('myapp.can_make_mistakes'))
def publish(self):
    pass

@transition(field=state, source='*', target='removed',
            permission='myapp.can_remove_post')
def remove(self):
    pass

You can check permission with has_transition_permission method

from django_fsm import has_transition_perm
def publish_view(request, post_id):
    post = get_object_or_404(BlogPost, pk=post_id)
    if not has_transition_perm(post.publish, request.user):
        raise PermissionDenied

    post.publish()
    post.save()
    return redirect('/')

Model methods

get_all_FIELD_transitions Enumerates all declared transitions

get_available_FIELD_transitions Returns all transitions data available in current state

get_available_user_FIELD_transitions Enumerates all transitions data available in current state for provided user

Foreign Key constraints support

If you store the states in the db table you could use FSMKeyField to ensure Foreign Key database integrity.

In your model :

class DbState(models.Model):
    id = models.CharField(primary_key=True, max_length=50)
    label = models.CharField(max_length=255)

    def __unicode__(self):
        return self.label


class BlogPost(models.Model):
    state = FSMKeyField(DbState, default='new')

    @transition(field=state, source='new', target='published')
    def publish(self):
        pass

In your fixtures/initial_data.json :

[
    {
        "pk": "new",
        "model": "myapp.dbstate",
        "fields": {
            "label": "_NEW_"
        }
    },
    {
        "pk": "published",
        "model": "myapp.dbstate",
        "fields": {
            "label": "_PUBLISHED_"
        }
    }
]

Note : source and target parameters in @transition decorator use pk values of DBState model as names, even if field "real" name is used, without _id postfix, as field parameter.

Integer Field support

You can also use FSMIntegerField. This is handy when you want to use enum style constants.

class BlogPostStateEnum(object):
    NEW = 10
    PUBLISHED = 20
    HIDDEN = 30

class BlogPostWithIntegerField(models.Model):
    state = FSMIntegerField(default=BlogPostStateEnum.NEW)

    @transition(field=state, source=BlogPostStateEnum.NEW, target=BlogPostStateEnum.PUBLISHED)
    def publish(self):
        pass

Signals

django_fsm.signals.pre_transition and django_fsm.signals.post_transition are called before and after allowed transition. No signals on invalid transition are called.

Arguments sent with these signals:

sender The model class.

instance The actual instance being processed

name Transition name

source Source model state

target Target model state

Optimistic locking

django-fsm provides optimistic locking mixin, to avoid concurrent model state changes. If model state was changed in database django_fsm.ConcurrentTransition exception would be raised on model.save()

from django_fsm import FSMField, ConcurrentTransitionMixin

class BlogPost(ConcurrentTransitionMixin, models.Model):
    state = FSMField(default='new')

For guaranteed protection against race conditions caused by concurrently executed transitions, make sure:

  • Your transitions do not have any side effects except for changes in the database,
  • You always run the save() method on the object within django.db.transaction.atomic() block.

Following these recommendations, you can rely on ConcurrentTransitionMixin to cause a rollback of all the changes that have been executed in an inconsistent (out of sync) state, thus practically negating their effect.

Drawing transitions

Renders a graphical overview of your models states transitions

You need pip install graphviz>=0.4 library and add django_fsm to your INSTALLED_APPS:

INSTALLED_APPS = (
    ...
    'django_fsm',
    ...
)
# Create a dot file
$ ./manage.py graph_transitions > transitions.dot

# Create a PNG image file only for specific model
$ ./manage.py graph_transitions -o blog_transitions.png myapp.Blog

Changelog

django-fsm 2.7.0 2019-12-03

  • Django 3.0 support
  • Test on Python 3.8
Comments
  • Django 3.2 breaks django-fsm: get_available_FIELD_transitions is empty for abstract models with FSMField in Django 3.2

    Django 3.2 breaks django-fsm: get_available_FIELD_transitions is empty for abstract models with FSMField in Django 3.2

    Hi, first of all, thanks for this great package! I've been using it for all Django apps with approval/QA workflows for 9 years now and it's an absolute game-changer.

    I've just noticed that after upgrading Django from 3.1 to 3.2, all django-fsm status transitions vanished. I read the code but didn't find any obvious breakage. There's no error message, the transitions simply don't show up. I can still execute the transition functions from the shell_plus. Obviously I have to do way more homework before I can file a useful bug report here, but I wanted to share my findings and ask whether any other django-fsm users have run into the same issue.

    • Django 3.2 with django-fsm is broken (no transitions). requirements.txt with obviously way too many dependencies for a MVE.
    • Downgrade to Django 3.1 (leave everything else up to latest) works again. requirements.txt

    I had to fix a few minor things to upgrade to Django 3.2 (app config and a patch for django-grappelli). The downgrade to Django 3.1 worked without any changes to the upgrade fixes.

    opened by florianm 22
  • Based on different transition condition failure, can we return multiple error message

    Based on different transition condition failure, can we return multiple error message

    Lets there is transition from "Order Processing" to "Order Accepted" on conditions = ['condition', 'condition2', 'condition3'..]

    if condition1 fails -> can i get any error message, so that i can retrieve that message return in the response.

    Similarly for failed conditions, multiple error message, error codes

    feature rejected 
    opened by ikbhal-blackbuck 15
  • FSMField and django model validation integration

    FSMField and django model validation integration

    Hi, is it possible to create a condition for the object creation with the same syntax that exists for other transitions?

    Say I have a default state = FsmField(default='new'), the only way I found to check the initial conditions is to use the clean or save methods and check that the id is null and then run some initial tests.

    I would have been nice to be able to do it in the same fashion as all the other state related tests.

    Is it possible to do it that way? How would you recommend to implement this using django-fsm?

    feature rejected 
    opened by khlumzeemee 10
  • Translation of states

    Translation of states

    Hi, I'm having a hard time trying to make the different states available for translation. Code:

        @transition(field=state, source='new', target='accepted')
        def accept(self):
    

    I tried with something like

    source=_('new') 
    

    but that's not the desired effect, because a different value will be saved to database depending on the user language. I just want to modify the label as a lazy translation on display.

    Could I use the custom dictionary for this?

    not an issue 
    opened by khlumzeemee 10
  • Allow additional arguments

    Allow additional arguments

    This allows passing additional arguments to the transition and condition function. A useful application is passing a user. That way permissions can be allowed or forbidden based on what permissions the user has.

    feature rejected 
    opened by mbertheau 10
  • auto-save workflow state

    auto-save workflow state

    An earlier version (django-fsm 1.6.0) allowed the transition decorator to automatically save the workflow state to the db by setting argument save=True

    @transition(source='new', target='published', save=True)
    def publish(self):
        ...
    

    It appears this feature was removed? Is overriding django_fsm.signals.post_transition the new recommended approach?

    opened by xavella 8
  • Allow nested transitions

    Allow nested transitions

    Now it is possible to use the signal 'post_transition' to handle your things after your state has been changed. But the signal is send out to all of the FSM receivers you configure, and has to filtered out again in each receiver. What will be more intensive if your project is growing.

    When you have a child object that will change state you will want to auto check your parent object if all his children are to the wanted state to proceed to the next parent state. But the last child that is still in the previous state is not yet transferred to the new state and has to be saved first, and then we are outside the state machine, and we can not automagically change the parent object. While I think this is still something to be handled inside the state machine. And I get why you don't want to auto save. But now it is not possible for others to implement a after save method within the FSM.

    If there would be a method that is called after the transition, we can implement our own save at that moment without working with signals and receivers, and a lot of more is possible with this FSM.

    Thanks,

    feature 3.0 
    opened by vanhoofseppe 8
  • Re-add save parameter to @transition decorator as a feature

    Re-add save parameter to @transition decorator as a feature

    I looked through the history and it looks like this used to be a feature. Why was it removed? I would like to use something like this for my own project.

    @transition(... save=True)

    Thanks for providing a nice library for me to use! :)

    feature rejected 
    opened by zenefitsbob 8
  • "non-explicit field transition" warning firing on inherited classes.

    I have a class that inherits its state field from a base class. This means that the FSMField doesn't exist in the class itself, and so I'm getting the "Non explicid (sic) field transition support going to be removed" deprecation warning.

    The decorator should take inheritance into account?

    bug accepted 
    opened by hugorodgerbrown 8
  • Do there any blockers to bump new release?

    Do there any blockers to bump new release?

    We are looking forward to using django-fsm with Django 3.2 on edX.

    Required commit is already merged into the master branch. Do there any chance a new version will be bumped soon?

    If any blockers with the release I would be happy to help.

    Thank you in advance.

    opened by jramnai 7
  • Fix the tests and update the supported python/django versions

    Fix the tests and update the supported python/django versions

    I've fixed the tests, added new versions of python (3.7) and django (2.0 and 2.1). I've also removed deprecated versions of python (2.6 and 3.3) and django (1.6, 1.7, 1.8 and 1.10).

    If you want to merge it into your master branch, I can put back your Travis and Gitter icons.

    opened by MDziwny 7
  • Fix command for django 4+

    Fix command for django 4+

    This PR solve issue #289

    Here is a reference to a related Django issue : https://github.com/pfouque/django-fsm/pull/new/fix_command

    Basically 'True' need to be replaced by __all__ : Django provides a constant but tell me If you prefer to hardcode it (what Django does)

    Cf: https://github.com/django/django/commit/c60524c658f197f645b638f9bcc553103bfe2630#diff-b24fd1a4236b2598b0c5de3e4aba7032cecacb4586f5c0082387fc7888993434L225

    opened by pfouque 2
  • Added quotes around graphviz>=0.4

    Added quotes around graphviz>=0.4

    "zsh: 0.4 not found" is the error message returned if quotes aren't added around the package name when it has the ">=" characters on Pip 22.3

    opened by pfcodes 0
  • docs: fix simple typo, exluding -> excluding

    docs: fix simple typo, exluding -> excluding

    There is a small typo in README.rst.

    Should read excluding rather than exluding.

    Semi-automated pull request generated by https://github.com/timgates42/meticulous/blob/master/docs/NOTE.md

    opened by timgates42 0
  • graph_transitions raise exception > TypeError: requires_system_checks must be a list or tuple

    graph_transitions raise exception > TypeError: requires_system_checks must be a list or tuple

    Hi guys,

    I am trying to graph transitions and I encountered this TypeError: requires_system_checks must be a list or tuple. Any help?

    Python 3.8.8 (tags/v3.8.8:024d805, Feb 19 2021, 13:18:16) [MSC v.1928 64 bit (AMD64)] on win32
    Django 4.1
    Graphviz 0.20.1
    

    Traceback (most recent call last): File "manage.py", line 22, in main() File "manage.py", line 18, in main execute_from_command_line(sys.argv) File "E:\mahmoud\django\workflow\env\lib\site-packages\django\core\management
    init.py", line 446, in execute_from_command_line utility.execute() File "E:\mahmoud\django\workflow\env\lib\site-packages\django\core\management
    init.py", line 440, in execute self.fetch_command(subcommand).run_from_argv(self.argv) File "E:\mahmoud\django\workflow\env\lib\site-packages\django\core\management
    init.py", line 279, in fetch_command klass = load_command_class(app_name, subcommand) File "E:\mahmoud\django\workflow\env\lib\site-packages\django\core\management
    init.py", line 49, in load_command_class return module.Command() File "E:\mahmoud\django\workflow\env\lib\site-packages\django\core\management
    base.py", line 274, in init raise TypeError("requires_system_checks must be a list or tuple.") TypeError: requires_system_checks must be a list or tuple.

    opened by mahmoud-ali 3
  • Drop support for old Python versions

    Drop support for old Python versions

    Python < 3.7 is no longer supported, so drop support and old syntax. Add python_requires in setup.py so installs on old Python versions will get older versions of django-fsm.

    postponed 
    opened by adamchainz 1
Releases(2.8.0)
  • 2.6.1(Apr 19, 2019)

  • 2.5.0(Mar 7, 2017)

    • graph_transition command fix for django 1.10
    • graph_transition command supports GET_STATE targets
    • signal data extended with method args/kwargs and field
    • sets allowed to be passed to the transition decorator
    Source code(tar.gz)
    Source code(zip)
  • 2.2.0(Sep 3, 2014)

    • Support for class substitution to proxy classes depending on the state
    • Added ConcurrentTransitionMixin with optimistic locking support
    • Default db_index=True for FSMIntegerField removed
    • Graph transition code migrated to new graphviz library with python 3 support
    • Ability to change state on transition exception
    Source code(tar.gz)
    Source code(zip)
Owner
Viewflow
Viewflow
Bootstrap 4 integration with Django.

django-bootstrap 4 Bootstrap 4 integration for Django. Goal The goal of this project is to seamlessly blend Django and Bootstrap 4. Requirements Pytho

Zostera B.V. 980 Dec 29, 2022
Meta package to combine turbo-django and stimulus-django

Hotwire + Django This repository aims to help you integrate Hotwire with Django 🚀 Inspiration might be taken from @hotwired/hotwire-rails. We are sti

Hotwire for Django 31 Aug 09, 2022
Utilities to make function-based views cleaner, more efficient, and better tasting.

django-fbv Utilities to make Django function-based views cleaner, more efficient, and better tasting. 💥 📖 Complete documentation: https://django-fbv

Adam Hill 49 Dec 30, 2022
Generate generic activity streams from the actions on your site. Users can follow any actors' activities for personalized streams.

Django Activity Stream What is Django Activity Stream? Django Activity Stream is a way of creating activities generated by the actions on your site. I

Justin Quick 2.1k Dec 29, 2022
Rosetta is a Django application that eases the translation process of your Django projects

Rosetta Rosetta is a Django application that facilitates the translation process of your Django projects. Because it doesn't export any models, Rosett

Marco Bonetti 909 Dec 26, 2022
This is a simple Todo web application built Django (back-end) and React JS (front-end)

Django REST Todo app This is a simple Todo web application built with Django (back-end) and React JS (front-end). The project enables you to systemati

Maxim Mukhin 5 May 06, 2022
An orgizational tool to keep track of tasks/projects and the time spent on them.

Django-Task-Manager Task Tracker using Python Django About The Project This project is an orgizational tool to keep track of tasks/projects and the ti

Nick Newton 1 Dec 21, 2021
Simple API written in Python using FastAPI to store and retrieve Books and Authors.

Simple API made with Python FastAPI WIP: Deploy in AWS with Terraform Simple API written in Python using FastAPI to store and retrieve Books and Autho

Caio Delgado 9 Oct 26, 2022
Strawberry-django-plus - Enhanced Strawberry GraphQL integration with Django

strawberry-django-plus Enhanced Strawberry integration with Django. Built on top

BLB Ventures 138 Dec 28, 2022
A drop-in replacement for django's ImageField that provides a flexible, intuitive and easily-extensible interface for quickly creating new images from the one assigned to the field.

django-versatileimagefield A drop-in replacement for django's ImageField that provides a flexible, intuitive and easily-extensible interface for creat

Jonathan Ellenberger 490 Dec 13, 2022
Учебное пособие по основам Django и сопутствующим технологиям

Учебный проект для закрепления основ Django Подробный разбор проекта здесь. Инструкция по запуску проекта на своей машине: Скачиваем репозиторий Устан

Stanislav Garanzha 12 Dec 30, 2022
A Django chatbot that is capable of doing math and searching Chinese poet online. Developed with django, channels, celery and redis.

Django Channels Websocket Chatbot A Django chatbot that is capable of doing math and searching Chinese poet online. Developed with django, channels, c

Yunbo Shi 8 Oct 28, 2022
System checks for your project's environment.

django-version-checks System checks for your project's environment. Requirements Python 3.6 to 3.9 supported. Django 2.2 to 3.2 supported. Are your te

Adam Johnson 33 Dec 22, 2022
Simply integrate Summernote editor with Django project.

django-summernote Summernote is a simple WYSIWYG editor. django-summernote allows you to embed Summernote into Django very handy. Support admin mixins

Summernote 936 Jan 02, 2023
Application made in Django to generate random passwords as based on certain criteria .

PASSWORD GENERATOR Welcome to Password Generator About The App Password Generator is an Open Source project brought to you by Iot Lab,KIIT and it brin

IoT Lab KIIT 3 Oct 21, 2021
A Django app for working with BTCPayServer

btcpay-django A Django app for working with BTCPayServer Installation pip install btcpay-django Developers Release To cut a release, run bumpversion,

Crawford 3 Nov 20, 2022
Built from scratch to replicate some of the Django admin functionality and add some more, to serve as an introspective interface for Django and Mongo.

django-mongonaut Info: An introspective interface for Django and MongoDB. Version: 0.2.21 Maintainer: Jazzband (jazzband.co) This Project is Being Mov

Jazzband 238 Dec 26, 2022
Developer-friendly asynchrony for Django

Django Channels Channels augments Django to bring WebSocket, long-poll HTTP, task offloading and other async support to your code, using familiar Djan

Django 5.5k Dec 29, 2022
Money fields for Django forms and models.

django-money A little Django app that uses py-moneyed to add support for Money fields in your models and forms. Django versions supported: 1.11, 2.1,

1.4k Jan 06, 2023
Phoenix LiveView but for Django

Reactor, a LiveView library for Django Reactor enables you to do something similar to Phoenix framework LiveView using Django Channels. What's in the

Eddy Ernesto del Valle Pino 526 Jan 02, 2023