[1.7.x] Fixed #24104 -- Fixed check to look on field.get_internal_type() instead of class instance
This commit is contained in:
parent
6dc6ec2188
commit
3d4a826174
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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`).
|
||||||
|
|
|
@ -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']
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue