Skip to content

Commit 6f592f7

Browse files
committed
INTPYTHON-916 Serialize embedded model references in migrations as strings
1 parent a17ad61 commit 6f592f7

10 files changed

Lines changed: 96 additions & 13 deletions

django_mongodb_backend/fields/embedded_model.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import difflib
22

3+
from django.apps import apps
34
from django.core import checks
45
from django.core.exceptions import FieldDoesNotExist, ValidationError
56
from django.db import models
@@ -9,6 +10,8 @@
910

1011
from django_mongodb_backend import forms
1112

13+
from .utils import serialize_model_reference
14+
1215

1316
class EmbeddedModelField(models.Field):
1417
"""Field that stores a model instance."""
@@ -58,7 +61,7 @@ def deconstruct(self):
5861
path = path.replace(
5962
"django_mongodb_backend.fields.embedded_model", "django_mongodb_backend.fields"
6063
)
61-
kwargs["embedded_model"] = self.embedded_model
64+
kwargs["embedded_model"] = serialize_model_reference(self.embedded_model)
6265
return name, path, args, kwargs
6366

6467
def get_internal_type(self):
@@ -128,11 +131,18 @@ def get_db_prep_save(self, embedded_instance, connection):
128131
embedded_instance._state.adding = False
129132
return field_values
130133

134+
@cached_property
135+
def resolved_embedded_model(self):
136+
# Since Field.deconstruct() serializes self.embedded_model as a string,
137+
# it may need to be resolved after a Field.clone() in querying.
138+
model = self.embedded_model
139+
return apps.get_model(model) if isinstance(model, str) else model
140+
131141
def get_transform(self, name):
132142
transform = super().get_transform(name)
133143
if transform:
134144
return transform
135-
field = self.embedded_model._meta.get_field(name)
145+
field = self.resolved_embedded_model._meta.get_field(name)
136146
return EmbeddedModelTransformFactory(field)
137147

138148
def validate(self, value, model_instance):

django_mongodb_backend/fields/embedded_model_array.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from django_mongodb_backend.query_utils import process_lhs, process_rhs
1414

1515
from .mixins import NoEncryptedEmbeddedFieldsMixin
16+
from .utils import serialize_model_reference
1617

1718

1819
class EmbeddedModelArrayField(NoEncryptedEmbeddedFieldsMixin, ArrayField):
@@ -37,7 +38,7 @@ def deconstruct(self):
3738
name, path, args, kwargs = super().deconstruct()
3839
if path == "django_mongodb_backend.fields.embedded_model_array.EmbeddedModelArrayField":
3940
path = "django_mongodb_backend.fields.EmbeddedModelArrayField"
40-
kwargs["embedded_model"] = self.embedded_model
41+
kwargs["embedded_model"] = serialize_model_reference(self.embedded_model)
4142
del kwargs["base_field"]
4243
return name, path, args, kwargs
4344

@@ -70,7 +71,7 @@ def get_transform(self, name):
7071
transform = super().get_transform(name)
7172
if transform:
7273
return transform
73-
field = self.base_field.embedded_model._meta.get_field(name)
74+
field = self.base_field.resolved_embedded_model._meta.get_field(name)
7475
return EmbeddedModelArrayFieldTransformFactory(field)
7576

7677
def _get_lookup(self, lookup_name):

django_mongodb_backend/fields/polymorphic_embedded_model.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import contextlib
22

3+
from django.apps import apps
34
from django.core import checks
45
from django.core.exceptions import FieldDoesNotExist, ValidationError
56
from django.db import models
67
from django.db.models.fields.related import lazy_related_operation
8+
from django.utils.functional import cached_property
79

810
from .embedded_model import EmbeddedModelTransformFactory
9-
from .utils import get_mongodb_connection
11+
from .utils import get_mongodb_connection, serialize_model_reference
1012

1113

1214
class PolymorphicEmbeddedModelField(models.Field):
@@ -80,7 +82,9 @@ def deconstruct(self):
8082
"django_mongodb_backend.fields.polymorphic_embedded_model",
8183
"django_mongodb_backend.fields",
8284
)
83-
kwargs["embedded_models"] = self.embedded_models
85+
kwargs["embedded_models"] = tuple(
86+
serialize_model_reference(x) for x in self.embedded_models
87+
)
8488
del kwargs["editable"]
8589
return name, path, args, kwargs
8690

@@ -158,11 +162,21 @@ def get_db_prep_save(self, embedded_instance, connection):
158162
embedded_instance._state.adding = False
159163
return field_values
160164

165+
@cached_property
166+
def resolved_embedded_models(self):
167+
# Since Field.deconstruct() serializes self.embedded_models as a list
168+
# of strings, these strings may need to be resolved after a
169+
# Field.clone() in querying.
170+
return tuple(
171+
apps.get_model(model) if isinstance(model, str) else model
172+
for model in self.embedded_models
173+
)
174+
161175
def get_transform(self, name):
162176
transform = super().get_transform(name)
163177
if transform:
164178
return transform
165-
for model in self.embedded_models:
179+
for model in self.resolved_embedded_models:
166180
with contextlib.suppress(FieldDoesNotExist):
167181
field = model._meta.get_field(name)
168182
break

django_mongodb_backend/fields/polymorphic_embedded_model_array.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
EmbeddedModelArrayFieldTransform,
1212
EmbeddedModelArrayFieldTransformFactory,
1313
)
14+
from .utils import serialize_model_reference
1415

1516

1617
class PolymorphicEmbeddedModelArrayField(ArrayField):
@@ -39,7 +40,9 @@ def deconstruct(self):
3940
"PolymorphicEmbeddedModelArrayField"
4041
):
4142
path = "django_mongodb_backend.fields.PolymorphicEmbeddedModelArrayField"
42-
kwargs["embedded_models"] = self.embedded_models
43+
kwargs["embedded_models"] = tuple(
44+
serialize_model_reference(x) for x in self.embedded_models
45+
)
4346
del kwargs["base_field"]
4447
del kwargs["editable"]
4548
return name, path, args, kwargs
@@ -64,7 +67,7 @@ def get_transform(self, name):
6467
transform = super().get_transform(name)
6568
if transform:
6669
return transform
67-
for model in self.base_field.embedded_models:
70+
for model in self.base_field.resolved_embedded_models:
6871
with contextlib.suppress(FieldDoesNotExist):
6972
field = model._meta.get_field(name)
7073
break

django_mongodb_backend/fields/utils.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,15 @@ def get_mongodb_connection():
66
if connections[alias].vendor == "mongodb":
77
return connections[alias]
88
return None
9+
10+
11+
def serialize_model_reference(model):
12+
"""Serialize a model to its label for use in migrations."""
13+
if isinstance(model, str):
14+
# For "app_label.Model", lowercase the model name only.
15+
if "." in model:
16+
app_label, model_name = model.split(".")
17+
return f"{app_label}.{model_name.lower()}"
18+
# For "Model", lowercase it.
19+
return model.lower()
20+
return model._meta.label_lower

docs/releases/6.0.x.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ Bug fixes
1919
- Made model saving raise an exception to prevent a misconfiguration from
2020
resulting in the creation of models with encrypted fields in non-encrypted
2121
databases.
22+
- Made ``makemigrations`` serialize embedded model references as strings so
23+
that references in existing migrations don't need to be adjusted if models
24+
are later removed.
2225

2326
6.0.2
2427
=====

tests/model_fields_/test_embedded_model.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,17 @@ def test_deconstruct(self):
4242
self.assertEqual(name, "field_name")
4343
self.assertEqual(path, "django_mongodb_backend.fields.EmbeddedModelField")
4444
self.assertEqual(args, [])
45-
self.assertEqual(kwargs, {"embedded_model": "Data", "null": True})
45+
self.assertEqual(kwargs, {"embedded_model": "data", "null": True})
46+
47+
def test_deconstruct_model_class(self):
48+
field = EmbeddedModelField(Data)
49+
*_, kwargs = field.deconstruct()
50+
self.assertEqual(kwargs["embedded_model"], "model_fields_.data")
51+
52+
def test_deconstruct_model_string(self):
53+
field = EmbeddedModelField("app_label.Model")
54+
*_, kwargs = field.deconstruct()
55+
self.assertEqual(kwargs["embedded_model"], "app_label.model")
4656

4757
def test_get_db_prep_save_invalid(self):
4858
msg = "Expected instance of type <class 'model_fields_.models.Data'>, not <class 'int'>."

tests/model_fields_/test_embedded_model_array.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,17 @@ def test_deconstruct(self):
2424
self.assertEqual(name, "field_name")
2525
self.assertEqual(path, "django_mongodb_backend.fields.EmbeddedModelArrayField")
2626
self.assertEqual(args, [])
27-
self.assertEqual(kwargs, {"embedded_model": "Data", "null": True})
27+
self.assertEqual(kwargs, {"embedded_model": "data", "null": True})
28+
29+
def test_deconstruct_model_class(self):
30+
field = EmbeddedModelArrayField(Movie)
31+
*_, kwargs = field.deconstruct()
32+
self.assertEqual(kwargs["embedded_model"], "model_fields_.movie")
33+
34+
def test_deconstruct_model_string(self):
35+
field = EmbeddedModelArrayField("app_label.Model")
36+
*_, kwargs = field.deconstruct()
37+
self.assertEqual(kwargs["embedded_model"], "app_label.model")
2838

2939
def test_size_not_supported(self):
3040
msg = "EmbeddedModelArrayField does not support size."

tests/model_fields_/test_polymorphic_embedded_model.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,17 @@ def test_deconstruct(self):
2828
self.assertEqual(name, "field_name")
2929
self.assertEqual(path, "django_mongodb_backend.fields.PolymorphicEmbeddedModelField")
3030
self.assertEqual(args, [])
31-
self.assertEqual(kwargs, {"embedded_models": ["Data"], "null": True})
31+
self.assertEqual(kwargs, {"embedded_models": ("data",), "null": True})
32+
33+
def test_deconstruct_model_class(self):
34+
field = PolymorphicEmbeddedModelField([Cat, Dog])
35+
*_, kwargs = field.deconstruct()
36+
self.assertEqual(kwargs["embedded_models"], ("model_fields_.cat", "model_fields_.dog"))
37+
38+
def test_deconstruct_model_string(self):
39+
field = PolymorphicEmbeddedModelField(["app_label.Model"])
40+
*_, kwargs = field.deconstruct()
41+
self.assertEqual(kwargs["embedded_models"], ("app_label.model",))
3242

3343
def test_get_db_prep_save_invalid(self):
3444
msg = (

tests/model_fields_/test_polymorphic_embedded_model_array.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,17 @@ def test_deconstruct(self):
2323
self.assertEqual(name, "field_name")
2424
self.assertEqual(path, "django_mongodb_backend.fields.PolymorphicEmbeddedModelArrayField")
2525
self.assertEqual(args, [])
26-
self.assertEqual(kwargs, {"embedded_models": ["Dog"], "null": True})
26+
self.assertEqual(kwargs, {"embedded_models": ("dog",), "null": True})
27+
28+
def test_deconstruct_model_class(self):
29+
field = PolymorphicEmbeddedModelArrayField([Cat, Dog])
30+
*_, kwargs = field.deconstruct()
31+
self.assertEqual(kwargs["embedded_models"], ("model_fields_.cat", "model_fields_.dog"))
32+
33+
def test_deconstruct_model_string(self):
34+
field = PolymorphicEmbeddedModelArrayField(["app_label.Model"])
35+
*_, kwargs = field.deconstruct()
36+
self.assertEqual(kwargs["embedded_models"], ("app_label.model",))
2737

2838
def test_size_not_supported(self):
2939
msg = "PolymorphicEmbeddedModelArrayField does not support size."

0 commit comments

Comments
 (0)