Django

Code

Ticket #6095: 6095-beta-01.diff

File 6095-beta-01.diff, 21.5 kB (added by floguy, 1 year ago)
  • django/db/models/fields/related.py

    old new  
    11from django.db import connection, transaction 
    2 from django.db.models import signals, get_model 
     2from django.db.models import signals, get_model, get_models 
    33from django.db.models.fields import AutoField, Field, IntegerField, PositiveIntegerField, PositiveSmallIntegerField, get_ul_class 
    44from django.db.models.related import RelatedObject 
    55from django.utils.text import capfirst 
     
    5454    except klass.DoesNotExist: 
    5555        raise validators.ValidationError, _("Please enter a valid %s.") % f.verbose_name 
    5656 
     57def 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 
     65def get_model_for_db_table(db_table): 
     66    "Gets a model class from a db_table string." 
     67    for model in get_models(): 
     68        if model._meta.db_table == db_table: 
     69            return model 
     70    return None 
     71 
    5772#HACK 
    5873class RelatedField(object): 
    5974    def contribute_to_class(self, cls, name): 
     
    267282    and adds behavior for many-to-many related objects.""" 
    268283    class ManyRelatedManager(superclass): 
    269284        def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None, 
    270                 join_table=None, source_col_name=None, target_col_name=None): 
     285                join_table=None, source_col_name=None, source_attname=None, 
     286                target_attname=None, target_col_name=None): 
    271287            super(ManyRelatedManager, self).__init__() 
    272288            self.core_filters = core_filters 
    273289            self.model = model 
     
    276292            self.join_table = join_table 
    277293            self.source_col_name = source_col_name 
    278294            self.target_col_name = target_col_name 
     295            self.source_attname = source_attname 
     296            self.target_attname = target_attname 
     297            self.intermediary_model = get_model_for_db_table(self.join_table.replace('"','')) 
    279298            self._pk_val = self.instance._get_pk_val() 
    280299            if self._pk_val is None: 
    281300                raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % model) 
     
    340359 
    341360                # Add the ones that aren't there already 
    342361                for obj_id in (new_ids - existing_ids): 
    343                     cursor.execute("INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \ 
     362                    if self.intermediary_model == None: 
     363                        cursor.execute("INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \ 
    344364                        (self.join_table, source_col_name, target_col_name), 
    345365                        [self._pk_val, obj_id]) 
     366                    else: 
     367                        new_obj = self.intermediary_model() 
     368                        setattr(new_obj, self.source_attname, self._pk_val) 
     369                        setattr(new_obj, self.target_attname, obj_id) 
     370                        new_obj.save() 
    346371                transaction.commit_unless_managed() 
    347372 
    348373        def _remove_items(self, source_col_name, target_col_name, *objs): 
     
    398423        RelatedManager = create_many_related_manager(superclass) 
    399424 
    400425        qn = connection.ops.quote_name 
     426        rel_field = self.related.field 
    401427        manager = RelatedManager( 
    402428            model=rel_model, 
    403429            core_filters={'%s__pk' % self.related.field.name: instance._get_pk_val()}, 
    404430            instance=instance, 
    405431            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()) 
     432            join_table=qn(rel_field.m2m_db_table()), 
     433            source_col_name=qn(rel_field.m2m_reverse_name()), 
     434            target_col_name=qn(rel_field.m2m_column_name()), 
     435            source_attname=rel_field.m2m_reverse_attname(), 
     436            target_attname=rel_field.m2m_attname() 
    409437        ) 
    410438 
    411439        return manager 
     
    446474            symmetrical=(self.field.rel.symmetrical and instance.__class__ == rel_model), 
    447475            join_table=qn(self.field.m2m_db_table()), 
    448476            source_col_name=qn(self.field.m2m_column_name()), 
    449             target_col_name=qn(self.field.m2m_reverse_name()) 
     477            target_col_name=qn(self.field.m2m_reverse_name()), 
     478            source_attname=self.field.m2m_attname(), 
     479            target_attname=self.field.m2m_reverse_attname() 
    450480        ) 
    451481 
    452482        return manager 
     
    648678            filter_interface=kwargs.pop('filter_interface', None), 
    649679            limit_choices_to=kwargs.pop('limit_choices_to', None), 
    650680            raw_id_admin=kwargs.pop('raw_id_admin', False), 
    651             symmetrical=kwargs.pop('symmetrical', True)) 
     681            symmetrical=kwargs.pop('symmetrical', True), 
     682            through=kwargs.pop('through', None)) 
    652683        self.db_table = kwargs.pop('db_table', None) 
     684        if kwargs['rel'].through: 
     685            assert not self.db_table, "Cannot specify a db_table if an intermediary model is used." 
    653686        if kwargs["rel"].raw_id_admin: 
    654687            kwargs.setdefault("validator_list", []).append(self.isValidIDList) 
    655688        Field.__init__(self, **kwargs) 
     
    672705 
    673706    def _get_m2m_db_table(self, opts): 
    674707        "Function that can be curried to provide the m2m table name for this relation" 
    675         if self.db_table: 
     708        if self.rel.through != None: 
     709            return get_model(opts.app_label, self.rel.through)._meta.db_table 
     710        elif self.db_table: 
    676711            return self.db_table 
    677712        else: 
    678713            return '%s_%s' % (opts.db_table, self.name) 
    679714 
     715    def _get_m2m_attname(self, related): 
     716        try: 
     717            through = get_model(related.opts.app_label, self.rel.through) 
     718            field = get_reverse_rel_field(through, related.model, self.rel.related_name) 
     719            attname, column = field.get_attname_column() 
     720            return attname 
     721        except: 
     722            return None 
     723 
    680724    def _get_m2m_column_name(self, related): 
    681725        "Function that can be curried to provide the source column name for the m2m table" 
    682726        # If this is an m2m relation to self, avoid the inevitable name clash 
    683         if related.model == related.parent_model: 
     727        if self.rel.through != None: 
     728            through = get_model(related.opts.app_label, self.rel.through) 
     729            field = get_reverse_rel_field(through, related.model, self.rel.related_name) 
     730            attname, column = field.get_attname_column() 
     731            return column 
     732        elif related.model == related.parent_model: 
    684733            return 'from_' + related.model._meta.object_name.lower() + '_id' 
    685734        else: 
    686735            return related.model._meta.object_name.lower() + '_id' 
    687736 
     737    def _get_m2m_reverse_attname(self, related): 
     738        try: 
     739            through = get_model(related.opts.app_label, self.rel.through) 
     740            field = get_reverse_rel_field(through, related.parent_model, self.rel.related_name) 
     741            attname, column = field.get_attname_column() 
     742            return attname 
     743        except: 
     744            return None 
     745 
    688746    def _get_m2m_reverse_name(self, related): 
    689747        "Function that can be curried to provide the related column name for the m2m table" 
    690748        # If this is an m2m relation to self, avoid the inevitable name clash 
    691         if related.model == related.parent_model: 
     749        if self.rel.through != None: 
     750            through = get_model(related.opts.app_label, self.rel.through) 
     751            field = get_reverse_rel_field(through, related.parent_model, self.rel.related_name) 
     752            attname, column = field.get_attname_column() 
     753            return column 
     754        elif related.model == related.parent_model: 
    692755            return 'to_' + related.parent_model._meta.object_name.lower() + '_id' 
    693756        else: 
    694757            return related.parent_model._meta.object_name.lower() + '_id' 
     
    745808        # Set up the accessors for the column names on the m2m table 
    746809        self.m2m_column_name = curry(self._get_m2m_column_name, related) 
    747810        self.m2m_reverse_name = curry(self._get_m2m_reverse_name, related) 
     811        self.m2m_attname = curry(self._get_m2m_attname, related) 
     812        self.m2m_reverse_attname = curry(self._get_m2m_reverse_attname, related) 
    748813 
    749814    def set_attributes_from_rel(self): 
    750815        pass 
     
    809874 
    810875class ManyToManyRel(object): 
    811876    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): 
     877        filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True, 
     878        through = None): 
    813879        self.to = to 
    814880        self.num_in_admin = num_in_admin 
    815881        self.related_name = related_name 
     
    821887        self.raw_id_admin = raw_id_admin 
    822888        self.symmetrical = symmetrical 
    823889        self.multiple = True 
     890        self.through = through 
    824891 
    825892        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

    old new  
    104104                        if r.get_accessor_name() == rel_query_name: 
    105105                            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)) 
    106106 
     107        seen_intermediary_signatures = [] 
     108 
    107109        for i, f in enumerate(opts.many_to_many): 
    108110            # Check to see if the related m2m field will clash with any 
    109111            # existing fields, m2m fields, m2m related objects or related objects 
     
    113115                # so skip the next section 
    114116                if isinstance(f.rel.to, (str, unicode)): 
    115117                    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, is_related = False, False, False 
     132                    for field in intermediary_model._meta.fields: 
     133                        if field.rel: 
     134                            if field.rel.to == f.rel.to: 
     135                                is_related = True 
     136                                seen_related_fk = True 
     137                            elif field.rel.to == cls: 
     138                                is_related = True 
     139                                seen_this_fk = True 
     140                        if is_related == True: 
     141                            is_related = False 
     142                            if field.default == None and field.null == False: 
     143                                e.add(opts, "%s is an intermediary model which has a non-nullable field (%s) with no default value" % (intermediary_model._meta.object_name, field.name)) 
     144                    if not seen_related_fk or not seen_this_fk: 
     145                        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)) 
    116146 
    117147            rel_opts = f.rel.to._meta 
    118148            rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name() 
  • django/core/management/sql.py

    old new  
    352352    qn = connection.ops.quote_name 
    353353    inline_references = connection.features.inline_fk_references 
    354354    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
    356356            tablespace = f.db_tablespace or opts.db_tablespace 
    357357            if tablespace and connection.features.supports_tablespaces and connection.features.autoindexes_primary_keys: 
    358358                tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace, inline=True) 
  • tests/modeltests/invalid_models/models.py

    old new  
    111111class MissingRelations(models.Model): 
    112112    rel1 = models.ForeignKey("Rel1") 
    113113    rel2 = models.ManyToManyField("Rel2") 
     114     
     115class MissingManualM2MModel(models.Model): 
     116    name = models.CharField(max_length=5) 
     117    missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel") 
     118     
     119class Person(models.Model): 
     120    name = models.CharField(max_length=5) 
    114121 
     122class 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 
     127class 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 
     132class Membership(models.Model): 
     133    person = models.ForeignKey(Person) 
     134    group = models.ForeignKey(Group) 
     135    not_default_or_null = models.CharField(max_length=5) 
     136 
     137class MembershipMissingFK(models.Model): 
     138    person = models.ForeignKey(Person) 
     139 
    115140model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute. 
    116141invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute. 
    117142invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute. 
     
    197222invalid_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'. 
    198223invalid_models.missingrelations: 'rel2' has m2m relation with model Rel2, which has not been installed 
    199224invalid_models.missingrelations: 'rel1' has relation with model Rel1, which has not been installed 
     225invalid_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. 
     226invalid_models.missingmanualm2mmodel: missing_m2m has a manually-defined m2m relationship through a model (MissingM2MModel) which does not exist. 
     227invalid_models.grouptwo: primary has a manualy-defined m2m relationship through a model (Membership) which does not have foreign keys to Person and GroupTwo 
     228invalid_models.grouptwo: secondary has a manualy-defined m2m relationship through a model (MembershipMissingFK) which does not have foreign keys to Group and GroupTwo 
    200229""" 
  • tests/modeltests/m2m_manual/models.py

    old new  
     1from django.db import models 
     2from datetime import datetime 
     3 
     4# M2M described on one of the models 
     5class Person(models.Model): 
     6    name = models.CharField(max_length=128) 
     7 
     8    def __unicode__(self): 
     9        return self.name 
     10 
     11class 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 
     19class 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 
     28class 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 awesome." 
     76>>> m.date_joined = datetime(2006, 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  
     106# attname 
     107>>> Group.objects.filter(members__name='Bob') 
     108[<Group: Roll>] 
     109 
     110# Queries involving the relationship model (Membership, in the case of Group)  
     111# use its model name 
     112>>> Group.objects.filter(membership__invite_reason = "She was just awesome.") 
     113[<Group: Rock>] 
     114 
     115# Queries involving the reverse related model (Group, in the case of Person)  
     116# use its model name 
     117>>> Person.objects.filter(group__name="Rock") 
     118[<Person: Jim>, <Person: Jane>] 
     119 
     120# If the m2m field has specified a related_name, using that will work. 
     121>>> Person.objects.filter(custom__name="Rock") 
     122[<Person: Bob>, <Person: Jim>] 
     123 
     124# Queries involving the relationship model (Membership, in the case of Group)  
     125# use its model name 
     126>>> Person.objects.filter(membership__invite_reason = "She was just awesome.") 
     127[<Person: Jane>] 
     128 
     129# Let's see all of the groups that Jane joined after 1 Jan 2005: 
     130>>> Group.objects.filter(membership__date_joined__gt = datetime(2005, 1, 1),  
     131... membership__person = jane) 
     132[<Group: Rock>] 
     133 
     134# Now let's see all of the people that have joined Rock since 1 Jan 2005: 
     135>>> Person.objects.filter(membership__date_joined__gt = datetime(2005, 1, 1),  
     136... membership__group = rock) 
     137[<Person: Jim>, <Person: Jane>] 
     138 
     139# Conceivably, queries through membership could return non-unique querysets. 
     140# To demonstrate this, query for all people who have joined a group after 2004: 
     141>>> Person.objects.filter(membership__date_joined__gt = datetime(2004, 1, 1)) 
     142[<Person: Jim>, <Person: Jim>, <Person: Jane>] 
     143 
     144# Jim showed up twice, because he joined two groups ('Rock', and 'Roll'): 
     145>>> [(m.person.name, m.group.name) for m in  
     146... Membership.objects.filter(date_joined__gt = datetime(2004, 1, 1))] 
     147[(u'Jim', u'Rock'), (u'Jim', u'Roll'), (u'Jane', u'Rock')] 
     148 
     149# QuerySet's distinct() method can correct this problem. 
     150>>> Person.objects.filter(membership__date_joined__gt = datetime(2004, 1, 1)).distinct() 
     151[<Person: Jim>, <Person: Jane>] 
     152"""} 
  • AUTHORS

    old new  
    351351    ymasuda@ethercube.com 
    352352    Jarek Zgoda <jarek.zgoda@gmail.com> 
    353353    Cheng Zhang 
     354    Eric Florenzano <floguy@gmail.com> 
    354355 
    355356A big THANK YOU goes to: 
    356357