Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/contrib/admin/widgets.py: 40%
260 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1"""
2Form Widget classes specific to the Django admin site.
3"""
4import copy
5import json
7from django import forms
8from django.conf import settings
9from django.core.exceptions import ValidationError
10from django.core.validators import URLValidator
11from django.db.models import CASCADE
12from django.urls import reverse
13from django.urls.exceptions import NoReverseMatch
14from django.utils.html import smart_urlquote
15from django.utils.http import urlencode
16from django.utils.text import Truncator
17from django.utils.translation import get_language
18from django.utils.translation import gettext as _
21class FilteredSelectMultiple(forms.SelectMultiple):
22 """
23 A SelectMultiple with a JavaScript filter interface.
25 Note that the resulting JavaScript assumes that the jsi18n
26 catalog has been loaded in the page
27 """
29 class Media:
30 js = [
31 "admin/js/core.js",
32 "admin/js/SelectBox.js",
33 "admin/js/SelectFilter2.js",
34 ]
36 def __init__(self, verbose_name, is_stacked, attrs=None, choices=()):
37 self.verbose_name = verbose_name
38 self.is_stacked = is_stacked
39 super().__init__(attrs, choices)
41 def get_context(self, name, value, attrs):
42 context = super().get_context(name, value, attrs)
43 context["widget"]["attrs"]["class"] = "selectfilter"
44 if self.is_stacked:
45 context["widget"]["attrs"]["class"] += "stacked"
46 context["widget"]["attrs"]["data-field-name"] = self.verbose_name
47 context["widget"]["attrs"]["data-is-stacked"] = int(self.is_stacked)
48 return context
51class AdminDateWidget(forms.DateInput):
52 class Media:
53 js = [
54 "admin/js/calendar.js",
55 "admin/js/admin/DateTimeShortcuts.js",
56 ]
58 def __init__(self, attrs=None, format=None):
59 attrs = {"class": "vDateField", "size": "10", **(attrs or {})}
60 super().__init__(attrs=attrs, format=format)
63class AdminTimeWidget(forms.TimeInput):
64 class Media:
65 js = [
66 "admin/js/calendar.js",
67 "admin/js/admin/DateTimeShortcuts.js",
68 ]
70 def __init__(self, attrs=None, format=None):
71 attrs = {"class": "vTimeField", "size": "8", **(attrs or {})}
72 super().__init__(attrs=attrs, format=format)
75class AdminSplitDateTime(forms.SplitDateTimeWidget):
76 """
77 A SplitDateTime Widget that has some admin-specific styling.
78 """
80 template_name = "admin/widgets/split_datetime.html"
82 def __init__(self, attrs=None):
83 widgets = [AdminDateWidget, AdminTimeWidget]
84 # Note that we're calling MultiWidget, not SplitDateTimeWidget, because
85 # we want to define widgets.
86 forms.MultiWidget.__init__(self, widgets, attrs)
88 def get_context(self, name, value, attrs):
89 context = super().get_context(name, value, attrs)
90 context["date_label"] = _("Date:")
91 context["time_label"] = _("Time:")
92 return context
95class AdminRadioSelect(forms.RadioSelect):
96 template_name = "admin/widgets/radio.html"
99class AdminFileWidget(forms.ClearableFileInput):
100 template_name = "admin/widgets/clearable_file_input.html"
103def url_params_from_lookup_dict(lookups):
104 """
105 Convert the type of lookups specified in a ForeignKey limit_choices_to
106 attribute to a dictionary of query parameters
107 """
108 params = {}
109 if lookups and hasattr(lookups, "items"):
110 for k, v in lookups.items():
111 if callable(v):
112 v = v()
113 if isinstance(v, (tuple, list)):
114 v = ",".join(str(x) for x in v)
115 elif isinstance(v, bool):
116 v = ("0", "1")[v]
117 else:
118 v = str(v)
119 params[k] = v
120 return params
123class ForeignKeyRawIdWidget(forms.TextInput):
124 """
125 A Widget for displaying ForeignKeys in the "raw_id" interface rather than
126 in a <select> box.
127 """
129 template_name = "admin/widgets/foreign_key_raw_id.html"
131 def __init__(self, rel, admin_site, attrs=None, using=None):
132 self.rel = rel
133 self.admin_site = admin_site
134 self.db = using
135 super().__init__(attrs)
137 def get_context(self, name, value, attrs):
138 context = super().get_context(name, value, attrs)
139 rel_to = self.rel.model
140 if rel_to in self.admin_site._registry:
141 # The related object is registered with the same AdminSite
142 related_url = reverse(
143 "admin:%s_%s_changelist"
144 % (
145 rel_to._meta.app_label,
146 rel_to._meta.model_name,
147 ),
148 current_app=self.admin_site.name,
149 )
151 params = self.url_parameters()
152 if params:
153 related_url += "?" + urlencode(params)
154 context["related_url"] = related_url
155 context["link_title"] = _("Lookup")
156 # The JavaScript code looks for this class.
157 context["widget"]["attrs"].setdefault("class", "vForeignKeyRawIdAdminField")
158 else:
159 context["related_url"] = None
160 if context["widget"]["value"]:
161 context["link_label"], context["link_url"] = self.label_and_url_for_value(
162 value
163 )
164 else:
165 context["link_label"] = None
166 return context
168 def base_url_parameters(self):
169 limit_choices_to = self.rel.limit_choices_to
170 if callable(limit_choices_to):
171 limit_choices_to = limit_choices_to()
172 return url_params_from_lookup_dict(limit_choices_to)
174 def url_parameters(self):
175 from django.contrib.admin.views.main import TO_FIELD_VAR
177 params = self.base_url_parameters()
178 params.update({TO_FIELD_VAR: self.rel.get_related_field().name})
179 return params
181 def label_and_url_for_value(self, value):
182 key = self.rel.get_related_field().name
183 try:
184 obj = self.rel.model._default_manager.using(self.db).get(**{key: value})
185 except (ValueError, self.rel.model.DoesNotExist, ValidationError):
186 return "", ""
188 try:
189 url = reverse(
190 "%s:%s_%s_change"
191 % (
192 self.admin_site.name,
193 obj._meta.app_label,
194 obj._meta.object_name.lower(),
195 ),
196 args=(obj.pk,),
197 )
198 except NoReverseMatch:
199 url = "" # Admin not registered for target model.
201 return Truncator(obj).words(14), url
204class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
205 """
206 A Widget for displaying ManyToMany ids in the "raw_id" interface rather than
207 in a <select multiple> box.
208 """
210 template_name = "admin/widgets/many_to_many_raw_id.html"
212 def get_context(self, name, value, attrs):
213 context = super().get_context(name, value, attrs)
214 if self.rel.model in self.admin_site._registry:
215 # The related object is registered with the same AdminSite
216 context["widget"]["attrs"]["class"] = "vManyToManyRawIdAdminField"
217 return context
219 def url_parameters(self):
220 return self.base_url_parameters()
222 def label_and_url_for_value(self, value):
223 return "", ""
225 def value_from_datadict(self, data, files, name):
226 value = data.get(name)
227 if value:
228 return value.split(",")
230 def format_value(self, value):
231 return ",".join(str(v) for v in value) if value else ""
234class RelatedFieldWidgetWrapper(forms.Widget):
235 """
236 This class is a wrapper to a given widget to add the add icon for the
237 admin interface.
238 """
240 template_name = "admin/widgets/related_widget_wrapper.html"
242 def __init__(
243 self,
244 widget,
245 rel,
246 admin_site,
247 can_add_related=None,
248 can_change_related=False,
249 can_delete_related=False,
250 can_view_related=False,
251 ):
252 self.needs_multipart_form = widget.needs_multipart_form
253 self.attrs = widget.attrs
254 self.choices = widget.choices
255 self.widget = widget
256 self.rel = rel
257 # Backwards compatible check for whether a user can add related
258 # objects.
259 if can_add_related is None:
260 can_add_related = rel.model in admin_site._registry
261 self.can_add_related = can_add_related
262 # XXX: The UX does not support multiple selected values.
263 multiple = getattr(widget, "allow_multiple_selected", False)
264 self.can_change_related = not multiple and can_change_related
265 # XXX: The deletion UX can be confusing when dealing with cascading deletion.
266 cascade = getattr(rel, "on_delete", None) is CASCADE
267 self.can_delete_related = not multiple and not cascade and can_delete_related
268 self.can_view_related = not multiple and can_view_related
269 # so we can check if the related object is registered with this AdminSite
270 self.admin_site = admin_site
272 def __deepcopy__(self, memo):
273 obj = copy.copy(self)
274 obj.widget = copy.deepcopy(self.widget, memo)
275 obj.attrs = self.widget.attrs
276 memo[id(self)] = obj
277 return obj
279 @property
280 def is_hidden(self):
281 return self.widget.is_hidden
283 @property
284 def media(self):
285 return self.widget.media
287 def get_related_url(self, info, action, *args):
288 return reverse(
289 "admin:%s_%s_%s" % (info + (action,)),
290 current_app=self.admin_site.name,
291 args=args,
292 )
294 def get_context(self, name, value, attrs):
295 from django.contrib.admin.views.main import IS_POPUP_VAR, TO_FIELD_VAR
297 rel_opts = self.rel.model._meta
298 info = (rel_opts.app_label, rel_opts.model_name)
299 self.widget.choices = self.choices
300 url_params = "&".join(
301 "%s=%s" % param
302 for param in [
303 (TO_FIELD_VAR, self.rel.get_related_field().name),
304 (IS_POPUP_VAR, 1),
305 ]
306 )
307 context = {
308 "rendered_widget": self.widget.render(name, value, attrs),
309 "is_hidden": self.is_hidden,
310 "name": name,
311 "url_params": url_params,
312 "model": rel_opts.verbose_name,
313 "can_add_related": self.can_add_related,
314 "can_change_related": self.can_change_related,
315 "can_delete_related": self.can_delete_related,
316 "can_view_related": self.can_view_related,
317 }
318 if self.can_add_related:
319 context["add_related_url"] = self.get_related_url(info, "add")
320 if self.can_delete_related:
321 context["delete_related_template_url"] = self.get_related_url(
322 info, "delete", "__fk__"
323 )
324 if self.can_view_related or self.can_change_related:
325 context["change_related_template_url"] = self.get_related_url(
326 info, "change", "__fk__"
327 )
328 return context
330 def value_from_datadict(self, data, files, name):
331 return self.widget.value_from_datadict(data, files, name)
333 def value_omitted_from_data(self, data, files, name):
334 return self.widget.value_omitted_from_data(data, files, name)
336 def id_for_label(self, id_):
337 return self.widget.id_for_label(id_)
340class AdminTextareaWidget(forms.Textarea):
341 def __init__(self, attrs=None):
342 super().__init__(attrs={"class": "vLargeTextField", **(attrs or {})})
345class AdminTextInputWidget(forms.TextInput):
346 def __init__(self, attrs=None):
347 super().__init__(attrs={"class": "vTextField", **(attrs or {})})
350class AdminEmailInputWidget(forms.EmailInput):
351 def __init__(self, attrs=None):
352 super().__init__(attrs={"class": "vTextField", **(attrs or {})})
355class AdminURLFieldWidget(forms.URLInput):
356 template_name = "admin/widgets/url.html"
358 def __init__(self, attrs=None, validator_class=URLValidator):
359 super().__init__(attrs={"class": "vURLField", **(attrs or {})})
360 self.validator = validator_class()
362 def get_context(self, name, value, attrs):
363 try:
364 self.validator(value if value else "")
365 url_valid = True
366 except ValidationError:
367 url_valid = False
368 context = super().get_context(name, value, attrs)
369 context["current_label"] = _("Currently:")
370 context["change_label"] = _("Change:")
371 context["widget"]["href"] = (
372 smart_urlquote(context["widget"]["value"]) if value else ""
373 )
374 context["url_valid"] = url_valid
375 return context
378class AdminIntegerFieldWidget(forms.NumberInput):
379 class_name = "vIntegerField"
381 def __init__(self, attrs=None):
382 super().__init__(attrs={"class": self.class_name, **(attrs or {})})
385class AdminBigIntegerFieldWidget(AdminIntegerFieldWidget):
386 class_name = "vBigIntegerField"
389class AdminUUIDInputWidget(forms.TextInput):
390 def __init__(self, attrs=None):
391 super().__init__(attrs={"class": "vUUIDField", **(attrs or {})})
394# Mapping of lowercase language codes [returned by Django's get_language()] to
395# language codes supported by select2.
396# See django/contrib/admin/static/admin/js/vendor/select2/i18n/*
397SELECT2_TRANSLATIONS = {
398 x.lower(): x
399 for x in [
400 "ar",
401 "az",
402 "bg",
403 "ca",
404 "cs",
405 "da",
406 "de",
407 "el",
408 "en",
409 "es",
410 "et",
411 "eu",
412 "fa",
413 "fi",
414 "fr",
415 "gl",
416 "he",
417 "hi",
418 "hr",
419 "hu",
420 "id",
421 "is",
422 "it",
423 "ja",
424 "km",
425 "ko",
426 "lt",
427 "lv",
428 "mk",
429 "ms",
430 "nb",
431 "nl",
432 "pl",
433 "pt-BR",
434 "pt",
435 "ro",
436 "ru",
437 "sk",
438 "sr-Cyrl",
439 "sr",
440 "sv",
441 "th",
442 "tr",
443 "uk",
444 "vi",
445 ]
446}
447SELECT2_TRANSLATIONS.update({"zh-hans": "zh-CN", "zh-hant": "zh-TW"})
450class AutocompleteMixin:
451 """
452 Select widget mixin that loads options from AutocompleteJsonView via AJAX.
454 Renders the necessary data attributes for select2 and adds the static form
455 media.
456 """
458 url_name = "%s:autocomplete"
460 def __init__(self, field, admin_site, attrs=None, choices=(), using=None):
461 self.field = field
462 self.admin_site = admin_site
463 self.db = using
464 self.choices = choices
465 self.attrs = {} if attrs is None else attrs.copy()
466 self.i18n_name = SELECT2_TRANSLATIONS.get(get_language())
468 def get_url(self):
469 return reverse(self.url_name % self.admin_site.name)
471 def build_attrs(self, base_attrs, extra_attrs=None):
472 """
473 Set select2's AJAX attributes.
475 Attributes can be set using the html5 data attribute.
476 Nested attributes require a double dash as per
477 https://select2.org/configuration/data-attributes#nested-subkey-options
478 """
479 attrs = super().build_attrs(base_attrs, extra_attrs=extra_attrs)
480 attrs.setdefault("class", "")
481 attrs.update(
482 {
483 "data-ajax--cache": "true",
484 "data-ajax--delay": 250,
485 "data-ajax--type": "GET",
486 "data-ajax--url": self.get_url(),
487 "data-app-label": self.field.model._meta.app_label,
488 "data-model-name": self.field.model._meta.model_name,
489 "data-field-name": self.field.name,
490 "data-theme": "admin-autocomplete",
491 "data-allow-clear": json.dumps(not self.is_required),
492 "data-placeholder": "", # Allows clearing of the input.
493 "lang": self.i18n_name,
494 "class": attrs["class"]
495 + (" " if attrs["class"] else "")
496 + "admin-autocomplete",
497 }
498 )
499 return attrs
501 def optgroups(self, name, value, attr=None):
502 """Return selected options based on the ModelChoiceIterator."""
503 default = (None, [], 0)
504 groups = [default]
505 has_selected = False
506 selected_choices = {
507 str(v) for v in value if str(v) not in self.choices.field.empty_values
508 }
509 if not self.is_required and not self.allow_multiple_selected:
510 default[1].append(self.create_option(name, "", "", False, 0))
511 remote_model_opts = self.field.remote_field.model._meta
512 to_field_name = getattr(
513 self.field.remote_field, "field_name", remote_model_opts.pk.attname
514 )
515 to_field_name = remote_model_opts.get_field(to_field_name).attname
516 choices = (
517 (getattr(obj, to_field_name), self.choices.field.label_from_instance(obj))
518 for obj in self.choices.queryset.using(self.db).filter(
519 **{"%s__in" % to_field_name: selected_choices}
520 )
521 )
522 for option_value, option_label in choices:
523 selected = str(option_value) in value and (
524 has_selected is False or self.allow_multiple_selected
525 )
526 has_selected |= selected
527 index = len(default[1])
528 subgroup = default[1]
529 subgroup.append(
530 self.create_option(
531 name, option_value, option_label, selected_choices, index
532 )
533 )
534 return groups
536 @property
537 def media(self):
538 extra = "" if settings.DEBUG else ".min"
539 i18n_file = (
540 ("admin/js/vendor/select2/i18n/%s.js" % self.i18n_name,)
541 if self.i18n_name
542 else ()
543 )
544 return forms.Media(
545 js=(
546 "admin/js/vendor/jquery/jquery%s.js" % extra,
547 "admin/js/vendor/select2/select2.full%s.js" % extra,
548 )
549 + i18n_file
550 + (
551 "admin/js/jquery.init.js",
552 "admin/js/autocomplete.js",
553 ),
554 css={
555 "screen": (
556 "admin/css/vendor/select2/select2%s.css" % extra,
557 "admin/css/autocomplete.css",
558 ),
559 },
560 )
563class AutocompleteSelect(AutocompleteMixin, forms.Select):
564 pass
567class AutocompleteSelectMultiple(AutocompleteMixin, forms.SelectMultiple):
568 pass