Mercurial > libervia-web
comparison src/server/pages.py @ 995:f88325b56a6a
server: dynamic pages first draft:
/!\ new dependency: autobahn
This patch introduce server part of dynamic pages.
Dynamic pages use websockets to establish constant connection with a Libervia page, allowing to receive real time data or update it.
The feature is activated by specifying "dynamic = true" in the page.
Once activated, page can implement "on_data" method which will be called when data are sent by the page.
To send data the other way, the page can use request.sendData.
The new "registerSignal" method allows to use an "on_signal" method to be called each time given signal is received, with automatic (and optional) filtering on profile.
New renderPartial and renderAndUpdate method allow to append new HTML elements to the dynamic page.
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 03 Jan 2018 01:10:12 +0100 |
parents | b92b06f023cb |
children | 0848b8b0188d |
comparison
equal
deleted
inserted
replaced
994:b92b06f023cb | 995:f88325b56a6a |
---|---|
31 from libervia.server.constants import Const as C | 31 from libervia.server.constants import Const as C |
32 from libervia.server import session_iface | 32 from libervia.server import session_iface |
33 from libervia.server.utils import quote | 33 from libervia.server.utils import quote |
34 import libervia | 34 import libervia |
35 | 35 |
36 from collections import namedtuple | |
37 import uuid | |
36 import os.path | 38 import os.path |
37 import urllib | 39 import urllib |
38 import time | 40 import time |
41 | |
42 WebsocketMeta = namedtuple("WebsocketMeta", ('url', 'token', 'debug')) | |
39 | 43 |
40 | 44 |
41 class Cache(object): | 45 class Cache(object): |
42 | 46 |
43 def __init__(self, rendered): | 47 def __init__(self, rendered): |
64 | 68 |
65 class LiberviaPage(web_resource.Resource): | 69 class LiberviaPage(web_resource.Resource): |
66 isLeaf = True # we handle subpages ourself | 70 isLeaf = True # we handle subpages ourself |
67 named_pages = {} | 71 named_pages = {} |
68 uri_callbacks = {} | 72 uri_callbacks = {} |
73 signals_handlers = {} | |
69 pages_redirects = {} | 74 pages_redirects = {} |
70 cache = {} | 75 cache = {} |
71 # Set of tuples (service/node/sub_id) of nodes subscribed for caching | 76 # Set of tuples (service/node/sub_id) of nodes subscribed for caching |
72 # sub_id can be empty string if not handled by service | 77 # sub_id can be empty string if not handled by service |
73 cache_pubsub_sub = set() | 78 cache_pubsub_sub = set() |
74 main_menu = None | 79 main_menu = None |
75 | 80 |
76 def __init__(self, host, root_dir, url, name=None, redirect=None, access=None, parse_url=None, | 81 def __init__(self, host, root_dir, url, name=None, redirect=None, access=None, dynamic=False, parse_url=None, |
77 prepare_render=None, render=None, template=None, on_data_post=None): | 82 prepare_render=None, render=None, template=None, |
83 on_data_post=None, on_data=None, on_signal=None): | |
78 """initiate LiberviaPages | 84 """initiate LiberviaPages |
79 | 85 |
80 LiberviaPages are the main resources of Libervia, using easy to set python files | 86 LiberviaPages are the main resources of Libervia, using easy to set python files |
81 The arguments are the variables found in page_meta.py | 87 The arguments are the variables found in page_meta.py |
82 @param host(Libervia): the running instance of Libervia | 88 @param host(Libervia): the running instance of Libervia |
94 @param access(unicode, None): permission needed to access the page | 100 @param access(unicode, None): permission needed to access the page |
95 None means public access. | 101 None means public access. |
96 Pages inherit from parent pages: e.g. if a "settings" page is restricted to admins, | 102 Pages inherit from parent pages: e.g. if a "settings" page is restricted to admins, |
97 and if "settings/blog" is public, it still can only be accessed by admins. | 103 and if "settings/blog" is public, it still can only be accessed by admins. |
98 see C.PAGES_ACCESS_* for details | 104 see C.PAGES_ACCESS_* for details |
105 @param dynamic(bool): if True, activate websocket for bidirectional communication | |
99 @param parse_url(callable, None): if set it will be called to handle the URL path | 106 @param parse_url(callable, None): if set it will be called to handle the URL path |
100 after this method, the page will be rendered if noting is left in path (request.postpath) | 107 after this method, the page will be rendered if noting is left in path (request.postpath) |
101 else a the request will be transmitted to a subpage | 108 else a the request will be transmitted to a subpage |
102 @param prepare_render(callable, None): if set, will be used to prepare the rendering | 109 @param prepare_render(callable, None): if set, will be used to prepare the rendering |
103 that often means gathering data using the bridge | 110 that often means gathering data using the bridge |
108 This method is mutually exclusive with render | 115 This method is mutually exclusive with render |
109 @param on_data_post(callable, None): method to call when data is posted | 116 @param on_data_post(callable, None): method to call when data is posted |
110 None if not post is handled | 117 None if not post is handled |
111 on_data_post can return a string with following value: | 118 on_data_post can return a string with following value: |
112 - C.POST_NO_CONFIRM: confirm flag will not be set | 119 - C.POST_NO_CONFIRM: confirm flag will not be set |
120 @param on_data(callable, None): method to call when dynamic data is sent | |
121 this method is used with Libervia's websocket mechanism | |
122 @param on_signal(callable, None): method to call when a registered signal is received | |
123 this method is used with Libervia's websocket mechanism | |
113 """ | 124 """ |
114 | 125 |
115 web_resource.Resource.__init__(self) | 126 web_resource.Resource.__init__(self) |
116 self.host = host | 127 self.host = host |
117 self.root_dir = root_dir | 128 self.root_dir = root_dir |
128 if access is None: | 139 if access is None: |
129 access = C.PAGES_ACCESS_PUBLIC | 140 access = C.PAGES_ACCESS_PUBLIC |
130 if access not in (C.PAGES_ACCESS_PUBLIC, C.PAGES_ACCESS_PROFILE, C.PAGES_ACCESS_NONE): | 141 if access not in (C.PAGES_ACCESS_PUBLIC, C.PAGES_ACCESS_PROFILE, C.PAGES_ACCESS_NONE): |
131 raise NotImplementedError(_(u"{} access is not implemented yet").format(access)) | 142 raise NotImplementedError(_(u"{} access is not implemented yet").format(access)) |
132 self.access = access | 143 self.access = access |
144 self.dynamic = dynamic | |
133 if redirect is not None: | 145 if redirect is not None: |
134 # only page access and name make sense in case of full redirection | 146 # only page access and name make sense in case of full redirection |
135 # so we check that rendering methods/values are not set | 147 # so we check that rendering methods/values are not set |
136 if not all(lambda x: x is not None | 148 if not all(lambda x: x is not None |
137 for x in (parse_url, prepare_render, render, template)): | 149 for x in (parse_url, prepare_render, render, template)): |
143 self.parse_url = parse_url | 155 self.parse_url = parse_url |
144 self.prepare_render = prepare_render | 156 self.prepare_render = prepare_render |
145 self.template = template | 157 self.template = template |
146 self.render_method = render | 158 self.render_method = render |
147 self.on_data_post = on_data_post | 159 self.on_data_post = on_data_post |
160 self.on_data = on_data | |
161 self.on_signal = on_signal | |
148 if access == C.PAGES_ACCESS_NONE: | 162 if access == C.PAGES_ACCESS_NONE: |
149 # none pages just return a 404, no further check is needed | 163 # none pages just return a 404, no further check is needed |
150 return | 164 return |
151 if template is None: | 165 if template is None: |
152 if not callable(render): | 166 if not callable(render): |
198 dir_path, | 212 dir_path, |
199 u'/' + u'/'.join(new_path), | 213 u'/' + u'/'.join(new_path), |
200 name=page_data.get('name'), | 214 name=page_data.get('name'), |
201 redirect=page_data.get('redirect'), | 215 redirect=page_data.get('redirect'), |
202 access=page_data.get('access'), | 216 access=page_data.get('access'), |
217 dynamic=page_data.get('dynamic', False), | |
203 parse_url=page_data.get('parse_url'), | 218 parse_url=page_data.get('parse_url'), |
204 prepare_render=page_data.get('prepare_render'), | 219 prepare_render=page_data.get('prepare_render'), |
205 render=page_data.get('render'), | 220 render=page_data.get('render'), |
206 template=page_data.get('template'), | 221 template=page_data.get('template'), |
207 on_data_post=page_data.get('on_data_post')) | 222 on_data_post=page_data.get('on_data_post'), |
223 on_data=page_data.get('on_data'), | |
224 on_signal=page_data.get('on_signal'), | |
225 ) | |
208 parent.putChild(d, resource) | 226 parent.putChild(d, resource) |
209 log.info(u"Added /{path} page".format(path=u'[...]/'.join(new_path))) | 227 log.info(u"Added /{path} page".format(path=u'[...]/'.join(new_path))) |
210 if 'uri_handlers' in page_data: | 228 if 'uri_handlers' in page_data: |
211 if not isinstance(page_data, dict): | 229 if not isinstance(page_data, dict): |
212 log.error(_(u'uri_handlers must be a dict')) | 230 log.error(_(u'uri_handlers must be a dict')) |
255 """ | 273 """ |
256 if uri_tuple in cls.uri_callbacks: | 274 if uri_tuple in cls.uri_callbacks: |
257 log.info(_(u"{}/{} URIs are already handled, replacing by the new handler").format(*uri_tuple)) | 275 log.info(_(u"{}/{} URIs are already handled, replacing by the new handler").format(*uri_tuple)) |
258 cls.uri_callbacks[uri_tuple] = {u'callback': get_uri_cb, | 276 cls.uri_callbacks[uri_tuple] = {u'callback': get_uri_cb, |
259 u'pre_path': pre_path} | 277 u'pre_path': pre_path} |
278 | |
279 def registerSignal(self, request, signal, check_profile=True): | |
280 r"""register a signal handler | |
281 | |
282 the page must be dynamic | |
283 when signal is received, self.on_signal will be called with: | |
284 - request | |
285 - signal name | |
286 - signal arguments | |
287 signal handler will be removed when connection with dynamic page will be lost | |
288 @param signal(unicode): name of the signal | |
289 last arg of signal must be profile, as it will be checked to filter signals | |
290 @param check_profile(bool): if True, signal profile (which MUST be last arg) will be | |
291 checked against session profile. | |
292 /!\ if False, profile will not be checked/filtered, be sure to know what you are doing | |
293 if you unset this option /!\ | |
294 """ | |
295 # FIXME: add a timeout, if socket is not opened before it, signal handler must be removed | |
296 if not self.dynamic: | |
297 log.error(_(u"You can't register signal if page is not dynamic")) | |
298 return | |
299 LiberviaPage.signals_handlers.setdefault(signal, {})[id(request)] = (self, request, check_profile) | |
300 request._signals_registered.append(signal) | |
260 | 301 |
261 def getPagePathFromURI(self, uri): | 302 def getPagePathFromURI(self, uri): |
262 """Retrieve page URL from xmpp: URI | 303 """Retrieve page URL from xmpp: URI |
263 | 304 |
264 @param uri(unicode): URI with a xmpp: scheme | 305 @param uri(unicode): URI with a xmpp: scheme |
465 log.warning(_(u"Can't remove watch for {service}/{node}: {msg}").format( | 506 log.warning(_(u"Can't remove watch for {service}/{node}: {msg}").format( |
466 service=service, node=node, msg=failure_))) | 507 service=service, node=node, msg=failure_))) |
467 else: | 508 else: |
468 cache.clear() | 509 cache.clear() |
469 | 510 |
511 @classmethod | |
512 def onSignal(cls, host, signal, *args): | |
513 """Generic method which receive registered signals | |
514 | |
515 if a callback is registered for this signal, call it | |
516 @param host: Libervia instance | |
517 @param signal(unicode): name of the signal | |
518 @param *args: args of the signals | |
519 """ | |
520 for page, request, check_profile in cls.signals_handlers.get(signal, {}).itervalues(): | |
521 if check_profile: | |
522 signal_profile = args[-1] | |
523 request_profile = page.getProfile(request) | |
524 if not request_profile: | |
525 # if you want to use signal without session, unset check_profile | |
526 # (be sure to know what you are doing) | |
527 log.error(_(u"no session started, signal can't be checked")) | |
528 continue | |
529 if signal_profile != request_profile: | |
530 # we ignore the signal, it's not for our profile | |
531 continue | |
532 if request._signals_cache is not None: | |
533 # socket is not yet opened, we cache the signal | |
534 request._signals_cache.append((request, signal, args)) | |
535 log.debug(u"signal [{signal}] cached: {args}".format( | |
536 signal = signal, | |
537 args = args)) | |
538 else: | |
539 page.on_signal(page, request, signal, *args) | |
540 | |
541 def onSocketOpen(self, request): | |
542 """Called for dynamic pages when socket has just been opened | |
543 | |
544 we send all cached signals | |
545 """ | |
546 assert request._signals_cache is not None | |
547 cache = request._signals_cache | |
548 request._signals_cache = None | |
549 for request, signal, args in cache: | |
550 self.on_signal(self, request, signal, *args) | |
551 | |
552 def onSocketClose(self, request): | |
553 """Called for dynamic pages when socket has just been closed | |
554 | |
555 we remove signal handler | |
556 """ | |
557 for signal in request._signals_registered: | |
558 try: | |
559 del LiberviaPage.signals_handlers[signal][id(request)] | |
560 except KeyError: | |
561 log.error(_(u"Can't find signal handler for [{signal}], this should not happen").format( | |
562 signal = signal)) | |
563 else: | |
564 log.debug(_(u"Removed signal handler")) | |
565 | |
470 def HTTPRedirect(self, request, url): | 566 def HTTPRedirect(self, request, url): |
471 """redirect to an URL using HTTP redirection | 567 """redirect to an URL using HTTP redirection |
472 | 568 |
473 @param request(server.Request): current HTTP request | 569 @param request(server.Request): current HTTP request |
474 @param url(unicode): url to redirect to | 570 @param url(unicode): url to redirect to |
602 | 698 |
603 return self.host.renderer.render( | 699 return self.host.renderer.render( |
604 self.template, | 700 self.template, |
605 root_path = '/templates/', | 701 root_path = '/templates/', |
606 media_path = '/' + C.MEDIA_DIR, | 702 media_path = '/' + C.MEDIA_DIR, |
703 cache_path = session_data.cache_dir, | |
607 main_menu = LiberviaPage.main_menu, | 704 main_menu = LiberviaPage.main_menu, |
608 **template_data) | 705 **template_data) |
609 | 706 |
610 def _renderEb(self, failure_, request): | 707 def _renderEb(self, failure_, request): |
611 """don't raise error on CancelError""" | 708 """don't raise error on CancelError""" |
763 login_url = self.getPageRedirectURL(request) | 860 login_url = self.getPageRedirectURL(request) |
764 self.HTTPRedirect(request, login_url) | 861 self.HTTPRedirect(request, login_url) |
765 | 862 |
766 return data | 863 return data |
767 | 864 |
865 def renderPartial(self, request, template, template_data): | |
866 """Render a template to be inserted in dynamic page | |
867 | |
868 this is NOT the normal page rendering method, it is used only to update | |
869 dynamic pages | |
870 @param template(unicode): path of the template to render | |
871 @param template_data(dict): template_data to use | |
872 """ | |
873 if not self.dynamic: | |
874 raise exceptions.InternalError(_(u"renderPartial must only be used with dynamic pages")) | |
875 session_data = self.host.getSessionData(request, session_iface.ISATSession) | |
876 | |
877 return self.host.renderer.render( | |
878 template, | |
879 root_path = '/templates/', | |
880 media_path = '/' + C.MEDIA_DIR, | |
881 cache_path = session_data.cache_dir, | |
882 main_menu = LiberviaPage.main_menu, | |
883 **template_data) | |
884 | |
885 def renderAndUpdate(self, request, template, selectors, template_data_update, update_type="append"): | |
886 """Helper method to render a partial page element and update the page | |
887 | |
888 this is NOT the normal page rendering method, it is used only to update | |
889 dynamic pages | |
890 @param request(server.Request): current HTTP request | |
891 @param template: same as for [renderPartial] | |
892 @param selectors: CSS selectors to use | |
893 @param template_data_update: template data to use | |
894 template data cached in request will be copied then updated | |
895 with this data | |
896 @parap update_type(unicode): one of: | |
897 append: append rendered element to selected element | |
898 """ | |
899 template_data = request.template_data.copy() | |
900 template_data.update(template_data_update) | |
901 html = self.renderPartial(request, template, template_data) | |
902 request.sendData(u'dom', | |
903 selectors=selectors, | |
904 update_type=update_type, | |
905 html=html) | |
906 | |
768 def renderPage(self, request, skip_parse_url=False): | 907 def renderPage(self, request, skip_parse_url=False): |
769 """Main method to handle the workflow of a LiberviaPage""" | 908 """Main method to handle the workflow of a LiberviaPage""" |
770 | 909 |
771 # template_data are the variables passed to template | 910 # template_data are the variables passed to template |
772 if not hasattr(request, 'template_data'): | 911 if not hasattr(request, 'template_data'): |
773 session_data = self.host.getSessionData(request, session_iface.ISATSession) | 912 session_data = self.host.getSessionData(request, session_iface.ISATSession) |
774 csrf_token = session_data.csrf_token | 913 csrf_token = session_data.csrf_token |
775 request.template_data = {u'profile': session_data.profile, | 914 request.template_data = {u'profile': session_data.profile, |
776 u'csrf_token': csrf_token} | 915 u'csrf_token': csrf_token} |
916 if self.dynamic: | |
917 # we need to activate dynamic page | |
918 # we set data for template, and create/register token | |
919 socket_token = unicode(uuid.uuid4()) | |
920 socket_url = self.host.getWebsocketURL(request) | |
921 socket_debug = C.boolConst(self.host.debug) | |
922 request.template_data['websocket'] = WebsocketMeta(socket_url, socket_token, socket_debug) | |
923 self.host.registerWSToken(socket_token, self, request) | |
924 # we will keep track of handlers to remove | |
925 request._signals_registered = [] | |
926 # we will cache registered signals until socket is opened | |
927 request._signals_cache = [] | |
777 | 928 |
778 # XXX: here is the code which need to be executed once | 929 # XXX: here is the code which need to be executed once |
779 # at the beginning of the request hanling | 930 # at the beginning of the request hanling |
780 if request.postpath and not request.postpath[-1]: | 931 if request.postpath and not request.postpath[-1]: |
781 # we don't differenciate URLs finishing with '/' or not | 932 # we don't differenciate URLs finishing with '/' or not |