From 202bf69c2f69d9ee20ac3fd409116a8946dc0784 Mon Sep 17 00:00:00 2001 From: Andrew Gorcester Date: Wed, 19 Feb 2014 15:44:57 -0800 Subject: [PATCH] Fixed #22095 -- Enabled backward migrations for RunPython operations Added reversible property to RunPython so that migrations will not refuse to reverse migrations including RunPython operations, so long as reverse_code is set in the RunPython constructor. Included tests to check the reversible property on RunPython and the similar RunSQL. --- django/db/migrations/operations/special.py | 5 ++++- tests/migrations/test_operations.py | 24 ++++++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/django/db/migrations/operations/special.py b/django/db/migrations/operations/special.py index 714a6d59f9..40ae1f85b7 100644 --- a/django/db/migrations/operations/special.py +++ b/django/db/migrations/operations/special.py @@ -103,7 +103,6 @@ class RunPython(Operation): """ reduces_to_sql = False - reversible = False def __init__(self, code, reverse_code=None): # Forwards code @@ -118,6 +117,10 @@ class RunPython(Operation): raise ValueError("RunPython must be supplied with callable arguments") self.reverse_code = reverse_code + @property + def reversible(self): + return self.reverse_code is not None + def state_forwards(self, app_label, state): # RunPython objects have no state effect. To add some, combine this # with SeparateDatabaseAndState. diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py index ef23dcf80f..c755ff0305 100644 --- a/tests/migrations/test_operations.py +++ b/tests/migrations/test_operations.py @@ -468,6 +468,7 @@ class OperationTests(MigrationTestBase): operation.database_forwards("test_runsql", editor, project_state, new_state) self.assertTableExists("i_love_ponies") # And test reversal + self.assertTrue(operation.reversible) with connection.schema_editor() as editor: operation.database_backwards("test_runsql", editor, new_state, project_state) self.assertTableNotExists("i_love_ponies") @@ -484,7 +485,11 @@ class OperationTests(MigrationTestBase): Pony = models.get_model("test_runpython", "Pony") Pony.objects.create(pink=1, weight=3.55) Pony.objects.create(weight=5) - operation = migrations.RunPython(inner_method) + def inner_method_reverse(models, schema_editor): + Pony = models.get_model("test_runpython", "Pony") + Pony.objects.filter(pink=1, weight=3.55).delete() + Pony.objects.filter(weight=5).delete() + operation = migrations.RunPython(inner_method, reverse_code=inner_method_reverse) # Test the state alteration does nothing new_state = project_state.clone() operation.state_forwards("test_runpython", new_state) @@ -494,13 +499,24 @@ class OperationTests(MigrationTestBase): with connection.schema_editor() as editor: operation.database_forwards("test_runpython", editor, project_state, new_state) self.assertEqual(project_state.render().get_model("test_runpython", "Pony").objects.count(), 2) - # And test reversal fails - with self.assertRaises(NotImplementedError): - operation.database_backwards("test_runpython", None, new_state, project_state) + # Now test reversal + self.assertTrue(operation.reversible) + with connection.schema_editor() as editor: + operation.database_backwards("test_runpython", editor, project_state, new_state) + self.assertEqual(project_state.render().get_model("test_runpython", "Pony").objects.count(), 0) # Now test we can't use a string with self.assertRaises(ValueError): operation = migrations.RunPython("print 'ahahaha'") + # Also test reversal fails, with an operation identical to above but without reverse_code set + no_reverse_operation = migrations.RunPython(inner_method) + self.assertFalse(no_reverse_operation.reversible) + with connection.schema_editor() as editor: + no_reverse_operation.database_forwards("test_runpython", editor, project_state, new_state) + with self.assertRaises(NotImplementedError): + no_reverse_operation.database_backwards("test_runpython", editor, new_state, project_state) + + class MigrateNothingRouter(object): """