Ticket #9475: 9475.m2m_add_remove.r12281.diff
File 9475.m2m_add_remove.r12281.diff, 24.4 KB (added by , 15 years ago) |
---|
-
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/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/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
433 433 434 434 return manager 435 435 436 def create_many_related_manager(superclass, rel=False):436 def create_many_related_manager(superclass, field): 437 437 """Creates a manager that subclasses 'superclass' (which is a Manager) 438 438 and adds behavior for many-to-many related objects.""" 439 through = rel.through 439 through = field.rel.through 440 can_add = field.can_add() 441 can_remove = field.can_remove() 442 440 443 class ManyRelatedManager(superclass): 441 444 def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None, 442 445 join_table=None, source_field_name=None, target_field_name=None, … … 458 461 db = router.db_for_read(self.instance.__class__, instance=self.instance) 459 462 return superclass.get_query_set(self).using(db)._next_is_sticky().filter(**(self.core_filters)) 460 463 461 # If the ManyToMany relation has an intermediary model, 462 # the add and remove methods do not exist. 463 if rel.through._meta.auto_created: 464 if can_add: 464 465 def add(self, *objs): 465 466 self._add_items(self.source_field_name, self.target_field_name, *objs) 466 467 … … 469 470 self._add_items(self.target_field_name, self.source_field_name, *objs) 470 471 add.alters_data = True 471 472 473 if can_remove: 472 474 def remove(self, *objs): 473 475 self._remove_items(self.source_field_name, self.target_field_name, *objs) 474 476 … … 488 490 def create(self, **kwargs): 489 491 # This check needs to be done here, since we can't later remove this 490 492 # from the method lookup table, as we do with add and remove. 491 if not rel.through._meta.auto_created:493 if not can_add: 492 494 opts = through._meta 493 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))495 raise AttributeError("Cannot use create() on this ManyToManyField. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)) 494 496 db = router.db_for_write(self.instance.__class__, instance=self.instance) 495 497 new_obj = super(ManyRelatedManager, self).using(db).create(**kwargs) 496 498 self.add(new_obj) … … 498 500 create.alters_data = True 499 501 500 502 def get_or_create(self, **kwargs): 503 # This check needs to be done here, since we can't later remove this 504 # from the method lookup table, as we do with add and remove. 505 if not can_add: 506 opts = through._meta 507 raise AttributeError, "Cannot use get_or_create() on this ManyToManyField. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name) 501 508 db = router.db_for_write(self.instance.__class__, instance=self.instance) 502 509 obj, created = \ 503 510 super(ManyRelatedManager, self).using(db).get_or_create(**kwargs) … … 544 551 if self.reverse or source_field_name == self.source_field_name: 545 552 # Don't send the signal when we are inserting the 546 553 # duplicate data row for symmetrical reverse entries. 547 signals.m2m_changed.send(sender= rel.through, action='add',554 signals.m2m_changed.send(sender=through, action='add', 548 555 instance=self.instance, reverse=self.reverse, 549 556 model=self.model, pk_set=new_ids) 550 557 … … 571 578 if self.reverse or source_field_name == self.source_field_name: 572 579 # Don't send the signal when we are deleting the 573 580 # duplicate data row for symmetrical reverse entries. 574 signals.m2m_changed.send(sender= rel.through, action="remove",581 signals.m2m_changed.send(sender=through, action="remove", 575 582 instance=self.instance, reverse=self.reverse, 576 583 model=self.model, pk_set=old_ids) 577 584 … … 580 587 if self.reverse or source_field_name == self.source_field_name: 581 588 # Don't send the signal when we are clearing the 582 589 # duplicate data rows for symmetrical reverse entries. 583 signals.m2m_changed.send(sender= rel.through, action="clear",590 signals.m2m_changed.send(sender=through, action="clear", 584 591 instance=self.instance, reverse=self.reverse, 585 592 model=self.model, pk_set=None) 586 593 db = router.db_for_write(self.through.__class__, instance=self.instance) … … 608 615 # model's default manager. 609 616 rel_model = self.related.model 610 617 superclass = rel_model._default_manager.__class__ 611 RelatedManager = create_many_related_manager(superclass, self.related.field .rel)618 RelatedManager = create_many_related_manager(superclass, self.related.field) 612 619 613 620 manager = RelatedManager( 614 621 model=rel_model, … … 626 633 if instance is None: 627 634 raise AttributeError("Manager must be accessed via instance") 628 635 629 if not self.related.field. rel.through._meta.auto_created:636 if not self.related.field.can_add(): 630 637 opts = self.related.field.rel.through._meta 631 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))638 raise AttributeError("Cannot set values on this ManyToManyField. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)) 632 639 633 640 manager = self.__get__(instance) 634 641 manager.clear() … … 660 667 # model's default manager. 661 668 rel_model=self.field.rel.to 662 669 superclass = rel_model._default_manager.__class__ 663 RelatedManager = create_many_related_manager(superclass, self.field .rel)670 RelatedManager = create_many_related_manager(superclass, self.field) 664 671 665 672 manager = RelatedManager( 666 673 model=rel_model, … … 678 685 if instance is None: 679 686 raise AttributeError("Manager must be accessed via instance") 680 687 681 if not self.field. rel.through._meta.auto_created:688 if not self.field.can_add(): 682 689 opts = self.field.rel.through._meta 683 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))690 raise AttributeError("Cannot set values a this ManyToManyField. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)) 684 691 685 692 manager = self.__get__(instance) 686 693 manager.clear() … … 953 960 self.db_table = kwargs.pop('db_table', None) 954 961 if kwargs['rel'].through is not None: 955 962 assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used." 963 964 self._can_add = kwargs.pop('can_add', None) 965 self._can_remove = kwargs.pop('can_remove', None) 956 966 957 967 Field.__init__(self, **kwargs) 958 968 … … 971 981 else: 972 982 return util.truncate_name('%s_%s' % (opts.db_table, self.name), 973 983 connection.ops.max_name_length()) 974 975 def _get_m2m_attr(self, related, attr): 976 "Function that can be curried to provide the source column name for the m2m table" 977 cache_attr = '_m2m_%s_cache' % attr 984 985 def _get_intermediary_fields(self, related): 986 cache_attr = '_m2m_intermediary_fields_cache' 978 987 if hasattr(self, cache_attr): 979 988 return getattr(self, cache_attr) 989 990 candidates = [] 991 related_candidates = [] 992 auto_add = True 980 993 for f in self.rel.through._meta.fields: 981 if hasattr(f,'rel') and f.rel and f.rel.to == related.model: 982 setattr(self, cache_attr, getattr(f, attr)) 983 return getattr(self, cache_attr) 994 if hasattr(f,'rel') and f.rel: 995 if f.rel.to == related.model: 996 candidates.append(f) 997 continue 998 elif f.rel.to == related.parent_model: 999 related_candidates.append(f) 1000 continue 1001 if isinstance(f, AutoField) or f.null or f.has_default(): 1002 continue 1003 elif getattr(f, 'auto_now_add', False) or getattr(f, 'auto_now', False): 1004 continue 1005 else: 1006 auto_add = False 1007 if related.model == related.parent_model: 1008 # m2m to self 1009 assert len(candidates) == 2, "There are too many ForeignKeys to %s" % related.model 1010 field, related_field = candidates 1011 else: 1012 assert len(candidates) == 1, "There are no ForeignKeys to %s" % related.model 1013 assert len(related_candidates) == 1, "There are no ForeignKeys to %s" % related.parent_model 1014 # TODO: intelligently pick a candidate if there is more than one. For now, just use the first. 1015 field, related_field = candidates[0], related_candidates[0] 984 1016 1017 if self._can_add is None: 1018 self._can_add = auto_add 1019 1020 if self._can_remove is None: 1021 self._can_remove = False 1022 unique_together = [frozenset(ut) for ut in self.rel.through._meta.unique_together] 1023 names = frozenset([field.name, related_field.name]) 1024 for ut in unique_together: 1025 if names <= ut: 1026 self._can_remove = True 1027 break 1028 1029 setattr(self, cache_attr, (field, related_field)) 1030 return (field, related_field) 1031 1032 def _get_can_add(self, related): 1033 if self._can_add is None: 1034 self._get_intermediary_fields(related) 1035 return self._can_add 1036 1037 def _get_can_remove(self, related): 1038 if self._can_remove is None: 1039 self._get_intermediary_fields(related) 1040 return self._can_remove 1041 1042 def _get_m2m_attr(self, related, attr): 1043 "Function that can be curried to provide a source field attribute" 1044 cache_attr = '_m2m_%s_cache' % attr 1045 if not hasattr(self, cache_attr): 1046 field, _ = self._get_intermediary_fields(related) 1047 setattr(self, cache_attr, getattr(field, attr)) 1048 return getattr(self, cache_attr) 1049 985 1050 def _get_m2m_reverse_attr(self, related, attr): 986 "Function that can be curried to provide the related column name for the m2m table"1051 "Function that can be curried to provide a related field attribute" 987 1052 cache_attr = '_m2m_reverse_%s_cache' % attr 988 if hasattr(self, cache_attr): 989 return getattr(self, cache_attr) 990 found = False 991 for f in self.rel.through._meta.fields: 992 if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model: 993 if related.model == related.parent_model: 994 # If this is an m2m-intermediate to self, 995 # the first foreign key you find will be 996 # the source column. Keep searching for 997 # the second foreign key. 998 if found: 999 setattr(self, cache_attr, getattr(f, attr)) 1000 break 1001 else: 1002 found = True 1003 else: 1004 setattr(self, cache_attr, getattr(f, attr)) 1005 break 1053 if not hasattr(self, cache_attr): 1054 _, related_field = self._get_intermediary_fields(related) 1055 setattr(self, cache_attr, getattr(related_field, attr)) 1006 1056 return getattr(self, cache_attr) 1007 1057 1008 1058 def isValidIDList(self, field_data, all_data): … … 1087 1137 1088 1138 self.m2m_field_name = curry(self._get_m2m_attr, related, 'name') 1089 1139 self.m2m_reverse_field_name = curry(self._get_m2m_reverse_attr, related, 'name') 1140 1141 self.can_add = curry(self._get_can_add, related) 1142 self.can_remove = curry(self._get_can_remove, related) 1090 1143 1091 1144 def set_attributes_from_rel(self): 1092 1145 pass