comparison src/plugins/plugin_exp_pubsub_schema.py @ 2471:544c4d2fec45

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.
author Goffi <goffi@goffi.org>
date Fri, 12 Jan 2018 15:58:54 +0100
parents 6c39f30444a0
children 0046283a285d
comparison
equal deleted inserted replaced
2470:8084066ac95b 2471:544c4d2fec45
19 19
20 from sat.core.i18n import _ 20 from sat.core.i18n import _
21 from sat.core import exceptions 21 from sat.core import exceptions
22 from sat.core.constants import Const as C 22 from sat.core.constants import Const as C
23 from sat.tools import xml_tools 23 from sat.tools import xml_tools
24 from sat.tools import utils
24 from twisted.words.protocols.jabber import jid 25 from twisted.words.protocols.jabber import jid
25 from twisted.words.protocols.jabber.xmlstream import XMPPHandler 26 from twisted.words.protocols.jabber.xmlstream import XMPPHandler
26 from twisted.internet import defer 27 from twisted.internet import defer
27 from sat.core.log import getLogger 28 from sat.core.log import getLogger
28 log = getLogger(__name__) 29 log = getLogger(__name__)
39 PLUGIN_INFO = { 40 PLUGIN_INFO = {
40 C.PI_NAME: "PubSub Schema", 41 C.PI_NAME: "PubSub Schema",
41 C.PI_IMPORT_NAME: "PUBSUB_SCHEMA", 42 C.PI_IMPORT_NAME: "PUBSUB_SCHEMA",
42 C.PI_TYPE: "EXP", 43 C.PI_TYPE: "EXP",
43 C.PI_PROTOCOLS: [], 44 C.PI_PROTOCOLS: [],
44 C.PI_DEPENDENCIES: ["XEP-0060"], 45 C.PI_DEPENDENCIES: ["XEP-0060", "IDENTITY"],
45 C.PI_MAIN: "PubsubSchema", 46 C.PI_MAIN: "PubsubSchema",
46 C.PI_HANDLER: "yes", 47 C.PI_HANDLER: "yes",
47 C.PI_DESCRIPTION: _("""Handle Pubsub data schemas""") 48 C.PI_DESCRIPTION: _("""Handle Pubsub data schemas""")
48 } 49 }
49 50
52 53
53 def __init__(self, host): 54 def __init__(self, host):
54 log.info(_(u"PubSub Schema initialization")) 55 log.info(_(u"PubSub Schema initialization"))
55 self.host = host 56 self.host = host
56 self._p = self.host.plugins["XEP-0060"] 57 self._p = self.host.plugins["XEP-0060"]
58 self._i = self.host.plugins["IDENTITY"]
57 host.bridge.addMethod("psSchemaGet", ".plugin", 59 host.bridge.addMethod("psSchemaGet", ".plugin",
58 in_sign='sss', out_sign='s', 60 in_sign='sss', out_sign='s',
59 method=self._getSchema, 61 method=self._getSchema,
60 async=True 62 async=True
61 ) 63 )
64 method=self._setSchema, 66 method=self._setSchema,
65 async=True 67 async=True
66 ) 68 )
67 host.bridge.addMethod("psSchemaUIGet", ".plugin", 69 host.bridge.addMethod("psSchemaUIGet", ".plugin",
68 in_sign='sss', out_sign='s', 70 in_sign='sss', out_sign='s',
69 method=self._getUISchema, 71 method=utils.partial(self._getUISchema, default_node=None),
70 async=True 72 async=True
71 ) 73 )
72 host.bridge.addMethod("psItemsFormGet", ".plugin", 74 host.bridge.addMethod("psItemsFormGet", ".plugin",
73 in_sign='ssssiassa{ss}s', out_sign='(asa{ss})', 75 in_sign='ssssiassa{ss}s', out_sign='(asa{ss})',
74 method=self._getDataFormItems, 76 method=self._getDataFormItems,
161 def schema2XMLUI(self, schema_elt): 163 def schema2XMLUI(self, schema_elt):
162 form = data_form.Form.fromElement(schema_elt) 164 form = data_form.Form.fromElement(schema_elt)
163 xmlui = xml_tools.dataForm2XMLUI(form, '') 165 xmlui = xml_tools.dataForm2XMLUI(form, '')
164 return xmlui 166 return xmlui
165 167
166 def _getUISchema(self, service, nodeIdentifier, profile_key=C.PROF_KEY_NONE): 168 def _getUISchema(self, service, nodeIdentifier, default_node=None, profile_key=C.PROF_KEY_NONE):
169 if not nodeIdentifier:
170 if not default_node:
171 raise ValueError(_(u"nodeIndentifier needs to be set"))
172 nodeIdentifier = default_node
167 client = self.host.getClient(profile_key) 173 client = self.host.getClient(profile_key)
168 service = None if not service else jid.JID(service) 174 service = None if not service else jid.JID(service)
169 d = self.getUISchema(client, service, nodeIdentifier) 175 d = self.getUISchema(client, service, nodeIdentifier)
170 d.addCallback(lambda xmlui: xmlui.toXml()) 176 d.addCallback(lambda xmlui: xmlui.toXml())
171 return d 177 return d
206 schema = generic.parseXml(schema.encode('utf-8')) 212 schema = generic.parseXml(schema.encode('utf-8'))
207 else: 213 else:
208 schema = None 214 schema = None
209 max_items = None if max_items == C.NO_LIMIT else max_items 215 max_items = None if max_items == C.NO_LIMIT else max_items
210 extra = self._p.parseExtra(extra_dict) 216 extra = self._p.parseExtra(extra_dict)
211 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) 217 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)
212 d.addCallback(self._p.serItemsData) 218 d.addCallback(self._p.serItemsData)
213 return d 219 return d
214 220
215 @defer.inlineCallbacks 221 @defer.inlineCallbacks
216 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): 222 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):
217 """Get items known as being data forms, and convert them to XMLUI 223 """Get items known as being data forms, and convert them to XMLUI
218 224
225 @param schema(domish.Element, data_form.Form, None): schema of the node if known
226 if None, it will be retrieved from node
227 @param default_node(unicode): node to use if nodeIdentifier is None or empty
219 @param form_ns (unicode, None): namespace of the form 228 @param form_ns (unicode, None): namespace of the form
220 None to accept everything, even if form has no namespace 229 None to accept everything, even if form has no namespace
221 @param schema(domish.Element, data_form.Form, None): schema of the node if known
222 if None, it will be retrieved from node
223 @param filters(dict, None): same as for xml_tools.dataFormResult2XMLUI 230 @param filters(dict, None): same as for xml_tools.dataFormResult2XMLUI
224 other parameters as the same as for [getItems] 231 other parameters as the same as for [getItems]
225 @return (list[unicode]): XMLUI of the forms 232 @return (list[unicode]): XMLUI of the forms
226 if an item is invalid (not corresponding to form_ns or not a data_form) 233 if an item is invalid (not corresponding to form_ns or not a data_form)
227 it will be skipped 234 it will be skipped
228 """ 235 @raise ValueError: one argument is invalid
236 """
237 if not nodeIdentifier:
238 if not default_node:
239 raise ValueError(_(u"default_node must be set if nodeIdentifier is not set"))
240 nodeIdentifier = default_node
229 # we need the initial form to get options of fields when suitable 241 # we need the initial form to get options of fields when suitable
230 schema_form = yield self.getSchemaForm(client, service, nodeIdentifier, schema, form_type='result', copy_form=False) 242 schema_form = yield self.getSchemaForm(client, service, nodeIdentifier, schema, form_type='result', copy_form=False)
231 items_data = yield self._p.getItems(client, service, nodeIdentifier, max_items, item_ids, sub_id, rsm_request, extra) 243 items_data = yield self._p.getItems(client, service, nodeIdentifier, max_items, item_ids, sub_id, rsm_request, extra)
232 items, metadata = items_data 244 items, metadata = items_data
233 items_xmlui = [] 245 items_xmlui = []
304 values_list = field.values 316 values_list = field.values
305 field.values = values_list 317 field.values = values_list
306 318
307 yield self._p.sendItem(client, service, nodeIdentifier, form.toElement(), item_id, extra) 319 yield self._p.sendItem(client, service, nodeIdentifier, form.toElement(), item_id, extra)
308 320
321 ## filters ##
322 # filters useful for data form to XMLUI conversion #
323
324 def valueOrPublisherFilter(self, form_xmlui, widget_type, args, kwargs):
325 """Replace missing value by publisher's user part"""
326 if not args[0]:
327 # value is not filled: we use user part of publisher (if we have it)
328 try:
329 publisher = jid.JID(form_xmlui.named_widgets['publisher'].value)
330 except (KeyError, RuntimeError):
331 pass
332 else:
333 args[0] = publisher.user.capitalize()
334 return widget_type, args, kwargs
335
336 def textbox2ListFilter(self, form_xmlui, widget_type, args, kwargs):
337 """Split lines of a textbox in a list
338
339 main use case is using a textbox for labels
340 """
341 if widget_type != u'textbox':
342 return widget_type, args, kwargs
343 widget_type = u'list'
344 options = [o for o in args.pop(0).split(u'\n') if o]
345 kwargs = {'options': options,
346 'name': kwargs.get('name'),
347 'styles': (u'noselect', u'extensible', u'reducible')}
348 return widget_type, args, kwargs
349
350 def dateFilter(self, form_xmlui, widget_type, args, kwargs):
351 """Convert a string with a date to a unix timestamp"""
352 if widget_type != u'string' or not args[0]:
353 return widget_type, args, kwargs
354 # we convert XMPP date to timestamp
355 try:
356 args[0] = unicode(utils.date_parse(args[0]))
357 except Exception as e:
358 log.warning(_(u"Can't parse date field: {msg}").format(msg=e))
359 return widget_type, args, kwargs
360
361 ## Helper methods ##
362
363 def prepareBridgeGet(self, service, node, max_items, sub_id, extra_dict, profile_key):
364 """Parse arguments received from bridge *Get methods and return higher level data
365
366 @return (tuple): (client, service, node, max_items, extra, sub_id) usable for internal methods
367 """
368 client = self.host.getClient(profile_key)
369 service = jid.JID(service) if service else None
370 if not node:
371 node = None
372 max_items = None if max_items == C.NO_LIMIT else max_items
373 if not sub_id:
374 sub_id = None
375 extra = self._p.parseExtra(extra_dict)
376
377 return client, service, node, max_items, extra, sub_id
378
379 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):
380 """Bridge method to retrieve data from node with schema
381
382 this method is a helper so dependant plugins can use it directly
383 when adding *Get methods
384 """
385 client, service, node, max_items, extra, sub_id = self.prepareBridgeGet(service, node, max_items, sub_id, extra_dict, profile_key)
386 d = self.getDataFormItems(client, service, node or None,
387 max_items=max_items,
388 item_ids=item_ids,
389 sub_id=sub_id,
390 rsm_request=extra.rsm_request,
391 extra=extra.extra,
392 default_node=default_node,
393 form_ns=form_ns,
394 filters=filters)
395 d.addCallback(self._p.serItemsData)
396 return d
397
398 def prepareBridgeSet(self, service, node, schema, item_id, extra, profile_key):
399 """Parse arguments received from bridge *Set methods and return higher level data
400
401 @return (tuple): (client, service, node, schema, item_id, extra) usable for internal methods
402 """
403 client = self.host.getClient(profile_key)
404 service = None if not service else jid.JID(service)
405 if schema:
406 schema = generic.parseXml(schema.encode('utf-8'))
407 else:
408 schema = None
409 if extra and u'update' in extra:
410 extra[u'update'] = C.bool(extra[u'update'])
411 return client, service, node or None, schema, item_id or None, extra
412
413 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):
414 """Bridge method to set item in node with schema
415
416 this method is a helper so dependant plugins can use it directly
417 when adding *Set methods
418 """
419 client, service, node, schema, item_id, extra = self.prepareBridgeSet(service, node, schema, item_id, extra)
420 d = self.set(client, service, node, values, schema, item_id, extra,
421 deserialise=True,
422 form_ns=form_ns,
423 default_node=default_node,
424 fill_author=fill_author)
425 d.addCallback(lambda ret: ret or u'')
426 return d
427
428 @defer.inlineCallbacks
429 def set(self, client, service, node, values, schema, item_id, extra, deserialise, form_ns, default_node=None, fill_author=True):
430 """Set an item in a node with a schema
431
432 This method can be used directly by *Set methods added by dependant plugin
433 @param values(dict[key(unicode), [iterable[object], object]]): values of the items
434 if not iterable, will be put in a list
435 'created' and 'updated' will be forced to current time:
436 - 'created' is set if item_id is None, i.e. if it's a new ticket
437 - 'updated' is set everytime
438 @param extra(dict, None): same as for [XEP-0060.sendItem] with additional keys:
439 - update(bool): if True, get previous item data to merge with current one
440 if True, item_id must be None
441 @param form_ns (unicode, None): namespace of the form
442 needed when an update is done
443 @param default_node(unicode, None): value to use if node is not set
444 other arguments are same as for [self._s.sendDataFormItem]
445 @return (unicode): id of the created item
446 """
447 if not node:
448 if default_node is None:
449 raise ValueError(_(u"default_node must be set if node is not set"))
450 node = default_node
451 now = utils.xmpp_date()
452 if not item_id:
453 values['created'] = now
454 elif extra.get(u'update', False):
455 if item_id is None:
456 raise exceptions.DataError(_(u'if extra["update"] is set, item_id must be set too'))
457 try:
458 # we get previous item
459 items_data = yield self._p.getItems(client, service, node, item_ids=[item_id])
460 item_elt = items_data[0][0]
461 except Exception as e:
462 log.warning(_(u"Can't get previous item, update ignored: {reason}").format(
463 reason = e))
464 else:
465 # and parse it
466 form = data_form.findForm(item_elt, form_ns)
467 if form is None:
468 log.warning(_(u"Can't parse previous item, update ignored: data form not found").format(
469 reason = e))
470 else:
471 for name, field in form.fields.iteritems():
472 if name not in values:
473 values[name] = u'\n'.join(unicode(v) for v in field.values)
474
475 values['updated'] = now
476 if fill_author:
477 if not values.get('author'):
478 identity = yield self._i.getIdentity(client, client.jid)
479 values['author'] = identity['nick']
480 if not values.get('author_jid'):
481 values['author_jid'] = client.jid.full()
482 item_id = yield self.sendDataFormItem(client, service, node, values, schema, item_id, extra, deserialise)
483 defer.returnValue(item_id)
484
309 485
310 class SchemaHandler(XMPPHandler): 486 class SchemaHandler(XMPPHandler):
311 implements(iwokkel.IDisco) 487 implements(iwokkel.IDisco)
312 488
313 def getDiscoInfo(self, requestor, service, nodeIdentifier=''): 489 def getDiscoInfo(self, requestor, service, nodeIdentifier=''):