Ticket #6095: 6095-beta-04.diff

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

Updated patch to [7104] and now this includes the init.py file that all of the other patches didn't.

  • 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..bf51fc9 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 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)
  • django/core/management/validation.py

    diff --git a/django/core/management/validation.py b/django/core/management/validation.py
    index bc9faae..ab28638 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 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))
    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..6972e62 100644
    a b def manipulator_valid_rel_key(f, self, field_data, all_data):  
    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
    5765#HACK
    5866class RelatedField(object):
    5967    def contribute_to_class(self, cls, name):
    def create_many_related_manager(superclass):  
    267275    and adds behavior for many-to-many related objects."""
    268276    class ManyRelatedManager(superclass):
    269277        def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None,
    270                 join_table=None, source_col_name=None, target_col_name=None):
     278                join_table=None, source_col_name=None, target_col_name=None,
     279                through=None):
    271280            super(ManyRelatedManager, self).__init__()
    272281            self.core_filters = core_filters
    273282            self.model = model
    def create_many_related_manager(superclass):  
    276285            self.join_table = join_table
    277286            self.source_col_name = source_col_name
    278287            self.target_col_name = target_col_name
     288            self.through = through
    279289            self._pk_val = self.instance._get_pk_val()
    280290            if self._pk_val is None:
    281291                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):  
    284294            return superclass.get_query_set(self).filter(**(self.core_filters))
    285295
    286296        def add(self, *objs):
     297            if self.through:
     298                raise NotImplementedError, "Add not possible for ManyToManyFields which specify a through model.  Try %s.objects.create(...) instead." % self.through
    287299            self._add_items(self.source_col_name, self.target_col_name, *objs)
    288300
    289301            # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
    def create_many_related_manager(superclass):  
    292304        add.alters_data = True
    293305
    294306        def remove(self, *objs):
     307            if self.through:
     308                raise NotImplementedError, "Remove not possible for ManyToManyFields which specify a through model."
    295309            self._remove_items(self.source_col_name, self.target_col_name, *objs)
    296 
     310           
    297311            # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table
    298312            if self.symmetrical:
    299313                self._remove_items(self.target_col_name, self.source_col_name, *objs)
    class ManyRelatedObjectsDescriptor(object):  
    405419            symmetrical=False,
    406420            join_table=qn(self.related.field.m2m_db_table()),
    407421            source_col_name=qn(self.related.field.m2m_reverse_name()),
    408             target_col_name=qn(self.related.field.m2m_column_name())
     422            target_col_name=qn(self.related.field.m2m_column_name()),
     423            through=getattr(self.related.field.rel, 'through', None)
    409424        )
    410425
    411426        return manager
    class ManyRelatedObjectsDescriptor(object):  
    414429        if instance is None:
    415430            raise AttributeError, "Manager must be accessed via instance"
    416431
     432        through = getattr(self.related.field.rel, 'through', None)
     433        if through:
     434            raise NotImplementedError, "Cannot set values on a ManyToManyFields which specify a through model.  Use %s's Manager instead." % through
     435       
    417436        manager = self.__get__(instance)
    418437        manager.clear()
    419438        manager.add(*value)
    class ReverseManyRelatedObjectsDescriptor(object):  
    446465            symmetrical=(self.field.rel.symmetrical and instance.__class__ == rel_model),
    447466            join_table=qn(self.field.m2m_db_table()),
    448467            source_col_name=qn(self.field.m2m_column_name()),
    449             target_col_name=qn(self.field.m2m_reverse_name())
     468            target_col_name=qn(self.field.m2m_reverse_name()),
     469            through=getattr(self.field.rel, 'through', None)
    450470        )
    451471
    452472        return manager
    class ReverseManyRelatedObjectsDescriptor(object):  
    455475        if instance is None:
    456476            raise AttributeError, "Manager must be accessed via instance"
    457477
     478        through = getattr(self.field.rel, 'through', None)
     479        if through:
     480            raise NotImplementedError, "Cannot set values on a ManyToManyFields which specify a through model.  Use %s's Manager instead." % through
     481
    458482        manager = self.__get__(instance)
    459483        manager.clear()
    460484        manager.add(*value)
    class ManyToManyField(RelatedField, Field):  
    648672            filter_interface=kwargs.pop('filter_interface', None),
    649673            limit_choices_to=kwargs.pop('limit_choices_to', None),
    650674            raw_id_admin=kwargs.pop('raw_id_admin', False),
    651             symmetrical=kwargs.pop('symmetrical', True))
     675            symmetrical=kwargs.pop('symmetrical', True),
     676            through=kwargs.pop('through', None))
    652677        self.db_table = kwargs.pop('db_table', None)
     678        if kwargs['rel'].through:
     679            assert not self.db_table, "Cannot specify a db_table if an intermediary model is used."
    653680        if kwargs["rel"].raw_id_admin:
    654681            kwargs.setdefault("validator_list", []).append(self.isValidIDList)
    655682        Field.__init__(self, **kwargs)
    class ManyToManyField(RelatedField, Field):  
    672699
    673700    def _get_m2m_db_table(self, opts):
    674701        "Function that can be curried to provide the m2m table name for this relation"
    675         if self.db_table:
     702        if self.rel.through != None:
     703            return get_model(opts.app_label, self.rel.through)._meta.db_table
     704        elif self.db_table:
    676705            return self.db_table
    677706        else:
    678707            return '%s_%s' % (opts.db_table, self.name)
    class ManyToManyField(RelatedField, Field):  
    680709    def _get_m2m_column_name(self, related):
    681710        "Function that can be curried to provide the source column name for the m2m table"
    682711        # If this is an m2m relation to self, avoid the inevitable name clash
    683         if related.model == related.parent_model:
     712        if self.rel.through != None:
     713            through = get_model(related.opts.app_label, self.rel.through)
     714            field = get_reverse_rel_field(through, related.model, self.rel.related_name)
     715            attname, column = field.get_attname_column()
     716            return column
     717        elif related.model == related.parent_model:
    684718            return 'from_' + related.model._meta.object_name.lower() + '_id'
    685719        else:
    686720            return related.model._meta.object_name.lower() + '_id'
    class ManyToManyField(RelatedField, Field):  
    688722    def _get_m2m_reverse_name(self, related):
    689723        "Function that can be curried to provide the related column name for the m2m table"
    690724        # If this is an m2m relation to self, avoid the inevitable name clash
    691         if related.model == related.parent_model:
     725        if self.rel.through != None:
     726            through = get_model(related.opts.app_label, self.rel.through)
     727            field = get_reverse_rel_field(through, related.parent_model, self.rel.related_name)
     728            attname, column = field.get_attname_column()
     729            return column
     730        elif related.model == related.parent_model:
    692731            return 'to_' + related.parent_model._meta.object_name.lower() + '_id'
    693732        else:
    694733            return related.parent_model._meta.object_name.lower() + '_id'
    class OneToOneRel(ManyToOneRel):  
    809848
    810849class ManyToManyRel(object):
    811850    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):
     851        filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True,
     852        through = None):
    813853        self.to = to
    814854        self.num_in_admin = num_in_admin
    815855        self.related_name = related_name
    class ManyToManyRel(object):  
    821861        self.raw_id_admin = raw_id_admin
    822862        self.symmetrical = symmetrical
    823863        self.multiple = True
     864        self.through = through
    824865
    825866        assert not (self.raw_id_admin and self.filter_interface), "ManyToManyRels may not use both raw_id_admin and filter_interface"
  • tests/modeltests/invalid_models/models.py

    diff --git a/tests/modeltests/invalid_models/models.py b/tests/modeltests/invalid_models/models.py
    index 8a480a2..bc68959 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 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"""
  • new file tests/modeltests/m2m_manual/models.py

    diff --git a/tests/modeltests/m2m_manual/__init__.py b/tests/modeltests/m2m_manual/__init__.py
    new file mode 100644
    index 0000000..e69de29
    diff --git a/tests/modeltests/m2m_manual/models.py b/tests/modeltests/m2m_manual/models.py
    new file mode 100644
    index 0000000..512aa7e
    - +  
     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    nodefaultsnonulls = models.ManyToManyField(Person, through='TestNoDefaultsOrNulls', related_name="testnodefaultsnonulls")
     16   
     17    def __unicode__(self):
     18        return self.name
     19
     20class Membership(models.Model):
     21    person = models.ForeignKey(Person)
     22    group = models.ForeignKey(Group)
     23    date_joined = models.DateTimeField(default=datetime.now)
     24    invite_reason = models.CharField(max_length=64, null=True)
     25   
     26    def __unicode__(self):
     27        return "%s is a member of %s" % (self.person.name, self.group.name)
     28
     29class CustomMembership(models.Model):
     30    person = models.ForeignKey(Person, db_column="custom_person_column", related_name="custom_person_related_name")
     31    group = models.ForeignKey(Group)
     32    weird_fk = models.ForeignKey(Membership, null=True)
     33    date_joined = models.DateTimeField(default=datetime.now)
     34   
     35    def __unicode__(self):
     36        return "%s is a member of %s" % (self.person.name, self.group.name)
     37   
     38    class Meta:
     39        db_table = "test_table"
     40
     41class TestNoDefaultsOrNulls(models.Model):
     42    person = models.ForeignKey(Person)
     43    group = models.ForeignKey(Group)
     44    nodefaultnonull = models.CharField(max_length=5)
     45
     46__test__ = {'API_TESTS':"""
     47>>> from datetime import datetime
     48
     49### Creation and Saving Tests ###
     50>>> bob = Person.objects.create(name = 'Bob')
     51>>> jim = Person.objects.create(name = 'Jim')
     52>>> jane = Person.objects.create(name = 'Jane')
     53>>> rock = Group.objects.create(name = 'Rock')
     54>>> roll = Group.objects.create(name = 'Roll')
     55
     56>>> rock.members.all()
     57[]
     58
     59>>> m1 = Membership.objects.create(person = jim, group = rock)
     60>>> m2 = Membership.objects.create(person = jane, group = rock)
     61
     62>>> rock.members.all()
     63[<Person: Jim>, <Person: Jane>]
     64
     65>>> m3 = Membership.objects.create(person = bob, group = roll)
     66>>> m4 = Membership.objects.create(person = jim, group = roll)
     67>>> m5 = Membership.objects.create(person = jane, group = roll)
     68
     69>>> jim.group_set.all()
     70[<Group: Rock>, <Group: Roll>]
     71
     72# Check to make sure that querying via intermediary model works as normal
     73>>> m = Membership.objects.get(person = jane, group = rock)
     74>>> m
     75<Membership: Jane is a member of Rock>
     76
     77# Setting some date_joined dates
     78>>> m2.invite_reason = "She was just awesome."
     79>>> m2.date_joined = datetime(2006, 1, 1)
     80>>> m2.save()
     81
     82>>> m5.date_joined = datetime(2004, 1, 1)
     83>>> m5.save()
     84
     85>>> m3.date_joined = datetime(2004, 1, 1)
     86>>> m3.save()
     87
     88>>> Membership.objects.filter(person = jim)
     89[<Membership: Jim is a member of Rock>, <Membership: Jim is a member of Roll>]
     90
     91
     92### Forward Descriptors Tests ###
     93# Ensure that using the add or remove function errors, due to using a through model.
     94>>> rock.members.add(bob)
     95Traceback (most recent call last):
     96...
     97NotImplementedError: Add not possible for ManyToManyFields which specify a through model.  Try Membership.objects.create(...) instead.
     98
     99>>> rock.members.remove(jim)
     100Traceback (most recent call last):
     101...
     102NotImplementedError: Remove not possible for ManyToManyFields which specify a through model.
     103
     104>>> backup = list(rock.members.all())
     105>>> backup
     106[<Person: Jim>, <Person: Jane>]
     107
     108# The clear function should still work.
     109>>> rock.members.clear()
     110>>> rock.members.all()
     111[]
     112
     113# Assignment should not work with models specifying a through model.
     114>>> rock.members = backup
     115Traceback (most recent call last):
     116...
     117NotImplementedError: Cannot set values on a ManyToManyFields which specify a through model.  Use Membership's Manager instead.
     118
     119# Let's re-add those instances that we've cleared.
     120>>> m1.save()
     121>>> m2.save()
     122
     123>>> rock.members.all()
     124[<Person: Jim>, <Person: Jane>]
     125
     126
     127### Reverse Descriptors Tests ###
     128# Ensure that using the add or remove function errors, due to using a through model.
     129>>> bob.group_set.add(rock)
     130Traceback (most recent call last):
     131...
     132NotImplementedError: Add not possible for ManyToManyFields which specify a through model.  Try Membership.objects.create(...) instead.
     133
     134>>> jim.group_set.remove(rock)
     135Traceback (most recent call last):
     136...
     137NotImplementedError: Remove not possible for ManyToManyFields which specify a through model.
     138
     139>>> backup = list(jim.group_set.all())
     140>>> backup
     141[<Group: Rock>, <Group: Roll>]
     142
     143# The clear function should still work.
     144>>> jim.group_set.clear()
     145>>> jim.group_set.all()
     146[]
     147
     148# Assignment should not work with models specifying a through model.
     149>>> jim.group_set = backup
     150Traceback (most recent call last):
     151...
     152NotImplementedError: Cannot set values on a ManyToManyFields which specify a through model.  Use Membership's Manager instead.
     153
     154# Let's re-add those instances that we've cleared.
     155>>> m1.save()
     156>>> m4.save()
     157
     158>>> jim.group_set.all()
     159[<Group: Rock>, <Group: Roll>]
     160
     161### Custom Tests ###
     162
     163>>> rock.custom_members.all()
     164[]
     165>>> bob.custom.all()
     166[]
     167>>> cm1 = CustomMembership.objects.create(person = bob, group = rock)
     168>>> cm2 = CustomMembership.objects.create(person = jim, group = rock)
     169
     170>>> rock.custom_members.all()
     171[<Person: Bob>, <Person: Jim>]
     172>>> bob.custom.all()
     173[<Group: Rock>]
     174
     175# Let's make sure our new descriptors don't conflict with the FK related_name.
     176>>> bob.custom_person_related_name.all()
     177[<CustomMembership: Bob is a member of Rock>]
     178
     179###QUERY TESTS###
     180# Queries involving the related model (Person, in the case of Group) use its
     181# attname
     182>>> Group.objects.filter(members__name='Bob')
     183[<Group: Roll>]
     184
     185# Queries involving the relationship model (Membership, in the case of Group)
     186# use its model name
     187>>> Group.objects.filter(membership__invite_reason = "She was just awesome.")
     188[<Group: Rock>]
     189
     190# Queries involving the reverse related model (Group, in the case of Person)
     191# use its model name
     192>>> Person.objects.filter(group__name="Rock")
     193[<Person: Jim>, <Person: Jane>]
     194
     195# If the m2m field has specified a related_name, using that will work.
     196>>> Person.objects.filter(custom__name="Rock")
     197[<Person: Bob>, <Person: Jim>]
     198
     199# Queries involving the relationship model (Membership, in the case of Group)
     200# use its model name
     201>>> Person.objects.filter(membership__invite_reason = "She was just awesome.")
     202[<Person: Jane>]
     203
     204# Let's see all of the groups that Jane joined after 1 Jan 2005:
     205>>> Group.objects.filter(membership__date_joined__gt = datetime(2005, 1, 1),
     206... membership__person = jane)
     207[<Group: Rock>]
     208
     209# Now let's see all of the people that have joined Rock since 1 Jan 2005:
     210>>> Person.objects.filter(membership__date_joined__gt = datetime(2005, 1, 1),
     211... membership__group = rock)
     212[<Person: Jim>, <Person: Jane>]
     213
     214# Conceivably, queries through membership could return non-unique querysets.
     215# To demonstrate this, query for all people who have joined a group after 2004:
     216>>> Person.objects.filter(membership__date_joined__gt = datetime(2004, 1, 1))
     217[<Person: Jim>, <Person: Jim>, <Person: Jane>]
     218
     219# Jim showed up twice, because he joined two groups ('Rock', and 'Roll'):
     220>>> [(m.person.name, m.group.name) for m in
     221... Membership.objects.filter(date_joined__gt = datetime(2004, 1, 1))]
     222[(u'Jim', u'Rock'), (u'Jane', u'Rock'), (u'Jim', u'Roll')]
     223
     224# QuerySet's distinct() method can correct this problem.
     225>>> Person.objects.filter(membership__date_joined__gt = datetime(2004, 1, 1)).distinct()
     226[<Person: Jim>, <Person: Jane>]
     227"""}
     228 No newline at end of file
Back to Top