plugins schema, merge_requests, tickets*: factorisation

Dode common in plugins using schema have been factorised in pubsub schema plugin, and filters users in tickets handling have been renamed in a more generic way and put there too.
"reporter*" fields in tickets have been renamed to "author*" as it is a more generic term which can be used elsewhere.
The use of new utils.partial function make easy the creation of simple plugins using schema.
This commit is contained in:
Goffi 2018-01-12 15:58:54 +01:00
commit 3e76e826d0
5 changed files with 222 additions and 169 deletions

View file

@ -21,6 +21,7 @@ from sat.core.i18n import _
from sat.core import exceptions
from sat.core.constants import Const as C
from sat.tools import xml_tools
from sat.tools import utils
from twisted.words.protocols.jabber import jid
from twisted.words.protocols.jabber.xmlstream import XMPPHandler
from twisted.internet import defer
@ -41,7 +42,7 @@ PLUGIN_INFO = {
C.PI_IMPORT_NAME: "PUBSUB_SCHEMA",
C.PI_TYPE: "EXP",
C.PI_PROTOCOLS: [],
C.PI_DEPENDENCIES: ["XEP-0060"],
C.PI_DEPENDENCIES: ["XEP-0060", "IDENTITY"],
C.PI_MAIN: "PubsubSchema",
C.PI_HANDLER: "yes",
C.PI_DESCRIPTION: _("""Handle Pubsub data schemas""")
@ -54,6 +55,7 @@ class PubsubSchema(object):
log.info(_(u"PubSub Schema initialization"))
self.host = host
self._p = self.host.plugins["XEP-0060"]
self._i = self.host.plugins["IDENTITY"]
host.bridge.addMethod("psSchemaGet", ".plugin",
in_sign='sss', out_sign='s',
method=self._getSchema,
@ -66,7 +68,7 @@ class PubsubSchema(object):
)
host.bridge.addMethod("psSchemaUIGet", ".plugin",
in_sign='sss', out_sign='s',
method=self._getUISchema,
method=utils.partial(self._getUISchema, default_node=None),
async=True
)
host.bridge.addMethod("psItemsFormGet", ".plugin",
@ -163,7 +165,11 @@ class PubsubSchema(object):
xmlui = xml_tools.dataForm2XMLUI(form, '')
return xmlui
def _getUISchema(self, service, nodeIdentifier, profile_key=C.PROF_KEY_NONE):
def _getUISchema(self, service, nodeIdentifier, default_node=None, profile_key=C.PROF_KEY_NONE):
if not nodeIdentifier:
if not default_node:
raise ValueError(_(u"nodeIndentifier needs to be set"))
nodeIdentifier = default_node
client = self.host.getClient(profile_key)
service = None if not service else jid.JID(service)
d = self.getUISchema(client, service, nodeIdentifier)
@ -208,24 +214,30 @@ class PubsubSchema(object):
schema = None
max_items = None if max_items == C.NO_LIMIT else max_items
extra = self._p.parseExtra(extra_dict)
d = self.getDataFormItems(client, form_ns or None, service, node, schema, max_items or None, item_ids, sub_id or None, extra.rsm_request, extra.extra)
d = self.getDataFormItems(client, service, node, schema, max_items or None, item_ids, sub_id or None, extra.rsm_request, extra.extra, form_ns=form_ns or None)
d.addCallback(self._p.serItemsData)
return d
@defer.inlineCallbacks
def getDataFormItems(self, client, form_ns, service, nodeIdentifier, schema=None, max_items=None, item_ids=None, sub_id=None, rsm_request=None, extra=None, filters=None):
def getDataFormItems(self, client, service, nodeIdentifier, schema=None, max_items=None, item_ids=None, sub_id=None, rsm_request=None, extra=None, default_node=None, form_ns=None, filters=None):
"""Get items known as being data forms, and convert them to XMLUI
@param form_ns (unicode, None): namespace of the form
None to accept everything, even if form has no namespace
@param schema(domish.Element, data_form.Form, None): schema of the node if known
if None, it will be retrieved from node
@param default_node(unicode): node to use if nodeIdentifier is None or empty
@param form_ns (unicode, None): namespace of the form
None to accept everything, even if form has no namespace
@param filters(dict, None): same as for xml_tools.dataFormResult2XMLUI
other parameters as the same as for [getItems]
@return (list[unicode]): XMLUI of the forms
if an item is invalid (not corresponding to form_ns or not a data_form)
it will be skipped
@raise ValueError: one argument is invalid
"""
if not nodeIdentifier:
if not default_node:
raise ValueError(_(u"default_node must be set if nodeIdentifier is not set"))
nodeIdentifier = default_node
# we need the initial form to get options of fields when suitable
schema_form = yield self.getSchemaForm(client, service, nodeIdentifier, schema, form_type='result', copy_form=False)
items_data = yield self._p.getItems(client, service, nodeIdentifier, max_items, item_ids, sub_id, rsm_request, extra)
@ -306,6 +318,170 @@ class PubsubSchema(object):
yield self._p.sendItem(client, service, nodeIdentifier, form.toElement(), item_id, extra)
## filters ##
# filters useful for data form to XMLUI conversion #
def valueOrPublisherFilter(self, form_xmlui, widget_type, args, kwargs):
"""Replace missing value by publisher's user part"""
if not args[0]:
# value is not filled: we use user part of publisher (if we have it)
try:
publisher = jid.JID(form_xmlui.named_widgets['publisher'].value)
except (KeyError, RuntimeError):
pass
else:
args[0] = publisher.user.capitalize()
return widget_type, args, kwargs
def textbox2ListFilter(self, form_xmlui, widget_type, args, kwargs):
"""Split lines of a textbox in a list
main use case is using a textbox for labels
"""
if widget_type != u'textbox':
return widget_type, args, kwargs
widget_type = u'list'
options = [o for o in args.pop(0).split(u'\n') if o]
kwargs = {'options': options,
'name': kwargs.get('name'),
'styles': (u'noselect', u'extensible', u'reducible')}
return widget_type, args, kwargs
def dateFilter(self, form_xmlui, widget_type, args, kwargs):
"""Convert a string with a date to a unix timestamp"""
if widget_type != u'string' or not args[0]:
return widget_type, args, kwargs
# we convert XMPP date to timestamp
try:
args[0] = unicode(utils.date_parse(args[0]))
except Exception as e:
log.warning(_(u"Can't parse date field: {msg}").format(msg=e))
return widget_type, args, kwargs
## Helper methods ##
def prepareBridgeGet(self, service, node, max_items, sub_id, extra_dict, profile_key):
"""Parse arguments received from bridge *Get methods and return higher level data
@return (tuple): (client, service, node, max_items, extra, sub_id) usable for internal methods
"""
client = self.host.getClient(profile_key)
service = jid.JID(service) if service else None
if not node:
node = None
max_items = None if max_items == C.NO_LIMIT else max_items
if not sub_id:
sub_id = None
extra = self._p.parseExtra(extra_dict)
return client, service, node, max_items, extra, sub_id
def _get(self, service='', node='', max_items=10, item_ids=None, sub_id=None, extra_dict=None, default_node=None, form_ns=None, filters=None, profile_key=C.PROF_KEY_NONE):
"""Bridge method to retrieve data from node with schema
this method is a helper so dependant plugins can use it directly
when adding *Get methods
"""
client, service, node, max_items, extra, sub_id = self.prepareBridgeGet(service, node, max_items, sub_id, extra_dict, profile_key)
d = self.getDataFormItems(client, service, node or None,
max_items=max_items,
item_ids=item_ids,
sub_id=sub_id,
rsm_request=extra.rsm_request,
extra=extra.extra,
default_node=default_node,
form_ns=form_ns,
filters=filters)
d.addCallback(self._p.serItemsData)
return d
def prepareBridgeSet(self, service, node, schema, item_id, extra, profile_key):
"""Parse arguments received from bridge *Set methods and return higher level data
@return (tuple): (client, service, node, schema, item_id, extra) usable for internal methods
"""
client = self.host.getClient(profile_key)
service = None if not service else jid.JID(service)
if schema:
schema = generic.parseXml(schema.encode('utf-8'))
else:
schema = None
if extra and u'update' in extra:
extra[u'update'] = C.bool(extra[u'update'])
return client, service, node or None, schema, item_id or None, extra
def _set(self, service, node, values, schema=None, item_id=None, extra=None, default_node=None, form_ns=None, fill_author=True, profile_key=C.PROF_KEY_NONE):
"""Bridge method to set item in node with schema
this method is a helper so dependant plugins can use it directly
when adding *Set methods
"""
client, service, node, schema, item_id, extra = self.prepareBridgeSet(service, node, schema, item_id, extra)
d = self.set(client, service, node, values, schema, item_id, extra,
deserialise=True,
form_ns=form_ns,
default_node=default_node,
fill_author=fill_author)
d.addCallback(lambda ret: ret or u'')
return d
@defer.inlineCallbacks
def set(self, client, service, node, values, schema, item_id, extra, deserialise, form_ns, default_node=None, fill_author=True):
"""Set an item in a node with a schema
This method can be used directly by *Set methods added by dependant plugin
@param values(dict[key(unicode), [iterable[object], object]]): values of the items
if not iterable, 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
@param form_ns (unicode, None): namespace of the form
needed when an update is done
@param default_node(unicode, None): value to use if node is not set
other arguments are same as for [self._s.sendDataFormItem]
@return (unicode): id of the created item
"""
if not node:
if default_node is None:
raise ValueError(_(u"default_node must be set if node is not set"))
node = default_node
now = utils.xmpp_date()
if not item_id:
values['created'] = now
elif extra.get(u'update', False):
if item_id is None:
raise exceptions.DataError(_(u'if extra["update"] is set, item_id must be set too'))
try:
# we get previous item
items_data = yield self._p.getItems(client, service, node, item_ids=[item_id])
item_elt = items_data[0][0]
except Exception as e:
log.warning(_(u"Can't get previous item, update ignored: {reason}").format(
reason = e))
else:
# and parse it
form = data_form.findForm(item_elt, form_ns)
if form is None:
log.warning(_(u"Can't parse previous item, update ignored: data form not found").format(
reason = e))
else:
for name, field in form.fields.iteritems():
if name not in values:
values[name] = u'\n'.join(unicode(v) for v in field.values)
values['updated'] = now
if fill_author:
if not values.get('author'):
identity = yield self._i.getIdentity(client, client.jid)
values['author'] = identity['nick']
if not values.get('author_jid'):
values['author_jid'] = client.jid.full()
item_id = yield self.sendDataFormItem(client, service, node, values, schema, item_id, extra, deserialise)
defer.returnValue(item_id)
class SchemaHandler(XMPPHandler):
implements(iwokkel.IDisco)

View file

@ -20,10 +20,9 @@
from sat.core.i18n import _
from sat.core.constants import Const as C
from sat.core import exceptions
from twisted.words.protocols.jabber import jid
from twisted.internet import defer
from wokkel import generic
from collections import namedtuple
from sat.tools import utils
from sat.core.log import getLogger
log = getLogger(__name__)
@ -34,7 +33,7 @@ PLUGIN_INFO = {
C.PI_IMPORT_NAME: "MERGE_REQUESTS",
C.PI_TYPE: "EXP",
C.PI_PROTOCOLS: [],
C.PI_DEPENDENCIES: ["XEP-0060", "PUBSUB_SCHEMA", "TICKETS"],
C.PI_DEPENDENCIES: ["XEP-0060", "PUBSUB_SCHEMA"],
C.PI_MAIN: "MergeRequests",
C.PI_HANDLER: "no",
C.PI_DESCRIPTION: _("""Merge requests management plugin""")
@ -69,7 +68,6 @@ class MergeRequests(object):
host.registerNamespace('merge_requests', NS_MERGE_REQUESTS)
self._p = self.host.plugins["XEP-0060"]
self._s = self.host.plugins["PUBSUB_SCHEMA"]
self._t = self.host.plugins["TICKETS"]
self._handlers = {}
self._handlers_list = [] # handlers sorted by priority
self._type_handlers = {} # data type => handler map
@ -84,7 +82,7 @@ class MergeRequests(object):
async=True)
host.bridge.addMethod("mergeRequestsSchemaGet", ".plugin",
in_sign='sss', out_sign='s',
method=self._getSchema,
method=utils.partial(self._s._getUISchema, default_node=NS_MERGE_REQUESTS),
async=True)
host.bridge.addMethod("mergeRequestParseData", ".plugin",
in_sign='ss', out_sign='aa{ss}',
@ -127,12 +125,9 @@ class MergeRequests(object):
self._type_handlers[data_type] = self._handlers[name]
def _get(self, service='', node='', max_items=10, item_ids=None, sub_id=None, extra_dict=None, profile_key=C.PROF_KEY_NONE):
client = self.host.getClient(profile_key)
service = jid.JID(service) if service else None
max_items = None if max_items == C.NO_LIMIT else max_items
if extra_dict and 'parse' in extra_dict:
extra_dict['parse'] = C.bool(extra_dict['parse'])
extra = self._p.parseExtra(extra_dict)
client, service, node, max_items, extra, sub_id = self._s.prepareBridgeGet(service, node, max_items, sub_id, extra_dict, profile_key)
d = self.get(client, service, node or None, max_items, item_ids, sub_id or None, extra.rsm_request, extra.extra)
d.addCallback(lambda (tickets, metadata, parsed_patches): (
self._p.serItemsData((tickets, metadata)) +
@ -153,7 +148,16 @@ class MergeRequests(object):
"""
if not node:
node = NS_MERGE_REQUESTS
tickets_xmlui, metadata = yield self._t.get(client, service, node, max_items, item_ids, sub_id, rsm_request, extra, form_ns=NS_MERGE_REQUESTS)
tickets_xmlui, metadata = yield self._s.getDataFormItems(
client,
service,
node,
max_items=max_items,
item_ids=item_ids,
sub_id=sub_id,
rsm_request=rsm_request,
extra=extra,
form_ns=NS_MERGE_REQUESTS)
parsed_patches = []
if extra.get('parse', False):
for ticket in tickets_xmlui:
@ -164,14 +168,7 @@ class MergeRequests(object):
defer.returnValue((tickets_xmlui, metadata, parsed_patches))
def _set(self, service, node, repository, method, values, schema=None, item_id=None, extra=None, profile_key=C.PROF_KEY_NONE):
client = self.host.getClient(profile_key)
service = None if not service else jid.JID(service)
if extra and 'update' in extra:
extra['update'] = C.bool(extra['update'])
if schema:
schema = generic.parseXml(schema.encode('utf-8'))
else:
schema = None
client, service, schema, extra = self._s.prepareBridgeSet(service, node, schema, extra)
d = self.set(client, service, node or None, repository, method, values, schema, item_id or None, extra, deserialise=True)
d.addCallback(lambda ret: ret or u'')
return d
@ -188,9 +185,6 @@ class MergeRequests(object):
other arguments are same as for [TICKETS.set]
@return (unicode): id of the created item
"""
if not node:
node = NS_MERGE_REQUESTS
if values is None:
values = {}
@ -230,14 +224,9 @@ class MergeRequests(object):
values[FIELD_DATA] = data
item_id = yield self._t.set(client, service, node, values, schema, item_id, extra, deserialise, form_ns=NS_MERGE_REQUESTS)
item_id = yield self._t.set(client, service, node, values, schema, item_id, extra, deserialise, default_node=NS_MERGE_REQUESTS, form_ns=NS_MERGE_REQUESTS)
defer.returnValue(item_id)
def _getSchema(self, service, node, profile_key=C.PROF_KEY_NONE):
if not node:
node = NS_MERGE_REQUESTS
return self._s._getUISchema(service, node, profile_key)
def _parseData(self, data_type, data):
d = self.parseData(data_type, data)
d.addCallback(lambda parsed_patches:

View file

@ -19,15 +19,11 @@
from sat.core.i18n import _
from sat.core.constants import Const as C
from sat.core import exceptions
from twisted.words.protocols.jabber import jid
from twisted.internet import defer
from wokkel import generic
from sat.tools import utils
from sat.tools.common import uri
from sat.core.log import getLogger
from sat.tools import utils
import shortuuid
from wokkel import data_form
from sat.core.log import getLogger
log = getLogger(__name__)
NS_TICKETS = 'org.salut-a-toi.tickets:0'
@ -53,10 +49,18 @@ class Tickets(object):
self._p = self.host.plugins["XEP-0060"]
self._s = self.host.plugins["PUBSUB_SCHEMA"]
self._m = self.host.plugins["XEP-0277"]
self._i = self.host.plugins["IDENTITY"]
host.bridge.addMethod("ticketsGet", ".plugin",
in_sign='ssiassa{ss}s', out_sign='(asa{ss})',
method=self._get,
method=utils.partial(
self._s._get,
default_node=NS_TICKETS,
form_ns=NS_TICKETS,
filters = {u'author': self._s.valueOrPublisherFilter,
u'labels': self._s.textbox2ListFilter,
u'created': self._s.dateFilter,
u'updated': self._s.dateFilter,
}),
async=True
)
host.bridge.addMethod("ticketSet", ".plugin",
@ -65,93 +69,11 @@ class Tickets(object):
async=True)
host.bridge.addMethod("ticketsSchemaGet", ".plugin",
in_sign='sss', out_sign='s',
method=self._getSchema,
method=utils.partial(self._s._getUISchema, default_node=NS_TICKETS),
async=True)
def _reporterFilter(self, client, form_xmlui, widget_type, args, kwargs):
if not args[0]:
# if reporter is not filled, we use user part of publisher
# (if we have it)
try:
publisher = jid.JID(form_xmlui.named_widgets['publisher'].value)
except (KeyError, RuntimeError):
pass
else:
args[0] = publisher.user.capitalize()
return widget_type, args, kwargs
def _labelsFilter(self, form_xmlui, widget_type, args, kwargs):
if widget_type != u'textbox':
return widget_type, args, kwargs
widget_type = u'list'
options = [o for o in args.pop(0).split(u'\n') if o]
kwargs = {'options': options,
'name': kwargs.get('name'),
'styles': (u'noselect', u'extensible', u'reducible')}
return widget_type, args, kwargs
def _dateFilter(self, form_xmlui, widget_type, args, kwargs):
if widget_type != u'string' or not args[0]:
return widget_type, args, kwargs
# we convert XMPP date to timestamp
try:
args[0] = unicode(utils.date_parse(args[0]))
except Exception as e:
log.warning(_(u"Can't parse date field: {msg}").format(msg=e))
return widget_type, args, kwargs
def _get(self, service='', node='', max_items=10, item_ids=None, sub_id=None, extra_dict=None, profile_key=C.PROF_KEY_NONE):
client = self.host.getClient(profile_key)
service = jid.JID(service) if service else None
max_items = None if max_items == C.NO_LIMIT else max_items
extra = self._p.parseExtra(extra_dict)
d = self.get(client, service, node or None, max_items, item_ids, sub_id or None, extra.rsm_request, extra.extra)
d.addCallback(self._p.serItemsData)
return d
@defer.inlineCallbacks
def get(self, client, service=None, node=None, max_items=None, item_ids=None, sub_id=None, rsm_request=None, extra=None, form_ns=NS_TICKETS):
"""Retrieve tickets and convert them to XMLUI
@param service(None, jid.JID): Pubsub service to use
@param node(unicode, None): PubSub node to use
if None, default ticket node will be used
other parameters as the same as for [XEP_0060.getItems]
@return (tuple(list[unicode], dict[unicode, object])):
- XMLUI of the tickets
- metadata dict
"""
if not node:
node = NS_TICKETS
filters = {u'reporter': lambda *args: self._reporterFilter(client, *args),
u'labels': self._labelsFilter,
u'created': self._dateFilter,
u'updated': self._dateFilter,
}
tickets, metadata = yield self._s.getDataFormItems(
client,
form_ns,
service,
node,
max_items = max_items,
item_ids = item_ids,
sub_id = sub_id,
rsm_request = rsm_request,
extra = extra,
filters = filters,
)
defer.returnValue((tickets, metadata))
def _set(self, service, node, values, schema=None, item_id=None, extra=None, profile_key=C.PROF_KEY_NONE):
client = self.host.getClient(profile_key)
service = None if not service else jid.JID(service)
if schema:
schema = generic.parseXml(schema.encode('utf-8'))
else:
schema = None
if extra and u'update' in extra:
extra[u'update'] = C.bool(extra[u'update'])
client, service, schema, extra = self._s.prepareBridgeSet(service, node, schema, item_id, extra)
d = self.set(client, service, node or None, values, schema, item_id or None, extra, deserialise=True)
d.addCallback(lambda ret: ret or u'')
return d
@ -175,9 +97,7 @@ class Tickets(object):
"""
if not node:
node = NS_TICKETS
now = utils.xmpp_date()
if not item_id:
values['created'] = now
comments_service = yield self._m.getCommentsService(client, service)
# we need to use uuid for comments node, because we don't know item id in advance
@ -193,37 +113,5 @@ class Tickets(object):
}
yield self._p.createNode(client, comments_service, comments_node, options)
values['comments_uri'] = uri.buildXMPPUri(u'pubsub', subtype='microblog', path=comments_service.full(), node=comments_node)
elif extra.get(u'update', False):
if item_id is None:
raise exceptions.DataError(_(u'if extra["update"] is set, item_id must be set too'))
try:
# we get previous item
items_data = yield self._p.getItems(client, service, node, item_ids=[item_id])
item_elt = items_data[0][0]
except Exception as e:
log.warning(_(u"Can't get previous item, update ignored: {reason}").format(
reason = e))
else:
# and parse it
form = data_form.findForm(item_elt, form_ns)
if form is None:
log.warning(_(u"Can't parse previous item, update ignored: data form not found").format(
reason = e))
else:
for name, field in form.fields.iteritems():
if name not in values:
values[name] = u'\n'.join(unicode(v) for v in field.values)
values['updated'] = now
if not values.get('reporter'):
identity = yield self._i.getIdentity(client, client.jid)
values['reporter'] = identity['nick']
if not values.get('reporter_jid'):
values['reporter_jid'] = client.jid.full()
item_id = yield self._s.sendDataFormItem(client, service, node, values, schema, item_id, extra, deserialise)
item_id = yield self._s.set(client, service, node, values, schema, item_id, extra, deserialise, form_ns)
defer.returnValue(item_id)
def _getSchema(self, service, node, profile_key=C.PROF_KEY_NONE):
if not node:
node = NS_TICKETS
return self._s._getUISchema(service, node, profile_key)

View file

@ -71,9 +71,9 @@ class TicketsImportPlugin(object):
'body': main description of the ticket
'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
'author': full name of reporter
'author_jid': jid of reporter
'author_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': list of emails subscribed to the ticket

View file

@ -72,13 +72,13 @@ class BugzillaParser(object):
ticket['updated'] = utils.date_parse(bug.findtext('delta_ts'))
ticket['title'] = bug.findtext('short_desc')
reporter_elt = bug.find('reporter')
ticket['reporter'] = reporter_elt.get('name')
if ticket['reporter'] is None:
ticket['author'] = reporter_elt.get('name')
if ticket['author'] is None:
if '@' in reporter_elt.text:
ticket['reporter'] = reporter_elt.text[:reporter_elt.text.find('@')].title()
ticket['author'] = reporter_elt.text[:reporter_elt.text.find('@')].title()
else:
ticket['reporter'] = u'no name'
ticket['reporter_email'] = reporter_elt.text
ticket['author'] = u'no name'
ticket['author_email'] = reporter_elt.text
assigned_to_elt = bug.find('assigned_to')
ticket['assigned_to_name'] = assigned_to_elt.get('name')
ticket['assigned_to_email'] = assigned_to_elt.text