mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	[1.8.x] Fixed #24104 -- Fixed check to look on field.many_to_many instead of class instance
Backport of 38c17871bb from master
			
			
This commit is contained in:
		
				
					committed by
					
						 Markus Holtermann
						Markus Holtermann
					
				
			
			
				
	
			
			
			
						parent
						
							0580133971
						
					
				
				
					commit
					11a5e45b96
				
			| @@ -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: | ||||||
|   | |||||||
| @@ -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
									
								
							
							
						
						
									
										54
									
								
								tests/schema/fields.py
									
									
									
									
									
										Normal 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'] | ||||||
| @@ -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) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user