Ticket #6191: 6191_r12556.diff
File 6191_r12556.diff, 29.7 KB (added by , 15 years ago) |
---|
-
django/db/models/base.py
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
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 -
django/contrib/admin/options.py
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/util.py
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) 65 66 def get_change_view_url(app_label, module_name, pk, admin_site, levels_to_root): 61 def get_deleted_objects(objs, opts, user, admin_site, levels_to_root=4): 67 62 """ 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) 63 Find all objects related to ``objs`` that should also be 64 deleted. ``objs`` should be an iterable of objects. 75 65 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. 66 Returns a nested list of strings suitable for display in the 67 template with the ``unordered_list`` filter. 79 68 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) 125 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 77 collector = NestedObjects() 78 for obj in objs: 79 # TODO using a private model API! 80 obj._collect_sub_objects(collector) 158 81 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 82 perms_needed = set() 164 83 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()) 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()) 187 101 if not user.has_perm(p): 188 perms_needed.add(related.opts.verbose_name) 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))) 108 else: 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)) 189 113 114 to_delete = collector.nested(_format_callback) 115 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 205 190 206 def model_format_dict(obj): 191 207 """ 192 208 Return a `dict` with keys 'verbose_name' and 'verbose_name_plural', -
django/contrib/admin/actions.py
36 36 37 37 # Populate deletable_objects, a data structure of all related objects that 38 38 # will also be deleted. 39 (deletable_objects, perms_needed) = get_deleted_objects(queryset, opts, request.user, modeladmin.admin_site, levels_to_root=2) 39 40 40 # deletable_objects must be a list if we want to use '|unordered_list' in the template41 deletable_objects = []42 perms_needed = set()43 i = 044 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+148 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. 51 43 if request.POST.get('post'): -
django/contrib/admin/templates/admin/delete_selected_confirmation.html
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 %} -
tests/regressiontests/admin_views/fixtures/deleted-objects.xml
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/tests.py
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 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 640 716 class AdminViewStringPrimaryKeyTest(TestCase): 641 717 fixtures = ['admin-views-users.xml', 'string-primary-key.xml'] 642 718 … … 699 775 def test_deleteconfirmation_link(self): 700 776 "The link from the delete confirmation page referring back to the changeform of the object should be quoted" 701 777 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)) 778 # this URL now comes through reverse(), thus iri_to_uri encoding 779 should_contain = """/%s/">%s</a>""" % (iri_to_uri(quote(self.pk)), escape(self.pk)) 703 780 self.assertContains(response, should_contain) 704 781 705 782 def test_url_conflicts_with_add(self): -
tests/regressiontests/admin_views/models.py
494 494 def get_changelist(self, request, **kwargs): 495 495 return CustomChangeList 496 496 497 class Villain(models.Model): 498 name = models.CharField(max_length=100) 499 500 def __unicode__(self): 501 return self.name 502 503 class Plot(models.Model): 504 name = models.CharField(max_length=100) 505 team_leader = models.ForeignKey(Villain, related_name='lead_plots') 506 contact = models.ForeignKey(Villain, related_name='contact_plots') 507 508 def __unicode__(self): 509 return self.name 510 511 class PlotDetails(models.Model): 512 details = models.CharField(max_length=100) 513 plot = models.OneToOneField(Plot) 514 515 def __unicode__(self): 516 return self.details 517 518 class SecretHideout(models.Model): 519 """ Secret! Not registered with the admin! """ 520 location = models.CharField(max_length=100) 521 villain = models.ForeignKey(Villain) 522 523 def __unicode__(self): 524 return self.location 525 526 class CyclicOne(models.Model): 527 name = models.CharField(max_length=25) 528 two = models.ForeignKey('CyclicTwo') 529 530 def __unicode__(self): 531 return self.name 532 533 class CyclicTwo(models.Model): 534 name = models.CharField(max_length=25) 535 one = models.ForeignKey(CyclicOne) 536 537 def __unicode__(self): 538 return self.name 539 497 540 admin.site.register(Article, ArticleAdmin) 498 541 admin.site.register(CustomArticle, CustomArticleAdmin) 499 542 admin.site.register(Section, save_as=True, inlines=[ArticleInline]) … … 519 562 admin.site.register(Category, CategoryAdmin) 520 563 admin.site.register(Post, PostAdmin) 521 564 admin.site.register(Gadget, GadgetAdmin) 565 admin.site.register(Villain) 566 admin.site.register(Plot) 567 admin.site.register(PlotDetails) 568 admin.site.register(CyclicOne) 569 admin.site.register(CyclicTwo) 522 570 523 571 # We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2. 524 572 # That way we cover all four cases: -
tests/regressiontests/admin_util/tests.py
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. 15 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 68 16 69 class UtilTests(unittest.TestCase): 17 70 def test_values_from_lookup_field(self): 18 71 """ -
tests/regressiontests/admin_util/models.py
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()