Browse Source

Fixed #33471 -- Made AlterField operation a noop when changing "choices".

This also allows customizing attributes of fields that don't affect
a column definition.
sarahboyce 3 years ago
parent
commit
65effbdb10

+ 2 - 15
django/db/backends/base/schema.py

@@ -1376,22 +1376,9 @@ class BaseDatabaseSchemaEditor:
         # - changing only a field name
         # - changing an attribute that doesn't affect the schema
         # - adding only a db_column and the column name is not changed
-        non_database_attrs = [
-            "blank",
-            "db_column",
-            "editable",
-            "error_messages",
-            "help_text",
-            "limit_choices_to",
-            # Database-level options are not supported, see #21961.
-            "on_delete",
-            "related_name",
-            "related_query_name",
-            "validators",
-            "verbose_name",
-        ]
-        for attr in non_database_attrs:
+        for attr in old_field.non_db_attrs:
             old_kwargs.pop(attr, None)
+        for attr in new_field.non_db_attrs:
             new_kwargs.pop(attr, None)
         return self.quote_name(old_field.column) != self.quote_name(
             new_field.column

+ 18 - 0
django/db/models/fields/__init__.py

@@ -140,6 +140,24 @@ class Field(RegisterLookupMixin):
     system_check_deprecated_details = None
     system_check_removed_details = None
 
+    # Attributes that don't affect a column definition.
+    # These attributes are ignored when altering the field.
+    non_db_attrs = (
+        "blank",
+        "choices",
+        "db_column",
+        "editable",
+        "error_messages",
+        "help_text",
+        "limit_choices_to",
+        # Database-level options are not supported, see #21961.
+        "on_delete",
+        "related_name",
+        "related_query_name",
+        "validators",
+        "verbose_name",
+    )
+
     # Field flags
     hidden = False
 

+ 20 - 0
docs/howto/custom-model-fields.txt

@@ -314,6 +314,26 @@ reconstructing the field::
     new_instance = MyField(*args, **kwargs)
     self.assertEqual(my_field_instance.some_attribute, new_instance.some_attribute)
 
+.. _custom-field-non_db_attrs:
+
+Field attributes not affecting database column definition
+---------------------------------------------------------
+
+.. versionadded:: 4.1
+
+You can override ``Field.non_db_attrs`` to customize attributes of a field that
+don't affect a column definition. It's used during model migrations to detect
+no-op ``AlterField`` operations.
+
+For example::
+
+    class CommaSepField(models.Field):
+
+        @property
+        def non_db_attrs(self):
+            return super().non_db_attrs + ("separator",)
+
+
 Changing a custom field's base class
 ------------------------------------
 

+ 4 - 0
docs/releases/4.1.txt

@@ -288,6 +288,10 @@ Models
   on MariaDB and MySQL. For databases that do not support ``XOR``, the query
   will be converted to an equivalent using ``AND``, ``OR``, and ``NOT``.
 
+* The new :ref:`Field.non_db_attrs <custom-field-non_db_attrs>` attribute
+  allows customizing attributes of fields that don't affect a column
+  definition.
+
 Requests and Responses
 ~~~~~~~~~~~~~~~~~~~~~~
 

+ 14 - 0
tests/schema/tests.py

@@ -3961,6 +3961,20 @@ class SchemaTests(TransactionTestCase):
         with connection.schema_editor() as editor, self.assertNumQueries(0):
             editor.alter_field(Book, new_field, old_field, strict=True)
 
+    def test_alter_field_choices_noop(self):
+        with connection.schema_editor() as editor:
+            editor.create_model(Author)
+        old_field = Author._meta.get_field("name")
+        new_field = CharField(
+            choices=(("Jane", "Jane"), ("Joe", "Joe")),
+            max_length=255,
+        )
+        new_field.set_attributes_from_name("name")
+        with connection.schema_editor() as editor, self.assertNumQueries(0):
+            editor.alter_field(Author, old_field, new_field, strict=True)
+        with connection.schema_editor() as editor, self.assertNumQueries(0):
+            editor.alter_field(Author, new_field, old_field, strict=True)
+
     def test_add_textfield_unhashable_default(self):
         # Create the table
         with connection.schema_editor() as editor: