1"""
2HTML Widget classes
3"""
4
5import copy
6import datetime
7import warnings
8from collections import defaultdict
9from graphlib import CycleError, TopologicalSorter
10from itertools import chain
11
12from django.forms.utils import flatatt, to_current_timezone
13from django.templatetags.static import static
14from django.utils import formats
15from django.utils.choices import normalize_choices
16from django.utils.dates import MONTHS
17from django.utils.formats import get_format
18from django.utils.html import format_html, html_safe
19from django.utils.regex_helper import _lazy_re_compile
20from django.utils.safestring import mark_safe
21from django.utils.translation import gettext_lazy as _
22
23from .renderers import get_default_renderer
24
25__all__ = (
26 "Script",
27 "Media",
28 "MediaDefiningClass",
29 "Widget",
30 "TextInput",
31 "NumberInput",
32 "EmailInput",
33 "URLInput",
34 "ColorInput",
35 "SearchInput",
36 "TelInput",
37 "PasswordInput",
38 "HiddenInput",
39 "MultipleHiddenInput",
40 "FileInput",
41 "ClearableFileInput",
42 "Textarea",
43 "DateInput",
44 "DateTimeInput",
45 "TimeInput",
46 "CheckboxInput",
47 "Select",
48 "NullBooleanSelect",
49 "SelectMultiple",
50 "RadioSelect",
51 "CheckboxSelectMultiple",
52 "MultiWidget",
53 "SplitDateTimeWidget",
54 "SplitHiddenDateTimeWidget",
55 "SelectDateWidget",
56)
57
58MEDIA_TYPES = ("css", "js")
59
60
61class MediaOrderConflictWarning(RuntimeWarning):
62 pass
63
64
65@html_safe
66class MediaAsset:
67 element_template = "{path}"
68
69 def __init__(self, path, **attributes):
70 self._path = path
71 self.attributes = attributes
72
73 def __eq__(self, other):
74 # Compare the path only, to ensure performant comparison in Media.merge.
75 return (self.__class__ is other.__class__ and self.path == other.path) or (
76 isinstance(other, str) and self._path == other
77 )
78
79 def __hash__(self):
80 # Hash the path only, to ensure performant comparison in Media.merge.
81 return hash(self._path)
82
83 def __str__(self):
84 return format_html(
85 self.element_template,
86 path=self.path,
87 attributes=flatatt(self.attributes),
88 )
89
90 def __repr__(self):
91 return f"{type(self).__qualname__}({self._path!r})"
92
93 @property
94 def path(self):
95 """
96 Ensure an absolute path.
97 Relative paths are resolved via the {% static %} template tag.
98 """
99 if self._path.startswith(("http://", "https://", "/")):
100 return self._path
101 return static(self._path)
102
103
104class Script(MediaAsset):
105 element_template = '<script src="{path}"{attributes}></script>'
106
107 def __init__(self, src, **attributes):
108 # Alter the signature to allow src to be passed as a keyword argument.
109 super().__init__(src, **attributes)
110
111
112@html_safe
113class Media:
114 def __init__(self, media=None, css=None, js=None):
115 if media is not None:
116 css = getattr(media, "css", {})
117 js = getattr(media, "js", [])
118 else:
119 if css is None:
120 css = {}
121 if js is None:
122 js = []
123 self._css_lists = [css]
124 self._js_lists = [js]
125
126 def __repr__(self):
127 return "Media(css=%r, js=%r)" % (self._css, self._js)
128
129 def __str__(self):
130 return self.render()
131
132 @property
133 def _css(self):
134 css = defaultdict(list)
135 for css_list in self._css_lists:
136 for medium, sublist in css_list.items():
137 css[medium].append(sublist)
138 return {medium: self.merge(*lists) for medium, lists in css.items()}
139
140 @property
141 def _js(self):
142 return self.merge(*self._js_lists)
143
144 def render(self):
145 return mark_safe(
146 "\n".join(
147 chain.from_iterable(
148 getattr(self, "render_" + name)() for name in MEDIA_TYPES
149 )
150 )
151 )
152
153 def render_js(self):
154 return [
155 (
156 path.__html__()
157 if hasattr(path, "__html__")
158 else format_html('<script src="{}"></script>', self.absolute_path(path))
159 )
160 for path in self._js
161 ]
162
163 def render_css(self):
164 # To keep rendering order consistent, we can't just iterate over items().
165 # We need to sort the keys, and iterate over the sorted list.
166 media = sorted(self._css)
167 return chain.from_iterable(
168 [
169 (
170 path.__html__()
171 if hasattr(path, "__html__")
172 else format_html(
173 '<link href="{}" media="{}" rel="stylesheet">',
174 self.absolute_path(path),
175 medium,
176 )
177 )
178 for path in self._css[medium]
179 ]
180 for medium in media
181 )
182
183 def absolute_path(self, path):
184 """
185 Given a relative or absolute path to a static asset, return an absolute
186 path. An absolute path will be returned unchanged while a relative path
187 will be passed to django.templatetags.static.static().
188 """
189 if path.startswith(("http://", "https://", "/")):
190 return path
191 return static(path)
192
193 def __getitem__(self, name):
194 """Return a Media object that only contains media of the given type."""
195 if name in MEDIA_TYPES:
196 return Media(**{str(name): getattr(self, "_" + name)})
197 raise KeyError('Unknown media type "%s"' % name)
198
199 @staticmethod
200 def merge(*lists):
201 """
202 Merge lists while trying to keep the relative order of the elements.
203 Warn if the lists have the same elements in a different relative order.
204
205 For static assets it can be important to have them included in the DOM
206 in a certain order. In JavaScript you may not be able to reference a
207 global or in CSS you might want to override a style.
208 """
209 ts = TopologicalSorter()
210 for head, *tail in filter(None, lists):
211 ts.add(head) # Ensure that the first items are included.
212 for item in tail:
213 if head != item: # Avoid circular dependency to self.
214 ts.add(item, head)
215 head = item
216 try:
217 return list(ts.static_order())
218 except CycleError:
219 warnings.warn(
220 "Detected duplicate Media files in an opposite order: {}".format(
221 ", ".join(repr(list_) for list_ in lists)
222 ),
223 MediaOrderConflictWarning,
224 )
225 return list(dict.fromkeys(chain.from_iterable(filter(None, lists))))
226
227 def __add__(self, other):
228 combined = Media()
229 combined._css_lists = self._css_lists[:]
230 combined._js_lists = self._js_lists[:]
231 for item in other._css_lists:
232 if item and item not in self._css_lists:
233 combined._css_lists.append(item)
234 for item in other._js_lists:
235 if item and item not in self._js_lists:
236 combined._js_lists.append(item)
237 return combined
238
239
240def media_property(cls):
241 def _media(self):
242 # Get the media property of the superclass, if it exists
243 sup_cls = super(cls, self)
244 try:
245 base = sup_cls.media
246 except AttributeError:
247 base = Media()
248
249 # Get the media definition for this class
250 definition = getattr(cls, "Media", None)
251 if definition:
252 extend = getattr(definition, "extend", True)
253 if extend:
254 if extend is True:
255 m = base
256 else:
257 m = Media()
258 for medium in extend:
259 m += base[medium]
260 return m + Media(definition)
261 return Media(definition)
262 return base
263
264 return property(_media)
265
266
267class MediaDefiningClass(type):
268 """
269 Metaclass for classes that can have media definitions.
270 """
271
272 def __new__(mcs, name, bases, attrs):
273 new_class = super().__new__(mcs, name, bases, attrs)
274
275 if "media" not in attrs:
276 new_class.media = media_property(new_class)
277
278 return new_class
279
280
281class Widget(metaclass=MediaDefiningClass):
282 needs_multipart_form = False # Determines does this widget need multipart form
283 is_localized = False
284 is_required = False
285 supports_microseconds = True
286 use_fieldset = False
287
288 def __init__(self, attrs=None):
289 self.attrs = {} if attrs is None else attrs.copy()
290
291 def __deepcopy__(self, memo):
292 obj = copy.copy(self)
293 obj.attrs = self.attrs.copy()
294 memo[id(self)] = obj
295 return obj
296
297 @property
298 def is_hidden(self):
299 return self.input_type == "hidden" if hasattr(self, "input_type") else False
300
301 def subwidgets(self, name, value, attrs=None):
302 context = self.get_context(name, value, attrs)
303 yield context["widget"]
304
305 def format_value(self, value):
306 """
307 Return a value as it should appear when rendered in a template.
308 """
309 if value == "" or value is None:
310 return None
311 if self.is_localized:
312 return formats.localize_input(value)
313 return str(value)
314
315 def get_context(self, name, value, attrs):
316 return {
317 "widget": {
318 "name": name,
319 "is_hidden": self.is_hidden,
320 "required": self.is_required,
321 "value": self.format_value(value),
322 "attrs": self.build_attrs(self.attrs, attrs),
323 "template_name": self.template_name,
324 },
325 }
326
327 def render(self, name, value, attrs=None, renderer=None):
328 """Render the widget as an HTML string."""
329 context = self.get_context(name, value, attrs)
330 return self._render(self.template_name, context, renderer)
331
332 def _render(self, template_name, context, renderer=None):
333 if renderer is None:
334 renderer = get_default_renderer()
335 return mark_safe(renderer.render(template_name, context))
336
337 def build_attrs(self, base_attrs, extra_attrs=None):
338 """Build an attribute dictionary."""
339 return {**base_attrs, **(extra_attrs or {})}
340
341 def value_from_datadict(self, data, files, name):
342 """
343 Given a dictionary of data and this widget's name, return the value
344 of this widget or None if it's not provided.
345 """
346 return data.get(name)
347
348 def value_omitted_from_data(self, data, files, name):
349 return name not in data
350
351 def id_for_label(self, id_):
352 """
353 Return the HTML ID attribute of this Widget for use by a <label>, given
354 the ID of the field. Return an empty string if no ID is available.
355
356 This hook is necessary because some widgets have multiple HTML
357 elements and, thus, multiple IDs. In that case, this method should
358 return an ID value that corresponds to the first ID in the widget's
359 tags.
360 """
361 return id_
362
363 def use_required_attribute(self, initial):
364 return not self.is_hidden
365
366
367class Input(Widget):
368 """
369 Base class for all <input> widgets.
370 """
371
372 input_type = None # Subclasses must define this.
373 template_name = "django/forms/widgets/input.html"
374
375 def __init__(self, attrs=None):
376 if attrs is not None:
377 attrs = attrs.copy()
378 self.input_type = attrs.pop("type", self.input_type)
379 super().__init__(attrs)
380
381 def get_context(self, name, value, attrs):
382 context = super().get_context(name, value, attrs)
383 context["widget"]["type"] = self.input_type
384 return context
385
386
387class TextInput(Input):
388 input_type = "text"
389 template_name = "django/forms/widgets/text.html"
390
391
392class NumberInput(Input):
393 input_type = "number"
394 template_name = "django/forms/widgets/number.html"
395
396
397class EmailInput(Input):
398 input_type = "email"
399 template_name = "django/forms/widgets/email.html"
400
401
402class URLInput(Input):
403 input_type = "url"
404 template_name = "django/forms/widgets/url.html"
405
406
407class ColorInput(Input):
408 input_type = "color"
409 template_name = "django/forms/widgets/color.html"
410
411
412class SearchInput(Input):
413 input_type = "search"
414 template_name = "django/forms/widgets/search.html"
415
416
417class TelInput(Input):
418 input_type = "tel"
419 template_name = "django/forms/widgets/tel.html"
420
421
422class PasswordInput(Input):
423 input_type = "password"
424 template_name = "django/forms/widgets/password.html"
425
426 def __init__(self, attrs=None, render_value=False):
427 super().__init__(attrs)
428 self.render_value = render_value
429
430 def get_context(self, name, value, attrs):
431 if not self.render_value:
432 value = None
433 return super().get_context(name, value, attrs)
434
435
436class HiddenInput(Input):
437 input_type = "hidden"
438 template_name = "django/forms/widgets/hidden.html"
439
440
441class MultipleHiddenInput(HiddenInput):
442 """
443 Handle <input type="hidden"> for fields that have a list
444 of values.
445 """
446
447 template_name = "django/forms/widgets/multiple_hidden.html"
448
449 def get_context(self, name, value, attrs):
450 context = super().get_context(name, value, attrs)
451 final_attrs = context["widget"]["attrs"]
452 id_ = context["widget"]["attrs"].get("id")
453
454 subwidgets = []
455 for index, value_ in enumerate(context["widget"]["value"]):
456 widget_attrs = final_attrs.copy()
457 if id_:
458 # An ID attribute was given. Add a numeric index as a suffix
459 # so that the inputs don't all have the same ID attribute.
460 widget_attrs["id"] = "%s_%s" % (id_, index)
461 widget = HiddenInput()
462 widget.is_required = self.is_required
463 subwidgets.append(widget.get_context(name, value_, widget_attrs)["widget"])
464
465 context["widget"]["subwidgets"] = subwidgets
466 return context
467
468 def value_from_datadict(self, data, files, name):
469 try:
470 getter = data.getlist
471 except AttributeError:
472 getter = data.get
473 return getter(name)
474
475 def format_value(self, value):
476 return [] if value is None else value
477
478
479class FileInput(Input):
480 allow_multiple_selected = False
481 input_type = "file"
482 needs_multipart_form = True
483 template_name = "django/forms/widgets/file.html"
484
485 def __init__(self, attrs=None):
486 if (
487 attrs is not None
488 and not self.allow_multiple_selected
489 and attrs.get("multiple", False)
490 ):
491 raise ValueError(
492 "%s doesn't support uploading multiple files."
493 % self.__class__.__qualname__
494 )
495 if self.allow_multiple_selected:
496 if attrs is None:
497 attrs = {"multiple": True}
498 else:
499 attrs.setdefault("multiple", True)
500 super().__init__(attrs)
501
502 def format_value(self, value):
503 """File input never renders a value."""
504 return
505
506 def value_from_datadict(self, data, files, name):
507 "File widgets take data from FILES, not POST"
508 getter = files.get
509 if self.allow_multiple_selected:
510 try:
511 getter = files.getlist
512 except AttributeError:
513 pass
514 return getter(name)
515
516 def value_omitted_from_data(self, data, files, name):
517 return name not in files
518
519 def use_required_attribute(self, initial):
520 return super().use_required_attribute(initial) and not initial
521
522
523FILE_INPUT_CONTRADICTION = object()
524
525
526class ClearableFileInput(FileInput):
527 clear_checkbox_label = _("Clear")
528 initial_text = _("Currently")
529 input_text = _("Change")
530 template_name = "django/forms/widgets/clearable_file_input.html"
531 checked = False
532
533 def clear_checkbox_name(self, name):
534 """
535 Given the name of the file input, return the name of the clear checkbox
536 input.
537 """
538 return name + "-clear"
539
540 def clear_checkbox_id(self, name):
541 """
542 Given the name of the clear checkbox input, return the HTML id for it.
543 """
544 return name + "_id"
545
546 def is_initial(self, value):
547 """
548 Return whether value is considered to be initial value.
549 """
550 return bool(value and getattr(value, "url", False))
551
552 def format_value(self, value):
553 """
554 Return the file object if it has a defined url attribute.
555 """
556 if self.is_initial(value):
557 return value
558
559 def get_context(self, name, value, attrs):
560 context = super().get_context(name, value, attrs)
561 checkbox_name = self.clear_checkbox_name(name)
562 checkbox_id = self.clear_checkbox_id(checkbox_name)
563 context["widget"].update(
564 {
565 "checkbox_name": checkbox_name,
566 "checkbox_id": checkbox_id,
567 "is_initial": self.is_initial(value),
568 "input_text": self.input_text,
569 "initial_text": self.initial_text,
570 "clear_checkbox_label": self.clear_checkbox_label,
571 }
572 )
573 context["widget"]["attrs"].setdefault("disabled", False)
574 context["widget"]["attrs"]["checked"] = self.checked
575 return context
576
577 def value_from_datadict(self, data, files, name):
578 upload = super().value_from_datadict(data, files, name)
579 self.checked = self.clear_checkbox_name(name) in data
580 if not self.is_required and CheckboxInput().value_from_datadict(
581 data, files, self.clear_checkbox_name(name)
582 ):
583 if upload:
584 # If the user contradicts themselves (uploads a new file AND
585 # checks the "clear" checkbox), we return a unique marker
586 # object that FileField will turn into a ValidationError.
587 return FILE_INPUT_CONTRADICTION
588 # False signals to clear any existing value, as opposed to just None
589 return False
590 return upload
591
592 def value_omitted_from_data(self, data, files, name):
593 return (
594 super().value_omitted_from_data(data, files, name)
595 and self.clear_checkbox_name(name) not in data
596 )
597
598
599class Textarea(Widget):
600 template_name = "django/forms/widgets/textarea.html"
601
602 def __init__(self, attrs=None):
603 # Use slightly better defaults than HTML's 20x2 box
604 default_attrs = {"cols": "40", "rows": "10"}
605 if attrs:
606 default_attrs.update(attrs)
607 super().__init__(default_attrs)
608
609
610class DateTimeBaseInput(TextInput):
611 format_key = ""
612 supports_microseconds = False
613
614 def __init__(self, attrs=None, format=None):
615 super().__init__(attrs)
616 self.format = format or None
617
618 def format_value(self, value):
619 return formats.localize_input(
620 value, self.format or formats.get_format(self.format_key)[0]
621 )
622
623
624class DateInput(DateTimeBaseInput):
625 format_key = "DATE_INPUT_FORMATS"
626 template_name = "django/forms/widgets/date.html"
627
628
629class DateTimeInput(DateTimeBaseInput):
630 format_key = "DATETIME_INPUT_FORMATS"
631 template_name = "django/forms/widgets/datetime.html"
632
633
634class TimeInput(DateTimeBaseInput):
635 format_key = "TIME_INPUT_FORMATS"
636 template_name = "django/forms/widgets/time.html"
637
638
639# Defined at module level so that CheckboxInput is picklable (#17976)
640def boolean_check(v):
641 return not (v is False or v is None or v == "")
642
643
644class CheckboxInput(Input):
645 input_type = "checkbox"
646 template_name = "django/forms/widgets/checkbox.html"
647
648 def __init__(self, attrs=None, check_test=None):
649 super().__init__(attrs)
650 # check_test is a callable that takes a value and returns True
651 # if the checkbox should be checked for that value.
652 self.check_test = boolean_check if check_test is None else check_test
653
654 def format_value(self, value):
655 """Only return the 'value' attribute if value isn't empty."""
656 if value is True or value is False or value is None or value == "":
657 return
658 return str(value)
659
660 def get_context(self, name, value, attrs):
661 if self.check_test(value):
662 attrs = {**(attrs or {}), "checked": True}
663 return super().get_context(name, value, attrs)
664
665 def value_from_datadict(self, data, files, name):
666 if name not in data:
667 # A missing value means False because HTML form submission does not
668 # send results for unselected checkboxes.
669 return False
670 value = data.get(name)
671 # Translate true and false strings to boolean values.
672 values = {"true": True, "false": False}
673 if isinstance(value, str):
674 value = values.get(value.lower(), value)
675 return bool(value)
676
677 def value_omitted_from_data(self, data, files, name):
678 # HTML checkboxes don't appear in POST data if not checked, so it's
679 # never known if the value is actually omitted.
680 return False
681
682
683class ChoiceWidget(Widget):
684 allow_multiple_selected = False
685 input_type = None
686 template_name = None
687 option_template_name = None
688 add_id_index = True
689 checked_attribute = {"checked": True}
690 option_inherits_attrs = True
691
692 def __init__(self, attrs=None, choices=()):
693 super().__init__(attrs)
694 self.choices = choices
695
696 def __deepcopy__(self, memo):
697 obj = copy.copy(self)
698 obj.attrs = self.attrs.copy()
699 obj.choices = copy.copy(self.choices)
700 memo[id(self)] = obj
701 return obj
702
703 def subwidgets(self, name, value, attrs=None):
704 """
705 Yield all "subwidgets" of this widget. Used to enable iterating
706 options from a BoundField for choice widgets.
707 """
708 value = self.format_value(value)
709 yield from self.options(name, value, attrs)
710
711 def options(self, name, value, attrs=None):
712 """Yield a flat list of options for this widget."""
713 for group in self.optgroups(name, value, attrs):
714 yield from group[1]
715
716 def optgroups(self, name, value, attrs=None):
717 """Return a list of optgroups for this widget."""
718 groups = []
719 has_selected = False
720
721 for index, (option_value, option_label) in enumerate(self.choices):
722 if option_value is None:
723 option_value = ""
724
725 subgroup = []
726 if isinstance(option_label, (list, tuple)):
727 group_name = option_value
728 subindex = 0
729 choices = option_label
730 else:
731 group_name = None
732 subindex = None
733 choices = [(option_value, option_label)]
734 groups.append((group_name, subgroup, index))
735
736 for subvalue, sublabel in choices:
737 selected = (not has_selected or self.allow_multiple_selected) and str(
738 subvalue
739 ) in value
740 has_selected |= selected
741 subgroup.append(
742 self.create_option(
743 name,
744 subvalue,
745 sublabel,
746 selected,
747 index,
748 subindex=subindex,
749 attrs=attrs,
750 )
751 )
752 if subindex is not None:
753 subindex += 1
754 return groups
755
756 def create_option(
757 self, name, value, label, selected, index, subindex=None, attrs=None
758 ):
759 index = str(index) if subindex is None else "%s_%s" % (index, subindex)
760 option_attrs = (
761 self.build_attrs(self.attrs, attrs) if self.option_inherits_attrs else {}
762 )
763 if selected:
764 option_attrs.update(self.checked_attribute)
765 if "id" in option_attrs:
766 option_attrs["id"] = self.id_for_label(option_attrs["id"], index)
767 return {
768 "name": name,
769 "value": value,
770 "label": label,
771 "selected": selected,
772 "index": index,
773 "attrs": option_attrs,
774 "type": self.input_type,
775 "template_name": self.option_template_name,
776 "wrap_label": True,
777 }
778
779 def get_context(self, name, value, attrs):
780 context = super().get_context(name, value, attrs)
781 context["widget"]["optgroups"] = self.optgroups(
782 name, context["widget"]["value"], attrs
783 )
784 return context
785
786 def id_for_label(self, id_, index="0"):
787 """
788 Use an incremented id for each option where the main widget
789 references the zero index.
790 """
791 if id_ and self.add_id_index:
792 id_ = "%s_%s" % (id_, index)
793 return id_
794
795 def value_from_datadict(self, data, files, name):
796 getter = data.get
797 if self.allow_multiple_selected:
798 try:
799 getter = data.getlist
800 except AttributeError:
801 pass
802 return getter(name)
803
804 def format_value(self, value):
805 """Return selected values as a list."""
806 if value is None and self.allow_multiple_selected:
807 return []
808 if not isinstance(value, (tuple, list)):
809 value = [value]
810 return [str(v) if v is not None else "" for v in value]
811
812 @property
813 def choices(self):
814 return self._choices
815
816 @choices.setter
817 def choices(self, value):
818 self._choices = normalize_choices(value)
819
820
821class Select(ChoiceWidget):
822 input_type = "select"
823 template_name = "django/forms/widgets/select.html"
824 option_template_name = "django/forms/widgets/select_option.html"
825 add_id_index = False
826 checked_attribute = {"selected": True}
827 option_inherits_attrs = False
828
829 def get_context(self, name, value, attrs):
830 context = super().get_context(name, value, attrs)
831 if self.allow_multiple_selected:
832 context["widget"]["attrs"]["multiple"] = True
833 return context
834
835 @staticmethod
836 def _choice_has_empty_value(choice):
837 """Return True if the choice's value is empty string or None."""
838 value, _ = choice
839 return value is None or value == ""
840
841 def use_required_attribute(self, initial):
842 """
843 Don't render 'required' if the first <option> has a value, as that's
844 invalid HTML.
845 """
846 use_required_attribute = super().use_required_attribute(initial)
847 # 'required' is always okay for <select multiple>.
848 if self.allow_multiple_selected:
849 return use_required_attribute
850
851 first_choice = next(iter(self.choices), None)
852 return (
853 use_required_attribute
854 and first_choice is not None
855 and self._choice_has_empty_value(first_choice)
856 )
857
858
859class NullBooleanSelect(Select):
860 """
861 A Select Widget intended to be used with NullBooleanField.
862 """
863
864 def __init__(self, attrs=None):
865 choices = (
866 ("unknown", _("Unknown")),
867 ("true", _("Yes")),
868 ("false", _("No")),
869 )
870 super().__init__(attrs, choices)
871
872 def format_value(self, value):
873 try:
874 return {
875 True: "true",
876 False: "false",
877 "true": "true",
878 "false": "false",
879 # For backwards compatibility with Django < 2.2.
880 "2": "true",
881 "3": "false",
882 }[value]
883 except KeyError:
884 return "unknown"
885
886 def value_from_datadict(self, data, files, name):
887 value = data.get(name)
888 return {
889 True: True,
890 "True": True,
891 "False": False,
892 False: False,
893 "true": True,
894 "false": False,
895 # For backwards compatibility with Django < 2.2.
896 "2": True,
897 "3": False,
898 }.get(value)
899
900
901class SelectMultiple(Select):
902 allow_multiple_selected = True
903
904 def value_from_datadict(self, data, files, name):
905 try:
906 getter = data.getlist
907 except AttributeError:
908 getter = data.get
909 return getter(name)
910
911 def value_omitted_from_data(self, data, files, name):
912 # An unselected <select multiple> doesn't appear in POST data, so it's
913 # never known if the value is actually omitted.
914 return False
915
916
917class RadioSelect(ChoiceWidget):
918 input_type = "radio"
919 template_name = "django/forms/widgets/radio.html"
920 option_template_name = "django/forms/widgets/radio_option.html"
921 use_fieldset = True
922
923 def id_for_label(self, id_, index=None):
924 """
925 Don't include for="field_0" in <label> to improve accessibility when
926 using a screen reader, in addition clicking such a label would toggle
927 the first input.
928 """
929 if index is None:
930 return ""
931 return super().id_for_label(id_, index)
932
933
934class CheckboxSelectMultiple(RadioSelect):
935 allow_multiple_selected = True
936 input_type = "checkbox"
937 template_name = "django/forms/widgets/checkbox_select.html"
938 option_template_name = "django/forms/widgets/checkbox_option.html"
939
940 def use_required_attribute(self, initial):
941 # Don't use the 'required' attribute because browser validation would
942 # require all checkboxes to be checked instead of at least one.
943 return False
944
945 def value_omitted_from_data(self, data, files, name):
946 # HTML checkboxes don't appear in POST data if not checked, so it's
947 # never known if the value is actually omitted.
948 return False
949
950
951class MultiWidget(Widget):
952 """
953 A widget that is composed of multiple widgets.
954
955 In addition to the values added by Widget.get_context(), this widget
956 adds a list of subwidgets to the context as widget['subwidgets'].
957 These can be looped over and rendered like normal widgets.
958
959 You'll probably want to use this class with MultiValueField.
960 """
961
962 template_name = "django/forms/widgets/multiwidget.html"
963 use_fieldset = True
964
965 def __init__(self, widgets, attrs=None):
966 if isinstance(widgets, dict):
967 self.widgets_names = [("_%s" % name) if name else "" for name in widgets]
968 widgets = widgets.values()
969 else:
970 self.widgets_names = ["_%s" % i for i in range(len(widgets))]
971 self.widgets = [w() if isinstance(w, type) else w for w in widgets]
972 super().__init__(attrs)
973
974 @property
975 def is_hidden(self):
976 return all(w.is_hidden for w in self.widgets)
977
978 def get_context(self, name, value, attrs):
979 context = super().get_context(name, value, attrs)
980 if self.is_localized:
981 for widget in self.widgets:
982 widget.is_localized = self.is_localized
983 # value is a list/tuple of values, each corresponding to a widget
984 # in self.widgets.
985 if not isinstance(value, (list, tuple)):
986 value = self.decompress(value)
987
988 final_attrs = context["widget"]["attrs"]
989 input_type = final_attrs.pop("type", None)
990 id_ = final_attrs.get("id")
991 subwidgets = []
992 for i, (widget_name, widget) in enumerate(
993 zip(self.widgets_names, self.widgets)
994 ):
995 if input_type is not None:
996 widget.input_type = input_type
997 widget_name = name + widget_name
998 try:
999 widget_value = value[i]
1000 except IndexError:
1001 widget_value = None
1002 if id_:
1003 widget_attrs = final_attrs.copy()
1004 widget_attrs["id"] = "%s_%s" % (id_, i)
1005 else:
1006 widget_attrs = final_attrs
1007 subwidgets.append(
1008 widget.get_context(widget_name, widget_value, widget_attrs)["widget"]
1009 )
1010 context["widget"]["subwidgets"] = subwidgets
1011 return context
1012
1013 def id_for_label(self, id_):
1014 return ""
1015
1016 def value_from_datadict(self, data, files, name):
1017 return [
1018 widget.value_from_datadict(data, files, name + widget_name)
1019 for widget_name, widget in zip(self.widgets_names, self.widgets)
1020 ]
1021
1022 def value_omitted_from_data(self, data, files, name):
1023 return all(
1024 widget.value_omitted_from_data(data, files, name + widget_name)
1025 for widget_name, widget in zip(self.widgets_names, self.widgets)
1026 )
1027
1028 def decompress(self, value):
1029 """
1030 Return a list of decompressed values for the given compressed value.
1031 The given value can be assumed to be valid, but not necessarily
1032 non-empty.
1033 """
1034 raise NotImplementedError("Subclasses must implement this method.")
1035
1036 def _get_media(self):
1037 """
1038 Media for a multiwidget is the combination of all media of the
1039 subwidgets.
1040 """
1041 media = Media()
1042 for w in self.widgets:
1043 media += w.media
1044 return media
1045
1046 media = property(_get_media)
1047
1048 def __deepcopy__(self, memo):
1049 obj = super().__deepcopy__(memo)
1050 obj.widgets = copy.deepcopy(self.widgets)
1051 return obj
1052
1053 @property
1054 def needs_multipart_form(self):
1055 return any(w.needs_multipart_form for w in self.widgets)
1056
1057
1058class SplitDateTimeWidget(MultiWidget):
1059 """
1060 A widget that splits datetime input into two <input type="text"> boxes.
1061 """
1062
1063 supports_microseconds = False
1064 template_name = "django/forms/widgets/splitdatetime.html"
1065
1066 def __init__(
1067 self,
1068 attrs=None,
1069 date_format=None,
1070 time_format=None,
1071 date_attrs=None,
1072 time_attrs=None,
1073 ):
1074 widgets = (
1075 DateInput(
1076 attrs=attrs if date_attrs is None else date_attrs,
1077 format=date_format,
1078 ),
1079 TimeInput(
1080 attrs=attrs if time_attrs is None else time_attrs,
1081 format=time_format,
1082 ),
1083 )
1084 super().__init__(widgets)
1085
1086 def decompress(self, value):
1087 if value:
1088 value = to_current_timezone(value)
1089 return [value.date(), value.time()]
1090 return [None, None]
1091
1092
1093class SplitHiddenDateTimeWidget(SplitDateTimeWidget):
1094 """
1095 A widget that splits datetime input into two <input type="hidden"> inputs.
1096 """
1097
1098 template_name = "django/forms/widgets/splithiddendatetime.html"
1099
1100 def __init__(
1101 self,
1102 attrs=None,
1103 date_format=None,
1104 time_format=None,
1105 date_attrs=None,
1106 time_attrs=None,
1107 ):
1108 super().__init__(attrs, date_format, time_format, date_attrs, time_attrs)
1109 for widget in self.widgets:
1110 widget.input_type = "hidden"
1111
1112
1113class SelectDateWidget(Widget):
1114 """
1115 A widget that splits date input into three <select> boxes.
1116
1117 This also serves as an example of a Widget that has more than one HTML
1118 element and hence implements value_from_datadict.
1119 """
1120
1121 none_value = ("", "---")
1122 month_field = "%s_month"
1123 day_field = "%s_day"
1124 year_field = "%s_year"
1125 template_name = "django/forms/widgets/select_date.html"
1126 input_type = "select"
1127 select_widget = Select
1128 date_re = _lazy_re_compile(r"(\d{4}|0)-(\d\d?)-(\d\d?)$")
1129 use_fieldset = True
1130
1131 def __init__(self, attrs=None, years=None, months=None, empty_label=None):
1132 self.attrs = attrs or {}
1133
1134 # Optional list or tuple of years to use in the "year" select box.
1135 if years:
1136 self.years = years
1137 else:
1138 this_year = datetime.date.today().year
1139 self.years = range(this_year, this_year + 10)
1140
1141 # Optional dict of months to use in the "month" select box.
1142 if months:
1143 self.months = months
1144 else:
1145 self.months = MONTHS
1146
1147 # Optional string, list, or tuple to use as empty_label.
1148 if isinstance(empty_label, (list, tuple)):
1149 if not len(empty_label) == 3:
1150 raise ValueError("empty_label list/tuple must have 3 elements.")
1151
1152 self.year_none_value = ("", empty_label[0])
1153 self.month_none_value = ("", empty_label[1])
1154 self.day_none_value = ("", empty_label[2])
1155 else:
1156 if empty_label is not None:
1157 self.none_value = ("", empty_label)
1158
1159 self.year_none_value = self.none_value
1160 self.month_none_value = self.none_value
1161 self.day_none_value = self.none_value
1162
1163 def get_context(self, name, value, attrs):
1164 context = super().get_context(name, value, attrs)
1165 date_context = {}
1166 year_choices = [(i, str(i)) for i in self.years]
1167 if not self.is_required:
1168 year_choices.insert(0, self.year_none_value)
1169 year_name = self.year_field % name
1170 date_context["year"] = self.select_widget(
1171 attrs, choices=year_choices
1172 ).get_context(
1173 name=year_name,
1174 value=context["widget"]["value"]["year"],
1175 attrs={**context["widget"]["attrs"], "id": "id_%s" % year_name},
1176 )
1177 month_choices = list(self.months.items())
1178 if not self.is_required:
1179 month_choices.insert(0, self.month_none_value)
1180 month_name = self.month_field % name
1181 date_context["month"] = self.select_widget(
1182 attrs, choices=month_choices
1183 ).get_context(
1184 name=month_name,
1185 value=context["widget"]["value"]["month"],
1186 attrs={**context["widget"]["attrs"], "id": "id_%s" % month_name},
1187 )
1188 day_choices = [(i, i) for i in range(1, 32)]
1189 if not self.is_required:
1190 day_choices.insert(0, self.day_none_value)
1191 day_name = self.day_field % name
1192 date_context["day"] = self.select_widget(
1193 attrs,
1194 choices=day_choices,
1195 ).get_context(
1196 name=day_name,
1197 value=context["widget"]["value"]["day"],
1198 attrs={**context["widget"]["attrs"], "id": "id_%s" % day_name},
1199 )
1200 subwidgets = []
1201 for field in self._parse_date_fmt():
1202 subwidgets.append(date_context[field]["widget"])
1203 context["widget"]["subwidgets"] = subwidgets
1204 return context
1205
1206 def format_value(self, value):
1207 """
1208 Return a dict containing the year, month, and day of the current value.
1209 Use dict instead of a datetime to allow invalid dates such as February
1210 31 to display correctly.
1211 """
1212 year, month, day = None, None, None
1213 if isinstance(value, (datetime.date, datetime.datetime)):
1214 year, month, day = value.year, value.month, value.day
1215 elif isinstance(value, str):
1216 match = self.date_re.match(value)
1217 if match:
1218 # Convert any zeros in the date to empty strings to match the
1219 # empty option value.
1220 year, month, day = [int(val) or "" for val in match.groups()]
1221 else:
1222 input_format = get_format("DATE_INPUT_FORMATS")[0]
1223 try:
1224 d = datetime.datetime.strptime(value, input_format)
1225 except ValueError:
1226 pass
1227 else:
1228 year, month, day = d.year, d.month, d.day
1229 return {"year": year, "month": month, "day": day}
1230
1231 @staticmethod
1232 def _parse_date_fmt():
1233 fmt = get_format("DATE_FORMAT")
1234 escaped = False
1235 for char in fmt:
1236 if escaped:
1237 escaped = False
1238 elif char == "\\":
1239 escaped = True
1240 elif char in "Yy":
1241 yield "year"
1242 elif char in "bEFMmNn":
1243 yield "month"
1244 elif char in "dj":
1245 yield "day"
1246
1247 def id_for_label(self, id_):
1248 for first_select in self._parse_date_fmt():
1249 return "%s_%s" % (id_, first_select)
1250 return "%s_month" % id_
1251
1252 def value_from_datadict(self, data, files, name):
1253 y = data.get(self.year_field % name)
1254 m = data.get(self.month_field % name)
1255 d = data.get(self.day_field % name)
1256 if y == m == d == "":
1257 return None
1258 if y is not None and m is not None and d is not None:
1259 input_format = get_format("DATE_INPUT_FORMATS")[0]
1260 input_format = formats.sanitize_strftime_format(input_format)
1261 try:
1262 date_value = datetime.date(int(y), int(m), int(d))
1263 except ValueError:
1264 # Return pseudo-ISO dates with zeros for any unselected values,
1265 # e.g. '2017-0-23'.
1266 return "%s-%s-%s" % (y or 0, m or 0, d or 0)
1267 except OverflowError:
1268 return "0-0-0"
1269 return date_value.strftime(input_format)
1270 return data.get(name)
1271
1272 def value_omitted_from_data(self, data, files, name):
1273 return not any(
1274 ("{}_{}".format(name, interval) in data)
1275 for interval in ("year", "month", "day")
1276 )