changeset 1378:e3e303a30a74

pages (tickets): renamed "tickets" to "lists": "lists" is more generic, and tickets is actually a specific kind of list. /!\ "tickets_trackers_json" option has been renamed to "lists_directory_json".
author Goffi <goffi@goffi.org>
date Thu, 28 Jan 2021 18:51:44 +0100
parents 46ce79eac754
children 4c51f22a813a
files libervia/pages/_browser/invitation.py libervia/pages/events/page_meta.py libervia/pages/lists/create/page_meta.py libervia/pages/lists/disco/page_meta.py libervia/pages/lists/edit/page_meta.py libervia/pages/lists/new/page_meta.py libervia/pages/lists/page_meta.py libervia/pages/lists/view/page_meta.py libervia/pages/merge-requests/disco/page_meta.py libervia/pages/merge-requests/edit/page_meta.py libervia/pages/merge-requests/new/page_meta.py libervia/pages/merge-requests/page_meta.py libervia/pages/merge-requests/view/page_meta.py libervia/pages/tickets/disco/page_meta.py libervia/pages/tickets/edit/page_meta.py libervia/pages/tickets/new/page_meta.py libervia/pages/tickets/page_meta.py libervia/pages/tickets/view/page_meta.py libervia/server/constants.py libervia/server/pages.py twisted/plugins/libervia_server.py
diffstat 21 files changed, 503 insertions(+), 438 deletions(-) [+]
line wrap: on
line diff
--- a/libervia/pages/_browser/invitation.py	Fri Dec 11 17:35:06 2020 +0100
+++ b/libervia/pages/_browser/invitation.py	Thu Jan 28 18:51:44 2021 +0100
@@ -3,7 +3,7 @@
 from template import Template
 from dialog import notification
 from cache import cache
-# we use JS RegExp because Python're is really long to import in Brython
+# we use JS RegExp because Python's re is really long to import in Brython
 from javascript import RegExp
 
 bridge = Bridge()
--- a/libervia/pages/events/page_meta.py	Fri Dec 11 17:35:06 2020 +0100
+++ b/libervia/pages/events/page_meta.py	Thu Jan 28 18:51:44 2021 +0100
@@ -7,7 +7,7 @@
 from sat.core.log import getLogger
 
 log = getLogger(__name__)
-"""ticket handling pages"""
+
 
 name = "events"
 access = C.PAGES_ACCESS_PUBLIC
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/pages/lists/create/page_meta.py	Thu Jan 28 18:51:44 2021 +0100
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+
+
+from libervia.server.constants import Const as C
+from sat.tools.common import template_xmlui
+from sat.tools.common import data_objects
+from sat.tools.common import data_format
+from sat.core.log import getLogger
+
+log = getLogger(__name__)
+
+name = "list_create"
+access = C.PAGES_ACCESS_PUBLIC
+template = "list/overview.html"
+
+
+def parse_url(self, request):
+    self.getPathArgs(request, ["service", "node"], service="jid")
+    data = self.getRData(request)
+    service, node = data["service"], data["node"]
+    if node is None:
+        self.pageRedirect("lists_disco", request)
+    if node == "@":
+        node = data["node"] = ""
+    template_data = request.template_data
+    template_data["url_list_items"] = self.getURL(service.full(), node or "@")
+    template_data["url_list_new"] = self.getSubPageURL(request, "list_new")
+
+
+async def prepare_render(self, request):
+    data = self.getRData(request)
+    template_data = request.template_data
+    service, node = data["service"], data["node"]
+    profile = self.getProfile(request) or C.SERVICE_PROFILE
+
+    self.checkCache(request, C.CACHE_PUBSUB, service=service, node=node, short="tickets")
+
+    extra = self.getPubsubExtra(request)
+    extra["labels_as_list"] = C.BOOL_TRUE
+    self.handleSearch(request, extra)
+
+    list_data_raw = await self.host.bridgeCall(
+        "listGet",
+        service.full() if service else "",
+        node,
+        C.NO_LIMIT,
+        [],
+        "",
+        extra,
+        profile,
+    )
+    list_items, metadata = data_format.deserialise(list_data_raw, type_check=list)
+    template_data["list_items"] = [template_xmlui.create(self.host, x) for x in
+                                   list_items]
+    template_data["on_list_item_click"] = data_objects.OnClick(
+        url=self.getSubPageURL(request, "list_view") + "/{item.id}"
+    )
+    self.setPagination(request, metadata)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/pages/lists/disco/page_meta.py	Thu Jan 28 18:51:44 2021 +0100
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+
+
+from libervia.server.constants import Const as C
+from twisted.words.protocols.jabber import jid
+from sat.core.log import getLogger
+
+log = getLogger(__name__)
+
+name = "lists_disco"
+access = C.PAGES_ACCESS_PUBLIC
+template = "list/discover.html"
+
+
+def prepare_render(self, request):
+    lists_directory_config = self.host.options["lists_directory_json"]
+    if lists_directory_config:
+        trackers = request.template_data["lists_directory"] = []
+        try:
+            for tracker_data in lists_directory_config:
+                service = tracker_data["service"]
+                node = tracker_data["node"]
+                name = tracker_data["name"]
+                url = self.getPageByName("lists").getURL(service, node)
+                trackers.append({"name": name, "url": url})
+        except KeyError as e:
+            log.warning("Missing field in lists_directory_json: {msg}".format(msg=e))
+        except Exception as e:
+            log.warning("Can't decode lists directory: {msg}".format(msg=e))
+
+
+def on_data_post(self, request):
+    jid_str = self.getPostedData(request, "jid")
+    try:
+        jid_ = jid.JID(jid_str)
+    except RuntimeError:
+        self.pageError(request, C.HTTP_BAD_REQUEST)
+    # for now we just use default node
+    url = self.getPageByName("lists").getURL(jid_.full(), "@")
+    self.HTTPRedirect(request, url)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/pages/lists/edit/page_meta.py	Thu Jan 28 18:51:44 2021 +0100
@@ -0,0 +1,117 @@
+#!/usr/bin/env python3
+
+
+from libervia.server.constants import Const as C
+from sat.core.i18n import _
+from twisted.internet import defer
+from sat.tools.common import template_xmlui
+from sat.tools.common import data_format
+from sat.core.log import getLogger
+
+log = getLogger(__name__)
+
+name = "list_edit"
+access = C.PAGES_ACCESS_PROFILE
+template = "list/edit.html"
+
+
+def parse_url(self, request):
+    try:
+        item_id = self.nextPath(request)
+    except IndexError:
+        log.warning(_("no list item id specified"))
+        self.pageError(request, C.HTTP_BAD_REQUEST)
+
+    data = self.getRData(request)
+    data["list_item_id"] = item_id
+
+
+@defer.inlineCallbacks
+def prepare_render(self, request):
+    data = self.getRData(request)
+    template_data = request.template_data
+    service, node, list_item_id = (
+        data.get("service", ""),
+        data.get("node", ""),
+        data["list_item_id"],
+    )
+    profile = self.getProfile(request)
+
+    # we don't ignore "author" below to keep it when a list item is edited
+    # by node owner/admin and "consistent publisher" is activated
+    ignore = (
+        "publisher",
+        "author",
+        "author_jid",
+        "author_email",
+        "created",
+        "updated",
+        "comments_uri",
+    )
+    list_raw = yield self.host.bridgeCall(
+        "listGet",
+        service.full() if service else "",
+        node,
+        C.NO_LIMIT,
+        [list_item_id],
+        "",
+        {},
+        profile,
+    )
+    list_items, metadata = data_format.deserialise(list_raw, type_check=list)
+    list_item = [template_xmlui.create(self.host, x, ignore=ignore) for x in list_items][0]
+
+    try:
+        # small trick to get a one line text input instead of the big textarea
+        list_item.widgets["labels"].type = "string"
+        list_item.widgets["labels"].value = list_item.widgets["labels"].value.replace(
+            "\n", ", "
+        )
+    except KeyError:
+        pass
+
+    # for now we don't have XHTML editor, so we'll go with a TextBox and a convertion
+    # to a text friendly syntax using markdown
+    wid = list_item.widgets['body']
+    if wid.type == "xhtmlbox":
+        wid.type = "textbox"
+        wid.value =  yield self.host.bridgeCall(
+            "syntaxConvert", wid.value, C.SYNTAX_XHTML, "markdown",
+            False, profile)
+
+    template_data["new_list_item_xmlui"] = list_item
+
+
+@defer.inlineCallbacks
+def on_data_post(self, request):
+    data = self.getRData(request)
+    service = data["service"]
+    node = data["node"]
+    list_item_id = data["list_item_id"]
+    posted_data = self.getAllPostedData(request)
+    if not posted_data["title"] or not posted_data["body"]:
+        self.pageError(request, C.HTTP_BAD_REQUEST)
+    try:
+        posted_data["labels"] = [l.strip() for l in posted_data["labels"][0].split(",")]
+    except (KeyError, IndexError):
+        pass
+    profile = self.getProfile(request)
+
+    # we convert back body to XHTML
+    body = yield self.host.bridgeCall(
+        "syntaxConvert", posted_data['body'][0], "markdown", C.SYNTAX_XHTML,
+        False, profile)
+    posted_data['body'] = ['<div xmlns="{ns}">{body}</div>'.format(ns=C.NS_XHTML,
+                                                                     body=body)]
+
+    extra = {'update': True}
+    yield self.host.bridgeCall(
+        "listSet", service.full(), node, posted_data, "", list_item_id,
+        data_format.serialise(extra), profile
+    )
+    # we don't want to redirect to edit page on success, but to list overview
+    data["post_redirect_page"] = (
+        self.getPageByName("lists"),
+        service.full(),
+        node or "@",
+    )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/pages/lists/new/page_meta.py	Thu Jan 28 18:51:44 2021 +0100
@@ -0,0 +1,80 @@
+#!/usr/bin/env python3
+
+from libervia.server.constants import Const as C
+from sat.tools.common import template_xmlui
+from sat.core.log import getLogger
+
+log = getLogger(__name__)
+
+
+name = "list_new"
+access = C.PAGES_ACCESS_PROFILE
+template = "list/create_item.html"
+
+
+async def prepare_render(self, request):
+    data = self.getRData(request)
+    template_data = request.template_data
+    service, node = data.get("service", ""), data.get("node", "")
+    profile = self.getProfile(request)
+    schema = await self.host.bridgeCall("listSchemaGet", service.full(), node, profile)
+    data["schema"] = schema
+    # following fields are handled in backend
+    ignore = (
+        "author",
+        "author_jid",
+        "author_email",
+        "created",
+        "updated",
+        "comments_uri",
+        "status",
+        "milestone",
+        "priority",
+    )
+    xmlui_obj = template_xmlui.create(self.host, schema, ignore=ignore)
+    try:
+        # small trick to get a one line text input instead of the big textarea
+        xmlui_obj.widgets["labels"].type = "string"
+    except KeyError:
+        pass
+
+    # same as for list_edit, we have to convert for now
+    wid = xmlui_obj.widgets['body']
+    if wid.type == "xhtmlbox":
+        wid.type = "textbox"
+        wid.value =  await self.host.bridgeCall(
+            "syntaxConvert", wid.value, C.SYNTAX_XHTML, "markdown",
+            False, profile)
+    template_data["new_list_item_xmlui"] = xmlui_obj
+
+
+async def on_data_post(self, request):
+    data = self.getRData(request)
+    service = data["service"]
+    node = data["node"]
+    posted_data = self.getAllPostedData(request)
+    if not posted_data["title"] or not posted_data["body"]:
+        self.pageError(request, C.HTTP_BAD_REQUEST)
+    try:
+        posted_data["labels"] = [l.strip() for l in posted_data["labels"][0].split(",")]
+    except (KeyError, IndexError):
+        pass
+    profile = self.getProfile(request)
+
+    # we convert back body to XHTML
+    body = await self.host.bridgeCall(
+        "syntaxConvert", posted_data['body'][0], "markdown", C.SYNTAX_XHTML,
+        False, profile)
+    posted_data['body'] = ['<div xmlns="{ns}">{body}</div>'.format(ns=C.NS_XHTML,
+                                                                     body=body)]
+
+
+    await self.host.bridgeCall(
+        "listSet", service.full(), node, posted_data, "", "", "", profile
+    )
+    # we don't want to redirect to creation page on success, but to list overview
+    data["post_redirect_page"] = (
+        self.getPageByName("lists"),
+        service.full(),
+        node or "@",
+    )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/pages/lists/page_meta.py	Thu Jan 28 18:51:44 2021 +0100
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+
+from libervia.server.constants import Const as C
+from sat.tools.common import template_xmlui
+from sat.tools.common import data_objects
+from sat.tools.common import data_format
+from sat.core.log import getLogger
+
+log = getLogger(__name__)
+
+name = "lists"
+access = C.PAGES_ACCESS_PUBLIC
+template = "list/overview.html"
+
+
+def parse_url(self, request):
+    self.getPathArgs(request, ["service", "node"], service="jid")
+    data = self.getRData(request)
+    service, node = data["service"], data["node"]
+    if node is None:
+        self.pageRedirect("lists_disco", request)
+    if node == "@":
+        node = data["node"] = ""
+    template_data = request.template_data
+    template_data["url_list_items"] = self.getURL(service.full(), node or "@")
+    template_data["url_list_new"] = self.getSubPageURL(request, "list_new")
+
+
+async def prepare_render(self, request):
+    data = self.getRData(request)
+    template_data = request.template_data
+    service, node = data["service"], data["node"]
+    profile = self.getProfile(request) or C.SERVICE_PROFILE
+
+    self.checkCache(request, C.CACHE_PUBSUB, service=service, node=node, short="tickets")
+
+    extra = self.getPubsubExtra(request)
+    extra["labels_as_list"] = C.BOOL_TRUE
+    self.handleSearch(request, extra)
+
+    list_raw = await self.host.bridgeCall(
+        "listGet",
+        service.full() if service else "",
+        node,
+        C.NO_LIMIT,
+        [],
+        "",
+        extra,
+        profile,
+    )
+    list_items, metadata = data_format.deserialise(list_raw, type_check=list)
+    template_data["list_items"] = [
+        template_xmlui.create(self.host, x) for x in list_items
+    ]
+    template_data["on_list_item_click"] = data_objects.OnClick(
+        url=self.getSubPageURL(request, "list_view") + "/{item.id}"
+    )
+    self.setPagination(request, metadata)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/pages/lists/view/page_meta.py	Thu Jan 28 18:51:44 2021 +0100
@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+
+
+from libervia.server.constants import Const as C
+from sat.core.i18n import _
+from libervia.server.utils import SubPage
+from libervia.server import session_iface
+from twisted.words.protocols.jabber import jid
+from sat.tools.common import template_xmlui
+from sat.tools.common import uri
+from sat.tools.common import data_format
+from sat.core.log import getLogger
+
+log = getLogger(__name__)
+
+
+name = "list_view"
+access = C.PAGES_ACCESS_PUBLIC
+template = "list/item.html"
+
+
+def parse_url(self, request):
+    try:
+        item_id = self.nextPath(request)
+    except IndexError:
+        log.warning(_("no list item id specified"))
+        self.pageError(request, C.HTTP_BAD_REQUEST)
+
+    data = self.getRData(request)
+    data["list_item_id"] = item_id
+
+
+async def prepare_render(self, request):
+    data = self.getRData(request)
+    template_data = request.template_data
+    session = self.host.getSessionData(request, session_iface.ISATSession)
+    service, node, list_item_id = (
+        data.get("service", ""),
+        data.get("node", ""),
+        data["list_item_id"],
+    )
+    profile = self.getProfile(request)
+
+    if profile is None:
+        profile = C.SERVICE_PROFILE
+
+    list_raw = await self.host.bridgeCall(
+        "listGet",
+        service.full() if service else "",
+        node,
+        C.NO_LIMIT,
+        [list_item_id],
+        "",
+        {"labels_as_list": C.BOOL_TRUE},
+        profile,
+    )
+    list_items, metadata = data_format.deserialise(list_raw, type_check=list)
+    list_item = [template_xmlui.create(self.host, x) for x in list_items][0]
+    template_data["item"] = list_item
+    comments_uri = list_item.widgets["comments_uri"].value
+    if comments_uri:
+        uri_data = uri.parseXMPPUri(comments_uri)
+        template_data["comments_node"] = comments_node = uri_data["node"]
+        template_data["comments_service"] = comments_service = uri_data["path"]
+        comments = data_format.deserialise(await self.host.bridgeCall(
+            "mbGet", comments_service, comments_node, C.NO_LIMIT, [], {}, profile
+        ))
+
+        template_data["comments"] = comments
+        template_data["login_url"] = self.getPageRedirectURL(request)
+
+    if session.connected:
+        # we set edition URL only if user is the publisher or the node owner
+        publisher = jid.JID(list_item.widgets["publisher"].value)
+        is_publisher = publisher.userhostJID() == session.jid.userhostJID()
+        affiliation = None
+        if not is_publisher:
+            node = node or self.host.ns_map["tickets"]
+            affiliation = await self.host.getAffiliation(request, service, node)
+        if is_publisher or affiliation == "owner":
+            template_data["url_list_item_edit"] = self.getURLByPath(
+                SubPage("lists"),
+                service.full(),
+                node or "@",
+                SubPage("list_edit"),
+                list_item_id,
+            )
+
+    # we add xmpp: URI
+    uri_args = {'path': service.full()}
+    uri_args['node'] = node or self.host.ns_map["tickets"]
+    if list_item_id:
+        uri_args['item'] = list_item_id
+    template_data['xmpp_uri'] = uri.buildXMPPUri('pubsub', **uri_args)
+
+
+async def on_data_post(self, request):
+    type_ = self.getPostedData(request, "type")
+    if type_ == "comment":
+        blog_page = self.getPageByName("blog_view")
+        await blog_page.on_data_post(self, request)
+    else:
+        log.warning(_("Unhandled data type: {}").format(type_))
--- a/libervia/pages/merge-requests/disco/page_meta.py	Fri Dec 11 17:35:06 2020 +0100
+++ b/libervia/pages/merge-requests/disco/page_meta.py	Thu Jan 28 18:51:44 2021 +0100
@@ -6,7 +6,7 @@
 from sat.core.log import getLogger
 
 log = getLogger(__name__)
-"""ticket handling pages"""
+
 
 name = "merge-requests_disco"
 access = C.PAGES_ACCESS_PUBLIC
--- a/libervia/pages/merge-requests/edit/page_meta.py	Fri Dec 11 17:35:06 2020 +0100
+++ b/libervia/pages/merge-requests/edit/page_meta.py	Thu Jan 28 18:51:44 2021 +0100
@@ -3,7 +3,6 @@
 
 from libervia.server.constants import Const as C
 from sat.core.i18n import _
-from twisted.internet import defer
 from sat.tools.common import template_xmlui
 from sat.tools.common import data_format
 from sat.core.log import getLogger
@@ -20,20 +19,20 @@
     try:
         item_id = self.nextPath(request)
     except IndexError:
-        log.warning(_("no ticket id specified"))
+        log.warning(_("no list item id specified"))
         self.pageError(request, C.HTTP_BAD_REQUEST)
 
     data = self.getRData(request)
-    data["ticket_id"] = item_id
+    data["list_item_id"] = item_id
 
 
 async def prepare_render(self, request):
     data = self.getRData(request)
     template_data = request.template_data
-    service, node, ticket_id = (
+    service, node, list_item_id = (
         data.get("service", ""),
         data.get("node", ""),
-        data["ticket_id"],
+        data["list_item_id"],
     )
     profile = self.getProfile(request)
 
@@ -54,39 +53,41 @@
             service.full() if service else "",
             node,
             C.NO_LIMIT,
-            [ticket_id],
+            [list_item_id],
             "",
             {},
             profile,
         )
     )
-    ticket = template_xmlui.create(self.host, merge_requests['items'][0], ignore=ignore)
+    list_item = template_xmlui.create(
+        self.host, merge_requests['items'][0], ignore=ignore
+    )
 
     try:
         # small trick to get a one line text input instead of the big textarea
-        ticket.widgets["labels"].type = "string"
-        ticket.widgets["labels"].value = ticket.widgets["labels"].value.replace(
+        list_item.widgets["labels"].type = "string"
+        list_item.widgets["labels"].value = list_item.widgets["labels"].value.replace(
             "\n", ", "
         )
     except KeyError:
         pass
 
-    # same as tickets_edit
-    wid = ticket.widgets['body']
+    # same as list_edit
+    wid = list_item.widgets['body']
     if wid.type == "xhtmlbox":
         wid.type = "textbox"
         wid.value =  await self.host.bridgeCall(
             "syntaxConvert", wid.value, C.SYNTAX_XHTML, "markdown",
             False, profile)
 
-    template_data["new_ticket_xmlui"] = ticket
+    template_data["new_list_item_xmlui"] = list_item
 
 
 async def on_data_post(self, request):
     data = self.getRData(request)
     service = data["service"]
     node = data["node"]
-    ticket_id = data["ticket_id"]
+    list_item_id = data["list_item_id"]
     posted_data = self.getAllPostedData(request)
     if not posted_data["title"] or not posted_data["body"]:
         self.pageError(request, C.HTTP_BAD_REQUEST)
@@ -112,11 +113,11 @@
         "auto",
         posted_data,
         "",
-        ticket_id,
+        list_item_id,
         data_format.serialise(extra),
         profile,
     )
-    # we don't want to redirect to edit page on success, but to tickets list
+    # we don't want to redirect to edit page on success, but to list overview
     data["post_redirect_page"] = (
         self.getPageByName("merge-requests"),
         service.full(),
--- a/libervia/pages/merge-requests/new/page_meta.py	Fri Dec 11 17:35:06 2020 +0100
+++ b/libervia/pages/merge-requests/new/page_meta.py	Thu Jan 28 18:51:44 2021 +0100
@@ -5,7 +5,7 @@
 from sat.core.log import getLogger
 
 log = getLogger(__name__)
-"""ticket handling pages"""
+
 
 name = "merge-requests_new"
 access = C.PAGES_ACCESS_PUBLIC
--- a/libervia/pages/merge-requests/page_meta.py	Fri Dec 11 17:35:06 2020 +0100
+++ b/libervia/pages/merge-requests/page_meta.py	Thu Jan 28 18:51:44 2021 +0100
@@ -2,18 +2,17 @@
 
 
 from libervia.server.constants import Const as C
-from twisted.internet import defer
 from sat.tools.common import template_xmlui
 from sat.tools.common import data_format
 from sat.tools.common import data_objects
 from sat.core.log import getLogger
 
 log = getLogger(__name__)
-"""ticket handling pages"""
+
 
 name = "merge-requests"
 access = C.PAGES_ACCESS_PUBLIC
-template = "ticket/overview.html"
+template = "list/overview.html"
 
 
 def parse_url(self, request):
@@ -28,10 +27,10 @@
         request, C.CACHE_PUBSUB, service=service, node=node, short="merge-requests"
     )
     template_data = request.template_data
-    template_data["url_tickets_list"] = self.getPageByName("merge-requests").getURL(
+    template_data["url_list_items"] = self.getPageByName("merge-requests").getURL(
         service.full(), node
     )
-    template_data["url_tickets_new"] = self.getSubPageURL(request, "merge-requests_new")
+    template_data["url_list_new"] = self.getSubPageURL(request, "merge-requests_new")
 
 
 async def prepare_render(self, request):
@@ -53,9 +52,9 @@
         )
     )
 
-    template_data["tickets"] = [
+    template_data["list_items"] = [
         template_xmlui.create(self.host, x) for x in merge_requests['items']
     ]
-    template_data["on_ticket_click"] = data_objects.OnClick(
+    template_data["on_list_item_click"] = data_objects.OnClick(
         url=self.getSubPageURL(request, "merge-requests_view") + "/{item.id}"
     )
--- a/libervia/pages/merge-requests/view/page_meta.py	Fri Dec 11 17:35:06 2020 +0100
+++ b/libervia/pages/merge-requests/view/page_meta.py	Thu Jan 28 18:51:44 2021 +0100
@@ -5,7 +5,6 @@
 from sat.core.i18n import _
 from libervia.server.utils import SubPage
 from libervia.server import session_iface
-from twisted.internet import defer
 from twisted.words.protocols.jabber import jid
 from sat.tools.common import template_xmlui
 from sat.tools.common import uri
@@ -22,21 +21,21 @@
     try:
         item_id = self.nextPath(request)
     except IndexError:
-        log.warning(_("no ticket id specified"))
+        log.warning(_("no list item id specified"))
         self.pageError(request, C.HTTP_BAD_REQUEST)
 
     data = self.getRData(request)
-    data["ticket_id"] = item_id
+    data["list_item_id"] = item_id
 
 
 async def prepare_render(self, request):
     data = self.getRData(request)
     template_data = request.template_data
     session = self.host.getSessionData(request, session_iface.ISATSession)
-    service, node, ticket_id = (
+    service, node, list_item_id = (
         data.get("service", ""),
         data.get("node", ""),
-        data["ticket_id"],
+        data["list_item_id"],
     )
     profile = self.getProfile(request)
 
@@ -49,18 +48,18 @@
             service.full() if service else "",
             node,
             C.NO_LIMIT,
-            [ticket_id],
+            [list_item_id],
             "",
             {"parse": C.BOOL_TRUE, "labels_as_list": C.BOOL_TRUE},
             profile,
         )
     )
-    ticket = template_xmlui.create(
+    list_item = template_xmlui.create(
         self.host, merge_requests['items'][0], ignore=["request_data", "type"]
     )
-    template_data["item"] = ticket
+    template_data["item"] = list_item
     template_data["patches"] = merge_requests['items_patches'][0]
-    comments_uri = ticket.widgets["comments_uri"].value
+    comments_uri = list_item.widgets["comments_uri"].value
     if comments_uri:
         uri_data = uri.parseXMPPUri(comments_uri)
         template_data["comments_node"] = comments_node = uri_data["node"]
@@ -73,19 +72,19 @@
 
     if session.connected:
         # we set edition URL only if user is the publisher or the node owner
-        publisher = jid.JID(ticket.widgets["publisher"].value)
+        publisher = jid.JID(list_item.widgets["publisher"].value)
         is_publisher = publisher.userhostJID() == session.jid.userhostJID()
         affiliation = None
         if not is_publisher:
             node = node or self.host.ns_map["merge_requests"]
             affiliation = await self.host.getAffiliation(request, service, node)
         if is_publisher or affiliation == "owner":
-            template_data["url_ticket_edit"] = self.getURLByPath(
+            template_data["url_list_item_edit"] = self.getURLByPath(
                 SubPage("merge-requests"),
                 service.full(),
                 node or "@",
                 SubPage("merge-requests_edit"),
-                ticket_id,
+                list_item_id,
             )
 
 
--- a/libervia/pages/tickets/disco/page_meta.py	Fri Dec 11 17:35:06 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,41 +0,0 @@
-#!/usr/bin/env python3
-
-
-from libervia.server.constants import Const as C
-from twisted.words.protocols.jabber import jid
-from sat.core.log import getLogger
-
-log = getLogger(__name__)
-"""ticket handling pages"""
-
-name = "tickets_disco"
-access = C.PAGES_ACCESS_PUBLIC
-template = "ticket/discover.html"
-
-
-def prepare_render(self, request):
-    tickets_trackers_config = self.host.options["tickets_trackers_json"]
-    if tickets_trackers_config:
-        trackers = request.template_data["tickets_trackers"] = []
-        try:
-            for tracker_data in tickets_trackers_config:
-                service = tracker_data["service"]
-                node = tracker_data["node"]
-                name = tracker_data["name"]
-                url = self.getPageByName("tickets").getURL(service, node)
-                trackers.append({"name": name, "url": url})
-        except KeyError as e:
-            log.warning("Missing field in tickets_trackers_json: {msg}".format(msg=e))
-        except Exception as e:
-            log.warning("Can't decode tickets trackers: {msg}".format(msg=e))
-
-
-def on_data_post(self, request):
-    jid_str = self.getPostedData(request, "jid")
-    try:
-        jid_ = jid.JID(jid_str)
-    except RuntimeError:
-        self.pageError(request, C.HTTP_BAD_REQUEST)
-    # for now we just use default node
-    url = self.getPageByName("tickets").getURL(jid_.full(), "@")
-    self.HTTPRedirect(request, url)
--- a/libervia/pages/tickets/edit/page_meta.py	Fri Dec 11 17:35:06 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,118 +0,0 @@
-#!/usr/bin/env python3
-
-
-from libervia.server.constants import Const as C
-from sat.core.i18n import _
-from twisted.internet import defer
-from sat.tools.common import template_xmlui
-from sat.tools.common import data_format
-from sat.core.log import getLogger
-
-log = getLogger(__name__)
-"""ticket handling pages"""
-
-name = "tickets_edit"
-access = C.PAGES_ACCESS_PROFILE
-template = "ticket/edit.html"
-
-
-def parse_url(self, request):
-    try:
-        item_id = self.nextPath(request)
-    except IndexError:
-        log.warning(_("no ticket id specified"))
-        self.pageError(request, C.HTTP_BAD_REQUEST)
-
-    data = self.getRData(request)
-    data["ticket_id"] = item_id
-
-
-@defer.inlineCallbacks
-def prepare_render(self, request):
-    data = self.getRData(request)
-    template_data = request.template_data
-    service, node, ticket_id = (
-        data.get("service", ""),
-        data.get("node", ""),
-        data["ticket_id"],
-    )
-    profile = self.getProfile(request)
-
-    # we don't ignore "author" below to keep it when a ticket is edited
-    # by node owner/admin and "consistent publisher" is activated
-    ignore = (
-        "publisher",
-        "author",
-        "author_jid",
-        "author_email",
-        "created",
-        "updated",
-        "comments_uri",
-    )
-    tickets_raw = yield self.host.bridgeCall(
-        "ticketsGet",
-        service.full() if service else "",
-        node,
-        C.NO_LIMIT,
-        [ticket_id],
-        "",
-        {},
-        profile,
-    )
-    tickets, metadata = data_format.deserialise(tickets_raw, type_check=list)
-    ticket = [template_xmlui.create(self.host, x, ignore=ignore) for x in tickets][0]
-
-    try:
-        # small trick to get a one line text input instead of the big textarea
-        ticket.widgets["labels"].type = "string"
-        ticket.widgets["labels"].value = ticket.widgets["labels"].value.replace(
-            "\n", ", "
-        )
-    except KeyError:
-        pass
-
-    # for now we don't have XHTML editor, so we'll go with a TextBox and a convertion
-    # to a text friendly syntax using markdown
-    wid = ticket.widgets['body']
-    if wid.type == "xhtmlbox":
-        wid.type = "textbox"
-        wid.value =  yield self.host.bridgeCall(
-            "syntaxConvert", wid.value, C.SYNTAX_XHTML, "markdown",
-            False, profile)
-
-    template_data["new_ticket_xmlui"] = ticket
-
-
-@defer.inlineCallbacks
-def on_data_post(self, request):
-    data = self.getRData(request)
-    service = data["service"]
-    node = data["node"]
-    ticket_id = data["ticket_id"]
-    posted_data = self.getAllPostedData(request)
-    if not posted_data["title"] or not posted_data["body"]:
-        self.pageError(request, C.HTTP_BAD_REQUEST)
-    try:
-        posted_data["labels"] = [l.strip() for l in posted_data["labels"][0].split(",")]
-    except (KeyError, IndexError):
-        pass
-    profile = self.getProfile(request)
-
-    # we convert back body to XHTML
-    body = yield self.host.bridgeCall(
-        "syntaxConvert", posted_data['body'][0], "markdown", C.SYNTAX_XHTML,
-        False, profile)
-    posted_data['body'] = ['<div xmlns="{ns}">{body}</div>'.format(ns=C.NS_XHTML,
-                                                                     body=body)]
-
-    extra = {'update': True}
-    yield self.host.bridgeCall(
-        "ticketSet", service.full(), node, posted_data, "", ticket_id,
-        data_format.serialise(extra), profile
-    )
-    # we don't want to redirect to edit page on success, but to tickets list
-    data["post_redirect_page"] = (
-        self.getPageByName("tickets"),
-        service.full(),
-        node or "@",
-    )
--- a/libervia/pages/tickets/new/page_meta.py	Fri Dec 11 17:35:06 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,81 +0,0 @@
-#!/usr/bin/env python3
-
-from libervia.server.constants import Const as C
-from twisted.internet import defer
-from sat.tools.common import template_xmlui
-from sat.core.log import getLogger
-
-log = getLogger(__name__)
-"""ticket handling pages"""
-
-name = "tickets_new"
-access = C.PAGES_ACCESS_PROFILE
-template = "ticket/create.html"
-
-
-async def prepare_render(self, request):
-    data = self.getRData(request)
-    template_data = request.template_data
-    service, node = data.get("service", ""), data.get("node", "")
-    profile = self.getProfile(request)
-    schema = await self.host.bridgeCall("ticketsSchemaGet", service.full(), node, profile)
-    data["schema"] = schema
-    # following fields are handled in backend
-    ignore = (
-        "author",
-        "author_jid",
-        "author_email",
-        "created",
-        "updated",
-        "comments_uri",
-        "status",
-        "milestone",
-        "priority",
-    )
-    xmlui_obj = template_xmlui.create(self.host, schema, ignore=ignore)
-    try:
-        # small trick to get a one line text input instead of the big textarea
-        xmlui_obj.widgets["labels"].type = "string"
-    except KeyError:
-        pass
-
-    # same as for tickets_edit, we have to convert for now
-    wid = xmlui_obj.widgets['body']
-    if wid.type == "xhtmlbox":
-        wid.type = "textbox"
-        wid.value =  await self.host.bridgeCall(
-            "syntaxConvert", wid.value, C.SYNTAX_XHTML, "markdown",
-            False, profile)
-    template_data["new_ticket_xmlui"] = xmlui_obj
-
-
-async def on_data_post(self, request):
-    data = self.getRData(request)
-    service = data["service"]
-    node = data["node"]
-    posted_data = self.getAllPostedData(request)
-    if not posted_data["title"] or not posted_data["body"]:
-        self.pageError(request, C.HTTP_BAD_REQUEST)
-    try:
-        posted_data["labels"] = [l.strip() for l in posted_data["labels"][0].split(",")]
-    except (KeyError, IndexError):
-        pass
-    profile = self.getProfile(request)
-
-    # we convert back body to XHTML
-    body = await self.host.bridgeCall(
-        "syntaxConvert", posted_data['body'][0], "markdown", C.SYNTAX_XHTML,
-        False, profile)
-    posted_data['body'] = ['<div xmlns="{ns}">{body}</div>'.format(ns=C.NS_XHTML,
-                                                                     body=body)]
-
-
-    await self.host.bridgeCall(
-        "ticketSet", service.full(), node, posted_data, "", "", "", profile
-    )
-    # we don't want to redirect to creation page on success, but to tickets list
-    data["post_redirect_page"] = (
-        self.getPageByName("tickets"),
-        service.full(),
-        node or "@",
-    )
--- a/libervia/pages/tickets/page_meta.py	Fri Dec 11 17:35:06 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-#!/usr/bin/env python3
-
-
-from libervia.server.constants import Const as C
-from sat.tools.common import template_xmlui
-from sat.tools.common import data_objects
-from sat.tools.common import data_format
-from sat.core.log import getLogger
-
-log = getLogger(__name__)
-"""ticket handling pages"""
-
-name = "tickets"
-access = C.PAGES_ACCESS_PUBLIC
-template = "ticket/overview.html"
-
-
-def parse_url(self, request):
-    self.getPathArgs(request, ["service", "node"], service="jid")
-    data = self.getRData(request)
-    service, node = data["service"], data["node"]
-    if node is None:
-        self.pageRedirect("tickets_disco", request)
-    if node == "@":
-        node = data["node"] = ""
-    template_data = request.template_data
-    template_data["url_tickets_list"] = self.getURL(service.full(), node or "@")
-    template_data["url_tickets_new"] = self.getSubPageURL(request, "tickets_new")
-
-
-async def prepare_render(self, request):
-    data = self.getRData(request)
-    template_data = request.template_data
-    service, node = data["service"], data["node"]
-    profile = self.getProfile(request) or C.SERVICE_PROFILE
-
-    self.checkCache(request, C.CACHE_PUBSUB, service=service, node=node, short="tickets")
-
-    extra = self.getPubsubExtra(request)
-    extra["labels_as_list"] = C.BOOL_TRUE
-    self.handleSearch(request, extra)
-
-    tickets_data_raw = await self.host.bridgeCall(
-        "ticketsGet",
-        service.full() if service else "",
-        node,
-        C.NO_LIMIT,
-        [],
-        "",
-        extra,
-        profile,
-    )
-    tickets, metadata = data_format.deserialise(tickets_data_raw, type_check=list)
-    template_data["tickets"] = [template_xmlui.create(self.host, x) for x in tickets]
-    template_data["on_ticket_click"] = data_objects.OnClick(
-        url=self.getSubPageURL(request, "tickets_view") + "/{item.id}"
-    )
-    self.setPagination(request, metadata)
--- a/libervia/pages/tickets/view/page_meta.py	Fri Dec 11 17:35:06 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,98 +0,0 @@
-#!/usr/bin/env python3
-
-
-from libervia.server.constants import Const as C
-from sat.core.i18n import _
-from libervia.server.utils import SubPage
-from libervia.server import session_iface
-from twisted.internet import defer
-from twisted.words.protocols.jabber import jid
-from sat.tools.common import template_xmlui
-from sat.tools.common import uri
-from sat.tools.common import data_format
-from sat.core.log import getLogger
-
-log = getLogger(__name__)
-"""ticket handling pages"""
-
-name = "tickets_view"
-access = C.PAGES_ACCESS_PUBLIC
-template = "ticket/item.html"
-
-
-def parse_url(self, request):
-    try:
-        item_id = self.nextPath(request)
-    except IndexError:
-        log.warning(_("no ticket id specified"))
-        self.pageError(request, C.HTTP_BAD_REQUEST)
-
-    data = self.getRData(request)
-    data["ticket_id"] = item_id
-
-
-async def prepare_render(self, request):
-    data = self.getRData(request)
-    template_data = request.template_data
-    session = self.host.getSessionData(request, session_iface.ISATSession)
-    service, node, ticket_id = (
-        data.get("service", ""),
-        data.get("node", ""),
-        data["ticket_id"],
-    )
-    profile = self.getProfile(request)
-
-    if profile is None:
-        profile = C.SERVICE_PROFILE
-
-    tickets_raw = await self.host.bridgeCall(
-        "ticketsGet",
-        service.full() if service else "",
-        node,
-        C.NO_LIMIT,
-        [ticket_id],
-        "",
-        {"labels_as_list": C.BOOL_TRUE},
-        profile,
-    )
-    tickets, metadata = data_format.deserialise(tickets_raw, type_check=list)
-    ticket = [template_xmlui.create(self.host, x) for x in tickets][0]
-    template_data["item"] = ticket
-    comments_uri = ticket.widgets["comments_uri"].value
-    if comments_uri:
-        uri_data = uri.parseXMPPUri(comments_uri)
-        template_data["comments_node"] = comments_node = uri_data["node"]
-        template_data["comments_service"] = comments_service = uri_data["path"]
-        comments = data_format.deserialise(await self.host.bridgeCall(
-            "mbGet", comments_service, comments_node, C.NO_LIMIT, [], {}, profile
-        ))
-
-        template_data["comments"] = comments
-        template_data["login_url"] = self.getPageRedirectURL(request)
-
-    if session.connected:
-        # we set edition URL only if user is the publisher or the node owner
-        publisher = jid.JID(ticket.widgets["publisher"].value)
-        is_publisher = publisher.userhostJID() == session.jid.userhostJID()
-        affiliation = None
-        if not is_publisher:
-            node = node or self.host.ns_map["tickets"]
-            affiliation = await self.host.getAffiliation(request, service, node)
-        if is_publisher or affiliation == "owner":
-            template_data["url_ticket_edit"] = self.getURLByPath(
-                SubPage("tickets"),
-                service.full(),
-                node or "@",
-                SubPage("tickets_edit"),
-                ticket_id,
-            )
-
-
-@defer.inlineCallbacks
-def on_data_post(self, request):
-    type_ = self.getPostedData(request, "type")
-    if type_ == "comment":
-        blog_page = self.getPageByName("blog_view")
-        yield blog_page.on_data_post(self, request)
-    else:
-        log.warning(_("Unhandled data type: {}").format(type_))
--- a/libervia/server/constants.py	Fri Dec 11 17:35:06 2020 +0100
+++ b/libervia/server/constants.py	Thu Jan 28 18:51:44 2021 +0100
@@ -89,7 +89,7 @@
         "photos",
         "files",
         "events",
-        "tickets",
+        "lists",
         "merge-requests",
         # XXX: app is not available anymore since removal of pyjamas code with Python 3
         #    port. It should come back at a later point with an alternative (Brython
--- a/libervia/server/pages.py	Fri Dec 11 17:35:06 2020 +0100
+++ b/libervia/server/pages.py	Thu Jan 28 18:51:44 2021 +0100
@@ -726,7 +726,9 @@
                 continue
             if child_name == subpage_name:
                 return path.decode('utf-8'), child
-        raise exceptions.NotFound(_("requested sub page has not been found"))
+        raise exceptions.NotFound(
+            _("requested sub page has not been found ({subpage_name})").format(
+            subpage_name=subpage_name))
 
     def getSubPageURL(self, request, page_name, *args):
         """retrieve a page in direct children and build its URL according to request
@@ -1786,7 +1788,11 @@
                             log.debug(f"using URI cache for {self}")
                             cache_url.use(request)
                     else:
-                        await asDeferred(self.parse_url, self, request)
+                        try:
+                            await asDeferred(self.parse_url, self, request)
+                        except Exception as e:
+                            __import__('pudb').set_trace()
+                            print(e)
 
                 self._subpagesHandler(request)
 
--- a/twisted/plugins/libervia_server.py	Fri Dec 11 17:35:06 2020 +0100
+++ b/twisted/plugins/libervia_server.py	Thu Jan 28 18:51:44 2021 +0100
@@ -142,7 +142,7 @@
     ["url_redirections_dict", None, {}, None],
     ["menu_json", None, {'': C.DEFAULT_MENU}, None],
     ["menu_extra_json", None, {}, None],
-    ["tickets_trackers_json", None, None, None],
+    ["lists_directory_json", None, None, None],
     ["mr_handlers_json", None, None, None],
 ]