Ticket #6095: 6095-alpha-05.diff

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

Added extra model validation checks and tests.

  • django/db/models/fields/related.py

     
    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    "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
     78def 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
     84get_model_for_db_table = memoize(get_model_for_db_table, model_db_table_cache, 1)
     85
    5786#HACK
    5887class RelatedField(object):
    5988    def contribute_to_class(self, cls, name):
     
    267296    and adds behavior for many-to-many related objects."""
    268297    class ManyRelatedManager(superclass):
    269298        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):
    271301            super(ManyRelatedManager, self).__init__()
    272302            self.core_filters = core_filters
    273303            self.model = model
     
    276306            self.join_table = join_table
    277307            self.source_col_name = source_col_name
    278308            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('"',''))
    279312            self._pk_val = self.instance._get_pk_val()
    280313            if self._pk_val is None:
    281314                raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % model)
     
    340373
    341374                # Add the ones that aren't there already
    342375                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)" % \
    344378                        (self.join_table, source_col_name, target_col_name),
    345379                        [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()
    346385                transaction.commit_unless_managed()
    347386
    348387        def _remove_items(self, source_col_name, target_col_name, *objs):
     
    398437        RelatedManager = create_many_related_manager(superclass)
    399438
    400439        qn = connection.ops.quote_name
     440        rel_field = self.related.field
    401441        manager = RelatedManager(
    402442            model=rel_model,
    403443            core_filters={'%s__pk' % self.related.field.name: instance._get_pk_val()},
    404444            instance=instance,
    405445            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()
    409451        )
    410452
    411453        return manager
     
    446488            symmetrical=(self.field.rel.symmetrical and instance.__class__ == rel_model),
    447489            join_table=qn(self.field.m2m_db_table()),
    448490            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()
    450494        )
    451495
    452496        return manager
     
    648692            filter_interface=kwargs.pop('filter_interface', None),
    649693            limit_choices_to=kwargs.pop('limit_choices_to', None),
    650694            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))
    652697        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."
    653700        if kwargs["rel"].raw_id_admin:
    654701            kwargs.setdefault("validator_list", []).append(self.isValidIDList)
    655702        Field.__init__(self, **kwargs)
     
    672719
    673720    def _get_m2m_db_table(self, opts):
    674721        "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:
    676725            return self.db_table
    677726        else:
    678727            return '%s_%s' % (opts.db_table, self.name)
    679728
     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
    680738    def _get_m2m_column_name(self, related):
    681739        "Function that can be curried to provide the source column name for the m2m table"
    682740        # 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:
    684747            return 'from_' + related.model._meta.object_name.lower() + '_id'
    685748        else:
    686749            return related.model._meta.object_name.lower() + '_id'
    687750
     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
    688760    def _get_m2m_reverse_name(self, related):
    689761        "Function that can be curried to provide the related column name for the m2m table"
    690762        # 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:
    692769            return 'to_' + related.parent_model._meta.object_name.lower() + '_id'
    693770        else:
    694771            return related.parent_model._meta.object_name.lower() + '_id'
     
    745822        # Set up the accessors for the column names on the m2m table
    746823        self.m2m_column_name = curry(self._get_m2m_column_name, related)
    747824        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)
    748827
    749828    def set_attributes_from_rel(self):
    750829        pass
     
    809888
    810889class ManyToManyRel(object):
    811890    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):
    813893        self.to = to
    814894        self.num_in_admin = num_in_admin
    815895        self.related_name = related_name
     
    821901        self.raw_id_admin = raw_id_admin
    822902        self.symmetrical = symmetrical
    823903        self.multiple = True
     904        self.through = through
    824905
    825906        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

     
    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

     
    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/invalid_models/models.py

     
    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

     
     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 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
Back to Top