Ticket #6095: 6095-r8090.diff
File 6095-r8090.diff, 39.5 KB (added by , 16 years ago) |
---|
-
django/db/models/fields/related.py
23 23 24 24 pending_lookups = {} 25 25 26 def add_lazy_relation(cls, field, relation ):26 def add_lazy_relation(cls, field, relation, operation): 27 27 """ 28 28 Adds a lookup on ``cls`` when a related field is defined using a string, 29 29 i.e.:: … … 45 45 If the other model hasn't yet been loaded -- almost a given if you're using 46 46 lazy relationships -- then the relation won't be set up until the 47 47 class_prepared signal fires at the end of model initialization. 48 49 operation is the work that must be performed once the relation can be resolved. 48 50 """ 49 51 # Check for recursive relations 50 52 if relation == RECURSIVE_RELATIONSHIP_CONSTANT: … … 66 68 # is prepared. 67 69 model = get_model(app_label, model_name, False) 68 70 if model: 69 field.rel.to = model 70 field.do_related_class(model, cls) 71 operation(field, model, cls) 71 72 else: 72 73 key = (app_label, model_name) 73 value = (cls, field )74 value = (cls, field, operation) 74 75 pending_lookups.setdefault(key, []).append(value) 75 76 76 77 def do_pending_lookups(sender): … … 78 79 Handle any pending relations to the sending model. Sent from class_prepared. 79 80 """ 80 81 key = (sender._meta.app_label, sender.__name__) 81 for cls, field in pending_lookups.pop(key, []): 82 field.rel.to = sender 83 field.do_related_class(sender, cls) 82 for cls, field, operation in pending_lookups.pop(key, []): 83 operation(field, sender, cls) 84 84 85 85 dispatcher.connect(do_pending_lookups, signal=signals.class_prepared) 86 86 … … 108 108 109 109 other = self.rel.to 110 110 if isinstance(other, basestring): 111 add_lazy_relation(cls, self, other) 111 def resolve_related_class(field, model, cls): 112 field.rel.to = model 113 field.do_related_class(model, cls) 114 add_lazy_relation(cls, self, other, resolve_related_class) 112 115 else: 113 116 self.do_related_class(other, cls) 114 117 … … 339 342 manager.clear() 340 343 manager.add(*value) 341 344 342 def create_many_related_manager(superclass ):345 def create_many_related_manager(superclass, through=False): 343 346 """Creates a manager that subclasses 'superclass' (which is a Manager) 344 347 and adds behavior for many-to-many related objects.""" 345 348 class ManyRelatedManager(superclass): … … 353 356 self.join_table = join_table 354 357 self.source_col_name = source_col_name 355 358 self.target_col_name = target_col_name 359 self.through = through 356 360 self._pk_val = self.instance._get_pk_val() 357 361 if self._pk_val is None: 358 362 raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__) … … 360 364 def get_query_set(self): 361 365 return superclass.get_query_set(self).filter(**(self.core_filters)) 362 366 363 def add(self, *objs): 364 self._add_items(self.source_col_name, self.target_col_name, *objs) 367 # If the ManyToMany relation has an intermediary model, 368 # the add and remove methods do not exist. 369 if through is None: 370 def add(self, *objs): 371 self._add_items(self.source_col_name, self.target_col_name, *objs) 365 372 366 # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table367 if self.symmetrical:368 self._add_items(self.target_col_name, self.source_col_name, *objs)369 add.alters_data = True373 # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table 374 if self.symmetrical: 375 self._add_items(self.target_col_name, self.source_col_name, *objs) 376 add.alters_data = True 370 377 371 def remove(self, *objs):372 self._remove_items(self.source_col_name, self.target_col_name, *objs)378 def remove(self, *objs): 379 self._remove_items(self.source_col_name, self.target_col_name, *objs) 373 380 374 # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table375 if self.symmetrical:376 self._remove_items(self.target_col_name, self.source_col_name, *objs)377 remove.alters_data = True381 # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table 382 if self.symmetrical: 383 self._remove_items(self.target_col_name, self.source_col_name, *objs) 384 remove.alters_data = True 378 385 379 386 def clear(self): 380 387 self._clear_items(self.source_col_name) … … 385 392 clear.alters_data = True 386 393 387 394 def create(self, **kwargs): 395 # This check needs to be done here, since we can't later remove this 396 # from the method lookup table, as we do with add and remove. 397 if through is not None: 398 raise AttributeError, "Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through 388 399 new_obj = self.model(**kwargs) 389 400 new_obj.save() 390 401 self.add(new_obj) … … 472 483 # model's default manager. 473 484 rel_model = self.related.model 474 485 superclass = rel_model._default_manager.__class__ 475 RelatedManager = create_many_related_manager(superclass )486 RelatedManager = create_many_related_manager(superclass, self.related.field.rel.through) 476 487 477 488 qn = connection.ops.quote_name 478 489 manager = RelatedManager( … … 491 502 if instance is None: 492 503 raise AttributeError, "Manager must be accessed via instance" 493 504 505 through = getattr(self.related.field.rel, 'through', None) 506 if through is not None: 507 raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through 508 494 509 manager = self.__get__(instance) 495 510 manager.clear() 496 511 manager.add(*value) … … 513 528 # model's default manager. 514 529 rel_model=self.field.rel.to 515 530 superclass = rel_model._default_manager.__class__ 516 RelatedManager = create_many_related_manager(superclass )531 RelatedManager = create_many_related_manager(superclass, self.field.rel.through) 517 532 518 533 qn = connection.ops.quote_name 519 534 manager = RelatedManager( … … 532 547 if instance is None: 533 548 raise AttributeError, "Manager must be accessed via instance" 534 549 550 through = getattr(self.field.rel, 'through', None) 551 if through is not None: 552 raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through 553 535 554 manager = self.__get__(instance) 536 555 manager.clear() 537 556 manager.add(*value) … … 583 602 584 603 class ManyToManyRel(object): 585 604 def __init__(self, to, num_in_admin=0, related_name=None, 586 limit_choices_to=None, symmetrical=True ):605 limit_choices_to=None, symmetrical=True, through=None): 587 606 self.to = to 588 607 self.num_in_admin = num_in_admin 589 608 self.related_name = related_name … … 593 612 self.edit_inline = False 594 613 self.symmetrical = symmetrical 595 614 self.multiple = True 615 self.through = through 596 616 617 597 618 class ForeignKey(RelatedField, Field): 598 619 empty_strings_allowed = False 599 620 def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs): … … 722 743 num_in_admin=kwargs.pop('num_in_admin', 0), 723 744 related_name=kwargs.pop('related_name', None), 724 745 limit_choices_to=kwargs.pop('limit_choices_to', None), 725 symmetrical=kwargs.pop('symmetrical', True)) 746 symmetrical=kwargs.pop('symmetrical', True), 747 through=kwargs.pop('through', None)) 748 726 749 self.db_table = kwargs.pop('db_table', None) 750 if kwargs['rel'].through is not None: 751 self.creates_table = False 752 assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used." 753 else: 754 self.creates_table = True 755 727 756 Field.__init__(self, **kwargs) 728 757 729 758 msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.') … … 738 767 739 768 def _get_m2m_db_table(self, opts): 740 769 "Function that can be curried to provide the m2m table name for this relation" 741 if self.db_table: 770 if self.rel.through is not None: 771 return self.rel.through_model._meta.db_table 772 elif self.db_table: 742 773 return self.db_table 743 774 else: 744 775 return '%s_%s' % (opts.db_table, self.name) 745 776 746 777 def _get_m2m_column_name(self, related): 747 778 "Function that can be curried to provide the source column name for the m2m table" 779 if self.rel.through is not None: 780 for f in self.rel.through_model._meta.fields: 781 if hasattr(f,'rel') and f.rel and f.rel.to == related.model: 782 return f.column 748 783 # If this is an m2m relation to self, avoid the inevitable name clash 749 if related.model == related.parent_model:784 elif related.model == related.parent_model: 750 785 return 'from_' + related.model._meta.object_name.lower() + '_id' 751 786 else: 752 787 return related.model._meta.object_name.lower() + '_id' 753 788 754 789 def _get_m2m_reverse_name(self, related): 755 790 "Function that can be curried to provide the related column name for the m2m table" 791 if self.rel.through is not None: 792 found = False 793 for f in self.rel.through_model._meta.fields: 794 if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model: 795 if related.model == related.parent_model: 796 # If this is an m2m-intermediate to self, 797 # the first foreign key you find will be 798 # the source column. Keep searching for 799 # the second foreign key. 800 if found: 801 return f.column 802 else: 803 found = True 804 else: 805 return f.column 756 806 # If this is an m2m relation to self, avoid the inevitable name clash 757 if related.model == related.parent_model:807 elif related.model == related.parent_model: 758 808 return 'to_' + related.parent_model._meta.object_name.lower() + '_id' 759 809 else: 760 810 return related.parent_model._meta.object_name.lower() + '_id' … … 797 847 798 848 # Set up the accessor for the m2m table name for the relation 799 849 self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta) 800 850 851 # Populate some necessary rel arguments so that cross-app relations 852 # work correctly. 853 if isinstance(self.rel.through, basestring): 854 def resolve_through_model(field, model, cls): 855 field.rel.through_model = model 856 add_lazy_relation(cls, self, self.rel.through, resolve_through_model) 857 elif self.rel.through: 858 self.rel.through_model = self.rel.through 859 self.rel.through = self.rel.through._meta.object_name 860 801 861 if isinstance(self.rel.to, basestring): 802 862 target = self.rel.to 803 863 else: -
django/core/management/validation.py
102 102 if r.get_accessor_name() == rel_query_name: 103 103 e.add(opts, "Reverse query name for field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) 104 104 105 seen_intermediary_signatures = [] 105 106 for i, f in enumerate(opts.local_many_to_many): 106 107 # Check to see if the related m2m field will clash with any 107 108 # existing fields, m2m fields, m2m related objects or related … … 112 113 # so skip the next section 113 114 if isinstance(f.rel.to, (str, unicode)): 114 115 continue 115 116 if getattr(f.rel, 'through', None) is not None: 117 if hasattr(f.rel, 'through_model'): 118 intermediary_model = f.rel.through_model 119 if intermediary_model not in models.get_models(): 120 e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed" % (f.name, f.rel.through)) 121 signature = (f.rel.to, cls, intermediary_model) 122 if signature in seen_intermediary_signatures: 123 e.add(opts, "The model %s has two manually-defined m2m relations through the model %s, which is not permitted. Please consider using an extra field on your intermediary model instead." % (cls._meta.object_name, intermediary_model._meta.object_name)) 124 else: 125 seen_intermediary_signatures.append(signature) 126 seen_related_fk, seen_this_fk = False, False 127 for field in intermediary_model._meta.fields: 128 if field.rel: 129 if not seen_related_fk and field.rel.to == f.rel.to: 130 seen_related_fk = True 131 elif field.rel.to == cls: 132 seen_this_fk = True 133 if not seen_related_fk or not seen_this_fk: 134 e.add(opts, "'%s' has a manually-defined m2m relation through model %s, which does not have foreign keys to %s and %s" % (f.name, f.rel.through, f.rel.to._meta.object_name, cls._meta.object_name)) 135 else: 136 e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed" % (f.name, f.rel.through)) 137 116 138 rel_opts = f.rel.to._meta 117 139 rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name() 118 140 rel_query_name = f.related_query_name() -
django/core/management/sql.py
353 353 qn = connection.ops.quote_name 354 354 inline_references = connection.features.inline_fk_references 355 355 for f in opts.local_many_to_many: 356 if not isinstance(f.rel, generic.GenericRel):356 if f.creates_table: 357 357 tablespace = f.db_tablespace or opts.db_tablespace 358 358 if tablespace and connection.features.supports_tablespaces: 359 359 tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace, inline=True) -
django/contrib/contenttypes/generic.py
104 104 limit_choices_to=kwargs.pop('limit_choices_to', None), 105 105 symmetrical=kwargs.pop('symmetrical', True)) 106 106 107 # By its very nature, a GenericRelation doesn't create a table. 108 self.creates_table = False 109 107 110 # Override content-type/object-id field names on the related class 108 111 self.object_id_field_name = kwargs.pop("object_id_field", "object_id") 109 112 self.content_type_field_name = kwargs.pop("content_type_field", "content_type") -
tests/modeltests/invalid_models/models.py
110 110 class MissingRelations(models.Model): 111 111 rel1 = models.ForeignKey("Rel1") 112 112 rel2 = models.ManyToManyField("Rel2") 113 114 class MissingManualM2MModel(models.Model): 115 name = models.CharField(max_length=5) 116 missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel") 117 118 class Person(models.Model): 119 name = models.CharField(max_length=5) 113 120 121 class Group(models.Model): 122 name = models.CharField(max_length=5) 123 primary = models.ManyToManyField(Person, through="Membership", related_name="primary") 124 secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary") 125 126 class GroupTwo(models.Model): 127 name = models.CharField(max_length=5) 128 primary = models.ManyToManyField(Person, through="Membership") 129 secondary = models.ManyToManyField(Group, through="MembershipMissingFK") 130 131 class Membership(models.Model): 132 person = models.ForeignKey(Person) 133 group = models.ForeignKey(Group) 134 not_default_or_null = models.CharField(max_length=5) 135 136 class MembershipMissingFK(models.Model): 137 person = models.ForeignKey(Person) 138 139 class PersonSelfRefM2M(models.Model): 140 name = models.CharField(max_length=5) 141 friends = models.ManyToManyField('self', through="Relationship") 142 143 class Relationship(models.Model): 144 first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set") 145 second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set") 146 date_added = models.DateTimeField() 147 114 148 model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute. 115 149 invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute. 116 150 invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute. … … 195 229 invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_4' clashes with field 'SelfClashM2M.selfclashm2m'. Add a related_name argument to the definition for 'm2m_4'. 196 230 invalid_models.missingrelations: 'rel2' has m2m relation with model Rel2, which has not been installed 197 231 invalid_models.missingrelations: 'rel1' has relation with model Rel1, which has not been installed 232 invalid_models.group: The model Group has two manually-defined m2m relations through the model Membership, which is not permitted. Please consider using an extra field on your intermediary model instead. 233 invalid_models.grouptwo: 'primary' has a manually-defined m2m relation through model Membership, which does not have foreign keys to Person and GroupTwo 234 invalid_models.grouptwo: 'secondary' has a manually-defined m2m relation through model MembershipMissingFK, which does not have foreign keys to Group and GroupTwo 235 invalid_models.missingmanualm2mmodel: 'missing_m2m' specifies an m2m relation through model MissingM2MModel, which has not been installed 198 236 """ -
tests/modeltests/m2m_through/__init__.py
1 -
tests/modeltests/m2m_through/models.py
1 from django.db import models 2 from datetime import datetime 3 4 # M2M described on one of the models 5 class Person(models.Model): 6 name = models.CharField(max_length=128) 7 8 def __unicode__(self): 9 return self.name 10 11 class Group(models.Model): 12 name = models.CharField(max_length=128) 13 members = models.ManyToManyField(Person, through='Membership') 14 custom_members = models.ManyToManyField(Person, through='CustomMembership', related_name="custom") 15 nodefaultsnonulls = models.ManyToManyField(Person, through='TestNoDefaultsOrNulls', related_name="testnodefaultsnonulls") 16 17 def __unicode__(self): 18 return self.name 19 20 class Membership(models.Model): 21 person = models.ForeignKey(Person) 22 group = models.ForeignKey(Group) 23 date_joined = models.DateTimeField(default=datetime.now) 24 invite_reason = models.CharField(max_length=64, null=True) 25 26 def __unicode__(self): 27 return "%s is a member of %s" % (self.person.name, self.group.name) 28 29 class CustomMembership(models.Model): 30 person = models.ForeignKey(Person, db_column="custom_person_column", related_name="custom_person_related_name") 31 group = models.ForeignKey(Group) 32 weird_fk = models.ForeignKey(Membership, null=True) 33 date_joined = models.DateTimeField(default=datetime.now) 34 35 def __unicode__(self): 36 return "%s is a member of %s" % (self.person.name, self.group.name) 37 38 class Meta: 39 db_table = "test_table" 40 41 class TestNoDefaultsOrNulls(models.Model): 42 person = models.ForeignKey(Person) 43 group = models.ForeignKey(Group) 44 nodefaultnonull = models.CharField(max_length=5) 45 46 class PersonSelfRefM2M(models.Model): 47 name = models.CharField(max_length=5) 48 friends = models.ManyToManyField('self', through="Friendship") 49 50 def __unicode__(self): 51 return self.name 52 53 class Friendship(models.Model): 54 first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set") 55 second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set") 56 date_friended = models.DateTimeField() 57 58 __test__ = {'API_TESTS':""" 59 >>> from datetime import datetime 60 61 ### Creation and Saving Tests ### 62 63 >>> bob = Person.objects.create(name='Bob') 64 >>> jim = Person.objects.create(name='Jim') 65 >>> jane = Person.objects.create(name='Jane') 66 >>> rock = Group.objects.create(name='Rock') 67 >>> roll = Group.objects.create(name='Roll') 68 69 # We start out by making sure that the Group 'rock' has no members. 70 >>> rock.members.all() 71 [] 72 73 # To make Jim a member of Group Rock, simply create a Membership object. 74 >>> m1 = Membership.objects.create(person=jim, group=rock) 75 76 # We can do the same for Jane and Rock. 77 >>> m2 = Membership.objects.create(person=jane, group=rock) 78 79 # Let's check to make sure that it worked. Jane and Jim should be members of Rock. 80 >>> rock.members.all() 81 [<Person: Jim>, <Person: Jane>] 82 83 # Now we can add a bunch more Membership objects to test with. 84 >>> m3 = Membership.objects.create(person=bob, group=roll) 85 >>> m4 = Membership.objects.create(person=jim, group=roll) 86 >>> m5 = Membership.objects.create(person=jane, group=roll) 87 88 # We can get Jim's Group membership as with any ForeignKey. 89 >>> jim.group_set.all() 90 [<Group: Rock>, <Group: Roll>] 91 92 # Querying the intermediary model works like normal. 93 # In this case we get Jane's membership to Rock. 94 >>> m = Membership.objects.get(person=jane, group=rock) 95 >>> m 96 <Membership: Jane is a member of Rock> 97 98 # Now we set some date_joined dates for further testing. 99 >>> m2.invite_reason = "She was just awesome." 100 >>> m2.date_joined = datetime(2006, 1, 1) 101 >>> m2.save() 102 103 >>> m5.date_joined = datetime(2004, 1, 1) 104 >>> m5.save() 105 106 >>> m3.date_joined = datetime(2004, 1, 1) 107 >>> m3.save() 108 109 # It's not only get that works. Filter works like normal as well. 110 >>> Membership.objects.filter(person=jim) 111 [<Membership: Jim is a member of Rock>, <Membership: Jim is a member of Roll>] 112 113 114 ### Forward Descriptors Tests ### 115 116 # Due to complications with adding via an intermediary model, 117 # the add method is not provided. 118 >>> rock.members.add(bob) 119 Traceback (most recent call last): 120 ... 121 AttributeError: 'ManyRelatedManager' object has no attribute 'add' 122 123 # Create is also disabled as it suffers from the same problems as add. 124 >>> rock.members.create(name='Anne') 125 Traceback (most recent call last): 126 ... 127 AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. 128 129 # Remove has similar complications, and is not provided either. 130 >>> rock.members.remove(jim) 131 Traceback (most recent call last): 132 ... 133 AttributeError: 'ManyRelatedManager' object has no attribute 'remove' 134 135 # Here we back up the list of all members of Rock. 136 >>> backup = list(rock.members.all()) 137 138 # ...and we verify that it has worked. 139 >>> backup 140 [<Person: Jim>, <Person: Jane>] 141 142 # The clear function should still work. 143 >>> rock.members.clear() 144 145 # Now there will be no members of Rock. 146 >>> rock.members.all() 147 [] 148 149 # Assignment should not work with models specifying a through model for many of 150 # the same reasons as adding. 151 >>> rock.members = backup 152 Traceback (most recent call last): 153 ... 154 AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. 155 156 # Let's re-save those instances that we've cleared. 157 >>> m1.save() 158 >>> m2.save() 159 160 # Verifying that those instances were re-saved successfully. 161 >>> rock.members.all() 162 [<Person: Jim>, <Person: Jane>] 163 164 165 ### Reverse Descriptors Tests ### 166 167 # Due to complications with adding via an intermediary model, 168 # the add method is not provided. 169 >>> bob.group_set.add(rock) 170 Traceback (most recent call last): 171 ... 172 AttributeError: 'ManyRelatedManager' object has no attribute 'add' 173 174 # Create is also disabled as it suffers from the same problems as add. 175 >>> bob.group_set.create(name='Funk') 176 Traceback (most recent call last): 177 ... 178 AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. 179 180 # Remove has similar complications, and is not provided either. 181 >>> jim.group_set.remove(rock) 182 Traceback (most recent call last): 183 ... 184 AttributeError: 'ManyRelatedManager' object has no attribute 'remove' 185 186 # Here we back up the list of all of Jim's groups. 187 >>> backup = list(jim.group_set.all()) 188 >>> backup 189 [<Group: Rock>, <Group: Roll>] 190 191 # The clear function should still work. 192 >>> jim.group_set.clear() 193 194 # Now Jim will be in no groups. 195 >>> jim.group_set.all() 196 [] 197 198 # Assignment should not work with models specifying a through model for many of 199 # the same reasons as adding. 200 >>> jim.group_set = backup 201 Traceback (most recent call last): 202 ... 203 AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. 204 205 # Let's re-save those instances that we've cleared. 206 >>> m1.save() 207 >>> m4.save() 208 209 # Verifying that those instances were re-saved successfully. 210 >>> jim.group_set.all() 211 [<Group: Rock>, <Group: Roll>] 212 213 ### Custom Tests ### 214 215 # Let's see if we can query through our second relationship. 216 >>> rock.custom_members.all() 217 [] 218 219 # We can query in the opposite direction as well. 220 >>> bob.custom.all() 221 [] 222 223 # Let's create some membership objects in this custom relationship. 224 >>> cm1 = CustomMembership.objects.create(person=bob, group=rock) 225 >>> cm2 = CustomMembership.objects.create(person=jim, group=rock) 226 227 # If we get the number of people in Rock, it should be both Bob and Jim. 228 >>> rock.custom_members.all() 229 [<Person: Bob>, <Person: Jim>] 230 231 # Bob should only be in one custom group. 232 >>> bob.custom.all() 233 [<Group: Rock>] 234 235 # Let's make sure our new descriptors don't conflict with the FK related_name. 236 >>> bob.custom_person_related_name.all() 237 [<CustomMembership: Bob is a member of Rock>] 238 239 ### SELF-REFERENTIAL TESTS ### 240 241 # Let's first create a person who has no friends. 242 >>> tony = PersonSelfRefM2M.objects.create(name="Tony") 243 >>> tony.friends.all() 244 [] 245 246 # Now let's create another person for Tony to be friends with. 247 >>> chris = PersonSelfRefM2M.objects.create(name="Chris") 248 >>> f = Friendship.objects.create(first=tony, second=chris, date_friended=datetime.now()) 249 250 # Tony should now show that Chris is his friend. 251 >>> tony.friends.all() 252 [<PersonSelfRefM2M: Chris>] 253 254 # But we haven't established that Chris is Tony's Friend. 255 >>> chris.friends.all() 256 [] 257 258 # So let's do that now. 259 >>> f2 = Friendship.objects.create(first=chris, second=tony, date_friended=datetime.now()) 260 261 # Having added Chris as a friend, let's make sure that his friend set reflects 262 # that addition. 263 >>> chris.friends.all() 264 [<PersonSelfRefM2M: Tony>] 265 266 # Chris gets mad and wants to get rid of all of his friends. 267 >>> chris.friends.clear() 268 269 # Now he should not have any more friends. 270 >>> chris.friends.all() 271 [] 272 273 # Since this is a symmetrical relation, Tony's friend link is deleted as well. 274 >>> tony.friends.all() 275 [] 276 277 278 279 ### QUERY TESTS ### 280 281 # We can query for the related model by using its attribute name (members, in 282 # this case). 283 >>> Group.objects.filter(members__name='Bob') 284 [<Group: Roll>] 285 286 # To query through the intermediary model, we specify its model name. 287 # In this case, membership. 288 >>> Group.objects.filter(membership__invite_reason="She was just awesome.") 289 [<Group: Rock>] 290 291 # If we want to query in the reverse direction by the related model, use its 292 # model name (group, in this case). 293 >>> Person.objects.filter(group__name="Rock") 294 [<Person: Jim>, <Person: Jane>] 295 296 # If the m2m field has specified a related_name, using that will work. 297 >>> Person.objects.filter(custom__name="Rock") 298 [<Person: Bob>, <Person: Jim>] 299 300 # To query through the intermediary model in the reverse direction, we again 301 # specify its model name (membership, in this case). 302 >>> Person.objects.filter(membership__invite_reason="She was just awesome.") 303 [<Person: Jane>] 304 305 # Let's see all of the groups that Jane joined after 1 Jan 2005: 306 >>> Group.objects.filter(membership__date_joined__gt=datetime(2005, 1, 1), membership__person =jane) 307 [<Group: Rock>] 308 309 # Queries also work in the reverse direction: Now let's see all of the people 310 # that have joined Rock since 1 Jan 2005: 311 >>> Person.objects.filter(membership__date_joined__gt=datetime(2005, 1, 1), membership__group=rock) 312 [<Person: Jim>, <Person: Jane>] 313 314 # Conceivably, queries through membership could return correct, but non-unique 315 # querysets. To demonstrate this, we query for all people who have joined a 316 # group after 2004: 317 >>> Person.objects.filter(membership__date_joined__gt=datetime(2004, 1, 1)) 318 [<Person: Jim>, <Person: Jim>, <Person: Jane>] 319 320 # Jim showed up twice, because he joined two groups ('Rock', and 'Roll'): 321 >>> [(m.person.name, m.group.name) for m in 322 ... Membership.objects.filter(date_joined__gt=datetime(2004, 1, 1))] 323 [(u'Jim', u'Rock'), (u'Jane', u'Rock'), (u'Jim', u'Roll')] 324 325 # QuerySet's distinct() method can correct this problem. 326 >>> Person.objects.filter(membership__date_joined__gt=datetime(2004, 1, 1)).distinct() 327 [<Person: Jim>, <Person: Jane>] 328 """} 329 No newline at end of file -
tests/regressiontests/m2m_through_regress/models.py
1 from django.db import models 2 from datetime import datetime 3 from django.contrib.auth.models import User 4 5 # Forward declared intermediate model 6 class Membership(models.Model): 7 person = models.ForeignKey('Person') 8 group = models.ForeignKey('Group') 9 date_joined = models.DateTimeField(default=datetime.now) 10 11 def __unicode__(self): 12 return "%s is a member of %s" % (self.person.name, self.group.name) 13 14 class UserMembership(models.Model): 15 user = models.ForeignKey(User) 16 group = models.ForeignKey('Group') 17 date_joined = models.DateTimeField(default=datetime.now) 18 19 def __unicode__(self): 20 return "%s is a user and member of %s" % (self.user.username, self.group.name) 21 22 class Person(models.Model): 23 name = models.CharField(max_length=128) 24 25 def __unicode__(self): 26 return self.name 27 28 class Group(models.Model): 29 name = models.CharField(max_length=128) 30 # Membership object defined as a class 31 members = models.ManyToManyField(Person, through=Membership) 32 user_members = models.ManyToManyField(User, through='UserMembership') 33 34 def __unicode__(self): 35 return self.name 36 37 __test__ = {'API_TESTS':""" 38 # Create some dummy data 39 >>> bob = Person.objects.create(name='Bob') 40 >>> jim = Person.objects.create(name='Jim') 41 42 >>> rock = Group.objects.create(name='Rock') 43 >>> roll = Group.objects.create(name='Roll') 44 45 >>> frank = User.objects.create_user('frank','frank@example.com','password') 46 >>> jane = User.objects.create_user('jane','jane@example.com','password') 47 48 # Now test that the forward declared Membership works 49 >>> Membership.objects.create(person=bob, group=rock) 50 <Membership: Bob is a member of Rock> 51 52 >>> Membership.objects.create(person=bob, group=roll) 53 <Membership: Bob is a member of Roll> 54 55 >>> Membership.objects.create(person=jim, group=rock) 56 <Membership: Jim is a member of Rock> 57 58 >>> bob.group_set.all() 59 [<Group: Rock>, <Group: Roll>] 60 61 >>> roll.members.all() 62 [<Person: Bob>] 63 64 # Error messages use the model name, not repr of the class name 65 >>> bob.group_set = [] 66 Traceback (most recent call last): 67 ... 68 AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. 69 70 >>> roll.members = [] 71 Traceback (most recent call last): 72 ... 73 AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. 74 75 >>> rock.members.create(name='Anne') 76 Traceback (most recent call last): 77 ... 78 AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. 79 80 >>> bob.group_set.create(name='Funk') 81 Traceback (most recent call last): 82 ... 83 AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. 84 85 # Now test that the intermediate with a relationship outside 86 # the current app (i.e., UserMembership) workds 87 >>> UserMembership.objects.create(user=frank, group=rock) 88 <UserMembership: frank is a user and member of Rock> 89 90 >>> UserMembership.objects.create(user=frank, group=roll) 91 <UserMembership: frank is a user and member of Roll> 92 93 >>> UserMembership.objects.create(user=jane, group=rock) 94 <UserMembership: jane is a user and member of Rock> 95 96 >>> frank.group_set.all() 97 [<Group: Rock>, <Group: Roll>] 98 99 >>> roll.user_members.all() 100 [<User: frank>] 101 102 """} 103 No newline at end of file -
AUTHORS
154 154 Maciej Fijalkowski 155 155 Matthew Flanagan <http://wadofstuff.blogspot.com> 156 156 Eric Floehr <eric@intellovations.com> 157 Eric Florenzano <floguy@gmail.com> 157 158 Vincent Foley <vfoleybourgon@yahoo.ca> 158 159 Rudolph Froger <rfroger@estrate.nl> 159 160 Jorge Gajon <gajon@gajon.org> -
docs/model-api.txt
945 945 946 946 ======================= ============================================================ 947 947 948 Extra fields on many-to-many relationships 949 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 950 951 When you're only dealing with mixing and matching pizzas and toppings, a 952 standard ``ManyToManyField`` works great. However, sometimes you may want 953 to associated data with the relationship between two models. 954 955 For example, consider the case of an application tracking the musical groups 956 which musicians belong to. There is a many-to-many relationship between a person 957 and the groups of which they are a member, so you could use a ManyToManyField 958 to represent this relationship. However, there is a lot of detail about the 959 membership that you might want to collect, such as the date at which the person 960 joined the group. 961 962 For these situations, Django allows you to specify the model that will be used 963 to govern the many-to-many relationship. You can then put extra fields on the 964 intermediate model. The intermediate model is associated with the 965 ``ManyToManyField`` by using the ``through`` argument to point the model that 966 will act as an intermediary. For our musician example, the code would look 967 something like this:: 968 969 class Person(models.Model): 970 # ... 971 name = models.CharField(max_length=128) 972 973 def __unicode__(self): 974 return self.name 975 976 class Group(models.Model): 977 # ... 978 name = models.CharField(max_length=128) 979 members = models.ManyToManyField(Person, through='Membership') 980 981 def __unicode__(self): 982 return self.name 983 984 class Membership(models.Model): 985 person = models.ForeignKey(Person) 986 group = models.ForeignKey(Group) 987 date_joined = models.DateField() 988 invite_reason = models.CharField(max_length=64) 989 990 When you set up the intermediary model, you must explicitly specify foreign 991 keys to the models in ManyToMany relation. This explicit declaration makes 992 it clear how two models are related. 993 994 Now that you have set up your ``ManyToManyField`` to use your intermediary 995 model (Membership, in this case), you're ready to use the convenience methods 996 provided by that ``ManyToManyField``. Here's an example of how you can query 997 for and use these models:: 998 999 >>> ringo = Person.objects.create(name="Ringo Starr") 1000 >>> paul = Person.objects.create(name="Paul McCartney") 1001 >>> beatles = Group.objects.create(name="The Beatles") 1002 >>> m1 = Membership.objects.create(person=ringo, group=beatles, 1003 ... date_joined=date(1962, 8, 16), 1004 ... invite_reason= "Needed a new drummer.") 1005 >>> beatles.members.all() 1006 [<Person: Ringo Starr>] 1007 >>> ringo.group_set.all() 1008 [<Group: The Beatles>] 1009 >>> m2 = Membership.objects.create(person=paul, group=beatles, 1010 ... date_joined=date(1960, 8, 1), 1011 ... invite_reason= "Wanted to form a band.") 1012 >>> beatles.members.all() 1013 [<Person: Ringo Starr>, <Person: Paul McCartney>] 1014 1015 Unlike normal many-to-many fields, you *can't* use ``add``, ``create``, 1016 or assignment (i.e., ``beatles.members = [...]``) to create relationships:: 1017 1018 # THIS WILL NOT WORK 1019 >>> beatles.members.add(john) 1020 # NEITHER WILL THIS 1021 >>> beatles.members.create(name="George Harrison") 1022 # AND NEITHER WILL THIS 1023 >>> beatles.members = [john, paul, ringo, george] 1024 1025 Why? You can't just create a relationship between a Person and a Group - you 1026 need to specify all the detail for the relationship required by the 1027 Membership table. The simple ``add``, ``create`` and assignment calls 1028 don't provide a way to specify this extra detail. As a result, they are 1029 disabled for many-to-many relationships that use an intermediate model. 1030 The only way to create a many-to-many relationship with an intermediate table 1031 is to create instances of the intermediate model. 1032 1033 The ``remove`` method is disabled for similar reasons. However, the 1034 ``clear()`` method can be used to remove all many-to-many relationships 1035 for an instance:: 1036 1037 # Beatles have broken up 1038 >>> beatles.members.clear() 1039 1040 Once you have established the many-to-many relationships by creating instances 1041 of your intermediate model, you can issue queries. You can query using the 1042 attributes of the many-to-many-related model:: 1043 1044 # Find all the people in the Beatles whose name starts with 'Paul' 1045 >>> beatles.objects.filter(person__name__startswith='Paul') 1046 [<Person: Paul McCartney>] 1047 1048 You can also query on the attributes of the intermediate model:: 1049 1050 # Find all the members of the Beatles that joined after 1 Jan 1961 1051 >>> beatles.objects.filter(membership__date_joined__gt=date(1961,1,1)) 1052 [<Person: Ringo Starr] 1053 948 1054 One-to-one relationships 949 1055 ~~~~~~~~~~~~~~~~~~~~~~~~ 950 1056