371 lines
13 KiB
Python
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__,
|
|
)
|