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.
_images/lifcycle_state_diagram.svg

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)