diff libervia/server/pages.py @ 1153:94f9d81a475e

pages: auto reloading when developer mode is activated, pages are automatically reloaded when page_meta.py is modified.
author Goffi <goffi@goffi.org>
date Fri, 22 Feb 2019 16:57:37 +0100
parents 3c7a64adfd42
children 64952ba7affe
line wrap: on
line diff
--- a/libervia/server/pages.py	Fri Feb 22 16:57:37 2019 +0100
+++ b/libervia/server/pages.py	Fri Feb 22 16:57:37 2019 +0100
@@ -111,6 +111,7 @@
         self, host, vhost_root, root_dir, url, name=None, redirect=None, access=None,
         dynamic=False, parse_url=None, prepare_render=None, render=None, template=None,
         on_data_post=None, on_data=None, on_signal=None, url_cache=False,
+        replace_on_conflict=False
         ):
         """Initiate LiberviaPage instance
 
@@ -158,6 +159,8 @@
             received. This method is used with Libervia's websocket mechanism
         @param url_cache(boolean): if set, result of parse_url is cached (per profile).
             Useful when costly calls (e.g. network) are done while parsing URL.
+        @param replace_on_conflict(boolean): if True, don't raise ConflictError if a
+            page of this name already exists, but replace it
         """
 
         web_resource.Resource.__init__(self)
@@ -167,7 +170,8 @@
         self.url = url
         self.name = name
         if name is not None:
-            if name in self.named_pages:
+            if (name in self.named_pages
+                and not (replace_on_conflict and self.named_pages[name].url == url)):
                 raise exceptions.ConflictError(
                     _(u'a Libervia page named "{}" already exists'.format(name)))
             if u"/" in name:
@@ -249,6 +253,45 @@
     def main_menu(self):
         return self.vhost_root.main_menu
 
+    @staticmethod
+    def createPage(host, meta_path, vhost_root, url_elts, replace_on_conflict=False):
+        """Create a LiberviaPage instance
+
+        @param meta_path(unicode): path to the page_meta.py file
+        @param vhost_root(resource.Resource): root resource of the virtual host
+        @param url_elts(list[unicode]): list of path element from root site to this page
+        @param replace_on_conflict(bool): same as for [LiberviaPage]
+        @return (tuple[dict, LiberviaPage]): tuple with:
+            - page_data: dict containing data of the page
+            - libervia_page: created resource
+        """
+        dir_path = os.path.dirname(meta_path)
+        page_data = {"__name__": u".".join([u"page"] + url_elts)}
+        # 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)
+        return page_data, LiberviaPage(
+            host=host,
+            vhost_root=vhost_root,
+            root_dir=dir_path,
+            url=u"/" + u"/".join(url_elts),
+            name=page_data.get(u"name"),
+            redirect=page_data.get(u"redirect"),
+            access=page_data.get(u"access"),
+            dynamic=page_data.get(u"dynamic", False),
+            parse_url=page_data.get(u"parse_url"),
+            prepare_render=page_data.get(u"prepare_render"),
+            render=page_data.get(u"render"),
+            template=page_data.get(u"template"),
+            on_data_post=page_data.get(u"on_data_post"),
+            on_data=page_data.get(u"on_data"),
+            on_signal=page_data.get(u"on_signal"),
+            url_cache=page_data.get(u"url_cache", False),
+            replace_on_conflict=replace_on_conflict
+        )
+
     @classmethod
     def importPages(cls, host, vhost_root, root_path=None, _parent=None, _path=None,
         _extra_pages=False):
@@ -286,32 +329,9 @@
                 continue
             meta_path = os.path.join(dir_path, C.PAGES_META_FILE)
             if os.path.isfile(meta_path):
-                page_data = {"__name__": u".".join([u"page"] + _path + [d])}
                 new_path = _path + [d]
-                # 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)
                 try:
-                    resource = LiberviaPage(
-                        host=host,
-                        vhost_root=vhost_root,
-                        root_dir=dir_path,
-                        url=u"/" + u"/".join(new_path),
-                        name=page_data.get(u"name"),
-                        redirect=page_data.get(u"redirect"),
-                        access=page_data.get(u"access"),
-                        dynamic=page_data.get(u"dynamic", False),
-                        parse_url=page_data.get(u"parse_url"),
-                        prepare_render=page_data.get(u"prepare_render"),
-                        render=page_data.get(u"render"),
-                        template=page_data.get(u"template"),
-                        on_data_post=page_data.get(u"on_data_post"),
-                        on_data=page_data.get(u"on_data"),
-                        on_signal=page_data.get(u"on_signal"),
-                        url_cache=page_data.get(u"url_cache", False),
-                    )
+                    page_data, resource = cls.createPage(host, meta_path, vhost_root, new_path)
                 except exceptions.ConflictError as e:
                     if _extra_pages:
                         # extra pages are discarded if there is already an existing page
@@ -347,10 +367,85 @@
                                 resource.registerURI(uri_tuple, cb)
 
                 LiberviaPage.importPages(
-                    host, vhost_root, _parent=resource, _path=new_path, _extra_pages=_extra_pages)
+                    host, vhost_root, _parent=resource, _path=new_path,
+                    _extra_pages=_extra_pages)
+
+    @classmethod
+    def onFileChange(cls, host, file_path, flags, site_root, site_path):
+        """Method triggered by file_watcher when something is changed in files
+
+        This method is used in dev mode to reload pages when needed
+        @param file_path(filepath.FilePath): path of the file which triggered the event
+        @param flags[list[unicode]): human readable flags of the event (from
+            internet.inotify)
+        @param site_root(LiberviaRootResource): root of the site
+        @param site_path(unicode): absolute path of the site
+        """
+        if flags == ['create']:
+            return
+        path = file_path.path.decode('utf-8')
+        base_name = os.path.basename(path)
+        if base_name != u"page_meta.py":
+            # we only handle libervia pages
+            return
+
+        log.debug(u"{flags} event(s) received for {file_path}".format(
+            flags=u", ".join(flags), file_path=file_path))
+
+        dir_path = os.path.dirname(path)
+        if not dir_path.startswith(site_path):
+            raise exceptions.InternalError(u"watched file should start with site path")
+
+        path_elts = [p for p in dir_path[len(site_path):].split('/') if p]
+        if not path_elts:
+            return
+
+        if path_elts[0] == C.PAGES_DIR:
+            # a page has been modified
+            del path_elts[0]
+            if not path_elts:
+                # we need at least one element to parse
+                return
+            # we retrieve page by starting from site root and finding each path element
+            parent = page = site_root
+            new_page = False
+            for idx, child_name in enumerate(path_elts):
+                try:
+                    page = page.children[child_name]
+                except KeyError:
+                    if idx != len(path_elts)-1:
+                        # a page has been created in a subdir when one or more
+                        # page_meta.py are missing on the way
+                        log.warning(_(u"Can't create a page at {path}, missing parents")
+                                    .format(path=path))
+                        return
+                    new_page = True
+                else:
+                    if idx<len(path_elts)-1:
+                        parent = page.original
+
+            try:
+                # we (re)create a page with the new/modified code
+                __, resource = cls.createPage(host, path, site_root, path_elts,
+                                              replace_on_conflict=True)
+                if not new_page:
+                    resource.children = page.original.children
+            except Exception as e:
+                log.warning(_(u"Can't create page: {reason}").format(reason=e))
+            else:
+                url_elt = path_elts[-1]
+                if not new_page:
+                    # the page was already existing, we remove it
+                    del parent.children[url_elt]
+                # we can now add the new page
+                parent.putChild(url_elt, resource)
+                if new_page:
+                    log.info(_(u"{page} created").format(page=resource))
+                else:
+                    log.info(_(u"{page} reloaded").format(page=resource))
 
     def registerURI(self, uri_tuple, get_uri_cb):
-        """register a URI handler
+        """Register a URI handler
 
         @param uri_tuple(tuple[unicode, unicode]): type or URIs handler
             type/subtype as returned by tools/common/parseXMPPUri