Skip to content

Commit

Permalink
add json export for endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
pbauer committed Aug 23, 2020
1 parent bc3d7bc commit b5ea62b
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 0 deletions.
8 changes: 8 additions & 0 deletions src/pcp/contenttypes/configure.zcml
Expand Up @@ -83,4 +83,12 @@
handler=".content.downtime.handleDowntimeTransition"
/>

<browser:page
name="export_restapi"
for="zope.interface.Interface"
class=".export.ExportRestapi"
template="export_restapi.pt"
permission="cmf.ManagePortal"
/>

</configure>
159 changes: 159 additions & 0 deletions src/pcp/contenttypes/export.py
@@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
from operator import itemgetter
from plone import api
from plone.app.textfield.interfaces import IRichText
from plone.dexterity.interfaces import IDexterityContainer
from plone.dexterity.interfaces import IDexterityFTI
from plone.namedfile.interfaces import INamedFileField
from plone.namedfile.interfaces import INamedImageField
from plone.restapi.interfaces import IJsonCompatible
from plone.restapi.interfaces import ISerializeToJson
from plone.restapi.serializer.converters import json_compatible
from plone.restapi.serializer.dxfields import DefaultFieldSerializer
from Products.Five import BrowserView
from z3c.relationfield.interfaces import IRelationValue
from zope.component import adapter
from zope.component import getMultiAdapter
from zope.i18n import translate
from zope.interface import alsoProvides
from zope.interface import implementer
from zope.interface import Interface
from zope.interface import noLongerProvides

import base64
import json
import logging

logger = logging.getLogger(__name__)


class ExportRestapi(BrowserView):

QUERY = {}
DROP_PATHS = []

def __call__(self, portal_type=None, include_blobs=False):
self.portal_type = portal_type
if not self.request.form.get('form.submitted', False) or not self.portal_type:
return self.index()

data = self.export_content(include_blobs=include_blobs)
number = len(data)
msg = u'Exported {} {}'.format(number, self.portal_type)
logger.info(msg)
data = json.dumps(data, sort_keys=True, indent=4)
response = self.request.response
response.setHeader('content-type', 'application/json')
response.setHeader('content-length', len(data))
response.setHeader(
'content-disposition',
'attachment; filename="{0}.json"'.format(self.portal_type))
return response.write(data)

def export_content(self, include_blobs=False):
data = []
query = {'portal_type': self.portal_type, 'Language': 'all'}
# custom setting per type
query.update(self.QUERY.get(self.portal_type, {}))
brains = api.content.find(**query)
logger.info(u'Exporting {} {}'.format(len(brains), self.portal_type))

if not include_blobs:
# remove browserlayer to skip finding the custom serializer
noLongerProvides(self.request, IThemeSpecific)

for index, brain in enumerate(brains, start=1):
skip = False
for drop in self.DROP_PATHS:
if drop in brain.getPath():
skip = True

if skip:
continue

if not index % 100:
logger.info(u'Handled {} items...'.format(index))
obj = brain.getObject()
try:
serializer = getMultiAdapter((obj, self.request), ISerializeToJson)
item = serializer()
data.append(item)
except Exception as e:
logger.info(e)

if not include_blobs:
# restore browserlayer
alsoProvides(self.request, IThemeSpecific)

return data

def portal_types(self):
"""A list with info on all content types with existing items.
"""
catalog = api.portal.get_tool('portal_catalog')
portal_types = api.portal.get_tool('portal_types')
results = []
for fti in portal_types.listTypeInfo():
if not IDexterityFTI.providedBy(fti):
continue
number = len(catalog(portal_type=fti.id, Language='all'))
if number >= 1:
results.append({
'number': number,
'value': fti.id,
'title': translate(
fti.title, domain='plone', context=self.request)
})
return sorted(results, key=itemgetter('title'))


# make sure the adapter is more specific than the default from restapi
@adapter(INamedImageField, IDexterityContainer, Interface)
class ImageFieldSerializerWithBlobs(DefaultFieldSerializer):
def __call__(self):
image = self.field.get(self.context)
if not image:
return None
result = {
"filename": image.filename,
"content-type": image.contentType,
"data": base64.b64encode(image.data),
"encoding": "base64",
}
return json_compatible(result)


@adapter(INamedFileField, IDexterityContainer, Interface)
class FileFieldSerializerWithBlobs(DefaultFieldSerializer):
def __call__(self):
namedfile = self.field.get(self.context)
if namedfile is None:
return None

result = {
"filename": namedfile.filename,
"content-type": namedfile.contentType,
"content-type": base64.b64encode(namedfile.data),
"encoding": "base64",
}
return json_compatible(result)


@adapter(IRichText, IDexterityContainer, Interface)
class RichttextFieldSerializerWithRawText(DefaultFieldSerializer):
def __call__(self):
value = self.get_value()
if value:
output = value.raw
return {
u"data": json_compatible(output),
u"content-type": json_compatible(value.mimeType),
u"encoding": json_compatible(value.encoding),
}


@adapter(IRelationValue)
@implementer(IJsonCompatible)
def relationvalue_converter_uuid(value):
if value.to_object:
return value.to_object.UID()
53 changes: 53 additions & 0 deletions src/pcp/contenttypes/export_restapi.pt
@@ -0,0 +1,53 @@
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:metal="http://xml.zope.org/namespaces/metal"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
i18n:domain="plone.z3cform"
metal:use-macro="context/main_template/macros/master">

<div metal:fill-slot="main">
<tal:main-macro metal:define-macro="main">

<h1 class="documentFirstHeading">Export content using plone.restapi</h1>

<p class="documentDescription">Export all instances of one content types as a json file.</p>

<form action="@@export_restapi" method="post" enctype="multipart/form-data">
<div class="field">
<label for="portal_type">
<span i18n:translate="">Content Type to export</span>
</label>
<select id="portal_type"
name="portal_type">
<option selected="" value="" title="" i18n:translate="">Choose one</option>
<option tal:repeat="ptype view/portal_types"
tal:content="string:${ptype/title} - ${ptype/value} (${ptype/number})"
tal:attributes="value ptype/value; title ptype/title;">
</option>
</select>
</div>

<div class="field">
<label>
<input
type="checkbox"
name="include_blobs:boolean"
id="include_blobs"
value="0"
/>
Include images and files as base-64 encoded strings?
</label>
</div>

<div class="formControls" class="form-group">
<input type="hidden" name="form.submitted" value="1"/>
<button class="btn btn-primary submit-widget button-field context"
type="submit" name="submit" value="export">Export
</button>
</div>
</form>

</tal:main-macro>
</div>

</html>
9 changes: 9 additions & 0 deletions src/pcp/contenttypes/overrides.zcml
@@ -0,0 +1,9 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:zcml="http://namespaces.zope.org/zcml">

<configure zcml:condition="installed z3c.relationfield">
<adapter factory=".export.relationvalue_converter_uuid" />
</configure>

</configure>

0 comments on commit b5ea62b

Please sign in to comment.