|
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177 |
- """
- HTML Widget classes
- """
-
- import copy
- import datetime
- import warnings
- from collections import defaultdict
- from itertools import chain
-
- from django.forms.utils import to_current_timezone
- from django.templatetags.static import static
- from django.utils import formats
- from django.utils.datastructures import OrderedSet
- from django.utils.dates import MONTHS
- from django.utils.formats import get_format
- from django.utils.html import format_html, html_safe
- from django.utils.regex_helper import _lazy_re_compile
- from django.utils.safestring import mark_safe
- from django.utils.topological_sort import CyclicDependencyError, stable_topological_sort
- from django.utils.translation import gettext_lazy as _
-
- from .renderers import get_default_renderer
-
- __all__ = (
- "Media",
- "MediaDefiningClass",
- "Widget",
- "TextInput",
- "NumberInput",
- "EmailInput",
- "URLInput",
- "PasswordInput",
- "HiddenInput",
- "MultipleHiddenInput",
- "FileInput",
- "ClearableFileInput",
- "Textarea",
- "DateInput",
- "DateTimeInput",
- "TimeInput",
- "CheckboxInput",
- "Select",
- "NullBooleanSelect",
- "SelectMultiple",
- "RadioSelect",
- "CheckboxSelectMultiple",
- "MultiWidget",
- "SplitDateTimeWidget",
- "SplitHiddenDateTimeWidget",
- "SelectDateWidget",
- )
-
- MEDIA_TYPES = ("css", "js")
-
-
- class MediaOrderConflictWarning(RuntimeWarning):
- pass
-
-
- @html_safe
- class Media:
- def __init__(self, media=None, css=None, js=None):
- if media is not None:
- css = getattr(media, "css", {})
- js = getattr(media, "js", [])
- else:
- if css is None:
- css = {}
- if js is None:
- js = []
- self._css_lists = [css]
- self._js_lists = [js]
-
- def __repr__(self):
- return "Media(css=%r, js=%r)" % (self._css, self._js)
-
- def __str__(self):
- return self.render()
-
- @property
- def _css(self):
- css = defaultdict(list)
- for css_list in self._css_lists:
- for medium, sublist in css_list.items():
- css[medium].append(sublist)
- return {medium: self.merge(*lists) for medium, lists in css.items()}
-
- @property
- def _js(self):
- return self.merge(*self._js_lists)
-
- def render(self):
- return mark_safe(
- "\n".join(
- chain.from_iterable(
- getattr(self, "render_" + name)() for name in MEDIA_TYPES
- )
- )
- )
-
- def render_js(self):
- return [
- path.__html__()
- if hasattr(path, "__html__")
- else format_html('<script src="{}"></script>', self.absolute_path(path))
- for path in self._js
- ]
-
- def render_css(self):
- # To keep rendering order consistent, we can't just iterate over items().
- # We need to sort the keys, and iterate over the sorted list.
- media = sorted(self._css)
- return chain.from_iterable(
- [
- path.__html__()
- if hasattr(path, "__html__")
- else format_html(
- '<link href="{}" media="{}" rel="stylesheet">',
- self.absolute_path(path),
- medium,
- )
- for path in self._css[medium]
- ]
- for medium in media
- )
-
- def absolute_path(self, path):
- """
- Given a relative or absolute path to a static asset, return an absolute
- path. An absolute path will be returned unchanged while a relative path
- will be passed to django.templatetags.static.static().
- """
- if path.startswith(("http://", "https://", "/")):
- return path
- return static(path)
-
- def __getitem__(self, name):
- """Return a Media object that only contains media of the given type."""
- if name in MEDIA_TYPES:
- return Media(**{str(name): getattr(self, "_" + name)})
- raise KeyError('Unknown media type "%s"' % name)
-
- @staticmethod
- def merge(*lists):
- """
- Merge lists while trying to keep the relative order of the elements.
- Warn if the lists have the same elements in a different relative order.
-
- For static assets it can be important to have them included in the DOM
- in a certain order. In JavaScript you may not be able to reference a
- global or in CSS you might want to override a style.
- """
- dependency_graph = defaultdict(set)
- all_items = OrderedSet()
- for list_ in filter(None, lists):
- head = list_[0]
- # The first items depend on nothing but have to be part of the
- # dependency graph to be included in the result.
- dependency_graph.setdefault(head, set())
- for item in list_:
- all_items.add(item)
- # No self dependencies
- if head != item:
- dependency_graph[item].add(head)
- head = item
- try:
- return stable_topological_sort(all_items, dependency_graph)
- except CyclicDependencyError:
- warnings.warn(
- "Detected duplicate Media files in an opposite order: {}".format(
- ", ".join(repr(list_) for list_ in lists)
- ),
- MediaOrderConflictWarning,
- )
- return list(all_items)
-
- def __add__(self, other):
- combined = Media()
- combined._css_lists = self._css_lists[:]
- combined._js_lists = self._js_lists[:]
- for item in other._css_lists:
- if item and item not in self._css_lists:
- combined._css_lists.append(item)
- for item in other._js_lists:
- if item and item not in self._js_lists:
- combined._js_lists.append(item)
- return combined
-
-
- def media_property(cls):
- def _media(self):
- # Get the media property of the superclass, if it exists
- sup_cls = super(cls, self)
- try:
- base = sup_cls.media
- except AttributeError:
- base = Media()
-
- # Get the media definition for this class
- definition = getattr(cls, "Media", None)
- if definition:
- extend = getattr(definition, "extend", True)
- if extend:
- if extend is True:
- m = base
- else:
- m = Media()
- for medium in extend:
- m = m + base[medium]
- return m + Media(definition)
- return Media(definition)
- return base
-
- return property(_media)
-
-
- class MediaDefiningClass(type):
- """
- Metaclass for classes that can have media definitions.
- """
-
- def __new__(mcs, name, bases, attrs):
- new_class = super().__new__(mcs, name, bases, attrs)
-
- if "media" not in attrs:
- new_class.media = media_property(new_class)
-
- return new_class
-
-
- class Widget(metaclass=MediaDefiningClass):
- needs_multipart_form = False # Determines does this widget need multipart form
- is_localized = False
- is_required = False
- supports_microseconds = True
- use_fieldset = False
-
- def __init__(self, attrs=None):
- self.attrs = {} if attrs is None else attrs.copy()
-
- def __deepcopy__(self, memo):
- obj = copy.copy(self)
- obj.attrs = self.attrs.copy()
- memo[id(self)] = obj
- return obj
-
- @property
- def is_hidden(self):
- return self.input_type == "hidden" if hasattr(self, "input_type") else False
-
- def subwidgets(self, name, value, attrs=None):
- context = self.get_context(name, value, attrs)
- yield context["widget"]
-
- def format_value(self, value):
- """
- Return a value as it should appear when rendered in a template.
- """
- if value == "" or value is None:
- return None
- if self.is_localized:
- return formats.localize_input(value)
- return str(value)
-
- def get_context(self, name, value, attrs):
- return {
- "widget": {
- "name": name,
- "is_hidden": self.is_hidden,
- "required": self.is_required,
- "value": self.format_value(value),
- "attrs": self.build_attrs(self.attrs, attrs),
- "template_name": self.template_name,
- },
- }
-
- def render(self, name, value, attrs=None, renderer=None):
- """Render the widget as an HTML string."""
- context = self.get_context(name, value, attrs)
- return self._render(self.template_name, context, renderer)
-
- def _render(self, template_name, context, renderer=None):
- if renderer is None:
- renderer = get_default_renderer()
- return mark_safe(renderer.render(template_name, context))
-
- def build_attrs(self, base_attrs, extra_attrs=None):
- """Build an attribute dictionary."""
- return {**base_attrs, **(extra_attrs or {})}
-
- def value_from_datadict(self, data, files, name):
- """
- Given a dictionary of data and this widget's name, return the value
- of this widget or None if it's not provided.
- """
- return data.get(name)
-
- def value_omitted_from_data(self, data, files, name):
- return name not in data
-
- def id_for_label(self, id_):
- """
- Return the HTML ID attribute of this Widget for use by a <label>, given
- the ID of the field. Return an empty string if no ID is available.
-
- This hook is necessary because some widgets have multiple HTML
- elements and, thus, multiple IDs. In that case, this method should
- return an ID value that corresponds to the first ID in the widget's
- tags.
- """
- return id_
-
- def use_required_attribute(self, initial):
- return not self.is_hidden
-
-
- class Input(Widget):
- """
- Base class for all <input> widgets.
- """
-
- input_type = None # Subclasses must define this.
- template_name = "django/forms/widgets/input.html"
-
- def __init__(self, attrs=None):
- if attrs is not None:
- attrs = attrs.copy()
- self.input_type = attrs.pop("type", self.input_type)
- super().__init__(attrs)
-
- def get_context(self, name, value, attrs):
- context = super().get_context(name, value, attrs)
- context["widget"]["type"] = self.input_type
- return context
-
-
- class TextInput(Input):
- input_type = "text"
- template_name = "django/forms/widgets/text.html"
-
-
- class NumberInput(Input):
- input_type = "number"
- template_name = "django/forms/widgets/number.html"
-
-
- class EmailInput(Input):
- input_type = "email"
- template_name = "django/forms/widgets/email.html"
-
-
- class URLInput(Input):
- input_type = "url"
- template_name = "django/forms/widgets/url.html"
-
-
- class PasswordInput(Input):
- input_type = "password"
- template_name = "django/forms/widgets/password.html"
-
- def __init__(self, attrs=None, render_value=False):
- super().__init__(attrs)
- self.render_value = render_value
-
- def get_context(self, name, value, attrs):
- if not self.render_value:
- value = None
- return super().get_context(name, value, attrs)
-
-
- class HiddenInput(Input):
- input_type = "hidden"
- template_name = "django/forms/widgets/hidden.html"
-
-
- class MultipleHiddenInput(HiddenInput):
- """
- Handle <input type="hidden"> for fields that have a list
- of values.
- """
-
- template_name = "django/forms/widgets/multiple_hidden.html"
-
- def get_context(self, name, value, attrs):
- context = super().get_context(name, value, attrs)
- final_attrs = context["widget"]["attrs"]
- id_ = context["widget"]["attrs"].get("id")
-
- subwidgets = []
- for index, value_ in enumerate(context["widget"]["value"]):
- widget_attrs = final_attrs.copy()
- if id_:
- # An ID attribute was given. Add a numeric index as a suffix
- # so that the inputs don't all have the same ID attribute.
- widget_attrs["id"] = "%s_%s" % (id_, index)
- widget = HiddenInput()
- widget.is_required = self.is_required
- subwidgets.append(widget.get_context(name, value_, widget_attrs)["widget"])
-
- context["widget"]["subwidgets"] = subwidgets
- return context
-
- def value_from_datadict(self, data, files, name):
- try:
- getter = data.getlist
- except AttributeError:
- getter = data.get
- return getter(name)
-
- def format_value(self, value):
- return [] if value is None else value
-
-
- class FileInput(Input):
- input_type = "file"
- needs_multipart_form = True
- template_name = "django/forms/widgets/file.html"
-
- def format_value(self, value):
- """File input never renders a value."""
- return
-
- def value_from_datadict(self, data, files, name):
- "File widgets take data from FILES, not POST"
- return files.get(name)
-
- def value_omitted_from_data(self, data, files, name):
- return name not in files
-
- def use_required_attribute(self, initial):
- return super().use_required_attribute(initial) and not initial
-
-
- FILE_INPUT_CONTRADICTION = object()
-
-
- class ClearableFileInput(FileInput):
- clear_checkbox_label = _("Clear")
- initial_text = _("Currently")
- input_text = _("Change")
- template_name = "django/forms/widgets/clearable_file_input.html"
-
- def clear_checkbox_name(self, name):
- """
- Given the name of the file input, return the name of the clear checkbox
- input.
- """
- return name + "-clear"
-
- def clear_checkbox_id(self, name):
- """
- Given the name of the clear checkbox input, return the HTML id for it.
- """
- return name + "_id"
-
- def is_initial(self, value):
- """
- Return whether value is considered to be initial value.
- """
- return bool(value and getattr(value, "url", False))
-
- def format_value(self, value):
- """
- Return the file object if it has a defined url attribute.
- """
- if self.is_initial(value):
- return value
-
- def get_context(self, name, value, attrs):
- context = super().get_context(name, value, attrs)
- checkbox_name = self.clear_checkbox_name(name)
- checkbox_id = self.clear_checkbox_id(checkbox_name)
- context["widget"].update(
- {
- "checkbox_name": checkbox_name,
- "checkbox_id": checkbox_id,
- "is_initial": self.is_initial(value),
- "input_text": self.input_text,
- "initial_text": self.initial_text,
- "clear_checkbox_label": self.clear_checkbox_label,
- }
- )
- return context
-
- def value_from_datadict(self, data, files, name):
- upload = super().value_from_datadict(data, files, name)
- if not self.is_required and CheckboxInput().value_from_datadict(
- data, files, self.clear_checkbox_name(name)
- ):
- if upload:
- # If the user contradicts themselves (uploads a new file AND
- # checks the "clear" checkbox), we return a unique marker
- # object that FileField will turn into a ValidationError.
- return FILE_INPUT_CONTRADICTION
- # False signals to clear any existing value, as opposed to just None
- return False
- return upload
-
- def value_omitted_from_data(self, data, files, name):
- return (
- super().value_omitted_from_data(data, files, name)
- and self.clear_checkbox_name(name) not in data
- )
-
-
- class Textarea(Widget):
- template_name = "django/forms/widgets/textarea.html"
-
- def __init__(self, attrs=None):
- # Use slightly better defaults than HTML's 20x2 box
- default_attrs = {"cols": "40", "rows": "10"}
- if attrs:
- default_attrs.update(attrs)
- super().__init__(default_attrs)
-
-
- class DateTimeBaseInput(TextInput):
- format_key = ""
- supports_microseconds = False
-
- def __init__(self, attrs=None, format=None):
- super().__init__(attrs)
- self.format = format or None
-
- def format_value(self, value):
- return formats.localize_input(
- value, self.format or formats.get_format(self.format_key)[0]
- )
-
-
- class DateInput(DateTimeBaseInput):
- format_key = "DATE_INPUT_FORMATS"
- template_name = "django/forms/widgets/date.html"
-
-
- class DateTimeInput(DateTimeBaseInput):
- format_key = "DATETIME_INPUT_FORMATS"
- template_name = "django/forms/widgets/datetime.html"
-
-
- class TimeInput(DateTimeBaseInput):
- format_key = "TIME_INPUT_FORMATS"
- template_name = "django/forms/widgets/time.html"
-
-
- # Defined at module level so that CheckboxInput is picklable (#17976)
- def boolean_check(v):
- return not (v is False or v is None or v == "")
-
-
- class CheckboxInput(Input):
- input_type = "checkbox"
- template_name = "django/forms/widgets/checkbox.html"
-
- def __init__(self, attrs=None, check_test=None):
- super().__init__(attrs)
- # check_test is a callable that takes a value and returns True
- # if the checkbox should be checked for that value.
- self.check_test = boolean_check if check_test is None else check_test
-
- def format_value(self, value):
- """Only return the 'value' attribute if value isn't empty."""
- if value is True or value is False or value is None or value == "":
- return
- return str(value)
-
- def get_context(self, name, value, attrs):
- if self.check_test(value):
- attrs = {**(attrs or {}), "checked": True}
- return super().get_context(name, value, attrs)
-
- def value_from_datadict(self, data, files, name):
- if name not in data:
- # A missing value means False because HTML form submission does not
- # send results for unselected checkboxes.
- return False
- value = data.get(name)
- # Translate true and false strings to boolean values.
- values = {"true": True, "false": False}
- if isinstance(value, str):
- value = values.get(value.lower(), value)
- return bool(value)
-
- def value_omitted_from_data(self, data, files, name):
- # HTML checkboxes don't appear in POST data if not checked, so it's
- # never known if the value is actually omitted.
- return False
-
-
- class ChoiceWidget(Widget):
- allow_multiple_selected = False
- input_type = None
- template_name = None
- option_template_name = None
- add_id_index = True
- checked_attribute = {"checked": True}
- option_inherits_attrs = True
-
- def __init__(self, attrs=None, choices=()):
- super().__init__(attrs)
- # choices can be any iterable, but we may need to render this widget
- # multiple times. Thus, collapse it into a list so it can be consumed
- # more than once.
- self.choices = list(choices)
-
- def __deepcopy__(self, memo):
- obj = copy.copy(self)
- obj.attrs = self.attrs.copy()
- obj.choices = copy.copy(self.choices)
- memo[id(self)] = obj
- return obj
-
- def subwidgets(self, name, value, attrs=None):
- """
- Yield all "subwidgets" of this widget. Used to enable iterating
- options from a BoundField for choice widgets.
- """
- value = self.format_value(value)
- yield from self.options(name, value, attrs)
-
- def options(self, name, value, attrs=None):
- """Yield a flat list of options for this widget."""
- for group in self.optgroups(name, value, attrs):
- yield from group[1]
-
- def optgroups(self, name, value, attrs=None):
- """Return a list of optgroups for this widget."""
- groups = []
- has_selected = False
-
- for index, (option_value, option_label) in enumerate(self.choices):
- if option_value is None:
- option_value = ""
-
- subgroup = []
- if isinstance(option_label, (list, tuple)):
- group_name = option_value
- subindex = 0
- choices = option_label
- else:
- group_name = None
- subindex = None
- choices = [(option_value, option_label)]
- groups.append((group_name, subgroup, index))
-
- for subvalue, sublabel in choices:
- selected = (not has_selected or self.allow_multiple_selected) and str(
- subvalue
- ) in value
- has_selected |= selected
- subgroup.append(
- self.create_option(
- name,
- subvalue,
- sublabel,
- selected,
- index,
- subindex=subindex,
- attrs=attrs,
- )
- )
- if subindex is not None:
- subindex += 1
- return groups
-
- def create_option(
- self, name, value, label, selected, index, subindex=None, attrs=None
- ):
- index = str(index) if subindex is None else "%s_%s" % (index, subindex)
- option_attrs = (
- self.build_attrs(self.attrs, attrs) if self.option_inherits_attrs else {}
- )
- if selected:
- option_attrs.update(self.checked_attribute)
- if "id" in option_attrs:
- option_attrs["id"] = self.id_for_label(option_attrs["id"], index)
- return {
- "name": name,
- "value": value,
- "label": label,
- "selected": selected,
- "index": index,
- "attrs": option_attrs,
- "type": self.input_type,
- "template_name": self.option_template_name,
- "wrap_label": True,
- }
-
- def get_context(self, name, value, attrs):
- context = super().get_context(name, value, attrs)
- context["widget"]["optgroups"] = self.optgroups(
- name, context["widget"]["value"], attrs
- )
- return context
-
- def id_for_label(self, id_, index="0"):
- """
- Use an incremented id for each option where the main widget
- references the zero index.
- """
- if id_ and self.add_id_index:
- id_ = "%s_%s" % (id_, index)
- return id_
-
- def value_from_datadict(self, data, files, name):
- getter = data.get
- if self.allow_multiple_selected:
- try:
- getter = data.getlist
- except AttributeError:
- pass
- return getter(name)
-
- def format_value(self, value):
- """Return selected values as a list."""
- if value is None and self.allow_multiple_selected:
- return []
- if not isinstance(value, (tuple, list)):
- value = [value]
- return [str(v) if v is not None else "" for v in value]
-
-
- class Select(ChoiceWidget):
- input_type = "select"
- template_name = "django/forms/widgets/select.html"
- option_template_name = "django/forms/widgets/select_option.html"
- add_id_index = False
- checked_attribute = {"selected": True}
- option_inherits_attrs = False
-
- def get_context(self, name, value, attrs):
- context = super().get_context(name, value, attrs)
- if self.allow_multiple_selected:
- context["widget"]["attrs"]["multiple"] = True
- return context
-
- @staticmethod
- def _choice_has_empty_value(choice):
- """Return True if the choice's value is empty string or None."""
- value, _ = choice
- return value is None or value == ""
-
- def use_required_attribute(self, initial):
- """
- Don't render 'required' if the first <option> has a value, as that's
- invalid HTML.
- """
- use_required_attribute = super().use_required_attribute(initial)
- # 'required' is always okay for <select multiple>.
- if self.allow_multiple_selected:
- return use_required_attribute
-
- first_choice = next(iter(self.choices), None)
- return (
- use_required_attribute
- and first_choice is not None
- and self._choice_has_empty_value(first_choice)
- )
-
-
- class NullBooleanSelect(Select):
- """
- A Select Widget intended to be used with NullBooleanField.
- """
-
- def __init__(self, attrs=None):
- choices = (
- ("unknown", _("Unknown")),
- ("true", _("Yes")),
- ("false", _("No")),
- )
- super().__init__(attrs, choices)
-
- def format_value(self, value):
- try:
- return {
- True: "true",
- False: "false",
- "true": "true",
- "false": "false",
- # For backwards compatibility with Django < 2.2.
- "2": "true",
- "3": "false",
- }[value]
- except KeyError:
- return "unknown"
-
- def value_from_datadict(self, data, files, name):
- value = data.get(name)
- return {
- True: True,
- "True": True,
- "False": False,
- False: False,
- "true": True,
- "false": False,
- # For backwards compatibility with Django < 2.2.
- "2": True,
- "3": False,
- }.get(value)
-
-
- class SelectMultiple(Select):
- allow_multiple_selected = True
-
- def value_from_datadict(self, data, files, name):
- try:
- getter = data.getlist
- except AttributeError:
- getter = data.get
- return getter(name)
-
- def value_omitted_from_data(self, data, files, name):
- # An unselected <select multiple> doesn't appear in POST data, so it's
- # never known if the value is actually omitted.
- return False
-
-
- class RadioSelect(ChoiceWidget):
- input_type = "radio"
- template_name = "django/forms/widgets/radio.html"
- option_template_name = "django/forms/widgets/radio_option.html"
- use_fieldset = True
-
- def id_for_label(self, id_, index=None):
- """
- Don't include for="field_0" in <label> to improve accessibility when
- using a screen reader, in addition clicking such a label would toggle
- the first input.
- """
- if index is None:
- return ""
- return super().id_for_label(id_, index)
-
-
- class CheckboxSelectMultiple(RadioSelect):
- allow_multiple_selected = True
- input_type = "checkbox"
- template_name = "django/forms/widgets/checkbox_select.html"
- option_template_name = "django/forms/widgets/checkbox_option.html"
-
- def use_required_attribute(self, initial):
- # Don't use the 'required' attribute because browser validation would
- # require all checkboxes to be checked instead of at least one.
- return False
-
- def value_omitted_from_data(self, data, files, name):
- # HTML checkboxes don't appear in POST data if not checked, so it's
- # never known if the value is actually omitted.
- return False
-
-
- class MultiWidget(Widget):
- """
- A widget that is composed of multiple widgets.
-
- In addition to the values added by Widget.get_context(), this widget
- adds a list of subwidgets to the context as widget['subwidgets'].
- These can be looped over and rendered like normal widgets.
-
- You'll probably want to use this class with MultiValueField.
- """
-
- template_name = "django/forms/widgets/multiwidget.html"
- use_fieldset = True
-
- def __init__(self, widgets, attrs=None):
- if isinstance(widgets, dict):
- self.widgets_names = [("_%s" % name) if name else "" for name in widgets]
- widgets = widgets.values()
- else:
- self.widgets_names = ["_%s" % i for i in range(len(widgets))]
- self.widgets = [w() if isinstance(w, type) else w for w in widgets]
- super().__init__(attrs)
-
- @property
- def is_hidden(self):
- return all(w.is_hidden for w in self.widgets)
-
- def get_context(self, name, value, attrs):
- context = super().get_context(name, value, attrs)
- if self.is_localized:
- for widget in self.widgets:
- widget.is_localized = self.is_localized
- # value is a list/tuple of values, each corresponding to a widget
- # in self.widgets.
- if not isinstance(value, (list, tuple)):
- value = self.decompress(value)
-
- final_attrs = context["widget"]["attrs"]
- input_type = final_attrs.pop("type", None)
- id_ = final_attrs.get("id")
- subwidgets = []
- for i, (widget_name, widget) in enumerate(
- zip(self.widgets_names, self.widgets)
- ):
- if input_type is not None:
- widget.input_type = input_type
- widget_name = name + widget_name
- try:
- widget_value = value[i]
- except IndexError:
- widget_value = None
- if id_:
- widget_attrs = final_attrs.copy()
- widget_attrs["id"] = "%s_%s" % (id_, i)
- else:
- widget_attrs = final_attrs
- subwidgets.append(
- widget.get_context(widget_name, widget_value, widget_attrs)["widget"]
- )
- context["widget"]["subwidgets"] = subwidgets
- return context
-
- def id_for_label(self, id_):
- return ""
-
- def value_from_datadict(self, data, files, name):
- return [
- widget.value_from_datadict(data, files, name + widget_name)
- for widget_name, widget in zip(self.widgets_names, self.widgets)
- ]
-
- def value_omitted_from_data(self, data, files, name):
- return all(
- widget.value_omitted_from_data(data, files, name + widget_name)
- for widget_name, widget in zip(self.widgets_names, self.widgets)
- )
-
- def decompress(self, value):
- """
- Return a list of decompressed values for the given compressed value.
- The given value can be assumed to be valid, but not necessarily
- non-empty.
- """
- raise NotImplementedError("Subclasses must implement this method.")
-
- def _get_media(self):
- """
- Media for a multiwidget is the combination of all media of the
- subwidgets.
- """
- media = Media()
- for w in self.widgets:
- media = media + w.media
- return media
-
- media = property(_get_media)
-
- def __deepcopy__(self, memo):
- obj = super().__deepcopy__(memo)
- obj.widgets = copy.deepcopy(self.widgets)
- return obj
-
- @property
- def needs_multipart_form(self):
- return any(w.needs_multipart_form for w in self.widgets)
-
-
- class SplitDateTimeWidget(MultiWidget):
- """
- A widget that splits datetime input into two <input type="text"> boxes.
- """
-
- supports_microseconds = False
- template_name = "django/forms/widgets/splitdatetime.html"
-
- def __init__(
- self,
- attrs=None,
- date_format=None,
- time_format=None,
- date_attrs=None,
- time_attrs=None,
- ):
- widgets = (
- DateInput(
- attrs=attrs if date_attrs is None else date_attrs,
- format=date_format,
- ),
- TimeInput(
- attrs=attrs if time_attrs is None else time_attrs,
- format=time_format,
- ),
- )
- super().__init__(widgets)
-
- def decompress(self, value):
- if value:
- value = to_current_timezone(value)
- return [value.date(), value.time()]
- return [None, None]
-
-
- class SplitHiddenDateTimeWidget(SplitDateTimeWidget):
- """
- A widget that splits datetime input into two <input type="hidden"> inputs.
- """
-
- template_name = "django/forms/widgets/splithiddendatetime.html"
-
- def __init__(
- self,
- attrs=None,
- date_format=None,
- time_format=None,
- date_attrs=None,
- time_attrs=None,
- ):
- super().__init__(attrs, date_format, time_format, date_attrs, time_attrs)
- for widget in self.widgets:
- widget.input_type = "hidden"
-
-
- class SelectDateWidget(Widget):
- """
- A widget that splits date input into three <select> boxes.
-
- This also serves as an example of a Widget that has more than one HTML
- element and hence implements value_from_datadict.
- """
-
- none_value = ("", "---")
- month_field = "%s_month"
- day_field = "%s_day"
- year_field = "%s_year"
- template_name = "django/forms/widgets/select_date.html"
- input_type = "select"
- select_widget = Select
- date_re = _lazy_re_compile(r"(\d{4}|0)-(\d\d?)-(\d\d?)$")
- use_fieldset = True
-
- def __init__(self, attrs=None, years=None, months=None, empty_label=None):
- self.attrs = attrs or {}
-
- # Optional list or tuple of years to use in the "year" select box.
- if years:
- self.years = years
- else:
- this_year = datetime.date.today().year
- self.years = range(this_year, this_year + 10)
-
- # Optional dict of months to use in the "month" select box.
- if months:
- self.months = months
- else:
- self.months = MONTHS
-
- # Optional string, list, or tuple to use as empty_label.
- if isinstance(empty_label, (list, tuple)):
- if not len(empty_label) == 3:
- raise ValueError("empty_label list/tuple must have 3 elements.")
-
- self.year_none_value = ("", empty_label[0])
- self.month_none_value = ("", empty_label[1])
- self.day_none_value = ("", empty_label[2])
- else:
- if empty_label is not None:
- self.none_value = ("", empty_label)
-
- self.year_none_value = self.none_value
- self.month_none_value = self.none_value
- self.day_none_value = self.none_value
-
- def get_context(self, name, value, attrs):
- context = super().get_context(name, value, attrs)
- date_context = {}
- year_choices = [(i, str(i)) for i in self.years]
- if not self.is_required:
- year_choices.insert(0, self.year_none_value)
- year_name = self.year_field % name
- date_context["year"] = self.select_widget(
- attrs, choices=year_choices
- ).get_context(
- name=year_name,
- value=context["widget"]["value"]["year"],
- attrs={**context["widget"]["attrs"], "id": "id_%s" % year_name},
- )
- month_choices = list(self.months.items())
- if not self.is_required:
- month_choices.insert(0, self.month_none_value)
- month_name = self.month_field % name
- date_context["month"] = self.select_widget(
- attrs, choices=month_choices
- ).get_context(
- name=month_name,
- value=context["widget"]["value"]["month"],
- attrs={**context["widget"]["attrs"], "id": "id_%s" % month_name},
- )
- day_choices = [(i, i) for i in range(1, 32)]
- if not self.is_required:
- day_choices.insert(0, self.day_none_value)
- day_name = self.day_field % name
- date_context["day"] = self.select_widget(
- attrs,
- choices=day_choices,
- ).get_context(
- name=day_name,
- value=context["widget"]["value"]["day"],
- attrs={**context["widget"]["attrs"], "id": "id_%s" % day_name},
- )
- subwidgets = []
- for field in self._parse_date_fmt():
- subwidgets.append(date_context[field]["widget"])
- context["widget"]["subwidgets"] = subwidgets
- return context
-
- def format_value(self, value):
- """
- Return a dict containing the year, month, and day of the current value.
- Use dict instead of a datetime to allow invalid dates such as February
- 31 to display correctly.
- """
- year, month, day = None, None, None
- if isinstance(value, (datetime.date, datetime.datetime)):
- year, month, day = value.year, value.month, value.day
- elif isinstance(value, str):
- match = self.date_re.match(value)
- if match:
- # Convert any zeros in the date to empty strings to match the
- # empty option value.
- year, month, day = [int(val) or "" for val in match.groups()]
- else:
- input_format = get_format("DATE_INPUT_FORMATS")[0]
- try:
- d = datetime.datetime.strptime(value, input_format)
- except ValueError:
- pass
- else:
- year, month, day = d.year, d.month, d.day
- return {"year": year, "month": month, "day": day}
-
- @staticmethod
- def _parse_date_fmt():
- fmt = get_format("DATE_FORMAT")
- escaped = False
- for char in fmt:
- if escaped:
- escaped = False
- elif char == "\\":
- escaped = True
- elif char in "Yy":
- yield "year"
- elif char in "bEFMmNn":
- yield "month"
- elif char in "dj":
- yield "day"
-
- def id_for_label(self, id_):
- for first_select in self._parse_date_fmt():
- return "%s_%s" % (id_, first_select)
- return "%s_month" % id_
-
- def value_from_datadict(self, data, files, name):
- y = data.get(self.year_field % name)
- m = data.get(self.month_field % name)
- d = data.get(self.day_field % name)
- if y == m == d == "":
- return None
- if y is not None and m is not None and d is not None:
- input_format = get_format("DATE_INPUT_FORMATS")[0]
- input_format = formats.sanitize_strftime_format(input_format)
- try:
- date_value = datetime.date(int(y), int(m), int(d))
- except ValueError:
- # Return pseudo-ISO dates with zeros for any unselected values,
- # e.g. '2017-0-23'.
- return "%s-%s-%s" % (y or 0, m or 0, d or 0)
- return date_value.strftime(input_format)
- return data.get(name)
-
- def value_omitted_from_data(self, data, files, name):
- return not any(
- ("{}_{}".format(name, interval) in data)
- for interval in ("year", "month", "day")
- )
|