Ticket #6191: 6191_r12203.2.diff

File 6191_r12203.2.diff, 28.9 KB (added by Carl Meyer, 14 years ago)

new patch that removes the algorithm duplication entirely

  • django/contrib/admin/actions.py

    diff --git a/django/contrib/admin/actions.py b/django/contrib/admin/actions.py
    a b  
    3636
    3737    # Populate deletable_objects, a data structure of all related objects that
    3838    # will also be deleted.
    39 
    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
     39    (deletable_objects, perms_needed) = get_deleted_objects(queryset, opts, request.user, modeladmin.admin_site, levels_to_root=2)
    4840
    4941    # The user has already confirmed the deletion.
    5042    # Do the deletion and return a None to display the change list view again.
  • django/contrib/admin/options.py

    diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
    a b  
    10681068
    10691069        # Populate deleted_objects, a data structure of all related objects that
    10701070        # will also be deleted.
    1071         deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), object_id, escape(obj))), []]
    1072         perms_needed = set()
    1073         get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site)
     1071        (deleted_objects, perms_needed) = get_deleted_objects((obj,), opts, request.user, self.admin_site)
    10741072
    10751073        if request.POST: # The user has already confirmed the deletion.
    10761074            if perms_needed:
  • django/contrib/admin/templates/admin/delete_selected_confirmation.html

    diff --git a/django/contrib/admin/templates/admin/delete_selected_confirmation.html b/django/contrib/admin/templates/admin/delete_selected_confirmation.html
    a b  
    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 %}
  • django/contrib/admin/util.py

    diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py
    a b  
     1import urllib
     2
    13from django.core.exceptions import ObjectDoesNotExist
    24from django.db import models
    35from django.utils import formats
     
    79from django.utils.encoding import force_unicode, smart_unicode, smart_str
    810from django.utils.translation import ungettext, ugettext as _
    911from django.core.urlresolvers import reverse, NoReverseMatch
     12from django.utils.datastructures import SortedDict
    1013
    1114
    1215def quote(s):
     
    5760                field_names.append(field)
    5861    return field_names
    5962
    60 def _nest_help(obj, depth, val):
    61     current = obj
    62     for i in range(depth):
    63         current = current[-1]
    64     current.append(val)
     63def get_deleted_objects(objs, opts, user, admin_site, levels_to_root=4):
     64    """
     65    Find all objects related to ``objs`` that should also be
     66    deleted. ``objs`` should be an iterable of objects.
    6567
    66 def get_change_view_url(app_label, module_name, pk, admin_site, levels_to_root):
    67     """
    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)
     68    Returns a nested list of strings suitable for display in the
     69    template with the ``unordered_list`` filter.
    7570
    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.
    79 
    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.
     71    `levels_to_root` defines the number of directories (../) to reach
     72    the admin root path. In a change_view this is 4, in a change_list
     73    view 2.
    8274
    8375    This is for backwards compatibility since the options.delete_selected
    8476    method uses this function also from a change_list view.
    8577    This will not be used if we can reverse the URL.
    8678    """
    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)
     79    collector = NestedObjects()
     80    for obj in objs:
     81        # TODO using a private model API!
     82        obj._collect_sub_objects(collector)
     83
     84    perms_needed = set()
     85
     86    def _format_callback(obj):
     87        has_admin = obj.__class__ in admin_site._registry
     88        opts = obj._meta
     89        try:
     90            admin_url = reverse('%s:%s_%s_change'
     91                                % (admin_site.name,
     92                                   opts.app_label,
     93                                   opts.object_name.lower()),
     94                                None, (quote(obj._get_pk_val()),))
     95        except NoReverseMatch:
     96            admin_url = '%s%s/%s/%s/' % ('../'*levels_to_root,
     97                                         opts.app_label,
     98                                         opts.object_name.lower(),
     99                                         quote(obj._get_pk_val()))
     100        if has_admin:
     101            p = '%s.%s' % (opts.app_label,
     102                           opts.get_delete_permission())
     103            if not user.has_perm(p):
     104                perms_needed.add(opts.verbose_name)
     105            # Display a link to the admin page.
     106            return mark_safe(u'%s: <a href="%s">%s</a>' %
     107                             (escape(capfirst(opts.verbose_name)),
     108                              admin_url,
     109                              escape(obj)))
    125110        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
     111            # Don't display link to edit, because it either has no
     112            # admin or is edited inline.
     113            return u'%s: %s' % (capfirst(opts.verbose_name),
     114                                force_unicode(obj))
    158115
    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
     116    to_delete = collector.nested(_format_callback)
    164117
    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())
    187             if not user.has_perm(p):
    188                 perms_needed.add(related.opts.verbose_name)
     118    return to_delete, perms_needed
     119
     120
     121class NestedObjects(object):
     122    """
     123    A directed acyclic graph collection that exposes the add() API
     124    expected by Model._collect_sub_objects and can present its data as
     125    a nested list of objects.
     126
     127    """
     128    def __init__(self):
     129        # Use object keys of the form (model, pk) because actual model
     130        # objects may not be unique
     131
     132        # maps object key to set of child keys
     133        self.children = SortedDict()
     134
     135        # maps object key to parent key
     136        self.parents = SortedDict()
     137
     138        # maps object key to actual object
     139        self.seen = SortedDict()
     140
     141    def add(self, model, pk, obj,
     142            parent_model=None, parent_obj=None, nullable=False):
     143        """
     144        Add item ``obj`` to the graph. Returns True (and does nothing)
     145        if the item has been seen already.
     146
     147        The ``parent_obj`` argument must already exist in the graph; if
     148        not, it's ignored (but ``obj`` is still added with no
     149        parent). In any case, Model._collect_sub_objects (for whom
     150        this API exists) will never pass a parent that hasn't already
     151        been added itself.
     152
     153        These restrictions in combination ensure the graph will remain
     154        acyclic (but can have multiple roots).
     155
     156        ``model``, ``pk``, and ``parent_model`` arguments are ignored
     157        in favor of the appropriate lookups on ``obj`` and
     158        ``parent_obj``; unlike CollectedObjects, we can't maintain
     159        independence from the knowledge that we're operating on model
     160        instances, and we don't want to allow for inconsistency.
     161
     162        ``nullable`` arg is ignored: it doesn't affect how the tree of
     163        collected objects should be nested for display.
     164        """
     165        model, pk = type(obj), obj._get_pk_val()
     166
     167        key = model, pk
     168
     169        if key in self.seen:
     170            return True
     171        self.seen.setdefault(key, obj)
     172
     173        if parent_obj is not None:
     174            parent_model, parent_pk = (type(parent_obj),
     175                                       parent_obj._get_pk_val())
     176            parent_key = (parent_model, parent_pk)
     177            if parent_key in self.seen:
     178                self.children.setdefault(parent_key, set()).add(key)
     179                self.parents.setdefault(key, parent_key)
     180
     181    def _nested(self, key, format_callback=None):
     182        obj = self.seen[key]
     183        if format_callback:
     184            ret = [format_callback(obj)]
     185        else:
     186            ret = [obj]
     187
     188        ret.extend([self._nested(child, format_callback)
     189                    for child in self.children.get(key, ())])
     190        return ret
     191
     192    def nested(self, format_callback=None):
     193        """
     194        Return the graph as a nested list.
     195
     196        """
     197        roots = []
     198        for key in self.seen.keys():
     199            if key not in self.parents:
     200                roots.extend(self._nested(key, format_callback))
     201        return roots
     202
    189203
    190204def model_format_dict(obj):
    191205    """
  • django/db/models/base.py

    diff --git a/django/db/models/base.py b/django/db/models/base.py
    a b  
    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

    diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py
    a b  
    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
  • tests/regressiontests/admin_util/models.py

    diff --git a/tests/regressiontests/admin_util/models.py b/tests/regressiontests/admin_util/models.py
    a b  
    1616    def test_from_model_with_override(self):
    1717        return "nothing"
    1818    test_from_model_with_override.short_description = "not what you expect"
     19
     20class Count(models.Model):
     21    num = models.PositiveSmallIntegerField()
  • tests/regressiontests/admin_util/tests.py

    diff --git a/tests/regressiontests/admin_util/tests.py b/tests/regressiontests/admin_util/tests.py
    a b  
    11import unittest
    22
    33from django.db import models
     4from django.test import TestCase
    45
    56from django.contrib import admin
    67from django.contrib.admin.util import display_for_field, label_for_field
    78from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
     9from django.contrib.admin.util import NestedObjects
    810
    9 from models import Article
     11from models import Article, Count
    1012
    1113
     14class NestedObjectsTests(TestCase):
     15    """
     16    Tests for ``NestedObject`` utility collection.
    1217
    13 class UtilTests(unittest.TestCase):
     18    """
     19    def setUp(self):
     20        self.n = NestedObjects()
     21        self.objs = [Count.objects.create(num=i) for i in range(5)]
     22
     23    def _check(self, target):
     24        self.assertEquals(self.n.nested(lambda obj: obj.num), target)
     25
     26    def _add(self, obj, parent=None):
     27        # don't bother providing the extra args that NestedObjects ignores
     28        self.n.add(None, None, obj, None, parent)
     29
     30    def test_unrelated_roots(self):
     31        self._add(self.objs[0])
     32        self._add(self.objs[1])
     33        self._add(self.objs[2], self.objs[1])
     34
     35        self._check([0, 1, [2]])
     36
     37    def test_duplicate_instances(self):
     38        self._add(self.objs[0])
     39        self._add(self.objs[1])
     40        dupe = Count.objects.get(num=1)
     41        self._add(dupe, self.objs[0])
     42
     43        self._check([0, 1])
     44
     45    def test_non_added_parent(self):
     46        self._add(self.objs[0], self.objs[1])
     47
     48        self._check([0])
     49
     50    def test_cyclic(self):
     51        self._add(self.objs[0], self.objs[2])
     52        self._add(self.objs[1], self.objs[0])
     53        self._add(self.objs[2], self.objs[1])
     54        self._add(self.objs[0], self.objs[2])
     55
     56        self._check([0, [1, [2]]])
     57
     58class FieldDisplayTests(unittest.TestCase):
    1459
    1560    def test_null_display_for_field(self):
    1661        """
  • new file tests/regressiontests/admin_views/fixtures/deleted-objects.xml

    diff --git a/tests/regressiontests/admin_views/fixtures/deleted-objects.xml b/tests/regressiontests/admin_views/fixtures/deleted-objects.xml
    new file mode 100644
    - +  
     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/models.py

    diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py
    a b  
    478478    def get_changelist(self, request, **kwargs):
    479479        return CustomChangeList
    480480
     481class Villain(models.Model):
     482    name = models.CharField(max_length=100)
     483
     484    def __unicode__(self):
     485        return self.name
     486
     487class Plot(models.Model):
     488    name = models.CharField(max_length=100)
     489    team_leader = models.ForeignKey(Villain, related_name='lead_plots')
     490    contact = models.ForeignKey(Villain, related_name='contact_plots')
     491
     492    def __unicode__(self):
     493        return self.name
     494
     495class PlotDetails(models.Model):
     496    details = models.CharField(max_length=100)
     497    plot = models.OneToOneField(Plot)
     498
     499    def __unicode__(self):
     500        return self.details
     501
     502class SecretHideout(models.Model):
     503    """ Secret! Not registered with the admin! """
     504    location = models.CharField(max_length=100)
     505    villain = models.ForeignKey(Villain)
     506
     507    def __unicode__(self):
     508        return self.location
     509
     510class CyclicOne(models.Model):
     511    name = models.CharField(max_length=25)
     512    two = models.ForeignKey('CyclicTwo')
     513
     514    def __unicode__(self):
     515        return self.name
     516
     517class CyclicTwo(models.Model):
     518    name = models.CharField(max_length=25)
     519    one = models.ForeignKey(CyclicOne)
     520
     521    def __unicode__(self):
     522        return self.name
     523
    481524admin.site.register(Article, ArticleAdmin)
    482525admin.site.register(CustomArticle, CustomArticleAdmin)
    483526admin.site.register(Section, save_as=True, inlines=[ArticleInline])
     
    503546admin.site.register(Category, CategoryAdmin)
    504547admin.site.register(Post, PostAdmin)
    505548admin.site.register(Gadget, GadgetAdmin)
     549admin.site.register(Villain)
     550admin.site.register(Plot)
     551admin.site.register(PlotDetails)
     552admin.site.register(CyclicOne)
     553admin.site.register(CyclicTwo)
    506554
    507555# We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
    508556# That way we cover all four cases:
  • tests/regressiontests/admin_views/tests.py

    diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py
    a b  
    1414from django.utils.cache import get_max_age
    1515from django.utils.html import escape
    1616from django.utils.translation import get_date_formats
     17from django.utils.encoding import iri_to_uri
    1718
    1819# local test models
    1920from models import Article, BarAccount, CustomArticle, EmptyModel, \
    2021    ExternalSubscriber, FooAccount, Gallery, ModelWithStringPrimaryKey, \
    2122    Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast, \
    2223    Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit, \
    23     Category, Post
     24    Category, Post, Plot
    2425
    2526
    2627class AdminViewBasicTest(TestCase):
     
    615616        response = self.client.get('/test_admin/admin/secure-view/')
    616617        self.assertContains(response, 'id="login-form"')
    617618
     619
     620class AdminViewDeletedObjectsTest(TestCase):
     621    fixtures = ['admin-views-users.xml', 'deleted-objects.xml']
     622
     623    def setUp(self):
     624        self.client.login(username='super', password='secret')
     625
     626    def tearDown(self):
     627        self.client.logout()
     628
     629    def test_nesting(self):
     630        """
     631        Objects should be nested to display the relationships that
     632        cause them to be scheduled for deletion.
     633        """
     634        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>""")
     635        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1))
     636        self.failUnless(pattern.search(response.content))
     637
     638    def test_cyclic(self):
     639        """
     640        Cyclic relationships should still cause each object to only be
     641        listed once.
     642
     643        """
     644        one = """<li>Cyclic one: <a href="/test_admin/admin/admin_views/cyclicone/1/">I am recursive</a>"""
     645        two = """<li>Cyclic two: <a href="/test_admin/admin/admin_views/cyclictwo/1/">I am recursive too</a>"""
     646        response = self.client.get('/test_admin/admin/admin_views/cyclicone/%s/delete/' % quote(1))
     647
     648        self.assertContains(response, one, 1)
     649        self.assertContains(response, two, 1)
     650
     651    def test_perms_needed(self):
     652        self.client.logout()
     653        delete_user = User.objects.get(username='deleteuser')
     654        delete_user.user_permissions.add(get_perm(Plot,
     655            Plot._meta.get_delete_permission()))
     656
     657        self.failUnless(self.client.login(username='deleteuser',
     658                                          password='secret'))
     659
     660        response = self.client.get('/test_admin/admin/admin_views/plot/%s/delete/' % quote(1))
     661        self.assertContains(response, "your account doesn't have permission to delete the following types of objects")
     662        self.assertContains(response, "<li>plot details</li>")
     663
     664
     665    def test_not_registered(self):
     666        should_contain = """<li>Secret hideout: underground bunker"""
     667        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1))
     668        self.assertContains(response, should_contain, 1)
     669
     670    def test_multiple_fkeys_to_same_model(self):
     671        """
     672        If a deleted object has two relationships from another model,
     673        both of those should be followed in looking for related
     674        objects to delete.
     675
     676        """
     677        should_contain = """<li>Plot: <a href="/test_admin/admin/admin_views/plot/1/">World Domination</a>"""
     678        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1))
     679        self.assertContains(response, should_contain)
     680        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(2))
     681        self.assertContains(response, should_contain)
     682
     683    def test_multiple_fkeys_to_same_instance(self):
     684        """
     685        If a deleted object has two relationships pointing to it from
     686        another object, the other object should still only be listed
     687        once.
     688
     689        """
     690        should_contain = """<li>Plot: <a href="/test_admin/admin/admin_views/plot/2/">World Peace</a></li>"""
     691        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(2))
     692        self.assertContains(response, should_contain, 1)
     693
    618694class AdminViewStringPrimaryKeyTest(TestCase):
    619695    fixtures = ['admin-views-users.xml', 'string-primary-key.xml']
    620696
     
    677753    def test_deleteconfirmation_link(self):
    678754        "The link from the delete confirmation page referring back to the changeform of the object should be quoted"
    679755        response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/delete/' % quote(self.pk))
    680         should_contain = """<a href="../../%s/">%s</a>""" % (quote(self.pk), escape(self.pk))
     756        # this URL now comes through reverse(), thus iri_to_uri encoding
     757        should_contain = """/%s/">%s</a>""" % (iri_to_uri(quote(self.pk)), escape(self.pk))
    681758        self.assertContains(response, should_contain)
    682759
    683760    def test_url_conflicts_with_add(self):
Back to Top