ostinato.statemachine¶
Overview¶
Ostinato includes a statemachine that will allow you to create complex
workflows for your models. A common workflow, for example, is a publishing
workflow where an item can be either private
or public
. The change from
the one state to the next is called a transition.
In ostinato our main aim was to have the ability to “attach” a statemachine to a model, without having to change any fields on that model. So you can create your States and StateMachines completely independent of your models, and just attach it when needed.
Ok, lets build an actual statemachine so you can see how it works. For this example we will create the following statemachine:
For our example we will assume you are creating a statemachine for the following model:
class NewsItem(models.Model):
title = models.CharField(max_length=150)
content = models.TextField()
publish_date = models.DateTimeField(null=True, blank=True)
state = models.CharField(max_length=50, default='private')
We start by creating our States…
1 2 3 4 5 6 7 8 9 10 11 12 13 | from ostinato.statemachine import State, StateMachine
class Private(State):
verbose_name = 'Private'
transitions = {'publish': 'public'}
class Public(State):
verbose_name = 'Public'
transitions = {'retract': 'private', 'archive': 'archived'}
class Archived(State):
verbose_name = 'Archived'
transitions = {}
|
This is simple enough. Every state is a subclass of
ostinato.statemachine.core.State
and each of these states specifies two
attributes.
verbose_name
is just a nice human readable name.transitions
is a dict where the keys are transition/action names, and- the values is the target state for the transition.
Now we have to glue these states together into a statemachine.
1 2 3 | class NewsWorkflow(StateMachine):
state_map = {'private': Private, 'public': Public, 'archived': Archived}
initial_state = 'private'
|
state_map
is a dict where keys are unique id’s/names for the states;- values are the actual
State
subclass
initial_state
is the starting state key
Thats all you need to set up a fully functioning statemachine.
Lets have a quick look at what this allows you to do:
>>> from odemo.news.models import NewsItem, NewsWorkflow
# We need an instance to work with. We just get one from the db in this case
>>> item = NewsItem.objects.get(id=1)
>>> item.state
u'public'
# Create a statemachine for our instance
>>> sm = NewsWorkflow(instance=item)
# We can see that the statemachine automatically takes on the state of the
# newsitem instance.
>>> sm.state
'Public'
# We can view available actions based on the current state
>>> sm.actions
['retract', 'archive']
# We can tell the statemachine to take action
>>> sm.take_action('retract')
# State is now changed in the statemachine ...
>>> sm.state
'Private'
# ... and we can see that our original instance was also updated.
>>> item.state
'private'
>>> item.save() # Now we save our news item
Custom Action methods¶
You can create custom action methods for states, which allows you to do extra stuff, like updating the publish_date.
Our example NewsItem
already has a empty publish_date
field, so lets
create a method that will update the publish date when the publish
action
is performed.
1 2 3 4 5 6 7 8 9 | from django.utils import timezone
class Private(State):
verbose_name = 'Private'
transitions = {'publish': 'public'}
def publish(self, **kwargs):
if self.instance:
self.instance.publish_date = timezone.now()
|
Now, whenever the publish
action is called on our statemachine, it will
update the publish_date
for the instance that was passed to the
StateMachine
when it was created.
Note
The name of the method is important. The State
class tries to look
for a method with the same name as the transition
key.
You can use the kwargs
to pass extra arguments to your custom methods.
These arguments are passed through from the StateMachine.take_action()
method eg.
sm.take_action('publish', author=request.user)
Admin Integration¶
Integrating your statemachine into the admin is quite simple. You just need to use the statemachine form factory function that generates the form for your model, and then use that form in your ModelAdmin.
1 2 3 4 5 6 7 8 9 10 11 12 13 | from odemo.news.models import NewsItem, NewsWorkflow
from ostinato.statemachine.forms import sm_form_factory
class NewsItemAdmin(admin.ModelAdmin):
form = sm_form_factory(NewsWorkflow)
list_display = ('title', 'state', 'publish_date')
list_filter = ('state',)
date_hierarchy = 'publish_date'
admin.site.register(NewsItem, NewsItemAdmin)
|
Lines 2 and 6 are all that you need. sm_form_factory
takes as it’s first
argument your Statemachine Class.
Custom state_field
¶
The statemachine assumes by default that the model field that stores the state
is called, state
, but you can easilly tell the statemachine (and the
statemachine form factory function) what the field name for the state will be.
- Statemachine -
sm = NewsWorkflow(instance=obj, state_field='field_name')
- Form Factory -
sm_form_factory(NewsWorkflow, state_field='field_name')