Files
old-saburly-wagtail-web/env/lib/python3.10/site-packages/wagtail/admin/panels/field_panel.py
2024-08-27 20:33:44 +02:00

371 lines
13 KiB
Python

import functools
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
from django.db.models import ForeignKey
from django.forms.models import ModelChoiceIterator
from django.template.loader import get_template
from django.utils.functional import cached_property
from django.utils.text import capfirst
from wagtail.admin import compare
from wagtail.admin.forms.models import registry as model_field_registry
from wagtail.blocks import BlockField
from .base import Panel
class FieldPanel(Panel):
TEMPLATE_VAR = "field_panel"
read_only_output_template_name = "wagtailadmin/panels/read_only_output.html"
def __init__(
self,
field_name,
widget=None,
disable_comments=None,
permission=None,
read_only=False,
**kwargs,
):
super().__init__(**kwargs)
self.field_name = field_name
self.widget = widget
self.disable_comments = disable_comments
self.permission = permission
self.read_only = read_only
def clone_kwargs(self):
kwargs = super().clone_kwargs()
kwargs.update(
field_name=self.field_name,
widget=self.widget,
disable_comments=self.disable_comments,
permission=self.permission,
read_only=self.read_only,
)
return kwargs
def get_form_options(self):
if self.read_only:
return {}
opts = {
"fields": [self.field_name],
}
if self.widget:
opts["widgets"] = {self.field_name: self.widget}
if self.permission:
opts["field_permissions"] = {self.field_name: self.permission}
return opts
def get_comparison_class(self):
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
@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)
@property
def clean_name(self):
return self.field_name
def format_value_for_display(self, value):
"""
Overrides ``Panel.format_value_for_display()`` to add additional treatment
for choice fields.
"""
# NOTE: We look for formfield.choices over db_field.choices here,
# as more field types can benefit that way.
choices = getattr(self.db_field.formfield(), "choices", None)
if not isinstance(choices, ModelChoiceIterator) and choices:
labels = dict(choices)
display_values = [
str(labels.get(v, v)) # Use raw value if no match found
for v in
(
# Account for single AND multiple choice fields
tuple(value) if isinstance(value, (list, tuple)) else (value,)
)
]
return ", ".join(display_values)
return super().format_value_for_display(value)
def __repr__(self):
return "<{} '{}' with model={}>".format(
self.__class__.__name__,
self.field_name,
self.model,
)
class BoundPanel(Panel.BoundPanel):
template_name = "wagtailadmin/panels/field_panel.html"
# Default icons for common model field types,
# based on the corresponding FieldBlock's icon.
default_field_icons = {
"DateField": "date",
"TimeField": "time",
"DateTimeField": "date",
"URLField": "link-external",
"TaggableManager": "tag",
"EmailField": "mail",
"TextField": "pilcrow",
"RichTextField": "pilcrow",
"FloatField": "decimal",
"DecimalField": "decimal",
"BooleanField": "tick-inverse",
}
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.bound_field = None
self.read_only = False
if self.form is None:
return
try:
self.bound_field = self.form[self.field_name]
except KeyError:
if self.panel.read_only:
self.read_only = True
# Ensure heading and help_text are set to something useful
self.heading = self.panel.heading or capfirst(
self.panel.db_field.verbose_name
)
self.help_text = self.panel.help_text or capfirst(
self.panel.db_field.help_text
)
return
# Ensure heading and help_text are consistent across
# Panel, BoundPanel and Field
if self.panel.heading:
self.heading = self.bound_field.label = self.panel.heading
else:
self.heading = self.bound_field.label
self.help_text = self.panel.help_text or self.bound_field.help_text
@property
def field_name(self):
return self.panel.field_name
def is_shown(self):
if (
self.form is not None
and self.bound_field is None
and not self.read_only
):
# this field is missing from the form
return False
if (
self.panel.permission
and self.request
and not self.request.user.has_perm(self.panel.permission)
):
return False
return True
def is_required(self):
if self.bound_field is None:
return False
return self.bound_field.field.required
def classes(self):
classes = self.panel.classes()
if self.bound_field and isinstance(self.bound_field.field, BlockField):
classes.append("w-panel--nested")
return classes
@property
def icon(self):
"""
Display a different icon depending on the field's type.
"""
# If the panel has an icon, use that.
if self.panel.icon:
return self.panel.icon
# Try to use the model field first, then the form field because it's
# possible to use FieldPanel without a model field by using a custom
# form class.
try:
field = self.panel.db_field
except FieldDoesNotExist:
# The defined default icons are for model fields, but most of them
# have a corresponding form field with the same name, so we just
# hope the name matches.
field = self.bound_field.field
field_type = type(field)
# ForeignKey fields can have a custom icon defined in the form field's widget
# (e.g. page, image, and document choosers). If there's an overridden widget
# with an icon attribute, use that.
if issubclass(field_type, ForeignKey):
overrides = model_field_registry.get(field) or {}
widget = overrides.get("widget", None)
return getattr(widget, "icon", None)
# Otherwise, find a default icon based on the field's class or superclasses.
for field_class in field_type.mro():
field_name = field_class.__name__
if field_name in self.default_field_icons:
return self.default_field_icons[field_name]
return None
def id_for_label(self):
if self.read_only:
return self.prefix
return self.bound_field.id_for_label
@property
def comments_enabled(self):
if self.panel.disable_comments is None and not self.read_only:
# 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.panel.disable_comments
@cached_property
def value_from_instance(self):
return getattr(self.instance, self.field_name)
def get_context_data(self, parent_context=None):
context = super().get_context_data(parent_context)
if self.read_only:
context.update(self.get_read_only_context_data())
else:
context.update(self.get_editable_context_data())
return context
def get_editable_context_data(self):
widget_described_by_ids = []
help_text_id = "%s-helptext" % self.prefix
error_message_id = "%s-errors" % self.prefix
widget_described_by_ids = []
if self.help_text:
widget_described_by_ids.append(help_text_id)
if self.bound_field.errors:
widget = self.bound_field.field.widget
if hasattr(widget, "render_with_errors"):
widget_attrs = {
"id": self.bound_field.auto_id,
}
if widget_described_by_ids:
widget_attrs["aria-describedby"] = " ".join(
widget_described_by_ids
)
rendered_field = widget.render_with_errors(
self.bound_field.html_name,
self.bound_field.value(),
attrs=widget_attrs,
errors=self.bound_field.errors,
)
else:
widget_described_by_ids.append(error_message_id)
rendered_field = self.bound_field.as_widget(
attrs={
"aria-invalid": "true",
"aria-describedby": " ".join(widget_described_by_ids),
}
)
else:
widget_attrs = {}
if widget_described_by_ids:
widget_attrs["aria-describedby"] = " ".join(widget_described_by_ids)
rendered_field = self.bound_field.as_widget(attrs=widget_attrs)
return {
"field": self.bound_field,
"rendered_field": rendered_field,
"error_message_id": error_message_id,
"help_text": self.help_text,
"help_text_id": help_text_id,
"show_add_comment_button": self.comments_enabled
and getattr(
self.bound_field.field.widget,
"show_add_comment_button",
True,
),
}
def get_read_only_context_data(self):
# Define context data for BoundPanel AND read-only output rendering
context = {
"id_for_label": self.id_for_label(),
"help_text_id": "%s-helptext" % self.prefix,
"help_text": self.help_text,
"show_add_comment_button": self.comments_enabled,
"raw_value": self.value_from_instance,
"display_value": self.panel.format_value_for_display(
self.value_from_instance
),
}
# Render read-only output
template = get_template(self.panel.read_only_output_template_name)
rendered_field = template.render(context)
# Add rendered output to BoundPanel context data
context["rendered_field"] = rendered_field
return context
def get_comparison(self):
comparator_class = self.panel.get_comparison_class()
if comparator_class and self.is_shown():
try:
return [functools.partial(comparator_class, self.panel.db_field)]
except FieldDoesNotExist:
return []
return []
def __repr__(self):
return "<{} '{}' with model={} instance={} request={} form={}>".format(
self.__class__.__name__,
self.field_name,
self.panel.model,
self.instance,
self.request,
self.form.__class__.__name__,
)