App Engine Python SDK version 1.7.4 (2)
[gae.git] / python / lib / django_1_4 / django / contrib / admin / views / main.py
blob9d5c30434d7388c65e8bfec3433b5cac7caebda2
1 import operator
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)
17 # Changelist settings
18 ALL_VAR = 'all'
19 ORDER_VAR = 'o'
20 ORDER_TYPE_VAR = 'ot'
21 PAGE_VAR = 'p'
22 SEARCH_VAR = 'q'
23 TO_FIELD_VAR = 't'
24 IS_POPUP_VAR = 'pop'
25 ERROR_FLAG = 'e'
27 IGNORED_PARAMS = (
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):
38 self.model = model
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.
53 try:
54 self.page_num = int(request.GET.get(PAGE_VAR, 0))
55 except ValueError:
56 self.page_num = 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]
66 if self.is_popup:
67 self.list_editable = ()
68 else:
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)
73 if self.is_popup:
74 title = ugettext('Select %s')
75 else:
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
82 use_distinct = False
84 # Remove all the parameters that are globally and systematically
85 # ignored.
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)
101 filter_specs = []
102 if self.list_filter:
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)
108 else:
109 field_path = None
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
113 else:
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):
119 field_path = 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,
126 field_path))
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.
136 try:
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()
149 for r in remove:
150 for k in p.keys():
151 if k.startswith(r):
152 del p[k]
153 for k, v in new_params.items():
154 if v is None:
155 if k in p:
156 del p[k]
157 else:
158 p[k] = v
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
172 else:
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()
181 else:
182 try:
183 result_list = paginator.page(self.page_num+1).object_list
184 except InvalidPage:
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):
195 ordering = []
196 if self.model_admin.ordering:
197 ordering = self.model_admin.ordering
198 elif self.lookup_opts.ordering:
199 ordering = self.lookup_opts.ordering
200 return 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.
210 try:
211 field = self.lookup_opts.get_field(field_name)
212 return 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):
217 attr = field_name
218 elif hasattr(self.model_admin, field_name):
219 attr = getattr(self.model_admin, field_name)
220 else:
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
231 ordering field.
233 params = self.params
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
238 ordering = []
239 order_params = params[ORDER_VAR].split('.')
240 for p in order_params:
241 try:
242 none, pfx, idx = p.rpartition('-')
243 field_name = self.list_display[int(idx)]
244 order_field = self.get_ordering_field(field_name)
245 if not order_field:
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
256 # database backends.
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
260 # we add it.
261 ordering.append('-pk')
263 return ordering
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('-'):
280 field = field[1:]
281 order_type = 'desc'
282 else:
283 order_type = 'asc'
284 for index, attr in enumerate(self.list_display):
285 if self.get_ordering_field(attr) == field:
286 ordering_fields[index] = order_type
287 break
288 else:
289 for p in self.params[ORDER_VAR].split('.'):
290 none, pfx, idx = p.rpartition('-')
291 try:
292 idx = int(idx)
293 except ValueError:
294 continue # skip it
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:
308 qs = new_qs
310 try:
311 # Finally, we apply the remaining lookup parameters from the query
312 # string (i.e. those that haven't already been processed by the
313 # filters).
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.
318 raise
319 except Exception, e:
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()
333 else:
334 for field_name in self.list_display:
335 try:
336 field = self.lookup_opts.get_field(field_name)
337 except models.FieldDoesNotExist:
338 pass
339 else:
340 if isinstance(field.rel, models.ManyToOneRel):
341 qs = qs.select_related()
342 break
344 # Set ordering.
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:]
356 else:
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))
366 if not use_distinct:
367 for search_spec in orm_lookups:
368 if lookup_needs_distinct(self.lookup_opts, search_spec):
369 use_distinct = True
370 break
372 if use_distinct:
373 return qs.distinct()
374 else:
375 return qs
377 def url_for_result(self, result):
378 return "%s/" % quote(getattr(result, self.pk_attname))