PageRenderTime 85ms CodeModel.GetById 44ms app.highlight 35ms RepoModel.GetById 1ms app.codeStats 0ms

/django/contrib/admin/util.py

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