Transition options

Source

You can specify a single transition source or list, tuple, set of source states. There is the special State.ANY marker, allows to specify a transition from any state except current to a selected target

@stage.transition(source={Stage.NEW, Stage.DONE}, target=Stage.CANCELED)
def cancel(self):
    pass

Target

A target could be a single value. State change happens before the actual function call, that makes possible to chain transition methods execution.

@stage.transition(source=Stage.NEW, target=Stage.IN_PROCESS)
def pay(self):
    try:
        self.perform_payment()
    except:
        self.error()

@stage.transition(source=Stage.IN_PROCESS, target=Stage.DONE)
def perform_payment(self):
    pass

@stage.transition(source=State.IN_PROCESS, target=Stage.ERROR)
def payment_error(self):
    pass

If target is not provided, no state change would happen.

@stage.transition(source=Stage.NEW)
def notify(self):
    mail_admins('New flow is waiting', self.text)

To specify different source/target on a single method, multiple transition decorators could be used

@stage.transition(source=Stage.NEW, target=State.DONE)
@stage.transition(source=Stage.DONE, target=State.NEW)
def toggle(self):
    pass

Label

Human readable transition label could be specified as .label attribute of a transition method, or passed as parameter to @State.transition decorator

@stage.transition(source=Stage.NEW, target=State.DONE)
@stage.transition(source=Stage.DONE, target=State.NEW, label=_("Toggle back to New"))
def toggle(self):
    pass
toggle.label = _("Toggle report state")

Conditions

If some conditions are required to be met before changing the state, use the conditions argument to transition. conditions must be a list of functions taking one argument, the model instance. The function must return either True or False or a value that evaluates to True or False. If all functions return True, all conditions are considered to be met and the transition is allowed to happen. If one of the functions returns False, the transition will not happen. These functions should not have any side effects.

You can use ordinary functions

def can_publish(instance):
    # No publishing after 17 hours
    if datetime.datetime.now().hour > 17:
        return False
    return True

Or model methods

def can_destroy(self):
    return self.is_under_investigation()

Use the conditions like this:

@state.transition(Stage.NEW, target=State.DONE, conditions=[this.can_publish])
def publish(self):
    pass

Permissions

It is common to have permissions attached to each model transition. State handles this with permission keyword on the transition decorator. permission accepts a this-reference or callable that expects instance and user arguments and returns True if the user can perform the transition.

@stage.transition(
    source=Stage.NEW,
    target=State.DONE
    permission=lambda flow, user: user.has_perm('myapp.delete_review', obj=flow.report)
)
def remove(self):
    pass

@stage.transition(source=Stage.NEW, target=State.DONE, permission=this.is_owner)
def hide(self):
    pass

def is_owner(self, user):
    return self.author == user

Usage:

if not flow.remove.has_permission(request.user):
    raise PermissionDenied

Custom properties

Custom properties can be added by providing a dictionary to the custom keyword on the transition decorator.

@state.transition(
    field=state,
    source=STATE.ANY,
    target=STATE.ON_HOLD,
    custom=dict(verbose='Hold for legal reasons'))
def legal_hold(self):
    """
    Side effects galore
    """