Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django_filters/fields.py: 47%
171 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 import namedtuple
2from datetime import datetime, time
4from django import forms
5from django.utils.dateparse import parse_datetime
6from django.utils.encoding import force_str
7from django.utils.translation import gettext_lazy as _
9from .conf import settings
10from .constants import EMPTY_VALUES
11from .utils import handle_timezone
12from .widgets import (
13 BaseCSVWidget,
14 CSVWidget,
15 DateRangeWidget,
16 LookupChoiceWidget,
17 RangeWidget,
18)
21class RangeField(forms.MultiValueField):
22 widget = RangeWidget
24 def __init__(self, fields=None, *args, **kwargs):
25 if fields is None:
26 fields = (forms.DecimalField(), forms.DecimalField())
27 super().__init__(fields, *args, **kwargs)
29 def compress(self, data_list):
30 if data_list:
31 return slice(*data_list)
32 return None
35class DateRangeField(RangeField):
36 widget = DateRangeWidget
38 def __init__(self, *args, **kwargs):
39 fields = (forms.DateField(), forms.DateField())
40 super().__init__(fields, *args, **kwargs)
42 def compress(self, data_list):
43 if data_list:
44 start_date, stop_date = data_list
45 if start_date:
46 start_date = handle_timezone(
47 datetime.combine(start_date, time.min), False
48 )
49 if stop_date:
50 stop_date = handle_timezone(
51 datetime.combine(stop_date, time.max), False
52 )
53 return slice(start_date, stop_date)
54 return None
57class DateTimeRangeField(RangeField):
58 widget = DateRangeWidget
60 def __init__(self, *args, **kwargs):
61 fields = (forms.DateTimeField(), forms.DateTimeField())
62 super().__init__(fields, *args, **kwargs)
65class IsoDateTimeRangeField(RangeField):
66 widget = DateRangeWidget
68 def __init__(self, *args, **kwargs):
69 fields = (IsoDateTimeField(), IsoDateTimeField())
70 super().__init__(fields, *args, **kwargs)
73class TimeRangeField(RangeField):
74 widget = DateRangeWidget
76 def __init__(self, *args, **kwargs):
77 fields = (forms.TimeField(), forms.TimeField())
78 super().__init__(fields, *args, **kwargs)
81class Lookup(namedtuple("Lookup", ("value", "lookup_expr"))):
82 def __new__(cls, value, lookup_expr):
83 if value in EMPTY_VALUES or lookup_expr in EMPTY_VALUES:
84 raise ValueError(
85 "Empty values ([], (), {}, '', None) are not "
86 "valid Lookup arguments. Return None instead."
87 )
89 return super().__new__(cls, value, lookup_expr)
92class LookupChoiceField(forms.MultiValueField):
93 default_error_messages = {
94 "lookup_required": _("Select a lookup."),
95 }
97 def __init__(self, field, lookup_choices, *args, **kwargs):
98 empty_label = kwargs.pop("empty_label", settings.EMPTY_CHOICE_LABEL)
99 fields = (field, ChoiceField(choices=lookup_choices, empty_label=empty_label))
100 widget = LookupChoiceWidget(widgets=[f.widget for f in fields])
101 kwargs["widget"] = widget
102 kwargs["help_text"] = field.help_text
103 super().__init__(fields, *args, **kwargs)
105 def compress(self, data_list):
106 if len(data_list) == 2:
107 value, lookup_expr = data_list
108 if value not in EMPTY_VALUES:
109 if lookup_expr not in EMPTY_VALUES:
110 return Lookup(value=value, lookup_expr=lookup_expr)
111 else:
112 raise forms.ValidationError(
113 self.error_messages["lookup_required"], code="lookup_required"
114 )
115 return None
118class IsoDateTimeField(forms.DateTimeField):
119 """
120 Supports 'iso-8601' date format too which is out the scope of
121 the ``datetime.strptime`` standard library
123 # ISO 8601: ``http://www.w3.org/TR/NOTE-datetime``
125 Based on Gist example by David Medina https://gist.github.com/copitux/5773821
126 """
128 ISO_8601 = "iso-8601"
129 input_formats = [ISO_8601]
131 def strptime(self, value, format):
132 value = force_str(value)
134 if format == self.ISO_8601:
135 parsed = parse_datetime(value)
136 if parsed is None: # Continue with other formats if doesn't match
137 raise ValueError
138 return handle_timezone(parsed)
139 return super().strptime(value, format)
142class BaseCSVField(forms.Field):
143 """
144 Base field for validating CSV types. Value validation is performed by
145 secondary base classes.
147 ex::
148 class IntegerCSVField(BaseCSVField, filters.IntegerField):
149 pass
151 """
153 base_widget_class = BaseCSVWidget
155 def __init__(self, *args, **kwargs):
156 widget = kwargs.get("widget") or self.widget
157 kwargs["widget"] = self._get_widget_class(widget)
159 super().__init__(*args, **kwargs)
161 def _get_widget_class(self, widget):
162 # passthrough, allows for override
163 if isinstance(widget, BaseCSVWidget) or (
164 isinstance(widget, type) and issubclass(widget, BaseCSVWidget)
165 ):
166 return widget
168 # complain since we are unable to reconstruct widget instances
169 assert isinstance(
170 widget, type
171 ), "'%s.widget' must be a widget class, not %s." % (
172 self.__class__.__name__,
173 repr(widget),
174 )
176 bases = (
177 self.base_widget_class,
178 widget,
179 )
180 return type(str("CSV%s" % widget.__name__), bases, {})
182 def clean(self, value):
183 if value in self.empty_values and self.required:
184 raise forms.ValidationError(
185 self.error_messages["required"], code="required"
186 )
188 if value is None:
189 return None
190 return [super(BaseCSVField, self).clean(v) for v in value]
193class BaseRangeField(BaseCSVField):
194 # Force use of text input, as range must always have two inputs. A date
195 # input would only allow a user to input one value and would always fail.
196 widget = CSVWidget
198 default_error_messages = {"invalid_values": _("Range query expects two values.")}
200 def clean(self, value):
201 value = super().clean(value)
203 assert value is None or isinstance(value, list)
205 if value and len(value) != 2:
206 raise forms.ValidationError(
207 self.error_messages["invalid_values"], code="invalid_values"
208 )
210 return value
213class ChoiceIterator:
214 # Emulates the behavior of ModelChoiceIterator, but instead wraps
215 # the field's _choices iterable.
217 def __init__(self, field, choices):
218 self.field = field
219 self.choices = choices
221 def __iter__(self):
222 if self.field.empty_label is not None:
223 yield ("", self.field.empty_label)
224 if self.field.null_label is not None:
225 yield (self.field.null_value, self.field.null_label)
226 yield from self.choices
228 def __len__(self):
229 add = 1 if self.field.empty_label is not None else 0
230 add += 1 if self.field.null_label is not None else 0
231 return len(self.choices) + add
234class ModelChoiceIterator(forms.models.ModelChoiceIterator):
235 # Extends the base ModelChoiceIterator to add in 'null' choice handling.
236 # This is a bit verbose since we have to insert the null choice after the
237 # empty choice, but before the remainder of the choices.
239 def __iter__(self):
240 iterable = super().__iter__()
242 if self.field.empty_label is not None:
243 yield next(iterable)
244 if self.field.null_label is not None:
245 yield (self.field.null_value, self.field.null_label)
246 yield from iterable
248 def __len__(self):
249 add = 1 if self.field.null_label is not None else 0
250 return super().__len__() + add
253class ChoiceIteratorMixin:
254 def __init__(self, *args, **kwargs):
255 self.null_label = kwargs.pop("null_label", settings.NULL_CHOICE_LABEL)
256 self.null_value = kwargs.pop("null_value", settings.NULL_CHOICE_VALUE)
258 super().__init__(*args, **kwargs)
260 def _get_choices(self):
261 return super()._get_choices()
263 def _set_choices(self, value):
264 super()._set_choices(value)
265 value = self.iterator(self, self._choices)
267 self._choices = self.widget.choices = value
269 choices = property(_get_choices, _set_choices)
272# Unlike their Model* counterparts, forms.ChoiceField and forms.MultipleChoiceField do not set empty_label
273class ChoiceField(ChoiceIteratorMixin, forms.ChoiceField):
274 iterator = ChoiceIterator
276 def __init__(self, *args, **kwargs):
277 self.empty_label = kwargs.pop("empty_label", settings.EMPTY_CHOICE_LABEL)
278 super().__init__(*args, **kwargs)
281class MultipleChoiceField(ChoiceIteratorMixin, forms.MultipleChoiceField):
282 iterator = ChoiceIterator
284 def __init__(self, *args, **kwargs):
285 self.empty_label = None
286 super().__init__(*args, **kwargs)
289class ModelChoiceField(ChoiceIteratorMixin, forms.ModelChoiceField):
290 iterator = ModelChoiceIterator
292 def to_python(self, value):
293 # bypass the queryset value check
294 if self.null_label is not None and value == self.null_value: 294 ↛ 295line 294 didn't jump to line 295, because the condition on line 294 was never true
295 return value
296 return super().to_python(value)
299class ModelMultipleChoiceField(ChoiceIteratorMixin, forms.ModelMultipleChoiceField):
300 iterator = ModelChoiceIterator
302 def _check_values(self, value):
303 null = self.null_label is not None and value and self.null_value in value
304 if null: # remove the null value and any potential duplicates
305 value = [v for v in value if v != self.null_value]
307 result = list(super()._check_values(value))
308 result += [self.null_value] if null else []
309 return result