changeset 2396:66baa687c682

plugins tickets import, jp (ticket/import): implemented mapping: - mapping is an option allowing do map imported field to a specific field in the newly created item - importers can now use complexe data types in options using JSON
author Goffi <goffi@goffi.org>
date Fri, 27 Oct 2017 17:58:05 +0200 (2017-10-27)
parents 713cedc99752
children 7fff98d64ab5
files frontends/src/jp/cmd_ticket.py src/plugins/plugin_blog_import.py src/plugins/plugin_import.py src/plugins/plugin_tickets_import.py
diffstat 4 files changed, 53 insertions(+), 5 deletions(-) [+]
line wrap: on
line diff
--- a/frontends/src/jp/cmd_ticket.py	Fri Oct 27 17:54:00 2017 +0200
+++ b/frontends/src/jp/cmd_ticket.py	Fri Oct 27 17:58:05 2017 +0200
@@ -24,7 +24,7 @@
 
 __commands__ = ["Ticket"]
 
-FIELDS_MAP = u'fields_map'
+FIELDS_MAP = u'mapping'
 
 
 class Import(base.CommandAnswering):
--- a/src/plugins/plugin_blog_import.py	Fri Oct 27 17:54:00 2017 +0200
+++ b/src/plugins/plugin_blog_import.py	Fri Oct 27 17:58:05 2017 +0200
@@ -54,6 +54,7 @@
 
 class BlogImportPlugin(object):
     BOOL_OPTIONS = (OPT_UPLOAD_IMAGES, OPT_IGNORE_TLS)
+    JSON_OPTIONS = ()
     OPT_DEFAULTS = {OPT_UPLOAD_IMAGES: True,
                     OPT_IGNORE_TLS: False}
 
--- a/src/plugins/plugin_import.py	Fri Oct 27 17:54:00 2017 +0200
+++ b/src/plugins/plugin_import.py	Fri Oct 27 17:58:05 2017 +0200
@@ -26,6 +26,7 @@
 from functools import partial
 import collections
 import uuid
+import json
 
 
 PLUGIN_INFO = {
@@ -110,6 +111,11 @@
                 options[option] = C.bool(options[option])
             except KeyError:
                 pass
+        for option in import_handler.JSON_OPTIONS:
+            try:
+                options[option] = json.loads(options[option])
+            except ValueError:
+                raise exceptions.DataError(_(u'invalid json option: {name}').format(name=option))
         return self.doImport(client, import_handler, unicode(name), unicode(location), options, pubsub_service or None, pubsub_node or None)
 
     @defer.inlineCallbacks
--- a/src/plugins/plugin_tickets_import.py	Fri Oct 27 17:54:00 2017 +0200
+++ b/src/plugins/plugin_tickets_import.py	Fri Oct 27 17:58:05 2017 +0200
@@ -24,6 +24,7 @@
 log = getLogger(__name__)
 from twisted.internet import defer
 from sat.tools.common import uri
+from sat.tools import utils
 
 
 PLUGIN_INFO = {
@@ -37,11 +38,16 @@
 This plugin manage the different tickets importers which can register to it, and handle generic importing tasks.""")
 }
 
+OPT_MAPPING = 'mapping'
+FIELDS_LIST = (u'labels', u'cc_emails')  # fields which must have a list as value
+FIELDS_DATE = (u'created', u'updated')
+
 NS_TICKETS = 'org.salut-a-toi.tickets:0'
 
 
 class TicketsImportPlugin(object):
     BOOL_OPTIONS = ()
+    JSON_OPTIONS = (OPT_MAPPING,)
     OPT_DEFAULTS = {}
 
     def __init__(self, host):
@@ -59,19 +65,21 @@
 
         @param item_import_data(dict): no key is mandatory, but if a key doesn't exists in dest form, it will be ignored.
             Following names are recommendations which should be used where suitable in importers.
+            except if specified in description, values are unicode
             'id': unique id (must be unique in the node) of the ticket
             'title': title (or short description/summary) of the ticket
             'body': main description of the ticket
-            'creation': date of creation
-            'update': date of last update
+            'created': date of creation (unix time)
+            'updated': date of last update (unix time)
             'reporter': full name of reporter
             'reporter_jid': jid of reporter
             'reporter_email': email of reporter
             'assigned_to_name': full name of person working on it
             'assigned_to_email': email of person working on it
-            'cc_emails': iterable of emails subscribed to the ticket
+            'cc_emails': list of emails subscribed to the ticket
             'priority': priority of the ticket
             'severity': severity of the ticket
+            'labels': list of unicode values to use as label
             'product': product concerned by this ticket
             'component': part of the product concerned by this ticket
             'version': version of the product/component concerned by this ticket
@@ -84,9 +92,25 @@
                 - "closed": ticket is finished or invalid
             'milestone': target milestone for this ticket
             'comments': list of microblog data (comment metadata, check [XEP_0277.send] data argument)
+        @param options(dict, None): Below are the generic options,
+            tickets importer can have specific ones. All options are serialized unicode values
+            generic options:
+                - OPT_MAPPING (json): dict of imported ticket key => exported ticket key
+                    e.g.: if you want to map "component" to "labels", you can specify:
+                        {'component': 'labels'}
+                    If you specify several import ticket key to the same dest key,
+                    the values will be joined with line feeds
         """
         if 'comments_uri' in item_import_data:
             raise exceptions.DataError(_(u'comments_uri key will be generated and must not be used by importer'))
+        for key in FIELDS_LIST:
+            if not isinstance(item_import_data.get(key, []), list):
+                raise exceptions.DataError(_(u'{key} must be a list').format(key=key))
+        for key in FIELDS_DATE:
+            try:
+                item_import_data[key] = utils.xmpp_date(item_import_data[key])
+            except KeyError:
+                continue
         if session[u'root_node'] is None:
             session[u'root_node'] = NS_TICKETS
         if not 'schema' in session:
@@ -123,4 +147,21 @@
         return self._s.sendDataFormItem(client, service, node, ticket_data, session['schema'], id_)
 
     def itemFilters(self, client, ticket_data, session, options):
-        return None
+        mapping = options.get(OPT_MAPPING)
+        if mapping is not None:
+            if not isinstance(mapping, dict):
+                raise exceptions.DataError(_(u'mapping option must be a dictionary'))
+
+            for source, dest in mapping.iteritems():
+                if not isinstance(source, unicode) or not isinstance(dest, unicode):
+                    raise exceptions.DataError(_(u'keys and values of mapping must be sources and destinations ticket fields'))
+                if source in ticket_data:
+                    value = ticket_data.pop(source)
+                    if dest in FIELDS_LIST:
+                        values = ticket_data[dest] = ticket_data.get(dest, [])
+                        values.append(value)
+                    else:
+                        if dest in ticket_data:
+                            ticket_data[dest] = ticket_data[dest] + u'\n' + value
+                        else:
+                            ticket_data[dest] = value