Introduction to Workflows¶
This tutorial explains how to define and use workflows.
This tutorial extends the application created in Watching database changes, so you might want follow that tutorial before reading on here.
About this document¶
Examples on this page use the
>>> from lino import startup >>> startup('lino_book.projects.workflows.settings') >>> from lino.api.doctest import *
You can play with this application by installing the latest
development version of Lino, then
cd to the workflows demo project
where you can run:
$ go workflows $ python manage.py prep $ python manage.py runserver
What is a workflow?¶
A workflow in Lino is defined by (1) a "state" field on a model and (2) a list of "transitions", i.e. actions which change a given object from one state to another state.
For example consider this workflow:
It consists of five states (New, Started, Done, Sleeping, Cancelled) and a series of transitions.
Defining the states¶
We use a choicelist to define the states.
That choicelist must be a special kind of choicelist: a
Workflow. The choices of a
workflow are instances of
State and behave mostly like normal
choices, but have an additional method
add_transition and an additional
# -*- coding: UTF-8 -*- # Copyright 2013-2021 Rumma & Ko Ltd # License: GNU Affero General Public License v3 (see file COPYING for details) from lino.api import dd, _ class EntryStates(dd.Workflow): pass add = EntryStates.add_item add('10', _("New"), 'new', button_text="☐") add('20', _("Started"), 'started', button_text="⚒") add('30', _("Done"), 'done', button_text="☑") add('40', _("Sleeping"), 'sleeping', button_text="☾") add('50', _("Cancelled"), 'cancelled', button_text="☒")
Here is the result of above definition:
>>> rt.show(entries.EntryStates) ======= =========== =========== ============= value name text Button text ------- ----------- ----------- ------------- 10 new New ☐ 20 started Started ⚒ 30 done Done ☑ 40 sleeping Sleeping ☾ 50 cancelled Cancelled ☒ ======= =========== =========== =============
We define the transitions in a separate module:
# Copyright 2013-2021 Rumma & Ko Ltd # License: GNU Affero General Public License v3 (see file COPYING for details) from django.utils.translation import gettext_lazy as _ from .choicelists import EntryStates from .actions import WakeupEntry, StartEntry, FinishEntry # EntryStates.new.add_transition( # _("Reopen"), required_states='done cancelled') EntryStates.new.add_transition(WakeupEntry) EntryStates.started.add_transition(StartEntry) EntryStates.sleeping.add_transition(required_states="new") EntryStates.done.add_transition(FinishEntry) EntryStates.cancelled.add_transition( required_states='sleeping started', help_text=_("""This is a rather verbose help text for the action which triggers transition from 'sleeping' or 'started' to 'cancelled'."""), icon_name='cancel')
This module is being loaded at startup because its name is defined in
Defining explicit transition actions¶
Note how the first argument to
add_transition can be either a string or
a class. If it is a class, then it defines an action. Here are the
actions used in this tutorial:
# Copyright 2013-2018 Rumma & Ko Ltd # License: GNU Affero General Public License v3 (see file COPYING for details) from django.utils.translation import gettext_lazy as _ from lino.api import dd, rt from lino.modlib.notify.actions import NotifyingAction class StartEntry(dd.ChangeStateAction): label = _("Start") help_text = _("This action is not allowed when company, body or subject is empty.") required_states = 'new cancelled' def get_action_permission(self,ar,obj,state): # cannot start entries with empty company, subject or body fields if not obj.company or not obj.subject or not obj.body: return False return super(StartEntry,self).get_action_permission(ar, obj, state) class FinishEntry(StartEntry): icon_name='accept' label = _("Finish") required_states = 'new started' help_text = _("Inherts from StartEntry and thus is not allowed when company, body or subject is empty.") class WakeupEntry(dd.ChangeStateAction, NotifyingAction): label = _("Reopen") # label = _("Wake up") # required_states = 'sleeping' # in our example, waking up an antry will send a notification def get_notify_recipients(self, ar, obj): if not obj.state.name in ('done', 'cancelled'): return for u in rt.models.users.User.objects.all(): yield (u, u.mail_mode) def get_notify_subject(self, ar, obj): return _("Entry %s has been reactivated!") % obj
Defining an explicit class for transition action is useful when you want your application to decide at runtime whether a transition is available or not.
get_action_permission method, as used by
StartEntry action in our example. It is called once for every
record and thus should not take too much energy. In the example
application, you can see that the "Start" action is not shown for
entries with one of the company, subject or body fields empty. Follow
the link to the API reference for details.
Note that if you test only the user_type of the requesting user, or
some value from
settings, then you'll rather define a
get_view_permission method. That's more
efficient because it is called only once for every user user_type at
Why do I have to declare
workflow_state_field is required if you want
your permission handlers to get the state argument.
models.py there are only a few changes (compared to
Watching database changes), first we need import the
and then add a
state field to the
from .choicelists import EntryStates ... class Entry(dd.CreatedModified, dd.UserAuthored): workflow_state_field = 'state' ... state = EntryStates.field(blank=True, default='todo')
Showing the workflow actions¶
And finally we added the
workflow_buttons at different places: in the
detail_layout of the
Entries data view, and in a
column_names attribute to EntriesByCompany and MyEntries. Note that
workflow_buttons is a virtual field and therefore
not automatically shown.