|
1 from django.contrib.admin.filterspecs import FilterSpec |
|
2 from django.contrib.admin.options import IncorrectLookupParameters |
|
3 from django.contrib.admin.util import quote |
|
4 from django.core.paginator import Paginator, InvalidPage |
|
5 from django.db import models |
|
6 from django.db.models.query import QuerySet |
|
7 from django.utils.encoding import force_unicode, smart_str |
|
8 from django.utils.translation import ugettext |
|
9 from django.utils.http import urlencode |
|
10 import operator |
|
11 |
|
12 # The system will display a "Show all" link on the change list only if the |
|
13 # total result count is less than or equal to this setting. |
|
14 MAX_SHOW_ALL_ALLOWED = 200 |
|
15 |
|
16 # Changelist settings |
|
17 ALL_VAR = 'all' |
|
18 ORDER_VAR = 'o' |
|
19 ORDER_TYPE_VAR = 'ot' |
|
20 PAGE_VAR = 'p' |
|
21 SEARCH_VAR = 'q' |
|
22 TO_FIELD_VAR = 't' |
|
23 IS_POPUP_VAR = 'pop' |
|
24 ERROR_FLAG = 'e' |
|
25 |
|
26 # Text to display within change-list table cells if the value is blank. |
|
27 EMPTY_CHANGELIST_VALUE = '(None)' |
|
28 |
|
29 class ChangeList(object): |
|
30 def __init__(self, request, model, list_display, list_display_links, list_filter, date_hierarchy, search_fields, list_select_related, list_per_page, list_editable, model_admin): |
|
31 self.model = model |
|
32 self.opts = model._meta |
|
33 self.lookup_opts = self.opts |
|
34 self.root_query_set = model_admin.queryset(request) |
|
35 self.list_display = list_display |
|
36 self.list_display_links = list_display_links |
|
37 self.list_filter = list_filter |
|
38 self.date_hierarchy = date_hierarchy |
|
39 self.search_fields = search_fields |
|
40 self.list_select_related = list_select_related |
|
41 self.list_per_page = list_per_page |
|
42 self.list_editable = list_editable |
|
43 self.model_admin = model_admin |
|
44 |
|
45 # Get search parameters from the query string. |
|
46 try: |
|
47 self.page_num = int(request.GET.get(PAGE_VAR, 0)) |
|
48 except ValueError: |
|
49 self.page_num = 0 |
|
50 self.show_all = ALL_VAR in request.GET |
|
51 self.is_popup = IS_POPUP_VAR in request.GET |
|
52 self.to_field = request.GET.get(TO_FIELD_VAR) |
|
53 self.params = dict(request.GET.items()) |
|
54 if PAGE_VAR in self.params: |
|
55 del self.params[PAGE_VAR] |
|
56 if TO_FIELD_VAR in self.params: |
|
57 del self.params[TO_FIELD_VAR] |
|
58 if ERROR_FLAG in self.params: |
|
59 del self.params[ERROR_FLAG] |
|
60 |
|
61 self.order_field, self.order_type = self.get_ordering() |
|
62 self.query = request.GET.get(SEARCH_VAR, '') |
|
63 self.query_set = self.get_query_set() |
|
64 self.get_results(request) |
|
65 self.title = (self.is_popup and ugettext('Select %s') % force_unicode(self.opts.verbose_name) or ugettext('Select %s to change') % force_unicode(self.opts.verbose_name)) |
|
66 self.filter_specs, self.has_filters = self.get_filters(request) |
|
67 self.pk_attname = self.lookup_opts.pk.attname |
|
68 |
|
69 def get_filters(self, request): |
|
70 filter_specs = [] |
|
71 if self.list_filter: |
|
72 filter_fields = [self.lookup_opts.get_field(field_name) for field_name in self.list_filter] |
|
73 for f in filter_fields: |
|
74 spec = FilterSpec.create(f, request, self.params, self.model, self.model_admin) |
|
75 if spec and spec.has_output(): |
|
76 filter_specs.append(spec) |
|
77 return filter_specs, bool(filter_specs) |
|
78 |
|
79 def get_query_string(self, new_params=None, remove=None): |
|
80 if new_params is None: new_params = {} |
|
81 if remove is None: remove = [] |
|
82 p = self.params.copy() |
|
83 for r in remove: |
|
84 for k in p.keys(): |
|
85 if k.startswith(r): |
|
86 del p[k] |
|
87 for k, v in new_params.items(): |
|
88 if v is None: |
|
89 if k in p: |
|
90 del p[k] |
|
91 else: |
|
92 p[k] = v |
|
93 return '?%s' % urlencode(p) |
|
94 |
|
95 def get_results(self, request): |
|
96 paginator = Paginator(self.query_set, self.list_per_page) |
|
97 # Get the number of objects, with admin filters applied. |
|
98 result_count = paginator.count |
|
99 |
|
100 # Get the total number of objects, with no admin filters applied. |
|
101 # Perform a slight optimization: Check to see whether any filters were |
|
102 # given. If not, use paginator.hits to calculate the number of objects, |
|
103 # because we've already done paginator.hits and the value is cached. |
|
104 if not self.query_set.query.where: |
|
105 full_result_count = result_count |
|
106 else: |
|
107 full_result_count = self.root_query_set.count() |
|
108 |
|
109 can_show_all = result_count <= MAX_SHOW_ALL_ALLOWED |
|
110 multi_page = result_count > self.list_per_page |
|
111 |
|
112 # Get the list of objects to display on this page. |
|
113 if (self.show_all and can_show_all) or not multi_page: |
|
114 result_list = self.query_set._clone() |
|
115 else: |
|
116 try: |
|
117 result_list = paginator.page(self.page_num+1).object_list |
|
118 except InvalidPage: |
|
119 result_list = () |
|
120 |
|
121 self.result_count = result_count |
|
122 self.full_result_count = full_result_count |
|
123 self.result_list = result_list |
|
124 self.can_show_all = can_show_all |
|
125 self.multi_page = multi_page |
|
126 self.paginator = paginator |
|
127 |
|
128 def get_ordering(self): |
|
129 lookup_opts, params = self.lookup_opts, self.params |
|
130 # For ordering, first check the "ordering" parameter in the admin |
|
131 # options, then check the object's default ordering. If neither of |
|
132 # those exist, order descending by ID by default. Finally, look for |
|
133 # manually-specified ordering from the query string. |
|
134 ordering = self.model_admin.ordering or lookup_opts.ordering or ['-' + lookup_opts.pk.name] |
|
135 |
|
136 if ordering[0].startswith('-'): |
|
137 order_field, order_type = ordering[0][1:], 'desc' |
|
138 else: |
|
139 order_field, order_type = ordering[0], 'asc' |
|
140 if ORDER_VAR in params: |
|
141 try: |
|
142 field_name = self.list_display[int(params[ORDER_VAR])] |
|
143 try: |
|
144 f = lookup_opts.get_field(field_name) |
|
145 except models.FieldDoesNotExist: |
|
146 # See whether field_name is a name of a non-field |
|
147 # that allows sorting. |
|
148 try: |
|
149 if callable(field_name): |
|
150 attr = field_name |
|
151 elif hasattr(self.model_admin, field_name): |
|
152 attr = getattr(self.model_admin, field_name) |
|
153 else: |
|
154 attr = getattr(self.model, field_name) |
|
155 order_field = attr.admin_order_field |
|
156 except AttributeError: |
|
157 pass |
|
158 else: |
|
159 order_field = f.name |
|
160 except (IndexError, ValueError): |
|
161 pass # Invalid ordering specified. Just use the default. |
|
162 if ORDER_TYPE_VAR in params and params[ORDER_TYPE_VAR] in ('asc', 'desc'): |
|
163 order_type = params[ORDER_TYPE_VAR] |
|
164 return order_field, order_type |
|
165 |
|
166 def get_query_set(self): |
|
167 qs = self.root_query_set |
|
168 lookup_params = self.params.copy() # a dictionary of the query string |
|
169 for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR): |
|
170 if i in lookup_params: |
|
171 del lookup_params[i] |
|
172 for key, value in lookup_params.items(): |
|
173 if not isinstance(key, str): |
|
174 # 'key' will be used as a keyword argument later, so Python |
|
175 # requires it to be a string. |
|
176 del lookup_params[key] |
|
177 lookup_params[smart_str(key)] = value |
|
178 |
|
179 # if key ends with __in, split parameter into separate values |
|
180 if key.endswith('__in'): |
|
181 lookup_params[key] = value.split(',') |
|
182 |
|
183 # if key ends with __isnull, special case '' and false |
|
184 if key.endswith('__isnull'): |
|
185 if value.lower() in ('', 'false'): |
|
186 lookup_params[key] = False |
|
187 else: |
|
188 lookup_params[key] = True |
|
189 |
|
190 # Apply lookup parameters from the query string. |
|
191 try: |
|
192 qs = qs.filter(**lookup_params) |
|
193 # Naked except! Because we don't have any other way of validating "params". |
|
194 # They might be invalid if the keyword arguments are incorrect, or if the |
|
195 # values are not in the correct type, so we might get FieldError, ValueError, |
|
196 # ValicationError, or ? from a custom field that raises yet something else |
|
197 # when handed impossible data. |
|
198 except: |
|
199 raise IncorrectLookupParameters |
|
200 |
|
201 # Use select_related() if one of the list_display options is a field |
|
202 # with a relationship and the provided queryset doesn't already have |
|
203 # select_related defined. |
|
204 if not qs.query.select_related: |
|
205 if self.list_select_related: |
|
206 qs = qs.select_related() |
|
207 else: |
|
208 for field_name in self.list_display: |
|
209 try: |
|
210 f = self.lookup_opts.get_field(field_name) |
|
211 except models.FieldDoesNotExist: |
|
212 pass |
|
213 else: |
|
214 if isinstance(f.rel, models.ManyToOneRel): |
|
215 qs = qs.select_related() |
|
216 break |
|
217 |
|
218 # Set ordering. |
|
219 if self.order_field: |
|
220 qs = qs.order_by('%s%s' % ((self.order_type == 'desc' and '-' or ''), self.order_field)) |
|
221 |
|
222 # Apply keyword searches. |
|
223 def construct_search(field_name): |
|
224 if field_name.startswith('^'): |
|
225 return "%s__istartswith" % field_name[1:] |
|
226 elif field_name.startswith('='): |
|
227 return "%s__iexact" % field_name[1:] |
|
228 elif field_name.startswith('@'): |
|
229 return "%s__search" % field_name[1:] |
|
230 else: |
|
231 return "%s__icontains" % field_name |
|
232 |
|
233 if self.search_fields and self.query: |
|
234 for bit in self.query.split(): |
|
235 or_queries = [models.Q(**{construct_search(str(field_name)): bit}) for field_name in self.search_fields] |
|
236 qs = qs.filter(reduce(operator.or_, or_queries)) |
|
237 for field_name in self.search_fields: |
|
238 if '__' in field_name: |
|
239 qs = qs.distinct() |
|
240 break |
|
241 |
|
242 return qs |
|
243 |
|
244 def url_for_result(self, result): |
|
245 return "%s/" % quote(getattr(result, self.pk_attname)) |