Ticket #6095: 6095-trunk-withdocs.rc3.diff

File 6095-trunk-withdocs.rc3.diff, 32.4 KB (added by floguy, 7 years ago)

Fixed cross-app problem. My solution is perhaps a bit hackish. Seems like the better solution would be to patch the add_lazy_relation function to work with intermediary models, but when I tried to do so I ran into non-obvious race conditions. This method works, is fairly transparent in how it works, and doesn't sacrificce performance. If anyone has feedback on better ways to do this, feel free to chime in.

  • django/db/models/options.py

     
    1010from django.db.models.fields.related import ManyToManyRel
    1111from django.db.models.fields import AutoField, FieldDoesNotExist
    1212from django.db.models.fields.proxy import OrderWrt
    13 from django.db.models.loading import get_models, app_cache_ready
     13from django.db.models.loading import get_models, get_model, app_cache_ready
    1414from django.db.models import Manager
    1515from django.utils.translation import activate, deactivate_all, get_language, string_concat
    1616from django.utils.encoding import force_unicode, smart_str
     
    402402            follow = self.get_follow()
    403403        return [f for f in self.get_all_related_objects() if follow.get(f.name, None)]
    404404
     405    def get_related_object(self, from_model, self_ref=False):
     406        "Gets the RelatedObject which links from from_model to this model."
     407        for related_object in self.get_all_related_objects():
     408            if related_object.model == from_model:
     409                if self_ref:
     410                    self_ref = False
     411                else:
     412                    return related_object
     413        return None
     414
    405415    def get_data_holders(self, follow=None):
    406416        if follow == None:
    407417            follow = self.get_follow()
  • django/db/models/fields/related.py

     
    340340            manager.clear()
    341341        manager.add(*value)
    342342
    343 def create_many_related_manager(superclass):
     343def create_many_related_manager(superclass, through=False):
    344344    """Creates a manager that subclasses 'superclass' (which is a Manager)
    345345    and adds behavior for many-to-many related objects."""
    346346    class ManyRelatedManager(superclass):
     
    354354            self.join_table = join_table
    355355            self.source_col_name = source_col_name
    356356            self.target_col_name = target_col_name
     357            self.through = through
    357358            self._pk_val = self.instance._get_pk_val()
    358359            if self._pk_val is None:
    359360                raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__)
     
    386387        clear.alters_data = True
    387388
    388389        def create(self, **kwargs):
     390            # This check needs to be done here, since we can't later remove this
     391            # from the method lookup table, as we do with add and remove.
     392            if through is not None:
     393                raise AttributeError, "'ManyRelatedManager' object has no attribute 'create'"
    389394            new_obj = self.model(**kwargs)
    390395            new_obj.save()
    391396            self.add(new_obj)
     
    453458                [self._pk_val])
    454459            transaction.commit_unless_managed()
    455460
     461    # If it's an intermediary model, detach the add and remove methods from this
     462    # ManyRelatedManager.  Create cannot be detached this way due to an
     463    # inherited method in the dynamic method lookup table that gets in the way.
     464    if through is not None:
     465        del ManyRelatedManager.add
     466        del ManyRelatedManager.remove
     467
    456468    return ManyRelatedManager
    457469
    458470class ManyRelatedObjectsDescriptor(object):
     
    473485        # model's default manager.
    474486        rel_model = self.related.model
    475487        superclass = rel_model._default_manager.__class__
    476         RelatedManager = create_many_related_manager(superclass)
     488        RelatedManager = create_many_related_manager(superclass, self.related.field.rel.through)
    477489
    478490        qn = connection.ops.quote_name
    479491        manager = RelatedManager(
     
    492504        if instance is None:
    493505            raise AttributeError, "Manager must be accessed via instance"
    494506
     507        through = getattr(self.related.field.rel, 'through', None)
     508        if through is not None:
     509            raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model.  Use %s's Manager instead." % through
     510
    495511        manager = self.__get__(instance)
    496512        manager.clear()
    497513        manager.add(*value)
     
    514530        # model's default manager.
    515531        rel_model=self.field.rel.to
    516532        superclass = rel_model._default_manager.__class__
    517         RelatedManager = create_many_related_manager(superclass)
     533        RelatedManager = create_many_related_manager(superclass, self.field.rel.through)
    518534
    519535        qn = connection.ops.quote_name
    520536        manager = RelatedManager(
     
    533549        if instance is None:
    534550            raise AttributeError, "Manager must be accessed via instance"
    535551
     552        through = getattr(self.field.rel, 'through', None)
     553        if through is not None:
     554            raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model.  Use %s's Manager instead." % through
     555
    536556        manager = self.__get__(instance)
    537557        manager.clear()
    538558        manager.add(*value)
     
    586606
    587607class ManyToManyRel(object):
    588608    def __init__(self, to, num_in_admin=0, related_name=None,
    589         filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True):
     609        filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True, through=None):
    590610        self.to = to
    591611        self.num_in_admin = num_in_admin
    592612        self.related_name = related_name
     
    598618        self.raw_id_admin = raw_id_admin
    599619        self.symmetrical = symmetrical
    600620        self.multiple = True
     621        self.through = through
     622        self.through_app_label = None
     623        self._model_cache = None
    601624
    602625        assert not (self.raw_id_admin and self.filter_interface), "ManyToManyRels may not use both raw_id_admin and filter_interface"
    603626
     627    def _get_through_model(self):
     628        if self._model_cache:
     629            return self._model_cache
     630        if self.through and self.through_app_label:
     631            self._model_cache = get_model(self.through_app_label, self.through)
     632            return self._model_cache
     633        return None
     634    through_model = property(_get_through_model)
     635
    604636class ForeignKey(RelatedField, Field):
    605637    empty_strings_allowed = False
    606638    def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs):
     
    746778            filter_interface=kwargs.pop('filter_interface', None),
    747779            limit_choices_to=kwargs.pop('limit_choices_to', None),
    748780            raw_id_admin=kwargs.pop('raw_id_admin', False),
    749             symmetrical=kwargs.pop('symmetrical', True))
     781            symmetrical=kwargs.pop('symmetrical', True),
     782            through=kwargs.pop('through', None))
    750783        self.db_table = kwargs.pop('db_table', None)
     784        if kwargs['rel'].through is not None:
     785            self.creates_table = False
     786            assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used."
     787        else:
     788            self.creates_table = True
    751789        if kwargs["rel"].raw_id_admin:
    752790            kwargs.setdefault("validator_list", []).append(self.isValidIDList)
    753791        Field.__init__(self, **kwargs)
     
    770808
    771809    def _get_m2m_db_table(self, opts):
    772810        "Function that can be curried to provide the m2m table name for this relation"
    773         if self.db_table:
     811        if self.rel.through is not None:
     812            return self.rel.through_model._meta.db_table
     813        elif self.db_table:
    774814            return self.db_table
    775815        else:
    776816            return '%s_%s' % (opts.db_table, self.name)
    777817
    778818    def _get_m2m_column_name(self, related):
    779819        "Function that can be curried to provide the source column name for the m2m table"
     820        if self.rel.through is not None:
     821            field = related.model._meta.get_related_object(self.rel.through_model).field
     822            return field.get_attname_column()[1]
    780823        # If this is an m2m relation to self, avoid the inevitable name clash
    781         if related.model == related.parent_model:
     824        elif related.model == related.parent_model:
    782825            return 'from_' + related.model._meta.object_name.lower() + '_id'
    783826        else:
    784827            return related.model._meta.object_name.lower() + '_id'
    785828
    786829    def _get_m2m_reverse_name(self, related):
    787830        "Function that can be curried to provide the related column name for the m2m table"
     831        if self.rel.through is not None:
     832            meta = related.parent_model._meta
     833            if self.parent == self.rel.to:
     834                related = meta.get_related_object(self.rel.through_model, self_ref = True)
     835            else:
     836                related = meta.get_related_object(self.rel.through_model)
     837            return related.field.get_attname_column()[1]
    788838        # If this is an m2m relation to self, avoid the inevitable name clash
    789         if related.model == related.parent_model:
     839        elif related.model == related.parent_model:
    790840            return 'to_' + related.parent_model._meta.object_name.lower() + '_id'
    791841        else:
    792842            return related.parent_model._meta.object_name.lower() + '_id'
     
    832882
    833883        # Set up the accessor for the m2m table name for the relation
    834884        self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta)
     885       
     886        # Get a reference to the parent model
     887        self.parent = cls
     888       
     889        # Populate some necessary rel arguments so that cross-app relations
     890        # work correctly.
     891        if isinstance(self.rel.through, basestring):
     892            try:
     893                self.rel.through_app_label, self.rel.through = self.rel.through.split('.')
     894            except ValueError:
     895                self.rel.through_app_label = cls._meta.app_label
    835896
    836897        if isinstance(self.rel.to, basestring):
    837898            target = self.rel.to
  • django/core/management/validation.py

     
    104104                        if r.get_accessor_name() == rel_query_name:
    105105                            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))
    106106
     107        seen_intermediary_signatures = []
    107108        for i, f in enumerate(opts.local_many_to_many):
    108109            # Check to see if the related m2m field will clash with any
    109110            # existing fields, m2m fields, m2m related objects or related
     
    114115                # so skip the next section
    115116                if isinstance(f.rel.to, (str, unicode)):
    116117                    continue
    117 
     118            if getattr(f.rel, 'through', None) is not None:
     119                intermediary_model = None
     120                for model in models.get_models():
     121                    if model._meta.module_name == f.rel.through.lower():
     122                        intermediary_model = model
     123                if intermediary_model is None:
     124                    e.add(opts, "%s has a manually-defined m2m relation through model %s, which does not exist." % (f.name, f.rel.through))
     125                else:
     126                    signature = (f.rel.to, cls, intermediary_model)
     127                    if signature in seen_intermediary_signatures:
     128                        e.add(opts, "%s has two manually-defined m2m relations through model %s, which is not possible.  Please consider using an extra field on your intermediary model instead." % (cls._meta.object_name, intermediary_model._meta.object_name))
     129                    else:
     130                        seen_intermediary_signatures.append(signature)
     131                    seen_related_fk, seen_this_fk = False, False
     132                    for field in intermediary_model._meta.fields:
     133                        if field.rel:
     134                            if not seen_related_fk and field.rel.to == f.rel.to:
     135                                seen_related_fk = True
     136                            elif field.rel.to == cls:
     137                                seen_this_fk = True
     138                    if not seen_related_fk or not seen_this_fk:
     139                        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))
     140           
    118141            rel_opts = f.rel.to._meta
    119142            rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
    120143            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/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

     
    111111class MissingRelations(models.Model):
    112112    rel1 = models.ForeignKey("Rel1")
    113113    rel2 = models.ManyToManyField("Rel2")
     114   
     115class MissingManualM2MModel(models.Model):
     116    name = models.CharField(max_length=5)
     117    missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel")
     118   
     119class Person(models.Model):
     120    name = models.CharField(max_length=5)
    114121
     122class Group(models.Model):
     123    name = models.CharField(max_length=5)
     124    primary = models.ManyToManyField(Person, through="Membership", related_name="primary")
     125    secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary")
     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
     144class Relationship(models.Model):
     145    first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set")
     146    second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set")
     147    date_added = models.DateTimeField()
     148
    115149model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute.
    116150invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute.
    117151invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute.
     
    197231invalid_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'.
    198232invalid_models.missingrelations: 'rel2' has m2m relation with model Rel2, which has not been installed
    199233invalid_models.missingrelations: 'rel1' has relation with model Rel1, which has not been installed
     234invalid_models.group: Group has two manually-defined m2m relations through model Membership, which is not possible.  Please consider using an extra field on your intermediary model instead.
     235invalid_models.missingmanualm2mmodel: missing_m2m has a manually-defined m2m relation through model MissingM2MModel, which does not exist.
     236invalid_models.grouptwo: primary has a manually-defined m2m relation through model Membership, which does not have foreign keys to Person and GroupTwo
     237invalid_models.grouptwo: secondary has a manually-defined m2m relation through model MembershipMissingFK, which does not have foreign keys to Group and GroupTwo
    200238"""
  • tests/modeltests/m2m_through/__init__.py

     
     1
  • 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")
     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, the add method is
     117# not provided.
     118>>> rock.members.add(bob)
     119Traceback (most recent call last):
     120...
     121AttributeError: 'ManyRelatedManager' object has no attribute 'add'
     122
     123>>> rock.members.create(name = 'Anne')
     124Traceback (most recent call last):
     125...
     126AttributeError: 'ManyRelatedManager' object has no attribute 'create'
     127
     128# Remove has similar complications, and is not provided either.
     129>>> rock.members.remove(jim)
     130Traceback (most recent call last):
     131...
     132AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
     133
     134# Here we back up the list of all members of Rock.
     135>>> backup = list(rock.members.all())
     136
     137# ...and we verify that it has worked.
     138>>> backup
     139[<Person: Jim>, <Person: Jane>]
     140
     141# The clear function should still work.
     142>>> rock.members.clear()
     143
     144# Now there will be no members of Rock.
     145>>> rock.members.all()
     146[]
     147
     148# Assignment should not work with models specifying a through model for many of
     149# the same reasons as adding.
     150>>> rock.members = backup
     151Traceback (most recent call last):
     152...
     153AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead.
     154
     155# Let's re-save those instances that we've cleared.
     156>>> m1.save()
     157>>> m2.save()
     158
     159# Verifying that those instances were re-saved successfully.
     160>>> rock.members.all()
     161[<Person: Jim>, <Person: Jane>]
     162
     163
     164### Reverse Descriptors Tests ###
     165
     166# Due to complications with adding via an intermediary model, the add method is
     167# not provided.
     168>>> bob.group_set.add(rock)
     169Traceback (most recent call last):
     170...
     171AttributeError: 'ManyRelatedManager' object has no attribute 'add'
     172
     173# Create is another method that should not work correctly, as it suffers from
     174# the same problems as add.
     175>>> bob.group_set.create(name = 'Funk')
     176Traceback (most recent call last):
     177...
     178AttributeError: 'ManyRelatedManager' object has no attribute 'create'
     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,
     249...     date_friended=datetime.now())
     250
     251# Tony should now show that Chris is his friend.
     252>>> tony.friends.all()
     253[<PersonSelfRefM2M: Chris>]
     254
     255# But we haven't established that Chris is Tony's Friend.
     256>>> chris.friends.all()
     257[]
     258
     259# So let's do that now.
     260>>> f2 = Friendship.objects.create(first=chris, second=tony,
     261...     date_friended=datetime.now())
     262
     263# Having added Chris as a friend, let's make sure that his friend set reflects
     264# that addition.
     265>>> chris.friends.all()
     266[<PersonSelfRefM2M: Tony>]
     267
     268# Chris gets mad and wants to get rid of all of his friends.
     269>>> chris.friends.clear()
     270
     271# Now he should not have any more friends.
     272>>> chris.friends.all()
     273[]
     274
     275# Since this is a symmetrical relation, Tony's friend link is deleted as well.
     276>>> tony.friends.all()
     277[]
     278
     279
     280
     281### QUERY TESTS ###
     282
     283# We can query for the related model by using its attribute name (members, in
     284# this case).
     285>>> Group.objects.filter(members__name='Bob')
     286[<Group: Roll>]
     287
     288# To query through the intermediary model, we specify its model name.
     289# In this case, membership.
     290>>> Group.objects.filter(membership__invite_reason = "She was just awesome.")
     291[<Group: Rock>]
     292
     293# If we want to query in the reverse direction by the related model, use its
     294# model name (group, in this case).
     295>>> Person.objects.filter(group__name="Rock")
     296[<Person: Jim>, <Person: Jane>]
     297
     298# If the m2m field has specified a related_name, using that will work.
     299>>> Person.objects.filter(custom__name="Rock")
     300[<Person: Bob>, <Person: Jim>]
     301
     302# To query through the intermediary model in the reverse direction, we again
     303# specify its model name (membership, in this case).
     304>>> Person.objects.filter(membership__invite_reason = "She was just awesome.")
     305[<Person: Jane>]
     306
     307# Let's see all of the groups that Jane joined after 1 Jan 2005:
     308>>> Group.objects.filter(membership__date_joined__gt = datetime(2005, 1, 1),
     309... membership__person = jane)
     310[<Group: Rock>]
     311
     312# Queries also work in the reverse direction: Now let's see all of the people
     313# that have joined Rock since 1 Jan 2005:
     314>>> Person.objects.filter(membership__date_joined__gt = datetime(2005, 1, 1),
     315... membership__group = rock)
     316[<Person: Jim>, <Person: Jane>]
     317
     318# Conceivably, queries through membership could return correct, but non-unique
     319# querysets.  To demonstrate this, we query for all people who have joined a
     320# group after 2004:
     321>>> Person.objects.filter(membership__date_joined__gt = datetime(2004, 1, 1))
     322[<Person: Jim>, <Person: Jim>, <Person: Jane>]
     323
     324# Jim showed up twice, because he joined two groups ('Rock', and 'Roll'):
     325>>> [(m.person.name, m.group.name) for m in
     326... Membership.objects.filter(date_joined__gt = datetime(2004, 1, 1))]
     327[(u'Jim', u'Rock'), (u'Jane', u'Rock'), (u'Jim', u'Roll')]
     328
     329# QuerySet's distinct() method can correct this problem.
     330>>> Person.objects.filter(membership__date_joined__gt = datetime(2004, 1, 1)).distinct()
     331[<Person: Jim>, <Person: Jane>]
     332"""}
     333 No newline at end of file
  • AUTHORS

     
    151151    J. Pablo Fernandez <pupeno@pupeno.com>
    152152    Matthew Flanagan <http://wadofstuff.blogspot.com>
    153153    Eric Floehr <eric@intellovations.com>
     154    Eric Florenzano <floguy@gmail.com>
    154155    Vincent Foley <vfoleybourgon@yahoo.ca>
    155156    Rudolph Froger <rfroger@estrate.nl>
    156157    Jorge Gajon <gajon@gajon.org>
  • docs/model-api.txt

     
    991991
    992992    =======================  ============================================================
    993993
     994Extra fields on many-to-many relationships
     995~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     996
     997When you're only dealing with mixing and matching pizzas and toppings, a standard
     998``ManyToManyField`` works great.  For many situations, however, some extra
     999fields are required on the relationships between models.  For situations like
     1000this, Django allows for the specification of an intermediary many-to-many model.
     1001To enable this functionality, simply specify a ``through`` keyword argument onto
     1002the ``ManyToManyField``.  This is best illustrated with an example::
     1003
     1004    class Person(models.Model):
     1005        # ...
     1006        name = models.CharField(max_length=128)
     1007
     1008        def __unicode__(self):
     1009            return self.name
     1010
     1011    class Group(models.Model):
     1012        # ...
     1013        name = models.CharField(max_length=128)
     1014        members = models.ManyToManyField(Person, through='Membership')
     1015
     1016        def __unicode__(self):
     1017            return self.name
     1018
     1019    class Membership(models.Model):
     1020        person = models.ForeignKey(Person)
     1021        group = models.ForeignKey(Group)
     1022        date_joined = models.DateTimeField()
     1023        invite_reason = models.CharField(max_length=64)
     1024
     1025.. note::
     1026
     1027    The intermediary model must provide foreign keys to both of the models in
     1028    the relation.  This explicit declaration makes it clear how two models are
     1029    related.  More importantly, perhaps, these foreign keys also provide access
     1030    to the intermediary model from either of the other models in the relation
     1031    (as with any other foreign key).
     1032
     1033Now that you have set up your ``ManyToManyField`` to use your intermediary
     1034model (Membership, in this case), you're ready to use the convenience methods
     1035provided by that ``ManyToManyField``.  Here's an example of how you can query
     1036for and use these models::
     1037   
     1038    >>> ringo = Person.objects.create(name="Ringo Starr")
     1039    >>> paul = Person.objects.create(name="Paul McCartney")
     1040    >>> beatles = Group.objects.create(name="The Beatles")
     1041    >>> m1 = Membership.objects.create(person=ringo, group=beatles,
     1042    ...     date_joined=datetime(1962, 8, 16),
     1043    ...     invite_reason= "Needed a new drummer.")
     1044    >>> beatles.members.all()
     1045    [<Person: Ringo Starr>]
     1046    >>> ringo.group_set.all()
     1047    [<Group: The Beatles>]
     1048    >>> m2 = Membership.objects.create(person=paul, group=beatles,
     1049    ...     date_joined=datetime(1960, 8, 1),
     1050    ...     invite_reason= "Wanted to form a band.")
     1051    >>> beatles.members.all()
     1052    [<Person: Ringo Starr>, <Person: Paul McCartney>]
     1053
     1054As you can see, creating ``Membership`` objects automatically adds the
     1055``Person`` objects to the ``beatles.members`` queryset.  This means that you
     1056can do anything that you would do on a normal queryset, like ``filter`` or
     1057``exclude``.
     1058
     1059.. note::
     1060
     1061    As soon as an intermediary model is specified, the ``add`` and 
     1062    ``remove`` methods become unavailable on the descriptors added by the
     1063    ``ManyToManyField``.  For example, something like 
     1064    ``beatles.members.add(paul)`` will no longer work.
     1065
     1066For more examples and ideas on how to work with intermediary models, 
     1067`see the tests`_.
     1068
     1069.. _`see the tests`: http://code.djangoproject.com/browser/django/trunk/tests/modeltests/m2m_through/models.py
     1070
    9941071One-to-one relationships
    9951072~~~~~~~~~~~~~~~~~~~~~~~~
    9961073
Back to Top