Move base Panel class to wagtail.admin.panels.base

pull/9867/head
Matt Westcott 2022-12-12 22:40:13 +00:00 zatwierdzone przez LB (Ben Johnston)
rodzic d94dc975dd
commit 5713031eed
2 zmienionych plików z 361 dodań i 350 usunięć

Wyświetl plik

@ -12,7 +12,6 @@ from django.forms import Media
from django.forms.formsets import DELETION_FIELD_NAME, ORDERING_FIELD_NAME
from django.forms.models import fields_for_model
from django.utils.functional import cached_property
from django.utils.safestring import mark_safe
from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy
from modelcluster.models import get_serializable_data_for_fields
@ -26,56 +25,20 @@ from wagtail.admin.forms.comments import CommentForm
from wagtail.admin.forms.models import ( # NOQA
DIRECT_FORM_FIELD_OVERRIDES,
FORM_FIELD_OVERRIDES,
WagtailAdminDraftStateFormMixin,
WagtailAdminModelForm,
formfield_for_dbfield,
)
from wagtail.admin.forms.pages import WagtailAdminPageForm
from wagtail.admin.staticfiles import versioned_static
from wagtail.admin.templatetags.wagtailadmin_tags import avatar_url, user_display_name
from wagtail.admin.ui.components import Component
from wagtail.admin.widgets import AdminPageChooser
from wagtail.admin.widgets.datetime import AdminDateTimeInput
from wagtail.blocks import BlockField
from wagtail.coreutils import safe_snake_case
from wagtail.models import COMMENTS_RELATION_NAME, DraftStateMixin, Page
from wagtail.models import COMMENTS_RELATION_NAME, Page
from wagtail.utils.decorators import cached_classmethod
from wagtail.utils.deprecation import RemovedInWagtail50Warning
def get_form_for_model(
model,
form_class=WagtailAdminModelForm,
**kwargs,
):
"""
Construct a ModelForm subclass using the given model and base form class. Any additional
keyword arguments are used to populate the form's Meta class.
"""
# This is really just Django's modelform_factory, tweaked to accept arbitrary kwargs.
meta_class_attrs = kwargs
meta_class_attrs["model"] = model
# The kwargs passed here are expected to come from EditHandler.get_form_options, which collects
# them by descending the tree of child edit handlers. If there are no edit handlers that
# specify form fields, this can legitimately result in both 'fields' and 'exclude' being
# absent, which ModelForm doesn't normally allow. In this case, explicitly set fields to [].
if "fields" not in meta_class_attrs and "exclude" not in meta_class_attrs:
meta_class_attrs["fields"] = []
# Give this new form class a reasonable name.
class_name = model.__name__ + "Form"
bases = (form_class.Meta,) if hasattr(form_class, "Meta") else ()
Meta = type("Meta", bases, meta_class_attrs)
form_class_attrs = {"Meta": Meta}
metaclass = type(form_class)
bases = [form_class]
if issubclass(model, DraftStateMixin):
bases.insert(0, WagtailAdminDraftStateFormMixin)
return metaclass(class_name, tuple(bases), form_class_attrs)
from .base import * # NOQA
from .base import Panel
def extract_panel_definitions_from_model_class(model, exclude=None):
@ -104,316 +67,6 @@ def extract_panel_definitions_from_model_class(model, exclude=None):
return panels
class Panel:
"""
Defines part (or all) of the edit form interface for pages and other models within the Wagtail
admin. Each model has an associated panel definition, consisting of a nested structure of Panel
objects - this provides methods for obtaining a ModelForm subclass, with the field list and
other parameters collated from all panels in the structure. It then handles rendering that form
as HTML.
"""
def __init__(
self,
heading="",
classname="",
help_text="",
base_form_class=None,
icon="",
):
self.heading = heading
self.classname = classname
self.help_text = help_text
self.base_form_class = base_form_class
self.icon = icon
self.model = None
def clone(self):
"""
Create a clone of this panel definition. By default, constructs a new instance, passing the
keyword arguments returned by ``clone_kwargs``.
"""
return self.__class__(**self.clone_kwargs())
def clone_kwargs(self):
"""
Return a dictionary of keyword arguments that can be used to create a clone of this panel definition.
"""
return {
"icon": self.icon,
"heading": self.heading,
"classname": self.classname,
"help_text": self.help_text,
"base_form_class": self.base_form_class,
}
def get_form_options(self):
"""
Return a dictionary of attributes such as 'fields', 'formsets' and 'widgets'
which should be incorporated into the form class definition to generate a form
that this panel can use.
This will only be called after binding to a model (i.e. self.model is available).
"""
options = {}
if not getattr(self.widget_overrides, "is_original_method", False):
warn(
"The `widget_overrides` method (on %r) is deprecated; "
"these should be returned from `get_form_options` as a "
"`widgets` item instead." % type(self),
category=RemovedInWagtail50Warning,
)
options["widgets"] = self.widget_overrides()
if not getattr(self.required_fields, "is_original_method", False):
warn(
"The `required_fields` method (on %r) is deprecated; "
"these should be returned from `get_form_options` as a "
"`fields` item instead." % type(self),
category=RemovedInWagtail50Warning,
)
options["fields"] = self.required_fields()
if not getattr(self.required_formsets, "is_original_method", False):
warn(
"The `required_formsets` method (on %r) is deprecated; "
"these should be returned from `get_form_options` as a "
"`formsets` item instead." % type(self),
category=RemovedInWagtail50Warning,
)
options["formsets"] = self.required_formsets()
return options
# RemovedInWagtail50Warning - edit handlers should override get_form_options instead
def widget_overrides(self):
return {}
widget_overrides.is_original_method = True
# RemovedInWagtail50Warning - edit handlers should override get_form_options instead
def required_fields(self):
return []
required_fields.is_original_method = True
# RemovedInWagtail50Warning - edit handlers should override get_form_options instead
def required_formsets(self):
return {}
required_formsets.is_original_method = True
def get_form_class(self):
"""
Construct a form class that has all the fields and formsets named in
the children of this edit handler.
"""
form_options = self.get_form_options()
# 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,
**form_options,
)
def bind_to_model(self, model):
"""
Create a clone of this panel definition with a ``model`` attribute pointing to the linked model class.
"""
new = self.clone()
new.model = model
new.on_model_bound()
return new
def bind_to(self, model=None, instance=None, request=None, form=None):
warn(
"The %s.bind_to() method has been replaced by bind_to_model(model) and get_bound_panel(instance=instance, request=request, form=form)"
% type(self).__name__,
category=RemovedInWagtail50Warning,
stacklevel=2,
)
return self.get_bound_panel(instance=instance, request=request, form=form)
def get_bound_panel(self, instance=None, request=None, form=None, prefix="panel"):
"""
Return a ``BoundPanel`` instance that can be rendered onto the template as a component. By default, this creates an instance
of the panel class's inner ``BoundPanel`` class, which must inherit from ``Panel.BoundPanel``.
"""
if self.model is None:
raise ImproperlyConfigured(
"%s.bind_to_model(model) must be called before get_bound_panel"
% type(self).__name__
)
if not issubclass(self.BoundPanel, EditHandler.BoundPanel):
raise ImproperlyConfigured(
"%s.BoundPanel must be a subclass of EditHandler.BoundPanel"
% type(self).__name__
)
return self.BoundPanel(
panel=self, instance=instance, request=request, form=form, prefix=prefix
)
def on_model_bound(self):
"""
Called after the panel has been associated with a model class and the ``self.model`` attribute is available;
panels can override this method to perform additional initialisation related to the model.
"""
pass
def __repr__(self):
return "<%s with model=%s>" % (
self.__class__.__name__,
self.model,
)
def classes(self):
"""
Additional CSS classnames to add to whatever kind of object this is at output.
Subclasses of Panel should override this, invoking super().classes() to
append more classes specific to the situation.
"""
if self.classname:
return [self.classname]
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 ""
@property
def clean_name(self):
"""
A name for this panel, consisting only of ASCII alphanumerics and underscores, suitable for use in identifiers.
Usually generated from the panel heading. Note that this is not guaranteed to be unique or non-empty; anything
making use of this and requiring uniqueness should validate and modify the return value as needed.
"""
return safe_snake_case(self.heading)
class BoundPanel(Component):
"""
A template component for a panel that has been associated with a model instance, form, and request.
"""
def __init__(self, panel, instance, request, form, prefix):
#: The panel definition corresponding to this bound panel
self.panel = panel
#: The model instance associated with this panel
self.instance = instance
#: The request object associated with this panel
self.request = request
#: The form object associated with this panel
self.form = form
#: A unique prefix for this panel, for use in HTML IDs
self.prefix = prefix
self.heading = self.panel.heading
self.help_text = self.panel.help_text
@property
def classname(self):
return self.panel.classname
def classes(self):
return self.panel.classes()
@property
def icon(self):
return self.panel.icon
def id_for_label(self):
"""
Returns an HTML ID to be used as the target for any label referencing this panel.
"""
return self.panel.id_for_label()
def is_shown(self):
"""
Whether this panel should be rendered; if false, it is skipped in the template output.
"""
return True
def show_panel_furniture(self):
"""
Whether this panel shows the panel furniture instead of being rendered outside of it.
"""
return self.is_shown()
def is_required(self):
return False
def render_as_object(self):
warn(
"Panel.render_as_object is deprecated. Use render_html instead",
category=RemovedInWagtail50Warning,
stacklevel=2,
)
return self.render_html()
def render_as_field(self):
warn(
"Panel.render_as_field is deprecated. Use render_html instead",
category=RemovedInWagtail50Warning,
stacklevel=2,
)
return self.render_html()
def get_context_data(self, parent_context=None):
context = super().get_context_data(parent_context)
context["self"] = self
return context
def get_comparison(self):
return []
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.panel.get_form_options().get("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_html() + self.render_missing_fields())
def __repr__(self):
return "<%s with model=%s instance=%s request=%s form=%s>" % (
self.__class__.__name__,
self.panel.model,
self.instance,
self.request,
self.form.__class__.__name__,
)
class EditHandler(Panel):
def __init__(self, *args, **kwargs):
warn(

Wyświetl plik

@ -0,0 +1,358 @@
from warnings import warn
from django.core.exceptions import ImproperlyConfigured
from django.utils.safestring import mark_safe
from wagtail.admin.forms.models import (
WagtailAdminDraftStateFormMixin,
WagtailAdminModelForm,
)
from wagtail.admin.ui.components import Component
from wagtail.coreutils import safe_snake_case
from wagtail.models import DraftStateMixin
from wagtail.utils.deprecation import RemovedInWagtail50Warning
def get_form_for_model(
model,
form_class=WagtailAdminModelForm,
**kwargs,
):
"""
Construct a ModelForm subclass using the given model and base form class. Any additional
keyword arguments are used to populate the form's Meta class.
"""
# This is really just Django's modelform_factory, tweaked to accept arbitrary kwargs.
meta_class_attrs = kwargs
meta_class_attrs["model"] = model
# The kwargs passed here are expected to come from Panel.get_form_options, which collects
# them by descending the tree of child edit handlers. If there are no edit handlers that
# specify form fields, this can legitimately result in both 'fields' and 'exclude' being
# absent, which ModelForm doesn't normally allow. In this case, explicitly set fields to [].
if "fields" not in meta_class_attrs and "exclude" not in meta_class_attrs:
meta_class_attrs["fields"] = []
# Give this new form class a reasonable name.
class_name = model.__name__ + "Form"
bases = (form_class.Meta,) if hasattr(form_class, "Meta") else ()
Meta = type("Meta", bases, meta_class_attrs)
form_class_attrs = {"Meta": Meta}
metaclass = type(form_class)
bases = [form_class]
if issubclass(model, DraftStateMixin):
bases.insert(0, WagtailAdminDraftStateFormMixin)
return metaclass(class_name, tuple(bases), form_class_attrs)
class Panel:
"""
Defines part (or all) of the edit form interface for pages and other models within the Wagtail
admin. Each model has an associated panel definition, consisting of a nested structure of Panel
objects - this provides methods for obtaining a ModelForm subclass, with the field list and
other parameters collated from all panels in the structure. It then handles rendering that form
as HTML.
"""
def __init__(
self,
heading="",
classname="",
help_text="",
base_form_class=None,
icon="",
):
self.heading = heading
self.classname = classname
self.help_text = help_text
self.base_form_class = base_form_class
self.icon = icon
self.model = None
def clone(self):
"""
Create a clone of this panel definition. By default, constructs a new instance, passing the
keyword arguments returned by ``clone_kwargs``.
"""
return self.__class__(**self.clone_kwargs())
def clone_kwargs(self):
"""
Return a dictionary of keyword arguments that can be used to create a clone of this panel definition.
"""
return {
"icon": self.icon,
"heading": self.heading,
"classname": self.classname,
"help_text": self.help_text,
"base_form_class": self.base_form_class,
}
def get_form_options(self):
"""
Return a dictionary of attributes such as 'fields', 'formsets' and 'widgets'
which should be incorporated into the form class definition to generate a form
that this panel can use.
This will only be called after binding to a model (i.e. self.model is available).
"""
options = {}
if not getattr(self.widget_overrides, "is_original_method", False):
warn(
"The `widget_overrides` method (on %r) is deprecated; "
"these should be returned from `get_form_options` as a "
"`widgets` item instead." % type(self),
category=RemovedInWagtail50Warning,
)
options["widgets"] = self.widget_overrides()
if not getattr(self.required_fields, "is_original_method", False):
warn(
"The `required_fields` method (on %r) is deprecated; "
"these should be returned from `get_form_options` as a "
"`fields` item instead." % type(self),
category=RemovedInWagtail50Warning,
)
options["fields"] = self.required_fields()
if not getattr(self.required_formsets, "is_original_method", False):
warn(
"The `required_formsets` method (on %r) is deprecated; "
"these should be returned from `get_form_options` as a "
"`formsets` item instead." % type(self),
category=RemovedInWagtail50Warning,
)
options["formsets"] = self.required_formsets()
return options
# RemovedInWagtail50Warning - edit handlers should override get_form_options instead
def widget_overrides(self):
return {}
widget_overrides.is_original_method = True
# RemovedInWagtail50Warning - edit handlers should override get_form_options instead
def required_fields(self):
return []
required_fields.is_original_method = True
# RemovedInWagtail50Warning - edit handlers should override get_form_options instead
def required_formsets(self):
return {}
required_formsets.is_original_method = True
def get_form_class(self):
"""
Construct a form class that has all the fields and formsets named in
the children of this edit handler.
"""
form_options = self.get_form_options()
# If a custom form class was passed to the panel, 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,
**form_options,
)
def bind_to_model(self, model):
"""
Create a clone of this panel definition with a ``model`` attribute pointing to the linked model class.
"""
new = self.clone()
new.model = model
new.on_model_bound()
return new
def bind_to(self, model=None, instance=None, request=None, form=None):
warn(
"The %s.bind_to() method has been replaced by bind_to_model(model) and get_bound_panel(instance=instance, request=request, form=form)"
% type(self).__name__,
category=RemovedInWagtail50Warning,
stacklevel=2,
)
return self.get_bound_panel(instance=instance, request=request, form=form)
def get_bound_panel(self, instance=None, request=None, form=None, prefix="panel"):
"""
Return a ``BoundPanel`` instance that can be rendered onto the template as a component. By default, this creates an instance
of the panel class's inner ``BoundPanel`` class, which must inherit from ``Panel.BoundPanel``.
"""
if self.model is None:
raise ImproperlyConfigured(
"%s.bind_to_model(model) must be called before get_bound_panel"
% type(self).__name__
)
if not issubclass(self.BoundPanel, Panel.BoundPanel):
raise ImproperlyConfigured(
"%s.BoundPanel must be a subclass of Panel.BoundPanel"
% type(self).__name__
)
return self.BoundPanel(
panel=self, instance=instance, request=request, form=form, prefix=prefix
)
def on_model_bound(self):
"""
Called after the panel has been associated with a model class and the ``self.model`` attribute is available;
panels can override this method to perform additional initialisation related to the model.
"""
pass
def __repr__(self):
return "<%s with model=%s>" % (
self.__class__.__name__,
self.model,
)
def classes(self):
"""
Additional CSS classnames to add to whatever kind of object this is at output.
Subclasses of Panel should override this, invoking super().classes() to
append more classes specific to the situation.
"""
if self.classname:
return [self.classname]
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 ""
@property
def clean_name(self):
"""
A name for this panel, consisting only of ASCII alphanumerics and underscores, suitable for use in identifiers.
Usually generated from the panel heading. Note that this is not guaranteed to be unique or non-empty; anything
making use of this and requiring uniqueness should validate and modify the return value as needed.
"""
return safe_snake_case(self.heading)
class BoundPanel(Component):
"""
A template component for a panel that has been associated with a model instance, form, and request.
"""
def __init__(self, panel, instance, request, form, prefix):
#: The panel definition corresponding to this bound panel
self.panel = panel
#: The model instance associated with this panel
self.instance = instance
#: The request object associated with this panel
self.request = request
#: The form object associated with this panel
self.form = form
#: A unique prefix for this panel, for use in HTML IDs
self.prefix = prefix
self.heading = self.panel.heading
self.help_text = self.panel.help_text
@property
def classname(self):
return self.panel.classname
def classes(self):
return self.panel.classes()
@property
def icon(self):
return self.panel.icon
def id_for_label(self):
"""
Returns an HTML ID to be used as the target for any label referencing this panel.
"""
return self.panel.id_for_label()
def is_shown(self):
"""
Whether this panel should be rendered; if false, it is skipped in the template output.
"""
return True
def show_panel_furniture(self):
"""
Whether this panel shows the panel furniture instead of being rendered outside of it.
"""
return self.is_shown()
def is_required(self):
return False
def render_as_object(self):
warn(
"Panel.render_as_object is deprecated. Use render_html instead",
category=RemovedInWagtail50Warning,
stacklevel=2,
)
return self.render_html()
def render_as_field(self):
warn(
"Panel.render_as_field is deprecated. Use render_html instead",
category=RemovedInWagtail50Warning,
stacklevel=2,
)
return self.render_html()
def get_context_data(self, parent_context=None):
context = super().get_context_data(parent_context)
context["self"] = self
return context
def get_comparison(self):
return []
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.panel.get_form_options().get("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_html() + self.render_missing_fields())
def __repr__(self):
return "<%s with model=%s instance=%s request=%s form=%s>" % (
self.__class__.__name__,
self.panel.model,
self.instance,
self.request,
self.form.__class__.__name__,
)