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
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 Jan 6, 2023
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
Django URL Shortener is a Django app to to include URL Shortening feature in your Django Project

Django URL Shortener Django URL Shortener is a Django app to to include URL Shortening feature in your Django Project Install this package to your Dja

Rishav Sinha 4 Nov 18, 2021
The friendly PIL fork (Python Imaging Library)

Pillow Python Imaging Library (Fork) Pillow is the friendly PIL fork by Alex Clark and Contributors. PIL is the Python Imaging Library by Fredrik Lund

Pillow 10.4k Jan 3, 2023
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 9, 2022
django-reversion is an extension to the Django web framework that provides version control for model instances.

django-reversion django-reversion is an extension to the Django web framework that provides version control for model instances. Requirements Python 3

Dave Hall 2.8k Jan 2, 2023
Django-environ allows you to utilize 12factor inspired environment variables to configure your Django application.

Django-environ django-environ allows you to use Twelve-factor methodology to configure your Django application with environment variables. import envi

Daniele Faraglia 2.7k Jan 7, 2023
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
Cookiecutter Django is a framework for jumpstarting production-ready Django projects quickly.

Cookiecutter Django Powered by Cookiecutter, Cookiecutter Django is a framework for jumpstarting production-ready Django projects quickly. Documentati

Daniel Feldroy 10k Dec 31, 2022
Django project starter on steroids: quickly create a Django app AND generate source code for data models + REST/GraphQL APIs (the generated code is auto-linted and has 100% test coverage).

Create Django App ?? We're a Django project starter on steroids! One-line command to create a Django app with all the dependencies auto-installed AND

imagine.ai 68 Oct 19, 2022
django-quill-editor makes Quill.js easy to use on Django Forms and admin sites

django-quill-editor django-quill-editor makes Quill.js easy to use on Django Forms and admin sites No configuration required for static files! The ent

lhy 139 Dec 5, 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
A handy tool for generating Django-based backend projects without coding. On the other hand, it is a code generator of the Django framework.

Django Sage Painless The django-sage-painless is a valuable package based on Django Web Framework & Django Rest Framework for high-level and rapid web

sageteam 51 Sep 15, 2022
A beginner django project and also my first Django project which involves shortening of a longer URL into a short one using a unique id.

Django-URL-Shortener A beginner django project and also my first Django project which involves shortening of a longer URL into a short one using a uni

Rohini Rao 3 Aug 8, 2021
Dockerizing Django with Postgres, Gunicorn, Nginx and Certbot. A fully Django starter project.

Dockerizing Django with Postgres, Gunicorn, Nginx and Certbot ?? Features A Django stater project with fully basic requirements for a production-ready

null 8 Jun 27, 2022
pytest-django allows you to test your Django project/applications with the pytest testing tool.

pytest-django allows you to test your Django project/applications with the pytest testing tool.

pytest-dev 1.1k Dec 14, 2022
APIs for a Chat app. Written with Django Rest framework and Django channels.

ChatAPI APIs for a Chat app. Written with Django Rest framework and Django channels. The documentation for the http end points can be found here This

Victor Aderibigbe 18 Sep 9, 2022
django-dashing is a customisable, modular dashboard application framework for Django to visualize interesting data about your project. Inspired in the dashboard framework Dashing

django-dashing django-dashing is a customisable, modular dashboard application framework for Django to visualize interesting data about your project.

talPor Solutions 703 Dec 22, 2022
Django-Audiofield is a simple app that allows Audio files upload, management and conversion to different audio format (mp3, wav & ogg), which also makes it easy to play audio files into your Django application.

Django-Audiofield Description: Django Audio Management Tools Maintainer: Areski Contributors: list of contributors Django-Audiofield is a simple app t

Areski Belaid 167 Nov 10, 2022