Finite State Machines With Django

How to define finite state machines for models using Django

Besmire Thaqi
The Startup

--

Photo by Dušan Veverkolog on Unsplash

In software engineering, we often need to design a model of our application’s behavior. Finite state machines represent a design pattern that is widely used for this topic.

Before jumping into any example or piece of code, let’s explain what does a finite state machine mean.

What are finite state machines?

A finite state machine is simply a model of computation to simulate a sequential logic. The model which we define the finite state machines for, can be in exactly one of the finite number of states at a time.

The state (or status) can change from one to another in response to some external inputs. This process is also known as a transition.

Traffic lights as finite state machines 🚦

This is the simplest example to use and understand because it has three states.

The default (or the first) state is the green light. The state changes allowed are as followed:

  • Green → Yellow
  • Yellow → Red
  • Red → Green

In Django, these finite state machines and transitions can be defined by specifying the source and the target of the transition. Let’s dig into code!

The Django FSM

The library is called “django-fsm” and it’s very simple to use. First, make sure to install it:

pip install django-fsm

The state field has to be an FSM field with a default value. Therefore; the FSMField is imported. In our case, we have the green light as the default one:

from django_fsm import FSMField, transitionclass TrafficLight(models.Model):
state = FSMField(default="green", protected=True)

The attribute protected=True prevents changing the state directly, as shown in the example below:

model = TrafficLight()
model.state = "red" # raises AttributeError

Additionally; the transitions need to be defined using the @transition as a decorator and specifying the FSM field we added above. Other variables that the decorator takes are for example the source and the target.

The source is the state from which the transition is allowed to happen and the target is the state to which the transition will happen:

@transition(field=state, source="green", target="yellow")
def to_state_yellow(self):
return "Light switched to yellow!"
@transition(field=state, source="yellow", target="red")
def to_state_red(self):
return "Light switched to red!"
@transition(field=state, source="red", target="green")
def to_state_green(self):
return "Light switched to green!"

When these transitions are defined as shown in the example, it means that they are the only transitions allowed. Any other behavior is not allowed. For example, we don’t have a transition specified as “greenred”. In this case an exception will be thrown.

So far, our model looks something like this:

Furthermore; let’s take a more advanced example with a ticketing system. There are tickets with different state machines and transitions defined like in the graph below:

The ticket can be created, picked up, marked as failed or closed by a particular user.

Notice that if a ticket is “in progress” state or in “failed” state, they can both transition to “closed” state. In this case we have two sources with the same target.

The source parameter in the @transition decorator accepts an array to specify more than one state. This is how it looks code-wise:

@transition(field=state, source=["in_progress", "failed"], target="closed")
def to_state_closed(self):
pass

Based on the transitions, this is how the model currently looks:

from django_fsm import FSMField, transitionclass Ticket(models.Model):
state = FSMField(default="created")

...
@transition(field=state, source="created", target="in_progress")
def to_state_in_progress(self):
pass
@transition(field=state, source=["in_progress", "created"], target="failed")
def to_state_failed(self):
pass
@transition(field=state, source=["in_progress", "failed"], target="closed")
def to_state_closed(self):
pass

What about logging? 🔍

Now that the basic concept is clear regarding Django FSM, we want to add some logging for these transitions. This helps to add some description and track by whom the transition was made.

The Django FSM logs

There is another library supported for logs which has to be installed first:

pip install django-fsm-log

And then, also added in the Django apps:

INSTALLED_APPS = (
...,
'django_fsm_log',
...,
)

From this library, some decorators like fsm_log_by and fsm_log_description can be imported. These ones are added above the transition we want to use and also added as parameters in the method, like this:

@fsm_log_by
@fsm_log_description
@transition(field=state, source=["in_progress", "failed"], target="closed")
def to_state_closed(self, by=None, description=None):
pass

The parameters can be updated in the method when calling the transition:

ticket = Ticket.objects.create()
ticket.to_state_in_progress(by=some_account, description="Ticket has been updated")

In the end, the model for ticket will look like shown in the code below:

We either want to see the logs by using ORM (1) or directly in the Django admin panel (2). Fortunately; they are both possible!

To view the logs (1) we can import and use the StateLog model from django_fsm_log:

from django_fsm_log.models import StateLog# get all recorded logs
StateLog.objects.all()
# get the first ticket
ticket = Ticket.objects.first()
# use the "for_" manager method to get logs for the specific ticket
StateLog.objects.for_(ticket)

Implementing logs to see them in Django admin (2):

from django.contrib import admin
from django_fsm_log.admin import StateLogInline


@admin.register(FSMModel)
class TicketModelAdmin(admin.ModelAdmin):
inlines = [StateLogInline]
...

A short recap

A finite state machine is a model of computation that simulates a sequential logic. Changing from one state to another is known as a transition.

These transitions can be defined for a model in Django. The Django FSM libraries are used for implementing the states, transitions and the logging. More than one source state can be specified for the same target state.

Logs can be implemented to use with ORM or also view them in a cool table in the Django admin.

--

--

Besmire Thaqi
The Startup

An experienced Software Engineer based in Munich.