Ticket #6191: 6191_r12556.diff

File 6191_r12556.diff, 29.7 KB (added by Carl Meyer, 14 years ago)

updated patch, applies cleanly

  • django/db/models/base.py

     
    545545             (model_class, {pk_val: obj, pk_val: obj, ...}), ...]
    546546        """
    547547        pk_val = self._get_pk_val()
    548         if seen_objs.add(self.__class__, pk_val, self, parent, nullable):
     548        if seen_objs.add(self.__class__, pk_val, self,
     549                         type(parent), parent, nullable):
    549550            return
    550551
    551552        for related in self._meta.get_all_related_objects():
     
    556557                except ObjectDoesNotExist:
    557558                    pass
    558559                else:
    559                     sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
     560                    sub_obj._collect_sub_objects(seen_objs, self, related.field.null)
    560561            else:
    561562                # To make sure we can access all elements, we can't use the
    562563                # normal manager on the related object. So we work directly
     
    574575                        continue
    575576                delete_qs = rel_descriptor.delete_manager(self).all()
    576577                for sub_obj in delete_qs:
    577                     sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
     578                    sub_obj._collect_sub_objects(seen_objs, self, related.field.null)
    578579
    579580        # Handle any ancestors (for the model-inheritance case). We do this by
    580581        # traversing to the most remote parent classes -- those with no parents
  • django/db/models/query_utils.py

     
    5050        else:
    5151            self.blocked = {}
    5252
    53     def add(self, model, pk, obj, parent_model, nullable=False):
     53    def add(self, model, pk, obj, parent_model, parent_obj=None, nullable=False):
    5454        """
    5555        Adds an item to the container.
    5656
     
    6060        * obj - the object itself.
    6161        * parent_model - the model of the parent object that this object was
    6262          reached through.
     63        * parent_obj - the parent object this object was reached
     64          through (not used here, but needed in the API for use elsewhere)
    6365        * nullable - should be True if this relation is nullable.
    6466
    6567        Returns True if the item already existed in the structure and
  • django/contrib/admin/options.py

     
    10801080
    10811081        # Populate deleted_objects, a data structure of all related objects that
    10821082        # will also be deleted.
    1083         deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), object_id, escape(obj))), []]
    1084         perms_needed = set()
    1085         get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site)
     1083        (deleted_objects, perms_needed) = get_deleted_objects((obj,), opts, request.user, self.admin_site)
    10861084
    10871085        if request.POST: # The user has already confirmed the deletion.
    10881086            if perms_needed:
  • django/contrib/admin/util.py

     
    77from django.utils.encoding import force_unicode, smart_unicode, smart_str
    88from django.utils.translation import ungettext, ugettext as _
    99from django.core.urlresolvers import reverse, NoReverseMatch
     10from django.utils.datastructures import SortedDict
    1011
    1112
    1213def quote(s):
     
    5758                field_names.append(field)
    5859    return field_names
    5960
    60 def _nest_help(obj, depth, val):
    61     current = obj
    62     for i in range(depth):
    63         current = current[-1]
    64     current.append(val)
    65 
    66 def get_change_view_url(app_label, module_name, pk, admin_site, levels_to_root):
     61def get_deleted_objects(objs, opts, user, admin_site, levels_to_root=4):
    6762    """
    68     Returns the url to the admin change view for the given app_label,
    69     module_name and primary key.
    70     """
    71     try:
    72         return reverse('%sadmin_%s_%s_change' % (admin_site.name, app_label, module_name), None, (pk,))
    73     except NoReverseMatch:
    74         return '%s%s/%s/%s/' % ('../'*levels_to_root, app_label, module_name, pk)
     63    Find all objects related to ``objs`` that should also be
     64    deleted. ``objs`` should be an iterable of objects.
    7565
    76 def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_depth, admin_site, levels_to_root=4):
    77     """
    78     Helper function that recursively populates deleted_objects.
     66    Returns a nested list of strings suitable for display in the
     67    template with the ``unordered_list`` filter.
    7968
    80     `levels_to_root` defines the number of directories (../) to reach the
    81     admin root path. In a change_view this is 4, in a change_list view 2.
     69    `levels_to_root` defines the number of directories (../) to reach
     70    the admin root path. In a change_view this is 4, in a change_list
     71    view 2.
    8272
    8373    This is for backwards compatibility since the options.delete_selected
    8474    method uses this function also from a change_list view.
    8575    This will not be used if we can reverse the URL.
    8676    """
    87     nh = _nest_help # Bind to local variable for performance
    88     if current_depth > 16:
    89         return # Avoid recursing too deep.
    90     opts_seen = []
    91     for related in opts.get_all_related_objects():
    92         has_admin = related.model in admin_site._registry
    93         if related.opts in opts_seen:
    94             continue
    95         opts_seen.append(related.opts)
    96         rel_opts_name = related.get_accessor_name()
    97         if isinstance(related.field.rel, models.OneToOneRel):
    98             try:
    99                 sub_obj = getattr(obj, rel_opts_name)
    100             except ObjectDoesNotExist:
    101                 pass
    102             else:
    103                 if has_admin:
    104                     p = '%s.%s' % (related.opts.app_label, related.opts.get_delete_permission())
    105                     if not user.has_perm(p):
    106                         perms_needed.add(related.opts.verbose_name)
    107                         # We don't care about populating deleted_objects now.
    108                         continue
    109                 if not has_admin:
    110                     # Don't display link to edit, because it either has no
    111                     # admin or is edited inline.
    112                     nh(deleted_objects, current_depth,
    113                         [u'%s: %s' % (capfirst(related.opts.verbose_name), force_unicode(sub_obj)), []])
    114                 else:
    115                     # Display a link to the admin page.
    116                     nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="%s">%s</a>' %
    117                         (escape(capfirst(related.opts.verbose_name)),
    118                         get_change_view_url(related.opts.app_label,
    119                                             related.opts.object_name.lower(),
    120                                             sub_obj._get_pk_val(),
    121                                             admin_site,
    122                                             levels_to_root),
    123                         escape(sub_obj))), []])
    124                 get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2, admin_site)
    125         else:
    126             has_related_objs = False
    127             for sub_obj in getattr(obj, rel_opts_name).all():
    128                 has_related_objs = True
    129                 if not has_admin:
    130                     # Don't display link to edit, because it either has no
    131                     # admin or is edited inline.
    132                     nh(deleted_objects, current_depth,
    133                         [u'%s: %s' % (capfirst(related.opts.verbose_name), force_unicode(sub_obj)), []])
    134                 else:
    135                     # Display a link to the admin page.
    136                     nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="%s">%s</a>' %
    137                         (escape(capfirst(related.opts.verbose_name)),
    138                         get_change_view_url(related.opts.app_label,
    139                                             related.opts.object_name.lower(),
    140                                             sub_obj._get_pk_val(),
    141                                             admin_site,
    142                                             levels_to_root),
    143                         escape(sub_obj))), []])
    144                 get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2, admin_site)
    145             # If there were related objects, and the user doesn't have
    146             # permission to delete them, add the missing perm to perms_needed.
    147             if has_admin and has_related_objs:
    148                 p = '%s.%s' % (related.opts.app_label, related.opts.get_delete_permission())
    149                 if not user.has_perm(p):
    150                     perms_needed.add(related.opts.verbose_name)
    151     for related in opts.get_all_related_many_to_many_objects():
    152         has_admin = related.model in admin_site._registry
    153         if related.opts in opts_seen:
    154             continue
    155         opts_seen.append(related.opts)
    156         rel_opts_name = related.get_accessor_name()
    157         has_related_objs = False
     77    collector = NestedObjects()
     78    for obj in objs:
     79        # TODO using a private model API!
     80        obj._collect_sub_objects(collector)
    15881
    159         # related.get_accessor_name() could return None for symmetrical relationships
    160         if rel_opts_name:
    161             rel_objs = getattr(obj, rel_opts_name, None)
    162             if rel_objs:
    163                 has_related_objs = True
     82    perms_needed = set()
    16483
    165         if has_related_objs:
    166             for sub_obj in rel_objs.all():
    167                 if not has_admin:
    168                     # Don't display link to edit, because it either has no
    169                     # admin or is edited inline.
    170                     nh(deleted_objects, current_depth, [_('One or more %(fieldname)s in %(name)s: %(obj)s') % \
    171                         {'fieldname': force_unicode(related.field.verbose_name), 'name': force_unicode(related.opts.verbose_name), 'obj': escape(sub_obj)}, []])
    172                 else:
    173                     # Display a link to the admin page.
    174                     nh(deleted_objects, current_depth, [
    175                         mark_safe((_('One or more %(fieldname)s in %(name)s:') % {'fieldname': escape(force_unicode(related.field.verbose_name)), 'name': escape(force_unicode(related.opts.verbose_name))}) + \
    176                         (u' <a href="%s">%s</a>' % \
    177                             (get_change_view_url(related.opts.app_label,
    178                                                  related.opts.object_name.lower(),
    179                                                  sub_obj._get_pk_val(),
    180                                                  admin_site,
    181                                                  levels_to_root),
    182                             escape(sub_obj)))), []])
    183         # If there were related objects, and the user doesn't have
    184         # permission to change them, add the missing perm to perms_needed.
    185         if has_admin and has_related_objs:
    186             p = u'%s.%s' % (related.opts.app_label, related.opts.get_change_permission())
     84    def _format_callback(obj):
     85        has_admin = obj.__class__ in admin_site._registry
     86        opts = obj._meta
     87        try:
     88            admin_url = reverse('%s:%s_%s_change'
     89                                % (admin_site.name,
     90                                   opts.app_label,
     91                                   opts.object_name.lower()),
     92                                None, (quote(obj._get_pk_val()),))
     93        except NoReverseMatch:
     94            admin_url = '%s%s/%s/%s/' % ('../'*levels_to_root,
     95                                         opts.app_label,
     96                                         opts.object_name.lower(),
     97                                         quote(obj._get_pk_val()))
     98        if has_admin:
     99            p = '%s.%s' % (opts.app_label,
     100                           opts.get_delete_permission())
    187101            if not user.has_perm(p):
    188                 perms_needed.add(related.opts.verbose_name)
     102                perms_needed.add(opts.verbose_name)
     103            # Display a link to the admin page.
     104            return mark_safe(u'%s: <a href="%s">%s</a>' %
     105                             (escape(capfirst(opts.verbose_name)),
     106                              admin_url,
     107                              escape(obj)))
     108        else:
     109            # Don't display link to edit, because it either has no
     110            # admin or is edited inline.
     111            return u'%s: %s' % (capfirst(opts.verbose_name),
     112                                force_unicode(obj))
    189113
     114    to_delete = collector.nested(_format_callback)
     115
     116    return to_delete, perms_needed
     117
     118
     119class NestedObjects(object):
     120    """
     121    A directed acyclic graph collection that exposes the add() API
     122    expected by Model._collect_sub_objects and can present its data as
     123    a nested list of objects.
     124
     125    """
     126    def __init__(self):
     127        # Use object keys of the form (model, pk) because actual model
     128        # objects may not be unique
     129
     130        # maps object key to set of child keys
     131        self.children = SortedDict()
     132
     133        # maps object key to parent key
     134        self.parents = SortedDict()
     135
     136        # maps object key to actual object
     137        self.seen = SortedDict()
     138
     139    def add(self, model, pk, obj,
     140            parent_model=None, parent_obj=None, nullable=False):
     141        """
     142        Add item ``obj`` to the graph. Returns True (and does nothing)
     143        if the item has been seen already.
     144
     145        The ``parent_obj`` argument must already exist in the graph; if
     146        not, it's ignored (but ``obj`` is still added with no
     147        parent). In any case, Model._collect_sub_objects (for whom
     148        this API exists) will never pass a parent that hasn't already
     149        been added itself.
     150
     151        These restrictions in combination ensure the graph will remain
     152        acyclic (but can have multiple roots).
     153
     154        ``model``, ``pk``, and ``parent_model`` arguments are ignored
     155        in favor of the appropriate lookups on ``obj`` and
     156        ``parent_obj``; unlike CollectedObjects, we can't maintain
     157        independence from the knowledge that we're operating on model
     158        instances, and we don't want to allow for inconsistency.
     159
     160        ``nullable`` arg is ignored: it doesn't affect how the tree of
     161        collected objects should be nested for display.
     162        """
     163        model, pk = type(obj), obj._get_pk_val()
     164
     165        key = model, pk
     166
     167        if key in self.seen:
     168            return True
     169        self.seen.setdefault(key, obj)
     170
     171        if parent_obj is not None:
     172            parent_model, parent_pk = (type(parent_obj),
     173                                       parent_obj._get_pk_val())
     174            parent_key = (parent_model, parent_pk)
     175            if parent_key in self.seen:
     176                self.children.setdefault(parent_key, set()).add(key)
     177                self.parents.setdefault(key, parent_key)
     178
     179    def _nested(self, key, format_callback=None):
     180        obj = self.seen[key]
     181        if format_callback:
     182            ret = [format_callback(obj)]
     183        else:
     184            ret = [obj]
     185
     186        children = []
     187        for child in self.children.get(key, ()):
     188            children.extend(self._nested(child, format_callback))
     189        if children:
     190            ret.append(children)
     191
     192        return ret
     193
     194    def nested(self, format_callback=None):
     195        """
     196        Return the graph as a nested list.
     197
     198        """
     199        roots = []
     200        for key in self.seen.keys():
     201            if key not in self.parents:
     202                roots.extend(self._nested(key, format_callback))
     203        return roots
     204
     205
    190206def model_format_dict(obj):
    191207    """
    192208    Return a `dict` with keys 'verbose_name' and 'verbose_name_plural',
  • django/contrib/admin/actions.py

     
    3636
    3737    # Populate deletable_objects, a data structure of all related objects that
    3838    # will also be deleted.
     39    (deletable_objects, perms_needed) = get_deleted_objects(queryset, opts, request.user, modeladmin.admin_site, levels_to_root=2)
    3940
    40     # deletable_objects must be a list if we want to use '|unordered_list' in the template
    41     deletable_objects = []
    42     perms_needed = set()
    43     i = 0
    44     for obj in queryset:
    45         deletable_objects.append([mark_safe(u'%s: <a href="%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), obj.pk, escape(obj))), []])
    46         get_deleted_objects(deletable_objects[i], perms_needed, request.user, obj, opts, 1, modeladmin.admin_site, levels_to_root=2)
    47         i=i+1
    48 
    4941    # The user has already confirmed the deletion.
    5042    # Do the deletion and return a None to display the change list view again.
    5143    if request.POST.get('post'):
  • django/contrib/admin/templates/admin/delete_selected_confirmation.html

     
    2020    </ul>
    2121{% else %}
    2222    <p>{% blocktrans %}Are you sure you want to delete the selected {{ object_name }} objects? All of the following objects and their related items will be deleted:{% endblocktrans %}</p>
    23     {% for deleteable_object in deletable_objects %}
    24         <ul>{{ deleteable_object|unordered_list }}</ul>
    25     {% endfor %}
     23    <ul>{{ deletable_objects|unordered_list }}</ul>
    2624    <form action="" method="post">{% csrf_token %}
    2725    <div>
    2826    {% for obj in queryset %}
  • tests/regressiontests/admin_views/fixtures/deleted-objects.xml

     
     1<?xml version="1.0" encoding="utf-8"?>
     2<django-objects version="1.0">
     3    <object pk="1" model="admin_views.villain">
     4        <field type="CharField" name="name">Adam</field>
     5    </object>
     6    <object pk="2" model="admin_views.villain">
     7        <field type="CharField" name="name">Sue</field>
     8    </object>
     9    <object pk="1" model="admin_views.plot">
     10        <field type="CharField" name="name">World Domination</field>
     11        <field type="ForeignKey" name="team_leader">1</field>
     12        <field type="ForeignKey" name="contact">2</field>
     13    </object>
     14    <object pk="2" model="admin_views.plot">
     15        <field type="CharField" name="name">World Peace</field>
     16        <field type="ForeignKey" name="team_leader">2</field>
     17        <field type="ForeignKey" name="contact">2</field>
     18    </object>
     19    <object pk="1" model="admin_views.plotdetails">
     20        <field type="CharField" name="details">almost finished</field>
     21        <field type="ForeignKey" name="plot">1</field>
     22    </object>
     23    <object pk="1" model="admin_views.secrethideout">
     24        <field type="CharField" name="location">underground bunker</field>
     25        <field type="ForeignKey" name="villain">1</field>
     26    </object>
     27    <object pk="1" model="admin_views.cyclicone">
     28        <field type="CharField" name="name">I am recursive</field>
     29        <field type="ForeignKey" name="two">1</field>
     30    </object>
     31    <object pk="1" model="admin_views.cyclictwo">
     32        <field type="CharField" name="name">I am recursive too</field>
     33        <field type="ForeignKey" name="one">1</field>
     34    </object>
     35</django-objects>
  • tests/regressiontests/admin_views/tests.py

     
    1616from django.utils.cache import get_max_age
    1717from django.utils.html import escape
    1818from django.utils.translation import get_date_formats
     19from django.utils.encoding import iri_to_uri
    1920
    2021# local test models
    2122from models import Article, BarAccount, CustomArticle, EmptyModel, \
    2223    ExternalSubscriber, FooAccount, Gallery, ModelWithStringPrimaryKey, \
    2324    Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast, \
    2425    Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit, \
    25     Category, Post
     26    Category, Post, Plot
    2627
    2728
    2829class AdminViewBasicTest(TestCase):
     
    637638        response = self.client.get('/test_admin/admin/secure-view/')
    638639        self.assertContains(response, 'id="login-form"')
    639640
     641
     642class AdminViewDeletedObjectsTest(TestCase):
     643    fixtures = ['admin-views-users.xml', 'deleted-objects.xml']
     644
     645    def setUp(self):
     646        self.client.login(username='super', password='secret')
     647
     648    def tearDown(self):
     649        self.client.logout()
     650
     651    def test_nesting(self):
     652        """
     653        Objects should be nested to display the relationships that
     654        cause them to be scheduled for deletion.
     655        """
     656        pattern = re.compile(r"""<li>Plot: <a href=".+/admin_views/plot/1/">World Domination</a>\s*<ul>\s*<li>Plot details: <a href=".+/admin_views/plotdetails/1/">almost finished</a>""")
     657        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1))
     658        self.failUnless(pattern.search(response.content))
     659
     660    def test_cyclic(self):
     661        """
     662        Cyclic relationships should still cause each object to only be
     663        listed once.
     664
     665        """
     666        one = """<li>Cyclic one: <a href="/test_admin/admin/admin_views/cyclicone/1/">I am recursive</a>"""
     667        two = """<li>Cyclic two: <a href="/test_admin/admin/admin_views/cyclictwo/1/">I am recursive too</a>"""
     668        response = self.client.get('/test_admin/admin/admin_views/cyclicone/%s/delete/' % quote(1))
     669
     670        self.assertContains(response, one, 1)
     671        self.assertContains(response, two, 1)
     672
     673    def test_perms_needed(self):
     674        self.client.logout()
     675        delete_user = User.objects.get(username='deleteuser')
     676        delete_user.user_permissions.add(get_perm(Plot,
     677            Plot._meta.get_delete_permission()))
     678
     679        self.failUnless(self.client.login(username='deleteuser',
     680                                          password='secret'))
     681
     682        response = self.client.get('/test_admin/admin/admin_views/plot/%s/delete/' % quote(1))
     683        self.assertContains(response, "your account doesn't have permission to delete the following types of objects")
     684        self.assertContains(response, "<li>plot details</li>")
     685
     686
     687    def test_not_registered(self):
     688        should_contain = """<li>Secret hideout: underground bunker"""
     689        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1))
     690        self.assertContains(response, should_contain, 1)
     691
     692    def test_multiple_fkeys_to_same_model(self):
     693        """
     694        If a deleted object has two relationships from another model,
     695        both of those should be followed in looking for related
     696        objects to delete.
     697
     698        """
     699        should_contain = """<li>Plot: <a href="/test_admin/admin/admin_views/plot/1/">World Domination</a>"""
     700        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1))
     701        self.assertContains(response, should_contain)
     702        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(2))
     703        self.assertContains(response, should_contain)
     704
     705    def test_multiple_fkeys_to_same_instance(self):
     706        """
     707        If a deleted object has two relationships pointing to it from
     708        another object, the other object should still only be listed
     709        once.
     710
     711        """
     712        should_contain = """<li>Plot: <a href="/test_admin/admin/admin_views/plot/2/">World Peace</a></li>"""
     713        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(2))
     714        self.assertContains(response, should_contain, 1)
     715
    640716class AdminViewStringPrimaryKeyTest(TestCase):
    641717    fixtures = ['admin-views-users.xml', 'string-primary-key.xml']
    642718
     
    699775    def test_deleteconfirmation_link(self):
    700776        "The link from the delete confirmation page referring back to the changeform of the object should be quoted"
    701777        response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/delete/' % quote(self.pk))
    702         should_contain = """<a href="../../%s/">%s</a>""" % (quote(self.pk), escape(self.pk))
     778        # this URL now comes through reverse(), thus iri_to_uri encoding
     779        should_contain = """/%s/">%s</a>""" % (iri_to_uri(quote(self.pk)), escape(self.pk))
    703780        self.assertContains(response, should_contain)
    704781
    705782    def test_url_conflicts_with_add(self):
  • tests/regressiontests/admin_views/models.py

     
    494494    def get_changelist(self, request, **kwargs):
    495495        return CustomChangeList
    496496
     497class Villain(models.Model):
     498    name = models.CharField(max_length=100)
     499
     500    def __unicode__(self):
     501        return self.name
     502
     503class Plot(models.Model):
     504    name = models.CharField(max_length=100)
     505    team_leader = models.ForeignKey(Villain, related_name='lead_plots')
     506    contact = models.ForeignKey(Villain, related_name='contact_plots')
     507
     508    def __unicode__(self):
     509        return self.name
     510
     511class PlotDetails(models.Model):
     512    details = models.CharField(max_length=100)
     513    plot = models.OneToOneField(Plot)
     514
     515    def __unicode__(self):
     516        return self.details
     517
     518class SecretHideout(models.Model):
     519    """ Secret! Not registered with the admin! """
     520    location = models.CharField(max_length=100)
     521    villain = models.ForeignKey(Villain)
     522
     523    def __unicode__(self):
     524        return self.location
     525
     526class CyclicOne(models.Model):
     527    name = models.CharField(max_length=25)
     528    two = models.ForeignKey('CyclicTwo')
     529
     530    def __unicode__(self):
     531        return self.name
     532
     533class CyclicTwo(models.Model):
     534    name = models.CharField(max_length=25)
     535    one = models.ForeignKey(CyclicOne)
     536
     537    def __unicode__(self):
     538        return self.name
     539
    497540admin.site.register(Article, ArticleAdmin)
    498541admin.site.register(CustomArticle, CustomArticleAdmin)
    499542admin.site.register(Section, save_as=True, inlines=[ArticleInline])
     
    519562admin.site.register(Category, CategoryAdmin)
    520563admin.site.register(Post, PostAdmin)
    521564admin.site.register(Gadget, GadgetAdmin)
     565admin.site.register(Villain)
     566admin.site.register(Plot)
     567admin.site.register(PlotDetails)
     568admin.site.register(CyclicOne)
     569admin.site.register(CyclicTwo)
    522570
    523571# We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
    524572# That way we cover all four cases:
  • tests/regressiontests/admin_util/tests.py

     
    22import unittest
    33
    44from django.db import models
     5from django.utils.formats import localize
     6from django.test import TestCase
    57
    68from django.contrib import admin
    79from django.contrib.admin.util import display_for_field, label_for_field, lookup_field
    810from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
    911from django.contrib.sites.models import Site
    10 from django.utils.formats import localize
     12from django.contrib.admin.util import NestedObjects
    1113
    12 from models import Article
     14from models import Article, Count
    1315
    1416
     17class NestedObjectsTests(TestCase):
     18    """
     19    Tests for ``NestedObject`` utility collection.
    1520
     21    """
     22    def setUp(self):
     23        self.n = NestedObjects()
     24        self.objs = [Count.objects.create(num=i) for i in range(5)]
     25
     26    def _check(self, target):
     27        self.assertEquals(self.n.nested(lambda obj: obj.num), target)
     28
     29    def _add(self, obj, parent=None):
     30        # don't bother providing the extra args that NestedObjects ignores
     31        self.n.add(None, None, obj, None, parent)
     32
     33    def test_unrelated_roots(self):
     34        self._add(self.objs[0])
     35        self._add(self.objs[1])
     36        self._add(self.objs[2], self.objs[1])
     37
     38        self._check([0, 1, [2]])
     39
     40    def test_siblings(self):
     41        self._add(self.objs[0])
     42        self._add(self.objs[1], self.objs[0])
     43        self._add(self.objs[2], self.objs[0])
     44
     45        self._check([0, [1, 2]])
     46
     47    def test_duplicate_instances(self):
     48        self._add(self.objs[0])
     49        self._add(self.objs[1])
     50        dupe = Count.objects.get(num=1)
     51        self._add(dupe, self.objs[0])
     52
     53        self._check([0, 1])
     54
     55    def test_non_added_parent(self):
     56        self._add(self.objs[0], self.objs[1])
     57
     58        self._check([0])
     59
     60    def test_cyclic(self):
     61        self._add(self.objs[0], self.objs[2])
     62        self._add(self.objs[1], self.objs[0])
     63        self._add(self.objs[2], self.objs[1])
     64        self._add(self.objs[0], self.objs[2])
     65
     66        self._check([0, [1, [2]]])
     67
     68
    1669class UtilTests(unittest.TestCase):
    1770    def test_values_from_lookup_field(self):
    1871        """
  • tests/regressiontests/admin_util/models.py

     
    1717    def test_from_model_with_override(self):
    1818        return "nothing"
    1919    test_from_model_with_override.short_description = "not what you expect"
     20
     21class Count(models.Model):
     22    num = models.PositiveSmallIntegerField()
Back to Top