diff --git a/django_mongodb_backend/fields/__init__.py b/django_mongodb_backend/fields/__init__.py index 26678e1c3..87a7cac82 100644 --- a/django_mongodb_backend/fields/__init__.py +++ b/django_mongodb_backend/fields/__init__.py @@ -6,6 +6,7 @@ from .json import register_json_field from .objectid import ObjectIdField from .polymorphic_embedded_model import PolymorphicEmbeddedModelField +from .polymorphic_embedded_model_array import PolymorphicEmbeddedModelArrayField __all__ = [ "register_fields", @@ -15,6 +16,7 @@ "ObjectIdAutoField", "ObjectIdField", "PolymorphicEmbeddedModelField", + "PolymorphicEmbeddedModelArrayField", ] diff --git a/django_mongodb_backend/fields/embedded_model_array.py b/django_mongodb_backend/fields/embedded_model_array.py index 77e91f808..b4f84aa8f 100644 --- a/django_mongodb_backend/fields/embedded_model_array.py +++ b/django_mongodb_backend/fields/embedded_model_array.py @@ -224,6 +224,8 @@ class EmbeddedModelArrayFieldLessThanOrEqual( class KeyTransform(Transform): + field_class_name = "EmbeddedModelArrayField" + def __init__(self, key_name, array_field, *args, **kwargs): super().__init__(*args, **kwargs) self.array_field = array_field @@ -269,7 +271,7 @@ def get_transform(self, name): suggestion = "" raise FieldDoesNotExist( f"Unsupported lookup '{name}' for " - f"EmbeddedModelArrayField of '{output_field.__class__.__name__}'" + f"{self.field_class_name} of '{output_field.__class__.__name__}'" f"{suggestion}" ) diff --git a/django_mongodb_backend/fields/polymorphic_embedded_model_array.py b/django_mongodb_backend/fields/polymorphic_embedded_model_array.py new file mode 100644 index 000000000..8d5563ec9 --- /dev/null +++ b/django_mongodb_backend/fields/polymorphic_embedded_model_array.py @@ -0,0 +1,110 @@ +import contextlib + +from django.core.exceptions import FieldDoesNotExist +from django.db.models.expressions import Col +from django.db.models.fields.related import lazy_related_operation +from django.db.models.lookups import Lookup, Transform + +from . import PolymorphicEmbeddedModelField +from .array import ArrayField, ArrayLenTransform +from .embedded_model_array import KeyTransform as ArrayFieldKeyTransform +from .embedded_model_array import KeyTransformFactory as ArrayFieldKeyTransformFactory + + +class PolymorphicEmbeddedModelArrayField(ArrayField): + def __init__(self, embedded_models, **kwargs): + if "size" in kwargs: + raise ValueError("PolymorphicEmbeddedModelArrayField does not support size.") + kwargs["editable"] = False + super().__init__(PolymorphicEmbeddedModelField(embedded_models), **kwargs) + self.embedded_models = embedded_models + + def contribute_to_class(self, cls, name, private_only=False, **kwargs): + super().contribute_to_class(cls, name, private_only=private_only, **kwargs) + + if not cls._meta.abstract: + # If the embedded_model argument is a string, resolve it to the + # actual model class. + def _resolve_lookup(_, *resolved_models): + self.embedded_models = resolved_models + + lazy_related_operation(_resolve_lookup, cls, *self.embedded_models) + + def deconstruct(self): + name, path, args, kwargs = super().deconstruct() + if path == ( + "django_mongodb_backend.fields.polymorphic_embedded_model_array." + "PolymorphicEmbeddedModelArrayField" + ): + path = "django_mongodb_backend.fields.PolymorphicEmbeddedModelArrayField" + kwargs["embedded_models"] = self.embedded_models + del kwargs["base_field"] + del kwargs["editable"] + return name, path, args, kwargs + + def get_db_prep_value(self, value, connection, prepared=False): + if isinstance(value, list | tuple): + # Must call get_db_prep_save() rather than get_db_prep_value() + # to transform model instances to dicts. + return [self.base_field.get_db_prep_save(i, connection) for i in value] + if value is not None: + raise TypeError( + f"Expected list of {self.embedded_models!r} instances, not {type(value)!r}." + ) + return value + + def formfield(self, **kwargs): + raise NotImplementedError("PolymorphicEmbeddedModelField does not support forms.") + + _get_model_from_label = PolymorphicEmbeddedModelField._get_model_from_label + + def get_transform(self, name): + transform = super().get_transform(name) + if transform: + return transform + return KeyTransformFactory(name, self) + + def _get_lookup(self, lookup_name): + lookup = super()._get_lookup(lookup_name) + if lookup is None or lookup is ArrayLenTransform: + return lookup + + class EmbeddedModelArrayFieldLookups(Lookup): + def as_mql(self, compiler, connection): + raise ValueError( + "Lookups aren't supported on PolymorphicEmbeddedModelArrayField. " + "Try querying one of its embedded fields instead." + ) + + return EmbeddedModelArrayFieldLookups + + +class KeyTransform(ArrayFieldKeyTransform): + field_class_name = "PolymorphicEmbeddedModelArrayField" + + def __init__(self, key_name, array_field, *args, **kwargs): + # Skip ArrayFieldKeyTransform.__init__() + Transform.__init__(self, *args, **kwargs) + self.array_field = array_field + self.key_name = key_name + for model in array_field.base_field.embedded_models: + with contextlib.suppress(FieldDoesNotExist): + field = model._meta.get_field(key_name) + break + else: + raise FieldDoesNotExist( + f"The models of field '{array_field.name}' have no field named '{key_name}'." + ) + # Lookups iterate over the array of embedded models. A virtual column + # of the queried field's type represents each element. + column_target = field.clone() + column_name = f"$item.{key_name}" + column_target.db_column = column_name + column_target.set_attributes_from_name(column_name) + self._lhs = Col(None, column_target) + self._sub_transform = None + + +class KeyTransformFactory(ArrayFieldKeyTransformFactory): + def __call__(self, *args, **kwargs): + return KeyTransform(self.key_name, self.base_field, *args, **kwargs) diff --git a/django_mongodb_backend/operations.py b/django_mongodb_backend/operations.py index 8c3e1e590..bd4651eae 100644 --- a/django_mongodb_backend/operations.py +++ b/django_mongodb_backend/operations.py @@ -89,7 +89,7 @@ def convert_value(value, expression, connection): def get_db_converters(self, expression): converters = super().get_db_converters(expression) internal_type = expression.output_field.get_internal_type() - if internal_type == "ArrayField": + if internal_type.endswith("ArrayField"): converters.extend( [ self._get_arrayfield_converter(converter) @@ -111,15 +111,6 @@ def get_db_converters(self, expression): converters.append(self.convert_decimalfield_value) elif internal_type == "EmbeddedModelField": converters.append(self.convert_embeddedmodelfield_value) - elif internal_type == "EmbeddedModelArrayField": - converters.extend( - [ - self._get_arrayfield_converter(converter) - for converter in self.get_db_converters( - Expression(output_field=expression.output_field.base_field) - ) - ] - ) elif internal_type == "JSONField": converters.append(self.convert_jsonfield_value) elif internal_type == "PolymorphicEmbeddedModelField": diff --git a/docs/source/ref/models/fields.rst b/docs/source/ref/models/fields.rst index 7f36db295..870d97061 100644 --- a/docs/source/ref/models/fields.rst +++ b/docs/source/ref/models/fields.rst @@ -343,3 +343,39 @@ These indexes use 0-based indexing. .. admonition:: Forms are not supported ``PolymorphicEmbeddedModelField``\s don't appear in model forms. + +``PolymorphicEmbeddedModelArrayField`` +-------------------------------------- + +.. class:: PolymorphicEmbeddedModelArrayField(embedded_models, **kwargs) + + .. versionadded:: 5.2.0b2 + + Similar to :class:`PolymorphicEmbeddedModelField`, but stores a **list** of + models of type ``embedded_models`` rather than a single instance. + + .. attribute:: embedded_models + + This is a required argument that works just like + :attr:`PolymorphicEmbeddedModelField.embedded_models`. + + .. attribute:: max_size + + This is an optional argument. + + If passed, the list will have a maximum size as specified, validated + by forms and model validation, but not enforced by the database. + + See :ref:`the embedded model topic guide + ` for more details and + examples. + +.. admonition:: Migrations support is limited + + :djadmin:`makemigrations` does not yet detect changes to embedded models, + nor does it create indexes or constraints for embedded models referenced + by ``PolymorphicEmbeddedModelArrayField``. + +.. admonition:: Forms are not supported + + ``PolymorphicEmbeddedModelArrayField``\s don't appear in model forms. diff --git a/docs/source/releases/5.2.x.rst b/docs/source/releases/5.2.x.rst index 54438be83..01232119e 100644 --- a/docs/source/releases/5.2.x.rst +++ b/docs/source/releases/5.2.x.rst @@ -14,8 +14,9 @@ New features - Added the ``options`` parameter to :func:`~django_mongodb_backend.utils.parse_uri`. - Added support for :ref:`database transactions `. -- Added :class:`~.fields.PolymorphicEmbeddedModelField` for storing a model - instance that may be of more than one model class. +- Added :class:`~.fields.PolymorphicEmbeddedModelField` and + :class:`~.fields.PolymorphicEmbeddedModelArrayField` for storing a model + instance or list of model instances that may be of more than one model class. 5.2.0 beta 1 ============ diff --git a/docs/source/topics/embedded-models.rst b/docs/source/topics/embedded-models.rst index 7d0ddc143..6f8c08a7d 100644 --- a/docs/source/topics/embedded-models.rst +++ b/docs/source/topics/embedded-models.rst @@ -116,6 +116,8 @@ Represented in BSON, the post's structure looks like this: tags: [ { name: 'welcome' }, { name: 'test' } ] } +.. _querying-embedded-model-array-field: + Querying ``EmbeddedModelArrayField`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -264,6 +266,8 @@ bark:: >>> Person.objects.filter(pet__barks=True) +.. _polymorphic-embedded-model-field-clashing-field-names: + Clashing field names ~~~~~~~~~~~~~~~~~~~~ @@ -292,3 +296,87 @@ field it finds, ``Target1.number`` in this case. Similarly, querying into nested embedded model fields with the same name isn't well supported: the first model in ``embedded_models`` is the one that will be used for nested lookups. + +.. _polymorphic-embedded-model-array-field-example: + +``PolymorphicEmbeddedModelArrayField`` +-------------------------------------- + +The basics +~~~~~~~~~~ + +Let's consider this example:: + + from django.db import models + + from django_mongodb_backend.fields import PolymorphicEmbeddedModelArrayField + from django_mongodb_backend.models import EmbeddedModel + + + class Person(models.Model): + name = models.CharField(max_length=255) + pets = PolymorphicEmbeddedModelArrayField(["Cat", "Dog"]) + + def __str__(self): + return self.name + + + class Cat(EmbeddedModel): + name = models.CharField(max_length=255) + purrs = models.BooleanField(default=True) + + def __str__(self): + return self.name + + + class Dog(EmbeddedModel): + name = models.CharField(max_length=255) + barks = models.BooleanField(default=True) + + def __str__(self): + return self.name + + +The API is similar to that of Django's relational fields:: + + >>> bob = Person.objects.create( + ... name="Bob", + ... pets=[Dog(name="Woofer"), Cat(name="Phoebe")], + ... ) + >>> bob.pets + [, ] + >>> bob.pets[0].name + 'Woofer' + +Represented in BSON, the Bob's structure looks like this: + +.. code-block:: js + + { + _id: ObjectId('6875605cf6dc6f95cadf2d75'), + name: 'Bob', + pets: [ + { name: 'Woofer', barks: true, _label: 'polymorphic_array.Dog' }, + { name: 'Phoebe', purrs: true, _label: 'polymorphic_array.Cat' } + ] + } + +The ``_label`` field tracks each model's :attr:`~django.db.models.Options.label` +so that the models can be initialized properly. + +Querying ``PolymorphicEmbeddedModelArrayField`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can query into an embedded model array using :ref:`the same syntax and operators +` as :class:`~.fields.EmbeddedModelArrayField`. + +Like :class:`~.fields.PolymorphicEmbeddedModelField`, if you filter on fields that aren't shared +among the embedded models, you'll only get back objects that have embedded models with +those fields. + +Clashing field names +~~~~~~~~~~~~~~~~~~~~ + +As with :class:`~.fields.PolymorphicEmbeddedModelField`, take care that your embedded +models don't use :ref:`clashing field names +`. diff --git a/tests/model_fields_/models.py b/tests/model_fields_/models.py index 1a6d73ebf..4ce67bb31 100644 --- a/tests/model_fields_/models.py +++ b/tests/model_fields_/models.py @@ -7,6 +7,7 @@ EmbeddedModelArrayField, EmbeddedModelField, ObjectIdField, + PolymorphicEmbeddedModelArrayField, PolymorphicEmbeddedModelField, ) from django_mongodb_backend.models import EmbeddedModel @@ -239,7 +240,8 @@ class Dog(EmbeddedModel): barks = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) - toys = PolymorphicEmbeddedModelField(["Bone"], blank=True, null=True) + favorite_toy = PolymorphicEmbeddedModelField(["Bone"], blank=True, null=True) + toys = PolymorphicEmbeddedModelArrayField(["Bone"], blank=True, null=True) def __str__(self): return self.name @@ -249,7 +251,8 @@ class Cat(EmbeddedModel): name = models.CharField(max_length=100) purs = models.BooleanField(default=True) weight = models.DecimalField(max_digits=4, decimal_places=2, blank=True, null=True) - toys = PolymorphicEmbeddedModelField(["Mouse"], blank=True, null=True) + favorite_toy = PolymorphicEmbeddedModelField(["Mouse"], blank=True, null=True) + toys = PolymorphicEmbeddedModelArrayField(["Mouse"], blank=True, null=True) def __str__(self): return self.name @@ -267,3 +270,12 @@ class Mouse(EmbeddedModel): def __str__(self): return self.manufacturer + + +# PolymorphicEmbeddedModelArrayField +class Owner(models.Model): + name = models.CharField(max_length=100) + pets = PolymorphicEmbeddedModelArrayField(("Dog", "Cat"), blank=True, null=True) + + def __str__(self): + return self.name diff --git a/tests/model_fields_/test_polymorphic_embedded_model.py b/tests/model_fields_/test_polymorphic_embedded_model.py index 9d9057db5..a1a605a33 100644 --- a/tests/model_fields_/test_polymorphic_embedded_model.py +++ b/tests/model_fields_/test_polymorphic_embedded_model.py @@ -99,7 +99,7 @@ def setUpTestData(cls): pet=Cat( name=f"Cat {x}", weight=f"{x}.5", - toys=Mouse(manufacturer=f"Maker {x}"), + favorite_toy=Mouse(manufacturer=f"Maker {x}"), ), ) for x in range(6) @@ -110,7 +110,7 @@ def setUpTestData(cls): pet=Dog( name=f"Dog {x}", barks=x % 2 == 0, - toys=Bone(brand=f"Brand {x}"), + favorite_toy=Bone(brand=f"Brand {x}"), ), ) for x in range(6) @@ -147,17 +147,17 @@ def test_boolean(self): ) def test_nested(self): - # Cat and Dog both have field toys = PolymorphicEmbeddedModelField(...) + # Cat and Dog both have favorite_toy = PolymorphicEmbeddedModelField(...) # but with different models. It's possible to query the fields of the - # Dog's toys because it's the first model in Person.pet. + # Dog's favorite_toy because it's the first model in Person.pet. self.assertCountEqual( - Person.objects.filter(pet__toys__brand="Brand 1"), + Person.objects.filter(pet__favorite_toy__brand="Brand 1"), [self.dog_owners[1]], ) # The fields of Cat can't be queried. - msg = "The models of field 'toys' have no field named 'manufacturer'." + msg = "The models of field 'favorite_toy' have no field named 'manufacturer'." with self.assertRaisesMessage(FieldDoesNotExist, msg): - (Person.objects.filter(pet__toys__manufacturer="Maker 1"),) + (Person.objects.filter(pet__favorite_toy__manufacturer="Maker 1"),) class InvalidLookupTests(SimpleTestCase): diff --git a/tests/model_fields_/test_polymorphic_embedded_model_array.py b/tests/model_fields_/test_polymorphic_embedded_model_array.py new file mode 100644 index 000000000..73612524e --- /dev/null +++ b/tests/model_fields_/test_polymorphic_embedded_model_array.py @@ -0,0 +1,331 @@ +from decimal import Decimal + +from django.core.exceptions import FieldDoesNotExist +from django.db import models +from django.test import SimpleTestCase, TestCase +from django.test.utils import isolate_apps + +from django_mongodb_backend.fields import PolymorphicEmbeddedModelArrayField +from django_mongodb_backend.models import EmbeddedModel + +from .models import Bone, Cat, Dog, Owner + + +class MethodTests(SimpleTestCase): + def test_not_editable(self): + field = PolymorphicEmbeddedModelArrayField(["Dog"]) + self.assertIs(field.editable, False) + + def test_deconstruct(self): + field = PolymorphicEmbeddedModelArrayField(["Dog"], null=True) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django_mongodb_backend.fields.PolymorphicEmbeddedModelArrayField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"embedded_models": ["Dog"], "null": True}) + + def test_size_not_supported(self): + msg = "PolymorphicEmbeddedModelArrayField does not support size." + with self.assertRaisesMessage(ValueError, msg): + PolymorphicEmbeddedModelArrayField("Data", size=1) + + def test_get_db_prep_save_invalid(self): + msg = ( + "Expected list of (, " + ") instances, not ." + ) + with self.assertRaisesMessage(TypeError, msg): + Owner(pets=42).save() + + def test_get_db_prep_save_invalid_list(self): + msg = ( + "Expected instance of type (, " + "), not ." + ) + with self.assertRaisesMessage(TypeError, msg): + Owner(pets=[42]).save() + + +class ModelTests(TestCase): + def test_save_load(self): + pets = [Dog(name="Woofer"), Cat(name="Phoebe", weight="3.5")] + Owner.objects.create(name="Bob", pets=pets) + owner = Owner.objects.get(name="Bob") + self.assertEqual(owner.pets[0].name, "Woofer") + self.assertEqual(owner.pets[1].name, "Phoebe") + self.assertEqual(owner.pets[1].weight, Decimal("3.5")) + self.assertEqual(len(owner.pets), 2) + + def test_save_load_null(self): + Owner.objects.create(name="Bob") + owner = Owner.objects.get(name="Bob") + self.assertIsNone(owner.pets) + + +class QueryingTests(TestCase): + @classmethod + def setUpTestData(cls): + cls.fred = Owner.objects.create( + name="Fred", + pets=[ + Dog(name="Woofer", toys=[Bone(brand="Brand 1")]), + Cat(name="Phoebe", weight="3.5"), + ], + ) + cls.bob = Owner.objects.create( + name="Bob", + pets=[Dog(name="Lassy", toys=[Bone(brand="Brand 1")])], + ) + cls.mary = Owner.objects.create( + name="Mary", + pets=[ + Dog(name="Doodle"), + Cat(name="Tyler"), + ], + ) + cls.julie = Owner.objects.create( + name="Mary", + pets=[ + Cat(name="General"), + Dog(name="Skip", toys=[Bone(brand="Brand 2")]), + ], + ) + + def test_exact(self): + self.assertCountEqual(Owner.objects.filter(pets__name="Woofer"), [self.fred]) + + def test_array_index(self): + self.assertCountEqual(Owner.objects.filter(pets__0__name="Lassy"), [self.bob]) + + def test_nested_array_index(self): + self.assertCountEqual( + Owner.objects.filter(pets__toys__0__brand="Brand 1"), [self.fred, self.bob] + ) + + def test_array_slice(self): + self.assertSequenceEqual(Owner.objects.filter(pets__0_1__name="Woofer"), [self.fred]) + + # def test_filter_unsupported_lookups_in_json(self): + # """Unsupported lookups can be used as keys in a JSONField.""" + # for lookup in ["contains", "range"]: + # kwargs = {f"main_sectionigin__{lookup}": ["Pergamon", "Egypt"]} + # with CaptureQueriesContext(connection) as captured_queries: + # self.assertCountEqual(Exhibit.objects.filter(**kwargs), []) + # self.assertIn(f"'field': '{lookup}'", captured_queries[0]["sql"]) + + def test_len(self): + self.assertCountEqual(Owner.objects.filter(pets__len=3), []) + self.assertCountEqual( + Owner.objects.filter(pets__len=2), + [self.fred, self.mary, self.julie], + ) + # Nested EMF + self.assertCountEqual( + Owner.objects.filter(pets__toys__len=1), [self.fred, self.bob, self.julie] + ) + self.assertCountEqual(Owner.objects.filter(pets__toys__len=2), []) + # Nested Indexed Array + self.assertCountEqual(Owner.objects.filter(pets__0__toys__len=1), [self.fred, self.bob]) + self.assertCountEqual(Owner.objects.filter(pets__0__toys__len=0), []) + self.assertCountEqual(Owner.objects.filter(pets__1__toys__len=1), [self.julie]) + + def test_in(self): + self.assertCountEqual(Owner.objects.filter(pets__weight__in=["4.0"]), []) + self.assertCountEqual(Owner.objects.filter(pets__weight__in=["3.5"]), [self.fred]) + + def test_iexact(self): + self.assertCountEqual(Owner.objects.filter(pets__name__iexact="woofer"), [self.fred]) + + def test_gt(self): + self.assertCountEqual(Owner.objects.filter(pets__weight__gt=1), [self.fred]) + + def test_gte(self): + self.assertCountEqual(Owner.objects.filter(pets__weight__gte=1), [self.fred]) + + def test_lt(self): + self.assertCountEqual(Owner.objects.filter(pets__weight__lt=2), []) + + def test_lte(self): + self.assertCountEqual(Owner.objects.filter(pets__weight__lte=2), []) + + def test_querying_array_not_allowed(self): + msg = ( + "Lookups aren't supported on PolymorphicEmbeddedModelArrayField. " + "Try querying one of its embedded fields instead." + ) + with self.assertRaisesMessage(ValueError, msg): + Owner.objects.filter(pets=10).first() + + with self.assertRaisesMessage(ValueError, msg): + Owner.objects.filter(pets__0_1=10).first() + + def test_invalid_field(self): + msg = "The models of field 'pets' have no field named 'xxx'." + with self.assertRaisesMessage(FieldDoesNotExist, msg): + Owner.objects.filter(pets__xxx=10).first() + + def test_invalid_lookup(self): + msg = "Unsupported lookup 'return' for PolymorphicEmbeddedModelArrayField " "of 'CharField'" + with self.assertRaisesMessage(FieldDoesNotExist, msg): + Owner.objects.filter(pets__name__return="xxx") + + def test_unsupported_lookup(self): + msg = ( + "Unsupported lookup 'range' for PolymorphicEmbeddedModelArrayField " "of 'DecimalField'" + ) + with self.assertRaisesMessage(FieldDoesNotExist, msg): + Owner.objects.filter(pets__weight__range=[10]) + + def test_missing_lookup_suggestions(self): + msg = ( + "Unsupported lookup 'ltee' for PolymorphicEmbeddedModelArrayField " + "of 'DecimalField', perhaps you meant lte or lt?" + ) + with self.assertRaisesMessage(FieldDoesNotExist, msg): + Owner.objects.filter(pets__weight__ltee=3) + + def test_nested_lookup(self): + msg = "Cannot perform multiple levels of array traversal in a query." + with self.assertRaisesMessage(ValueError, msg): + Owner.objects.filter(pets__toys__name="") + + +# def test_foreign_field_exact(self): +# """Querying from a foreign key to an PolymorphicEmbeddedModelArrayField.""" +# qs = Tour.objects.filter(exhibit__sections__number=1) +# self.assertCountEqual(qs, [self.egypt_tour, self.wonders_tour]) + +# def test_foreign_field_with_slice(self): +# qs = Tour.objects.filter(exhibit__sections__0_2__number__in=[1, 2]) +# self.assertCountEqual(qs, [self.wonders_tour, self.egypt_tour]) + +# def test_subquery_numeric_lookups(self): +# subquery = Audit.objects.filter( +# section_number__in=models.OuterRef("sections__number") +# ).values("section_number")[:1] +# tests = [ +# ("exact", [self.egypt, self.new_descoveries, self.wonders]), +# ("lt", []), +# ("lte", [self.egypt, self.new_descoveries, self.wonders]), +# ("gt", [self.wonders]), +# ("gte", [self.egypt, self.new_descoveries, self.wonders]), +# ] +# for lookup, expected in tests: +# with self.subTest(lookup=lookup): +# kwargs = {f"sections__number__{lookup}": subquery} +# self.assertCountEqual(Exhibit.objects.filter(**kwargs), expected) + +# def test_subquery_in_lookup(self): +# subquery = Audit.objects.filter(reviewed=True).values_list("section_number", flat=True) +# result = Exhibit.objects.filter(sections__number__in=subquery) +# self.assertCountEqual(result, [self.wonders, self.new_descoveries, self.egypt]) + +# def test_array_as_rhs(self): +# result = Exhibit.objects.filter(main_section__number__in=models.F("sections__number")) +# self.assertCountEqual(result, [self.new_descoveries]) + +# def test_array_annotation_lookup(self): +# result = Exhibit.objects.annotate(section_numbers=models.F("main_section__number")).filter( +# section_numbers__in=models.F("sections__number") +# ) +# self.assertCountEqual(result, [self.new_descoveries]) + +# def test_array_as_rhs_for_arrayfield_lookups(self): +# tests = [ +# ("exact", [self.wonders]), +# ("lt", [self.new_descoveries]), +# ("lte", [self.wonders, self.new_descoveries]), +# ("gt", [self.egypt, self.lost_empires]), +# ("gte", [self.egypt, self.wonders, self.lost_empires]), +# ("overlap", [self.egypt, self.wonders, self.new_descoveries]), +# ("contained_by", [self.wonders]), +# ("contains", [self.egypt, self.wonders, self.new_descoveries, self.lost_empires]), +# ] +# for lookup, expected in tests: +# with self.subTest(lookup=lookup): +# kwargs = {f"section_numbers__{lookup}": models.F("sections__number")} +# result = Exhibit.objects.annotate( +# section_numbers=Value( +# [1, 2], output_field=ArrayField(base_field=models.IntegerField()) +# ) +# ).filter(**kwargs) +# self.assertCountEqual(result, expected) + +# def test_array_annotation(self): +# qs = Exhibit.objects.annotate(section_numbers=models.F("sections__nber")).order_by("name") +# self.assertQuerySetEqual(qs, [[1], [], [2], [1, 2]], attrgetter("section_numbers")) + + +@isolate_apps("model_fields_") +class CheckTests(SimpleTestCase): + def test_no_relational_fields(self): + class Target(EmbeddedModel): + key = models.ForeignKey("MyModel", models.CASCADE) + + class MyModel(models.Model): + field = PolymorphicEmbeddedModelArrayField([Target]) + + errors = MyModel().check() + self.assertEqual(len(errors), 1) + self.assertEqual(errors[0].id, "django_mongodb_backend.array.E001") + msg = errors[0].msg + self.assertEqual( + msg, + "Base field for array has errors:\n " + "Embedded models cannot have relational fields (Target.key is a ForeignKey). " + "(django_mongodb_backend.embedded_model.E001)", + ) + + def test_embedded_model_subclass(self): + class Target(models.Model): + pass + + class MyModel(models.Model): + field = PolymorphicEmbeddedModelArrayField([Target]) + + errors = MyModel().check() + self.assertEqual(len(errors), 1) + self.assertEqual(errors[0].id, "django_mongodb_backend.array.E001") + msg = errors[0].msg + self.assertEqual( + msg, + "Base field for array has errors:\n " + "Embedded models must be a subclass of " + "django_mongodb_backend.models.EmbeddedModel. " + "(django_mongodb_backend.embedded_model.E002)", + ) + + def test_clashing_fields(self): + class Target1(EmbeddedModel): + clash = models.DecimalField(max_digits=4, decimal_places=2) + + class Target2(EmbeddedModel): + clash = models.CharField(max_length=255) + + class MyModel(models.Model): + field = PolymorphicEmbeddedModelArrayField([Target1, Target2]) + + errors = MyModel().check() + self.assertEqual(len(errors), 1) + self.assertEqual(errors[0].id, "django_mongodb_backend.array.W004") + self.assertEqual( + errors[0].msg, + "Base field for array has warnings:\n " + "Embedded models model_fields_.Target1 and model_fields_.Target2 " + "both have field 'clash' of different type. " + "(django_mongodb_backend.embedded_model.E003)", + ) + + def test_clashing_fields_of_same_type(self): + """Fields of different type don't clash if they use the same db_type.""" + + class Target1(EmbeddedModel): + clash = models.TextField() + + class Target2(EmbeddedModel): + clash = models.CharField(max_length=255) + + class MyModel(models.Model): + field = PolymorphicEmbeddedModelArrayField([Target1, Target2]) + + errors = MyModel().check() + self.assertEqual(len(errors), 0)