From fd3564171e16f34e675aa6ba7f859537c6351910 Mon Sep 17 00:00:00 2001 From: MrTango Date: Tue, 5 Aug 2014 18:51:47 +0200 Subject: [PATCH 1/2] add TraversalNames as api_params to request and use ApiDispatcherView to call API methods in RestApi utility --- src/plone/app/angularjs/api/api.py | 31 +++++++++++++---- src/plone/app/angularjs/api/configure.zcml | 1 + src/plone/app/angularjs/traversal.py | 40 ++++++++++++++-------- 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/src/plone/app/angularjs/api/api.py b/src/plone/app/angularjs/api/api.py index 622eff4..bcdfb56 100644 --- a/src/plone/app/angularjs/api/api.py +++ b/src/plone/app/angularjs/api/api.py @@ -1,14 +1,34 @@ # -*- coding: utf-8 -*- -from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile +from Products.CMFCore.utils import getToolByName from Products.Five.browser import BrowserView +from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile +from plone.app.angularjs.interfaces import IRestApi +from zope.component import getUtility from zope.component.hooks import getSite -from Products.CMFCore.utils import getToolByName from zope.interface import implements -from plone.app.angularjs.interfaces import IRestApi import json +class ApiDispatcherView(BrowserView): + + def __call__(self, context=None, request=None): + api_params = self.request.get('api_params') + if api_params: + name = api_params[0] + else: + name = '' + api = getUtility(IRestApi) + if getattr(api, name, None): + return getattr(api, name)(self.context, self.request) + else: + return json.dumps({ + 'code': '404', + 'message': "API method '%s' not found." % name, + 'data': '' + }) + + class ApiOverview(BrowserView): template = ViewPageTemplateFile('api.pt') @@ -30,7 +50,7 @@ def api_methods(self): class RestApi(object): implements(IRestApi) - def traversal(self, request): + def traversal(self, context, request): portal = getSite() path = request.get('path') if not path: @@ -53,7 +73,7 @@ def traversal(self, request): 'text': text }) - def top_navigation(self, request): + def top_navigation(self, context, request): portal = getSite() catalog = getToolByName(portal, 'portal_catalog') portal_path = '/'.join(portal.getPhysicalPath()) @@ -77,7 +97,6 @@ def top_navigation(self, request): }) if brain.exclude_from_nav is not True ] ) - def navigation_tree(self, request): portal = getSite() catalog = getToolByName(portal, 'portal_catalog') diff --git a/src/plone/app/angularjs/api/configure.zcml b/src/plone/app/angularjs/api/configure.zcml index 7bdad8b..26a07e3 100644 --- a/src/plone/app/angularjs/api/configure.zcml +++ b/src/plone/app/angularjs/api/configure.zcml @@ -1,6 +1,7 @@ 0 + + class AngularAppRedirectorTraverser(DefaultPublishTraverse): adapts(IDexterityItem, IBrowserRequest) # XXX: Adapting IContentish works only for Archetypes content objects: From 45a7edca5f25bde48d90fbdffe08433625189473 Mon Sep 17 00:00:00 2001 From: MrTango Date: Thu, 7 Aug 2014 19:04:13 +0200 Subject: [PATCH 2/2] refactor api to use ApiDispatcherView to lookup for named api views --- src/plone/app/angularjs/api/api.pt | 43 +++-- src/plone/app/angularjs/api/api.py | 174 +++++++-------------- src/plone/app/angularjs/api/configure.zcml | 28 +++- src/plone/app/angularjs/api/navigation.py | 75 +++++++++ src/plone/app/angularjs/api/traversal.py | 31 ++++ src/plone/app/angularjs/traversal.py | 23 +-- 6 files changed, 231 insertions(+), 143 deletions(-) create mode 100644 src/plone/app/angularjs/api/navigation.py create mode 100644 src/plone/app/angularjs/api/traversal.py diff --git a/src/plone/app/angularjs/api/api.pt b/src/plone/app/angularjs/api/api.pt index 4160044..384328f 100644 --- a/src/plone/app/angularjs/api/api.pt +++ b/src/plone/app/angularjs/api/api.pt @@ -1,14 +1,29 @@ -
-

REST API

-

Root Methods

- -

RESTish Methods

- soon... -
+ + +

+ REST API +

+ +

+ Above you can see all IRestApi providing views. +

+ +
+

API Methods

+ +
+ + + diff --git a/src/plone/app/angularjs/api/api.py b/src/plone/app/angularjs/api/api.py index bcdfb56..25bd81d 100644 --- a/src/plone/app/angularjs/api/api.py +++ b/src/plone/app/angularjs/api/api.py @@ -1,139 +1,83 @@ # -*- coding: utf-8 -*- -from Products.CMFCore.utils import getToolByName +import json + from Products.Five.browser import BrowserView from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile from plone.app.angularjs.interfaces import IRestApi -from zope.component import getUtility +from zope.component import ( + getMultiAdapter, getGlobalSiteManager) +from zope.component.interfaces import ComponentLookupError from zope.component.hooks import getSite -from zope.interface import implements -import json +import logging +log = logging.getLogger("plone.app.angularjs.api") -class ApiDispatcherView(BrowserView): +def json_api_call(func): + """ decorator to return all values as json + """ + def decorator(*args, **kwargs): + instance = args[0] + request = getattr(instance, 'request', None) + request.response.setHeader( + 'Content-Type', + 'application/json; charset=utf-8' + ) + result = func(*args, **kwargs) + return json.dumps(result, indent=2, sort_keys=True) + + return decorator + +class ApiDispatcherView(BrowserView): + """ dispatch api calls to api views via lookup on view name and IRestApi + """ def __call__(self, context=None, request=None): api_params = self.request.get('api_params') if api_params: name = api_params[0] else: name = '' - api = getUtility(IRestApi) - if getattr(api, name, None): - return getattr(api, name)(self.context, self.request) - else: - return json.dumps({ - 'code': '404', - 'message': "API method '%s' not found." % name, - 'data': '' - }) + try: + view = getMultiAdapter( + (self.context, self.request), + IRestApi, + name=name) + return view() + except ComponentLookupError: + log.debug("No API View found with name '%s'" % name) + return json.dumps({ + 'code': '404', + 'message': "API method '%s' not found." % name, + 'data': '' + }) class ApiOverview(BrowserView): - + """ Api overview, list all api endpoints. + """ template = ViewPageTemplateFile('api.pt') def __call__(self): return self.template() - def api_methods(self): + def api_views(self): portal_url = getSite().absolute_url() - return [ - { - 'id': x, - 'description': IRestApi.get(x).getDoc(), - 'url': '%s/++api++v1/%s' % (portal_url, x), - } for x in IRestApi.names() - ] - - -class RestApi(object): - implements(IRestApi) - - def traversal(self, context, request): - portal = getSite() - path = request.get('path') - if not path: - return - path = '/'.join(portal.getPhysicalPath()) + '/' + path - try: - obj = portal.restrictedTraverse(path) - except KeyError: - return json.dumps({'title': 'Object not found.'}) - try: - text = obj.getText() - except AttributeError: - text = '' - request.response.setHeader("Content-Type", "application/json") - return json.dumps({ - 'route': path, - 'id': obj.id, - 'title': obj.title, - 'description': obj.Description(), - 'text': text - }) - - def top_navigation(self, context, request): - portal = getSite() - catalog = getToolByName(portal, 'portal_catalog') - portal_path = '/'.join(portal.getPhysicalPath()) - return json.dumps( - [ - { - 'id': brain.id, - 'title': brain.Title, - 'description': brain.description, - 'url': brain.getPath().replace( - portal_path, '' - ).lstrip('/') - } - for brain in catalog({ - 'path': { - 'query': '/'.join(portal.getPhysicalPath()), - 'depth': 1 - }, - 'portal_type': 'Folder', - 'sort_on': 'getObjPositionInParent' - }) if brain.exclude_from_nav is not True - ] - ) - def navigation_tree(self, request): - portal = getSite() - catalog = getToolByName(portal, 'portal_catalog') - portal_path = '/'.join(portal.getPhysicalPath()) - - def _get_children(context): - return [ - { - 'id': brain.id, - 'title': brain.Title, - 'description': brain.description, - 'url': brain.getPath().replace( - portal_path, '' - ).lstrip('/'), - 'children': [] - } for brain in catalog({ - 'path': {'query': context.getPath(), 'depth': 1}, - 'sort_on': 'getObjPositionInParent', - } - ) if brain.exclude_from_nav is not True - ] - return json.dumps( - [ - { - 'id': brain.id, - 'title': brain.Title, - 'description': brain.description, - 'url': brain.getPath().replace( - portal_path, '' - ).lstrip('/'), - 'children': _get_children(brain) - } - for brain in catalog( - { - 'path': {'query': portal_path, 'depth': 1}, - 'sort_on': 'getObjPositionInParent', - } - ) if brain.exclude_from_nav is not True - ] - ) + gsm = getGlobalSiteManager() + api_views = [] + for api_adapter in gsm.registeredAdapters(): + if not api_adapter.provided == IRestApi: + continue + api_view = {} + api_view['id'] = api_adapter.name + view = getMultiAdapter( + (self.context, self.request), + IRestApi, + name=api_adapter.name) + # XXX this doesn't work because we don't have the original + # class here. How we can get the original class? + api_view['description'] = view.__doc__ + api_view['url'] = '%s/++api++v1/%s' % ( + portal_url, api_adapter.name) + api_views.append(api_view) + return api_views diff --git a/src/plone/app/angularjs/api/configure.zcml b/src/plone/app/angularjs/api/configure.zcml index 26a07e3..1db07cd 100644 --- a/src/plone/app/angularjs/api/configure.zcml +++ b/src/plone/app/angularjs/api/configure.zcml @@ -4,10 +4,6 @@ xmlns:browser="http://namespaces.zope.org/browser" i18n_domain="plone.app.angularjs"> - - + + + + + +
diff --git a/src/plone/app/angularjs/api/navigation.py b/src/plone/app/angularjs/api/navigation.py new file mode 100644 index 0000000..31f3981 --- /dev/null +++ b/src/plone/app/angularjs/api/navigation.py @@ -0,0 +1,75 @@ +from Products.CMFCore.utils import getToolByName +from Products.Five.browser import BrowserView +from zope.component.hooks import getSite + +from .api import json_api_call + + +class TopNavigation(BrowserView): + + @json_api_call + def __call__(self): + portal = getSite() + catalog = getToolByName(portal, 'portal_catalog') + portal_path = '/'.join(portal.getPhysicalPath()) + return [ + { + 'id': brain.id, + 'title': brain.Title, + 'description': brain.description, + 'url': brain.getPath().replace( + portal_path, '' + ).lstrip('/') + } + for brain in catalog({ + 'path': { + 'query': '/'.join(portal.getPhysicalPath()), + 'depth': 1 + }, + 'portal_type': 'Folder', + 'sort_on': 'getObjPositionInParent' + }) if brain.exclude_from_nav is not True + ] + + +class NavigationTree(BrowserView): + + @json_api_call + def __call__(self): + portal = getSite() + catalog = getToolByName(portal, 'portal_catalog') + portal_path = '/'.join(portal.getPhysicalPath()) + + def _get_children(context): + return [ + { + 'id': brain.id, + 'title': brain.Title, + 'description': brain.description, + 'url': brain.getPath().replace( + portal_path, '' + ).lstrip('/'), + 'children': [] + } for brain in catalog({ + 'path': {'query': context.getPath(), 'depth': 1}, + 'sort_on': 'getObjPositionInParent', + } + ) if brain.exclude_from_nav is not True + ] + return [ + { + 'id': brain.id, + 'title': brain.Title, + 'description': brain.description, + 'url': brain.getPath().replace( + portal_path, '' + ).lstrip('/'), + 'children': _get_children(brain) + } + for brain in catalog( + { + 'path': {'query': portal_path, 'depth': 1}, + 'sort_on': 'getObjPositionInParent', + } + ) if brain.exclude_from_nav is not True + ] diff --git a/src/plone/app/angularjs/api/traversal.py b/src/plone/app/angularjs/api/traversal.py new file mode 100644 index 0000000..1b2f8d6 --- /dev/null +++ b/src/plone/app/angularjs/api/traversal.py @@ -0,0 +1,31 @@ +from Products.Five.browser import BrowserView +from zope.component.hooks import getSite + +from .api import json_api_call + + +class TraversalView(BrowserView): + + @json_api_call + def __call__(self): + portal = getSite() + path = self.request.get('path') + if not path: + return + path = '/'.join(portal.getPhysicalPath()) + '/' + path + try: + obj = portal.restrictedTraverse(path) + except KeyError: + return {'title': 'Object not found.'} + try: + text = obj.getText() + except AttributeError: + text = '' + self.request.response.setHeader("Content-Type", "application/json") + return { + 'route': path, + 'id': obj.id, + 'title': obj.title, + 'description': obj.Description(), + 'text': text + } diff --git a/src/plone/app/angularjs/traversal.py b/src/plone/app/angularjs/traversal.py index cca051b..2d93b25 100644 --- a/src/plone/app/angularjs/traversal.py +++ b/src/plone/app/angularjs/traversal.py @@ -7,7 +7,7 @@ from zope.publisher.interfaces.browser import IBrowserRequest from plone.app.angularjs.app.index import AngularAppRootView from plone.app.angularjs.api.traverser import IAPIRequest -from plone.app.angularjs.api.api import ApiDispatcherView +from plone.app.angularjs.api.api import ApiDispatcherView, ApiOverview class AngularAppPortalRootTraverser(DefaultPublishTraverse): @@ -15,17 +15,20 @@ class AngularAppPortalRootTraverser(DefaultPublishTraverse): def publishTraverse(self, request, name): if IAPIRequest.providedBy(request): - parameters = [] - while self.hasMoreNames(): + if name in ['', 'folder_listing', 'front-page']: + return ApiOverview(self.context, self.request) + else: + parameters = [] + while self.hasMoreNames(): + if not name.startswith('@@'): + parameters.append(name) + name = self.nextName() + if not name.startswith('@@'): + # don't add the last param if it starts with '@@' parameters.append(name) - name = self.nextName() - - if not name.startswith('@@'): - # don't add the last param if it starts with '@@' - parameters.append(name) - request.set('api_params', parameters) - return ApiDispatcherView(self.context, request) + request.set('api_params', parameters) + return ApiDispatcherView(self.context, request) is_front_page = request.URL.endswith('front-page') no_front_page = \ request.URL.endswith('folder_listing') or \