3 from django
.core
.exceptions
import SuspiciousOperation
, ImproperlyConfigured
4 from django
.core
.paginator
import InvalidPage
5 from django
.db
import models
6 from django
.db
.models
.fields
import FieldDoesNotExist
7 from django
.utils
.datastructures
import SortedDict
8 from django
.utils
.encoding
import force_unicode
, smart_str
9 from django
.utils
.translation
import ugettext
, ugettext_lazy
10 from django
.utils
.http
import urlencode
12 from django
.contrib
.admin
import FieldListFilter
13 from django
.contrib
.admin
.options
import IncorrectLookupParameters
14 from django
.contrib
.admin
.util
import (quote
, get_fields_from_path
,
15 lookup_needs_distinct
, prepare_lookup_value
)
28 ALL_VAR
, ORDER_VAR
, ORDER_TYPE_VAR
, SEARCH_VAR
, IS_POPUP_VAR
, TO_FIELD_VAR
)
30 # Text to display within change-list table cells if the value is blank.
31 EMPTY_CHANGELIST_VALUE
= ugettext_lazy('(None)')
34 class ChangeList(object):
35 def __init__(self
, request
, model
, list_display
, list_display_links
,
36 list_filter
, date_hierarchy
, search_fields
, list_select_related
,
37 list_per_page
, list_max_show_all
, list_editable
, model_admin
):
39 self
.opts
= model
._meta
40 self
.lookup_opts
= self
.opts
41 self
.root_query_set
= model_admin
.queryset(request
)
42 self
.list_display
= list_display
43 self
.list_display_links
= list_display_links
44 self
.list_filter
= list_filter
45 self
.date_hierarchy
= date_hierarchy
46 self
.search_fields
= search_fields
47 self
.list_select_related
= list_select_related
48 self
.list_per_page
= list_per_page
49 self
.list_max_show_all
= list_max_show_all
50 self
.model_admin
= model_admin
52 # Get search parameters from the query string.
54 self
.page_num
= int(request
.GET
.get(PAGE_VAR
, 0))
57 self
.show_all
= ALL_VAR
in request
.GET
58 self
.is_popup
= IS_POPUP_VAR
in request
.GET
59 self
.to_field
= request
.GET
.get(TO_FIELD_VAR
)
60 self
.params
= dict(request
.GET
.items())
61 if PAGE_VAR
in self
.params
:
62 del self
.params
[PAGE_VAR
]
63 if ERROR_FLAG
in self
.params
:
64 del self
.params
[ERROR_FLAG
]
67 self
.list_editable
= ()
69 self
.list_editable
= list_editable
70 self
.query
= request
.GET
.get(SEARCH_VAR
, '')
71 self
.query_set
= self
.get_query_set(request
)
72 self
.get_results(request
)
74 title
= ugettext('Select %s')
76 title
= ugettext('Select %s to change')
77 self
.title
= title
% force_unicode(self
.opts
.verbose_name
)
78 self
.pk_attname
= self
.lookup_opts
.pk
.attname
80 def get_filters(self
, request
):
81 lookup_params
= self
.params
.copy() # a dictionary of the query string
84 # Remove all the parameters that are globally and systematically
86 for ignored
in IGNORED_PARAMS
:
87 if ignored
in lookup_params
:
88 del lookup_params
[ignored
]
90 # Normalize the types of keys
91 for key
, value
in lookup_params
.items():
92 if not isinstance(key
, str):
93 # 'key' will be used as a keyword argument later, so Python
94 # requires it to be a string.
95 del lookup_params
[key
]
96 lookup_params
[smart_str(key
)] = value
98 if not self
.model_admin
.lookup_allowed(key
, value
):
99 raise SuspiciousOperation("Filtering by %s not allowed" % key
)
103 for list_filter
in self
.list_filter
:
104 if callable(list_filter
):
105 # This is simply a custom list filter class.
106 spec
= list_filter(request
, lookup_params
,
107 self
.model
, self
.model_admin
)
110 if isinstance(list_filter
, (tuple, list)):
111 # This is a custom FieldListFilter class for a given field.
112 field
, field_list_filter_class
= list_filter
114 # This is simply a field name, so use the default
115 # FieldListFilter class that has been registered for
116 # the type of the given field.
117 field
, field_list_filter_class
= list_filter
, FieldListFilter
.create
118 if not isinstance(field
, models
.Field
):
120 field
= get_fields_from_path(self
.model
, field_path
)[-1]
121 spec
= field_list_filter_class(field
, request
, lookup_params
,
122 self
.model
, self
.model_admin
, field_path
=field_path
)
123 # Check if we need to use distinct()
124 use_distinct
= (use_distinct
or
125 lookup_needs_distinct(self
.lookup_opts
,
127 if spec
and spec
.has_output():
128 filter_specs
.append(spec
)
130 # At this point, all the parameters used by the various ListFilters
131 # have been removed from lookup_params, which now only contains other
132 # parameters passed via the query string. We now loop through the
133 # remaining parameters both to ensure that all the parameters are valid
134 # fields and to determine if at least one of them needs distinct(). If
135 # the lookup parameters aren't real fields, then bail out.
137 for key
, value
in lookup_params
.items():
138 lookup_params
[key
] = prepare_lookup_value(key
, value
)
139 use_distinct
= (use_distinct
or
140 lookup_needs_distinct(self
.lookup_opts
, key
))
141 return filter_specs
, bool(filter_specs
), lookup_params
, use_distinct
142 except FieldDoesNotExist
, e
:
143 raise IncorrectLookupParameters(e
)
145 def get_query_string(self
, new_params
=None, remove
=None):
146 if new_params
is None: new_params
= {}
147 if remove
is None: remove
= []
148 p
= self
.params
.copy()
153 for k
, v
in new_params
.items():
159 return '?%s' % urlencode(p
)
161 def get_results(self
, request
):
162 paginator
= self
.model_admin
.get_paginator(request
, self
.query_set
, self
.list_per_page
)
163 # Get the number of objects, with admin filters applied.
164 result_count
= paginator
.count
166 # Get the total number of objects, with no admin filters applied.
167 # Perform a slight optimization: Check to see whether any filters were
168 # given. If not, use paginator.hits to calculate the number of objects,
169 # because we've already done paginator.hits and the value is cached.
170 if not self
.query_set
.query
.where
:
171 full_result_count
= result_count
173 full_result_count
= self
.root_query_set
.count()
175 can_show_all
= result_count
<= self
.list_max_show_all
176 multi_page
= result_count
> self
.list_per_page
178 # Get the list of objects to display on this page.
179 if (self
.show_all
and can_show_all
) or not multi_page
:
180 result_list
= self
.query_set
._clone
()
183 result_list
= paginator
.page(self
.page_num
+1).object_list
185 raise IncorrectLookupParameters
187 self
.result_count
= result_count
188 self
.full_result_count
= full_result_count
189 self
.result_list
= result_list
190 self
.can_show_all
= can_show_all
191 self
.multi_page
= multi_page
192 self
.paginator
= paginator
194 def _get_default_ordering(self
):
196 if self
.model_admin
.ordering
:
197 ordering
= self
.model_admin
.ordering
198 elif self
.lookup_opts
.ordering
:
199 ordering
= self
.lookup_opts
.ordering
202 def get_ordering_field(self
, field_name
):
204 Returns the proper model field name corresponding to the given
205 field_name to use for ordering. field_name may either be the name of a
206 proper model field or the name of a method (on the admin or model) or a
207 callable with the 'admin_order_field' attribute. Returns None if no
208 proper model field name can be matched.
211 field
= self
.lookup_opts
.get_field(field_name
)
213 except models
.FieldDoesNotExist
:
214 # See whether field_name is a name of a non-field
215 # that allows sorting.
216 if callable(field_name
):
218 elif hasattr(self
.model_admin
, field_name
):
219 attr
= getattr(self
.model_admin
, field_name
)
221 attr
= getattr(self
.model
, field_name
)
222 return getattr(attr
, 'admin_order_field', None)
224 def get_ordering(self
, request
, queryset
):
226 Returns the list of ordering fields for the change list.
227 First we check the get_ordering() method in model admin, then we check
228 the object's default ordering. Then, any manually-specified ordering
229 from the query string overrides anything. Finally, a deterministic
230 order is guaranteed by ensuring the primary key is used as the last
234 ordering
= list(self
.model_admin
.get_ordering(request
)
235 or self
._get
_default
_ordering
())
236 if ORDER_VAR
in params
:
237 # Clear ordering and used params
239 order_params
= params
[ORDER_VAR
].split('.')
240 for p
in order_params
:
242 none
, pfx
, idx
= p
.rpartition('-')
243 field_name
= self
.list_display
[int(idx
)]
244 order_field
= self
.get_ordering_field(field_name
)
246 continue # No 'admin_order_field', skip it
247 ordering
.append(pfx
+ order_field
)
248 except (IndexError, ValueError):
249 continue # Invalid ordering specified, skip it.
251 # Add the given query's ordering fields, if any.
252 ordering
.extend(queryset
.query
.order_by
)
254 # Ensure that the primary key is systematically present in the list of
255 # ordering fields so we can guarantee a deterministic order across all
257 pk_name
= self
.lookup_opts
.pk
.name
258 if not (set(ordering
) & set(['pk', '-pk', pk_name
, '-' + pk_name
])):
259 # The two sets do not intersect, meaning the pk isn't present. So
261 ordering
.append('-pk')
265 def get_ordering_field_columns(self
):
267 Returns a SortedDict of ordering field column numbers and asc/desc
270 # We must cope with more than one column having the same underlying sort
271 # field, so we base things on column numbers.
272 ordering
= self
._get
_default
_ordering
()
273 ordering_fields
= SortedDict()
274 if ORDER_VAR
not in self
.params
:
275 # for ordering specified on ModelAdmin or model Meta, we don't know
276 # the right column numbers absolutely, because there might be more
277 # than one column associated with that ordering, so we guess.
278 for field
in ordering
:
279 if field
.startswith('-'):
284 for index
, attr
in enumerate(self
.list_display
):
285 if self
.get_ordering_field(attr
) == field
:
286 ordering_fields
[index
] = order_type
289 for p
in self
.params
[ORDER_VAR
].split('.'):
290 none
, pfx
, idx
= p
.rpartition('-')
295 ordering_fields
[idx
] = 'desc' if pfx
== '-' else 'asc'
296 return ordering_fields
298 def get_query_set(self
, request
):
299 # First, we collect all the declared list filters.
300 (self
.filter_specs
, self
.has_filters
, remaining_lookup_params
,
301 use_distinct
) = self
.get_filters(request
)
303 # Then, we let every list filter modify the queryset to its liking.
304 qs
= self
.root_query_set
305 for filter_spec
in self
.filter_specs
:
306 new_qs
= filter_spec
.queryset(request
, qs
)
307 if new_qs
is not None:
311 # Finally, we apply the remaining lookup parameters from the query
312 # string (i.e. those that haven't already been processed by the
314 qs
= qs
.filter(**remaining_lookup_params
)
315 except (SuspiciousOperation
, ImproperlyConfigured
):
316 # Allow certain types of errors to be re-raised as-is so that the
317 # caller can treat them in a special way.
320 # Every other error is caught with a naked except, because we don't
321 # have any other way of validating lookup parameters. They might be
322 # invalid if the keyword arguments are incorrect, or if the values
323 # are not in the correct type, so we might get FieldError,
324 # ValueError, ValidationError, or ?.
325 raise IncorrectLookupParameters(e
)
327 # Use select_related() if one of the list_display options is a field
328 # with a relationship and the provided queryset doesn't already have
329 # select_related defined.
330 if not qs
.query
.select_related
:
331 if self
.list_select_related
:
332 qs
= qs
.select_related()
334 for field_name
in self
.list_display
:
336 field
= self
.lookup_opts
.get_field(field_name
)
337 except models
.FieldDoesNotExist
:
340 if isinstance(field
.rel
, models
.ManyToOneRel
):
341 qs
= qs
.select_related()
345 ordering
= self
.get_ordering(request
, qs
)
346 qs
= qs
.order_by(*ordering
)
348 # Apply keyword searches.
349 def construct_search(field_name
):
350 if field_name
.startswith('^'):
351 return "%s__istartswith" % field_name
[1:]
352 elif field_name
.startswith('='):
353 return "%s__iexact" % field_name
[1:]
354 elif field_name
.startswith('@'):
355 return "%s__search" % field_name
[1:]
357 return "%s__icontains" % field_name
359 if self
.search_fields
and self
.query
:
360 orm_lookups
= [construct_search(str(search_field
))
361 for search_field
in self
.search_fields
]
362 for bit
in self
.query
.split():
363 or_queries
= [models
.Q(**{orm_lookup
: bit
})
364 for orm_lookup
in orm_lookups
]
365 qs
= qs
.filter(reduce(operator
.or_
, or_queries
))
367 for search_spec
in orm_lookups
:
368 if lookup_needs_distinct(self
.lookup_opts
, search_spec
):
377 def url_for_result(self
, result
):
378 return "%s/" % quote(getattr(result
, self
.pk_attname
))