diff --git a/django/db/migrations/operations/fields.py b/django/db/migrations/operations/fields.py index b80345e0de..b2c552c546 100644 --- a/django/db/migrations/operations/fields.py +++ b/django/db/migrations/operations/fields.py @@ -1,3 +1,4 @@ +from django.core.exceptions import FieldDoesNotExist from django.db.models.fields import NOT_PROVIDED from django.utils.functional import cached_property @@ -261,25 +262,27 @@ class RenameField(FieldOperation): ) def state_forwards(self, app_label, state): + model_state = state.models[app_label, self.model_name_lower] # Rename the field - state.models[app_label, self.model_name_lower].fields = [ - (self.new_name if n == self.old_name else n, f) - for n, f in state.models[app_label, self.model_name_lower].fields - ] + fields = model_state.fields + for index, (name, field) in enumerate(fields): + if name == self.old_name: + fields[index] = (self.new_name, field) + # Delay rendering of relationships if it's not a relational field. + delay = not field.is_relation + break + else: + raise FieldDoesNotExist( + "%s.%s has no field named '%s'" % (app_label, self.model_name, self.old_name) + ) # Fix index/unique_together to refer to the new field - options = state.models[app_label, self.model_name_lower].options + options = model_state.options for option in ('index_together', 'unique_together'): if option in options: options[option] = [ [self.new_name if n == self.old_name else n for n in together] for together in options[option] ] - for n, f in state.models[app_label, self.model_name_lower].fields: - if n == self.new_name: - field = f - break - # Delay rendering of relationships if it's not a relational field - delay = not field.is_relation state.reload_model(app_label, self.model_name_lower, delay=delay) def database_forwards(self, app_label, schema_editor, from_state, to_state): diff --git a/docs/releases/1.11.3.txt b/docs/releases/1.11.3.txt index 1c716bf5a3..227c94abb4 100644 --- a/docs/releases/1.11.3.txt +++ b/docs/releases/1.11.3.txt @@ -54,3 +54,6 @@ Bugfixes * Prevented a primary key alteration from adding a foreign key constraint if ``db_constraint=False`` (:ticket:`28298`). + +* Fixed ``UnboundLocalError`` crash in ``RenameField`` with nonexistent field + (:ticket:`28350`). diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py index f0d24443a4..ec55ab912e 100644 --- a/tests/migrations/test_operations.py +++ b/tests/migrations/test_operations.py @@ -1,5 +1,6 @@ import unittest +from django.core.exceptions import FieldDoesNotExist from django.db import connection, migrations, models, transaction from django.db.migrations.migration import Migration from django.db.migrations.operations import CreateModel @@ -1368,6 +1369,12 @@ class OperationTests(OperationTestBase): self.assertEqual(definition[1], []) self.assertEqual(definition[2], {'model_name': "Pony", 'old_name': "pink", 'new_name': "blue"}) + def test_rename_missing_field(self): + state = ProjectState() + state.add_model(ModelState('app', 'model', [])) + with self.assertRaisesMessage(FieldDoesNotExist, "app.model has no field named 'field'"): + migrations.RenameField('model', 'field', 'new_field').state_forwards('app', state) + def test_alter_unique_together(self): """ Tests the AlterUniqueTogether operation.