Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/django/forms/widgets.py: 39%
632 statements
« prev ^ index » next coverage.py v7.0.5, created at 2023-01-17 06:13 +0000
« prev ^ index » next coverage.py v7.0.5, created at 2023-01-17 06:13 +0000
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:
65 css = getattr(media, "css", {})
66 js = getattr(media, "js", [])
67 else:
68 if css is None:
69 css = {}
70 if js is None:
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 path.__html__()
105 if hasattr(path, "__html__")
106 else format_html('<script src="{}"></script>', self.absolute_path(path))
107 for path in self._js
108 ]
110 def render_css(self):
111 # To keep rendering order consistent, we can't just iterate over items().
112 # We need to sort the keys, and iterate over the sorted list.
113 media = sorted(self._css)
114 return chain.from_iterable(
115 [
116 path.__html__()
117 if hasattr(path, "__html__")
118 else format_html(
119 '<link href="{}" media="{}" rel="stylesheet">',
120 self.absolute_path(path),
121 medium,
122 )
123 for path in self._css[medium]
124 ]
125 for medium in media
126 )
128 def absolute_path(self, path):
129 """
130 Given a relative or absolute path to a static asset, return an absolute
131 path. An absolute path will be returned unchanged while a relative path
132 will be passed to django.templatetags.static.static().
133 """
134 if path.startswith(("http://", "https://", "/")):
135 return path
136 return static(path)
138 def __getitem__(self, name):
139 """Return a Media object that only contains media of the given type."""
140 if name in MEDIA_TYPES:
141 return Media(**{str(name): getattr(self, "_" + name)})
142 raise KeyError('Unknown media type "%s"' % name)
144 @staticmethod
145 def merge(*lists):
146 """
147 Merge lists while trying to keep the relative order of the elements.
148 Warn if the lists have the same elements in a different relative order.
150 For static assets it can be important to have them included in the DOM
151 in a certain order. In JavaScript you may not be able to reference a
152 global or in CSS you might want to override a style.
153 """
154 dependency_graph = defaultdict(set)
155 all_items = OrderedSet()
156 for list_ in filter(None, lists):
157 head = list_[0]
158 # The first items depend on nothing but have to be part of the
159 # dependency graph to be included in the result.
160 dependency_graph.setdefault(head, set())
161 for item in list_:
162 all_items.add(item)
163 # No self dependencies
164 if head != item:
165 dependency_graph[item].add(head)
166 head = item
167 try:
168 return stable_topological_sort(all_items, dependency_graph)
169 except CyclicDependencyError:
170 warnings.warn(
171 "Detected duplicate Media files in an opposite order: {}".format(
172 ", ".join(repr(list_) for list_ in lists)
173 ),
174 MediaOrderConflictWarning,
175 )
176 return list(all_items)
178 def __add__(self, other):
179 combined = Media()
180 combined._css_lists = self._css_lists[:]
181 combined._js_lists = self._js_lists[:]
182 for item in other._css_lists:
183 if item and item not in self._css_lists:
184 combined._css_lists.append(item)
185 for item in other._js_lists:
186 if item and item not in self._js_lists:
187 combined._js_lists.append(item)
188 return combined
191def media_property(cls):
192 def _media(self):
193 # Get the media property of the superclass, if it exists
194 sup_cls = super(cls, self)
195 try:
196 base = sup_cls.media
197 except AttributeError:
198 base = Media()
200 # Get the media definition for this class
201 definition = getattr(cls, "Media", None)
202 if definition:
203 extend = getattr(definition, "extend", True)
204 if extend:
205 if extend is True:
206 m = base
207 else:
208 m = Media()
209 for medium in extend:
210 m += base[medium]
211 return m + Media(definition)
212 return Media(definition)
213 return base
215 return property(_media)
218class MediaDefiningClass(type):
219 """
220 Metaclass for classes that can have media definitions.
221 """
223 def __new__(mcs, name, bases, attrs):
224 new_class = super().__new__(mcs, name, bases, attrs)
226 if "media" not in attrs:
227 new_class.media = media_property(new_class)
229 return new_class
232class Widget(metaclass=MediaDefiningClass):
233 needs_multipart_form = False # Determines does this widget need multipart form
234 is_localized = False
235 is_required = False
236 supports_microseconds = True
237 use_fieldset = False
239 def __init__(self, attrs=None):
240 self.attrs = {} if attrs is None else attrs.copy()
242 def __deepcopy__(self, memo):
243 obj = copy.copy(self)
244 obj.attrs = self.attrs.copy()
245 memo[id(self)] = obj
246 return obj
248 @property
249 def is_hidden(self):
250 return self.input_type == "hidden" if hasattr(self, "input_type") else False
252 def subwidgets(self, name, value, attrs=None):
253 context = self.get_context(name, value, attrs)
254 yield context["widget"]
256 def format_value(self, value):
257 """
258 Return a value as it should appear when rendered in a template.
259 """
260 if value == "" or value is None:
261 return None
262 if self.is_localized:
263 return formats.localize_input(value)
264 return str(value)
266 def get_context(self, name, value, attrs):
267 return {
268 "widget": {
269 "name": name,
270 "is_hidden": self.is_hidden,
271 "required": self.is_required,
272 "value": self.format_value(value),
273 "attrs": self.build_attrs(self.attrs, attrs),
274 "template_name": self.template_name,
275 },
276 }
278 def render(self, name, value, attrs=None, renderer=None):
279 """Render the widget as an HTML string."""
280 context = self.get_context(name, value, attrs)
281 return self._render(self.template_name, context, renderer)
283 def _render(self, template_name, context, renderer=None):
284 if renderer is None:
285 renderer = get_default_renderer()
286 return mark_safe(renderer.render(template_name, context))
288 def build_attrs(self, base_attrs, extra_attrs=None):
289 """Build an attribute dictionary."""
290 return {**base_attrs, **(extra_attrs or {})}
292 def value_from_datadict(self, data, files, name):
293 """
294 Given a dictionary of data and this widget's name, return the value
295 of this widget or None if it's not provided.
296 """
297 return data.get(name)
299 def value_omitted_from_data(self, data, files, name):
300 return name not in data
302 def id_for_label(self, id_):
303 """
304 Return the HTML ID attribute of this Widget for use by a <label>, given
305 the ID of the field. Return an empty string if no ID is available.
307 This hook is necessary because some widgets have multiple HTML
308 elements and, thus, multiple IDs. In that case, this method should
309 return an ID value that corresponds to the first ID in the widget's
310 tags.
311 """
312 return id_
314 def use_required_attribute(self, initial):
315 return not self.is_hidden
318class Input(Widget):
319 """
320 Base class for all <input> widgets.
321 """
323 input_type = None # Subclasses must define this.
324 template_name = "django/forms/widgets/input.html"
326 def __init__(self, attrs=None):
327 if attrs is not None:
328 attrs = attrs.copy()
329 self.input_type = attrs.pop("type", self.input_type)
330 super().__init__(attrs)
332 def get_context(self, name, value, attrs):
333 context = super().get_context(name, value, attrs)
334 context["widget"]["type"] = self.input_type
335 return context
338class TextInput(Input):
339 input_type = "text"
340 template_name = "django/forms/widgets/text.html"
343class NumberInput(Input):
344 input_type = "number"
345 template_name = "django/forms/widgets/number.html"
348class EmailInput(Input):
349 input_type = "email"
350 template_name = "django/forms/widgets/email.html"
353class URLInput(Input):
354 input_type = "url"
355 template_name = "django/forms/widgets/url.html"
358class PasswordInput(Input):
359 input_type = "password"
360 template_name = "django/forms/widgets/password.html"
362 def __init__(self, attrs=None, render_value=False):
363 super().__init__(attrs)
364 self.render_value = render_value
366 def get_context(self, name, value, attrs):
367 if not self.render_value:
368 value = None
369 return super().get_context(name, value, attrs)
372class HiddenInput(Input):
373 input_type = "hidden"
374 template_name = "django/forms/widgets/hidden.html"
377class MultipleHiddenInput(HiddenInput):
378 """
379 Handle <input type="hidden"> for fields that have a list
380 of values.
381 """
383 template_name = "django/forms/widgets/multiple_hidden.html"
385 def get_context(self, name, value, attrs):
386 context = super().get_context(name, value, attrs)
387 final_attrs = context["widget"]["attrs"]
388 id_ = context["widget"]["attrs"].get("id")
390 subwidgets = []
391 for index, value_ in enumerate(context["widget"]["value"]):
392 widget_attrs = final_attrs.copy()
393 if id_:
394 # An ID attribute was given. Add a numeric index as a suffix
395 # so that the inputs don't all have the same ID attribute.
396 widget_attrs["id"] = "%s_%s" % (id_, index)
397 widget = HiddenInput()
398 widget.is_required = self.is_required
399 subwidgets.append(widget.get_context(name, value_, widget_attrs)["widget"])
401 context["widget"]["subwidgets"] = subwidgets
402 return context
404 def value_from_datadict(self, data, files, name):
405 try:
406 getter = data.getlist
407 except AttributeError:
408 getter = data.get
409 return getter(name)
411 def format_value(self, value):
412 return [] if value is None else value
415class FileInput(Input):
416 input_type = "file"
417 needs_multipart_form = True
418 template_name = "django/forms/widgets/file.html"
420 def format_value(self, value):
421 """File input never renders a value."""
422 return
424 def value_from_datadict(self, data, files, name):
425 "File widgets take data from FILES, not POST"
426 return files.get(name)
428 def value_omitted_from_data(self, data, files, name):
429 return name not in files
431 def use_required_attribute(self, initial):
432 return super().use_required_attribute(initial) and not initial
435FILE_INPUT_CONTRADICTION = object()
438class ClearableFileInput(FileInput):
439 clear_checkbox_label = _("Clear")
440 initial_text = _("Currently")
441 input_text = _("Change")
442 template_name = "django/forms/widgets/clearable_file_input.html"
444 def clear_checkbox_name(self, name):
445 """
446 Given the name of the file input, return the name of the clear checkbox
447 input.
448 """
449 return name + "-clear"
451 def clear_checkbox_id(self, name):
452 """
453 Given the name of the clear checkbox input, return the HTML id for it.
454 """
455 return name + "_id"
457 def is_initial(self, value):
458 """
459 Return whether value is considered to be initial value.
460 """
461 return bool(value and getattr(value, "url", False))
463 def format_value(self, value):
464 """
465 Return the file object if it has a defined url attribute.
466 """
467 if self.is_initial(value):
468 return value
470 def get_context(self, name, value, attrs):
471 context = super().get_context(name, value, attrs)
472 checkbox_name = self.clear_checkbox_name(name)
473 checkbox_id = self.clear_checkbox_id(checkbox_name)
474 context["widget"].update(
475 {
476 "checkbox_name": checkbox_name,
477 "checkbox_id": checkbox_id,
478 "is_initial": self.is_initial(value),
479 "input_text": self.input_text,
480 "initial_text": self.initial_text,
481 "clear_checkbox_label": self.clear_checkbox_label,
482 }
483 )
484 context["widget"]["attrs"].setdefault("disabled", False)
485 return context
487 def value_from_datadict(self, data, files, name):
488 upload = super().value_from_datadict(data, files, name)
489 if not self.is_required and CheckboxInput().value_from_datadict(
490 data, files, self.clear_checkbox_name(name)
491 ):
493 if upload:
494 # If the user contradicts themselves (uploads a new file AND
495 # checks the "clear" checkbox), we return a unique marker
496 # object that FileField will turn into a ValidationError.
497 return FILE_INPUT_CONTRADICTION
498 # False signals to clear any existing value, as opposed to just None
499 return False
500 return upload
502 def value_omitted_from_data(self, data, files, name):
503 return (
504 super().value_omitted_from_data(data, files, name)
505 and self.clear_checkbox_name(name) not in data
506 )
509class Textarea(Widget):
510 template_name = "django/forms/widgets/textarea.html"
512 def __init__(self, attrs=None):
513 # Use slightly better defaults than HTML's 20x2 box
514 default_attrs = {"cols": "40", "rows": "10"}
515 if attrs:
516 default_attrs.update(attrs)
517 super().__init__(default_attrs)
520class DateTimeBaseInput(TextInput):
521 format_key = ""
522 supports_microseconds = False
524 def __init__(self, attrs=None, format=None):
525 super().__init__(attrs)
526 self.format = format or None
528 def format_value(self, value):
529 return formats.localize_input(
530 value, self.format or formats.get_format(self.format_key)[0]
531 )
534class DateInput(DateTimeBaseInput):
535 format_key = "DATE_INPUT_FORMATS"
536 template_name = "django/forms/widgets/date.html"
539class DateTimeInput(DateTimeBaseInput):
540 format_key = "DATETIME_INPUT_FORMATS"
541 template_name = "django/forms/widgets/datetime.html"
544class TimeInput(DateTimeBaseInput):
545 format_key = "TIME_INPUT_FORMATS"
546 template_name = "django/forms/widgets/time.html"
549# Defined at module level so that CheckboxInput is picklable (#17976)
550def boolean_check(v):
551 return not (v is False or v is None or v == "")
554class CheckboxInput(Input):
555 input_type = "checkbox"
556 template_name = "django/forms/widgets/checkbox.html"
558 def __init__(self, attrs=None, check_test=None):
559 super().__init__(attrs)
560 # check_test is a callable that takes a value and returns True
561 # if the checkbox should be checked for that value.
562 self.check_test = boolean_check if check_test is None else check_test
564 def format_value(self, value):
565 """Only return the 'value' attribute if value isn't empty."""
566 if value is True or value is False or value is None or value == "":
567 return
568 return str(value)
570 def get_context(self, name, value, attrs):
571 if self.check_test(value):
572 attrs = {**(attrs or {}), "checked": True}
573 return super().get_context(name, value, attrs)
575 def value_from_datadict(self, data, files, name):
576 if name not in data:
577 # A missing value means False because HTML form submission does not
578 # send results for unselected checkboxes.
579 return False
580 value = data.get(name)
581 # Translate true and false strings to boolean values.
582 values = {"true": True, "false": False}
583 if isinstance(value, str):
584 value = values.get(value.lower(), value)
585 return bool(value)
587 def value_omitted_from_data(self, data, files, name):
588 # HTML checkboxes don't appear in POST data if not checked, so it's
589 # never known if the value is actually omitted.
590 return False
593class ChoiceWidget(Widget):
594 allow_multiple_selected = False
595 input_type = None
596 template_name = None
597 option_template_name = None
598 add_id_index = True
599 checked_attribute = {"checked": True}
600 option_inherits_attrs = True
602 def __init__(self, attrs=None, choices=()):
603 super().__init__(attrs)
604 # choices can be any iterable, but we may need to render this widget
605 # multiple times. Thus, collapse it into a list so it can be consumed
606 # more than once.
607 self.choices = list(choices)
609 def __deepcopy__(self, memo):
610 obj = copy.copy(self)
611 obj.attrs = self.attrs.copy()
612 obj.choices = copy.copy(self.choices)
613 memo[id(self)] = obj
614 return obj
616 def subwidgets(self, name, value, attrs=None):
617 """
618 Yield all "subwidgets" of this widget. Used to enable iterating
619 options from a BoundField for choice widgets.
620 """
621 value = self.format_value(value)
622 yield from self.options(name, value, attrs)
624 def options(self, name, value, attrs=None):
625 """Yield a flat list of options for this widget."""
626 for group in self.optgroups(name, value, attrs):
627 yield from group[1]
629 def optgroups(self, name, value, attrs=None):
630 """Return a list of optgroups for this widget."""
631 groups = []
632 has_selected = False
634 for index, (option_value, option_label) in enumerate(self.choices):
635 if option_value is None:
636 option_value = ""
638 subgroup = []
639 if isinstance(option_label, (list, tuple)):
640 group_name = option_value
641 subindex = 0
642 choices = option_label
643 else:
644 group_name = None
645 subindex = None
646 choices = [(option_value, option_label)]
647 groups.append((group_name, subgroup, index))
649 for subvalue, sublabel in choices:
650 selected = (not has_selected or self.allow_multiple_selected) and str(
651 subvalue
652 ) in value
653 has_selected |= selected
654 subgroup.append(
655 self.create_option(
656 name,
657 subvalue,
658 sublabel,
659 selected,
660 index,
661 subindex=subindex,
662 attrs=attrs,
663 )
664 )
665 if subindex is not None:
666 subindex += 1
667 return groups
669 def create_option(
670 self, name, value, label, selected, index, subindex=None, attrs=None
671 ):
672 index = str(index) if subindex is None else "%s_%s" % (index, subindex)
673 option_attrs = (
674 self.build_attrs(self.attrs, attrs) if self.option_inherits_attrs else {}
675 )
676 if selected:
677 option_attrs.update(self.checked_attribute)
678 if "id" in option_attrs:
679 option_attrs["id"] = self.id_for_label(option_attrs["id"], index)
680 return {
681 "name": name,
682 "value": value,
683 "label": label,
684 "selected": selected,
685 "index": index,
686 "attrs": option_attrs,
687 "type": self.input_type,
688 "template_name": self.option_template_name,
689 "wrap_label": True,
690 }
692 def get_context(self, name, value, attrs):
693 context = super().get_context(name, value, attrs)
694 context["widget"]["optgroups"] = self.optgroups(
695 name, context["widget"]["value"], attrs
696 )
697 return context
699 def id_for_label(self, id_, index="0"):
700 """
701 Use an incremented id for each option where the main widget
702 references the zero index.
703 """
704 if id_ and self.add_id_index:
705 id_ = "%s_%s" % (id_, index)
706 return id_
708 def value_from_datadict(self, data, files, name):
709 getter = data.get
710 if self.allow_multiple_selected:
711 try:
712 getter = data.getlist
713 except AttributeError:
714 pass
715 return getter(name)
717 def format_value(self, value):
718 """Return selected values as a list."""
719 if value is None and self.allow_multiple_selected:
720 return []
721 if not isinstance(value, (tuple, list)):
722 value = [value]
723 return [str(v) if v is not None else "" for v in value]
726class Select(ChoiceWidget):
727 input_type = "select"
728 template_name = "django/forms/widgets/select.html"
729 option_template_name = "django/forms/widgets/select_option.html"
730 add_id_index = False
731 checked_attribute = {"selected": True}
732 option_inherits_attrs = False
734 def get_context(self, name, value, attrs):
735 context = super().get_context(name, value, attrs)
736 if self.allow_multiple_selected:
737 context["widget"]["attrs"]["multiple"] = True
738 return context
740 @staticmethod
741 def _choice_has_empty_value(choice):
742 """Return True if the choice's value is empty string or None."""
743 value, _ = choice
744 return value is None or value == ""
746 def use_required_attribute(self, initial):
747 """
748 Don't render 'required' if the first <option> has a value, as that's
749 invalid HTML.
750 """
751 use_required_attribute = super().use_required_attribute(initial)
752 # 'required' is always okay for <select multiple>.
753 if self.allow_multiple_selected:
754 return use_required_attribute
756 first_choice = next(iter(self.choices), None)
757 return (
758 use_required_attribute
759 and first_choice is not None
760 and self._choice_has_empty_value(first_choice)
761 )
764class NullBooleanSelect(Select):
765 """
766 A Select Widget intended to be used with NullBooleanField.
767 """
769 def __init__(self, attrs=None):
770 choices = (
771 ("unknown", _("Unknown")),
772 ("true", _("Yes")),
773 ("false", _("No")),
774 )
775 super().__init__(attrs, choices)
777 def format_value(self, value):
778 try:
779 return {
780 True: "true",
781 False: "false",
782 "true": "true",
783 "false": "false",
784 # For backwards compatibility with Django < 2.2.
785 "2": "true",
786 "3": "false",
787 }[value]
788 except KeyError:
789 return "unknown"
791 def value_from_datadict(self, data, files, name):
792 value = data.get(name)
793 return {
794 True: True,
795 "True": True,
796 "False": False,
797 False: False,
798 "true": True,
799 "false": False,
800 # For backwards compatibility with Django < 2.2.
801 "2": True,
802 "3": False,
803 }.get(value)
806class SelectMultiple(Select):
807 allow_multiple_selected = True
809 def value_from_datadict(self, data, files, name):
810 try:
811 getter = data.getlist
812 except AttributeError:
813 getter = data.get
814 return getter(name)
816 def value_omitted_from_data(self, data, files, name):
817 # An unselected <select multiple> doesn't appear in POST data, so it's
818 # never known if the value is actually omitted.
819 return False
822class RadioSelect(ChoiceWidget):
823 input_type = "radio"
824 template_name = "django/forms/widgets/radio.html"
825 option_template_name = "django/forms/widgets/radio_option.html"
826 use_fieldset = True
828 def id_for_label(self, id_, index=None):
829 """
830 Don't include for="field_0" in <label> to improve accessibility when
831 using a screen reader, in addition clicking such a label would toggle
832 the first input.
833 """
834 if index is None:
835 return ""
836 return super().id_for_label(id_, index)
839class CheckboxSelectMultiple(RadioSelect):
840 allow_multiple_selected = True
841 input_type = "checkbox"
842 template_name = "django/forms/widgets/checkbox_select.html"
843 option_template_name = "django/forms/widgets/checkbox_option.html"
845 def use_required_attribute(self, initial):
846 # Don't use the 'required' attribute because browser validation would
847 # require all checkboxes to be checked instead of at least one.
848 return False
850 def value_omitted_from_data(self, data, files, name):
851 # HTML checkboxes don't appear in POST data if not checked, so it's
852 # never known if the value is actually omitted.
853 return False
856class MultiWidget(Widget):
857 """
858 A widget that is composed of multiple widgets.
860 In addition to the values added by Widget.get_context(), this widget
861 adds a list of subwidgets to the context as widget['subwidgets'].
862 These can be looped over and rendered like normal widgets.
864 You'll probably want to use this class with MultiValueField.
865 """
867 template_name = "django/forms/widgets/multiwidget.html"
868 use_fieldset = True
870 def __init__(self, widgets, attrs=None):
871 if isinstance(widgets, dict):
872 self.widgets_names = [("_%s" % name) if name else "" for name in widgets]
873 widgets = widgets.values()
874 else:
875 self.widgets_names = ["_%s" % i for i in range(len(widgets))]
876 self.widgets = [w() if isinstance(w, type) else w for w in widgets]
877 super().__init__(attrs)
879 @property
880 def is_hidden(self):
881 return all(w.is_hidden for w in self.widgets)
883 def get_context(self, name, value, attrs):
884 context = super().get_context(name, value, attrs)
885 if self.is_localized:
886 for widget in self.widgets:
887 widget.is_localized = self.is_localized
888 # value is a list/tuple of values, each corresponding to a widget
889 # in self.widgets.
890 if not isinstance(value, (list, tuple)):
891 value = self.decompress(value)
893 final_attrs = context["widget"]["attrs"]
894 input_type = final_attrs.pop("type", None)
895 id_ = final_attrs.get("id")
896 subwidgets = []
897 for i, (widget_name, widget) in enumerate(
898 zip(self.widgets_names, self.widgets)
899 ):
900 if input_type is not None:
901 widget.input_type = input_type
902 widget_name = name + widget_name
903 try:
904 widget_value = value[i]
905 except IndexError:
906 widget_value = None
907 if id_:
908 widget_attrs = final_attrs.copy()
909 widget_attrs["id"] = "%s_%s" % (id_, i)
910 else:
911 widget_attrs = final_attrs
912 subwidgets.append(
913 widget.get_context(widget_name, widget_value, widget_attrs)["widget"]
914 )
915 context["widget"]["subwidgets"] = subwidgets
916 return context
918 def id_for_label(self, id_):
919 return ""
921 def value_from_datadict(self, data, files, name):
922 return [
923 widget.value_from_datadict(data, files, name + widget_name)
924 for widget_name, widget in zip(self.widgets_names, self.widgets)
925 ]
927 def value_omitted_from_data(self, data, files, name):
928 return all(
929 widget.value_omitted_from_data(data, files, name + widget_name)
930 for widget_name, widget in zip(self.widgets_names, self.widgets)
931 )
933 def decompress(self, value):
934 """
935 Return a list of decompressed values for the given compressed value.
936 The given value can be assumed to be valid, but not necessarily
937 non-empty.
938 """
939 raise NotImplementedError("Subclasses must implement this method.")
941 def _get_media(self):
942 """
943 Media for a multiwidget is the combination of all media of the
944 subwidgets.
945 """
946 media = Media()
947 for w in self.widgets:
948 media += w.media
949 return media
951 media = property(_get_media)
953 def __deepcopy__(self, memo):
954 obj = super().__deepcopy__(memo)
955 obj.widgets = copy.deepcopy(self.widgets)
956 return obj
958 @property
959 def needs_multipart_form(self):
960 return any(w.needs_multipart_form for w in self.widgets)
963class SplitDateTimeWidget(MultiWidget):
964 """
965 A widget that splits datetime input into two <input type="text"> boxes.
966 """
968 supports_microseconds = False
969 template_name = "django/forms/widgets/splitdatetime.html"
971 def __init__(
972 self,
973 attrs=None,
974 date_format=None,
975 time_format=None,
976 date_attrs=None,
977 time_attrs=None,
978 ):
979 widgets = (
980 DateInput(
981 attrs=attrs if date_attrs is None else date_attrs,
982 format=date_format,
983 ),
984 TimeInput(
985 attrs=attrs if time_attrs is None else time_attrs,
986 format=time_format,
987 ),
988 )
989 super().__init__(widgets)
991 def decompress(self, value):
992 if value:
993 value = to_current_timezone(value)
994 return [value.date(), value.time()]
995 return [None, None]
998class SplitHiddenDateTimeWidget(SplitDateTimeWidget):
999 """
1000 A widget that splits datetime input into two <input type="hidden"> inputs.
1001 """
1003 template_name = "django/forms/widgets/splithiddendatetime.html"
1005 def __init__(
1006 self,
1007 attrs=None,
1008 date_format=None,
1009 time_format=None,
1010 date_attrs=None,
1011 time_attrs=None,
1012 ):
1013 super().__init__(attrs, date_format, time_format, date_attrs, time_attrs)
1014 for widget in self.widgets:
1015 widget.input_type = "hidden"
1018class SelectDateWidget(Widget):
1019 """
1020 A widget that splits date input into three <select> boxes.
1022 This also serves as an example of a Widget that has more than one HTML
1023 element and hence implements value_from_datadict.
1024 """
1026 none_value = ("", "---")
1027 month_field = "%s_month"
1028 day_field = "%s_day"
1029 year_field = "%s_year"
1030 template_name = "django/forms/widgets/select_date.html"
1031 input_type = "select"
1032 select_widget = Select
1033 date_re = _lazy_re_compile(r"(\d{4}|0)-(\d\d?)-(\d\d?)$")
1034 use_fieldset = True
1036 def __init__(self, attrs=None, years=None, months=None, empty_label=None):
1037 self.attrs = attrs or {}
1039 # Optional list or tuple of years to use in the "year" select box.
1040 if years:
1041 self.years = years
1042 else:
1043 this_year = datetime.date.today().year
1044 self.years = range(this_year, this_year + 10)
1046 # Optional dict of months to use in the "month" select box.
1047 if months:
1048 self.months = months
1049 else:
1050 self.months = MONTHS
1052 # Optional string, list, or tuple to use as empty_label.
1053 if isinstance(empty_label, (list, tuple)):
1054 if not len(empty_label) == 3:
1055 raise ValueError("empty_label list/tuple must have 3 elements.")
1057 self.year_none_value = ("", empty_label[0])
1058 self.month_none_value = ("", empty_label[1])
1059 self.day_none_value = ("", empty_label[2])
1060 else:
1061 if empty_label is not None:
1062 self.none_value = ("", empty_label)
1064 self.year_none_value = self.none_value
1065 self.month_none_value = self.none_value
1066 self.day_none_value = self.none_value
1068 def get_context(self, name, value, attrs):
1069 context = super().get_context(name, value, attrs)
1070 date_context = {}
1071 year_choices = [(i, str(i)) for i in self.years]
1072 if not self.is_required:
1073 year_choices.insert(0, self.year_none_value)
1074 year_name = self.year_field % name
1075 date_context["year"] = self.select_widget(
1076 attrs, choices=year_choices
1077 ).get_context(
1078 name=year_name,
1079 value=context["widget"]["value"]["year"],
1080 attrs={**context["widget"]["attrs"], "id": "id_%s" % year_name},
1081 )
1082 month_choices = list(self.months.items())
1083 if not self.is_required:
1084 month_choices.insert(0, self.month_none_value)
1085 month_name = self.month_field % name
1086 date_context["month"] = self.select_widget(
1087 attrs, choices=month_choices
1088 ).get_context(
1089 name=month_name,
1090 value=context["widget"]["value"]["month"],
1091 attrs={**context["widget"]["attrs"], "id": "id_%s" % month_name},
1092 )
1093 day_choices = [(i, i) for i in range(1, 32)]
1094 if not self.is_required:
1095 day_choices.insert(0, self.day_none_value)
1096 day_name = self.day_field % name
1097 date_context["day"] = self.select_widget(
1098 attrs,
1099 choices=day_choices,
1100 ).get_context(
1101 name=day_name,
1102 value=context["widget"]["value"]["day"],
1103 attrs={**context["widget"]["attrs"], "id": "id_%s" % day_name},
1104 )
1105 subwidgets = []
1106 for field in self._parse_date_fmt():
1107 subwidgets.append(date_context[field]["widget"])
1108 context["widget"]["subwidgets"] = subwidgets
1109 return context
1111 def format_value(self, value):
1112 """
1113 Return a dict containing the year, month, and day of the current value.
1114 Use dict instead of a datetime to allow invalid dates such as February
1115 31 to display correctly.
1116 """
1117 year, month, day = None, None, None
1118 if isinstance(value, (datetime.date, datetime.datetime)):
1119 year, month, day = value.year, value.month, value.day
1120 elif isinstance(value, str):
1121 match = self.date_re.match(value)
1122 if match:
1123 # Convert any zeros in the date to empty strings to match the
1124 # empty option value.
1125 year, month, day = [int(val) or "" for val in match.groups()]
1126 else:
1127 input_format = get_format("DATE_INPUT_FORMATS")[0]
1128 try:
1129 d = datetime.datetime.strptime(value, input_format)
1130 except ValueError:
1131 pass
1132 else:
1133 year, month, day = d.year, d.month, d.day
1134 return {"year": year, "month": month, "day": day}
1136 @staticmethod
1137 def _parse_date_fmt():
1138 fmt = get_format("DATE_FORMAT")
1139 escaped = False
1140 for char in fmt:
1141 if escaped:
1142 escaped = False
1143 elif char == "\\":
1144 escaped = True
1145 elif char in "Yy":
1146 yield "year"
1147 elif char in "bEFMmNn":
1148 yield "month"
1149 elif char in "dj":
1150 yield "day"
1152 def id_for_label(self, id_):
1153 for first_select in self._parse_date_fmt():
1154 return "%s_%s" % (id_, first_select)
1155 return "%s_month" % id_
1157 def value_from_datadict(self, data, files, name):
1158 y = data.get(self.year_field % name)
1159 m = data.get(self.month_field % name)
1160 d = data.get(self.day_field % name)
1161 if y == m == d == "":
1162 return None
1163 if y is not None and m is not None and d is not None:
1164 input_format = get_format("DATE_INPUT_FORMATS")[0]
1165 input_format = formats.sanitize_strftime_format(input_format)
1166 try:
1167 date_value = datetime.date(int(y), int(m), int(d))
1168 except ValueError:
1169 # Return pseudo-ISO dates with zeros for any unselected values,
1170 # e.g. '2017-0-23'.
1171 return "%s-%s-%s" % (y or 0, m or 0, d or 0)
1172 return date_value.strftime(input_format)
1173 return data.get(name)
1175 def value_omitted_from_data(self, data, files, name):
1176 return not any(
1177 ("{}_{}".format(name, interval) in data)
1178 for interval in ("year", "month", "day")
1179 )