Ticket #6191: 6191_r12203.2.diff
File 6191_r12203.2.diff, 28.9 KB (added by , 15 years ago) |
---|
-
django/contrib/admin/actions.py
diff --git a/django/contrib/admin/actions.py b/django/contrib/admin/actions.py
a b 36 36 37 37 # Populate deletable_objects, a data structure of all related objects that 38 38 # 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) 48 40 49 41 # The user has already confirmed the deletion. 50 42 # 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 1068 1068 1069 1069 # Populate deleted_objects, a data structure of all related objects that 1070 1070 # 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) 1074 1072 1075 1073 if request.POST: # The user has already confirmed the deletion. 1076 1074 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 20 20 </ul> 21 21 {% else %} 22 22 <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> 26 24 <form action="" method="post">{% csrf_token %} 27 25 <div> 28 26 {% 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 1 import urllib 2 1 3 from django.core.exceptions import ObjectDoesNotExist 2 4 from django.db import models 3 5 from django.utils import formats … … 7 9 from django.utils.encoding import force_unicode, smart_unicode, smart_str 8 10 from django.utils.translation import ungettext, ugettext as _ 9 11 from django.core.urlresolvers import reverse, NoReverseMatch 12 from django.utils.datastructures import SortedDict 10 13 11 14 12 15 def quote(s): … … 57 60 field_names.append(field) 58 61 return field_names 59 62 60 def _nest_help(obj, depth, val): 61 current = obj 62 for i in range(depth): 63 current = current[-1] 64 current.append(val) 63 def 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. 65 67 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. 75 70 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. 82 74 83 75 This is for backwards compatibility since the options.delete_selected 84 76 method uses this function also from a change_list view. 85 77 This will not be used if we can reverse the URL. 86 78 """ 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))) 125 110 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)) 158 115 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) 164 117 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 121 class 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 189 203 190 204 def model_format_dict(obj): 191 205 """ -
django/db/models/base.py
diff --git a/django/db/models/base.py b/django/db/models/base.py
a b 545 545 (model_class, {pk_val: obj, pk_val: obj, ...}), ...] 546 546 """ 547 547 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): 549 550 return 550 551 551 552 for related in self._meta.get_all_related_objects(): … … 556 557 except ObjectDoesNotExist: 557 558 pass 558 559 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) 560 561 else: 561 562 # To make sure we can access all elements, we can't use the 562 563 # normal manager on the related object. So we work directly … … 574 575 continue 575 576 delete_qs = rel_descriptor.delete_manager(self).all() 576 577 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) 578 579 579 580 # Handle any ancestors (for the model-inheritance case). We do this by 580 581 # 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 50 50 else: 51 51 self.blocked = {} 52 52 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): 54 54 """ 55 55 Adds an item to the container. 56 56 … … 60 60 * obj - the object itself. 61 61 * parent_model - the model of the parent object that this object was 62 62 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) 63 65 * nullable - should be True if this relation is nullable. 64 66 65 67 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 16 16 def test_from_model_with_override(self): 17 17 return "nothing" 18 18 test_from_model_with_override.short_description = "not what you expect" 19 20 class 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 1 1 import unittest 2 2 3 3 from django.db import models 4 from django.test import TestCase 4 5 5 6 from django.contrib import admin 6 7 from django.contrib.admin.util import display_for_field, label_for_field 7 8 from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE 9 from django.contrib.admin.util import NestedObjects 8 10 9 from models import Article 11 from models import Article, Count 10 12 11 13 14 class NestedObjectsTests(TestCase): 15 """ 16 Tests for ``NestedObject`` utility collection. 12 17 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 58 class FieldDisplayTests(unittest.TestCase): 14 59 15 60 def test_null_display_for_field(self): 16 61 """ -
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 478 478 def get_changelist(self, request, **kwargs): 479 479 return CustomChangeList 480 480 481 class Villain(models.Model): 482 name = models.CharField(max_length=100) 483 484 def __unicode__(self): 485 return self.name 486 487 class 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 495 class 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 502 class 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 510 class 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 517 class 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 481 524 admin.site.register(Article, ArticleAdmin) 482 525 admin.site.register(CustomArticle, CustomArticleAdmin) 483 526 admin.site.register(Section, save_as=True, inlines=[ArticleInline]) … … 503 546 admin.site.register(Category, CategoryAdmin) 504 547 admin.site.register(Post, PostAdmin) 505 548 admin.site.register(Gadget, GadgetAdmin) 549 admin.site.register(Villain) 550 admin.site.register(Plot) 551 admin.site.register(PlotDetails) 552 admin.site.register(CyclicOne) 553 admin.site.register(CyclicTwo) 506 554 507 555 # We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2. 508 556 # 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 14 14 from django.utils.cache import get_max_age 15 15 from django.utils.html import escape 16 16 from django.utils.translation import get_date_formats 17 from django.utils.encoding import iri_to_uri 17 18 18 19 # local test models 19 20 from models import Article, BarAccount, CustomArticle, EmptyModel, \ 20 21 ExternalSubscriber, FooAccount, Gallery, ModelWithStringPrimaryKey, \ 21 22 Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast, \ 22 23 Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit, \ 23 Category, Post 24 Category, Post, Plot 24 25 25 26 26 27 class AdminViewBasicTest(TestCase): … … 615 616 response = self.client.get('/test_admin/admin/secure-view/') 616 617 self.assertContains(response, 'id="login-form"') 617 618 619 620 class 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 618 694 class AdminViewStringPrimaryKeyTest(TestCase): 619 695 fixtures = ['admin-views-users.xml', 'string-primary-key.xml'] 620 696 … … 677 753 def test_deleteconfirmation_link(self): 678 754 "The link from the delete confirmation page referring back to the changeform of the object should be quoted" 679 755 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)) 681 758 self.assertContains(response, should_contain) 682 759 683 760 def test_url_conflicts_with_add(self):