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