Skip to content

Commit 23fe035

Browse files
committed
docs: add wagtail-support design decision doc
Covers: empty migration directories vs None (and why None causes InvalidBasesError), removal of the home app, custom AppConfig subclasses for default_auto_field, ObjectId serialization patches, custom admin URL patterns with object_id converters, initial data setup in code, and flag-based enablement via --wagtail.
1 parent 66b450a commit 23fe035

2 files changed

Lines changed: 120 additions & 0 deletions

File tree

docs/design/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ Design Decisions
77
command-structure
88
standalone-installation
99
venv-strategy
10+
wagtail-support

docs/design/wagtail-support.rst

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
Wagtail CMS Support
2+
===================
3+
4+
This document explains the design decisions behind ``dbx project add --wagtail``, which scaffolds a Django project with Wagtail CMS pre-configured for the MongoDB backend.
5+
6+
Overview
7+
--------
8+
9+
Wagtail is a Django-based CMS with its own migration history, admin URL patterns, and serialization assumptions. Several of those assumptions are incompatible with MongoDB out of the box. The template ships with a set of targeted patches and conventions that make Wagtail work with ``django-mongodb-backend`` without forking Wagtail itself.
10+
11+
Empty Migration Directories
12+
----------------------------
13+
14+
**Decision: redirect all Wagtail app migrations to empty in-project directories**
15+
16+
Wagtail ships with its own migration files (``wagtail/migrations/0001_initial.py``, etc.). On MongoDB those migrations are not needed: the driver creates collections automatically on first insert. Running them would also fail because they include SQL-style ``ALTER TABLE`` assumptions.
17+
18+
The ``WAGTAIL_MIGRATION_MODULES`` dict in the project's ``settings/wagtail.py`` redirects every Wagtail app to an empty package inside the project's own ``migrations/`` directory:
19+
20+
.. code-block:: python
21+
22+
WAGTAIL_MIGRATION_MODULES = {
23+
"wagtailcore": "myproject.migrations.wagtailcore",
24+
"wagtailadmin": "myproject.migrations.wagtailadmin",
25+
# ... one empty __init__.py per app
26+
}
27+
28+
Each of those packages contains only an ``__init__.py``. Django sees zero migration files and skips the app during ``migrate``, while MongoDB creates the underlying collections on demand.
29+
30+
**Why not ``MIGRATION_MODULES = {"wagtailcore": None}``?**
31+
32+
Setting an app to ``None`` opts it into Django's syncdb path. When an app that inherits from a migration-framework app (e.g. ``wagtailcore``) is itself set to ``None``, Django raises ``InvalidBasesError: Cannot resolve bases for [<ModelState: 'home.HomePage'>]`` because it cannot reconcile the inheritance hierarchy across the syncdb/migration boundary. Empty directories avoid this entirely — both sides participate in the migration framework with zero files each, so no base resolution is attempted.
33+
34+
No ``home`` App
35+
---------------
36+
37+
**Decision: do not ship a custom ``HomePage`` model**
38+
39+
Early versions of the template included a ``home`` app with a ``HomePage(Page)`` model. This triggered the ``InvalidBasesError`` described above. Rather than add migration plumbing for a trivial subclass, the template was simplified: the initial Wagtail root page and home page are created as plain ``wagtail.models.Page`` instances by ``_setup_wagtail_initial_data`` at first ``dbx project run``.
40+
41+
This keeps the template lean and avoids a class of migration issues whenever Wagtail is upgraded.
42+
43+
Custom App Configurations
44+
--------------------------
45+
46+
**Decision: subclass every Wagtail AppConfig to inject ``default_auto_field``**
47+
48+
Django MongoDB Backend requires models to use ``ObjectIdAutoField`` as their primary key type, while standard Django and PostgreSQL use ``BigAutoField``. Wagtail's own ``AppConfig`` subclasses hard-code no ``default_auto_field``, so they inherit the project-level default.
49+
50+
The template defines thin wrappers in ``settings/apps/wagtail.py``:
51+
52+
.. code-block:: python
53+
54+
class CustomWagtailConfig(WagtailAppConfig):
55+
@property
56+
def default_auto_field(self):
57+
return getattr(settings, "DEFAULT_AUTO_FIELD", "django.db.models.BigAutoField")
58+
59+
``WAGTAIL_INSTALLED_APPS`` uses these wrappers instead of the originals, so the correct field type is applied for whichever backend is active.
60+
61+
ObjectId Serialization Patches
62+
--------------------------------
63+
64+
**Decision: monkey-patch JSON encoders and Wagtail internals at startup**
65+
66+
Wagtail's admin makes extensive use of JSON serialization — chooser widgets, telepath adapters, API endpoints. None of these are aware of MongoDB's ``ObjectId`` type. Rather than fork Wagtail, ``CustomWagtailAdminConfig.ready()`` applies a series of targeted patches:
67+
68+
1. **Telepath registry** — registers an ``_ObjectIdAdapter`` that serializes ``ObjectId`` as its hex string for Wagtail's sidebar.
69+
2. **DjangoJSONEncoder** — patches ``default()`` so ``JsonResponse`` can serialize ``ObjectId`` values.
70+
3. **Base ``json.JSONEncoder``** — patches the stdlib encoder for direct ``json.dumps()`` calls inside Wagtail widgets.
71+
4. **API v2 filters** — patches ``ChildOfFilter``, ``AncestorOfFilter``, ``DescendantOfFilter``, and ``TranslationOfFilter`` to accept 24-character hex ObjectId strings in addition to integers.
72+
5. **``BaseSerializer`` field mapping** — maps ``ObjectIdAutoField`` to DRF's ``CharField`` so page PKs are serialised as strings in API responses.
73+
6. **``BaseAPIViewSet`` URL patterns** — replaces the ``<int:pk>`` converter with a regex accepting both ObjectId hex strings and plain integers.
74+
7. **``ModelViewSet.pk_path_converter``** — detects ``ObjectIdAutoField`` and returns ``"object_id"`` instead of ``"int"``.
75+
76+
All patches are wrapped in ``try/except ImportError`` so the app config is safe even without Wagtail or Django MongoDB Backend installed.
77+
78+
Custom Admin URL Patterns
79+
--------------------------
80+
81+
**Decision: copy and patch Wagtail's admin URLs into the project**
82+
83+
Wagtail's admin URL conf uses ``<int:parent_page_id>`` path converters throughout, which reject MongoDB ObjectId values. The template ships its own copy of ``wagtail/admin/urls/__init__.py`` at ``project_name/wagtail_urls/admin/__init__.py`` with every ``<int:`` converter replaced by ``<object_id:``.
84+
85+
Sub-module imports (``pages``, ``collections``, ``editing_sessions``, ``workflows``) are loaded dynamically from the project package rather than hard-coded, so the patched copy tracks the Wagtail version installed without requiring a full fork.
86+
87+
A ``viewsets.populate()`` idempotency guard is also included: both the local copy and Wagtail's own URL module call ``register_admin_urls`` hooks, which would double-populate the viewsets list and produce Django W005 warnings without the guard.
88+
89+
**Maintenance note:** this file must be re-diffed against the upstream ``wagtail/admin/urls/__init__.py`` on each Wagtail upgrade.
90+
91+
Initial Data Setup
92+
------------------
93+
94+
**Decision: create root page and default site in code, not via migrations**
95+
96+
Because all Wagtail migrations are empty, Wagtail's ``0001_initial`` data migration (which normally creates the root ``Page`` and default ``Site``) never runs. ``_setup_wagtail_initial_data()`` in ``project.py`` fills this gap by running a small inline Python script via the project's venv after ``migrate`` completes:
97+
98+
.. code-block:: python
99+
100+
# Simplified
101+
if not Page.objects.filter(depth=1).exists():
102+
root = Page.add_root(title="Root", slug="root", ...)
103+
home = root.add_child(instance=Page(title="Home", slug="home", ...))
104+
Site.objects.create(hostname="localhost", root_page=home, is_default_site=True)
105+
106+
This is called automatically by ``dbx project run`` before the development server starts, so the Wagtail admin is immediately usable.
107+
108+
Flag-Based Enablement
109+
---------------------
110+
111+
**Decision: Wagtail config is commented out by default, activated by ``--wagtail``**
112+
113+
The project template always includes the Wagtail settings file and URL infrastructure, but the settings are commented out in ``<project_name>/settings/<project_name>.py``. Running ``dbx project add --wagtail`` calls ``_enable_wagtail()`` which uncomments the five relevant lines and appends Wagtail URL patterns to ``urls.py``.
114+
115+
This approach means:
116+
117+
- Non-Wagtail projects pay no runtime cost for the Wagtail infrastructure sitting in the template.
118+
- The same project can be inspected or upgraded to Wagtail by editing a few lines, without re-scaffolding.
119+
- ``--qe`` can be stacked with ``--wagtail`` to produce a Wagtail site with Queryable Encryption in a single command.

0 commit comments

Comments
 (0)