Skip to content

Commit

Permalink
Merge 9087c1d into ada28b9
Browse files Browse the repository at this point in the history
  • Loading branch information
csenger committed Mar 13, 2018
2 parents ada28b9 + 9087c1d commit 33665a8
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 3 deletions.
4 changes: 3 additions & 1 deletion CHANGES.rst
Expand Up @@ -4,7 +4,9 @@ Changelog
1.3.1 (unreleased)
------------------

- Nothing changed yet.
- Support null in content PATCH requests to delete a field value
(dexterity only). Fixes #187.
[csenger]


1.3.0 (2018-03-05)
Expand Down
8 changes: 7 additions & 1 deletion docs/source/content.rst
Expand Up @@ -176,7 +176,13 @@ Updating a Resource with PATCH
------------------------------

To update an existing resource we send a PATCH request to the server.
PATCH allows to provide just a subset of the resource (the values you actually want to change):
PATCH allows to provide just a subset of the resource
(the values you actually want to change).

If you send the value ``null`` for a field, the field's content will be
deleted and the ``missing_value`` defined for the field in the schema
will be set. Note that this is not possible if the field is ``required``,
and it only works for Dexterity types, not Archetypes:

.. http:example:: curl httpie python-requests
:request: _json/content_patch.req
Expand Down
15 changes: 15 additions & 0 deletions src/plone/restapi/deserializer/dxcontent.py
Expand Up @@ -62,6 +62,21 @@ def __call__(self, validate_all=False, data=None): # noqa: ignore=C901
if not self.check_permission(write_permissions.get(name)):
continue

# set the field to missing_value if we receive null
if data[name] is None:
if not field.required:
dm.set(field.missing_value)
else:
errors.append({
'field': field.__name__,
'message': (
'{} is a required field.'.format(
field.__name__
),
'Setting it to null is not allowed.'
)})
continue

# Deserialize to field value
deserializer = queryMultiAdapter(
(field, self.context, self.request),
Expand Down
7 changes: 7 additions & 0 deletions src/plone/restapi/tests/dxtypes.py
Expand Up @@ -118,6 +118,13 @@ class IDXTestDocumentSchema(model.Schema):
test_invariant_field1 = schema.TextLine(required=False)
test_invariant_field2 = schema.TextLine(required=False)

test_missing_value_field = schema.TextLine(required=False,
missing_value=u'missing',
default=u'default')

test_missing_value_required_field = schema.TextLine(
required=True, missing_value=u'missing', default=u'some value')

@invariant
def validate_same_value(data):
if data.test_invariant_field1 != data.test_invariant_field2:
Expand Down
38 changes: 37 additions & 1 deletion src/plone/restapi/tests/test_content_patch.py
Expand Up @@ -27,7 +27,8 @@ def setUp(self):
self.portal.invokeFactory(
'Document',
id='doc1',
title='My Document'
title='My Document',
description='Some Description'
)
wftool = getToolByName(self.portal, 'portal_workflow')
wftool.doActionFor(self.portal.doc1, 'publish')
Expand All @@ -44,6 +45,41 @@ def test_patch_document(self):
transaction.begin()
self.assertEqual("Patched Document", self.portal.doc1.Title())

def test_patch_document_will_delete_value_with_null(self):
self.assertEqual(self.portal.doc1.description, 'Some Description')
response = requests.patch(
self.portal.doc1.absolute_url(),
headers={'Accept': 'application/json'},
auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD),
data='{"description": null}',
)
transaction.commit()

# null will set field.missing_value which is u'' for the field
self.assertEqual(204, response.status_code)
self.assertEqual(u'', self.portal.doc1.description)

def test_patch_document_will_not_delete_value_with_null_if_required(self):
response = requests.patch(
self.portal.doc1.absolute_url(),
headers={'Accept': 'application/json'},
auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD),
data='{"title": null}',
)
transaction.commit()

# null will set field.missing_value which is u'' for the field
self.assertEqual(400, response.status_code)
self.assertTrue("\'field\': \'title\'" in response.text)
self.assertTrue(
'title is a required field.'
in response.text
)
self.assertTrue(
'Setting it to null is not allowed.'
in response.text
)

def test_patch_document_with_representation(self):
response = requests.patch(
self.portal.doc1.absolute_url(),
Expand Down
24 changes: 24 additions & 0 deletions src/plone/restapi/tests/test_dxcontent_deserializer.py
Expand Up @@ -162,6 +162,30 @@ def test_deserializer_passes_validation_with_not_provided_defaults(self):
self.assertEquals(u'DefaultFactory',
self.portal.doc1.test_default_factory_field)

def test_deserializer_sets_missing_value_when_receiving_null(self):
self.deserialize(body='{"test_missing_value_field": null}')
self.assertEquals(u'missing',
self.portal.doc1.test_missing_value_field)

def test_deserializer_sets_missing_value_on_required_field(self):
'''We don't set missing_value if the field is required'''
self.deserialize(
body='{"test_missing_value_required_field": "valid value"}')
with self.assertRaises(BadRequest) as cm:
self.deserialize(
body='{"test_missing_value_required_field": null}')
self.assertEquals(u'valid value',
self.portal.doc1.test_missing_value_required_field)
self.assertEquals(
(
'test_missing_value_required_field is a required field.',
'Setting it to null is not allowed.'
),
cm.exception.message[0]['message']
)
self.assertEquals(u'test_missing_value_required_field',
cm.exception.message[0]['field'])

def test_set_layout(self):
current_layout = self.portal.doc1.getLayout()
self.assertNotEquals(current_layout, "my_new_layout")
Expand Down

0 comments on commit 33665a8

Please sign in to comment.