1179 lines
38 KiB
Python
1179 lines
38 KiB
Python
|
"""
|
||
|
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")
|
||
|
)
|