Ticket #6095: 6095-nfadmin.3.diff
File 6095-nfadmin.3.diff, 26.7 KB (added by , 17 years ago) |
---|
-
AUTHORS
diff --git a/AUTHORS b/AUTHORS index fe4755b..6622871 100644
a b answer newbie questions, and generally made Django that much better: 144 144 Afonso Fernández Nogueira <fonzzo.django@gmail.com> 145 145 Matthew Flanagan <http://wadofstuff.blogspot.com> 146 146 Eric Floehr <eric@intellovations.com> 147 Eric Florenzano <floguy@gmail.com> 147 148 Vincent Foley <vfoleybourgon@yahoo.ca> 148 149 Rudolph Froger <rfroger@estrate.nl> 149 150 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 29ce10a..671278c 100644
a b class BaseModelAdmin(object): 174 174 kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel) 175 175 else: 176 176 if isinstance(db_field, models.ManyToManyField): 177 if db_field.name in self.raw_id_fields: 177 # If it uses an intermediary model, don't show field in admin. 178 if db_field.rel.through != None: 179 return None 180 elif db_field.name in self.raw_id_fields: 178 181 kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel) 179 182 kwargs['help_text'] = '' 180 183 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 1ccb100..c3279c1 100644
a b def many_to_many_sql_for_model(model, style): 354 354 qn = connection.ops.quote_name 355 355 inline_references = connection.features.inline_fk_references 356 356 for f in opts.local_many_to_many: 357 if not isinstance(f.rel, generic.GenericRel) :357 if not isinstance(f.rel, generic.GenericRel) and f.creates_table: 358 358 tablespace = f.db_tablespace or opts.db_tablespace 359 359 if tablespace and connection.features.supports_tablespaces and connection.features.autoindexes_primary_keys: 360 360 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 45dc899..693bf65 100644
a b def get_validation_errors(outfile, app=None): 102 102 if r.get_accessor_name() == rel_query_name: 103 103 e.add(opts, "Reverse query name for field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) 104 104 105 seen_intermediary_signatures = [] 105 106 for i, f in enumerate(opts.local_many_to_many): 106 107 # Check to see if the related m2m field will clash with any 107 108 # existing fields, m2m fields, m2m related objects or related … … def get_validation_errors(outfile, app=None): 112 113 # so skip the next section 113 114 if isinstance(f.rel.to, (str, unicode)): 114 115 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)) 115 138 116 139 rel_opts = f.rel.to._meta 117 140 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 0278081..ec2ca03 100644
a b class ForeignRelatedObjectsDescriptor(object): 310 310 manager.clear() 311 311 manager.add(*value) 312 312 313 def create_many_related_manager(superclass ):313 def create_many_related_manager(superclass, through=False): 314 314 """Creates a manager that subclasses 'superclass' (which is a Manager) 315 315 and adds behavior for many-to-many related objects.""" 316 316 class ManyRelatedManager(superclass): … … def create_many_related_manager(superclass): 324 324 self.join_table = join_table 325 325 self.source_col_name = source_col_name 326 326 self.target_col_name = target_col_name 327 self.through = through 327 328 self._pk_val = self.instance._get_pk_val() 328 329 if self._pk_val is None: 329 330 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): 333 334 334 335 def add(self, *objs): 335 336 self._add_items(self.source_col_name, self.target_col_name, *objs) 336 337 337 # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table 338 338 if self.symmetrical: 339 339 self._add_items(self.target_col_name, self.source_col_name, *objs) 340 340 add.alters_data = True 341 341 342 342 def remove(self, *objs): 343 343 self._remove_items(self.source_col_name, self.target_col_name, *objs) 344 345 344 # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table 346 345 if self.symmetrical: 347 346 self._remove_items(self.target_col_name, self.source_col_name, *objs) … … def create_many_related_manager(superclass): 356 355 clear.alters_data = True 357 356 358 357 def create(self, **kwargs): 358 if through: 359 raise AttributeError, "'ManyRelatedManager' object has no attribute 'create'" 359 360 new_obj = self.model(**kwargs) 360 361 new_obj.save() 361 362 self.add(new_obj) … … def create_many_related_manager(superclass): 423 424 [self._pk_val]) 424 425 transaction.commit_unless_managed() 425 426 427 # If it's an intermediary model, detach the add, remove, and create methods 428 # from this ManyRelatedManager. 429 if through: 430 del ManyRelatedManager.add 431 del ManyRelatedManager.remove 432 426 433 return ManyRelatedManager 427 434 428 435 class ManyRelatedObjectsDescriptor(object): … … class ManyRelatedObjectsDescriptor(object): 443 450 # model's default manager. 444 451 rel_model = self.related.model 445 452 superclass = rel_model._default_manager.__class__ 446 RelatedManager = create_many_related_manager(superclass )453 RelatedManager = create_many_related_manager(superclass, self.related.field.rel.through) 447 454 448 455 qn = connection.ops.quote_name 449 456 manager = RelatedManager( … … class ManyRelatedObjectsDescriptor(object): 462 469 if instance is None: 463 470 raise AttributeError, "Manager must be accessed via instance" 464 471 472 through = getattr(self.related.field.rel, 'through', None) 473 if through: 474 raise AttributeError, "Cannot set values on a ManyToManyField which specifies a through model. Use %s's Manager instead." % through 475 465 476 manager = self.__get__(instance) 466 477 manager.clear() 467 478 manager.add(*value) … … class ReverseManyRelatedObjectsDescriptor(object): 484 495 # model's default manager. 485 496 rel_model=self.field.rel.to 486 497 superclass = rel_model._default_manager.__class__ 487 RelatedManager = create_many_related_manager(superclass )498 RelatedManager = create_many_related_manager(superclass, self.field.rel.through) 488 499 489 500 qn = connection.ops.quote_name 490 501 manager = RelatedManager( … … class ReverseManyRelatedObjectsDescriptor(object): 503 514 if instance is None: 504 515 raise AttributeError, "Manager must be accessed via instance" 505 516 517 through = getattr(self.field.rel, 'through', None) 518 if through: 519 raise AttributeError, "Cannot set values on a ManyToManyField which specifies a through model. Use %s's Manager instead." % through 520 506 521 manager = self.__get__(instance) 507 522 manager.clear() 508 523 manager.add(*value) … … class OneToOneRel(ManyToOneRel): 554 569 555 570 class ManyToManyRel(object): 556 571 def __init__(self, to, num_in_admin=0, related_name=None, 557 limit_choices_to=None, symmetrical=True ):572 limit_choices_to=None, symmetrical=True, through=None): 558 573 self.to = to 559 574 self.num_in_admin = num_in_admin 560 575 self.related_name = related_name … … class ManyToManyRel(object): 564 579 self.edit_inline = False 565 580 self.symmetrical = symmetrical 566 581 self.multiple = True 582 self.through = through 567 583 568 584 class ForeignKey(RelatedField, Field): 569 585 empty_strings_allowed = False … … class ManyToManyField(RelatedField, Field): 695 711 num_in_admin=kwargs.pop('num_in_admin', 0), 696 712 related_name=kwargs.pop('related_name', None), 697 713 limit_choices_to=kwargs.pop('limit_choices_to', None), 698 symmetrical=kwargs.pop('symmetrical', True)) 714 symmetrical=kwargs.pop('symmetrical', True), 715 through=kwargs.pop('through', None)) 699 716 self.db_table = kwargs.pop('db_table', None) 717 if kwargs['rel'].through: 718 self.creates_table = False 719 assert not self.db_table, "Cannot specify a db_table if an intermediary model is used." 720 else: 721 self.creates_table = True 700 722 Field.__init__(self, **kwargs) 701 723 702 724 msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.') … … class ManyToManyField(RelatedField, Field): 711 733 712 734 def _get_m2m_db_table(self, opts): 713 735 "Function that can be curried to provide the m2m table name for this relation" 714 if self.db_table: 736 if self.rel.through != None: 737 return get_model(opts.app_label, self.rel.through)._meta.db_table 738 elif self.db_table: 715 739 return self.db_table 716 740 else: 717 741 return '%s_%s' % (opts.db_table, self.name) … … class ManyToManyField(RelatedField, Field): 719 743 def _get_m2m_column_name(self, related): 720 744 "Function that can be curried to provide the source column name for the m2m table" 721 745 # If this is an m2m relation to self, avoid the inevitable name clash 722 if related.model == related.parent_model: 746 if self.rel.through != None: 747 field = related.model._meta.get_related_object(self.rel.through).field 748 attname, column = field.get_attname_column() 749 return column 750 elif related.model == related.parent_model: 723 751 return 'from_' + related.model._meta.object_name.lower() + '_id' 724 752 else: 725 753 return related.model._meta.object_name.lower() + '_id' … … class ManyToManyField(RelatedField, Field): 727 755 def _get_m2m_reverse_name(self, related): 728 756 "Function that can be curried to provide the related column name for the m2m table" 729 757 # If this is an m2m relation to self, avoid the inevitable name clash 730 if related.model == related.parent_model: 758 if self.rel.through != None: 759 field = related.parent_model._meta.get_related_object(self.rel.through).field 760 attname, column = field.get_attname_column() 761 return column 762 elif related.model == related.parent_model: 731 763 return 'to_' + related.parent_model._meta.object_name.lower() + '_id' 732 764 else: 733 765 return related.parent_model._meta.object_name.lower() + '_id' -
django/db/models/options.py
diff --git a/django/db/models/options.py b/django/db/models/options.py index 3948a5f..02c75b1 100644
a b from django.db.models.related import RelatedObject 10 10 from django.db.models.fields.related import ManyToManyRel 11 11 from django.db.models.fields import AutoField, FieldDoesNotExist 12 12 from django.db.models.fields.proxy import OrderWrt 13 from django.db.models.loading import get_models, app_cache_ready13 from django.db.models.loading import get_models, get_model, app_cache_ready 14 14 from django.utils.translation import activate, deactivate_all, get_language, string_concat 15 15 from django.utils.encoding import force_unicode, smart_str 16 16 from django.utils.datastructures import SortedDict … … class Options(object): 373 373 follow = self.get_follow() 374 374 return [f for f in self.get_all_related_objects() if follow.get(f.name, None)] 375 375 376 def get_related_object(self, from_model): 377 "Gets the RelatedObject which links from from_model to this model." 378 if isinstance(from_model, str): 379 from_model = get_model(self.app_label, from_model) 380 for related_object in self.get_all_related_objects(): 381 if related_object.model == from_model: 382 return related_object 383 return None 384 376 385 def get_data_holders(self, follow=None): 377 386 if follow == None: 378 387 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): 110 110 class MissingRelations(models.Model): 111 111 rel1 = models.ForeignKey("Rel1") 112 112 rel2 = models.ManyToManyField("Rel2") 113 114 class MissingManualM2MModel(models.Model): 115 name = models.CharField(max_length=5) 116 missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel") 117 118 class Person(models.Model): 119 name = models.CharField(max_length=5) 120 121 class Group(models.Model): 122 name = models.CharField(max_length=5) 123 primary = models.ManyToManyField(Person, through="Membership", related_name="primary") 124 secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary") 125 126 class 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 131 class Membership(models.Model): 132 person = models.ForeignKey(Person) 133 group = models.ForeignKey(Group) 134 not_default_or_null = models.CharField(max_length=5) 135 136 class MembershipMissingFK(models.Model): 137 person = models.ForeignKey(Person) 113 138 114 139 model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute. 115 140 invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute. … … invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_3' clashes wi 195 220 invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_4' clashes with field 'SelfClashM2M.selfclashm2m'. Add a related_name argument to the definition for 'm2m_4'. 196 221 invalid_models.missingrelations: 'rel2' has m2m relation with model Rel2, which has not been installed 197 222 invalid_models.missingrelations: 'rel1' has relation with model Rel1, which has not been installed 223 invalid_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. 224 invalid_models.missingmanualm2mmodel: missing_m2m has a manually-defined m2m relationship through a model (MissingM2MModel) which does not exist. 225 invalid_models.grouptwo: primary has a manually-defined m2m relationship through a model (Membership) which does not have foreign keys to Person and GroupTwo 226 invalid_models.grouptwo: secondary has a manually-defined m2m relationship through a model (MembershipMissingFK) which does not have foreign keys to Group and GroupTwo 198 227 """ -
new file tests/modeltests/m2m_manual/__init__.py
diff --git a/tests/modeltests/m2m_manual/__init__.py b/tests/modeltests/m2m_manual/__init__.py new file mode 100644 index 0000000..8b13789
- + 1 -
new file tests/modeltests/m2m_manual/models.py
diff --git a/tests/modeltests/m2m_manual/models.py b/tests/modeltests/m2m_manual/models.py new file mode 100644 index 0000000..01764e3
- + 1 from django.db import models 2 from datetime import datetime 3 4 # M2M described on one of the models 5 class Person(models.Model): 6 name = models.CharField(max_length=128) 7 8 def __unicode__(self): 9 return self.name 10 11 class Group(models.Model): 12 name = models.CharField(max_length=128) 13 members = models.ManyToManyField(Person, through='Membership') 14 custom_members = models.ManyToManyField(Person, through='CustomMembership', related_name="custom") 15 nodefaultsnonulls = models.ManyToManyField(Person, through='TestNoDefaultsOrNulls', related_name="testnodefaultsnonulls") 16 17 def __unicode__(self): 18 return self.name 19 20 class Membership(models.Model): 21 person = models.ForeignKey(Person) 22 group = models.ForeignKey(Group) 23 date_joined = models.DateTimeField(default=datetime.now) 24 invite_reason = models.CharField(max_length=64, null=True) 25 26 def __unicode__(self): 27 return "%s is a member of %s" % (self.person.name, self.group.name) 28 29 class CustomMembership(models.Model): 30 person = models.ForeignKey(Person, db_column="custom_person_column", related_name="custom_person_related_name") 31 group = models.ForeignKey(Group) 32 weird_fk = models.ForeignKey(Membership, null=True) 33 date_joined = models.DateTimeField(default=datetime.now) 34 35 def __unicode__(self): 36 return "%s is a member of %s" % (self.person.name, self.group.name) 37 38 class Meta: 39 db_table = "test_table" 40 41 class TestNoDefaultsOrNulls(models.Model): 42 person = models.ForeignKey(Person) 43 group = models.ForeignKey(Group) 44 nodefaultnonull = models.CharField(max_length=5) 45 46 __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) 107 Traceback (most recent call last): 108 ... 109 AttributeError: 'ManyRelatedManager' object has no attribute 'add' 110 111 >>> rock.members.create(name = 'Anne') 112 Traceback (most recent call last): 113 ... 114 AttributeError: 'ManyRelatedManager' object has no attribute 'create' 115 116 # Remove has similar complications, and is not provided either. 117 >>> rock.members.remove(jim) 118 Traceback (most recent call last): 119 ... 120 AttributeError: 'ManyRelatedManager' object has no attribute 'remove' 121 122 # Here we back up the list of all members of Rock. 123 >>> backup = list(rock.members.all()) 124 125 # ...and we verify that it has worked. 126 >>> backup 127 [<Person: Jim>, <Person: Jane>] 128 129 # The clear function should still work. 130 >>> rock.members.clear() 131 132 # Now there will be no members of Rock. 133 >>> rock.members.all() 134 [] 135 136 # Assignment should not work with models specifying a through model for many of 137 # the same reasons as adding. 138 >>> rock.members = backup 139 Traceback (most recent call last): 140 ... 141 AttributeError: Cannot set values on a ManyToManyField which specifies a through model. Use Membership's Manager instead. 142 143 # Let's re-save those instances that we've cleared. 144 >>> m1.save() 145 >>> m2.save() 146 147 # Verifying that those instances were re-saved successfully. 148 >>> rock.members.all() 149 [<Person: Jim>, <Person: Jane>] 150 151 152 ### Reverse Descriptors Tests ### 153 154 # Due to complications with adding via an intermediary model, the add method is 155 # not provided. 156 >>> bob.group_set.add(rock) 157 Traceback (most recent call last): 158 ... 159 AttributeError: 'ManyRelatedManager' object has no attribute 'add' 160 161 # Create is another method that should not work correctly, as it suffers from 162 # the same problems as add. 163 >>> bob.group_set.create(name = 'Funk') 164 Traceback (most recent call last): 165 ... 166 AttributeError: 'ManyRelatedManager' object has no attribute 'create' 167 168 # Remove has similar complications, and is not provided either. 169 >>> jim.group_set.remove(rock) 170 Traceback (most recent call last): 171 ... 172 AttributeError: 'ManyRelatedManager' object has no attribute 'remove' 173 174 # Here we back up the list of all of Jim's groups. 175 >>> backup = list(jim.group_set.all()) 176 >>> backup 177 [<Group: Rock>, <Group: Roll>] 178 179 # The clear function should still work. 180 >>> jim.group_set.clear() 181 182 # Now Jim will be in no groups. 183 >>> jim.group_set.all() 184 [] 185 186 # Assignment should not work with models specifying a through model for many of 187 # the same reasons as adding. 188 >>> jim.group_set = backup 189 Traceback (most recent call last): 190 ... 191 AttributeError: Cannot set values on a ManyToManyField which specifies a through model. Use Membership's Manager instead. 192 193 # Let's re-save those instances that we've cleared. 194 >>> m1.save() 195 >>> m4.save() 196 197 # Verifying that those instances were re-saved successfully. 198 >>> jim.group_set.all() 199 [<Group: Rock>, <Group: Roll>] 200 201 ### Custom Tests ### 202 203 # Let's see if we can query through our second relationship. 204 >>> rock.custom_members.all() 205 [] 206 207 # We can query in the opposite direction as well. 208 >>> bob.custom.all() 209 [] 210 211 # Let's create some membership objects in this custom relationship. 212 >>> cm1 = CustomMembership.objects.create(person = bob, group = rock) 213 >>> cm2 = CustomMembership.objects.create(person = jim, group = rock) 214 215 # If we get the number of people in Rock, it should be both Bob and Jim. 216 >>> rock.custom_members.all() 217 [<Person: Bob>, <Person: Jim>] 218 219 # Bob should only be in one custom group. 220 >>> bob.custom.all() 221 [<Group: Rock>] 222 223 # Let's make sure our new descriptors don't conflict with the FK related_name. 224 >>> bob.custom_person_related_name.all() 225 [<CustomMembership: Bob is a member of Rock>] 226 227 ### QUERY TESTS ### 228 229 # We can query for the related model by using its attribute name (members, in 230 # this case). 231 >>> Group.objects.filter(members__name='Bob') 232 [<Group: Roll>] 233 234 # To query through the intermediary model, we specify its model name. 235 # In this case, membership. 236 >>> Group.objects.filter(membership__invite_reason = "She was just awesome.") 237 [<Group: Rock>] 238 239 # If we want to query in the reverse direction by the related model, use its 240 # model name (group, in this case). 241 >>> Person.objects.filter(group__name="Rock") 242 [<Person: Jim>, <Person: Jane>] 243 244 # If the m2m field has specified a related_name, using that will work. 245 >>> Person.objects.filter(custom__name="Rock") 246 [<Person: Bob>, <Person: Jim>] 247 248 # To query through the intermediary model in the reverse direction, we again 249 # specify its model name (membership, in this case). 250 >>> Person.objects.filter(membership__invite_reason = "She was just awesome.") 251 [<Person: Jane>] 252 253 # Let's see all of the groups that Jane joined after 1 Jan 2005: 254 >>> Group.objects.filter(membership__date_joined__gt = datetime(2005, 1, 1), 255 ... membership__person = jane) 256 [<Group: Rock>] 257 258 # Queries also work in the reverse direction: Now let's see all of the people 259 # that have joined Rock since 1 Jan 2005: 260 >>> Person.objects.filter(membership__date_joined__gt = datetime(2005, 1, 1), 261 ... membership__group = rock) 262 [<Person: Jim>, <Person: Jane>] 263 264 # Conceivably, queries through membership could return correct, but non-unique 265 # querysets. To demonstrate this, we query for all people who have joined a 266 # group after 2004: 267 >>> Person.objects.filter(membership__date_joined__gt = datetime(2004, 1, 1)) 268 [<Person: Jim>, <Person: Jim>, <Person: Jane>] 269 270 # Jim showed up twice, because he joined two groups ('Rock', and 'Roll'): 271 >>> [(m.person.name, m.group.name) for m in 272 ... Membership.objects.filter(date_joined__gt = datetime(2004, 1, 1))] 273 [(u'Jim', u'Rock'), (u'Jane', u'Rock'), (u'Jim', u'Roll')] 274 275 # QuerySet's distinct() method can correct this problem. 276 >>> Person.objects.filter(membership__date_joined__gt = datetime(2004, 1, 1)).distinct() 277 [<Person: Jim>, <Person: Jane>] 278 """} 279 No newline at end of file