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
django social media app with real time features

django-social-media django social media app with these features: signup, login and old registered users are saved by cookies posts, comments, replies,

8 Apr 30, 2022
Django Girls Tutorial Workshop

Django Girls Tutorial Workshop A log of activities during the workshop. this is an H2 git remote add origin https://github.com/ahuimanu/django_girls_t

Jeffry Babb 1 Oct 27, 2021
A package to handle images in django

Django Image Tools Django Image Tools is a small app that will allow you to manage your project's images without worrying much about image sizes, how

The Bonsai Studio 42 Jun 02, 2022
A fresh approach to autocomplete implementations, specially for Django.

A fresh approach to autocomplete implementations, specially for Django. Status: v3 stable, 2.x.x stable, 1.x.x deprecated. Please DO regularely ping us with your link at #yourlabs IRC channel

YourLabs 1.6k Dec 22, 2022
scaffold django rest apis like a champion 🚀

dr_scaffold Scaffold django rest apis like a champion âš¡ . said no one before Overview This library will help you to scaffold full Restful API Resource

Abdenasser Elidrissi 133 Jan 05, 2023
Improved Django model inheritance with automatic downcasting

Polymorphic Models for Django Django-polymorphic simplifies using inherited models in Django projects. When a query is made at the base model, the inh

1.4k Jan 03, 2023
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
Use webpack to generate your static bundles without django's staticfiles or opaque wrappers.

django-webpack-loader Use webpack to generate your static bundles without django's staticfiles or opaque wrappers. Django webpack loader consumes the

2.4k Dec 24, 2022
Super simple bar charts for django admin list views visualizing the number of objects based on date_hierarchy using Chart.js.

Super simple bar charts for django admin list views visualizing the number of objects based on date_hierarchy using Chart.js.

foorilla LLC 4 May 18, 2022
Docker django app

Hmmmmm... What I should write here? Maybe "Hello World". Hello World Build Docker compose: sudo docker-compose build Run Docker compose: sudo docker-

Andrew 0 Nov 10, 2022
Simple alternative to Doodle polls and scheduling (Python 3, Django 3, JavaScript)

What is jawanndenn? jawanndenn is a simple web application to schedule meetings and run polls, a libre alternative to Doodle. It is using the followin

Sebastian Pipping 169 Jan 06, 2023
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 05, 2022
A simple plugin to attach a debugger in Django on runserver command.

django-debugger A simple plugin to attach a debugger in Django during runserver Installation pip install django-debugger Usage Prepend django_debugger

Sajal Shrestha 11 Nov 15, 2021
A CBV to handle multiple forms in one view

django-shapeshifter A common problem in Django is how to have a view, especially a class-based view that can display and process multiple forms at onc

Kenneth Love 167 Nov 26, 2022
Django Phyton Web Apps template themes

Django Phyton Web Apps template themes Free download source code project for build a modern website using django phyton web apps. Documentation instal

Mesin Kasir 4 Dec 15, 2022
Easy thumbnails for Django

Easy Thumbnails A powerful, yet easy to implement thumbnailing application for Django 1.11+ Below is a quick summary of usage. For more comprehensive

Chris Beaven 1.3k Dec 30, 2022
Updates redisearch instance with igdb data used for kimosabe

igdb-pdt Update RediSearch with IGDB games data in the following Format: { "game_slug": { "name": "game_name", "cover": "igdb_coverart_url",

6rotoms 0 Jul 30, 2021
xsendfile etc wrapper

Django Sendfile This is a wrapper around web-server specific methods for sending files to web clients. This is useful when Django needs to check permi

John Montgomery 476 Dec 01, 2022
GeoDjango provides geospatial extensions to the Django web dev framework

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

Paul Smith 20 Sep 20, 2022
Django Audit is a simple Django app that tracks and logs requests to your application.

django-audit Django Audit is a simple Django app that tracks and logs requests to your application. Quick Start Install django-audit pip install dj-au

Oluwafemi Tairu 6 Dec 01, 2022