Skip to content

Commit

Permalink
Merge 62a82fc into d6597a5
Browse files Browse the repository at this point in the history
  • Loading branch information
buchi committed Jun 24, 2019
2 parents d6597a5 + 62a82fc commit a07d046
Show file tree
Hide file tree
Showing 12 changed files with 261 additions and 137 deletions.
19 changes: 17 additions & 2 deletions docs/source/content.rst
Expand Up @@ -112,8 +112,6 @@ After a successful POST, we can access the resource by sending a GET request to
.. http:example:: curl httpie python-requests
:request: ../../src/plone/restapi/tests/http-examples/content_get.req

You can also set the `include_items` GET parameter to false if you don't want to include children.


Successful Response (200 OK)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand All @@ -123,6 +121,23 @@ If a resource has been retrieved successfully, the server responds with :term:`2
.. literalinclude:: ../../src/plone/restapi/tests/http-examples/content_get.resp
:language: http


For folderish types, their childrens are automatically included in the response
as ``items``. To disable the inclusion, add the GET parameter ``include_items=false``
to the URL.

By default only basic metadata is included. To include additional metadata,
you can specify the names of the properties with the ``metadata_fields`` parameter.
See also :ref:`retrieving-additional-metadata`.

The following example additionaly retrieves the UID and Creator:

.. http:example:: curl httpie python-requests
:request: ../../src/plone/restapi/tests/http-examples/content_get_folder.req

.. literalinclude:: ../../src/plone/restapi/tests/http-examples/content_get_folder.resp
:language: http

.. note::
For folderish types, collections or search results, the results will
be **batched** if the size of the resultset exceeds the batch size.
Expand Down
1 change: 1 addition & 0 deletions docs/source/searching.rst
Expand Up @@ -119,6 +119,7 @@ In that case, ``plone.restapi`` simply can't know what data type to cast your qu
Please refer to the `Documentation on Argument Conversion in ZPublisher <http://docs.zope.org/zope2/zdgbook/ObjectPublishing.html#argument-conversion>`_ for details.

.. _retrieving-additional-metadata:

Retrieving additional metadata
------------------------------
Expand Down
4 changes: 4 additions & 0 deletions news/681.feature
@@ -0,0 +1,4 @@
Support retrieval of additional metadata fields in summaries in the same way as
in search results.
[buchi]

57 changes: 0 additions & 57 deletions src/plone/restapi/serializer/catalog.py
Expand Up @@ -2,63 +2,13 @@
from plone.restapi.batching import HypermediaBatch
from plone.restapi.interfaces import ISerializeToJson
from plone.restapi.interfaces import ISerializeToJsonSummary
from plone.restapi.serializer.converters import json_compatible
from Products.CMFCore.utils import getToolByName
from Products.ZCatalog.interfaces import ICatalogBrain
from Products.ZCatalog.Lazy import Lazy
from zope.component import adapter
from zope.component import getMultiAdapter
from zope.component.hooks import getSite
from zope.interface import implementer
from zope.interface import Interface


BRAIN_METHODS = ["getPath", "getURL"]


@implementer(ISerializeToJson)
@adapter(ICatalogBrain, Interface)
class BrainSerializer(object):
"""Serializes a catalog brain to a Python data structure that can in turn
be serialized to JSON.
"""

def __init__(self, brain, request):
self.brain = brain
self.request = request

def _get_metadata_to_include(self, metadata_fields):
if metadata_fields and "_all" in metadata_fields:
site = getSite()
catalog = getToolByName(site, "portal_catalog")
metadata_attrs = list(catalog.schema()) + BRAIN_METHODS
return metadata_attrs

return metadata_fields

def __call__(self, metadata_fields=("_all",)):
metadata_to_include = self._get_metadata_to_include(metadata_fields)

# Start with a summary representation as our base
result = getMultiAdapter((self.brain, self.request), ISerializeToJsonSummary)()

for attr in metadata_to_include:
value = getattr(self.brain, attr, None)

# Handle values that are provided via methods on brains, like
# getPath or getURL (see ICatalogBrain for details)
if attr in BRAIN_METHODS:
value = value()

value = json_compatible(value)

# TODO: Deal with metadata attributes that already contain
# timestamps as isoformat strings, like 'Created'
result[attr] = value

return result


@implementer(ISerializeToJson)
@adapter(Lazy, Interface)
class LazyCatalogResultSerializer(object):
Expand Down Expand Up @@ -91,13 +41,6 @@ def __call__(self, metadata_fields=(), fullobjects=False):
(brain, self.request), ISerializeToJsonSummary
)()

# Merge additional metadata into the summary we already have
if metadata_fields:
metadata = getMultiAdapter((brain, self.request), ISerializeToJson)(
metadata_fields=metadata_fields
)
result.update(metadata)

results["items"].append(result)

return results
1 change: 0 additions & 1 deletion src/plone/restapi/serializer/configure.zcml
Expand Up @@ -59,7 +59,6 @@
<adapter factory=".relationfield.relationvalue_converter" />
</configure>

<adapter factory=".catalog.BrainSerializer" />
<adapter factory=".catalog.LazyCatalogResultSerializer" />

<adapter factory=".user.SerializeUserToJson" />
Expand Down
64 changes: 55 additions & 9 deletions src/plone/restapi/serializer/summary.py
Expand Up @@ -2,11 +2,40 @@
from plone.app.contentlisting.interfaces import IContentListingObject
from plone.restapi.interfaces import ISerializeToJsonSummary
from plone.restapi.serializer.converters import json_compatible
from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.interfaces import IPloneSiteRoot
from zope.component import adapter
from zope.interface import implementer
from zope.interface import Interface

# fmt: off
DEFAULT_METADATA_FIELDS = set([
'@id',
'@type',
'description',
'review_state',
'title',
])

FIELD_ACCESSORS = {
"@id": "getURL",
"@type": "PortalType",
"description": "Description",
"title": "Title",
}

NON_METADATA_ATTRIBUTES = set([
"getPath",
"getURL",
])

BLACKLISTED_ATTRIBUTES = set([
'getDataOrigin',
'getObject',
'getUserData',
])
# fmt: on


@implementer(ISerializeToJsonSummary)
@adapter(Interface, Interface)
Expand All @@ -23,17 +52,34 @@ def __init__(self, context, request):

def __call__(self):
obj = IContentListingObject(self.context)
summary = json_compatible(
{
"@id": obj.getURL(),
"@type": obj.PortalType(),
"title": obj.Title(),
"description": obj.Description(),
"review_state": obj.review_state(),
}
)

summary = {}
for field in self.metadata_fields():
if field.startswith("_") or field in BLACKLISTED_ATTRIBUTES:
continue
accessor = FIELD_ACCESSORS.get(field, field)
value = getattr(obj, accessor, None)
if callable(value):
value = value()
summary[field] = json_compatible(value)
return summary

def metadata_fields(self):
additional_metadata_fields = self.request.form.get("metadata_fields", [])
if not isinstance(additional_metadata_fields, list):
additional_metadata_fields = [additional_metadata_fields]
additional_metadata_fields = set(additional_metadata_fields)

if "_all" in additional_metadata_fields:
fields_cache = self.request.get('_summary_fields_cache', None)
if fields_cache is None:
catalog = getToolByName(self.context, "portal_catalog")
fields_cache = set(catalog.schema()) | NON_METADATA_ATTRIBUTES
self.request.set('_summary_fields_cache', fields_cache)
additional_metadata_fields = fields_cache

return DEFAULT_METADATA_FIELDS | additional_metadata_fields


@implementer(ISerializeToJsonSummary)
@adapter(IPloneSiteRoot, Interface)
Expand Down
3 changes: 3 additions & 0 deletions src/plone/restapi/testing.py
Expand Up @@ -33,6 +33,7 @@
from zope.interface import implementer

import collective.MockMailHost
import os
import pkg_resources
import re
import requests
Expand Down Expand Up @@ -105,6 +106,7 @@ def enable_request_language_negotiation(portal):
class DateTimeFixture(Layer):
def setUp(self):
tz = "UTC"
os.environ['TZ'] = tz
# Patch DateTime's timezone for deterministic behavior.
from DateTime import DateTime

Expand All @@ -114,6 +116,7 @@ def setUp(self):

content.FLOOR_DATE = DateTime(1970, 0)
content.CEILING_DATE = DateTime(2500, 0)
content._zone = tz

def tearDown(self):
from DateTime import DateTime
Expand Down
3 changes: 3 additions & 0 deletions src/plone/restapi/tests/http-examples/content_get_folder.req
@@ -0,0 +1,3 @@
GET /plone/folder?metadata_fields=UID&metadata_fields=Creator HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
80 changes: 80 additions & 0 deletions src/plone/restapi/tests/http-examples/content_get_folder.resp
@@ -0,0 +1,80 @@
HTTP/1.1 200 OK
Content-Type: application/json

{
"@components": {
"actions": {
"@id": "http://localhost:55001/plone/folder/@actions"
},
"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?metadata_fields=UID&metadata_fields=Creator",
"@type": "Folder",
"UID": "SomeUUID000000000000000000000002",
"allow_discussion": false,
"contributors": [],
"created": "2016-01-21T07:14:48+00:00",
"creators": [
"test_user_1_"
],
"description": "This is a folder with two documents",
"effective": null,
"exclude_from_nav": false,
"expires": null,
"id": "folder",
"is_folderish": true,
"items": [
{
"@id": "http://localhost:55001/plone/folder/doc1",
"@type": "Document",
"Creator": "test_user_1_",
"UID": "SomeUUID000000000000000000000003",
"description": "",
"review_state": "private",
"title": "A document within a folder"
},
{
"@id": "http://localhost:55001/plone/folder/doc2",
"@type": "Document",
"Creator": "test_user_1_",
"UID": "SomeUUID000000000000000000000004",
"description": "",
"review_state": "private",
"title": "A document within a folder"
},
{
"@id": "http://localhost:55001/plone/folder/my-document",
"@type": "Document",
"Creator": "admin",
"UID": "SomeUUID000000000000000000000005",
"description": "",
"review_state": "private",
"title": "My Document"
}
],
"items_total": 3,
"language": "",
"layout": "listing_view",
"modified": "2016-10-21T19:00:00+00:00",
"nextPreviousEnabled": false,
"parent": {
"@id": "http://localhost:55001/plone",
"@type": "Plone Site",
"description": "",
"title": "Plone site"
},
"relatedItems": [],
"review_state": "private",
"rights": "",
"subjects": [],
"title": "My Folder",
"version": "current"
}
5 changes: 5 additions & 0 deletions src/plone/restapi/tests/test_documentation.py
Expand Up @@ -233,6 +233,11 @@ def test_documentation_content_crud(self):
response = self.api_session.get(document.absolute_url())
save_request_and_response_for_docs("content_get", response)

response = self.api_session.get(
folder.absolute_url() + "?metadata_fields=UID&metadata_fields=Creator"
)
save_request_and_response_for_docs("content_get_folder", response)

response = self.api_session.patch(
document.absolute_url(), json={"title": "My New Document Title"}
)
Expand Down

0 comments on commit a07d046

Please sign in to comment.