Ticket #6095: 6095-alpha-04.diff
File 6095-alpha-04.diff, 20.4 KB (added by , 17 years ago) |
---|
-
django/db/models/fields/related.py
1 1 from django.db import connection, transaction 2 from django.db.models import signals, get_model 2 from django.db.models import signals, get_model, get_models 3 3 from django.db.models.fields import AutoField, Field, IntegerField, PositiveIntegerField, PositiveSmallIntegerField, get_ul_class 4 4 from django.db.models.related import RelatedObject 5 5 from django.utils.text import capfirst 6 6 from django.utils.translation import ugettext_lazy, string_concat, ungettext, ugettext as _ 7 from django.utils.functional import curry 7 from django.utils.functional import curry, memoize 8 8 from django.utils.encoding import smart_unicode 9 9 from django.core import validators 10 10 from django import oldforms … … 23 23 24 24 pending_lookups = {} 25 25 26 memoized_fk_field_reversals = {} 27 28 model_db_table_cache = {} 29 26 30 def add_lookup(rel_cls, field): 27 31 name = field.rel.to 28 32 module = rel_cls.__module__ … … 54 58 except klass.DoesNotExist: 55 59 raise validators.ValidationError, _("Please enter a valid %s.") % f.verbose_name 56 60 61 def get_reverse_rel_field(from_model, to_model, related_name): 62 "Gets the related field which points from one model to another." 63 key = (from_model._meta.app_label, from_model._meta.object_name, 64 to_model._meta.app_label, to_model._meta.object_name, 65 related_name) 66 try: 67 found_field = memoized_fk_field_reversals[key] 68 except KeyError: 69 found_field = None 70 for field in from_model._meta.fields: 71 if field.__class__ in (ForeignKey, OneToOneField, ManyToManyField): 72 if field.rel.to == to_model: 73 found_field = field 74 break 75 memoized_fk_field_reversals[key] = found_field 76 return found_field 77 78 def get_model_for_db_table(db_table): 79 "Gets a model class from a db_table string." 80 for model in get_models(): 81 if model._meta.db_table == db_table: 82 return model 83 return None 84 get_model_for_db_table = memoize(get_model_for_db_table, model_db_table_cache, 1) 85 57 86 #HACK 58 87 class RelatedField(object): 59 88 def contribute_to_class(self, cls, name): … … 267 296 and adds behavior for many-to-many related objects.""" 268 297 class ManyRelatedManager(superclass): 269 298 def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None, 270 join_table=None, source_col_name=None, target_col_name=None): 299 join_table=None, source_col_name=None, source_attname=None, 300 target_attname=None, target_col_name=None): 271 301 super(ManyRelatedManager, self).__init__() 272 302 self.core_filters = core_filters 273 303 self.model = model … … 276 306 self.join_table = join_table 277 307 self.source_col_name = source_col_name 278 308 self.target_col_name = target_col_name 309 self.source_attname = source_attname 310 self.target_attname = target_attname 311 self.intermediary_model = get_model_for_db_table(self.join_table.replace('"','')) 279 312 self._pk_val = self.instance._get_pk_val() 280 313 if self._pk_val is None: 281 314 raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % model) … … 340 373 341 374 # Add the ones that aren't there already 342 375 for obj_id in (new_ids - existing_ids): 343 cursor.execute("INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \ 376 if self.intermediary_model == None: 377 cursor.execute("INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \ 344 378 (self.join_table, source_col_name, target_col_name), 345 379 [self._pk_val, obj_id]) 380 else: 381 new_obj = self.intermediary_model() 382 setattr(new_obj, self.source_attname, self._pk_val) 383 setattr(new_obj, self.target_attname, obj_id) 384 new_obj.save() 346 385 transaction.commit_unless_managed() 347 386 348 387 def _remove_items(self, source_col_name, target_col_name, *objs): … … 398 437 RelatedManager = create_many_related_manager(superclass) 399 438 400 439 qn = connection.ops.quote_name 440 rel_field = self.related.field 401 441 manager = RelatedManager( 402 442 model=rel_model, 403 443 core_filters={'%s__pk' % self.related.field.name: instance._get_pk_val()}, 404 444 instance=instance, 405 445 symmetrical=False, 406 join_table=qn(self.related.field.m2m_db_table()), 407 source_col_name=qn(self.related.field.m2m_reverse_name()), 408 target_col_name=qn(self.related.field.m2m_column_name()) 446 join_table=qn(rel_field.m2m_db_table()), 447 source_col_name=qn(rel_field.m2m_reverse_name()), 448 target_col_name=qn(rel_field.m2m_column_name()), 449 source_attname=rel_field.m2m_reverse_attname(), 450 target_attname=rel_field.m2m_attname() 409 451 ) 410 452 411 453 return manager … … 446 488 symmetrical=(self.field.rel.symmetrical and instance.__class__ == rel_model), 447 489 join_table=qn(self.field.m2m_db_table()), 448 490 source_col_name=qn(self.field.m2m_column_name()), 449 target_col_name=qn(self.field.m2m_reverse_name()) 491 target_col_name=qn(self.field.m2m_reverse_name()), 492 source_attname=self.field.m2m_attname(), 493 target_attname=self.field.m2m_reverse_attname() 450 494 ) 451 495 452 496 return manager … … 648 692 filter_interface=kwargs.pop('filter_interface', None), 649 693 limit_choices_to=kwargs.pop('limit_choices_to', None), 650 694 raw_id_admin=kwargs.pop('raw_id_admin', False), 651 symmetrical=kwargs.pop('symmetrical', True)) 695 symmetrical=kwargs.pop('symmetrical', True), 696 through=kwargs.pop('through', None)) 652 697 self.db_table = kwargs.pop('db_table', None) 698 if kwargs['rel'].through: 699 assert not self.db_table, "Cannot specify a db_table if an intermediary model is used." 653 700 if kwargs["rel"].raw_id_admin: 654 701 kwargs.setdefault("validator_list", []).append(self.isValidIDList) 655 702 Field.__init__(self, **kwargs) … … 672 719 673 720 def _get_m2m_db_table(self, opts): 674 721 "Function that can be curried to provide the m2m table name for this relation" 675 if self.db_table: 722 if self.rel.through != None: 723 return get_model(opts.app_label, self.rel.through)._meta.db_table 724 elif self.db_table: 676 725 return self.db_table 677 726 else: 678 727 return '%s_%s' % (opts.db_table, self.name) 679 728 729 def _get_m2m_attname(self, related): 730 try: 731 through = get_model(related.opts.app_label, self.rel.through) 732 field = get_reverse_rel_field(through, related.model, self.rel.related_name) 733 attname, column = field.get_attname_column() 734 return attname 735 except: 736 return None 737 680 738 def _get_m2m_column_name(self, related): 681 739 "Function that can be curried to provide the source column name for the m2m table" 682 740 # If this is an m2m relation to self, avoid the inevitable name clash 683 if related.model == related.parent_model: 741 if self.rel.through != None: 742 through = get_model(related.opts.app_label, self.rel.through) 743 field = get_reverse_rel_field(through, related.model, self.rel.related_name) 744 attname, column = field.get_attname_column() 745 return column 746 elif related.model == related.parent_model: 684 747 return 'from_' + related.model._meta.object_name.lower() + '_id' 685 748 else: 686 749 return related.model._meta.object_name.lower() + '_id' 687 750 751 def _get_m2m_reverse_attname(self, related): 752 try: 753 through = get_model(related.opts.app_label, self.rel.through) 754 field = get_reverse_rel_field(through, related.parent_model, self.rel.related_name) 755 attname, column = field.get_attname_column() 756 return attname 757 except: 758 return None 759 688 760 def _get_m2m_reverse_name(self, related): 689 761 "Function that can be curried to provide the related column name for the m2m table" 690 762 # If this is an m2m relation to self, avoid the inevitable name clash 691 if related.model == related.parent_model: 763 if self.rel.through != None: 764 through = get_model(related.opts.app_label, self.rel.through) 765 field = get_reverse_rel_field(through, related.parent_model, self.rel.related_name) 766 attname, column = field.get_attname_column() 767 return column 768 elif related.model == related.parent_model: 692 769 return 'to_' + related.parent_model._meta.object_name.lower() + '_id' 693 770 else: 694 771 return related.parent_model._meta.object_name.lower() + '_id' … … 745 822 # Set up the accessors for the column names on the m2m table 746 823 self.m2m_column_name = curry(self._get_m2m_column_name, related) 747 824 self.m2m_reverse_name = curry(self._get_m2m_reverse_name, related) 825 self.m2m_attname = curry(self._get_m2m_attname, related) 826 self.m2m_reverse_attname = curry(self._get_m2m_reverse_attname, related) 748 827 749 828 def set_attributes_from_rel(self): 750 829 pass … … 809 888 810 889 class ManyToManyRel(object): 811 890 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): 891 filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True, 892 through = None): 813 893 self.to = to 814 894 self.num_in_admin = num_in_admin 815 895 self.related_name = related_name … … 821 901 self.raw_id_admin = raw_id_admin 822 902 self.symmetrical = symmetrical 823 903 self.multiple = True 904 self.through = through 824 905 825 906 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
349 349 qn = connection.ops.quote_name 350 350 inline_references = connection.features.inline_fk_references 351 351 for f in opts.many_to_many: 352 if not isinstance(f.rel, generic.GenericRel) :352 if not isinstance(f.rel, generic.GenericRel) and getattr(f.rel, 'through', None) == None: 353 353 tablespace = f.db_tablespace or opts.db_tablespace 354 354 if tablespace and connection.features.supports_tablespaces and connection.features.autoindexes_primary_keys: 355 355 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 Membership(models.Model): 128 person = models.ForeignKey(Person) 129 group = models.ForeignKey(Group) 130 115 131 model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute. 116 132 invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute. 117 133 invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute. … … 197 213 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 214 invalid_models.missingrelations: 'rel2' has m2m relation with model Rel2, which has not been installed 199 215 invalid_models.missingrelations: 'rel1' has relation with model Rel1, which has not been installed 216 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. 217 invalid_models.missingmanualm2mmodel: missing_m2m has a manually-defined m2m relationship through a model (MissingM2MModel) which does not exist. 200 218 """ -
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 16 def __unicode__(self): 17 return self.name 18 19 class Membership(models.Model): 20 person = models.ForeignKey(Person) 21 group = models.ForeignKey(Group) 22 date_joined = models.DateTimeField(default=datetime.now) 23 invite_reason = models.CharField(max_length=64, null=True, blank=True) 24 25 def __unicode__(self): 26 return "%s is a member of %s" % (self.person.name, self.group.name) 27 28 class CustomMembership(models.Model): 29 person = models.ForeignKey(Person, db_column="custom_person_column", related_name="custom_person_related_name") 30 group = models.ForeignKey(Group) 31 weird_fk = models.ForeignKey(Membership, null=True) 32 date_joined = models.DateTimeField(default=datetime.now) 33 34 def __unicode__(self): 35 return "%s is a member of %s" % (self.person.name, self.group.name) 36 37 __test__ = {'API_TESTS':""" 38 >>> from datetime import datetime 39 40 >>> bob = Person(name = 'Bob') 41 >>> bob.save() 42 >>> jim = Person(name = 'Jim') 43 >>> jim.save() 44 >>> jane = Person(name = 'Jane') 45 >>> jane.save() 46 >>> rock = Group(name = 'Rock') 47 >>> rock.save() 48 >>> roll = Group(name = 'Roll') 49 >>> roll.save() 50 51 >>> rock.members.add(jim, jane) 52 >>> rock.members.all() 53 [<Person: Jim>, <Person: Jane>] 54 55 >>> roll.members.add(bob, jim) 56 >>> roll.members.all() 57 [<Person: Bob>, <Person: Jim>] 58 59 >>> jane.group_set.all() 60 [<Group: Rock>] 61 62 >>> jane.group_set.add(roll) 63 >>> jane.group_set.all() 64 [<Group: Rock>, <Group: Roll>] 65 66 >>> jim.group_set.all() 67 [<Group: Rock>, <Group: Roll>] 68 69 # Check to make sure that the associated Membership object is created. 70 >>> m = Membership.objects.get(person = jane, group = rock) 71 >>> m 72 <Membership: Jane is a member of Rock> 73 74 # Setting some date_joined dates 75 >>> m.invite_reason = "She was just so awesome." 76 >>> m.date_joined = datetime(2004, 1, 1) 77 >>> m.save() 78 79 >>> m = Membership.objects.get(person = jane, group = roll) 80 >>> m.date_joined = datetime(2004, 1, 1) 81 >>> m.save() 82 83 >>> m = Membership.objects.get(person = bob, group = roll) 84 >>> m.date_joined = datetime(2004, 1, 1) 85 >>> m.save() 86 87 >>> Membership.objects.filter(person = jim) 88 [<Membership: Jim is a member of Rock>, <Membership: Jim is a member of Roll>] 89 90 >>> rock.custom_members.add(bob) 91 >>> rock.custom_members.all() 92 [<Person: Bob>] 93 94 >>> jim.custom.add(rock) 95 >>> rock.custom_members.all() 96 [<Person: Bob>, <Person: Jim>] 97 98 >>> jim.custom.all() 99 [<Group: Rock>] 100 101 >>> jim.custom_person_related_name.all() 102 [<CustomMembership: Jim is a member of Rock>] 103 104 ###QUERY TESTS### 105 # Queries involving the related model (Person, in the case of Group) use its attname 106 >>> Group.objects.filter(members__name='Bob') 107 [<Group: Roll>] 108 109 # Queries involving the relationship model (Membership, in the case of Group) use its model name 110 >>> Group.objects.filter(membership__invite_reason = "She was just so awesome.") 111 [<Group: Rock>] 112 113 # Queries involving the reverse related model (Group, in the case of Person) use its model name 114 >>> Person.objects.filter(group__name="Rock") 115 [<Person: Jim>, <Person: Jane>] 116 117 # If the m2m field has specified a related_name, using that will work. 118 >>> Person.objects.filter(custom__name="Rock") 119 [<Person: Bob>, <Person: Jim>] 120 121 # Queries involving the relationship model (Membership, in the case of Group) use its model name 122 >>> Person.objects.filter(membership__invite_reason = "She was just so awesome.") 123 [<Person: Jane>] 124 125 # Let's see all of the groups that Jane joined after 1 Jan 2005: 126 >>> Group.objects.filter(membership__date_joined__gt = datetime(2005, 1, 1)) 127 [<Group: Rock>, <Group: Roll>] 128 129 # Now let's see all of the people that have joined Rock since 1 Jan 2005: 130 >>> Person.objects.filter(membership__date_joined__gt = datetime(2005, 1, 1)) 131 [<Person: Jim>, <Person: Jim>] 132 133 # Oops, that returned non-distinct results, let's fix that: 134 >>> Person.objects.filter(membership__date_joined__gt = datetime(2005, 1, 1)).distinct() 135 [<Person: Jim>] 136 """} 137 No newline at end of file