You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
271 lines
9.0 KiB
271 lines
9.0 KiB
4 months ago
|
from collections.abc import Iterable
|
||
|
from copy import deepcopy
|
||
|
from itertools import chain
|
||
|
from re import search, sub
|
||
|
|
||
|
from django import forms
|
||
|
from django.db.models.fields import BLANK_CHOICE_DASH
|
||
|
from django.forms.utils import flatatt
|
||
|
from django.utils.datastructures import MultiValueDict
|
||
|
from django.utils.encoding import force_str
|
||
|
from django.utils.http import urlencode
|
||
|
from django.utils.safestring import mark_safe
|
||
|
from django.utils.translation import gettext as _
|
||
|
|
||
|
|
||
|
class LinkWidget(forms.Widget):
|
||
|
def __init__(self, attrs=None, choices=()):
|
||
|
super().__init__(attrs)
|
||
|
|
||
|
self.choices = choices
|
||
|
|
||
|
def value_from_datadict(self, data, files, name):
|
||
|
value = super().value_from_datadict(data, files, name)
|
||
|
self.data = data
|
||
|
return value
|
||
|
|
||
|
def render(self, name, value, attrs=None, choices=(), renderer=None):
|
||
|
if not hasattr(self, "data"):
|
||
|
self.data = {}
|
||
|
if value is None:
|
||
|
value = ""
|
||
|
final_attrs = self.build_attrs(self.attrs, extra_attrs=attrs)
|
||
|
output = ["<ul%s>" % flatatt(final_attrs)]
|
||
|
options = self.render_options(choices, [value], name)
|
||
|
if options:
|
||
|
output.append(options)
|
||
|
output.append("</ul>")
|
||
|
return mark_safe("\n".join(output))
|
||
|
|
||
|
def render_options(self, choices, selected_choices, name):
|
||
|
selected_choices = set(force_str(v) for v in selected_choices)
|
||
|
output = []
|
||
|
for option_value, option_label in chain(self.choices, choices):
|
||
|
if isinstance(option_label, (list, tuple)):
|
||
|
for option in option_label:
|
||
|
output.append(self.render_option(name, selected_choices, *option))
|
||
|
else:
|
||
|
output.append(
|
||
|
self.render_option(
|
||
|
name, selected_choices, option_value, option_label
|
||
|
)
|
||
|
)
|
||
|
return "\n".join(output)
|
||
|
|
||
|
def render_option(self, name, selected_choices, option_value, option_label):
|
||
|
option_value = force_str(option_value)
|
||
|
if option_label == BLANK_CHOICE_DASH[0][1]:
|
||
|
option_label = _("All")
|
||
|
data = self.data.copy()
|
||
|
data[name] = option_value
|
||
|
selected = data == self.data or option_value in selected_choices
|
||
|
try:
|
||
|
url = data.urlencode()
|
||
|
except AttributeError:
|
||
|
url = urlencode(data)
|
||
|
return self.option_string() % {
|
||
|
"attrs": selected and ' class="selected"' or "",
|
||
|
"query_string": url,
|
||
|
"label": force_str(option_label),
|
||
|
}
|
||
|
|
||
|
def option_string(self):
|
||
|
return '<li><a%(attrs)s href="?%(query_string)s">%(label)s</a></li>'
|
||
|
|
||
|
|
||
|
class SuffixedMultiWidget(forms.MultiWidget):
|
||
|
"""
|
||
|
A MultiWidget that allows users to provide custom suffixes instead of indexes.
|
||
|
|
||
|
- Suffixes must be unique.
|
||
|
- There must be the same number of suffixes as fields.
|
||
|
"""
|
||
|
|
||
|
suffixes = []
|
||
|
|
||
|
def __init__(self, *args, **kwargs):
|
||
|
super().__init__(*args, **kwargs)
|
||
|
|
||
|
assert len(self.widgets) == len(self.suffixes)
|
||
|
assert len(self.suffixes) == len(set(self.suffixes))
|
||
|
|
||
|
def suffixed(self, name, suffix):
|
||
|
return "_".join([name, suffix]) if suffix else name
|
||
|
|
||
|
def get_context(self, name, value, attrs):
|
||
|
context = super().get_context(name, value, attrs)
|
||
|
for subcontext, suffix in zip(context["widget"]["subwidgets"], self.suffixes):
|
||
|
subcontext["name"] = self.suffixed(name, suffix)
|
||
|
|
||
|
return context
|
||
|
|
||
|
def value_from_datadict(self, data, files, name):
|
||
|
return [
|
||
|
widget.value_from_datadict(data, files, self.suffixed(name, suffix))
|
||
|
for widget, suffix in zip(self.widgets, self.suffixes)
|
||
|
]
|
||
|
|
||
|
def value_omitted_from_data(self, data, files, name):
|
||
|
return all(
|
||
|
widget.value_omitted_from_data(data, files, self.suffixed(name, suffix))
|
||
|
for widget, suffix in zip(self.widgets, self.suffixes)
|
||
|
)
|
||
|
|
||
|
def replace_name(self, output, index):
|
||
|
result = search(r'name="(?P<name>.*)_%d"' % index, output)
|
||
|
name = result.group("name")
|
||
|
name = self.suffixed(name, self.suffixes[index])
|
||
|
name = 'name="%s"' % name
|
||
|
|
||
|
return sub(r'name=".*_%d"' % index, name, output)
|
||
|
|
||
|
def decompress(self, value):
|
||
|
if value is None:
|
||
|
return [None, None]
|
||
|
return value
|
||
|
|
||
|
|
||
|
class RangeWidget(SuffixedMultiWidget):
|
||
|
template_name = "django_filters/widgets/multiwidget.html"
|
||
|
suffixes = ["min", "max"]
|
||
|
|
||
|
def __init__(self, attrs=None):
|
||
|
widgets = (forms.TextInput, forms.TextInput)
|
||
|
super().__init__(widgets, attrs)
|
||
|
|
||
|
def decompress(self, value):
|
||
|
if value:
|
||
|
return [value.start, value.stop]
|
||
|
return [None, None]
|
||
|
|
||
|
|
||
|
class DateRangeWidget(RangeWidget):
|
||
|
suffixes = ["after", "before"]
|
||
|
|
||
|
|
||
|
class LookupChoiceWidget(SuffixedMultiWidget):
|
||
|
suffixes = [None, "lookup"]
|
||
|
|
||
|
def decompress(self, value):
|
||
|
if value is None:
|
||
|
return [None, None]
|
||
|
return value
|
||
|
|
||
|
|
||
|
class BooleanWidget(forms.Select):
|
||
|
"""Convert true/false values into the internal Python True/False.
|
||
|
This can be used for AJAX queries that pass true/false from JavaScript's
|
||
|
internal types through.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, attrs=None):
|
||
|
choices = (("", _("Unknown")), ("true", _("Yes")), ("false", _("No")))
|
||
|
super().__init__(attrs, choices)
|
||
|
|
||
|
def render(self, name, value, attrs=None, renderer=None):
|
||
|
try:
|
||
|
value = {True: "true", False: "false", "1": "true", "0": "false"}[value]
|
||
|
except KeyError:
|
||
|
value = ""
|
||
|
return super().render(name, value, attrs, renderer=renderer)
|
||
|
|
||
|
def value_from_datadict(self, data, files, name):
|
||
|
value = data.get(name, None)
|
||
|
if isinstance(value, str):
|
||
|
value = value.lower()
|
||
|
|
||
|
return {
|
||
|
"1": True,
|
||
|
"0": False,
|
||
|
"true": True,
|
||
|
"false": False,
|
||
|
True: True,
|
||
|
False: False,
|
||
|
}.get(value, None)
|
||
|
|
||
|
|
||
|
class BaseCSVWidget(forms.Widget):
|
||
|
# Surrogate widget for rendering multiple values
|
||
|
surrogate = forms.TextInput
|
||
|
|
||
|
def __init__(self, *args, **kwargs):
|
||
|
super().__init__(*args, **kwargs)
|
||
|
|
||
|
if isinstance(self.surrogate, type):
|
||
|
self.surrogate = self.surrogate()
|
||
|
else:
|
||
|
self.surrogate = deepcopy(self.surrogate)
|
||
|
|
||
|
def _isiterable(self, value):
|
||
|
return isinstance(value, Iterable) and not isinstance(value, str)
|
||
|
|
||
|
def value_from_datadict(self, data, files, name):
|
||
|
value = super().value_from_datadict(data, files, name)
|
||
|
|
||
|
if value is not None:
|
||
|
if value == "": # empty value should parse as an empty list
|
||
|
return []
|
||
|
if isinstance(value, list):
|
||
|
# since django.forms.widgets.SelectMultiple tries to use getlist
|
||
|
# if available, we should return value if it's already an array
|
||
|
return value
|
||
|
return value.split(",")
|
||
|
return None
|
||
|
|
||
|
def render(self, name, value, attrs=None, renderer=None):
|
||
|
if not self._isiterable(value):
|
||
|
value = [value]
|
||
|
|
||
|
if len(value) <= 1:
|
||
|
# delegate to main widget (Select, etc...) if not multiple values
|
||
|
value = value[0] if value else ""
|
||
|
return super().render(name, value, attrs, renderer=renderer)
|
||
|
|
||
|
# if we have multiple values, we need to force render as a text input
|
||
|
# (otherwise, the additional values are lost)
|
||
|
value = [force_str(self.surrogate.format_value(v)) for v in value]
|
||
|
value = ",".join(list(value))
|
||
|
|
||
|
return self.surrogate.render(name, value, attrs, renderer=renderer)
|
||
|
|
||
|
|
||
|
class CSVWidget(BaseCSVWidget, forms.TextInput):
|
||
|
def __init__(self, *args, attrs=None, **kwargs):
|
||
|
super().__init__(*args, attrs, **kwargs)
|
||
|
|
||
|
if attrs is not None:
|
||
|
self.surrogate.attrs.update(attrs)
|
||
|
|
||
|
|
||
|
class QueryArrayWidget(BaseCSVWidget, forms.TextInput):
|
||
|
"""
|
||
|
Enables request query array notation that might be consumed by MultipleChoiceFilter
|
||
|
|
||
|
1. Values can be provided as csv string: ?foo=bar,baz
|
||
|
2. Values can be provided as query array: ?foo[]=bar&foo[]=baz
|
||
|
3. Values can be provided as query array: ?foo=bar&foo=baz
|
||
|
|
||
|
Note: Duplicate and empty values are skipped from results
|
||
|
"""
|
||
|
|
||
|
def value_from_datadict(self, data, files, name):
|
||
|
if not isinstance(data, MultiValueDict):
|
||
|
data = data.copy()
|
||
|
for key, value in data.items():
|
||
|
# treat value as csv string: ?foo=1,2
|
||
|
if isinstance(value, str):
|
||
|
data[key] = [x.strip() for x in value.rstrip(",").split(",") if x]
|
||
|
data = MultiValueDict(data)
|
||
|
|
||
|
values_list = data.getlist(name, data.getlist("%s[]" % name)) or []
|
||
|
|
||
|
# apparently its an array, so no need to process it's values as csv
|
||
|
# ?foo=1&foo=2 -> data.getlist(foo) -> foo = [1, 2]
|
||
|
# ?foo[]=1&foo[]=2 -> data.getlist(foo[]) -> foo = [1, 2]
|
||
|
if len(values_list) > 0:
|
||
|
ret = [x for x in values_list if x]
|
||
|
else:
|
||
|
ret = []
|
||
|
|
||
|
return list(set(ret))
|