Ticket #6191: 6191_r12299.diff

File 6191_r12299.diff, 29.0 KB (added by Carl Meyer, 15 years ago)

updated patch; removes unused added import

  • 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  
    10731073
    10741074        # Populate deleted_objects, a data structure of all related objects that
    10751075        # will also be deleted.
    1076         deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), object_id, escape(obj))), []]
    1077         perms_needed = set()
    1078         get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site)
     1076        (deleted_objects, perms_needed) = get_deleted_objects((obj,), opts, request.user, self.admin_site)
    10791077
    10801078        if request.POST: # The user has already confirmed the deletion.
    10811079            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  
    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)
     61def get_deleted_objects(objs, opts, user, admin_site, levels_to_root=4):
     62    """
     63    Find all objects related to ``objs`` that should also be
     64    deleted. ``objs`` should be an iterable of objects.
    6565
    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)
     66    Returns a nested list of strings suitable for display in the
     67    template with the ``unordered_list`` filter.
    7568
    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.
     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)
     77    collector = NestedObjects()
     78    for obj in objs:
     79        # TODO using a private model API!
     80        obj._collect_sub_objects(collector)
     81
     82    perms_needed = set()
     83
     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())
     101            if not user.has_perm(p):
     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)))
    125108        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
     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))
    158113
    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
     114    to_delete = collector.nested(_format_callback)
    164115
    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)
     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
    189205
    190206def model_format_dict(obj):
    191207    """
  • 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_siblings(self):
     38        self._add(self.objs[0])
     39        self._add(self.objs[1], self.objs[0])
     40        self._add(self.objs[2], self.objs[0])
     41
     42        self._check([0, [1, 2]])
     43
     44    def test_duplicate_instances(self):
     45        self._add(self.objs[0])
     46        self._add(self.objs[1])
     47        dupe = Count.objects.get(num=1)
     48        self._add(dupe, self.objs[0])
     49
     50        self._check([0, 1])
     51
     52    def test_non_added_parent(self):
     53        self._add(self.objs[0], self.objs[1])
     54
     55        self._check([0])
     56
     57    def test_cyclic(self):
     58        self._add(self.objs[0], self.objs[2])
     59        self._add(self.objs[1], self.objs[0])
     60        self._add(self.objs[2], self.objs[1])
     61        self._add(self.objs[0], self.objs[2])
     62
     63        self._check([0, [1, [2]]])
     64
     65class FieldDisplayTests(unittest.TestCase):
    1466
    1567    def test_null_display_for_field(self):
    1668        """
  • 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  
    479479    def get_changelist(self, request, **kwargs):
    480480        return CustomChangeList
    481481
     482class Villain(models.Model):
     483    name = models.CharField(max_length=100)
     484
     485    def __unicode__(self):
     486        return self.name
     487
     488class Plot(models.Model):
     489    name = models.CharField(max_length=100)
     490    team_leader = models.ForeignKey(Villain, related_name='lead_plots')
     491    contact = models.ForeignKey(Villain, related_name='contact_plots')
     492
     493    def __unicode__(self):
     494        return self.name
     495
     496class PlotDetails(models.Model):
     497    details = models.CharField(max_length=100)
     498    plot = models.OneToOneField(Plot)
     499
     500    def __unicode__(self):
     501        return self.details
     502
     503class SecretHideout(models.Model):
     504    """ Secret! Not registered with the admin! """
     505    location = models.CharField(max_length=100)
     506    villain = models.ForeignKey(Villain)
     507
     508    def __unicode__(self):
     509        return self.location
     510
     511class CyclicOne(models.Model):
     512    name = models.CharField(max_length=25)
     513    two = models.ForeignKey('CyclicTwo')
     514
     515    def __unicode__(self):
     516        return self.name
     517
     518class CyclicTwo(models.Model):
     519    name = models.CharField(max_length=25)
     520    one = models.ForeignKey(CyclicOne)
     521
     522    def __unicode__(self):
     523        return self.name
     524
    482525admin.site.register(Article, ArticleAdmin)
    483526admin.site.register(CustomArticle, CustomArticleAdmin)
    484527admin.site.register(Section, save_as=True, inlines=[ArticleInline])
     
    504547admin.site.register(Category, CategoryAdmin)
    505548admin.site.register(Post, PostAdmin)
    506549admin.site.register(Gadget, GadgetAdmin)
     550admin.site.register(Villain)
     551admin.site.register(Plot)
     552admin.site.register(PlotDetails)
     553admin.site.register(CyclicOne)
     554admin.site.register(CyclicTwo)
    507555
    508556# We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
    509557# 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  
    1515from django.utils.cache import get_max_age
    1616from django.utils.html import escape
    1717from django.utils.translation import get_date_formats
     18from django.utils.encoding import iri_to_uri
    1819
    1920# local test models
    2021from models import Article, BarAccount, CustomArticle, EmptyModel, \
    2122    ExternalSubscriber, FooAccount, Gallery, ModelWithStringPrimaryKey, \
    2223    Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast, \
    2324    Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit, \
    24     Category, Post
     25    Category, Post, Plot
    2526
    2627
    2728class AdminViewBasicTest(TestCase):
     
    634635        response = self.client.get('/test_admin/admin/secure-view/')
    635636        self.assertContains(response, 'id="login-form"')
    636637
     638
     639class AdminViewDeletedObjectsTest(TestCase):
     640    fixtures = ['admin-views-users.xml', 'deleted-objects.xml']
     641
     642    def setUp(self):
     643        self.client.login(username='super', password='secret')
     644
     645    def tearDown(self):
     646        self.client.logout()
     647
     648    def test_nesting(self):
     649        """
     650        Objects should be nested to display the relationships that
     651        cause them to be scheduled for deletion.
     652        """
     653        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>""")
     654        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1))
     655        self.failUnless(pattern.search(response.content))
     656
     657    def test_cyclic(self):
     658        """
     659        Cyclic relationships should still cause each object to only be
     660        listed once.
     661
     662        """
     663        one = """<li>Cyclic one: <a href="/test_admin/admin/admin_views/cyclicone/1/">I am recursive</a>"""
     664        two = """<li>Cyclic two: <a href="/test_admin/admin/admin_views/cyclictwo/1/">I am recursive too</a>"""
     665        response = self.client.get('/test_admin/admin/admin_views/cyclicone/%s/delete/' % quote(1))
     666
     667        self.assertContains(response, one, 1)
     668        self.assertContains(response, two, 1)
     669
     670    def test_perms_needed(self):
     671        self.client.logout()
     672        delete_user = User.objects.get(username='deleteuser')
     673        delete_user.user_permissions.add(get_perm(Plot,
     674            Plot._meta.get_delete_permission()))
     675
     676        self.failUnless(self.client.login(username='deleteuser',
     677                                          password='secret'))
     678
     679        response = self.client.get('/test_admin/admin/admin_views/plot/%s/delete/' % quote(1))
     680        self.assertContains(response, "your account doesn't have permission to delete the following types of objects")
     681        self.assertContains(response, "<li>plot details</li>")
     682
     683
     684    def test_not_registered(self):
     685        should_contain = """<li>Secret hideout: underground bunker"""
     686        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1))
     687        self.assertContains(response, should_contain, 1)
     688
     689    def test_multiple_fkeys_to_same_model(self):
     690        """
     691        If a deleted object has two relationships from another model,
     692        both of those should be followed in looking for related
     693        objects to delete.
     694
     695        """
     696        should_contain = """<li>Plot: <a href="/test_admin/admin/admin_views/plot/1/">World Domination</a>"""
     697        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1))
     698        self.assertContains(response, should_contain)
     699        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(2))
     700        self.assertContains(response, should_contain)
     701
     702    def test_multiple_fkeys_to_same_instance(self):
     703        """
     704        If a deleted object has two relationships pointing to it from
     705        another object, the other object should still only be listed
     706        once.
     707
     708        """
     709        should_contain = """<li>Plot: <a href="/test_admin/admin/admin_views/plot/2/">World Peace</a></li>"""
     710        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(2))
     711        self.assertContains(response, should_contain, 1)
     712
    637713class AdminViewStringPrimaryKeyTest(TestCase):
    638714    fixtures = ['admin-views-users.xml', 'string-primary-key.xml']
    639715
     
    696772    def test_deleteconfirmation_link(self):
    697773        "The link from the delete confirmation page referring back to the changeform of the object should be quoted"
    698774        response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/delete/' % quote(self.pk))
    699         should_contain = """<a href="../../%s/">%s</a>""" % (quote(self.pk), escape(self.pk))
     775        # this URL now comes through reverse(), thus iri_to_uri encoding
     776        should_contain = """/%s/">%s</a>""" % (iri_to_uri(quote(self.pk)), escape(self.pk))
    700777        self.assertContains(response, should_contain)
    701778
    702779    def test_url_conflicts_with_add(self):
Back to Top