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.blocks import StreamValue from wagtail.coreutils import safe_snake_case from wagtail.models import DraftStateMixin from wagtail.rich_text import RichText from wagtail.utils.text import text_from_html 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 top-level panel definition (also known as an edit handler), consisting of a nested structure of ``Panel`` objects. This provides methods for obtaining a :class:`~django.forms.ModelForm` subclass, with the field list and other parameters collated from all panels in the structure. It then handles rendering that form as HTML. The following parameters can be used to customize how the panel is displayed. For more details, see :ref:`customizing_panels`. :param heading: The heading text to display for the panel. :param classname: A CSS class name to add to the panel's HTML element. :param help_text: Help text to display within the panel. :param base_form_class: The base form class to use for the panel. Defaults to the model's ``base_form_class``, before falling back to :class:`~wagtail.admin.forms.WagtailAdminModelForm`. This is only relevant for the top-level panel. :param icon: The name of the icon to display next to the panel heading. :param attrs: A dictionary of HTML attributes to add to the panel's HTML element. """ BASE_ATTRS = {} def __init__( self, heading="", classname="", help_text="", base_form_class=None, icon="", attrs=None, ): self.heading = heading self.classname = classname self.help_text = help_text self.base_form_class = base_form_class self.icon = icon self.model = None self.attrs = self.BASE_ATTRS.copy() if attrs is not None: self.attrs.update(attrs) 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, "attrs": self.attrs, "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). """ return {} 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 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 "<{} with model={}>".format( 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