changeset 3460:d4a71a1dac88

plugin misc lists: templates: Templates are a way to create lists with pre-filled schemas.
author Goffi <goffi@goffi.org>
date Fri, 19 Feb 2021 15:49:58 +0100
parents 8dc26e5edcd3
children 02a8d227d5bb
files sat/plugins/plugin_misc_lists.py sat/plugins/plugin_xep_0346.py
diffstat 2 files changed, 278 insertions(+), 4 deletions(-) [+]
line wrap: on
line diff
--- a/sat/plugins/plugin_misc_lists.py	Thu Feb 04 21:05:21 2021 +0100
+++ b/sat/plugins/plugin_misc_lists.py	Fri Feb 19 15:49:58 2021 +0100
@@ -15,11 +15,16 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import shortuuid
+from typing import Tuple
+from twisted.internet import defer
+from twisted.words.xish import domish
+from twisted.words.protocols.jabber import jid
+from wokkel import data_form
 from sat.core.i18n import _, D_
 from sat.core.constants import Const as C
-from twisted.internet import defer
 from sat.tools.common import uri
-import shortuuid
+from sat.tools.common import data_format
 from sat.core.log import getLogger
 
 log = getLogger(__name__)
@@ -27,6 +32,7 @@
 # XXX: this plugin was formely named "tickets", thus the namespace keeps this
 # name
 APP_NS_TICKETS = "org.salut-a-toi.tickets:0"
+NS_TICKETS_TYPE = "org.salut-a-toi.tickets#type:0"
 
 PLUGIN_INFO = {
     C.PI_NAME: _("Pubsub Lists"),
@@ -39,6 +45,160 @@
     C.PI_DESCRIPTION: _("""Pubsub lists management plugin"""),
 }
 
+TEMPLATES = {
+    "todo": {
+        "name": D_("TODO List"),
+        "icon": "check",
+        "fields": [
+            {"name": "title"},
+            {"name": "author"},
+            {"name": "created"},
+            {"name": "updated"},
+            {"name": "time_limit"},
+            {"name": "labels", "type": "text-multi"},
+            {
+                "name": "status",
+                "label": D_("status"),
+                "type": "list-single",
+                "options": [
+                    {
+                        "label": D_("to do"),
+                        "value": "todo"
+                    },
+                    {
+                        "label": D_("in progress"),
+                        "value": "in_progress"
+                    },
+                    {
+                        "label": D_("done"),
+                        "value": "done"
+                    },
+                ],
+                "value": "todo"
+            },
+            {
+                "name": "priority",
+                "label": D_("priority"),
+                "type": "list-single",
+                "options": [
+                    {
+                        "label": D_("major"),
+                        "value": "major"
+                    },
+                    {
+                        "label": D_("normal"),
+                        "value": "normal"
+                    },
+                    {
+                        "label": D_("minor"),
+                        "value": "minor"
+                    },
+                ],
+                "value": "normal"
+            },
+            {"name": "body", "type": "xhtml"},
+            {"name": "comments_uri"},
+        ]
+    },
+    "shopping": {
+        "name": D_("Shopping List"),
+        "icon": "basket",
+        "fields": [
+            {"name": "name", "label": D_("name")},
+            {"name": "quantity", "label": D_("quantity")},
+            {
+                "name": "status",
+                "label": D_("status"),
+                "type": "list-single",
+                "options": [
+                    {
+                        "label": D_("to buy"),
+                        "value": "to_buy"
+                    },
+                    {
+                        "label": D_("bought"),
+                        "value": "bought"
+                    },
+                ],
+                "value": "to_buy"
+            },
+        ]
+    },
+    "tickets": {
+        "name": D_("Tickets"),
+        "icon": "clipboard",
+        "fields": [
+            {"name": "title"},
+            {"name": "author"},
+            {"name": "created"},
+            {"name": "updated"},
+            {"name": "labels", "type": "text-multi"},
+            {
+                "name": "type",
+                "label": D_("type"),
+                "type": "list-single",
+                "options": [
+                    {
+                        "label": D_("bug"),
+                        "value": "bug"
+                    },
+                    {
+                        "label": D_("feature request"),
+                        "value": "feature"
+                    },
+                ],
+                "value": "bug"
+            },
+            {
+                "name": "status",
+                "label": D_("status"),
+                "type": "list-single",
+                "options": [
+                    {
+                        "label": D_("queued"),
+                        "value": "queued"
+                    },
+                    {
+                        "label": D_("started"),
+                        "value": "started"
+                    },
+                    {
+                        "label": D_("review"),
+                        "value": "review"
+                    },
+                    {
+                        "label": D_("closed"),
+                        "value": "closed"
+                    },
+                ],
+                "value": "queued"
+            },
+            {
+                "name": "priority",
+                "label": D_("priority"),
+                "type": "list-single",
+                "options": [
+                    {
+                        "label": D_("major"),
+                        "value": "major"
+                    },
+                    {
+                        "label": D_("normal"),
+                        "value": "normal"
+                    },
+                    {
+                        "label": D_("minor"),
+                        "value": "minor"
+                    },
+                ],
+                "value": "normal"
+            },
+            {"name": "body", "type": "xhtml"},
+            {"name": "comments_uri"},
+        ]
+    }
+}
+
 
 class PubsubLists:
 
@@ -69,6 +229,7 @@
                     "author": self._s.valueOrPublisherFilter,
                     "created": self._s.dateFilter,
                     "updated": self._s.dateFilter,
+                    "time_limit": self._s.dateFilter,
                 },
                 profile_key=profile_key),
             async_=True,
@@ -91,6 +252,28 @@
                 profile_key=profile_key),
             async_=True,
         )
+        host.bridge.addMethod(
+            "listTemplatesNamesGet",
+            ".plugin",
+            in_sign="ss",
+            out_sign="s",
+            method=self._getTemplatesNames,
+        )
+        host.bridge.addMethod(
+            "listTemplateGet",
+            ".plugin",
+            in_sign="sss",
+            out_sign="s",
+            method=self._getTemplate,
+        )
+        host.bridge.addMethod(
+            "listTemplateCreate",
+            ".plugin",
+            in_sign="ssss",
+            out_sign="(ss)",
+            method=self._createTemplate,
+            async_=True,
+        )
 
     def _set(self, service, node, values, schema=None, item_id=None, extra='',
              profile_key=C.PROF_KEY_NONE):
@@ -103,8 +286,10 @@
         d.addCallback(lambda ret: ret or "")
         return d
 
-    async def set(self, client, service, node, values, schema=None, item_id=None, extra=None,
-            deserialise=False, form_ns=APP_NS_TICKETS):
+    async def set(
+        self, client, service, node, values, schema=None, item_id=None, extra=None,
+        deserialise=False, form_ns=APP_NS_TICKETS
+    ):
         """Publish a tickets
 
         @param node(unicode, None): Pubsub node to use
@@ -152,3 +337,88 @@
         return await self._s.set(
             client, service, node, values, schema, item_id, extra, deserialise, form_ns
         )
+
+    def _getTemplatesNames(self, language, profile):
+        client = self.host.getClient(profile)
+        return data_format.serialise(self.getTemplatesNames(client, language))
+
+    def getTemplatesNames(self, client, language: str) -> list:
+        """Retrieve well known list templates"""
+
+        templates = [{"id": tpl_id, "name": d["name"], "icon": d["icon"]}
+                     for tpl_id, d in TEMPLATES.items()]
+        return templates
+
+    def _getTemplate(self, name, language, profile):
+        client = self.host.getClient(profile)
+        return data_format.serialise(self.getTemplate(client, name, language))
+
+    def getTemplate(self, client, name: str, language: str) -> dict:
+        """Retrieve a well known template"""
+        return TEMPLATES[name]
+
+    def _createTemplate(self, template_id, name, access_model, profile):
+        client = self.host.getClient(profile)
+        d = defer.ensureDeferred(self.createTemplate(
+            client, template_id, name, access_model
+        ))
+        d.addCallback(lambda node_data: (node_data[0].full(), node_data[1]))
+        return d
+
+    async def createTemplate(
+        self, client, template_id: str, name: str, access_model: str
+    ) -> Tuple[jid.JID, str]:
+        """Create a list from a template"""
+        name = name.strip()
+        if not name:
+            name = shortuuid.uuid()
+        template = TEMPLATES[template_id]
+
+        fields = [
+            data_form.Field(fieldType="hidden", var=NS_TICKETS_TYPE, value=template_id)
+        ]
+        for field_data in template['fields']:
+            field_type = field_data.get('type', 'text-single')
+            kwargs = {
+                "fieldType": field_type,
+                "var": field_data["name"],
+                "label": field_data.get('label'),
+                "value": field_data.get("value"),
+            }
+            if field_type == "xhtml":
+                kwargs.update({
+                    "fieldType": None,
+                    "ext_type": "xml",
+                })
+                if kwargs["value"] is None:
+                    kwargs["value"] = domish.Element((C.NS_XHTML, "div"))
+            elif "options" in field_data:
+                kwargs["options"] = [
+                    data_form.Option(o["value"], o.get("label"))
+                    for o in field_data["options"]
+                ]
+            field = data_form.Field(**kwargs)
+            fields.append(field)
+
+        schema = data_form.Form(
+            "form",
+            formNamespace=APP_NS_TICKETS,
+            fields=fields
+        ).toElement()
+
+        service = client.jid.userhostJID()
+        node = self._s.getSubmittedNS(f"{APP_NS_TICKETS}_{name}")
+        options = {
+            self._p.OPT_ACCESS_MODEL: access_model
+        }
+        await self._p.createNode(client, service, node, options)
+        await self._s.setSchema(client, service, node, schema)
+        list_elt = domish.Element((APP_NS_TICKETS, "list"))
+        list_elt["type"] = template_id
+        try:
+            await self.host.plugins['LIST_INTEREST'].registerPubsub(
+                client, APP_NS_TICKETS, service, node, creator=True,
+                name=name, element=list_elt)
+        except Exception as e:
+            log.warning(f"Can't add list to interests: {e}")
+        return service, node
--- a/sat/plugins/plugin_xep_0346.py	Thu Feb 04 21:05:21 2021 +0100
+++ b/sat/plugins/plugin_xep_0346.py	Fri Feb 19 15:49:58 2021 +0100
@@ -321,6 +321,10 @@
             for x_elt in item_elt.elements((data_form.NS_X_DATA, "x")):
                 form = data_form.Form.fromElement(x_elt)
                 if form_ns and form.formNamespace != form_ns:
+                    log.debug(
+                        f"form's namespace ({form.formNamespace!r}) differs from expected"
+                        f"{form_ns!r}"
+                    )
                     continue
                 prepend = [
                     ("label", "id"),