changeset 917:86563d6c83b0

server: Libervia pages: - introduce new LiberviaPage which are easy to create web pages tailored for SàT architecture. They are expected to be the cornerstone of the future of Libervia. - webpages paths reflected by files paths in src/pages directory (directory should be changeable in the future) - if a pages subdirectory as a page_meta.py file, it is a webpage, same for sub-subdirectories and so on - page_meta.py variable are used to instanciate the resource. Check __init__ docstring for details - access is set with a page_meta.py variable (only public for now), and subdirectories access are restricted by parent directory ones. - callables in page_meta.py get 2 arguments: the LiberviaPage resource instance, and the request - callables there can use Deferred (or not) - LiberviaPage has a couple of helper method to e.g. parse URL or return an error
author Goffi <goffi@goffi.org>
date Sun, 05 Mar 2017 23:56:31 +0100
parents e9bb7257d051
children 96a56856d357
files src/server/constants.py src/server/server.py
diffstat 2 files changed, 209 insertions(+), 6 deletions(-) [+]
line wrap: on
line diff
--- a/src/server/constants.py	Sun Mar 05 23:16:32 2017 +0100
+++ b/src/server/constants.py	Sun Mar 05 23:56:31 2017 +0100
@@ -32,6 +32,7 @@
     THEMES_URL = "themes"
     MEDIA_DIR = "media/"
     CARDS_DIR = "games/cards/tarot"
+    PAGES_DIR = u"pages"
 
     ERRNUM_BRIDGE_ERRBACK = 0  # FIXME
     ERRNUM_LIBERVIA = 0  # FIXME
@@ -48,3 +49,10 @@
     STATIC_RSM_MAX_LIMIT = 100
     STATIC_RSM_MAX_DEFAULT = 10
     STATIC_RSM_MAX_COMMENTS_DEFAULT = 10
+
+    ## Libervia pages ##
+    PAGES_META_FILE = u"page_meta.py"
+    PAGES_ACCESS_PUBLIC = u"public"
+    PAGES_ACCESS_PROFILE = u"profile"  # a session with an existing profile must be started
+    PAGES_ACCESS_ADMIN = u"admin"  # only profiles set in admins_list can access the page
+    PAGES_ACCESS_ALL = (PAGES_ACCESS_PUBLIC, PAGES_ACCESS_PROFILE, PAGES_ACCESS_ADMIN)
--- a/src/server/server.py	Sun Mar 05 23:16:32 2017 +0100
+++ b/src/server/server.py	Sun Mar 05 23:56:31 2017 +0100
@@ -25,7 +25,7 @@
 from twisted.web import util as web_util
 from twisted.web import http
 from twisted.python.components import registerAdapter
-from twisted.python.failure import Failure
+from twisted.python import failure
 from twisted.words.protocols.jabber import jid
 
 from txjsonrpc.web import jsonrpc
@@ -38,6 +38,7 @@
 from sat.core import exceptions
 from sat.tools import utils
 from sat.tools.common import regex
+from sat.tools.common import template
 
 import re
 import glob
@@ -347,7 +348,7 @@
                 d.callback(args[0])
 
         def _errback(result):
-            d.errback(Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, result.classname)))
+            d.errback(failure.Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, result.classname)))
 
         kwargs["callback"] = _callback
         kwargs["errback"] = _errback
@@ -765,7 +766,7 @@
         try:
             return self.sat_host.bridge.getEntitiesData(jids, keys, profile)
         except Exception as e:
-            raise Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(e)))
+            raise failure.Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(e)))
 
     def jsonrpc_getEntityData(self, jid, keys):
         """Get cached data for an entity
@@ -779,7 +780,7 @@
         try:
             return self.sat_host.bridge.getEntityData(jid, keys, profile)
         except Exception as e:
-            raise Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(e)))
+            raise failure.Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(e)))
 
     def jsonrpc_getCard(self, jid_):
         """Get VCard for entiry
@@ -1385,7 +1386,7 @@
             f.write(request.args[self.NAME][0])
 
         def finish(d):
-            error = isinstance(d, Exception) or isinstance(d, Failure)
+            error = isinstance(d, Exception) or isinstance(d, failure.Failure)
             request.write(C.UPLOAD_KO if error else C.UPLOAD_OK)
             # TODO: would be great to re-use the original Exception class and message
             # but it is lost in the middle of the backtrace and encapsulated within
@@ -1432,9 +1433,199 @@
         return ("setAvatar", filepath, profile)
 
 
+class LiberviaPage(web_resource.Resource):
+    isLeaf = True  # we handle subpages ourself
+    named_pages = {}
+
+    def __init__(self, host, root_dir, name=None, access=None, parse_url=None,
+                 prepare_render=None, render=None, template=None):
+        """initiate LiberviaPages
+
+        LiberviaPages are the main resources of Libervia, using easy to set python files
+        The arguments are the variables found in page_meta.py
+        @param host(Libervia): the running instance of Libervia
+        @param root_dir(unicode): aboslute path of the page
+        @param name(unicode, None): if not None, a unique name to identify the page
+            can then be used for e.g. redirection
+        @param access(unicode, None): permission needed to access the page
+            None means public access.
+            Pages inherit from parent pages: e.g. if a "settings" page is restricted to admins,
+            and if "settings/blog" is public, it still can only be accessed by admins.
+            see C.PAGES_ACCESS_* for details
+        @param parse_url(callable, None): if set it will be called to handle the URL path
+            after this method, the page will be rendered if noting is left in path (request.postpath)
+            else a the request will be transmitted to a subpage
+        @param prepare_render(callable, None): if set, will be used to prepare the rendering
+            that often means gathering data using the bridge
+        @param render(callable, None): if not template is set, this method will be called and
+            what it returns will be rendered.
+            This method is mutually exclusive with template and must return a unicode string.
+        @param template(unicode, None): path to the template to render.
+            This method is mutually exclusive with render
+        """
+
+        web_resource.Resource.__init__(self)
+        self.host = host
+        self.root_dir = root_dir
+        if name is not None:
+            if name in self.named_pages:
+                raise exceptions.ConflictError(_(u'a Libervia page named "{}" already exists'.format(name)))
+            self.named_pages[name] = self
+        if access is None:
+            access = C.PAGES_ACCESS_PUBLIC
+        if access != C.PAGES_ACCESS_PUBLIC:
+            raise NotImplementedError(u"Non public access are not implemented yet")
+        self.parse_url = parse_url
+        self.prepare_render = prepare_render
+        self.template = template
+        self.render_method = render
+        if template is None:
+            if not callable(render):
+                log.error(_(u"render must be implemented and callable if template is not set"))
+        else:
+            if render is not None:
+                log.error(_(u"render can't be used at the same time as template"))
+        if parse_url is not None and not callable(parse_url):
+            log.error(_(u"parse_url must be a callable"))
+
+    @staticmethod
+    def importPages(host, parent=None, path=None):
+        if path is None:
+            path = []
+        if parent is None:
+            root_dir = os.path.join(os.path.dirname(libervia.__file__), C.PAGES_DIR)
+            parent = host
+        else:
+            root_dir = parent.root_dir
+        for d in os.listdir(root_dir):
+            dir_path = os.path.join(root_dir, d)
+            if not os.path.isdir(dir_path):
+                continue
+            meta_path = os.path.join(dir_path, C.PAGES_META_FILE)
+            if os.path.isfile(meta_path):
+                page_data = {}
+                # we don't want to force the presence of __init__.py
+                # so we use execfile instead of import.
+                # TODO: when moved to Python 3, __init__.py is not mandatory anymore
+                #       so we can switch to import
+                execfile(meta_path, page_data)
+                resource = LiberviaPage(
+                    host,
+                    dir_path,
+                    name=page_data.get('name'),
+                    access=page_data.get('access'),
+                    parse_url=page_data.get('parse_url'),
+                    prepare_render=page_data.get('prepare_render'),
+                    render=page_data.get('render'),
+                    template=page_data.get('template'))
+                parent.putChild(d, resource)
+                new_path = path + [d]
+                log.info(u"Added /{path} page".format(path=u'[...]/'.join(new_path)))
+                LiberviaPage.importPages(host, resource, new_path)
+
+    def getChildWithDefault(self, path, request):
+        # we handle children ourselves
+        raise exceptions.InternalError(u"this method should not be used with LiberviaPage")
+
+    def nextPath(self, request):
+        """get next URL path segment, and update request accordingly
+
+        will move first segment of postpath in prepath
+        @param request(server.Request): current HTTP request
+        @return (unicode): unquoted segment
+        @raise IndexError: there is no segment left
+        """
+        pathElement = request.postpath.pop(0)
+        request.prepath.append(pathElement)
+        return urllib.unquote(pathElement).decode('utf-8')
+
+    def pageError(self, request, code=404):
+        """generate an error page and terminate the request
+
+        @param request(server.Request): HTTP request
+        @param core(int): error code to use
+        """
+        template = u'error/' + unicode(code) + '.html'
+
+        request.setResponseCode(code)
+
+        rendered = self.host.renderer.render(
+            template,
+            root_path = '/templates/',
+            error_code = code,
+            **request.template_data)
+
+        self.writeData(rendered, request)
+        raise failure.Failure(exceptions.CancelError(u'error page is used'))
+
+    def writeData(self, data, request):
+        if data is None:
+            self.pageError(request)
+        request.write(data.encode('utf-8'))
+        request.finish()
+
+    def _subpagesHandler(self, dummy, request):
+        """render subpage if suitable
+
+        this method checks if there is still an unmanaged part of the path
+        and check if it corresponds to a subpage. If so, it render the subpage
+        else it render a NoResource.
+        If there is no unmanaged part of the segment, current page workflow is pursued
+        """
+        if request.postpath:
+            subpage = self.nextPath(request)
+            try:
+                child = self.children[subpage]
+            except KeyError:
+                self.pageError(request)
+            else:
+                child.render(request)
+                raise failure.Failure(exceptions.CancelError(u'subpage page is used'))
+
+    def _prepare_render(self, dummy, request):
+        defer.maybeDeferred(self.prepare_render, self, request)
+
+    def _render_method(self, dummy, request):
+        return defer.maybeDeferred(self.render_method, self, request)
+
+    def _render_template(self, dummy, template_data):
+        return self.host.renderer.render(
+            self.template,
+            root_path = '/templates/',
+            **template_data)
+
+    def _renderEb(self, failure_, request):
+        """don't raise error on CancelError"""
+        failure_.trap(exceptions.CancelError)
+
+    def render_GET(self, request):
+        if not hasattr(request, 'template_data'):
+            request.template_data = {}
+            if request.postpath and not request.postpath[-1]:
+                # we don't differenciate URLs finishing with '/' or not
+                del request.postpath[-1]
+        d = defer.Deferred()
+        if self.parse_url is not None:
+            d.addCallback(self.parse_url, request)
+
+        d.addCallback(self._subpagesHandler, request)
+
+        if self.prepare_render:
+            d.addCallback(self._prepare_render, request)
+
+        if self.template:
+            d.addCallback(self._render_template, request.template_data)
+        elif self.render_method:
+            d.addCallback(self._render_method, request)
+
+        d.addCallback(self.writeData, request)
+        d.addErrback(self._renderEb, request)
+        d.callback(self)
+        return server.NOT_DONE_YET
+
+
 class Libervia(service.Service):
 
-
     def __init__(self, options):
         self.options = options
         self.initialised = defer.Deferred()
@@ -1511,6 +1702,8 @@
         self.putChild('blog', MicroBlog(self))
         self.putChild(C.THEMES_URL, ProtectedFile(self.themes_dir))
 
+        LiberviaPage.importPages(self)
+
         # media dirs
         # FIXME: get rid of dirname and "/" in C.XXX_DIR
         self.putChild(os.path.dirname(C.MEDIA_DIR), ProtectedFile(self.media_dir))
@@ -1527,6 +1720,8 @@
         wrapped = web_resource.EncodingResourceWrapper(root, [server.GzipEncoderFactory()])
         self.site = server.Site(wrapped)
         self.site.sessionFactory = LiberviaSession
+        self.renderer = template.Renderer(self)
+        self.putChild('templates', ProtectedFile(self.renderer.base_dir))
 
 
     def _bridgeCb(self):