Transition definitions
Core ideas
- Store a state in an
FSMField(orFSMIntegerField/FSMKeyField). - Declare transitions once with the
@transitiondecorator. - Transition methods can contain business logic and side effects.
- The in-memory state changes on success;
save()persists it.
Declaring a transition
from django_fsm import transition
@transition(field=state, source='new', target='published')
def publish(self, **kwargs):
"""
This function may contain side effects,
like updating caches, notifying users, etc.
The return value will be discarded.
"""
The field parameter accepts a string attribute name or a field instance.
If calling publish() succeeds without raising an exception, the state
changes in memory. You must call save() to persist it.
from django_fsm import can_proceed
def publish_view(request, post_id, **kwargs):
post = get_object_or_404(BlogPost, pk=post_id)
if not can_proceed(post.publish):
raise PermissionDenied
post.publish()
post.save()
return redirect('/')
Source and target states
source accepts a list of states, a single state, or a django_fsm.State
implementation.
source='*'allows switching totargetfrom any state.source='+'allows switching totargetfrom any state excepttarget.
target can be a specific state or a django_fsm.State implementation.
Preconditions (conditions)
Use conditions to restrict transitions. Each function receives the
instance and must return truthy/falsey. The functions should not have
side effects.
def can_publish(instance):
# No publishing after 17 hours
return datetime.datetime.now().hour <= 17
class XXX(FSMModelMixin, models.Model):
@transition(
field=state,
source='new',
target='published',
conditions=[can_publish]
)
def publish(self, **kwargs):
pass
You can also use model methods:
class XXX(FSMModelMixin, models.Model):
def can_destroy(self):
return self.is_under_investigation()
@transition(
field=state,
source='*',
target='destroyed',
conditions=[can_destroy]
)
def destroy(self, **kwargs):
pass
Permissions
Attach permissions to transitions with the permission argument. It accepts
a permission string or a callable that receives (instance, user).
@transition(
field=state,
source='*',
target='published',
permission=lambda instance, user: not user.has_perm('myapp.can_make_mistakes'),
)
def publish(self, **kwargs):
pass
@transition(
field=state,
source='*',
target='removed',
permission='myapp.can_remove_post',
)
def remove(self, **kwargs):
pass
Check permission with has_transition_perm:
from django_fsm import has_transition_perm
def publish_view(request, post_id):
post = get_object_or_404(BlogPost, pk=post_id)
if not has_transition_perm(post.publish, request.user):
raise PermissionDenied
post.publish()
post.save()
return redirect('/')
Dynamic targets
Use RETURN_VALUE or GET_STATE to compute a target state at runtime.
from django_fsm import FSMField, transition, RETURN_VALUE, GET_STATE
@transition(
field=state,
source='*',
target=RETURN_VALUE('for_moderators', 'published'),
)
def publish(self, is_public=False, **kwargs):
return 'for_moderators' if is_public else 'published'
@transition(
field=state,
source='for_moderators',
target=GET_STATE(
lambda self, allowed: 'published' if allowed else 'rejected',
states=['published', 'rejected'],
),
)
def moderate(self, allowed, **kwargs):
pass
@transition(
field=state,
source='for_moderators',
target=GET_STATE(
lambda self, **kwargs: 'published' if kwargs.get('allowed', True) else 'rejected',
states=['published', 'rejected'],
),
)
def moderate(self, allowed=True, **kwargs):
pass
Custom transition metadata
Use custom to attach arbitrary data to a transition.
@transition(
field=state,
source='*',
target='onhold',
custom=dict(verbose='Hold for legal reasons'),
)
def legal_hold(self, **kwargs):
pass
Error target state
If a transition method raises an exception, you can specify an on_error
state.
@transition(
field=state,
source='new',
target='published',
on_error='failed'
)
def publish(self, **kwargs):
"""
Some exception could happen here
"""