Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 19 additions & 6 deletions src/senaite/jsonapi/api/mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand All @@ -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:
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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")
Expand Down
37 changes: 37 additions & 0 deletions src/senaite/jsonapi/tests/doctests/update.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading