Ticket #6095: 6095-r8068.diff

File 6095-r8068.diff, 34.9 KB (added by Russell Keith-Magee, 16 years ago)

Updated patch for r8068; some minor revisions

  • 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.utils.translation import activate, deactivate_all, get_language, string_concat
    1515from django.utils.encoding import force_unicode, smart_str
    1616from django.utils.datastructures import SortedDict
     
    401401            follow = self.get_follow()
    402402        return [f for f in self.get_all_related_objects() if follow.get(f.name, None)]
    403403
     404    def get_related_object(self, from_model, self_ref=False):
     405        "Gets the RelatedObject which links from from_model to this model."
     406        for related_object in self.get_all_related_objects():
     407            if related_object.model == from_model:
     408                if self_ref:
     409                    self_ref = False
     410                else:
     411                    return related_object
     412        return None
     413
    404414    def get_data_holders(self, follow=None):
    405415        if follow == None:
    406416            follow = self.get_follow()
  • django/db/models/fields/related.py

     
    339339            manager.clear()
    340340        manager.add(*value)
    341341
    342 def create_many_related_manager(superclass):
     342def create_many_related_manager(superclass, through=False):
    343343    """Creates a manager that subclasses 'superclass' (which is a Manager)
    344344    and adds behavior for many-to-many related objects."""
    345345    class ManyRelatedManager(superclass):
     
    353353            self.join_table = join_table
    354354            self.source_col_name = source_col_name
    355355            self.target_col_name = target_col_name
     356            self.through = through
    356357            self._pk_val = self.instance._get_pk_val()
    357358            if self._pk_val is None:
    358359                raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__)
     
    360361        def get_query_set(self):
    361362            return superclass.get_query_set(self).filter(**(self.core_filters))
    362363
    363         def add(self, *objs):
    364             self._add_items(self.source_col_name, self.target_col_name, *objs)
     364        # If the ManyToMany relation has an intermediary model,
     365        # the add and remove methods do not exist.
     366        if through is None:
     367            def add(self, *objs):
     368                self._add_items(self.source_col_name, self.target_col_name, *objs)
    365369
    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
     370                # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
     371                if self.symmetrical:
     372                    self._add_items(self.target_col_name, self.source_col_name, *objs)
     373            add.alters_data = True
    370374
    371         def remove(self, *objs):
    372             self._remove_items(self.source_col_name, self.target_col_name, *objs)
     375            def remove(self, *objs):
     376                self._remove_items(self.source_col_name, self.target_col_name, *objs)
    373377
    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
     378                # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table
     379                if self.symmetrical:
     380                    self._remove_items(self.target_col_name, self.source_col_name, *objs)
     381            remove.alters_data = True
    378382
    379383        def clear(self):
    380384            self._clear_items(self.source_col_name)
     
    385389        clear.alters_data = True
    386390
    387391        def create(self, **kwargs):
     392            # This check needs to be done here, since we can't later remove this
     393            # from the method lookup table, as we do with add and remove.
     394            if through is not None:
     395                raise AttributeError, "Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through
    388396            new_obj = self.model(**kwargs)
    389397            new_obj.save()
    390398            self.add(new_obj)
     
    472480        # model's default manager.
    473481        rel_model = self.related.model
    474482        superclass = rel_model._default_manager.__class__
    475         RelatedManager = create_many_related_manager(superclass)
     483        RelatedManager = create_many_related_manager(superclass, self.related.field.rel.through)
    476484
    477485        qn = connection.ops.quote_name
    478486        manager = RelatedManager(
     
    491499        if instance is None:
    492500            raise AttributeError, "Manager must be accessed via instance"
    493501
     502        through = getattr(self.related.field.rel, 'through', None)
     503        if through is not None:
     504            raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through
     505
    494506        manager = self.__get__(instance)
    495507        manager.clear()
    496508        manager.add(*value)
     
    513525        # model's default manager.
    514526        rel_model=self.field.rel.to
    515527        superclass = rel_model._default_manager.__class__
    516         RelatedManager = create_many_related_manager(superclass)
     528        RelatedManager = create_many_related_manager(superclass, self.field.rel.through)
    517529
    518530        qn = connection.ops.quote_name
    519531        manager = RelatedManager(
     
    532544        if instance is None:
    533545            raise AttributeError, "Manager must be accessed via instance"
    534546
     547        through = getattr(self.field.rel, 'through', None)
     548        if through is not None:
     549            raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model.  Use %s's Manager instead." % through
     550
    535551        manager = self.__get__(instance)
    536552        manager.clear()
    537553        manager.add(*value)
     
    583599
    584600class ManyToManyRel(object):
    585601    def __init__(self, to, num_in_admin=0, related_name=None,
    586         limit_choices_to=None, symmetrical=True):
     602        limit_choices_to=None, symmetrical=True, through=None):
    587603        self.to = to
    588604        self.num_in_admin = num_in_admin
    589605        self.related_name = related_name
     
    593609        self.edit_inline = False
    594610        self.symmetrical = symmetrical
    595611        self.multiple = True
     612        self.through = through
     613        self.through_app_label = None
     614        self._model_cache = None
    596615
     616    def _get_through_model(self):
     617        if self._model_cache:
     618            return self._model_cache
     619        if self.through and self.through_app_label:
     620            self._model_cache = get_model(self.through_app_label, self.through)
     621            return self._model_cache
     622        return None
     623    through_model = property(_get_through_model)
     624
     625
    597626class ForeignKey(RelatedField, Field):
    598627    empty_strings_allowed = False
    599628    def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs):
     
    722751            num_in_admin=kwargs.pop('num_in_admin', 0),
    723752            related_name=kwargs.pop('related_name', None),
    724753            limit_choices_to=kwargs.pop('limit_choices_to', None),
    725             symmetrical=kwargs.pop('symmetrical', True))
     754            symmetrical=kwargs.pop('symmetrical', True),
     755            through=kwargs.pop('through', None))
     756           
    726757        self.db_table = kwargs.pop('db_table', None)
     758        if kwargs['rel'].through is not None:
     759            self.creates_table = False
     760            assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used."
     761        else:
     762            self.creates_table = True
     763
    727764        Field.__init__(self, **kwargs)
    728765
    729766        msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.')
     
    738775
    739776    def _get_m2m_db_table(self, opts):
    740777        "Function that can be curried to provide the m2m table name for this relation"
    741         if self.db_table:
     778        if self.rel.through is not None:
     779            return self.rel.through_model._meta.db_table
     780        elif self.db_table:
    742781            return self.db_table
    743782        else:
    744783            return '%s_%s' % (opts.db_table, self.name)
    745784
    746785    def _get_m2m_column_name(self, related):
    747786        "Function that can be curried to provide the source column name for the m2m table"
     787        if self.rel.through is not None:
     788            field = related.model._meta.get_related_object(self.rel.through_model).field
     789            return field.column
    748790        # If this is an m2m relation to self, avoid the inevitable name clash
    749         if related.model == related.parent_model:
     791        elif related.model == related.parent_model:
    750792            return 'from_' + related.model._meta.object_name.lower() + '_id'
    751793        else:
    752794            return related.model._meta.object_name.lower() + '_id'
    753795
    754796    def _get_m2m_reverse_name(self, related):
    755797        "Function that can be curried to provide the related column name for the m2m table"
     798        if self.rel.through is not None:
     799            meta = related.parent_model._meta
     800            if self.parent == self.rel.to:
     801                related = meta.get_related_object(self.rel.through_model, self_ref=True)
     802            else:
     803                related = meta.get_related_object(self.rel.through_model)
     804            return related.field.column
    756805        # If this is an m2m relation to self, avoid the inevitable name clash
    757         if related.model == related.parent_model:
     806        elif related.model == related.parent_model:
    758807            return 'to_' + related.parent_model._meta.object_name.lower() + '_id'
    759808        else:
    760809            return related.parent_model._meta.object_name.lower() + '_id'
     
    797846
    798847        # Set up the accessor for the m2m table name for the relation
    799848        self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta)
     849       
     850        # Get a reference to the parent model
     851        self.parent = cls
     852       
     853        # Populate some necessary rel arguments so that cross-app relations
     854        # work correctly.
     855        if isinstance(self.rel.through, basestring):
     856            try:
     857                self.rel.through_app_label, self.rel.through = self.rel.through.split('.')
     858            except ValueError:
     859                self.rel.through_app_label = cls._meta.app_label
    800860
    801861        if isinstance(self.rel.to, basestring):
    802862            target = self.rel.to
  • 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                intermediary_model = None
     118                for model in models.get_models():
     119                    if model._meta.module_name == f.rel.through.lower():
     120                        intermediary_model = model
     121                if intermediary_model is None:
     122                    e.add(opts, "%s has a manually-defined m2m relation through model %s, which does not exist." % (f.name, f.rel.through))
     123                else:
     124                    signature = (f.rel.to, cls, intermediary_model)
     125                    if signature in seen_intermediary_signatures:
     126                        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))
     127                    else:
     128                        seen_intermediary_signatures.append(signature)
     129                    seen_related_fk, seen_this_fk = False, False
     130                    for field in intermediary_model._meta.fields:
     131                        if field.rel:
     132                            if not seen_related_fk and field.rel.to == f.rel.to:
     133                                seen_related_fk = True
     134                            elif field.rel.to == cls:
     135                                seen_this_fk = True
     136                    if not seen_related_fk or not seen_this_fk:
     137                        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))
     138           
    116139            rel_opts = f.rel.to._meta
    117140            rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
    118141            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

     
    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
     126class GroupTwo(models.Model):
     127    name = models.CharField(max_length=5)
     128    primary = models.ManyToManyField(Person, through="Membership")
     129    secondary = models.ManyToManyField(Group, through="MembershipMissingFK")
     130
     131class Membership(models.Model):
     132    person = models.ForeignKey(Person)
     133    group = models.ForeignKey(Group)
     134    not_default_or_null = models.CharField(max_length=5)
     135
     136class MembershipMissingFK(models.Model):
     137    person = models.ForeignKey(Person)
     138
     139class PersonSelfRefM2M(models.Model):
     140    name = models.CharField(max_length=5)
     141    friends = models.ManyToManyField('self', through="Relationship")
     142
     143class Relationship(models.Model):
     144    first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set")
     145    second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set")
     146    date_added = models.DateTimeField()
     147
    114148model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute.
    115149invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute.
    116150invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute.
     
    195229invalid_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'.
    196230invalid_models.missingrelations: 'rel2' has m2m relation with model Rel2, which has not been installed
    197231invalid_models.missingrelations: 'rel1' has relation with model Rel1, which has not been installed
     232invalid_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.
     233invalid_models.missingmanualm2mmodel: missing_m2m has a manually-defined m2m relation through model MissingM2MModel, which does not exist.
     234invalid_models.grouptwo: primary has a manually-defined m2m relation through model Membership, which does not have foreign keys to Person and GroupTwo
     235invalid_models.grouptwo: secondary has a manually-defined m2m relation through model MembershipMissingFK, which does not have foreign keys to Group and GroupTwo
    198236"""
  • 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,
     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 is a symmetrical relation, Tony's friend link is deleted as well.
     274>>> tony.friends.all()
     275[]
     276
     277
     278
     279### QUERY TESTS ###
     280
     281# We can query for the related model by using its attribute name (members, in
     282# this case).
     283>>> Group.objects.filter(members__name='Bob')
     284[<Group: Roll>]
     285
     286# To query through the intermediary model, we specify its model name.
     287# In this case, membership.
     288>>> Group.objects.filter(membership__invite_reason="She was just awesome.")
     289[<Group: Rock>]
     290
     291# If we want to query in the reverse direction by the related model, use its
     292# model name (group, in this case).
     293>>> Person.objects.filter(group__name="Rock")
     294[<Person: Jim>, <Person: Jane>]
     295
     296# If the m2m field has specified a related_name, using that will work.
     297>>> Person.objects.filter(custom__name="Rock")
     298[<Person: Bob>, <Person: Jim>]
     299
     300# To query through the intermediary model in the reverse direction, we again
     301# specify its model name (membership, in this case).
     302>>> Person.objects.filter(membership__invite_reason="She was just awesome.")
     303[<Person: Jane>]
     304
     305# Let's see all of the groups that Jane joined after 1 Jan 2005:
     306>>> Group.objects.filter(membership__date_joined__gt=datetime(2005, 1, 1), membership__person =jane)
     307[<Group: Rock>]
     308
     309# Queries also work in the reverse direction: Now let's see all of the people
     310# that have joined Rock since 1 Jan 2005:
     311>>> Person.objects.filter(membership__date_joined__gt=datetime(2005, 1, 1), membership__group=rock)
     312[<Person: Jim>, <Person: Jane>]
     313
     314# Conceivably, queries through membership could return correct, but non-unique
     315# querysets.  To demonstrate this, we query for all people who have joined a
     316# group after 2004:
     317>>> Person.objects.filter(membership__date_joined__gt=datetime(2004, 1, 1))
     318[<Person: Jim>, <Person: Jim>, <Person: Jane>]
     319
     320# Jim showed up twice, because he joined two groups ('Rock', and 'Roll'):
     321>>> [(m.person.name, m.group.name) for m in
     322... Membership.objects.filter(date_joined__gt=datetime(2004, 1, 1))]
     323[(u'Jim', u'Rock'), (u'Jane', u'Rock'), (u'Jim', u'Roll')]
     324
     325# QuerySet's distinct() method can correct this problem.
     326>>> Person.objects.filter(membership__date_joined__gt=datetime(2004, 1, 1)).distinct()
     327[<Person: Jim>, <Person: Jane>]
     328"""}
     329 No newline at end of file
  • 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

     
    942942
    943943    =======================  ============================================================
    944944
     945Extra fields on many-to-many relationships
     946~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     947
     948When you're only dealing with mixing and matching pizzas and toppings, a
     949standard ``ManyToManyField`` works great. However, sometimes you may want
     950to associated data with the relationship between two models.
     951
     952For example, consider the case of an application tracking the musical groups
     953which musicians belong to. There is a many-to-many relationship between a person
     954and the groups of which they are a member, so you could use a ManyToManyField
     955to represent this relationship. However, there is a lot of detail about the
     956membership that you might want to collect, such as the date at which the person
     957joined the group.
     958
     959For these situations, Django allows you to specify the model that will be used
     960to govern the many-to-many relationship. You can then put extra fields on the
     961intermediate model. The intermediate model is associated with the
     962``ManyToManyField`` by using the ``through`` argument to point the model that
     963will act as an intermediary. For our musician example, the code would look
     964something like this::
     965
     966    class Person(models.Model):
     967        # ...
     968        name = models.CharField(max_length=128)
     969
     970        def __unicode__(self):
     971            return self.name
     972
     973    class Group(models.Model):
     974        # ...
     975        name = models.CharField(max_length=128)
     976        members = models.ManyToManyField(Person, through='Membership')
     977
     978        def __unicode__(self):
     979            return self.name
     980
     981    class Membership(models.Model):
     982        person = models.ForeignKey(Person)
     983        group = models.ForeignKey(Group)
     984        date_joined = models.DateField()
     985        invite_reason = models.CharField(max_length=64)
     986
     987When you set up the intermediary model, you must explicitly specify foreign
     988keys to the models in ManyToMany relation. This explicit declaration makes
     989it clear how two models are related.
     990
     991Now that you have set up your ``ManyToManyField`` to use your intermediary
     992model (Membership, in this case), you're ready to use the convenience methods
     993provided by that ``ManyToManyField``.  Here's an example of how you can query
     994for and use these models::
     995   
     996    >>> ringo = Person.objects.create(name="Ringo Starr")
     997    >>> paul = Person.objects.create(name="Paul McCartney")
     998    >>> beatles = Group.objects.create(name="The Beatles")
     999    >>> m1 = Membership.objects.create(person=ringo, group=beatles,
     1000    ...     date_joined=date(1962, 8, 16),
     1001    ...     invite_reason= "Needed a new drummer.")
     1002    >>> beatles.members.all()
     1003    [<Person: Ringo Starr>]
     1004    >>> ringo.group_set.all()
     1005    [<Group: The Beatles>]
     1006    >>> m2 = Membership.objects.create(person=paul, group=beatles,
     1007    ...     date_joined=date(1960, 8, 1),
     1008    ...     invite_reason= "Wanted to form a band.")
     1009    >>> beatles.members.all()
     1010    [<Person: Ringo Starr>, <Person: Paul McCartney>]
     1011
     1012Unlike normal many-to-many fields, you *can't* use ``add``, ``create``,
     1013or assignment (i.e., ``beatles.members = [...]``) to create relationships::
     1014
     1015    # THIS WILL NOT WORK
     1016    >>> beatles.members.add(john)
     1017    # NEITHER WILL THIS
     1018    >>> beatles.members.create(name="George Harrison")
     1019    # AND NEITHER WILL THIS
     1020    >>> beatles.members = [john, paul, ringo, george]
     1021   
     1022Why? You can't just create a relationship between a Person and a Group - you
     1023need to specify all the detail for the relationship required by the
     1024Membership table. The simple ``add``, ``create`` and assignment calls
     1025don't provide a way to specify this extra detail. As a result, they are
     1026disabled for many-to-many relationships that use an intermediate model.
     1027The only way to create a many-to-many relationship with an intermediate table
     1028is to create instances of the intermediate model.
     1029
     1030The ``remove`` method is disabled for similar reasons. However, the
     1031``clear()`` method can be used to remove all many-to-many relationships
     1032for an instance::
     1033
     1034    # Beatles have broken up
     1035    >>> beatles.members.clear()
     1036
     1037Once you have established the many-to-many relationships by creating instances
     1038of your intermediate model, you can issue queries. You can query using the
     1039attributes of the many-to-many-related model::
     1040
     1041    # Find all the people in the Beatles whose name starts with 'Paul'
     1042    >>> beatles.objects.filter(person__name__startswith='Paul')
     1043    [<Person: Paul McCartney>]
     1044
     1045You can also query on the attributes of the intermediate model::
     1046
     1047    # Find all the members of the Beatles that joined after 1 Jan 1961
     1048    >>> beatles.objects.filter(membership__date_joined__gt=date(1961,1,1))
     1049    [<Person: Ringo Starr]
     1050   
    9451051One-to-one relationships
    9461052~~~~~~~~~~~~~~~~~~~~~~~~
    9471053
Back to Top