angrybeanie_wagtail/env/lib/python3.12/site-packages/wagtail/admin/widgets/chooser.py
2025-07-25 21:32:16 +10:00

351 lines
12 KiB
Python

import json
from django import forms
from django.core.exceptions import ImproperlyConfigured
from django.forms import widgets
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.safestring import mark_safe
from django.utils.text import capfirst
from django.utils.translation import gettext_lazy as _
from wagtail.admin.admin_url_finder import AdminURLFinder
from wagtail.admin.staticfiles import versioned_static
from wagtail.coreutils import resolve_model_string
from wagtail.models import Page
from wagtail.telepath import register
from wagtail.widget_adapters import WidgetAdapter
class BaseChooser(widgets.Input):
choose_one_text = _("Choose an item")
choose_another_text = _("Choose another item")
clear_choice_text = _("Clear choice")
link_to_chosen_text = _("Edit this item")
show_edit_link = True
show_clear_link = True
template_name = "wagtailadmin/widgets/chooser.html"
display_title_key = (
"title" # key to use for the display title within the value data dict
)
icon = None
classname = None
model = None
js_constructor = "Chooser"
linked_fields = {}
# when looping over form fields, this one should appear in visible_fields, not hidden_fields
# despite the underlying input being type="hidden"
input_type = "hidden"
is_hidden = False
def __init__(self, **kwargs):
# allow attributes to be overridden by kwargs
for var in [
"choose_one_text",
"choose_another_text",
"clear_choice_text",
"link_to_chosen_text",
"show_edit_link",
"show_clear_link",
"icon",
"linked_fields",
]:
if var in kwargs:
setattr(self, var, kwargs.pop(var))
super().__init__(**kwargs)
@cached_property
def model_class(self):
return resolve_model_string(self.model)
def value_from_datadict(self, data, files, name):
# treat the empty string as None
result = super().value_from_datadict(data, files, name)
if result == "":
return None
else:
return result
def get_hidden_input_context(self, name, value, attrs):
"""
Return the context variables required to render the underlying hidden input element
"""
return super().get_context(name, value, attrs)
def render_hidden_input(self, name, value, attrs):
"""Render the HTML for the underlying hidden input element"""
return self._render(
"django/forms/widgets/input.html",
self.get_hidden_input_context(name, value, attrs),
)
def get_chooser_modal_url(self):
return reverse(self.chooser_modal_url_name)
def get_context(self, name, value_data, attrs):
original_field_html = self.render_hidden_input(
name, value_data.get("id"), attrs
)
return {
"widget": self,
"original_field_html": original_field_html,
"attrs": attrs,
"value": bool(
value_data
), # only used by chooser.html to identify blank values
"edit_url": value_data.get("edit_url", ""),
"display_title": value_data.get(self.display_title_key, ""),
"chooser_url": self.get_chooser_modal_url(),
"icon": self.icon,
"classname": self.classname,
}
def render_html(self, name, value_data, attrs):
return render_to_string(
self.template_name,
self.get_context(name, value_data or {}, attrs),
)
def get_instance(self, value):
"""
Given a value passed to this widget for rendering (which may be None, an id, or a model
instance), return a model instance or None
"""
if value is None:
return None
elif isinstance(value, self.model_class):
return value
else: # assume instance ID
try:
return self.model_class.objects.get(pk=value)
except self.model_class.DoesNotExist:
return None
def get_display_title(self, instance):
"""
Return the text to display as the title for this instance
"""
return str(instance)
def get_value_data_from_instance(self, instance):
"""
Given a model instance, return a value that we can pass to both the server-side template
and the client-side rendering code (via telepath) that contains all the information needed
for display. Typically this is a dict of id, title etc; it must be JSON-serialisable.
"""
return {
"id": instance.pk,
"edit_url": AdminURLFinder().get_edit_url(instance),
self.display_title_key: self.get_display_title(instance),
}
def get_value_data(self, value):
"""
Given a value passed to this widget for rendering (which may be None, an id, or a model
instance), return a value that we can pass to both the server-side template and the
client-side rendering code (via telepath) that contains all the information needed
for display. Typically this is a dict of id, title etc; it must be JSON-serialisable.
"""
instance = self.get_instance(value)
if instance:
return self.get_value_data_from_instance(instance)
def render(self, name, value, attrs=None, renderer=None):
# no point trying to come up with sensible semantics for when 'id' is missing from attrs,
# so let's make sure it fails early in the process
try:
id_ = attrs["id"]
except (KeyError, TypeError):
raise TypeError("BaseChooser cannot be rendered without an 'id' attribute")
value_data = self.get_value_data(value)
widget_html = self.render_html(name, value_data, attrs)
js = self.render_js_init(id_, name, value_data)
out = f"{widget_html}<script>{js}</script>"
return mark_safe(out)
@property
def base_js_init_options(self):
"""The set of options to pass to the JS initialiser that are constant every time this widget
instance is rendered (i.e. do not vary based on id / name / value)"""
opts = {
"modalUrl": self.get_chooser_modal_url(),
}
if self.linked_fields:
opts["linkedFields"] = self.linked_fields
return opts
def get_js_init_options(self, id_, name, value_data):
return {**self.base_js_init_options}
def render_js_init(self, id_, name, value_data):
opts = self.get_js_init_options(id_, name, value_data)
return f"new {self.js_constructor}({json.dumps(id_)}, {json.dumps(opts)});"
@cached_property
def media(self):
return forms.Media(
js=[
versioned_static("wagtailadmin/js/chooser-widget.js"),
]
)
class BaseChooserAdapter(WidgetAdapter):
js_constructor = "wagtail.admin.widgets.Chooser"
def js_args(self, widget):
return [
widget.render_html("__NAME__", None, attrs={"id": "__ID__"}),
widget.id_for_label("__ID__"),
widget.base_js_init_options,
]
@cached_property
def media(self):
return forms.Media(
js=[
versioned_static("wagtailadmin/js/chooser-widget-telepath.js"),
]
)
register(BaseChooserAdapter(), BaseChooser)
class AdminPageChooser(BaseChooser):
choose_one_text = _("Choose a page")
choose_another_text = _("Choose another page")
link_to_chosen_text = _("Edit this page")
display_title_key = "display_title"
chooser_modal_url_name = "wagtailadmin_choose_page"
icon = "doc-empty-inverse"
classname = "page-chooser"
js_constructor = "PageChooser"
def __init__(
self, target_models=None, can_choose_root=False, user_perms=None, **kwargs
):
super().__init__(**kwargs)
if target_models:
if not isinstance(target_models, (set, list, tuple)):
# assume we've been passed a single instance; wrap it as a list
target_models = [target_models]
# normalise the list of target models to a list of page classes
cleaned_target_models = []
for model in target_models:
try:
cleaned_target_models.append(resolve_model_string(model))
except (ValueError, LookupError):
raise ImproperlyConfigured(
"Could not resolve %r into a model. "
"Model names should be in the form app_label.model_name"
% (model,)
)
else:
cleaned_target_models = [Page]
if len(cleaned_target_models) == 1 and cleaned_target_models[0] is not Page:
model_name = capfirst(cleaned_target_models[0]._meta.verbose_name)
self.choose_one_text += " (" + model_name + ")"
self.user_perms = user_perms
self.target_models = cleaned_target_models
if len(self.target_models) == 1:
self.model = self.target_models[0]
else:
self.model = Page
self.can_choose_root = bool(can_choose_root)
@property
def model_names(self):
return [
"{app}.{model}".format(
app=model._meta.app_label, model=model._meta.model_name
)
for model in self.target_models
]
@property
def base_js_init_options(self):
# a JSON-serializable representation of the configuration options needed for the
# client-side behaviour of this widget
return {
"modelNames": self.model_names,
"canChooseRoot": self.can_choose_root,
"userPerms": self.user_perms,
**super().base_js_init_options,
}
def get_instance(self, value):
instance = super().get_instance(value)
if instance:
return instance.specific
def get_display_title(self, instance):
return instance.get_admin_display_title()
def get_value_data_from_instance(self, instance):
data = super().get_value_data_from_instance(instance)
parent_page = instance.get_parent()
data["parent_id"] = parent_page.pk if parent_page else None
return data
def get_js_init_options(self, id_, name, value_data):
opts = super().get_js_init_options(id_, name, value_data)
value_data = value_data or {}
parent_id = value_data.get("parent_id")
if parent_id is not None:
opts["parentId"] = parent_id
return opts
@property
def media(self):
return forms.Media(
js=[
versioned_static("wagtailadmin/js/page-chooser-modal.js"),
versioned_static("wagtailadmin/js/page-chooser.js"),
]
)
class PageChooserAdapter(BaseChooserAdapter):
js_constructor = "wagtail.widgets.PageChooser"
@cached_property
def media(self):
return forms.Media(
js=[
versioned_static("wagtailadmin/js/page-chooser-modal.js"),
versioned_static("wagtailadmin/js/page-chooser-telepath.js"),
]
)
class AdminPageMoveChooser(AdminPageChooser):
def __init__(
self, target_models=None, can_choose_root=False, user_perms=None, **kwargs
):
self.pages_to_move = kwargs.pop("pages_to_move", [])
super().__init__(
target_models=target_models,
can_choose_root=can_choose_root,
user_perms=user_perms,
**kwargs,
)
@property
def base_js_init_options(self):
return {
"targetPages": self.pages_to_move,
"matchSubclass": False,
**super().base_js_init_options,
}
register(PageChooserAdapter(), AdminPageChooser)