Quickstart¶
Lets implement the following state machine.
- The object starts of as ‘under development’ which can then be made ‘live’.
- From the ‘live’ state it can be marked as ‘under maintenance’.
- From all states the object can be marked as ‘deleted’.
- A ‘deleted’ object can be recovered into the ‘under maintenance’ state.
- Whenever a transition occurs the datetime will be recorded in a datefield.
Import the dependencies:
from django_transitions.workflow import StateMachineMixinBase
from django_transitions.workflow import StatusBase
from transitions import Machine
States and Transitions¶
We start by defining the states and transitions
class LiveStatus(StatusBase):
"""Workflow for Lifecycle."""
# Define the states as constants
DEVELOP = 'develop'
LIVE = 'live'
MAINTENANCE = 'maintenance'
DELETED = 'deleted'
# Give the states a human readable label
STATE_CHOICES = (
(DEVELOP, 'Under Development'),
(LIVE, 'Live'),
(MAINTENANCE, 'Under Maintenance'),
(DELETED, 'Deleted'),
)
# Define the transitions as constants
PUBLISH = 'publish'
MAKE_PRIVATE = 'make_private'
MARK_DELETED = 'mark_deleted'
REVERT_DELETED = 'revert_delete'
# Give the transitions a human readable label and css class
# which will be used in the django admin
TRANSITION_LABELS = {
PUBLISH : {'label': 'Make live', 'cssclass': 'default'},
MAKE_PRIVATE: {'label': 'Under maintenance'},
MARK_DELETED: {'label': 'Mark as deleted', 'cssclass': 'deletelink'},
REVERT_DELETED: {'label': 'Revert Delete', 'cssclass': 'default'},
}
# Construct the values to pass to the state machine constructor
# The states of the machine
SM_STATES = [
DEVELOP, LIVE, MAINTENANCE, DELETED,
]
# The machines initial state
SM_INITIAL_STATE = DEVELOP
# The transititions as a list of dictionaries
SM_TRANSITIONS = [
# trigger, source, destination
{
'trigger': PUBLISH,
'source': [DEVELOP, MAINTENANCE],
'dest': LIVE,
},
{
'trigger': MAKE_PRIVATE,
'source': LIVE,
'dest': MAINTENANCE,
},
{
'trigger': MARK_DELETED,
'source': [
DEVELOP, LIVE, MAINTENANCE,
],
'dest': DELETED,
},
{
'trigger': REVERT_DELETED,
'source': DELETED,
'dest': MAINTENANCE,
},
]
Statemachine Mixin¶
Next we create a mixin to create a state machine for the django model.
Note
The mixin or the model must provide a state property.
In this implementation state is mapped to the
django model field wf_state
The mixin must override the machine
of the StateMachineMixinBase
class.
The minimum boilerplate to achieve this is:
machine = Machine(
model=None,
**status_class.get_kwargs()
)
In the example we also define a wf_finalize
method that will set the
date when the last transition occurred on every transaction.
class LifecycleStateMachineMixin(StateMachineMixinBase):
"""Lifecycle workflow state machine."""
status_class = LiveStatus
machine = Machine(
model=None,
finalize_event='wf_finalize',
auto_transitions=False,
**status_class.get_kwargs() # noqa: C815
)
@property
def state(self):
"""Get the items workflowstate or the initial state if none is set."""
if self.wf_state:
return self.wf_state
return self.machine.initial
@state.setter
def state(self, value):
"""Set the items workflow state."""
self.wf_state = value
return self.wf_state
def wf_finalize(self, *args, **kwargs):
"""Run this on all transitions."""
self.wf_date = timezone.now()
Model¶
Set up the django model
class Lifecycle(LifecycleStateMachineMixin, models.Model):
"""
A model that provides workflow state and workflow date fields.
This is a minimal example implementation.
"""
class Meta: # noqa: D106
abstract = False
wf_state = models.CharField(
verbose_name = 'Workflow Status',
null=False,
blank=False,
default=LiveStatus.SM_INITIAL_STATE,
choices=LiveStatus.STATE_CHOICES,
max_length=32,
help_text='Workflow state',
)
wf_date = models.DateTimeField(
verbose_name = 'Workflow Date',
null=False,
blank=False,
default=timezone.now,
help_text='Indicates when this workflowstate was entered.',
)
We can now inspect the behaviour of the model model with
python manage.py shell
>>> from testapp.models import Lifecycle
>>> lcycle = Lifecycle()
>>> lcycle.state
'develop'
>>> lcycle.publish()
True
>>> lcycle.state
'live'
>>> lcycle.publish()
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/home/christian/devel/django-transitions/.venv/lib/python3.5/site-packages/transitions/core.py", line 383, in trigger
return self.machine._process(func)
File "/home/christian/devel/django-transitions/.venv/lib/python3.5/site-packages/transitions/core.py", line 1047, in _process
return trigger()
File "/home/christian/devel/django-transitions/.venv/lib/python3.5/site-packages/transitions/core.py", line 397, in _trigger
raise MachineError(msg)
transitions.core.MachineError: "Can't trigger event publish from state live!"
>>> lcycle.save()
>>> graph = lcycle.get_wf_graph()
>>> graph.draw('lifcycle_state_diagram.svg', prog='dot') # This produces the above diagram
Admin¶
Set up the django admin to include the workflow actions.
# -*- coding: utf-8 -*-
"""Example django admin."""
from django_transitions.admin import WorkflowAdminMixin
from django.contrib import admin
from .models import Lifecycle
class LifecycleAdmin(WorkflowAdminMixin, admin.ModelAdmin):
"""
Minimal Admin for Lifecycles Example.
You probably want to make the workflow fields
read only so yo can not change these values
manually.
readonly_fields = ['wf_state', 'wf_date']
"""
list_display = ['wf_date', 'wf_state']
list_filter = ['wf_state']
admin.site.register(Lifecycle, LifecycleAdmin)