changeset 1387:a84383c659b4

lists: creation, invitation, item deletion: this big patch includes: - reorganisation of pages for consistency, discovery is now the main list page, and list overview is now in `view` while item view is moved to `view_item` - lists from lists of interest are now shown in discovery page - list deletion from discory page - list can now be created, using templates now available from backend - invitation manager can now be used from list overview - list item can now be deleted from `view_item`
author Goffi <goffi@goffi.org>
date Sat, 20 Feb 2021 14:07:22 +0100
parents 83be300d17e3
children 68ffd60a58a5
files libervia/pages/lists/_browser/__init__.py libervia/pages/lists/create/page_meta.py libervia/pages/lists/create_from_tpl/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/_browser/__init__.py libervia/pages/lists/view/page_meta.py libervia/pages/lists/view_item/page_meta.py
diffstat 10 files changed, 451 insertions(+), 221 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/pages/lists/_browser/__init__.py	Sat Feb 20 14:07:22 2021 +0100
@@ -0,0 +1,66 @@
+from browser import bind, DOMNode
+from javascript import JSON
+from bridge import Bridge
+import dialog
+
+bridge = Bridge()
+
+
+def list_delete_cb(item_elt, item):
+    dialog.notification.show(f"{item['name']!r} has been deleted")
+
+
+def list_delete_eb(failure_, item_elt, item):
+    dialog.notification.show(
+        f"Error while deleting {item['name']!r}: {failure_['message']}",
+        "error"
+    )
+
+
+def interest_retract_cb(item_elt, item):
+    print(f"{item['name']!r} removed successfuly from list of interests")
+    item_elt.classList.add("state_deleted")
+    item_elt.bind("transitionend", lambda evt: item_elt.remove())
+    bridge.psNodeDelete(
+        item['service'],
+        item['node'],
+        callback=lambda: list_delete_cb(item_elt, item),
+        errback=lambda failure: list_delete_eb(failure, item_elt, item),
+    )
+
+
+def interest_retract_eb(failure_, item_elt, item):
+    dialog.notification.show(
+        f"Can't remove list {item['name']!r} from personal interests: "
+        f"{failure_['message']}",
+        "error"
+    )
+
+
+def delete_ok(evt, notif_elt, item_elt, item):
+    bridge.interestRetract(
+        "", item['id'],
+        callback=lambda: interest_retract_cb(item_elt, item),
+        errback=lambda failure:interest_retract_eb(failure, item_elt, item))
+
+
+def delete_cancel(evt, notif_elt, item_elt, item):
+    notif_elt.remove()
+    item_elt.classList.remove("selected_for_deletion")
+
+
+@bind(".action_delete", "click")
+def on_delete(evt):
+    evt.stopPropagation()
+    evt.preventDefault()
+    target = evt.currentTarget
+    item_elt = DOMNode(target.closest('.item'))
+    item_elt.classList.add("selected_for_deletion")
+    item = JSON.parse(item_elt.dataset.item)
+    dialog.Confirm(
+        f"List {item['name']!r} will be deleted, are you sure?",
+        ok_label="delete",
+    ).show(
+        ok_cb=lambda evt, notif_elt: delete_ok(evt, notif_elt, item_elt, item),
+        cancel_cb=lambda evt, notif_elt: delete_cancel(evt, notif_elt, item_elt, item),
+    )
--- a/libervia/pages/lists/create/page_meta.py	Sat Feb 20 13:58:42 2021 +0100
+++ b/libervia/pages/lists/create/page_meta.py	Sat Feb 20 14:07:22 2021 +0100
@@ -1,58 +1,41 @@
 #!/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"
+access = C.PAGES_ACCESS_PROFILE
+template = "list/create.html"
 
 
 def parse_url(self, request):
-    self.getPathArgs(request, ["service", "node"], service="jid")
+    self.getPathArgs(request, ["template_id"])
     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")
+    if data["template_id"]:
+        self.HTTPRedirect(
+            request,
+            self.getPageByName("list_create_from_tpl").getURL(data["template_id"])
+        )
 
 
 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,
-        [],
+    profile = self.getProfile(request)
+    tpl_raw = await self.host.bridgeCall(
+        "listTemplatesNamesGet",
         "",
-        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)
+    lists_templates = data_format.deserialise(tpl_raw, type_check=list)
+    template_data["icons_names"] = {tpl['icon'] for tpl in lists_templates}
+    template_data["lists_templates"] = [
+        {
+            "icon": tpl["icon"],
+            "name": tpl["name"],
+            "url": self.getURL(tpl["id"]),
+        }
+        for tpl in lists_templates
+    ]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/pages/lists/create_from_tpl/page_meta.py	Sat Feb 20 14:07:22 2021 +0100
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+
+from sat.tools.common import data_format
+from sat.core.log import getLogger
+from sat.core.i18n import D_
+from sat.core import exceptions
+from libervia.server.constants import Const as C
+from sat_frontends.bridge.bridge_frontend import BridgeException
+
+log = getLogger(__name__)
+
+name = "list_create_from_tpl"
+access = C.PAGES_ACCESS_PROFILE
+template = "list/create_from_template.html"
+
+
+def parse_url(self, request):
+    self.getPathArgs(request, ["template_id"])
+
+async def prepare_render(self, request):
+    data = self.getRData(request)
+    template_id = data["template_id"]
+    if not template_id:
+        self.pageError(request, C.HTTP_BAD_REQUEST)
+
+    template_data = request.template_data
+    profile = self.getProfile(request)
+    tpl_raw = await self.host.bridgeCall(
+        "listTemplateGet",
+        template_id,
+        "",
+        profile,
+    )
+    template = data_format.deserialise(tpl_raw)
+    template['id'] = template_id
+    template_data["list_template"] = template
+
+async def on_data_post(self, request):
+    data = self.getRData(request)
+    template_id = data['template_id']
+    name, access = self.getPostedData(request, ('name', 'access'))
+    if access == 'private':
+        access_model = 'whitelist'
+    elif access == 'public':
+        access_model = 'open'
+    else:
+        log.warning(f"Unknown access for template creation: {access}")
+        self.pageError(request, C.HTTP_BAD_REQUEST)
+    profile = self.getProfile(request)
+    try:
+        service, node = await self.host.bridgeCall(
+            "listTemplateCreate", template_id, name, access_model, profile
+        )
+    except BridgeException as e:
+        if e.condition == "conflict":
+            raise exceptions.DataError(D_("A list with this name already exists"))
+        else:
+            log.error(f"Can't create list from template: {e}")
+            raise e
+    data["post_redirect_page"] = (
+        self.getPageByName("lists"),
+        service,
+        node or "@",
+    )
--- a/libervia/pages/lists/disco/page_meta.py	Sat Feb 20 13:58:42 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,40 +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__)
-
-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)
--- a/libervia/pages/lists/edit/page_meta.py	Sat Feb 20 13:58:42 2021 +0100
+++ b/libervia/pages/lists/edit/page_meta.py	Sat Feb 20 14:07:22 2021 +0100
@@ -16,24 +16,20 @@
 
 
 def parse_url(self, request):
-    try:
-        item_id = self.nextPath(request)
-    except IndexError:
+    self.getPathArgs(request, ["service", "node", "item_id"], service="jid", node="@")
+    data = self.getRData(request)
+    if data["item_id"] is None:
         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 = (
+    service, node, item_id = (
         data.get("service", ""),
         data.get("node", ""),
-        data["list_item_id"],
+        data["item_id"],
     )
     profile = self.getProfile(request)
 
@@ -53,7 +49,7 @@
         service.full() if service else "",
         node,
         C.NO_LIMIT,
-        [list_item_id],
+        [item_id],
         "",
         {},
         profile,
@@ -87,7 +83,7 @@
     data = self.getRData(request)
     service = data["service"]
     node = data["node"]
-    list_item_id = data["list_item_id"]
+    item_id = data["item_id"]
     posted_data = self.getAllPostedData(request)
     if not posted_data["title"] or not posted_data["body"]:
         self.pageError(request, C.HTTP_BAD_REQUEST)
@@ -106,7 +102,7 @@
 
     extra = {'update': True}
     yield self.host.bridgeCall(
-        "listSet", service.full(), node, posted_data, "", list_item_id,
+        "listSet", service.full(), node, posted_data, "", item_id,
         data_format.serialise(extra), profile
     )
     # we don't want to redirect to edit page on success, but to list overview
--- a/libervia/pages/lists/new/page_meta.py	Sat Feb 20 13:58:42 2021 +0100
+++ b/libervia/pages/lists/new/page_meta.py	Sat Feb 20 14:07:22 2021 +0100
@@ -12,6 +12,9 @@
 template = "list/create_item.html"
 
 
+def parse_url(self, request):
+    self.getPathArgs(request, ["service", "node"], service="jid", node="@")
+
 async def prepare_render(self, request):
     data = self.getRData(request)
     template_data = request.template_data
@@ -38,13 +41,17 @@
     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)
+    try:
+        wid = xmlui_obj.widgets['body']
+    except KeyError:
+        pass
+    else:
+        if wid.type == "xhtmlbox":
+            # same as for list_edit, we have to convert for now
+            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
 
 
@@ -53,7 +60,8 @@
     service = data["service"]
     node = data["node"]
     posted_data = self.getAllPostedData(request)
-    if not posted_data["title"] or not posted_data["body"]:
+    if (("title" in posted_data and not posted_data["title"])
+         or ("body" in posted_data and 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(",")]
@@ -62,10 +70,11 @@
     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,
+    if "body" in posted_data:
+        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)]
 
 
--- a/libervia/pages/lists/page_meta.py	Sat Feb 20 13:58:42 2021 +0100
+++ b/libervia/pages/lists/page_meta.py	Sat Feb 20 14:07:22 2021 +0100
@@ -1,58 +1,69 @@
 #!/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 twisted.words.protocols.jabber import jid
+from sat.core.i18n import _
+from sat.core.log import getLogger
 from sat.tools.common import data_format
-from sat.core.log import getLogger
 
 log = getLogger(__name__)
 
-name = "lists"
+name = "lists_disco"
 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")
-
+template = "list/discover.html"
 
 async def prepare_render(self, request):
-    data = self.getRData(request)
+    profile = self.getProfile(request)
     template_data = request.template_data
-    service, node = data["service"], data["node"]
-    profile = self.getProfile(request) or C.SERVICE_PROFILE
+    template_data["url_list_create"] = self.getPageByName("list_create").url
+    lists_directory_config = self.host.options["lists_directory_json"]
+    lists_directory = request.template_data["lists_directory"] = []
 
-    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)
+    if lists_directory_config:
+        try:
+            for list_data in lists_directory_config:
+                service = list_data["service"]
+                node = list_data["node"]
+                name = list_data["name"]
+                url = self.getPageByName("lists").getURL(service, node)
+                lists_directory.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))
 
-    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)
+    if profile is not None:
+        try:
+            lists_list_raw = await self.host.bridgeCall("listsList", "", "", profile)
+        except Exception as e:
+            log.warning(
+                _("Can't get list of registered lists for {profile}: {reason}")
+                .format(profile=profile, reason=e)
+            )
+        else:
+            lists_list = data_format.deserialise(lists_list_raw, type_check=list)
+            for list_data in lists_list:
+                service = list_data["service"]
+                node = list_data["node"]
+                list_data["url"] = self.getPageByName("lists").getURL(service, node)
+                lists_directory.append(list_data)
+
+    icons_names = set()
+    for list_data in lists_directory:
+        try:
+            icons_names.add(list_data['icon_name'])
+        except KeyError:
+            pass
+    if icons_names:
+        template_data["icons_names"] = icons_names
+
+
+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/view/_browser/__init__.py	Sat Feb 20 14:07:22 2021 +0100
@@ -0,0 +1,31 @@
+from browser import window, bind
+from invitation import InvitationManager
+
+
+lists_ns = window.lists_ns
+pubsub_service = window.pubsub_service
+pubsub_node = window.pubsub_node
+try:
+    affiliations = window.affiliations.to_dict()
+except AttributeError:
+    pass
+
+@bind("#button_manage", "click")
+def manage_click(evt):
+    evt.stopPropagation()
+    evt.preventDefault()
+    pubsub_data = {
+        "namespace": lists_ns,
+        "service": pubsub_service,
+        "node": pubsub_node
+    }
+    try:
+        name = pubsub_node.split('_', 1)[1]
+    except IndexError:
+        pass
+    else:
+        name = name.strip()
+        if name:
+            pubsub_data['name'] = name
+    manager = InvitationManager("pubsub", pubsub_data)
+    manager.attach(affiliations=affiliations)
--- a/libervia/pages/lists/view/page_meta.py	Sat Feb 20 13:58:42 2021 +0100
+++ b/libervia/pages/lists/view/page_meta.py	Sat Feb 20 14:07:22 2021 +0100
@@ -1,103 +1,89 @@
 #!/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_objects
 from sat.tools.common import data_format
 from sat.core.log import getLogger
+from sat_frontends.bridge.bridge_frontend import BridgeException
+from libervia.server.constants import Const as C
 
 log = getLogger(__name__)
 
-
-name = "list_view"
+name = "lists"
 access = C.PAGES_ACCESS_PUBLIC
-template = "list/item.html"
+template = "list/overview.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)
-
+    self.getPathArgs(request, ["service", "node"], service="jid")
     data = self.getRData(request)
-    data["list_item_id"] = item_id
+    service, node = data["service"], data["node"]
+    if node is None:
+        self.HTTPRedirect(request, self.getPageByName("lists_disco").url)
+    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.getPageByName("list_new").getURL(
+        service.full(), node or "@")
 
 
 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)
+    service, node = data["service"], data["node"]
+    profile = self.getProfile(request) or C.SERVICE_PROFILE
 
-    if profile is None:
-        profile = 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,
-        [list_item_id],
+        [],
         "",
-        {"labels_as_list": C.BOOL_TRUE},
+        extra,
         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 profile != C.SERVICE_PROFILE:
+        try:
+            affiliations = await self.host.bridgeCall(
+                "psNodeAffiliationsGet",
+                service.full() if service else "",
+                node,
+                profile
+            )
+        except BridgeException as e:
+            log.warning(f"Can't get affiliations for node {node!r} at {service}: {e}")
+            template_data["owner"] = False
+        else:
+            is_owner = affiliations.get(self.getJid(request).userhost()) == 'owner'
+            template_data["owner"] = is_owner
+            if is_owner:
+                self.exposeToScripts(
+                    request,
+                    affiliations={str(e): str(a) for e, a in affiliations.items()}
+                )
+    else:
+        template_data["owner"] = False
 
-    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_))
+    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
+    ]
+    view_url = self.getPageByName('list_view').getURL(service.full(), node or '@')
+    template_data["on_list_item_click"] = data_objects.OnClick(
+        url=f"{view_url}/{{item.id}}"
+    )
+    self.setPagination(request, metadata)
+    self.exposeToScripts(
+        request,
+        lists_ns=self.host.ns_map["tickets"],
+        pubsub_service=service.full(),
+        pubsub_node=node,
+    )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/pages/lists/view_item/page_meta.py	Sat Feb 20 14:07:22 2021 +0100
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+
+from twisted.words.protocols.jabber import jid
+from sat.core.i18n import _
+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
+from sat_frontends.bridge.bridge_frontend import BridgeException
+from libervia.server.constants import Const as C
+from libervia.server.utils import SubPage
+from libervia.server import session_iface
+
+log = getLogger(__name__)
+
+
+name = "list_view"
+access = C.PAGES_ACCESS_PUBLIC
+template = "list/item.html"
+
+
+def parse_url(self, request):
+    self.getPathArgs(request, ["service", "node", "item_id"], service="jid", node="@")
+    data = self.getRData(request)
+    if data["item_id"] is None:
+        log.warning(_("no list item id specified"))
+        self.pageError(request, C.HTTP_BAD_REQUEST)
+
+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, item_id = (
+        data.get("service", ""),
+        data.get("node", ""),
+        data["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,
+        [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
+    try:
+        comments_uri = list_item.widgets["comments_uri"].value
+    except KeyError:
+        pass
+    else:
+        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"]
+            try:
+                comments = data_format.deserialise(await self.host.bridgeCall(
+                    "mbGet", comments_service, comments_node, C.NO_LIMIT, [], {}, profile
+                ))
+            except BridgeException as e:
+                if e.classname == 'NotFound' or e.condition == 'item-not-found':
+                    log.warning(
+                        _("Can't find comment node at [{service}] {node!r}")
+                        .format(service=comments_service, node=comments_node)
+                    )
+                else:
+                    raise e
+            else:
+                template_data["comments"] = comments
+                template_data["login_url"] = self.getPageRedirectURL(request)
+                self.exposeToScripts(
+                    request,
+                    comments_node=comments_node,
+                    comments_service=comments_service,
+                )
+
+    if session.connected:
+        # we activate modification action (edit, delete) 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":
+            self.exposeToScripts(
+                request,
+                pubsub_service = service.full(),
+                pubsub_node = node,
+                pubsub_item = item_id,
+            )
+            template_data["can_modify"] = True
+            template_data["url_list_item_edit"] = self.getURLByPath(
+                SubPage("list_edit"),
+                service.full(),
+                node or "@",
+                item_id,
+            )
+
+    # we add xmpp: URI
+    uri_args = {'path': service.full()}
+    uri_args['node'] = node or self.host.ns_map["tickets"]
+    if item_id:
+        uri_args['item'] = 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_))