28 | | def prepare_lookup_value(key, value): |
29 | | """ |
30 | | Returns a lookup value prepared to be used in queryset filtering. |
31 | | """ |
32 | | # if key ends with __in, split parameter into separate values |
33 | | if key.endswith('__in'): |
34 | | value = value.split(',') |
35 | | # if key ends with __isnull, special case '' and false |
36 | | if key.endswith('__isnull'): |
37 | | if value.lower() in ('', 'false'): |
38 | | value = False |
39 | | else: |
40 | | value = True |
41 | | return value |
42 | | |
43 | | def quote(s): |
44 | | """ |
45 | | Ensure that primary key values do not confuse the admin URLs by escaping |
46 | | any '/', '_' and ':' characters. Similar to urllib.quote, except that the |
47 | | quoting is slightly different so that it doesn't get automatically |
48 | | unquoted by the Web browser. |
49 | | """ |
50 | | if not isinstance(s, basestring): |
51 | | return s |
52 | | res = list(s) |
53 | | for i in range(len(res)): |
54 | | c = res[i] |
55 | | if c in """:/_#?;@&=+$,"<>%\\""": |
56 | | res[i] = '_%02X' % ord(c) |
57 | | return ''.join(res) |
58 | | |
59 | | |
60 | | def unquote(s): |
61 | | """ |
62 | | Undo the effects of quote(). Based heavily on urllib.unquote(). |
63 | | """ |
64 | | mychr = chr |
65 | | myatoi = int |
66 | | list = s.split('_') |
67 | | res = [list[0]] |
68 | | myappend = res.append |
69 | | del list[0] |
70 | | for item in list: |
71 | | if item[1:2]: |
72 | | try: |
73 | | myappend(mychr(myatoi(item[:2], 16)) + item[2:]) |
74 | | except ValueError: |
75 | | myappend('_' + item) |
76 | | else: |
77 | | myappend('_' + item) |
78 | | return "".join(res) |
79 | | |
80 | | |
81 | | def flatten_fieldsets(fieldsets): |
82 | | """Returns a list of field names from an admin fieldsets structure.""" |
83 | | field_names = [] |
84 | | for name, opts in fieldsets: |
85 | | for field in opts['fields']: |
86 | | # type checking feels dirty, but it seems like the best way here |
87 | | if type(field) == tuple: |
88 | | field_names.extend(field) |
89 | | else: |
90 | | field_names.append(field) |
91 | | return field_names |
92 | | |
93 | | |
94 | | def get_deleted_objects(objs, opts, user, admin_site, using): |
95 | | """ |
96 | | Find all objects related to ``objs`` that should also be deleted. ``objs`` |
97 | | must be a homogenous iterable of objects (e.g. a QuerySet). |
98 | | |
99 | | Returns a nested list of strings suitable for display in the |
100 | | template with the ``unordered_list`` filter. |
101 | | |
102 | | """ |
103 | | collector = NestedObjects(using=using) |
104 | | collector.collect(objs) |
105 | | perms_needed = set() |
106 | | |
107 | | def format_callback(obj): |
108 | | has_admin = obj.__class__ in admin_site._registry |
109 | | opts = obj._meta |
110 | | |
111 | | if has_admin: |
112 | | admin_url = reverse('%s:%s_%s_change' |
113 | | % (admin_site.name, |
114 | | opts.app_label, |
115 | | opts.object_name.lower()), |
116 | | None, (quote(obj._get_pk_val()),)) |
117 | | p = '%s.%s' % (opts.app_label, |
118 | | opts.get_delete_permission()) |
119 | | if not user.has_perm(p): |
120 | | perms_needed.add(opts.verbose_name) |
121 | | # Display a link to the admin page. |
122 | | return mark_safe(u'%s: <a href="%s">%s</a>' % |
123 | | (escape(capfirst(opts.verbose_name)), |
124 | | admin_url, |
125 | | escape(obj))) |
126 | | else: |
127 | | # Don't display link to edit, because it either has no |
128 | | # admin or is edited inline. |
129 | | return u'%s: %s' % (capfirst(opts.verbose_name), |
130 | | force_unicode(obj)) |
131 | | |
132 | | to_delete = collector.nested(format_callback) |
133 | | |
134 | | protected = [format_callback(obj) for obj in collector.protected] |
135 | | |
136 | | return to_delete, perms_needed, protected |
137 | | |
138 | | |
139 | | class NestedObjects(Collector): |
140 | | def __init__(self, *args, **kwargs): |
141 | | super(NestedObjects, self).__init__(*args, **kwargs) |
142 | | self.edges = {} # {from_instance: [to_instances]} |
143 | | self.protected = set() |
144 | | |
145 | | def add_edge(self, source, target): |
146 | | self.edges.setdefault(source, []).append(target) |
147 | | |
148 | | def collect(self, objs, source_attr=None, **kwargs): |
149 | | for obj in objs: |
150 | | if source_attr: |
151 | | self.add_edge(getattr(obj, source_attr), obj) |
152 | | else: |
153 | | self.add_edge(None, obj) |
154 | | try: |
155 | | return super(NestedObjects, self).collect(objs, source_attr=source_attr, **kwargs) |
156 | | except models.ProtectedError, e: |
157 | | self.protected.update(e.protected_objects) |
158 | | |
159 | | def related_objects(self, related, objs): |
160 | | qs = super(NestedObjects, self).related_objects(related, objs) |
161 | | return qs.select_related(related.field.name) |
162 | | |
163 | | def _nested(self, obj, seen, format_callback): |
164 | | if obj in seen: |
165 | | return [] |
166 | | seen.add(obj) |
167 | | children = [] |
168 | | for child in self.edges.get(obj, ()): |
169 | | children.extend(self._nested(child, seen, format_callback)) |
170 | | if format_callback: |
171 | | ret = [format_callback(obj)] |
172 | | else: |
173 | | ret = [obj] |
174 | | if children: |
175 | | ret.append(children) |
176 | | return ret |
177 | | |
178 | | def nested(self, format_callback=None): |
179 | | """ |
180 | | Return the graph as a nested list. |
181 | | |
182 | | """ |
183 | | seen = set() |
184 | | roots = [] |
185 | | for root in self.edges.get(None, ()): |
186 | | roots.extend(self._nested(root, seen, format_callback)) |
187 | | return roots |
188 | | |
189 | | |
190 | | def model_format_dict(obj): |
191 | | """ |
192 | | Return a `dict` with keys 'verbose_name' and 'verbose_name_plural', |
193 | | typically for use with string formatting. |
194 | | |
195 | | `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance. |
196 | | |
197 | | """ |
198 | | if isinstance(obj, (models.Model, models.base.ModelBase)): |
199 | | opts = obj._meta |
200 | | elif isinstance(obj, models.query.QuerySet): |
201 | | opts = obj.model._meta |
202 | | else: |
203 | | opts = obj |
204 | | return { |
205 | | 'verbose_name': force_unicode(opts.verbose_name), |
206 | | 'verbose_name_plural': force_unicode(opts.verbose_name_plural) |
207 | | } |
208 | | |
209 | | |
210 | | def model_ngettext(obj, n=None): |
211 | | """ |
212 | | Return the appropriate `verbose_name` or `verbose_name_plural` value for |
213 | | `obj` depending on the count `n`. |
214 | | |
215 | | `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance. |
216 | | If `obj` is a `QuerySet` instance, `n` is optional and the length of the |
217 | | `QuerySet` is used. |
218 | | |
219 | | """ |
220 | | if isinstance(obj, models.query.QuerySet): |
221 | | if n is None: |
222 | | n = obj.count() |
223 | | obj = obj.model |
224 | | d = model_format_dict(obj) |
225 | | singular, plural = d["verbose_name"], d["verbose_name_plural"] |
226 | | return ungettext(singular, plural, n or 0) |
227 | | |
228 | | |
229 | | def lookup_field(name, obj, model_admin=None): |
230 | | opts = obj._meta |
231 | | try: |
232 | | f = opts.get_field(name) |
233 | | except models.FieldDoesNotExist: |
234 | | # For non-field values, the value is either a method, property or |
235 | | # returned via a callable. |
236 | | if callable(name): |
237 | | attr = name |
238 | | value = attr(obj) |
239 | | elif (model_admin is not None and hasattr(model_admin, name) and |
240 | | not name == '__str__' and not name == '__unicode__'): |
241 | | attr = getattr(model_admin, name) |
242 | | value = attr(obj) |
243 | | else: |
244 | | attr = getattr(obj, name) |
245 | | if callable(attr): |
246 | | value = attr() |
247 | | else: |
248 | | value = attr |
249 | | f = None |
250 | | else: |
251 | | attr = None |
252 | | value = getattr(obj, name) |
253 | | return f, attr, value |
254 | | |
255 | | |
256 | | def label_for_field(name, model, model_admin=None, return_attr=False): |
257 | | """ |
258 | | Returns a sensible label for a field name. The name can be a callable or the |
259 | | name of an object attributes, as well as a genuine fields. If return_attr is |
260 | | True, the resolved attribute (which could be a callable) is also returned. |
261 | | This will be None if (and only if) the name refers to a field. |
262 | | """ |
263 | | attr = None |
264 | | try: |
265 | | field = model._meta.get_field_by_name(name)[0] |
266 | | if isinstance(field, RelatedObject): |
267 | | label = field.opts.verbose_name |
268 | | else: |
269 | | label = field.verbose_name |
270 | | except models.FieldDoesNotExist: |
271 | | if name == "__unicode__": |
272 | | label = force_unicode(model._meta.verbose_name) |
273 | | attr = unicode |
274 | | elif name == "__str__": |
275 | | label = smart_str(model._meta.verbose_name) |
276 | | attr = str |
277 | | else: |
278 | | if callable(name): |
279 | | attr = name |
280 | | elif model_admin is not None and hasattr(model_admin, name): |
281 | | attr = getattr(model_admin, name) |
282 | | elif hasattr(model, name): |
283 | | attr = getattr(model, name) |
284 | | else: |
285 | | message = "Unable to lookup '%s' on %s" % (name, model._meta.object_name) |
286 | | if model_admin: |
287 | | message += " or %s" % (model_admin.__class__.__name__,) |
288 | | raise AttributeError(message) |
289 | | |
290 | | if hasattr(attr, "short_description"): |
291 | | label = attr.short_description |
292 | | elif callable(attr): |
293 | | if attr.__name__ == "<lambda>": |
294 | | label = "--" |
295 | | else: |
296 | | label = pretty_name(attr.__name__) |
297 | | else: |
298 | | label = pretty_name(name) |
299 | | if return_attr: |
300 | | return (label, attr) |
301 | | else: |
302 | | return label |
303 | | |
304 | | def help_text_for_field(name, model): |
305 | | try: |
306 | | help_text = model._meta.get_field_by_name(name)[0].help_text |
307 | | except models.FieldDoesNotExist: |
308 | | help_text = "" |
309 | | return smart_unicode(help_text) |
310 | | |
311 | | |
312 | | def display_for_field(value, field): |
313 | | from django.contrib.admin.templatetags.admin_list import _boolean_icon |
314 | | from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE |
315 | | |
316 | | if field.flatchoices: |
317 | | return dict(field.flatchoices).get(value, EMPTY_CHANGELIST_VALUE) |
318 | | # NullBooleanField needs special-case null-handling, so it comes |
319 | | # before the general null test. |
320 | | elif isinstance(field, models.BooleanField) or isinstance(field, models.NullBooleanField): |
321 | | return _boolean_icon(value) |
322 | | elif value is None: |
323 | | return EMPTY_CHANGELIST_VALUE |
324 | | elif isinstance(field, models.DateTimeField): |
325 | | return formats.localize(timezone.localtime(value)) |
326 | | elif isinstance(field, models.DateField) or isinstance(field, models.TimeField): |
327 | | return formats.localize(value) |
328 | | elif isinstance(field, models.DecimalField): |
329 | | return formats.number_format(value, field.decimal_places) |
330 | | elif isinstance(field, models.FloatField): |
331 | | return formats.number_format(value) |
332 | | else: |
333 | | return smart_unicode(value) |
334 | | |
335 | | |
336 | | class NotRelationField(Exception): |
337 | | pass |
338 | | |
339 | | |
340 | | def get_model_from_relation(field): |
341 | | if isinstance(field, models.related.RelatedObject): |
342 | | return field.model |
343 | | elif getattr(field, 'rel'): # or isinstance? |
344 | | return field.rel.to |
345 | | else: |
346 | | raise NotRelationField |
347 | | |
348 | | |
349 | | def reverse_field_path(model, path): |
350 | | """ Create a reversed field path. |
351 | | |
352 | | E.g. Given (Order, "user__groups"), |
353 | | return (Group, "user__order"). |
354 | | |
355 | | Final field must be a related model, not a data field. |
356 | | |
357 | | """ |
358 | | reversed_path = [] |
359 | | parent = model |
360 | | pieces = path.split(LOOKUP_SEP) |
361 | | for piece in pieces: |
362 | | field, model, direct, m2m = parent._meta.get_field_by_name(piece) |
363 | | # skip trailing data field if extant: |
364 | | if len(reversed_path) == len(pieces)-1: # final iteration |
365 | | try: |
366 | | get_model_from_relation(field) |
367 | | except NotRelationField: |
368 | | break |
369 | | if direct: |
370 | | related_name = field.related_query_name() |
371 | | parent = field.rel.to |
372 | | else: |
373 | | related_name = field.field.name |
374 | | parent = field.model |
375 | | reversed_path.insert(0, related_name) |
376 | | return (parent, LOOKUP_SEP.join(reversed_path)) |
377 | | |
378 | | |
379 | | def get_fields_from_path(model, path): |
380 | | """ Return list of Fields given path relative to model. |
381 | | |
382 | | e.g. (ModelX, "user__groups__name") -> [ |
383 | | <django.db.models.fields.related.ForeignKey object at 0x...>, |
384 | | <django.db.models.fields.related.ManyToManyField object at 0x...>, |
385 | | <django.db.models.fields.CharField object at 0x...>, |
386 | | ] |
387 | | """ |
388 | | pieces = path.split(LOOKUP_SEP) |
389 | | fields = [] |
390 | | for piece in pieces: |
391 | | if fields: |
392 | | parent = get_model_from_relation(fields[-1]) |
393 | | else: |
394 | | parent = model |
395 | | fields.append(parent._meta.get_field_by_name(piece)[0]) |
396 | | return fields |
397 | | |
398 | | |
399 | | def remove_trailing_data_field(fields): |
400 | | """ Discard trailing non-relation field if extant. """ |
401 | | try: |
402 | | get_model_from_relation(fields[-1]) |
403 | | except NotRelationField: |
404 | | fields = fields[:-1] |
405 | | return fields |
406 | | |
407 | | |
408 | | def get_limit_choices_to_from_path(model, path): |
409 | | """ Return Q object for limiting choices if applicable. |
410 | | |
411 | | If final model in path is linked via a ForeignKey or ManyToManyField which |
412 | | has a `limit_choices_to` attribute, return it as a Q object. |
413 | | """ |
414 | | |
415 | | fields = get_fields_from_path(model, path) |
416 | | fields = remove_trailing_data_field(fields) |
417 | | limit_choices_to = ( |
418 | | fields and hasattr(fields[-1], 'rel') and |
419 | | getattr(fields[-1].rel, 'limit_choices_to', None)) |
420 | | if not limit_choices_to: |
421 | | return models.Q() # empty Q |
422 | | elif isinstance(limit_choices_to, models.Q): |
423 | | return limit_choices_to # already a Q |
424 | | else: |
425 | | return models.Q(**limit_choices_to) # convert dict to Q |
| 7 | from django.contrib.admin.utils import * |