Wrapping Django Models

To persist state in a database, create a flow class that wraps a Django model.

First, define your model with a state field:

# models.py

from django.db import models
from django.db.models import TextChoices
from django.utils.translation import gettext_lazy as _


class ReportState(TextChoices):
    NEW = 'NEW', _('New')
    APPROVED = 'APPROVED', _('Approved')
    REJECTED = 'REJECTED', _('Rejected')
    PUBLISHED = 'PUBLISHED', _('Published')

class Report(models.Model):
    text = models.TextField()
    state_field = models.CharField(
        max_length=150,
        choices=ReportState.choices,
        default=ReportState.NEW
    )

Then create a flow class. Use State.setter and State.getter to connect the FSM to your model field:

# flow.py

from viewflow import fsm
from .models import Report, ReportState

class ReportFlow(object):
    state_field = fsm.State(ReportState, default=ReportState.NEW)

    def __init__(self, report):
        self.report = report

    @state_field.setter()
    def _set_report_state(self, value):
        self.report.state_field = value

    @state_field.getter()
    def _get_report_state(self):
        return self.report.state_field

    @state_field.transition(source=ReportState.NEW, target=ReportState.APPROVED)
    def approve(self):
        pass

    @state_field.transition(source=ReportState.NEW, target=ReportState.REJECTED)
    def reject(self):
        pass

    @state_field.transition(source=ReportState.APPROVED, target=ReportState.PUBLISHED)
    def publish(self):
        pass

    @state_field.on_success()
    def _on_transition_success(self, descriptor, source, target):
        self.report.save()

The on_success decorator runs after a transition completes. Use it to save the model or trigger other actions.

State Change Views

Handle transitions in Django views with permission checks:

# views.py

from django.shortcuts import get_object_or_404, redirect, render
from django.core.exceptions import PermissionDenied
from .models import Report
from .forms import ApproveForm
from .flow import ReportFlow

def approve(request, report_pk):
    report = get_object_or_404(Report, pk=report_pk)
    flow = ReportFlow(report)

    if not flow.approve.has_perm(request.user):
        raise PermissionDenied

    form = ApproveForm(request.POST or None, instance=report)
    if form.is_valid():
        form.save(commit=False)
        flow.approve()
        return redirect('../')

    return render(request, 'approve.html', {
        'report': report,
        'flow': flow,
        'form': form
    })

Logging State Changes

Track state changes with a log model:

# models.py

from django.db import models
from django.utils import timezone

class ReportChangeLog(models.Model):
    report = models.ForeignKey(Report, on_delete=models.CASCADE)
    changed = models.DateTimeField(default=timezone.now)
    source = models.CharField(max_length=150)
    target = models.CharField(max_length=150)

    class Meta:
        ordering = ['-changed']

Add logging to your flow class:

# flow.py

from django.db import transaction
from .models import Report, ReportState, ReportChangeLog

class ReportFlow(object):
    state_field = fsm.State(ReportState, default=ReportState.NEW)
    # ... other methods ...

    @state_field.on_success()
    def _log_state_change(self, descriptor, source, target, **kwargs):
        with transaction.atomic():
            self.report.save()
            ReportChangeLog.objects.create(
                report=self.report,
                source=source.value,
                target=target.value
            )