diff --git a/docs/source/_json/translations_delete.req b/docs/source/_json/translations_delete.req new file mode 100644 index 0000000000..a753cb69be --- /dev/null +++ b/docs/source/_json/translations_delete.req @@ -0,0 +1,6 @@ +DELETE /plone/en/test-document/@translations HTTP/1.1 +Accept: application/json +Authorization: Basic YWRtaW46c2VjcmV0 +Content-Type: application/json + +{"language": "es"} \ No newline at end of file diff --git a/docs/source/_json/translations_delete.resp b/docs/source/_json/translations_delete.resp new file mode 100644 index 0000000000..0074ded3bc --- /dev/null +++ b/docs/source/_json/translations_delete.resp @@ -0,0 +1,2 @@ +HTTP/1.1 204 No Content + diff --git a/docs/source/_json/translations_get.req b/docs/source/_json/translations_get.req new file mode 100644 index 0000000000..e17d071bdc --- /dev/null +++ b/docs/source/_json/translations_get.req @@ -0,0 +1,3 @@ +GET /plone/en/test-document/@translations HTTP/1.1 +Accept: application/json +Authorization: Basic YWRtaW46c2VjcmV0 diff --git a/docs/source/_json/translations_get.resp b/docs/source/_json/translations_get.resp new file mode 100644 index 0000000000..f27b50ee45 --- /dev/null +++ b/docs/source/_json/translations_get.resp @@ -0,0 +1,13 @@ +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" + } + ] +} \ No newline at end of file diff --git a/docs/source/_json/translations_post.req b/docs/source/_json/translations_post.req new file mode 100644 index 0000000000..708288fd65 --- /dev/null +++ b/docs/source/_json/translations_post.req @@ -0,0 +1,6 @@ +POST /plone/en/test-document/@translations HTTP/1.1 +Accept: application/json +Authorization: Basic YWRtaW46c2VjcmV0 +Content-Type: application/json + +{"id": "http://localhost:55001/plone/es/test-document"} \ No newline at end of file diff --git a/docs/source/_json/translations_post.resp b/docs/source/_json/translations_post.resp new file mode 100644 index 0000000000..b0d80491db --- /dev/null +++ b/docs/source/_json/translations_post.resp @@ -0,0 +1,5 @@ +HTTP/1.1 201 Created +Content-Type: application/json +Location: http://localhost:55001/plone/en/test-document + +{} \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 66c5b74261..7b9dd9337a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -35,6 +35,7 @@ Contents vocabularies customization conventions + translations .. include:: ../../README.rst @@ -48,4 +49,3 @@ Appendix, Indices and tables glossary * :ref:`genindex` - diff --git a/docs/source/translations.rst b/docs/source/translations.rst new file mode 100644 index 0000000000..3817ee92e0 --- /dev/null +++ b/docs/source/translations.rst @@ -0,0 +1,60 @@ +Translations +============ + +Since Plone 5 the product `plone.app.multilingual`_ is included in the base +Plone installation although it is not enabled by default. plone.restapi +provides a `@translations` endpoint to handle the translation information +of the content objects. + +Once we have installed `plone.app.multilingual`_ and enabled more than one +language we can link two content-items of different languages to be the +translation of each other issuing a `POST` query to the `@translations` +endpoint including the `id` of the content which should be linked to. The +`id` of the content must be a full URL of the content object: + + +.. http:example:: curl httpie python-requests + :request: _json/translations_post.req + + +.. note:: + "id" is a required field and needs to point to an existing content on the site. + +The API will return a `201 Created` response if the linking was successful. + + +.. literalinclude:: _json/translations_post.resp + :language: http + + +After linking the contents we can get the list of the translations of that +content item by issuing a ``GET`` request on the `@translations` endpoint of +that content item.: + +.. http:example:: curl httpie python-requests + :request: _json/translations_get.req + +.. literalinclude:: _json/translations_get.resp + :language: http + + +To unlink the content, issue a ``DELETE`` request on the `@translations` +endpoint of the content item and provide the language code you want to unlink.: + + +.. http:example:: curl httpie python-requests + :request: _json/translations_delete.req + +.. note:: + "language" is a required field. + +.. literalinclude:: _json/translations_delete.resp + :language: http + +.. note:: + The `@translations` endpoint works also when using `Products.LinguaPlone`_ + in Plone 4.3.x + + +.. _`plone.app.multilingual`: https://pypi.python.org/pypi/plone.app.multilingual +.. _`Products.LinguaPlone`: https://pypi.python.org/pypi/Products.LinguaPlone. diff --git a/src/plone/restapi/testing.py b/src/plone/restapi/testing.py index 932a7ee9e0..fd07589c01 100644 --- a/src/plone/restapi/testing.py +++ b/src/plone/restapi/testing.py @@ -32,6 +32,14 @@ import requests import collective.MockMailHost +import pkg_resources + + +try: + pkg_resources.get_distribution('plone.app.multilingual') + PAM_INSTALLED = True +except pkg_resources.DistributionNotFound: + PAM_INSTALLED = False def set_available_languages(): diff --git a/src/plone/restapi/tests/test_documentation.py b/src/plone/restapi/tests/test_documentation.py index d3e95a077d..ca1e71ffcf 100644 --- a/src/plone/restapi/tests/test_documentation.py +++ b/src/plone/restapi/tests/test_documentation.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from Products.CMFCore.utils import getToolByName from datetime import datetime from DateTime import DateTime from datetime import timedelta @@ -6,16 +7,19 @@ from plone import api from plone.app.testing import popGlobalRegistry from plone.app.testing import pushGlobalRegistry +from plone.app.testing import applyProfile 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.app.textfield.value import RichTextValue +from plone.dexterity.utils import createContentInContainer from plone.namedfile.file import NamedBlobFile from plone.namedfile.file import NamedBlobImage from plone.restapi.testing import PLONE_RESTAPI_DX_FUNCTIONAL_TESTING from plone.restapi.testing import register_static_uuid_utility from plone.restapi.testing import RelativeSession +from plone.restapi.testing import PAM_INSTALLED from plone.testing.z2 import Browser from zope.site.hooks import getSite @@ -25,7 +29,6 @@ import transaction import unittest - REQUEST_HEADER_KEYS = [ 'accept', 'authorization', @@ -777,3 +780,77 @@ def test_documentation_sharing_folder_post(self): json=payload ) save_request_and_response_for_docs('sharing_folder_post', response) + + +if PAM_INSTALLED: + from plone.app.multilingual.interfaces import ITranslationManager + from plone.restapi.testing import PLONE_RESTAPI_DX_PAM_FUNCTIONAL_TESTING + + class TestPAMDocumentation(unittest.TestCase): + + layer = PLONE_RESTAPI_DX_PAM_FUNCTIONAL_TESTING + + def setUp(self): + self.app = self.layer['app'] + self.request = self.layer['request'] + self.portal = self.layer['portal'] + self.portal_url = self.portal.absolute_url() + + self.time_freezer = freeze_time("2016-10-21 19:00:00") + self.frozen_time = self.time_freezer.start() + + 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) + + setRoles(self.portal, TEST_USER_ID, ['Manager']) + + language_tool = getToolByName(self.portal, 'portal_languages') + language_tool.addSupportedLanguage('en') + language_tool.addSupportedLanguage('es') + applyProfile(self.portal, 'plone.app.multilingual:default') + self.en_content = createContentInContainer( + self.portal['en'], 'Document', title='Test document') + self.es_content = createContentInContainer( + self.portal['es'], 'Document', title='Test document') + + import transaction + transaction.commit() + self.browser = Browser(self.app) + self.browser.handleErrors = False + self.browser.addHeader( + 'Authorization', + 'Basic %s:%s' % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD,) + ) + + def tearDown(self): + self.time_freezer.stop() + + def test_documentation_translations_post(self): + response = self.api_session.post( + '{}/@translations'.format(self.en_content.absolute_url()), + json={ + 'id': self.es_content.absolute_url() + } + ) + save_request_and_response_for_docs('translations_post', response) + + def test_documentation_translations_get(self): + ITranslationManager(self.en_content).register_translation( + 'es', self.es_content) + transaction.commit() + response = self.api_session.get( + '{}/@translations'.format(self.en_content.absolute_url())) + + save_request_and_response_for_docs('translations_get', response) + + def test_documentation_translations_delete(self): + ITranslationManager(self.en_content).register_translation( + 'es', self.es_content) + transaction.commit() + response = self.api_session.delete( + '{}/@translations'.format(self.en_content.absolute_url()), + json={ + "language": "es" + }) + save_request_and_response_for_docs('translations_delete', response)