Skip to content

Instantly share code, notes, and snippets.

@Nagyman
Last active January 27, 2024 08:29
Star You must be signed in to star a gist
Save Nagyman/9502133 to your computer and use it in GitHub Desktop.
Workflows in Django

Workflows (States) in Django

I'm going to cover a simple, but effective, utility for managing state and transitions (aka workflow). We often need to store the state (status) of a model and it should only be in one state at a time.

Common Software Uses

  • Publishing (Draft->Approved->Published->Expired->Deleted)
  • Payments
  • Account Authorization (New->Active->Suspended->Deleted)
  • Membership (Trial->Paid->Cancelled)
  • Quality Assurance, Games
  • Anything with a series of steps

Definitely avoid ...

Booleans for states

  • is_new
  • is_active
  • is_published
  • is_draft
  • is_deleted
  • is_paid
  • is_member
  • is_*

Mutually exclusive states ... sort of finite, but the number of states increases with each bool:

  • 2 bools = 2^2 = 4 states
  • 3 bools = 2^3 = 8 states
  • etc (2^N)

Brittle and too many states to check.

Finite State Machine

  • finite list of states
  • one state at a time; the current state
  • transition state by triggering event or condition

The behavior of state machines can be observed in many devices in modern society which perform a predetermined sequence of actions depending on a sequence of events with which they are presented.

Simple approach ...

CharField with defined choices

state = CharField(
    default=1,
    choices=[(1, "draft"), (2, "approved"), (3, "published")]
)

Define methods to change state:

def publish(self):
    self.state = 3
    email_sombody(self)
    self.save()

def approve(self):
    self.state = 2
    self.save()

Better, but ...

  • not enforced
    • Can I go from draft to published, skipping approval?
    • What happens if I publish something that's already published?
  • repetitive
  • side-effects mix with transition code

Some Goals

  • Safe, verifiable transitions between states
  • Conditions for the transition
  • Clear side effects from state transitions
  • DRY

django-fsm

  • declarative transitions and conditions (via decorators)
  • specialized field to contain state

https://github.com/kmmbvnr/django-fsm

P.S. RoR has similar apps too

FSMField

  • Specialized CharField
  • Set protected=True
    • to prevent direct/accidental manipulation
    • forces use of transition methods
    • raises an AttributeError "Direct state modification is not allowed"

Example

state = FSMField(
    default=State.DRAFT,
    verbose_name='Publication State',
    choices=State.CHOICES,
    protected=True,
)

(alternatives FSMIntegerField, FSMKeyField)

Transition decorator

@transition(field=state, source=[State.APPROVED, State.EXPIRED],
    target=State.PUBLISHED,
    conditions=[can_display])
def publish(self):
    '''
    Publish the object.
    '''
    email_the_team()
    update_sitemap()
    busta_cache()

What does this get us?

  • defined source and target states (valid transitions)
  • a method to complete the transition and define side-effects
  • a list of conditions (aside from state), that must be met for the transition to occur

Extras

Graphing state transitions

./manage.py graph_transitions -o example-graph.png fsm_example.PublishableModel

Something a bit more complex:

django-fsm-admin

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

  • submit row
  • logging history
  • noting what's required to change state (messages)

django-fsm-log

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

If you'd like your state transitions stored in something other than the admin history.

Alternatives?

Not much out there. django-fsm has the most activity.

Fin

Craig Nagy @nagyman G Adventures - Software Engineering, eComm Mgr.

@ericntd-legacy
Copy link

Hi Nagy,

Thanks for your sharing.
I don't understand how to use the handle the signals from django-fsm though.

My model is as follows:

from django.db import models
from django.dispatch import receiver
class Article(models.Model):
    state = FSMField(default=STATE_DRAFT, verbose_name='Publication State', choices=STATE_CHOICES, protected=False)
    @receiver(pre_transition)
    def on_pre_transition(self, sender, instance, name, source, target, **kwargs):
        print('pre_transition')

Then when I "published" an article using django-fsm-admin, I got an error:

TypeError at /admin/articles/article/32/
on_pre_transition() missing 1 required positional argument: 'self'

How do I use these callbacks?

@jrief
Copy link

jrief commented Mar 20, 2016

You should rewrite your pre/post-transition methods to:

    @staticmethod
    @receiver(pre_transition)
    def on_pre_transition(sender, instance=None, target=None, **kwargs):
        if not isinstance(instance, sender):
            return
        print('pre_transition')

and use instance as you would with self. Keep in mind, that instance can be an object of any type, hence the optional check if not isinstance(instance, sender)

@kishan3
Copy link

kishan3 commented May 12, 2016

in your example

state = FSMField(
default=State.DRAFT,
verbose_name='Publication State',
choices=State.CHOICES,
protected=True,
)

Is the State another model? What are the values for State.CHOICES
???

@hoshomoh
Copy link

hoshomoh commented Aug 1, 2016

How do you update the state of a model from an API not necessary the admin dashboard. In my case I am using django-rest-framework

@caruccio
Copy link

caruccio commented Aug 3, 2016

@hoshomoh Just call the transition function from your view.

@caruccio
Copy link

caruccio commented Aug 3, 2016

It took me a while to understand that, in order to persist the state, one must call model.save() outside/after @transition function.

@transition(field=state, source='new', target='paid')
def charge(self):
    '''do whatever you need to get paid'''

def pay_invoice_view(request):
    invoice = models.Invoice.get_user_invoice(request.user)
    invoice.charge()
    invoice.save() ## must save here, not inside charge()

@sharmaswati6
Copy link

Can anybody share a complete sample app pushed to git pls?

@fenilgandhi
Copy link

Thank you. been stuck on this for over a week until i discovered this.

@beneon
Copy link

beneon commented Nov 1, 2018

Thanks for the post. For the "a bit more complex" example, can you share a little bit about how to implement these transitions?

  1. pre_auth >> payment_auth
  2. payment_auth>>post_auth
    3.pre_auth>>post_auth
    Thanks again.

@neilmillard
Copy link

in your example

state = FSMField(
default=State.DRAFT,
verbose_name='Publication State',
choices=State.CHOICES,
protected=True,
)

Is the State another model? What are the values for State.CHOICES
???

an example is in the admin example.
https://github.com/gadventures/django-fsm-admin/blob/master/example/fsm_example/models.py

@cssovs
Copy link

cssovs commented Mar 14, 2021

do you support lock?

@sans712
Copy link

sans712 commented Jun 29, 2021

does this support parallel states in workflows?

@wnmurphy
Copy link

Would love to see some explicit documentation, i.e. what is source? what is target? How do I change the state?

It wasn't clear that I still need to call .save() after calling the helper fn, or that the helper fn doesn't detect a state change if I directly set the value; it only provides a home for side effects + enforces restrictions on state change directions.

@blueyed
Copy link

blueyed commented Jun 1, 2022

@CARocha
Copy link

CARocha commented Mar 14, 2023

You say "P.S. RoR has similar apps too !! " which?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment