Welcome | Get started | Dive | Contribute | Topics | Reference | Changes | More
Introduction to Workflows¶
This tutorial explains how to define and use workflows.
This tutorial extends the application created in watch – Watching database changes, so you might want follow that tutorial before reading on here.
About this document¶
Examples on this page use the lino_book.projects.workflows
sample application.
>>> 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
(optional) attribute button_text
.
# -*- 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 ☒
======= =========== =========== =============
Defining transitions¶
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
the Site’s workflows_module
.
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.
The get_action_permission
method, as used by
the 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
server startup.
Why do I have to declare workflow_state_field
?¶
The workflow_state_field
is required if you want
your permission handlers to get the state argument.
In models.py
there are only a few changes (compared to
watch – Watching database changes), first we need import the EntryStates
workflow,
and then add a state
field to the Entry
model:
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 table, and in a
column_names attribute to EntriesByCompany and MyEntries. Note that
workflow_buttons is a virtual field and therefore
not automatically shown.