At work we run a large multitenant version of Wagtail (~500 separate websites on a single
installation). To achieve this and to make some other changes to the way Wagtail behaves, we have a
number of monkey patches. So we have consolidated all of them in their own Django app which we
called wagtail_patches
. This is loaded into our INSTALLED_APPS
after most of our own apps but
before any of the Wagtail apps:
# settings.py
INSTALLED_APPS = [
# Multitenant apps. These are ordered with regard to template overrides.
'core',
'search',
'site_creator',
'calendar',
'theme_v6_5',
'theme_v7_0',
'robots_txt',
'wagtail_patches', #####
'sitemap',
'features',
'custom_auth',
# Wagtail apps.
'wagtail.embeds',
'wagtail.sites',
'wagtail.users',
'wagtail.snippets',
'wagtail.documents',
# We use a custom replacement for wagtail.images that makes it add decoding="async" and loading="lazy" attrs.
# 'wagtail.images',
'wagtail_patches.apps.MultitenantImagesAppConfig',
'wagtail.search',
'wagtail.admin',
'wagtail',
'wagtail.contrib.modeladmin',
'wagtail.contrib.settings',
'wagtail.contrib.routable_page',
# Wagtail dependencies, django, etc.....
]
And then in that app, we use the apps.py
file to load everything from the patches directory:
from django.apps import AppConfig
from wagtail.images.apps import WagtailImagesAppConfig
class WagtailPatchesConfig(AppConfig):
name = 'wagtail_patches'
verbose_name = 'Wagtail Patches'
ready_is_done = False
# If there are multiple AppConfigs in a single apps.py, one of them needs to be default=True.
default = True
def ready(self):
"""
This function runs as soon as the app is loaded. It executes our monkey patches to various parts of Wagtail
that change it to support our architecture of fully separated tenants.
"""
# As suggested by the Django docs, we need to make absolutely certain that this code runs only once.
if not self.ready_is_done:
# The act of performing this import executes all the code in patches/__init__.py.
from . import patches # noqa
self.ready_is_done = True
else:
print("{}.ready() executed more than once! This method's code is skipped on subsequent runs.".format(
self.__class__.__name__
))
class MultitenantImagesAppConfig(WagtailImagesAppConfig):
default_attrs = {"decoding": "async", "loading": "lazy"}
You will note that the first of our customizations is right in apps.py
. We use this file to
configure default html attributes for image tags generated by Wagtail - per the instructions in
“Adding default attributes to all images”.
Patching views
We have a handful of views that need overrides. Mostly these involve changing querysets or altering filters so the choices are limited to users belonging to the current site. The easiest option is to subclass the existing view, make our changes, then assign our subclass to the same path as the original.
I use the show_urls
command from
django_extensions
to find the existing mapping. And then I map my replacement view to the same
pattern. So for replacing the page explorer view, I added the following two
lines:
# patched_urls.py
from .views.page_explorer import MultitenantPageIndexView
patched_wagtail_urlpatterns = [
# This overrides the wagtailadmin_explore_page (aka page listing view) so we can monkey patch the filters
path('admin/pages/', MultitenantPageIndexView.as_view()),
path('admin/pages/<int:parent_page_id>/', MultitenantPageIndexView.as_view()),
]
Because we have a bunch of overrides, we have a patched_urls.py
in our
wagtail_patches
app. Then, in our main urls.py
file, we add that pattern
before our other mappings:
# urls.py
from wagtail import views as wagtailcore_views
from wagtail_patches.patched_urls import patched_wagtail_urlpatterns
# We override several /admin/* URLs with our own custom versions
urlpatterns = patched_wagtail_urlpatterns + [
# We now include wagtails' own admin URLs.
path('admin/', include('wagtail.admin.urls')),
path('documents/', include('wagtail.documents.urls')),
... our custom urls and the rest of the standard Wagtail url mappings
]
I then use show_urls
to check my mapping. As long as our version is the second
one, then it will get used. If you feel like your changes are getting ignored,
start by checking to see that the url pattern for your override exactly matches
the original pattern.