Skip to content

Commit dcaacef

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 dcaacef

2 files changed

Lines changed: 60 additions & 2 deletions

File tree

openwisp_utils/fields.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,18 @@ class FallbackMixin(object):
4747
field will save `None` in the database.
4848
"""
4949

50+
non_db_attrs = ("fallback",)
51+
5052
def __init__(self, *args, **kwargs):
51-
self.fallback = kwargs.pop("fallback")
53+
self.fallback = kwargs.pop("fallback", None)
5254
opts = dict(blank=True, null=True, default=None)
5355
opts.update(kwargs)
5456
super().__init__(*args, **opts)
5557

5658
def deconstruct(self):
5759
name, path, args, kwargs = super().deconstruct()
58-
kwargs["fallback"] = self.fallback
60+
if self.fallback is not None:
61+
kwargs["fallback"] = self.fallback
5962
return (name, path, args, kwargs)
6063

6164
def from_db_value(self, value, expression, connection):

tests/test_project/tests/test_model.py

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

33
from django.core.exceptions import ValidationError
44
from django.db import connection
5+
from django.db.migrations.autodetector import MigrationAutodetector
6+
from django.db.migrations.graph import MigrationGraph
7+
from django.db.migrations.questioner import MigrationQuestioner
8+
from django.db.migrations.state import ModelState, ProjectState
59
from django.test import TestCase
610

711
from ..models import Book, OrganizationRadiusSettings, Project, Shelf
@@ -180,3 +184,54 @@ def test_fallback_decimal_field(self):
180184
book.save(update_fields=["price"])
181185
book.refresh_from_db(fields=["price"])
182186
self.assertEqual(book.price, 56)
187+
188+
def test_fallback_field_deconstruct(self):
189+
with self.subTest("FallbackBooleanChoiceField"):
190+
field = OrganizationRadiusSettings._meta.get_field("is_active")
191+
name, path, args, kwargs = field.deconstruct()
192+
self.assertIn("fallback", kwargs)
193+
with self.subTest("FallbackCharField"):
194+
field = OrganizationRadiusSettings._meta.get_field("greeting_text")
195+
name, path, args, kwargs = field.deconstruct()
196+
self.assertIn("fallback", kwargs)
197+
with self.subTest("FallbackDecimalField"):
198+
field = Book._meta.get_field("price")
199+
name, path, args, kwargs = field.deconstruct()
200+
self.assertIn("fallback", kwargs)
201+
with self.subTest("FallbackPositiveIntegerField"):
202+
field = Shelf._meta.get_field("books_count")
203+
name, path, args, kwargs = field.deconstruct()
204+
self.assertIn("fallback", kwargs)
205+
with self.subTest("Plain field without fallback"):
206+
field = Shelf._meta.get_field("name")
207+
name, path, args, kwargs = field.deconstruct()
208+
self.assertNotIn("fallback", kwargs)
209+
210+
def test_fallback_field_no_migration_on_fallback_change(self):
211+
from django.apps import apps
212+
213+
field_specs = [
214+
("test_project", "organizationradiussettings", "is_active"),
215+
("test_project", "book", "price"),
216+
("test_project", "shelf", "books_count"),
217+
]
218+
for app_label, model_name, field_name in field_specs:
219+
with self.subTest(field=field_name):
220+
before = ProjectState.from_apps(apps)
221+
after = before.clone()
222+
223+
model_state = after.models[(app_label, model_name)]
224+
cloned = model_state.fields[field_name].clone()
225+
cloned.fallback = object()
226+
model_state.fields[field_name] = cloned
227+
228+
changes = MigrationAutodetector(
229+
before,
230+
after,
231+
MigrationQuestioner(),
232+
).changes(graph=MigrationGraph())
233+
self.assertEqual(
234+
changes,
235+
{},
236+
msg=f"Changing fallback on {field_name} should not generate migrations",
237+
)

0 commit comments

Comments
 (0)