Ticket #6095: 6095-nfadmin.2.diff
File 6095-nfadmin.2.diff, 25.9 KB (added by , 17 years ago) |
---|
-
django/db/models/options.py
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 … … 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() -
django/db/models/fields/related.py
298 298 manager.clear() 299 299 manager.add(*value) 300 300 301 def create_many_related_manager(superclass ):301 def create_many_related_manager(superclass, through=False): 302 302 """Creates a manager that subclasses 'superclass' (which is a Manager) 303 303 and adds behavior for many-to-many related objects.""" 304 304 class ManyRelatedManager(superclass): … … 312 312 self.join_table = join_table 313 313 self.source_col_name = source_col_name 314 314 self.target_col_name = target_col_name 315 self.through = through 315 316 self._pk_val = self.instance._get_pk_val() 316 317 if self._pk_val is None: 317 318 raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % model) … … 321 322 322 323 def add(self, *objs): 323 324 self._add_items(self.source_col_name, self.target_col_name, *objs) 324 325 325 # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table 326 326 if self.symmetrical: 327 327 self._add_items(self.target_col_name, self.source_col_name, *objs) 328 328 add.alters_data = True 329 329 330 330 def remove(self, *objs): 331 331 self._remove_items(self.source_col_name, self.target_col_name, *objs) 332 333 332 # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table 334 333 if self.symmetrical: 335 334 self._remove_items(self.target_col_name, self.source_col_name, *objs) … … 344 343 clear.alters_data = True 345 344 346 345 def create(self, **kwargs): 346 if through: 347 raise AttributeError, "'ManyRelatedManager' object has no attribute 'create'" 347 348 new_obj = self.model(**kwargs) 348 349 new_obj.save() 349 350 self.add(new_obj) … … 411 412 [self._pk_val]) 412 413 transaction.commit_unless_managed() 413 414 415 # If it's an intermediary model, detach the add, remove, and create methods 416 # from this ManyRelatedManager. 417 if through: 418 del ManyRelatedManager.add 419 del ManyRelatedManager.remove 420 414 421 return ManyRelatedManager 415 422 416 423 class ManyRelatedObjectsDescriptor(object): … … 431 438 # model's default manager. 432 439 rel_model = self.related.model 433 440 superclass = rel_model._default_manager.__class__ 434 RelatedManager = create_many_related_manager(superclass )441 RelatedManager = create_many_related_manager(superclass, self.related.field.rel.through) 435 442 436 443 qn = connection.ops.quote_name 437 444 manager = RelatedManager( … … 450 457 if instance is None: 451 458 raise AttributeError, "Manager must be accessed via instance" 452 459 460 through = getattr(self.related.field.rel, 'through', None) 461 if through: 462 raise AttributeError, "Cannot set values on a ManyToManyField which specifies a through model. Use %s's Manager instead." % through 463 453 464 manager = self.__get__(instance) 454 465 manager.clear() 455 466 manager.add(*value) … … 472 483 # model's default manager. 473 484 rel_model=self.field.rel.to 474 485 superclass = rel_model._default_manager.__class__ 475 RelatedManager = create_many_related_manager(superclass )486 RelatedManager = create_many_related_manager(superclass, self.field.rel.through) 476 487 477 488 qn = connection.ops.quote_name 478 489 manager = RelatedManager( … … 491 502 if instance is None: 492 503 raise AttributeError, "Manager must be accessed via instance" 493 504 505 through = getattr(self.field.rel, 'through', None) 506 if through: 507 raise AttributeError, "Cannot set values on a ManyToManyField which specifies a through model. Use %s's Manager instead." % through 508 494 509 manager = self.__get__(instance) 495 510 manager.clear() 496 511 manager.add(*value) … … 669 684 num_in_admin=kwargs.pop('num_in_admin', 0), 670 685 related_name=kwargs.pop('related_name', None), 671 686 limit_choices_to=kwargs.pop('limit_choices_to', None), 672 symmetrical=kwargs.pop('symmetrical', True)) 687 symmetrical=kwargs.pop('symmetrical', True), 688 through=kwargs.pop('through', None)) 673 689 self.db_table = kwargs.pop('db_table', None) 690 if kwargs['rel'].through: 691 self.creates_table = False 692 assert not self.db_table, "Cannot specify a db_table if an intermediary model is used." 693 else: 694 self.creates_table = True 674 695 Field.__init__(self, **kwargs) 675 696 676 697 msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.') … … 685 706 686 707 def _get_m2m_db_table(self, opts): 687 708 "Function that can be curried to provide the m2m table name for this relation" 688 if self.db_table: 709 if self.rel.through != None: 710 return get_model(opts.app_label, self.rel.through)._meta.db_table 711 elif self.db_table: 689 712 return self.db_table 690 713 else: 691 714 return '%s_%s' % (opts.db_table, self.name) … … 693 716 def _get_m2m_column_name(self, related): 694 717 "Function that can be curried to provide the source column name for the m2m table" 695 718 # If this is an m2m relation to self, avoid the inevitable name clash 696 if related.model == related.parent_model: 719 if self.rel.through != None: 720 field = related.model._meta.get_related_object(self.rel.through).field 721 attname, column = field.get_attname_column() 722 return column 723 elif related.model == related.parent_model: 697 724 return 'from_' + related.model._meta.object_name.lower() + '_id' 698 725 else: 699 726 return related.model._meta.object_name.lower() + '_id' … … 701 728 def _get_m2m_reverse_name(self, related): 702 729 "Function that can be curried to provide the related column name for the m2m table" 703 730 # If this is an m2m relation to self, avoid the inevitable name clash 704 if related.model == related.parent_model: 731 if self.rel.through != None: 732 field = related.parent_model._meta.get_related_object(self.rel.through).field 733 attname, column = field.get_attname_column() 734 return column 735 elif related.model == related.parent_model: 705 736 return 'to_' + related.parent_model._meta.object_name.lower() + '_id' 706 737 else: 707 738 return related.parent_model._meta.object_name.lower() + '_id' … … 816 847 817 848 class ManyToManyRel(object): 818 849 def __init__(self, to, num_in_admin=0, related_name=None, 819 limit_choices_to=None, symmetrical=True ):850 limit_choices_to=None, symmetrical=True, through=None): 820 851 self.to = to 821 852 self.num_in_admin = num_in_admin 822 853 self.related_name = related_name … … 826 857 self.edit_inline = False 827 858 self.symmetrical = symmetrical 828 859 self.multiple = True 860 self.through = through -
django/core/management/validation.py
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 … … 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/core/management/sql.py
354 354 qn = connection.ops.quote_name 355 355 inline_references = connection.features.inline_fk_references 356 356 for f in opts.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/contrib/admin/options.py
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): -
tests/modeltests/invalid_models/models.py
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) 113 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) 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. 116 141 invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute. … … 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 """ -
tests/modeltests/m2m_manual/__init__.py
1 -
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 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 -
AUTHORS
137 137 Afonso Fernández Nogueira <fonzzo.django@gmail.com> 138 138 Matthew Flanagan <http://wadofstuff.blogspot.com> 139 139 Eric Floehr <eric@intellovations.com> 140 Eric Florenzano <floguy@gmail.com> 140 141 Vincent Foley <vfoleybourgon@yahoo.ca> 141 142 Rudolph Froger <rfroger@estrate.nl> 142 143 Jorge Gajon <gajon@gajon.org>