Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django_filters/widgets.py: 33%
157 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1from collections.abc import Iterable
2from copy import deepcopy
3from itertools import chain
4from re import search, sub
6from django import forms
7from django.db.models.fields import BLANK_CHOICE_DASH
8from django.forms.utils import flatatt
9from django.utils.datastructures import MultiValueDict
10from django.utils.encoding import force_str
11from django.utils.http import urlencode
12from django.utils.safestring import mark_safe
13from django.utils.translation import gettext as _
16class LinkWidget(forms.Widget):
17 def __init__(self, attrs=None, choices=()):
18 super().__init__(attrs)
20 self.choices = choices
22 def value_from_datadict(self, data, files, name):
23 value = super().value_from_datadict(data, files, name)
24 self.data = data
25 return value
27 def render(self, name, value, attrs=None, choices=(), renderer=None):
28 if not hasattr(self, "data"):
29 self.data = {}
30 if value is None:
31 value = ""
32 final_attrs = self.build_attrs(self.attrs, extra_attrs=attrs)
33 output = ["<ul%s>" % flatatt(final_attrs)]
34 options = self.render_options(choices, [value], name)
35 if options:
36 output.append(options)
37 output.append("</ul>")
38 return mark_safe("\n".join(output))
40 def render_options(self, choices, selected_choices, name):
41 selected_choices = set(force_str(v) for v in selected_choices)
42 output = []
43 for option_value, option_label in chain(self.choices, choices):
44 if isinstance(option_label, (list, tuple)):
45 for option in option_label:
46 output.append(self.render_option(name, selected_choices, *option))
47 else:
48 output.append(
49 self.render_option(
50 name, selected_choices, option_value, option_label
51 )
52 )
53 return "\n".join(output)
55 def render_option(self, name, selected_choices, option_value, option_label):
56 option_value = force_str(option_value)
57 if option_label == BLANK_CHOICE_DASH[0][1]:
58 option_label = _("All")
59 data = self.data.copy()
60 data[name] = option_value
61 selected = data == self.data or option_value in selected_choices
62 try:
63 url = data.urlencode()
64 except AttributeError:
65 url = urlencode(data)
66 return self.option_string() % {
67 "attrs": selected and ' class="selected"' or "",
68 "query_string": url,
69 "label": force_str(option_label),
70 }
72 def option_string(self):
73 return '<li><a%(attrs)s href="?%(query_string)s">%(label)s</a></li>'
76class SuffixedMultiWidget(forms.MultiWidget):
77 """
78 A MultiWidget that allows users to provide custom suffixes instead of indexes.
80 - Suffixes must be unique.
81 - There must be the same number of suffixes as fields.
82 """
84 suffixes = []
86 def __init__(self, *args, **kwargs):
87 super().__init__(*args, **kwargs)
89 assert len(self.widgets) == len(self.suffixes)
90 assert len(self.suffixes) == len(set(self.suffixes))
92 def suffixed(self, name, suffix):
93 return "_".join([name, suffix]) if suffix else name
95 def get_context(self, name, value, attrs):
96 context = super().get_context(name, value, attrs)
97 for subcontext, suffix in zip(context["widget"]["subwidgets"], self.suffixes):
98 subcontext["name"] = self.suffixed(name, suffix)
100 return context
102 def value_from_datadict(self, data, files, name):
103 return [
104 widget.value_from_datadict(data, files, self.suffixed(name, suffix))
105 for widget, suffix in zip(self.widgets, self.suffixes)
106 ]
108 def value_omitted_from_data(self, data, files, name):
109 return all(
110 widget.value_omitted_from_data(data, files, self.suffixed(name, suffix))
111 for widget, suffix in zip(self.widgets, self.suffixes)
112 )
114 def replace_name(self, output, index):
115 result = search(r'name="(?P<name>.*)_%d"' % index, output)
116 name = result.group("name")
117 name = self.suffixed(name, self.suffixes[index])
118 name = 'name="%s"' % name
120 return sub(r'name=".*_%d"' % index, name, output)
122 def decompress(self, value):
123 if value is None:
124 return [None, None]
125 return value
128class RangeWidget(SuffixedMultiWidget):
129 template_name = "django_filters/widgets/multiwidget.html"
130 suffixes = ["min", "max"]
132 def __init__(self, attrs=None):
133 widgets = (forms.TextInput, forms.TextInput)
134 super().__init__(widgets, attrs)
136 def decompress(self, value):
137 if value:
138 return [value.start, value.stop]
139 return [None, None]
142class DateRangeWidget(RangeWidget):
143 suffixes = ["after", "before"]
146class LookupChoiceWidget(SuffixedMultiWidget):
147 suffixes = [None, "lookup"]
149 def decompress(self, value):
150 if value is None:
151 return [None, None]
152 return value
155class BooleanWidget(forms.Select):
156 """Convert true/false values into the internal Python True/False.
157 This can be used for AJAX queries that pass true/false from JavaScript's
158 internal types through.
159 """
161 def __init__(self, attrs=None):
162 choices = (("", _("Unknown")), ("true", _("Yes")), ("false", _("No")))
163 super().__init__(attrs, choices)
165 def render(self, name, value, attrs=None, renderer=None):
166 try:
167 value = {True: "true", False: "false", "1": "true", "0": "false"}[value]
168 except KeyError:
169 value = ""
170 return super().render(name, value, attrs, renderer=renderer)
172 def value_from_datadict(self, data, files, name):
173 value = data.get(name, None)
174 if isinstance(value, str): 174 ↛ 175line 174 didn't jump to line 175, because the condition on line 174 was never true
175 value = value.lower()
177 return {
178 "1": True,
179 "0": False,
180 "true": True,
181 "false": False,
182 True: True,
183 False: False,
184 }.get(value, None)
187class BaseCSVWidget(forms.Widget):
188 # Surrogate widget for rendering multiple values
189 surrogate = forms.TextInput
191 def __init__(self, *args, **kwargs):
192 super().__init__(*args, **kwargs)
194 if isinstance(self.surrogate, type):
195 self.surrogate = self.surrogate()
196 else:
197 self.surrogate = deepcopy(self.surrogate)
199 def _isiterable(self, value):
200 return isinstance(value, Iterable) and not isinstance(value, str)
202 def value_from_datadict(self, data, files, name):
203 value = super().value_from_datadict(data, files, name)
205 if value is not None:
206 if value == "": # empty value should parse as an empty list
207 return []
208 return value.split(",")
209 return None
211 def render(self, name, value, attrs=None, renderer=None):
212 if not self._isiterable(value):
213 value = [value]
215 if len(value) <= 1:
216 # delegate to main widget (Select, etc...) if not multiple values
217 value = value[0] if value else ""
218 return super().render(name, value, attrs, renderer=renderer)
220 # if we have multiple values, we need to force render as a text input
221 # (otherwise, the additional values are lost)
222 value = [force_str(self.surrogate.format_value(v)) for v in value]
223 value = ",".join(list(value))
225 return self.surrogate.render(name, value, attrs, renderer=renderer)
228class CSVWidget(BaseCSVWidget, forms.TextInput):
229 def __init__(self, *args, attrs=None, **kwargs):
230 super().__init__(*args, attrs, **kwargs)
232 if attrs is not None:
233 self.surrogate.attrs.update(attrs)
236class QueryArrayWidget(BaseCSVWidget, forms.TextInput):
237 """
238 Enables request query array notation that might be consumed by MultipleChoiceFilter
240 1. Values can be provided as csv string: ?foo=bar,baz
241 2. Values can be provided as query array: ?foo[]=bar&foo[]=baz
242 3. Values can be provided as query array: ?foo=bar&foo=baz
244 Note: Duplicate and empty values are skipped from results
245 """
247 def value_from_datadict(self, data, files, name):
248 if not isinstance(data, MultiValueDict):
249 for key, value in data.items():
250 # treat value as csv string: ?foo=1,2
251 if isinstance(value, str):
252 data[key] = [x.strip() for x in value.rstrip(",").split(",") if x]
253 data = MultiValueDict(data)
255 values_list = data.getlist(name, data.getlist("%s[]" % name)) or []
257 # apparently its an array, so no need to process it's values as csv
258 # ?foo=1&foo=2 -> data.getlist(foo) -> foo = [1, 2]
259 # ?foo[]=1&foo[]=2 -> data.getlist(foo[]) -> foo = [1, 2]
260 if len(values_list) > 0:
261 ret = [x for x in values_list if x]
262 else:
263 ret = []
265 return list(set(ret))