Welcome | Get started | Dive | Contribute | Topics | Reference | Changes | More

How plugins build the application menu

We have seen the application menu. And we have mentioned the Lino Extensions Library, a library of shared plugins that are designed to work together. One aspect of this collaboration is how to integrate the functionalities into the application menu.

Side note: Code snippets (lines starting with >>>) in this document get tested as part of our development workflow. The following initialization snippet tells you which demo project is being used in this document.

>>> from lino import startup
>>> startup('lino_book.projects.tera1.settings.demo')
>>> from lino.api.doctest import *

The lino_book.projects.tera1 project has the following menu:

>>> show_menu('robin')  
- Office : My Excerpts, My Upload files
- Therapies : My Therapies, -, Individual therapies, Life groups, Other groups, -, My Patients, My Notes
- Contacts : Persons, Organizations, Households, Patients, Partner Lists
- Calendar : My appointments, Overdue appointments, My unconfirmed appointments, My tasks, My guests, My presences, My overdue appointments, My cash roll, Calendar
- Invoicing : My invoicing plan, Sales invoices (SLS), Sales credit notes (SLC)
- Accounting :
  - Purchases : Purchase invoices (PRC)
  - Wages : Paychecks (SAL)
  - Financial : Bestbank Payment Orders (PMO), Cash book (CSH), Bestbank (BNK)
  - VAT : VAT declarations (VAT)
  - Miscellaneous transactions : Miscellaneous transactions (MSC), Preliminary transactions (PRE)
- Reports :
  - Therapies : Status Report
  - Invoicing : Due invoices
  - Accounting : Purchase journal (analytic), Analytic Account Balances, Accounting Report, Debtors, Creditors
  - VAT : Intra-Community purchases, Intra-Community sales
- Configure :
  - System : Users, Divisions, Site configuration, System tasks
  - Office : Excerpt Types, My Text Field Templates, Library volumes, Upload types
  - Therapies : Dossier types, Topics, Note Types, Event Types
  - Places : Countries, Places
  - Contacts : Legal forms, Functions, Household Types, Healthcare plans, Healthcare rules, Procurers, Life modes, List Types
  - Clients : Client Contact types
  - Invoicing : Fees, Cash daybooks, Price rules, Paper types, Flatrates, Follow-up rules, Invoicing tasks
  - Calendar : Calendars, Rooms, Recurring events, Guest roles, Service types, Recurrency policies, Remote Calendars, Planner rows
  - Accounting : Analytical accounts, Sheet items, Fiscal years, Accounting periods, Accounts, Journals, Payment terms
  - Topics : Topics
- Explorer :
  - System : Authorities, User types, User roles, content types, All dashboard widgets, Background procedures, Data checkers, Data problem messages
  - Office : Excerpts, Text Field Templates, Mentions, Upload files, Upload areas
  - Therapies : Dossiers, Enrolments, Enrolment states, Course layouts, Dossier states, Notes
  - Contacts : Contact persons, Partners, Household member roles, Household Members, Healthcare tariffs, Healthcare situations, List memberships
  - Clients : Client Contacts, Known contact types
  - Invoicing : Price factors, Sales invoices, Sales invoice items, Invoicing plans, Sales rules
  - Calendar : Calendar entries, Tasks, Presences, Subscriptions, Entry states, Presence states, Task states, Planner columns, Display colors
  - SEPA : Bank accounts
  - Financial : Bank Statements, Journal Entries, Payment Orders
  - Accounting : Purchase invoices, Accounting Reports, Common sheet items, General account balances, Analytic accounts balances, Partner balances, Sheet item entries, Common accounts, Match rules, Vouchers, Voucher types, Movements, Trade types, Journal groups
  - Topics : Tags
  - VAT : Special Belgian VAT declarations, Declaration fields, VAT areas, VAT regimes, VAT classes, VAT columns, Invoices, VAT rules
- Site : About, User sessions

This menu is not defined by overriding the setup_menu method. It was “automatically” generated by the installed plugins. Because as an application developer you wouldn’t want to maintain such a menu for each application.

Let’s explore how this works.

The top-level menus

The default implementation of setup_menu uses a set of predefined top-level menus:

Main menu

The root menu of the application menu, which contains a series of sub-menus called top-level menus.

Master menu

A standard top-level menu meant to contain commands for editing “master data”, i.e. data that is visible to normal users but doesn’t change very often.

Configuration menu

A standard top-level menu meant to contain commands for editing “configuration data”. Actions in this menu usually require some Staff or Admin role.

Explorer menu

A standard top-level menu meant to contain commands for editing “explorer data”. These actions are not meant to be generally useful, but might be interesting when exploring database content.

Site menu

A standard top-level menu meant to contain commands about this site.

These predefined top-level menus are stored in the top_level_menus attribute of your application.

>>> pprint(settings.SITE.top_level_menus)
[('master', 'Master'),
 ('main', None),
 ('reports', 'Reports'),
 ('config', 'Configure'),
 ('explorer', 'Explorer'),
 ('site', 'Site')]

It is a list of tuples with 2 items each. Each tuple has an internal name and a display name. Only the main entry has no display name.

You might specify your own set of top-level menus, but the Lino Extensions Library assumes that your application uses the default value described above.

These top-level menus are “filled” by the different installed plugins.

When a Lino application starts up, it loops over the installed plugins and, for each of them, loops again over these top-level menus and checks whether the plugin has a method called setup_XXX_menu() (where XXX is the top-level menu name).

As a result, with the default top_level_menus, each plugin is checked whether it has one of the following methods:

  • setup_master_menu

  • setup_main_menu

  • setup_reports_menu

  • setup_config_menu

  • setup_explorer_menu

  • setup_site_menu

Introduction to menu groups

The next level after the top-level menu are the menu groups.

menu group

The group to which a plugin belongs in the application menu.

As long as the application developer doesn’t say anything, The default rule is that each plugin creates its own menu group, labelled using the plugin’s verbose_name attribute.

But this internal grouping does not always match how the end users would think about their application.

The application developer can define an explicit menu_group. This is the name of another plugin.

When a plugin was automatically installed because some other plugin needs it (as given by needs_plugins), then it gets grouped together with this other plugin.

When a plugin A is automatically being installed because needed by a plugin B which is itself being installed automatically because needed by a third plugin C, then A.get_menu_group returns C (and not B). A case where this happens is lino_welfare.modlib.pcsw, which needs lino_xl.lib.coachings, which in turn needs lino_xl.lib.clients.

Defining menu items

The setup_XXX_menu() methods of a lino.core.plugin.Plugin are expected to add menu items to some menu.

The lino.modlib.about plugin adds the “About” action to the “Site” menu:

def setup_site_menu(self, site, user_type, m):
    m.add_action(site.models.about.About)
    # or: m.add_action('about.About')

The lino_xl.lib.invoicing plugin adds an action “Invoicing plan” to the “Sales” menu:

def setup_main_menu(self, site, user_type, m):
    mg = site.plugins.sales
    m = m.add_menu(mg.app_label, mg.verbose_name)
    m.add_action('invoicing.MyPlans.start_plan')

The lino.modlib.about plugin also adds a quick link:

def get_quicklinks(site, user):
    yield 'about.SiteSearch'

The resolve_action function

lino.core.actors.resolve_action(spec, action=None)

Return the bound action corresponding to the given specifier spec and the optional action name.

When the specifier is a string, it is resolved as follows:

  • foo.Bar resolves to the value of Bar in the models.py module of plugin foo.

  • foo.Bar.baz resolves to a bound action when Bar is an actor, otherwise to the class attribute baz of Bar.

The resolved specifier can be:

  • a database model

  • an actor

  • an action instance

  • a bound action

An action instance will return the bound action on its Action.defining_actor. A database model will return the default action of the model’s default table. An actor will return its default action.

>>> from lino.core.actors import resolve_action
>>> resolve_action('contacts.Persons')
<BoundAction(contacts.Persons, <lino.core.actions.ShowTable grid>)>
>>> resolve_action('contacts.Person')
<BoundAction(contacts.Persons, <lino.core.actions.ShowTable grid>)>
>>> resolve_action('users.User')
<BoundAction(users.AllUsers, <lino.core.actions.ShowTable grid>)>
>>> resolve_action(rt.models.about.About)
<BoundAction(about.About, <lino.core.actions.ShowEmptyTable show ('Detail')>)>
>>> resolve_action('about.About')
<BoundAction(about.About, <lino.core.actions.ShowEmptyTable show ('Detail')>)>

Here is the lino_xl.lib.invoicing.Plan.execute_plan action:

>>> resolve_action('invoicing.Plan', action='execute_plan')
<BoundAction(invoicing.Plans, <lino_xl.lib.invoicing.actions.ExecutePlan execute_plan ('Execute plan')>)>
>>> resolve_action('invoicing.Plan.execute_plan')
<BoundAction(invoicing.Plans, <lino_xl.lib.invoicing.actions.ExecutePlan execute_plan ('Execute plan')>)>
>>> resolve_action(rt.models.invoicing.Plan.execute_plan)
<BoundAction(invoicing.Plans, <lino_xl.lib.invoicing.actions.ExecutePlan execute_plan ('Execute plan')>)>
>>> resolve_action('foo')
Traceback (most recent call last):
...
Exception: Invalid action specifier 'foo' (must be of form `plugin_name.ClassName[.action_name]`).
>>> resolve_action('foo.bar.baz.trump')
Traceback (most recent call last):
...
Exception: Invalid action specifier 'foo.bar.baz.trump' (must be of form `plugin_name.ClassName[.action_name]`).
>>> resolve_action('')
Traceback (most recent call last):
...
Exception: Invalid action specifier '' (must be of form `plugin_name.ClassName[.action_name]`).
>>> resolve_action('invoicing.Plan', action='trump')
Traceback (most recent call last):
...
Exception: invoicing.Plans has no action named 'trump'
>>> resolve_action('invoicing.Plan.execute_plan', action='trump')
Traceback (most recent call last):
...
Exception: Invalid action specifier 'invoicing.Plan.execute_plan' when action keyword is specified.