Kolibri plugin architecture
The behavior of Kolibri can be extended using plugins. The following is a guide to developing plugins.
Core vs. Plugins: When to Use Each
Kolibri’s architecture separates core functionality from plugin-based features. Understanding when to add code to core versus plugins is important for maintaining a clean architecture.
Core modules (kolibri/core/*)
Core modules provide essential, always-available functionality that other parts of Kolibri depend on. Add code to core when:
The functionality is fundamental to Kolibri’s operation (auth, content, logger, tasks)
It provides shared infrastructure used across multiple plugins
It defines base models, APIs, or utilities that plugins extend
It cannot be disabled without breaking Kolibri
Examples of core modules:
kolibri.core.auth- User authentication and permissionskolibri.core.content- Content channel and metadata managementkolibri.core.logger- Event logging and analyticskolibri.core.tasks- Background task queuekolibri.core.device- Device-level settings and management
Plugins (kolibri/plugins/*)
Plugins provide specific features or user-facing functionality that can be enabled or disabled. Add code as a plugin when:
The feature can be optionally enabled/disabled by administrators
It provides a specific user interface or workflow (Learn, Coach, Facility)
It’s a self-contained feature with minimal dependencies on other plugins
Different deployments might want different combinations of features
Examples of plugins:
kolibri.plugins.learn- Learner interface for browsing and accessing contentkolibri.plugins.coach- Coach tools for managing classes and assignmentskolibri.plugins.facility- Facility management and user administrationkolibri.plugins.device- Device configuration interface
Decision flowchart:
Is this functionality required for Kolibri to operate? → Core
Will other plugins depend on this code? → Core (or shared utilities)
Should administrators be able to disable this feature? → Plugin
Does this provide a specific user interface or workflow? → Plugin
Is this extending existing core functionality? → Plugin
Shared utilities:
For code that needs to be reused across multiple plugins but isn’t core functionality:
Backend: Consider adding to an existing core utility module if it’s truly general-purpose
Frontend: Use the
kolibri-commonpackage to avoid expanding the core API
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 @property 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
Note
Using externally-built plugins with PEX
When using externally-built plugins (plugins installed separately from Kolibri’s core installation) with a PEX distribution of Kolibri, you must set the environment variable PEX_INHERIT_PATH=fallback to enable the PEX file to access plugins installed in the system Python path.
For example:
PEX_INHERIT_PATH=1 python kolibri.pex start
This allows Kolibri to discover and use plugins that were installed via pip install outside of the PEX environment.
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!