Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django_filters/filters.py: 51%
357 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 OrderedDict
2from datetime import timedelta
4from django import forms
5from django.core.validators import MaxValueValidator
6from django.db.models import Q
7from django.db.models.constants import LOOKUP_SEP
8from django.forms.utils import pretty_name
9from django.utils.itercompat import is_iterable
10from django.utils.timezone import now
11from django.utils.translation import gettext_lazy as _
13from .conf import settings
14from .constants import EMPTY_VALUES
15from .fields import (
16 BaseCSVField,
17 BaseRangeField,
18 ChoiceField,
19 DateRangeField,
20 DateTimeRangeField,
21 IsoDateTimeField,
22 IsoDateTimeRangeField,
23 LookupChoiceField,
24 ModelChoiceField,
25 ModelMultipleChoiceField,
26 MultipleChoiceField,
27 RangeField,
28 TimeRangeField,
29)
30from .utils import get_model_field, label_for_filter
32__all__ = [
33 "AllValuesFilter",
34 "AllValuesMultipleFilter",
35 "BaseCSVFilter",
36 "BaseInFilter",
37 "BaseRangeFilter",
38 "BooleanFilter",
39 "CharFilter",
40 "ChoiceFilter",
41 "DateFilter",
42 "DateFromToRangeFilter",
43 "DateRangeFilter",
44 "DateTimeFilter",
45 "DateTimeFromToRangeFilter",
46 "DurationFilter",
47 "Filter",
48 "IsoDateTimeFilter",
49 "IsoDateTimeFromToRangeFilter",
50 "LookupChoiceFilter",
51 "ModelChoiceFilter",
52 "ModelMultipleChoiceFilter",
53 "MultipleChoiceFilter",
54 "NumberFilter",
55 "NumericRangeFilter",
56 "OrderingFilter",
57 "RangeFilter",
58 "TimeFilter",
59 "TimeRangeFilter",
60 "TypedChoiceFilter",
61 "TypedMultipleChoiceFilter",
62 "UUIDFilter",
63]
66class Filter:
67 creation_counter = 0
68 field_class = forms.Field
70 def __init__(
71 self,
72 field_name=None,
73 lookup_expr=None,
74 *,
75 label=None,
76 method=None,
77 distinct=False,
78 exclude=False,
79 **kwargs
80 ):
81 if lookup_expr is None:
82 lookup_expr = settings.DEFAULT_LOOKUP_EXPR
83 self.field_name = field_name
84 self.lookup_expr = lookup_expr
85 self.label = label
86 self.method = method
87 self.distinct = distinct
88 self.exclude = exclude
90 self.extra = kwargs
91 self.extra.setdefault("required", False)
93 self.creation_counter = Filter.creation_counter
94 Filter.creation_counter += 1
96 def get_method(self, qs):
97 """Return filter method based on whether we're excluding
98 or simply filtering.
99 """
100 return qs.exclude if self.exclude else qs.filter
102 def method():
103 """
104 Filter method needs to be lazily resolved, as it may be dependent on
105 the 'parent' FilterSet.
106 """
108 def fget(self):
109 return self._method
111 def fset(self, value):
112 self._method = value
114 # clear existing FilterMethod
115 if isinstance(self.filter, FilterMethod): 115 ↛ 116line 115 didn't jump to line 116, because the condition on line 115 was never true
116 del self.filter
118 # override filter w/ FilterMethod.
119 if value is not None:
120 self.filter = FilterMethod(self)
122 return locals()
124 method = property(**method())
126 def label():
127 def fget(self):
128 if self._label is None and hasattr(self, "model"): 128 ↛ 132line 128 didn't jump to line 132, because the condition on line 128 was never false
129 self._label = label_for_filter(
130 self.model, self.field_name, self.lookup_expr, self.exclude
131 )
132 return self._label
134 def fset(self, value):
135 self._label = value
137 return locals()
139 label = property(**label())
141 @property
142 def field(self):
143 if not hasattr(self, "_field"): 143 ↛ 150line 143 didn't jump to line 150, because the condition on line 143 was never false
144 field_kwargs = self.extra.copy()
146 if settings.DISABLE_HELP_TEXT: 146 ↛ 147line 146 didn't jump to line 147, because the condition on line 146 was never true
147 field_kwargs.pop("help_text", None)
149 self._field = self.field_class(label=self.label, **field_kwargs)
150 return self._field
152 def filter(self, qs, value):
153 if value in EMPTY_VALUES:
154 return qs
155 if self.distinct: 155 ↛ 156line 155 didn't jump to line 156, because the condition on line 155 was never true
156 qs = qs.distinct()
157 lookup = "%s__%s" % (self.field_name, self.lookup_expr)
158 qs = self.get_method(qs)(**{lookup: value})
159 return qs
162class CharFilter(Filter):
163 field_class = forms.CharField
166class BooleanFilter(Filter):
167 field_class = forms.NullBooleanField
170class ChoiceFilter(Filter):
171 field_class = ChoiceField
173 def __init__(self, *args, **kwargs):
174 self.null_value = kwargs.get("null_value", settings.NULL_CHOICE_VALUE)
175 super().__init__(*args, **kwargs)
177 def filter(self, qs, value):
178 if value != self.null_value: 178 ↛ 181line 178 didn't jump to line 181, because the condition on line 178 was never false
179 return super().filter(qs, value)
181 qs = self.get_method(qs)(
182 **{"%s__%s" % (self.field_name, self.lookup_expr): None}
183 )
184 return qs.distinct() if self.distinct else qs
187class TypedChoiceFilter(Filter):
188 field_class = forms.TypedChoiceField
191class UUIDFilter(Filter):
192 field_class = forms.UUIDField
195class MultipleChoiceFilter(Filter):
196 """
197 This filter performs OR(by default) or AND(using conjoined=True) query
198 on the selected options.
200 Advanced usage
201 --------------
202 Depending on your application logic, when all or no choices are selected,
203 filtering may be a no-operation. In this case you may wish to avoid the
204 filtering overhead, particularly if using a `distinct` call.
206 You can override `get_filter_predicate` to use a custom filter.
207 By default it will use the filter's name for the key, and the value will
208 be the model object - or in case of passing in `to_field_name` the
209 value of that attribute on the model.
211 Set `always_filter` to `False` after instantiation to enable the default
212 `is_noop` test. You can override `is_noop` if you need a different test
213 for your application.
215 `distinct` defaults to `True` as to-many relationships will generally
216 require this.
217 """
219 field_class = MultipleChoiceField
221 always_filter = True
223 def __init__(self, *args, **kwargs):
224 kwargs.setdefault("distinct", True)
225 self.conjoined = kwargs.pop("conjoined", False)
226 self.null_value = kwargs.get("null_value", settings.NULL_CHOICE_VALUE)
227 super().__init__(*args, **kwargs)
229 def is_noop(self, qs, value):
230 """
231 Return `True` to short-circuit unnecessary and potentially slow
232 filtering.
233 """
234 if self.always_filter:
235 return False
237 # A reasonable default for being a noop...
238 if self.extra.get("required") and len(value) == len(self.field.choices):
239 return True
241 return False
243 def filter(self, qs, value):
244 if not value:
245 # Even though not a noop, no point filtering if empty.
246 return qs
248 if self.is_noop(qs, value):
249 return qs
251 if not self.conjoined:
252 q = Q()
253 for v in set(value):
254 if v == self.null_value:
255 v = None
256 predicate = self.get_filter_predicate(v)
257 if self.conjoined:
258 qs = self.get_method(qs)(**predicate)
259 else:
260 q |= Q(**predicate)
262 if not self.conjoined:
263 qs = self.get_method(qs)(q)
265 return qs.distinct() if self.distinct else qs
267 def get_filter_predicate(self, v):
268 name = self.field_name
269 if name and self.lookup_expr != settings.DEFAULT_LOOKUP_EXPR:
270 name = LOOKUP_SEP.join([name, self.lookup_expr])
271 try:
272 return {name: getattr(v, self.field.to_field_name)}
273 except (AttributeError, TypeError):
274 return {name: v}
277class TypedMultipleChoiceFilter(MultipleChoiceFilter):
278 field_class = forms.TypedMultipleChoiceField
281class DateFilter(Filter):
282 field_class = forms.DateField
285class DateTimeFilter(Filter):
286 field_class = forms.DateTimeField
289class IsoDateTimeFilter(DateTimeFilter):
290 """
291 Uses IsoDateTimeField to support filtering on ISO 8601 formatted datetimes.
293 For context see:
295 * https://code.djangoproject.com/ticket/23448
296 * https://github.com/encode/django-rest-framework/issues/1338
297 * https://github.com/carltongibson/django-filter/pull/264
298 """
300 field_class = IsoDateTimeField
303class TimeFilter(Filter):
304 field_class = forms.TimeField
307class DurationFilter(Filter):
308 field_class = forms.DurationField
311class QuerySetRequestMixin:
312 """
313 Add callable functionality to filters that support the ``queryset``
314 argument. If the ``queryset`` is callable, then it **must** accept the
315 ``request`` object as a single argument.
317 This is useful for filtering querysets by properties on the ``request``
318 object, such as the user.
320 Example::
322 def departments(request):
323 company = request.user.company
324 return company.department_set.all()
326 class EmployeeFilter(filters.FilterSet):
327 department = filters.ModelChoiceFilter(queryset=departments)
328 ...
330 The above example restricts the set of departments to those in the logged-in
331 user's associated company.
333 """
335 def __init__(self, *args, **kwargs):
336 self.queryset = kwargs.get("queryset")
337 super().__init__(*args, **kwargs)
339 def get_request(self):
340 try:
341 return self.parent.request
342 except AttributeError:
343 return None
345 def get_queryset(self, request):
346 queryset = self.queryset
348 if callable(queryset): 348 ↛ 349line 348 didn't jump to line 349, because the condition on line 348 was never true
349 return queryset(request)
350 return queryset
352 @property
353 def field(self):
354 request = self.get_request()
355 queryset = self.get_queryset(request)
357 if queryset is not None: 357 ↛ 360line 357 didn't jump to line 360, because the condition on line 357 was never false
358 self.extra["queryset"] = queryset
360 return super().field
363class ModelChoiceFilter(QuerySetRequestMixin, ChoiceFilter):
364 field_class = ModelChoiceField
366 def __init__(self, *args, **kwargs):
367 kwargs.setdefault("empty_label", settings.EMPTY_CHOICE_LABEL)
368 super().__init__(*args, **kwargs)
371class ModelMultipleChoiceFilter(QuerySetRequestMixin, MultipleChoiceFilter):
372 field_class = ModelMultipleChoiceField
375class NumberFilter(Filter):
376 field_class = forms.DecimalField
378 def get_max_validator(self):
379 """
380 Return a MaxValueValidator for the field, or None to disable.
381 """
382 return MaxValueValidator(1e50)
384 @property
385 def field(self):
386 if not hasattr(self, "_field"):
387 field = super().field
388 max_validator = self.get_max_validator()
389 if max_validator:
390 field.validators.append(max_validator)
392 self._field = field
393 return self._field
396class NumericRangeFilter(Filter):
397 field_class = RangeField
399 def filter(self, qs, value):
400 if value:
401 if value.start is not None and value.stop is not None:
402 value = (value.start, value.stop)
403 elif value.start is not None:
404 self.lookup_expr = "startswith"
405 value = value.start
406 elif value.stop is not None:
407 self.lookup_expr = "endswith"
408 value = value.stop
410 return super().filter(qs, value)
413class RangeFilter(Filter):
414 field_class = RangeField
416 def filter(self, qs, value):
417 if value:
418 if value.start is not None and value.stop is not None:
419 self.lookup_expr = "range"
420 value = (value.start, value.stop)
421 elif value.start is not None:
422 self.lookup_expr = "gte"
423 value = value.start
424 elif value.stop is not None:
425 self.lookup_expr = "lte"
426 value = value.stop
428 return super().filter(qs, value)
431def _truncate(dt):
432 return dt.date()
435class DateRangeFilter(ChoiceFilter):
436 choices = [
437 ("today", _("Today")),
438 ("yesterday", _("Yesterday")),
439 ("week", _("Past 7 days")),
440 ("month", _("This month")),
441 ("year", _("This year")),
442 ]
444 filters = { 444 ↛ exitline 444 didn't jump to the function exit
445 "today": lambda qs, name: qs.filter(
446 **{
447 "%s__year" % name: now().year,
448 "%s__month" % name: now().month,
449 "%s__day" % name: now().day,
450 }
451 ),
452 "yesterday": lambda qs, name: qs.filter(
453 **{
454 "%s__year" % name: (now() - timedelta(days=1)).year,
455 "%s__month" % name: (now() - timedelta(days=1)).month,
456 "%s__day" % name: (now() - timedelta(days=1)).day,
457 }
458 ),
459 "week": lambda qs, name: qs.filter(
460 **{
461 "%s__gte" % name: _truncate(now() - timedelta(days=7)),
462 "%s__lt" % name: _truncate(now() + timedelta(days=1)),
463 }
464 ),
465 "month": lambda qs, name: qs.filter(
466 **{"%s__year" % name: now().year, "%s__month" % name: now().month}
467 ),
468 "year": lambda qs, name: qs.filter(
469 **{
470 "%s__year" % name: now().year,
471 }
472 ),
473 }
475 def __init__(self, choices=None, filters=None, *args, **kwargs):
476 if choices is not None:
477 self.choices = choices
478 if filters is not None:
479 self.filters = filters
481 unique = set([x[0] for x in self.choices]) ^ set(self.filters)
482 assert not unique, (
483 "Keys must be present in both 'choices' and 'filters'. Missing keys: "
484 "'%s'" % ", ".join(sorted(unique))
485 )
487 # null choice not relevant
488 kwargs.setdefault("null_label", None)
489 super().__init__(choices=self.choices, *args, **kwargs)
491 def filter(self, qs, value):
492 if not value:
493 return qs
495 assert value in self.filters
497 qs = self.filters[value](qs, self.field_name)
498 return qs.distinct() if self.distinct else qs
501class DateFromToRangeFilter(RangeFilter):
502 field_class = DateRangeField
505class DateTimeFromToRangeFilter(RangeFilter):
506 field_class = DateTimeRangeField
509class IsoDateTimeFromToRangeFilter(RangeFilter):
510 field_class = IsoDateTimeRangeField
513class TimeRangeFilter(RangeFilter):
514 field_class = TimeRangeField
517class AllValuesFilter(ChoiceFilter):
518 @property
519 def field(self):
520 qs = self.model._default_manager.distinct()
521 qs = qs.order_by(self.field_name).values_list(self.field_name, flat=True)
522 self.extra["choices"] = [(o, o) for o in qs]
523 return super().field
526class AllValuesMultipleFilter(MultipleChoiceFilter):
527 @property
528 def field(self):
529 qs = self.model._default_manager.distinct()
530 qs = qs.order_by(self.field_name).values_list(self.field_name, flat=True)
531 self.extra["choices"] = [(o, o) for o in qs]
532 return super().field
535class BaseCSVFilter(Filter):
536 """
537 Base class for CSV type filters, such as IN and RANGE.
538 """
540 base_field_class = BaseCSVField
542 def __init__(self, *args, **kwargs):
543 kwargs.setdefault("help_text", _("Multiple values may be separated by commas."))
544 super().__init__(*args, **kwargs)
546 class ConcreteCSVField(self.base_field_class, self.field_class):
547 pass
549 ConcreteCSVField.__name__ = self._field_class_name(
550 self.field_class, self.lookup_expr
551 )
553 self.field_class = ConcreteCSVField
555 @classmethod
556 def _field_class_name(cls, field_class, lookup_expr):
557 """
558 Generate a suitable class name for the concrete field class. This is not
559 completely reliable, as not all field class names are of the format
560 <Type>Field.
562 ex::
564 BaseCSVFilter._field_class_name(DateTimeField, 'year__in')
566 returns 'DateTimeYearInField'
568 """
569 # DateTimeField => DateTime
570 type_name = field_class.__name__
571 if type_name.endswith("Field"):
572 type_name = type_name[:-5]
574 # year__in => YearIn
575 parts = lookup_expr.split(LOOKUP_SEP)
576 expression_name = "".join(p.capitalize() for p in parts)
578 # DateTimeYearInField
579 return str("%s%sField" % (type_name, expression_name))
582class BaseInFilter(BaseCSVFilter):
583 def __init__(self, *args, **kwargs):
584 kwargs.setdefault("lookup_expr", "in")
585 super().__init__(*args, **kwargs)
588class BaseRangeFilter(BaseCSVFilter):
589 base_field_class = BaseRangeField
591 def __init__(self, *args, **kwargs):
592 kwargs.setdefault("lookup_expr", "range")
593 super().__init__(*args, **kwargs)
596class LookupChoiceFilter(Filter):
597 """
598 A combined filter that allows users to select the lookup expression from a dropdown.
600 * ``lookup_choices`` is an optional argument that accepts multiple input
601 formats, and is ultimately normlized as the choices used in the lookup
602 dropdown. See ``.get_lookup_choices()`` for more information.
604 * ``field_class`` is an optional argument that allows you to set the inner
605 form field class used to validate the value. Default: ``forms.CharField``
607 ex::
609 price = django_filters.LookupChoiceFilter(
610 field_class=forms.DecimalField,
611 lookup_choices=[
612 ('exact', 'Equals'),
613 ('gt', 'Greater than'),
614 ('lt', 'Less than'),
615 ]
616 )
618 """
620 field_class = forms.CharField
621 outer_class = LookupChoiceField
623 def __init__(
624 self, field_name=None, lookup_choices=None, field_class=None, **kwargs
625 ):
626 self.empty_label = kwargs.pop("empty_label", settings.EMPTY_CHOICE_LABEL)
628 super(LookupChoiceFilter, self).__init__(field_name=field_name, **kwargs)
630 self.lookup_choices = lookup_choices
631 if field_class is not None:
632 self.field_class = field_class
634 @classmethod
635 def normalize_lookup(cls, lookup):
636 """
637 Normalize the lookup into a tuple of ``(lookup expression, display value)``
639 If the ``lookup`` is already a tuple, the tuple is not altered.
640 If the ``lookup`` is a string, a tuple is returned with the lookup
641 expression used as the basis for the display value.
643 ex::
645 >>> LookupChoiceFilter.normalize_lookup(('exact', 'Equals'))
646 ('exact', 'Equals')
648 >>> LookupChoiceFilter.normalize_lookup('has_key')
649 ('has_key', 'Has key')
651 """
652 if isinstance(lookup, str):
653 return (lookup, pretty_name(lookup))
654 return (lookup[0], lookup[1])
656 def get_lookup_choices(self):
657 """
658 Get the lookup choices in a format suitable for ``django.forms.ChoiceField``.
659 If the filter is initialized with ``lookup_choices``, this value is normalized
660 and passed to the underlying ``LookupChoiceField``. If no choices are provided,
661 they are generated from the corresponding model field's registered lookups.
662 """
663 lookups = self.lookup_choices
664 if lookups is None:
665 field = get_model_field(self.model, self.field_name)
666 lookups = field.get_lookups()
668 return [self.normalize_lookup(lookup) for lookup in lookups]
670 @property
671 def field(self):
672 if not hasattr(self, "_field"):
673 inner_field = super().field
674 lookups = self.get_lookup_choices()
676 self._field = self.outer_class(
677 inner_field,
678 lookups,
679 label=self.label,
680 empty_label=self.empty_label,
681 required=self.extra["required"],
682 )
684 return self._field
686 def filter(self, qs, lookup):
687 if not lookup:
688 return super().filter(qs, None)
690 self.lookup_expr = lookup.lookup_expr
691 return super().filter(qs, lookup.value)
694class OrderingFilter(BaseCSVFilter, ChoiceFilter):
695 """
696 Enable queryset ordering. As an extension of ``ChoiceFilter`` it accepts
697 two additional arguments that are used to build the ordering choices.
699 * ``fields`` is a mapping of {model field name: parameter name}. The
700 parameter names are exposed in the choices and mask/alias the field
701 names used in the ``order_by()`` call. Similar to field ``choices``,
702 ``fields`` accepts the 'list of two-tuples' syntax that retains order.
703 ``fields`` may also just be an iterable of strings. In this case, the
704 field names simply double as the exposed parameter names.
706 * ``field_labels`` is an optional argument that allows you to customize
707 the display label for the corresponding parameter. It accepts a mapping
708 of {field name: human readable label}. Keep in mind that the key is the
709 field name, and not the exposed parameter name.
711 Additionally, you can just provide your own ``choices`` if you require
712 explicit control over the exposed options. For example, when you might
713 want to disable descending sort options.
715 This filter is also CSV-based, and accepts multiple ordering params. The
716 default select widget does not enable the use of this, but it is useful
717 for APIs.
719 """
721 descending_fmt = _("%s (descending)")
723 def __init__(self, *args, **kwargs):
724 """
725 ``fields`` may be either a mapping or an iterable.
726 ``field_labels`` must be a map of field names to display labels
727 """
728 fields = kwargs.pop("fields", {})
729 fields = self.normalize_fields(fields)
730 field_labels = kwargs.pop("field_labels", {})
732 self.param_map = {v: k for k, v in fields.items()}
734 if "choices" not in kwargs:
735 kwargs["choices"] = self.build_choices(fields, field_labels)
737 kwargs.setdefault("label", _("Ordering"))
738 kwargs.setdefault("help_text", "")
739 kwargs.setdefault("null_label", None)
740 super().__init__(*args, **kwargs)
742 def get_ordering_value(self, param):
743 descending = param.startswith("-")
744 param = param[1:] if descending else param
745 field_name = self.param_map.get(param, param)
747 return "-%s" % field_name if descending else field_name
749 def filter(self, qs, value):
750 if value in EMPTY_VALUES:
751 return qs
753 ordering = [self.get_ordering_value(param) for param in value]
754 return qs.order_by(*ordering)
756 @classmethod
757 def normalize_fields(cls, fields):
758 """
759 Normalize the fields into an ordered map of {field name: param name}
760 """
761 # fields is a mapping, copy into new OrderedDict
762 if isinstance(fields, dict):
763 return OrderedDict(fields)
765 # convert iterable of values => iterable of pairs (field name, param name)
766 assert is_iterable(
767 fields
768 ), "'fields' must be an iterable (e.g., a list, tuple, or mapping)."
770 # fields is an iterable of field names
771 assert all(
772 isinstance(field, str)
773 or is_iterable(field)
774 and len(field) == 2 # may need to be wrapped in parens
775 for field in fields
776 ), "'fields' must contain strings or (field name, param name) pairs."
778 return OrderedDict([(f, f) if isinstance(f, str) else f for f in fields])
780 def build_choices(self, fields, labels):
781 ascending = [
782 (param, labels.get(field, _(pretty_name(param))))
783 for field, param in fields.items()
784 ]
785 descending = [
786 ("-%s" % param, labels.get("-%s" % param, self.descending_fmt % label))
787 for param, label in ascending
788 ]
790 # interleave the ascending and descending choices
791 return [val for pair in zip(ascending, descending) for val in pair]
794class FilterMethod:
795 """
796 This helper is used to override Filter.filter() when a 'method' argument
797 is passed. It proxies the call to the actual method on the filter's parent.
798 """
800 def __init__(self, filter_instance):
801 self.f = filter_instance
803 def __call__(self, qs, value):
804 if value in EMPTY_VALUES: 804 ↛ 807line 804 didn't jump to line 807, because the condition on line 804 was never false
805 return qs
807 return self.method(qs, self.f.field_name, value)
809 @property
810 def method(self):
811 """
812 Resolve the method on the parent filterset.
813 """
814 instance = self.f
816 # noop if 'method' is a function
817 if callable(instance.method):
818 return instance.method
820 # otherwise, method is the name of a method on the parent FilterSet.
821 assert hasattr(
822 instance, "parent"
823 ), "Filter '%s' must have a parent FilterSet to find '.%s()'" % (
824 instance.field_name,
825 instance.method,
826 )
828 parent = instance.parent
829 method = getattr(parent, instance.method, None)
831 assert callable(
832 method
833 ), "Expected parent FilterSet '%s.%s' to have a '.%s()' method." % (
834 parent.__class__.__module__,
835 parent.__class__.__name__,
836 instance.method,
837 )
839 return method