Ticket #17627: 0001-17627-renaming-contrib.admin.util-contrib.admin.util.patch

File 0001-17627-renaming-contrib.admin.util-contrib.admin.util.patch, 60.4 KB (added by viciu, 3 years ago)

Patch + deprecation warning + backwards compatibility import

  • AUTHORS

    From 3d244b1ec350cf85572520bd6dacfa245c4e6292 Mon Sep 17 00:00:00 2001
    From: Wiktor Kolodziej <wiktor@pykonik.org>
    Date: Sun, 5 Feb 2012 15:46:51 +0100
    Subject: [PATCH] Fixing #17627: renaming contrib.admin.util -> contrib.admin.utils; renaming also corresponding regression tests; remaining backwards-compatibile
    
    ---
     AUTHORS                                         |    1 +
     django/contrib/admin/actions.py                 |    2 +-
     django/contrib/admin/filters.py                 |    2 +-
     django/contrib/admin/helpers.py                 |    2 +-
     django/contrib/admin/models.py                  |    2 +-
     django/contrib/admin/options.py                 |    2 +-
     django/contrib/admin/templatetags/admin_list.py |    2 +-
     django/contrib/admin/util.py                    |  429 +----------------------
     django/contrib/admin/utils.py                   |  425 ++++++++++++++++++++++
     django/contrib/admin/validation.py              |    2 +-
     django/contrib/admin/views/main.py              |    2 +-
     django/contrib/auth/admin.py                    |    2 +-
     tests/regressiontests/admin_util/models.py      |   38 --
     tests/regressiontests/admin_util/tests.py       |  286 ---------------
     tests/regressiontests/admin_utils/models.py     |   38 ++
     tests/regressiontests/admin_utils/tests.py      |  286 +++++++++++++++
     tests/regressiontests/admin_views/tests.py      |    2 +-
     17 files changed, 767 insertions(+), 756 deletions(-)
     create mode 100644 django/contrib/admin/utils.py
     delete mode 100644 tests/regressiontests/admin_util/__init__.py
     delete mode 100644 tests/regressiontests/admin_util/models.py
     delete mode 100644 tests/regressiontests/admin_util/tests.py
     create mode 100644 tests/regressiontests/admin_utils/__init__.py
     create mode 100644 tests/regressiontests/admin_utils/models.py
     create mode 100644 tests/regressiontests/admin_utils/tests.py
    
    diff --git a/AUTHORS b/AUTHORS
    index 9ffd6cd..253be88 100644
    a b answer newbie questions, and generally made Django that much better: 
    442442    Alex Robbins <alexander.j.robbins@gmail.com>
    443443    Henrique Romano <onaiort@gmail.com>
    444444    Armin Ronacher
     445    Wiktor Kołodziej
    445446    Daniel Roseman <http://roseman.org.uk/>
    446447    Rozza <ross.lawley@gmail.com>
    447448    Oliver Rutherfurd <http://rutherfurd.net/>
  • django/contrib/admin/actions.py

    diff --git a/django/contrib/admin/actions.py b/django/contrib/admin/actions.py
    index 5b56402..4a03a97 100644
    a b Built-in, globally-available admin actions. 
    44
    55from django.core.exceptions import PermissionDenied
    66from django.contrib.admin import helpers
    7 from django.contrib.admin.util import get_deleted_objects, model_ngettext
     7from django.contrib.admin.utils import get_deleted_objects, model_ngettext
    88from django.db import router
    99from django.template.response import TemplateResponse
    1010from django.utils.encoding import force_unicode
  • django/contrib/admin/filters.py

    diff --git a/django/contrib/admin/filters.py b/django/contrib/admin/filters.py
    index b2d74ce..ac210fd 100644
    a b from django.core.exceptions import ImproperlyConfigured 
    1212from django.utils.encoding import smart_unicode
    1313from django.utils.translation import ugettext_lazy as _
    1414
    15 from django.contrib.admin.util import (get_model_from_relation,
     15from django.contrib.admin.utils import (get_model_from_relation,
    1616    reverse_field_path, get_limit_choices_to_from_path, prepare_lookup_value)
    1717
    1818class ListFilter(object):
  • django/contrib/admin/helpers.py

    diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py
    index b08d1c8..eb5b001 100644
    a b  
    11from django import forms
    2 from django.contrib.admin.util import (flatten_fieldsets, lookup_field,
     2from django.contrib.admin.utils import (flatten_fieldsets, lookup_field,
    33    display_for_field, label_for_field, help_text_for_field)
    44from django.contrib.admin.templatetags.admin_static import static
    55from django.contrib.contenttypes.models import ContentType
  • django/contrib/admin/models.py

    diff --git a/django/contrib/admin/models.py b/django/contrib/admin/models.py
    index 0e5b8a7..013ae44 100644
    a b  
    11from django.db import models
    22from django.contrib.contenttypes.models import ContentType
    33from django.contrib.auth.models import User
    4 from django.contrib.admin.util import quote
     4from django.contrib.admin.utils import quote
    55from django.utils.translation import ugettext_lazy as _
    66from django.utils.encoding import smart_unicode
    77from django.utils.safestring import mark_safe
  • django/contrib/admin/options.py

    diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
    index 5bfa891..6bedeec 100644
    a b from django.forms.models import (modelform_factory, modelformset_factory, 
    66    inlineformset_factory, BaseInlineFormSet)
    77from django.contrib.contenttypes.models import ContentType
    88from django.contrib.admin import widgets, helpers
    9 from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_format_dict
     9from django.contrib.admin.utils import unquote, flatten_fieldsets, get_deleted_objects, model_format_dict
    1010from django.contrib.admin.templatetags.admin_static import static
    1111from django.contrib import messages
    1212from django.views.decorators.csrf import csrf_protect
  • django/contrib/admin/templatetags/admin_list.py

    diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
    index ba3d132..aad7e47 100644
    a b  
    11import datetime
    22
    3 from django.contrib.admin.util import lookup_field, display_for_field, label_for_field
     3from django.contrib.admin.utils import lookup_field, display_for_field, label_for_field
    44from django.contrib.admin.views.main import (ALL_VAR, EMPTY_CHANGELIST_VALUE,
    55    ORDER_VAR, PAGE_VAR, SEARCH_VAR)
    66from django.contrib.admin.templatetags.admin_static import static
  • django/contrib/admin/util.py

    diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py
    index 61182a6..5ec137e 100644
    a b  
    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
     1#encoding: utf-8
    142
    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
     3import warnings
    274
    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
     5warnings.warn("The django.contrib.admin.util module has been renamed "
     6              "(https://code.djangoproject.com/ticket/17627). "
     7              "Use django.contrib.admin.utils instead.", DeprecationWarning)
    428
    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
     9#for backwards compatibility in 1.4.x versions
     10from utils import *
  • new file django/contrib/admin/utils.py

    diff --git a/django/contrib/admin/utils.py b/django/contrib/admin/utils.py
    new file mode 100644
    index 0000000..61182a6
    - +  
     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 import timezone
     11from django.utils.encoding import force_unicode, smart_unicode, smart_str
     12from django.utils.translation import ungettext
     13from django.core.urlresolvers import reverse
     14
     15def 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
     28def 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
     43def 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
     60def 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
     81def 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
     94def 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
     139class 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
     190def 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
     210def 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
     229def 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
     256def 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
     304def 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
     312def 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
     336class NotRelationField(Exception):
     337    pass
     338
     339
     340def 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
     349def 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
     379def 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
     399def 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
     408def 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
  • django/contrib/admin/validation.py

    diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
    index 733f89d..bd99b84 100644
    a b from django.db.models.fields import FieldDoesNotExist 
    44from django.forms.models import (BaseModelForm, BaseModelFormSet, fields_for_model,
    55    _get_foreign_key)
    66from django.contrib.admin import ListFilter, FieldListFilter
    7 from django.contrib.admin.util import get_fields_from_path, NotRelationField
     7from django.contrib.admin.utils import get_fields_from_path, NotRelationField
    88from django.contrib.admin.options import (flatten_fieldsets, BaseModelAdmin,
    99    HORIZONTAL, VERTICAL)
    1010
  • django/contrib/admin/views/main.py

    diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
    index 32113f5..cb18a3a 100644
    a b from django.utils.http import urlencode 
    1010
    1111from django.contrib.admin import FieldListFilter
    1212from django.contrib.admin.options import IncorrectLookupParameters
    13 from django.contrib.admin.util import (quote, get_fields_from_path,
     13from django.contrib.admin.utils import (quote, get_fields_from_path,
    1414    lookup_needs_distinct, prepare_lookup_value)
    1515
    1616# Changelist settings
  • django/contrib/auth/admin.py

    diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py
    index 5e9ac9d..1376d3e 100644
    a b class UserAdmin(admin.ModelAdmin): 
    7070        if obj is None:
    7171            defaults.update({
    7272                'form': self.add_form,
    73                 'fields': admin.util.flatten_fieldsets(self.add_fieldsets),
     73                'fields': admin.utils.flatten_fieldsets(self.add_fieldsets),
    7474            })
    7575        defaults.update(kwargs)
    7676        return super(UserAdmin, self).get_form(request, obj, **defaults)
  • deleted file tests/regressiontests/admin_util/models.py

    diff --git a/tests/regressiontests/admin_util/__init__.py b/tests/regressiontests/admin_util/__init__.py
    deleted file mode 100644
    index e69de29..0000000
    diff --git a/tests/regressiontests/admin_util/models.py b/tests/regressiontests/admin_util/models.py
    deleted file mode 100644
    index 0e81df3..0000000
    + -  
    1 from django.db import models
    2 
    3 
    4 class Article(models.Model):
    5     """
    6     A simple Article model for testing
    7     """
    8     site = models.ForeignKey('sites.Site', related_name="admin_articles")
    9     title = models.CharField(max_length=100)
    10     title2 = models.CharField(max_length=100, verbose_name="another name")
    11     created = models.DateTimeField()
    12 
    13     def test_from_model(self):
    14         return "nothing"
    15 
    16     def test_from_model_with_override(self):
    17         return "nothing"
    18     test_from_model_with_override.short_description = "not What you Expect"
    19 
    20 class Count(models.Model):
    21     num = models.PositiveSmallIntegerField()
    22     parent = models.ForeignKey('self', null=True)
    23 
    24     def __unicode__(self):
    25         return unicode(self.num)
    26 
    27 class Event(models.Model):
    28     date = models.DateTimeField(auto_now_add=True)
    29 
    30 class Location(models.Model):
    31     event = models.OneToOneField(Event, verbose_name='awesome event')
    32 
    33 class Guest(models.Model):
    34     event = models.OneToOneField(Event)
    35     name = models.CharField(max_length=255)
    36 
    37     class Meta:
    38         verbose_name = "awesome guest"
  • deleted file tests/regressiontests/admin_util/tests.py

    diff --git a/tests/regressiontests/admin_util/tests.py b/tests/regressiontests/admin_util/tests.py
    deleted file mode 100644
    index 8113f2e..0000000
    + -  
    1 from __future__ import absolute_import
    2 
    3 from datetime import datetime
    4 
    5 from django.conf import settings
    6 from django.contrib import admin
    7 from django.contrib.admin import helpers
    8 from django.contrib.admin.util import (display_for_field, label_for_field,
    9     lookup_field, NestedObjects)
    10 from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
    11 from django.contrib.sites.models import Site
    12 from django.db import models, DEFAULT_DB_ALIAS
    13 from django import forms
    14 from django.test import TestCase
    15 from django.utils import unittest
    16 from django.utils.formats import localize
    17 from django.utils.safestring import mark_safe
    18 
    19 from .models import Article, Count, Event, Location
    20 
    21 
    22 class NestedObjectsTests(TestCase):
    23     """
    24     Tests for ``NestedObject`` utility collection.
    25 
    26     """
    27     def setUp(self):
    28         self.n = NestedObjects(using=DEFAULT_DB_ALIAS)
    29         self.objs = [Count.objects.create(num=i) for i in range(5)]
    30 
    31     def _check(self, target):
    32         self.assertEqual(self.n.nested(lambda obj: obj.num), target)
    33 
    34     def _connect(self, i, j):
    35         self.objs[i].parent = self.objs[j]
    36         self.objs[i].save()
    37 
    38     def _collect(self, *indices):
    39         self.n.collect([self.objs[i] for i in indices])
    40 
    41     def test_unrelated_roots(self):
    42         self._connect(2, 1)
    43         self._collect(0)
    44         self._collect(1)
    45         self._check([0, 1, [2]])
    46 
    47     def test_siblings(self):
    48         self._connect(1, 0)
    49         self._connect(2, 0)
    50         self._collect(0)
    51         self._check([0, [1, 2]])
    52 
    53     def test_non_added_parent(self):
    54         self._connect(0, 1)
    55         self._collect(0)
    56         self._check([0])
    57 
    58     def test_cyclic(self):
    59         self._connect(0, 2)
    60         self._connect(1, 0)
    61         self._connect(2, 1)
    62         self._collect(0)
    63         self._check([0, [1, [2]]])
    64 
    65     def test_queries(self):
    66         self._connect(1, 0)
    67         self._connect(2, 0)
    68         # 1 query to fetch all children of 0 (1 and 2)
    69         # 1 query to fetch all children of 1 and 2 (none)
    70         # Should not require additional queries to populate the nested graph.
    71         self.assertNumQueries(2, self._collect, 0)
    72 
    73 class UtilTests(unittest.TestCase):
    74     def test_values_from_lookup_field(self):
    75         """
    76         Regression test for #12654: lookup_field
    77         """
    78         SITE_NAME = 'example.com'
    79         TITLE_TEXT = 'Some title'
    80         CREATED_DATE = datetime.min
    81         ADMIN_METHOD = 'admin method'
    82         SIMPLE_FUNCTION = 'function'
    83         INSTANCE_ATTRIBUTE = 'attr'
    84 
    85         class MockModelAdmin(object):
    86             def get_admin_value(self, obj):
    87                 return ADMIN_METHOD
    88 
    89         simple_function = lambda obj: SIMPLE_FUNCTION
    90 
    91         article = Article(
    92             site=Site(domain=SITE_NAME),
    93             title=TITLE_TEXT,
    94             created=CREATED_DATE,
    95         )
    96         article.non_field = INSTANCE_ATTRIBUTE
    97 
    98         verifications = (
    99             ('site', SITE_NAME),
    100             ('created', localize(CREATED_DATE)),
    101             ('title', TITLE_TEXT),
    102             ('get_admin_value', ADMIN_METHOD),
    103             (simple_function, SIMPLE_FUNCTION),
    104             ('test_from_model', article.test_from_model()),
    105             ('non_field', INSTANCE_ATTRIBUTE)
    106         )
    107 
    108         mock_admin = MockModelAdmin()
    109         for name, value in verifications:
    110             field, attr, resolved_value = lookup_field(name, article, mock_admin)
    111 
    112             if field is not None:
    113                 resolved_value = display_for_field(resolved_value, field)
    114 
    115             self.assertEqual(value, resolved_value)
    116 
    117     def test_null_display_for_field(self):
    118         """
    119         Regression test for #12550: display_for_field should handle None
    120         value.
    121         """
    122         display_value = display_for_field(None, models.CharField())
    123         self.assertEqual(display_value, EMPTY_CHANGELIST_VALUE)
    124 
    125         display_value = display_for_field(None, models.CharField(
    126             choices=(
    127                 (None, "test_none"),
    128             )
    129         ))
    130         self.assertEqual(display_value, "test_none")
    131 
    132         display_value = display_for_field(None, models.DateField())
    133         self.assertEqual(display_value, EMPTY_CHANGELIST_VALUE)
    134 
    135         display_value = display_for_field(None, models.TimeField())
    136         self.assertEqual(display_value, EMPTY_CHANGELIST_VALUE)
    137 
    138         # Regression test for #13071: NullBooleanField has special
    139         # handling.
    140         display_value = display_for_field(None, models.NullBooleanField())
    141         expected = u'<img src="%sadmin/img/icon-unknown.gif" alt="None" />' % settings.STATIC_URL
    142         self.assertEqual(display_value, expected)
    143 
    144         display_value = display_for_field(None, models.DecimalField())
    145         self.assertEqual(display_value, EMPTY_CHANGELIST_VALUE)
    146 
    147         display_value = display_for_field(None, models.FloatField())
    148         self.assertEqual(display_value, EMPTY_CHANGELIST_VALUE)
    149 
    150     def test_label_for_field(self):
    151         """
    152         Tests for label_for_field
    153         """
    154         self.assertEqual(
    155             label_for_field("title", Article),
    156             "title"
    157         )
    158         self.assertEqual(
    159             label_for_field("title2", Article),
    160             "another name"
    161         )
    162         self.assertEqual(
    163             label_for_field("title2", Article, return_attr=True),
    164             ("another name", None)
    165         )
    166 
    167         self.assertEqual(
    168             label_for_field("__unicode__", Article),
    169             "article"
    170         )
    171         self.assertEqual(
    172             label_for_field("__str__", Article),
    173             "article"
    174         )
    175 
    176         self.assertRaises(
    177             AttributeError,
    178             lambda: label_for_field("unknown", Article)
    179         )
    180 
    181         def test_callable(obj):
    182             return "nothing"
    183         self.assertEqual(
    184             label_for_field(test_callable, Article),
    185             "Test callable"
    186         )
    187         self.assertEqual(
    188             label_for_field(test_callable, Article, return_attr=True),
    189             ("Test callable", test_callable)
    190         )
    191 
    192         self.assertEqual(
    193             label_for_field("test_from_model", Article),
    194             "Test from model"
    195         )
    196         self.assertEqual(
    197             label_for_field("test_from_model", Article, return_attr=True),
    198             ("Test from model", Article.test_from_model)
    199         )
    200         self.assertEqual(
    201             label_for_field("test_from_model_with_override", Article),
    202             "not What you Expect"
    203         )
    204 
    205         self.assertEqual(
    206             label_for_field(lambda x: "nothing", Article),
    207             "--"
    208         )
    209 
    210         class MockModelAdmin(object):
    211             def test_from_model(self, obj):
    212                 return "nothing"
    213             test_from_model.short_description = "not Really the Model"
    214 
    215         self.assertEqual(
    216             label_for_field("test_from_model", Article, model_admin=MockModelAdmin),
    217             "not Really the Model"
    218         )
    219         self.assertEqual(
    220             label_for_field("test_from_model", Article,
    221                 model_admin = MockModelAdmin,
    222                 return_attr = True
    223             ),
    224             ("not Really the Model", MockModelAdmin.test_from_model)
    225         )
    226 
    227     def test_related_name(self):
    228         """
    229         Regression test for #13963
    230         """
    231         self.assertEqual(
    232             label_for_field('location', Event, return_attr=True),
    233             ('location', None),
    234         )
    235         self.assertEqual(
    236             label_for_field('event', Location, return_attr=True),
    237             ('awesome event', None),
    238         )
    239         self.assertEqual(
    240             label_for_field('guest', Event, return_attr=True),
    241             ('awesome guest', None),
    242         )
    243 
    244     def test_logentry_unicode(self):
    245         """
    246         Regression test for #15661
    247         """
    248         log_entry = admin.models.LogEntry()
    249 
    250         log_entry.action_flag = admin.models.ADDITION
    251         self.assertTrue(
    252             unicode(log_entry).startswith('Added ')
    253         )
    254 
    255         log_entry.action_flag = admin.models.CHANGE
    256         self.assertTrue(
    257             unicode(log_entry).startswith('Changed ')
    258         )
    259 
    260         log_entry.action_flag = admin.models.DELETION
    261         self.assertTrue(
    262             unicode(log_entry).startswith('Deleted ')
    263         )
    264 
    265     def test_safestring_in_field_label(self):
    266         # safestring should not be escaped
    267         class MyForm(forms.Form):
    268             text = forms.CharField(label=mark_safe('<i>text</i>'))
    269             cb   = forms.BooleanField(label=mark_safe('<i>cb</i>'))
    270 
    271         form = MyForm()
    272         self.assertEqual(helpers.AdminField(form, 'text', is_first=False).label_tag(),
    273                          '<label for="id_text" class="required inline"><i>text</i>:</label>')
    274         self.assertEqual(helpers.AdminField(form, 'cb', is_first=False).label_tag(),
    275                          '<label for="id_cb" class="vCheckboxLabel required inline"><i>cb</i></label>')
    276 
    277         # normal strings needs to be escaped
    278         class MyForm(forms.Form):
    279             text = forms.CharField(label='&text')
    280             cb   = forms.BooleanField(label='&cb')
    281 
    282         form = MyForm()
    283         self.assertEqual(helpers.AdminField(form, 'text', is_first=False).label_tag(),
    284                          '<label for="id_text" class="required inline">&amp;text:</label>')
    285         self.assertEqual(helpers.AdminField(form, 'cb', is_first=False).label_tag(),
    286                          '<label for="id_cb" class="vCheckboxLabel required inline">&amp;cb</label>')
  • new file tests/regressiontests/admin_utils/models.py

    diff --git a/tests/regressiontests/admin_utils/__init__.py b/tests/regressiontests/admin_utils/__init__.py
    new file mode 100644
    index 0000000..e69de29
    diff --git a/tests/regressiontests/admin_utils/models.py b/tests/regressiontests/admin_utils/models.py
    new file mode 100644
    index 0000000..0e81df3
    - +  
     1from django.db import models
     2
     3
     4class Article(models.Model):
     5    """
     6    A simple Article model for testing
     7    """
     8    site = models.ForeignKey('sites.Site', related_name="admin_articles")
     9    title = models.CharField(max_length=100)
     10    title2 = models.CharField(max_length=100, verbose_name="another name")
     11    created = models.DateTimeField()
     12
     13    def test_from_model(self):
     14        return "nothing"
     15
     16    def test_from_model_with_override(self):
     17        return "nothing"
     18    test_from_model_with_override.short_description = "not What you Expect"
     19
     20class Count(models.Model):
     21    num = models.PositiveSmallIntegerField()
     22    parent = models.ForeignKey('self', null=True)
     23
     24    def __unicode__(self):
     25        return unicode(self.num)
     26
     27class Event(models.Model):
     28    date = models.DateTimeField(auto_now_add=True)
     29
     30class Location(models.Model):
     31    event = models.OneToOneField(Event, verbose_name='awesome event')
     32
     33class Guest(models.Model):
     34    event = models.OneToOneField(Event)
     35    name = models.CharField(max_length=255)
     36
     37    class Meta:
     38        verbose_name = "awesome guest"
  • new file tests/regressiontests/admin_utils/tests.py

    diff --git a/tests/regressiontests/admin_utils/tests.py b/tests/regressiontests/admin_utils/tests.py
    new file mode 100644
    index 0000000..d7eaabe
    - +  
     1from __future__ import absolute_import
     2
     3from datetime import datetime
     4
     5from django.conf import settings
     6from django.contrib import admin
     7from django.contrib.admin import helpers
     8from django.contrib.admin.utils import (display_for_field, label_for_field,
     9    lookup_field, NestedObjects)
     10from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
     11from django.contrib.sites.models import Site
     12from django.db import models, DEFAULT_DB_ALIAS
     13from django import forms
     14from django.test import TestCase
     15from django.utils import unittest
     16from django.utils.formats import localize
     17from django.utils.safestring import mark_safe
     18
     19from .models import Article, Count, Event, Location
     20
     21
     22class NestedObjectsTests(TestCase):
     23    """
     24    Tests for ``NestedObject`` utility collection.
     25
     26    """
     27    def setUp(self):
     28        self.n = NestedObjects(using=DEFAULT_DB_ALIAS)
     29        self.objs = [Count.objects.create(num=i) for i in range(5)]
     30
     31    def _check(self, target):
     32        self.assertEqual(self.n.nested(lambda obj: obj.num), target)
     33
     34    def _connect(self, i, j):
     35        self.objs[i].parent = self.objs[j]
     36        self.objs[i].save()
     37
     38    def _collect(self, *indices):
     39        self.n.collect([self.objs[i] for i in indices])
     40
     41    def test_unrelated_roots(self):
     42        self._connect(2, 1)
     43        self._collect(0)
     44        self._collect(1)
     45        self._check([0, 1, [2]])
     46
     47    def test_siblings(self):
     48        self._connect(1, 0)
     49        self._connect(2, 0)
     50        self._collect(0)
     51        self._check([0, [1, 2]])
     52
     53    def test_non_added_parent(self):
     54        self._connect(0, 1)
     55        self._collect(0)
     56        self._check([0])
     57
     58    def test_cyclic(self):
     59        self._connect(0, 2)
     60        self._connect(1, 0)
     61        self._connect(2, 1)
     62        self._collect(0)
     63        self._check([0, [1, [2]]])
     64
     65    def test_queries(self):
     66        self._connect(1, 0)
     67        self._connect(2, 0)
     68        # 1 query to fetch all children of 0 (1 and 2)
     69        # 1 query to fetch all children of 1 and 2 (none)
     70        # Should not require additional queries to populate the nested graph.
     71        self.assertNumQueries(2, self._collect, 0)
     72
     73class UtilTests(unittest.TestCase):
     74    def test_values_from_lookup_field(self):
     75        """
     76        Regression test for #12654: lookup_field
     77        """
     78        SITE_NAME = 'example.com'
     79        TITLE_TEXT = 'Some title'
     80        CREATED_DATE = datetime.min
     81        ADMIN_METHOD = 'admin method'
     82        SIMPLE_FUNCTION = 'function'
     83        INSTANCE_ATTRIBUTE = 'attr'
     84
     85        class MockModelAdmin(object):
     86            def get_admin_value(self, obj):
     87                return ADMIN_METHOD
     88
     89        simple_function = lambda obj: SIMPLE_FUNCTION
     90
     91        article = Article(
     92            site=Site(domain=SITE_NAME),
     93            title=TITLE_TEXT,
     94            created=CREATED_DATE,
     95        )
     96        article.non_field = INSTANCE_ATTRIBUTE
     97
     98        verifications = (
     99            ('site', SITE_NAME),
     100            ('created', localize(CREATED_DATE)),
     101            ('title', TITLE_TEXT),
     102            ('get_admin_value', ADMIN_METHOD),
     103            (simple_function, SIMPLE_FUNCTION),
     104            ('test_from_model', article.test_from_model()),
     105            ('non_field', INSTANCE_ATTRIBUTE)
     106        )
     107
     108        mock_admin = MockModelAdmin()
     109        for name, value in verifications:
     110            field, attr, resolved_value = lookup_field(name, article, mock_admin)
     111
     112            if field is not None:
     113                resolved_value = display_for_field(resolved_value, field)
     114
     115            self.assertEqual(value, resolved_value)
     116
     117    def test_null_display_for_field(self):
     118        """
     119        Regression test for #12550: display_for_field should handle None
     120        value.
     121        """
     122        display_value = display_for_field(None, models.CharField())
     123        self.assertEqual(display_value, EMPTY_CHANGELIST_VALUE)
     124
     125        display_value = display_for_field(None, models.CharField(
     126            choices=(
     127                (None, "test_none"),
     128            )
     129        ))
     130        self.assertEqual(display_value, "test_none")
     131
     132        display_value = display_for_field(None, models.DateField())
     133        self.assertEqual(display_value, EMPTY_CHANGELIST_VALUE)
     134
     135        display_value = display_for_field(None, models.TimeField())
     136        self.assertEqual(display_value, EMPTY_CHANGELIST_VALUE)
     137
     138        # Regression test for #13071: NullBooleanField has special
     139        # handling.
     140        display_value = display_for_field(None, models.NullBooleanField())
     141        expected = u'<img src="%sadmin/img/icon-unknown.gif" alt="None" />' % settings.STATIC_URL
     142        self.assertEqual(display_value, expected)
     143
     144        display_value = display_for_field(None, models.DecimalField())
     145        self.assertEqual(display_value, EMPTY_CHANGELIST_VALUE)
     146
     147        display_value = display_for_field(None, models.FloatField())
     148        self.assertEqual(display_value, EMPTY_CHANGELIST_VALUE)
     149
     150    def test_label_for_field(self):
     151        """
     152        Tests for label_for_field
     153        """
     154        self.assertEqual(
     155            label_for_field("title", Article),
     156            "title"
     157        )
     158        self.assertEqual(
     159            label_for_field("title2", Article),
     160            "another name"
     161        )
     162        self.assertEqual(
     163            label_for_field("title2", Article, return_attr=True),
     164            ("another name", None)
     165        )
     166
     167        self.assertEqual(
     168            label_for_field("__unicode__", Article),
     169            "article"
     170        )
     171        self.assertEqual(
     172            label_for_field("__str__", Article),
     173            "article"
     174        )
     175
     176        self.assertRaises(
     177            AttributeError,
     178            lambda: label_for_field("unknown", Article)
     179        )
     180
     181        def test_callable(obj):
     182            return "nothing"
     183        self.assertEqual(
     184            label_for_field(test_callable, Article),
     185            "Test callable"
     186        )
     187        self.assertEqual(
     188            label_for_field(test_callable, Article, return_attr=True),
     189            ("Test callable", test_callable)
     190        )
     191
     192        self.assertEqual(
     193            label_for_field("test_from_model", Article),
     194            "Test from model"
     195        )
     196        self.assertEqual(
     197            label_for_field("test_from_model", Article, return_attr=True),
     198            ("Test from model", Article.test_from_model)
     199        )
     200        self.assertEqual(
     201            label_for_field("test_from_model_with_override", Article),
     202            "not What you Expect"
     203        )
     204
     205        self.assertEqual(
     206            label_for_field(lambda x: "nothing", Article),
     207            "--"
     208        )
     209
     210        class MockModelAdmin(object):
     211            def test_from_model(self, obj):
     212                return "nothing"
     213            test_from_model.short_description = "not Really the Model"
     214
     215        self.assertEqual(
     216            label_for_field("test_from_model", Article, model_admin=MockModelAdmin),
     217            "not Really the Model"
     218        )
     219        self.assertEqual(
     220            label_for_field("test_from_model", Article,
     221                model_admin = MockModelAdmin,
     222                return_attr = True
     223            ),
     224            ("not Really the Model", MockModelAdmin.test_from_model)
     225        )
     226
     227    def test_related_name(self):
     228        """
     229        Regression test for #13963
     230        """
     231        self.assertEqual(
     232            label_for_field('location', Event, return_attr=True),
     233            ('location', None),
     234        )
     235        self.assertEqual(
     236            label_for_field('event', Location, return_attr=True),
     237            ('awesome event', None),
     238        )
     239        self.assertEqual(
     240            label_for_field('guest', Event, return_attr=True),
     241            ('awesome guest', None),
     242        )
     243
     244    def test_logentry_unicode(self):
     245        """
     246        Regression test for #15661
     247        """
     248        log_entry = admin.models.LogEntry()
     249
     250        log_entry.action_flag = admin.models.ADDITION
     251        self.assertTrue(
     252            unicode(log_entry).startswith('Added ')
     253        )
     254
     255        log_entry.action_flag = admin.models.CHANGE
     256        self.assertTrue(
     257            unicode(log_entry).startswith('Changed ')
     258        )
     259
     260        log_entry.action_flag = admin.models.DELETION
     261        self.assertTrue(
     262            unicode(log_entry).startswith('Deleted ')
     263        )
     264
     265    def test_safestring_in_field_label(self):
     266        # safestring should not be escaped
     267        class MyForm(forms.Form):
     268            text = forms.CharField(label=mark_safe('<i>text</i>'))
     269            cb   = forms.BooleanField(label=mark_safe('<i>cb</i>'))
     270
     271        form = MyForm()
     272        self.assertEqual(helpers.AdminField(form, 'text', is_first=False).label_tag(),
     273                         '<label for="id_text" class="required inline"><i>text</i>:</label>')
     274        self.assertEqual(helpers.AdminField(form, 'cb', is_first=False).label_tag(),
     275                         '<label for="id_cb" class="vCheckboxLabel required inline"><i>cb</i></label>')
     276
     277        # normal strings needs to be escaped
     278        class MyForm(forms.Form):
     279            text = forms.CharField(label='&text')
     280            cb   = forms.BooleanField(label='&cb')
     281
     282        form = MyForm()
     283        self.assertEqual(helpers.AdminField(form, 'text', is_first=False).label_tag(),
     284                         '<label for="id_text" class="required inline">&amp;text:</label>')
     285        self.assertEqual(helpers.AdminField(form, 'cb', is_first=False).label_tag(),
     286                         '<label for="id_cb" class="vCheckboxLabel required inline">&amp;cb</label>')
  • tests/regressiontests/admin_views/tests.py

    diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py
    index ab40c69..b9f05d6 100644
    a b from django.core.urlresolvers import reverse 
    1414from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
    1515from django.contrib.admin.models import LogEntry, DELETION
    1616from django.contrib.admin.sites import LOGIN_FORM_KEY
    17 from django.contrib.admin.util import quote
     17from django.contrib.admin.utils import quote
    1818from django.contrib.admin.views.main import IS_POPUP_VAR
    1919from django.contrib.auth import REDIRECT_FIELD_NAME, admin
    2020from django.contrib.auth.models import Group, User, Permission, UNUSABLE_PASSWORD
Back to Top