# HG changeset patch # User Goffi # Date 1612469121 -3600 # Node ID 8dc26e5edcd327172710dcb80385feda562213f9 # Parent b68346a52920fa59e6e0b8b46eb27b80542ac94c plugin tickets, merge_requests: renamed "tickets" feature to "lists": this feature is more generic than only "tickets" for technical stuff, thus the name "lists" seems more appropriate. diff -r b68346a52920 -r 8dc26e5edcd3 sat/plugins/plugin_misc_lists.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat/plugins/plugin_misc_lists.py Thu Feb 04 21:05:21 2021 +0100 @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2009-2020 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +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.core.log import getLogger + +log = getLogger(__name__) + +# XXX: this plugin was formely named "tickets", thus the namespace keeps this +# name +APP_NS_TICKETS = "org.salut-a-toi.tickets:0" + +PLUGIN_INFO = { + C.PI_NAME: _("Pubsub Lists"), + C.PI_IMPORT_NAME: "LISTS", + C.PI_TYPE: "EXP", + C.PI_PROTOCOLS: [], + C.PI_DEPENDENCIES: ["XEP-0060", "XEP-0346", "XEP-0277", "IDENTITY"], + C.PI_MAIN: "PubsubLists", + C.PI_HANDLER: "no", + C.PI_DESCRIPTION: _("""Pubsub lists management plugin"""), +} + + +class PubsubLists: + + def __init__(self, host): + log.info(_("Pubsub lists plugin initialization")) + self.host = host + self._s = self.host.plugins["XEP-0346"] + self.namespace = self._s.getSubmittedNS(APP_NS_TICKETS) + host.registerNamespace("tickets", self.namespace) + self._p = self.host.plugins["XEP-0060"] + self._m = self.host.plugins["XEP-0277"] + host.bridge.addMethod( + "listGet", + ".plugin", + in_sign="ssiassa{ss}s", + out_sign="s", + method=lambda service, node, max_items, items_ids, sub_id, extra, profile_key: + self._s._get( + service, + node, + max_items, + items_ids, + sub_id, + extra, + default_node=self.namespace, + form_ns=APP_NS_TICKETS, + filters={ + "author": self._s.valueOrPublisherFilter, + "created": self._s.dateFilter, + "updated": self._s.dateFilter, + }, + profile_key=profile_key), + async_=True, + ) + host.bridge.addMethod( + "listSet", + ".plugin", + in_sign="ssa{sas}ssss", + out_sign="s", + method=self._set, + async_=True, + ) + host.bridge.addMethod( + "listSchemaGet", + ".plugin", + in_sign="sss", + out_sign="s", + method=lambda service, nodeIdentifier, profile_key: self._s._getUISchema( + service, nodeIdentifier, default_node=self.namespace, + profile_key=profile_key), + async_=True, + ) + + def _set(self, service, node, values, schema=None, item_id=None, extra='', + profile_key=C.PROF_KEY_NONE): + client, service, node, schema, item_id, extra = self._s.prepareBridgeSet( + service, node, schema, item_id, extra, profile_key + ) + d = defer.ensureDeferred(self.set( + client, service, node, values, schema, item_id, extra, deserialise=True + )) + 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): + """Publish a tickets + + @param node(unicode, None): Pubsub node to use + None to use default tickets node + @param values(dict[key(unicode), [iterable[object]|object]]): values of the ticket + + if value is not iterable, it will be put in a list + 'created' and 'updated' will be forced to current time: + - 'created' is set if item_id is None, i.e. if it's a new ticket + - 'updated' is set everytime + @param extra(dict, None): same as for [XEP-0060.sendItem] with additional keys: + - update(bool): if True, get previous item data to merge with current one + if True, item_id must be None + other arguments are same as for [self._s.sendDataFormItem] + @return (unicode): id of the created item + """ + if not node: + node = self.namespace + + if not item_id: + comments_service = await self._m.getCommentsService(client, service) + + # we need to use uuid for comments node, because we don't know item id in + # advance (we don't want to set it ourselves to let the server choose, so we + # can have a nicer id if serial ids is activated) + comments_node = self._m.getCommentsNode( + node + "_" + str(shortuuid.uuid()) + ) + options = { + self._p.OPT_ACCESS_MODEL: self._p.ACCESS_OPEN, + self._p.OPT_PERSIST_ITEMS: 1, + self._p.OPT_MAX_ITEMS: -1, + self._p.OPT_DELIVER_PAYLOADS: 1, + self._p.OPT_SEND_ITEM_SUBSCRIBE: 1, + self._p.OPT_PUBLISH_MODEL: self._p.ACCESS_OPEN, + } + await self._p.createNode(client, comments_service, comments_node, options) + values["comments_uri"] = uri.buildXMPPUri( + "pubsub", + subtype="microblog", + path=comments_service.full(), + node=comments_node, + ) + + return await self._s.set( + client, service, node, values, schema, item_id, extra, deserialise, form_ns + ) diff -r b68346a52920 -r 8dc26e5edcd3 sat/plugins/plugin_misc_merge_requests.py --- a/sat/plugins/plugin_misc_merge_requests.py Thu Feb 04 21:07:49 2021 +0100 +++ b/sat/plugins/plugin_misc_merge_requests.py Thu Feb 04 21:05:21 2021 +0100 @@ -36,7 +36,7 @@ C.PI_IMPORT_NAME: "MERGE_REQUESTS", C.PI_TYPE: "EXP", C.PI_PROTOCOLS: [], - C.PI_DEPENDENCIES: ["XEP-0060", "XEP-0346", "TICKETS", "TEXT_SYNTAXES"], + C.PI_DEPENDENCIES: ["XEP-0060", "XEP-0346", "LISTS", "TEXT_SYNTAXES"], C.PI_MAIN: "MergeRequests", C.PI_HANDLER: "no", C.PI_DESCRIPTION: _("""Merge requests management plugin""") @@ -72,7 +72,7 @@ self.namespace = self._s.getSubmittedNS(APP_NS_MERGE_REQUESTS) host.registerNamespace('merge_requests', self.namespace) self._p = self.host.plugins["XEP-0060"] - self._t = self.host.plugins["TICKETS"] + self._t = self.host.plugins["LISTS"] self._handlers = {} self._handlers_list = [] # handlers sorted by priority self._type_handlers = {} # data type => handler map diff -r b68346a52920 -r 8dc26e5edcd3 sat/plugins/plugin_misc_tickets.py --- a/sat/plugins/plugin_misc_tickets.py Thu Feb 04 21:07:49 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,152 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright (C) 2009-2020 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from sat.core.i18n import _ -from sat.core.constants import Const as C -from twisted.internet import defer -from sat.tools.common import uri -import shortuuid -from sat.core.log import getLogger - -log = getLogger(__name__) - -APP_NS_TICKETS = "org.salut-a-toi.tickets:0" - -PLUGIN_INFO = { - C.PI_NAME: _("Tickets management"), - C.PI_IMPORT_NAME: "TICKETS", - C.PI_TYPE: "EXP", - C.PI_PROTOCOLS: [], - C.PI_DEPENDENCIES: ["XEP-0060", "XEP-0346", "XEP-0277", "IDENTITY"], - C.PI_MAIN: "Tickets", - C.PI_HANDLER: "no", - C.PI_DESCRIPTION: _("""Tickets management plugin"""), -} - - -class Tickets: - - def __init__(self, host): - log.info(_("Tickets plugin initialization")) - self.host = host - self._s = self.host.plugins["XEP-0346"] - self.namespace = self._s.getSubmittedNS(APP_NS_TICKETS) - host.registerNamespace("tickets", self.namespace) - self._p = self.host.plugins["XEP-0060"] - self._m = self.host.plugins["XEP-0277"] - host.bridge.addMethod( - "ticketsGet", - ".plugin", - in_sign="ssiassa{ss}s", - out_sign="s", - method=lambda service, node, max_items, items_ids, sub_id, extra, profile_key: - self._s._get( - service, - node, - max_items, - items_ids, - sub_id, - extra, - default_node=self.namespace, - form_ns=APP_NS_TICKETS, - filters={ - "author": self._s.valueOrPublisherFilter, - "created": self._s.dateFilter, - "updated": self._s.dateFilter, - }, - profile_key=profile_key), - async_=True, - ) - host.bridge.addMethod( - "ticketSet", - ".plugin", - in_sign="ssa{sas}ssss", - out_sign="s", - method=self._set, - async_=True, - ) - host.bridge.addMethod( - "ticketsSchemaGet", - ".plugin", - in_sign="sss", - out_sign="s", - method=lambda service, nodeIdentifier, profile_key: self._s._getUISchema( - service, nodeIdentifier, default_node=self.namespace, - profile_key=profile_key), - async_=True, - ) - - def _set(self, service, node, values, schema=None, item_id=None, extra='', - profile_key=C.PROF_KEY_NONE): - client, service, node, schema, item_id, extra = self._s.prepareBridgeSet( - service, node, schema, item_id, extra, profile_key - ) - d = defer.ensureDeferred(self.set( - client, service, node, values, schema, item_id, extra, deserialise=True - )) - 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): - """Publish a tickets - - @param node(unicode, None): Pubsub node to use - None to use default tickets node - @param values(dict[key(unicode), [iterable[object]|object]]): values of the ticket - - if value is not iterable, it will be put in a list - 'created' and 'updated' will be forced to current time: - - 'created' is set if item_id is None, i.e. if it's a new ticket - - 'updated' is set everytime - @param extra(dict, None): same as for [XEP-0060.sendItem] with additional keys: - - update(bool): if True, get previous item data to merge with current one - if True, item_id must be None - other arguments are same as for [self._s.sendDataFormItem] - @return (unicode): id of the created item - """ - if not node: - node = self.namespace - - if not item_id: - comments_service = await self._m.getCommentsService(client, service) - - # we need to use uuid for comments node, because we don't know item id in - # advance (we don't want to set it ourselves to let the server choose, so we - # can have a nicer id if serial ids is activated) - comments_node = self._m.getCommentsNode( - node + "_" + str(shortuuid.uuid()) - ) - options = { - self._p.OPT_ACCESS_MODEL: self._p.ACCESS_OPEN, - self._p.OPT_PERSIST_ITEMS: 1, - self._p.OPT_MAX_ITEMS: -1, - self._p.OPT_DELIVER_PAYLOADS: 1, - self._p.OPT_SEND_ITEM_SUBSCRIBE: 1, - self._p.OPT_PUBLISH_MODEL: self._p.ACCESS_OPEN, - } - await self._p.createNode(client, comments_service, comments_node, options) - values["comments_uri"] = uri.buildXMPPUri( - "pubsub", - subtype="microblog", - path=comments_service.full(), - node=comments_node, - ) - - return await self._s.set( - client, service, node, values, schema, item_id, extra, deserialise, form_ns - ) diff -r b68346a52920 -r 8dc26e5edcd3 sat_frontends/jp/cmd_list.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat_frontends/jp/cmd_list.py Thu Feb 04 21:05:21 2021 +0100 @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 + + +# jp: a SàT command line tool +# Copyright (C) 2009-2020 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +import json +import os +from sat.core.i18n import _ +from sat.tools.common import data_format +from sat_frontends.jp import common +from sat_frontends.jp.constants import Const as C +from . import base + +__commands__ = ["List"] + +FIELDS_MAP = "mapping" + + +class Get(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "get", + use_verbose=True, + use_pubsub=True, + pubsub_flags={C.MULTI_ITEMS}, + pubsub_defaults={"service": _("auto"), "node": _("auto")}, + use_output=C.OUTPUT_LIST_XMLUI, + help=_("get lists"), + ) + + def add_parser_options(self): + pass + + async def start(self): + await common.fill_well_known_uri(self, os.getcwd(), "tickets", meta_map={}) + try: + lists_data = data_format.deserialise( + await self.host.bridge.listGet( + self.args.service, + self.args.node, + self.args.max, + self.args.items, + "", + self.getPubsubExtra(), + self.profile, + ), + type_check=list + ) + except Exception as e: + self.disp(f"can't get lists: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.output(lists_data[0]) + self.host.quit(C.EXIT_OK) + + +class Import(base.CommandBase): + # TODO: factorize with blog/import + + def __init__(self, host): + super(Import, self).__init__( + host, + "import", + use_progress=True, + use_verbose=True, + help=_("import tickets from external software/dataset"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "importer", + nargs="?", + help=_("importer name, nothing to display importers list"), + ) + self.parser.add_argument( + "-o", + "--option", + action="append", + nargs=2, + default=[], + metavar=("NAME", "VALUE"), + help=_("importer specific options (see importer description)"), + ) + self.parser.add_argument( + "-m", + "--map", + action="append", + nargs=2, + default=[], + metavar=("IMPORTED_FIELD", "DEST_FIELD"), + help=_( + "specified field in import data will be put in dest field (default: use " + "same field name, or ignore if it doesn't exist)" + ), + ) + self.parser.add_argument( + "-s", + "--service", + default="", + metavar="PUBSUB_SERVICE", + help=_("PubSub service where the items must be uploaded (default: server)"), + ) + self.parser.add_argument( + "-n", + "--node", + default="", + metavar="PUBSUB_NODE", + help=_( + "PubSub node where the items must be uploaded (default: tickets' " + "defaults)" + ), + ) + self.parser.add_argument( + "location", + nargs="?", + help=_( + "importer data location (see importer description), nothing to show " + "importer description" + ), + ) + + async def onProgressStarted(self, metadata): + self.disp(_("Tickets upload started"), 2) + + async def onProgressFinished(self, metadata): + self.disp(_("Tickets uploaded successfully"), 2) + + async def onProgressError(self, error_msg): + self.disp(_(f"Error while uploading tickets: {error_msg}"), error=True) + + async def start(self): + if self.args.location is None: + # no location, the list of importer or description is requested + for name in ("option", "service", "node"): + if getattr(self.args, name): + self.parser.error( + _(f"{name} argument can't be used without location argument")) + if self.args.importer is None: + self.disp( + "\n".join( + [ + f"{name}: {desc}" + for name, desc in await self.host.bridge.ticketsImportList() + ] + ) + ) + else: + try: + short_desc, long_desc = await self.host.bridge.ticketsImportDesc( + self.args.importer + ) + except Exception as e: + self.disp(f"can't get importer description: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(f"{name}: {short_desc}\n\n{long_desc}") + self.host.quit() + else: + # we have a location, an import is requested + + if self.args.progress: + # we use a custom progress bar template as we want a counter + self.pbar_template = [ + _("Progress: "), ["Percentage"], " ", ["Bar"], " ", + ["Counter"], " ", ["ETA"] + ] + + options = {key: value for key, value in self.args.option} + fields_map = dict(self.args.map) + if fields_map: + if FIELDS_MAP in options: + self.parser.error( + _("fields_map must be specified either preencoded in --option or " + "using --map, but not both at the same time") + ) + options[FIELDS_MAP] = json.dumps(fields_map) + + try: + progress_id = await self.host.bridge.ticketsImport( + self.args.importer, + self.args.location, + options, + self.args.service, + self.args.node, + self.profile, + ) + except Exception as e: + self.disp( + _(f"Error while trying to import tickets: {e}"), + error=True, + ) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.set_progress_id(progress_id) + + +class List(base.CommandBase): + subcommands = (Get, Import) + + def __init__(self, host): + super(List, self).__init__( + host, "list", use_profile=False, help=_("pubsub lists handling") + ) diff -r b68346a52920 -r 8dc26e5edcd3 sat_frontends/jp/cmd_ticket.py --- a/sat_frontends/jp/cmd_ticket.py Thu Feb 04 21:07:49 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,220 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SàT command line tool -# Copyright (C) 2009-2020 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - - -import json -import os -from sat.core.i18n import _ -from sat.tools.common import data_format -from sat_frontends.jp import common -from sat_frontends.jp.constants import Const as C -from . import base - -__commands__ = ["Ticket"] - -FIELDS_MAP = "mapping" - - -class Get(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "get", - use_verbose=True, - use_pubsub=True, - pubsub_flags={C.MULTI_ITEMS}, - pubsub_defaults={"service": _("auto"), "node": _("auto")}, - use_output=C.OUTPUT_LIST_XMLUI, - help=_("get tickets"), - ) - - def add_parser_options(self): - pass - - async def start(self): - await common.fill_well_known_uri(self, os.getcwd(), "tickets", meta_map={}) - try: - tickets_data = data_format.deserialise( - await self.host.bridge.ticketsGet( - self.args.service, - self.args.node, - self.args.max, - self.args.items, - "", - self.getPubsubExtra(), - self.profile, - ), - type_check=list - ) - except Exception as e: - self.disp(f"can't get tickets: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - await self.output(tickets_data[0]) - self.host.quit(C.EXIT_OK) - - -class Import(base.CommandBase): - # TODO: factorize with blog/import - - def __init__(self, host): - super(Import, self).__init__( - host, - "import", - use_progress=True, - use_verbose=True, - help=_("import tickets from external software/dataset"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "importer", - nargs="?", - help=_("importer name, nothing to display importers list"), - ) - self.parser.add_argument( - "-o", - "--option", - action="append", - nargs=2, - default=[], - metavar=("NAME", "VALUE"), - help=_("importer specific options (see importer description)"), - ) - self.parser.add_argument( - "-m", - "--map", - action="append", - nargs=2, - default=[], - metavar=("IMPORTED_FIELD", "DEST_FIELD"), - help=_( - "specified field in import data will be put in dest field (default: use " - "same field name, or ignore if it doesn't exist)" - ), - ) - self.parser.add_argument( - "-s", - "--service", - default="", - metavar="PUBSUB_SERVICE", - help=_("PubSub service where the items must be uploaded (default: server)"), - ) - self.parser.add_argument( - "-n", - "--node", - default="", - metavar="PUBSUB_NODE", - help=_( - "PubSub node where the items must be uploaded (default: tickets' " - "defaults)" - ), - ) - self.parser.add_argument( - "location", - nargs="?", - help=_( - "importer data location (see importer description), nothing to show " - "importer description" - ), - ) - - async def onProgressStarted(self, metadata): - self.disp(_("Tickets upload started"), 2) - - async def onProgressFinished(self, metadata): - self.disp(_("Tickets uploaded successfully"), 2) - - async def onProgressError(self, error_msg): - self.disp(_(f"Error while uploading tickets: {error_msg}"), error=True) - - async def start(self): - if self.args.location is None: - # no location, the list of importer or description is requested - for name in ("option", "service", "node"): - if getattr(self.args, name): - self.parser.error( - _(f"{name} argument can't be used without location argument")) - if self.args.importer is None: - self.disp( - "\n".join( - [ - f"{name}: {desc}" - for name, desc in await self.host.bridge.ticketsImportList() - ] - ) - ) - else: - try: - short_desc, long_desc = await self.host.bridge.ticketsImportDesc( - self.args.importer - ) - except Exception as e: - self.disp(f"can't get importer description: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp(f"{name}: {short_desc}\n\n{long_desc}") - self.host.quit() - else: - # we have a location, an import is requested - - if self.args.progress: - # we use a custom progress bar template as we want a counter - self.pbar_template = [ - _("Progress: "), ["Percentage"], " ", ["Bar"], " ", - ["Counter"], " ", ["ETA"] - ] - - options = {key: value for key, value in self.args.option} - fields_map = dict(self.args.map) - if fields_map: - if FIELDS_MAP in options: - self.parser.error( - _("fields_map must be specified either preencoded in --option or " - "using --map, but not both at the same time") - ) - options[FIELDS_MAP] = json.dumps(fields_map) - - try: - progress_id = await self.host.bridge.ticketsImport( - self.args.importer, - self.args.location, - options, - self.args.service, - self.args.node, - self.profile, - ) - except Exception as e: - self.disp( - _(f"Error while trying to import tickets: {e}"), - error=True, - ) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - await self.set_progress_id(progress_id) - - -class Ticket(base.CommandBase): - subcommands = (Get, Import) - - def __init__(self, host): - super(Ticket, self).__init__( - host, "ticket", use_profile=False, help=_("tickets handling") - )