Handling statuses in Django #1October 2016
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.
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.
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()
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()
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
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:
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.
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.