From 8b53ce38a554d7148fb8a80a3d2c34a5e9e0650d Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Tue, 6 Oct 2026 13:22:05 +0200 Subject: [PATCH] Validate only the submitted fields on update update_object_with_data re-validated the whole object after applying the record. The field managers already validate each submitted field on set, so that second pass only re-checks untouched fields, and fails when one of them no longer passes a full validation on its own: an Archetypes field holding a stored legacy value, or a Dexterity behavior invariant such as IExcludeFromNavigationDefault. A partial update of a setup object was refused even when the submitted field itself was fine. Creation still validates the whole object, which is what #66 is about. Rewritten against the current 2.x: the route helpers moved to api/mutation in #97 after this change was first written. --- docs/changelog.rst | 1 + src/senaite/jsonapi/api/mutation.py | 25 ++++++++++--- src/senaite/jsonapi/tests/doctests/update.rst | 37 +++++++++++++++++++ 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 3478b8d..ebb3e4d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,7 @@ Changelog applied. Uninstalling from the same panel removes the PAS plugin and the per-user JWT signing secrets. +- #107 Validate only the submitted fields on update - #109 Add a partition operation endpoint - #106 Fix single-valued UID reference fields not settable through the JSON API - #112 Allow updating the Laboratory through the API diff --git a/src/senaite/jsonapi/api/mutation.py b/src/senaite/jsonapi/api/mutation.py index 86d55d8..96a78a5 100644 --- a/src/senaite/jsonapi/api/mutation.py +++ b/src/senaite/jsonapi/api/mutation.py @@ -158,7 +158,7 @@ def update_items(portal_type=None, uid=None, endpoint=None, **kw): if not is_update_allowed(obj): raise ForbiddenError( "Update of {} is not allowed".format(bika_api.get_path(obj))) - obj = update_object_with_data(obj, record) + obj = update_object_with_data(obj, record, partial=True) return make_items_for([obj], endpoint=endpoint) results = [] @@ -169,7 +169,7 @@ def update_items(portal_type=None, uid=None, endpoint=None, **kw): if not is_update_allowed(obj): raise ForbiddenError( "Update of {} is not allowed".format(bika_api.get_path(obj))) - obj = update_object_with_data(obj, record) + obj = update_object_with_data(obj, record, partial=True) results.append(obj) if not results: @@ -290,11 +290,15 @@ def create_analysisrequest(container, **data): return _create_ar(container, request, data) -def update_object_with_data(content, record): +def update_object_with_data(content, record, partial=False): """Update `content` with the fields from `record`. Delegates to a registered `IUpdate` adapter if one exists; otherwise uses the default `IDataManager` machinery. + + :param partial: True on the update route, where only the submitted + fields are checked. False on creation, where the whole object is + validated. """ content = get_object(content) @@ -332,9 +336,18 @@ def update_object_with_data(content, record): # worksheet's addAnalyses populates system-managed fields such as # the layout that the generic schema validator cannot check) and # is responsible for its own validity. - invalid = bika_api.validate(content) - if invalid: - raise BadRequestError(u.to_json(invalid)) + # + # And only on creation. The field managers have already validated + # each submitted field on set, so a second pass over the whole + # object re-checks fields nobody touched, and fails on one that no + # longer passes a full validation on its own: an Archetypes field + # holding a stored legacy value, or a Dexterity behavior invariant + # such as IExcludeFromNavigationDefault. That used to refuse a + # partial update of a setup object whose submitted field was fine. + if not partial: + invalid = bika_api.validate(content) + if invalid: + raise BadRequestError(u.to_json(invalid)) if record.get("transition", None): t = record.get("transition") diff --git a/src/senaite/jsonapi/tests/doctests/update.rst b/src/senaite/jsonapi/tests/doctests/update.rst index b12a332..f55fc77 100644 --- a/src/senaite/jsonapi/tests/doctests/update.rst +++ b/src/senaite/jsonapi/tests/doctests/update.rst @@ -192,3 +192,40 @@ We cannot update the `id` of an object: >>> obj = get_item_object(response) >>> api.get_id(obj) == original_id True + + +Partial validation on update +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An update only validates the fields that were submitted. Fields left untouched +are not re-validated, so a pre-existing problem on another field does not block +an unrelated update. + + >>> data = {"portal_type": "Client", + ... "parent_path": api.get_path(clients), + ... "title": "Dolphin Corp", + ... "ClientID": "DC"} + >>> client4 = create(data) + +Clear its required `ClientID` field, bypassing the API so the stored object no +longer passes a full validation: + + >>> client4.setClientID("") + >>> transaction.commit() + +Updating a different field still succeeds (the empty `ClientID` is not +re-validated): + + >>> data = {"uid": api.get_uid(client4), "title": "Dolphin Ltd"} + >>> response = post("update", data) + >>> obj = get_item_object(response) + >>> obj.Title() + 'Dolphin Ltd' + +But submitting the offending field itself is still validated and rejected: + + >>> data = {"uid": api.get_uid(client4), "ClientID": ""} + >>> post("update", data) + Traceback (most recent call last): + [...] + HTTPError: HTTP Error 400: Bad Request