30 May 2017
A Guide to Managing Finite State Machine Using Django FSM
   
Nagesh Dhope
#Django | 5 Min Read
Imagine an application like JIRA with complex workflow. Building such application needs a support for managing finite state machine.Well, if you are building your application with Django, Django FSM provides you out of the box support for managing finite state machine. Let’s assume that our application has the following workflow for a Task.
The basic model for a Task is as follows:
from django.db import models
class Task(models.Model):
   title = models.CharField(max_length=100, null=False)
   description = models.TextField()
Now, let’s see how can we build this using Django FSM.
Installation Process
$ pip install django-fsm
Or, you can install latest git version
$ pip install -e git://github.com/kmmbvnr/django-fsm.git#egg=django-fsm
Define States and State Field in Model
You have to define an FSMState field. On this field, Django FSM will perform transitions. Define these using FSMField.
from django.db import models
from django_fsm import FSMField
class Task(models.Model):
   title = models.CharField(max_length=100, null=False)
   description = models.TextField()
   state = FSMField()
We defined FSMState field but we didn’t associate any states to that field. We can do that as follows:
from django.db import models
from django_fsm import FSMField
STATES = ('Open', 'In Progress', 'Resolved', 'Re Opened', 'Closed')
STATES = list(zip(STATES, STATES))
class Task(models.Model):
   title = models.CharField(max_length=100, null=False)
   description = models.TextField()
   state = FSMField(default=STATES[0], choices=STATES)
Creating Transitions
Django FSM provides a decorator called transition. Once you decorate the model method with transition decorator, you can call the model method to perform transition on each object of a model.
from django.db import models
from django_fsm import FSMField, transition
STATES = ('Open', 'In Progress', 'Resolved', 'Re Opened', 'Closed')
STATES = list(zip(STATES, STATES))
class Task(models.Model):
   ...
   state = FSMField(default=STATES[0], choices=STATES)
    @transition(field=state, source=['Open', 'Re Opened'], target='In Progress')
    def start(self):
    	"""
         	This method will contain the action that needs to be taken once the
         	state is changed. Such as notifying Users.
         	"""
        pass
    @transition(field=state, source='In Progress', target='Resolved')
    def resolve(self):
        """
         	This method will contain the action that needs to be taken once the
         	state is changed. Such as notifying Users
         	"""
        pass
Source parameter accepts a list of states or an individual state. You can use * for the source, to allow switching to the target from any state. The field parameter accepts both the string attribute name or the actual field instance.
Changing State of Object
To change the state of an object, call the model method which is decorated with transition decorator.
task = Task.objects.get(pk=task_id)
task.start()
If start() gets executed successfully without raising any exceptions, state of task object will get updated but it will not be saved into a database. Call save() method to save the object in the database.
task.save()
Adding Conditions on Transitions
There might be situations wherein you want a check of the certain condition prior to performing the transition. You do so in Django FSM using Conditions parameter of transition decorator. Conditions must be a list of functions taking one argument(model instance). The function/method must return,True or False. A conditioning method can be a normal python function.
def can_close(instance):
   """ Return True or False, depending upon some condition """
   pass
or it can be a model method.
def can_close(self):
   """ Return True or False, depending upon some conditions """
   pass
Use conditions like this:
@transition(field=state, source='Resolved', target='Closed', conditions=[can_close])
def close(self):
   """ This method will contain the action that needs to be taken once the state is changed. """
   pass
Handling Exception in Transition Method
Django FSM provides a way to set a fall-back state in case transition method raises an exception.
@transition(field=state, source='Resolved', target='Closed', on_error='failed')
def close(self):
   """ Some exception could happen here """
   pass
Permission-Based Transition
Transition decorator has permission parameter that can be used to check permission prior to transition taking place. Permission accepts a permission string, or callable that expects instance and user arguments and returns True if the user can perform the transition.
@transition(field=state, source='Resolved', target='Closed', permission='myapp.can_change_task')
def close(self):
   """ This method will contain the action that needs to be taken once state is changed. """
   pass
or
@transition(field=state, source='Resolved', target='Closed',
	 permission=lambda instance, user: not user.has_perm('myapp.can_change_task'))
def close(self):
   """ This method will contain the action that needs to be taken once the state is changed. """
   pass
You can check permission with the has_transition_permission method.
from django_fsm import has_transition_perm
def publish_view(request, task_id):
   task = get_object_or_404(Task, pk=task_id)
   if not has_transition_perm(task.close, request.user):
       raise PermissionDenied
   task.close()
State Transition Diagram
You can generate a state transition diagram based on transition methods. You need the pip install graphviz>=0.4library and add django_fsm to your INSTALLED_APPS
INSTALLED_APPS = (
    ...
    'django_fsm',
    ...
)
Now run following command.
$ ./manage.py graph_transitions -o task_transitions.png myapp.Task
Summary
Django FSM has everything that you need to manage finite state machine in your application. In addition to this if want to perform transitions from admin site use Django-FSM-admin. Also, you can maintain transition logs using Django-FSM-log.