Mercurial > libervia-web
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):