Commit 3d4a8261 authored by Andriy Sokolovskiy's avatar Andriy Sokolovskiy Committed by Markus Holtermann
Browse files

[1.7.x] Fixed #24104 -- Fixed check to look on field.get_internal_type() instead of class instance

parent 6dc6ec21
Loading
Loading
Loading
Loading
+2 −3
Original line number Diff line number Diff line
@@ -3,7 +3,6 @@ import operator

from django.db.backends.creation import BaseDatabaseCreation
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.encoding import force_bytes
from django.utils.log import getLogger
@@ -359,7 +358,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.get_internal_type() == 'ManyToManyField' 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)
@@ -403,7 +402,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.get_internal_type() == 'ManyToManyField' 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.utils import six
from django.apps.registry import Apps
from django.db.backends.schema import BaseDatabaseSchemaEditor
from django.db.models.fields.related import ManyToManyField


class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
@@ -70,7 +69,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.get_internal_type() == 'ManyToManyField':
                mapping[field.column] = self.quote_value(
                    self.effective_default(field)
                )
@@ -93,7 +92,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.get_internal_type() == 'ManyToManyField' and field.rel.through._meta.auto_created:
                return self.delete_model(field.rel.through)
        # Work inside a new app registry
        apps = Apps()
@@ -172,7 +171,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.get_internal_type() == 'ManyToManyField' and field.rel.through._meta.auto_created:
            return self.create_model(field.rel.through)
        self._remake_table(model, create_fields=[field])

@@ -182,7 +181,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
        but for M2Ms may involve deleting a table.
        """
        # M2M fields are a special case
        if isinstance(field, ManyToManyField):
        if field.get_internal_type() == 'ManyToManyField':
            # For implicit M2M tables, delete the auto-created table
            if field.rel.through._meta.auto_created:
                self.delete_model(field.rel.through)
+4 −0
Original line number Diff line number Diff line
@@ -20,3 +20,7 @@ Bugfixes

* Prevented the ``static.serve`` view from producing ``ResourceWarning``\s in
  certain circumstances (security fix regression, :ticket:`24193`).

* Fixed schema check for ``ManyToManyField`` to look for internal type instead
  of checking class instance, so you can write custom m2m-like fields with the
  same behavior. (:ticket:`24104`).

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.
    """

    def __init__(self, to, db_constraint=True, swappable=True, **kwargs):
        try:
            to._meta
        except AttributeError:
            to = str(to)
        kwargs['verbose_name'] = kwargs.get('verbose_name', None)
        kwargs['rel'] = ManyToManyRel(
            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):
        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)
        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']
    set_attributes_from_rel = ManyToManyField.__dict__['set_attributes_from_rel']
    _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,
@@ -1310,3 +1311,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 internal type of field
        """
        # 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)