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

This commit is contained in:
Andriy Sokolovskiy 2015-01-24 00:16:05 +02:00 committed by Markus Holtermann
parent 6dc6ec2188
commit 3d4a826174
5 changed files with 109 additions and 8 deletions

View File

@ -3,7 +3,6 @@ import operator
from django.db.backends.creation import BaseDatabaseCreation from django.db.backends.creation import BaseDatabaseCreation
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.encoding import force_bytes from django.utils.encoding import force_bytes
from django.utils.log import getLogger from django.utils.log import getLogger
@ -359,7 +358,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.get_internal_type() == 'ManyToManyField' 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)
@ -403,7 +402,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.get_internal_type() == 'ManyToManyField' 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.utils import six from django.utils import six
from django.apps.registry import Apps from django.apps.registry import Apps
from django.db.backends.schema import BaseDatabaseSchemaEditor from django.db.backends.schema import BaseDatabaseSchemaEditor
from django.db.models.fields.related import ManyToManyField
class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
@ -70,7 +69,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.get_internal_type() == 'ManyToManyField':
mapping[field.column] = self.quote_value( mapping[field.column] = self.quote_value(
self.effective_default(field) self.effective_default(field)
) )
@ -93,7 +92,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.get_internal_type() == 'ManyToManyField' 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()
@ -172,7 +171,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.get_internal_type() == 'ManyToManyField' 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])
@ -182,7 +181,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.get_internal_type() == 'ManyToManyField':
# 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)

View File

@ -20,3 +20,7 @@ Bugfixes
* Prevented the ``static.serve`` view from producing ``ResourceWarning``\s in * Prevented the ``static.serve`` view from producing ``ResourceWarning``\s in
certain circumstances (security fix regression, :ticket:`24193`). 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`).

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

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,
@ -1310,3 +1311,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 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)