Ticket #6095: 6095-r8090.2.diff
File 6095-r8090.2.diff, 46.3 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): … … 718 739 class ManyToManyField(RelatedField, Field): 719 740 def __init__(self, to, **kwargs): 720 741 kwargs['verbose_name'] = kwargs.get('verbose_name', None) 742 self.symmetrical_specified = kwargs.get('symmetrical', None) is not None 721 743 kwargs['rel'] = ManyToManyRel(to, 722 744 num_in_admin=kwargs.pop('num_in_admin', 0), 723 745 related_name=kwargs.pop('related_name', None), 724 746 limit_choices_to=kwargs.pop('limit_choices_to', None), 725 symmetrical=kwargs.pop('symmetrical', True)) 747 symmetrical=kwargs.pop('symmetrical', True), 748 through=kwargs.pop('through', None)) 749 726 750 self.db_table = kwargs.pop('db_table', None) 751 if kwargs['rel'].through is not None: 752 self.creates_table = False 753 assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used." 754 else: 755 self.creates_table = True 756 727 757 Field.__init__(self, **kwargs) 728 758 729 759 msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.') … … 738 768 739 769 def _get_m2m_db_table(self, opts): 740 770 "Function that can be curried to provide the m2m table name for this relation" 741 if self.db_table: 771 if self.rel.through is not None: 772 return self.rel.through_model._meta.db_table 773 elif self.db_table: 742 774 return self.db_table 743 775 else: 744 776 return '%s_%s' % (opts.db_table, self.name) 745 777 746 778 def _get_m2m_column_name(self, related): 747 779 "Function that can be curried to provide the source column name for the m2m table" 780 if self.rel.through is not None: 781 for f in self.rel.through_model._meta.fields: 782 if hasattr(f,'rel') and f.rel and f.rel.to == related.model: 783 return f.column 748 784 # If this is an m2m relation to self, avoid the inevitable name clash 749 if related.model == related.parent_model:785 elif related.model == related.parent_model: 750 786 return 'from_' + related.model._meta.object_name.lower() + '_id' 751 787 else: 752 788 return related.model._meta.object_name.lower() + '_id' 753 789 754 790 def _get_m2m_reverse_name(self, related): 755 791 "Function that can be curried to provide the related column name for the m2m table" 792 if self.rel.through is not None: 793 found = False 794 for f in self.rel.through_model._meta.fields: 795 if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model: 796 if related.model == related.parent_model: 797 # If this is an m2m-intermediate to self, 798 # the first foreign key you find will be 799 # the source column. Keep searching for 800 # the second foreign key. 801 if found: 802 return f.column 803 else: 804 found = True 805 else: 806 return f.column 756 807 # If this is an m2m relation to self, avoid the inevitable name clash 757 if related.model == related.parent_model:808 elif related.model == related.parent_model: 758 809 return 'to_' + related.parent_model._meta.object_name.lower() + '_id' 759 810 else: 760 811 return related.parent_model._meta.object_name.lower() + '_id' … … 797 848 798 849 # Set up the accessor for the m2m table name for the relation 799 850 self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta) 800 851 852 # Populate some necessary rel arguments so that cross-app relations 853 # work correctly. 854 if isinstance(self.rel.through, basestring): 855 def resolve_through_model(field, model, cls): 856 field.rel.through_model = model 857 add_lazy_relation(cls, self, self.rel.through, resolve_through_model) 858 elif self.rel.through: 859 self.rel.through_model = self.rel.through 860 self.rel.through = self.rel.through._meta.object_name 861 801 862 if isinstance(self.rel.to, basestring): 802 863 target = self.rel.to 803 864 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 if f.symmetrical_specified: 119 e.add(opts, "The symmetrical field has no meaning on m2m relations with intermediary models.") 120 from_model, to_model = cls, f.rel.to 121 seen_from, seen_to, seen_self = False, False, 0 122 for inter_field in f.rel.through_model._meta.fields: 123 rel_to = getattr(inter_field.rel, 'to', None) 124 if from_model == to_model: 125 if rel_to == from_model: 126 seen_self += 1 127 if seen_self > 2: 128 e.add(opts, "Intermediary model %s has more than two foreign keys through itself, which is ambiguous and is not permitted." % f.rel.through_model._meta.object_name) 129 else: 130 if rel_to == from_model: 131 if seen_from: 132 e.add(opts, "Intermediary model %s has more than one foreign key to one of its sides, %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, rel_from._meta.object_name)) 133 else: 134 seen_from = True 135 elif rel_to == to_model: 136 if seen_to: 137 e.add(opts, "Intermediary model %s has more than one foreign key to one of its sides, %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, rel_to._meta.object_name)) 138 else: 139 seen_to = True 140 if f.rel.through_model not in models.get_models(): 141 e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed" % (f.name, f.rel.through)) 142 signature = (f.rel.to, cls, f.rel.through_model) 143 if signature in seen_intermediary_signatures: 144 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, f.rel.through_model._meta.object_name)) 145 else: 146 seen_intermediary_signatures.append(signature) 147 seen_related_fk, seen_this_fk = False, False 148 for field in f.rel.through_model._meta.fields: 149 if field.rel: 150 if not seen_related_fk and field.rel.to == f.rel.to: 151 seen_related_fk = True 152 elif field.rel.to == cls: 153 seen_this_fk = True 154 if not seen_related_fk or not seen_this_fk: 155 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)) 156 else: 157 e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed" % (f.name, f.rel.through)) 158 116 159 rel_opts = f.rel.to._meta 117 160 rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name() 118 161 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/admin/options.py
161 161 kwargs['empty_label'] = db_field.blank and _('None') or None 162 162 else: 163 163 if isinstance(db_field, models.ManyToManyField): 164 if db_field.name in self.raw_id_fields: 164 # If it uses an intermediary model, don't show field in admin. 165 if db_field.rel.through is not None: 166 return None 167 elif db_field.name in self.raw_id_fields: 165 168 kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel) 166 169 kwargs['help_text'] = '' 167 170 elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)): -
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 tertiary = models.ManyToManyField(Person, through="RelationshipDoubleFK", related_name="tertiary") 126 127 class GroupTwo(models.Model): 128 name = models.CharField(max_length=5) 129 primary = models.ManyToManyField(Person, through="Membership") 130 secondary = models.ManyToManyField(Group, through="MembershipMissingFK") 131 132 class Membership(models.Model): 133 person = models.ForeignKey(Person) 134 group = models.ForeignKey(Group) 135 not_default_or_null = models.CharField(max_length=5) 136 137 class MembershipMissingFK(models.Model): 138 person = models.ForeignKey(Person) 139 140 class PersonSelfRefM2M(models.Model): 141 name = models.CharField(max_length=5) 142 friends = models.ManyToManyField('self', through="Relationship", symmetrical=True) 143 too_many_friends = models.ManyToManyField('self', through="RelationshipTripleFK") 144 145 class Relationship(models.Model): 146 first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set") 147 second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set") 148 date_added = models.DateTimeField() 149 150 class RelationshipTripleFK(models.Model): 151 first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set_2") 152 second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set_2") 153 third = models.ForeignKey(PersonSelfRefM2M, related_name="too_many_by_far") 154 date_added = models.DateTimeField() 155 156 class RelationshipDoubleFK(models.Model): 157 first = models.ForeignKey(Person, related_name="first_related_name") 158 second = models.ForeignKey(Person, related_name="second_related_name") 159 third = models.ForeignKey(Group, related_name="rel_to_set") 160 date_added = models.DateTimeField() 161 114 162 model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute. 115 163 invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute. 116 164 invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute. … … 195 243 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 244 invalid_models.missingrelations: 'rel2' has m2m relation with model Rel2, which has not been installed 197 245 invalid_models.missingrelations: 'rel1' has relation with model Rel1, which has not been installed 246 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. 247 invalid_models.grouptwo: 'primary' has a manually-defined m2m relation through model Membership, which does not have foreign keys to Person and GroupTwo 248 invalid_models.grouptwo: 'secondary' has a manually-defined m2m relation through model MembershipMissingFK, which does not have foreign keys to Group and GroupTwo 249 invalid_models.missingmanualm2mmodel: 'missing_m2m' specifies an m2m relation through model MissingM2MModel, which has not been installed 250 invalid_models.group: Intermediary model RelationshipDoubleFK has more than one foreign key to one of its sides, Person, which is ambiguous and is not permitted. 251 invalid_models.personselfrefm2m: The symmetrical field has no meaning on m2m relations with intermediary models. 252 invalid_models.personselfrefm2m: Intermediary model RelationshipTripleFK has more than two foreign keys through itself, which is ambiguous and is not permitted. 198 253 """ -
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/__init__.py
1 -
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 associate 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 to the model 966 that 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 -
docs/admin.txt
613 613 FriendshipInline, 614 614 ] 615 615 616 Working with Many-to-Many Intermediary Models 617 ---------------------------------------------- 618 619 By default, admin widgets for many-to-many relations will be displayed inline 620 on whichever model contains the actual reference to the `ManyToManyField`. 621 However, as soon as you specify an intermediary model using the ``through`` 622 argument to a ``ManyToManyField``, the admin will no longer display that 623 widget. The reason for this is because each instance of that intermediary 624 model has more information than could be displayed in any one widget. 625 626 But we still want to be able to edit that information inline. Fortunately, 627 this is easy to do with the admin application. Suppose we have the following 628 models:: 629 630 class Person(models.Model): 631 name = models.CharField(max_length=128) 632 633 class Group(models.Model): 634 name = models.CharField(max_length=128) 635 members = models.ManyToManyField(Person, through='Membership') 636 637 class Membership(models.Model): 638 person = models.ForeignKey(Person) 639 group = models.ForeignKey(Group) 640 date_joined = models.DateField() 641 invite_reason = models.CharField(max_length=64) 642 643 In this case, it is easy to create an admin to view the ``Person`` and 644 ``Group`` models, like so:: 645 646 from django.contrib import admin 647 648 class MembershipAdmin(admin.ModelAdmin): 649 # Nothing here yet. 650 pass 651 652 admin.site.register(Person, MembershipAdmin) 653 admin.site.register(Group, MembershipAdmin) 654 655 Now we need create a custom ``ModelInline`` subclass, like so:: 656 657 class MembershipInline(admin.InlineModelAdmin): 658 model = Membership 659 extra = 1 660 661 Of course, this subclass could look much different, based on the amount of 662 customization that we want to do. For demonstration purposes, however, this 663 simple sublclass will do just fine. Now, the only thing left to do is ensure 664 that the ``MembershipAdmin`` class knows which inline to display. We can 665 rewrite it to look like this:: 666 667 class MembershipAdmin(admin.ModelAdmin): 668 inlines = (MembershipInline,) 669 670 Now your admin site is set up to edit ``Membership`` objects inline from either 671 the ``Person`` or the ``Group`` detail pages. 672 673 616 674 ``AdminSite`` objects 617 675 ===================== 618 676