|
| 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