Ticket #6191: 6191_r12557.diff
File 6191_r12557.diff, 31.8 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 1080 1080 1081 1081 # Populate deleted_objects, a data structure of all related objects that 1082 1082 # will also be deleted. 1083 deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), object_id, escape(obj))), []] 1084 perms_needed = set() 1085 get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site) 1083 (deleted_objects, perms_needed) = get_deleted_objects((obj,), opts, request.user, self.admin_site) 1086 1084 1087 1085 if request.POST: # The user has already confirmed the deletion. 1088 1086 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 10 from django.utils.datastructures import SortedDict 11 11 12 12 def quote(s): 13 13 """ … … 57 57 field_names.append(field) 58 58 return field_names 59 59 60 def _nest_help(obj, depth, val): 61 current = obj 62 for i in range(depth): 63 current = current[-1] 64 current.append(val) 60 def get_deleted_objects(objs, opts, user, admin_site, levels_to_root=4): 61 """ 62 Find all objects related to ``objs`` that should also be 63 deleted. ``objs`` should be an iterable of objects. 65 64 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) 65 Returns a nested list of strings suitable for display in the 66 template with the ``unordered_list`` filter. 75 67 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. 68 `levels_to_root` defines the number of directories (../) to reach 69 the admin root path. In a change_view this is 4, in a change_list 70 view 2. 82 71 83 72 This is for backwards compatibility since the options.delete_selected 84 73 method uses this function also from a change_list view. 85 74 This will not be used if we can reverse the URL. 86 75 """ 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) 76 collector = NestedObjects() 77 for obj in objs: 78 # TODO using a private model API! 79 obj._collect_sub_objects(collector) 80 81 # TODO this is needed only because GenericRelations are 82 # cascaded-deleted way down in the internals in 83 # DeleteQuery.delete_batch_related, instead of being found by 84 # _collect_sub_objects. 85 from django.contrib.contenttypes import generic 86 for f in obj._meta.many_to_many: 87 if isinstance(f, generic.GenericRelation): 88 rel_manager = f.value_from_object(obj) 89 for related in rel_manager.all(): 90 # There's a wierdness here in the case that the 91 # generic-related object also has FKs pointing to it 92 # from elsewhere. DeleteQuery does not follow those 93 # FKs or delete any such objects explicitly (which is 94 # probably a bug). Some databases may cascade those 95 # deletes themselves, and some won't. So do we report 96 # those objects as to-be-deleted? No right answer; for 97 # now we opt to report only on objects that Django 98 # will explicitly delete, at risk that some further 99 # objects will be silently deleted by a 100 # referential-integrity-maintaining database. 101 collector.add(related.__class__, related.pk, related, 102 obj.__class__, obj) 103 104 perms_needed = set() 105 106 def _format_callback(obj): 107 has_admin = obj.__class__ in admin_site._registry 108 opts = obj._meta 109 try: 110 admin_url = reverse('%s:%s_%s_change' 111 % (admin_site.name, 112 opts.app_label, 113 opts.object_name.lower()), 114 None, (quote(obj._get_pk_val()),)) 115 except NoReverseMatch: 116 admin_url = '%s%s/%s/%s/' % ('../'*levels_to_root, 117 opts.app_label, 118 opts.object_name.lower(), 119 quote(obj._get_pk_val())) 120 if has_admin: 121 p = '%s.%s' % (opts.app_label, 122 opts.get_delete_permission()) 123 if not user.has_perm(p): 124 perms_needed.add(opts.verbose_name) 125 # Display a link to the admin page. 126 return mark_safe(u'%s: <a href="%s">%s</a>' % 127 (escape(capfirst(opts.verbose_name)), 128 admin_url, 129 escape(obj))) 125 130 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 131 # Don't display link to edit, because it either has no 132 # admin or is edited inline. 133 return u'%s: %s' % (capfirst(opts.verbose_name), 134 force_unicode(obj)) 158 135 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 136 to_delete = collector.nested(_format_callback) 164 137 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) 138 return to_delete, perms_needed 139 140 141 class NestedObjects(object): 142 """ 143 A directed acyclic graph collection that exposes the add() API 144 expected by Model._collect_sub_objects and can present its data as 145 a nested list of objects. 146 147 """ 148 def __init__(self): 149 # Use object keys of the form (model, pk) because actual model 150 # objects may not be unique 151 152 # maps object key to set of child keys 153 self.children = SortedDict() 154 155 # maps object key to parent key 156 self.parents = SortedDict() 157 158 # maps object key to actual object 159 self.seen = SortedDict() 160 161 def add(self, model, pk, obj, 162 parent_model=None, parent_obj=None, nullable=False): 163 """ 164 Add item ``obj`` to the graph. Returns True (and does nothing) 165 if the item has been seen already. 166 167 The ``parent_obj`` argument must already exist in the graph; if 168 not, it's ignored (but ``obj`` is still added with no 169 parent). In any case, Model._collect_sub_objects (for whom 170 this API exists) will never pass a parent that hasn't already 171 been added itself. 172 173 These restrictions in combination ensure the graph will remain 174 acyclic (but can have multiple roots). 175 176 ``model``, ``pk``, and ``parent_model`` arguments are ignored 177 in favor of the appropriate lookups on ``obj`` and 178 ``parent_obj``; unlike CollectedObjects, we can't maintain 179 independence from the knowledge that we're operating on model 180 instances, and we don't want to allow for inconsistency. 181 182 ``nullable`` arg is ignored: it doesn't affect how the tree of 183 collected objects should be nested for display. 184 """ 185 model, pk = type(obj), obj._get_pk_val() 186 187 key = model, pk 188 189 if key in self.seen: 190 return True 191 self.seen.setdefault(key, obj) 192 193 if parent_obj is not None: 194 parent_model, parent_pk = (type(parent_obj), 195 parent_obj._get_pk_val()) 196 parent_key = (parent_model, parent_pk) 197 if parent_key in self.seen: 198 self.children.setdefault(parent_key, set()).add(key) 199 self.parents.setdefault(key, parent_key) 200 201 def _nested(self, key, format_callback=None): 202 obj = self.seen[key] 203 if format_callback: 204 ret = [format_callback(obj)] 205 else: 206 ret = [obj] 207 208 children = [] 209 for child in self.children.get(key, ()): 210 children.extend(self._nested(child, format_callback)) 211 if children: 212 ret.append(children) 213 214 return ret 215 216 def nested(self, format_callback=None): 217 """ 218 Return the graph as a nested list. 219 220 """ 221 roots = [] 222 for key in self.seen.keys(): 223 if key not in self.parents: 224 roots.extend(self._nested(key, format_callback)) 225 return roots 226 189 227 190 228 def model_format_dict(obj): 191 229 """ -
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 17 17 def test_from_model_with_override(self): 18 18 return "nothing" 19 19 test_from_model_with_override.short_description = "not what you expect" 20 21 class Count(models.Model): 22 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 2 2 import unittest 3 3 4 4 from django.db import models 5 from django.utils.formats import localize 6 from django.test import TestCase 5 7 6 8 from django.contrib import admin 7 9 from django.contrib.admin.util import display_for_field, label_for_field, lookup_field 8 10 from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE 9 11 from django.contrib.sites.models import Site 10 from django. utils.formats import localize12 from django.contrib.admin.util import NestedObjects 11 13 12 from models import Article 14 from models import Article, Count 13 15 14 16 17 class NestedObjectsTests(TestCase): 18 """ 19 Tests for ``NestedObject`` utility collection. 20 21 """ 22 def setUp(self): 23 self.n = NestedObjects() 24 self.objs = [Count.objects.create(num=i) for i in range(5)] 25 26 def _check(self, target): 27 self.assertEquals(self.n.nested(lambda obj: obj.num), target) 28 29 def _add(self, obj, parent=None): 30 # don't bother providing the extra args that NestedObjects ignores 31 self.n.add(None, None, obj, None, parent) 32 33 def test_unrelated_roots(self): 34 self._add(self.objs[0]) 35 self._add(self.objs[1]) 36 self._add(self.objs[2], self.objs[1]) 37 38 self._check([0, 1, [2]]) 39 40 def test_siblings(self): 41 self._add(self.objs[0]) 42 self._add(self.objs[1], self.objs[0]) 43 self._add(self.objs[2], self.objs[0]) 44 45 self._check([0, [1, 2]]) 46 47 def test_duplicate_instances(self): 48 self._add(self.objs[0]) 49 self._add(self.objs[1]) 50 dupe = Count.objects.get(num=1) 51 self._add(dupe, self.objs[0]) 52 53 self._check([0, 1]) 54 55 def test_non_added_parent(self): 56 self._add(self.objs[0], self.objs[1]) 57 58 self._check([0]) 59 60 def test_cyclic(self): 61 self._add(self.objs[0], self.objs[2]) 62 self._add(self.objs[1], self.objs[0]) 63 self._add(self.objs[2], self.objs[1]) 64 self._add(self.objs[0], self.objs[2]) 65 66 self._check([0, [1, [2]]]) 67 15 68 16 69 class UtilTests(unittest.TestCase): 17 70 def test_values_from_lookup_field(self): -
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 <object pk="3" model="admin_views.plot"> 36 <field type="CharField" name="name">Corn Conspiracy</field> 37 <field type="ForeignKey" name="team_leader">1</field> 38 <field type="ForeignKey" name="contact">1</field> 39 </object> 40 </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 10 10 from django.db import models 11 11 from django import forms 12 12 from django.forms.models import BaseModelFormSet 13 13 from django.contrib.contenttypes import generic 14 from django.contrib.contenttypes.models import ContentType 14 15 15 16 class Section(models.Model): 16 17 """ … … 494 495 def get_changelist(self, request, **kwargs): 495 496 return CustomChangeList 496 497 498 class Villain(models.Model): 499 name = models.CharField(max_length=100) 500 501 def __unicode__(self): 502 return self.name 503 504 class FunkyTag(models.Model): 505 "Because we all know there's only one real use case for GFKs." 506 name = models.CharField(max_length=25) 507 content_type = models.ForeignKey(ContentType) 508 object_id = models.PositiveIntegerField() 509 content_object = generic.GenericForeignKey('content_type', 'object_id') 510 511 def __unicode__(self): 512 return self.name 513 514 class Plot(models.Model): 515 name = models.CharField(max_length=100) 516 team_leader = models.ForeignKey(Villain, related_name='lead_plots') 517 contact = models.ForeignKey(Villain, related_name='contact_plots') 518 tags = generic.GenericRelation(FunkyTag) 519 520 def __unicode__(self): 521 return self.name 522 523 class PlotDetails(models.Model): 524 details = models.CharField(max_length=100) 525 plot = models.OneToOneField(Plot) 526 527 def __unicode__(self): 528 return self.details 529 530 class SecretHideout(models.Model): 531 """ Secret! Not registered with the admin! """ 532 location = models.CharField(max_length=100) 533 villain = models.ForeignKey(Villain) 534 535 def __unicode__(self): 536 return self.location 537 538 class CyclicOne(models.Model): 539 name = models.CharField(max_length=25) 540 two = models.ForeignKey('CyclicTwo') 541 542 def __unicode__(self): 543 return self.name 544 545 class CyclicTwo(models.Model): 546 name = models.CharField(max_length=25) 547 one = models.ForeignKey(CyclicOne) 548 549 def __unicode__(self): 550 return self.name 551 497 552 admin.site.register(Article, ArticleAdmin) 498 553 admin.site.register(CustomArticle, CustomArticleAdmin) 499 554 admin.site.register(Section, save_as=True, inlines=[ArticleInline]) … … 519 574 admin.site.register(Category, CategoryAdmin) 520 575 admin.site.register(Post, PostAdmin) 521 576 admin.site.register(Gadget, GadgetAdmin) 577 admin.site.register(Villain) 578 admin.site.register(Plot) 579 admin.site.register(PlotDetails) 580 admin.site.register(CyclicOne) 581 admin.site.register(CyclicTwo) 522 582 523 583 # We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2. 524 584 # 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 16 16 from django.utils.cache import get_max_age 17 17 from django.utils.html import escape 18 18 from django.utils.translation import get_date_formats 19 from django.utils.encoding import iri_to_uri 19 20 20 21 # local test models 21 22 from models import Article, BarAccount, CustomArticle, EmptyModel, \ 22 23 ExternalSubscriber, FooAccount, Gallery, ModelWithStringPrimaryKey, \ 23 24 Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast, \ 24 25 Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit, \ 25 Category, Post 26 Category, Post, Plot, FunkyTag 26 27 27 28 28 29 class AdminViewBasicTest(TestCase): … … 637 638 response = self.client.get('/test_admin/admin/secure-view/') 638 639 self.assertContains(response, 'id="login-form"') 639 640 641 642 class AdminViewDeletedObjectsTest(TestCase): 643 fixtures = ['admin-views-users.xml', 'deleted-objects.xml'] 644 645 def setUp(self): 646 self.client.login(username='super', password='secret') 647 648 def tearDown(self): 649 self.client.logout() 650 651 def test_nesting(self): 652 """ 653 Objects should be nested to display the relationships that 654 cause them to be scheduled for deletion. 655 """ 656 pattern = re.compile(r"""<li>Plot: <a href=".+/admin_views/plot/1/">World Domination</a>\s*<ul>\s*<li>Plot details: <a href=".+/admin_views/plotdetails/1/">almost finished</a>""") 657 response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1)) 658 self.failUnless(pattern.search(response.content)) 659 660 def test_cyclic(self): 661 """ 662 Cyclic relationships should still cause each object to only be 663 listed once. 664 665 """ 666 one = """<li>Cyclic one: <a href="/test_admin/admin/admin_views/cyclicone/1/">I am recursive</a>""" 667 two = """<li>Cyclic two: <a href="/test_admin/admin/admin_views/cyclictwo/1/">I am recursive too</a>""" 668 response = self.client.get('/test_admin/admin/admin_views/cyclicone/%s/delete/' % quote(1)) 669 670 self.assertContains(response, one, 1) 671 self.assertContains(response, two, 1) 672 673 def test_perms_needed(self): 674 self.client.logout() 675 delete_user = User.objects.get(username='deleteuser') 676 delete_user.user_permissions.add(get_perm(Plot, 677 Plot._meta.get_delete_permission())) 678 679 self.failUnless(self.client.login(username='deleteuser', 680 password='secret')) 681 682 response = self.client.get('/test_admin/admin/admin_views/plot/%s/delete/' % quote(1)) 683 self.assertContains(response, "your account doesn't have permission to delete the following types of objects") 684 self.assertContains(response, "<li>plot details</li>") 685 686 687 def test_not_registered(self): 688 should_contain = """<li>Secret hideout: underground bunker""" 689 response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1)) 690 self.assertContains(response, should_contain, 1) 691 692 def test_multiple_fkeys_to_same_model(self): 693 """ 694 If a deleted object has two relationships from another model, 695 both of those should be followed in looking for related 696 objects to delete. 697 698 """ 699 should_contain = """<li>Plot: <a href="/test_admin/admin/admin_views/plot/1/">World Domination</a>""" 700 response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1)) 701 self.assertContains(response, should_contain) 702 response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(2)) 703 self.assertContains(response, should_contain) 704 705 def test_multiple_fkeys_to_same_instance(self): 706 """ 707 If a deleted object has two relationships pointing to it from 708 another object, the other object should still only be listed 709 once. 710 711 """ 712 should_contain = """<li>Plot: <a href="/test_admin/admin/admin_views/plot/2/">World Peace</a></li>""" 713 response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(2)) 714 self.assertContains(response, should_contain, 1) 715 716 def test_generic_relations(self): 717 """ 718 If a deleted object has GenericForeignKeys pointing to it, 719 those objects should be listed for deletion. 720 721 """ 722 plot = Plot.objects.get(pk=3) 723 tag = FunkyTag.objects.create(content_object=plot, name='hott') 724 should_contain = """<li>Funky tag: hott""" 725 response = self.client.get('/test_admin/admin/admin_views/plot/%s/delete/' % quote(3)) 726 self.assertContains(response, should_contain) 727 640 728 class AdminViewStringPrimaryKeyTest(TestCase): 641 729 fixtures = ['admin-views-users.xml', 'string-primary-key.xml'] 642 730 … … 699 787 def test_deleteconfirmation_link(self): 700 788 "The link from the delete confirmation page referring back to the changeform of the object should be quoted" 701 789 response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/delete/' % quote(self.pk)) 702 should_contain = """<a href="../../%s/">%s</a>""" % (quote(self.pk), escape(self.pk)) 790 # this URL now comes through reverse(), thus iri_to_uri encoding 791 should_contain = """/%s/">%s</a>""" % (iri_to_uri(quote(self.pk)), escape(self.pk)) 703 792 self.assertContains(response, should_contain) 704 793 705 794 def test_url_conflicts_with_add(self):