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,
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.
Each app in
ACTIVE_PLUGINS in the kolibri conf is searched for the
Everything that a plugin does is expected to be defined through
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 -
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
Defining abstract hooks in
<myapp>/hooks.py isn’t mandatory, but
loading a concrete hook in
Do not define abstract and registered hooks in the same module. Or to put it
in other words: Always put registered hooks in
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¶
The example shows a NavigationHook which is simplified for the sake of readability. The actual implementation in Kolibri will differ.
Here is an example of how to use a hook in
from kolibri.core.hooks import NavigationHook from kolibri.plugins.hooks import register_hook @register_hook class MyPluginNavItem(NavigationHook): bundle_id = "side_nav"
@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
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
@define_hook decorator is used instead, to signal that this instance of
inheritance is not intended to register anything against the parent
However, because of the inheritance relationship, any hook registered against
(like our example registered hook above), will also be registered against the
so we should expect to see our plugin’s nav item listed in the
property as well as in the
Usage of the hook¶
The hook can then be used to collect all the information from the hooks, as per this usage
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.
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
Defining a plugin¶
A plugin must have a Python module inside it called
kolibri_plugin, inside this there must be an object subclassed
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
translated_view_urls should both be standard Django urls modules in the plugin that expose
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
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
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!