Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
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 commented Feb 29, 2016

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 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 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 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 commented Aug 3, 2016

@hoshomoh Just call the transition function from your view.

caruccio commented Aug 3, 2016 edited

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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment