diff --git a/docs/changelog.rst b/docs/changelog.rst index ff2dd38c1..872b517eb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -24,6 +24,7 @@ Development - BugFix - Calling .clear on a ListField wasn't being marked as changed (and flushed to db upon .save()) #2858 - Improve error message in case a document assigned to a ReferenceField wasn't saved yet #1955 - BugFix - Take `where()` into account when using `.modify()`, as in MyDocument.objects().where("this[field] >= this[otherfield]").modify(field='new') #2044 +- BugFix - Unable to add new fields during `QuerySet.update` on `DynamicEmbeddedDocument` fields #2486 Changes in 0.29.0 ================= diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 980098dfb..03ec20379 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -777,6 +777,12 @@ def lookup_member(self, member_name): if field: return field + # DynamicEmbeddedDocuments should always return a field except for positional operators + if any( + doc_type._dynamic for doc_type in doc_and_subclasses + ) and member_name not in ("$", "S"): + return DynamicField(db_field=member_name) + def prepare_query_value(self, op, value): if value is not None and not isinstance(value, self.document_type): # Short circuit for special operators, returning them as is @@ -837,6 +843,12 @@ def lookup_member(self, member_name): if field: return field + # DynamicEmbeddedDocuments should always return a field except for positional operators + if any( + document_choice._dynamic for document_choice in document_choices + ) and member_name not in ("$", "S"): + return DynamicField(db_field=member_name) + def to_mongo(self, document, use_db_field=True, fields=None): if document is None: return None diff --git a/tests/fields/test_embedded_document_field.py b/tests/fields/test_embedded_document_field.py index a892c0dcd..b2a0b8d91 100644 --- a/tests/fields/test_embedded_document_field.py +++ b/tests/fields/test_embedded_document_field.py @@ -6,6 +6,7 @@ from mongoengine import ( Document, + DynamicEmbeddedDocument, EmbeddedDocument, EmbeddedDocumentField, EmbeddedDocumentListField, @@ -224,6 +225,34 @@ class Record(Document): assert Record.objects(posts__title="foo").count() == 2 + def test_update_dynamic_embedded_document_with_new_fields(self): + class Wheel(DynamicEmbeddedDocument): + position = StringField() + + class Car(Document): + wheels = EmbeddedDocumentListField(Wheel) + + car = Car( + wheels=[ + Wheel(position="front-passenger"), + Wheel(position="rear-passenger"), + Wheel(position="front-driver"), + Wheel(position="rear-driver"), + ] + ).save() + + Car.objects(wheels__position="front-driver").update( + set__wheels__S__damaged=True + ) + car.reload() + + for wheel in car.wheels: + if wheel.position == "front-driver": + assert wheel.damaged + else: + with pytest.raises(AttributeError): + wheel.damaged + class TestGenericEmbeddedDocumentField(MongoDBTestCase): def test_generic_embedded_document(self): @@ -455,3 +484,17 @@ class Person(Document): copied_map_emb_doc = deepcopy(doc.wallet_map) assert copied_map_emb_doc["test"]._instance is None + + def test_update_dynamic_embedded_document_with_new_fields(self): + class Laptop(DynamicEmbeddedDocument): + operating_system = StringField() + + class Backpack(Document): + content = GenericEmbeddedDocumentField(choices=[Laptop]) + + backpack = Backpack(content=Laptop(operating_system="Windows")).save() + + Backpack.objects.update(set__content__manufacturer="Acer") + backpack.reload() + + assert backpack.content.manufacturer == "Acer"