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.options import IncorrectLookupParameters
17
from django.contrib.admin.exceptions import DisallowedModelAdminLookup
18
from django.contrib.admin.options import IncorrectLookupParameters, IS_POPUP_VAR
16
19
from django.contrib.admin.util import (quote, get_fields_from_path,
17
20
lookup_needs_distinct, prepare_lookup_value)
33
35
EMPTY_CHANGELIST_VALUE = ugettext_lazy('(None)')
36
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)):
37
68
def __init__(self, request, model, list_display, list_display_links,
38
69
list_filter, date_hierarchy, search_fields, list_select_related,
39
70
list_per_page, list_max_show_all, list_editable, model_admin):
41
72
self.opts = model._meta
42
73
self.lookup_opts = self.opts
43
self.root_query_set = model_admin.queryset(request)
74
self.root_queryset = model_admin.get_queryset(request)
44
75
self.list_display = list_display
45
76
self.list_display_links = list_display_links
46
77
self.list_filter = list_filter
79
111
self.title = title % force_text(self.opts.verbose_name)
80
112
self.pk_attname = self.lookup_opts.pk.attname
82
def get_filters(self, request):
83
lookup_params = self.params.copy() # a dictionary of the query string
115
def root_query_set(self):
116
warnings.warn("`ChangeList.root_query_set` is deprecated, "
117
"use `root_queryset` instead.",
118
PendingDeprecationWarning, 2)
119
return self.root_queryset
123
warnings.warn("`ChangeList.query_set` is deprecated, "
124
"use `queryset` instead.",
125
PendingDeprecationWarning, 2)
128
def get_filters_params(self, params=None):
130
Returns all params except IGNORED_PARAMS
134
lookup_params = params.copy() # a dictionary of the query string
86
135
# Remove all the parameters that are globally and systematically
88
137
for ignored in IGNORED_PARAMS:
89
138
if ignored in lookup_params:
90
139
del lookup_params[ignored]
142
def get_filters(self, request):
143
lookup_params = self.get_filters_params()
92
146
# Normalize the types of keys
93
147
for key, value in lookup_params.items():
142
196
lookup_needs_distinct(self.lookup_opts, key))
143
197
return filter_specs, bool(filter_specs), lookup_params, use_distinct
144
198
except FieldDoesNotExist as e:
145
raise IncorrectLookupParameters(e)
199
six.reraise(IncorrectLookupParameters, IncorrectLookupParameters(e), sys.exc_info()[2])
147
201
def get_query_string(self, new_params=None, remove=None):
148
202
if new_params is None: new_params = {}
161
215
return '?%s' % urlencode(sorted(p.items()))
163
217
def get_results(self, request):
164
paginator = self.model_admin.get_paginator(request, self.query_set, self.list_per_page)
218
paginator = self.model_admin.get_paginator(request, self.queryset, self.list_per_page)
165
219
# Get the number of objects, with admin filters applied.
166
220
result_count = paginator.count
168
222
# Get the total number of objects, with no admin filters applied.
169
# Perform a slight optimization: Check to see whether any filters were
170
# given. If not, use paginator.hits to calculate the number of objects,
171
# because we've already done paginator.hits and the value is cached.
172
if not self.query_set.query.where:
223
# Perform a slight optimization:
224
# full_result_count is equal to paginator.count if no filters
226
if self.get_filters_params():
227
full_result_count = self.root_queryset.count()
173
229
full_result_count = result_count
175
full_result_count = self.root_query_set.count()
177
230
can_show_all = result_count <= self.list_max_show_all
178
231
multi_page = result_count > self.list_per_page
180
233
# Get the list of objects to display on this page.
181
234
if (self.show_all and can_show_all) or not multi_page:
182
result_list = self.query_set._clone()
235
result_list = self.queryset._clone()
185
238
result_list = paginator.page(self.page_num+1).object_list
297
350
ordering_fields[idx] = 'desc' if pfx == '-' else 'asc'
298
351
return ordering_fields
300
def get_query_set(self, request):
353
def get_queryset(self, request):
301
354
# First, we collect all the declared list filters.
302
355
(self.filter_specs, self.has_filters, remaining_lookup_params,
303
use_distinct) = self.get_filters(request)
356
filters_use_distinct) = self.get_filters(request)
305
358
# Then, we let every list filter modify the queryset to its liking.
306
qs = self.root_query_set
359
qs = self.root_queryset
307
360
for filter_spec in self.filter_specs:
308
361
new_qs = filter_spec.queryset(request, qs)
309
362
if new_qs is not None:
326
379
# ValueError, ValidationError, or ?.
327
380
raise IncorrectLookupParameters(e)
329
# Use select_related() if one of the list_display options is a field
330
# with a relationship and the provided queryset doesn't already have
331
# select_related defined.
332
382
if not qs.query.select_related:
333
if self.list_select_related:
334
qs = qs.select_related()
336
for field_name in self.list_display:
338
field = self.lookup_opts.get_field(field_name)
339
except models.FieldDoesNotExist:
342
if isinstance(field.rel, models.ManyToOneRel):
343
qs = qs.select_related()
383
qs = self.apply_select_related(qs)
347
386
ordering = self.get_ordering(request, qs)
348
387
qs = qs.order_by(*ordering)
350
# Apply keyword searches.
351
def construct_search(field_name):
352
if field_name.startswith('^'):
353
return "%s__istartswith" % field_name[1:]
354
elif field_name.startswith('='):
355
return "%s__iexact" % field_name[1:]
356
elif field_name.startswith('@'):
357
return "%s__search" % field_name[1:]
359
return "%s__icontains" % field_name
361
if self.search_fields and self.query:
362
orm_lookups = [construct_search(str(search_field))
363
for search_field in self.search_fields]
364
for bit in self.query.split():
365
or_queries = [models.Q(**{orm_lookup: bit})
366
for orm_lookup in orm_lookups]
367
qs = qs.filter(reduce(operator.or_, or_queries))
369
for search_spec in orm_lookups:
370
if lookup_needs_distinct(self.lookup_opts, search_spec):
389
# Apply search results
390
qs, search_use_distinct = self.model_admin.get_search_results(
391
request, qs, self.query)
393
# Remove duplicates from results, if necessary
394
if filters_use_distinct | search_use_distinct:
375
395
return qs.distinct()
399
def apply_select_related(self, qs):
400
if self.list_select_related is True:
401
return qs.select_related()
403
if self.list_select_related is False:
404
if self.has_related_field_in_list_display():
405
return qs.select_related()
407
if self.list_select_related:
408
return qs.select_related(*self.list_select_related)
411
def has_related_field_in_list_display(self):
412
for field_name in self.list_display:
414
field = self.lookup_opts.get_field(field_name)
415
except models.FieldDoesNotExist:
418
if isinstance(field.rel, models.ManyToOneRel):
379
422
def url_for_result(self, result):
380
423
pk = getattr(result, self.pk_attname)
381
424
return reverse('admin:%s_%s_change' % (self.opts.app_label,
382
self.opts.module_name),
425
self.opts.model_name),
383
426
args=(quote(pk),),
384
427
current_app=self.model_admin.admin_site.name)