1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
|
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))
|