Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/contrib/admin/filters.py: 28%
243 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
1"""
2This encapsulates the logic for displaying filters in the Django admin.
3Filters are specified in models with the "list_filter" option.
5Each filter subclass knows how to display a filter for a field that passes a
6certain test -- e.g. being a DateField or ForeignKey.
7"""
8import datetime
10from django.contrib.admin.options import IncorrectLookupParameters
11from django.contrib.admin.utils import (
12 get_model_from_relation,
13 prepare_lookup_value,
14 reverse_field_path,
15)
16from django.core.exceptions import ImproperlyConfigured, ValidationError
17from django.db import models
18from django.utils import timezone
19from django.utils.translation import gettext_lazy as _
22class ListFilter:
23 title = None # Human-readable title to appear in the right sidebar.
24 template = "admin/filter.html"
26 def __init__(self, request, params, model, model_admin):
27 # This dictionary will eventually contain the request's query string
28 # parameters actually used by this filter.
29 self.used_parameters = {}
30 if self.title is None:
31 raise ImproperlyConfigured(
32 "The list filter '%s' does not specify a 'title'."
33 % self.__class__.__name__
34 )
36 def has_output(self):
37 """
38 Return True if some choices would be output for this filter.
39 """
40 raise NotImplementedError(
41 "subclasses of ListFilter must provide a has_output() method"
42 )
44 def choices(self, changelist):
45 """
46 Return choices ready to be output in the template.
48 `changelist` is the ChangeList to be displayed.
49 """
50 raise NotImplementedError(
51 "subclasses of ListFilter must provide a choices() method"
52 )
54 def queryset(self, request, queryset):
55 """
56 Return the filtered queryset.
57 """
58 raise NotImplementedError(
59 "subclasses of ListFilter must provide a queryset() method"
60 )
62 def expected_parameters(self):
63 """
64 Return the list of parameter names that are expected from the
65 request's query string and that will be used by this filter.
66 """
67 raise NotImplementedError(
68 "subclasses of ListFilter must provide an expected_parameters() method"
69 )
72class SimpleListFilter(ListFilter):
73 # The parameter that should be used in the query string for that filter.
74 parameter_name = None
76 def __init__(self, request, params, model, model_admin):
77 super().__init__(request, params, model, model_admin)
78 if self.parameter_name is None:
79 raise ImproperlyConfigured(
80 "The list filter '%s' does not specify a 'parameter_name'."
81 % self.__class__.__name__
82 )
83 if self.parameter_name in params:
84 value = params.pop(self.parameter_name)
85 self.used_parameters[self.parameter_name] = value
86 lookup_choices = self.lookups(request, model_admin)
87 if lookup_choices is None:
88 lookup_choices = ()
89 self.lookup_choices = list(lookup_choices)
91 def has_output(self):
92 return len(self.lookup_choices) > 0
94 def value(self):
95 """
96 Return the value (in string format) provided in the request's
97 query string for this filter, if any, or None if the value wasn't
98 provided.
99 """
100 return self.used_parameters.get(self.parameter_name)
102 def lookups(self, request, model_admin):
103 """
104 Must be overridden to return a list of tuples (value, verbose value)
105 """
106 raise NotImplementedError(
107 "The SimpleListFilter.lookups() method must be overridden to "
108 "return a list of tuples (value, verbose value)."
109 )
111 def expected_parameters(self):
112 return [self.parameter_name]
114 def choices(self, changelist):
115 yield {
116 "selected": self.value() is None,
117 "query_string": changelist.get_query_string(remove=[self.parameter_name]),
118 "display": _("All"),
119 }
120 for lookup, title in self.lookup_choices:
121 yield {
122 "selected": self.value() == str(lookup),
123 "query_string": changelist.get_query_string(
124 {self.parameter_name: lookup}
125 ),
126 "display": title,
127 }
130class FieldListFilter(ListFilter):
131 _field_list_filters = []
132 _take_priority_index = 0
134 def __init__(self, field, request, params, model, model_admin, field_path):
135 self.field = field
136 self.field_path = field_path
137 self.title = getattr(field, "verbose_name", field_path)
138 super().__init__(request, params, model, model_admin)
139 for p in self.expected_parameters():
140 if p in params:
141 value = params.pop(p)
142 self.used_parameters[p] = prepare_lookup_value(p, value)
144 def has_output(self):
145 return True
147 def queryset(self, request, queryset):
148 try:
149 return queryset.filter(**self.used_parameters)
150 except (ValueError, ValidationError) as e:
151 # Fields may raise a ValueError or ValidationError when converting
152 # the parameters to the correct type.
153 raise IncorrectLookupParameters(e)
155 @classmethod
156 def register(cls, test, list_filter_class, take_priority=False):
157 if take_priority: 157 ↛ 161line 157 didn't jump to line 161, because the condition on line 157 was never true
158 # This is to allow overriding the default filters for certain types
159 # of fields with some custom filters. The first found in the list
160 # is used in priority.
161 cls._field_list_filters.insert(
162 cls._take_priority_index, (test, list_filter_class)
163 )
164 cls._take_priority_index += 1
165 else:
166 cls._field_list_filters.append((test, list_filter_class))
168 @classmethod
169 def create(cls, field, request, params, model, model_admin, field_path):
170 for test, list_filter_class in cls._field_list_filters:
171 if test(field):
172 return list_filter_class(
173 field, request, params, model, model_admin, field_path=field_path
174 )
177class RelatedFieldListFilter(FieldListFilter):
178 def __init__(self, field, request, params, model, model_admin, field_path):
179 other_model = get_model_from_relation(field)
180 self.lookup_kwarg = "%s__%s__exact" % (field_path, field.target_field.name)
181 self.lookup_kwarg_isnull = "%s__isnull" % field_path
182 self.lookup_val = params.get(self.lookup_kwarg)
183 self.lookup_val_isnull = params.get(self.lookup_kwarg_isnull)
184 super().__init__(field, request, params, model, model_admin, field_path)
185 self.lookup_choices = self.field_choices(field, request, model_admin)
186 if hasattr(field, "verbose_name"):
187 self.lookup_title = field.verbose_name
188 else:
189 self.lookup_title = other_model._meta.verbose_name
190 self.title = self.lookup_title
191 self.empty_value_display = model_admin.get_empty_value_display()
193 @property
194 def include_empty_choice(self):
195 """
196 Return True if a "(None)" choice should be included, which filters
197 out everything except empty relationships.
198 """
199 return self.field.null or (self.field.is_relation and self.field.many_to_many)
201 def has_output(self):
202 if self.include_empty_choice:
203 extra = 1
204 else:
205 extra = 0
206 return len(self.lookup_choices) + extra > 1
208 def expected_parameters(self):
209 return [self.lookup_kwarg, self.lookup_kwarg_isnull]
211 def field_admin_ordering(self, field, request, model_admin):
212 """
213 Return the model admin's ordering for related field, if provided.
214 """
215 related_admin = model_admin.admin_site._registry.get(field.remote_field.model)
216 if related_admin is not None:
217 return related_admin.get_ordering(request)
218 return ()
220 def field_choices(self, field, request, model_admin):
221 ordering = self.field_admin_ordering(field, request, model_admin)
222 return field.get_choices(include_blank=False, ordering=ordering)
224 def choices(self, changelist):
225 yield {
226 "selected": self.lookup_val is None and not self.lookup_val_isnull,
227 "query_string": changelist.get_query_string(
228 remove=[self.lookup_kwarg, self.lookup_kwarg_isnull]
229 ),
230 "display": _("All"),
231 }
232 for pk_val, val in self.lookup_choices:
233 yield {
234 "selected": self.lookup_val == str(pk_val),
235 "query_string": changelist.get_query_string(
236 {self.lookup_kwarg: pk_val}, [self.lookup_kwarg_isnull]
237 ),
238 "display": val,
239 }
240 if self.include_empty_choice:
241 yield {
242 "selected": bool(self.lookup_val_isnull),
243 "query_string": changelist.get_query_string(
244 {self.lookup_kwarg_isnull: "True"}, [self.lookup_kwarg]
245 ),
246 "display": self.empty_value_display,
247 }
250FieldListFilter.register(lambda f: f.remote_field, RelatedFieldListFilter) 250 ↛ exitline 250 didn't run the lambda on line 250
253class BooleanFieldListFilter(FieldListFilter):
254 def __init__(self, field, request, params, model, model_admin, field_path):
255 self.lookup_kwarg = "%s__exact" % field_path
256 self.lookup_kwarg2 = "%s__isnull" % field_path
257 self.lookup_val = params.get(self.lookup_kwarg)
258 self.lookup_val2 = params.get(self.lookup_kwarg2)
259 super().__init__(field, request, params, model, model_admin, field_path)
260 if (
261 self.used_parameters
262 and self.lookup_kwarg in self.used_parameters
263 and self.used_parameters[self.lookup_kwarg] in ("1", "0")
264 ):
265 self.used_parameters[self.lookup_kwarg] = bool(
266 int(self.used_parameters[self.lookup_kwarg])
267 )
269 def expected_parameters(self):
270 return [self.lookup_kwarg, self.lookup_kwarg2]
272 def choices(self, changelist):
273 field_choices = dict(self.field.flatchoices)
274 for lookup, title in (
275 (None, _("All")),
276 ("1", field_choices.get(True, _("Yes"))),
277 ("0", field_choices.get(False, _("No"))),
278 ):
279 yield {
280 "selected": self.lookup_val == lookup and not self.lookup_val2,
281 "query_string": changelist.get_query_string(
282 {self.lookup_kwarg: lookup}, [self.lookup_kwarg2]
283 ),
284 "display": title,
285 }
286 if self.field.null:
287 yield {
288 "selected": self.lookup_val2 == "True",
289 "query_string": changelist.get_query_string(
290 {self.lookup_kwarg2: "True"}, [self.lookup_kwarg]
291 ),
292 "display": field_choices.get(None, _("Unknown")),
293 }
296FieldListFilter.register( 296 ↛ exitline 296 didn't jump to the function exit
297 lambda f: isinstance(f, models.BooleanField), BooleanFieldListFilter
298)
301class ChoicesFieldListFilter(FieldListFilter):
302 def __init__(self, field, request, params, model, model_admin, field_path):
303 self.lookup_kwarg = "%s__exact" % field_path
304 self.lookup_kwarg_isnull = "%s__isnull" % field_path
305 self.lookup_val = params.get(self.lookup_kwarg)
306 self.lookup_val_isnull = params.get(self.lookup_kwarg_isnull)
307 super().__init__(field, request, params, model, model_admin, field_path)
309 def expected_parameters(self):
310 return [self.lookup_kwarg, self.lookup_kwarg_isnull]
312 def choices(self, changelist):
313 yield {
314 "selected": self.lookup_val is None,
315 "query_string": changelist.get_query_string(
316 remove=[self.lookup_kwarg, self.lookup_kwarg_isnull]
317 ),
318 "display": _("All"),
319 }
320 none_title = ""
321 for lookup, title in self.field.flatchoices:
322 if lookup is None:
323 none_title = title
324 continue
325 yield {
326 "selected": str(lookup) == self.lookup_val,
327 "query_string": changelist.get_query_string(
328 {self.lookup_kwarg: lookup}, [self.lookup_kwarg_isnull]
329 ),
330 "display": title,
331 }
332 if none_title:
333 yield {
334 "selected": bool(self.lookup_val_isnull),
335 "query_string": changelist.get_query_string(
336 {self.lookup_kwarg_isnull: "True"}, [self.lookup_kwarg]
337 ),
338 "display": none_title,
339 }
342FieldListFilter.register(lambda f: bool(f.choices), ChoicesFieldListFilter) 342 ↛ exitline 342 didn't run the lambda on line 342
345class DateFieldListFilter(FieldListFilter):
346 def __init__(self, field, request, params, model, model_admin, field_path):
347 self.field_generic = "%s__" % field_path
348 self.date_params = {
349 k: v for k, v in params.items() if k.startswith(self.field_generic)
350 }
352 now = timezone.now()
353 # When time zone support is enabled, convert "now" to the user's time
354 # zone so Django's definition of "Today" matches what the user expects.
355 if timezone.is_aware(now):
356 now = timezone.localtime(now)
358 if isinstance(field, models.DateTimeField):
359 today = now.replace(hour=0, minute=0, second=0, microsecond=0)
360 else: # field is a models.DateField
361 today = now.date()
362 tomorrow = today + datetime.timedelta(days=1)
363 if today.month == 12:
364 next_month = today.replace(year=today.year + 1, month=1, day=1)
365 else:
366 next_month = today.replace(month=today.month + 1, day=1)
367 next_year = today.replace(year=today.year + 1, month=1, day=1)
369 self.lookup_kwarg_since = "%s__gte" % field_path
370 self.lookup_kwarg_until = "%s__lt" % field_path
371 self.links = (
372 (_("Any date"), {}),
373 (
374 _("Today"),
375 {
376 self.lookup_kwarg_since: str(today),
377 self.lookup_kwarg_until: str(tomorrow),
378 },
379 ),
380 (
381 _("Past 7 days"),
382 {
383 self.lookup_kwarg_since: str(today - datetime.timedelta(days=7)),
384 self.lookup_kwarg_until: str(tomorrow),
385 },
386 ),
387 (
388 _("This month"),
389 {
390 self.lookup_kwarg_since: str(today.replace(day=1)),
391 self.lookup_kwarg_until: str(next_month),
392 },
393 ),
394 (
395 _("This year"),
396 {
397 self.lookup_kwarg_since: str(today.replace(month=1, day=1)),
398 self.lookup_kwarg_until: str(next_year),
399 },
400 ),
401 )
402 if field.null:
403 self.lookup_kwarg_isnull = "%s__isnull" % field_path
404 self.links += (
405 (_("No date"), {self.field_generic + "isnull": "True"}),
406 (_("Has date"), {self.field_generic + "isnull": "False"}),
407 )
408 super().__init__(field, request, params, model, model_admin, field_path)
410 def expected_parameters(self):
411 params = [self.lookup_kwarg_since, self.lookup_kwarg_until]
412 if self.field.null:
413 params.append(self.lookup_kwarg_isnull)
414 return params
416 def choices(self, changelist):
417 for title, param_dict in self.links:
418 yield {
419 "selected": self.date_params == param_dict,
420 "query_string": changelist.get_query_string(
421 param_dict, [self.field_generic]
422 ),
423 "display": title,
424 }
427FieldListFilter.register(lambda f: isinstance(f, models.DateField), DateFieldListFilter) 427 ↛ exitline 427 didn't run the lambda on line 427
430# This should be registered last, because it's a last resort. For example,
431# if a field is eligible to use the BooleanFieldListFilter, that'd be much
432# more appropriate, and the AllValuesFieldListFilter won't get used for it.
433class AllValuesFieldListFilter(FieldListFilter):
434 def __init__(self, field, request, params, model, model_admin, field_path):
435 self.lookup_kwarg = field_path
436 self.lookup_kwarg_isnull = "%s__isnull" % field_path
437 self.lookup_val = params.get(self.lookup_kwarg)
438 self.lookup_val_isnull = params.get(self.lookup_kwarg_isnull)
439 self.empty_value_display = model_admin.get_empty_value_display()
440 parent_model, reverse_path = reverse_field_path(model, field_path)
441 # Obey parent ModelAdmin queryset when deciding which options to show
442 if model == parent_model:
443 queryset = model_admin.get_queryset(request)
444 else:
445 queryset = parent_model._default_manager.all()
446 self.lookup_choices = (
447 queryset.distinct().order_by(field.name).values_list(field.name, flat=True)
448 )
449 super().__init__(field, request, params, model, model_admin, field_path)
451 def expected_parameters(self):
452 return [self.lookup_kwarg, self.lookup_kwarg_isnull]
454 def choices(self, changelist):
455 yield {
456 "selected": self.lookup_val is None and self.lookup_val_isnull is None,
457 "query_string": changelist.get_query_string(
458 remove=[self.lookup_kwarg, self.lookup_kwarg_isnull]
459 ),
460 "display": _("All"),
461 }
462 include_none = False
463 for val in self.lookup_choices:
464 if val is None:
465 include_none = True
466 continue
467 val = str(val)
468 yield {
469 "selected": self.lookup_val == val,
470 "query_string": changelist.get_query_string(
471 {self.lookup_kwarg: val}, [self.lookup_kwarg_isnull]
472 ),
473 "display": val,
474 }
475 if include_none:
476 yield {
477 "selected": bool(self.lookup_val_isnull),
478 "query_string": changelist.get_query_string(
479 {self.lookup_kwarg_isnull: "True"}, [self.lookup_kwarg]
480 ),
481 "display": self.empty_value_display,
482 }
485FieldListFilter.register(lambda f: True, AllValuesFieldListFilter) 485 ↛ exitline 485 didn't run the lambda on line 485
488class RelatedOnlyFieldListFilter(RelatedFieldListFilter):
489 def field_choices(self, field, request, model_admin):
490 pk_qs = (
491 model_admin.get_queryset(request)
492 .distinct()
493 .values_list("%s__pk" % self.field_path, flat=True)
494 )
495 ordering = self.field_admin_ordering(field, request, model_admin)
496 return field.get_choices(
497 include_blank=False, limit_choices_to={"pk__in": pk_qs}, ordering=ordering
498 )
501class EmptyFieldListFilter(FieldListFilter):
502 def __init__(self, field, request, params, model, model_admin, field_path):
503 if not field.empty_strings_allowed and not field.null:
504 raise ImproperlyConfigured(
505 "The list filter '%s' cannot be used with field '%s' which "
506 "doesn't allow empty strings and nulls."
507 % (
508 self.__class__.__name__,
509 field.name,
510 )
511 )
512 self.lookup_kwarg = "%s__isempty" % field_path
513 self.lookup_val = params.get(self.lookup_kwarg)
514 super().__init__(field, request, params, model, model_admin, field_path)
516 def queryset(self, request, queryset):
517 if self.lookup_kwarg not in self.used_parameters:
518 return queryset
519 if self.lookup_val not in ("0", "1"):
520 raise IncorrectLookupParameters
522 lookup_conditions = []
523 if self.field.empty_strings_allowed:
524 lookup_conditions.append((self.field_path, ""))
525 if self.field.null:
526 lookup_conditions.append((f"{self.field_path}__isnull", True))
527 lookup_condition = models.Q(*lookup_conditions, _connector=models.Q.OR)
528 if self.lookup_val == "1":
529 return queryset.filter(lookup_condition)
530 return queryset.exclude(lookup_condition)
532 def expected_parameters(self):
533 return [self.lookup_kwarg]
535 def choices(self, changelist):
536 for lookup, title in (
537 (None, _("All")),
538 ("1", _("Empty")),
539 ("0", _("Not empty")),
540 ):
541 yield {
542 "selected": self.lookup_val == lookup,
543 "query_string": changelist.get_query_string(
544 {self.lookup_kwarg: lookup}
545 ),
546 "display": title,
547 }