2
from functools import reduce
4
4
from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured
5
5
from django.core.paginator import InvalidPage
6
6
from django.core.urlresolvers import reverse
7
7
from django.db import models
8
8
from django.db.models.fields import FieldDoesNotExist
9
from django.utils import six
9
10
from django.utils.datastructures import SortedDict
11
from django.utils.deprecation import RenameMethodsBase
10
12
from django.utils.encoding import force_str, force_text
11
13
from django.utils.translation import ugettext, ugettext_lazy
12
14
from django.utils.http import urlencode
14
16
from django.contrib.admin import FieldListFilter
15
from django.contrib.admin.exceptions import DisallowedModelAdminToField
16
from django.contrib.admin.options import IncorrectLookupParameters
17
from django.contrib.admin.exceptions import DisallowedModelAdminLookup, DisallowedModelAdminToField
18
from django.contrib.admin.options import IncorrectLookupParameters, IS_POPUP_VAR
17
19
from django.contrib.admin.util import (quote, get_fields_from_path,
18
20
lookup_needs_distinct, prepare_lookup_value)
34
35
EMPTY_CHANGELIST_VALUE = ugettext_lazy('(None)')
37
class ChangeList(object):
38
def _is_changelist_popup(request):
40
Returns True if the popup GET parameter is set.
42
This function is introduced to facilitate deprecating the legacy
43
value for IS_POPUP_VAR and should be removed at the end of the
47
if IS_POPUP_VAR in request.GET:
50
IS_LEGACY_POPUP_VAR = 'pop'
51
if IS_LEGACY_POPUP_VAR in request.GET:
53
"The `%s` GET parameter has been renamed to `%s`." %
54
(IS_LEGACY_POPUP_VAR, IS_POPUP_VAR),
55
PendingDeprecationWarning, 2)
61
class RenameChangeListMethods(RenameMethodsBase):
63
('get_query_set', 'get_queryset', PendingDeprecationWarning),
67
class ChangeList(six.with_metaclass(RenameChangeListMethods)):
38
68
def __init__(self, request, model, list_display, list_display_links,
39
69
list_filter, date_hierarchy, search_fields, list_select_related,
40
70
list_per_page, list_max_show_all, list_editable, model_admin):
42
72
self.opts = model._meta
43
73
self.lookup_opts = self.opts
44
self.root_query_set = model_admin.queryset(request)
74
self.root_queryset = model_admin.get_queryset(request)
45
75
self.list_display = list_display
46
76
self.list_display_links = list_display_links
47
77
self.list_filter = list_filter
83
114
self.title = title % force_text(self.opts.verbose_name)
84
115
self.pk_attname = self.lookup_opts.pk.attname
86
def get_filters(self, request):
87
lookup_params = self.params.copy() # a dictionary of the query string
118
def root_query_set(self):
119
warnings.warn("`ChangeList.root_query_set` is deprecated, "
120
"use `root_queryset` instead.",
121
PendingDeprecationWarning, 2)
122
return self.root_queryset
126
warnings.warn("`ChangeList.query_set` is deprecated, "
127
"use `queryset` instead.",
128
PendingDeprecationWarning, 2)
131
def get_filters_params(self, params=None):
133
Returns all params except IGNORED_PARAMS
137
lookup_params = params.copy() # a dictionary of the query string
90
138
# Remove all the parameters that are globally and systematically
92
140
for ignored in IGNORED_PARAMS:
93
141
if ignored in lookup_params:
94
142
del lookup_params[ignored]
145
def get_filters(self, request):
146
lookup_params = self.get_filters_params()
96
149
# Normalize the types of keys
97
150
for key, value in lookup_params.items():
146
199
lookup_needs_distinct(self.lookup_opts, key))
147
200
return filter_specs, bool(filter_specs), lookup_params, use_distinct
148
201
except FieldDoesNotExist as e:
149
raise IncorrectLookupParameters(e)
202
six.reraise(IncorrectLookupParameters, IncorrectLookupParameters(e), sys.exc_info()[2])
151
204
def get_query_string(self, new_params=None, remove=None):
152
205
if new_params is None: new_params = {}
165
218
return '?%s' % urlencode(sorted(p.items()))
167
220
def get_results(self, request):
168
paginator = self.model_admin.get_paginator(request, self.query_set, self.list_per_page)
221
paginator = self.model_admin.get_paginator(request, self.queryset, self.list_per_page)
169
222
# Get the number of objects, with admin filters applied.
170
223
result_count = paginator.count
172
225
# Get the total number of objects, with no admin filters applied.
173
# Perform a slight optimization: Check to see whether any filters were
174
# given. If not, use paginator.hits to calculate the number of objects,
175
# because we've already done paginator.hits and the value is cached.
176
if not self.query_set.query.where:
226
# Perform a slight optimization:
227
# full_result_count is equal to paginator.count if no filters
229
if self.get_filters_params() or self.params.get(SEARCH_VAR):
230
full_result_count = self.root_queryset.count()
177
232
full_result_count = result_count
179
full_result_count = self.root_query_set.count()
181
233
can_show_all = result_count <= self.list_max_show_all
182
234
multi_page = result_count > self.list_per_page
184
236
# Get the list of objects to display on this page.
185
237
if (self.show_all and can_show_all) or not multi_page:
186
result_list = self.query_set._clone()
238
result_list = self.queryset._clone()
189
241
result_list = paginator.page(self.page_num+1).object_list
301
353
ordering_fields[idx] = 'desc' if pfx == '-' else 'asc'
302
354
return ordering_fields
304
def get_query_set(self, request):
356
def get_queryset(self, request):
305
357
# First, we collect all the declared list filters.
306
358
(self.filter_specs, self.has_filters, remaining_lookup_params,
307
use_distinct) = self.get_filters(request)
359
filters_use_distinct) = self.get_filters(request)
309
361
# Then, we let every list filter modify the queryset to its liking.
310
qs = self.root_query_set
362
qs = self.root_queryset
311
363
for filter_spec in self.filter_specs:
312
364
new_qs = filter_spec.queryset(request, qs)
313
365
if new_qs is not None:
330
382
# ValueError, ValidationError, or ?.
331
383
raise IncorrectLookupParameters(e)
333
# Use select_related() if one of the list_display options is a field
334
# with a relationship and the provided queryset doesn't already have
335
# select_related defined.
336
385
if not qs.query.select_related:
337
if self.list_select_related:
338
qs = qs.select_related()
340
for field_name in self.list_display:
342
field = self.lookup_opts.get_field(field_name)
343
except models.FieldDoesNotExist:
346
if isinstance(field.rel, models.ManyToOneRel):
347
qs = qs.select_related()
386
qs = self.apply_select_related(qs)
351
389
ordering = self.get_ordering(request, qs)
352
390
qs = qs.order_by(*ordering)
354
# Apply keyword searches.
355
def construct_search(field_name):
356
if field_name.startswith('^'):
357
return "%s__istartswith" % field_name[1:]
358
elif field_name.startswith('='):
359
return "%s__iexact" % field_name[1:]
360
elif field_name.startswith('@'):
361
return "%s__search" % field_name[1:]
363
return "%s__icontains" % field_name
365
if self.search_fields and self.query:
366
orm_lookups = [construct_search(str(search_field))
367
for search_field in self.search_fields]
368
for bit in self.query.split():
369
or_queries = [models.Q(**{orm_lookup: bit})
370
for orm_lookup in orm_lookups]
371
qs = qs.filter(reduce(operator.or_, or_queries))
373
for search_spec in orm_lookups:
374
if lookup_needs_distinct(self.lookup_opts, search_spec):
392
# Apply search results
393
qs, search_use_distinct = self.model_admin.get_search_results(
394
request, qs, self.query)
396
# Remove duplicates from results, if necessary
397
if filters_use_distinct | search_use_distinct:
379
398
return qs.distinct()
402
def apply_select_related(self, qs):
403
if self.list_select_related is True:
404
return qs.select_related()
406
if self.list_select_related is False:
407
if self.has_related_field_in_list_display():
408
return qs.select_related()
410
if self.list_select_related:
411
return qs.select_related(*self.list_select_related)
414
def has_related_field_in_list_display(self):
415
for field_name in self.list_display:
417
field = self.lookup_opts.get_field(field_name)
418
except models.FieldDoesNotExist:
421
if isinstance(field.rel, models.ManyToOneRel):
383
425
def url_for_result(self, result):
384
426
pk = getattr(result, self.pk_attname)
385
427
return reverse('admin:%s_%s_change' % (self.opts.app_label,
386
self.opts.module_name),
428
self.opts.model_name),
387
429
args=(quote(pk),),
388
430
current_app=self.model_admin.admin_site.name)