Ticket #6095: 6095-beta-05.diff

File 6095-beta-05.diff, 18.2 KB (added by floguy, 16 years ago)

Made add() and remove() dynamically added depending on intermediary model. Added an attribute on ManyToManyField which specifies whether table creation is necessary. Fixed a few typos in the error messages. Moved get_reverse_rel_field logic into django.db.models.options as get_related_object() and refactored code accordingly. More to come.

  • AUTHORS

    diff --git a/AUTHORS b/AUTHORS
    index be18d21..49a5b31 100644
    a b answer newbie questions, and generally made Django that much better:  
    133133    Afonso Fernández Nogueira <fonzzo.django@gmail.com>
    134134    Matthew Flanagan <http://wadofstuff.blogspot.com>
    135135    Eric Floehr <eric@intellovations.com>
     136    Eric Florenzano <floguy@gmail.com>
    136137    Vincent Foley <vfoleybourgon@yahoo.ca>
    137138    Rudolph Froger <rfroger@estrate.nl>
    138139    Jorge Gajon <gajon@gajon.org>
  • django/core/management/sql.py

    diff --git a/django/core/management/sql.py b/django/core/management/sql.py
    index 15bffce..5430dea 100644
    a b def many_to_many_sql_for_model(model, style):  
    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 f.creates_table:
    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)
  • django/core/management/validation.py

    diff --git a/django/core/management/validation.py b/django/core/management/validation.py
    index bc9faae..a84cf35 100644
    a b def get_validation_errors(outfile, app=None):  
    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
    def get_validation_errors(outfile, app=None):  
    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 = 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 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))
    116140
    117141            rel_opts = f.rel.to._meta
    118142            rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
  • django/db/models/fields/related.py

    diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
    index 7d28ba1..9da7073 100644
    a b from django.core import validators  
    1010from django import oldforms
    1111from django import newforms as forms
    1212from django.dispatch import dispatcher
     13from new import instancemethod
    1314
    1415try:
    1516    set
    class ForeignRelatedObjectsDescriptor(object):  
    262263            manager.clear()
    263264        manager.add(*value)
    264265
    265 def create_many_related_manager(superclass):
     266def create_many_related_manager(superclass, through=False):
    266267    """Creates a manager that subclasses 'superclass' (which is a Manager)
    267268    and adds behavior for many-to-many related objects."""
    268269    class ManyRelatedManager(superclass):
    269270        def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None,
    270                 join_table=None, source_col_name=None, target_col_name=None):
     271                join_table=None, source_col_name=None, target_col_name=None,
     272                through=None):
    271273            super(ManyRelatedManager, self).__init__()
    272274            self.core_filters = core_filters
    273275            self.model = model
    def create_many_related_manager(superclass):  
    276278            self.join_table = join_table
    277279            self.source_col_name = source_col_name
    278280            self.target_col_name = target_col_name
     281            self.through = through
    279282            self._pk_val = self.instance._get_pk_val()
    280283            if self._pk_val is None:
    281284                raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % model)
    def create_many_related_manager(superclass):  
    283286        def get_query_set(self):
    284287            return superclass.get_query_set(self).filter(**(self.core_filters))
    285288
    286         def add(self, *objs):
    287             self._add_items(self.source_col_name, self.target_col_name, *objs)
    288 
    289             # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
    290             if self.symmetrical:
    291                 self._add_items(self.target_col_name, self.source_col_name, *objs)
    292         add.alters_data = True
    293 
    294         def remove(self, *objs):
    295             self._remove_items(self.source_col_name, self.target_col_name, *objs)
    296 
    297             # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table
    298             if self.symmetrical:
    299                 self._remove_items(self.target_col_name, self.source_col_name, *objs)
    300         remove.alters_data = True
    301 
    302289        def clear(self):
    303290            self._clear_items(self.source_col_name)
    304291
    def create_many_related_manager(superclass):  
    375362                [self._pk_val])
    376363            transaction.commit_unless_managed()
    377364
     365    def add(self, *objs):
     366        self._add_items(self.source_col_name, self.target_col_name, *objs)
     367        # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
     368        if self.symmetrical:
     369            self._add_items(self.target_col_name, self.source_col_name, *objs)
     370    add.alters_data = True
     371
     372    def remove(self, *objs):
     373        self._remove_items(self.source_col_name, self.target_col_name, *objs)
     374        # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table
     375        if self.symmetrical:
     376            self._remove_items(self.target_col_name, self.source_col_name, *objs)
     377    remove.alters_data = True
     378
     379    if not through:
     380        ManyRelatedManager.add = instancemethod(add, None, ManyRelatedManager)
     381        ManyRelatedManager.remove = instancemethod(remove, None, ManyRelatedManager)
     382
    378383    return ManyRelatedManager
    379384
    380385class ManyRelatedObjectsDescriptor(object):
    class ManyRelatedObjectsDescriptor(object):  
    395400        # model's default manager.
    396401        rel_model = self.related.model
    397402        superclass = rel_model._default_manager.__class__
    398         RelatedManager = create_many_related_manager(superclass)
     403        RelatedManager = create_many_related_manager(superclass, self.related.field.rel.through)
    399404
    400405        qn = connection.ops.quote_name
    401406        manager = RelatedManager(
    class ManyRelatedObjectsDescriptor(object):  
    405410            symmetrical=False,
    406411            join_table=qn(self.related.field.m2m_db_table()),
    407412            source_col_name=qn(self.related.field.m2m_reverse_name()),
    408             target_col_name=qn(self.related.field.m2m_column_name())
     413            target_col_name=qn(self.related.field.m2m_column_name()),
     414            through=self.related.field.rel.through
    409415        )
    410416
    411417        return manager
    class ManyRelatedObjectsDescriptor(object):  
    414420        if instance is None:
    415421            raise AttributeError, "Manager must be accessed via instance"
    416422
     423        through = getattr(self.related.field.rel, 'through', None)
     424        if through:
     425            raise AttributeError, "Cannot set values on a ManyToManyField which specifies a through model.  Use %s's Manager instead." % through
     426       
    417427        manager = self.__get__(instance)
    418428        manager.clear()
    419429        manager.add(*value)
    class ReverseManyRelatedObjectsDescriptor(object):  
    436446        # model's default manager.
    437447        rel_model=self.field.rel.to
    438448        superclass = rel_model._default_manager.__class__
    439         RelatedManager = create_many_related_manager(superclass)
     449        RelatedManager = create_many_related_manager(superclass, self.field.rel.through)
    440450
    441451        qn = connection.ops.quote_name
    442452        manager = RelatedManager(
    class ReverseManyRelatedObjectsDescriptor(object):  
    446456            symmetrical=(self.field.rel.symmetrical and instance.__class__ == rel_model),
    447457            join_table=qn(self.field.m2m_db_table()),
    448458            source_col_name=qn(self.field.m2m_column_name()),
    449             target_col_name=qn(self.field.m2m_reverse_name())
     459            target_col_name=qn(self.field.m2m_reverse_name()),
     460            through=self.field.rel.through
    450461        )
    451462
    452463        return manager
    class ReverseManyRelatedObjectsDescriptor(object):  
    455466        if instance is None:
    456467            raise AttributeError, "Manager must be accessed via instance"
    457468
     469        through = getattr(self.field.rel, 'through', None)
     470        if through:
     471            raise AttributeError, "Cannot set values on a ManyToManyField which specifies a through model.  Use %s's Manager instead." % through
     472
    458473        manager = self.__get__(instance)
    459474        manager.clear()
    460475        manager.add(*value)
    class ManyToManyField(RelatedField, Field):  
    648663            filter_interface=kwargs.pop('filter_interface', None),
    649664            limit_choices_to=kwargs.pop('limit_choices_to', None),
    650665            raw_id_admin=kwargs.pop('raw_id_admin', False),
    651             symmetrical=kwargs.pop('symmetrical', True))
     666            symmetrical=kwargs.pop('symmetrical', True),
     667            through=kwargs.pop('through', None))
    652668        self.db_table = kwargs.pop('db_table', None)
     669        if kwargs['rel'].through:
     670            self.creates_table = False
     671            assert not self.db_table, "Cannot specify a db_table if an intermediary model is used."
     672        else:
     673            self.creates_table = True
    653674        if kwargs["rel"].raw_id_admin:
    654675            kwargs.setdefault("validator_list", []).append(self.isValidIDList)
    655676        Field.__init__(self, **kwargs)
    class ManyToManyField(RelatedField, Field):  
    672693
    673694    def _get_m2m_db_table(self, opts):
    674695        "Function that can be curried to provide the m2m table name for this relation"
    675         if self.db_table:
     696        if self.rel.through != None:
     697            return get_model(opts.app_label, self.rel.through)._meta.db_table
     698        elif self.db_table:
    676699            return self.db_table
    677700        else:
    678701            return '%s_%s' % (opts.db_table, self.name)
    class ManyToManyField(RelatedField, Field):  
    680703    def _get_m2m_column_name(self, related):
    681704        "Function that can be curried to provide the source column name for the m2m table"
    682705        # If this is an m2m relation to self, avoid the inevitable name clash
    683         if related.model == related.parent_model:
     706        if self.rel.through != None:
     707            field = related.model._meta.get_related_object(self.rel.through).field
     708            attname, column = field.get_attname_column()
     709            return column
     710        elif related.model == related.parent_model:
    684711            return 'from_' + related.model._meta.object_name.lower() + '_id'
    685712        else:
    686713            return related.model._meta.object_name.lower() + '_id'
    class ManyToManyField(RelatedField, Field):  
    688715    def _get_m2m_reverse_name(self, related):
    689716        "Function that can be curried to provide the related column name for the m2m table"
    690717        # If this is an m2m relation to self, avoid the inevitable name clash
    691         if related.model == related.parent_model:
     718        if self.rel.through != None:
     719            field = related.parent_model._meta.get_related_object(self.rel.through).field
     720            attname, column = field.get_attname_column()
     721            return column
     722        elif related.model == related.parent_model:
    692723            return 'to_' + related.parent_model._meta.object_name.lower() + '_id'
    693724        else:
    694725            return related.parent_model._meta.object_name.lower() + '_id'
    class OneToOneRel(ManyToOneRel):  
    809840
    810841class ManyToManyRel(object):
    811842    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):
     843        filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True,
     844        through = None):
    813845        self.to = to
    814846        self.num_in_admin = num_in_admin
    815847        self.related_name = related_name
    class ManyToManyRel(object):  
    821853        self.raw_id_admin = raw_id_admin
    822854        self.symmetrical = symmetrical
    823855        self.multiple = True
     856        self.through = through
    824857
    825858        assert not (self.raw_id_admin and self.filter_interface), "ManyToManyRels may not use both raw_id_admin and filter_interface"
  • django/db/models/options.py

    diff --git a/django/db/models/options.py b/django/db/models/options.py
    index 37ace0a..f0af416 100644
    a b from django.conf import settings  
    22from django.db.models.related import RelatedObject
    33from django.db.models.fields.related import ManyToManyRel
    44from django.db.models.fields import AutoField, FieldDoesNotExist
    5 from django.db.models.loading import get_models, app_cache_ready
     5from django.db.models.loading import get_models, get_model, app_cache_ready
    66from django.db.models.query import orderlist2sql
    77from django.db.models import Manager
    88from django.utils.translation import activate, deactivate_all, get_language, string_concat
    class Options(object):  
    162162            follow = self.get_follow()
    163163        return [f for f in self.get_all_related_objects() if follow.get(f.name, None)]
    164164
     165    def get_related_object(self, from_model):
     166        "Gets the RelatedObject which links from from_model to this model."
     167        if isinstance(from_model, str):
     168            from_model = get_model(self.app_label, from_model)
     169        for related_object in self.get_all_related_objects():
     170            if related_object.model == from_model:
     171                return related_object
     172        return None
     173
    165174    def get_data_holders(self, follow=None):
    166175        if follow == None:
    167176            follow = self.get_follow()
  • tests/modeltests/invalid_models/models.py

    diff --git a/tests/modeltests/invalid_models/models.py b/tests/modeltests/invalid_models/models.py
    index 8a480a2..191950d 100644
    a b class Car(models.Model):  
    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)
     121
     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)
    114139
    115140model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute.
    116141invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute.
    invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_3' clashes wi  
    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 manually-defined m2m relationship through a model (Membership) which does not have foreign keys to Person and GroupTwo
     228invalid_models.grouptwo: secondary has a manually-defined m2m relationship through a model (MembershipMissingFK) which does not have foreign keys to Group and GroupTwo
    200229"""
Back to Top