"""
HTML Widget classes
"""
import copy
import datetime
import warnings
from collections import defaultdict
from graphlib import CycleError, TopologicalSorter
from itertools import chain
from django.forms.utils import flatatt, to_current_timezone
from django.templatetags.static import static
from django.utils import formats
from django.utils.choices import normalize_choices
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.translation import gettext_lazy as _
from .renderers import get_default_renderer
__all__ = (
"Script",
"Media",
"MediaDefiningClass",
"Widget",
"TextInput",
"NumberInput",
"EmailInput",
"URLInput",
"ColorInput",
"SearchInput",
"TelInput",
"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 MediaAsset:
element_template = "{path}"
def __init__(self, path, **attributes):
self._path = path
self.attributes = attributes
def __eq__(self, other):
# Compare the path only, to ensure performant comparison in
# Media.merge.
return (self.__class__ is other.__class__ and self.path == other.path) or (
isinstance(other, str) and self._path == other
)
def __hash__(self):
# Hash the path only, to ensure performant comparison in Media.merge.
return hash(self._path)
def __str__(self):
return format_html(
self.element_template,
path=self.path,
attributes=flatatt(self.attributes),
)
def __repr__(self):
return f"{type(self).__qualname__}({self._path!r})"
@property
def path(self):
"""
Ensure an absolute path.
Relative paths are resolved via the {% static %} template tag.
"""
if self._path.startswith(("http://", "https://", "/")):
return self._path
return static(self._path)
class Script(MediaAsset):
element_template = '<script src="{path}"{attributes}></script>'
def __init__(self, src, **attributes):
# Alter the signature to allow src to be passed as a keyword argument.
super().__init__(src, **attributes)
@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.
"""
ts = TopologicalSorter()
for head, *tail in filter(None, lists):
ts.add(head) # Ensure that the first items are included.
for item in tail:
if head != item: # Avoid circular dependency to self.
ts.add(item, head)
head = item
try:
return list(ts.static_order())
except CycleError:
warnings.warn(
"Detected duplicate Media files in an opposite order: {}".format(
", ".join(repr(list_) for list_ in lists)
),
MediaOrderConflictWarning,
)
return list(dict.fromkeys(chain.from_iterable(filter(None, lists))))
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 += 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 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
[docs]
class TextInput(Input):
input_type = "text"
template_name = "django/forms/widgets/text.html"
class ColorInput(Input):
input_type = "color"
template_name = "django/forms/widgets/color.html"
class SearchInput(Input):
input_type = "search"
template_name = "django/forms/widgets/search.html"
class TelInput(Input):
input_type = "tel"
template_name = "django/forms/widgets/tel.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):
allow_multiple_selected = False
input_type = "file"
needs_multipart_form = True
template_name = "django/forms/widgets/file.html"
def __init__(self, attrs=None):
if (
attrs is not None
and not self.allow_multiple_selected
and attrs.get("multiple", False)
):
raise ValueError(
"%s doesn't support uploading multiple files."
% self.__class__.__qualname__
)
if self.allow_multiple_selected:
if attrs is None:
attrs = {"multiple": True}
else:
attrs.setdefault("multiple", True)
super().__init__(attrs)
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"
getter = files.get
if self.allow_multiple_selected:
try:
getter = files.getlist
except AttributeError:
pass
return getter(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"
checked = False
use_fieldset = False
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,
}
)
context["widget"]["attrs"].setdefault("disabled", False)
context["widget"]["attrs"]["checked"] = self.checked
return context
def value_from_datadict(self, data, files, name):
upload = super().value_from_datadict(data, files, name)
self.checked = self.clear_checkbox_name(name) in data
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
)
[docs]
class Textarea(Widget):
template_name = "django/forms/widgets/textarea.html"
[docs]
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]
)
# 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 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)
self.choices = 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]
@property
def choices(self):
return self._choices
@choices.setter
def choices(self, value):
self._choices = normalize_choices(value)
[docs]
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
[docs]
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 == ""
[docs]
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)
[docs]
class SelectMultiple(Select):
allow_multiple_selected = True
[docs]
def value_from_datadict(self, data, files, name):
try:
getter = data.getlist
except AttributeError:
getter = data.get
return getter(name)
[docs]
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 += 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)
except OverflowError:
return "0-0-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")
)