Statistics
| Branch: | Revision:

root / env / lib / python2.7 / site-packages / django / contrib / admin / util.py @ 1a305335

History | View | Annotate | Download (14 KB)

1
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