Introduction to plugins

Besides the Site class (which encapsules what Lino calls an application), Lino defines the Plugin class which extends what Django calls an "application".

The Plugin class is comparable to Django's AppConfig class, but has some advantages over Django's approach which makes that they are the preferred way.

What is a plugin?

A plugin is a Python package which can be yielded by get_installed_apps.

A plugin encapsulates a limited set of functionality designed to be potentially used in more than on application.

A plugin can define database models, actors, actions, fixtures, template files, javascript snippets, and metadata. None of these components are mandatory.

The metadata about a plugin (configuration values, menu commands, dependencies, ...) is specified by defining a subclass of Plugin in the __init__.py file of your plugin.

Here is a fictive example:

from lino.api import ad, _

class Plugin(ad.Plugin):
    verbose_name = _("Better calendar")
    extends = 'lino.modlib.cal'
    needs_plugins  = ['lino_xl.lib.contacts']

    def setup_main_menu(self, site, user_type, m):
        m = m.add_menu(self.app_label, self.verbose_name)
        m.add_action('cal.Teams')
        m.add_action('cal.Agendas')

A plugin can depend on other plugins by specifying them in the needs_plugins attribute. This means that when you install this plugin, Lino will automatically install these other plugins as well

A plugin can define a set of menu commands using methods like setup_main_menu. This is explained in The menu tree.

And last but not least, a plugin can extend another plugin by specifying its name in extends_models. This is explained in Plugin inheritance.

Accessing plugins

Django developers are used to code like this:

from myapp.models import Foo

def print_foo(pk=1):
    print(Foo.objects.get(pk=pk))

In Lino we recommend to use the rt.models dict as follows:

from lino.api import rt

def print_foo(pk=1):
    Foo = rt.models.myapp.Foo
    print(Foo.objects.get(pk=pk))

At least if you want to use Plugin inheritance. One of the basic reasons for using plugins is that users of some plugin can extend it and use their extension instead of the original plugin. Which means that the plugin developer does not know (and does not want to know) where the model classes are actually defined.

Note that rt.models is populated only after having imported the models. So you cannot use it at the module-level namespace of a models.py module. For example the following variant of above code would not work:

from lino.api import rt
Foo = rt.models.foos.Foo  # error `AttrDict has no item "foos"`
def print_foo(pk=1):
    print(Foo.objects.get(pk=pk))

Configuring plugins

Plugins can have attributes for holding configurable options.

Examples of configurable plugin attributes:

The values of plugin attributes can be configured at three levels.

As a plugin developer you specify a hard-coded default value.

As an application developer you can specify default values in your application* by overriding the Site.get_plugin_configs() or the Site.setup_plugins() method of your Site class. For example:

class Site(Site):

    def get_plugin_configs(self):
        yield super(Site, self).get_plugin_configs()
        yield ('countries', 'country_code', 'BE')
        yield ('contacts', 'hide_region', True)

The old style works also:

class Site(Site):

    def setup_plugins(self):
        super(Site, self).setup_plugins()
        self.plugins.countries.configure(country_code='BE')
        self.plugins.contacts.configure(hide_region=True)

Note that Site.setup_plugins() is called after Site.get_plugin_configs(). This can cause unexpected behaviour when you mix both methods.

As a system administrator you can override these configuration defaults in your project's settings.py using one of the following methods:

  • by overriding the Site class as described above for application developers

  • by setting the value directly after instantiation of your SITE object.

Another (deprecated) method is by using the configure_plugin() function. For example:

from lino_cosi.lib.cosi.settings import *
configure_plugin('countries', country_code='BE')
SITE = Site(globals())

Beware the pitfall: configure_plugin() must be called before the SITE has been instantiated, otherwise they will be ignored silently. (It is not easy to prevent accidental calls to it after Site initialization because there are scenarios where you want to instantiate several Site objects.)

Keep in mind that you can indeed never be sure that your SITE instance is actually being used. A local system admin can always decide to import your settings.py module and to re-instantiate your Site class another time. That's part of our game and we don't want it to be forbidden.