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
This commit is contained in:
Goffi 2022-09-22 00:01:48 +02:00
commit 4bae534e0f

View file

@ -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.ansi import ANSI as A
from sat_frontends.jp.constants import Const as C
from sat_frontends.jp import common
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.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 @@ class Get(base.CommandBase):
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 @@ class Get(base.CommandBase):
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,
self.args.service,
self.args.node,
await self.host.bridge.eventCreate(
data_format.serialise(event_data),
self.args.id,
self.args.node,
self.args.service,
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 @@ class Modify(EventBase, base.CommandBase):
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 @@ class InviteeGet(base.CommandBase):
"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 @@ class InviteeSet(base.CommandBase):
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 @@ class InviteeSet(base.CommandBase):
)
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 Invitee(base.CommandBase):
class Event(base.CommandBase):
subcommands = (List, Get, Create, Modify, Invitee)
subcommands = (Get, Create, Modify, Invitee)
def __init__(self, host):
super(Event, self).__init__(