root / env / lib / python2.7 / site-packages / django / contrib / admin / util.py @ 1a305335
History | View | Annotate | Download (14 KB)
1 | 1a305335 | officers | from django.db import models |
---|---|---|---|
2 | from django.db.models.sql.constants import LOOKUP_SEP |
||
3 | from django.db.models.deletion import Collector |
||
4 | from django.db.models.related import RelatedObject |
||
5 | from django.forms.forms import pretty_name |
||
6 | from django.utils import formats |
||
7 | from django.utils.html import escape |
||
8 | from django.utils.safestring import mark_safe |
||
9 | from django.utils.text import capfirst |
||
10 | from django.utils import timezone |
||
11 | from django.utils.encoding import force_unicode, smart_unicode, smart_str |
||
12 | from django.utils.translation import ungettext |
||
13 | from django.core.urlresolvers import reverse |
||
14 | |||
15 | def lookup_needs_distinct(opts, lookup_path): |
||
16 | """
|
||
17 | Returns True if 'distinct()' should be used to query the given lookup path.
|
||
18 | """
|
||
19 | field_name = lookup_path.split('__', 1)[0] |
||
20 | field = opts.get_field_by_name(field_name)[0]
|
||
21 | if ((hasattr(field, 'rel') and |
||
22 | isinstance(field.rel, models.ManyToManyRel)) or |
||
23 | (isinstance(field, models.related.RelatedObject) and |
||
24 | not field.field.unique)):
|
||
25 | return True |
||
26 | return False |
||
27 | |||
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 |