Ticket #6095: 6095-nfadmin.diff

File 6095-nfadmin.diff, 26.8 KB (added by floguy, 7 years ago)

Updated patch for newforms-admin, with the added bonus of fixing the integration problem with the admin. Now when a ManyToManyField specifies an intermediary model, it does not display the field in the admin.

  • AUTHORS

    diff --git a/AUTHORS b/AUTHORS
    index ea898de..6278a73 100644
    a b answer newbie questions, and generally made Django that much better: 
    134134    Afonso Fernández Nogueira <fonzzo.django@gmail.com>
    135135    Matthew Flanagan <http://wadofstuff.blogspot.com>
    136136    Eric Floehr <eric@intellovations.com>
     137    Eric Florenzano <floguy@gmail.com>
    137138    Vincent Foley <vfoleybourgon@yahoo.ca>
    138139    Rudolph Froger <rfroger@estrate.nl>
    139140    Jorge Gajon <gajon@gajon.org>
  • django/contrib/admin/options.py

    diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
    index ec32c89..6f010e7 100644
    a b class BaseModelAdmin(object): 
    172172                kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel)
    173173            else:
    174174                if isinstance(db_field, models.ManyToManyField):
    175                     if db_field.name in self.raw_id_fields:
     175                    # If it uses an intermediary model, don't show field in admin.
     176                    if db_field.rel.through != None:
     177                        return None
     178                    elif db_field.name in self.raw_id_fields:
    176179                        kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel)
    177180                        kwargs['help_text'] = ''
    178181                    elif db_field.name in (self.filter_vertical + self.filter_horizontal):
  • django/core/management/sql.py

    diff --git a/django/core/management/sql.py b/django/core/management/sql.py
    index 15bffce..5430dea 100644
    a b def many_to_many_sql_for_model(model, style): 
    352352    qn = connection.ops.quote_name
    353353    inline_references = connection.features.inline_fk_references
    354354    for f in opts.many_to_many:
    355         if not isinstance(f.rel, generic.GenericRel):
     355        if not isinstance(f.rel, generic.GenericRel) and f.creates_table:
    356356            tablespace = f.db_tablespace or opts.db_tablespace
    357357            if tablespace and connection.features.supports_tablespaces and connection.features.autoindexes_primary_keys:
    358358                tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace, inline=True)
  • django/core/management/validation.py

    diff --git a/django/core/management/validation.py b/django/core/management/validation.py
    index 6e8e6d0..241563e 100644
    a b def get_validation_errors(outfile, app=None): 
    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 = []
     106
    105107        for i, f in enumerate(opts.many_to_many):
    106108            # Check to see if the related m2m field will clash with any
    107109            # existing fields, m2m fields, m2m related objects or related objects
    def get_validation_errors(outfile, app=None): 
    111113                # so skip the next section
    112114                if isinstance(f.rel.to, (str, unicode)):
    113115                    continue
     116            if hasattr(f.rel, 'through') and f.rel.through != 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 == None:
     122                    e.add(opts, "%s has a manually-defined m2m relationship through a 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 relationships through the same model (%s), which is not possible.  Please use a 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 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 relationship through a 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))
    114138
    115139            rel_opts = f.rel.to._meta
    116140            rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
  • django/db/models/fields/related.py

    diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
    index 2bd31c9..86215e4 100644
    a b from django.core import validators 
    1010from django import oldforms
    1111from django import newforms as forms
    1212from django.dispatch import dispatcher
     13from new import instancemethod
    1314
    1415try:
    1516    set
    class ForeignRelatedObjectsDescriptor(object): 
    262263            manager.clear()
    263264        manager.add(*value)
    264265
    265 def create_many_related_manager(superclass):
     266def create_many_related_manager(superclass, through=False):
    266267    """Creates a manager that subclasses 'superclass' (which is a Manager)
    267268    and adds behavior for many-to-many related objects."""
    268269    class ManyRelatedManager(superclass):
    def create_many_related_manager(superclass): 
    276277            self.join_table = join_table
    277278            self.source_col_name = source_col_name
    278279            self.target_col_name = target_col_name
     280            self.through = through
    279281            self._pk_val = self.instance._get_pk_val()
    280282            if self._pk_val is None:
    281283                raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % model)
    def create_many_related_manager(superclass): 
    283285        def get_query_set(self):
    284286            return superclass.get_query_set(self).filter(**(self.core_filters))
    285287
    286         def add(self, *objs):
    287             self._add_items(self.source_col_name, self.target_col_name, *objs)
    288 
    289             # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
    290             if self.symmetrical:
    291                 self._add_items(self.target_col_name, self.source_col_name, *objs)
    292         add.alters_data = True
    293 
    294         def remove(self, *objs):
    295             self._remove_items(self.source_col_name, self.target_col_name, *objs)
    296 
    297             # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table
    298             if self.symmetrical:
    299                 self._remove_items(self.target_col_name, self.source_col_name, *objs)
    300         remove.alters_data = True
    301 
    302288        def clear(self):
    303289            self._clear_items(self.source_col_name)
    304290
    def create_many_related_manager(superclass): 
    375361                [self._pk_val])
    376362            transaction.commit_unless_managed()
    377363
     364    def add(self, *objs):
     365        self._add_items(self.source_col_name, self.target_col_name, *objs)
     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
     371    def remove(self, *objs):
     372        self._remove_items(self.source_col_name, self.target_col_name, *objs)
     373        # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table
     374        if self.symmetrical:
     375            self._remove_items(self.target_col_name, self.source_col_name, *objs)
     376    remove.alters_data = True
     377
     378    if not through:
     379        ManyRelatedManager.add = instancemethod(add, None, ManyRelatedManager)
     380        ManyRelatedManager.remove = instancemethod(remove, None, ManyRelatedManager)
     381
    378382    return ManyRelatedManager
    379383
    380384class ManyRelatedObjectsDescriptor(object):
    class ManyRelatedObjectsDescriptor(object): 
    395399        # model's default manager.
    396400        rel_model = self.related.model
    397401        superclass = rel_model._default_manager.__class__
    398         RelatedManager = create_many_related_manager(superclass)
     402        RelatedManager = create_many_related_manager(superclass, self.related.field.rel.through)
    399403
    400404        qn = connection.ops.quote_name
    401405        manager = RelatedManager(
    class ManyRelatedObjectsDescriptor(object): 
    414418        if instance is None:
    415419            raise AttributeError, "Manager must be accessed via instance"
    416420
     421        through = getattr(self.related.field.rel, 'through', None)
     422        if through:
     423            raise AttributeError, "Cannot set values on a ManyToManyField which specifies a through model.  Use %s's Manager instead." % through
     424       
    417425        manager = self.__get__(instance)
    418426        manager.clear()
    419427        manager.add(*value)
    class ReverseManyRelatedObjectsDescriptor(object): 
    436444        # model's default manager.
    437445        rel_model=self.field.rel.to
    438446        superclass = rel_model._default_manager.__class__
    439         RelatedManager = create_many_related_manager(superclass)
     447        RelatedManager = create_many_related_manager(superclass, self.field.rel.through)
    440448
    441449        qn = connection.ops.quote_name
    442450        manager = RelatedManager(
    class ReverseManyRelatedObjectsDescriptor(object): 
    455463        if instance is None:
    456464            raise AttributeError, "Manager must be accessed via instance"
    457465
     466        through = getattr(self.field.rel, 'through', None)
     467        if through:
     468            raise AttributeError, "Cannot set values on a ManyToManyField which specifies a through model.  Use %s's Manager instead." % through
     469
    458470        manager = self.__get__(instance)
    459471        manager.clear()
    460472        manager.add(*value)
    class ManyToManyField(RelatedField, Field): 
    633645            num_in_admin=kwargs.pop('num_in_admin', 0),
    634646            related_name=kwargs.pop('related_name', None),
    635647            limit_choices_to=kwargs.pop('limit_choices_to', None),
    636             symmetrical=kwargs.pop('symmetrical', True))
     648            symmetrical=kwargs.pop('symmetrical', True),
     649            through=kwargs.pop('through', None))
    637650        self.db_table = kwargs.pop('db_table', None)
     651        if kwargs['rel'].through:
     652            self.creates_table = False
     653            assert not self.db_table, "Cannot specify a db_table if an intermediary model is used."
     654        else:
     655            self.creates_table = True
    638656        Field.__init__(self, **kwargs)
    639657
    640658        msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.')
    class ManyToManyField(RelatedField, Field): 
    649667
    650668    def _get_m2m_db_table(self, opts):
    651669        "Function that can be curried to provide the m2m table name for this relation"
    652         if self.db_table:
     670        if self.rel.through != None:
     671            return get_model(opts.app_label, self.rel.through)._meta.db_table
     672        elif self.db_table:
    653673            return self.db_table
    654674        else:
    655675            return '%s_%s' % (opts.db_table, self.name)
    class ManyToManyField(RelatedField, Field): 
    657677    def _get_m2m_column_name(self, related):
    658678        "Function that can be curried to provide the source column name for the m2m table"
    659679        # If this is an m2m relation to self, avoid the inevitable name clash
    660         if related.model == related.parent_model:
     680        if self.rel.through != None:
     681            field = related.model._meta.get_related_object(self.rel.through).field
     682            attname, column = field.get_attname_column()
     683            return column
     684        elif related.model == related.parent_model:
    661685            return 'from_' + related.model._meta.object_name.lower() + '_id'
    662686        else:
    663687            return related.model._meta.object_name.lower() + '_id'
    class ManyToManyField(RelatedField, Field): 
    665689    def _get_m2m_reverse_name(self, related):
    666690        "Function that can be curried to provide the related column name for the m2m table"
    667691        # If this is an m2m relation to self, avoid the inevitable name clash
    668         if related.model == related.parent_model:
     692        if self.rel.through != None:
     693            field = related.parent_model._meta.get_related_object(self.rel.through).field
     694            attname, column = field.get_attname_column()
     695            return column
     696        elif related.model == related.parent_model:
    669697            return 'to_' + related.parent_model._meta.object_name.lower() + '_id'
    670698        else:
    671699            return related.parent_model._meta.object_name.lower() + '_id'
    class OneToOneRel(ManyToOneRel): 
    780808
    781809class ManyToManyRel(object):
    782810    def __init__(self, to, num_in_admin=0, related_name=None,
    783         limit_choices_to=None, symmetrical=True):
     811        limit_choices_to=None, symmetrical=True, through=None):
    784812        self.to = to
    785813        self.num_in_admin = num_in_admin
    786814        self.related_name = related_name
    class ManyToManyRel(object): 
    790818        self.edit_inline = False
    791819        self.symmetrical = symmetrical
    792820        self.multiple = True
     821        self.through = through
  • django/db/models/options.py

    diff --git a/django/db/models/options.py b/django/db/models/options.py
    index c0f36d9..00ce771 100644
    a b from django.conf import settings 
    22from django.db.models.related import RelatedObject
    33from django.db.models.fields.related import ManyToManyRel
    44from django.db.models.fields import AutoField, FieldDoesNotExist
    5 from django.db.models.loading import get_models, app_cache_ready
     5from django.db.models.loading import get_models, get_model, app_cache_ready
    66from django.db.models.query import orderlist2sql
    77from django.utils.translation import activate, deactivate_all, get_language, string_concat
    88from django.utils.encoding import force_unicode, smart_str
    class Options(object): 
    161161            follow = self.get_follow()
    162162        return [f for f in self.get_all_related_objects() if follow.get(f.name, None)]
    163163
     164    def get_related_object(self, from_model):
     165        "Gets the RelatedObject which links from from_model to this model."
     166        if isinstance(from_model, str):
     167            from_model = get_model(self.app_label, from_model)
     168        for related_object in self.get_all_related_objects():
     169            if related_object.model == from_model:
     170                return related_object
     171        return None
     172
    164173    def get_data_holders(self, follow=None):
    165174        if follow == None:
    166175            follow = self.get_follow()
  • tests/modeltests/invalid_models/models.py

    diff --git a/tests/modeltests/invalid_models/models.py b/tests/modeltests/invalid_models/models.py
    index 48e574a..ba74c4a 100644
    a b class Car(models.Model): 
    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)
     120
     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)
    113138
    114139model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute.
    115140invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute.
    invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_3' clashes wi 
    195220invalid_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'.
    196221invalid_models.missingrelations: 'rel2' has m2m relation with model Rel2, which has not been installed
    197222invalid_models.missingrelations: 'rel1' has relation with model Rel1, which has not been installed
     223invalid_models.group: Group has two manually-defined m2m relationships through the same model (Membership), which is not possible.  Please use a field on your intermediary model instead.
     224invalid_models.missingmanualm2mmodel: missing_m2m has a manually-defined m2m relationship through a model (MissingM2MModel) which does not exist.
     225invalid_models.grouptwo: primary has a manually-defined m2m relationship through a model (Membership) which does not have foreign keys to Person and GroupTwo
     226invalid_models.grouptwo: secondary has a manually-defined m2m relationship through a model (MembershipMissingFK) which does not have foreign keys to Group and GroupTwo
    198227"""
  • new file tests/modeltests/m2m_manual/models.py

    diff --git a/tests/modeltests/m2m_manual/__init__.py b/tests/modeltests/m2m_manual/__init__.py
    new file mode 100644
    index 0000000..e69de29
    diff --git a/tests/modeltests/m2m_manual/models.py b/tests/modeltests/m2m_manual/models.py
    new file mode 100644
    index 0000000..c79d0c4
    - +  
     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
     46__test__ = {'API_TESTS':"""
     47>>> from datetime import datetime
     48
     49### Creation and Saving Tests ###
     50
     51>>> bob = Person.objects.create(name = 'Bob')
     52>>> jim = Person.objects.create(name = 'Jim')
     53>>> jane = Person.objects.create(name = 'Jane')
     54>>> rock = Group.objects.create(name = 'Rock')
     55>>> roll = Group.objects.create(name = 'Roll')
     56
     57# We start out by making sure that the Group 'rock' has no members.
     58>>> rock.members.all()
     59[]
     60
     61# To make Jim a member of Group Rock, simply create a Membership object.
     62>>> m1 = Membership.objects.create(person = jim, group = rock)
     63
     64# We can do the same for Jane and Rock.
     65>>> m2 = Membership.objects.create(person = jane, group = rock)
     66
     67# Let's check to make sure that it worked.  Jane and Jim should be members of Rock.
     68>>> rock.members.all()
     69[<Person: Jim>, <Person: Jane>]
     70
     71# Now we can add a bunch more Membership objects to test with.
     72>>> m3 = Membership.objects.create(person = bob, group = roll)
     73>>> m4 = Membership.objects.create(person = jim, group = roll)
     74>>> m5 = Membership.objects.create(person = jane, group = roll)
     75
     76# We can get Jim's Group membership as with any ForeignKey.
     77>>> jim.group_set.all()
     78[<Group: Rock>, <Group: Roll>]
     79
     80# Querying the intermediary model works like normal. 
     81# In this case we get Jane's membership to Rock.
     82>>> m = Membership.objects.get(person = jane, group = rock)
     83>>> m
     84<Membership: Jane is a member of Rock>
     85
     86# Now we set some date_joined dates for further testing.
     87>>> m2.invite_reason = "She was just awesome."
     88>>> m2.date_joined = datetime(2006, 1, 1)
     89>>> m2.save()
     90
     91>>> m5.date_joined = datetime(2004, 1, 1)
     92>>> m5.save()
     93
     94>>> m3.date_joined = datetime(2004, 1, 1)
     95>>> m3.save()
     96
     97# It's not only get that works.  Filter works like normal as well.
     98>>> Membership.objects.filter(person = jim)
     99[<Membership: Jim is a member of Rock>, <Membership: Jim is a member of Roll>]
     100
     101
     102### Forward Descriptors Tests ###
     103
     104# Due to complications with adding via an intermediary model, the add method is
     105# not provided.
     106>>> rock.members.add(bob)
     107Traceback (most recent call last):
     108...
     109AttributeError: 'ManyRelatedManager' object has no attribute 'add'
     110
     111# Remove has similar complications, and is not provided either.
     112>>> rock.members.remove(jim)
     113Traceback (most recent call last):
     114...
     115AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
     116
     117# Here we back up the list of all members of Rock.
     118>>> backup = list(rock.members.all())
     119
     120# ...and we verify that it has worked.
     121>>> backup
     122[<Person: Jim>, <Person: Jane>]
     123
     124# The clear function should still work.
     125>>> rock.members.clear()
     126
     127# Now there will be no members of Rock.
     128>>> rock.members.all()
     129[]
     130
     131# Assignment should not work with models specifying a through model for many of
     132# the same reasons as adding.
     133>>> rock.members = backup
     134Traceback (most recent call last):
     135...
     136AttributeError: Cannot set values on a ManyToManyField which specifies a through model.  Use Membership's Manager instead.
     137
     138# Let's re-save those instances that we've cleared.
     139>>> m1.save()
     140>>> m2.save()
     141
     142# Verifying that those instances were re-saved successfully.
     143>>> rock.members.all()
     144[<Person: Jim>, <Person: Jane>]
     145
     146
     147### Reverse Descriptors Tests ###
     148
     149# Due to complications with adding via an intermediary model, the add method is
     150# not provided.
     151>>> bob.group_set.add(rock)
     152Traceback (most recent call last):
     153...
     154AttributeError: 'ManyRelatedManager' object has no attribute 'add'
     155
     156# Remove has similar complications, and is not provided either.
     157>>> jim.group_set.remove(rock)
     158Traceback (most recent call last):
     159...
     160AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
     161
     162# Here we back up the list of all of Jim's groups.
     163>>> backup = list(jim.group_set.all())
     164>>> backup
     165[<Group: Rock>, <Group: Roll>]
     166
     167# The clear function should still work.
     168>>> jim.group_set.clear()
     169
     170# Now Jim will be in no groups.
     171>>> jim.group_set.all()
     172[]
     173
     174# Assignment should not work with models specifying a through model for many of
     175# the same reasons as adding.
     176>>> jim.group_set = backup
     177Traceback (most recent call last):
     178...
     179AttributeError: Cannot set values on a ManyToManyField which specifies a through model.  Use Membership's Manager instead.
     180
     181# Let's re-save those instances that we've cleared.
     182>>> m1.save()
     183>>> m4.save()
     184
     185# Verifying that those instances were re-saved successfully.
     186>>> jim.group_set.all()
     187[<Group: Rock>, <Group: Roll>]
     188
     189### Custom Tests ###
     190
     191# Let's see if we can query through our second relationship.
     192>>> rock.custom_members.all()
     193[]
     194
     195# We can query in the opposite direction as well.
     196>>> bob.custom.all()
     197[]
     198
     199# Let's create some membership objects in this custom relationship.
     200>>> cm1 = CustomMembership.objects.create(person = bob, group = rock)
     201>>> cm2 = CustomMembership.objects.create(person = jim, group = rock)
     202
     203# If we get the number of people in Rock, it should be both Bob and Jim.
     204>>> rock.custom_members.all()
     205[<Person: Bob>, <Person: Jim>]
     206
     207# Bob should only be in one custom group.
     208>>> bob.custom.all()
     209[<Group: Rock>]
     210
     211# Let's make sure our new descriptors don't conflict with the FK related_name.
     212>>> bob.custom_person_related_name.all()
     213[<CustomMembership: Bob is a member of Rock>]
     214
     215### QUERY TESTS ###
     216
     217# We can query for the related model by using its attribute name (members, in
     218# this case).
     219>>> Group.objects.filter(members__name='Bob')
     220[<Group: Roll>]
     221
     222# To query through the intermediary model, we specify its model name.
     223# In this case, membership.
     224>>> Group.objects.filter(membership__invite_reason = "She was just awesome.")
     225[<Group: Rock>]
     226
     227# If we want to query in the reverse direction by the related model, use its
     228# model name (group, in this case).
     229>>> Person.objects.filter(group__name="Rock")
     230[<Person: Jim>, <Person: Jane>]
     231
     232# If the m2m field has specified a related_name, using that will work.
     233>>> Person.objects.filter(custom__name="Rock")
     234[<Person: Bob>, <Person: Jim>]
     235
     236# To query through the intermediary model in the reverse direction, we again
     237# specify its model name (membership, in this case).
     238>>> Person.objects.filter(membership__invite_reason = "She was just awesome.")
     239[<Person: Jane>]
     240
     241# Let's see all of the groups that Jane joined after 1 Jan 2005:
     242>>> Group.objects.filter(membership__date_joined__gt = datetime(2005, 1, 1),
     243... membership__person = jane)
     244[<Group: Rock>]
     245
     246# Queries also work in the reverse direction: Now let's see all of the people
     247# that have joined Rock since 1 Jan 2005:
     248>>> Person.objects.filter(membership__date_joined__gt = datetime(2005, 1, 1),
     249... membership__group = rock)
     250[<Person: Jim>, <Person: Jane>]
     251
     252# Conceivably, queries through membership could return correct, but non-unique
     253# querysets.  To demonstrate this, we query for all people who have joined a
     254# group after 2004:
     255>>> Person.objects.filter(membership__date_joined__gt = datetime(2004, 1, 1))
     256[<Person: Jim>, <Person: Jim>, <Person: Jane>]
     257
     258# Jim showed up twice, because he joined two groups ('Rock', and 'Roll'):
     259>>> [(m.person.name, m.group.name) for m in
     260... Membership.objects.filter(date_joined__gt = datetime(2004, 1, 1))]
     261[(u'Jim', u'Rock'), (u'Jane', u'Rock'), (u'Jim', u'Roll')]
     262
     263# QuerySet's distinct() method can correct this problem.
     264>>> Person.objects.filter(membership__date_joined__gt = datetime(2004, 1, 1)).distinct()
     265[<Person: Jim>, <Person: Jane>]
     266"""}
     267 No newline at end of file
Back to Top