Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/contrib/admin/views/main.py: 9%
329 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 datetime import datetime, timedelta
3from django import forms
4from django.conf import settings
5from django.contrib import messages
6from django.contrib.admin import FieldListFilter
7from django.contrib.admin.exceptions import (
8 DisallowedModelAdminLookup,
9 DisallowedModelAdminToField,
10)
11from django.contrib.admin.options import (
12 IS_POPUP_VAR,
13 TO_FIELD_VAR,
14 IncorrectLookupParameters,
15)
16from django.contrib.admin.utils import (
17 get_fields_from_path,
18 lookup_spawns_duplicates,
19 prepare_lookup_value,
20 quote,
21)
22from django.core.exceptions import (
23 FieldDoesNotExist,
24 ImproperlyConfigured,
25 SuspiciousOperation,
26)
27from django.core.paginator import InvalidPage
28from django.db.models import Exists, F, Field, ManyToOneRel, OrderBy, OuterRef
29from django.db.models.expressions import Combinable
30from django.urls import reverse
31from django.utils.http import urlencode
32from django.utils.timezone import make_aware
33from django.utils.translation import gettext
35# Changelist settings
36ALL_VAR = "all"
37ORDER_VAR = "o"
38PAGE_VAR = "p"
39SEARCH_VAR = "q"
40ERROR_FLAG = "e"
42IGNORED_PARAMS = (ALL_VAR, ORDER_VAR, SEARCH_VAR, IS_POPUP_VAR, TO_FIELD_VAR)
45class ChangeListSearchForm(forms.Form):
46 def __init__(self, *args, **kwargs):
47 super().__init__(*args, **kwargs)
48 # Populate "fields" dynamically because SEARCH_VAR is a variable:
49 self.fields = {
50 SEARCH_VAR: forms.CharField(required=False, strip=False),
51 }
54class ChangeList:
55 search_form_class = ChangeListSearchForm
57 def __init__(
58 self,
59 request,
60 model,
61 list_display,
62 list_display_links,
63 list_filter,
64 date_hierarchy,
65 search_fields,
66 list_select_related,
67 list_per_page,
68 list_max_show_all,
69 list_editable,
70 model_admin,
71 sortable_by,
72 search_help_text,
73 ):
74 self.model = model
75 self.opts = model._meta
76 self.lookup_opts = self.opts
77 self.root_queryset = model_admin.get_queryset(request)
78 self.list_display = list_display
79 self.list_display_links = list_display_links
80 self.list_filter = list_filter
81 self.has_filters = None
82 self.has_active_filters = None
83 self.clear_all_filters_qs = None
84 self.date_hierarchy = date_hierarchy
85 self.search_fields = search_fields
86 self.list_select_related = list_select_related
87 self.list_per_page = list_per_page
88 self.list_max_show_all = list_max_show_all
89 self.model_admin = model_admin
90 self.preserved_filters = model_admin.get_preserved_filters(request)
91 self.sortable_by = sortable_by
92 self.search_help_text = search_help_text
94 # Get search parameters from the query string.
95 _search_form = self.search_form_class(request.GET)
96 if not _search_form.is_valid():
97 for error in _search_form.errors.values():
98 messages.error(request, ", ".join(error))
99 self.query = _search_form.cleaned_data.get(SEARCH_VAR) or ""
100 try:
101 self.page_num = int(request.GET.get(PAGE_VAR, 1))
102 except ValueError:
103 self.page_num = 1
104 self.show_all = ALL_VAR in request.GET
105 self.is_popup = IS_POPUP_VAR in request.GET
106 to_field = request.GET.get(TO_FIELD_VAR)
107 if to_field and not model_admin.to_field_allowed(request, to_field):
108 raise DisallowedModelAdminToField(
109 "The field %s cannot be referenced." % to_field
110 )
111 self.to_field = to_field
112 self.params = dict(request.GET.items())
113 if PAGE_VAR in self.params:
114 del self.params[PAGE_VAR]
115 if ERROR_FLAG in self.params:
116 del self.params[ERROR_FLAG]
118 if self.is_popup:
119 self.list_editable = ()
120 else:
121 self.list_editable = list_editable
122 self.queryset = self.get_queryset(request)
123 self.get_results(request)
124 if self.is_popup:
125 title = gettext("Select %s")
126 elif self.model_admin.has_change_permission(request):
127 title = gettext("Select %s to change")
128 else:
129 title = gettext("Select %s to view")
130 self.title = title % self.opts.verbose_name
131 self.pk_attname = self.lookup_opts.pk.attname
133 def __repr__(self):
134 return "<%s: model=%s model_admin=%s>" % (
135 self.__class__.__qualname__,
136 self.model.__qualname__,
137 self.model_admin.__class__.__qualname__,
138 )
140 def get_filters_params(self, params=None):
141 """
142 Return all params except IGNORED_PARAMS.
143 """
144 params = params or self.params
145 lookup_params = params.copy() # a dictionary of the query string
146 # Remove all the parameters that are globally and systematically
147 # ignored.
148 for ignored in IGNORED_PARAMS:
149 if ignored in lookup_params:
150 del lookup_params[ignored]
151 return lookup_params
153 def get_filters(self, request):
154 lookup_params = self.get_filters_params()
155 may_have_duplicates = False
156 has_active_filters = False
158 for key, value in lookup_params.items():
159 if not self.model_admin.lookup_allowed(key, value):
160 raise DisallowedModelAdminLookup("Filtering by %s not allowed" % key)
162 filter_specs = []
163 for list_filter in self.list_filter:
164 lookup_params_count = len(lookup_params)
165 if callable(list_filter):
166 # This is simply a custom list filter class.
167 spec = list_filter(request, lookup_params, self.model, self.model_admin)
168 else:
169 field_path = None
170 if isinstance(list_filter, (tuple, list)):
171 # This is a custom FieldListFilter class for a given field.
172 field, field_list_filter_class = list_filter
173 else:
174 # This is simply a field name, so use the default
175 # FieldListFilter class that has been registered for the
176 # type of the given field.
177 field, field_list_filter_class = list_filter, FieldListFilter.create
178 if not isinstance(field, Field):
179 field_path = field
180 field = get_fields_from_path(self.model, field_path)[-1]
182 spec = field_list_filter_class(
183 field,
184 request,
185 lookup_params,
186 self.model,
187 self.model_admin,
188 field_path=field_path,
189 )
190 # field_list_filter_class removes any lookup_params it
191 # processes. If that happened, check if duplicates should be
192 # removed.
193 if lookup_params_count > len(lookup_params):
194 may_have_duplicates |= lookup_spawns_duplicates(
195 self.lookup_opts,
196 field_path,
197 )
198 if spec and spec.has_output():
199 filter_specs.append(spec)
200 if lookup_params_count > len(lookup_params):
201 has_active_filters = True
203 if self.date_hierarchy:
204 # Create bounded lookup parameters so that the query is more
205 # efficient.
206 year = lookup_params.pop("%s__year" % self.date_hierarchy, None)
207 if year is not None:
208 month = lookup_params.pop("%s__month" % self.date_hierarchy, None)
209 day = lookup_params.pop("%s__day" % self.date_hierarchy, None)
210 try:
211 from_date = datetime(
212 int(year),
213 int(month if month is not None else 1),
214 int(day if day is not None else 1),
215 )
216 except ValueError as e:
217 raise IncorrectLookupParameters(e) from e
218 if day:
219 to_date = from_date + timedelta(days=1)
220 elif month:
221 # In this branch, from_date will always be the first of a
222 # month, so advancing 32 days gives the next month.
223 to_date = (from_date + timedelta(days=32)).replace(day=1)
224 else:
225 to_date = from_date.replace(year=from_date.year + 1)
226 if settings.USE_TZ:
227 from_date = make_aware(from_date)
228 to_date = make_aware(to_date)
229 lookup_params.update(
230 {
231 "%s__gte" % self.date_hierarchy: from_date,
232 "%s__lt" % self.date_hierarchy: to_date,
233 }
234 )
236 # At this point, all the parameters used by the various ListFilters
237 # have been removed from lookup_params, which now only contains other
238 # parameters passed via the query string. We now loop through the
239 # remaining parameters both to ensure that all the parameters are valid
240 # fields and to determine if at least one of them spawns duplicates. If
241 # the lookup parameters aren't real fields, then bail out.
242 try:
243 for key, value in lookup_params.items():
244 lookup_params[key] = prepare_lookup_value(key, value)
245 may_have_duplicates |= lookup_spawns_duplicates(self.lookup_opts, key)
246 return (
247 filter_specs,
248 bool(filter_specs),
249 lookup_params,
250 may_have_duplicates,
251 has_active_filters,
252 )
253 except FieldDoesNotExist as e:
254 raise IncorrectLookupParameters(e) from e
256 def get_query_string(self, new_params=None, remove=None):
257 if new_params is None:
258 new_params = {}
259 if remove is None:
260 remove = []
261 p = self.params.copy()
262 for r in remove:
263 for k in list(p):
264 if k.startswith(r):
265 del p[k]
266 for k, v in new_params.items():
267 if v is None:
268 if k in p:
269 del p[k]
270 else:
271 p[k] = v
272 return "?%s" % urlencode(sorted(p.items()))
274 def get_results(self, request):
275 paginator = self.model_admin.get_paginator(
276 request, self.queryset, self.list_per_page
277 )
278 # Get the number of objects, with admin filters applied.
279 result_count = paginator.count
281 # Get the total number of objects, with no admin filters applied.
282 if self.model_admin.show_full_result_count:
283 full_result_count = self.root_queryset.count()
284 else:
285 full_result_count = None
286 can_show_all = result_count <= self.list_max_show_all
287 multi_page = result_count > self.list_per_page
289 # Get the list of objects to display on this page.
290 if (self.show_all and can_show_all) or not multi_page:
291 result_list = self.queryset._clone()
292 else:
293 try:
294 result_list = paginator.page(self.page_num).object_list
295 except InvalidPage:
296 raise IncorrectLookupParameters
298 self.result_count = result_count
299 self.show_full_result_count = self.model_admin.show_full_result_count
300 # Admin actions are shown if there is at least one entry
301 # or if entries are not counted because show_full_result_count is disabled
302 self.show_admin_actions = not self.show_full_result_count or bool(
303 full_result_count
304 )
305 self.full_result_count = full_result_count
306 self.result_list = result_list
307 self.can_show_all = can_show_all
308 self.multi_page = multi_page
309 self.paginator = paginator
311 def _get_default_ordering(self):
312 ordering = []
313 if self.model_admin.ordering:
314 ordering = self.model_admin.ordering
315 elif self.lookup_opts.ordering:
316 ordering = self.lookup_opts.ordering
317 return ordering
319 def get_ordering_field(self, field_name):
320 """
321 Return the proper model field name corresponding to the given
322 field_name to use for ordering. field_name may either be the name of a
323 proper model field or the name of a method (on the admin or model) or a
324 callable with the 'admin_order_field' attribute. Return None if no
325 proper model field name can be matched.
326 """
327 try:
328 field = self.lookup_opts.get_field(field_name)
329 return field.name
330 except FieldDoesNotExist:
331 # See whether field_name is a name of a non-field
332 # that allows sorting.
333 if callable(field_name):
334 attr = field_name
335 elif hasattr(self.model_admin, field_name):
336 attr = getattr(self.model_admin, field_name)
337 else:
338 attr = getattr(self.model, field_name)
339 if isinstance(attr, property) and hasattr(attr, "fget"):
340 attr = attr.fget
341 return getattr(attr, "admin_order_field", None)
343 def get_ordering(self, request, queryset):
344 """
345 Return the list of ordering fields for the change list.
346 First check the get_ordering() method in model admin, then check
347 the object's default ordering. Then, any manually-specified ordering
348 from the query string overrides anything. Finally, a deterministic
349 order is guaranteed by calling _get_deterministic_ordering() with the
350 constructed ordering.
351 """
352 params = self.params
353 ordering = list(
354 self.model_admin.get_ordering(request) or self._get_default_ordering()
355 )
356 if ORDER_VAR in params:
357 # Clear ordering and used params
358 ordering = []
359 order_params = params[ORDER_VAR].split(".")
360 for p in order_params:
361 try:
362 none, pfx, idx = p.rpartition("-")
363 field_name = self.list_display[int(idx)]
364 order_field = self.get_ordering_field(field_name)
365 if not order_field:
366 continue # No 'admin_order_field', skip it
367 if isinstance(order_field, OrderBy):
368 if pfx == "-":
369 order_field = order_field.copy()
370 order_field.reverse_ordering()
371 ordering.append(order_field)
372 elif hasattr(order_field, "resolve_expression"):
373 # order_field is an expression.
374 ordering.append(
375 order_field.desc() if pfx == "-" else order_field.asc()
376 )
377 # reverse order if order_field has already "-" as prefix
378 elif order_field.startswith("-") and pfx == "-":
379 ordering.append(order_field[1:])
380 else:
381 ordering.append(pfx + order_field)
382 except (IndexError, ValueError):
383 continue # Invalid ordering specified, skip it.
385 # Add the given query's ordering fields, if any.
386 ordering.extend(queryset.query.order_by)
388 return self._get_deterministic_ordering(ordering)
390 def _get_deterministic_ordering(self, ordering):
391 """
392 Ensure a deterministic order across all database backends. Search for a
393 single field or unique together set of fields providing a total
394 ordering. If these are missing, augment the ordering with a descendant
395 primary key.
396 """
397 ordering = list(ordering)
398 ordering_fields = set()
399 total_ordering_fields = {"pk"} | {
400 field.attname
401 for field in self.lookup_opts.fields
402 if field.unique and not field.null
403 }
404 for part in ordering:
405 # Search for single field providing a total ordering.
406 field_name = None
407 if isinstance(part, str):
408 field_name = part.lstrip("-")
409 elif isinstance(part, F):
410 field_name = part.name
411 elif isinstance(part, OrderBy) and isinstance(part.expression, F):
412 field_name = part.expression.name
413 if field_name:
414 # Normalize attname references by using get_field().
415 try:
416 field = self.lookup_opts.get_field(field_name)
417 except FieldDoesNotExist:
418 # Could be "?" for random ordering or a related field
419 # lookup. Skip this part of introspection for now.
420 continue
421 # Ordering by a related field name orders by the referenced
422 # model's ordering. Skip this part of introspection for now.
423 if field.remote_field and field_name == field.name:
424 continue
425 if field.attname in total_ordering_fields:
426 break
427 ordering_fields.add(field.attname)
428 else:
429 # No single total ordering field, try unique_together and total
430 # unique constraints.
431 constraint_field_names = (
432 *self.lookup_opts.unique_together,
433 *(
434 constraint.fields
435 for constraint in self.lookup_opts.total_unique_constraints
436 ),
437 )
438 for field_names in constraint_field_names:
439 # Normalize attname references by using get_field().
440 fields = [
441 self.lookup_opts.get_field(field_name) for field_name in field_names
442 ]
443 # Composite unique constraints containing a nullable column
444 # cannot ensure total ordering.
445 if any(field.null for field in fields):
446 continue
447 if ordering_fields.issuperset(field.attname for field in fields):
448 break
449 else:
450 # If no set of unique fields is present in the ordering, rely
451 # on the primary key to provide total ordering.
452 ordering.append("-pk")
453 return ordering
455 def get_ordering_field_columns(self):
456 """
457 Return a dictionary of ordering field column numbers and asc/desc.
458 """
459 # We must cope with more than one column having the same underlying sort
460 # field, so we base things on column numbers.
461 ordering = self._get_default_ordering()
462 ordering_fields = {}
463 if ORDER_VAR not in self.params:
464 # for ordering specified on ModelAdmin or model Meta, we don't know
465 # the right column numbers absolutely, because there might be more
466 # than one column associated with that ordering, so we guess.
467 for field in ordering:
468 if isinstance(field, (Combinable, OrderBy)):
469 if not isinstance(field, OrderBy):
470 field = field.asc()
471 if isinstance(field.expression, F):
472 order_type = "desc" if field.descending else "asc"
473 field = field.expression.name
474 else:
475 continue
476 elif field.startswith("-"):
477 field = field[1:]
478 order_type = "desc"
479 else:
480 order_type = "asc"
481 for index, attr in enumerate(self.list_display):
482 if self.get_ordering_field(attr) == field:
483 ordering_fields[index] = order_type
484 break
485 else:
486 for p in self.params[ORDER_VAR].split("."):
487 none, pfx, idx = p.rpartition("-")
488 try:
489 idx = int(idx)
490 except ValueError:
491 continue # skip it
492 ordering_fields[idx] = "desc" if pfx == "-" else "asc"
493 return ordering_fields
495 def get_queryset(self, request):
496 # First, we collect all the declared list filters.
497 (
498 self.filter_specs,
499 self.has_filters,
500 remaining_lookup_params,
501 filters_may_have_duplicates,
502 self.has_active_filters,
503 ) = self.get_filters(request)
504 # Then, we let every list filter modify the queryset to its liking.
505 qs = self.root_queryset
506 for filter_spec in self.filter_specs:
507 new_qs = filter_spec.queryset(request, qs)
508 if new_qs is not None:
509 qs = new_qs
511 try:
512 # Finally, we apply the remaining lookup parameters from the query
513 # string (i.e. those that haven't already been processed by the
514 # filters).
515 qs = qs.filter(**remaining_lookup_params)
516 except (SuspiciousOperation, ImproperlyConfigured):
517 # Allow certain types of errors to be re-raised as-is so that the
518 # caller can treat them in a special way.
519 raise
520 except Exception as e:
521 # Every other error is caught with a naked except, because we don't
522 # have any other way of validating lookup parameters. They might be
523 # invalid if the keyword arguments are incorrect, or if the values
524 # are not in the correct type, so we might get FieldError,
525 # ValueError, ValidationError, or ?.
526 raise IncorrectLookupParameters(e)
528 # Apply search results
529 qs, search_may_have_duplicates = self.model_admin.get_search_results(
530 request,
531 qs,
532 self.query,
533 )
535 # Set query string for clearing all filters.
536 self.clear_all_filters_qs = self.get_query_string(
537 new_params=remaining_lookup_params,
538 remove=self.get_filters_params(),
539 )
540 # Remove duplicates from results, if necessary
541 if filters_may_have_duplicates | search_may_have_duplicates:
542 qs = qs.filter(pk=OuterRef("pk"))
543 qs = self.root_queryset.filter(Exists(qs))
545 # Set ordering.
546 ordering = self.get_ordering(request, qs)
547 qs = qs.order_by(*ordering)
549 if not qs.query.select_related:
550 qs = self.apply_select_related(qs)
552 return qs
554 def apply_select_related(self, qs):
555 if self.list_select_related is True:
556 return qs.select_related()
558 if self.list_select_related is False:
559 if self.has_related_field_in_list_display():
560 return qs.select_related()
562 if self.list_select_related:
563 return qs.select_related(*self.list_select_related)
564 return qs
566 def has_related_field_in_list_display(self):
567 for field_name in self.list_display:
568 try:
569 field = self.lookup_opts.get_field(field_name)
570 except FieldDoesNotExist:
571 pass
572 else:
573 if isinstance(field.remote_field, ManyToOneRel):
574 # <FK>_id field names don't require a join.
575 if field_name != field.get_attname():
576 return True
577 return False
579 def url_for_result(self, result):
580 pk = getattr(result, self.pk_attname)
581 return reverse(
582 "admin:%s_%s_change" % (self.opts.app_label, self.opts.model_name),
583 args=(quote(pk),),
584 current_app=self.model_admin.admin_site.name,
585 )