Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django_filters/filterset.py: 84%
186 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
1import copy
2from collections import OrderedDict
4from django import forms
5from django.db import models
6from django.db.models.constants import LOOKUP_SEP
7from django.db.models.fields.related import ManyToManyRel, ManyToOneRel, OneToOneRel
9from .conf import settings
10from .constants import ALL_FIELDS
11from .filters import (
12 BaseInFilter,
13 BaseRangeFilter,
14 BooleanFilter,
15 CharFilter,
16 ChoiceFilter,
17 DateFilter,
18 DateTimeFilter,
19 DurationFilter,
20 Filter,
21 ModelChoiceFilter,
22 ModelMultipleChoiceFilter,
23 NumberFilter,
24 TimeFilter,
25 UUIDFilter,
26)
27from .utils import get_all_model_fields, get_model_field, resolve_field, try_dbfield
30def remote_queryset(field):
31 """
32 Get the queryset for the other side of a relationship. This works
33 for both `RelatedField`s and `ForeignObjectRel`s.
34 """
35 model = field.related_model
37 # Reverse relationships do not have choice limits
38 if not hasattr(field, "get_limit_choices_to"):
39 return model._default_manager.all()
41 limit_choices_to = field.get_limit_choices_to()
42 return model._default_manager.complex_filter(limit_choices_to)
45class FilterSetOptions:
46 def __init__(self, options=None):
47 self.model = getattr(options, "model", None)
48 self.fields = getattr(options, "fields", None)
49 self.exclude = getattr(options, "exclude", None)
51 self.filter_overrides = getattr(options, "filter_overrides", {})
53 self.form = getattr(options, "form", forms.Form)
56class FilterSetMetaclass(type):
57 def __new__(cls, name, bases, attrs):
58 attrs["declared_filters"] = cls.get_declared_filters(bases, attrs)
60 new_class = super().__new__(cls, name, bases, attrs)
61 new_class._meta = FilterSetOptions(getattr(new_class, "Meta", None))
62 new_class.base_filters = new_class.get_filters()
64 return new_class
66 @classmethod
67 def get_declared_filters(cls, bases, attrs):
68 filters = [
69 (filter_name, attrs.pop(filter_name))
70 for filter_name, obj in list(attrs.items())
71 if isinstance(obj, Filter)
72 ]
74 # Default the `filter.field_name` to the attribute name on the filterset
75 for filter_name, f in filters:
76 if getattr(f, "field_name", None) is None:
77 f.field_name = filter_name
79 filters.sort(key=lambda x: x[1].creation_counter)
81 # Ensures a base class field doesn't override cls attrs, and maintains
82 # field precedence when inheriting multiple parents. e.g. if there is a
83 # class C(A, B), and A and B both define 'field', use 'field' from A.
84 known = set(attrs)
86 def visit(name):
87 known.add(name)
88 return name
90 base_filters = [
91 (visit(name), f)
92 for base in bases
93 if hasattr(base, "declared_filters")
94 for name, f in base.declared_filters.items()
95 if name not in known
96 ]
98 return OrderedDict(base_filters + filters)
101FILTER_FOR_DBFIELD_DEFAULTS = {
102 models.AutoField: {"filter_class": NumberFilter},
103 models.CharField: {"filter_class": CharFilter},
104 models.TextField: {"filter_class": CharFilter},
105 models.BooleanField: {"filter_class": BooleanFilter},
106 models.DateField: {"filter_class": DateFilter},
107 models.DateTimeField: {"filter_class": DateTimeFilter},
108 models.TimeField: {"filter_class": TimeFilter},
109 models.DurationField: {"filter_class": DurationFilter},
110 models.DecimalField: {"filter_class": NumberFilter},
111 models.SmallIntegerField: {"filter_class": NumberFilter},
112 models.IntegerField: {"filter_class": NumberFilter},
113 models.PositiveIntegerField: {"filter_class": NumberFilter},
114 models.PositiveSmallIntegerField: {"filter_class": NumberFilter},
115 models.FloatField: {"filter_class": NumberFilter},
116 models.NullBooleanField: {"filter_class": BooleanFilter},
117 models.SlugField: {"filter_class": CharFilter},
118 models.EmailField: {"filter_class": CharFilter},
119 models.FilePathField: {"filter_class": CharFilter},
120 models.URLField: {"filter_class": CharFilter},
121 models.GenericIPAddressField: {"filter_class": CharFilter},
122 models.CommaSeparatedIntegerField: {"filter_class": CharFilter},
123 models.UUIDField: {"filter_class": UUIDFilter},
124 # Forward relationships
125 models.OneToOneField: {
126 "filter_class": ModelChoiceFilter,
127 "extra": lambda f: {
128 "queryset": remote_queryset(f),
129 "to_field_name": f.remote_field.field_name,
130 "null_label": settings.NULL_CHOICE_LABEL if f.null else None,
131 },
132 },
133 models.ForeignKey: {
134 "filter_class": ModelChoiceFilter,
135 "extra": lambda f: {
136 "queryset": remote_queryset(f),
137 "to_field_name": f.remote_field.field_name,
138 "null_label": settings.NULL_CHOICE_LABEL if f.null else None,
139 },
140 },
141 models.ManyToManyField: {
142 "filter_class": ModelMultipleChoiceFilter,
143 "extra": lambda f: {
144 "queryset": remote_queryset(f),
145 },
146 },
147 # Reverse relationships
148 OneToOneRel: {
149 "filter_class": ModelChoiceFilter,
150 "extra": lambda f: {
151 "queryset": remote_queryset(f),
152 "null_label": settings.NULL_CHOICE_LABEL if f.null else None,
153 },
154 },
155 ManyToOneRel: {
156 "filter_class": ModelMultipleChoiceFilter,
157 "extra": lambda f: {
158 "queryset": remote_queryset(f),
159 },
160 },
161 ManyToManyRel: {
162 "filter_class": ModelMultipleChoiceFilter,
163 "extra": lambda f: {
164 "queryset": remote_queryset(f),
165 },
166 },
167}
170class BaseFilterSet:
171 FILTER_DEFAULTS = FILTER_FOR_DBFIELD_DEFAULTS
173 def __init__(self, data=None, queryset=None, *, request=None, prefix=None):
174 if queryset is None: 174 ↛ 175line 174 didn't jump to line 175, because the condition on line 174 was never true
175 queryset = self._meta.model._default_manager.all()
176 model = queryset.model
178 self.is_bound = data is not None
179 self.data = data or {}
180 self.queryset = queryset
181 self.request = request
182 self.form_prefix = prefix
184 self.filters = copy.deepcopy(self.base_filters)
186 # propagate the model and filterset to the filters
187 for filter_ in self.filters.values():
188 filter_.model = model
189 filter_.parent = self
191 def is_valid(self):
192 """
193 Return True if the underlying form has no errors, or False otherwise.
194 """
195 return self.is_bound and self.form.is_valid()
197 @property
198 def errors(self):
199 """
200 Return an ErrorDict for the data provided for the underlying form.
201 """
202 return self.form.errors
204 def filter_queryset(self, queryset):
205 """
206 Filter the queryset with the underlying form's `cleaned_data`. You must
207 call `is_valid()` or `errors` before calling this method.
209 This method should be overridden if additional filtering needs to be
210 applied to the queryset before it is cached.
211 """
212 for name, value in self.form.cleaned_data.items():
213 queryset = self.filters[name].filter(queryset, value)
214 assert isinstance(
215 queryset, models.QuerySet
216 ), "Expected '%s.%s' to return a QuerySet, but got a %s instead." % (
217 type(self).__name__,
218 name,
219 type(queryset).__name__,
220 )
221 return queryset
223 @property
224 def qs(self):
225 if not hasattr(self, "_qs"): 225 ↛ 232line 225 didn't jump to line 232, because the condition on line 225 was never false
226 qs = self.queryset.all()
227 if self.is_bound: 227 ↛ 231line 227 didn't jump to line 231, because the condition on line 227 was never false
228 # ensure form validation before filtering
229 self.errors
230 qs = self.filter_queryset(qs)
231 self._qs = qs
232 return self._qs
234 def get_form_class(self):
235 """
236 Returns a django Form suitable of validating the filterset data.
238 This method should be overridden if the form class needs to be
239 customized relative to the filterset instance.
240 """
241 fields = OrderedDict(
242 [(name, filter_.field) for name, filter_ in self.filters.items()]
243 )
245 return type(str("%sForm" % self.__class__.__name__), (self._meta.form,), fields)
247 @property
248 def form(self):
249 if not hasattr(self, "_form"):
250 Form = self.get_form_class()
251 if self.is_bound: 251 ↛ 254line 251 didn't jump to line 254, because the condition on line 251 was never false
252 self._form = Form(self.data, prefix=self.form_prefix)
253 else:
254 self._form = Form(prefix=self.form_prefix)
255 return self._form
257 @classmethod
258 def get_fields(cls):
259 """
260 Resolve the 'fields' argument that should be used for generating filters on the
261 filterset. This is 'Meta.fields' sans the fields in 'Meta.exclude'.
262 """
263 model = cls._meta.model
264 fields = cls._meta.fields
265 exclude = cls._meta.exclude
267 assert not (fields is None and exclude is None), (
268 "Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude' "
269 "has been deprecated since 0.15.0 and is now disallowed. Add an explicit "
270 "'Meta.fields' or 'Meta.exclude' to the %s class." % cls.__name__
271 )
273 # Setting exclude with no fields implies all other fields.
274 if exclude is not None and fields is None: 274 ↛ 275line 274 didn't jump to line 275, because the condition on line 274 was never true
275 fields = ALL_FIELDS
277 # Resolve ALL_FIELDS into all fields for the filterset's model.
278 if fields == ALL_FIELDS: 278 ↛ 279line 278 didn't jump to line 279, because the condition on line 278 was never true
279 fields = get_all_model_fields(model)
281 # Remove excluded fields
282 exclude = exclude or []
283 if not isinstance(fields, dict):
284 fields = [
285 (f, [settings.DEFAULT_LOOKUP_EXPR]) for f in fields if f not in exclude
286 ]
287 else:
288 fields = [(f, lookups) for f, lookups in fields.items() if f not in exclude]
290 return OrderedDict(fields)
292 @classmethod
293 def get_filter_name(cls, field_name, lookup_expr):
294 """
295 Combine a field name and lookup expression into a usable filter name.
296 Exact lookups are the implicit default, so "exact" is stripped from the
297 end of the filter name.
298 """
299 filter_name = LOOKUP_SEP.join([field_name, lookup_expr])
301 # This also works with transformed exact lookups, such as 'date__exact'
302 _default_expr = LOOKUP_SEP + settings.DEFAULT_LOOKUP_EXPR
303 if filter_name.endswith(_default_expr):
304 filter_name = filter_name[: -len(_default_expr)]
306 return filter_name
308 @classmethod
309 def get_filters(cls):
310 """
311 Get all filters for the filterset. This is the combination of declared and
312 generated filters.
313 """
315 # No model specified - skip filter generation
316 if not cls._meta.model:
317 return cls.declared_filters.copy()
319 # Determine the filters that should be included on the filterset.
320 filters = OrderedDict()
321 fields = cls.get_fields()
322 undefined = []
324 for field_name, lookups in fields.items():
325 field = get_model_field(cls._meta.model, field_name)
327 # warn if the field doesn't exist.
328 if field is None: 328 ↛ 329line 328 didn't jump to line 329, because the condition on line 328 was never true
329 undefined.append(field_name)
331 for lookup_expr in lookups:
332 filter_name = cls.get_filter_name(field_name, lookup_expr)
334 # If the filter is explicitly declared on the class, skip generation
335 if filter_name in cls.declared_filters:
336 filters[filter_name] = cls.declared_filters[filter_name]
337 continue
339 if field is not None: 339 ↛ 331line 339 didn't jump to line 331, because the condition on line 339 was never false
340 filters[filter_name] = cls.filter_for_field(
341 field, field_name, lookup_expr
342 )
344 # Allow Meta.fields to contain declared filters *only* when a list/tuple
345 if isinstance(cls._meta.fields, (list, tuple)):
346 undefined = [f for f in undefined if f not in cls.declared_filters]
348 if undefined: 348 ↛ 349line 348 didn't jump to line 349, because the condition on line 348 was never true
349 raise TypeError(
350 "'Meta.fields' must not contain non-model field names: %s"
351 % ", ".join(undefined)
352 )
354 # Add in declared filters. This is necessary since we don't enforce adding
355 # declared filters to the 'Meta.fields' option
356 filters.update(cls.declared_filters)
357 return filters
359 @classmethod
360 def filter_for_field(cls, field, field_name, lookup_expr=None):
361 if lookup_expr is None: 361 ↛ 362line 361 didn't jump to line 362, because the condition on line 361 was never true
362 lookup_expr = settings.DEFAULT_LOOKUP_EXPR
363 field, lookup_type = resolve_field(field, lookup_expr)
365 default = {
366 "field_name": field_name,
367 "lookup_expr": lookup_expr,
368 }
370 filter_class, params = cls.filter_for_lookup(field, lookup_type)
371 default.update(params)
373 assert filter_class is not None, (
374 "%s resolved field '%s' with '%s' lookup to an unrecognized field "
375 "type %s. Try adding an override to 'Meta.filter_overrides'. See: "
376 "https://django-filter.readthedocs.io/en/main/ref/filterset.html"
377 "#customise-filter-generation-with-filter-overrides"
378 ) % (cls.__name__, field_name, lookup_expr, field.__class__.__name__)
380 return filter_class(**default)
382 @classmethod
383 def filter_for_lookup(cls, field, lookup_type):
384 DEFAULTS = dict(cls.FILTER_DEFAULTS)
385 if hasattr(cls, "_meta"): 385 ↛ 388line 385 didn't jump to line 388, because the condition on line 385 was never false
386 DEFAULTS.update(cls._meta.filter_overrides)
388 data = try_dbfield(DEFAULTS.get, field.__class__) or {}
389 filter_class = data.get("filter_class")
390 params = data.get("extra", lambda field: {})(field)
392 # if there is no filter class, exit early
393 if not filter_class: 393 ↛ 394line 393 didn't jump to line 394, because the condition on line 393 was never true
394 return None, {}
396 # perform lookup specific checks
397 if lookup_type == "exact" and getattr(field, "choices", None):
398 return ChoiceFilter, {"choices": field.choices}
400 if lookup_type == "isnull":
401 data = try_dbfield(DEFAULTS.get, models.BooleanField)
403 filter_class = data.get("filter_class")
404 params = data.get("extra", lambda field: {})(field)
405 return filter_class, params
407 if lookup_type == "in": 407 ↛ 409line 407 didn't jump to line 409, because the condition on line 407 was never true
409 class ConcreteInFilter(BaseInFilter, filter_class):
410 pass
412 ConcreteInFilter.__name__ = cls._csv_filter_class_name(
413 filter_class, lookup_type
414 )
416 return ConcreteInFilter, params
418 if lookup_type == "range": 418 ↛ 420line 418 didn't jump to line 420, because the condition on line 418 was never true
420 class ConcreteRangeFilter(BaseRangeFilter, filter_class):
421 pass
423 ConcreteRangeFilter.__name__ = cls._csv_filter_class_name(
424 filter_class, lookup_type
425 )
427 return ConcreteRangeFilter, params
429 return filter_class, params
431 @classmethod
432 def _csv_filter_class_name(cls, filter_class, lookup_type):
433 """
434 Generate a suitable class name for a concrete filter class. This is not
435 completely reliable, as not all filter class names are of the format
436 <Type>Filter.
438 ex::
440 FilterSet._csv_filter_class_name(DateTimeFilter, 'in')
442 returns 'DateTimeInFilter'
444 """
445 # DateTimeFilter => DateTime
446 type_name = filter_class.__name__
447 if type_name.endswith("Filter"):
448 type_name = type_name[:-6]
450 # in => In
451 lookup_name = lookup_type.capitalize()
453 # DateTimeInFilter
454 return str("%s%sFilter" % (type_name, lookup_name))
457class FilterSet(BaseFilterSet, metaclass=FilterSetMetaclass):
458 pass
461def filterset_factory(model, fields=ALL_FIELDS):
462 meta = type(str("Meta"), (object,), {"model": model, "fields": fields})
463 filterset = type(
464 str("%sFilterSet" % model._meta.object_name), (FilterSet,), {"Meta": meta}
465 )
466 return filterset