Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add translation endpoint #345

Merged
merged 7 commits into from
Oct 28, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ Bugfixes:

New Features:

- Add @translations endpoint
[erral]

- Reorder children in a item using the content endpoint.
[jaroel]

Expand Down
6 changes: 6 additions & 0 deletions docs/source/_json/translations_delete.req
Original file line number Diff line number Diff line change
@@ -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"}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@erral cmon, you could at least use Basque or Catalan. ;)

2 changes: 2 additions & 0 deletions docs/source/_json/translations_delete.resp
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
HTTP/1.1 204 No Content

3 changes: 3 additions & 0 deletions docs/source/_json/translations_get.req
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
GET /plone/en/test-document/@translations HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
13 changes: 13 additions & 0 deletions docs/source/_json/translations_get.resp
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
6 changes: 6 additions & 0 deletions docs/source/_json/translations_post.req
Original file line number Diff line number Diff line change
@@ -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"}
5 changes: 5 additions & 0 deletions docs/source/_json/translations_post.resp
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
HTTP/1.1 201 Created
Content-Type: application/json
Location: http://localhost:55001/plone/en/test-document

{}
2 changes: 1 addition & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Contents
controlpanels
customization
conventions
translations

.. include:: ../../README.rst

Expand All @@ -58,4 +59,3 @@ Appendix, Indices and tables
glossary

* :ref:`genindex`

70 changes: 70 additions & 0 deletions docs/source/translations.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
Translations
============

.. note::
This is only available on Plone 5.

Since Plone 5 the product `plone.app.multilingual`_ is included in the base
Plone installation although it is not enabled by default.

Multilingualism in Plone not only allows the managers of the site to configure
the site interface texts to be in one language or another (such as the
configuration menus, error messages, information messages or other static
text) but also to configure Plone to handle multilingual content. To achieve
that it provides the user interface for managing content translations.

You can get additional information about the multilingual capabilities of Plone
in the `documentation`_.

In connection with that capabilities, 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


.. _`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
2 changes: 2 additions & 0 deletions src/plone/restapi/services/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,7 @@
<include package=".users"/>
<include package=".vocabularies"/>
<include package=".workflow"/>
<include package=".multilingual"
zcml:condition="have plone-5"/>

</configure>
Empty file.
30 changes: 30 additions & 0 deletions src/plone/restapi/services/multilingual/configure.zcml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:plone="http://namespaces.plone.org/plone"
xmlns:zcml="http://namespaces.zope.org/zcml">

<plone:service
method="GET"
name="@translations"
for="Products.CMFCore.interfaces.IContentish"
factory=".pam.TranslationInfo"
permission="zope2.View"
/>

<plone:service
method="POST"
name="@translations"
for="Products.CMFCore.interfaces.IContentish"
factory=".pam.LinkTranslations"
permission="plone.app.multilingual.ManageTranslations"
/>

<plone:service
method="DELETE"
name="@translations"
for="Products.CMFCore.interfaces.IContentish"
factory=".pam.UnlinkTranslations"
permission="plone.app.multilingual.ManageTranslations"
/>

</configure>
114 changes: 114 additions & 0 deletions src/plone/restapi/services/multilingual/pam.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
from zope.interface import alsoProvides
from Products.CMFCore.utils import getToolByName
from plone.app.multilingual.interfaces import ITranslationManager
from plone.restapi.services import Service
from plone.restapi.deserializer import json_body
from Products.CMFPlone.interfaces import ILanguage

import plone.protect.interfaces


class TranslationInfo(Service):
""" Get translation information
"""

def reply(self):
manager = ITranslationManager(self.context)
info = {
'@id': self.context.absolute_url(),
'language': ILanguage(self.context).get_language(),
'translations': []}
for language, translation in manager.get_translations().items():
if language != ILanguage(self.context).get_language():
info['translations'].append({
'@id': translation.absolute_url(),
'language': language,
})

return info


class LinkTranslations(Service):
""" Link two content objects as translations of each other
"""

def reply(self):
# Disable CSRF protection
if 'IDisableCSRFProtection' in dir(plone.protect.interfaces):
alsoProvides(self.request,
plone.protect.interfaces.IDisableCSRFProtection)

data = json_body(self.request)
id_ = data.get('id', None)
if id_ is None:
self.request.response.setStatus(400)
return dict(error=dict(
type='BadRequest',
message='Missing content id to link to'))

target = self._traverse(id_)
if target is None:
self.request.response.setStatus(400)
return dict(error=dict(
type='BadRequest',
message='Content does not exist'))

target_language = ILanguage(target).get_language()
manager = ITranslationManager(self.context)
current_translation = manager.get_translation(target_language)
if current_translation is not None:
self.request.response.setStatus(400)
return dict(error=dict(
type='BadRequest',
message='Already translated into language {}'.format(
target_language)))

manager.register_translation(target_language, target)
self.request.response.setStatus(201)
self.request.response.setHeader(
'Location', self.context.absolute_url())
return {}

def _traverse(self, url):
purl = getToolByName(self.context, 'portal_url')
portal = purl.getPortalObject()
portal_url = portal.absolute_url()
if url.startswith(portal_url):
content_path = url[len(portal_url)+1:]
content_path = content_path.split('/')
content_item = portal.restrictedTraverse(content_path)
return content_item

return None


class UnlinkTranslations(Service):
""" Unlink the translations for a content object
"""

def reply(self):
# Disable CSRF protection
if 'IDisableCSRFProtection' in dir(plone.protect.interfaces):
alsoProvides(self.request,
plone.protect.interfaces.IDisableCSRFProtection)

data = json_body(self.request)
manager = ITranslationManager(self.context)
language = data.get('language', None)
if language is None:
self.request.response.setStatus(400)
return dict(error=dict(
type='BadRequest',
message='You need to provide the language to unlink'))

if language not in manager.get_translations().keys():
self.request.response.setStatus(400)
return dict(error=dict(
type='BadRequest',
message='This objects is not translated into {}'.format(
language)))

manager.remove_translation(language)
self.request.response.setStatus(204)
return {}
55 changes: 55 additions & 0 deletions src/plone/restapi/testing.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from Products.CMFCore.utils import getToolByName

# pylint: disable=E1002
# E1002: Use of super on an old style class
Expand Down Expand Up @@ -31,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():
Expand Down Expand Up @@ -88,6 +97,52 @@ def setUpPloneSite(self, portal):
)


class PloneRestApiDXPAMLayer(PloneSandboxLayer):
defaultBases = (PLONE_APP_CONTENTTYPES_FIXTURE,)

def setUpZope(self, app, configurationContext):
import plone.restapi
xmlconfig.file(
'configure.zcml',
plone.restapi,
context=configurationContext
)
xmlconfig.file(
'testing.zcml',
plone.restapi,
context=configurationContext
)

z2.installProduct(app, 'plone.restapi')

def setUpPloneSite(self, portal):
portal.acl_users.userFolderAddUser(
SITE_OWNER_NAME, SITE_OWNER_PASSWORD, ['Manager'], [])
login(portal, SITE_OWNER_NAME)
setRoles(portal, TEST_USER_ID, ['Manager'])
language_tool = getToolByName(portal, 'portal_languages')
language_tool.addSupportedLanguage('en')
language_tool.addSupportedLanguage('es')
if portal.portal_setup.profileExists(
'plone.app.multilingual:default'):
applyProfile(portal, 'plone.app.multilingual:default')
applyProfile(portal, 'plone.restapi:default')
applyProfile(portal, 'plone.restapi:testing')
add_catalog_indexes(portal, DX_TYPES_INDEXES)
set_available_languages()


PLONE_RESTAPI_DX_PAM_FIXTURE = PloneRestApiDXPAMLayer()
PLONE_RESTAPI_DX_PAM_INTEGRATION_TESTING = IntegrationTesting(
bases=(PLONE_RESTAPI_DX_PAM_FIXTURE,),
name="PloneRestApiDXPAMLayer:Integration"
)
PLONE_RESTAPI_DX_PAM_FUNCTIONAL_TESTING = FunctionalTesting(
bases=(PLONE_RESTAPI_DX_PAM_FIXTURE, z2.ZSERVER_FIXTURE),
name="PloneRestApiDXPAMLayer:Functional"
)


class PloneRestApiATLayer(PloneSandboxLayer):

defaultBases = (PLONE_FIXTURE,)
Expand Down
Loading