Ticket #6095: 6095-beta-03.diff
File 6095-beta-03.diff, 22.6 KB (added by , 17 years ago) |
---|
-
django/db/models/fields/related.py
54 54 except klass.DoesNotExist: 55 55 raise validators.ValidationError, _("Please enter a valid %s.") % f.verbose_name 56 56 57 def get_reverse_rel_field(from_model, to_model, related_name): 58 "Gets the related field which points from one model to another." 59 for field in from_model._meta.fields: 60 if field.__class__ == ForeignKey: 61 if field.rel.to == to_model: 62 return field 63 return None 64 57 65 #HACK 58 66 class RelatedField(object): 59 67 def contribute_to_class(self, cls, name): … … 267 275 and adds behavior for many-to-many related objects.""" 268 276 class ManyRelatedManager(superclass): 269 277 def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None, 270 join_table=None, source_col_name=None, target_col_name=None): 278 join_table=None, source_col_name=None, target_col_name=None, 279 through=None): 271 280 super(ManyRelatedManager, self).__init__() 272 281 self.core_filters = core_filters 273 282 self.model = model … … 276 285 self.join_table = join_table 277 286 self.source_col_name = source_col_name 278 287 self.target_col_name = target_col_name 288 self.through = through 279 289 self._pk_val = self.instance._get_pk_val() 280 290 if self._pk_val is None: 281 291 raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % model) … … 284 294 return superclass.get_query_set(self).filter(**(self.core_filters)) 285 295 286 296 def add(self, *objs): 297 if self.through: 298 raise NotImplementedError, "Add not possible for ManyToManyFields which specify a through model. Try %s.objects.create(...) instead." % self.through 287 299 self._add_items(self.source_col_name, self.target_col_name, *objs) 288 300 289 301 # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table … … 292 304 add.alters_data = True 293 305 294 306 def remove(self, *objs): 307 if self.through: 308 raise NotImplementedError, "Remove not possible for ManyToManyFields which specify a through model." 295 309 self._remove_items(self.source_col_name, self.target_col_name, *objs) 296 310 297 311 # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table 298 312 if self.symmetrical: 299 313 self._remove_items(self.target_col_name, self.source_col_name, *objs) … … 405 419 symmetrical=False, 406 420 join_table=qn(self.related.field.m2m_db_table()), 407 421 source_col_name=qn(self.related.field.m2m_reverse_name()), 408 target_col_name=qn(self.related.field.m2m_column_name()) 422 target_col_name=qn(self.related.field.m2m_column_name()), 423 through=getattr(self.related.field.rel, 'through', None) 409 424 ) 410 425 411 426 return manager … … 414 429 if instance is None: 415 430 raise AttributeError, "Manager must be accessed via instance" 416 431 432 through = getattr(self.related.field.rel, 'through', None) 433 if through: 434 raise NotImplementedError, "Cannot set values on a ManyToManyFields which specify a through model. Use %s's Manager instead." % through 435 417 436 manager = self.__get__(instance) 418 437 manager.clear() 419 438 manager.add(*value) … … 446 465 symmetrical=(self.field.rel.symmetrical and instance.__class__ == rel_model), 447 466 join_table=qn(self.field.m2m_db_table()), 448 467 source_col_name=qn(self.field.m2m_column_name()), 449 target_col_name=qn(self.field.m2m_reverse_name()) 468 target_col_name=qn(self.field.m2m_reverse_name()), 469 through=getattr(self.field.rel, 'through', None) 450 470 ) 451 471 452 472 return manager … … 455 475 if instance is None: 456 476 raise AttributeError, "Manager must be accessed via instance" 457 477 478 through = getattr(self.field.rel, 'through', None) 479 if through: 480 raise NotImplementedError, "Cannot set values on a ManyToManyFields which specify a through model. Use %s's Manager instead." % through 481 458 482 manager = self.__get__(instance) 459 483 manager.clear() 460 484 manager.add(*value) … … 648 672 filter_interface=kwargs.pop('filter_interface', None), 649 673 limit_choices_to=kwargs.pop('limit_choices_to', None), 650 674 raw_id_admin=kwargs.pop('raw_id_admin', False), 651 symmetrical=kwargs.pop('symmetrical', True)) 675 symmetrical=kwargs.pop('symmetrical', True), 676 through=kwargs.pop('through', None)) 652 677 self.db_table = kwargs.pop('db_table', None) 678 if kwargs['rel'].through: 679 assert not self.db_table, "Cannot specify a db_table if an intermediary model is used." 653 680 if kwargs["rel"].raw_id_admin: 654 681 kwargs.setdefault("validator_list", []).append(self.isValidIDList) 655 682 Field.__init__(self, **kwargs) … … 672 699 673 700 def _get_m2m_db_table(self, opts): 674 701 "Function that can be curried to provide the m2m table name for this relation" 675 if self.db_table: 702 if self.rel.through != None: 703 return get_model(opts.app_label, self.rel.through)._meta.db_table 704 elif self.db_table: 676 705 return self.db_table 677 706 else: 678 707 return '%s_%s' % (opts.db_table, self.name) … … 680 709 def _get_m2m_column_name(self, related): 681 710 "Function that can be curried to provide the source column name for the m2m table" 682 711 # If this is an m2m relation to self, avoid the inevitable name clash 683 if related.model == related.parent_model: 712 if self.rel.through != None: 713 through = get_model(related.opts.app_label, self.rel.through) 714 field = get_reverse_rel_field(through, related.model, self.rel.related_name) 715 attname, column = field.get_attname_column() 716 return column 717 elif related.model == related.parent_model: 684 718 return 'from_' + related.model._meta.object_name.lower() + '_id' 685 719 else: 686 720 return related.model._meta.object_name.lower() + '_id' … … 688 722 def _get_m2m_reverse_name(self, related): 689 723 "Function that can be curried to provide the related column name for the m2m table" 690 724 # If this is an m2m relation to self, avoid the inevitable name clash 691 if related.model == related.parent_model: 725 if self.rel.through != None: 726 through = get_model(related.opts.app_label, self.rel.through) 727 field = get_reverse_rel_field(through, related.parent_model, self.rel.related_name) 728 attname, column = field.get_attname_column() 729 return column 730 elif related.model == related.parent_model: 692 731 return 'to_' + related.parent_model._meta.object_name.lower() + '_id' 693 732 else: 694 733 return related.parent_model._meta.object_name.lower() + '_id' … … 809 848 810 849 class ManyToManyRel(object): 811 850 def __init__(self, to, num_in_admin=0, related_name=None, 812 filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True): 851 filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True, 852 through = None): 813 853 self.to = to 814 854 self.num_in_admin = num_in_admin 815 855 self.related_name = related_name … … 821 861 self.raw_id_admin = raw_id_admin 822 862 self.symmetrical = symmetrical 823 863 self.multiple = True 864 self.through = through 824 865 825 866 assert not (self.raw_id_admin and self.filter_interface), "ManyToManyRels may not use both raw_id_admin and filter_interface" -
django/core/management/validation.py
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 = [] 108 107 109 for i, f in enumerate(opts.many_to_many): 108 110 # Check to see if the related m2m field will clash with any 109 111 # existing fields, m2m fields, m2m related objects or related objects … … 113 115 # so skip the next section 114 116 if isinstance(f.rel.to, (str, unicode)): 115 117 continue 118 if hasattr(f.rel, 'through') and f.rel.through != None: 119 intermediary_model = None 120 for model in models.get_models(): 121 if model._meta.module_name == f.rel.through.lower(): 122 intermediary_model = model 123 if intermediary_model == None: 124 e.add(opts, "%s has a manually-defined m2m relationship through a model (%s) which does not exist." % (f.name, f.rel.through)) 125 else: 126 signature = (f.rel.to, cls, intermediary_model) 127 if signature in seen_intermediary_signatures: 128 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)) 129 else: 130 seen_intermediary_signatures.append(signature) 131 seen_related_fk, seen_this_fk = False, False 132 for field in intermediary_model._meta.fields: 133 if field.rel: 134 if field.rel.to == f.rel.to: 135 seen_related_fk = True 136 elif field.rel.to == cls: 137 seen_this_fk = True 138 if not seen_related_fk or not seen_this_fk: 139 e.add(opts, "%s has a manualy-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)) 116 140 117 141 rel_opts = f.rel.to._meta 118 142 rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name() -
django/core/management/sql.py
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 getattr(f.rel, 'through', None) == None: 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) -
tests/modeltests/invalid_models/models.py
111 111 class MissingRelations(models.Model): 112 112 rel1 = models.ForeignKey("Rel1") 113 113 rel2 = models.ManyToManyField("Rel2") 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) 114 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. … … 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 manualy-defined m2m relationship through a model (Membership) which does not have foreign keys to Person and GroupTwo 228 invalid_models.grouptwo: secondary has a manualy-defined m2m relationship through a model (MembershipMissingFK) which does not have foreign keys to Group and GroupTwo 200 229 """ -
tests/modeltests/m2m_manual/models.py
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 >>> bob = Person.objects.create(name = 'Bob') 51 >>> jim = Person.objects.create(name = 'Jim') 52 >>> jane = Person.objects.create(name = 'Jane') 53 >>> rock = Group.objects.create(name = 'Rock') 54 >>> roll = Group.objects.create(name = 'Roll') 55 56 >>> rock.members.all() 57 [] 58 59 >>> m1 = Membership.objects.create(person = jim, group = rock) 60 >>> m2 = Membership.objects.create(person = jane, group = rock) 61 62 >>> rock.members.all() 63 [<Person: Jim>, <Person: Jane>] 64 65 >>> m3 = Membership.objects.create(person = bob, group = roll) 66 >>> m4 = Membership.objects.create(person = jim, group = roll) 67 >>> m5 = Membership.objects.create(person = jane, group = roll) 68 69 >>> jim.group_set.all() 70 [<Group: Rock>, <Group: Roll>] 71 72 # Check to make sure that querying via intermediary model works as normal 73 >>> m = Membership.objects.get(person = jane, group = rock) 74 >>> m 75 <Membership: Jane is a member of Rock> 76 77 # Setting some date_joined dates 78 >>> m2.invite_reason = "She was just awesome." 79 >>> m2.date_joined = datetime(2006, 1, 1) 80 >>> m2.save() 81 82 >>> m5.date_joined = datetime(2004, 1, 1) 83 >>> m5.save() 84 85 >>> m3.date_joined = datetime(2004, 1, 1) 86 >>> m3.save() 87 88 >>> Membership.objects.filter(person = jim) 89 [<Membership: Jim is a member of Rock>, <Membership: Jim is a member of Roll>] 90 91 92 ### Forward Descriptors Tests ### 93 # Ensure that using the add or remove function errors, due to using a through model. 94 >>> rock.members.add(bob) 95 Traceback (most recent call last): 96 ... 97 NotImplementedError: Add not possible for ManyToManyFields which specify a through model. Try Membership.objects.create(...) instead. 98 99 >>> rock.members.remove(jim) 100 Traceback (most recent call last): 101 ... 102 NotImplementedError: Remove not possible for ManyToManyFields which specify a through model. 103 104 >>> backup = list(rock.members.all()) 105 >>> backup 106 [<Person: Jim>, <Person: Jane>] 107 108 # The clear function should still work. 109 >>> rock.members.clear() 110 >>> rock.members.all() 111 [] 112 113 # Assignment should not work with models specifying a through model. 114 >>> rock.members = backup 115 Traceback (most recent call last): 116 ... 117 NotImplementedError: Cannot set values on a ManyToManyFields which specify a through model. Use Membership's Manager instead. 118 119 # Let's re-add those instances that we've cleared. 120 >>> m1.save() 121 >>> m2.save() 122 123 >>> rock.members.all() 124 [<Person: Jim>, <Person: Jane>] 125 126 127 ### Reverse Descriptors Tests ### 128 # Ensure that using the add or remove function errors, due to using a through model. 129 >>> bob.group_set.add(rock) 130 Traceback (most recent call last): 131 ... 132 NotImplementedError: Add not possible for ManyToManyFields which specify a through model. Try Membership.objects.create(...) instead. 133 134 >>> jim.group_set.remove(rock) 135 Traceback (most recent call last): 136 ... 137 NotImplementedError: Remove not possible for ManyToManyFields which specify a through model. 138 139 >>> backup = list(jim.group_set.all()) 140 >>> backup 141 [<Group: Rock>, <Group: Roll>] 142 143 # The clear function should still work. 144 >>> jim.group_set.clear() 145 >>> jim.group_set.all() 146 [] 147 148 # Assignment should not work with models specifying a through model. 149 >>> jim.group_set = backup 150 Traceback (most recent call last): 151 ... 152 NotImplementedError: Cannot set values on a ManyToManyFields which specify a through model. Use Membership's Manager instead. 153 154 # Let's re-add those instances that we've cleared. 155 >>> m1.save() 156 >>> m4.save() 157 158 >>> jim.group_set.all() 159 [<Group: Rock>, <Group: Roll>] 160 161 ### Custom Tests ### 162 163 >>> rock.custom_members.all() 164 [] 165 >>> bob.custom.all() 166 [] 167 >>> cm1 = CustomMembership.objects.create(person = bob, group = rock) 168 >>> cm2 = CustomMembership.objects.create(person = jim, group = rock) 169 170 >>> rock.custom_members.all() 171 [<Person: Bob>, <Person: Jim>] 172 >>> bob.custom.all() 173 [<Group: Rock>] 174 175 # Let's make sure our new descriptors don't conflict with the FK related_name. 176 >>> bob.custom_person_related_name.all() 177 [<CustomMembership: Bob is a member of Rock>] 178 179 ###QUERY TESTS### 180 # Queries involving the related model (Person, in the case of Group) use its 181 # attname 182 >>> Group.objects.filter(members__name='Bob') 183 [<Group: Roll>] 184 185 # Queries involving the relationship model (Membership, in the case of Group) 186 # use its model name 187 >>> Group.objects.filter(membership__invite_reason = "She was just awesome.") 188 [<Group: Rock>] 189 190 # Queries involving the reverse related model (Group, in the case of Person) 191 # use its model name 192 >>> Person.objects.filter(group__name="Rock") 193 [<Person: Jim>, <Person: Jane>] 194 195 # If the m2m field has specified a related_name, using that will work. 196 >>> Person.objects.filter(custom__name="Rock") 197 [<Person: Bob>, <Person: Jim>] 198 199 # Queries involving the relationship model (Membership, in the case of Group) 200 # use its model name 201 >>> Person.objects.filter(membership__invite_reason = "She was just awesome.") 202 [<Person: Jane>] 203 204 # Let's see all of the groups that Jane joined after 1 Jan 2005: 205 >>> Group.objects.filter(membership__date_joined__gt = datetime(2005, 1, 1), 206 ... membership__person = jane) 207 [<Group: Rock>] 208 209 # Now let's see all of the people that have joined Rock since 1 Jan 2005: 210 >>> Person.objects.filter(membership__date_joined__gt = datetime(2005, 1, 1), 211 ... membership__group = rock) 212 [<Person: Jim>, <Person: Jane>] 213 214 # Conceivably, queries through membership could return non-unique querysets. 215 # To demonstrate this, query for all people who have joined a group after 2004: 216 >>> Person.objects.filter(membership__date_joined__gt = datetime(2004, 1, 1)) 217 [<Person: Jim>, <Person: Jim>, <Person: Jane>] 218 219 # Jim showed up twice, because he joined two groups ('Rock', and 'Roll'): 220 >>> [(m.person.name, m.group.name) for m in 221 ... Membership.objects.filter(date_joined__gt = datetime(2004, 1, 1))] 222 [(u'Jim', u'Rock'), (u'Jane', u'Rock'), (u'Jim', u'Roll')] 223 224 # QuerySet's distinct() method can correct this problem. 225 >>> Person.objects.filter(membership__date_joined__gt = datetime(2004, 1, 1)).distinct() 226 [<Person: Jim>, <Person: Jane>] 227 """} 228 No newline at end of file -
AUTHORS
129 129 Afonso Fernández Nogueira <fonzzo.django@gmail.com> 130 130 Matthew Flanagan <http://wadofstuff.blogspot.com> 131 131 Eric Floehr <eric@intellovations.com> 132 Eric Florenzano <floguy@gmail.com> 132 133 Vincent Foley <vfoleybourgon@yahoo.ca> 133 134 Rudolph Froger <rfroger@estrate.nl> 134 135 Jorge Gajon <gajon@gajon.org>