eat some code

Handling statuses in Django #1

Choice field & finite state machine

October 2016 #Status  #State machine  #django-fsm  #Django 

Whether you're building up a CMS or a bespoke application, chances are that you will have to handle some states / statuses. Let's discuss your options in Django.

State & Transition definition (the boring part)

Feel free to skip this section if you know what states and transitions are.

Some typical examples are:

  • An article status (concerning its publication):
    • draft => ready to be moderated => published
  • An order status (money, money):
    • previewed => address selected => payment details entered => paid => packed => delivered

A status (or state) is finite, in other words,there can be only one; an article status is either "draft, "ready to be moderated" or "published" at any point in time (xor would be more accurate but I don't fully assume my nerdiness ! ). Going from one state to another is what we like to call a transition to sound fancy. "publish" is a transition going from the "ready to be moderated" state to the "published" state.

Note: the above examples are really simple. In practice you might have more possible transitions. For example we could add an un-publish functionality: "un-publish" would then be another transition.

Simple solutions

There are 2 obvious ways to handle this problem in Django: having a simple choice field or recording some dates (or both). These are appropriate in most cases.

1.Choice field:

DRAFT = "draft"
READY = "ready"
PUBLISHED = "published"

ARTICLE_STATUS_CHOICES = (
    (DRAFT, "Draft"),
    (READY, "Ready to be moderated"),
    (PUBLISHED, "Published")
)

class Article(models.Model):
    status = models.CharField(max_lenght=10, choices=ARTICLE_STATUS_CHOICES)

In this case, to move from one step to another, simply update the status field:

article.status = READY
article.save()

2.Recording timestamps:

It's often a good idea to know when specific updates were made. It's especially true with statuses. The implementation is straightforward:

...

class Article(models.Model):
    status = models.CharField(max_lenght=10, choices=ARTICLE_STATUS_CHOICES)
    ready_at = models.DateTimeField(blank=True, null=True)
    published_at = models.DateTimeField(blank=True, null=True)

Note: Having a "status" field is now optional - we could determine it based on the timestamps but caching it makes filtering easier and faster.

You've guessed it, to move from one step to another, simply update 2 fields:

article.status = READY
article.ready_at = now()
article.save()

Advanced requirements

Imagine you're building a more sophisticated publication platform where articles have 2 levels of approvals:

Now let's add some real world "fun" politics into the mix:

  • A limited set of users can give the first approval
  • Obviously a very limited set of users can give the second and final approval.
  • When one of these users requests a change - the article goes back to the draft status
  • The boss can publish at any time
  • Moderators can only publish after the second approval has been given
  • etc.

The bad news if that using above methods would be cumbersome (lot of fields and code), the good news is that there is a fantastic library to solve this particualr problem (quite common within the Django community) & it's called django-fsm which stands for Django Finit State Machine.

Using Django Finit State Machine

With django-fsm, we'll still use a status field and we'll use FSMField for it. However we can remove our timestamps and simply plug django-fsm-log in to log all the transitions. django-fsm basically helps with defining all the transitions and their respective permissions. As an example, here is what the code would look like (it's incomplete, I've only detailed publish_permission):

from django.db import models
from django_fsm import FSMField, transition


DRAFT = "draft"
APPROVAL_1 = "approval_1"
APPROVAL_2 = "approval_2"
PUBLISHED = "published"

ARTICLE_STATUS_CHOICES = (
    (DRAFT, "Draft"),
    (APPROVAL_1, "Approval 1"),
    (APPROVAL_2, "Approval 2"),
    (PUBLISHED, "Published")
)


def publish_permission(article, user):
    return (
        user.is_boss() or
        (article.status == APPROVAL_2 and user.is_manager())
    )


class Article(models.Model):
    status = FSMField(
        max_length=10, choices=ARTICLE_STATUS_CHOICES, default=DRAFT,
        protected=True    # force to use transitions to update status
    )

    @transition(field=status, source=[DRAFT], target=APPROVAL_1, permission=approve_1_permission)
    def approve_1(self):
        pass

    @transition(field=status, source=[APPROVAL_1], target=APPROVAL_2, permission=approve_2_permission)
    def approve_2(self):
        pass

    @transition(field=status, source=[APPROVAL_1, APPROVAL_2], target=DRAFT, permission=disapprove_permission)
    def disapprove(self):
        pass

    @transition(field=status, source=[DRAFT, APPROVAL_1, APPROVAL_2], target=PUBLISHED, permission=publish_permission)
    def publish(self):
        pass

At any moment, you can check what the logged-in user can do with a specific article by calling:

article.get_available_user_status_transitions(user)

As you can see, the transition decorator makes the code short & explicit. It's for example really easy to see who can publish an article. Please check Django FSM documentation for more information: https://github.com/kmmbvnr/django-fsm.

If you want to use this within an API, check-out the following article where I describe how to do this with DRF: http://eatsomecode.com/handling-statuses-django-2.

Conclusion

In most cases, handling statuses in Django is as easy as using the choices option. If you have to deal with many statuses and/or complex rules, django-fsm is your friend.

Image Credits

Header - steps - by Denise Palhares Mooney via Free Images