Ticket #6095: 6095-rc1.diff
File 6095-rc1.diff, 53.2 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 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
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
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") 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
Property changes on: tests/modeltests/m2m_through ___________________________________________________________________ Name: svn:ignore + *.pyc
1 2 -
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", 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 """} 329 No newline at end of file -
tests/regressiontests/m2m_through_regress/__init__.py
Property changes on: tests/regressiontests/m2m_through_regress ___________________________________________________________________ Name: svn:ignore + *.pyc
1 2 -
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 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 class Group(models.Model): 131 name = models.CharField(max_length=128) 132 # Membership object defined as a class 133 members = models.ManyToManyField(Person, through=Membership) 134 user_members = models.ManyToManyField(User, through='UserMembership') 135 136 def __unicode__(self): 137 return self.name 138 139 __test__ = {'API_TESTS':""" 140 # Create some dummy data 141 >>> bob = Person.objects.create(name='Bob') 142 >>> jim = Person.objects.create(name='Jim') 143 144 >>> rock = Group.objects.create(name='Rock') 145 >>> roll = Group.objects.create(name='Roll') 146 147 >>> frank = User.objects.create_user('frank','frank@example.com','password') 148 >>> jane = User.objects.create_user('jane','jane@example.com','password') 149 150 # Now test that the forward declared Membership works 151 >>> Membership.objects.create(person=bob, group=rock) 152 <Membership: Bob is a member of Rock> 153 154 >>> Membership.objects.create(person=bob, group=roll) 155 <Membership: Bob is a member of Roll> 156 157 >>> Membership.objects.create(person=jim, group=rock) 158 <Membership: Jim is a member of Rock> 159 160 >>> bob.group_set.all() 161 [<Group: Rock>, <Group: Roll>] 162 163 >>> roll.members.all() 164 [<Person: Bob>] 165 166 # Error messages use the model name, not repr of the class name 167 >>> bob.group_set = [] 168 Traceback (most recent call last): 169 ... 170 AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. 171 172 >>> roll.members = [] 173 Traceback (most recent call last): 174 ... 175 AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. 176 177 >>> rock.members.create(name='Anne') 178 Traceback (most recent call last): 179 ... 180 AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. 181 182 >>> bob.group_set.create(name='Funk') 183 Traceback (most recent call last): 184 ... 185 AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. 186 187 # Now test that the intermediate with a relationship outside 188 # the current app (i.e., UserMembership) workds 189 >>> UserMembership.objects.create(user=frank, group=rock) 190 <UserMembership: frank is a user and member of Rock> 191 192 >>> UserMembership.objects.create(user=frank, group=roll) 193 <UserMembership: frank is a user and member of Roll> 194 195 >>> UserMembership.objects.create(user=jane, group=rock) 196 <UserMembership: jane is a user and member of Rock> 197 198 >>> frank.group_set.all() 199 [<Group: Rock>, <Group: Roll>] 200 201 >>> roll.user_members.all() 202 [<User: frank>] 203 204 """} 205 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
656 656 interface. This lets you include HTML in ``help_text`` if you so desire. For 657 657 example:: 658 658 659 659 help_text="Please use the following format: <em>YYYY-MM-DD</em>." 660 660 661 661 Alternatively you can use plain text and 662 662 ``django.utils.html.escape()`` to escape any HTML special characters. … … 945 945 946 946 ======================= ============================================================ 947 947 948 Extra fields on many-to-many relationships 949 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 950 951 **New in Django development version** 952 953 When you're only dealing with simple many-to-many relationships such as 954 mixing and matching pizzas and toppings, a standard ``ManyToManyField`` 955 is all you need. However, sometimes you may need to associate data with the 956 relationship between two models. 957 958 For example, consider the case of an application tracking the musical groups 959 which musicians belong to. There is a many-to-many relationship between a person 960 and the groups of which they are a member, so you could use a ManyToManyField 961 to represent this relationship. However, there is a lot of detail about the 962 membership that you might want to collect, such as the date at which the person 963 joined the group. 964 965 For these situations, Django allows you to specify the model that will be used 966 to govern the many-to-many relationship. You can then put extra fields on the 967 intermediate model. The intermediate model is associated with the 968 ``ManyToManyField`` using the ``through`` argument to point to the model 969 that will act as an intermediary. For our musician example, the code would look 970 something like this:: 971 972 class Person(models.Model): 973 name = models.CharField(max_length=128) 974 975 def __unicode__(self): 976 return self.name 977 978 class Group(models.Model): 979 name = models.CharField(max_length=128) 980 members = models.ManyToManyField(Person, through='Membership') 981 982 def __unicode__(self): 983 return self.name 984 985 class Membership(models.Model): 986 person = models.ForeignKey(Person) 987 group = models.ForeignKey(Group) 988 date_joined = models.DateField() 989 invite_reason = models.CharField(max_length=64) 990 991 When you set up the intermediary model, you explicitly specify foreign 992 keys to the models that are involved in the ManyToMany relation. This 993 explicit declaration defines how the two models are related. 994 995 There are a few restrictions on the intermediate model: 996 997 * Your intermediate model must contain one - and *only* one - foreign key 998 on the target model (this would be ``Person`` in our example). If you 999 have more than one foreign key, a validation error will be raised. 1000 1001 * Your intermediate model must contain one - and *only* one - foreign key 1002 on the source model (this would be ``Group`` in our example). If you 1003 have more than one foreign key, a validation error will be raised. 1004 1005 * If the many-to-many relation is a relation on itself, the relationship 1006 must be non-symmetric. 1007 1008 Now that you have set up your ``ManyToManyField`` to use your intermediary 1009 model (Membership, in this case), you're ready to start creating some 1010 many-to-many relationships. You do this by creating instances of the 1011 intermediate model:: 1012 1013 >>> ringo = Person.objects.create(name="Ringo Starr") 1014 >>> paul = Person.objects.create(name="Paul McCartney") 1015 >>> beatles = Group.objects.create(name="The Beatles") 1016 >>> m1 = Membership(person=ringo, group=beatles, 1017 ... date_joined=date(1962, 8, 16), 1018 ... invite_reason= "Needed a new drummer.") 1019 >>> m1.save() 1020 >>> beatles.members.all() 1021 [<Person: Ringo Starr>] 1022 >>> ringo.group_set.all() 1023 [<Group: The Beatles>] 1024 >>> m2 = Membership.objects.create(person=paul, group=beatles, 1025 ... date_joined=date(1960, 8, 1), 1026 ... invite_reason= "Wanted to form a band.") 1027 >>> beatles.members.all() 1028 [<Person: Ringo Starr>, <Person: Paul McCartney>] 1029 1030 Unlike normal many-to-many fields, you *can't* use ``add``, ``create``, 1031 or assignment (i.e., ``beatles.members = [...]``) to create relationships:: 1032 1033 # THIS WILL NOT WORK 1034 >>> beatles.members.add(john) 1035 # NEITHER WILL THIS 1036 >>> beatles.members.create(name="George Harrison") 1037 # AND NEITHER WILL THIS 1038 >>> beatles.members = [john, paul, ringo, george] 1039 1040 Why? You can't just create a relationship between a Person and a Group - you 1041 need to specify all the detail for the relationship required by the 1042 Membership table. The simple ``add``, ``create`` and assignment calls 1043 don't provide a way to specify this extra detail. As a result, they are 1044 disabled for many-to-many relationships that use an intermediate model. 1045 The only way to create a many-to-many relationship with an intermediate table 1046 is to create instances of the intermediate model. 1047 1048 The ``remove`` method is disabled for similar reasons. However, the 1049 ``clear()`` method can be used to remove all many-to-many relationships 1050 for an instance:: 1051 1052 # Beatles have broken up 1053 >>> beatles.members.clear() 1054 1055 Once you have established the many-to-many relationships by creating instances 1056 of your intermediate model, you can issue queries. Just as with normal 1057 many-to-many relationships, you can query using the attributes of the 1058 many-to-many-related model:: 1059 1060 # Find all the groups with a member whose name starts with 'Paul' 1061 >>> Groups.objects.filter(person__name__startswith='Paul') 1062 [<Group: The Beatles>] 1063 1064 As you are using an intermediate table, you can also query on the attributes 1065 of the intermediate model:: 1066 1067 # Find all the members of the Beatles that joined after 1 Jan 1961 1068 >>> Person.objects.filter( 1069 ... group__name='The Beatles', 1070 ... membership__date_joined__gt=date(1961,1,1)) 1071 [<Person: Ringo Starr] 1072 948 1073 One-to-one relationships 949 1074 ~~~~~~~~~~~~~~~~~~~~~~~~ 950 1075 … … 1146 1271 For convenience, unique_together can be a single list when dealing 1147 1272 with a single set of fields:: 1148 1273 1149 1274 unique_together = ("driver", "restaurant") 1150 1275 1151 1276 ``verbose_name`` 1152 1277 ---------------- -
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, when you specify an intermediary model using the ``through`` 622 argument to a ``ManyToManyField``, the admin will not display a widget by 623 default. This is because each instance of that intermediary model requires 624 more information than could be displayed in a single widget, and the layout 625 required for multiple widgets will vary depending on the intermediate model. 626 627 However, we still want to be able to edit that information inline. Fortunately, 628 this is easy to do with inline admin models. Suppose we have the following 629 models:: 630 631 class Person(models.Model): 632 name = models.CharField(max_length=128) 633 634 class Group(models.Model): 635 name = models.CharField(max_length=128) 636 members = models.ManyToManyField(Person, through='Membership') 637 638 class Membership(models.Model): 639 person = models.ForeignKey(Person) 640 group = models.ForeignKey(Group) 641 date_joined = models.DateField() 642 invite_reason = models.CharField(max_length=64) 643 644 The first step in displaying this intermediate model in the admin is to 645 define an inline model for the Membership table:: 646 647 class MembershipInline(admin.TabularInline): 648 model = Membership 649 extra = 1 650 651 This simple example uses the defaults inline form for the Membership model, 652 and shows 1 extra line. This could be customized using any of the options 653 available to inline models. 654 655 Now create admin views for the ``Person`` and ``Group`` models:: 656 657 class PersonAdmin(admin.ModelAdmin): 658 inlines = (MembershipInline,) 659 660 class GroupAdmin(admin.ModelAdmin): 661 inlines = (MembershipInline,) 662 663 Finally, register your ``Person`` and ``Group`` models with the admin site:: 664 665 admin.site.register(Person, PersonAdmin) 666 admin.site.register(Group, GroupAdmin) 667 668 Now your admin site is set up to edit ``Membership`` objects inline from either 669 the ``Person`` or the ``Group`` detail pages. 670 616 671 ``AdminSite`` objects 617 672 ===================== 618 673