Django

Code

Ticket #6095: 6095-alpha-03.diff

File 6095-alpha-03.diff, 15.1 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 
    66from django.utils.translation import ugettext_lazy, string_concat, ungettext, ugettext as _ 
    7 from django.utils.functional import curry 
     7from django.utils.functional import curry, memoize 
    88from django.utils.encoding import smart_unicode 
    99from django.core import validators 
    1010from django import oldforms 
     
    2323 
    2424pending_lookups = {} 
    2525 
     26memoized_fk_field_reversals = {} 
     27 
     28model_db_table_cache = {} 
     29 
    2630def add_lookup(rel_cls, field): 
    2731    name = field.rel.to 
    2832    module = rel_cls.__module__ 
     
    5458    except klass.DoesNotExist: 
    5559        raise validators.ValidationError, _("Please enter a valid %s.") % f.verbose_name 
    5660 
     61def get_reverse_rel_field(from_model, to_model, related_name): 
     62    key = (from_model._meta.app_label, from_model._meta.object_name, 
     63            to_model._meta.app_label, to_model._meta.object_name, 
     64            related_name) 
     65    try: 
     66        found_field = memoized_fk_field_reversals[key] 
     67    except KeyError: 
     68        found_field = None 
     69        for field in from_model._meta.fields: 
     70            if field.__class__ in (ForeignKey, OneToOneField, ManyToManyField): 
     71                if field.rel.to == to_model: 
     72                    found_field = field 
     73                    break 
     74        memoized_fk_field_reversals[key] = found_field 
     75    return found_field 
     76 
     77def get_model_for_db_table(db_table): 
     78    for model in get_models(): 
     79        if model._meta.db_table == db_table: 
     80            return model 
     81    return None 
     82get_model_for_db_table = memoize(get_model_for_db_table, model_db_table_cache, 1) 
     83 
    5784#HACK 
    5885class RelatedField(object): 
    5986    def contribute_to_class(self, cls, name): 
     
    267294    and adds behavior for many-to-many related objects.""" 
    268295    class ManyRelatedManager(superclass): 
    269296        def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None, 
    270                 join_table=None, source_col_name=None, target_col_name=None): 
     297                join_table=None, source_col_name=None, source_attname=None, 
     298                target_attname=None, target_col_name=None): 
    271299            super(ManyRelatedManager, self).__init__() 
    272300            self.core_filters = core_filters 
    273301            self.model = model 
     
    276304            self.join_table = join_table 
    277305            self.source_col_name = source_col_name 
    278306            self.target_col_name = target_col_name 
     307            self.source_attname = source_attname 
     308            self.target_attname = target_attname 
     309            self.intermediary_model = get_model_for_db_table(self.join_table.replace('"','')) 
    279310            self._pk_val = self.instance._get_pk_val() 
    280311            if self._pk_val is None: 
    281312                raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % model) 
     
    340371 
    341372                # Add the ones that aren't there already 
    342373                for obj_id in (new_ids - existing_ids): 
    343                     cursor.execute("INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \ 
     374                    if self.intermediary_model == None: 
     375                        cursor.execute("INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \ 
    344376                        (self.join_table, source_col_name, target_col_name), 
    345377                        [self._pk_val, obj_id]) 
     378                    else: 
     379                        new_obj = self.intermediary_model() 
     380                        setattr(new_obj, self.source_attname, self._pk_val) 
     381                        setattr(new_obj, self.target_attname, obj_id) 
     382                        new_obj.save() 
    346383                transaction.commit_unless_managed() 
    347384 
    348385        def _remove_items(self, source_col_name, target_col_name, *objs): 
     
    398435        RelatedManager = create_many_related_manager(superclass) 
    399436 
    400437        qn = connection.ops.quote_name 
     438        rel_field = self.related.field 
    401439        manager = RelatedManager( 
    402440            model=rel_model, 
    403441            core_filters={'%s__pk' % self.related.field.name: instance._get_pk_val()}, 
    404442            instance=instance, 
    405443            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()) 
     444            join_table=qn(rel_field.m2m_db_table()), 
     445            source_col_name=qn(rel_field.m2m_reverse_name()), 
     446            target_col_name=qn(rel_field.m2m_column_name()), 
     447            source_attname=rel_field.m2m_reverse_attname(), 
     448            target_attname=rel_field.m2m_attname() 
    409449        ) 
    410450 
    411451        return manager 
     
    446486            symmetrical=(self.field.rel.symmetrical and instance.__class__ == rel_model), 
    447487            join_table=qn(self.field.m2m_db_table()), 
    448488            source_col_name=qn(self.field.m2m_column_name()), 
    449             target_col_name=qn(self.field.m2m_reverse_name()) 
     489            target_col_name=qn(self.field.m2m_reverse_name()), 
     490            source_attname=self.field.m2m_attname(), 
     491            target_attname=self.field.m2m_reverse_attname() 
    450492        ) 
    451493 
    452494        return manager 
     
    648690            filter_interface=kwargs.pop('filter_interface', None), 
    649691            limit_choices_to=kwargs.pop('limit_choices_to', None), 
    650692            raw_id_admin=kwargs.pop('raw_id_admin', False), 
    651             symmetrical=kwargs.pop('symmetrical', True)) 
     693            symmetrical=kwargs.pop('symmetrical', True), 
     694            through=kwargs.pop('through', None)) 
    652695        self.db_table = kwargs.pop('db_table', None) 
     696        if kwargs['rel'].through: 
     697            assert not self.db_table, "Cannot specify a db_table if an intermediary model is used." 
    653698        if kwargs["rel"].raw_id_admin: 
    654699            kwargs.setdefault("validator_list", []).append(self.isValidIDList) 
    655700        Field.__init__(self, **kwargs) 
     
    672717 
    673718    def _get_m2m_db_table(self, opts): 
    674719        "Function that can be curried to provide the m2m table name for this relation" 
    675         if self.db_table: 
     720        if self.rel.through != None: 
     721            return get_model(opts.app_label, self.rel.through)._meta.db_table 
     722        elif self.db_table: 
    676723            return self.db_table 
    677724        else: 
    678725            return '%s_%s' % (opts.db_table, self.name) 
    679726 
     727    def _get_m2m_attname(self, related): 
     728        try: 
     729            through = get_model(related.opts.app_label, self.rel.through) 
     730            field = get_reverse_rel_field(through, related.model, self.rel.related_name) 
     731            attname, column = field.get_attname_column() 
     732            return attname 
     733        except: 
     734            return None 
     735 
    680736    def _get_m2m_column_name(self, related): 
    681737        "Function that can be curried to provide the source column name for the m2m table" 
    682738        # If this is an m2m relation to self, avoid the inevitable name clash 
    683         if related.model == related.parent_model: 
     739        if self.rel.through != None: 
     740            through = get_model(related.opts.app_label, self.rel.through) 
     741            field = get_reverse_rel_field(through, related.model, self.rel.related_name) 
     742            attname, column = field.get_attname_column() 
     743            return column 
     744        elif related.model == related.parent_model: 
    684745            return 'from_' + related.model._meta.object_name.lower() + '_id' 
    685746        else: 
    686747            return related.model._meta.object_name.lower() + '_id' 
    687748 
     749    def _get_m2m_reverse_attname(self, related): 
     750        try: 
     751            through = get_model(related.opts.app_label, self.rel.through) 
     752            field = get_reverse_rel_field(through, related.parent_model, self.rel.related_name) 
     753            attname, column = field.get_attname_column() 
     754            return attname 
     755        except: 
     756            return None 
     757 
    688758    def _get_m2m_reverse_name(self, related): 
    689759        "Function that can be curried to provide the related column name for the m2m table" 
    690760        # If this is an m2m relation to self, avoid the inevitable name clash 
    691         if related.model == related.parent_model: 
     761        if self.rel.through != None: 
     762            through = get_model(related.opts.app_label, self.rel.through) 
     763            field = get_reverse_rel_field(through, related.parent_model, self.rel.related_name) 
     764            attname, column = field.get_attname_column() 
     765            return column 
     766        elif related.model == related.parent_model: 
    692767            return 'to_' + related.parent_model._meta.object_name.lower() + '_id' 
    693768        else: 
    694769            return related.parent_model._meta.object_name.lower() + '_id' 
     
    745820        # Set up the accessors for the column names on the m2m table 
    746821        self.m2m_column_name = curry(self._get_m2m_column_name, related) 
    747822        self.m2m_reverse_name = curry(self._get_m2m_reverse_name, related) 
     823        self.m2m_attname = curry(self._get_m2m_attname, related) 
     824        self.m2m_reverse_attname = curry(self._get_m2m_reverse_attname, related) 
    748825 
    749826    def set_attributes_from_rel(self): 
    750827        pass 
     
    809886 
    810887class ManyToManyRel(object): 
    811888    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): 
     889        filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True, 
     890        through = None): 
    813891        self.to = to 
    814892        self.num_in_admin = num_in_admin 
    815893        self.related_name = related_name 
     
    821899        self.raw_id_admin = raw_id_admin 
    822900        self.symmetrical = symmetrical 
    823901        self.multiple = True 
     902        self.through = through 
    824903 
    825904        assert not (self.raw_id_admin and self.filter_interface), "ManyToManyRels may not use both raw_id_admin and filter_interface" 
  • django/core/management/sql.py

    old new  
    349349    qn = connection.ops.quote_name 
    350350    inline_references = connection.features.inline_fk_references 
    351351    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
    353353            tablespace = f.db_tablespace or opts.db_tablespace 
    354354            if tablespace and connection.features.supports_tablespaces and connection.features.autoindexes_primary_keys: 
    355355                tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace, inline=True) 
  • 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    date_joined = models.DateTimeField(default=datetime.now) 
     32     
     33    def __unicode__(self): 
     34        return "%s is a member of %s" % (self.person.name, self.group.name) 
     35 
     36__test__ = {'API_TESTS':""" 
     37>>> from datetime import datetime 
     38 
     39>>> bob = Person(name = 'Bob') 
     40>>> bob.save() 
     41>>> jim = Person(name = 'Jim') 
     42>>> jim.save() 
     43>>> jane = Person(name = 'Jane') 
     44>>> jane.save() 
     45>>> rock = Group(name = 'Rock') 
     46>>> rock.save() 
     47>>> roll = Group(name = 'Roll') 
     48>>> roll.save() 
     49 
     50>>> rock.members.add(jim, jane) 
     51>>> rock.members.all() 
     52[<Person: Jim>, <Person: Jane>] 
     53 
     54>>> roll.members.add(bob, jim) 
     55>>> roll.members.all() 
     56[<Person: Bob>, <Person: Jim>] 
     57 
     58>>> jane.group_set.all() 
     59[<Group: Rock>] 
     60 
     61>>> jane.group_set.add(roll) 
     62>>> jane.group_set.all() 
     63[<Group: Rock>, <Group: Roll>] 
     64 
     65>>> jim.group_set.all() 
     66[<Group: Rock>, <Group: Roll>] 
     67 
     68# Check to make sure that the associated Membership object is created. 
     69>>> m = Membership.objects.get(person = jane, group = rock) 
     70>>> m 
     71<Membership: Jane is a member of Rock> 
     72 
     73>>> m.invite_reason = "She was just so awesome." 
     74>>> m.save() 
     75 
     76>>> Membership.objects.filter(person = jim) 
     77[<Membership: Jim is a member of Rock>, <Membership: Jim is a member of Roll>] 
     78 
     79>>> rock.custom_members.add(bob) 
     80>>> rock.custom_members.all() 
     81[<Person: Bob>] 
     82 
     83>>> jim.custom.add(rock) 
     84>>> rock.custom_members.all() 
     85[<Person: Bob>, <Person: Jim>] 
     86 
     87>>> jim.custom.all() 
     88[<Group: Rock>] 
     89 
     90>>> jim.custom_person_related_name.all() 
     91[<CustomMembership: Jim is a member of Rock>] 
     92 
     93###QUERY TESTS### 
     94# Queries involving the related model (Person, in the case of Group) use its attname 
     95>>> Group.objects.filter(members__name='Bob') 
     96[<Group: Roll>] 
     97 
     98# Queries involving the relationship model (Membership, in the case of Group) use its model name 
     99>>> Group.objects.filter(membership__invite_reason = "She was just so awesome.") 
     100[<Group: Rock>] 
     101 
     102# Queries involving the reverse related model (Group, in the case of Person) use its model name 
     103>>> Person.objects.filter(group__name="Rock") 
     104[<Person: Jim>, <Person: Jane>] 
     105 
     106# If the m2m field has specified a related_name, using that will work. 
     107>>> Person.objects.filter(custom__name="Rock") 
     108[<Person: Bob>, <Person: Jim>] 
     109 
     110# Queries involving the relationship model (Membership, in the case of Group) use its model name 
     111>>> Person.objects.filter(membership__invite_reason = "She was just so awesome.") 
     112[<Person: Jane>] 
     113 
     114# Sometimes these queries can return non-distinct resultsets. 
     115>>> Person.objects.filter(membership__date_joined__day = datetime.now().day) 
     116[<Person: Bob>, <Person: Jim>, <Person: Jim>, <Person: Jane>, <Person: Jane>] 
     117 
     118# Adding a .distinct() works to correct this. 
     119>>> Person.objects.filter(membership__date_joined__day = datetime.now().day).distinct() 
     120[<Person: Bob>, <Person: Jim>, <Person: Jane>] 
     121"""}