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