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.
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
})
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
)