diff --git a/wagtail/admin/panels/model_utils.py b/wagtail/admin/panels/model_utils.py index 0b98a81992..dd18f615b1 100644 --- a/wagtail/admin/panels/model_utils.py +++ b/wagtail/admin/panels/model_utils.py @@ -5,6 +5,7 @@ from django.db.models.fields.reverse_related import ManyToOneRel from django.forms.models import fields_for_model from wagtail.admin.forms.models import formfield_for_dbfield +from wagtail.models import PanelPlaceholder from .base import Panel from .field_panel import FieldPanel @@ -62,6 +63,10 @@ def expand_panel_list(model, panels): if isinstance(panel, Panel): result.append(panel) + elif isinstance(panel, PanelPlaceholder): + if real_panel := panel.construct(): + result.append(real_panel) + elif isinstance(panel, str): field = model._meta.get_field(panel) if isinstance(field, ManyToOneRel): diff --git a/wagtail/admin/panels/page_utils.py b/wagtail/admin/panels/page_utils.py index 58116638b1..093ee5330f 100644 --- a/wagtail/admin/panels/page_utils.py +++ b/wagtail/admin/panels/page_utils.py @@ -1,50 +1,12 @@ -from django.conf import settings from django.utils.translation import gettext_lazy from wagtail.admin.forms.pages import WagtailAdminPageForm from wagtail.models import Page from wagtail.utils.decorators import cached_classmethod -from .comment_panel import CommentPanel -from .field_panel import FieldPanel -from .group import MultiFieldPanel, ObjectList, TabbedInterface -from .publishing_panel import PublishingPanel -from .title_field_panel import TitleFieldPanel +from .group import ObjectList, TabbedInterface - -def set_default_page_edit_handlers(cls): - cls.content_panels = [ - TitleFieldPanel("title"), - ] - - cls.promote_panels = [ - MultiFieldPanel( - [ - FieldPanel("slug"), - FieldPanel("seo_title"), - FieldPanel("search_description"), - ], - gettext_lazy("For search engines"), - ), - MultiFieldPanel( - [ - FieldPanel("show_in_menus"), - ], - gettext_lazy("For site menus"), - ), - ] - - cls.settings_panels = [ - PublishingPanel(), - ] - - if getattr(settings, "WAGTAILADMIN_COMMENTS_ENABLED", True): - cls.settings_panels.append(CommentPanel()) - - cls.base_form_class = WagtailAdminPageForm - - -set_default_page_edit_handlers(Page) +Page.base_form_class = WagtailAdminPageForm @cached_classmethod diff --git a/wagtail/admin/panels/signal_handlers.py b/wagtail/admin/panels/signal_handlers.py index 0c108f469c..5c368c12a9 100644 --- a/wagtail/admin/panels/signal_handlers.py +++ b/wagtail/admin/panels/signal_handlers.py @@ -5,7 +5,6 @@ from django.dispatch import receiver from wagtail.models import Page from .model_utils import get_edit_handler -from .page_utils import set_default_page_edit_handlers @receiver(setting_changed) @@ -14,7 +13,6 @@ def reset_edit_handler_cache(**kwargs): Clear page edit handler cache when global WAGTAILADMIN_COMMENTS_ENABLED settings are changed """ if kwargs["setting"] == "WAGTAILADMIN_COMMENTS_ENABLED": - set_default_page_edit_handlers(Page) for model in apps.get_models(): if issubclass(model, Page): model.get_edit_handler.cache_clear() diff --git a/wagtail/admin/tests/pages/test_create_page.py b/wagtail/admin/tests/pages/test_create_page.py index c8a9a65727..6bf874e868 100644 --- a/wagtail/admin/tests/pages/test_create_page.py +++ b/wagtail/admin/tests/pages/test_create_page.py @@ -1,5 +1,4 @@ import datetime -import unittest from unittest import mock from django.contrib.auth.models import Group, Permission @@ -428,7 +427,6 @@ class TestPageCreation(WagtailTestUtils, TestCase): ) self.assertRedirects(response, "/admin/") - @unittest.expectedFailure def test_create_page_defined_before_admin_load(self): """ Test that a page model defined before wagtail.admin is loaded has all fields present diff --git a/wagtail/admin/tests/test_edit_handlers.py b/wagtail/admin/tests/test_edit_handlers.py index df261a2bbc..6a2a99a607 100644 --- a/wagtail/admin/tests/test_edit_handlers.py +++ b/wagtail/admin/tests/test_edit_handlers.py @@ -30,6 +30,7 @@ from wagtail.admin.panels import ( PublishingPanel, TabbedInterface, TitleFieldPanel, + expand_panel_list, extract_panel_definitions_from_model_class, get_form_for_model, ) @@ -1726,7 +1727,10 @@ class TestCommentPanel(WagtailTestUtils, TestCase): Test that the comment panel is missing if WAGTAILADMIN_COMMENTS_ENABLED=False """ self.assertFalse( - any(isinstance(panel, CommentPanel) for panel in Page.settings_panels) + any( + isinstance(panel, CommentPanel) + for panel in expand_panel_list(Page, Page.settings_panels) + ) ) form_class = Page.get_edit_handler().get_form_class() form = form_class() @@ -1737,7 +1741,10 @@ class TestCommentPanel(WagtailTestUtils, TestCase): Test that the comment panel is present by default """ self.assertTrue( - any(isinstance(panel, CommentPanel) for panel in Page.settings_panels) + any( + isinstance(panel, CommentPanel) + for panel in expand_panel_list(Page, Page.settings_panels) + ) ) form_class = Page.get_edit_handler().get_form_class() form = form_class() @@ -2024,7 +2031,10 @@ class TestPublishingPanel(WagtailTestUtils, TestCase): Test that the publishing panel is present by default """ self.assertTrue( - any(isinstance(panel, PublishingPanel) for panel in Page.settings_panels) + any( + isinstance(panel, PublishingPanel) + for panel in expand_panel_list(Page, Page.settings_panels) + ) ) form_class = Page.get_edit_handler().get_form_class() form = form_class() diff --git a/wagtail/models/__init__.py b/wagtail/models/__init__.py index 5d2d741e3d..58c9223339 100644 --- a/wagtail/models/__init__.py +++ b/wagtail/models/__init__.py @@ -128,6 +128,7 @@ from .media import ( # noqa: F401 UploadedFile, get_root_collection_id, ) +from .panels import CommentPanelPlaceholder, PanelPlaceholder from .reference_index import ReferenceIndex # noqa: F401 from .sites import Site, SiteManager, SiteRootPath # noqa: F401 from .specific import SpecificMixin @@ -1405,11 +1406,40 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase): COMMENTS_RELATION_NAME, ] - # Define these attributes early to avoid masking errors. (Issue #3078) - # The canonical definition is in wagtailadmin.panels. - content_panels = [] - promote_panels = [] - settings_panels = [] + # Real panel classes are defined in wagtail.admin.panels, which we can't import here + # because it would create a circular import. Instead, define them with placeholders + # to be replaced with the real classes by `wagtail.admin.panels.model_utils.expand_panel_list`. + content_panels = [ + PanelPlaceholder("wagtail.admin.panels.TitleFieldPanel", ["title"], {}), + ] + promote_panels = [ + PanelPlaceholder( + "wagtail.admin.panels.MultiFieldPanel", + [ + [ + "slug", + "seo_title", + "search_description", + ], + _("For search engines"), + ], + {}, + ), + PanelPlaceholder( + "wagtail.admin.panels.MultiFieldPanel", + [ + [ + "show_in_menus", + ], + _("For site menus"), + ], + {}, + ), + ] + settings_panels = [ + PanelPlaceholder("wagtail.admin.panels.PublishingPanel", [], {}), + CommentPanelPlaceholder(), + ] # Privacy options for page private_page_options = ["password", "groups", "login"] diff --git a/wagtail/models/panels.py b/wagtail/models/panels.py new file mode 100644 index 0000000000..8e3c606632 --- /dev/null +++ b/wagtail/models/panels.py @@ -0,0 +1,37 @@ +# Placeholder for panel types defined in wagtail.admin.panels. +# These are needed because we wish to define properties such as `content_panels` on core models +# such as Page, but importing from wagtail.admin would create a circular import. We therefore use a +# placeholder object, and swap it out for the real panel class inside +# `wagtail.admin.panels.model_utils.expand_panel_list` at the same time as converting strings to +# FieldPanel instances. + +from django.conf import settings +from django.utils.functional import cached_property +from django.utils.module_loading import import_string + + +class PanelPlaceholder: + def __init__(self, path, args, kwargs): + self.path = path + self.args = args + self.kwargs = kwargs + + @cached_property + def panel_class(self): + return import_string(self.path) + + def construct(self): + return self.panel_class(*self.args, **self.kwargs) + + +class CommentPanelPlaceholder(PanelPlaceholder): + def __init__(self): + super().__init__( + "wagtail.admin.panels.CommentPanel", + [], + {}, + ) + + def construct(self): + if getattr(settings, "WAGTAILADMIN_COMMENTS_ENABLED", True): + return super().construct()