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

This commit is contained in:
Andriy Sokolovskiy 2015-01-09 00:51:00 +02:00 committed by Markus Holtermann
parent d450af8a26
commit 38c17871bb
4 changed files with 105 additions and 8 deletions

View File

@ -1,7 +1,6 @@
import hashlib import hashlib
from django.db.backends.utils import truncate_name from django.db.backends.utils import truncate_name
from django.db.models.fields.related import ManyToManyField
from django.db.transaction import atomic from django.db.transaction import atomic
from django.utils import six from django.utils import six
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
@ -380,7 +379,7 @@ class BaseDatabaseSchemaEditor(object):
table instead (for M2M fields) table instead (for M2M fields)
""" """
# Special-case implicit M2M tables # 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) return self.create_model(field.rel.through)
# Get the column's definition # Get the column's definition
definition, params = self.column_sql(model, field, include_default=True) 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. but for M2Ms may involve deleting a table.
""" """
# Special-case implicit M2M tables # 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) return self.delete_model(field.rel.through)
# It might not actually have a column behind it # It might not actually have a column behind it
if field.db_parameters(connection=self.connection)['type'] is None: if field.db_parameters(connection=self.connection)['type'] is None:

View File

@ -4,7 +4,6 @@ from decimal import Decimal
from django.apps.registry import Apps from django.apps.registry import Apps
from django.db.backends.base.schema import BaseDatabaseSchemaEditor from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.models.fields.related import ManyToManyField
from django.utils import six from django.utils import six
import _sqlite3 import _sqlite3
@ -71,7 +70,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
for field in create_fields: for field in create_fields:
body[field.name] = field body[field.name] = field
# Choose a default and insert it into the copy map # 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( mapping[field.column] = self.quote_value(
self.effective_default(field) self.effective_default(field)
) )
@ -94,7 +93,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
del body[field.name] del body[field.name]
del mapping[field.column] del mapping[field.column]
# Remove any implicit M2M tables # 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) return self.delete_model(field.rel.through)
# Work inside a new app registry # Work inside a new app registry
apps = Apps() apps = Apps()
@ -173,7 +172,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
table instead (for M2M fields) table instead (for M2M fields)
""" """
# Special-case implicit M2M tables # 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) return self.create_model(field.rel.through)
self._remake_table(model, create_fields=[field]) self._remake_table(model, create_fields=[field])
@ -183,7 +182,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
but for M2Ms may involve deleting a table. but for M2Ms may involve deleting a table.
""" """
# M2M fields are a special case # M2M fields are a special case
if isinstance(field, ManyToManyField): if field.many_to_many:
# For implicit M2M tables, delete the auto-created table # For implicit M2M tables, delete the auto-created table
if field.rel.through._meta.auto_created: if field.rel.through._meta.auto_created:
self.delete_model(field.rel.through) self.delete_model(field.rel.through)

54
tests/schema/fields.py Normal file
View File

@ -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.
"""
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']

View File

@ -7,6 +7,7 @@ from django.db.models.fields import (BinaryField, BooleanField, CharField, Integ
PositiveIntegerField, SlugField, TextField) PositiveIntegerField, SlugField, TextField)
from django.db.models.fields.related import ForeignKey, ManyToManyField, OneToOneField from django.db.models.fields.related import ForeignKey, ManyToManyField, OneToOneField
from django.db.transaction import atomic from django.db.transaction import atomic
from .fields import CustomManyToManyField
from .models import (Author, AuthorWithDefaultHeight, AuthorWithM2M, Book, BookWithLongName, from .models import (Author, AuthorWithDefaultHeight, AuthorWithM2M, Book, BookWithLongName,
BookWithSlug, BookWithM2M, Tag, TagIndexed, TagM2MTest, TagUniqueRename, BookWithSlug, BookWithM2M, Tag, TagIndexed, TagM2MTest, TagUniqueRename,
UniqueTest, Thing, TagThrough, BookWithM2MThrough, AuthorTag, AuthorWithM2MThrough, UniqueTest, Thing, TagThrough, BookWithM2MThrough, AuthorTag, AuthorWithM2MThrough,
@ -1303,3 +1304,47 @@ class SchemaTests(TransactionTestCase):
cursor.execute("SELECT surname FROM schema_author;") cursor.execute("SELECT surname FROM schema_author;")
item = cursor.fetchall()[0] item = cursor.fetchall()[0]
self.assertEqual(item[0], None if connection.features.interprets_empty_strings_as_nulls else '') 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)