Skip to content

Commit 729fd87

Browse files
committed
INTPYTHON-658 Add PolymorphicEmbeddedModelArrayField
1 parent 8e87ad6 commit 729fd87

File tree

8 files changed

+512
-3
lines changed

8 files changed

+512
-3
lines changed

django_mongodb_backend/fields/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .json import register_json_field
77
from .objectid import ObjectIdField
88
from .polymorphic_embedded_model import PolymorphicEmbeddedModelField
9+
from .polymorphic_embedded_model_array import PolymorphicEmbeddedModelArrayField
910

1011
__all__ = [
1112
"register_fields",
@@ -15,6 +16,7 @@
1516
"ObjectIdAutoField",
1617
"ObjectIdField",
1718
"PolymorphicEmbeddedModelField",
19+
"PolymorphicEmbeddedModelArrayField",
1820
]
1921

2022

django_mongodb_backend/fields/embedded_model_array.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,8 @@ class EmbeddedModelArrayFieldLessThanOrEqual(
224224

225225

226226
class KeyTransform(Transform):
227+
field_class_name = "EmbeddedModelArrayField"
228+
227229
def __init__(self, key_name, array_field, *args, **kwargs):
228230
super().__init__(*args, **kwargs)
229231
self.array_field = array_field
@@ -269,7 +271,7 @@ def get_transform(self, name):
269271
suggestion = ""
270272
raise FieldDoesNotExist(
271273
f"Unsupported lookup '{name}' for "
272-
f"EmbeddedModelArrayField of '{output_field.__class__.__name__}'"
274+
f"{self.field_class_name} of '{output_field.__class__.__name__}'"
273275
f"{suggestion}"
274276
)
275277

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import contextlib
2+
3+
from django.core.exceptions import FieldDoesNotExist
4+
from django.db.models.expressions import Col
5+
from django.db.models.fields.related import lazy_related_operation
6+
from django.db.models.lookups import Lookup, Transform
7+
8+
from . import PolymorphicEmbeddedModelField
9+
from .array import ArrayField, ArrayLenTransform
10+
from .embedded_model_array import KeyTransform as ArrayFieldKeyTransform
11+
from .embedded_model_array import KeyTransformFactory as ArrayFieldKeyTransformFactory
12+
13+
14+
class PolymorphicEmbeddedModelArrayField(ArrayField):
15+
value_is_model_instance = True
16+
17+
def __init__(self, embedded_models, **kwargs):
18+
if "size" in kwargs:
19+
raise ValueError("PolymorphicEmbeddedModelArrayField does not support size.")
20+
kwargs["editable"] = False
21+
super().__init__(PolymorphicEmbeddedModelField(embedded_models), **kwargs)
22+
self.embedded_models = embedded_models
23+
24+
def contribute_to_class(self, cls, name, private_only=False, **kwargs):
25+
super().contribute_to_class(cls, name, private_only=private_only, **kwargs)
26+
27+
if not cls._meta.abstract:
28+
# If the embedded_model argument is a string, resolve it to the
29+
# actual model class.
30+
def _resolve_lookup(_, *resolved_models):
31+
self.embedded_models = resolved_models
32+
33+
lazy_related_operation(_resolve_lookup, cls, *self.embedded_models)
34+
35+
def deconstruct(self):
36+
name, path, args, kwargs = super().deconstruct()
37+
if path == (
38+
"django_mongodb_backend.fields.polymorphic_embedded_model_array."
39+
"PolymorphicEmbeddedModelArrayField"
40+
):
41+
path = "django_mongodb_backend.fields.PolymorphicEmbeddedModelArrayField"
42+
kwargs["embedded_models"] = self.embedded_models
43+
del kwargs["base_field"]
44+
del kwargs["editable"]
45+
return name, path, args, kwargs
46+
47+
def get_db_prep_value(self, value, connection, prepared=False):
48+
if isinstance(value, list | tuple):
49+
# Must call get_db_prep_save() rather than get_db_prep_value()
50+
# to transform model instances to dicts.
51+
return [self.base_field.get_db_prep_save(i, connection) for i in value]
52+
if value is not None:
53+
raise TypeError(
54+
f"Expected list of {self.embedded_models!r} instances, not {type(value)!r}."
55+
)
56+
return value
57+
58+
def formfield(self, **kwargs):
59+
raise NotImplementedError("PolymorphicEmbeddedModelField does not support forms.")
60+
61+
_get_model_from_label = PolymorphicEmbeddedModelField._get_model_from_label
62+
63+
def get_transform(self, name):
64+
transform = super().get_transform(name)
65+
if transform:
66+
return transform
67+
return KeyTransformFactory(name, self)
68+
69+
def _get_lookup(self, lookup_name):
70+
lookup = super()._get_lookup(lookup_name)
71+
if lookup is None or lookup is ArrayLenTransform:
72+
return lookup
73+
74+
class EmbeddedModelArrayFieldLookups(Lookup):
75+
def as_mql(self, compiler, connection):
76+
raise ValueError(
77+
"Lookups aren't supported on PolymorphicEmbeddedModelArrayField. "
78+
"Try querying one of its embedded fields instead."
79+
)
80+
81+
return EmbeddedModelArrayFieldLookups
82+
83+
84+
class KeyTransform(ArrayFieldKeyTransform):
85+
field_class_name = "PolymorphicEmbeddedModelArrayField"
86+
87+
def __init__(self, key_name, array_field, *args, **kwargs):
88+
# Skip ArrayFieldKeyTransform.__init__()
89+
Transform.__init__(self, *args, **kwargs)
90+
self.array_field = array_field
91+
self.key_name = key_name
92+
# Lookups iterate over the array of embedded models. A virtual column
93+
# of the queried field's type represents each element.
94+
for model in array_field.base_field.embedded_models:
95+
with contextlib.suppress(FieldDoesNotExist):
96+
field = model._meta.get_field(key_name)
97+
break
98+
else:
99+
raise FieldDoesNotExist(
100+
f"The models of field '{array_field.name}' have no field named '{key_name}'."
101+
)
102+
column_target = field.clone()
103+
column_name = f"$item.{key_name}"
104+
column_target.db_column = column_name
105+
column_target.set_attributes_from_name(column_name)
106+
self._lhs = Col(None, column_target)
107+
self._sub_transform = None
108+
109+
110+
class KeyTransformFactory(ArrayFieldKeyTransformFactory):
111+
def __init__(self, key_name, base_field):
112+
self.key_name = key_name
113+
self.base_field = base_field
114+
115+
def __call__(self, *args, **kwargs):
116+
return KeyTransform(self.key_name, self.base_field, *args, **kwargs)

django_mongodb_backend/operations.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,15 @@ def get_db_converters(self, expression):
124124
converters.append(self.convert_jsonfield_value)
125125
elif internal_type == "PolymorphicEmbeddedModelField":
126126
converters.append(self.convert_polymorphicembeddedmodelfield_value)
127+
elif internal_type == "PolymorphicEmbeddedModelArrayField":
128+
converters.extend(
129+
[
130+
self._get_arrayfield_converter(converter)
131+
for converter in self.get_db_converters(
132+
Expression(output_field=expression.output_field.base_field)
133+
)
134+
]
135+
)
127136
elif internal_type == "TimeField":
128137
# Trunc(... output_field="TimeField") values must remain datetime
129138
# until Trunc.convert_value() so they can be converted from UTC

docs/source/ref/models/fields.rst

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,3 +343,39 @@ These indexes use 0-based indexing.
343343
.. admonition:: Forms are not supported
344344

345345
``PolymorphicEmbeddedModelField``\s don't appear in model forms.
346+
347+
``PolymorphicEmbeddedModelArrayField``
348+
--------------------------------------
349+
350+
.. class:: PolymorphicEmbeddedModelArrayField(embedded_models, **kwargs)
351+
352+
.. versionadded:: 5.2.0b2
353+
354+
Similar to :class:`PolymorphicEmbeddedModelField`, but stores a **list** of
355+
models of type ``embedded_models`` rather than a single instance.
356+
357+
.. attribute:: embedded_models
358+
359+
This is a required argument that works just like
360+
:attr:`PolymorphicEmbeddedModelField.embedded_models`.
361+
362+
.. attribute:: max_size
363+
364+
This is an optional argument.
365+
366+
If passed, the list will have a maximum size as specified, validated
367+
by forms and model validation, but not enforced by the database.
368+
369+
See :ref:`the embedded model topic guide
370+
<polymorphic-embedded-model-array-field-example>` for more details and
371+
examples.
372+
373+
.. admonition:: Migrations support is limited
374+
375+
:djadmin:`makemigrations` does not yet detect changes to embedded models,
376+
nor does it create indexes or constraints for embedded models referenced
377+
by ``PolymorphicEmbeddedModelArrayField``.
378+
379+
.. admonition:: Forms are not supported
380+
381+
``PolymorphicEmbeddedModelArrayField``\s don't appear in model forms.

docs/source/releases/5.2.x.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ New features
1414
- Added the ``options`` parameter to
1515
:func:`~django_mongodb_backend.utils.parse_uri`.
1616
- Added support for :ref:`database transactions <transactions>`.
17-
- Added :class:`~.fields.PolymorphicEmbeddedModelField` for storing a model
18-
instance that may be of more than one model class.
17+
- Added :class:`~.fields.PolymorphicEmbeddedModelField` and
18+
:class:`~.fields.PolymorphicEmbeddedModelArrayField` for storing a model
19+
instance or list of model instances that may be of more than one model class.
1920

2021
5.2.0 beta 1
2122
============

tests/model_fields_/models.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
EmbeddedModelArrayField,
88
EmbeddedModelField,
99
ObjectIdField,
10+
PolymorphicEmbeddedModelArrayField,
1011
PolymorphicEmbeddedModelField,
1112
)
1213
from django_mongodb_backend.models import EmbeddedModel
@@ -240,6 +241,7 @@ class Dog(EmbeddedModel):
240241
created_at = models.DateTimeField(auto_now_add=True)
241242
updated_at = models.DateTimeField(auto_now=True)
242243
favorite_toy = PolymorphicEmbeddedModelField(["Bone"], blank=True, null=True)
244+
toys = PolymorphicEmbeddedModelArrayField(["Bone"], blank=True, null=True)
243245

244246
def __str__(self):
245247
return self.name
@@ -250,6 +252,7 @@ class Cat(EmbeddedModel):
250252
purs = models.BooleanField(default=True)
251253
weight = models.DecimalField(max_digits=4, decimal_places=2, blank=True, null=True)
252254
favorite_toy = PolymorphicEmbeddedModelField(["Mouse"], blank=True, null=True)
255+
toys = PolymorphicEmbeddedModelArrayField(["Mouse"], blank=True, null=True)
253256

254257
def __str__(self):
255258
return self.name
@@ -267,3 +270,12 @@ class Mouse(EmbeddedModel):
267270

268271
def __str__(self):
269272
return self.manufacturer
273+
274+
275+
# PolymorphicEmbeddedModelArrayField
276+
class Owner(models.Model):
277+
name = models.CharField(max_length=100)
278+
pets = PolymorphicEmbeddedModelArrayField(("Dog", "Cat"), blank=True, null=True)
279+
280+
def __str__(self):
281+
return self.name

0 commit comments

Comments
 (0)