changeset 3906:d8baf92cb921

cli (event): update commands following changes in events: - commands have been update to match changes in bridge method. - arguments have been completely reworked for `create` and `modify` commands, making it possible to fine tune events. - `list` has been removed in favor of `get` which can now display several events at once rel 372
author Goffi <goffi@goffi.org>
date Thu, 22 Sep 2022 00:01:48 +0200
parents 92482cc80d0b
children 755920bd30da
files sat_frontends/jp/cmd_event.py
diffstat 1 files changed, 291 insertions(+), 114 deletions(-) [+]
line wrap: on
line diff
--- a/sat_frontends/jp/cmd_event.py	Thu Sep 22 00:01:48 2022 +0200
+++ b/sat_frontends/jp/cmd_event.py	Thu Sep 22 00:01:48 2022 +0200
@@ -1,7 +1,7 @@
 #!/usr/bin/env python3
 
 
-# jp: a SàT command line tool
+# libervia-cli: Libervia CLI frontend
 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
 
 # This program is free software: you can redistribute it and/or modify
@@ -18,52 +18,28 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
-from dateutil import parser as du_parser
-import calendar
-import time
+import argparse
+import sys
+
+from sqlalchemy import desc
+
+from sat.core.i18n import _
 from sat.core.i18n import _
+from sat.tools.common import data_format
+from sat.tools.common import data_format
+from sat.tools.common import date_utils
 from sat.tools.common.ansi import ANSI as A
+from sat.tools.common.ansi import ANSI as A
+from sat_frontends.jp import common
 from sat_frontends.jp.constants import Const as C
-from sat_frontends.jp import common
-from sat.tools.common import data_format
+from sat_frontends.jp.constants import Const as C
+
 from . import base
 
 __commands__ = ["Event"]
 
 OUTPUT_OPT_TABLE = "table"
 
-# TODO: move date parsing to base, it may be useful for other commands
-
-
-class List(base.CommandBase):
-    def __init__(self, host):
-        base.CommandBase.__init__(
-            self,
-            host,
-            "list",
-            use_output=C.OUTPUT_LIST_DICT,
-            use_pubsub=True,
-            use_verbose=True,
-            help=_("get list of registered events"),
-        )
-
-    def add_parser_options(self):
-        pass
-
-    async def start(self):
-        try:
-            events = await self.host.bridge.eventsList(
-                self.args.service,
-                self.args.node,
-                self.profile,
-            )
-        except Exception as e:
-            self.disp(f"can't get list of events: {e}", error=True)
-            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
-        else:
-            await self.output(events)
-            self.host.quit()
-
 
 class Get(base.CommandBase):
     def __init__(self, host):
@@ -71,11 +47,14 @@
             self,
             host,
             "get",
-            use_output=C.OUTPUT_DICT,
+            use_output=C.OUTPUT_LIST_DICT,
             use_pubsub=True,
-            pubsub_flags={C.SINGLE_ITEM},
+            pubsub_flags={C.MULTI_ITEMS, C.CACHE},
             use_verbose=True,
-            help=_("get event data"),
+            extra_outputs={
+                "default": self.default_output,
+            },
+            help=_("get event(s) data"),
         )
 
     def add_parser_options(self):
@@ -83,91 +62,281 @@
 
     async def start(self):
         try:
-            event_tuple = await self.host.bridge.eventGet(
+            events_data_s = await self.host.bridge.eventsGet(
                 self.args.service,
                 self.args.node,
-                self.args.item,
+                self.args.items,
+                self.getPubsubExtra(),
                 self.profile,
             )
         except Exception as e:
-            self.disp(f"can't get event data: {e}", error=True)
+            self.disp(f"can't get events data: {e}", error=True)
             self.host.quit(C.EXIT_BRIDGE_ERRBACK)
         else:
-            event_date, event_data = event_tuple
-            event_data["date"] = event_date
-            await self.output(event_data)
+            events_data = data_format.deserialise(events_data_s, type_check=list)
+            await self.output(events_data)
             self.host.quit()
 
+    def default_output(self, events):
+        nb_events = len(events)
+        for idx, event in enumerate(events):
+            names = event["name"]
+            name = names.get("") or next(iter(names.values()))
+            start = event["start"]
+            start_human = date_utils.date_fmt(
+                start, "medium", tz_info=date_utils.TZ_LOCAL
+            )
+            end = event["end"]
+            self.disp(A.color(
+                A.BOLD, start_human, A.RESET, " ",
+                f"({date_utils.delta2human(start, end)}) ",
+                C.A_HEADER, name
+            ))
+            if self.verbosity > 0:
+                descriptions = event.get("descriptions", [])
+                if descriptions:
+                    self.disp(descriptions[0]["description"])
+            if idx < (nb_events-1):
+                self.disp("")
 
-class EventBase(object):
+
+class CategoryAction(argparse.Action):
+
+    def __init__(self, option_strings, dest, nargs=None, metavar=None, **kwargs):
+        if nargs is not None or metavar is not None:
+            raise ValueError("nargs and metavar must not be used")
+        if metavar is not None:
+            metavar="TERM WIKIDATA_ID LANG"
+        if "--help" in sys.argv:
+            # FIXME: dirty workaround to have correct --help message
+            #   argparse doesn't normally allow variable number of arguments beside "+"
+            #   and "*", this workaround show METAVAR as 3 arguments were expected, while
+            #   we can actuall use 1, 2 or 3.
+            nargs = 3
+            metavar = ("TERM", "[WIKIDATA_ID]", "[LANG]")
+        else:
+            nargs = "+"
+
+        super().__init__(option_strings, dest, metavar=metavar, nargs=nargs, **kwargs)
+
+    def __call__(self, parser, namespace, values, option_string=None):
+        categories = getattr(namespace, self.dest)
+        if categories is None:
+            categories = []
+            setattr(namespace, self.dest, categories)
+
+        if not values:
+            parser.error("category values must be set")
+
+        category = {
+            "term": values[0]
+        }
+
+        if len(values) == 1:
+            pass
+        elif len(values) == 2:
+            value = values[1]
+            if value.startswith("Q"):
+                category["wikidata_id"] = value
+            else:
+                category["language"] = value
+        elif len(values) == 3:
+            __, wd, lang = values
+            category["wikidata_id"] = wd
+            category["language"] = lang
+        else:
+            parser.error("Category can't have more than 3 arguments")
+
+        categories.append(category)
+
+
+class EventBase:
     def add_parser_options(self):
         self.parser.add_argument(
+            "-S", "--start", type=base.date_decoder, metavar="TIME_PATTERN",
+            help=_("the start time of the event"))
+        end_group = self.parser.add_mutually_exclusive_group()
+        end_group.add_argument(
+            "-E", "--end", type=base.date_decoder, metavar="TIME_PATTERN",
+            help=_("the time of the end of the event"))
+        end_group.add_argument(
+            "-D", "--duration", help=_("duration of the event"))
+        self.parser.add_argument(
+            "-H", "--head-picture", help="URL to a picture to use as head-picture"
+        )
+        self.parser.add_argument(
+            "-d", "--description", help="plain text description the event"
+        )
+        self.parser.add_argument(
+            "-C", "--category", action=CategoryAction, dest="categories",
+            help="Category of the event"
+        )
+        self.parser.add_argument(
+            "-l", "--location", action="append", nargs="+", metavar="[KEY] VALUE",
+            help="Location metadata"
+        )
+        rsvp_group = self.parser.add_mutually_exclusive_group()
+        rsvp_group.add_argument(
+            "--rsvp", action="store_true", help=_("RSVP is requested"))
+        rsvp_group.add_argument(
+            "--rsvp_json", metavar="JSON", help=_("JSON description of RSVP form"))
+        for node_type in ("invitees", "comments", "blog", "schedule"):
+            self.parser.add_argument(
+                f"--{node_type}",
+                nargs=2,
+                metavar=("JID", "NODE"),
+                help=_("link {node_type} pubsub node").format(node_type=node_type)
+            )
+        self.parser.add_argument(
+            "-a", "--attachment", action="append", dest="attachments",
+            help=_("attach a file")
+        )
+        self.parser.add_argument("--website", help=_("website of the event"))
+        self.parser.add_argument(
+            "--status", choices=["confirmed", "tentative", "cancelled"],
+            help=_("status of the event")
+        )
+        self.parser.add_argument(
+            "-T", "--language", metavar="LANG", action="append", dest="languages",
+            help=_("main languages spoken at the event")
+        )
+        self.parser.add_argument(
+            "--wheelchair", choices=["full", "partial", "no"],
+            help=_("is the location accessible by wheelchair")
+        )
+        self.parser.add_argument(
+            "--external",
+            nargs=3,
+            metavar=("JID", "NODE", "ITEM"),
+            help=_("link to an external event")
+        )
+
+    def get_event_data(self):
+        if self.args.duration is not None:
+            if self.args.start is None:
+                self.parser.error("--start must be send if --duration is used")
+            # if duration is used, we simply add it to start time to get end time
+            self.args.end = base.date_decoder(f"{self.args.start} + {self.args.duration}")
+
+        event = {}
+        if self.args.name is not None:
+            event["name"] = {"": self.args.name}
+
+        if self.args.start is not None:
+            event["start"] = self.args.start
+
+        if self.args.end is not None:
+            event["end"] = self.args.end
+
+        if self.args.head_picture:
+            event["head-picture"] = {
+                "sources": [{
+                    "url": self.args.head_picture
+                }]
+            }
+        if self.args.description:
+            event["descriptions"] = [
+                {
+                    "type": "text",
+                    "description": self.args.description
+                }
+            ]
+        if self.args.categories:
+            event["categories"] = self.args.categories
+        if self.args.location is not None:
+            location = {}
+            for location_data in self.args.location:
+                if len(location_data) == 1:
+                    location["description"] = location_data[0]
+                else:
+                    key, *values = location_data
+                    location[key] = " ".join(values)
+            event["locations"] = [location]
+
+        if self.args.rsvp:
+            event["rsvp"] = [{}]
+        elif self.args.rsvp_json:
+            if isinstance(self.args.rsvp_elt, dict):
+                event["rsvp"] = [self.args.rsvp_json]
+            else:
+                event["rsvp"] = self.args.rsvp_json
+
+        for node_type in ("invitees", "comments", "blog", "schedule"):
+            value = getattr(self.args, node_type)
+            if value:
+                service, node = value
+                event[node_type] = {"service": service, "node": node}
+
+        if self.args.attachments:
+            attachments = event["attachments"] = []
+            for attachment in self.args.attachments:
+                attachments.append({
+                    "sources": [{"url": attachment}]
+                })
+
+        extra = {}
+
+        for arg in ("website", "status", "languages"):
+            value = getattr(self.args, arg)
+            if value is not None:
+                extra[arg] = value
+        if self.args.wheelchair is not None:
+            extra["accessibility"] = {"wheelchair": self.args.wheelchair}
+
+        if extra:
+            event["extra"] = extra
+
+        if self.args.external:
+            ext_jid, ext_node, ext_item = self.args.external
+            event["external"] = {
+                "jid": ext_jid,
+                "node": ext_node,
+                "item": ext_item
+            }
+        return event
+
+
+class Create(EventBase, base.CommandBase):
+    def __init__(self, host):
+        super().__init__(
+            host,
+            "create",
+            use_pubsub=True,
+            help=_("create or replace event"),
+        )
+
+    def add_parser_options(self):
+        super().add_parser_options()
+        self.parser.add_argument(
             "-i",
             "--id",
             default="",
             help=_("ID of the PubSub Item"),
         )
-        self.parser.add_argument("-d", "--date", type=str, help=_("date of the event"))
-        self.parser.add_argument(
-            "-f",
-            "--field",
-            action="append",
-            nargs=2,
-            dest="fields",
-            metavar=("KEY", "VALUE"),
-            help=_("configuration field to set"),
-        )
-
-    def parseFields(self):
-        return dict(self.args.fields) if self.args.fields else {}
-
-    def parseDate(self):
-        if self.args.date:
-            try:
-                date = int(self.args.date)
-            except ValueError:
-                try:
-                    date_time = du_parser.parse(
-                        self.args.date, dayfirst=not ("-" in self.args.date)
-                    )
-                except ValueError as e:
-                    self.parser.error(_("Can't parse date: {msg}").format(msg=e))
-                if date_time.tzinfo is None:
-                    date = calendar.timegm(date_time.timetuple())
-                else:
-                    date = time.mktime(date_time.timetuple())
-        else:
-            date = -1
-        return date
-
-
-class Create(EventBase, base.CommandBase):
-    def __init__(self, host):
-        super(Create, self).__init__(
-            host,
-            "create",
-            use_pubsub=True,
-            help=_("create or replace event"),
-        )
-        EventBase.__init__(self)
+        # name is mandatory here
+        self.parser.add_argument("name", help=_("name of the event"))
 
     async def start(self):
-        fields = self.parseFields()
-        date = self.parseDate()
+        if self.args.start is None:
+            self.parser.error("--start must be set")
+        event_data = self.get_event_data()
+        # we check self.args.end after get_event_data because it may be set there id
+        # --duration is used
+        if self.args.end is None:
+            self.parser.error("--end or --duration must be set")
         try:
-            node = await self.host.bridge.eventCreate(
-                date,
-                fields,
+            await self.host.bridge.eventCreate(
+                data_format.serialise(event_data),
+                self.args.id,
+                self.args.node,
                 self.args.service,
-                self.args.node,
-                self.args.id,
                 self.profile,
             )
         except Exception as e:
             self.disp(f"can't create event: {e}", error=True)
             self.host.quit(C.EXIT_BRIDGE_ERRBACK)
         else:
-            self.disp(_("Event created successfuly on node {node}").format(node=node))
+            self.disp(_("Event created successfuly)"))
             self.host.quit()
 
 
@@ -177,21 +346,24 @@
             host,
             "modify",
             use_pubsub=True,
-            pubsub_flags={C.NODE},
+            pubsub_flags={C.SINGLE_ITEM},
             help=_("modify an existing event"),
         )
         EventBase.__init__(self)
 
+    def add_parser_options(self):
+        super().add_parser_options()
+        # name is optional here
+        self.parser.add_argument("-N", "--name", help=_("name of the event"))
+
     async def start(self):
-        fields = self.parseFields()
-        date = 0 if not self.args.date else self.parseDate()
+        event_data = self.get_event_data()
         try:
-            self.host.bridge.eventModify(
+            await self.host.bridge.eventModify(
+                data_format.serialise(event_data),
+                self.args.item,
                 self.args.service,
                 self.args.node,
-                self.args.id,
-                date,
-                fields,
                 self.profile,
             )
         except Exception as e:
@@ -209,28 +381,32 @@
             "get",
             use_output=C.OUTPUT_DICT,
             use_pubsub=True,
-            pubsub_flags={C.NODE},
+            pubsub_flags={C.SINGLE_ITEM},
             use_verbose=True,
             help=_("get event attendance"),
         )
 
     def add_parser_options(self):
         self.parser.add_argument(
-            "-j", "--jid", default="", help=_("bare jid of the invitee")
+            "-j", "--jid", action="append", dest="jids", default=[],
+            help=_("only retrieve RSVP from those JIDs")
         )
 
     async def start(self):
         try:
-            event_data = await self.host.bridge.eventInviteeGet(
+            event_data_s = await self.host.bridge.eventInviteeGet(
                 self.args.service,
                 self.args.node,
-                self.args.jid,
+                self.args.item,
+                self.args.jids,
+                "",
                 self.profile,
             )
         except Exception as e:
             self.disp(f"can't get event data: {e}", error=True)
             self.host.quit(C.EXIT_BRIDGE_ERRBACK)
         else:
+            event_data = data_format.deserialise(event_data_s)
             await self.output(event_data)
             self.host.quit()
 
@@ -240,9 +416,8 @@
         super(InviteeSet, self).__init__(
             host,
             "set",
-            use_output=C.OUTPUT_DICT,
             use_pubsub=True,
-            pubsub_flags={C.NODE},
+            pubsub_flags={C.SINGLE_ITEM},
             help=_("set event attendance"),
         )
 
@@ -258,12 +433,14 @@
         )
 
     async def start(self):
+        # TODO: handle RSVP with XMLUI in a similar way as for `ad-hoc run`
         fields = dict(self.args.fields) if self.args.fields else {}
         try:
             self.host.bridge.eventInviteeSet(
                 self.args.service,
                 self.args.node,
-                fields,
+                self.args.item,
+                data_format.serialise(fields),
                 self.profile,
             )
         except Exception as e:
@@ -570,7 +747,7 @@
 
 
 class Event(base.CommandBase):
-    subcommands = (List, Get, Create, Modify, Invitee)
+    subcommands = (Get, Create, Modify, Invitee)
 
     def __init__(self, host):
         super(Event, self).__init__(