You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

393 lines
16 KiB

  1. import sys
  2. from collections import OrderedDict
  3. from django.contrib.admin import FieldListFilter
  4. from django.contrib.admin.exceptions import (
  5. DisallowedModelAdminLookup, DisallowedModelAdminToField,
  6. )
  7. from django.contrib.admin.options import (
  8. IS_POPUP_VAR, TO_FIELD_VAR, IncorrectLookupParameters,
  9. )
  10. from django.contrib.admin.utils import (
  11. get_fields_from_path, lookup_needs_distinct, prepare_lookup_value, quote,
  12. )
  13. from django.core.exceptions import (
  14. FieldDoesNotExist, ImproperlyConfigured, SuspiciousOperation,
  15. )
  16. from django.core.paginator import InvalidPage
  17. from django.core.urlresolvers import reverse
  18. from django.db import models
  19. from django.utils import six
  20. from django.utils.encoding import force_text
  21. from django.utils.http import urlencode
  22. from django.utils.translation import ugettext
  23. # Changelist settings
  24. ALL_VAR = 'all'
  25. ORDER_VAR = 'o'
  26. ORDER_TYPE_VAR = 'ot'
  27. PAGE_VAR = 'p'
  28. SEARCH_VAR = 'q'
  29. ERROR_FLAG = 'e'
  30. IGNORED_PARAMS = (
  31. ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR, TO_FIELD_VAR)
  32. class ChangeList(object):
  33. def __init__(self, request, model, list_display, list_display_links,
  34. list_filter, date_hierarchy, search_fields, list_select_related,
  35. list_per_page, list_max_show_all, list_editable, model_admin):
  36. self.model = model
  37. self.opts = model._meta
  38. self.lookup_opts = self.opts
  39. self.root_queryset = model_admin.get_queryset(request)
  40. self.list_display = list_display
  41. self.list_display_links = list_display_links
  42. self.list_filter = list_filter
  43. self.date_hierarchy = date_hierarchy
  44. self.search_fields = search_fields
  45. self.list_select_related = list_select_related
  46. self.list_per_page = list_per_page
  47. self.list_max_show_all = list_max_show_all
  48. self.model_admin = model_admin
  49. self.preserved_filters = model_admin.get_preserved_filters(request)
  50. # Get search parameters from the query string.
  51. try:
  52. self.page_num = int(request.GET.get(PAGE_VAR, 0))
  53. except ValueError:
  54. self.page_num = 0
  55. self.show_all = ALL_VAR in request.GET
  56. self.is_popup = IS_POPUP_VAR in request.GET
  57. to_field = request.GET.get(TO_FIELD_VAR)
  58. if to_field and not model_admin.to_field_allowed(request, to_field):
  59. raise DisallowedModelAdminToField("The field %s cannot be referenced." % to_field)
  60. self.to_field = to_field
  61. self.params = dict(request.GET.items())
  62. if PAGE_VAR in self.params:
  63. del self.params[PAGE_VAR]
  64. if ERROR_FLAG in self.params:
  65. 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.queryset = self.get_queryset(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_text(self.opts.verbose_name)
  78. self.pk_attname = self.lookup_opts.pk.attname
  79. def get_filters_params(self, params=None):
  80. """
  81. Returns all params except IGNORED_PARAMS
  82. """
  83. if not params:
  84. params = self.params
  85. lookup_params = params.copy() # a dictionary of the query string
  86. # Remove all the parameters that are globally and systematically
  87. # ignored.
  88. for ignored in IGNORED_PARAMS:
  89. if ignored in lookup_params:
  90. del lookup_params[ignored]
  91. return lookup_params
  92. def get_filters(self, request):
  93. lookup_params = self.get_filters_params()
  94. use_distinct = False
  95. for key, value in lookup_params.items():
  96. if not self.model_admin.lookup_allowed(key, value):
  97. raise DisallowedModelAdminLookup("Filtering by %s not allowed" % key)
  98. filter_specs = []
  99. if self.list_filter:
  100. for list_filter in self.list_filter:
  101. if callable(list_filter):
  102. # This is simply a custom list filter class.
  103. spec = list_filter(request, lookup_params,
  104. self.model, self.model_admin)
  105. else:
  106. field_path = None
  107. if isinstance(list_filter, (tuple, list)):
  108. # This is a custom FieldListFilter class for a given field.
  109. field, field_list_filter_class = list_filter
  110. else:
  111. # This is simply a field name, so use the default
  112. # FieldListFilter class that has been registered for
  113. # the type of the given field.
  114. field, field_list_filter_class = list_filter, FieldListFilter.create
  115. if not isinstance(field, models.Field):
  116. field_path = field
  117. field = get_fields_from_path(self.model, field_path)[-1]
  118. spec = field_list_filter_class(field, request, lookup_params,
  119. self.model, self.model_admin, field_path=field_path)
  120. # Check if we need to use distinct()
  121. use_distinct = (use_distinct or
  122. lookup_needs_distinct(self.lookup_opts,
  123. field_path))
  124. if spec and spec.has_output():
  125. filter_specs.append(spec)
  126. # At this point, all the parameters used by the various ListFilters
  127. # have been removed from lookup_params, which now only contains other
  128. # parameters passed via the query string. We now loop through the
  129. # remaining parameters both to ensure that all the parameters are valid
  130. # fields and to determine if at least one of them needs distinct(). If
  131. # the lookup parameters aren't real fields, then bail out.
  132. try:
  133. for key, value in lookup_params.items():
  134. lookup_params[key] = prepare_lookup_value(key, value)
  135. use_distinct = (use_distinct or
  136. lookup_needs_distinct(self.lookup_opts, key))
  137. return filter_specs, bool(filter_specs), lookup_params, use_distinct
  138. except FieldDoesNotExist as e:
  139. six.reraise(IncorrectLookupParameters, IncorrectLookupParameters(e), sys.exc_info()[2])
  140. def get_query_string(self, new_params=None, remove=None):
  141. if new_params is None:
  142. new_params = {}
  143. if remove is None:
  144. remove = []
  145. p = self.params.copy()
  146. for r in remove:
  147. for k in list(p):
  148. if k.startswith(r):
  149. del p[k]
  150. for k, v in new_params.items():
  151. if v is None:
  152. if k in p:
  153. del p[k]
  154. else:
  155. p[k] = v
  156. return '?%s' % urlencode(sorted(p.items()))
  157. def get_results(self, request):
  158. paginator = self.model_admin.get_paginator(request, self.queryset, self.list_per_page)
  159. # Get the number of objects, with admin filters applied.
  160. result_count = paginator.count
  161. # Get the total number of objects, with no admin filters applied.
  162. # Perform a slight optimization:
  163. # full_result_count is equal to paginator.count if no filters
  164. # were applied
  165. if self.model_admin.show_full_result_count:
  166. if self.get_filters_params() or self.params.get(SEARCH_VAR):
  167. full_result_count = self.root_queryset.count()
  168. else:
  169. full_result_count = result_count
  170. else:
  171. full_result_count = None
  172. can_show_all = result_count <= self.list_max_show_all
  173. multi_page = result_count > self.list_per_page
  174. # Get the list of objects to display on this page.
  175. if (self.show_all and can_show_all) or not multi_page:
  176. result_list = self.queryset._clone()
  177. else:
  178. try:
  179. result_list = paginator.page(self.page_num + 1).object_list
  180. except InvalidPage:
  181. raise IncorrectLookupParameters
  182. self.result_count = result_count
  183. self.show_full_result_count = self.model_admin.show_full_result_count
  184. # Admin actions are shown if there is at least one entry
  185. # or if entries are not counted because show_full_result_count is disabled
  186. self.show_admin_actions = not self.show_full_result_count or bool(full_result_count)
  187. self.full_result_count = full_result_count
  188. self.result_list = result_list
  189. self.can_show_all = can_show_all
  190. self.multi_page = multi_page
  191. self.paginator = paginator
  192. def _get_default_ordering(self):
  193. ordering = []
  194. if self.model_admin.ordering:
  195. ordering = self.model_admin.ordering
  196. elif self.lookup_opts.ordering:
  197. ordering = self.lookup_opts.ordering
  198. return ordering
  199. def get_ordering_field(self, field_name):
  200. """
  201. Returns the proper model field name corresponding to the given
  202. field_name to use for ordering. field_name may either be the name of a
  203. proper model field or the name of a method (on the admin or model) or a
  204. callable with the 'admin_order_field' attribute. Returns None if no
  205. proper model field name can be matched.
  206. """
  207. try:
  208. field = self.lookup_opts.get_field(field_name)
  209. return field.name
  210. except FieldDoesNotExist:
  211. # See whether field_name is a name of a non-field
  212. # that allows sorting.
  213. if callable(field_name):
  214. attr = field_name
  215. elif hasattr(self.model_admin, field_name):
  216. attr = getattr(self.model_admin, field_name)
  217. else:
  218. attr = getattr(self.model, field_name)
  219. return getattr(attr, 'admin_order_field', None)
  220. def get_ordering(self, request, queryset):
  221. """
  222. Returns the list of ordering fields for the change list.
  223. First we check the get_ordering() method in model admin, then we check
  224. the object's default ordering. Then, any manually-specified ordering
  225. from the query string overrides anything. Finally, a deterministic
  226. order is guaranteed by ensuring the primary key is used as the last
  227. ordering field.
  228. """
  229. params = self.params
  230. ordering = list(self.model_admin.get_ordering(request)
  231. or self._get_default_ordering())
  232. if ORDER_VAR in params:
  233. # Clear ordering and used params
  234. ordering = []
  235. order_params = params[ORDER_VAR].split('.')
  236. for p in order_params:
  237. try:
  238. none, pfx, idx = p.rpartition('-')
  239. field_name = self.list_display[int(idx)]
  240. order_field = self.get_ordering_field(field_name)
  241. if not order_field:
  242. continue # No 'admin_order_field', skip it
  243. # reverse order if order_field has already "-" as prefix
  244. if order_field.startswith('-') and pfx == "-":
  245. ordering.append(order_field[1:])
  246. else:
  247. ordering.append(pfx + order_field)
  248. except (IndexError, ValueError):
  249. continue # Invalid ordering specified, skip it.
  250. # Add the given query's ordering fields, if any.
  251. ordering.extend(queryset.query.order_by)
  252. # Ensure that the primary key is systematically present in the list of
  253. # ordering fields so we can guarantee a deterministic order across all
  254. # database backends.
  255. pk_name = self.lookup_opts.pk.name
  256. if not (set(ordering) & {'pk', '-pk', pk_name, '-' + pk_name}):
  257. # The two sets do not intersect, meaning the pk isn't present. So
  258. # we add it.
  259. ordering.append('-pk')
  260. return ordering
  261. def get_ordering_field_columns(self):
  262. """
  263. Returns an OrderedDict of ordering field column numbers and asc/desc
  264. """
  265. # We must cope with more than one column having the same underlying sort
  266. # field, so we base things on column numbers.
  267. ordering = self._get_default_ordering()
  268. ordering_fields = OrderedDict()
  269. if ORDER_VAR not in self.params:
  270. # for ordering specified on ModelAdmin or model Meta, we don't know
  271. # the right column numbers absolutely, because there might be more
  272. # than one column associated with that ordering, so we guess.
  273. for field in ordering:
  274. if field.startswith('-'):
  275. field = field[1:]
  276. order_type = 'desc'
  277. else:
  278. order_type = 'asc'
  279. for index, attr in enumerate(self.list_display):
  280. if self.get_ordering_field(attr) == field:
  281. ordering_fields[index] = order_type
  282. break
  283. else:
  284. for p in self.params[ORDER_VAR].split('.'):
  285. none, pfx, idx = p.rpartition('-')
  286. try:
  287. idx = int(idx)
  288. except ValueError:
  289. continue # skip it
  290. ordering_fields[idx] = 'desc' if pfx == '-' else 'asc'
  291. return ordering_fields
  292. def get_queryset(self, request):
  293. # First, we collect all the declared list filters.
  294. (self.filter_specs, self.has_filters, remaining_lookup_params,
  295. filters_use_distinct) = self.get_filters(request)
  296. # Then, we let every list filter modify the queryset to its liking.
  297. qs = self.root_queryset
  298. for filter_spec in self.filter_specs:
  299. new_qs = filter_spec.queryset(request, qs)
  300. if new_qs is not None:
  301. qs = new_qs
  302. try:
  303. # Finally, we apply the remaining lookup parameters from the query
  304. # string (i.e. those that haven't already been processed by the
  305. # filters).
  306. qs = qs.filter(**remaining_lookup_params)
  307. except (SuspiciousOperation, ImproperlyConfigured):
  308. # Allow certain types of errors to be re-raised as-is so that the
  309. # caller can treat them in a special way.
  310. raise
  311. except Exception as e:
  312. # Every other error is caught with a naked except, because we don't
  313. # have any other way of validating lookup parameters. They might be
  314. # invalid if the keyword arguments are incorrect, or if the values
  315. # are not in the correct type, so we might get FieldError,
  316. # ValueError, ValidationError, or ?.
  317. raise IncorrectLookupParameters(e)
  318. if not qs.query.select_related:
  319. qs = self.apply_select_related(qs)
  320. # Set ordering.
  321. ordering = self.get_ordering(request, qs)
  322. qs = qs.order_by(*ordering)
  323. # Apply search results
  324. qs, search_use_distinct = self.model_admin.get_search_results(
  325. request, qs, self.query)
  326. # Remove duplicates from results, if necessary
  327. if filters_use_distinct | search_use_distinct:
  328. return qs.distinct()
  329. else:
  330. return qs
  331. def apply_select_related(self, qs):
  332. if self.list_select_related is True:
  333. return qs.select_related()
  334. if self.list_select_related is False:
  335. if self.has_related_field_in_list_display():
  336. return qs.select_related()
  337. if self.list_select_related:
  338. return qs.select_related(*self.list_select_related)
  339. return qs
  340. def has_related_field_in_list_display(self):
  341. for field_name in self.list_display:
  342. try:
  343. field = self.lookup_opts.get_field(field_name)
  344. except FieldDoesNotExist:
  345. pass
  346. else:
  347. if isinstance(field.remote_field, models.ManyToOneRel):
  348. return True
  349. return False
  350. def url_for_result(self, result):
  351. pk = getattr(result, self.pk_attname)
  352. return reverse('admin:%s_%s_change' % (self.opts.app_label,
  353. self.opts.model_name),
  354. args=(quote(pk),),
  355. current_app=self.model_admin.admin_site.name)