Ticket #6095: 6095-rc1.diff
| File 6095-rc1.diff, 53.2 kB (added by russellm, 4 months ago) |
|---|
-
django/db/models/fields/related.py
old new 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 597 617 class ForeignKey(RelatedField, Field): 598 618 empty_strings_allowed = False … … 722 742 num_in_admin=kwargs.pop('num_in_admin', 0), 723 743 related_name=kwargs.pop('related_name', None), 724 744 limit_choices_to=kwargs.pop('limit_choices_to', None), 725 symmetrical=kwargs.pop('symmetrical', True)) 745 symmetrical=kwargs.pop('symmetrical', True), 746 through=kwargs.pop('through', None)) 747 726 748 self.db_table = kwargs.pop('db_table', None) 749 if kwargs['rel'].through is not None: 750 self.creates_table = False 751 assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used." 752 else: 753 self.creates_table = True 754 727 755 Field.__init__(self, **kwargs) 728 756 729 757 msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.') … … 738 766 739 767 def _get_m2m_db_table(self, opts): 740 768 "Function that can be curried to provide the m2m table name for this relation" 741 if self.db_table: 769 if self.rel.through is not None: 770 return self.rel.through_model._meta.db_table 771 elif self.db_table: 742 772 return self.db_table 743 773 else: 744 774 return '%s_%s' % (opts.db_table, self.name) 745 775 746 776 def _get_m2m_column_name(self, related): 747 777 "Function that can be curried to provide the source column name for the m2m table" 748 # If this is an m2m relation to self, avoid the inevitable name clash 749 if related.model == related.parent_model: 750 return 'from_' + related.model._meta.object_name.lower() + '_id' 751 else: 752 return related.model._meta.object_name.lower() + '_id' 778 try: 779 return self._m2m_column_name_cache 780 except: 781 if self.rel.through is not None: 782 for f in self.rel.through_model._meta.fields: 783 if hasattr(f,'rel') and f.rel and f.rel.to == related.model: 784 self._m2m_column_name_cache = f.column 785 break 786 # If this is an m2m relation to self, avoid the inevitable name clash 787 elif related.model == related.parent_model: 788 self._m2m_column_name_cache = 'from_' + related.model._meta.object_name.lower() + '_id' 789 else: 790 self._m2m_column_name_cache = related.model._meta.object_name.lower() + '_id' 791 792 # Return the newly cached value 793 return self._m2m_column_name_cache 753 794 754 795 def _get_m2m_reverse_name(self, related): 755 796 "Function that can be curried to provide the related column name for the m2m table" 756 # If this is an m2m relation to self, avoid the inevitable name clash 757 if related.model == related.parent_model: 758 return 'to_' + related.parent_model._meta.object_name.lower() + '_id' 759 else: 760 return related.parent_model._meta.object_name.lower() + '_id' 797 try: 798 return self._m2m_reverse_name_cache 799 except: 800 if self.rel.through is not None: 801 found = False 802 for f in self.rel.through_model._meta.fields: 803 if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model: 804 if related.model == related.parent_model: 805 # If this is an m2m-intermediate to self, 806 # the first foreign key you find will be 807 # the source column. Keep searching for 808 # the second foreign key. 809 if found: 810 self._m2m_reverse_name_cache = f.column 811 break 812 else: 813 found = True 814 else: 815 self._m2m_reverse_name_cache = f.column 816 break 817 # If this is an m2m relation to self, avoid the inevitable name clash 818 elif related.model == related.parent_model: 819 self._m2m_reverse_name_cache = 'to_' + related.parent_model._meta.object_name.lower() + '_id' 820 else: 821 self._m2m_reverse_name_cache = related.parent_model._meta.object_name.lower() + '_id' 761 822 823 # Return the newly cached value 824 return self._m2m_reverse_name_cache 825 762 826 def isValidIDList(self, field_data, all_data): 763 827 "Validates that the value is a valid list of foreign keys" 764 828 mod = self.rel.to … … 791 855 return new_data 792 856 793 857 def contribute_to_class(self, cls, name): 794 super(ManyToManyField, self).contribute_to_class(cls, name) 858 super(ManyToManyField, self).contribute_to_class(cls, name) 795 859 # Add the descriptor for the m2m relation 796 860 setattr(cls, self.name, ReverseManyRelatedObjectsDescriptor(self)) 797 861 798 862 # Set up the accessor for the m2m table name for the relation 799 863 self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta) 800 864 865 # Populate some necessary rel arguments so that cross-app relations 866 # work correctly. 867 if isinstance(self.rel.through, basestring): 868 def resolve_through_model(field, model, cls): 869 field.rel.through_model = model 870 add_lazy_relation(cls, self, self.rel.through, resolve_through_model) 871 elif self.rel.through: 872 self.rel.through_model = self.rel.through 873 self.rel.through = self.rel.through._meta.object_name 874 801 875 if isinstance(self.rel.to, basestring): 802 876 target = self.rel.to 803 877 else: -
django/core/management/validation.py
old new 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 from_model, to_model = cls, f.rel.to 119 if from_model == to_model and f.rel.symmetrical: 120 e.add(opts, "Many-to-many fields with intermediate tables cannot be symmetrical.") 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: # relation to self 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 to %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, from_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 %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 %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
old new 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
old new 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
old new 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
old new 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") 143 too_many_friends = models.ManyToManyField('self', through="RelationshipTripleFK") 144 145 class PersonSelfRefM2MExplicit(models.Model): 146 name = models.CharField(max_length=5) 147 friends = models.ManyToManyField('self', through="ExplicitRelationship", symmetrical=True) 148 149 class Relationship(models.Model): 150 first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set") 151 second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set") 152 date_added = models.DateTimeField() 153 154 class ExplicitRelationship(models.Model): 155 first = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_from_set") 156 second = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_to_set") 157 date_added = models.DateTimeField() 158 159 class RelationshipTripleFK(models.Model): 160 first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set_2") 161 second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set_2") 162 third = models.ForeignKey(PersonSelfRefM2M, related_name="too_many_by_far") 163 date_added = models.DateTimeField() 164 165 class RelationshipDoubleFK(models.Model): 166 first = models.ForeignKey(Person, related_name="first_related_name") 167 second = models.ForeignKey(Person, related_name="second_related_name") 168 third = models.ForeignKey(Group, related_name="rel_to_set") 169 date_added = models.DateTimeField() 170 114 171 model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute. 115 172 invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute. 116 173 invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute. … … 195 252 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 253 invalid_models.missingrelations: 'rel2' has m2m relation with model Rel2, which has not been installed 197 254 invalid_models.missingrelations: 'rel1' has relation with model Rel1, which has not been installed 255 invalid_models.grouptwo: 'primary' has a manually-defined m2m relation through model Membership, which does not have foreign keys to Person and GroupTwo 256 invalid_models.grouptwo: 'secondary' has a manually-defined m2m relation through model MembershipMissingFK, which does not have foreign keys to Group and GroupTwo 257 invalid_models.missingmanualm2mmodel: 'missing_m2m' specifies an m2m relation through model MissingM2MModel, which has not been installed 258 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. 259 invalid_models.group: Intermediary model RelationshipDoubleFK has more than one foreign key to Person, which is ambiguous and is not permitted. 260 invalid_models.personselfrefm2m: Many-to-many fields with intermediate tables cannot be symmetrical. 261 invalid_models.personselfrefm2m: Intermediary model RelationshipTripleFK has more than two foreign keys to PersonSelfRefM2M, which is ambiguous and is not permitted. 262 invalid_models.personselfrefm2mexplicit: Many-to-many fields with intermediate tables cannot be symmetrical. 198 263 """ -
tests/modeltests/m2m_through/__init__.py
old new 1 2 -
tests/modeltests/m2m_through/models.py
old new 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", symmetrical=False) 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 isn't a symmetrical relation, Tony's friend link still exists. 274 >>> tony.friends.all() 275 [<PersonSelfRefM2M: Chris>] 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 """} -
tests/regressiontests/m2m_through_regress/__init__.py
old new 1 2 -
tests/regressiontests/m2m_through_regress/models.py
old new 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 from django.db import models 104 from datetime import datetime 105 from django.contrib.auth.models import User 106 107 # Forward declared intermediate model 108 class Membership(models.Model): 109 person = models.ForeignKey('Person') 110 group = models.ForeignKey('Group') 111 date_joined = models.DateTimeField(default=datetime.now) 112 113 def __unicode__(self): 114 return "%s is a member of %s" % (self.person.name, self.group.name) 115 116 class UserMembership(models.Model): 117 user = models.ForeignKey(User) 118 group = models.ForeignKey('Group') 119 date_joined = models.DateTimeField(default=datetime.now) 120 121 def __unicode__(self): 122 return "%s is a user and member of %s" % (self.user.username, self.group.name) 123 124 class Person(models.Model): 125 name = models.CharField(max_length=128) 126 127 def __unicode__(self): 128 return self.name 129 130
