Commit c627da0c authored by Akis Kesoglou's avatar Akis Kesoglou Committed by Anssi Kääriäinen
Browse files

Fixed #14549 - Removed restriction of single FKs on intermediary tables

Thanks to Loic Bistuer for review. Minor changes to error messages
done by committer.
parent 95c74b9d
Loading
Loading
Loading
Loading
+63 −13
Original line number Diff line number Diff line
@@ -1250,9 +1250,12 @@ class OneToOneRel(ManyToOneRel):

class ManyToManyRel(object):
    def __init__(self, to, related_name=None, limit_choices_to=None,
                 symmetrical=True, through=None, db_constraint=True, related_query_name=None):
                 symmetrical=True, through=None, through_fields=None,
                 db_constraint=True, related_query_name=None):
        if through and not db_constraint:
            raise ValueError("Can't supply a through model and db_constraint=False")
        if through_fields and not through:
            raise ValueError("Cannot specify through_fields without a through model")
        self.to = to
        self.related_name = related_name
        self.related_query_name = related_query_name
@@ -1262,6 +1265,7 @@ class ManyToManyRel(object):
        self.symmetrical = symmetrical
        self.multiple = True
        self.through = through
        self.through_fields = through_fields
        self.db_constraint = db_constraint

    def is_hidden(self):
@@ -1849,6 +1853,7 @@ class ManyToManyField(RelatedField):
            limit_choices_to=kwargs.pop('limit_choices_to', None),
            symmetrical=kwargs.pop('symmetrical', to == RECURSIVE_RELATIONSHIP_CONSTANT),
            through=kwargs.pop('through', None),
            through_fields=kwargs.pop('through_fields', None),
            db_constraint=db_constraint,
        )

@@ -1878,6 +1883,12 @@ class ManyToManyField(RelatedField):
        return []

    def _check_relationship_model(self, from_model=None, **kwargs):
        if hasattr(self.rel.through, '_meta'):
            qualified_model_name = "%s.%s" % (
                self.rel.through._meta.app_label, self.rel.through.__name__)
        else:
            qualified_model_name = self.rel.through

        errors = []

        if self.rel.through not in apps.get_models(include_auto_created=True):
@@ -1885,13 +1896,28 @@ class ManyToManyField(RelatedField):
            errors.append(
                checks.Error(
                    ("Field specifies a many-to-many relation through model "
                     "'%s', which has not been installed.") % self.rel.through,
                     "'%s', which has not been installed.") %
                    qualified_model_name,
                    hint=None,
                    obj=self,
                    id='fields.E331',
                )
            )

        elif self.rel.through_fields is not None:
            if not len(self.rel.through_fields) >= 2 or not (self.rel.through_fields[0] and self.rel.through_fields[1]):
                errors.append(
                    checks.Error(
                        ("The field is given an iterable for through_fields, "
                         "which does not provide the names for both link fields "
                         "that Django should use for the relation through model "
                         "'%s'.") % qualified_model_name,
                        hint=None,
                        obj=self,
                        id='fields.E337',
                    )
                )

        elif not isinstance(self.rel.through, six.string_types):

            assert from_model is not None, \
@@ -1926,13 +1952,16 @@ class ManyToManyField(RelatedField):
                seen_self = sum(from_model == getattr(field.rel, 'to', None)
                    for field in self.rel.through._meta.fields)

                if seen_self > 2:
                if seen_self > 2 and not self.rel.through_fields:
                    errors.append(
                        checks.Error(
                            ("The model is used as an intermediate model by "
                             "'%s', but it has more than two foreign keys "
                             "to '%s', which is ambiguous.") % (self, from_model_name),
                            hint=None,
                             "to '%s', which is ambiguous. You must specify "
                             "which two foreign keys Django should use via the "
                             "through_fields keyword argument.") % (self, from_model_name),
                            hint=("Use through_fields to specify which two "
                                  "foreign keys Django should use."),
                            obj=self.rel.through,
                            id='fields.E333',
                        )
@@ -1945,12 +1974,14 @@ class ManyToManyField(RelatedField):
                seen_to = sum(to_model == getattr(field.rel, 'to', None)
                    for field in self.rel.through._meta.fields)

                if seen_from > 1:
                if seen_from > 1 and not self.rel.through_fields:
                    errors.append(
                        checks.Error(
                            ("The model is used as an intermediate model by "
                             "'%s', but it has more than one foreign key "
                             "from '%s', which is ambiguous.") % (self, from_model_name),
                             "from '%s', which is ambiguous. You must specify "
                             "which foreign key Django should use via the "
                             "through_fields keyword argument.") % (self, from_model_name),
                            hint=('If you want to create a recursive relationship, '
                                  'use ForeignKey("self", symmetrical=False, '
                                  'through="%s").') % relationship_model_name,
@@ -1959,12 +1990,14 @@ class ManyToManyField(RelatedField):
                        )
                    )

                if seen_to > 1:
                if seen_to > 1 and not self.rel.through_fields:
                    errors.append(
                        checks.Error(
                            ("The model is used as an intermediate model by "
                             "'%s', but it has more than one foreign key "
                             "to '%s', which is ambiguous.") % (self, to_model_name),
                             "to '%s', which is ambiguous. You must specify "
                             "which foreign key Django should use via the "
                             "through_fields keyword argument.") % (self, to_model_name),
                            hint=('If you want to create a recursive '
                                  'relationship, use ForeignKey("self", '
                                  'symmetrical=False, through="%s").') % relationship_model_name,
@@ -2057,10 +2090,18 @@ class ManyToManyField(RelatedField):
        cache_attr = '_m2m_%s_cache' % attr
        if hasattr(self, cache_attr):
            return getattr(self, cache_attr)
        if self.rel.through_fields is not None:
            link_field_name = self.rel.through_fields[0]
        else:
            link_field_name = None
        for f in self.rel.through._meta.fields:
            if hasattr(f, 'rel') and f.rel and f.rel.to == related.model:
            if hasattr(f, 'rel') and f.rel and f.rel.to == related.model and \
                    (link_field_name is None or link_field_name == f.name):
                setattr(self, cache_attr, getattr(f, attr))
                return getattr(self, cache_attr)
        # We only reach here if we're given an invalid field name via the
        # `through_fields` argument
        raise FieldDoesNotExist(link_field_name)

    def _get_m2m_reverse_attr(self, related, attr):
        "Function that can be curried to provide the related accessor or DB column name for the m2m table"
@@ -2068,9 +2109,13 @@ class ManyToManyField(RelatedField):
        if hasattr(self, cache_attr):
            return getattr(self, cache_attr)
        found = False
        if self.rel.through_fields is not None:
            link_field_name = self.rel.through_fields[1]
        else:
            link_field_name = None
        for f in self.rel.through._meta.fields:
            if hasattr(f, 'rel') and f.rel and f.rel.to == related.parent_model:
                if related.model == related.parent_model:
                if link_field_name is None and related.model == related.parent_model:
                    # If this is an m2m-intermediate to self,
                    # the first foreign key you find will be
                    # the source column. Keep searching for
@@ -2080,10 +2125,15 @@ class ManyToManyField(RelatedField):
                        break
                    else:
                        found = True
                else:
                elif link_field_name is None or link_field_name == f.name:
                    setattr(self, cache_attr, getattr(f, attr))
                    break
        try:
            return getattr(self, cache_attr)
        except AttributeError:
            # We only reach here if we're given an invalid reverse field
            # name via the `through_fields` argument
            raise FieldDoesNotExist(link_field_name)

    def value_to_string(self, obj):
        data = ''
+5 −4
Original line number Diff line number Diff line
@@ -90,10 +90,11 @@ Related Fields
* **fields.E330**: ManyToManyFields cannot be unique.
* **fields.E331**: Field specifies a many-to-many relation through model ``%s``, which has not been installed.
* **fields.E332**: Many-to-many fields with intermediate tables must not be symmetrical.
* **fields.E333**: The model is used as an intermediate model by ``<model>``, but it has more than two foreign keys to ``<model>``, which is ambiguous.
* **fields.E334**: The model is used as an intermediate model by ``<model>``, but it has more than one foreign key from ``<model>``, which is ambiguous.
* **fields.E335**: The model is used as an intermediate model by ``<model>``, but it has more than one foreign key to ``<model>``, which is ambiguous.
* **fields.E336**: The model is used as an intermediary model by ``<model>``, but it does not have foreign key to ``<model>`` or ``<model>``."
* **fields.E333**: The model is used as an intermediate model by ``<model>``, but it has more than two foreign keys to ``<model>``, which is ambiguous. You must specify which two foreign keys Django should use via the through_fields keyword argument.
* **fields.E334**: The model is used as an intermediate model by ``<model>``, but it has more than one foreign key from ``<model>``, which is ambiguous. You must specify which foreign key Django should use via the through_fields keyword argument.
* **fields.E335**: The model is used as an intermediate model by ``<model>``, but it has more than one foreign key to ``<model>``, which is ambiguous. You must specify which foreign key Django should use via the through_fields keyword argument.
* **fields.E336**: The model is used as an intermediary model by ``<model>``, but it does not have foreign key to ``<model>`` or ``<model>``.
* **fields.E337**: The field is given an iterable for through_fields, which does not provide the names for both link fields that Django should use for the relation through <model>.

Signals
~~~~~~~
+49 −0
Original line number Diff line number Diff line
@@ -1337,6 +1337,55 @@ that control how the relationship functions.
    :ref:`extra data with a many-to-many relationship
    <intermediary-manytomany>`.

.. attribute:: ManyToManyField.through_fields

    .. versionadded:: 1.7

    Only used when a custom intermediary model is specified. Django will
    normally determine which fields of the intermediary model to use in order
    to establish a many-to-many relationship automatically. However,
    consider the following models::

        from django.db import models

        class Person(models.Model):
            name = models.CharField(max_length=50)

        class Group(models.Model):
            name = models.CharField(max_length=128)
            members = models.ManyToManyField(Person, through='Membership', through_fields=('person', 'group'))

        class Membership(models.Model):
            person = models.ForeignKey(Person)
            group = models.ForeignKey(Group)
            inviter = models.ForeignKey(Person, related_name="membership_invites")
            invite_reason = models.CharField(max_length=64)

    ``Membership`` has *two* foreign keys to ``Person`` (``person`` and
    ``inviter``), which makes the relationship ambiguous and Django can't know
    which one to use. In this case, you must explicitly specify which
    foreign keys Django should use using ``through_fields``, as in the example
    above.

    ``through_fields`` accepts a 2-tuple ``('field1', 'field2')``, where
    ``field1`` is the name of the foreign key to the target model (``person``
    in this case), and ``field2`` the name of the foreign key to the model the
    :class:`ManyToManyField` is defined on (``group`` in this case).

    When you have more than one foreign key on an intermediary model to any
    (or even both) of the models participating in a many-to-many relationship,
    you *must* specify ``through_fields``. This also applies to
    :ref:`recursive relationships <recursive-relationships>`
    when an intermediary model is used and there are more than two
    foreign keys to the model, or you want to explicitly specify which two
    Django should use.

    Recursive relationships using an intermediary model are always defined as
    non-symmetrical -- that is, with :attr:`symmetrical=False <ManyToManyField.symmetrical>`
    -- therefore, there is the concept of a "source" and a "target". In that
    case ``'field1'`` will be treated as the "source" of the relationship and
    ``'field2'`` as the "target".

.. attribute:: ManyToManyField.db_table

    The name of the table to create for storing the many-to-many data. If this
+6 −0
Original line number Diff line number Diff line
@@ -658,6 +658,12 @@ Models
* You can use a single list for :attr:`~django.db.models.Options.index_together`
  (rather than a list of lists) when specifying a single set of fields.

* Custom intermediate models having more than one foreign key to any of the
  models participating in a many-to-many relationship are now permitted,
  provided you explicitly specify which foreign keys should be used by setting
  the new :attr:`ManyToManyField.through_fields <django.db.models.ManyToManyField.through_fields>`
  argument.

Signals
^^^^^^^

+21 −13
Original line number Diff line number Diff line
@@ -434,30 +434,38 @@ something like this::
        invite_reason = models.CharField(max_length=64)

When you set up the intermediary model, you explicitly specify foreign
keys to the models that are involved in the ManyToMany relation. This
keys to the models that are involved in the many-to-many relationship. This
explicit declaration defines how the two models are related.

There are a few restrictions on the intermediate model:

* Your intermediate model must contain one - and *only* one - foreign key
  to the target model (this would be ``Person`` in our example). If you
  have more than one foreign key, a validation error will be raised.

* Your intermediate model must contain one - and *only* one - foreign key
  to the source model (this would be ``Group`` in our example). If you
  have more than one foreign key, a validation error will be raised.

* The only exception to this is a model which has a many-to-many
  relationship to itself, through an intermediary model. In this
  case, two foreign keys to the same model are permitted, but they
  will be treated as the two (different) sides of the many-to-many
  relation.
  to the target model (this would be ``Person`` in our example), or you must
  explicitly specify the foreign keys Django should use for the relationship
  using :attr:`ManyToManyField.through_fields <ManyToManyField.through_fields>`.
  If you have more than one foreign key and ``through_fields`` is not
  specified, a validation error will be raised. A similar restriction applies
  to the foreign key to the source model (this would be ``Group`` in our
  example).

* For a model which has a many-to-many relationship to itself through an
  intermediary model, two foreign keys to the same model are permitted, but
  they will be treated as the two (different) sides of the many-to-many
  relationship. If there are *more* than two foreign keys though, you
  must also specify ``through_fields`` as above, or a validation error
  will be raised.

* When defining a many-to-many relationship from a model to
  itself, using an intermediary model, you *must* use
  :attr:`symmetrical=False <ManyToManyField.symmetrical>` (see
  :ref:`the model field reference <manytomany-arguments>`).

.. versionchanged:: 1.7

    In Django 1.6 and earlier, intermediate models containing more than one
    foreign key to any of the models involved in the many-to-many relationship
    used to be prohibited.

Now that you have set up your :class:`~django.db.models.ManyToManyField` to use
your intermediary model (``Membership``, in this case), you're ready to start
creating some many-to-many relationships. You do this by creating instances of
Loading