Authors:
- Jonathan Slenders, City Live nv
- Gert van Gool, City Live nv
- Maarten Timmerman, City Live nv
- Steven (rh0dium), Pivotal Engergy Solutions, Phoenix Arizona
State engine for django models. Define a state graph for a model and remember the state of each object. State transitions can be logged for objects.
pip install django-states
It's basically these two things:
- Derived your model from
StateModel
- Add a
Machine
class to your model, for the state machine
from django_states.models import StateMachine, StateDefinition, StateTransition
from django_states.models import StateModel
class PurchaseStateMachine(StateMachine):
log_transitions = True
# possible states
class initiated(StateDefinition):
description = _('Purchase initiated')
initial = True
class paid(StateDefinition):
description = _('Purchase paid')
def handler(self, instance):
code_to_execute_when_arriving_in_this_state()
class shipped(StateDefinition):
description = _('Purchase shipped')
# state transitions
class mark_paid(StateTransition):
from_state = 'initiated'
to_state = 'paid'
description = 'Mark this purchase as paid'
class ship(StateTransition):
from_state = 'paid'
to_state = 'shipped'
description = 'Ship purchase'
def handler(transition, instance, user):
code_to_execute_during_this_transition()
def has_permission(transition, instance, user):
return true_when_user_can_make_this_transition()
class Purchase(StateModel):
Machine = PurchaseStateMachine
... (other fields for a purchase)
You may of course nest the Machine
class, like you would usually do
for Meta
.
This will create the necessary models. If log_transitions
is
enabled, another model is created. Everything should be compatible with
South_ for migrations.
Note: If you're creating a DataMigration
in South,
remember to use obj.save(no_state_validation=True)
Usage example::
p = Purchase()
# Will automatically create state object for this purchase, in the
# initial state.
p.save()
p.make_transition('initiate', request.user) # User parameter is optional
p.state # Will return 'paid'
p.state_description # Will return 'Purchase paid'
# Will return all the state transitions for this instance.
p.state_transitions.all()
# The user who triggered this transition
p.state_transitions.all()[0].user
# Will return 'complete' or 'failed', depending on the state of this state transition.
p.state_transitions.all()[0].state
# Returns an iterator of possible transitions for this purchase.
p.possible_transitions
# Which can be used like this..
[x.get_name() for x in p.possible_transitions]
For better transition control, override:
has_permission(self, instance, user)
: Check whether this user is allowed to make this transition.handler(self, instance, user)
: Code to run during this transition. When an exception has been raised in here, the transition will not be made.
Get all objects in a certain state::
Purchase.objects.filter(state='initiated')
You can add a test that needs to pass before a state transition can be
executed. Well, you can add 2: one based on the current user
(has_permission
) and one generic (validate
).
So on a StateTransition
-object you need to specify an extra validate
function (signature is validate(cls, instance)
). This should yield
TransitionValidationError
, this way you can return multiple errors on
that need to pass before the transition can happen.
The has_permission
function (signature has_permission(transition, instance, user)
) should check whether the given user is allowed to make the
transition. E.g. a super user can moderate all comments while other users can
only moderate comments on their blog-posts.
Sometimes you want to group several states together, since for a certain view (or other content) it doesn't really matter which of the states it is. We support 2 different state groups, inclusive (only these) or exclusive (everything but these)::
class is_paid(StateGroup):
states = ['paid', 'shipped']
class is_paid(StateGroup):
exclude_states = ['initiated']
By specifying actions for the Django Admin
(see admin actions), you can do
state transitions for the admin site. To support this in your model, update
your ModelAdmin
:
class PurchaseAdmin(admin.ModelAdmin):
actions = Purchase.Machine.get_admin_actions()
If your model didn't inherit from StateModel
, you can also specify the
field_name
:
class PurchaseAdmin(admin.ModelAdmin):
actions = Purchase.Machine.get_admin_actions(field_name='purchase_state')
You can get a graph of your states by running the graph_states
management
command.
python manage.py graph_states myapp.Purchase.state
This requires graphviz and python bindings for
graphviz: pygraphviz
and yapgvb
.