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.