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_nameis just a nice human readable name.transitionsis 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_mapis a dict where keys are unique id’s/names for the states;- values are the actual
Statesubclass
initial_stateis 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')