From c747253dc84c570ee1a8ba7e426b5b7cfc931e22 Mon Sep 17 00:00:00 2001 From: Brian Bernstein Date: Fri, 16 Apr 2021 23:16:47 -0400 Subject: [PATCH] When an object is already created, and is not replacing, skip all required fields that were not modified. --- tests/frameworks/test_motor_asyncio.py | 8 ++++++++ tests/frameworks/test_pymongo.py | 7 +++++++ tests/frameworks/test_txmongo.py | 7 +++++++ tests/test_data_proxy.py | 8 ++++++++ umongo/data_proxy.py | 4 +++- umongo/embedded_document.py | 4 ++-- umongo/frameworks/motor_asyncio.py | 2 +- umongo/frameworks/pymongo.py | 2 +- umongo/frameworks/txmongo.py | 2 +- 9 files changed, 38 insertions(+), 6 deletions(-) diff --git a/tests/frameworks/test_motor_asyncio.py b/tests/frameworks/test_motor_asyncio.py index e5b158bd..db0f628c 100644 --- a/tests/frameworks/test_motor_asyncio.py +++ b/tests/frameworks/test_motor_asyncio.py @@ -286,6 +286,7 @@ async def io_validate(field, value): class Dummy(Document): required_name = fields.StrField(required=True) always_io_fail = fields.IntField(io_validate=io_validate) + optional_field = fields.StrField() with pytest.raises(ma.ValidationError) as exc: await Dummy().commit() @@ -300,6 +301,13 @@ class Dummy(Document): with pytest.raises(ma.ValidationError) as exc: await dummy.commit() assert exc.value.messages == {'required_name': ['Missing data for required field.']} + # Test update of projected document does not require excluded fields + dummy = Dummy(required_name='do_not_fail_on_partial_update') + await dummy.commit() + dummy = await Dummy.find_one({'required_name': 'do_not_fail_on_partial_update'}, {'optional_field': 1}) + dummy.optional_field = 'update me!' + await dummy.commit() + loop.run_until_complete(do_test()) diff --git a/tests/frameworks/test_pymongo.py b/tests/frameworks/test_pymongo.py index 9a27761e..ace51a3b 100644 --- a/tests/frameworks/test_pymongo.py +++ b/tests/frameworks/test_pymongo.py @@ -201,6 +201,7 @@ def io_validate(field, value): class Dummy(Document): required_name = fields.StrField(required=True) always_io_fail = fields.IntField(io_validate=io_validate) + optional_field = fields.StrField() with pytest.raises(ma.ValidationError) as exc: Dummy().commit() @@ -215,6 +216,12 @@ class Dummy(Document): with pytest.raises(ma.ValidationError) as exc: dummy.commit() assert exc.value.messages == {'required_name': ['Missing data for required field.']} + # Test update of projected document does not require excluded fields + dummy = Dummy(required_name='do_not_fail_on_partial_update') + dummy.commit() + dummy = Dummy.find_one({'required_name': 'do_not_fail_on_partial_update'}, {'optional_field': 1}) + dummy.optional_field = 'update me!' + dummy.commit() def test_reference(self, classroom_model): teacher = classroom_model.Teacher(name='M. Strickland') diff --git a/tests/frameworks/test_txmongo.py b/tests/frameworks/test_txmongo.py index b9cdcb62..bbd6f11e 100644 --- a/tests/frameworks/test_txmongo.py +++ b/tests/frameworks/test_txmongo.py @@ -242,6 +242,7 @@ def io_validate(field, value): class Dummy(Document): required_name = fields.StrField(required=True) always_io_fail = fields.IntField(io_validate=io_validate) + optional_field = fields.StrField() with pytest.raises(ma.ValidationError) as exc: yield Dummy().commit() @@ -256,6 +257,12 @@ class Dummy(Document): with pytest.raises(ma.ValidationError) as exc: yield dummy.commit() assert exc.value.messages == {'required_name': ['Missing data for required field.']} + # Test update of projected document does not require excluded fields + dummy = Dummy(required_name='do_not_fail_on_partial_update') + yield dummy.commit() + dummy = yield Dummy.find_one({'required_name': 'do_not_fail_on_partial_update'}, {'optional_field': 1}) + dummy.optional_field = 'update me!' + yield dummy.commit() @pytest_inlineCallbacks def test_reference(self, classroom_model): diff --git a/tests/test_data_proxy.py b/tests/test_data_proxy.py index 95d2abbe..75dcdaf6 100644 --- a/tests/test_data_proxy.py +++ b/tests/test_data_proxy.py @@ -385,6 +385,14 @@ class MySchema(BaseSchema): 'listed': {0: {'required': ['Missing data for required field.']}}, 'dicted': {'a': {'value': {'required': ['Missing data for required field.']}}}, } + # Required validate should filter on modified fields if passed in + d.load({}) + d.required_validate(partial_fields=[]) + with pytest.raises(ma.ValidationError) as exc: + d.required_validate(partial_fields=['required']) + assert exc.value.messages == { + 'required': ['Missing data for required field.'] + } def test_unkown_field_in_db(self): class MySchema(BaseSchema): diff --git a/umongo/data_proxy.py b/umongo/data_proxy.py index b7351e3a..b65fe55a 100644 --- a/umongo/data_proxy.py +++ b/umongo/data_proxy.py @@ -162,9 +162,11 @@ def _add_missing_fields(self): else: self._data[mongo_name] = field.missing - def required_validate(self): + def required_validate(self, partial_fields=None): errors = {} for name, field in self.schema.fields.items(): + if partial_fields is not None and name not in partial_fields: + continue value = self._data[field.attribute or name] if field.required and value is ma.missing: errors[name] = [_("Missing data for required field.")] diff --git a/umongo/embedded_document.py b/umongo/embedded_document.py index a6d9ab0d..e074d3c2 100644 --- a/umongo/embedded_document.py +++ b/umongo/embedded_document.py @@ -114,8 +114,8 @@ def clear_modified(self): """ self._data.clear_modified() - def required_validate(self): - self._data.required_validate() + def required_validate(self, partial_fields=None): + self._data.required_validate(partial_fields=partial_fields) @classmethod def build_from_mongo(cls, data, use_cls=True): diff --git a/umongo/frameworks/motor_asyncio.py b/umongo/frameworks/motor_asyncio.py index 30c82709..b4ab4893 100644 --- a/umongo/frameworks/motor_asyncio.py +++ b/umongo/frameworks/motor_asyncio.py @@ -158,7 +158,7 @@ async def commit(self, io_validate_all=False, conditions=None, replace=False): additional_filter = await self.__coroutined_pre_update() if additional_filter: query.update(map_query(additional_filter, self.schema.fields)) - self.required_validate() + self.required_validate(self._data.get_modified_fields()) await self.io_validate(validate_all=io_validate_all) if replace: payload = self._data.to_mongo(update=False) diff --git a/umongo/frameworks/pymongo.py b/umongo/frameworks/pymongo.py index 88159501..fc80be91 100644 --- a/umongo/frameworks/pymongo.py +++ b/umongo/frameworks/pymongo.py @@ -108,7 +108,7 @@ def commit(self, io_validate_all=False, conditions=None, replace=False): additional_filter = self.pre_update() if additional_filter: query.update(map_query(additional_filter, self.schema.fields)) - self.required_validate() + self.required_validate(partial_fields=self._data.get_modified_fields()) self.io_validate(validate_all=io_validate_all) if replace: payload = self._data.to_mongo(update=False) diff --git a/umongo/frameworks/txmongo.py b/umongo/frameworks/txmongo.py index 9bbc074c..e2dd0e91 100644 --- a/umongo/frameworks/txmongo.py +++ b/umongo/frameworks/txmongo.py @@ -64,7 +64,7 @@ def commit(self, io_validate_all=False, conditions=None, replace=False): additional_filter = yield maybeDeferred(self.pre_update) if additional_filter: query.update(map_query(additional_filter, self.schema.fields)) - self.required_validate() + self.required_validate(self._data.get_modified_fields()) yield self.io_validate(validate_all=io_validate_all) if replace: payload = self._data.to_mongo(update=False)