summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndriy Sokolovskiy <sokandpal@yandex.ru>2015-01-24 00:16:05 +0200
committerMarkus Holtermann <info@markusholtermann.eu>2015-01-27 14:40:39 +0100
commit3d4a826174b7a411a03be39725e60c940944a7fe (patch)
tree8349711ea544bdbff108aee134b5b0933f5e3173
parent6dc6ec218860e4d4c5c1833c9b8607c4014c85d7 (diff)
[1.7.x] Fixed #24104 -- Fixed check to look on field.get_internal_type() instead of class instance
-rw-r--r--django/db/backends/schema.py5
-rw-r--r--django/db/backends/sqlite3/schema.py9
-rw-r--r--docs/releases/1.7.4.txt4
-rw-r--r--tests/schema/fields.py54
-rw-r--r--tests/schema/tests.py45
5 files changed, 109 insertions, 8 deletions
diff --git a/django/db/backends/schema.py b/django/db/backends/schema.py
index 7c10db7dec..12e2ab7657 100644
--- a/django/db/backends/schema.py
+++ b/django/db/backends/schema.py
@@ -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:
diff --git a/django/db/backends/sqlite3/schema.py b/django/db/backends/sqlite3/schema.py
index 73c70effb9..c5f56a04a7 100644
--- a/django/db/backends/sqlite3/schema.py
+++ b/django/db/backends/sqlite3/schema.py
@@ -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)
diff --git a/docs/releases/1.7.4.txt b/docs/releases/1.7.4.txt
index 0d53459068..133dd44369 100644
--- a/docs/releases/1.7.4.txt
+++ b/docs/releases/1.7.4.txt
@@ -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`).
diff --git a/tests/schema/fields.py b/tests/schema/fields.py
new file mode 100644
index 0000000000..596671a6bc
--- /dev/null
+++ b/tests/schema/fields.py
@@ -0,0 +1,54 @@
+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']
diff --git a/tests/schema/tests.py b/tests/schema/tests.py
index f86916b5a4..a0ca9f19eb 100644
--- a/tests/schema/tests.py
+++ b/tests/schema/tests.py
@@ -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)