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 NavigationHook in kolibri.core.hooks:
from kolibri.core.webpack.hooks import WebpackBundleHook
from kolibri.plugins.hooks import define_hook
@define_hook
class NavigationHook(WebpackBundleHook):
pass
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!