Kolibri plugin architecture

The behavior of Kolibri can be extended using plugins. The following is a guide to developing plugins.

Enabling and disabling plugins

Non-core plugins can be enabled or disabled using the kolibri plugin commands.

How plugins work

From a user’s perspective, plugins are enabled and disabled through the command line interface or through a UI. Users can also configure a plugin’s behavior through the main Kolibri interface.

From a developer’s perspective, plugins are wrappers around Django applications, listed in ACTIVE_PLUGINS on the kolibri config object. They are initialized before Django’s app registry is initialized and then their relevant Django apps are added to the INSTALLED_APPS of kolibri.

Loading a plugin

In general, a plugin should never modify internals of Kolibri or other plugins without using the hooks API or normal conventional Django scenarios.

Note

Each app in ACTIVE_PLUGINS in the kolibri conf is searched for the special kolibri_plugin module.

Everything that a plugin does is expected to be defined through <myapp>/kolibri_plugin.py.

Kolibri Hooks API

What are hooks

Hooks are classes that define something that happens at one or more places where the hook is looked for and applied. It means that you can “hook into a component” in Kolibri and have it do a predefined and parameterized thing. For instance, Kolibri could ask all its plugins who wants to add something to the user settings panel, and its then up to the plugins to inherit from that specific hook and feed back the parameters that the hook definition expects.

The consequences of a hook being applied can happen anywhere in Kolibri. Each hook is defined through a class inheriting from KolibriHook. But how the inheritor of that class deals with plugins using it, is entirely up to each specific implementation and can be applied in templates, views, middleware - basically everywhere!

That’s why you should consult the class definition and documentation of the hook you are adding plugin functionality with.

We have two different types of hooks:

Abstract hooks

Are definitions of hooks that are implemented by implementing hooks. These hooks are Python abstract base classes, and can use the @abstractproperty and @abstractmethod decorators from the abc module in order to define which properties and methods their descendant registered hooks should implement.

Registered hooks

Are concrete hooks that inherit from abstract hooks, thus embodying the definitions of the abstract hook into a specific case. If the abstract parent hook has any abstract properties or methods, the hook being registered as a descendant must implement those properties and methods, or an error will occur.

So what’s “a hook”?

Simply referring to “a hook” is okay, it can be ambiguous on purpose. For instance, in the example, we talk about “a navigation hook”. So we both mean the abstract definition of the navigation hook and everything that is registered for the navigation.

Where can I find hooks?

All Kolibri core applications and plugins alike should by convention define their abstract hooks inside <myapp>/hooks.py. Thus, to see which hooks a Kolibri component exposes, you can refer to its hooks module.

Note

Defining abstract hooks in <myapp>/hooks.py isn’t mandatory, but loading a concrete hook in <myapp>/kolibri_plugin.py is.

Warning

Do not define abstract and registered hooks in the same module. Or to put it in other words: Always put registered hooks in <myapp>/kolibri_plugin.py. The registered hooks will only be initialized for use by the Kolibri plugin registry if they are registered inside the kolibri_plugin.py module for the plugin.

In which order are hooks used/applied?

This is entirely up to the registering class. By default, hooks are applied in the same order that the registered hook gets registered! While it could be the case that plugins could be enabled in a certain order to get a specific ordering of hooks - it is best not to depend on this behaviour as it could result in brittleness.

An example of a plugin using a hook

Note

The example shows a NavigationHook which is simplified for the sake of readability. The actual implementation in Kolibri will differ.

Example implementation

Here is an example of how to use a hook in myplugin.kolibri_plugin.py:

from kolibri.core.hooks import NavigationHook
from kolibri.plugins.hooks import register_hook

@register_hook
class MyPluginNavItem(NavigationHook):
    bundle_id = "side_nav"

The decorator @register_hook signals that the wrapped class is intended to be registered against any abstract KolibriHook descendants that it inherits from. In this case, the hook being registered inherits from NavigationHook, so any hook registered will be available on the NavigationHook.registered_hooks property.

Here is the definition of the abstract NavigatonHook in kolibri.core.hooks:

from kolibri.core.webpack.hooks import WebpackBundleHook
from kolibri.plugins.hooks import define_hook


@define_hook
class NavigationHook(WebpackBundleHook):

    # Set this to True so that the resulting frontend code will be rendered inline.
    inline = True

As can be seen from above, to define an abstract hook, instead of using the @register_hook decorator, the @define_hook decorator is used instead, to signal that this instance of inheritance is not intended to register anything against the parent WebpackBundleHook. However, because of the inheritance relationship, any hook registered against NavigationHook (like our example registered hook above), will also be registered against the WebpackBundleHook, so we should expect to see our plugin’s nav item listed in the WebpackBundleHook.registered_hooks property as well as in the NavigationHook.registered_hooks property.

Usage of the hook

The hook can then be used to collect all the information from the hooks, as per this usage of the NavigationHook in kolibri/core/kolibri_plugin.py:

from kolibri.core.hooks import NavigationHook

...
    def navigation_tags(self):
        return [
            hook.render_to_page_load_sync_html()
            for hook in NavigationHook.registered_hooks
        ]

Each registered hook is iterated over and its appropriate HTML for rendering into the frontend are returned. When iterating over registered_hooks the returned objects are each instances of the hook classes that were registered.

Warning

Do not load registered hook classes outside of a plugin’s kolibri_plugin. Either define them there directly or import the modules that define them. Hook classes should all be seen at load time, and placing that logic in kolibri_plugin guarantees that things are registered correctly.

Defining a plugin

A plugin must have a Python module inside it called kolibri_plugin, inside this there must be an object subclassed from KolibriPluginBase - here is a minimal example:

from kolibri.plugins import KolibriPluginBase

class ExamplePlugin(KolibriPluginBase):
    pass

The Python module that contains this kolibri_plugin module can now be enabled and disabled as a plugin. If the module path for the plugin is kolibri.plugins.example_plugin then it could be enabled by:

kolibri plugin enable kolibri.plugins.example_plugin

The above command can be passed multiple plugin names to enable at once. If Kolibri is running, it needs to be restarted for the change to take effect.

Similarly, to disable the plugin the following command can be used:

kolibri plugin disable kolibri.plugins.example_plugin

To exactly set the currently enabled plugins (disabling all other plugins, and enabling the ones specified) you can do this:

kolibri plugin apply kolibri.plugins.learn kolibri.plugins.default_theme

This will disable all other plugins and only enable kolibri.plugins.learn and kolibri.plugins.default_theme`.

Creating a plugin

Plugins can be standalone Django apps in their own right, meaning they can define templates, models, new urls, and views just like any other app. Any activated plugin is added to the INSTALLED_APPS setting of Django, so any models, templates, or templatetags defined in the conventional way for Django inside an app will work inside of a Kolibri plugin.

In addition, Kolibri exposes some additional functionality that allows for the core URLs, Django settings, and Kolibri options to be extended by a plugin. These are set

class ExamplePlugin(KolibriPluginBase):
    untranslated_view_urls = "api_urls"
    translated_view_urls = "urls"
    options = "options"
    settings = "settings"

These are all path references to modules within the plugin itself, so options would be accessible on the Python module path as kolibri.plugins.example_plugin.options.

untranslated_view_urls, translated_view_urls should both be standard Django urls modules in the plugin that expose a urlpatterns variable - the first will be mounted as API urls - with no language prefixing, the second will be mounted with language prefixing and will be assumed to contain language specific content.

settings should be a module containing Django settings that should be added to the Kolibri settings. This should not be used to override existing settings (and an error will be thrown if it is used in this way), but rather as a way for plugins to add additional settings to the Django settings. This is particularly useful when a plugin is being used to wrap a Django library that requires its own settings to define its behaviour - this module can be used to add these extra settings in a way that is encapsulated to the plugin.

options should be a module that exposes a variable options_spec which defines Kolibri options specific to this plugin. For more information on how to configure these, see the base Kolibri options specification in kolibri/utils/options.py. These values can then be set either by environment variables or by editing the options.ini file in the KOLIBRI_HOME directory. These options values can also be used inside the settings module above, to provide customization of plugin specific behaviour. These options cannot clash with existing Kolibri options defined in kolibri.utils.options, except in order to change the default value of a Kolibri option - attempting to change any other value of a core Kolibri option will result in a Runtime Error.

A very common use case for plugins is to implement a single page app or other Kolibri module for adding frontend functionality using Kolibri Javascript code. Each of these Javascript bundles are defined in the kolibri_plugin.py file by subclassing the WebpackBundleHook class to define each frontend Kolibri module. This allows a webpack built Javascript bundle to be cross-referenced and loaded into Kolibri. For more information on developing frontend code for Kolibri please see Frontend architecture.

Learn plugin example

View the source to learn more!