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

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))

Powered by TurnKey Linux.