Wrapping Django Models

To store state in a database, a class with viewflow.fsm.State slot can be composed with a Django model. This approach allows you to leverage Django’s ORM for state persistence while utilizing Viewflow’s FSM capabilities.

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
    )

Next, create a flow class that connects the FSM to your model. Use the State.setter and State.getter decorators to specify how the state is stored and retrieved.

# 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):
        # Additional business logic can be added here
        pass

    @state_field.transition(source=ReportState.NEW, target=ReportState.REJECTED)
    def reject(self):
        # Additional business logic can be added here
        pass

    @state_field.transition(source=ReportState.APPROVED, target=ReportState.PUBLISHED)
    def publish(self):
        # Additional business logic can be added here
        pass

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

The State.on_success decorator allows you to specify actions to be performed at the end of a transition, such as saving the model or triggering additional operations.

State Change Views

In Django views, you can handle state transitions with proper permission checks and form validation. Here’s an example of a view that handles an approval action:

# 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)

    # Check if user has permission to perform this transition
    if not flow.approve.has_permission(request.user):
        raise PermissionDenied

    form = ApproveForm(request.POST or None, instance=report)
    if form.is_valid():
        form.save(commit=False)  # Don't save yet, let the flow handle it
        flow.approve()  # Perform the state transition
        return redirect('../')

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

Logging State Changes

You can track state changes by creating a dedicated model to log transitions. This is useful for auditing and maintaining a history of state changes.

First, define a model to log the changes:

# 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']

Then add an on_success handler to your flow class to record changes:

# 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
            )