Ticket #6191: 6191_r12578_incorporated_minor_review_items.diff
File 6191_r12578_incorporated_minor_review_items.diff, 34.3 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. … … 66 58 context = { 67 59 "title": _("Are you sure?"), 68 60 "object_name": force_unicode(opts.verbose_name), 69 "deletable_objects": deletable_objects,61 "deletable_objects": [deletable_objects], 70 62 'queryset': queryset, 71 63 "perms_lacking": perms_needed, 72 64 "opts": opts, -
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 delet eable_object in deletable_objects %}24 <ul>{{ delet eable_object|unordered_list }}</ul>23 {% for deletable_object in deletable_objects %} 24 <ul>{{ deletable_object|unordered_list }}</ul> 25 25 {% endfor %} 26 26 <form action="" method="post">{% csrf_token %} 27 27 <div> -
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 _format_callback(obj, user, admin_site, levels_to_root, perms_needed): 61 has_admin = obj.__class__ in admin_site._registry 62 opts = obj._meta 63 try: 64 admin_url = reverse('%s:%s_%s_change' 65 % (admin_site.name, 66 opts.app_label, 67 opts.object_name.lower()), 68 None, (quote(obj._get_pk_val()),)) 69 except NoReverseMatch: 70 admin_url = '%s%s/%s/%s/' % ('../'*levels_to_root, 71 opts.app_label, 72 opts.object_name.lower(), 73 quote(obj._get_pk_val())) 74 if has_admin: 75 p = '%s.%s' % (opts.app_label, 76 opts.get_delete_permission()) 77 if not user.has_perm(p): 78 perms_needed.add(opts.verbose_name) 79 # Display a link to the admin page. 80 return mark_safe(u'%s: <a href="%s">%s</a>' % 81 (escape(capfirst(opts.verbose_name)), 82 admin_url, 83 escape(obj))) 84 else: 85 # Don't display link to edit, because it either has no 86 # admin or is edited inline. 87 return u'%s: %s' % (capfirst(opts.verbose_name), 88 force_unicode(obj)) 65 89 66 def get_ change_view_url(app_label, module_name, pk, admin_site, levels_to_root):90 def get_deleted_objects(objs, opts, user, admin_site, levels_to_root=4): 67 91 """ 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) 92 Find all objects related to ``objs`` that should also be 93 deleted. ``objs`` should be an iterable of objects. 75 94 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. 95 Returns a nested list of strings suitable for display in the 96 template with the ``unordered_list`` filter. 79 97 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. 98 `levels_to_root` defines the number of directories (../) to reach 99 the admin root path. In a change_view this is 4, in a change_list 100 view 2. 82 101 83 102 This is for backwards compatibility since the options.delete_selected 84 103 method uses this function also from a change_list view. 85 104 This will not be used if we can reverse the URL. 86 105 """ 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) 106 collector = NestedObjects() 107 for obj in objs: 108 # TODO using a private model API! 109 obj._collect_sub_objects(collector) 110 111 # TODO this is needed only because GenericRelations are 112 # cascaded-deleted way down in the internals in 113 # DeleteQuery.delete_batch_related, instead of being found by 114 # _collect_sub_objects. 115 from django.contrib.contenttypes import generic 116 for f in obj._meta.many_to_many: 117 if isinstance(f, generic.GenericRelation): 118 rel_manager = f.value_from_object(obj) 119 for related in rel_manager.all(): 120 # There's a wierdness here in the case that the 121 # generic-related object also has FKs pointing to it 122 # from elsewhere. DeleteQuery does not follow those 123 # FKs or delete any such objects explicitly (which is 124 # probably a bug). Some databases may cascade those 125 # deletes themselves, and some won't. So do we report 126 # those objects as to-be-deleted? No right answer; for 127 # now we opt to report only on objects that Django 128 # will explicitly delete, at risk that some further 129 # objects will be silently deleted by a 130 # referential-integrity-maintaining database. 131 collector.add(related.__class__, related.pk, related, 132 obj.__class__, obj) 133 134 perms_needed = set() 135 136 to_delete = collector.nested(_format_callback, 137 user=user, 138 admin_site=admin_site, 139 levels_to_root=levels_to_root, 140 perms_needed=perms_needed) 141 142 return to_delete, perms_needed 143 144 145 class NestedObjects(object): 146 """ 147 A directed acyclic graph collection that exposes the add() API 148 expected by Model._collect_sub_objects and can present its data as 149 a nested list of objects. 150 151 """ 152 def __init__(self): 153 # Use object keys of the form (model, pk) because actual model 154 # objects may not be unique 155 156 # maps object key to list of child keys 157 self.children = SortedDict() 158 159 # maps object key to parent key 160 self.parents = SortedDict() 161 162 # maps object key to actual object 163 self.seen = SortedDict() 164 165 def add(self, model, pk, obj, 166 parent_model=None, parent_obj=None, nullable=False): 167 """ 168 Add item ``obj`` to the graph. Returns True (and does nothing) 169 if the item has been seen already. 170 171 The ``parent_obj`` argument must already exist in the graph; if 172 not, it's ignored (but ``obj`` is still added with no 173 parent). In any case, Model._collect_sub_objects (for whom 174 this API exists) will never pass a parent that hasn't already 175 been added itself. 176 177 These restrictions in combination ensure the graph will remain 178 acyclic (but can have multiple roots). 179 180 ``model``, ``pk``, and ``parent_model`` arguments are ignored 181 in favor of the appropriate lookups on ``obj`` and 182 ``parent_obj``; unlike CollectedObjects, we can't maintain 183 independence from the knowledge that we're operating on model 184 instances, and we don't want to allow for inconsistency. 185 186 ``nullable`` arg is ignored: it doesn't affect how the tree of 187 collected objects should be nested for display. 188 """ 189 model, pk = type(obj), obj._get_pk_val() 190 191 key = model, pk 192 193 if key in self.seen: 194 return True 195 self.seen.setdefault(key, obj) 196 197 if parent_obj is not None: 198 parent_model, parent_pk = (type(parent_obj), 199 parent_obj._get_pk_val()) 200 parent_key = (parent_model, parent_pk) 201 if parent_key in self.seen: 202 self.children.setdefault(parent_key, list()).append(key) 203 self.parents.setdefault(key, parent_key) 204 205 def _nested(self, key, format_callback=None, **kwargs): 206 obj = self.seen[key] 207 if format_callback: 208 ret = [format_callback(obj, **kwargs)] 125 209 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 210 ret = [obj] 158 211 159 # related.get_accessor_name() could return None for symmetrical relationships160 if rel_opts_name:161 rel_objs = getattr(obj, rel_opts_name, None)162 if rel_objs:163 has_related_objs = True212 children = [] 213 for child in self.children.get(key, ()): 214 children.extend(self._nested(child, format_callback, **kwargs)) 215 if children: 216 ret.append(children) 164 217 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) 218 return ret 219 220 def nested(self, format_callback=None, **kwargs): 221 """ 222 Return the graph as a nested list. 223 224 Passes **kwargs back to the format_callback as kwargs. 225 226 """ 227 roots = [] 228 for key in self.seen.keys(): 229 if key not in self.parents: 230 roots.extend(self._nested(key, format_callback, **kwargs)) 231 return roots 232 189 233 190 234 def model_format_dict(obj): 191 235 """ -
django/db/models/base.py
diff --git a/django/db/models/base.py b/django/db/models/base.py
a b 549 549 (model_class, {pk_val: obj, pk_val: obj, ...}), ...] 550 550 """ 551 551 pk_val = self._get_pk_val() 552 if seen_objs.add(self.__class__, pk_val, self, parent, nullable): 552 if seen_objs.add(self.__class__, pk_val, self, 553 type(parent), parent, nullable): 553 554 return 554 555 555 556 for related in self._meta.get_all_related_objects(): … … 560 561 except ObjectDoesNotExist: 561 562 pass 562 563 else: 563 sub_obj._collect_sub_objects(seen_objs, self .__class__, related.field.null)564 sub_obj._collect_sub_objects(seen_objs, self, related.field.null) 564 565 else: 565 566 # To make sure we can access all elements, we can't use the 566 567 # normal manager on the related object. So we work directly … … 578 579 continue 579 580 delete_qs = rel_descriptor.delete_manager(self).all() 580 581 for sub_obj in delete_qs: 581 sub_obj._collect_sub_objects(seen_objs, self .__class__, related.field.null)582 sub_obj._collect_sub_objects(seen_objs, self, related.field.null) 582 583 583 584 # Handle any ancestors (for the model-inheritance case). We do this by 584 585 # 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="3" model="admin_views.villain"> 10 <field type="CharField" name="name">Bob</field> 11 </object> 12 <object pk="3" model="admin_views.supervillain"> 13 </object> 14 <object pk="1" model="admin_views.plot"> 15 <field type="CharField" name="name">World Domination</field> 16 <field type="ForeignKey" name="team_leader">1</field> 17 <field type="ForeignKey" name="contact">2</field> 18 </object> 19 <object pk="2" model="admin_views.plot"> 20 <field type="CharField" name="name">World Peace</field> 21 <field type="ForeignKey" name="team_leader">2</field> 22 <field type="ForeignKey" name="contact">2</field> 23 </object> 24 <object pk="1" model="admin_views.plotdetails"> 25 <field type="CharField" name="details">almost finished</field> 26 <field type="ForeignKey" name="plot">1</field> 27 </object> 28 <object pk="1" model="admin_views.secrethideout"> 29 <field type="CharField" name="location">underground bunker</field> 30 <field type="ForeignKey" name="villain">1</field> 31 </object> 32 <object pk="2" model="admin_views.secrethideout"> 33 <field type="CharField" name="location">floating castle</field> 34 <field type="ForeignKey" name="villain">3</field> 35 </object> 36 <object pk="1" model="admin_views.supersecrethideout"> 37 <field type="CharField" name="location">super floating castle!</field> 38 <field type="ForeignKey" name="supervillain">3</field> 39 </object> 40 <object pk="1" model="admin_views.cyclicone"> 41 <field type="CharField" name="name">I am recursive</field> 42 <field type="ForeignKey" name="two">1</field> 43 </object> 44 <object pk="1" model="admin_views.cyclictwo"> 45 <field type="CharField" name="name">I am recursive too</field> 46 <field type="ForeignKey" name="one">1</field> 47 </object> 48 <object pk="3" model="admin_views.plot"> 49 <field type="CharField" name="name">Corn Conspiracy</field> 50 <field type="ForeignKey" name="team_leader">1</field> 51 <field type="ForeignKey" name="contact">1</field> 52 </object> 53 </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 SuperVillain(Villain): 505 pass 506 507 class FunkyTag(models.Model): 508 "Because we all know there's only one real use case for GFKs." 509 name = models.CharField(max_length=25) 510 content_type = models.ForeignKey(ContentType) 511 object_id = models.PositiveIntegerField() 512 content_object = generic.GenericForeignKey('content_type', 'object_id') 513 514 def __unicode__(self): 515 return self.name 516 517 class Plot(models.Model): 518 name = models.CharField(max_length=100) 519 team_leader = models.ForeignKey(Villain, related_name='lead_plots') 520 contact = models.ForeignKey(Villain, related_name='contact_plots') 521 tags = generic.GenericRelation(FunkyTag) 522 523 def __unicode__(self): 524 return self.name 525 526 class PlotDetails(models.Model): 527 details = models.CharField(max_length=100) 528 plot = models.OneToOneField(Plot) 529 530 def __unicode__(self): 531 return self.details 532 533 class SecretHideout(models.Model): 534 """ Secret! Not registered with the admin! """ 535 location = models.CharField(max_length=100) 536 villain = models.ForeignKey(Villain) 537 538 def __unicode__(self): 539 return self.location 540 541 class SuperSecretHideout(models.Model): 542 """ Secret! Not registered with the admin! """ 543 location = models.CharField(max_length=100) 544 supervillain = models.ForeignKey(SuperVillain) 545 546 def __unicode__(self): 547 return self.location 548 549 class CyclicOne(models.Model): 550 name = models.CharField(max_length=25) 551 two = models.ForeignKey('CyclicTwo') 552 553 def __unicode__(self): 554 return self.name 555 556 class CyclicTwo(models.Model): 557 name = models.CharField(max_length=25) 558 one = models.ForeignKey(CyclicOne) 559 560 def __unicode__(self): 561 return self.name 562 497 563 admin.site.register(Article, ArticleAdmin) 498 564 admin.site.register(CustomArticle, CustomArticleAdmin) 499 565 admin.site.register(Section, save_as=True, inlines=[ArticleInline]) … … 519 585 admin.site.register(Category, CategoryAdmin) 520 586 admin.site.register(Post, PostAdmin) 521 587 admin.site.register(Gadget, GadgetAdmin) 588 admin.site.register(Villain) 589 admin.site.register(SuperVillain) 590 admin.site.register(Plot) 591 admin.site.register(PlotDetails) 592 admin.site.register(CyclicOne) 593 admin.site.register(CyclicTwo) 522 594 523 595 # We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2. 524 596 # 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_inheritance(self): 717 """ 718 In the case of an inherited model, if either the child or 719 parent-model instance is deleted, both instances are listed 720 for deletion, as well as any relationships they have. 721 722 """ 723 should_contain = [ 724 """<li>Villain: <a href="/test_admin/admin/admin_views/villain/3/">Bob</a>""", 725 """<li>Super villain: <a href="/test_admin/admin/admin_views/supervillain/3/">Bob</a>""", 726 """<li>Secret hideout: floating castle""", 727 """<li>Super secret hideout: super floating castle!""" 728 ] 729 response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(3)) 730 for should in should_contain: 731 self.assertContains(response, should, 1) 732 response = self.client.get('/test_admin/admin/admin_views/supervillain/%s/delete/' % quote(3)) 733 for should in should_contain: 734 self.assertContains(response, should, 1) 735 736 def test_generic_relations(self): 737 """ 738 If a deleted object has GenericForeignKeys pointing to it, 739 those objects should be listed for deletion. 740 741 """ 742 plot = Plot.objects.get(pk=3) 743 tag = FunkyTag.objects.create(content_object=plot, name='hott') 744 should_contain = """<li>Funky tag: hott""" 745 response = self.client.get('/test_admin/admin/admin_views/plot/%s/delete/' % quote(3)) 746 self.assertContains(response, should_contain) 747 640 748 class AdminViewStringPrimaryKeyTest(TestCase): 641 749 fixtures = ['admin-views-users.xml', 'string-primary-key.xml'] 642 750 … … 699 807 def test_deleteconfirmation_link(self): 700 808 "The link from the delete confirmation page referring back to the changeform of the object should be quoted" 701 809 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)) 810 # this URL now comes through reverse(), thus iri_to_uri encoding 811 should_contain = """/%s/">%s</a>""" % (iri_to_uri(quote(self.pk)), escape(self.pk)) 703 812 self.assertContains(response, should_contain) 704 813 705 814 def test_url_conflicts_with_add(self):