Browse Source

Move wagtail.admin.edit_handlers to wagtail.admin.panels

Matt Westcott 3 years ago
parent
commit
b189ab8382
40 changed files with 1149 additions and 1153 deletions
  1. 3 3
      docs/advanced_topics/customisation/page_editing_interface.rst
  2. 5 5
      docs/getting_started/tutorial.md
  3. 7 7
      docs/reference/contrib/forms/customisation.md
  4. 1 1
      docs/reference/contrib/forms/index.md
  5. 1 1
      docs/reference/contrib/modeladmin/create_edit_delete_views.rst
  6. 1 1
      docs/reference/contrib/modeladmin/index.rst
  7. 1 1
      docs/reference/contrib/settings.rst
  8. 6 6
      docs/reference/pages/panels.rst
  9. 1 1
      docs/releases/2.17.md
  10. 7 7
      docs/topics/pages.md
  11. 1 1
      docs/topics/snippets.rst
  12. 1 1
      docs/topics/streamfield.rst
  13. 1 1
      wagtail/admin/checks.py
  14. 1 1080
      wagtail/admin/edit_handlers.py
  15. 1 1
      wagtail/admin/forms/workflows.py
  16. 2 2
      wagtail/admin/models.py
  17. 1080 0
      wagtail/admin/panels.py
  18. 1 1
      wagtail/admin/tests/pages/test_preview.py
  19. 2 2
      wagtail/admin/tests/test_edit_handlers.py
  20. 1 0
      wagtail/bin/wagtail.py
  21. 1 1
      wagtail/contrib/forms/edit_handlers.py
  22. 1 1
      wagtail/contrib/forms/models.py
  23. 1 1
      wagtail/contrib/forms/tests/test_views.py
  24. 1 4
      wagtail/contrib/modeladmin/options.py
  25. 1 1
      wagtail/contrib/modeladmin/tests/test_modeladmin_edit_handlers.py
  26. 1 1
      wagtail/contrib/modeladmin/tests/test_simple_modeladmin.py
  27. 1 1
      wagtail/contrib/settings/tests/test_admin.py
  28. 1 1
      wagtail/contrib/settings/views.py
  29. 2 2
      wagtail/documents/edit_handlers.py
  30. 2 2
      wagtail/images/edit_handlers.py
  31. 1 1
      wagtail/models/__init__.py
  32. 2 2
      wagtail/snippets/edit_handlers.py
  33. 1 1
      wagtail/snippets/tests.py
  34. 1 4
      wagtail/snippets/views/snippets.py
  35. 1 1
      wagtail/test/customuser/models.py
  36. 1 1
      wagtail/test/demosite/models.py
  37. 1 1
      wagtail/test/modeladmintest/models.py
  38. 1 1
      wagtail/test/modeladmintest/wagtail_hooks.py
  39. 1 1
      wagtail/test/snippets/models.py
  40. 3 3
      wagtail/test/testapp/models.py

+ 3 - 3
docs/advanced_topics/customisation/page_editing_interface.rst

@@ -10,7 +10,7 @@ As standard, Wagtail organises panels for pages into three tabs: 'Content', 'Pro
 
 
 .. code-block:: python
 .. code-block:: python
 
 
-    from wagtail.admin.edit_handlers import TabbedInterface, ObjectList
+    from wagtail.admin.panels import TabbedInterface, ObjectList
 
 
     class BlogPage(Page):
     class BlogPage(Page):
         # field definitions omitted
         # field definitions omitted
@@ -43,7 +43,7 @@ Wagtail provides a general-purpose WYSIWYG editor for creating rich text content
 .. code-block:: python
 .. code-block:: python
 
 
     from wagtail.fields import RichTextField
     from wagtail.fields import RichTextField
-    from wagtail.admin.edit_handlers import FieldPanel
+    from wagtail.admin.panels import FieldPanel
 
 
 
 
     class BookPage(Page):
     class BookPage(Page):
@@ -160,7 +160,7 @@ or to add custom validation logic for your models:
     from django import forms
     from django import forms
     from django.db import models
     from django.db import models
     import geocoder  # not in Wagtail, for example only - https://geocoder.readthedocs.io/
     import geocoder  # not in Wagtail, for example only - https://geocoder.readthedocs.io/
-    from wagtail.admin.edit_handlers import FieldPanel
+    from wagtail.admin.panels import FieldPanel
     from wagtail.admin.forms import WagtailAdminPageForm
     from wagtail.admin.forms import WagtailAdminPageForm
     from wagtail.models import Page
     from wagtail.models import Page
 
 

+ 5 - 5
docs/getting_started/tutorial.md

@@ -132,7 +132,7 @@ from django.db import models
 
 
 from wagtail.models import Page
 from wagtail.models import Page
 from wagtail.fields import RichTextField
 from wagtail.fields import RichTextField
-from wagtail.admin.edit_handlers import FieldPanel
+from wagtail.admin.panels import FieldPanel
 
 
 
 
 class HomePage(Page):
 class HomePage(Page):
@@ -224,7 +224,7 @@ Lets start with a simple index page for our blog. In `blog/models.py`:
 ```python
 ```python
 from wagtail.models import Page
 from wagtail.models import Page
 from wagtail.fields import RichTextField
 from wagtail.fields import RichTextField
-from wagtail.admin.edit_handlers import FieldPanel
+from wagtail.admin.panels import FieldPanel
 
 
 
 
 class BlogIndexPage(Page):
 class BlogIndexPage(Page):
@@ -278,7 +278,7 @@ from django.db import models
 
 
 from wagtail.models import Page
 from wagtail.models import Page
 from wagtail.fields import RichTextField
 from wagtail.fields import RichTextField
-from wagtail.admin.edit_handlers import FieldPanel
+from wagtail.admin.panels import FieldPanel
 from wagtail.search import index
 from wagtail.search import index
 
 
 
 
@@ -466,7 +466,7 @@ from modelcluster.fields import ParentalKey
 
 
 from wagtail.models import Page, Orderable
 from wagtail.models import Page, Orderable
 from wagtail.fields import RichTextField
 from wagtail.fields import RichTextField
-from wagtail.admin.edit_handlers import FieldPanel, InlinePanel
+from wagtail.admin.panels import FieldPanel, InlinePanel
 from wagtail.search import index
 from wagtail.search import index
 
 
 
 
@@ -621,7 +621,7 @@ from taggit.models import TaggedItemBase
 
 
 from wagtail.models import Page, Orderable
 from wagtail.models import Page, Orderable
 from wagtail.fields import RichTextField
 from wagtail.fields import RichTextField
-from wagtail.admin.edit_handlers import FieldPanel, InlinePanel, MultiFieldPanel
+from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel
 from wagtail.search import index
 from wagtail.search import index
 
 
 
 

+ 7 - 7
docs/reference/contrib/forms/customisation.md

@@ -11,7 +11,7 @@ You can do this as shown below.
 
 
 ```python
 ```python
 from modelcluster.fields import ParentalKey
 from modelcluster.fields import ParentalKey
-from wagtail.admin.edit_handlers import (
+from wagtail.admin.panels import (
     FieldPanel, FieldRowPanel,
     FieldPanel, FieldRowPanel,
     InlinePanel, MultiFieldPanel
     InlinePanel, MultiFieldPanel
 )
 )
@@ -61,7 +61,7 @@ from django.conf import settings
 from django.core.serializers.json import DjangoJSONEncoder
 from django.core.serializers.json import DjangoJSONEncoder
 from django.db import models
 from django.db import models
 from modelcluster.fields import ParentalKey
 from modelcluster.fields import ParentalKey
-from wagtail.admin.edit_handlers import (
+from wagtail.admin.panels import (
     FieldPanel, FieldRowPanel,
     FieldPanel, FieldRowPanel,
     InlinePanel, MultiFieldPanel
     InlinePanel, MultiFieldPanel
 )
 )
@@ -121,7 +121,7 @@ from django.conf import settings
 from django.core.serializers.json import DjangoJSONEncoder
 from django.core.serializers.json import DjangoJSONEncoder
 from django.db import models
 from django.db import models
 from modelcluster.fields import ParentalKey
 from modelcluster.fields import ParentalKey
-from wagtail.admin.edit_handlers import (
+from wagtail.admin.panels import (
     FieldPanel, FieldRowPanel,
     FieldPanel, FieldRowPanel,
     InlinePanel, MultiFieldPanel
     InlinePanel, MultiFieldPanel
 )
 )
@@ -195,7 +195,7 @@ from django.core.serializers.json import DjangoJSONEncoder
 from django.db import models
 from django.db import models
 from django.shortcuts import render
 from django.shortcuts import render
 from modelcluster.fields import ParentalKey
 from modelcluster.fields import ParentalKey
-from wagtail.admin.edit_handlers import (
+from wagtail.admin.panels import (
     FieldPanel, FieldRowPanel,
     FieldPanel, FieldRowPanel,
     InlinePanel, MultiFieldPanel
     InlinePanel, MultiFieldPanel
 )
 )
@@ -288,7 +288,7 @@ The following example shows how to create a multi-step form.
 from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
 from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
 from django.shortcuts import render
 from django.shortcuts import render
 from modelcluster.fields import ParentalKey
 from modelcluster.fields import ParentalKey
-from wagtail.admin.edit_handlers import (
+from wagtail.admin.panels import (
     FieldPanel, FieldRowPanel,
     FieldPanel, FieldRowPanel,
     InlinePanel, MultiFieldPanel
     InlinePanel, MultiFieldPanel
 )
 )
@@ -430,7 +430,7 @@ First, you need to collect results as shown below:
 
 
 ```python
 ```python
 from modelcluster.fields import ParentalKey
 from modelcluster.fields import ParentalKey
-from wagtail.admin.edit_handlers import (
+from wagtail.admin.panels import (
     FieldPanel, FieldRowPanel,
     FieldPanel, FieldRowPanel,
     InlinePanel, MultiFieldPanel
     InlinePanel, MultiFieldPanel
 )
 )
@@ -544,7 +544,7 @@ Finally, we add a URL param of `id` based on the `form_submission` if it exists.
 
 
 ```python
 ```python
 from django.shortcuts import redirect
 from django.shortcuts import redirect
-from wagtail.admin.edit_handlers import FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel
+from wagtail.admin.panels import FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel
 from wagtail.contrib.forms.models import AbstractEmailForm
 from wagtail.contrib.forms.models import AbstractEmailForm
 
 
 class FormPage(AbstractEmailForm):
 class FormPage(AbstractEmailForm):

+ 1 - 1
docs/reference/contrib/forms/index.md

@@ -26,7 +26,7 @@ Within the `models.py` of one of your apps, create a model that extends `wagtail
 ```python
 ```python
 from django.db import models
 from django.db import models
 from modelcluster.fields import ParentalKey
 from modelcluster.fields import ParentalKey
-from wagtail.admin.edit_handlers import (
+from wagtail.admin.panels import (
     FieldPanel, FieldRowPanel,
     FieldPanel, FieldRowPanel,
     InlinePanel, MultiFieldPanel
     InlinePanel, MultiFieldPanel
 )
 )

+ 1 - 1
docs/reference/contrib/modeladmin/create_edit_delete_views.rst

@@ -231,7 +231,7 @@ and ManyToManyField fields.
 ``ModelAdmin.get_edit_handler()``
 ``ModelAdmin.get_edit_handler()``
 -----------------------------------
 -----------------------------------
 
 
-**Must return**: An instance of ``wagtail.admin.edit_handlers.ObjectList``
+**Must return**: An instance of ``wagtail.admin.panels.ObjectList``
 
 
 Returns the appropriate ``edit_handler`` for the modeladmin class.
 Returns the appropriate ``edit_handler`` for the modeladmin class.
 ``edit_handlers`` can be defined either on the model itself or on the
 ``edit_handlers`` can be defined either on the model itself or on the

+ 1 - 1
docs/reference/contrib/modeladmin/index.rst

@@ -82,7 +82,7 @@ to create, view, and edit ``Book`` entries.
 .. code-block:: python
 .. code-block:: python
 
 
     from django.db import models
     from django.db import models
-    from wagtail.admin.edit_handlers import FieldPanel
+    from wagtail.admin.panels import FieldPanel
 
 
     class Book(models.Model):
     class Book(models.Model):
         title = models.CharField(max_length=255)
         title = models.CharField(max_length=255)

+ 1 - 1
docs/reference/contrib/settings.rst

@@ -65,7 +65,7 @@ with a custom ``edit_handler`` attribute:
 
 
 .. code-block:: python
 .. code-block:: python
 
 
-    from wagtail.admin.edit_handlers import TabbedInterface, ObjectList
+    from wagtail.admin.panels import TabbedInterface, ObjectList
 
 
     @register_setting
     @register_setting
     class MySettings(BaseSetting):
     class MySettings(BaseSetting):

+ 6 - 6
docs/reference/pages/panels.rst

@@ -6,11 +6,11 @@ Panel types
 Built-in Fields and Choosers
 Built-in Fields and Choosers
 ----------------------------
 ----------------------------
 
 
-Django's field types are automatically recognised and provided with an appropriate widget for input. Just define that field the normal Django way and pass the field name into :class:`~wagtail.admin.edit_handlers.FieldPanel` when defining your panels. Wagtail will take care of the rest.
+Django's field types are automatically recognised and provided with an appropriate widget for input. Just define that field the normal Django way and pass the field name into :class:`~wagtail.admin.panels.FieldPanel` when defining your panels. Wagtail will take care of the rest.
 
 
 Here are some Wagtail-specific types that you might include as fields in your models.
 Here are some Wagtail-specific types that you might include as fields in your models.
 
 
-.. module:: wagtail.admin.edit_handlers
+.. module:: wagtail.admin.panels
 
 
 FieldPanel
 FieldPanel
 ~~~~~~~~~~
 ~~~~~~~~~~
@@ -65,7 +65,7 @@ MultiFieldPanel
 
 
 .. class:: MultiFieldPanel(children, heading="", classname=None)
 .. class:: MultiFieldPanel(children, heading="", classname=None)
 
 
-    This panel condenses several :class:`~wagtail.admin.edit_handlers.FieldPanel` s or choosers, from a ``list`` or ``tuple``, under a single ``heading`` string.
+    This panel condenses several :class:`~wagtail.admin.panels.FieldPanel` s or choosers, from a ``list`` or ``tuple``, under a single ``heading`` string.
 
 
     .. attribute:: MultiFieldPanel.children
     .. attribute:: MultiFieldPanel.children
 
 
@@ -142,7 +142,7 @@ PageChooserPanel
     .. code-block:: python
     .. code-block:: python
 
 
         from wagtail.models import Page
         from wagtail.models import Page
-        from wagtail.admin.edit_handlers import PageChooserPanel
+        from wagtail.admin.panels import PageChooserPanel
 
 
 
 
         class BookPage(Page):
         class BookPage(Page):
@@ -242,7 +242,7 @@ By adding CSS classes to your panel definitions or adding extra parameters to yo
 Full-Width Input
 Full-Width Input
 ~~~~~~~~~~~~~~~~
 ~~~~~~~~~~~~~~~~
 
 
-Use ``classname="full"`` to make a field (input element) stretch the full width of the Wagtail page editor. This will not work if the field is encapsulated in a :class:`~wagtail.admin.edit_handlers.MultiFieldPanel`, which places its child fields into a formset.
+Use ``classname="full"`` to make a field (input element) stretch the full width of the Wagtail page editor. This will not work if the field is encapsulated in a :class:`~wagtail.admin.panels.MultiFieldPanel`, which places its child fields into a formset.
 
 
 
 
 Titles
 Titles
@@ -366,7 +366,7 @@ Let's look at the example of adding related links to a :class:`~wagtail.models.P
       InlinePanel('related_links', label="Related Links"),
       InlinePanel('related_links', label="Related Links"),
     ]
     ]
 
 
-The ``RelatedLink`` class is a vanilla Django abstract model. The ``BookPageRelatedLinks`` model extends it with capability for being ordered in the Wagtail interface via the ``Orderable`` class as well as adding a ``page`` property which links the model to the ``BookPage`` model we're adding the related links objects to. Finally, in the panel definitions for ``BookPage``, we'll add an :class:`~wagtail.admin.edit_handlers.InlinePanel` to provide an interface for it all. Let's look again at the parameters that :class:`~wagtail.admin.edit_handlers.InlinePanel` accepts:
+The ``RelatedLink`` class is a vanilla Django abstract model. The ``BookPageRelatedLinks`` model extends it with capability for being ordered in the Wagtail interface via the ``Orderable`` class as well as adding a ``page`` property which links the model to the ``BookPage`` model we're adding the related links objects to. Finally, in the panel definitions for ``BookPage``, we'll add an :class:`~wagtail.admin.panels.InlinePanel` to provide an interface for it all. Let's look again at the parameters that :class:`~wagtail.admin.panels.InlinePanel` accepts:
 
 
 .. code-block:: python
 .. code-block:: python
 
 

+ 1 - 1
docs/releases/2.17.md

@@ -25,7 +25,7 @@ The panel types `StreamFieldPanel`, `RichTextFieldPanel`, `ImageChooserPanel`, `
 
 
 ### Permission-dependent FieldPanels
 ### Permission-dependent FieldPanels
 
 
-[`FieldPanel`](wagtail.admin.edit_handlers.FieldPanel) now accepts a `permission` keyword argument to specify that the field should only be available to users with a given permission level. This feature was developed by Matt Westcott and sponsored by Google as part of Wagtail's page editor redevelopment.
+[`FieldPanel`](wagtail.admin.panels.FieldPanel) now accepts a `permission` keyword argument to specify that the field should only be available to users with a given permission level. This feature was developed by Matt Westcott and sponsored by Google as part of Wagtail's page editor redevelopment.
 
 
 
 
 ### Other features
 ### Other features

+ 7 - 7
docs/topics/pages.md

@@ -22,7 +22,7 @@ from modelcluster.fields import ParentalKey
 
 
 from wagtail.models import Page, Orderable
 from wagtail.models import Page, Orderable
 from wagtail.fields import RichTextField
 from wagtail.fields import RichTextField
-from wagtail.admin.edit_handlers import FieldPanel, MultiFieldPanel, InlinePanel
+from wagtail.admin.panels import FieldPanel, MultiFieldPanel, InlinePanel
 from wagtail.search import index
 from wagtail.search import index
 
 
 
 
@@ -129,8 +129,8 @@ Here's a summary of the `EditHandler` classes that Wagtail provides out of the b
 
 
 These allow editing of model fields. The `FieldPanel` class will choose the correct widget based on the type of the field, such as a rich text editor for `RichTextField`, or an image chooser for a `ForeignKey` to an image model. `FieldPanel` also provides a page chooser interface for `ForeignKey`s to page models, but for more fine-grained control over which page types can be chosen, `PageChooserPanel` provides additional configuration options.
 These allow editing of model fields. The `FieldPanel` class will choose the correct widget based on the type of the field, such as a rich text editor for `RichTextField`, or an image chooser for a `ForeignKey` to an image model. `FieldPanel` also provides a page chooser interface for `ForeignKey`s to page models, but for more fine-grained control over which page types can be chosen, `PageChooserPanel` provides additional configuration options.
 
 
-- {class}`~wagtail.admin.edit_handlers.FieldPanel`
-- {class}`~wagtail.admin.edit_handlers.PageChooserPanel`
+- {class}`~wagtail.admin.panels.FieldPanel`
+- {class}`~wagtail.admin.panels.PageChooserPanel`
 
 
 ```{versionchanged} 2.17
 ```{versionchanged} 2.17
 Previously, certain field types required special-purpose panels: `StreamFieldPanel`, `ImageChooserPanel`, `DocumentChooserPanel` and `SnippetChooserPanel`. These are now all handled by `FieldPanel`.
 Previously, certain field types required special-purpose panels: `StreamFieldPanel`, `ImageChooserPanel`, `DocumentChooserPanel` and `SnippetChooserPanel`. These are now all handled by `FieldPanel`.
@@ -140,9 +140,9 @@ Previously, certain field types required special-purpose panels: `StreamFieldPan
 
 
 These are used for structuring fields in the interface.
 These are used for structuring fields in the interface.
 
 
-- {class}`~wagtail.admin.edit_handlers.MultiFieldPanel`
-- {class}`~wagtail.admin.edit_handlers.InlinePanel`
-- {class}`~wagtail.admin.edit_handlers.FieldRowPanel`
+- {class}`~wagtail.admin.panels.MultiFieldPanel`
+- {class}`~wagtail.admin.panels.InlinePanel`
+- {class}`~wagtail.admin.panels.FieldRowPanel`
 
 
 
 
 #### Customising the page editor interface
 #### Customising the page editor interface
@@ -356,7 +356,7 @@ class BlogPageRelatedLink(Orderable):
     ]
     ]
 ```
 ```
 
 
-To add this to the admin interface, use the {class}`~wagtail.admin.edit_handlers.InlinePanel` edit panel class:
+To add this to the admin interface, use the {class}`~wagtail.admin.panels.InlinePanel` edit panel class:
 
 
 ```python
 ```python
 content_panels = [
 content_panels = [

+ 1 - 1
docs/topics/snippets.rst

@@ -17,7 +17,7 @@ Here's an example snippet model:
 
 
   from django.db import models
   from django.db import models
 
 
-  from wagtail.admin.edit_handlers import FieldPanel
+  from wagtail.admin.panels import FieldPanel
   from wagtail.snippets.models import register_snippet
   from wagtail.snippets.models import register_snippet
 
 
   ...
   ...

+ 1 - 1
docs/topics/streamfield.rst

@@ -22,7 +22,7 @@ Using StreamField
     from wagtail.models import Page
     from wagtail.models import Page
     from wagtail.fields import StreamField
     from wagtail.fields import StreamField
     from wagtail import blocks
     from wagtail import blocks
-    from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel
+    from wagtail.admin.panels import FieldPanel, StreamFieldPanel
     from wagtail.images.blocks import ImageChooserBlock
     from wagtail.images.blocks import ImageChooserBlock
 
 
     class BlogPage(Page):
     class BlogPage(Page):

+ 1 - 1
wagtail/admin/checks.py

@@ -104,7 +104,7 @@ def inline_panel_model_panels_check(app_configs, **kwargs):
 
 
 def check_panels_in_model(cls, context="model"):
 def check_panels_in_model(cls, context="model"):
     """Check panels configuration uses `panels` when `edit_handler` not in use."""
     """Check panels configuration uses `panels` when `edit_handler` not in use."""
-    from wagtail.admin.edit_handlers import BaseCompositeEditHandler, InlinePanel
+    from wagtail.admin.panels import BaseCompositeEditHandler, InlinePanel
     from wagtail.models import Page
     from wagtail.models import Page
 
 
     errors = []
     errors = []

+ 1 - 1080
wagtail/admin/edit_handlers.py

@@ -1,1080 +1 @@
-import functools
-import re
-from warnings import warn
-
-from django import forms
-from django.apps import apps
-from django.conf import settings
-from django.contrib.auth import get_user_model
-from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
-from django.core.signals import setting_changed
-from django.dispatch import receiver
-from django.forms.formsets import DELETION_FIELD_NAME, ORDERING_FIELD_NAME
-from django.forms.models import fields_for_model
-from django.template.loader import render_to_string
-from django.utils.functional import cached_property
-from django.utils.safestring import mark_safe
-from django.utils.translation import gettext_lazy
-from modelcluster.models import get_serializable_data_for_fields
-
-from wagtail.admin import compare, widgets
-from wagtail.admin.forms.comments import CommentForm, CommentReplyForm
-from wagtail.admin.templatetags.wagtailadmin_tags import avatar_url, user_display_name
-from wagtail.blocks import BlockField
-from wagtail.coreutils import camelcase_to_underscore
-from wagtail.models import COMMENTS_RELATION_NAME, Page
-from wagtail.utils.decorators import cached_classmethod
-from wagtail.utils.deprecation import RemovedInWagtail219Warning
-
-# DIRECT_FORM_FIELD_OVERRIDES, FORM_FIELD_OVERRIDES are imported for backwards
-# compatibility, as people are likely importing them from here and then
-# appending their own overrides
-from .forms.models import (  # NOQA
-    DIRECT_FORM_FIELD_OVERRIDES,
-    FORM_FIELD_OVERRIDES,
-    WagtailAdminModelForm,
-    formfield_for_dbfield,
-)
-from .forms.pages import WagtailAdminPageForm
-
-
-def widget_with_script(widget, script):
-    return mark_safe("{0}<script>{1}</script>".format(widget, script))
-
-
-def get_form_for_model(
-    model,
-    form_class=WagtailAdminModelForm,
-    fields=None,
-    exclude=None,
-    formsets=None,
-    exclude_formsets=None,
-    widgets=None,
-    field_permissions=None,
-):
-
-    # django's modelform_factory with a bit of custom behaviour
-    attrs = {"model": model}
-    if fields is not None:
-        attrs["fields"] = fields
-    if exclude is not None:
-        attrs["exclude"] = exclude
-    if widgets is not None:
-        attrs["widgets"] = widgets
-    if formsets is not None:
-        attrs["formsets"] = formsets
-    if exclude_formsets is not None:
-        attrs["exclude_formsets"] = exclude_formsets
-    if field_permissions is not None:
-        attrs["field_permissions"] = field_permissions
-
-    # Give this new form class a reasonable name.
-    class_name = model.__name__ + str("Form")
-    bases = (object,)
-    if hasattr(form_class, "Meta"):
-        bases = (form_class.Meta,) + bases
-
-    form_class_attrs = {"Meta": type(str("Meta"), bases, attrs)}
-
-    metaclass = type(form_class)
-
-    return metaclass(class_name, (form_class,), form_class_attrs)
-
-
-def extract_panel_definitions_from_model_class(model, exclude=None):
-    if hasattr(model, "panels"):
-        return model.panels
-
-    panels = []
-
-    _exclude = []
-    if exclude:
-        _exclude.extend(exclude)
-
-    fields = fields_for_model(
-        model, exclude=_exclude, formfield_callback=formfield_for_dbfield
-    )
-
-    for field_name, field in fields.items():
-        try:
-            panel_class = field.widget.get_panel()
-        except AttributeError:
-            panel_class = FieldPanel
-
-        panel = panel_class(field_name)
-        panels.append(panel)
-
-    return panels
-
-
-class EditHandler:
-    """
-    Abstract class providing sensible default behaviours for objects implementing
-    the EditHandler API
-    """
-
-    def __init__(self, heading="", classname="", help_text=""):
-        self.heading = heading
-        self.classname = classname
-        self.help_text = help_text
-        self.model = None
-        self.instance = None
-        self.request = None
-        self.form = None
-
-    def clone(self):
-        return self.__class__(**self.clone_kwargs())
-
-    def clone_kwargs(self):
-        return {
-            "heading": self.heading,
-            "classname": self.classname,
-            "help_text": self.help_text,
-        }
-
-    # return list of widget overrides that this EditHandler wants to be in place
-    # on the form it receives
-    def widget_overrides(self):
-        return {}
-
-    # return list of fields that this EditHandler expects to find on the form
-    def required_fields(self):
-        return []
-
-    # return a dict of formsets that this EditHandler requires to be present
-    # as children of the ClusterForm; the dict is a mapping from relation name
-    # to parameters to be passed as part of get_form_for_model's 'formsets' kwarg
-    def required_formsets(self):
-        return {}
-
-    # return a dict mapping field name to the permission codename that a user must have for that
-    # field to be included in the form
-    def field_permissions(self):
-        return {}
-
-    # return any HTML that needs to be output on the edit page once per edit handler definition.
-    # Typically this will be used to define snippets of HTML within <script type="text/x-template"></script> blocks
-    # for JavaScript code to work with.
-    def html_declarations(self):
-        return ""
-
-    def is_shown(self):
-        return True
-
-    def bind_to(self, model=None, instance=None, request=None, form=None):
-        if model is None and instance is not None and self.model is None:
-            model = instance._meta.model
-
-        new = self.clone()
-        new.model = self.model if model is None else model
-        new.instance = self.instance if instance is None else instance
-        new.request = self.request if request is None else request
-        new.form = self.form if form is None else form
-
-        if new.model is not None:
-            new.on_model_bound()
-
-        if new.instance is not None:
-            new.on_instance_bound()
-
-        if new.request is not None:
-            new.on_request_bound()
-
-        if new.form is not None:
-            new.on_form_bound()
-
-        return new
-
-    def on_model_bound(self):
-        pass
-
-    def on_instance_bound(self):
-        pass
-
-    def on_request_bound(self):
-        pass
-
-    def on_form_bound(self):
-        pass
-
-    def __repr__(self):
-        return "<%s with model=%s instance=%s request=%s form=%s>" % (
-            self.__class__.__name__,
-            self.model,
-            self.instance,
-            self.request,
-            self.form.__class__.__name__,
-        )
-
-    def classes(self):
-        """
-        Additional CSS classnames to add to whatever kind of object this is at output.
-        Subclasses of EditHandler should override this, invoking super().classes() to
-        append more classes specific to the situation.
-        """
-        if self.classname:
-            return [self.classname]
-        return []
-
-    def field_type(self):
-        """
-        The kind of field it is e.g boolean_field. Useful for better semantic markup of field display based on type
-        """
-        return ""
-
-    def id_for_label(self):
-        """
-        The ID to be used as the 'for' attribute of any <label> elements that refer
-        to this object but are rendered outside of it. Leave blank if this object does not render
-        as a single input field.
-        """
-        return ""
-
-    def render_as_object(self):
-        """
-        Render this object as it should appear within an ObjectList. Should not
-        include the <h2> heading or help text - ObjectList will supply those
-        """
-        # by default, assume that the subclass provides a catch-all render() method
-        return self.render()
-
-    def render_as_field(self):
-        """
-        Render this object as it should appear within a <ul class="fields"> list item
-        """
-        # by default, assume that the subclass provides a catch-all render() method
-        return self.render()
-
-    def render_missing_fields(self):
-        """
-        Helper function: render all of the fields that are defined on the form but not "claimed" by
-        any panels via required_fields. These fields are most likely to be hidden fields introduced
-        by the forms framework itself, such as ORDER / DELETE fields on formset members.
-
-        (If they aren't actually hidden fields, then they will appear as ugly unstyled / label-less fields
-        outside of the panel furniture. But there's not much we can do about that.)
-        """
-        rendered_fields = self.required_fields()
-        missing_fields_html = [
-            str(self.form[field_name])
-            for field_name in self.form.fields
-            if field_name not in rendered_fields
-        ]
-
-        return mark_safe("".join(missing_fields_html))
-
-    def render_form_content(self):
-        """
-        Render this as an 'object', ensuring that all fields necessary for a valid form
-        submission are included
-        """
-        return mark_safe(self.render_as_object() + self.render_missing_fields())
-
-    def get_comparison(self):
-        return []
-
-
-class BaseCompositeEditHandler(EditHandler):
-    """
-    Abstract class for EditHandlers that manage a set of sub-EditHandlers.
-    Concrete subclasses must attach a 'children' property
-    """
-
-    def __init__(self, children=(), *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self.children = children
-
-    def clone_kwargs(self):
-        kwargs = super().clone_kwargs()
-        kwargs["children"] = self.children
-        return kwargs
-
-    def widget_overrides(self):
-        # build a collated version of all its children's widget lists
-        widgets = {}
-        for handler_class in self.children:
-            widgets.update(handler_class.widget_overrides())
-        widget_overrides = widgets
-
-        return widget_overrides
-
-    def required_fields(self):
-        fields = []
-        for handler in self.children:
-            fields.extend(handler.required_fields())
-        return fields
-
-    def required_formsets(self):
-        formsets = {}
-        for handler_class in self.children:
-            formsets.update(handler_class.required_formsets())
-        return formsets
-
-    def field_permissions(self):
-        field_permissions = {}
-        for handler_class in self.children:
-            field_permissions.update(handler_class.field_permissions())
-        return field_permissions
-
-    @property
-    def visible_children(self):
-        return [child for child in self.children if child.is_shown()]
-
-    def is_shown(self):
-        return any(child.is_shown() for child in self.children)
-
-    def html_declarations(self):
-        return mark_safe("".join([c.html_declarations() for c in self.children]))
-
-    def on_model_bound(self):
-        self.children = [child.bind_to(model=self.model) for child in self.children]
-
-    def on_instance_bound(self):
-        self.children = [
-            child.bind_to(instance=self.instance) for child in self.children
-        ]
-
-    def on_request_bound(self):
-        self.children = [child.bind_to(request=self.request) for child in self.children]
-
-    def on_form_bound(self):
-        children = []
-        for child in self.children:
-            if isinstance(child, FieldPanel):
-                if self.form._meta.exclude:
-                    if child.field_name in self.form._meta.exclude:
-                        continue
-                if self.form._meta.fields:
-                    if child.field_name not in self.form._meta.fields:
-                        continue
-            children.append(child.bind_to(form=self.form))
-        self.children = children
-
-    def render(self):
-        return mark_safe(render_to_string(self.template, {"self": self}))
-
-    def get_comparison(self):
-        comparators = []
-
-        for child in self.children:
-            comparators.extend(child.get_comparison())
-
-        return comparators
-
-
-class BaseFormEditHandler(BaseCompositeEditHandler):
-    """
-    Base class for edit handlers that can construct a form class for all their
-    child edit handlers.
-    """
-
-    # The form class used as the base for constructing specific forms for this
-    # edit handler.  Subclasses can override this attribute to provide a form
-    # with custom validation, for example.  Custom forms must subclass
-    # WagtailAdminModelForm
-    base_form_class = None
-
-    def get_form_class(self):
-        """
-        Construct a form class that has all the fields and formsets named in
-        the children of this edit handler.
-        """
-        if self.model is None:
-            raise AttributeError(
-                "%s is not bound to a model yet. Use `.bind_to(model=model)` "
-                "before using this method." % self.__class__.__name__
-            )
-        # If a custom form class was passed to the EditHandler, use it.
-        # Otherwise, use the base_form_class from the model.
-        # If that is not defined, use WagtailAdminModelForm.
-        model_form_class = getattr(self.model, "base_form_class", WagtailAdminModelForm)
-        base_form_class = self.base_form_class or model_form_class
-
-        return get_form_for_model(
-            self.model,
-            form_class=base_form_class,
-            fields=self.required_fields(),
-            formsets=self.required_formsets(),
-            widgets=self.widget_overrides(),
-            field_permissions=self.field_permissions(),
-        )
-
-
-class TabbedInterface(BaseFormEditHandler):
-    template = "wagtailadmin/edit_handlers/tabbed_interface.html"
-
-    def __init__(self, *args, show_comments_toggle=None, **kwargs):
-        self.base_form_class = kwargs.pop("base_form_class", None)
-        super().__init__(*args, **kwargs)
-        if show_comments_toggle is not None:
-            self.show_comments_toggle = show_comments_toggle
-        else:
-            self.show_comments_toggle = (
-                "comment_notifications" in self.required_fields()
-            )
-
-    def get_form_class(self):
-        form_class = super().get_form_class()
-
-        # Set show_comments_toggle attribute on form class
-        return type(
-            form_class.__name__,
-            (form_class,),
-            {"show_comments_toggle": self.show_comments_toggle},
-        )
-
-    def clone_kwargs(self):
-        kwargs = super().clone_kwargs()
-        kwargs["base_form_class"] = self.base_form_class
-        kwargs["show_comments_toggle"] = self.show_comments_toggle
-        return kwargs
-
-
-class ObjectList(TabbedInterface):
-    template = "wagtailadmin/edit_handlers/object_list.html"
-
-
-class FieldRowPanel(BaseCompositeEditHandler):
-    template = "wagtailadmin/edit_handlers/field_row_panel.html"
-
-    def on_instance_bound(self):
-        super().on_instance_bound()
-
-        col_count = " col%s" % (12 // len(self.children))
-        # If child panel doesn't have a col# class then append default based on
-        # number of columns
-        for child in self.children:
-            if not re.search(r"\bcol\d+\b", child.classname):
-                child.classname += col_count
-
-
-class MultiFieldPanel(BaseCompositeEditHandler):
-    template = "wagtailadmin/edit_handlers/multi_field_panel.html"
-
-    def classes(self):
-        classes = super().classes()
-        classes.append("multi-field")
-        return classes
-
-
-class HelpPanel(EditHandler):
-    def __init__(
-        self,
-        content="",
-        template="wagtailadmin/edit_handlers/help_panel.html",
-        heading="",
-        classname="",
-    ):
-        super().__init__(heading=heading, classname=classname)
-        self.content = content
-        self.template = template
-
-    def clone_kwargs(self):
-        kwargs = super().clone_kwargs()
-        del kwargs["help_text"]
-        kwargs.update(
-            content=self.content,
-            template=self.template,
-        )
-        return kwargs
-
-    def render(self):
-        return mark_safe(render_to_string(self.template, {"self": self}))
-
-
-class FieldPanel(EditHandler):
-    TEMPLATE_VAR = "field_panel"
-
-    def __init__(self, field_name, *args, **kwargs):
-        widget = kwargs.pop("widget", None)
-        if widget is not None:
-            self.widget = widget
-        self.permission = kwargs.pop("permission", None)
-        self.disable_comments = kwargs.pop("disable_comments", None)
-        super().__init__(*args, **kwargs)
-        self.field_name = field_name
-
-    def clone_kwargs(self):
-        kwargs = super().clone_kwargs()
-        kwargs.update(
-            field_name=self.field_name,
-            widget=self.widget if hasattr(self, "widget") else None,
-            permission=self.permission,
-        )
-        return kwargs
-
-    def widget_overrides(self):
-        """check if a specific widget has been defined for this field"""
-        if hasattr(self, "widget"):
-            return {self.field_name: self.widget}
-        return {}
-
-    def field_permissions(self):
-        if self.permission:
-            return {self.field_name: self.permission}
-        else:
-            return {}
-
-    def is_shown(self):
-        if (
-            self.permission
-            and self.request
-            and not self.request.user.has_perm(self.permission)
-        ):
-            return False
-
-        return True
-
-    def classes(self):
-        classes = super().classes()
-
-        if self.bound_field.field.required:
-            classes.append("required")
-
-        # If field has any errors, add the classname 'error' to enable error styling
-        # (e.g. red background), unless the widget has its own mechanism for rendering errors
-        # via the render_with_errors mechanism (as StreamField does).
-        if self.bound_field.errors and not hasattr(
-            self.bound_field.field.widget, "render_with_errors"
-        ):
-            classes.append("error")
-
-        classes.append(self.field_type())
-
-        return classes
-
-    def field_type(self):
-        return camelcase_to_underscore(self.bound_field.field.__class__.__name__)
-
-    def id_for_label(self):
-        return self.bound_field.id_for_label
-
-    @property
-    def comments_enabled(self):
-        if self.disable_comments is None:
-            # by default, enable comments on all fields except StreamField (which has its own comment handling)
-            return not isinstance(self.bound_field.field, BlockField)
-        else:
-            return not self.disable_comments
-
-    object_template = "wagtailadmin/edit_handlers/single_field_panel.html"
-
-    def render_as_object(self):
-        return mark_safe(
-            render_to_string(
-                self.object_template,
-                {
-                    "self": self,
-                    self.TEMPLATE_VAR: self,
-                    "field": self.bound_field,
-                    "show_add_comment_button": self.comments_enabled
-                    and getattr(
-                        self.bound_field.field.widget, "show_add_comment_button", True
-                    ),
-                },
-            )
-        )
-
-    field_template = "wagtailadmin/edit_handlers/field_panel_field.html"
-
-    def render_as_field(self):
-        return mark_safe(
-            render_to_string(
-                self.field_template,
-                {
-                    "field": self.bound_field,
-                    "field_type": self.field_type(),
-                    "show_add_comment_button": self.comments_enabled
-                    and getattr(
-                        self.bound_field.field.widget, "show_add_comment_button", True
-                    ),
-                },
-            )
-        )
-
-    def required_fields(self):
-        return [self.field_name]
-
-    def get_comparison_class(self):
-        # Hide fields with hidden widget
-        widget_override = self.widget_overrides().get(self.field_name, None)
-        if widget_override and widget_override.is_hidden:
-            return
-
-        try:
-            field = self.db_field
-
-            if field.choices:
-                return compare.ChoiceFieldComparison
-
-            comparison_class = compare.comparison_class_registry.get(field)
-            if comparison_class:
-                return comparison_class
-
-            if field.is_relation:
-                if field.many_to_many:
-                    return compare.M2MFieldComparison
-
-                return compare.ForeignObjectComparison
-
-        except FieldDoesNotExist:
-            pass
-
-        return compare.FieldComparison
-
-    def get_comparison(self):
-        comparator_class = self.get_comparison_class()
-
-        if comparator_class and self.is_shown():
-            try:
-                return [functools.partial(comparator_class, self.db_field)]
-            except FieldDoesNotExist:
-                return []
-        return []
-
-    @cached_property
-    def db_field(self):
-        try:
-            model = self.model
-        except AttributeError:
-            raise ImproperlyConfigured(
-                "%r must be bound to a model before calling db_field" % self
-            )
-
-        return model._meta.get_field(self.field_name)
-
-    def on_form_bound(self):
-        try:
-            self.bound_field = self.form[self.field_name]
-        except KeyError:
-            return
-
-        if self.heading:
-            self.bound_field.label = self.heading
-        else:
-            self.heading = self.bound_field.label
-        self.help_text = self.bound_field.help_text
-
-    def __repr__(self):
-        return "<%s '%s' with model=%s instance=%s request=%s form=%s>" % (
-            self.__class__.__name__,
-            self.field_name,
-            self.model,
-            self.instance,
-            self.request,
-            self.form.__class__.__name__,
-        )
-
-
-class RichTextFieldPanel(FieldPanel):
-    def __init__(self, *args, **kwargs):
-        warn(
-            "wagtail.admin.edit_handlers.RichTextFieldPanel is obsolete and should be replaced by FieldPanel",
-            category=RemovedInWagtail219Warning,
-            stacklevel=2,
-        )
-        super().__init__(*args, **kwargs)
-
-
-class BaseChooserPanel(FieldPanel):
-    def __init__(self, *args, **kwargs):
-        warn(
-            "wagtail.admin.edit_handlers.BaseChooserPanel is obsolete and should be replaced by FieldPanel",
-            category=RemovedInWagtail219Warning,
-            stacklevel=2,
-        )
-        super().__init__(*args, **kwargs)
-
-
-class PageChooserPanel(FieldPanel):
-    def __init__(self, field_name, page_type=None, can_choose_root=False):
-        super().__init__(field_name=field_name)
-
-        self.page_type = page_type
-        self.can_choose_root = can_choose_root
-
-    def clone_kwargs(self):
-        return {
-            "field_name": self.field_name,
-            "page_type": self.page_type,
-            "can_choose_root": self.can_choose_root,
-        }
-
-    def widget_overrides(self):
-        if self.page_type or self.can_choose_root:
-            return {
-                self.field_name: widgets.AdminPageChooser(
-                    target_models=self.page_type, can_choose_root=self.can_choose_root
-                )
-            }
-        else:
-            return {}
-
-
-class InlinePanel(EditHandler):
-    def __init__(
-        self,
-        relation_name,
-        panels=None,
-        heading="",
-        label="",
-        min_num=None,
-        max_num=None,
-        *args,
-        **kwargs,
-    ):
-        super().__init__(*args, **kwargs)
-        self.relation_name = relation_name
-        self.panels = panels
-        self.heading = heading or label
-        self.label = label
-        self.min_num = min_num
-        self.max_num = max_num
-
-    def clone_kwargs(self):
-        kwargs = super().clone_kwargs()
-        kwargs.update(
-            relation_name=self.relation_name,
-            panels=self.panels,
-            label=self.label,
-            min_num=self.min_num,
-            max_num=self.max_num,
-        )
-        return kwargs
-
-    def get_panel_definitions(self):
-        # Look for a panels definition in the InlinePanel declaration
-        if self.panels is not None:
-            return self.panels
-        # Failing that, get it from the model
-        return extract_panel_definitions_from_model_class(
-            self.db_field.related_model, exclude=[self.db_field.field.name]
-        )
-
-    def get_child_edit_handler(self):
-        panels = self.get_panel_definitions()
-        child_edit_handler = MultiFieldPanel(panels, heading=self.heading)
-        return child_edit_handler.bind_to(model=self.db_field.related_model)
-
-    def required_formsets(self):
-        child_edit_handler = self.get_child_edit_handler()
-        return {
-            self.relation_name: {
-                "fields": child_edit_handler.required_fields(),
-                "widgets": child_edit_handler.widget_overrides(),
-                "min_num": self.min_num,
-                "validate_min": self.min_num is not None,
-                "max_num": self.max_num,
-                "validate_max": self.max_num is not None,
-                "formsets": child_edit_handler.required_formsets(),
-            }
-        }
-
-    def html_declarations(self):
-        return self.get_child_edit_handler().html_declarations()
-
-    def get_comparison(self):
-        field_comparisons = []
-
-        for panel in self.get_panel_definitions():
-            field_comparisons.extend(
-                panel.bind_to(model=self.db_field.related_model).get_comparison()
-            )
-
-        return [
-            functools.partial(
-                compare.ChildRelationComparison, self.db_field, field_comparisons
-            )
-        ]
-
-    def on_model_bound(self):
-        manager = getattr(self.model, self.relation_name)
-        self.db_field = manager.rel
-
-    def on_form_bound(self):
-        self.formset = self.form.formsets[self.relation_name]
-
-        self.children = []
-        for subform in self.formset.forms:
-            # override the DELETE field to have a hidden input
-            subform.fields[DELETION_FIELD_NAME].widget = forms.HiddenInput()
-
-            # ditto for the ORDER field, if present
-            if self.formset.can_order:
-                subform.fields[ORDERING_FIELD_NAME].widget = forms.HiddenInput()
-
-            child_edit_handler = self.get_child_edit_handler()
-            self.children.append(
-                child_edit_handler.bind_to(
-                    instance=subform.instance, request=self.request, form=subform
-                )
-            )
-
-        # if this formset is valid, it may have been re-ordered; respect that
-        # in case the parent form errored and we need to re-render
-        if self.formset.can_order and self.formset.is_valid():
-            self.children.sort(
-                key=lambda child: child.form.cleaned_data[ORDERING_FIELD_NAME] or 1
-            )
-
-        empty_form = self.formset.empty_form
-        empty_form.fields[DELETION_FIELD_NAME].widget = forms.HiddenInput()
-        if self.formset.can_order:
-            empty_form.fields[ORDERING_FIELD_NAME].widget = forms.HiddenInput()
-
-        self.empty_child = self.get_child_edit_handler()
-        self.empty_child = self.empty_child.bind_to(
-            instance=empty_form.instance, request=self.request, form=empty_form
-        )
-
-    template = "wagtailadmin/edit_handlers/inline_panel.html"
-
-    def render(self):
-        formset = render_to_string(
-            self.template,
-            {
-                "self": self,
-                "can_order": self.formset.can_order,
-            },
-        )
-        js = self.render_js_init()
-        return widget_with_script(formset, js)
-
-    js_template = "wagtailadmin/edit_handlers/inline_panel.js"
-
-    def render_js_init(self):
-        return mark_safe(
-            render_to_string(
-                self.js_template,
-                {
-                    "self": self,
-                    "can_order": self.formset.can_order,
-                },
-            )
-        )
-
-
-# This allows users to include the publishing panel in their own per-model override
-# without having to write these fields out by hand, potentially losing 'classname'
-# and therefore the associated styling of the publishing panel
-class PublishingPanel(MultiFieldPanel):
-    def __init__(self, **kwargs):
-        updated_kwargs = {
-            "children": [
-                FieldRowPanel(
-                    [
-                        FieldPanel("go_live_at"),
-                        FieldPanel("expire_at"),
-                    ],
-                    classname="label-above",
-                ),
-            ],
-            "heading": gettext_lazy("Scheduled publishing"),
-            "classname": "publishing",
-        }
-        updated_kwargs.update(kwargs)
-        super().__init__(**updated_kwargs)
-
-
-class PrivacyModalPanel(EditHandler):
-    def __init__(self, **kwargs):
-        updated_kwargs = {"heading": gettext_lazy("Privacy"), "classname": "privacy"}
-        updated_kwargs.update(kwargs)
-        super().__init__(**updated_kwargs)
-
-    def render(self):
-        content = render_to_string(
-            "wagtailadmin/pages/privacy_switch_panel.html",
-            {"self": self, "page": self.instance, "request": self.request},
-        )
-
-        from wagtail.admin.staticfiles import versioned_static
-
-        return mark_safe(
-            '{0}<script type="text/javascript" src="{1}"></script>'.format(
-                content, versioned_static("wagtailadmin/js/privacy-switch.js")
-            )
-        )
-
-
-class CommentPanel(EditHandler):
-    def required_fields(self):
-        # Adds the comment notifications field to the form.
-        # Note, this field is defined directly on WagtailAdminPageForm.
-        return ["comment_notifications"]
-
-    def required_formsets(self):
-        # add the comments formset
-        # we need to pass in the current user for validation on the formset
-        # this could alternatively be done on the page form itself if we added the
-        # comments formset there, but we typically only add fields via edit handlers
-        current_user = getattr(self.request, "user", None)
-
-        class CommentReplyFormWithRequest(CommentReplyForm):
-            user = current_user
-
-        class CommentFormWithRequest(CommentForm):
-            user = current_user
-
-            class Meta:
-                formsets = {"replies": {"form": CommentReplyFormWithRequest}}
-
-        return {
-            COMMENTS_RELATION_NAME: {
-                "form": CommentFormWithRequest,
-                "fields": ["text", "contentpath", "position"],
-                "formset_name": "comments",
-            }
-        }
-
-    template = "wagtailadmin/edit_handlers/comments/comment_panel.html"
-    declarations_template = (
-        "wagtailadmin/edit_handlers/comments/comment_declarations.html"
-    )
-
-    def html_declarations(self):
-        return render_to_string(self.declarations_template)
-
-    def get_context(self):
-        def user_data(user):
-            return {"name": user_display_name(user), "avatar_url": avatar_url(user)}
-
-        user = getattr(self.request, "user", None)
-        user_pks = {user.pk}
-        serialized_comments = []
-        bound = self.form.is_bound
-        comment_formset = self.form.formsets.get("comments")
-        comment_forms = comment_formset.forms if comment_formset else []
-        for form in comment_forms:
-            # iterate over comments to retrieve users (to get display names) and serialized versions
-            replies = []
-            for reply_form in form.formsets["replies"].forms:
-                user_pks.add(reply_form.instance.user_id)
-                reply_data = get_serializable_data_for_fields(reply_form.instance)
-                reply_data["deleted"] = (
-                    reply_form.cleaned_data.get("DELETE", False) if bound else False
-                )
-                replies.append(reply_data)
-            user_pks.add(form.instance.user_id)
-            data = get_serializable_data_for_fields(form.instance)
-            data["deleted"] = form.cleaned_data.get("DELETE", False) if bound else False
-            data["resolved"] = (
-                form.cleaned_data.get("resolved", False)
-                if bound
-                else form.instance.resolved_at is not None
-            )
-            data["replies"] = replies
-            serialized_comments.append(data)
-
-        authors = {
-            str(user.pk): user_data(user)
-            for user in get_user_model()
-            .objects.filter(pk__in=user_pks)
-            .select_related("wagtail_userprofile")
-        }
-
-        comments_data = {
-            "comments": serialized_comments,
-            "user": user.pk,
-            "authors": authors,
-        }
-
-        return {
-            "comments_data": comments_data,
-        }
-
-    def render(self):
-        panel = render_to_string(self.template, self.get_context())
-        return panel
-
-
-# Now that we've defined EditHandlers, we can set up wagtailcore.Page to have some.
-def set_default_page_edit_handlers(cls):
-    cls.content_panels = [
-        FieldPanel("title", classname="full 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(),
-        PrivacyModalPanel(),
-    ]
-
-    if getattr(settings, "WAGTAILADMIN_COMMENTS_ENABLED", True):
-        cls.settings_panels.append(CommentPanel())
-
-    cls.base_form_class = WagtailAdminPageForm
-
-
-set_default_page_edit_handlers(Page)
-
-
-@cached_classmethod
-def get_edit_handler(cls):
-    """
-    Get the EditHandler to use in the Wagtail admin when editing this page type.
-    """
-    if hasattr(cls, "edit_handler"):
-        edit_handler = cls.edit_handler
-    else:
-        # construct a TabbedInterface made up of content_panels, promote_panels
-        # and settings_panels, skipping any which are empty
-        tabs = []
-
-        if cls.content_panels:
-            tabs.append(ObjectList(cls.content_panels, heading=gettext_lazy("Content")))
-        if cls.promote_panels:
-            tabs.append(ObjectList(cls.promote_panels, heading=gettext_lazy("Promote")))
-        if cls.settings_panels:
-            tabs.append(
-                ObjectList(
-                    cls.settings_panels,
-                    heading=gettext_lazy("Settings"),
-                    classname="settings",
-                )
-            )
-
-        edit_handler = TabbedInterface(tabs, base_form_class=cls.base_form_class)
-
-    return edit_handler.bind_to(model=cls)
-
-
-Page.get_edit_handler = get_edit_handler
-
-
-@receiver(setting_changed)
-def reset_page_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()
-
-
-class StreamFieldPanel(FieldPanel):
-    def __init__(self, *args, **kwargs):
-        warn(
-            "wagtail.admin.edit_handlers.StreamFieldPanel is obsolete and should be replaced by FieldPanel",
-            category=RemovedInWagtail219Warning,
-            stacklevel=2,
-        )
-        super().__init__(*args, **kwargs)
+from wagtail.admin.panels import *  # noqa

+ 1 - 1
wagtail/admin/forms/workflows.py

@@ -5,8 +5,8 @@ from django.utils.translation import gettext as _
 from django.utils.translation import gettext_lazy as __
 from django.utils.translation import gettext_lazy as __
 
 
 from wagtail.admin import widgets
 from wagtail.admin import widgets
-from wagtail.admin.edit_handlers import FieldPanel, InlinePanel, ObjectList
 from wagtail.admin.forms import WagtailAdminModelForm
 from wagtail.admin.forms import WagtailAdminModelForm
+from wagtail.admin.panels import FieldPanel, InlinePanel, ObjectList
 from wagtail.admin.widgets.workflows import AdminTaskChooser
 from wagtail.admin.widgets.workflows import AdminTaskChooser
 from wagtail.coreutils import get_model_string
 from wagtail.coreutils import get_model_string
 from wagtail.models import Page, Task, Workflow, WorkflowPage
 from wagtail.models import Page, Task, Workflow, WorkflowPage

+ 2 - 2
wagtail/admin/models.py

@@ -3,11 +3,11 @@ from django.db.models import Count, Model
 from modelcluster.fields import ParentalKey
 from modelcluster.fields import ParentalKey
 from taggit.models import Tag
 from taggit.models import Tag
 
 
-# The edit_handlers module extends Page with some additional attributes required by
+# The panels module extends Page with some additional attributes required by
 # wagtail admin (namely, base_form_class and get_edit_handler). Importing this within
 # wagtail admin (namely, base_form_class and get_edit_handler). Importing this within
 # wagtail.admin.models ensures that this happens in advance of running wagtail.admin's
 # wagtail.admin.models ensures that this happens in advance of running wagtail.admin's
 # system checks.
 # system checks.
-from wagtail.admin import edit_handlers  # NOQA
+from wagtail.admin import panels  # NOQA
 from wagtail.models import Page
 from wagtail.models import Page
 
 
 
 

+ 1080 - 0
wagtail/admin/panels.py

@@ -0,0 +1,1080 @@
+import functools
+import re
+from warnings import warn
+
+from django import forms
+from django.apps import apps
+from django.conf import settings
+from django.contrib.auth import get_user_model
+from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
+from django.core.signals import setting_changed
+from django.dispatch import receiver
+from django.forms.formsets import DELETION_FIELD_NAME, ORDERING_FIELD_NAME
+from django.forms.models import fields_for_model
+from django.template.loader import render_to_string
+from django.utils.functional import cached_property
+from django.utils.safestring import mark_safe
+from django.utils.translation import gettext_lazy
+from modelcluster.models import get_serializable_data_for_fields
+
+from wagtail.admin import compare, widgets
+from wagtail.admin.forms.comments import CommentForm, CommentReplyForm
+from wagtail.admin.templatetags.wagtailadmin_tags import avatar_url, user_display_name
+from wagtail.blocks import BlockField
+from wagtail.coreutils import camelcase_to_underscore
+from wagtail.models import COMMENTS_RELATION_NAME, Page
+from wagtail.utils.decorators import cached_classmethod
+from wagtail.utils.deprecation import RemovedInWagtail219Warning
+
+# DIRECT_FORM_FIELD_OVERRIDES, FORM_FIELD_OVERRIDES are imported for backwards
+# compatibility, as people are likely importing them from here and then
+# appending their own overrides
+from .forms.models import (  # NOQA
+    DIRECT_FORM_FIELD_OVERRIDES,
+    FORM_FIELD_OVERRIDES,
+    WagtailAdminModelForm,
+    formfield_for_dbfield,
+)
+from .forms.pages import WagtailAdminPageForm
+
+
+def widget_with_script(widget, script):
+    return mark_safe("{0}<script>{1}</script>".format(widget, script))
+
+
+def get_form_for_model(
+    model,
+    form_class=WagtailAdminModelForm,
+    fields=None,
+    exclude=None,
+    formsets=None,
+    exclude_formsets=None,
+    widgets=None,
+    field_permissions=None,
+):
+
+    # django's modelform_factory with a bit of custom behaviour
+    attrs = {"model": model}
+    if fields is not None:
+        attrs["fields"] = fields
+    if exclude is not None:
+        attrs["exclude"] = exclude
+    if widgets is not None:
+        attrs["widgets"] = widgets
+    if formsets is not None:
+        attrs["formsets"] = formsets
+    if exclude_formsets is not None:
+        attrs["exclude_formsets"] = exclude_formsets
+    if field_permissions is not None:
+        attrs["field_permissions"] = field_permissions
+
+    # Give this new form class a reasonable name.
+    class_name = model.__name__ + str("Form")
+    bases = (object,)
+    if hasattr(form_class, "Meta"):
+        bases = (form_class.Meta,) + bases
+
+    form_class_attrs = {"Meta": type(str("Meta"), bases, attrs)}
+
+    metaclass = type(form_class)
+
+    return metaclass(class_name, (form_class,), form_class_attrs)
+
+
+def extract_panel_definitions_from_model_class(model, exclude=None):
+    if hasattr(model, "panels"):
+        return model.panels
+
+    panels = []
+
+    _exclude = []
+    if exclude:
+        _exclude.extend(exclude)
+
+    fields = fields_for_model(
+        model, exclude=_exclude, formfield_callback=formfield_for_dbfield
+    )
+
+    for field_name, field in fields.items():
+        try:
+            panel_class = field.widget.get_panel()
+        except AttributeError:
+            panel_class = FieldPanel
+
+        panel = panel_class(field_name)
+        panels.append(panel)
+
+    return panels
+
+
+class EditHandler:
+    """
+    Abstract class providing sensible default behaviours for objects implementing
+    the EditHandler API
+    """
+
+    def __init__(self, heading="", classname="", help_text=""):
+        self.heading = heading
+        self.classname = classname
+        self.help_text = help_text
+        self.model = None
+        self.instance = None
+        self.request = None
+        self.form = None
+
+    def clone(self):
+        return self.__class__(**self.clone_kwargs())
+
+    def clone_kwargs(self):
+        return {
+            "heading": self.heading,
+            "classname": self.classname,
+            "help_text": self.help_text,
+        }
+
+    # return list of widget overrides that this EditHandler wants to be in place
+    # on the form it receives
+    def widget_overrides(self):
+        return {}
+
+    # return list of fields that this EditHandler expects to find on the form
+    def required_fields(self):
+        return []
+
+    # return a dict of formsets that this EditHandler requires to be present
+    # as children of the ClusterForm; the dict is a mapping from relation name
+    # to parameters to be passed as part of get_form_for_model's 'formsets' kwarg
+    def required_formsets(self):
+        return {}
+
+    # return a dict mapping field name to the permission codename that a user must have for that
+    # field to be included in the form
+    def field_permissions(self):
+        return {}
+
+    # return any HTML that needs to be output on the edit page once per edit handler definition.
+    # Typically this will be used to define snippets of HTML within <script type="text/x-template"></script> blocks
+    # for JavaScript code to work with.
+    def html_declarations(self):
+        return ""
+
+    def is_shown(self):
+        return True
+
+    def bind_to(self, model=None, instance=None, request=None, form=None):
+        if model is None and instance is not None and self.model is None:
+            model = instance._meta.model
+
+        new = self.clone()
+        new.model = self.model if model is None else model
+        new.instance = self.instance if instance is None else instance
+        new.request = self.request if request is None else request
+        new.form = self.form if form is None else form
+
+        if new.model is not None:
+            new.on_model_bound()
+
+        if new.instance is not None:
+            new.on_instance_bound()
+
+        if new.request is not None:
+            new.on_request_bound()
+
+        if new.form is not None:
+            new.on_form_bound()
+
+        return new
+
+    def on_model_bound(self):
+        pass
+
+    def on_instance_bound(self):
+        pass
+
+    def on_request_bound(self):
+        pass
+
+    def on_form_bound(self):
+        pass
+
+    def __repr__(self):
+        return "<%s with model=%s instance=%s request=%s form=%s>" % (
+            self.__class__.__name__,
+            self.model,
+            self.instance,
+            self.request,
+            self.form.__class__.__name__,
+        )
+
+    def classes(self):
+        """
+        Additional CSS classnames to add to whatever kind of object this is at output.
+        Subclasses of EditHandler should override this, invoking super().classes() to
+        append more classes specific to the situation.
+        """
+        if self.classname:
+            return [self.classname]
+        return []
+
+    def field_type(self):
+        """
+        The kind of field it is e.g boolean_field. Useful for better semantic markup of field display based on type
+        """
+        return ""
+
+    def id_for_label(self):
+        """
+        The ID to be used as the 'for' attribute of any <label> elements that refer
+        to this object but are rendered outside of it. Leave blank if this object does not render
+        as a single input field.
+        """
+        return ""
+
+    def render_as_object(self):
+        """
+        Render this object as it should appear within an ObjectList. Should not
+        include the <h2> heading or help text - ObjectList will supply those
+        """
+        # by default, assume that the subclass provides a catch-all render() method
+        return self.render()
+
+    def render_as_field(self):
+        """
+        Render this object as it should appear within a <ul class="fields"> list item
+        """
+        # by default, assume that the subclass provides a catch-all render() method
+        return self.render()
+
+    def render_missing_fields(self):
+        """
+        Helper function: render all of the fields that are defined on the form but not "claimed" by
+        any panels via required_fields. These fields are most likely to be hidden fields introduced
+        by the forms framework itself, such as ORDER / DELETE fields on formset members.
+
+        (If they aren't actually hidden fields, then they will appear as ugly unstyled / label-less fields
+        outside of the panel furniture. But there's not much we can do about that.)
+        """
+        rendered_fields = self.required_fields()
+        missing_fields_html = [
+            str(self.form[field_name])
+            for field_name in self.form.fields
+            if field_name not in rendered_fields
+        ]
+
+        return mark_safe("".join(missing_fields_html))
+
+    def render_form_content(self):
+        """
+        Render this as an 'object', ensuring that all fields necessary for a valid form
+        submission are included
+        """
+        return mark_safe(self.render_as_object() + self.render_missing_fields())
+
+    def get_comparison(self):
+        return []
+
+
+class BaseCompositeEditHandler(EditHandler):
+    """
+    Abstract class for EditHandlers that manage a set of sub-EditHandlers.
+    Concrete subclasses must attach a 'children' property
+    """
+
+    def __init__(self, children=(), *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.children = children
+
+    def clone_kwargs(self):
+        kwargs = super().clone_kwargs()
+        kwargs["children"] = self.children
+        return kwargs
+
+    def widget_overrides(self):
+        # build a collated version of all its children's widget lists
+        widgets = {}
+        for handler_class in self.children:
+            widgets.update(handler_class.widget_overrides())
+        widget_overrides = widgets
+
+        return widget_overrides
+
+    def required_fields(self):
+        fields = []
+        for handler in self.children:
+            fields.extend(handler.required_fields())
+        return fields
+
+    def required_formsets(self):
+        formsets = {}
+        for handler_class in self.children:
+            formsets.update(handler_class.required_formsets())
+        return formsets
+
+    def field_permissions(self):
+        field_permissions = {}
+        for handler_class in self.children:
+            field_permissions.update(handler_class.field_permissions())
+        return field_permissions
+
+    @property
+    def visible_children(self):
+        return [child for child in self.children if child.is_shown()]
+
+    def is_shown(self):
+        return any(child.is_shown() for child in self.children)
+
+    def html_declarations(self):
+        return mark_safe("".join([c.html_declarations() for c in self.children]))
+
+    def on_model_bound(self):
+        self.children = [child.bind_to(model=self.model) for child in self.children]
+
+    def on_instance_bound(self):
+        self.children = [
+            child.bind_to(instance=self.instance) for child in self.children
+        ]
+
+    def on_request_bound(self):
+        self.children = [child.bind_to(request=self.request) for child in self.children]
+
+    def on_form_bound(self):
+        children = []
+        for child in self.children:
+            if isinstance(child, FieldPanel):
+                if self.form._meta.exclude:
+                    if child.field_name in self.form._meta.exclude:
+                        continue
+                if self.form._meta.fields:
+                    if child.field_name not in self.form._meta.fields:
+                        continue
+            children.append(child.bind_to(form=self.form))
+        self.children = children
+
+    def render(self):
+        return mark_safe(render_to_string(self.template, {"self": self}))
+
+    def get_comparison(self):
+        comparators = []
+
+        for child in self.children:
+            comparators.extend(child.get_comparison())
+
+        return comparators
+
+
+class BaseFormEditHandler(BaseCompositeEditHandler):
+    """
+    Base class for edit handlers that can construct a form class for all their
+    child edit handlers.
+    """
+
+    # The form class used as the base for constructing specific forms for this
+    # edit handler.  Subclasses can override this attribute to provide a form
+    # with custom validation, for example.  Custom forms must subclass
+    # WagtailAdminModelForm
+    base_form_class = None
+
+    def get_form_class(self):
+        """
+        Construct a form class that has all the fields and formsets named in
+        the children of this edit handler.
+        """
+        if self.model is None:
+            raise AttributeError(
+                "%s is not bound to a model yet. Use `.bind_to(model=model)` "
+                "before using this method." % self.__class__.__name__
+            )
+        # If a custom form class was passed to the EditHandler, use it.
+        # Otherwise, use the base_form_class from the model.
+        # If that is not defined, use WagtailAdminModelForm.
+        model_form_class = getattr(self.model, "base_form_class", WagtailAdminModelForm)
+        base_form_class = self.base_form_class or model_form_class
+
+        return get_form_for_model(
+            self.model,
+            form_class=base_form_class,
+            fields=self.required_fields(),
+            formsets=self.required_formsets(),
+            widgets=self.widget_overrides(),
+            field_permissions=self.field_permissions(),
+        )
+
+
+class TabbedInterface(BaseFormEditHandler):
+    template = "wagtailadmin/edit_handlers/tabbed_interface.html"
+
+    def __init__(self, *args, show_comments_toggle=None, **kwargs):
+        self.base_form_class = kwargs.pop("base_form_class", None)
+        super().__init__(*args, **kwargs)
+        if show_comments_toggle is not None:
+            self.show_comments_toggle = show_comments_toggle
+        else:
+            self.show_comments_toggle = (
+                "comment_notifications" in self.required_fields()
+            )
+
+    def get_form_class(self):
+        form_class = super().get_form_class()
+
+        # Set show_comments_toggle attribute on form class
+        return type(
+            form_class.__name__,
+            (form_class,),
+            {"show_comments_toggle": self.show_comments_toggle},
+        )
+
+    def clone_kwargs(self):
+        kwargs = super().clone_kwargs()
+        kwargs["base_form_class"] = self.base_form_class
+        kwargs["show_comments_toggle"] = self.show_comments_toggle
+        return kwargs
+
+
+class ObjectList(TabbedInterface):
+    template = "wagtailadmin/edit_handlers/object_list.html"
+
+
+class FieldRowPanel(BaseCompositeEditHandler):
+    template = "wagtailadmin/edit_handlers/field_row_panel.html"
+
+    def on_instance_bound(self):
+        super().on_instance_bound()
+
+        col_count = " col%s" % (12 // len(self.children))
+        # If child panel doesn't have a col# class then append default based on
+        # number of columns
+        for child in self.children:
+            if not re.search(r"\bcol\d+\b", child.classname):
+                child.classname += col_count
+
+
+class MultiFieldPanel(BaseCompositeEditHandler):
+    template = "wagtailadmin/edit_handlers/multi_field_panel.html"
+
+    def classes(self):
+        classes = super().classes()
+        classes.append("multi-field")
+        return classes
+
+
+class HelpPanel(EditHandler):
+    def __init__(
+        self,
+        content="",
+        template="wagtailadmin/edit_handlers/help_panel.html",
+        heading="",
+        classname="",
+    ):
+        super().__init__(heading=heading, classname=classname)
+        self.content = content
+        self.template = template
+
+    def clone_kwargs(self):
+        kwargs = super().clone_kwargs()
+        del kwargs["help_text"]
+        kwargs.update(
+            content=self.content,
+            template=self.template,
+        )
+        return kwargs
+
+    def render(self):
+        return mark_safe(render_to_string(self.template, {"self": self}))
+
+
+class FieldPanel(EditHandler):
+    TEMPLATE_VAR = "field_panel"
+
+    def __init__(self, field_name, *args, **kwargs):
+        widget = kwargs.pop("widget", None)
+        if widget is not None:
+            self.widget = widget
+        self.permission = kwargs.pop("permission", None)
+        self.disable_comments = kwargs.pop("disable_comments", None)
+        super().__init__(*args, **kwargs)
+        self.field_name = field_name
+
+    def clone_kwargs(self):
+        kwargs = super().clone_kwargs()
+        kwargs.update(
+            field_name=self.field_name,
+            widget=self.widget if hasattr(self, "widget") else None,
+            permission=self.permission,
+        )
+        return kwargs
+
+    def widget_overrides(self):
+        """check if a specific widget has been defined for this field"""
+        if hasattr(self, "widget"):
+            return {self.field_name: self.widget}
+        return {}
+
+    def field_permissions(self):
+        if self.permission:
+            return {self.field_name: self.permission}
+        else:
+            return {}
+
+    def is_shown(self):
+        if (
+            self.permission
+            and self.request
+            and not self.request.user.has_perm(self.permission)
+        ):
+            return False
+
+        return True
+
+    def classes(self):
+        classes = super().classes()
+
+        if self.bound_field.field.required:
+            classes.append("required")
+
+        # If field has any errors, add the classname 'error' to enable error styling
+        # (e.g. red background), unless the widget has its own mechanism for rendering errors
+        # via the render_with_errors mechanism (as StreamField does).
+        if self.bound_field.errors and not hasattr(
+            self.bound_field.field.widget, "render_with_errors"
+        ):
+            classes.append("error")
+
+        classes.append(self.field_type())
+
+        return classes
+
+    def field_type(self):
+        return camelcase_to_underscore(self.bound_field.field.__class__.__name__)
+
+    def id_for_label(self):
+        return self.bound_field.id_for_label
+
+    @property
+    def comments_enabled(self):
+        if self.disable_comments is None:
+            # by default, enable comments on all fields except StreamField (which has its own comment handling)
+            return not isinstance(self.bound_field.field, BlockField)
+        else:
+            return not self.disable_comments
+
+    object_template = "wagtailadmin/edit_handlers/single_field_panel.html"
+
+    def render_as_object(self):
+        return mark_safe(
+            render_to_string(
+                self.object_template,
+                {
+                    "self": self,
+                    self.TEMPLATE_VAR: self,
+                    "field": self.bound_field,
+                    "show_add_comment_button": self.comments_enabled
+                    and getattr(
+                        self.bound_field.field.widget, "show_add_comment_button", True
+                    ),
+                },
+            )
+        )
+
+    field_template = "wagtailadmin/edit_handlers/field_panel_field.html"
+
+    def render_as_field(self):
+        return mark_safe(
+            render_to_string(
+                self.field_template,
+                {
+                    "field": self.bound_field,
+                    "field_type": self.field_type(),
+                    "show_add_comment_button": self.comments_enabled
+                    and getattr(
+                        self.bound_field.field.widget, "show_add_comment_button", True
+                    ),
+                },
+            )
+        )
+
+    def required_fields(self):
+        return [self.field_name]
+
+    def get_comparison_class(self):
+        # Hide fields with hidden widget
+        widget_override = self.widget_overrides().get(self.field_name, None)
+        if widget_override and widget_override.is_hidden:
+            return
+
+        try:
+            field = self.db_field
+
+            if field.choices:
+                return compare.ChoiceFieldComparison
+
+            comparison_class = compare.comparison_class_registry.get(field)
+            if comparison_class:
+                return comparison_class
+
+            if field.is_relation:
+                if field.many_to_many:
+                    return compare.M2MFieldComparison
+
+                return compare.ForeignObjectComparison
+
+        except FieldDoesNotExist:
+            pass
+
+        return compare.FieldComparison
+
+    def get_comparison(self):
+        comparator_class = self.get_comparison_class()
+
+        if comparator_class and self.is_shown():
+            try:
+                return [functools.partial(comparator_class, self.db_field)]
+            except FieldDoesNotExist:
+                return []
+        return []
+
+    @cached_property
+    def db_field(self):
+        try:
+            model = self.model
+        except AttributeError:
+            raise ImproperlyConfigured(
+                "%r must be bound to a model before calling db_field" % self
+            )
+
+        return model._meta.get_field(self.field_name)
+
+    def on_form_bound(self):
+        try:
+            self.bound_field = self.form[self.field_name]
+        except KeyError:
+            return
+
+        if self.heading:
+            self.bound_field.label = self.heading
+        else:
+            self.heading = self.bound_field.label
+        self.help_text = self.bound_field.help_text
+
+    def __repr__(self):
+        return "<%s '%s' with model=%s instance=%s request=%s form=%s>" % (
+            self.__class__.__name__,
+            self.field_name,
+            self.model,
+            self.instance,
+            self.request,
+            self.form.__class__.__name__,
+        )
+
+
+class RichTextFieldPanel(FieldPanel):
+    def __init__(self, *args, **kwargs):
+        warn(
+            "wagtail.admin.edit_handlers.RichTextFieldPanel is obsolete and should be replaced by wagtail.admin.panels.FieldPanel",
+            category=RemovedInWagtail219Warning,
+            stacklevel=2,
+        )
+        super().__init__(*args, **kwargs)
+
+
+class BaseChooserPanel(FieldPanel):
+    def __init__(self, *args, **kwargs):
+        warn(
+            "wagtail.admin.edit_handlers.BaseChooserPanel is obsolete and should be replaced by wagtail.admin.panels.FieldPanel",
+            category=RemovedInWagtail219Warning,
+            stacklevel=2,
+        )
+        super().__init__(*args, **kwargs)
+
+
+class PageChooserPanel(FieldPanel):
+    def __init__(self, field_name, page_type=None, can_choose_root=False):
+        super().__init__(field_name=field_name)
+
+        self.page_type = page_type
+        self.can_choose_root = can_choose_root
+
+    def clone_kwargs(self):
+        return {
+            "field_name": self.field_name,
+            "page_type": self.page_type,
+            "can_choose_root": self.can_choose_root,
+        }
+
+    def widget_overrides(self):
+        if self.page_type or self.can_choose_root:
+            return {
+                self.field_name: widgets.AdminPageChooser(
+                    target_models=self.page_type, can_choose_root=self.can_choose_root
+                )
+            }
+        else:
+            return {}
+
+
+class InlinePanel(EditHandler):
+    def __init__(
+        self,
+        relation_name,
+        panels=None,
+        heading="",
+        label="",
+        min_num=None,
+        max_num=None,
+        *args,
+        **kwargs,
+    ):
+        super().__init__(*args, **kwargs)
+        self.relation_name = relation_name
+        self.panels = panels
+        self.heading = heading or label
+        self.label = label
+        self.min_num = min_num
+        self.max_num = max_num
+
+    def clone_kwargs(self):
+        kwargs = super().clone_kwargs()
+        kwargs.update(
+            relation_name=self.relation_name,
+            panels=self.panels,
+            label=self.label,
+            min_num=self.min_num,
+            max_num=self.max_num,
+        )
+        return kwargs
+
+    def get_panel_definitions(self):
+        # Look for a panels definition in the InlinePanel declaration
+        if self.panels is not None:
+            return self.panels
+        # Failing that, get it from the model
+        return extract_panel_definitions_from_model_class(
+            self.db_field.related_model, exclude=[self.db_field.field.name]
+        )
+
+    def get_child_edit_handler(self):
+        panels = self.get_panel_definitions()
+        child_edit_handler = MultiFieldPanel(panels, heading=self.heading)
+        return child_edit_handler.bind_to(model=self.db_field.related_model)
+
+    def required_formsets(self):
+        child_edit_handler = self.get_child_edit_handler()
+        return {
+            self.relation_name: {
+                "fields": child_edit_handler.required_fields(),
+                "widgets": child_edit_handler.widget_overrides(),
+                "min_num": self.min_num,
+                "validate_min": self.min_num is not None,
+                "max_num": self.max_num,
+                "validate_max": self.max_num is not None,
+                "formsets": child_edit_handler.required_formsets(),
+            }
+        }
+
+    def html_declarations(self):
+        return self.get_child_edit_handler().html_declarations()
+
+    def get_comparison(self):
+        field_comparisons = []
+
+        for panel in self.get_panel_definitions():
+            field_comparisons.extend(
+                panel.bind_to(model=self.db_field.related_model).get_comparison()
+            )
+
+        return [
+            functools.partial(
+                compare.ChildRelationComparison, self.db_field, field_comparisons
+            )
+        ]
+
+    def on_model_bound(self):
+        manager = getattr(self.model, self.relation_name)
+        self.db_field = manager.rel
+
+    def on_form_bound(self):
+        self.formset = self.form.formsets[self.relation_name]
+
+        self.children = []
+        for subform in self.formset.forms:
+            # override the DELETE field to have a hidden input
+            subform.fields[DELETION_FIELD_NAME].widget = forms.HiddenInput()
+
+            # ditto for the ORDER field, if present
+            if self.formset.can_order:
+                subform.fields[ORDERING_FIELD_NAME].widget = forms.HiddenInput()
+
+            child_edit_handler = self.get_child_edit_handler()
+            self.children.append(
+                child_edit_handler.bind_to(
+                    instance=subform.instance, request=self.request, form=subform
+                )
+            )
+
+        # if this formset is valid, it may have been re-ordered; respect that
+        # in case the parent form errored and we need to re-render
+        if self.formset.can_order and self.formset.is_valid():
+            self.children.sort(
+                key=lambda child: child.form.cleaned_data[ORDERING_FIELD_NAME] or 1
+            )
+
+        empty_form = self.formset.empty_form
+        empty_form.fields[DELETION_FIELD_NAME].widget = forms.HiddenInput()
+        if self.formset.can_order:
+            empty_form.fields[ORDERING_FIELD_NAME].widget = forms.HiddenInput()
+
+        self.empty_child = self.get_child_edit_handler()
+        self.empty_child = self.empty_child.bind_to(
+            instance=empty_form.instance, request=self.request, form=empty_form
+        )
+
+    template = "wagtailadmin/edit_handlers/inline_panel.html"
+
+    def render(self):
+        formset = render_to_string(
+            self.template,
+            {
+                "self": self,
+                "can_order": self.formset.can_order,
+            },
+        )
+        js = self.render_js_init()
+        return widget_with_script(formset, js)
+
+    js_template = "wagtailadmin/edit_handlers/inline_panel.js"
+
+    def render_js_init(self):
+        return mark_safe(
+            render_to_string(
+                self.js_template,
+                {
+                    "self": self,
+                    "can_order": self.formset.can_order,
+                },
+            )
+        )
+
+
+# This allows users to include the publishing panel in their own per-model override
+# without having to write these fields out by hand, potentially losing 'classname'
+# and therefore the associated styling of the publishing panel
+class PublishingPanel(MultiFieldPanel):
+    def __init__(self, **kwargs):
+        updated_kwargs = {
+            "children": [
+                FieldRowPanel(
+                    [
+                        FieldPanel("go_live_at"),
+                        FieldPanel("expire_at"),
+                    ],
+                    classname="label-above",
+                ),
+            ],
+            "heading": gettext_lazy("Scheduled publishing"),
+            "classname": "publishing",
+        }
+        updated_kwargs.update(kwargs)
+        super().__init__(**updated_kwargs)
+
+
+class PrivacyModalPanel(EditHandler):
+    def __init__(self, **kwargs):
+        updated_kwargs = {"heading": gettext_lazy("Privacy"), "classname": "privacy"}
+        updated_kwargs.update(kwargs)
+        super().__init__(**updated_kwargs)
+
+    def render(self):
+        content = render_to_string(
+            "wagtailadmin/pages/privacy_switch_panel.html",
+            {"self": self, "page": self.instance, "request": self.request},
+        )
+
+        from wagtail.admin.staticfiles import versioned_static
+
+        return mark_safe(
+            '{0}<script type="text/javascript" src="{1}"></script>'.format(
+                content, versioned_static("wagtailadmin/js/privacy-switch.js")
+            )
+        )
+
+
+class CommentPanel(EditHandler):
+    def required_fields(self):
+        # Adds the comment notifications field to the form.
+        # Note, this field is defined directly on WagtailAdminPageForm.
+        return ["comment_notifications"]
+
+    def required_formsets(self):
+        # add the comments formset
+        # we need to pass in the current user for validation on the formset
+        # this could alternatively be done on the page form itself if we added the
+        # comments formset there, but we typically only add fields via edit handlers
+        current_user = getattr(self.request, "user", None)
+
+        class CommentReplyFormWithRequest(CommentReplyForm):
+            user = current_user
+
+        class CommentFormWithRequest(CommentForm):
+            user = current_user
+
+            class Meta:
+                formsets = {"replies": {"form": CommentReplyFormWithRequest}}
+
+        return {
+            COMMENTS_RELATION_NAME: {
+                "form": CommentFormWithRequest,
+                "fields": ["text", "contentpath", "position"],
+                "formset_name": "comments",
+            }
+        }
+
+    template = "wagtailadmin/edit_handlers/comments/comment_panel.html"
+    declarations_template = (
+        "wagtailadmin/edit_handlers/comments/comment_declarations.html"
+    )
+
+    def html_declarations(self):
+        return render_to_string(self.declarations_template)
+
+    def get_context(self):
+        def user_data(user):
+            return {"name": user_display_name(user), "avatar_url": avatar_url(user)}
+
+        user = getattr(self.request, "user", None)
+        user_pks = {user.pk}
+        serialized_comments = []
+        bound = self.form.is_bound
+        comment_formset = self.form.formsets.get("comments")
+        comment_forms = comment_formset.forms if comment_formset else []
+        for form in comment_forms:
+            # iterate over comments to retrieve users (to get display names) and serialized versions
+            replies = []
+            for reply_form in form.formsets["replies"].forms:
+                user_pks.add(reply_form.instance.user_id)
+                reply_data = get_serializable_data_for_fields(reply_form.instance)
+                reply_data["deleted"] = (
+                    reply_form.cleaned_data.get("DELETE", False) if bound else False
+                )
+                replies.append(reply_data)
+            user_pks.add(form.instance.user_id)
+            data = get_serializable_data_for_fields(form.instance)
+            data["deleted"] = form.cleaned_data.get("DELETE", False) if bound else False
+            data["resolved"] = (
+                form.cleaned_data.get("resolved", False)
+                if bound
+                else form.instance.resolved_at is not None
+            )
+            data["replies"] = replies
+            serialized_comments.append(data)
+
+        authors = {
+            str(user.pk): user_data(user)
+            for user in get_user_model()
+            .objects.filter(pk__in=user_pks)
+            .select_related("wagtail_userprofile")
+        }
+
+        comments_data = {
+            "comments": serialized_comments,
+            "user": user.pk,
+            "authors": authors,
+        }
+
+        return {
+            "comments_data": comments_data,
+        }
+
+    def render(self):
+        panel = render_to_string(self.template, self.get_context())
+        return panel
+
+
+# Now that we've defined EditHandlers, we can set up wagtailcore.Page to have some.
+def set_default_page_edit_handlers(cls):
+    cls.content_panels = [
+        FieldPanel("title", classname="full 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(),
+        PrivacyModalPanel(),
+    ]
+
+    if getattr(settings, "WAGTAILADMIN_COMMENTS_ENABLED", True):
+        cls.settings_panels.append(CommentPanel())
+
+    cls.base_form_class = WagtailAdminPageForm
+
+
+set_default_page_edit_handlers(Page)
+
+
+@cached_classmethod
+def get_edit_handler(cls):
+    """
+    Get the EditHandler to use in the Wagtail admin when editing this page type.
+    """
+    if hasattr(cls, "edit_handler"):
+        edit_handler = cls.edit_handler
+    else:
+        # construct a TabbedInterface made up of content_panels, promote_panels
+        # and settings_panels, skipping any which are empty
+        tabs = []
+
+        if cls.content_panels:
+            tabs.append(ObjectList(cls.content_panels, heading=gettext_lazy("Content")))
+        if cls.promote_panels:
+            tabs.append(ObjectList(cls.promote_panels, heading=gettext_lazy("Promote")))
+        if cls.settings_panels:
+            tabs.append(
+                ObjectList(
+                    cls.settings_panels,
+                    heading=gettext_lazy("Settings"),
+                    classname="settings",
+                )
+            )
+
+        edit_handler = TabbedInterface(tabs, base_form_class=cls.base_form_class)
+
+    return edit_handler.bind_to(model=cls)
+
+
+Page.get_edit_handler = get_edit_handler
+
+
+@receiver(setting_changed)
+def reset_page_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()
+
+
+class StreamFieldPanel(FieldPanel):
+    def __init__(self, *args, **kwargs):
+        warn(
+            "wagtail.admin.edit_handlers.StreamFieldPanel is obsolete and should be replaced by wagtail.admin.panels.FieldPanel",
+            category=RemovedInWagtail219Warning,
+            stacklevel=2,
+        )
+        super().__init__(*args, **kwargs)

+ 1 - 1
wagtail/admin/tests/pages/test_preview.py

@@ -7,7 +7,7 @@ from django.urls import reverse
 from django.utils import timezone
 from django.utils import timezone
 from freezegun import freeze_time
 from freezegun import freeze_time
 
 
-from wagtail.admin.edit_handlers import FieldPanel, ObjectList, TabbedInterface
+from wagtail.admin.panels import FieldPanel, ObjectList, TabbedInterface
 from wagtail.admin.views.pages.preview import PreviewOnEdit
 from wagtail.admin.views.pages.preview import PreviewOnEdit
 from wagtail.models import Page
 from wagtail.models import Page
 from wagtail.test.testapp.models import EventCategory, EventPage, SimplePage, StreamPage
 from wagtail.test.testapp.models import EventCategory, EventPage, SimplePage, StreamPage

+ 2 - 2
wagtail/admin/tests/test_edit_handlers.py

@@ -13,7 +13,8 @@ from django.utils.html import json_script
 from freezegun import freeze_time
 from freezegun import freeze_time
 from pytz import utc
 from pytz import utc
 
 
-from wagtail.admin.edit_handlers import (
+from wagtail.admin.forms import WagtailAdminModelForm, WagtailAdminPageForm
+from wagtail.admin.panels import (
     CommentPanel,
     CommentPanel,
     FieldPanel,
     FieldPanel,
     FieldRowPanel,
     FieldRowPanel,
@@ -25,7 +26,6 @@ from wagtail.admin.edit_handlers import (
     extract_panel_definitions_from_model_class,
     extract_panel_definitions_from_model_class,
     get_form_for_model,
     get_form_for_model,
 )
 )
-from wagtail.admin.forms import WagtailAdminModelForm, WagtailAdminPageForm
 from wagtail.admin.rich_text import DraftailRichTextArea
 from wagtail.admin.rich_text import DraftailRichTextArea
 from wagtail.admin.widgets import (
 from wagtail.admin.widgets import (
     AdminAutoHeightTextInput,
     AdminAutoHeightTextInput,

+ 1 - 0
wagtail/bin/wagtail.py

@@ -153,6 +153,7 @@ class UpdateModulePaths(Command):
         # Added in Wagtail 3.0
         # Added in Wagtail 3.0
         (re.compile(r"\bwagtail\.tests\b"), "wagtail.test"),
         (re.compile(r"\bwagtail\.tests\b"), "wagtail.test"),
         (re.compile(r"\bwagtail\.core\b"), "wagtail"),
         (re.compile(r"\bwagtail\.core\b"), "wagtail"),
+        (re.compile(r"\bwagtail\.admin\.edit_handlers\b"), "wagtail.admin.panels"),
     ]
     ]
 
 
     def add_arguments(self, parser):
     def add_arguments(self, parser):

+ 1 - 1
wagtail/contrib/forms/edit_handlers.py

@@ -2,7 +2,7 @@ from django.template.loader import render_to_string
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext as _
 
 
-from wagtail.admin.edit_handlers import EditHandler
+from wagtail.admin.panels import EditHandler
 
 
 
 
 class FormSubmissionsPanel(EditHandler):
 class FormSubmissionsPanel(EditHandler):

+ 1 - 1
wagtail/contrib/forms/models.py

@@ -12,8 +12,8 @@ from django.utils.formats import date_format
 from django.utils.text import slugify
 from django.utils.text import slugify
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 
 
-from wagtail.admin.edit_handlers import FieldPanel
 from wagtail.admin.mail import send_mail
 from wagtail.admin.mail import send_mail
+from wagtail.admin.panels import FieldPanel
 from wagtail.contrib.forms.utils import get_field_clean_name
 from wagtail.contrib.forms.utils import get_field_clean_name
 from wagtail.models import Orderable, Page
 from wagtail.models import Orderable, Page
 
 

+ 1 - 1
wagtail/contrib/forms/tests/test_views.py

@@ -10,8 +10,8 @@ from django.test import RequestFactory, TestCase, override_settings
 from django.urls import reverse
 from django.urls import reverse
 from openpyxl import load_workbook
 from openpyxl import load_workbook
 
 
-from wagtail.admin.edit_handlers import get_form_for_model
 from wagtail.admin.forms import WagtailAdminPageForm
 from wagtail.admin.forms import WagtailAdminPageForm
+from wagtail.admin.panels import get_form_for_model
 from wagtail.contrib.forms.edit_handlers import FormSubmissionsPanel
 from wagtail.contrib.forms.edit_handlers import FormSubmissionsPanel
 from wagtail.contrib.forms.models import FormSubmission
 from wagtail.contrib.forms.models import FormSubmission
 from wagtail.contrib.forms.tests.utils import (
 from wagtail.contrib.forms.tests.utils import (

+ 1 - 4
wagtail/contrib/modeladmin/options.py

@@ -9,10 +9,7 @@ from django.utils.safestring import mark_safe
 from wagtail import hooks
 from wagtail import hooks
 from wagtail.admin.admin_url_finder import register_admin_url_finder
 from wagtail.admin.admin_url_finder import register_admin_url_finder
 from wagtail.admin.checks import check_panels_in_model
 from wagtail.admin.checks import check_panels_in_model
-from wagtail.admin.edit_handlers import (
-    ObjectList,
-    extract_panel_definitions_from_model_class,
-)
+from wagtail.admin.panels import ObjectList, extract_panel_definitions_from_model_class
 from wagtail.models import Page
 from wagtail.models import Page
 
 
 from .helpers import (
 from .helpers import (

+ 1 - 1
wagtail/contrib/modeladmin/tests/test_modeladmin_edit_handlers.py

@@ -2,7 +2,7 @@ from unittest import mock
 
 
 from django.test import RequestFactory, TestCase
 from django.test import RequestFactory, TestCase
 
 
-from wagtail.admin.edit_handlers import FieldPanel, ObjectList, TabbedInterface
+from wagtail.admin.panels import FieldPanel, ObjectList, TabbedInterface
 from wagtail.contrib.modeladmin.views import CreateView
 from wagtail.contrib.modeladmin.views import CreateView
 from wagtail.test.modeladmintest.models import Person
 from wagtail.test.modeladmintest.models import Person
 from wagtail.test.modeladmintest.wagtail_hooks import PersonAdmin
 from wagtail.test.modeladmintest.wagtail_hooks import PersonAdmin

+ 1 - 1
wagtail/contrib/modeladmin/tests/test_simple_modeladmin.py

@@ -10,7 +10,7 @@ from django.utils.timezone import make_aware
 from openpyxl import load_workbook
 from openpyxl import load_workbook
 
 
 from wagtail.admin.admin_url_finder import AdminURLFinder
 from wagtail.admin.admin_url_finder import AdminURLFinder
-from wagtail.admin.edit_handlers import FieldPanel, TabbedInterface
+from wagtail.admin.panels import FieldPanel, TabbedInterface
 from wagtail.contrib.modeladmin.helpers.search import DjangoORMSearchHandler
 from wagtail.contrib.modeladmin.helpers.search import DjangoORMSearchHandler
 from wagtail.images.models import Image
 from wagtail.images.models import Image
 from wagtail.images.tests.utils import get_test_image_file
 from wagtail.images.tests.utils import get_test_image_file

+ 1 - 1
wagtail/contrib/settings/tests/test_admin.py

@@ -5,7 +5,7 @@ from django.utils.text import capfirst
 
 
 from wagtail import hooks
 from wagtail import hooks
 from wagtail.admin.admin_url_finder import AdminURLFinder
 from wagtail.admin.admin_url_finder import AdminURLFinder
-from wagtail.admin.edit_handlers import FieldPanel, ObjectList, TabbedInterface
+from wagtail.admin.panels import FieldPanel, ObjectList, TabbedInterface
 from wagtail.contrib.settings.registry import SettingMenuItem
 from wagtail.contrib.settings.registry import SettingMenuItem
 from wagtail.contrib.settings.views import get_setting_edit_handler
 from wagtail.contrib.settings.views import get_setting_edit_handler
 from wagtail.models import Page, Site
 from wagtail.models import Page, Site

+ 1 - 1
wagtail/contrib/settings/views.py

@@ -9,7 +9,7 @@ from django.utils.text import capfirst
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext as _
 
 
 from wagtail.admin import messages
 from wagtail.admin import messages
-from wagtail.admin.edit_handlers import (
+from wagtail.admin.panels import (
     ObjectList,
     ObjectList,
     TabbedInterface,
     TabbedInterface,
     extract_panel_definitions_from_model_class,
     extract_panel_definitions_from_model_class,

+ 2 - 2
wagtail/documents/edit_handlers.py

@@ -1,13 +1,13 @@
 from warnings import warn
 from warnings import warn
 
 
-from wagtail.admin.edit_handlers import FieldPanel
+from wagtail.admin.panels import FieldPanel
 from wagtail.utils.deprecation import RemovedInWagtail219Warning
 from wagtail.utils.deprecation import RemovedInWagtail219Warning
 
 
 
 
 class DocumentChooserPanel(FieldPanel):
 class DocumentChooserPanel(FieldPanel):
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
         warn(
         warn(
-            "wagtail.documents.edit_handlers.DocumentChooserPanel is obsolete and should be replaced by wagtail.admin.edit_handlers.FieldPanel",
+            "wagtail.documents.edit_handlers.DocumentChooserPanel is obsolete and should be replaced by wagtail.admin.panels.FieldPanel",
             category=RemovedInWagtail219Warning,
             category=RemovedInWagtail219Warning,
             stacklevel=2,
             stacklevel=2,
         )
         )

+ 2 - 2
wagtail/images/edit_handlers.py

@@ -3,14 +3,14 @@ from warnings import warn
 from django.template.loader import render_to_string
 from django.template.loader import render_to_string
 
 
 from wagtail.admin.compare import ForeignObjectComparison
 from wagtail.admin.compare import ForeignObjectComparison
-from wagtail.admin.edit_handlers import FieldPanel
+from wagtail.admin.panels import FieldPanel
 from wagtail.utils.deprecation import RemovedInWagtail219Warning
 from wagtail.utils.deprecation import RemovedInWagtail219Warning
 
 
 
 
 class ImageChooserPanel(FieldPanel):
 class ImageChooserPanel(FieldPanel):
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
         warn(
         warn(
-            "wagtail.images.edit_handlers.ImageChooserPanel is obsolete and should be replaced by wagtail.admin.edit_handlers.FieldPanel",
+            "wagtail.images.edit_handlers.ImageChooserPanel is obsolete and should be replaced by wagtail.admin.panels.FieldPanel",
             category=RemovedInWagtail219Warning,
             category=RemovedInWagtail219Warning,
             stacklevel=2,
             stacklevel=2,
         )
         )

+ 1 - 1
wagtail/models/__init__.py

@@ -379,7 +379,7 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
     ]
     ]
 
 
     # Define these attributes early to avoid masking errors. (Issue #3078)
     # Define these attributes early to avoid masking errors. (Issue #3078)
-    # The canonical definition is in wagtailadmin.edit_handlers.
+    # The canonical definition is in wagtailadmin.panels.
     content_panels = []
     content_panels = []
     promote_panels = []
     promote_panels = []
     settings_panels = []
     settings_panels = []

+ 2 - 2
wagtail/snippets/edit_handlers.py

@@ -1,13 +1,13 @@
 from warnings import warn
 from warnings import warn
 
 
-from wagtail.admin.edit_handlers import FieldPanel
+from wagtail.admin.panels import FieldPanel
 from wagtail.utils.deprecation import RemovedInWagtail219Warning
 from wagtail.utils.deprecation import RemovedInWagtail219Warning
 
 
 
 
 class SnippetChooserPanel(FieldPanel):
 class SnippetChooserPanel(FieldPanel):
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
         warn(
         warn(
-            "wagtail.snippets.edit_handlers.SnippetChooserPanel is obsolete and should be replaced by wagtail.admin.edit_handlers.FieldPanel",
+            "wagtail.snippets.edit_handlers.SnippetChooserPanel is obsolete and should be replaced by wagtail.admin.panels.FieldPanel",
             category=RemovedInWagtail219Warning,
             category=RemovedInWagtail219Warning,
             stacklevel=2,
             stacklevel=2,
         )
         )

+ 1 - 1
wagtail/snippets/tests.py

@@ -19,8 +19,8 @@ from taggit.models import Tag
 
 
 from wagtail import hooks
 from wagtail import hooks
 from wagtail.admin.admin_url_finder import AdminURLFinder
 from wagtail.admin.admin_url_finder import AdminURLFinder
-from wagtail.admin.edit_handlers import FieldPanel, ObjectList
 from wagtail.admin.forms import WagtailAdminModelForm
 from wagtail.admin.forms import WagtailAdminModelForm
+from wagtail.admin.panels import FieldPanel, ObjectList
 from wagtail.blocks.field_block import FieldBlockAdapter
 from wagtail.blocks.field_block import FieldBlockAdapter
 from wagtail.models import Locale, ModelLogEntry, Page
 from wagtail.models import Locale, ModelLogEntry, Page
 from wagtail.snippets.action_menu import (
 from wagtail.snippets.action_menu import (

+ 1 - 4
wagtail/snippets/views/snippets.py

@@ -17,11 +17,8 @@ from django.views.generic import TemplateView
 
 
 from wagtail import hooks
 from wagtail import hooks
 from wagtail.admin import messages
 from wagtail.admin import messages
-from wagtail.admin.edit_handlers import (
-    ObjectList,
-    extract_panel_definitions_from_model_class,
-)
 from wagtail.admin.forms.search import SearchForm
 from wagtail.admin.forms.search import SearchForm
+from wagtail.admin.panels import ObjectList, extract_panel_definitions_from_model_class
 from wagtail.admin.ui.tables import Column, DateColumn, UserColumn
 from wagtail.admin.ui.tables import Column, DateColumn, UserColumn
 from wagtail.admin.views.generic.models import IndexView
 from wagtail.admin.views.generic.models import IndexView
 from wagtail.log_actions import log
 from wagtail.log_actions import log

+ 1 - 1
wagtail/test/customuser/models.py

@@ -9,7 +9,7 @@ from django.db import models
 # (which is easily done because it's dealing with django.contrib.auth views which depend
 # (which is easily done because it's dealing with django.contrib.auth views which depend
 # on the user model)
 # on the user model)
 from wagtail.admin.auth import permission_denied  # noqa
 from wagtail.admin.auth import permission_denied  # noqa
-from wagtail.admin.edit_handlers import FieldPanel
+from wagtail.admin.panels import FieldPanel
 
 
 from .fields import ConvertedValueField
 from .fields import ConvertedValueField
 
 

+ 1 - 1
wagtail/test/demosite/models.py

@@ -7,7 +7,7 @@ from modelcluster.contrib.taggit import ClusterTaggableManager
 from modelcluster.fields import ParentalKey
 from modelcluster.fields import ParentalKey
 from taggit.models import TaggedItemBase
 from taggit.models import TaggedItemBase
 
 
-from wagtail.admin.edit_handlers import FieldPanel, InlinePanel, MultiFieldPanel
+from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel
 from wagtail.api import APIField
 from wagtail.api import APIField
 from wagtail.fields import RichTextField
 from wagtail.fields import RichTextField
 from wagtail.images.api.fields import ImageRenditionField
 from wagtail.images.api.fields import ImageRenditionField

+ 1 - 1
wagtail/test/modeladmintest/models.py

@@ -1,6 +1,6 @@
 from django.db import models
 from django.db import models
 
 
-from wagtail.admin.edit_handlers import (
+from wagtail.admin.panels import (
     FieldPanel,
     FieldPanel,
     MultiFieldPanel,
     MultiFieldPanel,
     ObjectList,
     ObjectList,

+ 1 - 1
wagtail/test/modeladmintest/wagtail_hooks.py

@@ -1,4 +1,4 @@
-from wagtail.admin.edit_handlers import FieldPanel, ObjectList, TabbedInterface
+from wagtail.admin.panels import FieldPanel, ObjectList, TabbedInterface
 from wagtail.contrib.modeladmin.helpers import WagtailBackendSearchHandler
 from wagtail.contrib.modeladmin.helpers import WagtailBackendSearchHandler
 from wagtail.contrib.modeladmin.options import (
 from wagtail.contrib.modeladmin.options import (
     ModelAdmin,
     ModelAdmin,

+ 1 - 1
wagtail/test/snippets/models.py

@@ -2,7 +2,7 @@ from django.db import models
 from modelcluster.fields import ParentalKey
 from modelcluster.fields import ParentalKey
 from modelcluster.models import ClusterableModel
 from modelcluster.models import ClusterableModel
 
 
-from wagtail.admin.edit_handlers import FieldPanel, InlinePanel
+from wagtail.admin.panels import FieldPanel, InlinePanel
 from wagtail.fields import RichTextField
 from wagtail.fields import RichTextField
 from wagtail.models import TranslatableMixin
 from wagtail.models import TranslatableMixin
 from wagtail.search import index
 from wagtail.search import index

+ 3 - 3
wagtail/test/testapp/models.py

@@ -19,15 +19,15 @@ from modelcluster.models import ClusterableModel
 from taggit.managers import TaggableManager
 from taggit.managers import TaggableManager
 from taggit.models import ItemBase, TagBase, TaggedItemBase
 from taggit.models import ItemBase, TagBase, TaggedItemBase
 
 
-from wagtail.admin.edit_handlers import (
+from wagtail.admin.forms import WagtailAdminPageForm
+from wagtail.admin.mail import send_mail
+from wagtail.admin.panels import (
     FieldPanel,
     FieldPanel,
     InlinePanel,
     InlinePanel,
     MultiFieldPanel,
     MultiFieldPanel,
     ObjectList,
     ObjectList,
     TabbedInterface,
     TabbedInterface,
 )
 )
-from wagtail.admin.forms import WagtailAdminPageForm
-from wagtail.admin.mail import send_mail
 from wagtail.blocks import (
 from wagtail.blocks import (
     CharBlock,
     CharBlock,
     FieldBlock,
     FieldBlock,