Ticket #9475: 9475.m2m_add_remove.r11739.diff
File 9475.m2m_add_remove.r11739.diff, 22.6 KB (added by , 15 years ago) |
---|
-
tests/modeltests/m2m_through/models.py
122 122 123 123 ### Forward Descriptors Tests ### 124 124 125 # Due to complications with adding via an intermediary model, 126 # the add method is not provided. 127 >>> rock.members.add(bob) 128 Traceback (most recent call last): 129 ... 130 AttributeError: 'ManyRelatedManager' object has no attribute 'add' 131 132 # Create is also disabled as it suffers from the same problems as add. 133 >>> rock.members.create(name='Anne') 134 Traceback (most recent call last): 135 ... 136 AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use m2m_through.Membership's Manager instead. 137 138 # Remove has similar complications, and is not provided either. 139 >>> rock.members.remove(jim) 140 Traceback (most recent call last): 141 ... 142 AttributeError: 'ManyRelatedManager' object has no attribute 'remove' 143 144 # Here we back up the list of all members of Rock. 145 >>> backup = list(rock.members.all()) 146 147 # ...and we verify that it has worked. 148 >>> backup 149 [<Person: Jane>, <Person: Jim>] 150 151 # The clear function should still work. 125 # The clear function should work. 152 126 >>> rock.members.clear() 153 127 154 128 # Now there will be no members of Rock. 155 129 >>> rock.members.all() 156 130 [] 157 131 158 # Assignment should not work with models specifying a through model for many of159 # the same reasons as adding.160 >>> rock.members = backup161 Traceback (most recent call last):162 ...163 AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use m2m_through.Membership's Manager instead.164 165 132 # Let's re-save those instances that we've cleared. 166 133 >>> m1.save() 167 134 >>> m2.save() … … 173 140 174 141 ### Reverse Descriptors Tests ### 175 142 176 # Due to complications with adding via an intermediary model, 177 # the add method is not provided. 178 >>> bob.group_set.add(rock) 179 Traceback (most recent call last): 180 ... 181 AttributeError: 'ManyRelatedManager' object has no attribute 'add' 182 183 # Create is also disabled as it suffers from the same problems as add. 184 >>> bob.group_set.create(name='Funk') 185 Traceback (most recent call last): 186 ... 187 AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use m2m_through.Membership's Manager instead. 188 189 # Remove has similar complications, and is not provided either. 190 >>> jim.group_set.remove(rock) 191 Traceback (most recent call last): 192 ... 193 AttributeError: 'ManyRelatedManager' object has no attribute 'remove' 194 195 # Here we back up the list of all of Jim's groups. 196 >>> backup = list(jim.group_set.all()) 197 >>> backup 198 [<Group: Rock>, <Group: Roll>] 199 200 # The clear function should still work. 143 # The clear function should work. 201 144 >>> jim.group_set.clear() 202 145 203 146 # Now Jim will be in no groups. 204 147 >>> jim.group_set.all() 205 148 [] 206 149 207 # Assignment should not work with models specifying a through model for many of208 # the same reasons as adding.209 >>> jim.group_set = backup210 Traceback (most recent call last):211 ...212 AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use m2m_through.Membership's Manager instead.213 214 150 # Let's re-save those instances that we've cleared. 215 151 >>> m1.save() 216 152 >>> m4.save() -
tests/modeltests/m2m_add_and_remove/__init__.py
1 2 -
tests/modeltests/m2m_add_and_remove/models.py
1 from django.db import models 2 from django.test import TestCase 3 4 5 class M(models.Model): 6 default = models.ManyToManyField("R", related_name="default_m_set") 7 default_cannot_remove = models.ManyToManyField("R", can_remove=False, related_name="default_cannot_remove_m_set") 8 default_cannot_add = models.ManyToManyField("R", can_add=False, related_name="default_cannot_add_m_set") 9 through_default = models.ManyToManyField("R", through="ThroughDefault", related_name="through_default_m_set") 10 through_auto = models.ManyToManyField("R", through="ThroughAuto", related_name="through_auto_m_set") 11 through_ut = models.ManyToManyField("R", through="ThroughUT", related_name="through_ut_m_set") 12 through_can_add = models.ManyToManyField("R", can_add=True, through="ThroughCanAdd", related_name="through_can_add_m_set") 13 through_can_remove = models.ManyToManyField("R", can_remove=True, through="ThroughCanRemove", related_name="through_can_remove_m_set") 14 15 class R(models.Model): 16 name = models.CharField(max_length=30) 17 18 class Through(models.Model): 19 m = models.ForeignKey(M, related_name="%(class)s_set") 20 r = models.ForeignKey(R, related_name="%(class)s_set") 21 22 class Meta: 23 abstract = True 24 25 class ThroughDefault(Through): 26 extra = models.CharField(max_length=10) 27 28 class ThroughAuto(Through): 29 ctime = models.DateTimeField(auto_now_add=True) 30 mtime = models.DateTimeField(auto_now=True) 31 default = models.IntegerField(default=42) 32 null = models.DateTimeField(null=True) 33 34 class ThroughUT(Through): 35 extra = models.CharField(max_length=10) 36 37 class Meta: 38 unique_together = ('m', 'r') 39 40 class ThroughCanRemove(Through): 41 extra = models.CharField(max_length=10) 42 43 class ThroughCanAdd(Through): 44 extra = models.CharField(max_length=10) 45 46 def save(self, **kwargs): 47 self.extra = "foo" 48 return super(ThroughCanAdd, self).save(**kwargs) 49 50 class M2mAddRemoveTests(TestCase): 51 def assert_cannot_remove(self, name): 52 m = M.objects.create() 53 r = R.objects.create() 54 manager = getattr(m, name) 55 reverse_manager = getattr(r, "%s_m_set" % name) 56 self.assertRaises(AttributeError, getattr, manager, 'remove') 57 self.assertRaises(AttributeError, getattr, reverse_manager, 'remove') 58 59 def assert_cannot_add(self, name): 60 reverse_name = "%s_m_set" % name 61 m = M.objects.create() 62 r = R.objects.create() 63 manager = getattr(m, name) 64 reverse_manager = getattr(r, reverse_name) 65 self.assertRaises(AttributeError, getattr, manager, 'add') 66 self.assertRaises(AttributeError, getattr, reverse_manager, 'add') 67 self.assertRaises(AttributeError, manager.create) 68 self.assertRaises(AttributeError, reverse_manager.create) 69 def assign(): 70 setattr(m, name, []) 71 self.assertRaises(AttributeError, assign) 72 def assign_reverse(): 73 setattr(r, reverse_name, []) 74 self.assertRaises(AttributeError, assign_reverse) 75 76 def assert_can_add(self, name): 77 reverse_name = "%s_m_set" % name 78 m = M.objects.create() 79 r = R.objects.create() 80 manager = getattr(m, name) 81 reverse_manager = getattr(r, reverse_name) 82 83 manager.add(r) 84 self.failUnlessEqual(list(manager.all()), [r]) 85 self.failUnlessEqual(list(reverse_manager.all()), [m]) 86 manager.add(r) 87 self.failUnlessEqual(list(manager.all()), [r]) 88 self.failUnlessEqual(list(reverse_manager.all()), [m]) 89 manager.clear() 90 91 reverse_manager.add(m) 92 self.failUnlessEqual(list(manager.all()), [r]) 93 self.failUnlessEqual(list(reverse_manager.all()), [m]) 94 reverse_manager.add(m) 95 self.failUnlessEqual(list(manager.all()), [r]) 96 self.failUnlessEqual(list(reverse_manager.all()), [m]) 97 reverse_manager.clear() 98 99 r2 = manager.create() 100 reverse_manager2 = getattr(r2, reverse_name) 101 self.failUnlessEqual(list(manager.all()), [r2]) 102 self.failUnlessEqual(list(reverse_manager2.all()), [m]) 103 manager.clear() 104 105 m2 = reverse_manager.create() 106 manager2 = getattr(m2, name) 107 self.failUnlessEqual(list(manager2.all()), [r]) 108 self.failUnlessEqual(list(reverse_manager.all()), [m2]) 109 reverse_manager.clear() 110 111 setattr(m, name, [r]) 112 self.failUnlessEqual(list(manager.all()), [r]) 113 manager.clear() 114 115 setattr(r, reverse_name, [m]) 116 self.failUnlessEqual(list(reverse_manager.all()), [m]) 117 reverse_manager.clear() 118 119 def assert_can_remove(self, name, extra): 120 through = M._meta.get_field(name).rel.through 121 m = M.objects.create() 122 r = R.objects.create() 123 124 def fill(): 125 for extra_kwargs in extra: 126 kwargs = {'m': m, 'r': r} 127 kwargs.update(extra_kwargs) 128 through.objects.create(**kwargs) 129 130 manager = getattr(m, name) 131 reverse_manager = getattr(r, "%s_m_set" % name) 132 133 fill() 134 manager.remove(r) 135 self.failIf(manager.exists()) 136 self.failIf(reverse_manager.exists()) 137 138 fill() 139 reverse_manager.remove(m) 140 self.failIf(reverse_manager.exists()) 141 self.failIf(manager.exists()) 142 143 def _test_managers(self, name, can_remove=False, can_add=False, extra=()): 144 if can_add: 145 self.assert_can_add(name) 146 else: 147 self.assert_cannot_add(name) 148 if can_remove: 149 self.assert_can_remove(name, extra) 150 else: 151 self.assert_cannot_remove(name) 152 153 def test_default(self): 154 self._test_managers('default', can_add=True, can_remove=True, extra=[{}]) 155 156 def test_default_cannot_remove(self): 157 self._test_managers('default_cannot_remove', can_add=True, can_remove=False) 158 159 def test_default_cannot_add(self): 160 self._test_managers('default_cannot_add', can_add=False, can_remove=True, extra=[{}]) 161 162 def test_through_default(self): 163 self._test_managers('through_default', can_add=False, can_remove=False) 164 165 def test_through_auto(self): 166 self._test_managers('through_auto', can_add=True, can_remove=False) 167 168 def test_through_ut(self): 169 self._test_managers('through_ut', can_add=False, can_remove=True, extra=[{'extra': 'foo'}]) 170 171 def test_through_can_add(self): 172 self._test_managers('through_can_add', can_add=True, can_remove=False) 173 174 def test_through_can_remove(self): 175 self._test_managers('through_can_remove', can_add=False, can_remove=True, extra=[{'extra': 'foo'}, {'extra': 'bar'}]) 176 177 No newline at end of file -
tests/regressiontests/m2m_through_regress/models.py
80 80 >>> roll.members.all() 81 81 [<Person: Bob>] 82 82 83 # Error messages use the model name, not repr of the class name84 >>> bob.group_set = []85 Traceback (most recent call last):86 ...87 AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use m2m_through_regress.Membership's Manager instead.88 89 >>> roll.members = []90 Traceback (most recent call last):91 ...92 AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use m2m_through_regress.Membership's Manager instead.93 94 >>> rock.members.create(name='Anne')95 Traceback (most recent call last):96 ...97 AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use m2m_through_regress.Membership's Manager instead.98 99 >>> bob.group_set.create(name='Funk')100 Traceback (most recent call last):101 ...102 AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use m2m_through_regress.Membership's Manager instead.103 104 83 # Now test that the intermediate with a relationship outside 105 84 # the current app (i.e., UserMembership) workds 106 85 >>> UserMembership.objects.create(user=frank, group=rock) -
django/db/models/fields/related.py
405 405 406 406 return manager 407 407 408 def create_many_related_manager(superclass, rel=False):408 def create_many_related_manager(superclass, field): 409 409 """Creates a manager that subclasses 'superclass' (which is a Manager) 410 410 and adds behavior for many-to-many related objects.""" 411 through = rel.through 411 through = field.rel.through 412 can_add = field.can_add() 413 can_remove = field.can_remove() 414 412 415 class ManyRelatedManager(superclass): 413 416 def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None, 414 417 join_table=None, source_field_name=None, target_field_name=None): … … 427 430 def get_query_set(self): 428 431 return superclass.get_query_set(self)._next_is_sticky().filter(**(self.core_filters)) 429 432 430 # If the ManyToMany relation has an intermediary model, 431 # the add and remove methods do not exist. 432 if rel.through._meta.auto_created: 433 if can_add: 433 434 def add(self, *objs): 434 435 self._add_items(self.source_field_name, self.target_field_name, *objs) 435 436 … … 438 439 self._add_items(self.target_field_name, self.source_field_name, *objs) 439 440 add.alters_data = True 440 441 442 if can_remove: 441 443 def remove(self, *objs): 442 444 self._remove_items(self.source_field_name, self.target_field_name, *objs) 443 445 … … 457 459 def create(self, **kwargs): 458 460 # This check needs to be done here, since we can't later remove this 459 461 # from the method lookup table, as we do with add and remove. 460 if not rel.through._meta.auto_created:462 if not can_add: 461 463 opts = through._meta 462 raise AttributeError, "Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)464 raise AttributeError, "Cannot use create() on this ManyToManyField. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name) 463 465 new_obj = super(ManyRelatedManager, self).create(**kwargs) 464 466 self.add(new_obj) 465 467 return new_obj 466 468 create.alters_data = True 467 469 468 470 def get_or_create(self, **kwargs): 471 # This check needs to be done here, since we can't later remove this 472 # from the method lookup table, as we do with add and remove. 473 if not can_add: 474 opts = through._meta 475 raise AttributeError, "Cannot use get_or_create() on this ManyToManyField. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name) 469 476 obj, created = \ 470 477 super(ManyRelatedManager, self).get_or_create(**kwargs) 471 478 # We only need to add() if created because if we got an object back … … 552 559 # model's default manager. 553 560 rel_model = self.related.model 554 561 superclass = rel_model._default_manager.__class__ 555 RelatedManager = create_many_related_manager(superclass, self.related.field .rel)562 RelatedManager = create_many_related_manager(superclass, self.related.field) 556 563 557 564 manager = RelatedManager( 558 565 model=rel_model, … … 569 576 if instance is None: 570 577 raise AttributeError, "Manager must be accessed via instance" 571 578 572 if not self.related.field. rel.through._meta.auto_created:579 if not self.related.field.can_add(): 573 580 opts = self.related.field.rel.through._meta 574 raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)581 raise AttributeError, "Cannot set values on this ManyToManyField. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name) 575 582 576 583 manager = self.__get__(instance) 577 584 manager.clear() … … 602 609 # model's default manager. 603 610 rel_model=self.field.rel.to 604 611 superclass = rel_model._default_manager.__class__ 605 RelatedManager = create_many_related_manager(superclass, self.field .rel)612 RelatedManager = create_many_related_manager(superclass, self.field) 606 613 607 614 manager = RelatedManager( 608 615 model=rel_model, … … 619 626 if instance is None: 620 627 raise AttributeError, "Manager must be accessed via instance" 621 628 622 if not self.field. rel.through._meta.auto_created:629 if not self.field.can_add(): 623 630 opts = self.field.rel.through._meta 624 raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)631 raise AttributeError, "Cannot set values a this ManyToManyField. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name) 625 632 626 633 manager = self.__get__(instance) 627 634 manager.clear() … … 870 877 self.db_table = kwargs.pop('db_table', None) 871 878 if kwargs['rel'].through is not None: 872 879 assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used." 880 881 self._can_add = kwargs.pop('can_add', None) 882 self._can_remove = kwargs.pop('can_remove', None) 873 883 874 884 Field.__init__(self, **kwargs) 875 885 … … 888 898 else: 889 899 return util.truncate_name('%s_%s' % (opts.db_table, self.name), 890 900 connection.ops.max_name_length()) 891 892 def _get_m2m_attr(self, related, attr): 893 "Function that can be curried to provide the source column name for the m2m table" 894 cache_attr = '_m2m_%s_cache' % attr 901 902 def _get_intermediary_fields(self, related): 903 cache_attr = '_m2m_intermediary_fields_cache' 895 904 if hasattr(self, cache_attr): 896 905 return getattr(self, cache_attr) 906 907 candidates = [] 908 related_candidates = [] 909 auto_add = True 897 910 for f in self.rel.through._meta.fields: 898 if hasattr(f,'rel') and f.rel and f.rel.to == related.model: 899 setattr(self, cache_attr, getattr(f, attr)) 900 return getattr(self, cache_attr) 911 if hasattr(f,'rel') and f.rel: 912 if f.rel.to == related.model: 913 candidates.append(f) 914 continue 915 elif f.rel.to == related.parent_model: 916 related_candidates.append(f) 917 continue 918 if isinstance(f, AutoField) or f.null or f.has_default(): 919 continue 920 elif getattr(f, 'auto_now_add', False) or getattr(f, 'auto_now', False): 921 continue 922 else: 923 auto_add = False 924 if related.model == related.parent_model: 925 # m2m to self 926 assert len(candidates) == 2, "There are too many ForeignKeys to %s" % related.model 927 field, related_field = candidates 928 else: 929 assert len(candidates) == 1, "There are no ForeignKeys to %s" % related.model 930 assert len(related_candidates) == 1, "There are no ForeignKeys to %s" % related.parent_model 931 # TODO: intelligently pick a candidate if there is more than one. For now, just use the first. 932 field, related_field = candidates[0], related_candidates[0] 901 933 934 if self._can_add is None: 935 self._can_add = auto_add 936 937 if self._can_remove is None: 938 self._can_remove = False 939 unique_together = [frozenset(ut) for ut in self.rel.through._meta.unique_together] 940 names = frozenset([field.name, related_field.name]) 941 for ut in unique_together: 942 if names <= ut: 943 self._can_remove = True 944 break 945 946 setattr(self, cache_attr, (field, related_field)) 947 return (field, related_field) 948 949 def _get_can_add(self, related): 950 if self._can_add is None: 951 self._get_intermediary_fields(related) 952 return self._can_add 953 954 def _get_can_remove(self, related): 955 if self._can_remove is None: 956 self._get_intermediary_fields(related) 957 return self._can_remove 958 959 def _get_m2m_attr(self, related, attr): 960 "Function that can be curried to provide a source field attribute" 961 cache_attr = '_m2m_%s_cache' % attr 962 if not hasattr(self, cache_attr): 963 field, _ = self._get_intermediary_fields(related) 964 setattr(self, cache_attr, getattr(field, attr)) 965 return getattr(self, cache_attr) 966 902 967 def _get_m2m_reverse_attr(self, related, attr): 903 "Function that can be curried to provide the related column name for the m2m table"968 "Function that can be curried to provide a related field attribute" 904 969 cache_attr = '_m2m_reverse_%s_cache' % attr 905 if hasattr(self, cache_attr): 906 return getattr(self, cache_attr) 907 found = False 908 for f in self.rel.through._meta.fields: 909 if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model: 910 if related.model == related.parent_model: 911 # If this is an m2m-intermediate to self, 912 # the first foreign key you find will be 913 # the source column. Keep searching for 914 # the second foreign key. 915 if found: 916 setattr(self, cache_attr, getattr(f, attr)) 917 break 918 else: 919 found = True 920 else: 921 setattr(self, cache_attr, getattr(f, attr)) 922 break 970 if not hasattr(self, cache_attr): 971 _, related_field = self._get_intermediary_fields(related) 972 setattr(self, cache_attr, getattr(related_field, attr)) 923 973 return getattr(self, cache_attr) 924 974 925 975 def isValidIDList(self, field_data, all_data): … … 1004 1054 1005 1055 self.m2m_field_name = curry(self._get_m2m_attr, related, 'name') 1006 1056 self.m2m_reverse_field_name = curry(self._get_m2m_reverse_attr, related, 'name') 1057 1058 self.can_add = curry(self._get_can_add, related) 1059 self.can_remove = curry(self._get_can_remove, related) 1007 1060 1008 1061 def set_attributes_from_rel(self): 1009 1062 pass