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
A debug/profiling overlay for Django

Django Debug Toolbar The Django Debug Toolbar is a configurable set of panels that display various debug information about the current request/respons

David Cramer 228 Oct 17, 2022
Django Serverless Cron - Run cron jobs easily in a serverless environment

Django Serverless Cron - Run cron jobs easily in a serverless environment

Paul Onteri 41 Dec 16, 2022
Duckiter will Automatically dockerize your Django projects.

Duckiter Duckiter will Automatically dockerize your Django projects. Requirements : - python version : python version 3.6 or upper version - OS :

soroush safari 23 Sep 16, 2021
Source code for Django for Beginners 3.2

The official source code for https://djangoforbeginners.com/. Available as an ebook or in Paperback. If you have the 3.1 version, please refer to this

William Vincent 10 Jan 03, 2023
A simple Django dev environment setup with docker for demo purposes for GalsenDev community

GalsenDEV Docker Demo This is a basic Django dev environment setup with docker and docker-compose for a GalsenDev Meetup. The main purposes was to mak

3 Jul 03, 2021
Official clone of the Subversion repository.

Django is a high-level Python Web framework that encourages rapid development and clean, pragmatic design. All documentation is in the "docs" directo

Raymond Penners 3 May 06, 2022
Django API creation with signed requests utilizing forms for validation.

django-formapi Create JSON API:s with HMAC authentication and Django form-validation. Version compatibility See Travis-CI page for actual test results

5 Monkeys 34 Apr 04, 2022
Bootstrap 3 integration with Django.

django-bootstrap3 Bootstrap 3 integration for Django. Goal The goal of this project is to seamlessly blend Django and Bootstrap 3. Want to use Bootstr

Zostera B.V. 2.3k Jan 03, 2023
TinyMCE integration for Django

django-tinymce django-tinymce is a Django application that contains a widget to render a form field as a TinyMCE editor. Quickstart Install django-tin

Jazzband 1.1k Dec 26, 2022
Show how the redis works with Python (Django).

Redis Leaderboard Python (Django) Show how the redis works with Python (Django). Try it out deploying on Heroku (See notes: How to run on Google Cloud

Tom Xu 4 Nov 16, 2021
Flashback is an awesome, retro IRC based app built using Django

Flashback Flashback is an awesome, retro IRC based app built using Django (and the Django Rest Framework) for the backend as well as React for the fro

Unloading Gnat 1 Dec 22, 2021
Book search Django web project that uses requests python library and openlibrary API.

Book Search API Developer: Vladimir Vojtenko Book search Django web project that uses requests python library and openlibrary API. #requests #openlibr

1 Dec 08, 2021
Automatically reload your browser in development.

django-browser-reload Automatically reload your browser in development. Requirements Python 3.6 to 3.10 supported. Django 2.2 to 4.0 supported. Are yo

Adam Johnson 254 Jan 04, 2023
Advanced school management system written in Django :)

Advanced school management system written in Django :) ⚙️ Config the project First you should make venv for this project. So in the main root of proje

AminAli Mazarian 72 Dec 05, 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
A Django based shop system

django-SHOP Django-SHOP aims to be a the easy, fun and fast e-commerce counterpart to django-CMS. Here you can find the full documentation for django-

Awesto 2.9k Dec 30, 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
Send logs to RabbitMQ from Python/Django.

python-logging-rabbitmq Logging handler to ships logs to RabbitMQ. Compatible with Django. Installation Install using pip. pip install python_logging_

Alberto Menendez Romero 38 Nov 17, 2022
Актуальный сборник шаблонов для создания проектов и приложений на Django

О чем этот проект Этот репозиторий с шаблонами для быстрого создания Django проекта. В шаблоне проекта настроены следующий технологий: Django gunicorn

Denis Kustov 16 Oct 20, 2022
Website desenvolvido em Django para gerenciamento e upload de arquivos (.pdf).

Website para Gerenciamento de Arquivos Features Esta é uma aplicação full stack web construída para desenvolver habilidades com o framework Django. O

Alinne Grazielle 8 Sep 22, 2022