Ticket #6191: 6191_r12299.diff
File 6191_r12299.diff, 29.0 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 1073 1073 1074 1074 # Populate deleted_objects, a data structure of all related objects that 1075 1075 # 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) 1079 1077 1080 1078 if request.POST: # The user has already confirmed the deletion. 1081 1079 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 7 7 from django.utils.encoding import force_unicode, smart_unicode, smart_str 8 8 from django.utils.translation import ungettext, ugettext as _ 9 9 from django.core.urlresolvers import reverse, NoReverseMatch 10 from django.utils.datastructures import SortedDict 10 11 11 12 12 13 def quote(s): … … 57 58 field_names.append(field) 58 59 return field_names 59 60 60 def _nest_help(obj, depth, val): 61 current = obj 62 for i in range(depth): 63 current = current[-1] 64 current.append(val) 61 def 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. 65 65 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. 75 68 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. 82 72 83 73 This is for backwards compatibility since the options.delete_selected 84 74 method uses this function also from a change_list view. 85 75 This will not be used if we can reverse the URL. 86 76 """ 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))) 125 108 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)) 158 113 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) 164 115 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 119 class 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 189 205 190 206 def model_format_dict(obj): 191 207 """ -
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_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 65 class FieldDisplayTests(unittest.TestCase): 14 66 15 67 def test_null_display_for_field(self): 16 68 """ -
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 479 479 def get_changelist(self, request, **kwargs): 480 480 return CustomChangeList 481 481 482 class Villain(models.Model): 483 name = models.CharField(max_length=100) 484 485 def __unicode__(self): 486 return self.name 487 488 class 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 496 class 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 503 class 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 511 class 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 518 class 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 482 525 admin.site.register(Article, ArticleAdmin) 483 526 admin.site.register(CustomArticle, CustomArticleAdmin) 484 527 admin.site.register(Section, save_as=True, inlines=[ArticleInline]) … … 504 547 admin.site.register(Category, CategoryAdmin) 505 548 admin.site.register(Post, PostAdmin) 506 549 admin.site.register(Gadget, GadgetAdmin) 550 admin.site.register(Villain) 551 admin.site.register(Plot) 552 admin.site.register(PlotDetails) 553 admin.site.register(CyclicOne) 554 admin.site.register(CyclicTwo) 507 555 508 556 # We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2. 509 557 # 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 15 15 from django.utils.cache import get_max_age 16 16 from django.utils.html import escape 17 17 from django.utils.translation import get_date_formats 18 from django.utils.encoding import iri_to_uri 18 19 19 20 # local test models 20 21 from models import Article, BarAccount, CustomArticle, EmptyModel, \ 21 22 ExternalSubscriber, FooAccount, Gallery, ModelWithStringPrimaryKey, \ 22 23 Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast, \ 23 24 Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit, \ 24 Category, Post 25 Category, Post, Plot 25 26 26 27 27 28 class AdminViewBasicTest(TestCase): … … 634 635 response = self.client.get('/test_admin/admin/secure-view/') 635 636 self.assertContains(response, 'id="login-form"') 636 637 638 639 class 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 637 713 class AdminViewStringPrimaryKeyTest(TestCase): 638 714 fixtures = ['admin-views-users.xml', 'string-primary-key.xml'] 639 715 … … 696 772 def test_deleteconfirmation_link(self): 697 773 "The link from the delete confirmation page referring back to the changeform of the object should be quoted" 698 774 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)) 700 777 self.assertContains(response, should_contain) 701 778 702 779 def test_url_conflicts_with_add(self):