Template patches
Patches are the main mechanism that Tutor offers to plugin developers to modify the files that make up an Open edX platform. This page explains what patches are, how they work, and how to use them in your plugins. For the list of all patches available in Tutor, check the template patch catalog.
What is a patch?
As explained in How Tutor works, all the files that are needed to run Open edX, such as the LMS and CMS settings, the Dockerfiles, the docker-compose.yml files and the Kubernetes manifests, are generated by rendering Jinja2 templates. In many places, these templates include calls to {{ patch("patchname") }}. For instance, the common LMS settings template ends with:
{{ patch("openedx-lms-common-settings") }}
######## End of common LMS settings
Every such call is a named location where plugins can insert their own content. When the environment is rendered, each {{ patch("patchname") }} statement is replaced by all the content that plugins have added to the “patchname” patch. If no plugin adds anything to a patch, the statement is replaced by an empty string.
Thanks to patches, plugins can modify Tutor templates without having to copy them. This means that the changes from different plugins can be combined, and that plugins keep working when Tutor templates are modified from one release to the next.
Adding content to a patch
Plugins add content to patches with the tutor.hooks.Filters.ENV_PATCHES filter. Each item of this filter is a (name, content) tuple, where name is the name of the patch and content is the string that will be inserted in the templates. For instance, a Python plugin can disable account registration in the LMS with:
from tutor import hooks
hooks.Filters.ENV_PATCHES.add_item(
(
"openedx-lms-common-settings",
"ALLOW_PUBLIC_ACCOUNT_CREATION = False"
)
)
Multiple patches can be added at once with add_items:
hooks.Filters.ENV_PATCHES.add_items(
[
("openedx-lms-common-settings", "ALLOW_PUBLIC_ACCOUNT_CREATION = False"),
("openedx-cms-common-settings", "ALLOW_PUBLIC_ACCOUNT_CREATION = False"),
]
)
Patches are applied when the environment is rendered. Thus, after you modify a patch, you should run tutor config save (or tutor local launch) for your changes to be taken into account. Patches that modify a Dockerfile, such as openedx-dockerfile-post-python-requirements, are only taken into account once the corresponding Docker image is rebuilt, for instance with tutor images build openedx.
Storing patches in separate files
When patches grow larger than a few lines, it becomes more convenient to store them in separate files. Plugins that are created with the plugin cookiecutter follow this convention: every file in the patches/ folder of the plugin is a patch. The file name is the name of the patch, and the file content is the content of the patch. For instance, the following file adds a line to the openedx-lms-common-settings patch:
# myplugin/tutormyplugin/patches/openedx-lms-common-settings
ALLOW_PUBLIC_ACCOUNT_CREATION = False
This works because the plugin.py module generated by the cookiecutter loads all the files from that folder with the same ENV_PATCHES filter:
for path in glob(str(importlib_resources.files("tutormyplugin") / "patches" / "*")):
with open(path, encoding="utf-8") as patch_file:
hooks.Filters.ENV_PATCHES.add_item((os.path.basename(path), patch_file.read()))
Patches are templates, too
The content of a patch is itself rendered as a Jinja2 template, with the same configuration as the rest of the environment. This means that patches can make use of configuration settings, conditional statements and template filters. For instance:
hooks.Filters.ENV_PATCHES.add_item(
(
"openedx-lms-common-settings",
"ALLOW_PUBLIC_ACCOUNT_CREATION = {% if MYPLUGIN_PLATFORM_IS_PUBLIC %}True{% else %}False{% endif %}",
)
)
As a consequence, if your patch must include literal {{, }}, {% or %} characters, you should escape them with a {% raw %}...{% endraw %} block.
Indentation
Some patches are located in indentation-sensitive files, such as YAML files. In such cases, Tutor templates take care of indenting the content of the patch. For instance, the local-docker-compose-services patch is included in docker-compose.yml with:
services:
...
{{ patch("local-docker-compose-services")|indent(2) }}
You should thus write the content of your patch without any leading indentation, just as if it was located at the beginning of the line:
hooks.Filters.ENV_PATCHES.add_item(
(
"local-docker-compose-services",
"""\
myservice:
image: docker.io/myservice:latest
restart: unless-stopped
""",
)
)
Patches with multiple items
Multiple plugins can add content to the same patch, and a single plugin can add multiple items to the same patch. All items are rendered and joined with a newline character. You should not make any assumption about the order in which items from different plugins are inserted. If the order matters, you can change it with the priority argument: items with a higher priority, such as tutor.core.hooks.priorities.HIGH, are inserted first. For instance:
hooks.Filters.ENV_PATCHES.add_item(
("openedx-lms-common-settings", "MY_SETTING = 'this comes first'"),
priority=hooks.priorities.HIGH,
)
Finding the right patch
The template patch catalog lists all the patches that are available in Tutor core, with the file in which each one is located. In addition, you can list all the patches that are used in the templates of your environment, including the patches that are defined by the plugins that you have enabled, with:
$ tutor config patches list
PATCH LOCATIONS
caddyfile apps/caddy/Caddyfile
...
openedx-lms-common-settings apps/openedx/settings/lms/development.py
apps/openedx/settings/lms/production.py
...
To check the rendered content of a patch, as it will be inserted in the templates, run:
$ tutor config patches show openedx-lms-common-settings
ALLOW_PUBLIC_ACCOUNT_CREATION = False
Warning
Tutor does not complain when a plugin adds content to a patch that does not exist: the content is simply ignored. If your changes do not appear in the rendered environment, check for typos in the name of your patch by making sure that it appears in the output of tutor config patches list.
Creating patches in your plugin
Patches are not limited to the templates from Tutor core. Plugins that add their own templates can also include {{ patch("...") }} statements in them, such that other plugins can modify these templates in turn. For instance, the MFE plugin defines patches to customize the build of micro-frontends, such as mfe-dockerfile-post-npm-install. When you create a patch in your plugin, you should prefix its name with the name of your plugin, to avoid conflicts with Tutor core and with other plugins, and document it for your users.