In simple words, a finite-state machine is a computational model that helps to replicate any real-world scenario in which one object can have multiple states and can transition from one state to another. In FSM, an object can only have a single state at a given time.
We know a computer has basic three states of transition when computing something. We need to give input to our computer, it will process it and give us an output. Similarly in the worst-case scenario, the computer fails to process our input and throws an error. For example, dividing two numbers. If we give two numbers (other than zero) computer will process it and give us a result. But let’s say we give zero (0) as a denominator, and the computer gives an error.
Now let's implement a Finite state machine in your web application
As an employee, there are times when you have to take a leave from your job, for that you have to first apply for the leave then someone (maybe your project manager) has to review it before submitting it. After that, your leave request is either approved or rejected.
Let’s build this scenario in a Django application for that we first need to setup our basic Django application that has two apps: users & communications
Dependencies
pip install django
pip install djangorestframework
pip install django-fsm
pip install graphviz
Requirements:
(*) Add 'rest_framework' and 'django_fsm' in your INSTALLED_APPS list, in settings.py
Apps
python manage.py startapp users
python manage.py startapp communications
settings.py
INSTALLED_APPS [
'users.apps.UsersConfig',
'communications.apps.CommunicationsConfig', .....
users/models.py
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
def __str__(self):
return self.email
communications/models.py
from django.contrib.auth import get_user_model
from django.db import models
from django_fsm import FSMField, transition
from communications.choices import LeaveRequestStatus
User = get_user_model()
class LeaveRequest(models.Model):
text = models.TextField()
submitted_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name="leave_requests")
status = FSMField(default=LeaveRequestStatus.DRAFT, choices=LeaveRequestStatus.choices)
@transition(field=status, source=[LeaveRequestStatus.DRAFT, LeaveRequestStatus.CHANGES_REQUESTED], target=LeaveRequestStatus.REVIEW)
def move_to_review(self):
return
@transition(field=status, source=LeaveRequestStatus.REVIEW, target=LeaveRequestStatus.CHANGES_REQUESTED)
def request_changes(self):
return
@transition(field=status, source=LeaveRequestStatus.REVIEW, target=LeaveRequestStatus.SUBMITTED)
def mark_submitted(self):
return
@transition(field=status, source=LeaveRequestStatus.SUBMITTED, target=LeaveRequestStatus.APPROVED)
def mark_approved(self):
return
@transition(field=status, source=LeaveRequestStatus.SUBMITTED, target=LeaveRequestStatus.DECLINED)
def mark_declined(self):
return
def __str__(self):
return f"{self.text} | Request by: {self.submitted_by}"
Create a choices.py file in your communication app and place the following code
from django.db import models
class LeaveRequestStatus(models.TextChoices):
DRAFT = "DRAFT", "Draft"
REVIEW = "REVIEW", "Review"
CHANGES_REQUESTED = "CHANGES_REQUESTED", "Changes requested"
SUBMITTED = "SUBMITTED", "Submitted"
APPROVED = "APPROVED", "Approved"
DECLINED = "DECLINED", "Declined"
After that create migrations for your models and apply them
python manage.py makemigrations
python manage.py migrate
You can observe, we have created all the possible scenarios for the leave request in the LeaveRequest model class. One thing to remember, we can only update the status of leave requests with the rules we have defined in our model (@transition methods)
Now let's create views for our communication app, we need to create a view for each of the state transitions.
communications/views.py
from django_fsm import TransitionNotAllowed
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from communications.models import LeaveRequest
class ReviewLeaveRequestAPIView(APIView):
def post(self, request, pk):
leave_request = LeaveRequest.objects.get(pk=pk)
try:
leave_request.move_to_review()
leave_request.save()
response = Response(status=status.HTTP_200_OK)
except TransitionNotAllowed:
response = Response(status=status.HTTP_400_BAD_REQUEST)
return response
class ChangesRequiredLeaveRequestAPIView(APIView):
def post(self, request, pk):
leave_request = LeaveRequest.objects.get(pk=pk)
try:
leave_request.request_changes()
leave_request.save()
response = Response(status=status.HTTP_200_OK)
except TransitionNotAllowed:
response = Response(status=status.HTTP_400_BAD_REQUEST)
return response
class SubmitLeaveRequestAPIView(APIView):
def post(self, request, pk):
leave_request = LeaveRequest.objects.get(pk=pk)
try:
leave_request.mark_submitted()
leave_request.save()
response = Response(status=status.HTTP_200_OK)
except TransitionNotAllowed:
response = Response(status=status.HTTP_400_BAD_REQUEST)
return response
class ApproveLeaveRequestAPIView(APIView):
def post(self, request, pk):
leave_request = LeaveRequest.objects.get(pk=pk)
try:
leave_request.mark_approved()
leave_request.save()
response = Response(status=status.HTTP_200_OK)
except TransitionNotAllowed:
response = Response(status=status.HTTP_400_BAD_REQUEST)
return response
class DeclineLeaveRequestAPIView(APIView):
def post(self, request, pk):
leave_request = LeaveRequest.objects.get(pk=pk)
try:
leave_request.mark_declined()
leave_request.save()
response = Response(status=status.HTTP_200_OK)
except TransitionNotAllowed:
response = Response(status=status.HTTP_400_BAD_REQUEST)
return response
communication/urls.py
from django.urls import path
from communications.views import ReviewLeaveRequestAPIView, ChangesRequiredLeaveRequestAPIView, SubmitLeaveRequestAPIView, ApproveLeaveRequestAPIView, DeclineLeaveRequestAPIView
urlpatterns = [
path("review/<int:pk>/", ReviewLeaveRequestAPIView.as_view(), name="review_leave_request"),
path("changes-requested/<int:pk>/", ChangesRequiredLeaveRequestAPIView.as_view(), name="changes_reequested_leave_request"),
path("submit/<int:pk>/", SubmitLeaveRequestAPIView.as_view(), name="submit_leave_request"),
path("approve/<int:pk>/", ApproveLeaveRequestAPIView.as_view(), name="approve_leave_request"),
path("decline/<int:pk>/", DeclineLeaveRequestAPIView.as_view(), name="decline_leave_request"),
]
Update your project URLs too (project-name/urls.py)
urlpatterns = [
path('admin/', admin.site.urls),
path("api/requests/", include("communications.urls")),
]
Additional step (optional)
Add an admin.py file in both of your apps (users and communications) so that you can create dummy users and requests to test your APIs easily
users/admin.py
from django.contrib import admin
from users.models import User
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
list_display = (
"id",
"email",
"first_name",
"last_name",
"is_superuser",
"is_staff",
"is_active",
)
list_filter = (
"is_superuser",
"is_staff",
"is_active",
"last_login",
)
search_fields = ("=id", "email")
communications/admin.py
from django.contrib import admin
from communications.models import LeaveRequest
@admin.register(LeaveRequest)
class UserAdmin(admin.ModelAdmin):
list_display = (
"id",
"text",
"submitted_by",
"status",
)
list_filter = (
"status",
)
search_fields = ("=id", "status")
Now let’s test our APIs using Postman. I have created a dummy request with (DRAFT status) using Django admin. I am going to change its status to REVIEW by using review API.
After calling the same API, it throws an error because we can’t move to the REVIEW state from the REVIEW state.
Lastly, we can also draw our finite-state machine using the Graphviz tool. Just run the below two commands to get the image form of your finite-state machine.
python manage.py graph_transitions > transitions.dot
dot -Tpng transitions.dot -o fsm.png
Hope you like this article, feel free to share your feedback in the chatbox.