Imagine we want to publish blog posts on a website but need to manage the status of individual posts. Each blog post can be in one of two states; draft or approved. We want to create and modify blog posts via a REST interface which will be called from a web application. When a blog post is transitioned to the approved state we should send an email to recipients informing them of the change. The following code can be found in the example directory of this repository.
Model
We will create a Post
model with some basic fields but importantly a state field. All potential states must be defined using choices. We can optionally extend FlowModel
- this will add automatic state tracking so we can maintain and query an audit trail.
# example/models.py
from django.db import models
from yoflow.models import FlowModel
class Post(FlowModel):
DRAFT = 1
APPROVED = 2
STATES = (
(DRAFT, 'draft'),
(APPROVED, 'approved'),
)
name = models.CharField(max_length=256)
content = models.TextField()
state = models.IntegerField(choices=STATES, default=DRAFT)
Workflow
Our workflow must define the model, and all possible state transitions.
# example/flows.py
from django.core.mail import send_mail
from yoflow import flow
from example import models
class PostFlow(flow.Flow):
model = models.Post
transitions = {
model.DRAFT: [model.APPROVED],
model.APPROVED: [],
}
@staticmethod
def draft_to_approved(obj, meta):
pass
@staticmethod
def on_approved(obj, meta):
send_mail('Approved!', '{} was approved'.format(obj), 'from@example.com', ['to@example.com'])
@staticmethod
def all(obj, meta):
pass
Our transitions dict defines that blog posts in a ‘draft’ state can be updated to an ‘approved’ state, but once ‘approved’ they can’t revet back to ‘draft’. In reality you will likely have more states/choices.
We implement on_approved
to send an email whenever the state is updated to ‘approved’. There are several available transition hooks:
Function | Description |
---|---|
{state}_to_{state} | Called for specific state changes |
on_{state} | Called for all changes to state |
all | Called on all transitions |
More workflow information is available here.
View
You are free to use any view logic - in this example we use django-rest-framework because it provides a lot of useful functionality.
# example/views.py
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from yoflow.decorators import transition
from example import flows, models, serializers
class PostViewSet(viewsets.ModelViewSet):
queryset = models.Post.objects.all()
serializer_class = serializers.PostSerializer
flow = flows.PostFlow
@action(methods=['post'], detail=True)
@transition(to_state=models.Post.APPROVED)
def approved(self, request, pk=None):
return Response({ 'name': obj.name, 'state': obj.get_state_display() })
@action(methods=['get'], detail=True)
def history(self, request, pk=None):
qs = self.get_object().yoflow_history.all()
return Response(qs.values('created_at', 'new_state', 'previous_state', 'meta', 'user'))
You can decorate individual views with @transition
- this will:
- Validate the requested transition based on current state
- Update the state of the instance
- Create a yoflow_history instance if your model extends
FlowModel
- Run custom flow logic
HTTP Example
# create new instance
$ http POST localhost:8000/blog/post/ name='test' content='abc'
{"name": "test", "state": "draft"}
# update instance state to approved with meta data
$ http POST localhost:8000/blog/post/1/approved/ message='this is now approved'
{"name": "updated", "state": "approved"}
# view history
$ http GET localhost:8000/blog/post/1/history/
[
{
"created_at": "2018-01-29T17:00:00.000Z",
"new_state": "approved",
"previous_state": "draft",
"meta": {
"message": "this is now approved"
}
"user": null
}
]
Admin Integration
Support for admin via FlowAdmin
- limits available state choices based on transitions and shows inline historical state changes:
# example/admin.py
from django.contrib import admin
from yoflow.admin import FlowAdmin
from example import models, flows
@admin.register(models.Post)
class ParentAdmin(FlowAdmin):
flow = flows.PostFlow
list_display = ('name', 'state')
list_filter = ('state',)
Settings
YOFLOW_STATE_MAX_LENGTH
Max length of CharField
used to store value of before/after state transition. Default 256
YOFLOW_OBJECT_ID_MAX_LENGTH
Max length of CharField
used to store object pk. Usually this will be an integer primary key but in some cases you might wish to use uuid or something else. Default 256
YOFLOW_DEFAULT_STATE_FIELD
Default name of state field - useful if you following naming conventions and frequently use the same name for choices state field rather than defining in every view. Default ‘state’
YOFLOW_TYPE_ERROR
Type of exception to raise when invalid meta data sent to yoflow history. If using django-rest-framework it is useful to use APIException as this provides nice JSON resposnes. Default TypeError