Ticket #6095: 6095-qsrf.diff
File 6095-qsrf.diff, 26.3 KB (added by , 17 years ago) |
---|
-
AUTHORS
diff --git a/AUTHORS b/AUTHORS index 9c28d73..5e2ce7a 100644
a b answer newbie questions, and generally made Django that much better: 135 135 Afonso Fernández Nogueira <fonzzo.django@gmail.com> 136 136 Matthew Flanagan <http://wadofstuff.blogspot.com> 137 137 Eric Floehr <eric@intellovations.com> 138 Eric Florenzano <floguy@gmail.com> 138 139 Vincent Foley <vfoleybourgon@yahoo.ca> 139 140 Rudolph Froger <rfroger@estrate.nl> 140 141 Jorge Gajon <gajon@gajon.org> -
django/core/management/sql.py
diff --git a/django/core/management/sql.py b/django/core/management/sql.py index 9e606a7..079812a 100644
a b def many_to_many_sql_for_model(model, style): 352 352 qn = connection.ops.quote_name 353 353 inline_references = connection.features.inline_fk_references 354 354 for f in opts.local_many_to_many: 355 if not isinstance(f.rel, generic.GenericRel) :355 if not isinstance(f.rel, generic.GenericRel) and f.creates_table: 356 356 tablespace = f.db_tablespace or opts.db_tablespace 357 357 if tablespace and connection.features.supports_tablespaces and connection.features.autoindexes_primary_keys: 358 358 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 cd1f84f..3036a36 100644
a b def get_validation_errors(outfile, app=None): 104 104 if r.get_accessor_name() == rel_query_name: 105 105 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)) 106 106 107 seen_intermediary_signatures = [] 107 108 for i, f in enumerate(opts.local_many_to_many): 108 109 # Check to see if the related m2m field will clash with any 109 110 # existing fields, m2m fields, m2m related objects or related … … def get_validation_errors(outfile, app=None): 115 116 if isinstance(f.rel.to, (str, unicode)): 116 117 continue 117 118 119 if hasattr(f.rel, 'through') and f.rel.through != None: 120 intermediary_model = None 121 for model in models.get_models(): 122 if model._meta.module_name == f.rel.through.lower(): 123 intermediary_model = model 124 if intermediary_model == None: 125 e.add(opts, "%s has a manually-defined m2m relationship through a model (%s) which does not exist." % (f.name, f.rel.through)) 126 else: 127 signature = (f.rel.to, cls, intermediary_model) 128 if signature in seen_intermediary_signatures: 129 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)) 130 else: 131 seen_intermediary_signatures.append(signature) 132 seen_related_fk, seen_this_fk = False, False 133 for field in intermediary_model._meta.fields: 134 if field.rel: 135 if field.rel.to == f.rel.to: 136 seen_related_fk = True 137 elif field.rel.to == cls: 138 seen_this_fk = True 139 if not seen_related_fk or not seen_this_fk: 140 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)) 118 141 rel_opts = f.rel.to._meta 119 142 rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name() 120 143 rel_query_name = f.related_query_name() -
django/db/models/fields/related.py
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 818c8a9..f2ab3a1 100644
a b from django.core import validators 10 10 from django import oldforms 11 11 from django import newforms as forms 12 12 from django.dispatch import dispatcher 13 from new import instancemethod 13 14 14 15 try: 15 16 set … … class ForeignRelatedObjectsDescriptor(object): 306 307 manager.clear() 307 308 manager.add(*value) 308 309 309 def create_many_related_manager(superclass ):310 def create_many_related_manager(superclass, through=False): 310 311 """Creates a manager that subclasses 'superclass' (which is a Manager) 311 312 and adds behavior for many-to-many related objects.""" 312 313 class ManyRelatedManager(superclass): … … def create_many_related_manager(superclass): 320 321 self.join_table = join_table 321 322 self.source_col_name = source_col_name 322 323 self.target_col_name = target_col_name 324 self.through = through 323 325 self._pk_val = self.instance._get_pk_val() 324 326 if self._pk_val is None: 325 327 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): 327 329 def get_query_set(self): 328 330 return superclass.get_query_set(self).filter(**(self.core_filters)) 329 331 330 def add(self, *objs):331 self._add_items(self.source_col_name, self.target_col_name, *objs)332 333 # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table334 if self.symmetrical:335 self._add_items(self.target_col_name, self.source_col_name, *objs)336 add.alters_data = True337 338 def remove(self, *objs):339 self._remove_items(self.source_col_name, self.target_col_name, *objs)340 341 # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table342 if self.symmetrical:343 self._remove_items(self.target_col_name, self.source_col_name, *objs)344 remove.alters_data = True345 346 332 def clear(self): 347 333 self._clear_items(self.source_col_name) 348 334 … … def create_many_related_manager(superclass): 419 405 [self._pk_val]) 420 406 transaction.commit_unless_managed() 421 407 408 def add(self, *objs): 409 self._add_items(self.source_col_name, self.target_col_name, *objs) 410 411 # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table 412 if self.symmetrical: 413 self._add_items(self.target_col_name, self.source_col_name, *objs) 414 add.alters_data = True 415 416 def remove(self, *objs): 417 self._remove_items(self.source_col_name, self.target_col_name, *objs) 418 419 # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table 420 if self.symmetrical: 421 self._remove_items(self.target_col_name, self.source_col_name, *objs) 422 remove.alters_data = True 423 424 if not through: 425 ManyRelatedManager.add = instancemethod(add, None, ManyRelatedManager) 426 ManyRelatedManager.remove = instancemethod(remove, None, ManyRelatedManager) 427 422 428 return ManyRelatedManager 423 429 424 430 class ManyRelatedObjectsDescriptor(object): … … class ManyRelatedObjectsDescriptor(object): 439 445 # model's default manager. 440 446 rel_model = self.related.model 441 447 superclass = rel_model._default_manager.__class__ 442 RelatedManager = create_many_related_manager(superclass )448 RelatedManager = create_many_related_manager(superclass, self.related.field.rel.through) 443 449 444 450 qn = connection.ops.quote_name 445 451 manager = RelatedManager( … … class ManyRelatedObjectsDescriptor(object): 458 464 if instance is None: 459 465 raise AttributeError, "Manager must be accessed via instance" 460 466 467 through = getattr(self.related.field.rel, 'through', None) 468 if through is not None: 469 raise AttributeError, "Cannot set values on a ManyToManyField which specifies a through model. Use %s's Manager instead." % through 470 461 471 manager = self.__get__(instance) 462 472 manager.clear() 463 473 manager.add(*value) … … class ReverseManyRelatedObjectsDescriptor(object): 480 490 # model's default manager. 481 491 rel_model=self.field.rel.to 482 492 superclass = rel_model._default_manager.__class__ 483 RelatedManager = create_many_related_manager(superclass )493 RelatedManager = create_many_related_manager(superclass, self.field.rel.through) 484 494 485 495 qn = connection.ops.quote_name 486 496 manager = RelatedManager( … … class ReverseManyRelatedObjectsDescriptor(object): 499 509 if instance is None: 500 510 raise AttributeError, "Manager must be accessed via instance" 501 511 512 through = getattr(self.field.rel, 'through', None) 513 if through is not None: 514 raise AttributeError, "Cannot set values on a ManyToManyField which specifies a through model. Use %s's Manager instead." % through 515 502 516 manager = self.__get__(instance) 503 517 manager.clear() 504 518 manager.add(*value) … … class OneToOneRel(ManyToOneRel): 548 562 549 563 class ManyToManyRel(object): 550 564 def __init__(self, to, num_in_admin=0, related_name=None, 551 filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True): 565 filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True, 566 through=None): 552 567 self.to = to 553 568 self.num_in_admin = num_in_admin 554 569 self.related_name = related_name … … class ManyToManyRel(object): 560 575 self.raw_id_admin = raw_id_admin 561 576 self.symmetrical = symmetrical 562 577 self.multiple = True 578 self.through = through 563 579 564 580 assert not (self.raw_id_admin and self.filter_interface), "ManyToManyRels may not use both raw_id_admin and filter_interface" 565 581 … … class ManyToManyField(RelatedField, Field): 696 712 filter_interface=kwargs.pop('filter_interface', None), 697 713 limit_choices_to=kwargs.pop('limit_choices_to', None), 698 714 raw_id_admin=kwargs.pop('raw_id_admin', False), 699 symmetrical=kwargs.pop('symmetrical', True)) 715 symmetrical=kwargs.pop('symmetrical', True), 716 through=kwargs.pop('through', None)) 700 717 self.db_table = kwargs.pop('db_table', None) 718 if kwargs['rel'].through: 719 self.creates_table = False 720 assert not self.db_table, "Cannot specify a db_table if an intermediary model is used." 721 else: 722 self.creates_table = True 701 723 if kwargs["rel"].raw_id_admin: 702 724 kwargs.setdefault("validator_list", []).append(self.isValidIDList) 703 725 Field.__init__(self, **kwargs) … … class ManyToManyField(RelatedField, Field): 720 742 721 743 def _get_m2m_db_table(self, opts): 722 744 "Function that can be curried to provide the m2m table name for this relation" 723 if self.db_table: 745 if self.rel.through != None: 746 return get_model(opts.app_label, self.rel.through)._meta.db_table 747 elif self.db_table: 724 748 return self.db_table 725 749 else: 726 750 return '%s_%s' % (opts.db_table, self.name) … … class ManyToManyField(RelatedField, Field): 728 752 def _get_m2m_column_name(self, related): 729 753 "Function that can be curried to provide the source column name for the m2m table" 730 754 # If this is an m2m relation to self, avoid the inevitable name clash 731 if related.model == related.parent_model: 755 if self.rel.through != None: 756 field = related.model._meta.get_related_object(self.rel.through).field 757 attname, column = field.get_attname_column() 758 return column 759 elif related.model == related.parent_model: 732 760 return 'from_' + related.model._meta.object_name.lower() + '_id' 733 761 else: 734 762 return related.model._meta.object_name.lower() + '_id' … … class ManyToManyField(RelatedField, Field): 736 764 def _get_m2m_reverse_name(self, related): 737 765 "Function that can be curried to provide the related column name for the m2m table" 738 766 # If this is an m2m relation to self, avoid the inevitable name clash 739 if related.model == related.parent_model: 767 if self.rel.through != None: 768 field = related.parent_model._meta.get_related_object(self.rel.through).field 769 attname, column = field.get_attname_column() 770 return column 771 elif related.model == related.parent_model: 740 772 return 'to_' + related.parent_model._meta.object_name.lower() + '_id' 741 773 else: 742 774 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 0796a3f..9a7855e 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.db.models import Manager 15 15 from django.utils.translation import activate, deactivate_all, get_language, string_concat 16 16 from django.utils.encoding import force_unicode, smart_str … … class Options(object): 374 374 follow = self.get_follow() 375 375 return [f for f in self.get_all_related_objects() if follow.get(f.name, None)] 376 376 377 def get_related_object(self, from_model): 378 "Gets the RelatedObject which links from from_model to this model." 379 if isinstance(from_model, str): 380 from_model = get_model(self.app_label, from_model) 381 for related_object in self.get_all_related_objects(): 382 if related_object.model == from_model: 383 return related_object 384 return None 385 377 386 def get_data_holders(self, follow=None): 378 387 if follow == None: 379 388 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 8a480a2..2523f29 100644
a b class MissingRelations(models.Model): 112 112 rel1 = models.ForeignKey("Rel1") 113 113 rel2 = models.ManyToManyField("Rel2") 114 114 115 class MissingManualM2MModel(models.Model): 116 name = models.CharField(max_length=5) 117 missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel") 118 119 class Person(models.Model): 120 name = models.CharField(max_length=5) 121 122 class Group(models.Model): 123 name = models.CharField(max_length=5) 124 primary = models.ManyToManyField(Person, through="Membership", related_name="primary") 125 secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary") 126 127 class GroupTwo(models.Model): 128 name = models.CharField(max_length=5) 129 primary = models.ManyToManyField(Person, through="Membership") 130 secondary = models.ManyToManyField(Group, through="MembershipMissingFK") 131 132 class Membership(models.Model): 133 person = models.ForeignKey(Person) 134 group = models.ForeignKey(Group) 135 not_default_or_null = models.CharField(max_length=5) 136 137 class MembershipMissingFK(models.Model): 138 person = models.ForeignKey(Person) 139 115 140 model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute. 116 141 invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute. 117 142 invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute. … … invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_3' clashes wi 197 222 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'. 198 223 invalid_models.missingrelations: 'rel2' has m2m relation with model Rel2, which has not been installed 199 224 invalid_models.missingrelations: 'rel1' has relation with model Rel1, which has not been installed 225 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. 226 invalid_models.missingmanualm2mmodel: missing_m2m has a manually-defined m2m relationship through a model (MissingM2MModel) which does not exist. 227 invalid_models.grouptwo: primary has a manually-defined m2m relationship through a model (Membership) which does not have foreign keys to Person and GroupTwo 228 invalid_models.grouptwo: secondary has a manually-defined m2m relationship through a model (MembershipMissingFK) which does not have foreign keys to Group and GroupTwo 200 229 """ -
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
- + 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 # Remove has similar complications, and is not provided either. 112 >>> rock.members.remove(jim) 113 Traceback (most recent call last): 114 ... 115 AttributeError: '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 134 Traceback (most recent call last): 135 ... 136 AttributeError: 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) 152 Traceback (most recent call last): 153 ... 154 AttributeError: 'ManyRelatedManager' object has no attribute 'add' 155 156 # Remove has similar complications, and is not provided either. 157 >>> jim.group_set.remove(rock) 158 Traceback (most recent call last): 159 ... 160 AttributeError: '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 177 Traceback (most recent call last): 178 ... 179 AttributeError: 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