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
Bringing together django, django rest framework, and htmx

This is Just an Idea There is no code, this README just represents an idea for a minimal library that, as of now, does not exist. django-htmx-rest A l

Jack DeVries 5 Nov 24, 2022
A simple porfolio with Django, Bootstrap and Sqlite3

Django Portofolio Example this is a basic portfolio in dark mode Installation git clone https://github.com/FaztWeb/django-portfolio-simple.git cd djan

Fazt Web 16 Sep 26, 2022
Django based webapp pulling in crypto news and price data via api

Deploy Django in Production FTA project implementing containerization of Django Web Framework into Docker to be placed into Azure Container Services a

0 Sep 21, 2022
Django + AWS Elastic Transcoder

Django Elastic Transcoder django-elastic-transcoder is an Django app, let you integrate AWS Elastic Transcoder in Django easily. What is provided in t

StreetVoice 66 Dec 14, 2022
File and Image Management Application for django

Django Filer django Filer is a file management application for django that makes handling of files and images a breeze. Contributing This is a an open

django CMS Association 1.6k Dec 28, 2022
Vehicle registration using Python, Django and SQlite3

PythonCrud Cadastro de veículos utilizando Python, Django e SQlite3 Para acessar o deploy no Heroku:

Jorge Thiago 4 May 20, 2022
A reusable Django app that configures your project for deployment

django-simple-deploy This app gives you a management command that configures your project for an initial deployment. It targets Heroku at the moment,

Eric Matthes 205 Dec 26, 2022
Django StatusPage - App to display statuspage for your services

Django StatusPage - App to display statuspage for your services

Gorlik 1 Oct 27, 2021
DRF_commands is a Django package that helps you to create django rest framework endpoints faster using manage.py.

DRF_commands is a Django package that helps you to create django rest framework endpoints faster using manage.py.

Mokrani Yacine 2 Sep 28, 2022
django-idom allows Django to integrate with IDOM

django-idom allows Django to integrate with IDOM, a package inspired by ReactJS for creating responsive web interfaces in pure Python.

113 Jan 04, 2023
Use heroicons in your Django and Jinja templates.

heroicons Use heroicons in your Django and Jinja templates. Requirements Python 3.6 to 3.9 supported. Django 2.2 to 3.2 supported. Are your tests slow

Adam Johnson 52 Dec 14, 2022
Full control of form rendering in the templates.

django-floppyforms Full control of form rendering in the templates. Authors: Gregor Müllegger and many many contributors Original creator: Bruno Renié

Jazzband 811 Dec 01, 2022
🏭 An easy-to-use implementation of Creation Methods for Django, backed by Faker.

Django-fakery An easy-to-use implementation of Creation Methods (aka Object Factory) for Django, backed by Faker. django_fakery will try to guess the

Flavio Curella 93 Oct 12, 2022
Django friendly finite state machine support

Django friendly finite state machine support django-fsm adds simple declarative state management for django models. If you need parallel task executio

Viewflow 2.1k Dec 31, 2022
User Authentication In Django/Ajax/Jquery

User Authentication In Django/Ajax/Jquery Demo: Authentication System Using Django/Ajax/Jquery Demo: Authentication System Using Django Overview The D

Suman Raj Khanal 10 Mar 26, 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
Send push notifications to mobile devices through GCM or APNS in Django.

django-push-notifications A minimal Django app that implements Device models that can send messages through APNS, FCM/GCM and WNS. The app implements

Jazzband 2k Dec 26, 2022
A set of functions related with Django

django-extra-tools Table of contents Installation Quick start Template filters parse_datetime parse_date parse_time parse_duration Aggregation First L

Tomasz Jakub Rup 3 Mar 04, 2020
A collection of models, views, middlewares, and forms to help secure a Django project.

Django-Security This package offers a number of models, views, middlewares and forms to facilitate security hardening of Django applications. Full doc

SD Elements 258 Jan 03, 2023
Django Fett is an incomplete code generator used on several projects

Django Fett Django Fett is an incomplete code generator used on several projects. This is an attempt to clean it up and make it public for consumption

Jeff Triplett 6 Dec 31, 2021