comparison sat/plugins/plugin_tickets_import.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents src/plugins/plugin_tickets_import.py@0046283a285d
children 56f94936df1e
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # SàT plugin for import external ticketss
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
6
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
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
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/>.
19
20 from sat.core.i18n import _
21 from sat.core.constants import Const as C
22 from sat.core import exceptions
23 from sat.core.log import getLogger
24 log = getLogger(__name__)
25 from twisted.internet import defer
26 from sat.tools.common import uri
27 from sat.tools import utils
28
29
30 PLUGIN_INFO = {
31 C.PI_NAME: "tickets import",
32 C.PI_IMPORT_NAME: "TICKETS_IMPORT",
33 C.PI_TYPE: C.PLUG_TYPE_IMPORT,
34 C.PI_DEPENDENCIES: ["IMPORT", "XEP-0060", "XEP-0277", "PUBSUB_SCHEMA"],
35 C.PI_MAIN: "TicketsImportPlugin",
36 C.PI_HANDLER: "no",
37 C.PI_DESCRIPTION: _(u"""Tickets import management:
38 This plugin manage the different tickets importers which can register to it, and handle generic importing tasks.""")
39 }
40
41 OPT_MAPPING = 'mapping'
42 FIELDS_LIST = (u'labels', u'cc_emails') # fields which must have a list as value
43 FIELDS_DATE = (u'created', u'updated')
44
45 NS_TICKETS = 'org.salut-a-toi.tickets:0'
46
47
48 class TicketsImportPlugin(object):
49 BOOL_OPTIONS = ()
50 JSON_OPTIONS = (OPT_MAPPING,)
51 OPT_DEFAULTS = {}
52
53 def __init__(self, host):
54 log.info(_("plugin Tickets Import initialization"))
55 self.host = host
56 self._importers = {}
57 self._p = host.plugins['XEP-0060']
58 self._m = host.plugins['XEP-0277']
59 self._s = host.plugins['PUBSUB_SCHEMA']
60 host.plugins['IMPORT'].initialize(self, u'tickets')
61
62 @defer.inlineCallbacks
63 def importItem(self, client, item_import_data, session, options, return_data, service, node):
64 """
65
66 @param item_import_data(dict): no key is mandatory, but if a key doesn't exists in dest form, it will be ignored.
67 Following names are recommendations which should be used where suitable in importers.
68 except if specified in description, values are unicode
69 'id': unique id (must be unique in the node) of the ticket
70 'title': title (or short description/summary) of the ticket
71 'body': main description of the ticket
72 'created': date of creation (unix time)
73 'updated': date of last update (unix time)
74 'author': full name of reporter
75 'author_jid': jid of reporter
76 'author_email': email of reporter
77 'assigned_to_name': full name of person working on it
78 'assigned_to_email': email of person working on it
79 'cc_emails': list of emails subscribed to the ticket
80 'priority': priority of the ticket
81 'severity': severity of the ticket
82 'labels': list of unicode values to use as label
83 'product': product concerned by this ticket
84 'component': part of the product concerned by this ticket
85 'version': version of the product/component concerned by this ticket
86 'platform': platform converned by this ticket
87 'os': operating system concerned by this ticket
88 'status': current status of the ticket, values:
89 - "queued": ticket is waiting
90 - "started": progress is ongoing
91 - "review": ticket is fixed and waiting for review
92 - "closed": ticket is finished or invalid
93 'milestone': target milestone for this ticket
94 'comments': list of microblog data (comment metadata, check [XEP_0277.send] data argument)
95 @param options(dict, None): Below are the generic options,
96 tickets importer can have specific ones. All options are serialized unicode values
97 generic options:
98 - OPT_MAPPING (json): dict of imported ticket key => exported ticket key
99 e.g.: if you want to map "component" to "labels", you can specify:
100 {'component': 'labels'}
101 If you specify several import ticket key to the same dest key,
102 the values will be joined with line feeds
103 """
104 if 'comments_uri' in item_import_data:
105 raise exceptions.DataError(_(u'comments_uri key will be generated and must not be used by importer'))
106 for key in FIELDS_LIST:
107 if not isinstance(item_import_data.get(key, []), list):
108 raise exceptions.DataError(_(u'{key} must be a list').format(key=key))
109 for key in FIELDS_DATE:
110 try:
111 item_import_data[key] = utils.xmpp_date(item_import_data[key])
112 except KeyError:
113 continue
114 if session[u'root_node'] is None:
115 session[u'root_node'] = NS_TICKETS
116 if not 'schema' in session:
117 session['schema'] = yield self._s.getSchemaForm(client, service, node or session[u'root_node'])
118 defer.returnValue(item_import_data)
119
120 @defer.inlineCallbacks
121 def importSubItems(self, client, item_import_data, ticket_data, session, options):
122 # TODO: force "open" permission (except if private, check below)
123 # TODO: handle "private" metadata, to have non public access for node
124 # TODO: node access/publish model should be customisable
125 comments = ticket_data.get('comments', [])
126 service = yield self._m.getCommentsService(client)
127 node = self._m.getCommentsNode(session['root_node'] + u'_' + ticket_data['id'])
128 node_options = {self._p.OPT_ACCESS_MODEL: self._p.ACCESS_OPEN,
129 self._p.OPT_PERSIST_ITEMS: 1,
130 self._p.OPT_MAX_ITEMS: -1,
131 self._p.OPT_DELIVER_PAYLOADS: 1,
132 self._p.OPT_SEND_ITEM_SUBSCRIBE: 1,
133 self._p.OPT_PUBLISH_MODEL: self._p.ACCESS_OPEN,
134 }
135 yield self._p.createIfNewNode(client, service, node, options=node_options)
136 ticket_data['comments_uri'] = uri.buildXMPPUri(u'pubsub', subtype='microblog', path=service.full(), node=node)
137 for comment in comments:
138 if 'updated' not in comment and 'published' in comment:
139 # we don't want an automatic update date
140 comment['updated'] = comment['published']
141 yield self._m.send(client, comment, service, node)
142
143 def publishItem(self, client, ticket_data, service, node, session):
144 if node is None:
145 node = NS_TICKETS
146 id_ = ticket_data.pop('id', None)
147 log.debug(u"uploading item [{id}]: {title}".format(id=id_, title=ticket_data.get('title','')))
148 return self._s.sendDataFormItem(client, service, node, ticket_data, session['schema'], id_)
149
150 def itemFilters(self, client, ticket_data, session, options):
151 mapping = options.get(OPT_MAPPING)
152 if mapping is not None:
153 if not isinstance(mapping, dict):
154 raise exceptions.DataError(_(u'mapping option must be a dictionary'))
155
156 for source, dest in mapping.iteritems():
157 if not isinstance(source, unicode) or not isinstance(dest, unicode):
158 raise exceptions.DataError(_(u'keys and values of mapping must be sources and destinations ticket fields'))
159 if source in ticket_data:
160 value = ticket_data.pop(source)
161 if dest in FIELDS_LIST:
162 values = ticket_data[dest] = ticket_data.get(dest, [])
163 values.append(value)
164 else:
165 if dest in ticket_data:
166 ticket_data[dest] = ticket_data[dest] + u'\n' + value
167 else:
168 ticket_data[dest] = value