Ticket #6095: 6095-rc1.diff

File 6095-rc1.diff, 53.2 KB (added by russellm, 7 years ago)

First (hopefully final) release candidate for m2m-intermediates

  • django/db/models/fields/related.py

     
    2323
    2424pending_lookups = {}
    2525
    26 def add_lazy_relation(cls, field, relation):
     26def add_lazy_relation(cls, field, relation, operation):
    2727    """
    2828    Adds a lookup on ``cls`` when a related field is defined using a string,
    2929    i.e.::
     
    4545    If the other model hasn't yet been loaded -- almost a given if you're using
    4646    lazy relationships -- then the relation won't be set up until the
    4747    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.
    4850    """
    4951    # Check for recursive relations
    5052    if relation == RECURSIVE_RELATIONSHIP_CONSTANT:
     
    6668    # is prepared.
    6769    model = get_model(app_label, model_name, False)
    6870    if model:
    69         field.rel.to = model
    70         field.do_related_class(model, cls)
     71        operation(field, model, cls)
    7172    else:
    7273        key = (app_label, model_name)
    73         value = (cls, field)
     74        value = (cls, field, operation)
    7475        pending_lookups.setdefault(key, []).append(value)
    7576
    7677def do_pending_lookups(sender):
     
    7879    Handle any pending relations to the sending model. Sent from class_prepared.
    7980    """
    8081    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)
    8484
    8585dispatcher.connect(do_pending_lookups, signal=signals.class_prepared)
    8686
     
    108108
    109109        other = self.rel.to
    110110        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)
    112115        else:
    113116            self.do_related_class(other, cls)
    114117
     
    339342            manager.clear()
    340343        manager.add(*value)
    341344
    342 def create_many_related_manager(superclass):
     345def create_many_related_manager(superclass, through=False):
    343346    """Creates a manager that subclasses 'superclass' (which is a Manager)
    344347    and adds behavior for many-to-many related objects."""
    345348    class ManyRelatedManager(superclass):
     
    353356            self.join_table = join_table
    354357            self.source_col_name = source_col_name
    355358            self.target_col_name = target_col_name
     359            self.through = through
    356360            self._pk_val = self.instance._get_pk_val()
    357361            if self._pk_val is None:
    358362                raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__)
     
    360364        def get_query_set(self):
    361365            return superclass.get_query_set(self).filter(**(self.core_filters))
    362366
    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)
    365372
    366             # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
    367             if self.symmetrical:
    368                 self._add_items(self.target_col_name, self.source_col_name, *objs)
    369         add.alters_data = True
     373                # 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
    370377
    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)
    373380
    374             # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table
    375             if self.symmetrical:
    376                 self._remove_items(self.target_col_name, self.source_col_name, *objs)
    377         remove.alters_data = True
     381                # 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
    378385
    379386        def clear(self):
    380387            self._clear_items(self.source_col_name)
     
    385392        clear.alters_data = True
    386393
    387394        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
    388399            new_obj = self.model(**kwargs)
    389400            new_obj.save()
    390401            self.add(new_obj)
     
    472483        # model's default manager.
    473484        rel_model = self.related.model
    474485        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)
    476487
    477488        qn = connection.ops.quote_name
    478489        manager = RelatedManager(
     
    491502        if instance is None:
    492503            raise AttributeError, "Manager must be accessed via instance"
    493504
     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
    494509        manager = self.__get__(instance)
    495510        manager.clear()
    496511        manager.add(*value)
     
    513528        # model's default manager.
    514529        rel_model=self.field.rel.to
    515530        superclass = rel_model._default_manager.__class__
    516         RelatedManager = create_many_related_manager(superclass)
     531        RelatedManager = create_many_related_manager(superclass, self.field.rel.through)
    517532
    518533        qn = connection.ops.quote_name
    519534        manager = RelatedManager(
     
    532547        if instance is None:
    533548            raise AttributeError, "Manager must be accessed via instance"
    534549
     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
    535554        manager = self.__get__(instance)
    536555        manager.clear()
    537556        manager.add(*value)
     
    583602
    584603class ManyToManyRel(object):
    585604    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):
    587606        self.to = to
    588607        self.num_in_admin = num_in_admin
    589608        self.related_name = related_name
     
    593612        self.edit_inline = False
    594613        self.symmetrical = symmetrical
    595614        self.multiple = True
     615        self.through = through
    596616
    597617class ForeignKey(RelatedField, Field):
    598618    empty_strings_allowed = False
     
    722742            num_in_admin=kwargs.pop('num_in_admin', 0),
    723743            related_name=kwargs.pop('related_name', None),
    724744            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           
    726748        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
    727755        Field.__init__(self, **kwargs)
    728756
    729757        msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.')
     
    738766
    739767    def _get_m2m_db_table(self, opts):
    740768        "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:
    742772            return self.db_table
    743773        else:
    744774            return '%s_%s' % (opts.db_table, self.name)
    745775
    746776    def _get_m2m_column_name(self, related):
    747777        "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
    753794
    754795    def _get_m2m_reverse_name(self, related):
    755796        "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'
    761822
     823            # Return the newly cached value
     824            return self._m2m_reverse_name_cache
     825
    762826    def isValidIDList(self, field_data, all_data):
    763827        "Validates that the value is a valid list of foreign keys"
    764828        mod = self.rel.to
     
    791855        return new_data
    792856
    793857    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)       
    795859        # Add the descriptor for the m2m relation
    796860        setattr(cls, self.name, ReverseManyRelatedObjectsDescriptor(self))
    797861
    798862        # Set up the accessor for the m2m table name for the relation
    799863        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           
    801875        if isinstance(self.rel.to, basestring):
    802876            target = self.rel.to
    803877        else:
  • django/core/management/validation.py

     
    102102                        if r.get_accessor_name() == rel_query_name:
    103103                            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))
    104104
     105        seen_intermediary_signatures = []
    105106        for i, f in enumerate(opts.local_many_to_many):
    106107            # Check to see if the related m2m field will clash with any
    107108            # existing fields, m2m fields, m2m related objects or related
     
    112113                # so skip the next section
    113114                if isinstance(f.rel.to, (str, unicode)):
    114115                    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           
    116159            rel_opts = f.rel.to._meta
    117160            rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
    118161            rel_query_name = f.related_query_name()
  • django/core/management/sql.py

     
    353353    qn = connection.ops.quote_name
    354354    inline_references = connection.features.inline_fk_references
    355355    for f in opts.local_many_to_many:
    356         if not isinstance(f.rel, generic.GenericRel):
     356        if f.creates_table:
    357357            tablespace = f.db_tablespace or opts.db_tablespace
    358358            if tablespace and connection.features.supports_tablespaces:
    359359                tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace, inline=True)
  • django/contrib/admin/options.py

     
    161161                kwargs['empty_label'] = db_field.blank and _('None') or None
    162162            else:
    163163                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:
    165168                        kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel)
    166169                        kwargs['help_text'] = ''
    167170                    elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
  • django/contrib/contenttypes/generic.py

     
    104104                            limit_choices_to=kwargs.pop('limit_choices_to', None),
    105105                            symmetrical=kwargs.pop('symmetrical', True))
    106106
     107        # By its very nature, a GenericRelation doesn't create a table.
     108        self.creates_table = False
     109
    107110        # Override content-type/object-id field names on the related class
    108111        self.object_id_field_name = kwargs.pop("object_id_field", "object_id")
    109112        self.content_type_field_name = kwargs.pop("content_type_field", "content_type")
  • tests/modeltests/invalid_models/models.py

     
    110110class MissingRelations(models.Model):
    111111    rel1 = models.ForeignKey("Rel1")
    112112    rel2 = models.ManyToManyField("Rel2")
     113   
     114class MissingManualM2MModel(models.Model):
     115    name = models.CharField(max_length=5)
     116    missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel")
     117   
     118class Person(models.Model):
     119    name = models.CharField(max_length=5)
    113120
     121class 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
     127class 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
     132class Membership(models.Model):
     133    person = models.ForeignKey(Person)
     134    group = models.ForeignKey(Group)
     135    not_default_or_null = models.CharField(max_length=5)
     136
     137class MembershipMissingFK(models.Model):
     138    person = models.ForeignKey(Person)
     139
     140class 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
     145class PersonSelfRefM2MExplicit(models.Model):
     146    name = models.CharField(max_length=5)
     147    friends = models.ManyToManyField('self', through="ExplicitRelationship", symmetrical=True)
     148
     149class 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
     154class 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
     159class 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
     165class 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
    114171model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute.
    115172invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute.
    116173invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute.
     
    195252invalid_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'.
    196253invalid_models.missingrelations: 'rel2' has m2m relation with model Rel2, which has not been installed
    197254invalid_models.missingrelations: 'rel1' has relation with model Rel1, which has not been installed
     255invalid_models.grouptwo: 'primary' has a manually-defined m2m relation through model Membership, which does not have foreign keys to Person and GroupTwo
     256invalid_models.grouptwo: 'secondary' has a manually-defined m2m relation through model MembershipMissingFK, which does not have foreign keys to Group and GroupTwo
     257invalid_models.missingmanualm2mmodel: 'missing_m2m' specifies an m2m relation through model MissingM2MModel, which has not been installed
     258invalid_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.
     259invalid_models.group: Intermediary model RelationshipDoubleFK has more than one foreign key to Person, which is ambiguous and is not permitted.
     260invalid_models.personselfrefm2m: Many-to-many fields with intermediate tables cannot be symmetrical.
     261invalid_models.personselfrefm2m: Intermediary model RelationshipTripleFK has more than two foreign keys to PersonSelfRefM2M, which is ambiguous and is not permitted.
     262invalid_models.personselfrefm2mexplicit: Many-to-many fields with intermediate tables cannot be symmetrical.
    198263"""
  • 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

     
     1from django.db import models
     2from datetime import datetime
     3
     4# M2M described on one of the models
     5class Person(models.Model):
     6    name = models.CharField(max_length=128)
     7
     8    def __unicode__(self):
     9        return self.name
     10
     11class 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
     20class 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
     29class 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
     41class TestNoDefaultsOrNulls(models.Model):
     42    person = models.ForeignKey(Person)
     43    group = models.ForeignKey(Group)
     44    nodefaultnonull = models.CharField(max_length=5)
     45
     46class 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
     53class 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)
     119Traceback (most recent call last):
     120...
     121AttributeError: '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')
     125Traceback (most recent call last):
     126...
     127AttributeError: 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)
     131Traceback (most recent call last):
     132...
     133AttributeError: '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
     152Traceback (most recent call last):
     153...
     154AttributeError: 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)
     170Traceback (most recent call last):
     171...
     172AttributeError: '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')
     176Traceback (most recent call last):
     177...
     178AttributeError: 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)
     182Traceback (most recent call last):
     183...
     184AttributeError: '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
     201Traceback (most recent call last):
     202...
     203AttributeError: 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

     
     1from django.db import models
     2from datetime import datetime
     3from django.contrib.auth.models import User
     4
     5# Forward declared intermediate model
     6class 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
     14class 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
     22class Person(models.Model):
     23    name = models.CharField(max_length=128)
     24
     25    def __unicode__(self):
     26        return self.name
     27
     28class 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 = []
     66Traceback (most recent call last):
     67...
     68AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead.
     69
     70>>> roll.members = []
     71Traceback (most recent call last):
     72...
     73AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead.
     74
     75>>> rock.members.create(name='Anne')
     76Traceback (most recent call last):
     77...
     78AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead.
     79
     80>>> bob.group_set.create(name='Funk')
     81Traceback (most recent call last):
     82...
     83AttributeError: 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"""}
     103from django.db import models
     104from datetime import datetime
     105from django.contrib.auth.models import User
     106
     107# Forward declared intermediate model
     108class 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
     116class 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
     124class Person(models.Model):
     125    name = models.CharField(max_length=128)
     126
     127    def __unicode__(self):
     128        return self.name
     129
     130class 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 = []
     168Traceback (most recent call last):
     169...
     170AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead.
     171
     172>>> roll.members = []
     173Traceback (most recent call last):
     174...
     175AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead.
     176
     177>>> rock.members.create(name='Anne')
     178Traceback (most recent call last):
     179...
     180AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead.
     181
     182>>> bob.group_set.create(name='Funk')
     183Traceback (most recent call last):
     184...
     185AttributeError: 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

     
    154154    Maciej Fijalkowski
    155155    Matthew Flanagan <http://wadofstuff.blogspot.com>
    156156    Eric Floehr <eric@intellovations.com>
     157    Eric Florenzano <floguy@gmail.com>
    157158    Vincent Foley <vfoleybourgon@yahoo.ca>
    158159    Rudolph Froger <rfroger@estrate.nl>
    159160    Jorge Gajon <gajon@gajon.org>
  • docs/model-api.txt

     
    656656interface. This lets you include HTML in ``help_text`` if you so desire. For
    657657example::
    658658
    659         help_text="Please use the following format: <em>YYYY-MM-DD</em>."
     659    help_text="Please use the following format: <em>YYYY-MM-DD</em>."
    660660
    661661Alternatively you can use plain text and
    662662``django.utils.html.escape()`` to escape any HTML special characters.
     
    945945
    946946    =======================  ============================================================
    947947
     948Extra fields on many-to-many relationships
     949~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     950
     951**New in Django development version**
     952
     953When you're only dealing with simple many-to-many relationships such as
     954mixing and matching pizzas and toppings, a standard ``ManyToManyField``
     955is all you need. However, sometimes you may need to associate data with the
     956relationship between two models.
     957
     958For example, consider the case of an application tracking the musical groups
     959which musicians belong to. There is a many-to-many relationship between a person
     960and the groups of which they are a member, so you could use a ManyToManyField
     961to represent this relationship. However, there is a lot of detail about the
     962membership that you might want to collect, such as the date at which the person
     963joined the group.
     964
     965For these situations, Django allows you to specify the model that will be used
     966to govern the many-to-many relationship. You can then put extra fields on the
     967intermediate model. The intermediate model is associated with the
     968``ManyToManyField`` using the ``through`` argument to point to the model
     969that will act as an intermediary. For our musician example, the code would look
     970something 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
     991When you set up the intermediary model, you explicitly specify foreign
     992keys to the models that are involved in the ManyToMany relation. This
     993explicit declaration defines how the two models are related.
     994
     995There 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
     1008Now that you have set up your ``ManyToManyField`` to use your intermediary
     1009model (Membership, in this case), you're ready to start creating some
     1010many-to-many relationships. You do this by creating instances of the
     1011intermediate 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
     1030Unlike normal many-to-many fields, you *can't* use ``add``, ``create``,
     1031or 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   
     1040Why? You can't just create a relationship between a Person and a Group - you
     1041need to specify all the detail for the relationship required by the
     1042Membership table. The simple ``add``, ``create`` and assignment calls
     1043don't provide a way to specify this extra detail. As a result, they are
     1044disabled for many-to-many relationships that use an intermediate model.
     1045The only way to create a many-to-many relationship with an intermediate table
     1046is to create instances of the intermediate model.
     1047
     1048The ``remove`` method is disabled for similar reasons. However, the
     1049``clear()`` method can be used to remove all many-to-many relationships
     1050for an instance::
     1051
     1052    # Beatles have broken up
     1053    >>> beatles.members.clear()
     1054
     1055Once you have established the many-to-many relationships by creating instances
     1056of your intermediate model, you can issue queries. Just as with normal
     1057many-to-many relationships, you can query using the attributes of the
     1058many-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
     1064As you are using an intermediate table, you can also query on the attributes
     1065of 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   
    9481073One-to-one relationships
    9491074~~~~~~~~~~~~~~~~~~~~~~~~
    9501075
     
    11461271For convenience, unique_together can be a single list when dealing
    11471272with a single set of fields::
    11481273
    1149         unique_together = ("driver", "restaurant")
     1274    unique_together = ("driver", "restaurant")
    11501275
    11511276``verbose_name``
    11521277----------------
  • docs/admin.txt

     
    613613            FriendshipInline,
    614614        ]
    615615
     616Working with Many-to-Many Intermediary Models
     617----------------------------------------------
     618
     619By default, admin widgets for many-to-many relations will be displayed inline
     620on whichever model contains the actual reference to the `ManyToManyField`. 
     621However, when you specify an intermediary model using the ``through``
     622argument to a ``ManyToManyField``, the admin will not display a widget by
     623default. This is because each instance of that intermediary model requires
     624more information than could be displayed in a single widget, and the layout
     625required for multiple widgets will vary depending on the intermediate model.
     626
     627However, we still want to be able to edit that information inline. Fortunately,
     628this is easy to do with inline admin models. Suppose we have the following
     629models::
     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
     644The first step in displaying this intermediate model in the admin is to
     645define an inline model for the Membership table::
     646
     647    class MembershipInline(admin.TabularInline):
     648        model = Membership
     649        extra = 1
     650
     651This simple example uses the defaults inline form for the Membership model,
     652and shows 1 extra line. This could be customized using any of the options
     653available to inline models.
     654
     655Now 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
     663Finally, register your ``Person`` and ``Group`` models with the admin site::
     664   
     665    admin.site.register(Person, PersonAdmin)
     666    admin.site.register(Group, GroupAdmin)
     667
     668Now your admin site is set up to edit ``Membership`` objects inline from either
     669the ``Person`` or the ``Group`` detail pages.
     670
    616671``AdminSite`` objects
    617672=====================
    618673
Back to Top