plone.restapi/unify-expired-and-excluded-items-handling: Merge branch 'master' into translate-workflow-titles
Timo Stollenwerk <jenkins-z4DKO/[email protected]> Sat, 29 Jul 2017 16:31:39 -0700 (PDT)
| Newsgroups | gmane.comp.web.zope.plone.cvs |
|---|---|
| Message-ID | <[email protected]> |
Repository: plone.restapi Branch: refs/heads/unify-expired-and-excluded-items-handling Date: 2017-07-26T16:26:31+02:00 Author: Timo Stollenwerk (tisto) <tisto-z4DKO/[email protected]> Commit: https://github.com/plone/plone.restapi/commit/2d7420ab9a17c5cacb3c9749cc22303189da9c4b Merge branch 'master' into translate-workflow-titles Files changed: A docs/source/_json/expansion.req A docs/source/_json/expansion.resp A docs/source/_json/expansion_expanded.req A docs/source/_json/expansion_expanded.resp A docs/source/_json/expansion_expanded_full.req A docs/source/_json/expansion_expanded_full.resp A docs/source/expansion.rst A src/plone/restapi/serializer/expansion.py A src/plone/restapi/services/components/breadcrumbs.py A src/plone/restapi/services/components/navigation.py A src/plone/restapi/tests/test_expansion.py M CHANGES.rst M docs/source/_json/collection.resp M docs/source/_json/content_get.resp M docs/source/_json/content_post.resp M docs/source/_json/document.resp M docs/source/_json/event.resp M docs/source/_json/file.resp M docs/source/_json/folder.resp M docs/source/_json/image.resp M docs/source/_json/jwt_logged_in.resp M docs/source/_json/link.resp M docs/source/_json/newsitem.resp M docs/source/_json/siteroot.resp M docs/source/_json/workflow_get.resp M docs/source/index.rst M src/plone/restapi/interfaces.py M src/plone/restapi/serializer/atcontent.py M src/plone/restapi/serializer/dxcontent.py M src/plone/restapi/serializer/site.py M src/plone/restapi/services/breadcrumbs/configure.zcml M src/plone/restapi/services/breadcrumbs/get.py M src/plone/restapi/services/components/get.py M src/plone/restapi/services/configure.zcml M src/plone/restapi/services/navigation/configure.zcml M src/plone/restapi/services/navigation/get.py M src/plone/restapi/services/workflow/configure.zcml M src/plone/restapi/services/workflow/info.py M src/plone/restapi/tests/test_atcontent_serializer.py M src/plone/restapi/tests/test_components.py M src/plone/restapi/tests/test_documentation.py M src/plone/restapi/tests/test_dxcontent_serializer.py M src/plone/restapi/tests/test_workflow.py diff --git a/CHANGES.rst b/CHANGES.rst index c31345f9..375ea23f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,15 @@ Changelog ========= -- Translate titles in @workflow +1.0a21 (unreleased) +------------------- + +New Features: + +- Add support for expandable elements. + [buchi] + +- Translate titles in @workflow. [csenger] diff --git a/docs/source/_json/collection.resp b/docs/source/_json/collection.resp index c0fce081..da417bdf 100644 --- a/docs/source/_json/collection.resp +++ b/docs/source/_json/collection.resp @@ -2,6 +2,17 @@ HTTP/1.1 200 OK Content-Type: application/json { + "@components": { + "breadcrumbs": { + "@id": "http://localhost:55001/plone/collection/@breadcrumbs" + }, + "navigation": { + "@id": "http://localhost:55001/plone/collection/@navigation" + }, + "workflow": { + "@id": "http://localhost:55001/plone/collection/@workflow" + } + }, "@id": "http://localhost:55001/plone/collection", "@type": "Collection", "UID": "SomeUUID000000000000000000000002", diff --git a/docs/source/_json/content_get.resp b/docs/source/_json/content_get.resp index bb605400..dc9149da 100644 --- a/docs/source/_json/content_get.resp +++ b/docs/source/_json/content_get.resp @@ -2,6 +2,17 @@ HTTP/1.1 200 OK Content-Type: application/json { + "@components": { + "breadcrumbs": { + "@id": "http://localhost:55001/plone/folder/my-document/@breadcrumbs" + }, + "navigation": { + "@id": "http://localhost:55001/plone/folder/my-document/@navigation" + }, + "workflow": { + "@id": "http://localhost:55001/plone/folder/my-document/@workflow" + } + }, "@id": "http://localhost:55001/plone/folder/my-document", "@type": "Document", "UID": "SomeUUID000000000000000000000005", diff --git a/docs/source/_json/content_post.resp b/docs/source/_json/content_post.resp index 85f0a7b6..1de28ad2 100644 --- a/docs/source/_json/content_post.resp +++ b/docs/source/_json/content_post.resp @@ -3,6 +3,17 @@ Content-Type: application/json Location: http://localhost:55001/plone/folder/my-document { + "@components": { + "breadcrumbs": { + "@id": "http://localhost:55001/plone/folder/my-document/@breadcrumbs" + }, + "navigation": { + "@id": "http://localhost:55001/plone/folder/my-document/@navigation" + }, + "workflow": { + "@id": "http://localhost:55001/plone/folder/my-document/@workflow" + } + }, "@id": "http://localhost:55001/plone/folder/my-document", "@type": "Document", "UID": "SomeUUID000000000000000000000005", diff --git a/docs/source/_json/document.resp b/docs/source/_json/document.resp index 39c00b5b..998b22c3 100644 --- a/docs/source/_json/document.resp +++ b/docs/source/_json/document.resp @@ -2,6 +2,17 @@ HTTP/1.1 200 OK Content-Type: application/json { + "@components": { + "breadcrumbs": { + "@id": "http://localhost:55001/plone/front-page/@breadcrumbs" + }, + "navigation": { + "@id": "http://localhost:55001/plone/front-page/@navigation" + }, + "workflow": { + "@id": "http://localhost:55001/plone/front-page/@workflow" + } + }, "@id": "http://localhost:55001/plone/front-page", "@type": "Document", "UID": "SomeUUID000000000000000000000001", diff --git a/docs/source/_json/event.resp b/docs/source/_json/event.resp index 375ca779..52bfcbd0 100644 --- a/docs/source/_json/event.resp +++ b/docs/source/_json/event.resp @@ -2,6 +2,17 @@ HTTP/1.1 200 OK Content-Type: application/json { + "@components": { + "breadcrumbs": { + "@id": "http://localhost:55001/plone/event/@breadcrumbs" + }, + "navigation": { + "@id": "http://localhost:55001/plone/event/@navigation" + }, + "workflow": { + "@id": "http://localhost:55001/plone/event/@workflow" + } + }, "@id": "http://localhost:55001/plone/event", "@type": "Event", "UID": "SomeUUID000000000000000000000002", diff --git a/docs/source/_json/expansion.req b/docs/source/_json/expansion.req new file mode 100644 index 00000000..e2d9438e --- /dev/null +++ b/docs/source/_json/expansion.req @@ -0,0 +1,3 @@ +GET /plone/front-page HTTP/1.1 +Accept: application/json +Authorization: Basic YWRtaW46c2VjcmV0 diff --git a/docs/source/_json/expansion.resp b/docs/source/_json/expansion.resp new file mode 100644 index 00000000..998b22c3 --- /dev/null +++ b/docs/source/_json/expansion.resp @@ -0,0 +1,57 @@ +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "@components": { + "breadcrumbs": { + "@id": "http://localhost:55001/plone/front-page/@breadcrumbs" + }, + "navigation": { + "@id": "http://localhost:55001/plone/front-page/@navigation" + }, + "workflow": { + "@id": "http://localhost:55001/plone/front-page/@workflow" + } + }, + "@id": "http://localhost:55001/plone/front-page", + "@type": "Document", + "UID": "SomeUUID000000000000000000000001", + "allow_discussion": null, + "changeNote": "", + "contributors": [], + "created": "2016-01-21T01:14:48+00:00", + "creators": [ + "test_user_1_", + "admin" + ], + "description": "Congratulations! You have successfully installed Plone.", + "effective": null, + "exclude_from_nav": false, + "expires": null, + "id": "front-page", + "language": "", + "layout": "document_view", + "modified": "2016-01-21T01:24:11+00:00", + "parent": { + "@id": "http://localhost:55001/plone", + "@type": "Plone Site", + "description": "", + "title": "Plone site" + }, + "relatedItems": [], + "review_state": "private", + "rights": "", + "sharing": { + "@id": "http://localhost:55001/plone/front-page/@sharing", + "title": "Sharing" + }, + "subjects": [], + "table_of_contents": null, + "text": { + "content-type": "text/plain", + "data": "<p>If you're seeing this instead of the web site you were expecting, the owner of this web site has just installed Plone. Do not contact the Plone Team or the Plone mailing lists about this.</p>", + "encoding": "utf-8" + }, + "title": "Welcome to Plone", + "version": "current" +} \ No newline at end of file diff --git a/docs/source/_json/expansion_expanded.req b/docs/source/_json/expansion_expanded.req new file mode 100644 index 00000000..26879912 --- /dev/null +++ b/docs/source/_json/expansion_expanded.req @@ -0,0 +1,3 @@ +GET /plone/front-page?expand=breadcrumbs HTTP/1.1 +Accept: application/json +Authorization: Basic YWRtaW46c2VjcmV0 diff --git a/docs/source/_json/expansion_expanded.resp b/docs/source/_json/expansion_expanded.resp new file mode 100644 index 00000000..abbe6344 --- /dev/null +++ b/docs/source/_json/expansion_expanded.resp @@ -0,0 +1,63 @@ +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "@components": { + "breadcrumbs": { + "@id": "http://localhost:55001/plone/front-page/@breadcrumbs", + "items": [ + { + "title": "Welcome to Plone", + "url": "http://localhost:55001/plone/front-page" + } + ] + }, + "navigation": { + "@id": "http://localhost:55001/plone/front-page/@navigation" + }, + "workflow": { + "@id": "http://localhost:55001/plone/front-page/@workflow" + } + }, + "@id": "http://localhost:55001/plone/front-page", + "@type": "Document", + "UID": "SomeUUID000000000000000000000001", + "allow_discussion": null, + "changeNote": "", + "contributors": [], + "created": "2016-01-21T01:14:48+00:00", + "creators": [ + "test_user_1_", + "admin" + ], + "description": "Congratulations! You have successfully installed Plone.", + "effective": null, + "exclude_from_nav": false, + "expires": null, + "id": "front-page", + "language": "", + "layout": "document_view", + "modified": "2016-01-21T01:24:11+00:00", + "parent": { + "@id": "http://localhost:55001/plone", + "@type": "Plone Site", + "description": "", + "title": "Plone site" + }, + "relatedItems": [], + "review_state": "private", + "rights": "", + "sharing": { + "@id": "http://localhost:55001/plone/front-page/@sharing", + "title": "Sharing" + }, + "subjects": [], + "table_of_contents": null, + "text": { + "content-type": "text/plain", + "data": "<p>If you're seeing this instead of the web site you were expecting, the owner of this web site has just installed Plone. Do not contact the Plone Team or the Plone mailing lists about this.</p>", + "encoding": "utf-8" + }, + "title": "Welcome to Plone", + "version": "current" +} \ No newline at end of file diff --git a/docs/source/_json/expansion_expanded_full.req b/docs/source/_json/expansion_expanded_full.req new file mode 100644 index 00000000..258f6104 --- /dev/null +++ b/docs/source/_json/expansion_expanded_full.req @@ -0,0 +1,3 @@ +GET /plone/front-page?expand=breadcrumbs,navigation,schema,workflow HTTP/1.1 +Accept: application/json +Authorization: Basic YWRtaW46c2VjcmV0 diff --git a/docs/source/_json/expansion_expanded_full.resp b/docs/source/_json/expansion_expanded_full.resp new file mode 100644 index 00000000..6b92c4c5 --- /dev/null +++ b/docs/source/_json/expansion_expanded_full.resp @@ -0,0 +1,93 @@ +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "@components": { + "breadcrumbs": { + "@id": "http://localhost:55001/plone/front-page/@breadcrumbs", + "items": [ + { + "title": "Welcome to Plone", + "url": "http://localhost:55001/plone/front-page" + } + ] + }, + "navigation": { + "@id": "http://localhost:55001/plone/front-page/@navigation", + "items": [ + { + "title": "Home", + "url": "http://localhost:55001/plone" + }, + { + "title": "Welcome to Plone", + "url": "http://localhost:55001/plone/front-page" + } + ] + }, + "workflow": { + "@id": "http://localhost:55001/plone/front-page/@workflow", + "history": [ + { + "action": null, + "actor": "test_user_1_", + "comments": "", + "review_state": "private", + "time": "2016-10-21T19:00:00+00:00", + "title": "Private" + } + ], + "transitions": [ + { + "@id": "http://localhost:55001/plone/front-page/@workflow/publish", + "title": "Publish" + }, + { + "@id": "http://localhost:55001/plone/front-page/@workflow/submit", + "title": "Submit for publication" + } + ] + } + }, + "@id": "http://localhost:55001/plone/front-page", + "@type": "Document", + "UID": "SomeUUID000000000000000000000001", + "allow_discussion": null, + "changeNote": "", + "contributors": [], + "created": "2016-01-21T01:14:48+00:00", + "creators": [ + "test_user_1_", + "admin" + ], + "description": "Congratulations! You have successfully installed Plone.", + "effective": null, + "exclude_from_nav": false, + "expires": null, + "id": "front-page", + "language": "", + "layout": "document_view", + "modified": "2016-01-21T01:24:11+00:00", + "parent": { + "@id": "http://localhost:55001/plone", + "@type": "Plone Site", + "description": "", + "title": "Plone site" + }, + "relatedItems": [], + "review_state": "private", + "rights": "", + "sharing": { + "@id": "http://localhost:55001/plone/front-page/@sharing", + "title": "Sharing" + }, + "subjects": [], + "table_of_contents": null, + "text": { + "content-type": "text/plain", + "data": "<p>If you're seeing this instead of the web site you were expecting, the owner of this web site has just installed Plone. Do not contact the Plone Team or the Plone mailing lists about this.</p>", + "encoding": "utf-8" + }, + "title": "Welcome to Plone", + "version": "current" +} \ No newline at end of file diff --git a/docs/source/_json/file.resp b/docs/source/_json/file.resp index cac96f4f..979141bd 100644 --- a/docs/source/_json/file.resp +++ b/docs/source/_json/file.resp @@ -2,6 +2,17 @@ HTTP/1.1 200 OK Content-Type: application/json { + "@components": { + "breadcrumbs": { + "@id": "http://localhost:55001/plone/file/@breadcrumbs" + }, + "navigation": { + "@id": "http://localhost:55001/plone/file/@navigation" + }, + "workflow": { + "@id": "http://localhost:55001/plone/file/@workflow" + } + }, "@id": "http://localhost:55001/plone/file", "@type": "File", "UID": "SomeUUID000000000000000000000002", diff --git a/docs/source/_json/folder.resp b/docs/source/_json/folder.resp index efb105d3..39aa5b40 100644 --- a/docs/source/_json/folder.resp +++ b/docs/source/_json/folder.resp @@ -2,6 +2,17 @@ HTTP/1.1 200 OK Content-Type: application/json { + "@components": { + "breadcrumbs": { + "@id": "http://localhost:55001/plone/folder/@breadcrumbs" + }, + "navigation": { + "@id": "http://localhost:55001/plone/folder/@navigation" + }, + "workflow": { + "@id": "http://localhost:55001/plone/folder/@workflow" + } + }, "@id": "http://localhost:55001/plone/folder", "@type": "Folder", "UID": "SomeUUID000000000000000000000002", diff --git a/docs/source/_json/image.resp b/docs/source/_json/image.resp index d31dddc0..b198ac97 100644 --- a/docs/source/_json/image.resp +++ b/docs/source/_json/image.resp @@ -2,6 +2,17 @@ HTTP/1.1 200 OK Content-Type: application/json { + "@components": { + "breadcrumbs": { + "@id": "http://localhost:55001/plone/image/@breadcrumbs" + }, + "navigation": { + "@id": "http://localhost:55001/plone/image/@navigation" + }, + "workflow": { + "@id": "http://localhost:55001/plone/image/@workflow" + } + }, "@id": "http://localhost:55001/plone/image", "@type": "Image", "UID": "SomeUUID000000000000000000000002", diff --git a/docs/source/_json/jwt_logged_in.resp b/docs/source/_json/jwt_logged_in.resp index 0559a5bb..a5842fe6 100644 --- a/docs/source/_json/jwt_logged_in.resp +++ b/docs/source/_json/jwt_logged_in.resp @@ -2,6 +2,14 @@ HTTP/1.1 200 OK Content-Type: application/json { + "@components": { + "breadcrumbs": { + "@id": "http://localhost:55001/plone/@breadcrumbs" + }, + "navigation": { + "@id": "http://localhost:55001/plone/@navigation" + } + }, "@id": "http://localhost:55001/plone/", "@type": "Plone Site", "id": "plone", diff --git a/docs/source/_json/link.resp b/docs/source/_json/link.resp index 62a13159..d235c5cc 100644 --- a/docs/source/_json/link.resp +++ b/docs/source/_json/link.resp @@ -2,6 +2,17 @@ HTTP/1.1 200 OK Content-Type: application/json { + "@components": { + "breadcrumbs": { + "@id": "http://localhost:55001/plone/link/@breadcrumbs" + }, + "navigation": { + "@id": "http://localhost:55001/plone/link/@navigation" + }, + "workflow": { + "@id": "http://localhost:55001/plone/link/@workflow" + } + }, "@id": "http://localhost:55001/plone/link", "@type": "Link", "UID": "SomeUUID000000000000000000000002", diff --git a/docs/source/_json/newsitem.resp b/docs/source/_json/newsitem.resp index 8d5267f5..b926a819 100644 --- a/docs/source/_json/newsitem.resp +++ b/docs/source/_json/newsitem.resp @@ -2,6 +2,17 @@ HTTP/1.1 200 OK Content-Type: application/json { + "@components": { + "breadcrumbs": { + "@id": "http://localhost:55001/plone/newsitem/@breadcrumbs" + }, + "navigation": { + "@id": "http://localhost:55001/plone/newsitem/@navigation" + }, + "workflow": { + "@id": "http://localhost:55001/plone/newsitem/@workflow" + } + }, "@id": "http://localhost:55001/plone/newsitem", "@type": "News Item", "UID": "SomeUUID000000000000000000000002", diff --git a/docs/source/_json/siteroot.resp b/docs/source/_json/siteroot.resp index db02eb31..fa5c4db4 100644 --- a/docs/source/_json/siteroot.resp +++ b/docs/source/_json/siteroot.resp @@ -2,6 +2,14 @@ HTTP/1.1 200 OK Content-Type: application/json { + "@components": { + "breadcrumbs": { + "@id": "http://localhost:55001/plone/@breadcrumbs" + }, + "navigation": { + "@id": "http://localhost:55001/plone/@navigation" + } + }, "@id": "http://localhost:55001/plone", "@type": "Plone Site", "id": "plone", diff --git a/docs/source/_json/workflow_get.resp b/docs/source/_json/workflow_get.resp index 26f93780..6c365e9e 100644 --- a/docs/source/_json/workflow_get.resp +++ b/docs/source/_json/workflow_get.resp @@ -2,6 +2,7 @@ HTTP/1.1 200 OK Content-Type: application/json { + "@id": "http://localhost:55001/plone/front-page/@workflow", "history": [ { "action": null, diff --git a/docs/source/expansion.rst b/docs/source/expansion.rst new file mode 100644 index 00000000..a8f5052a --- /dev/null +++ b/docs/source/expansion.rst @@ -0,0 +1,112 @@ +Expansion +========= + +Expansion is a mechanism in plone.restapi to embed additional "components", +such as navigation, breadcrumbs, schema, or workflow within the main content +response. This helps the API consumers to avoid unneccesary request. + +Say you want to show a document in Plone together with the breadcrumbs and a +workflow switcher. Instead of doing three individual requests, you can just +expand the breadcrumbs and the workflow "components" within the document GET +request. + +The list of expandable components is listed in the "@components" attribute +in the reponse of any content GET request:: + + GET /plone/front-page HTTP/1.1 + Accept: application/json + Authorization: Basic YWRtaW46c2VjcmV0 + + { + "@id": "http://localhost:55001/plone/front-page", + "@type": "Document", + "@components": [ + {"@id": "http://localhost:55001/plone/front-page/@breadcrumbs"}, + {"@id": "http://localhost:55001/plone/front-page/@navigation"}, + {"@id": "http://localhost:55001/plone/front-page/@schema"}, + {"@id": "http://localhost:55001/plone/front-page/@workflow"} + }, + "UID": "1f699ffa110e45afb1ba502f75f7ec33", + "title": "Welcome to Plone", + ... + } + +Request Unexpanded: + +.. http:example:: curl httpie python-requests + :request: _json/expansion.req + +Response Unexpanded: + +.. literalinclude:: _json/expansion.resp + :language: http + +In order to expand and embed one or more components, use the "expand" GET +parameter and provide either a single component or a comma-separated list +of the components you want to embed. Say you want to expand the "breadcrumbs" +component:: + + GET /plone/front-page?expand=breadcrumbs HTTP/1.1 + Accept: application/json + Authorization: Basic YWRtaW46c2VjcmV0 + + { + "@id": "http://localhost:55001/plone/front-page", + "@type": "Document", + "@components": { + "breadcrumbs": { + "@id": "http://localhost:55001/plone/front-page/@components/breadcrumbs", + "items": [ + { + "title": "Welcome to Plone", + "url": "http://localhost:55001/plone/front-page" + } + ] + }, + "navigation": "http://localhost:55001/plone/front-page/@navigation", + "schema": "http://localhost:55001/plone/front-page/@schema", + "workflow": { + "history": [ + { + "action": null, + "actor": "test_user_1_", + "comments": "", + "review_state": "private", + "time": "2016-10-21T19:00:00+00:00" + } + ], + "transitions": [ + { + "@id": "http://localhost:55001/plone/front-page/@workflow/publish", + "title": "Publish" + }, + { + "@id": "http://localhost:55001/plone/front-page/@workflow/submit", + "title": "Submit for publication" + } + ] + }, + }, + "UID": "1f699ffa110e45afb1ba502f75f7ec33", + "title": "Welcome to Plone" + } + +Request Expanded: + +.. http:example:: curl httpie python-requests + :request: _json/expansion_expanded.req + +Response Expanded: + +.. literalinclude:: _json/expansion_expanded.resp + :language: http + +Here is an exaxmple of a request that expands all possible expansions: + +.. http:example:: curl httpie python-requests + :request: _json/expansion_expanded_full.req + +And the response: + +.. literalinclude:: _json/expansion_expanded_full.resp + :language: http diff --git a/docs/source/index.rst b/docs/source/index.rst index e7016298..ea4278fd 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -26,6 +26,7 @@ Contents batching comments copymove + expansion workflow sharing registry diff --git a/src/plone/restapi/interfaces.py b/src/plone/restapi/interfaces.py index e3b15f59..eb57f229 100644 --- a/src/plone/restapi/interfaces.py +++ b/src/plone/restapi/interfaces.py @@ -65,6 +65,16 @@ def __call__(value): """ +class IExpandableElement(Interface): + """A named adapter that deserializes an element in expanded or collapsed + form. + """ + + def __call__(expand=False): + """ + """ + + class IZCatalogCompatibleQuery(Interface): """A multi adapter responsible for converting a catalog query provided as a Python dictionary, but with possibly incorrect value types, to a diff --git a/src/plone/restapi/serializer/atcontent.py b/src/plone/restapi/serializer/atcontent.py index 42a68e18..53125363 100644 --- a/src/plone/restapi/serializer/atcontent.py +++ b/src/plone/restapi/serializer/atcontent.py @@ -1,18 +1,19 @@ # -*- coding: utf-8 -*- from Acquisition import aq_inner from Acquisition import aq_parent +from Products.Archetypes.interfaces import IBaseFolder +from Products.Archetypes.interfaces import IBaseObject +from Products.CMFCore.utils import getToolByName from plone.restapi.batching import HypermediaBatch from plone.restapi.interfaces import IFieldSerializer from plone.restapi.interfaces import ISerializeToJson from plone.restapi.interfaces import ISerializeToJsonSummary -from Products.Archetypes.interfaces import IBaseFolder -from Products.Archetypes.interfaces import IBaseObject -from Products.CMFCore.utils import getToolByName +from plone.restapi.serializer.expansion import expandable_elements from zope.component import adapter from zope.component import getMultiAdapter from zope.component import queryMultiAdapter -from zope.interface import implementer from zope.interface import Interface +from zope.interface import implementer @implementer(ISerializeToJson) @@ -48,6 +49,9 @@ def __call__(self, version=None): 'layout': self.context.getLayout(), } + # Insert expandable elements + result.update(expandable_elements(self.context, self.request)) + for field in obj.Schema().fields(): if 'r' not in field.mode or not field.checkPermission('r', obj): diff --git a/src/plone/restapi/serializer/dxcontent.py b/src/plone/restapi/serializer/dxcontent.py index b5a89239..3ca280eb 100644 --- a/src/plone/restapi/serializer/dxcontent.py +++ b/src/plone/restapi/serializer/dxcontent.py @@ -2,6 +2,7 @@ from AccessControl import getSecurityManager from Acquisition import aq_inner from Acquisition import aq_parent +from Products.CMFCore.utils import getToolByName from plone.autoform.interfaces import READ_PERMISSIONS_KEY from plone.dexterity.interfaces import IDexterityContainer from plone.dexterity.interfaces import IDexterityContent @@ -11,14 +12,14 @@ from plone.restapi.interfaces import ISerializeToJson from plone.restapi.interfaces import ISerializeToJsonSummary from plone.restapi.serializer.converters import json_compatible +from plone.restapi.serializer.expansion import expandable_elements from plone.supermodel.utils import mergedTaggedValueDict -from Products.CMFCore.utils import getToolByName from zope.component import adapter from zope.component import getMultiAdapter from zope.component import queryMultiAdapter from zope.component import queryUtility -from zope.interface import implementer from zope.interface import Interface +from zope.interface import implementer from zope.schema import getFields from zope.security.interfaces import IPermission @@ -61,7 +62,11 @@ def __call__(self, version=None): 'layout': self.context.getLayout(), } - for schema in iterSchemata(obj): + # Insert expandable elements + result.update(expandable_elements(self.context, self.request)) + + # Insert field values + for schema in iterSchemata(self.context): read_permissions = mergedTaggedValueDict( schema, READ_PERMISSIONS_KEY) diff --git a/src/plone/restapi/serializer/expansion.py b/src/plone/restapi/serializer/expansion.py new file mode 100644 index 00000000..cdf0eec8 --- /dev/null +++ b/src/plone/restapi/serializer/expansion.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from plone.restapi.interfaces import IExpandableElement +from zope.component import getAdapters + + +def expandable_elements(context, request): + """Returns a dict containing all expandable elements. + """ + expands = request.form.get('expand', '').split(',') + elements = getAdapters((context, request), IExpandableElement) + res = {} + for element in elements: + if element[0] in expands: + update_dict_recursively(res, element[1](expand=True)) + else: + update_dict_recursively(res, element[1](expand=False)) + return {"@components": res} + + +def update_dict_recursively(d, u): + for key, value in u.iteritems(): + if isinstance(value, dict): + r = update_dict_recursively(d.get(key, {}), value) + d[key] = r + else: + d[key] = u[key] + return d diff --git a/src/plone/restapi/serializer/site.py b/src/plone/restapi/serializer/site.py index 8476f64d..24b8284e 100644 --- a/src/plone/restapi/serializer/site.py +++ b/src/plone/restapi/serializer/site.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- +from Products.CMFCore.utils import getToolByName +from Products.CMFPlone.interfaces import IPloneSiteRoot from plone.restapi.batching import HypermediaBatch from plone.restapi.interfaces import ISerializeToJson from plone.restapi.interfaces import ISerializeToJsonSummary -from Products.CMFCore.utils import getToolByName -from Products.CMFPlone.interfaces import IPloneSiteRoot +from plone.restapi.serializer.expansion import expandable_elements from zope.component import adapter from zope.component import getMultiAdapter -from zope.interface import implementer from zope.interface import Interface +from zope.interface import implementer @implementer(ISerializeToJson) @@ -44,6 +45,9 @@ def __call__(self, version=None): 'parent': {}, } + # Insert expandable elements + result.update(expandable_elements(self.context, self.request)) + result['items_total'] = batch.items_total if batch.links: result['batching'] = batch.links diff --git a/src/plone/restapi/services/breadcrumbs/configure.zcml b/src/plone/restapi/services/breadcrumbs/configure.zcml index 0c39c889..167d7168 100644 --- a/src/plone/restapi/services/breadcrumbs/configure.zcml +++ b/src/plone/restapi/services/breadcrumbs/configure.zcml @@ -2,6 +2,8 @@ xmlns="http://namespaces.zope.org/zope" xmlns:plone="http://namespaces.plone.org/plone"> + <adapter factory=".get.Breadcrumbs" name="breadcrumbs"/> + <plone:service method="GET" for="zope.interface.Interface" @@ -9,5 +11,5 @@ name="@breadcrumbs" permission="zope2.View" /> - + </configure> diff --git a/src/plone/restapi/services/breadcrumbs/get.py b/src/plone/restapi/services/breadcrumbs/get.py index 8bfbe701..c09c4c9d 100644 --- a/src/plone/restapi/services/breadcrumbs/get.py +++ b/src/plone/restapi/services/breadcrumbs/get.py @@ -1,22 +1,44 @@ # -*- coding: utf-8 -*- +from plone.restapi.interfaces import IExpandableElement from plone.restapi.services import Service +from zope.component import adapter from zope.component import getMultiAdapter +from zope.interface import Interface +from zope.interface import implementer -class BreadcrumbsGet(Service): +@implementer(IExpandableElement) +@adapter(Interface, Interface) +class Breadcrumbs(object): - def reply(self): - breadcrumbs_view = getMultiAdapter((self.context, self.request), - name="breadcrumbs_view") + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self, expand=False): result = { - '@id': '{}/@breadcrumbs'.format( - self.context.absolute_url() - ), - 'items': [] + 'breadcrumbs': { + '@id': '{}/@breadcrumbs'.format(self.context.absolute_url()), + }, } + if not expand: + return result + + breadcrumbs_view = getMultiAdapter((self.context, self.request), + name="breadcrumbs_view") + items = [] for crumb in breadcrumbs_view.breadcrumbs(): - result['items'].append({ + items.append({ 'title': crumb['Title'], 'url': crumb['absolute_url'] }) + + result['breadcrumbs']['items'] = items return result + + +class BreadcrumbsGet(Service): + + def reply(self): + breadcrumbs = Breadcrumbs(self.context, self.request) + return breadcrumbs(expand=True)['breadcrumbs'] diff --git a/src/plone/restapi/services/components/breadcrumbs.py b/src/plone/restapi/services/components/breadcrumbs.py new file mode 100644 index 00000000..24287963 --- /dev/null +++ b/src/plone/restapi/services/components/breadcrumbs.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +from plone.restapi.interfaces import IExpandableElement +from zope.component import adapter +from zope.component import getMultiAdapter +from zope.interface import Interface +from zope.interface import implementer + + +@implementer(IExpandableElement) +@adapter(Interface, Interface) +class Breadcrumbs(object): + + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self, expand=False): + if not expand: + return {'@components': {'breadcrumbs': { + '@id': '{}/@components/breadcrumbs'.format( + self.context.absolute_url()), + }}} + + breadcrumbs_view = getMultiAdapter((self.context, self.request), + name="breadcrumbs_view") + result = [] + for crumb in breadcrumbs_view.breadcrumbs(): + result.append({ + 'title': crumb['Title'], + 'url': crumb['absolute_url'] + }) + return {'@components': {'breadcrumbs': result}} diff --git a/src/plone/restapi/services/components/get.py b/src/plone/restapi/services/components/get.py index 65345dd3..5947d0c3 100644 --- a/src/plone/restapi/services/components/get.py +++ b/src/plone/restapi/services/components/get.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- from plone.restapi.services import Service +from plone.restapi.services.components.breadcrumbs import Breadcrumbs +from plone.restapi.services.components.navigation import Navigation from zope.deprecation import deprecate -from zope.component import getMultiAdapter from zope.interface import implements from zope.publisher.interfaces import IPublishTraverse @@ -33,33 +34,13 @@ def _wrap_component_items(self, items, component_id): } return component - def get_navigation(self): - tabs = getMultiAdapter((self.context, self.request), - name="portal_tabs_view") - result = [] - for tab in tabs.topLevelTabs(): - result.append({ - 'title': tab.get('title', tab.get('name')), - 'url': tab['url'] + '' - }) - return result - - def get_breadcrumbs(self): - breadcrumbs_view = getMultiAdapter((self.context, self.request), - name="breadcrumbs_view") - result = [] - for crumb in breadcrumbs_view.breadcrumbs(): - result.append({ - 'title': crumb['Title'], - 'url': crumb['absolute_url'] - }) - return result - def _render_component(self, component_id): if component_id == 'navigation': - items = self.get_navigation() + items = Navigation(self.context, self.request)(expand=True)[ + '@components']['navigation'] elif component_id == 'breadcrumbs': - items = self.get_breadcrumbs() + items = Breadcrumbs(self.context, self.request)(expand=True)[ + '@components']['breadcrumbs'] else: raise NotImplementedError( 'This endpoint does not currently support the ' diff --git a/src/plone/restapi/services/components/navigation.py b/src/plone/restapi/services/components/navigation.py new file mode 100644 index 00000000..379cbb50 --- /dev/null +++ b/src/plone/restapi/services/components/navigation.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +from plone.restapi.interfaces import IExpandableElement +from zope.component import adapter +from zope.component import getMultiAdapter +from zope.interface import Interface +from zope.interface import implementer + + +@implementer(IExpandableElement) +@adapter(Interface, Interface) +class Navigation(object): + + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self, expand=False): + if not expand: + return {'@components': {'navigation': { + '@id': '{}/@components/navigation'.format( + self.context.absolute_url()), + }}} + + tabs = getMultiAdapter((self.context, self.request), + name="portal_tabs_view") + result = [] + for tab in tabs.topLevelTabs(): + result.append({ + 'title': tab.get('title', tab.get('name')), + 'url': tab['url'] + '' + }) + return {'@components': {'navigation': result}} diff --git a/src/plone/restapi/services/configure.zcml b/src/plone/restapi/services/configure.zcml index bc18fdfd..800995bb 100644 --- a/src/plone/restapi/services/configure.zcml +++ b/src/plone/restapi/services/configure.zcml @@ -2,13 +2,13 @@ xmlns="http://namespaces.zope.org/zope"> <include package=".auth" /> - <!--<include package=".breadcrumbs"/>--> + <include package=".breadcrumbs"/> <include package=".components"/> <include package=".content"/> <include package=".copymove"/> <include package=".discussion"/> <include package=".groups"/> - <!--<include package=".navigation"/>--> + <include package=".navigation"/> <include package=".history"/> <include package=".principals"/> <include package=".registry"/> diff --git a/src/plone/restapi/services/navigation/configure.zcml b/src/plone/restapi/services/navigation/configure.zcml index 05e200e4..090900cd 100644 --- a/src/plone/restapi/services/navigation/configure.zcml +++ b/src/plone/restapi/services/navigation/configure.zcml @@ -2,6 +2,8 @@ xmlns="http://namespaces.zope.org/zope" xmlns:plone="http://namespaces.plone.org/plone"> + <adapter factory=".get.Navigation" name="navigation"/> + <plone:service method="GET" for="zope.interface.Interface" diff --git a/src/plone/restapi/services/navigation/get.py b/src/plone/restapi/services/navigation/get.py index ab48b2e1..e59168c4 100644 --- a/src/plone/restapi/services/navigation/get.py +++ b/src/plone/restapi/services/navigation/get.py @@ -1,22 +1,43 @@ # -*- coding: utf-8 -*- +from plone.restapi.interfaces import IExpandableElement from plone.restapi.services import Service +from zope.component import adapter from zope.component import getMultiAdapter +from zope.interface import Interface +from zope.interface import implementer -class NavigationGet(Service): +@implementer(IExpandableElement) +@adapter(Interface, Interface) +class Navigation(object): - def reply(self): - tabs = getMultiAdapter((self.context, self.request), - name="portal_tabs_view") + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self, expand=False): result = { - '@id': '{}/@navigation'.format( - self.context.absolute_url() - ), - 'items': [] + 'navigation': { + '@id': '{}/@navigation'.format(self.context.absolute_url()), + }, } + if not expand: + return result + + tabs = getMultiAdapter((self.context, self.request), + name="portal_tabs_view") + items = [] for tab in tabs.topLevelTabs(): - result['items'].append({ + items.append({ 'title': tab.get('title', tab.get('name')), 'url': tab['url'] + '' }) + result['navigation']['items'] = items return result + + +class NavigationGet(Service): + + def reply(self): + navigation = Navigation(self.context, self.request) + return navigation(expand=True)['navigation'] diff --git a/src/plone/restapi/services/workflow/configure.zcml b/src/plone/restapi/services/workflow/configure.zcml index 1996ff01..e26831d0 100644 --- a/src/plone/restapi/services/workflow/configure.zcml +++ b/src/plone/restapi/services/workflow/configure.zcml @@ -7,7 +7,7 @@ method="GET" name="@workflow" for="Products.CMFCore.interfaces.IContentish" - factory=".info.WorkflowInfo" + factory=".info.WorkflowInfoService" permission="zope2.View" /> @@ -19,4 +19,6 @@ permission="zope2.View" /> + <adapter factory=".info.WorkflowInfo" name="workflow"/> + </configure> diff --git a/src/plone/restapi/services/workflow/info.py b/src/plone/restapi/services/workflow/info.py index e67f8a93..4b8e2063 100644 --- a/src/plone/restapi/services/workflow/info.py +++ b/src/plone/restapi/services/workflow/info.py @@ -1,13 +1,31 @@ # -*- coding: utf-8 -*- from Products.CMFCore.utils import getToolByName +from Products.CMFCore.interfaces._content import IWorkflowAware +from plone.restapi.interfaces import IExpandableElement from plone.restapi.serializer.converters import json_compatible from plone.restapi.services import Service +from zope.component import adapter +from zope.interface import Interface +from zope.interface import implementer -class WorkflowInfo(Service): - """Get workflow information - """ - def reply(self): +@implementer(IExpandableElement) +@adapter(IWorkflowAware, Interface) +class WorkflowInfo(object): + + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self, expand=False): + result = { + 'workflow': { + "@id": '{}/@workflow'.format(self.context.absolute_url()), + }, + } + if not expand: + return result + wftool = getToolByName(self.context, 'portal_workflow') history = wftool.getInfoFor(self.context, "review_history") @@ -29,7 +47,16 @@ def reply(self): action['review_state'], self.context.portal_type)) - return { + result['workflow'].update({ 'history': json_compatible(history), 'transitions': transitions, - } + }) + return result + + +class WorkflowInfoService(Service): + """Get workflow information + """ + def reply(self): + info = WorkflowInfo(self.context, self.request) + return info(expand=True)['workflow'] diff --git a/src/plone/restapi/tests/test_atcontent_serializer.py b/src/plone/restapi/tests/test_atcontent_serializer.py index 143dde64..ac4c1fad 100644 --- a/src/plone/restapi/tests/test_atcontent_serializer.py +++ b/src/plone/restapi/tests/test_atcontent_serializer.py @@ -2,9 +2,14 @@ from DateTime import DateTime from plone.app.testing import TEST_USER_ID from plone.app.testing import setRoles +from plone.restapi.interfaces import IExpandableElement from plone.restapi.interfaces import ISerializeToJson from plone.restapi.testing import PLONE_RESTAPI_AT_INTEGRATION_TESTING +from plone.restapi.tests.test_expansion import ExpandableElementFoo from zope.component import getMultiAdapter +from zope.component import provideAdapter +from zope.interface import Interface +from zope.publisher.interfaces.browser import IBrowserRequest import json import unittest @@ -149,3 +154,14 @@ def test_get_layout(self): obj = self.serialize(self.doc1) self.assertIn('layout', obj) self.assertEquals(current_layout, obj['layout']) + + def test_serializer_includes_expansion(self): + provideAdapter( + ExpandableElementFoo, + adapts=(Interface, IBrowserRequest), + provides=IExpandableElement, + name='foo' + ) + obj = self.serialize(self.doc1) + self.assertIn('foo', obj['@components']) + self.assertEqual('collapsed', obj['@components']['foo']) diff --git a/src/plone/restapi/tests/test_components.py b/src/plone/restapi/tests/test_components.py index 97f1390e..912510f9 100644 --- a/src/plone/restapi/tests/test_components.py +++ b/src/plone/restapi/tests/test_components.py @@ -54,6 +54,19 @@ def test_breadcrumb(self): }] ) + def test_collapsed_breadcrumbs_in_content_serialization(self): + obj = self.api_session.get('/folder').json() + self.assertIn('@components', obj) + self.assertIn('breadcrumbs', obj['@components']) + self.assertIn('@id', obj['@components']['breadcrumbs']) + self.assertNotIn('items', obj['@components']['breadcrumbs']) + + def test_expanded_breadcrumbs_in_content_serialization(self): + obj = self.api_session.get('/folder/doc1?expand=breadcrumbs').json() + self.assertIn('@components', obj) + self.assertIn('breadcrumbs', obj['@components']) + self.assertIn('items', obj['@components']['breadcrumbs']) + def test_navigation(self): response = self.api_session.get('/folder/@components/navigation') @@ -72,3 +85,16 @@ def test_navigation(self): }], }] ) + + def test_collapsed_navigation_in_content_serialization(self): + obj = self.api_session.get('/folder').json() + self.assertIn('@components', obj) + self.assertIn('navigation', obj['@components']) + self.assertIn('@id', obj['@components']['navigation']) + self.assertNotIn('items', obj['@components']['navigation']) + + def test_expanded_navigation_in_content_serialization(self): + obj = self.api_session.get('/folder?expand=navigation').json() + self.assertIn('@components', obj) + self.assertIn('navigation', obj['@components']) + self.assertIn('items', obj['@components']['navigation']) diff --git a/src/plone/restapi/tests/test_documentation.py b/src/plone/restapi/tests/test_documentation.py index 4f426083..dd66c871 100644 --- a/src/plone/restapi/tests/test_documentation.py +++ b/src/plone/restapi/tests/test_documentation.py @@ -826,6 +826,24 @@ def test_documentation_sharing_search(self): ) save_request_and_response_for_docs('sharing_search', response) + def test_documentation_expansion(self): + response = self.api_session.get( + '/front-page' + ) + save_request_and_response_for_docs('expansion', response) + + def test_documentation_expansion_expanded(self): + response = self.api_session.get( + '/front-page?expand=breadcrumbs' + ) + save_request_and_response_for_docs('expansion_expanded', response) + + def test_documentation_expansion_expanded_full(self): + response = self.api_session.get( + '/front-page?expand=breadcrumbs,navigation,schema,workflow' + ) + save_request_and_response_for_docs('expansion_expanded_full', response) + def test_history_get(self): self.document.setTitle('My new title') url = '{}/@history'.format(self.document.absolute_url()) @@ -1104,3 +1122,9 @@ def test_roles_get(self): url = '{}/@roles'.format(self.portal_url) response = self.api_session.get(url) save_request_and_response_for_docs('roles', response) + + def test_documentation_expansion(self): + response = self.api_session.get( + '/front-page?expand=breadcrumbs,workflow' + ) + save_request_and_response_for_docs('expansion', response) diff --git a/src/plone/restapi/tests/test_dxcontent_serializer.py b/src/plone/restapi/tests/test_dxcontent_serializer.py index ad695176..f9ad1c8f 100644 --- a/src/plone/restapi/tests/test_dxcontent_serializer.py +++ b/src/plone/restapi/tests/test_dxcontent_serializer.py @@ -6,10 +6,15 @@ from datetime import timedelta from plone.app.testing import TEST_USER_ID from plone.app.testing import setRoles +from plone.restapi.interfaces import IExpandableElement from plone.restapi.interfaces import ISerializeToJson from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING +from plone.restapi.tests.test_expansion import ExpandableElementFoo from plone.uuid.interfaces import IMutableUUID from zope.component import getMultiAdapter +from zope.component import provideAdapter +from zope.interface import Interface +from zope.publisher.interfaces.browser import IBrowserRequest import json import unittest @@ -122,3 +127,14 @@ def test_get_layout(self): obj = self.serialize() self.assertIn('layout', obj) self.assertEquals(current_layout, obj['layout']) + + def test_serializer_includes_expansion(self): + provideAdapter( + ExpandableElementFoo, + adapts=(Interface, IBrowserRequest), + provides=IExpandableElement, + name='foo' + ) + obj = self.serialize() + self.assertIn('foo', obj['@components']) + self.assertEqual('collapsed', obj['@components']['foo']) diff --git a/src/plone/restapi/tests/test_expansion.py b/src/plone/restapi/tests/test_expansion.py new file mode 100644 index 00000000..5d5ceafb --- /dev/null +++ b/src/plone/restapi/tests/test_expansion.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +from plone.app.testing import setRoles +from plone.app.testing import SITE_OWNER_NAME +from plone.app.testing import SITE_OWNER_PASSWORD +from plone.app.testing import TEST_USER_ID +from plone.dexterity.utils import createContentInContainer +from plone.restapi.testing import PLONE_RESTAPI_DX_FUNCTIONAL_TESTING +from plone.restapi.testing import RelativeSession +from plone.restapi.interfaces import IExpandableElement +from plone.restapi.serializer.expansion import expandable_elements +from zope.component import provideAdapter +from zope.interface import Interface +from zope.publisher.browser import TestRequest +from zope.publisher.interfaces.browser import IBrowserRequest + +import transaction +import unittest + + +class ExpandableElementFoo(object): + + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self, expand=False): + if expand: + return {'foo': 'expanded'} + else: + return {'foo': 'collapsed'} + + +class ExpandableElementBar(object): + + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self, expand=False): + if expand: + return {'bar': 'expanded'} + else: + return {'bar': 'collapsed'} + + +class TestExpansion(unittest.TestCase): + + def setUp(self): + provideAdapter( + ExpandableElementFoo, + adapts=(Interface, IBrowserRequest), + provides=IExpandableElement, + name='foo' + ) + provideAdapter( + ExpandableElementBar, + adapts=(Interface, IBrowserRequest), + provides=IExpandableElement, + name='bar' + ) + + def test_expansion_returns_collapsed_elements(self): + request = TestRequest() + self.assertEqual( + {'@components': {'bar': 'collapsed', 'foo': 'collapsed'}}, + expandable_elements(None, request)) + + def test_expansion_returns_expanded_element(self): + request = TestRequest(form={'expand': 'foo'}) + self.assertEqual( + {'@components': {'bar': 'collapsed', 'foo': 'expanded'}}, + expandable_elements(None, request)) + + def test_expansion_returns_multiple_expanded_elements(self): + request = TestRequest(form={'expand': 'foo,bar'}) + self.assertEqual( + {'@components': {'bar': 'expanded', 'foo': 'expanded'}}, + expandable_elements(None, request)) + + +class TestExpansionFunctional(unittest.TestCase): + + layer = PLONE_RESTAPI_DX_FUNCTIONAL_TESTING + + def setUp(self): + self.app = self.layer['app'] + self.portal = self.layer['portal'] + self.portal_url = self.portal.absolute_url() + setRoles(self.portal, TEST_USER_ID, ['Manager']) + + self.api_session = RelativeSession(self.portal_url) + self.api_session.headers.update({'Accept': 'application/json'}) + self.api_session.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD) + + self.folder = createContentInContainer( + self.portal, u'Folder', + id=u'folder', + title=u'Some Folder') + transaction.commit() + + def test_navigation_is_expandable(self): + response = self.api_session.get('/folder') + + self.assertEqual(response.status_code, 200) + self.assertIn( + 'navigation', + response.json().get('@components').keys() + ) + + def test_navigation_expanded(self): + response = self.api_session.get( + '/folder', + params={ + "expand": "navigation" + } + ) + self.assertEqual(response.status_code, 200) + self.assertEqual( + [ + { + u'title': u'Home', + u'url': u'http://localhost:55001/plone' + }, + { + u'title': u'Some Folder', + u'url': u'http://localhost:55001/plone/folder' + } + ], + response.json()['@components']['navigation']['items'] + ) + + def test_breadcrumbs_is_expandable(self): + response = self.api_session.get('/folder') + + self.assertEqual(response.status_code, 200) + self.assertIn( + 'breadcrumbs', + response.json().get('@components').keys() + ) + + def test_breadcrumbs_expanded(self): + response = self.api_session.get( + '/folder', + params={ + "expand": "breadcrumbs" + } + ) + self.assertEqual(response.status_code, 200) + self.assertEqual( + [ + { + u'title': u'Some Folder', + u'url': u'http://localhost:55001/plone/folder' + } + ], + response.json()['@components']['breadcrumbs']['items'] + ) + + def test_workflow_is_expandable(self): + response = self.api_session.get('/folder') + + self.assertEqual(response.status_code, 200) + self.assertIn( + 'workflow', + response.json().get('@components').keys() + ) + + def test_workflow_expanded(self): + response = self.api_session.get( + '/folder', + params={ + "expand": "workflow" + } + ) + + self.assertEqual(response.status_code, 200) + self.assertEqual( + u'http://localhost:55001/plone/folder/@workflow', + response.json().get('@components').get('workflow').get('@id') + ) + self.assertEqual( + u'private', + response.json()['@components']['workflow']['history'][0]['review_state'] # noqa + ) + self.assertEqual( + u'Private', + response.json()['@components']['workflow']['history'][0]['title'] + ) + self.assertEqual( + [ + { + u'@id': u'http://localhost:55001/plone/folder/@workflow/publish', # noqa + u'title': u'Publish' + }, + { + u'@id': u'http://localhost:55001/plone/folder/@workflow/submit', # noqa + u'title': u'Submit for publication' + } + ], + response.json()['@components']['workflow']['transitions'] + ) diff --git a/src/plone/restapi/tests/test_workflow.py b/src/plone/restapi/tests/test_workflow.py index e4526d35..ed96886f 100644 --- a/src/plone/restapi/tests/test_workflow.py +++ b/src/plone/restapi/tests/test_workflow.py @@ -5,6 +5,7 @@ from plone.app.testing import SITE_OWNER_NAME from plone.app.testing import SITE_OWNER_PASSWORD from plone.app.testing import login +from plone.restapi.interfaces import ISerializeToJson from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING from unittest import TestCase from zExceptions import NotFound @@ -43,6 +44,22 @@ def test_workflow_info_includes_transitions(self): transitions = info['transitions'] self.assertEqual(2, len(transitions)) + def test_collapsed_workflow_info_in_content_serialization(self): + serializer = getMultiAdapter((self.doc1, self.request), + ISerializeToJson) + obj = serializer() + self.assertIn('workflow', obj['@components']) + self.assertIn('@id', obj['@components']['workflow']) + + def test_expanded_workflow_info_in_content_serialization(self): + self.request.form.update({'expand': 'workflow'}) + serializer = getMultiAdapter((self.doc1, self.request), + ISerializeToJson) + obj = serializer() + self.assertIn('workflow', obj['@components']) + self.assertIn('transitions', obj['@components']['workflow']) + self.assertIn('history', obj['@components']['workflow']) + class TestWorkflowTransition(TestCase): ------------------------------------------------------------------------------ Check out the vibrant tech community on one of the world's most engaging tech sites, Slashdot.org! http://sdm.link/slashdot