Skip to content

Commit ad400a9

Browse files
committed
[fix]: Prevent FallbackMixin from generating spurious migrations
The deconstruct() method was serializing the fallback kwarg into Django migration files. This caused new migrations to be generated whenever the fallback default value changed in settings, even though no actual database schema change had occurred. The fix removes fallback from deconstruct() so Django no longer tracks it as part of the field migration state. fallback is also made optional in __init__ (defaulting to None) so existing migrations that omit the kwarg remain valid. Fixes: #1231
1 parent cc2e830 commit ad400a9

2 files changed

Lines changed: 73 additions & 2 deletions

File tree

openwisp_utils/fields.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,20 @@ class FallbackMixin(object):
4848
"""
4949

5050
def __init__(self, *args, **kwargs):
51-
self.fallback = kwargs.pop("fallback")
51+
self.fallback = kwargs.pop("fallback", None)
5252
opts = dict(blank=True, null=True, default=None)
5353
opts.update(kwargs)
5454
super().__init__(*args, **opts)
5555

5656
def deconstruct(self):
5757
name, path, args, kwargs = super().deconstruct()
58-
kwargs["fallback"] = self.fallback
5958
return (name, path, args, kwargs)
6059

60+
def clone(self):
61+
obj = super().clone()
62+
obj.fallback = self.fallback
63+
return obj
64+
6165
def from_db_value(self, value, expression, connection):
6266
"""Called when fetching value from the database."""
6367
if value is None:

tests/test_project/tests/test_model.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from django.core.exceptions import ValidationError
44
from django.db import connection
5+
from django.db.migrations.autodetector import MigrationAutodetector
56
from django.test import TestCase
67

78
from ..models import Book, OrganizationRadiusSettings, Project, Shelf
@@ -180,3 +181,69 @@ def test_fallback_decimal_field(self):
180181
book.save(update_fields=["price"])
181182
book.refresh_from_db(fields=["price"])
182183
self.assertEqual(book.price, 56)
184+
185+
def test_fallback_field_deconstruct(self):
186+
with self.subTest("FallbackBooleanChoiceField"):
187+
field = OrganizationRadiusSettings._meta.get_field("is_active")
188+
name, path, args, kwargs = field.deconstruct()
189+
self.assertNotIn("fallback", kwargs)
190+
with self.subTest("FallbackCharField"):
191+
field = OrganizationRadiusSettings._meta.get_field("greeting_text")
192+
name, path, args, kwargs = field.deconstruct()
193+
self.assertNotIn("fallback", kwargs)
194+
with self.subTest("FallbackDecimalField"):
195+
field = Book._meta.get_field("price")
196+
name, path, args, kwargs = field.deconstruct()
197+
self.assertNotIn("fallback", kwargs)
198+
with self.subTest("FallbackPositiveIntegerField"):
199+
field = Shelf._meta.get_field("books_count")
200+
name, path, args, kwargs = field.deconstruct()
201+
self.assertNotIn("fallback", kwargs)
202+
with self.subTest("Plain field without fallback"):
203+
field = Shelf._meta.get_field("name")
204+
name, path, args, kwargs = field.deconstruct()
205+
self.assertNotIn("fallback", kwargs)
206+
207+
def test_fallback_field_no_migration_on_fallback_change(self):
208+
import importlib
209+
210+
from django.db.migrations.autodetector import MigrationAutodetector
211+
from django.db.migrations.loader import MigrationLoader
212+
from django.db.migrations.questioner import NonInteractiveMigrationQuestioner
213+
214+
loader = MigrationLoader(None, ignore_no_migrations=True)
215+
current_state = loader.project_state()
216+
recorded_state = current_state.clone()
217+
218+
new_fallback_by_field = {
219+
"is_active": True,
220+
"price": 99.0,
221+
"books_count": 999,
222+
}
223+
field_specs = [
224+
("test_project", "organizationradiussettings", "is_active"),
225+
("test_project", "book", "price"),
226+
("test_project", "shelf", "books_count"),
227+
]
228+
for app_label, model_name, field_name in field_specs:
229+
live_field = current_state.models[(app_label, model_name)].fields[
230+
field_name
231+
]
232+
_, path, orig_args, orig_kwargs = live_field.deconstruct()
233+
orig_kwargs["fallback"] = new_fallback_by_field[field_name]
234+
module_path, class_name = path.rsplit(".", 1)
235+
cls = getattr(importlib.import_module(module_path), class_name)
236+
recorded_state.models[(app_label, model_name)].fields[field_name] = cls(
237+
*orig_args, **orig_kwargs
238+
)
239+
240+
changes = MigrationAutodetector(
241+
recorded_state,
242+
current_state,
243+
NonInteractiveMigrationQuestioner(),
244+
).changes(graph=loader.graph)
245+
self.assertEqual(
246+
changes,
247+
{},
248+
msg="Changing the fallback value should not generate migrations",
249+
)

0 commit comments

Comments
 (0)