Ticket #6095: 6095-nfadmin.diff
File 6095-nfadmin.diff, 26.8 KB (added by , 17 years ago) |
---|
-
AUTHORS
diff --git a/AUTHORS b/AUTHORS index ea898de..6278a73 100644
a b answer newbie questions, and generally made Django that much better: 134 134 Afonso Fernández Nogueira <fonzzo.django@gmail.com> 135 135 Matthew Flanagan <http://wadofstuff.blogspot.com> 136 136 Eric Floehr <eric@intellovations.com> 137 Eric Florenzano <floguy@gmail.com> 137 138 Vincent Foley <vfoleybourgon@yahoo.ca> 138 139 Rudolph Froger <rfroger@estrate.nl> 139 140 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): 172 172 kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel) 173 173 else: 174 174 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: 176 179 kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel) 177 180 kwargs['help_text'] = '' 178 181 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): 352 352 qn = connection.ops.quote_name 353 353 inline_references = connection.features.inline_fk_references 354 354 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: 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 6e8e6d0..241563e 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 = [] 106 105 107 for i, f in enumerate(opts.many_to_many): 106 108 # Check to see if the related m2m field will clash with any 107 109 # existing fields, m2m fields, m2m related objects or related objects … … def get_validation_errors(outfile, app=None): 111 113 # so skip the next section 112 114 if isinstance(f.rel.to, (str, unicode)): 113 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)) 114 138 115 139 rel_opts = f.rel.to._meta 116 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 2bd31c9..86215e4 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): 262 263 manager.clear() 263 264 manager.add(*value) 264 265 265 def create_many_related_manager(superclass ):266 def create_many_related_manager(superclass, through=False): 266 267 """Creates a manager that subclasses 'superclass' (which is a Manager) 267 268 and adds behavior for many-to-many related objects.""" 268 269 class ManyRelatedManager(superclass): … … def create_many_related_manager(superclass): 276 277 self.join_table = join_table 277 278 self.source_col_name = source_col_name 278 279 self.target_col_name = target_col_name 280 self.through = through 279 281 self._pk_val = self.instance._get_pk_val() 280 282 if self._pk_val is None: 281 283 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): 283 285 def get_query_set(self): 284 286 return superclass.get_query_set(self).filter(**(self.core_filters)) 285 287 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 table290 if self.symmetrical:291 self._add_items(self.target_col_name, self.source_col_name, *objs)292 add.alters_data = True293 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 table298 if self.symmetrical:299 self._remove_items(self.target_col_name, self.source_col_name, *objs)300 remove.alters_data = True301 302 288 def clear(self): 303 289 self._clear_items(self.source_col_name) 304 290 … … def create_many_related_manager(superclass): 375 361 [self._pk_val]) 376 362 transaction.commit_unless_managed() 377 363 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 378 382 return ManyRelatedManager 379 383 380 384 class ManyRelatedObjectsDescriptor(object): … … class ManyRelatedObjectsDescriptor(object): 395 399 # model's default manager. 396 400 rel_model = self.related.model 397 401 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) 399 403 400 404 qn = connection.ops.quote_name 401 405 manager = RelatedManager( … … class ManyRelatedObjectsDescriptor(object): 414 418 if instance is None: 415 419 raise AttributeError, "Manager must be accessed via instance" 416 420 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 417 425 manager = self.__get__(instance) 418 426 manager.clear() 419 427 manager.add(*value) … … class ReverseManyRelatedObjectsDescriptor(object): 436 444 # model's default manager. 437 445 rel_model=self.field.rel.to 438 446 superclass = rel_model._default_manager.__class__ 439 RelatedManager = create_many_related_manager(superclass )447 RelatedManager = create_many_related_manager(superclass, self.field.rel.through) 440 448 441 449 qn = connection.ops.quote_name 442 450 manager = RelatedManager( … … class ReverseManyRelatedObjectsDescriptor(object): 455 463 if instance is None: 456 464 raise AttributeError, "Manager must be accessed via instance" 457 465 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 458 470 manager = self.__get__(instance) 459 471 manager.clear() 460 472 manager.add(*value) … … class ManyToManyField(RelatedField, Field): 633 645 num_in_admin=kwargs.pop('num_in_admin', 0), 634 646 related_name=kwargs.pop('related_name', None), 635 647 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)) 637 650 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 638 656 Field.__init__(self, **kwargs) 639 657 640 658 msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.') … … class ManyToManyField(RelatedField, Field): 649 667 650 668 def _get_m2m_db_table(self, opts): 651 669 "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: 653 673 return self.db_table 654 674 else: 655 675 return '%s_%s' % (opts.db_table, self.name) … … class ManyToManyField(RelatedField, Field): 657 677 def _get_m2m_column_name(self, related): 658 678 "Function that can be curried to provide the source column name for the m2m table" 659 679 # 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: 661 685 return 'from_' + related.model._meta.object_name.lower() + '_id' 662 686 else: 663 687 return related.model._meta.object_name.lower() + '_id' … … class ManyToManyField(RelatedField, Field): 665 689 def _get_m2m_reverse_name(self, related): 666 690 "Function that can be curried to provide the related column name for the m2m table" 667 691 # 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: 669 697 return 'to_' + related.parent_model._meta.object_name.lower() + '_id' 670 698 else: 671 699 return related.parent_model._meta.object_name.lower() + '_id' … … class OneToOneRel(ManyToOneRel): 780 808 781 809 class ManyToManyRel(object): 782 810 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): 784 812 self.to = to 785 813 self.num_in_admin = num_in_admin 786 814 self.related_name = related_name … … class ManyToManyRel(object): 790 818 self.edit_inline = False 791 819 self.symmetrical = symmetrical 792 820 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 2 2 from django.db.models.related import RelatedObject 3 3 from django.db.models.fields.related import ManyToManyRel 4 4 from django.db.models.fields import AutoField, FieldDoesNotExist 5 from django.db.models.loading import get_models, app_cache_ready5 from django.db.models.loading import get_models, get_model, app_cache_ready 6 6 from django.db.models.query import orderlist2sql 7 7 from django.utils.translation import activate, deactivate_all, get_language, string_concat 8 8 from django.utils.encoding import force_unicode, smart_str … … class Options(object): 161 161 follow = self.get_follow() 162 162 return [f for f in self.get_all_related_objects() if follow.get(f.name, None)] 163 163 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 164 173 def get_data_holders(self, follow=None): 165 174 if follow == None: 166 175 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/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