From e7a9b14a457f5f9ffa148e687bfbc53c4d8e1bf7 Mon Sep 17 00:00:00 2001 From: Steve Jalim Date: Tue, 23 Jul 2024 22:20:48 +0100 Subject: [PATCH] Add support for controlling which CMS Page types can be used in the site (#14846) * Add support for controlling which CMS Page types can be used in the site This means we can control when a page is available for use in the CMS, versus simply being in the codebase. Also, note that removing a particular page class from this allowlist will not break existing pages that are of that class, but will stop anyone adding a _new_ one. NB: EVERY TIME we add a new Wagtail Page subclass to the CMS, we must add to the CMS_ALLOWED_PAGE_MODELS setting if we want it to be selectable as a new child page in Production (or ticket up when we do want to add it to the setting) * Fix DEV mode allowance of all page types --- bedrock/cms/models/base.py | 9 +++++++++ bedrock/cms/tests/test_models.py | 32 +++++++++++++++++++++++++++++++- bedrock/settings/base.py | 21 +++++++++++++++++++++ docs/cms.rst | 19 ++++++++++++++++++- 4 files changed, 79 insertions(+), 2 deletions(-) diff --git a/bedrock/cms/models/base.py b/bedrock/cms/models/base.py index d699ca4072e..979a67d3d73 100644 --- a/bedrock/cms/models/base.py +++ b/bedrock/cms/models/base.py @@ -2,6 +2,7 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. +from django.conf import settings from django.utils.cache import add_never_cache_headers from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache @@ -34,6 +35,14 @@ class AbstractBedrockCMSPage(WagtailBasePage): class Meta: abstract = True + @classmethod + def can_create_at(cls, parent): + """Only allow users to add new child pages that are permitted by configuration.""" + page_model_signature = f"{cls._meta.app_label}.{cls._meta.object_name}" + if settings.CMS_ALLOWED_PAGE_MODELS == ["__all__"] or page_model_signature in settings.CMS_ALLOWED_PAGE_MODELS: + return super().can_create_at(parent) + return False + def _patch_request_for_bedrock(self, request): # Add hints that help us integrate CMS pages with core Bedrock logic request.is_cms_page = True diff --git a/bedrock/cms/tests/test_models.py b/bedrock/cms/tests/test_models.py index bb849fdaf8f..5442b678429 100644 --- a/bedrock/cms/tests/test_models.py +++ b/bedrock/cms/tests/test_models.py @@ -4,9 +4,15 @@ from unittest import mock +from django.test import override_settings + import pytest -from bedrock.cms.models import AbstractBedrockCMSPage, SimpleRichTextPage +from bedrock.cms.models import ( + AbstractBedrockCMSPage, + SimpleRichTextPage, + StructuralPage, +) from bedrock.cms.tests.factories import StructuralPageFactory pytestmark = [ @@ -67,3 +73,27 @@ def test_StructuralPage_serve_methods( preview_result = sp.serve_preview(request) assert preview_result.headers["location"].endswith(root_page.url) + + +@pytest.mark.parametrize( + "config, page_class, success_expected", + ( + ("__all__", SimpleRichTextPage, True), # same as default + ("mozorg.SomeOtherPageClass,cms.StructuralPage,cms.SimpleRichTextPage", StructuralPage, True), + ("cms.SimpleRichTextPage", SimpleRichTextPage, True), + ("cms.SimpleRichTextPage,mozorg.SomeOtherPageClass", SimpleRichTextPage, True), + ("mozorg.SomeOtherPageClass,cms.SimpleRichTextPage", SimpleRichTextPage, True), + ("mozorg.SomeOtherPageClass,mozorg.SomeOtherPageClass", SimpleRichTextPage, False), + ("mozorg.SomeOtherPageClass", SimpleRichTextPage, False), + ("mozorg.SomeOtherPageClass,legal.SomeLegalPageClass", StructuralPage, False), + ), +) +def test_CMS_ALLOWED_PAGE_MODELS_controls_Page_can_create_at( + config, + page_class, + success_expected, + minimal_site, +): + home_page = SimpleRichTextPage.objects.last() + with override_settings(Dev=False, CMS_ALLOWED_PAGE_MODELS=config.split(",")): + assert page_class.can_create_at(home_page) == success_expected diff --git a/bedrock/settings/base.py b/bedrock/settings/base.py index 61ec51e8c46..7884f155b30 100644 --- a/bedrock/settings/base.py +++ b/bedrock/settings/base.py @@ -2164,3 +2164,24 @@ def lazy_wagtail_langs(): ] WAGTAILIMAGES_IMAGE_MODEL = "cms.BedrockImage" + +# Custom code in bedrock.cms.models.base.AbstractBedrockCMSPage limits what page +# models can be added as a child page. +# +# This means we can control when a page is available for use in the CMS, versus +# simply being in the codebase. Also, note that removing a particular page class +# from this allowlist will not break existing pages that are of that class, but +# will stop anyone adding a _new_ one. +# +# NB: EVERY TIME you add a new Wagtail Page subclass to the CMS, you must enable +# it here if you want it to be selectable as a new child page in Production + +_allowed_page_models = [ + "cms.SimpleRichTextPage", + "cms.StructuralPage", +] + +if DEV is True: + CMS_ALLOWED_PAGE_MODELS = ["__all__"] +else: + CMS_ALLOWED_PAGE_MODELS = _allowed_page_models diff --git a/docs/cms.rst b/docs/cms.rst index a07f4937d6f..76a8e955ae3 100644 --- a/docs/cms.rst +++ b/docs/cms.rst @@ -307,7 +307,24 @@ Editing current content surfaces `Wagtail Editor Guide`_. -Bedrock-specific details to come. +.. note:: + This is initial documentation, noting relevant things that exist already, but much fuller recommendations will follow + +The ``CMS_ALLOWED_PAGE_MODELS`` setting +======================================= + +When you add a new page to the CMS, it will be available to add as a new child page immediately if ``DEV=True``. This means it'll be on Dev (www-dev), but not in Staging or Prod. + +So if you ship a page that needs to be used immediately in Production (which will generally be most cases), you must remember to add it to ``CMS_ALLOWED_PAGE_MODELS`` in Bedrock's settings. If you do not, it will not be selectable as a new Child Page in the CMS. + +Why do we have this behaviour? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Two reasons: + +1. This setting allows us to complete initial/eager work to add a new page type, but stop it being used in Production until we are ready for it (e.g. a special new campaign page type that we wanted to get ready in good time). While there will be guard rails and approval workflows around publishing, without this it could still be possible for part of the org to start using a new page without us realising it was off-limits, and possibly before it is allowed to be released. + +2. This approach allows us to gracefully deprecate pages: if a page is removed in ``settings.CMS_ALLOWED_PAGE_MODELS``, that doesn't mean it disappears from Prod or can't be edited - it just stops a NEW one being added in Prod. Migrating Django pages to the CMS =================================