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 622eff4..25bd81d 100644 --- a/src/plone/app/angularjs/api/api.py +++ b/src/plone/app/angularjs/api/api.py @@ -1,120 +1,83 @@ # -*- coding: utf-8 -*- -from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile -from Products.Five.browser import BrowserView -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 +from Products.Five.browser import BrowserView +from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile +from plone.app.angularjs.interfaces import IRestApi +from zope.component import ( + getMultiAdapter, getGlobalSiteManager) +from zope.component.interfaces import ComponentLookupError +from zope.component.hooks import getSite -class ApiOverview(BrowserView): +import logging +log = logging.getLogger("plone.app.angularjs.api") - template = ViewPageTemplateFile('api.pt') - def __call__(self): - return self.template() - - def api_methods(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() - ] +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 RestApi(object): - implements(IRestApi) - def traversal(self, request): - portal = getSite() - path = request.get('path') - if not path: - return - path = '/'.join(portal.getPhysicalPath()) + '/' + path +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 = '' 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") + 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({ - 'route': path, - 'id': obj.id, - 'title': obj.title, - 'description': obj.Description(), - 'text': text + 'code': '404', + 'message': "API method '%s' not found." % name, + 'data': '' }) - def top_navigation(self, 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()) +class ApiOverview(BrowserView): + """ Api overview, list all api endpoints. + """ + template = ViewPageTemplateFile('api.pt') + + def __call__(self): + return self.template() - 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 - ] - ) + def api_views(self): + portal_url = getSite().absolute_url() + 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 7bdad8b..1db07cd 100644 --- a/src/plone/app/angularjs/api/configure.zcml +++ b/src/plone/app/angularjs/api/configure.zcml @@ -1,12 +1,9 @@ - - + + + + + + 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 2ad2c2c..2d93b25 100644 --- a/src/plone/app/angularjs/traversal.py +++ b/src/plone/app/angularjs/traversal.py @@ -1,5 +1,5 @@ -from zope.component import getUtility -from plone.app.angularjs.interfaces import IRestApi +# from zope.component import getUtility +# from plone.app.angularjs.interfaces import IRestApi from Products.CMFPlone.interfaces.siteroot import IPloneSiteRoot from ZPublisher.BaseRequest import DefaultPublishTraverse from zope.component import adapts @@ -7,8 +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 ApiOverview -import json +from plone.app.angularjs.api.api import ApiDispatcherView, ApiOverview class AngularAppPortalRootTraverser(DefaultPublishTraverse): @@ -16,17 +15,20 @@ class AngularAppPortalRootTraverser(DefaultPublishTraverse): def publishTraverse(self, request, name): if IAPIRequest.providedBy(request): - if name == '' or name == 'folder_listing' or name == 'front-page': + if name in ['', 'folder_listing', 'front-page']: return ApiOverview(self.context, self.request) - api = getUtility(IRestApi) - if getattr(api, name, None): - return getattr(api, name)(request) else: - return json.dumps({ - 'code': '404', - 'message': "API method '%s' not found." % name, - 'data': '' - }) + 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) + 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 \ @@ -42,6 +44,17 @@ def publishTraverse(self, request, name): ) + def nextName(self): + """ Pop the next name off of the traversal stack. + """ + return self.request['TraversalRequestNameStack'].pop() + + def hasMoreNames(self): + """ Are there names left for traversal? + """ + return len(self.request['TraversalRequestNameStack']) > 0 + + class AngularAppRedirectorTraverser(DefaultPublishTraverse): adapts(IDexterityItem, IBrowserRequest) # XXX: Adapting IContentish works only for Archetypes content objects: