comparison sat_frontends/jp/cmd_event.py @ 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 04283582966f
children 524856bd7b19
comparison
equal deleted inserted replaced
3905:92482cc80d0b 3906:d8baf92cb921
1 #!/usr/bin/env python3 1 #!/usr/bin/env python3
2 2
3 3
4 # jp: a SàT command line tool 4 # libervia-cli: Libervia CLI frontend
5 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) 5 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
6 6
7 # This program is free software: you can redistribute it and/or modify 7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by 8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or 9 # the Free Software Foundation, either version 3 of the License, or
16 16
17 # You should have received a copy of the GNU Affero General Public License 17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19
20 20
21 from dateutil import parser as du_parser 21 import argparse
22 import calendar 22 import sys
23 import time 23
24 from sqlalchemy import desc
25
24 from sat.core.i18n import _ 26 from sat.core.i18n import _
27 from sat.core.i18n import _
28 from sat.tools.common import data_format
29 from sat.tools.common import data_format
30 from sat.tools.common import date_utils
25 from sat.tools.common.ansi import ANSI as A 31 from sat.tools.common.ansi import ANSI as A
32 from sat.tools.common.ansi import ANSI as A
33 from sat_frontends.jp import common
26 from sat_frontends.jp.constants import Const as C 34 from sat_frontends.jp.constants import Const as C
27 from sat_frontends.jp import common 35 from sat_frontends.jp.constants import Const as C
28 from sat.tools.common import data_format 36
29 from . import base 37 from . import base
30 38
31 __commands__ = ["Event"] 39 __commands__ = ["Event"]
32 40
33 OUTPUT_OPT_TABLE = "table" 41 OUTPUT_OPT_TABLE = "table"
34 42
35 # TODO: move date parsing to base, it may be useful for other commands 43
36 44 class Get(base.CommandBase):
37
38 class List(base.CommandBase):
39 def __init__(self, host): 45 def __init__(self, host):
40 base.CommandBase.__init__( 46 base.CommandBase.__init__(
41 self, 47 self,
42 host, 48 host,
43 "list", 49 "get",
44 use_output=C.OUTPUT_LIST_DICT, 50 use_output=C.OUTPUT_LIST_DICT,
45 use_pubsub=True, 51 use_pubsub=True,
52 pubsub_flags={C.MULTI_ITEMS, C.CACHE},
46 use_verbose=True, 53 use_verbose=True,
47 help=_("get list of registered events"), 54 extra_outputs={
55 "default": self.default_output,
56 },
57 help=_("get event(s) data"),
48 ) 58 )
49 59
50 def add_parser_options(self): 60 def add_parser_options(self):
51 pass 61 pass
52 62
53 async def start(self): 63 async def start(self):
54 try: 64 try:
55 events = await self.host.bridge.eventsList( 65 events_data_s = await self.host.bridge.eventsGet(
66 self.args.service,
67 self.args.node,
68 self.args.items,
69 self.getPubsubExtra(),
70 self.profile,
71 )
72 except Exception as e:
73 self.disp(f"can't get events data: {e}", error=True)
74 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
75 else:
76 events_data = data_format.deserialise(events_data_s, type_check=list)
77 await self.output(events_data)
78 self.host.quit()
79
80 def default_output(self, events):
81 nb_events = len(events)
82 for idx, event in enumerate(events):
83 names = event["name"]
84 name = names.get("") or next(iter(names.values()))
85 start = event["start"]
86 start_human = date_utils.date_fmt(
87 start, "medium", tz_info=date_utils.TZ_LOCAL
88 )
89 end = event["end"]
90 self.disp(A.color(
91 A.BOLD, start_human, A.RESET, " ",
92 f"({date_utils.delta2human(start, end)}) ",
93 C.A_HEADER, name
94 ))
95 if self.verbosity > 0:
96 descriptions = event.get("descriptions", [])
97 if descriptions:
98 self.disp(descriptions[0]["description"])
99 if idx < (nb_events-1):
100 self.disp("")
101
102
103 class CategoryAction(argparse.Action):
104
105 def __init__(self, option_strings, dest, nargs=None, metavar=None, **kwargs):
106 if nargs is not None or metavar is not None:
107 raise ValueError("nargs and metavar must not be used")
108 if metavar is not None:
109 metavar="TERM WIKIDATA_ID LANG"
110 if "--help" in sys.argv:
111 # FIXME: dirty workaround to have correct --help message
112 # argparse doesn't normally allow variable number of arguments beside "+"
113 # and "*", this workaround show METAVAR as 3 arguments were expected, while
114 # we can actuall use 1, 2 or 3.
115 nargs = 3
116 metavar = ("TERM", "[WIKIDATA_ID]", "[LANG]")
117 else:
118 nargs = "+"
119
120 super().__init__(option_strings, dest, metavar=metavar, nargs=nargs, **kwargs)
121
122 def __call__(self, parser, namespace, values, option_string=None):
123 categories = getattr(namespace, self.dest)
124 if categories is None:
125 categories = []
126 setattr(namespace, self.dest, categories)
127
128 if not values:
129 parser.error("category values must be set")
130
131 category = {
132 "term": values[0]
133 }
134
135 if len(values) == 1:
136 pass
137 elif len(values) == 2:
138 value = values[1]
139 if value.startswith("Q"):
140 category["wikidata_id"] = value
141 else:
142 category["language"] = value
143 elif len(values) == 3:
144 __, wd, lang = values
145 category["wikidata_id"] = wd
146 category["language"] = lang
147 else:
148 parser.error("Category can't have more than 3 arguments")
149
150 categories.append(category)
151
152
153 class EventBase:
154 def add_parser_options(self):
155 self.parser.add_argument(
156 "-S", "--start", type=base.date_decoder, metavar="TIME_PATTERN",
157 help=_("the start time of the event"))
158 end_group = self.parser.add_mutually_exclusive_group()
159 end_group.add_argument(
160 "-E", "--end", type=base.date_decoder, metavar="TIME_PATTERN",
161 help=_("the time of the end of the event"))
162 end_group.add_argument(
163 "-D", "--duration", help=_("duration of the event"))
164 self.parser.add_argument(
165 "-H", "--head-picture", help="URL to a picture to use as head-picture"
166 )
167 self.parser.add_argument(
168 "-d", "--description", help="plain text description the event"
169 )
170 self.parser.add_argument(
171 "-C", "--category", action=CategoryAction, dest="categories",
172 help="Category of the event"
173 )
174 self.parser.add_argument(
175 "-l", "--location", action="append", nargs="+", metavar="[KEY] VALUE",
176 help="Location metadata"
177 )
178 rsvp_group = self.parser.add_mutually_exclusive_group()
179 rsvp_group.add_argument(
180 "--rsvp", action="store_true", help=_("RSVP is requested"))
181 rsvp_group.add_argument(
182 "--rsvp_json", metavar="JSON", help=_("JSON description of RSVP form"))
183 for node_type in ("invitees", "comments", "blog", "schedule"):
184 self.parser.add_argument(
185 f"--{node_type}",
186 nargs=2,
187 metavar=("JID", "NODE"),
188 help=_("link {node_type} pubsub node").format(node_type=node_type)
189 )
190 self.parser.add_argument(
191 "-a", "--attachment", action="append", dest="attachments",
192 help=_("attach a file")
193 )
194 self.parser.add_argument("--website", help=_("website of the event"))
195 self.parser.add_argument(
196 "--status", choices=["confirmed", "tentative", "cancelled"],
197 help=_("status of the event")
198 )
199 self.parser.add_argument(
200 "-T", "--language", metavar="LANG", action="append", dest="languages",
201 help=_("main languages spoken at the event")
202 )
203 self.parser.add_argument(
204 "--wheelchair", choices=["full", "partial", "no"],
205 help=_("is the location accessible by wheelchair")
206 )
207 self.parser.add_argument(
208 "--external",
209 nargs=3,
210 metavar=("JID", "NODE", "ITEM"),
211 help=_("link to an external event")
212 )
213
214 def get_event_data(self):
215 if self.args.duration is not None:
216 if self.args.start is None:
217 self.parser.error("--start must be send if --duration is used")
218 # if duration is used, we simply add it to start time to get end time
219 self.args.end = base.date_decoder(f"{self.args.start} + {self.args.duration}")
220
221 event = {}
222 if self.args.name is not None:
223 event["name"] = {"": self.args.name}
224
225 if self.args.start is not None:
226 event["start"] = self.args.start
227
228 if self.args.end is not None:
229 event["end"] = self.args.end
230
231 if self.args.head_picture:
232 event["head-picture"] = {
233 "sources": [{
234 "url": self.args.head_picture
235 }]
236 }
237 if self.args.description:
238 event["descriptions"] = [
239 {
240 "type": "text",
241 "description": self.args.description
242 }
243 ]
244 if self.args.categories:
245 event["categories"] = self.args.categories
246 if self.args.location is not None:
247 location = {}
248 for location_data in self.args.location:
249 if len(location_data) == 1:
250 location["description"] = location_data[0]
251 else:
252 key, *values = location_data
253 location[key] = " ".join(values)
254 event["locations"] = [location]
255
256 if self.args.rsvp:
257 event["rsvp"] = [{}]
258 elif self.args.rsvp_json:
259 if isinstance(self.args.rsvp_elt, dict):
260 event["rsvp"] = [self.args.rsvp_json]
261 else:
262 event["rsvp"] = self.args.rsvp_json
263
264 for node_type in ("invitees", "comments", "blog", "schedule"):
265 value = getattr(self.args, node_type)
266 if value:
267 service, node = value
268 event[node_type] = {"service": service, "node": node}
269
270 if self.args.attachments:
271 attachments = event["attachments"] = []
272 for attachment in self.args.attachments:
273 attachments.append({
274 "sources": [{"url": attachment}]
275 })
276
277 extra = {}
278
279 for arg in ("website", "status", "languages"):
280 value = getattr(self.args, arg)
281 if value is not None:
282 extra[arg] = value
283 if self.args.wheelchair is not None:
284 extra["accessibility"] = {"wheelchair": self.args.wheelchair}
285
286 if extra:
287 event["extra"] = extra
288
289 if self.args.external:
290 ext_jid, ext_node, ext_item = self.args.external
291 event["external"] = {
292 "jid": ext_jid,
293 "node": ext_node,
294 "item": ext_item
295 }
296 return event
297
298
299 class Create(EventBase, base.CommandBase):
300 def __init__(self, host):
301 super().__init__(
302 host,
303 "create",
304 use_pubsub=True,
305 help=_("create or replace event"),
306 )
307
308 def add_parser_options(self):
309 super().add_parser_options()
310 self.parser.add_argument(
311 "-i",
312 "--id",
313 default="",
314 help=_("ID of the PubSub Item"),
315 )
316 # name is mandatory here
317 self.parser.add_argument("name", help=_("name of the event"))
318
319 async def start(self):
320 if self.args.start is None:
321 self.parser.error("--start must be set")
322 event_data = self.get_event_data()
323 # we check self.args.end after get_event_data because it may be set there id
324 # --duration is used
325 if self.args.end is None:
326 self.parser.error("--end or --duration must be set")
327 try:
328 await self.host.bridge.eventCreate(
329 data_format.serialise(event_data),
330 self.args.id,
331 self.args.node,
332 self.args.service,
333 self.profile,
334 )
335 except Exception as e:
336 self.disp(f"can't create event: {e}", error=True)
337 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
338 else:
339 self.disp(_("Event created successfuly)"))
340 self.host.quit()
341
342
343 class Modify(EventBase, base.CommandBase):
344 def __init__(self, host):
345 super(Modify, self).__init__(
346 host,
347 "modify",
348 use_pubsub=True,
349 pubsub_flags={C.SINGLE_ITEM},
350 help=_("modify an existing event"),
351 )
352 EventBase.__init__(self)
353
354 def add_parser_options(self):
355 super().add_parser_options()
356 # name is optional here
357 self.parser.add_argument("-N", "--name", help=_("name of the event"))
358
359 async def start(self):
360 event_data = self.get_event_data()
361 try:
362 await self.host.bridge.eventModify(
363 data_format.serialise(event_data),
364 self.args.item,
56 self.args.service, 365 self.args.service,
57 self.args.node, 366 self.args.node,
58 self.profile, 367 self.profile,
59 ) 368 )
60 except Exception as e: 369 except Exception as e:
61 self.disp(f"can't get list of events: {e}", error=True) 370 self.disp(f"can't update event data: {e}", error=True)
62 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 371 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
63 else: 372 else:
64 await self.output(events)
65 self.host.quit() 373 self.host.quit()
66 374
67 375
68 class Get(base.CommandBase): 376 class InviteeGet(base.CommandBase):
69 def __init__(self, host): 377 def __init__(self, host):
70 base.CommandBase.__init__( 378 base.CommandBase.__init__(
71 self, 379 self,
72 host, 380 host,
73 "get", 381 "get",
74 use_output=C.OUTPUT_DICT, 382 use_output=C.OUTPUT_DICT,
75 use_pubsub=True, 383 use_pubsub=True,
76 pubsub_flags={C.SINGLE_ITEM}, 384 pubsub_flags={C.SINGLE_ITEM},
77 use_verbose=True, 385 use_verbose=True,
78 help=_("get event data"), 386 help=_("get event attendance"),
79 ) 387 )
80 388
81 def add_parser_options(self): 389 def add_parser_options(self):
82 pass 390 self.parser.add_argument(
391 "-j", "--jid", action="append", dest="jids", default=[],
392 help=_("only retrieve RSVP from those JIDs")
393 )
83 394
84 async def start(self): 395 async def start(self):
85 try: 396 try:
86 event_tuple = await self.host.bridge.eventGet( 397 event_data_s = await self.host.bridge.eventInviteeGet(
87 self.args.service, 398 self.args.service,
88 self.args.node, 399 self.args.node,
89 self.args.item, 400 self.args.item,
401 self.args.jids,
402 "",
90 self.profile, 403 self.profile,
91 ) 404 )
92 except Exception as e: 405 except Exception as e:
93 self.disp(f"can't get event data: {e}", error=True) 406 self.disp(f"can't get event data: {e}", error=True)
94 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 407 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
95 else: 408 else:
96 event_date, event_data = event_tuple 409 event_data = data_format.deserialise(event_data_s)
97 event_data["date"] = event_date
98 await self.output(event_data) 410 await self.output(event_data)
99 self.host.quit() 411 self.host.quit()
100 412
101 413
102 class EventBase(object): 414 class InviteeSet(base.CommandBase):
415 def __init__(self, host):
416 super(InviteeSet, self).__init__(
417 host,
418 "set",
419 use_pubsub=True,
420 pubsub_flags={C.SINGLE_ITEM},
421 help=_("set event attendance"),
422 )
423
103 def add_parser_options(self): 424 def add_parser_options(self):
104 self.parser.add_argument(
105 "-i",
106 "--id",
107 default="",
108 help=_("ID of the PubSub Item"),
109 )
110 self.parser.add_argument("-d", "--date", type=str, help=_("date of the event"))
111 self.parser.add_argument( 425 self.parser.add_argument(
112 "-f", 426 "-f",
113 "--field", 427 "--field",
114 action="append", 428 action="append",
115 nargs=2, 429 nargs=2,
116 dest="fields", 430 dest="fields",
117 metavar=("KEY", "VALUE"), 431 metavar=("KEY", "VALUE"),
118 help=_("configuration field to set"), 432 help=_("configuration field to set"),
119 ) 433 )
120 434
121 def parseFields(self):
122 return dict(self.args.fields) if self.args.fields else {}
123
124 def parseDate(self):
125 if self.args.date:
126 try:
127 date = int(self.args.date)
128 except ValueError:
129 try:
130 date_time = du_parser.parse(
131 self.args.date, dayfirst=not ("-" in self.args.date)
132 )
133 except ValueError as e:
134 self.parser.error(_("Can't parse date: {msg}").format(msg=e))
135 if date_time.tzinfo is None:
136 date = calendar.timegm(date_time.timetuple())
137 else:
138 date = time.mktime(date_time.timetuple())
139 else:
140 date = -1
141 return date
142
143
144 class Create(EventBase, base.CommandBase):
145 def __init__(self, host):
146 super(Create, self).__init__(
147 host,
148 "create",
149 use_pubsub=True,
150 help=_("create or replace event"),
151 )
152 EventBase.__init__(self)
153
154 async def start(self): 435 async def start(self):
155 fields = self.parseFields() 436 # TODO: handle RSVP with XMLUI in a similar way as for `ad-hoc run`
156 date = self.parseDate()
157 try:
158 node = await self.host.bridge.eventCreate(
159 date,
160 fields,
161 self.args.service,
162 self.args.node,
163 self.args.id,
164 self.profile,
165 )
166 except Exception as e:
167 self.disp(f"can't create event: {e}", error=True)
168 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
169 else:
170 self.disp(_("Event created successfuly on node {node}").format(node=node))
171 self.host.quit()
172
173
174 class Modify(EventBase, base.CommandBase):
175 def __init__(self, host):
176 super(Modify, self).__init__(
177 host,
178 "modify",
179 use_pubsub=True,
180 pubsub_flags={C.NODE},
181 help=_("modify an existing event"),
182 )
183 EventBase.__init__(self)
184
185 async def start(self):
186 fields = self.parseFields()
187 date = 0 if not self.args.date else self.parseDate()
188 try:
189 self.host.bridge.eventModify(
190 self.args.service,
191 self.args.node,
192 self.args.id,
193 date,
194 fields,
195 self.profile,
196 )
197 except Exception as e:
198 self.disp(f"can't update event data: {e}", error=True)
199 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
200 else:
201 self.host.quit()
202
203
204 class InviteeGet(base.CommandBase):
205 def __init__(self, host):
206 base.CommandBase.__init__(
207 self,
208 host,
209 "get",
210 use_output=C.OUTPUT_DICT,
211 use_pubsub=True,
212 pubsub_flags={C.NODE},
213 use_verbose=True,
214 help=_("get event attendance"),
215 )
216
217 def add_parser_options(self):
218 self.parser.add_argument(
219 "-j", "--jid", default="", help=_("bare jid of the invitee")
220 )
221
222 async def start(self):
223 try:
224 event_data = await self.host.bridge.eventInviteeGet(
225 self.args.service,
226 self.args.node,
227 self.args.jid,
228 self.profile,
229 )
230 except Exception as e:
231 self.disp(f"can't get event data: {e}", error=True)
232 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
233 else:
234 await self.output(event_data)
235 self.host.quit()
236
237
238 class InviteeSet(base.CommandBase):
239 def __init__(self, host):
240 super(InviteeSet, self).__init__(
241 host,
242 "set",
243 use_output=C.OUTPUT_DICT,
244 use_pubsub=True,
245 pubsub_flags={C.NODE},
246 help=_("set event attendance"),
247 )
248
249 def add_parser_options(self):
250 self.parser.add_argument(
251 "-f",
252 "--field",
253 action="append",
254 nargs=2,
255 dest="fields",
256 metavar=("KEY", "VALUE"),
257 help=_("configuration field to set"),
258 )
259
260 async def start(self):
261 fields = dict(self.args.fields) if self.args.fields else {} 437 fields = dict(self.args.fields) if self.args.fields else {}
262 try: 438 try:
263 self.host.bridge.eventInviteeSet( 439 self.host.bridge.eventInviteeSet(
264 self.args.service, 440 self.args.service,
265 self.args.node, 441 self.args.node,
266 fields, 442 self.args.item,
443 data_format.serialise(fields),
267 self.profile, 444 self.profile,
268 ) 445 )
269 except Exception as e: 446 except Exception as e:
270 self.disp(f"can't set event data: {e}", error=True) 447 self.disp(f"can't set event data: {e}", error=True)
271 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 448 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
568 host, "invitee", use_profile=False, help=_("manage invities") 745 host, "invitee", use_profile=False, help=_("manage invities")
569 ) 746 )
570 747
571 748
572 class Event(base.CommandBase): 749 class Event(base.CommandBase):
573 subcommands = (List, Get, Create, Modify, Invitee) 750 subcommands = (Get, Create, Modify, Invitee)
574 751
575 def __init__(self, host): 752 def __init__(self, host):
576 super(Event, self).__init__( 753 super(Event, self).__init__(
577 host, "event", use_profile=False, help=_("event management") 754 host, "event", use_profile=False, help=_("event management")
578 ) 755 )