diff --git a/CHANGES.rst b/CHANGES.rst index e74eb321e0..43dd2c3041 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,11 +10,16 @@ New Features: and breadcrumbs as components. [timo] +- Add support for setting/modifying 'layout' on DX and AT content endpoints. + [jaroel] + +- Add support for getting the defined layouts on the root types endpoint. + [jaroel] Bugfixes: -- Add the title to the workflow history in the @workflow endpoint. This fixes - #279. +- Add the title to the workflow history in the @workflow endpoint. + This fixes #279. [sneridagh] - Don't fetch unnecessary PasswordResetTool in Plone 5.1 diff --git a/docs/source/_json/collection.resp b/docs/source/_json/collection.resp index 39ac92c9ce..74ee02047f 100644 --- a/docs/source/_json/collection.resp +++ b/docs/source/_json/collection.resp @@ -44,6 +44,7 @@ Content-Type: application/json ], "items_total": 3, "language": "", + "layout": "listing_view", "limit": 1000, "modified": "2016-01-21T08:24:11+00:00", "parent": { diff --git a/docs/source/_json/content_get.resp b/docs/source/_json/content_get.resp index ead081b2ef..a9d3d5d727 100644 --- a/docs/source/_json/content_get.resp +++ b/docs/source/_json/content_get.resp @@ -18,6 +18,7 @@ Content-Type: application/json "expires": null, "id": "my-document", "language": "", + "layout": "document_view", "modified": "2016-10-21T19:00:00+00:00", "parent": { "@id": "http://localhost:55001/plone/folder", diff --git a/docs/source/_json/content_post.resp b/docs/source/_json/content_post.resp index ecee4712b7..4d466e03d7 100644 --- a/docs/source/_json/content_post.resp +++ b/docs/source/_json/content_post.resp @@ -19,6 +19,7 @@ Location: http://localhost:55001/plone/folder/my-document "expires": null, "id": "my-document", "language": "", + "layout": "document_view", "modified": "2016-10-21T19:00:00+00:00", "parent": { "@id": "http://localhost:55001/plone/folder", diff --git a/docs/source/_json/document.resp b/docs/source/_json/document.resp index c559fbb8b2..fe3981f42c 100644 --- a/docs/source/_json/document.resp +++ b/docs/source/_json/document.resp @@ -19,6 +19,7 @@ Content-Type: application/json "expires": null, "id": "front-page", "language": "", + "layout": "document_view", "modified": "2016-01-21T01:24:11+00:00", "parent": { "@id": "http://localhost:55001/plone", diff --git a/docs/source/_json/event.resp b/docs/source/_json/event.resp index 3f0d30ff35..102751e52d 100644 --- a/docs/source/_json/event.resp +++ b/docs/source/_json/event.resp @@ -25,6 +25,7 @@ Content-Type: application/json "expires": null, "id": "event", "language": "", + "layout": "event_view", "location": null, "modified": "2016-01-21T03:24:11+00:00", "open_end": null, diff --git a/docs/source/_json/file.resp b/docs/source/_json/file.resp index 4a963ddba8..a5a3692f3c 100644 --- a/docs/source/_json/file.resp +++ b/docs/source/_json/file.resp @@ -24,6 +24,7 @@ Content-Type: application/json }, "id": "file", "language": "", + "layout": "file_view", "modified": "2016-01-21T05:24:11+00:00", "parent": { "@id": "http://localhost:55001/plone", diff --git a/docs/source/_json/folder.resp b/docs/source/_json/folder.resp index 168592757f..8965e3ec3b 100644 --- a/docs/source/_json/folder.resp +++ b/docs/source/_json/folder.resp @@ -35,6 +35,7 @@ Content-Type: application/json ], "items_total": 2, "language": "", + "layout": "listing_view", "modified": "2016-01-21T07:24:11+00:00", "nextPreviousEnabled": false, "parent": { diff --git a/docs/source/_json/image.resp b/docs/source/_json/image.resp index 924d7f9eb7..3f7bb60fd3 100644 --- a/docs/source/_json/image.resp +++ b/docs/source/_json/image.resp @@ -63,6 +63,7 @@ Content-Type: application/json "width": 215 }, "language": "", + "layout": "image_view", "modified": "2016-01-21T06:24:11+00:00", "parent": { "@id": "http://localhost:55001/plone", diff --git a/docs/source/_json/link.resp b/docs/source/_json/link.resp index 0807699ecf..7d4cf8071e 100644 --- a/docs/source/_json/link.resp +++ b/docs/source/_json/link.resp @@ -19,6 +19,7 @@ Content-Type: application/json "expires": null, "id": "link", "language": "", + "layout": "link_redirect_view", "modified": "2016-01-21T04:24:11+00:00", "parent": { "@id": "http://localhost:55001/plone", diff --git a/docs/source/_json/newsitem.resp b/docs/source/_json/newsitem.resp index 093ab94d04..7709cf293c 100644 --- a/docs/source/_json/newsitem.resp +++ b/docs/source/_json/newsitem.resp @@ -65,6 +65,7 @@ Content-Type: application/json }, "image_caption": "This is an image caption.", "language": "", + "layout": "newsitem_view", "modified": "2016-01-21T02:24:11+00:00", "parent": { "@id": "http://localhost:55001/plone", diff --git a/docs/source/_json/types_document.resp b/docs/source/_json/types_document.resp index bc646053b0..7a4a181962 100644 --- a/docs/source/_json/types_document.resp +++ b/docs/source/_json/types_document.resp @@ -49,6 +49,9 @@ Content-Type: application/json+schema "title": "Ownership" } ], + "layouts": [ + "document_view" + ], "properties": { "allow_discussion": { "choices": [ diff --git a/docs/source/types.rst b/docs/source/types.rst index 189b7f69ff..f01d7ab736 100644 --- a/docs/source/types.rst +++ b/docs/source/types.rst @@ -4,7 +4,7 @@ Types .. note:: These docs are generated by code tests, therefore you will see some 'test' contenttypes appear here. -Available content types in a Plone site can be listed and queried by accessing the ``/@types`` endpoint on any context (requires an authenticated user). The 'addable' key specifies if the content type can be added to the current context. +Available content types in a Plone site can be listed and queried by accessing the ``/@types`` endpoint on any context (requires an authenticated user). The 'addable' key specifies if the content type can be added to the current context. The 'layouts' key specifies the defined views. .. http:example:: curl httpie python-requests diff --git a/src/plone/restapi/deserializer/atcontent.py b/src/plone/restapi/deserializer/atcontent.py index f7f05eb3ee..173f02acd1 100644 --- a/src/plone/restapi/deserializer/atcontent.py +++ b/src/plone/restapi/deserializer/atcontent.py @@ -67,6 +67,12 @@ def __call__(self, validate_all=False): notify(ObjectEditedEvent(obj)) obj.at_post_edit_script() + # We'll set the layout after the validation and and even if there + # are no other changes. + if 'layout' in data: + layout = data['layout'] + self.context.setLayout(layout) + return obj def validate(self): diff --git a/src/plone/restapi/deserializer/dxcontent.py b/src/plone/restapi/deserializer/dxcontent.py index 0ed09efcbf..90c1278979 100644 --- a/src/plone/restapi/deserializer/dxcontent.py +++ b/src/plone/restapi/deserializer/dxcontent.py @@ -32,7 +32,7 @@ def __init__(self, context, request): self.sm = getSecurityManager() self.permission_cache = {} - def __call__(self, validate_all=False): + def __call__(self, validate_all=False): # noqa: ignore=C901 data = json_body(self.request) modified = False @@ -105,6 +105,12 @@ def __call__(self, validate_all=False): if errors: raise BadRequest(errors) + # We'll set the layout after the validation and and even if there + # are no other changes. + if 'layout' in data: + layout = data['layout'] + self.context.setLayout(layout) + if modified: notify(ObjectModifiedEvent(self.context)) diff --git a/src/plone/restapi/serializer/atcontent.py b/src/plone/restapi/serializer/atcontent.py index 430cc132f3..3a41371548 100644 --- a/src/plone/restapi/serializer/atcontent.py +++ b/src/plone/restapi/serializer/atcontent.py @@ -35,6 +35,7 @@ def __call__(self): 'parent': parent_summary, 'review_state': self._get_workflow_state(), 'UID': self.context.UID(), + 'layout': self.context.getLayout(), } obj = self.context diff --git a/src/plone/restapi/serializer/dxcontent.py b/src/plone/restapi/serializer/dxcontent.py index 82d5645858..d2f37f3463 100644 --- a/src/plone/restapi/serializer/dxcontent.py +++ b/src/plone/restapi/serializer/dxcontent.py @@ -47,6 +47,7 @@ def __call__(self): 'modified': json_compatible(self.context.modified()), 'review_state': self._get_workflow_state(), 'UID': self.context.UID(), + 'layout': self.context.getLayout(), } for schema in iterSchemata(self.context): diff --git a/src/plone/restapi/tests/test_atcontent_deserializer.py b/src/plone/restapi/tests/test_atcontent_deserializer.py index 409ac3ee3c..8c4d428a36 100644 --- a/src/plone/restapi/tests/test_atcontent_deserializer.py +++ b/src/plone/restapi/tests/test_atcontent_deserializer.py @@ -132,6 +132,12 @@ def __call__(self, request): self.assertEquals( 'pre_validation_error', cm.exception.message[0]['message']) + def test_set_layout(self): + current_layout = self.doc1.getLayout() + self.assertNotEquals(current_layout, "my_new_layout") + self.deserialize(body='{"layout": "my_new_layout"}') + self.assertEquals('my_new_layout', self.doc1.getLayout()) + class TestValidationRequest(unittest.TestCase): diff --git a/src/plone/restapi/tests/test_atcontent_serializer.py b/src/plone/restapi/tests/test_atcontent_serializer.py index 76ddd9d81c..143dde64fa 100644 --- a/src/plone/restapi/tests/test_atcontent_serializer.py +++ b/src/plone/restapi/tests/test_atcontent_serializer.py @@ -143,3 +143,9 @@ def test_serializer_orders_folder_items_by_get_object_position_in_parent(self): 'review_state': 'private' }, ]) + + def test_get_layout(self): + current_layout = self.doc1.getLayout() + obj = self.serialize(self.doc1) + self.assertIn('layout', obj) + self.assertEquals(current_layout, obj['layout']) diff --git a/src/plone/restapi/tests/test_dxcontent_deserializer.py b/src/plone/restapi/tests/test_dxcontent_deserializer.py index 45fd157417..2f67121ea1 100644 --- a/src/plone/restapi/tests/test_dxcontent_deserializer.py +++ b/src/plone/restapi/tests/test_dxcontent_deserializer.py @@ -127,3 +127,9 @@ def test_deserializer_passes_validation_with_not_provided_defaults(self): self.portal.doc1.test_default_value_field) self.assertEquals(u'DefaultFactory', self.portal.doc1.test_default_factory_field) + + def test_set_layout(self): + current_layout = self.portal.doc1.getLayout() + self.assertNotEquals(current_layout, "my_new_layout") + self.deserialize(body='{"layout": "my_new_layout"}') + self.assertEquals('my_new_layout', self.portal.doc1.getLayout()) diff --git a/src/plone/restapi/tests/test_dxcontent_serializer.py b/src/plone/restapi/tests/test_dxcontent_serializer.py index 506868698b..ad695176c8 100644 --- a/src/plone/restapi/tests/test_dxcontent_serializer.py +++ b/src/plone/restapi/tests/test_dxcontent_serializer.py @@ -116,3 +116,9 @@ def test_serializer_includes_field_with_read_permission(self): obj = self.serialize() self.assertIn(u'test_read_permission_field', obj) self.assertEqual(u'Secret Stuff', obj[u'test_read_permission_field']) + + def test_get_layout(self): + current_layout = self.portal.doc1.getLayout() + obj = self.serialize() + self.assertIn('layout', obj) + self.assertEquals(current_layout, obj['layout']) diff --git a/src/plone/restapi/tests/test_types.py b/src/plone/restapi/tests/test_types.py index f4d20f6191..d7dd7053b3 100644 --- a/src/plone/restapi/tests/test_types.py +++ b/src/plone/restapi/tests/test_types.py @@ -98,6 +98,7 @@ def test_get_jsonschema_for_fti(self): self.assertIn('title', jsonschema['required']) self.assertEquals('default', jsonschema['fieldsets'][0]['id']) self.assertIn('title', jsonschema['fieldsets'][0]['fields']) + self.assertIn('layouts', jsonschema) jsonschema = get_jsonschema_for_fti( ttool['Document'], diff --git a/src/plone/restapi/types/utils.py b/src/plone/restapi/types/utils.py index 9ae191bcd2..1e7b5c9172 100644 --- a/src/plone/restapi/types/utils.py +++ b/src/plone/restapi/types/utils.py @@ -155,6 +155,7 @@ def get_jsonschema_for_fti(fti, context, request, excluded_fields=None): 'properties': properties, 'required': required, 'fieldsets': get_fieldset_infos(fieldsets), + 'layouts': getattr(fti, 'view_methods', []), } diff --git a/test-no-uncommitted-doc-changes.in b/test-no-uncommitted-doc-changes.in index ed41a81d63..f983b70104 100644 --- a/test-no-uncommitted-doc-changes.in +++ b/test-no-uncommitted-doc-changes.in @@ -20,7 +20,7 @@ if [ "$PLONE_VERSION" = "5.0.x" ]; then exit 0 fi -changes=$(git diff --exit-code $DUMPS_DIR) +changes=$(git diff --ignore-space-at-eol --exit-code $DUMPS_DIR) if [ $? -ne 0 ]; then red "ERROR: There are modified files in $DUMPS_DIR after running test_documentation.py!" red