Commit 38c17871 authored by Andriy Sokolovskiy's avatar Andriy Sokolovskiy Committed by Markus Holtermann
Browse files

Fixed #24104 -- Fixed check to look on field.many_to_many instead of class instance

parent d450af8a
Loading
Loading
Loading
Loading
+2 −3
Original line number Diff line number Diff line
import hashlib

from django.db.backends.utils import truncate_name
from django.db.models.fields.related import ManyToManyField
from django.db.transaction import atomic
from django.utils import six
from django.utils.encoding import force_bytes
@@ -380,7 +379,7 @@ class BaseDatabaseSchemaEditor(object):
        table instead (for M2M fields)
        """
        # Special-case implicit M2M tables
        if isinstance(field, ManyToManyField) and field.rel.through._meta.auto_created:
        if field.many_to_many and field.rel.through._meta.auto_created:
            return self.create_model(field.rel.through)
        # Get the column's definition
        definition, params = self.column_sql(model, field, include_default=True)
@@ -424,7 +423,7 @@ class BaseDatabaseSchemaEditor(object):
        but for M2Ms may involve deleting a table.
        """
        # Special-case implicit M2M tables
        if isinstance(field, ManyToManyField) and field.rel.through._meta.auto_created:
        if field.many_to_many and field.rel.through._meta.auto_created:
            return self.delete_model(field.rel.through)
        # It might not actually have a column behind it
        if field.db_parameters(connection=self.connection)['type'] is None:
+4 −5
Original line number Diff line number Diff line
@@ -4,7 +4,6 @@ from decimal import Decimal

from django.apps.registry import Apps
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.models.fields.related import ManyToManyField
from django.utils import six

import _sqlite3
@@ -71,7 +70,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
        for field in create_fields:
            body[field.name] = field
            # Choose a default and insert it into the copy map
            if not isinstance(field, ManyToManyField):
            if not field.many_to_many:
                mapping[field.column] = self.quote_value(
                    self.effective_default(field)
                )
@@ -94,7 +93,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
            del body[field.name]
            del mapping[field.column]
            # Remove any implicit M2M tables
            if isinstance(field, ManyToManyField) and field.rel.through._meta.auto_created:
            if field.many_to_many and field.rel.through._meta.auto_created:
                return self.delete_model(field.rel.through)
        # Work inside a new app registry
        apps = Apps()
@@ -173,7 +172,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
        table instead (for M2M fields)
        """
        # Special-case implicit M2M tables
        if isinstance(field, ManyToManyField) and field.rel.through._meta.auto_created:
        if field.many_to_many and field.rel.through._meta.auto_created:
            return self.create_model(field.rel.through)
        self._remake_table(model, create_fields=[field])

@@ -183,7 +182,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
        but for M2Ms may involve deleting a table.
        """
        # M2M fields are a special case
        if isinstance(field, ManyToManyField):
        if field.many_to_many:
            # For implicit M2M tables, delete the auto-created table
            if field.rel.through._meta.auto_created:
                self.delete_model(field.rel.through)

tests/schema/fields.py

0 → 100644
+54 −0
Original line number Diff line number Diff line
from django.db.models.fields.related import (
    create_many_to_many_intermediary_model,
    ManyToManyField, ManyToManyRel, RelatedField,
    RECURSIVE_RELATIONSHIP_CONSTANT, ReverseManyRelatedObjectsDescriptor,
)

from django.utils.functional import curry


class CustomManyToManyField(RelatedField):
    """
    Ticket #24104 - Need to have a custom ManyToManyField,
    which is not an inheritor of ManyToManyField.
    """
    many_to_many = True

    def __init__(self, to, db_constraint=True, swappable=True, **kwargs):
        try:
            to._meta
        except AttributeError:
            to = str(to)
        kwargs['rel'] = ManyToManyRel(
            self, to,
            related_name=kwargs.pop('related_name', None),
            related_query_name=kwargs.pop('related_query_name', None),
            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,
        )
        self.swappable = swappable
        self.db_table = kwargs.pop('db_table', None)
        if kwargs['rel'].through is not None:
            assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used."
        super(CustomManyToManyField, self).__init__(**kwargs)

    def contribute_to_class(self, cls, name, **kwargs):
        if self.rel.symmetrical and (self.rel.to == "self" or self.rel.to == cls._meta.object_name):
            self.rel.related_name = "%s_rel_+" % name
        super(CustomManyToManyField, self).contribute_to_class(cls, name, **kwargs)
        if not self.rel.through and not cls._meta.abstract and not cls._meta.swapped:
            self.rel.through = create_many_to_many_intermediary_model(self, cls)
        setattr(cls, self.name, ReverseManyRelatedObjectsDescriptor(self))
        self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta)

    def get_internal_type(self):
        return 'ManyToManyField'

    # Copy those methods from ManyToManyField because they don't call super() internally
    contribute_to_related_class = ManyToManyField.__dict__['contribute_to_related_class']
    _get_m2m_attr = ManyToManyField.__dict__['_get_m2m_attr']
    _get_m2m_reverse_attr = ManyToManyField.__dict__['_get_m2m_reverse_attr']
    _get_m2m_db_table = ManyToManyField.__dict__['_get_m2m_db_table']
+45 −0
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ from django.db.models.fields import (BinaryField, BooleanField, CharField, Integ
    PositiveIntegerField, SlugField, TextField)
from django.db.models.fields.related import ForeignKey, ManyToManyField, OneToOneField
from django.db.transaction import atomic
from .fields import CustomManyToManyField
from .models import (Author, AuthorWithDefaultHeight, AuthorWithM2M, Book, BookWithLongName,
    BookWithSlug, BookWithM2M, Tag, TagIndexed, TagM2MTest, TagUniqueRename,
    UniqueTest, Thing, TagThrough, BookWithM2MThrough, AuthorTag, AuthorWithM2MThrough,
@@ -1303,3 +1304,47 @@ class SchemaTests(TransactionTestCase):
            cursor.execute("SELECT surname FROM schema_author;")
            item = cursor.fetchall()[0]
            self.assertEqual(item[0], None if connection.features.interprets_empty_strings_as_nulls else '')

    def test_custom_manytomanyfield(self):
        """
        #24104 - Schema editors should look for many_to_many
        """
        # Create the tables
        with connection.schema_editor() as editor:
            editor.create_model(AuthorWithM2M)
            editor.create_model(TagM2MTest)
        # Create an M2M field
        new_field = CustomManyToManyField("schema.TagM2MTest", related_name="authors")
        new_field.contribute_to_class(AuthorWithM2M, "tags")
        # Ensure there's no m2m table there
        self.assertRaises(DatabaseError, self.column_classes, new_field.rel.through)
        try:
            # Add the field
            with connection.schema_editor() as editor:
                editor.add_field(
                    AuthorWithM2M,
                    new_field,
                )
            # Ensure there is now an m2m table there
            columns = self.column_classes(new_field.rel.through)
            self.assertEqual(columns['tagm2mtest_id'][0], "IntegerField")

            # "Alter" the field. This should not rename the DB table to itself.
            with connection.schema_editor() as editor:
                editor.alter_field(
                    AuthorWithM2M,
                    new_field,
                    new_field,
                )

            # Remove the M2M table again
            with connection.schema_editor() as editor:
                editor.remove_field(
                    AuthorWithM2M,
                    new_field,
                )
            # Ensure there's no m2m table there
            self.assertRaises(DatabaseError, self.column_classes, new_field.rel.through)
        finally:
            # Cleanup model states
            AuthorWithM2M._meta.local_many_to_many.remove(new_field)