Ticket #9475: 9475.m2m_add_remove.r12009.diff
File 9475.m2m_add_remove.r12009.diff, 22.7 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
418 418 419 419 return manager 420 420 421 def create_many_related_manager(superclass, rel=False):421 def create_many_related_manager(superclass, field): 422 422 """Creates a manager that subclasses 'superclass' (which is a Manager) 423 423 and adds behavior for many-to-many related objects.""" 424 through = rel.through 424 through = field.rel.through 425 can_add = field.can_add() 426 can_remove = field.can_remove() 427 425 428 class ManyRelatedManager(superclass): 426 429 def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None, 427 430 join_table=None, source_field_name=None, target_field_name=None): … … 440 443 def get_query_set(self): 441 444 return superclass.get_query_set(self).using(self.instance._state.db)._next_is_sticky().filter(**(self.core_filters)) 442 445 443 # If the ManyToMany relation has an intermediary model, 444 # the add and remove methods do not exist. 445 if rel.through._meta.auto_created: 446 if can_add: 446 447 def add(self, *objs): 447 448 self._add_items(self.source_field_name, self.target_field_name, *objs) 448 449 … … 451 452 self._add_items(self.target_field_name, self.source_field_name, *objs) 452 453 add.alters_data = True 453 454 455 if can_remove: 454 456 def remove(self, *objs): 455 457 self._remove_items(self.source_field_name, self.target_field_name, *objs) 456 458 … … 470 472 def create(self, **kwargs): 471 473 # This check needs to be done here, since we can't later remove this 472 474 # from the method lookup table, as we do with add and remove. 473 if not rel.through._meta.auto_created:475 if not can_add: 474 476 opts = through._meta 475 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)477 raise AttributeError("Cannot use create() on this ManyToManyField. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)) 476 478 new_obj = super(ManyRelatedManager, self).using(self.instance._state.db).create(**kwargs) 477 479 self.add(new_obj) 478 480 return new_obj 479 481 create.alters_data = True 480 482 481 483 def get_or_create(self, **kwargs): 484 # This check needs to be done here, since we can't later remove this 485 # from the method lookup table, as we do with add and remove. 486 if not can_add: 487 opts = through._meta 488 raise AttributeError, "Cannot use get_or_create() on this ManyToManyField. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name) 482 489 obj, created = \ 483 490 super(ManyRelatedManager, self).using(self.instance._state.db).get_or_create(**kwargs) 484 491 # We only need to add() if created because if we got an object back … … 568 575 # model's default manager. 569 576 rel_model = self.related.model 570 577 superclass = rel_model._default_manager.__class__ 571 RelatedManager = create_many_related_manager(superclass, self.related.field .rel)578 RelatedManager = create_many_related_manager(superclass, self.related.field) 572 579 573 580 manager = RelatedManager( 574 581 model=rel_model, … … 585 592 if instance is None: 586 593 raise AttributeError, "Manager must be accessed via instance" 587 594 588 if not self.related.field. rel.through._meta.auto_created:595 if not self.related.field.can_add(): 589 596 opts = self.related.field.rel.through._meta 590 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)597 raise AttributeError, "Cannot set values on this ManyToManyField. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name) 591 598 592 599 manager = self.__get__(instance) 593 600 manager.clear() … … 618 625 # model's default manager. 619 626 rel_model=self.field.rel.to 620 627 superclass = rel_model._default_manager.__class__ 621 RelatedManager = create_many_related_manager(superclass, self.field .rel)628 RelatedManager = create_many_related_manager(superclass, self.field) 622 629 623 630 manager = RelatedManager( 624 631 model=rel_model, … … 635 642 if instance is None: 636 643 raise AttributeError, "Manager must be accessed via instance" 637 644 638 if not self.field. rel.through._meta.auto_created:645 if not self.field.can_add(): 639 646 opts = self.field.rel.through._meta 640 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)647 raise AttributeError, "Cannot set values a this ManyToManyField. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name) 641 648 642 649 manager = self.__get__(instance) 643 650 manager.clear() … … 883 890 self.db_table = kwargs.pop('db_table', None) 884 891 if kwargs['rel'].through is not None: 885 892 assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used." 893 894 self._can_add = kwargs.pop('can_add', None) 895 self._can_remove = kwargs.pop('can_remove', None) 886 896 887 897 Field.__init__(self, **kwargs) 888 898 … … 901 911 else: 902 912 return util.truncate_name('%s_%s' % (opts.db_table, self.name), 903 913 connection.ops.max_name_length()) 904 905 def _get_m2m_attr(self, related, attr): 906 "Function that can be curried to provide the source column name for the m2m table" 907 cache_attr = '_m2m_%s_cache' % attr 914 915 def _get_intermediary_fields(self, related): 916 cache_attr = '_m2m_intermediary_fields_cache' 908 917 if hasattr(self, cache_attr): 909 918 return getattr(self, cache_attr) 919 920 candidates = [] 921 related_candidates = [] 922 auto_add = True 910 923 for f in self.rel.through._meta.fields: 911 if hasattr(f,'rel') and f.rel and f.rel.to == related.model: 912 setattr(self, cache_attr, getattr(f, attr)) 913 return getattr(self, cache_attr) 924 if hasattr(f,'rel') and f.rel: 925 if f.rel.to == related.model: 926 candidates.append(f) 927 continue 928 elif f.rel.to == related.parent_model: 929 related_candidates.append(f) 930 continue 931 if isinstance(f, AutoField) or f.null or f.has_default(): 932 continue 933 elif getattr(f, 'auto_now_add', False) or getattr(f, 'auto_now', False): 934 continue 935 else: 936 auto_add = False 937 if related.model == related.parent_model: 938 # m2m to self 939 assert len(candidates) == 2, "There are too many ForeignKeys to %s" % related.model 940 field, related_field = candidates 941 else: 942 assert len(candidates) == 1, "There are no ForeignKeys to %s" % related.model 943 assert len(related_candidates) == 1, "There are no ForeignKeys to %s" % related.parent_model 944 # TODO: intelligently pick a candidate if there is more than one. For now, just use the first. 945 field, related_field = candidates[0], related_candidates[0] 914 946 947 if self._can_add is None: 948 self._can_add = auto_add 949 950 if self._can_remove is None: 951 self._can_remove = False 952 unique_together = [frozenset(ut) for ut in self.rel.through._meta.unique_together] 953 names = frozenset([field.name, related_field.name]) 954 for ut in unique_together: 955 if names <= ut: 956 self._can_remove = True 957 break 958 959 setattr(self, cache_attr, (field, related_field)) 960 return (field, related_field) 961 962 def _get_can_add(self, related): 963 if self._can_add is None: 964 self._get_intermediary_fields(related) 965 return self._can_add 966 967 def _get_can_remove(self, related): 968 if self._can_remove is None: 969 self._get_intermediary_fields(related) 970 return self._can_remove 971 972 def _get_m2m_attr(self, related, attr): 973 "Function that can be curried to provide a source field attribute" 974 cache_attr = '_m2m_%s_cache' % attr 975 if not hasattr(self, cache_attr): 976 field, _ = self._get_intermediary_fields(related) 977 setattr(self, cache_attr, getattr(field, attr)) 978 return getattr(self, cache_attr) 979 915 980 def _get_m2m_reverse_attr(self, related, attr): 916 "Function that can be curried to provide the related column name for the m2m table"981 "Function that can be curried to provide a related field attribute" 917 982 cache_attr = '_m2m_reverse_%s_cache' % attr 918 if hasattr(self, cache_attr): 919 return getattr(self, cache_attr) 920 found = False 921 for f in self.rel.through._meta.fields: 922 if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model: 923 if related.model == related.parent_model: 924 # If this is an m2m-intermediate to self, 925 # the first foreign key you find will be 926 # the source column. Keep searching for 927 # the second foreign key. 928 if found: 929 setattr(self, cache_attr, getattr(f, attr)) 930 break 931 else: 932 found = True 933 else: 934 setattr(self, cache_attr, getattr(f, attr)) 935 break 983 if not hasattr(self, cache_attr): 984 _, related_field = self._get_intermediary_fields(related) 985 setattr(self, cache_attr, getattr(related_field, attr)) 936 986 return getattr(self, cache_attr) 937 987 938 988 def isValidIDList(self, field_data, all_data): … … 1017 1067 1018 1068 self.m2m_field_name = curry(self._get_m2m_attr, related, 'name') 1019 1069 self.m2m_reverse_field_name = curry(self._get_m2m_reverse_attr, related, 'name') 1070 1071 self.can_add = curry(self._get_can_add, related) 1072 self.can_remove = curry(self._get_can_remove, related) 1020 1073 1021 1074 def set_attributes_from_rel(self): 1022 1075 pass