Welcome | Get started | Dive into Lino | Contribute | Topics | Reference | More

Writing custom actions

A custom action is an action written by the application or plugin developer.

Examples of custom actions defined by certain libraries:

You can define actions

  • either on the Model or on the Table

  • either using the dd.action decorator on a method or by defining a custom subclass of Action (and adding an instance of this class to the Model or the Table)

Defining custom actions

Application developers can define new custom actions by (1) applying the action() decorator to a model or table method, or (2) by subclassing the Action class and instantiating them as an attribute of the model or table.

The Polls tutorial has a usage example of the first approach:

@dd.action(help_text="Click here to vote this.")
def vote(self, ar):
    def yes(ar):
        self.votes += 1
        self.save()
        return ar.success(
            "Thank you for voting %s" % self,
            "Voted!", refresh=True)
    if self.votes > 0:
        msg = "%s has already %d votes!" % (self, self.votes)
        msg += "\nDo you still want to vote for it?"
        return ar.confirm(yes, msg)
    return yes(ar)

The @dd.action decorator can have keyword parameters to specify information about the action, e.g. label, help_text and required.

The action method itself must have the following signature:

def vote(self, ar, **kw):
    ...
    return ar.success(kw)

Where ar is an ActionRequest instance that holds information about the web request which called the action.

  • callback and confirm lets you define a dialog with the user using callbacks.

  • success and error are possible return values where you can ask the client to do certain things.

The action() decorator

You can use the action() decorator to to define custom actions.

Any arguments you pass to the decorator are forwarded to Action.__init__(). In practice you'll possibly use: label, help_text and required_roles.

The Lino Polls tutorial shows the simplest form of defining an action by adding the action decorator to a method.

The following two approaches are equivalent. Using a custom class:

class MyAction(dd.Action):

    def run_from_ui(self, ar):
        # do something...

class MyModel(dd.Model):
    my_action = MyAction()

Using a decorator:

class MyModel(dd.Model):

    @dd.action()
    def my_action(self, ar):
        # do something...

In above case (and in some real cases) it might look tedious and redundant to define an action class and then instantiate it on the model. But in general we recommend this more verbose approach. We use the primitive approach (just a method on the model) only in very simple cases.

The advantages become visible e.g. when you have several similar actions and want them to inherit from a common base class. Or we can reuse a same action class on different models (most standard actions like lino.core.actions.ShowInsert do this). Or we have actions where we use instances of a same class with different instance values (e.g. the lino.core.actions.ShowSlaveTable action). Also an explicit separate class it is syntactically more readable.

Example project

The lino_book.projects.actions project shows some methods of defining actions. Here is the models.py file used for this small demo project:

from django.db import models
from django.utils.translation import gettext_lazy as _

from lino.api import dd, rt


class A(dd.Action):
    label = _("a")

    def run_from_ui(self, ar, **kw):
        obj = ar.selected_rows[0]
        return ar.success("Called a() on %s" % obj)


class Moo(dd.Model):
    a = A()

    def __str__(self):
        return '%s object' % (self.__class__.__name__)

    @dd.action(_("m"))
    def m(self, ar, **kw):
        return ar.success("Called m() on %s" % self)


class Moos(dd.Table):
    model = Moo

    b = A()

    @dd.action(_("t"))
    def t(obj, ar, **kw):
        return ar.success("Called t() on %s" % obj)


class S1(Moos):
    pass


class S2(Moos):
    a = None
    b = None
    m = None
    t = None
>>> from lino import startup
>>> startup('lino_book.projects.actions.settings')
>>> from lino.api.doctest import *
>>> from lino_book.projects.actions.models import *

To demonstrate this, we log in and instantiate an Moo object: >>> ses = rt.login() >>> obj = Moo()

Running an action programmatically is done using the run method of your session.

Since a and m are defined on the Model, we can run them directly:

>>> rv = ses.run(obj.a)
>>> print(rv["message"])
Called a() on Moo object
>>> rv["success"]
True
>>> print(ses.run(obj.m)['message'])
Called m() on Moo object

This wouldn't work for t and b since these are defined on Moos (which is only one of many possible tables on model Moo):

>>> ses.run(obj.t)
Traceback (most recent call last):
...
AttributeError: 'Moo' object has no attribute 't'

So in this case we need to specify them table as the first parameter. And because they are row actions, we need to pass the instance as mandatory first argument:

>>> print(ses.run(S1.t, obj)['message'])
Called t() on Moo object
>>> print(ses.run(S1.b, obj)['message'])
Called a() on Moo object

How to "remove" an inherited action or collected from a table

Here are the actions on Moos:

>>> pprint([ba.action for ba in Moos.get_actions()])
... 
[<lino.modlib.bootstrap3.models.ShowAsHtml show_as_html ('HTML')>,
 <lino.core.actions.CreateRow grid_post>,
 <lino.core.actions.SaveGridCell grid_put>,
 <lino.core.actions.SubmitInsert submit_insert ('Create')>,
 <lino.core.actions.DeleteSelected delete_selected ('Delete')>,
 <lino_book.projects.actions.models.A a ('a')>,
 <lino_book.projects.actions.models.A b ('a')>,
 <lino.core.actions.ShowTable grid>,
 <lino.core.actions.Action m ('m')>,
 <lino.core.actions.Action t ('t')>]

A subclass inherits all actions from her parent. When I define a second table S1(Moos), then S1 will have both actions m and t:

>>> pprint([ba.action for ba in S1.get_actions()])
... 
[<lino.modlib.bootstrap3.models.ShowAsHtml show_as_html ('HTML')>,
 <lino.core.actions.CreateRow grid_post>,
 <lino.core.actions.SaveGridCell grid_put>,
 <lino.core.actions.SubmitInsert submit_insert ('Create')>,
 <lino.core.actions.DeleteSelected delete_selected ('Delete')>,
 <lino_book.projects.actions.models.A a ('a')>,
 <lino_book.projects.actions.models.A b ('a')>,
 <lino.core.actions.ShowTable grid>,
 <lino.core.actions.Action m ('m')>,
 <lino.core.actions.Action t ('t')>]

S2 does not have these actions because we "removed" them by overriding them with None:

>>> pprint([ba.action for ba in S2.get_actions()])
... 
[<lino.modlib.bootstrap3.models.ShowAsHtml show_as_html ('HTML')>,
 <lino.core.actions.CreateRow grid_post>,
 <lino.core.actions.SaveGridCell grid_put>,
 <lino.core.actions.SubmitInsert submit_insert ('Create')>,
 <lino.core.actions.DeleteSelected delete_selected ('Delete')>,
 <lino.core.actions.ShowTable grid>]

Dialog actions

When you specify parameters on a custom action, then your action becomes a "dialog action". When a user invokes a dialog action, Lino opens a dialog window which asks for the values of these parameters. The action itself is being run only when the user submits the dialog window.

Examples of dialog actions:

  • users.Users.change_password

  • pcsw.Clients.refuse_client

  • countries.Places.merge_row

  • contacts.Persons.create_household

  • coachings.Coachings.create_visit

  • cal.Guests.checkin

  • lino_xl.lib.trading.VatProductInvoice.make_copy MakeCopy