diff --git a/CHANGES.rst b/CHANGES.rst index 1687046a4c..9f4255fafd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,7 @@ Changelog ========= -1.3.2 (unreleased) +1.6.1 (unreleased) ------------------ - Expose the tagged values for widgets in the @types endpoint. @@ -10,6 +10,48 @@ Changelog - Render subject vocabulary as items for subjects field. [jaroel] +Bugfixes: + +- Add VHM support to @search + [csenger] + + +1.6.0 (2018-04-17) +------------------ + +New Features: + +- Add `expand.navigation.depth` parameter to the `@navigation` endpoint. + [fulv, sneridagh] + + +1.5.0 (2018-04-03) +------------------ + +New Features: + +- Allow users to update their own properties and password. + [sneridagh] + + +1.4.1 (2018-03-22) +------------------ + +Bugfixes: + +- Fix serialization of `Discussion Item` and `Collection` content types when + called with `fullobjects` parameter. + [sneridagh] + + +1.4.0 (2018-03-19) +------------------ + +New Features: + +- Add expandable @actions endpoint to retrieve portal_actions. + [csenger,timo,sneridagh] + 1.3.1 (2018-03-14) ------------------ @@ -38,6 +80,20 @@ New Features: 1.2.0 (2018-02-28) ------------------ +Breaking Changes: + +- Make `@translations` endpoint expandable + [erral] + +- Rename the results attribute in `@translations` endpoint to be 'items' + [erral] + +- Remove 'language' attribute in `@translations` endpoint from the + top-level response entry + [erral] + + + New Features: - Allow users to get their own user information. diff --git a/base.cfg b/base.cfg index 1b7de9a50c..c212abe5a9 100644 --- a/base.cfg +++ b/base.cfg @@ -141,6 +141,7 @@ interpreter = ${buildout:directory}/bin/${sphinx-python:interpreter} [sphinx-python] recipe = zc.recipe.egg eggs = + sphinx_rtd_theme sphinxcontrib-httpdomain sphinxcontrib-httpexample interpreter = sphinxPython diff --git a/docs/source/_json/actions_get.req b/docs/source/_json/actions_get.req new file mode 100644 index 0000000000..369875e288 --- /dev/null +++ b/docs/source/_json/actions_get.req @@ -0,0 +1,3 @@ +GET /plone/@actions HTTP/1.1 +Accept: application/json +Authorization: Basic YWRtaW46c2VjcmV0 diff --git a/docs/source/_json/actions_get.resp b/docs/source/_json/actions_get.resp new file mode 100644 index 0000000000..d7e6bbbf9b --- /dev/null +++ b/docs/source/_json/actions_get.resp @@ -0,0 +1,71 @@ +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "object": [ + { + "icon": "", + "id": "view", + "title": "View" + }, + { + "icon": "", + "id": "edit", + "title": "Edit" + }, + { + "icon": "", + "id": "folderContents", + "title": "Contents" + }, + { + "icon": "", + "id": "history", + "title": "History" + }, + { + "icon": "", + "id": "local_roles", + "title": "Sharing" + } + ], + "object_buttons": [ + { + "icon": "", + "id": "cut", + "title": "Cut" + }, + { + "icon": "", + "id": "copy", + "title": "Copy" + }, + { + "icon": "", + "id": "delete", + "title": "Delete" + }, + { + "icon": "", + "id": "rename", + "title": "Rename" + } + ], + "user": [ + { + "icon": "", + "id": "preferences", + "title": "Preferences" + }, + { + "icon": "", + "id": "plone_setup", + "title": "Site Setup" + }, + { + "icon": "", + "id": "logout", + "title": "Log out" + } + ] +} \ No newline at end of file diff --git a/docs/source/_json/collection.resp b/docs/source/_json/collection.resp index 65d38338e2..86c6c4ced3 100644 --- a/docs/source/_json/collection.resp +++ b/docs/source/_json/collection.resp @@ -3,6 +3,9 @@ Content-Type: application/json { "@components": { + "actions": { + "@id": "http://localhost:55001/plone/collection/@actions" + }, "breadcrumbs": { "@id": "http://localhost:55001/plone/collection/@breadcrumbs" }, diff --git a/docs/source/_json/content_get.resp b/docs/source/_json/content_get.resp index f327f3e57b..c25c48353a 100644 --- a/docs/source/_json/content_get.resp +++ b/docs/source/_json/content_get.resp @@ -3,6 +3,9 @@ Content-Type: application/json { "@components": { + "actions": { + "@id": "http://localhost:55001/plone/folder/my-document/@actions" + }, "breadcrumbs": { "@id": "http://localhost:55001/plone/folder/my-document/@breadcrumbs" }, diff --git a/docs/source/_json/content_patch_representation.resp b/docs/source/_json/content_patch_representation.resp index d0043d7612..ee859de341 100644 --- a/docs/source/_json/content_patch_representation.resp +++ b/docs/source/_json/content_patch_representation.resp @@ -3,6 +3,9 @@ Content-Type: application/json { "@components": { + "actions": { + "@id": "http://localhost:55001/plone/folder/my-document/@actions" + }, "breadcrumbs": { "@id": "http://localhost:55001/plone/folder/my-document/@breadcrumbs" }, diff --git a/docs/source/_json/content_post.resp b/docs/source/_json/content_post.resp index 89063e1527..89664953cf 100644 --- a/docs/source/_json/content_post.resp +++ b/docs/source/_json/content_post.resp @@ -4,6 +4,9 @@ Location: http://localhost:55001/plone/folder/my-document { "@components": { + "actions": { + "@id": "http://localhost:55001/plone/folder/my-document/@actions" + }, "breadcrumbs": { "@id": "http://localhost:55001/plone/folder/my-document/@breadcrumbs" }, diff --git a/docs/source/_json/document.resp b/docs/source/_json/document.resp index f6b2bd9397..3429a2df07 100644 --- a/docs/source/_json/document.resp +++ b/docs/source/_json/document.resp @@ -3,6 +3,9 @@ Content-Type: application/json { "@components": { + "actions": { + "@id": "http://localhost:55001/plone/front-page/@actions" + }, "breadcrumbs": { "@id": "http://localhost:55001/plone/front-page/@breadcrumbs" }, diff --git a/docs/source/_json/event.resp b/docs/source/_json/event.resp index cf73c3ccbf..5aaa2269b9 100644 --- a/docs/source/_json/event.resp +++ b/docs/source/_json/event.resp @@ -3,6 +3,9 @@ Content-Type: application/json { "@components": { + "actions": { + "@id": "http://localhost:55001/plone/event/@actions" + }, "breadcrumbs": { "@id": "http://localhost:55001/plone/event/@breadcrumbs" }, diff --git a/docs/source/_json/expansion.resp b/docs/source/_json/expansion.resp index f6b2bd9397..3429a2df07 100644 --- a/docs/source/_json/expansion.resp +++ b/docs/source/_json/expansion.resp @@ -3,6 +3,9 @@ Content-Type: application/json { "@components": { + "actions": { + "@id": "http://localhost:55001/plone/front-page/@actions" + }, "breadcrumbs": { "@id": "http://localhost:55001/plone/front-page/@breadcrumbs" }, diff --git a/docs/source/_json/expansion_expanded.resp b/docs/source/_json/expansion_expanded.resp index e8507d4e19..ac46fbd7d3 100644 --- a/docs/source/_json/expansion_expanded.resp +++ b/docs/source/_json/expansion_expanded.resp @@ -3,6 +3,9 @@ Content-Type: application/json { "@components": { + "actions": { + "@id": "http://localhost:55001/plone/front-page/@actions" + }, "breadcrumbs": { "@id": "http://localhost:55001/plone/front-page/@breadcrumbs", "items": [ diff --git a/docs/source/_json/expansion_expanded_full.req b/docs/source/_json/expansion_expanded_full.req index 258f61044c..f0d7135fac 100644 --- a/docs/source/_json/expansion_expanded_full.req +++ b/docs/source/_json/expansion_expanded_full.req @@ -1,3 +1,3 @@ -GET /plone/front-page?expand=breadcrumbs,navigation,schema,workflow HTTP/1.1 +GET /plone/front-page?expand=actions,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 index f9d1835eaf..940cec1366 100644 --- a/docs/source/_json/expansion_expanded_full.resp +++ b/docs/source/_json/expansion_expanded_full.resp @@ -3,6 +3,99 @@ Content-Type: application/json { "@components": { + "actions": { + "document_actions": [], + "object": [ + { + "icon": "", + "id": "view", + "title": "View" + }, + { + "icon": "", + "id": "edit", + "title": "Edit" + }, + { + "icon": "", + "id": "folderContents", + "title": "Contents" + }, + { + "icon": "", + "id": "history", + "title": "History" + }, + { + "icon": "", + "id": "local_roles", + "title": "Sharing" + } + ], + "object_buttons": [ + { + "icon": "", + "id": "cut", + "title": "Cut" + }, + { + "icon": "", + "id": "copy", + "title": "Copy" + }, + { + "icon": "", + "id": "delete", + "title": "Delete" + }, + { + "icon": "", + "id": "rename", + "title": "Rename" + } + ], + "portal_tabs": [ + { + "icon": "", + "id": "index_html", + "title": "Home" + } + ], + "site_actions": [ + { + "icon": "", + "id": "sitemap", + "title": "Site Map" + }, + { + "icon": "", + "id": "accessibility", + "title": "Accessibility" + }, + { + "icon": "", + "id": "contact", + "title": "Contact" + } + ], + "user": [ + { + "icon": "", + "id": "preferences", + "title": "Preferences" + }, + { + "icon": "", + "id": "plone_setup", + "title": "Site Setup" + }, + { + "icon": "", + "id": "logout", + "title": "Log out" + } + ] + }, "breadcrumbs": { "@id": "http://localhost:55001/plone/front-page/@breadcrumbs", "items": [ @@ -17,10 +110,12 @@ Content-Type: application/json "items": [ { "@id": "http://localhost:55001/plone", + "description": "", "title": "Home" }, { "@id": "http://localhost:55001/plone/front-page", + "description": "Congratulations! You have successfully installed Plone.", "title": "Welcome to Plone" } ] diff --git a/docs/source/_json/file.resp b/docs/source/_json/file.resp index f7b4592466..b76c1f956c 100644 --- a/docs/source/_json/file.resp +++ b/docs/source/_json/file.resp @@ -3,6 +3,9 @@ Content-Type: application/json { "@components": { + "actions": { + "@id": "http://localhost:55001/plone/file/@actions" + }, "breadcrumbs": { "@id": "http://localhost:55001/plone/file/@breadcrumbs" }, diff --git a/docs/source/_json/folder.resp b/docs/source/_json/folder.resp index 87486953af..2905c0a2e3 100644 --- a/docs/source/_json/folder.resp +++ b/docs/source/_json/folder.resp @@ -3,6 +3,9 @@ Content-Type: application/json { "@components": { + "actions": { + "@id": "http://localhost:55001/plone/folder/@actions" + }, "breadcrumbs": { "@id": "http://localhost:55001/plone/folder/@breadcrumbs" }, diff --git a/docs/source/_json/image.resp b/docs/source/_json/image.resp index dc1966b3d8..ab96607b54 100644 --- a/docs/source/_json/image.resp +++ b/docs/source/_json/image.resp @@ -3,6 +3,9 @@ Content-Type: application/json { "@components": { + "actions": { + "@id": "http://localhost:55001/plone/image/@actions" + }, "breadcrumbs": { "@id": "http://localhost:55001/plone/image/@breadcrumbs" }, diff --git a/docs/source/_json/jwt_logged_in.resp b/docs/source/_json/jwt_logged_in.resp index 3eae74e64e..8ce3cff3a8 100644 --- a/docs/source/_json/jwt_logged_in.resp +++ b/docs/source/_json/jwt_logged_in.resp @@ -3,6 +3,9 @@ Content-Type: application/json { "@components": { + "actions": { + "@id": "http://localhost:55001/plone/@actions" + }, "breadcrumbs": { "@id": "http://localhost:55001/plone/@breadcrumbs" }, diff --git a/docs/source/_json/link.resp b/docs/source/_json/link.resp index 367ed230d5..38238edc50 100644 --- a/docs/source/_json/link.resp +++ b/docs/source/_json/link.resp @@ -3,6 +3,9 @@ Content-Type: application/json { "@components": { + "actions": { + "@id": "http://localhost:55001/plone/link/@actions" + }, "breadcrumbs": { "@id": "http://localhost:55001/plone/link/@breadcrumbs" }, diff --git a/docs/source/_json/navigation.resp b/docs/source/_json/navigation.resp index ded5c66e58..81f1be2eb2 100644 --- a/docs/source/_json/navigation.resp +++ b/docs/source/_json/navigation.resp @@ -6,10 +6,12 @@ Content-Type: application/json "items": [ { "@id": "http://localhost:55001/plone", + "description": "", "title": "Home" }, { "@id": "http://localhost:55001/plone/front-page", + "description": "Congratulations! You have successfully installed Plone.", "title": "Welcome to Plone" } ] diff --git a/docs/source/_json/navigation_tree.req b/docs/source/_json/navigation_tree.req new file mode 100644 index 0000000000..f839596b69 --- /dev/null +++ b/docs/source/_json/navigation_tree.req @@ -0,0 +1,3 @@ +GET /plone/front-page/@navigation?expand.navigation.depth=4 HTTP/1.1 +Accept: application/json +Authorization: Basic YWRtaW46c2VjcmV0 diff --git a/docs/source/_json/navigation_tree.resp b/docs/source/_json/navigation_tree.resp new file mode 100644 index 0000000000..67370a3ce1 --- /dev/null +++ b/docs/source/_json/navigation_tree.resp @@ -0,0 +1,62 @@ +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "@id": "http://localhost:55001/plone/front-page/@navigation", + "items": [ + { + "@id": "http://localhost:55001/plone", + "description": "", + "items": "", + "title": "Home" + }, + { + "@id": "http://localhost:55001/plone/front-page", + "description": "Congratulations! You have successfully installed Plone.", + "items": [], + "title": "Welcome to Plone" + }, + { + "@id": "http://localhost:55001/plone/folder", + "description": "", + "items": [ + { + "@id": "http://localhost:55001/plone/folder/subfolder1", + "description": "", + "items": [ + { + "@id": "http://localhost:55001/plone/folder/subfolder1/thirdlevelfolder", + "description": "", + "items": [ + { + "@id": "http://localhost:55001/plone/folder/subfolder1/thirdlevelfolder/fourthlevelfolder", + "description": "", + "title": "Fourth Level Folder" + } + ], + "title": "Third Level Folder" + } + ], + "title": "SubFolder 1" + }, + { + "@id": "http://localhost:55001/plone/folder/subfolder2", + "description": "", + "title": "SubFolder 2" + }, + { + "@id": "http://localhost:55001/plone/folder/doc1", + "description": "", + "title": "A document" + } + ], + "title": "Some Folder" + }, + { + "@id": "http://localhost:55001/plone/folder2", + "description": "", + "items": [], + "title": "Some Folder 2" + } + ] +} \ No newline at end of file diff --git a/docs/source/_json/newsitem.resp b/docs/source/_json/newsitem.resp index bea32707e5..dc8228aa32 100644 --- a/docs/source/_json/newsitem.resp +++ b/docs/source/_json/newsitem.resp @@ -3,6 +3,9 @@ Content-Type: application/json { "@components": { + "actions": { + "@id": "http://localhost:55001/plone/newsitem/@actions" + }, "breadcrumbs": { "@id": "http://localhost:55001/plone/newsitem/@breadcrumbs" }, diff --git a/docs/source/_json/siteroot.resp b/docs/source/_json/siteroot.resp index ffabcb8568..0b2f02311d 100644 --- a/docs/source/_json/siteroot.resp +++ b/docs/source/_json/siteroot.resp @@ -3,6 +3,9 @@ Content-Type: application/json { "@components": { + "actions": { + "@id": "http://localhost:55001/plone/@actions" + }, "breadcrumbs": { "@id": "http://localhost:55001/plone/@breadcrumbs" }, diff --git a/docs/source/_json/translations_get.resp b/docs/source/_json/translations_get.resp index f27b50ee45..b6b78eaf9c 100644 --- a/docs/source/_json/translations_get.resp +++ b/docs/source/_json/translations_get.resp @@ -2,9 +2,8 @@ HTTP/1.1 200 OK Content-Type: application/json { - "@id": "http://localhost:55001/plone/en/test-document", - "language": "en", - "translations": [ + "@id": "http://localhost:55001/plone/en/test-document/@translations", + "items": [ { "@id": "http://localhost:55001/plone/es/test-document", "language": "es" diff --git a/docs/source/_json/users_add.req b/docs/source/_json/users_add.req new file mode 100644 index 0000000000..eab44415e5 --- /dev/null +++ b/docs/source/_json/users_add.req @@ -0,0 +1,14 @@ +POST /plone/@users HTTP/1.1 +Accept: application/json +Authorization: Basic YWRtaW46c2VjcmV0 +Content-Type: application/json + +{ + "description": "Professor of Linguistics", + "email": "noam.chomsky@example.com", + "fullname": "Noam Avram Chomsky", + "home_page": "web.mit.edu/chomsky", + "location": "Cambridge, MA", + "sendPasswordReset": true, + "username": "noamchomsky" +} \ No newline at end of file diff --git a/docs/source/_json/users_add.resp b/docs/source/_json/users_add.resp new file mode 100644 index 0000000000..4e0df86726 --- /dev/null +++ b/docs/source/_json/users_add.resp @@ -0,0 +1,17 @@ +HTTP/1.1 201 Created +Content-Type: application/json +Location: http://localhost:55001/plone/@users/noamchomsky + +{ + "@id": "http://localhost:55001/plone/@users/noamchomsky", + "description": "Professor of Linguistics", + "email": "noam.chomsky@example.com", + "fullname": "Noam Avram Chomsky", + "home_page": "web.mit.edu/chomsky", + "id": "noamchomsky", + "location": "Cambridge, MA", + "roles": [ + "Member" + ], + "username": "noamchomsky" +} \ No newline at end of file diff --git a/docs/source/_static/img/postman_basic_auth.png b/docs/source/_static/img/postman_basic_auth.png index 2b0281c311..4483dcc993 100644 Binary files a/docs/source/_static/img/postman_basic_auth.png and b/docs/source/_static/img/postman_basic_auth.png differ diff --git a/docs/source/_static/img/postman_headers.png b/docs/source/_static/img/postman_headers.png index 1cc0409dc5..0943eb3c57 100644 Binary files a/docs/source/_static/img/postman_headers.png and b/docs/source/_static/img/postman_headers.png differ diff --git a/docs/source/_static/img/postman_request.png b/docs/source/_static/img/postman_request.png index 5bf20f0162..cab67e3e4f 100644 Binary files a/docs/source/_static/img/postman_request.png and b/docs/source/_static/img/postman_request.png differ diff --git a/docs/source/_static/img/postman_response.png b/docs/source/_static/img/postman_response.png new file mode 100644 index 0000000000..a02f083173 Binary files /dev/null and b/docs/source/_static/img/postman_response.png differ diff --git a/docs/source/_static/img/postman_retain_headers.png b/docs/source/_static/img/postman_retain_headers.png index 314ede8e36..6a2208a194 100644 Binary files a/docs/source/_static/img/postman_retain_headers.png and b/docs/source/_static/img/postman_retain_headers.png differ diff --git a/docs/source/actions.rst b/docs/source/actions.rst new file mode 100644 index 0000000000..2b57686cbd --- /dev/null +++ b/docs/source/actions.rst @@ -0,0 +1,34 @@ +Portal Actions +============== + +Plone has the concept of configurable actions (called "portal_actions"). +Each actions defines an id, a title, the required +permissions and a condition that will be checked to decide if the action +will be available for a user. +Actions are sorted by categories. + +Actions can be used to build UI elements that adapt to the available actions. +An example is the Plone toolbar where the "object_tabs" (view, edit, folder contents, sharing) +and the "user_actions" (login, logout, preferences) are used to display the user only the actions that are allowed for the currently logged in user. + +The available actions for the currently logged in user can be retrieved +by calling the @actions endpoint on a specific context. +This also works for not authenticated users. + +Listing available actions +------------------------- + +To list the available actions, send a GET request to the '@actions' endpoint on a specific content object: + +.. http:example:: curl httpie python-requests + :request: _json/actions_get.req + +The server will respond with a `200 OK` status code. +The JSON response contains the available actions categories (object, object_buttons, user) on the top level. +Each category contains a list of the available actions in that category: + +.. literalinclude:: _json/actions_get.resp + :language: http + +If you want to limit the categories that are retured, pass one or more parameters +``categories:list``, i.e. ``@action?categories:list=object&categories:list=user``. diff --git a/docs/source/expansion.rst b/docs/source/expansion.rst index a8f5052ab2..aeab6d3e26 100644 --- a/docs/source/expansion.rst +++ b/docs/source/expansion.rst @@ -1,3 +1,5 @@ +.. _`expansion name`: + Expansion ========= @@ -21,10 +23,12 @@ in the reponse of any content GET request:: "@id": "http://localhost:55001/plone/front-page", "@type": "Document", "@components": [ + {"@id": "http://localhost:55001/plone/front-page/@actions"}, {"@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"} + {"@id": "http://localhost:55001/plone/front-page/@workflow"}, + ... }, "UID": "1f699ffa110e45afb1ba502f75f7ec33", "title": "Welcome to Plone", @@ -54,6 +58,9 @@ component:: "@id": "http://localhost:55001/plone/front-page", "@type": "Document", "@components": { + "actions": { + "@id": "http://localhost:55001/plone/front-page/@actions" + }, "breadcrumbs": { "@id": "http://localhost:55001/plone/front-page/@components/breadcrumbs", "items": [ @@ -63,28 +70,14 @@ component:: } ] }, - "navigation": "http://localhost:55001/plone/front-page/@navigation", - "schema": "http://localhost:55001/plone/front-page/@schema", + "navigation": { + "@id": "http://localhost:55001/plone/front-page/@navigation" + }, + "schema": { + "@id": "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" - } - ] + "@id": http://localhost:55001/plone/front-page/@workflow" }, }, "UID": "1f699ffa110e45afb1ba502f75f7ec33", diff --git a/docs/source/exploring.rst b/docs/source/exploring.rst index 7b3499a411..a2405dc14e 100644 --- a/docs/source/exploring.rst +++ b/docs/source/exploring.rst @@ -3,7 +3,11 @@ Explore the API using Postman ============================= -To discover the API interactively, the Chrome-Extension Postman_ is a suitable solution. +To discover the API interactively, using Postman_ is recommended. + +.. admonition:: Note + + The Chrome-Extension version of Postman is deprecated and it is recommended to use the native app available instead. Configuration ------------- @@ -45,7 +49,7 @@ You have to select * in the drop-down menu :menuselection:`Basic Auth ->` the term :term:`Basic Auth` as the authentication method * A valid existing user with appropriate permissions -After providing these parameters you can create the resulting :term:`Authorization Header` and insert it into the prepared request by clicking on :guilabel:`Update Request`. +After providing these parameters you can create the resulting :term:`Authorization Header` and insert it into the prepared request by clicking on :guilabel:`Preview Request`. |postman-basic-auth| @@ -60,6 +64,9 @@ The request is now ready and can be send by clicking on :guilabel:`Send` button. The :term:`Response` of the server is now displayed below the :term:`Request`. You can easily follow the links on the ``@id`` attributes by clicking on them. For every link Postman_ has prepared another request sharing the same headers that can be send again by licking on the :guilabel:`Send` button. +|postman-response| + + .. admonition:: Conclusion You can now explore the whole stucture of your application easily via the API using `GET` requests. @@ -78,3 +85,4 @@ The :term:`Response` of the server is now displayed below the :term:`Request`. Y .. |postman-request| image:: ./_static/img/postman_request.png .. |postman-basic-auth| image:: ./_static/img/postman_basic_auth.png .. |postman-headers| image:: ./_static/img/postman_headers.png +.. |postman-response| image:: ./_static/img/postman_response.png diff --git a/docs/source/index.rst b/docs/source/index.rst index 18ed524b79..c5cfc71e19 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -7,8 +7,6 @@ plone.restapi: A RESTful hypermedia API for Plone. ================================================== -.. include:: /alert-noindex.rst - Contents -------- @@ -27,6 +25,7 @@ Contents comments copymove expansion + actions workflow locking sharing diff --git a/docs/source/navigation.rst b/docs/source/navigation.rst index 599a6d32e7..e0b5447dec 100644 --- a/docs/source/navigation.rst +++ b/docs/source/navigation.rst @@ -3,6 +3,9 @@ Navigation ========== +Top-Level Navigation +-------------------- + Getting the top navigation items: .. http:example:: curl httpie python-requests @@ -12,3 +15,17 @@ Example response: .. literalinclude:: _json/navigation.resp :language: http + + +Navigation Tree +--------------- + +Getting the navigation item tree providing a `expand.navigation.depth` parameter: + +.. http:example:: curl httpie python-requests + :request: _json/navigation_tree.req + +Example response: + +.. literalinclude:: _json/navigation_tree.resp + :language: http diff --git a/docs/source/translations.rst b/docs/source/translations.rst index 4544c6ca79..6a92aa3fd5 100644 --- a/docs/source/translations.rst +++ b/docs/source/translations.rst @@ -1,3 +1,5 @@ +.. _`translations`: + Translations ============ @@ -65,6 +67,17 @@ endpoint of the content item and provide the language code you want to unlink.: :language: http +Expansion +--------- + +This endpoint can be used with the :ref:`expansion name` mechanism which allows to get additional +information about a content item in one query, avoiding unnecesary requests. + +If a simple ``GET`` request is done on the content item, a new entry will be shown on the `@components` +entry with the URL of the `@translations` endpoint: + + + .. _`plone.app.multilingual`: https://pypi.python.org/pypi/plone.app.multilingual .. _`Products.LinguaPlone`: https://pypi.python.org/pypi/Products.LinguaPlone. .. _`documentation`: https://docs.plone.org/develop/plone/i18n/translating_content.html \ No newline at end of file diff --git a/docs/source/upgrade-guide.rst b/docs/source/upgrade-guide.rst index 24d6233c83..056ecaccd2 100644 --- a/docs/source/upgrade-guide.rst +++ b/docs/source/upgrade-guide.rst @@ -4,6 +4,52 @@ Upgrade Guide This upgrade guide lists all breaking changes in plone.restapi and explains the necessary steps that are needed to upgrade to the lastest version. +Upgrading from plone.restapi 1.x +-------------------------------- + +When using the `@translations` endpoint in plone.restapi 1.x, the endpoint returned a `language` key +with the content object's language and a `translations` key with all its translations. + +Now, as the endpoint is expandable we want the endpoint to behave like the other expandable endpoints. +As top level information we only include the name of the endpoint on the `@id` attribute and the actual +translations of the content object in an attribute called `items`. + +This means that now the JSON response to a GET request to the :ref:`translations` endpoint does not +include anymore the language of the actual content item and the translations in an attribute called +`items` instead of `translations`. + +Old response:: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "@id": "http://localhost:55001/plone/en/test-document", + "language": "en", + "translations": [ + { + "@id": "http://localhost:55001/plone/es/test-document", + "language": "es" + } + ] + } + +New response:: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "@id": "http://localhost:55001/plone/en/test-document/@translations", + "items": [ + { + "@id": "http://localhost:55001/plone/es/test-document", + "language": "es" + } + ] + } + + Upgrading to plone.restapi 1.0b1 -------------------------------- diff --git a/docs/source/users.rst b/docs/source/users.rst index b5e9c2613b..3dae824a0c 100644 --- a/docs/source/users.rst +++ b/docs/source/users.rst @@ -135,6 +135,7 @@ A successful response to a PATCH request will be indicated by a :term:`204 No Co .. note:: The 'roles' object is a mapping of a role and a boolean indicating adding or removing. +Any user is able to update their own properties and password (if allowed) by using the same request. Delete User ----------- @@ -153,9 +154,23 @@ A successful response will be indicated by a :term:`204 No Content` response: User registration ----------------- -Plone allows you to enable the auto registration of users. If it is enabled, then an anonymous user can register a new user using the user creation endpoint. +Plone allows you to enable the auto registration of users. +If it is enabled, then an anonymous user can register a new user using the user creation endpoint. This new user will have the role ``Member`` by default as the Plone registration process also does. +To create a new user send a POST request to the '@users' endpoint: + +.. http:example:: curl httpie python-requests + :request: _json/users_add.req + +If the user should receive an email to set her password, you should pass 'sendPasswordReset": true' in the JSON body of the request. +Keep in mind that Plone will send a URL that points to the URL of the Plone site, which might just be your API endpoint. + +If the user has been created, the server will respond with a :term:`201 Created` response: + +.. literalinclude:: _json/users_add.resp + :language: http + Reset User Password ------------------- @@ -169,7 +184,7 @@ Plone allows to reset a password for a user by sending a POST request to the use The server will respond with a :term:`200 OK` response and send an email to the user to reset her password. The token that is part of the reset url in the email can be used to -authorize setting a new password:: +authorize setting a new password: .. http:example:: curl httpie python-requests :request: _json/users_reset.req diff --git a/setup.py b/setup.py index e4bb2cf962..2463212302 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -version = '1.3.2-dev.0' +version = '1.6.1.dev0' long_description = ( open('README.rst').read() + '\n' + diff --git a/src/plone/restapi/search/handler.py b/src/plone/restapi/search/handler.py index 84f17e01d4..2f65f530a4 100644 --- a/src/plone/restapi/search/handler.py +++ b/src/plone/restapi/search/handler.py @@ -33,6 +33,20 @@ def _constrain_query_by_path(self, query): if 'path' not in query: query['path'] = {} + if isinstance(query['path'], str): + query['path'] = {'query': query['path']} + + # If this is accessed through a VHM the client does not know + # the complete physical path of an object. But the path index + # indexes the complete physical path. Complete the path. + vhm_physical_path = self.request.get('VirtualRootPhysicalPath') + if vhm_physical_path: + path = query['path'].get('query') + if path: + path = path.lstrip('/') + full_path = '/'.join(vhm_physical_path + (path,)) + query['path']['query'] = full_path + if isinstance(query['path'], dict) and 'query' not in query['path']: # We either had no 'path' parameter at all, or an incomplete # 'path' query dict (with just ExtendedPathIndex options (like diff --git a/src/plone/restapi/serializer/collection.py b/src/plone/restapi/serializer/collection.py index 603d478abc..e6145a5e9f 100644 --- a/src/plone/restapi/serializer/collection.py +++ b/src/plone/restapi/serializer/collection.py @@ -14,7 +14,7 @@ @adapter(ICollection, Interface) class SerializeCollectionToJson(SerializeToJson): - def __call__(self, version=None): + def __call__(self, version=None, include_items=True): collection_metadata = super(SerializeCollectionToJson, self).__call__( version=version, ) diff --git a/src/plone/restapi/serializer/discussion.py b/src/plone/restapi/serializer/discussion.py index 588b798f0e..22c8176ed2 100644 --- a/src/plone/restapi/serializer/discussion.py +++ b/src/plone/restapi/serializer/discussion.py @@ -54,7 +54,7 @@ def __init__(self, context, request): self.context = context self.request = request - def __call__(self): + def __call__(self, include_items=True): content_url = self.context.__parent__.__parent__.absolute_url() comments_url = '{}/@comments'.format(content_url) url = '{}/{}'.format(comments_url, self.context.id) diff --git a/src/plone/restapi/services/actions/__init__.py b/src/plone/restapi/services/actions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/plone/restapi/services/actions/configure.zcml b/src/plone/restapi/services/actions/configure.zcml new file mode 100644 index 0000000000..30787c8f10 --- /dev/null +++ b/src/plone/restapi/services/actions/configure.zcml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/src/plone/restapi/services/actions/get.py b/src/plone/restapi/services/actions/get.py new file mode 100644 index 0000000000..c707c5bc59 --- /dev/null +++ b/src/plone/restapi/services/actions/get.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +from Products.CMFCore.interfaces import IActionCategory +from Products.CMFCore.utils import getToolByName +from plone.restapi.interfaces import IExpandableElement +from plone.restapi.services import Service +from zope.component import adapter +from zope.component import getMultiAdapter +from zope.i18n import translate +from zope.interface import Interface +from zope.interface import implementer + + +@implementer(IExpandableElement) +@adapter(Interface, Interface) +class Actions(object): + + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self, expand=False): + result = { + 'actions': { + '@id': '{}/@actions'.format(self.context.absolute_url()), + }, + } + if not expand: + return result + + context_state = getMultiAdapter( + (self.context, self.request), name='plone_context_state') + + categories = self.request.form.get( + 'categories', + self.all_categories + ) + data = {} + for category in categories: + category_action_data = [] + actions = context_state.actions(category=category) + for action in actions: + category_action_data.append({ + 'title': translate(action['title'], context=self.request), + 'id': action['id'], + 'icon': action['icon'], + }) + data[category] = category_action_data + return {'actions': data} + + @property + def all_categories(self): + portal_actions = getToolByName(self.context, 'portal_actions') + categories = [] + for id, obj in portal_actions.objectItems(): + if IActionCategory.providedBy(obj): + categories.append(id) + return categories + + +class ActionsGet(Service): + + def reply(self): + actions = Actions(self.context, self.request) + return actions(expand=True)['actions'] diff --git a/src/plone/restapi/services/configure.zcml b/src/plone/restapi/services/configure.zcml index c864fbff60..2c7a6bd65f 100644 --- a/src/plone/restapi/services/configure.zcml +++ b/src/plone/restapi/services/configure.zcml @@ -6,6 +6,7 @@ + diff --git a/src/plone/restapi/services/multilingual/configure.zcml b/src/plone/restapi/services/multilingual/configure.zcml index c3ca9f36c7..2c6dae1b09 100644 --- a/src/plone/restapi/services/multilingual/configure.zcml +++ b/src/plone/restapi/services/multilingual/configure.zcml @@ -3,6 +3,8 @@ xmlns:plone="http://namespaces.plone.org/plone" xmlns:zcml="http://namespaces.zope.org/zcml"> + + 0 and self.bottomLevel > 0 and depth >= self.bottomLevel: + return False + else: + return True + + def getRootPath(self, topLevel=1): + rootPath = getNavigationRoot(self.context) + + contextPath = '/'.join(self.context.getPhysicalPath()) + if not contextPath.startswith(rootPath): + return None + contextSubPathElements = contextPath[len(rootPath) + 1:] + if contextSubPathElements: + contextSubPathElements = contextSubPathElements.split('/') + if len(contextSubPathElements) < topLevel: + return None + rootPath = rootPath + '/' + '/'.join(contextSubPathElements[:topLevel]) # noqa + else: + return None + + return rootPath @implementer(IExpandableElement) @@ -14,8 +68,14 @@ class Navigation(object): def __init__(self, context, request): self.context = context self.request = request + self.portal = getSite() def __call__(self, expand=False): + if self.request.form.get('expand.navigation.depth', False): + self.depth = int(self.request.form['expand.navigation.depth']) + else: + self.depth = 1 + result = { 'navigation': { '@id': '{}/@navigation'.format(self.context.absolute_url()), @@ -28,13 +88,71 @@ def __call__(self, expand=False): name="portal_tabs_view") items = [] for tab in tabs.topLevelTabs(): - items.append({ - 'title': tab.get('title', tab.get('name')), - '@id': tab['url'] + '' - }) + if self.depth > 1: + subitems = self.getTabSubTree(tabUrl=tab['url'], + tabPath=tab.get('path')) + items.append({ + 'title': tab.get('title', tab.get('name')), + '@id': tab['url'] + '', + 'description': tab.get('description', ''), + 'items': subitems, + }) + else: + items.append({ + 'title': tab.get('title', tab.get('name')), + '@id': tab['url'] + '', + 'description': tab.get('description', ''), + }) result['navigation']['items'] = items return result + def getTabSubTree(self, tabUrl='', tabPath=None): + if tabPath is None: + # get path for current tab's object + tabPath = tabUrl.split(self.portal.absolute_url())[-1] + + if tabPath == '' or '/view' in tabPath: + return '' + + if tabPath.startswith('/'): + tabPath = tabPath[1:] + elif tabPath.endswith('/'): + # we need a real path, without a slash that might appear + # at the end of the path occasionally + tabPath = str(tabPath.split('/')[0]) + + if '%20' in tabPath: + # we have the space in object's ID that has to be + # converted to the real spaces + tabPath = tabPath.replace('%20', ' ').strip() + + tabObj = self.portal.restrictedTraverse(tabPath, None) + if tabObj is None: + return '' + + strategy = CustomNavtreeStrategy(tabObj) + queryBuilder = NavigationTreeQueryBuilder(tabObj, self.depth) + query = queryBuilder() + data = buildFolderTree( + tabObj, obj=tabObj, query=query, strategy=strategy) + + return self.recurse( + children=data.get('children', []), + level=1) + + def recurse(self, children=None, level=0, bottomLevel=0): + li = [] + for node in children: + item = {'title': node['Title'], 'description': node['Description']} + item['@id'] = node['getURL'] + if bottomLevel <= 0 or level <= bottomLevel: + nc = node['children'] + nc = self.recurse(nc, level+1, bottomLevel) + if nc: + item['items'] = nc + li.append(item) + return li + class NavigationGet(Service): diff --git a/src/plone/restapi/services/users/configure.zcml b/src/plone/restapi/services/users/configure.zcml index 25d785b23d..ad07d1a5ce 100644 --- a/src/plone/restapi/services/users/configure.zcml +++ b/src/plone/restapi/services/users/configure.zcml @@ -16,7 +16,7 @@ name="@users" for="Products.CMFCore.interfaces.ISiteRoot" factory=".update.UsersPatch" - permission="cmf.ManagePortal" + permission="zope2.View" />