comparison sat/plugins/plugin_exp_pubsub_schema.py @ 3028:ab2696e34d29

Python 3 port: /!\ this is a huge commit /!\ starting from this commit, SàT is needs Python 3.6+ /!\ SàT maybe be instable or some feature may not work anymore, this will improve with time This patch port backend, bridge and frontends to Python 3. Roughly this has been done this way: - 2to3 tools has been applied (with python 3.7) - all references to python2 have been replaced with python3 (notably shebangs) - fixed files not handled by 2to3 (notably the shell script) - several manual fixes - fixed issues reported by Python 3 that where not handled in Python 2 - replaced "async" with "async_" when needed (it's a reserved word from Python 3.7) - replaced zope's "implements" with @implementer decorator - temporary hack to handle data pickled in database, as str or bytes may be returned, to be checked later - fixed hash comparison for password - removed some code which is not needed anymore with Python 3 - deactivated some code which needs to be checked (notably certificate validation) - tested with jp, fixed reported issues until some basic commands worked - ported Primitivus (after porting dependencies like urwid satext) - more manual fixes
author Goffi <goffi@goffi.org>
date Tue, 13 Aug 2019 19:08:41 +0200
parents 989b622faff6
children fee60f17ebac
comparison
equal deleted inserted replaced
3027:ff5bcb12ae60 3028:ab2696e34d29
1 #!/usr/bin/env python2 1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*- 2 # -*- coding: utf-8 -*-
3 3
4 # SAT plugin for Pubsub Schemas 4 # SAT plugin for Pubsub Schemas
5 # Copyright (C) 2009-2019 Jérôme Poisson (goffi@goffi.org) 5 # Copyright (C) 2009-2019 Jérôme Poisson (goffi@goffi.org)
6 6
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19
20 from collections import Iterable 20 from collections import Iterable
21 import copy 21 import copy
22 import itertools 22 import itertools
23 from zope.interface import implements 23 from zope.interface import implementer
24 from twisted.words.protocols.jabber import jid 24 from twisted.words.protocols.jabber import jid
25 from twisted.words.protocols.jabber.xmlstream import XMPPHandler 25 from twisted.words.protocols.jabber.xmlstream import XMPPHandler
26 from twisted.internet import defer 26 from twisted.internet import defer
27 from wokkel import disco, iwokkel 27 from wokkel import disco, iwokkel
28 from wokkel import data_form 28 from wokkel import data_form
36 from sat.tools.common import data_format 36 from sat.tools.common import data_format
37 from sat.core.log import getLogger 37 from sat.core.log import getLogger
38 38
39 log = getLogger(__name__) 39 log = getLogger(__name__)
40 40
41 NS_SCHEMA = u"https://salut-a-toi/protocol/schema:0" 41 NS_SCHEMA = "https://salut-a-toi/protocol/schema:0"
42 42
43 PLUGIN_INFO = { 43 PLUGIN_INFO = {
44 C.PI_NAME: u"PubSub Schema", 44 C.PI_NAME: "PubSub Schema",
45 C.PI_IMPORT_NAME: u"PUBSUB_SCHEMA", 45 C.PI_IMPORT_NAME: "PUBSUB_SCHEMA",
46 C.PI_TYPE: u"EXP", 46 C.PI_TYPE: "EXP",
47 C.PI_PROTOCOLS: [], 47 C.PI_PROTOCOLS: [],
48 C.PI_DEPENDENCIES: [u"XEP-0060", u"IDENTITY"], 48 C.PI_DEPENDENCIES: ["XEP-0060", "IDENTITY"],
49 C.PI_MAIN: u"PubsubSchema", 49 C.PI_MAIN: "PubsubSchema",
50 C.PI_HANDLER: u"yes", 50 C.PI_HANDLER: "yes",
51 C.PI_DESCRIPTION: _(u"""Handle Pubsub data schemas"""), 51 C.PI_DESCRIPTION: _("""Handle Pubsub data schemas"""),
52 } 52 }
53 53
54 54
55 class PubsubSchema(object): 55 class PubsubSchema(object):
56 def __init__(self, host): 56 def __init__(self, host):
57 log.info(_(u"PubSub Schema initialization")) 57 log.info(_("PubSub Schema initialization"))
58 self.host = host 58 self.host = host
59 self._p = self.host.plugins["XEP-0060"] 59 self._p = self.host.plugins["XEP-0060"]
60 self._i = self.host.plugins["IDENTITY"] 60 self._i = self.host.plugins["IDENTITY"]
61 host.bridge.addMethod( 61 host.bridge.addMethod(
62 "psSchemaGet", 62 "psSchemaGet",
63 ".plugin", 63 ".plugin",
64 in_sign="sss", 64 in_sign="sss",
65 out_sign="s", 65 out_sign="s",
66 method=self._getSchema, 66 method=self._getSchema,
67 async=True, 67 async_=True,
68 ) 68 )
69 host.bridge.addMethod( 69 host.bridge.addMethod(
70 "psSchemaSet", 70 "psSchemaSet",
71 ".plugin", 71 ".plugin",
72 in_sign="ssss", 72 in_sign="ssss",
73 out_sign="", 73 out_sign="",
74 method=self._setSchema, 74 method=self._setSchema,
75 async=True, 75 async_=True,
76 ) 76 )
77 host.bridge.addMethod( 77 host.bridge.addMethod(
78 "psSchemaUIGet", 78 "psSchemaUIGet",
79 ".plugin", 79 ".plugin",
80 in_sign="sss", 80 in_sign="sss",
81 out_sign="s", 81 out_sign="s",
82 method=utils.partial(self._getUISchema, default_node=None), 82 method=lambda service, nodeIdentifier, profile_key: self._getUISchema(
83 async=True, 83 service, nodeIdentifier, default_node=None, profile_key=profile_key),
84 async_=True,
84 ) 85 )
85 host.bridge.addMethod( 86 host.bridge.addMethod(
86 "psItemsFormGet", 87 "psItemsFormGet",
87 ".plugin", 88 ".plugin",
88 in_sign="ssssiassa{ss}s", 89 in_sign="ssssiassa{ss}s",
89 out_sign="(asa{ss})", 90 out_sign="(asa{ss})",
90 method=self._getDataFormItems, 91 method=self._getDataFormItems,
91 async=True, 92 async_=True,
92 ) 93 )
93 host.bridge.addMethod( 94 host.bridge.addMethod(
94 "psItemFormSend", 95 "psItemFormSend",
95 ".plugin", 96 ".plugin",
96 in_sign="ssa{sas}ssa{ss}s", 97 in_sign="ssa{sas}ssa{ss}s",
97 out_sign="s", 98 out_sign="s",
98 method=self._sendDataFormItem, 99 method=self._sendDataFormItem,
99 async=True, 100 async_=True,
100 ) 101 )
101 102
102 def getHandler(self, client): 103 def getHandler(self, client):
103 return SchemaHandler() 104 return SchemaHandler()
104 105
105 def _getSchemaBridgeCb(self, schema_elt): 106 def _getSchemaBridgeCb(self, schema_elt):
106 if schema_elt is None: 107 if schema_elt is None:
107 return u"" 108 return ""
108 return schema_elt.toXml() 109 return schema_elt.toXml()
109 110
110 def _getSchema(self, service, nodeIdentifier, profile_key=C.PROF_KEY_NONE): 111 def _getSchema(self, service, nodeIdentifier, profile_key=C.PROF_KEY_NONE):
111 client = self.host.getClient(profile_key) 112 client = self.host.getClient(profile_key)
112 service = None if not service else jid.JID(service) 113 service = None if not service else jid.JID(service)
133 None to use our PEP 134 None to use our PEP
134 @param nodeIdentifier(unicode): node to get schema from 135 @param nodeIdentifier(unicode): node to get schema from
135 @return (domish.Element, None): schema (<x> element) 136 @return (domish.Element, None): schema (<x> element)
136 None if not schema has been set on this node 137 None if not schema has been set on this node
137 """ 138 """
138 iq_elt = client.IQ(u"get") 139 iq_elt = client.IQ("get")
139 if service is not None: 140 if service is not None:
140 iq_elt["to"] = service.full() 141 iq_elt["to"] = service.full()
141 pubsub_elt = iq_elt.addElement((NS_SCHEMA, "pubsub")) 142 pubsub_elt = iq_elt.addElement((NS_SCHEMA, "pubsub"))
142 schema_elt = pubsub_elt.addElement((NS_SCHEMA, "schema")) 143 schema_elt = pubsub_elt.addElement((NS_SCHEMA, "schema"))
143 schema_elt["node"] = nodeIdentifier 144 schema_elt["node"] = nodeIdentifier
161 needed when the form is reused and it will be modified (e.g. in sendDataFormItem) 162 needed when the form is reused and it will be modified (e.g. in sendDataFormItem)
162 @return(data_form.Form): data form 163 @return(data_form.Form): data form
163 the form should not be modified if copy_form is not set 164 the form should not be modified if copy_form is not set
164 """ 165 """
165 if schema is None: 166 if schema is None:
166 log.debug(_(u"unspecified schema, we need to request it")) 167 log.debug(_("unspecified schema, we need to request it"))
167 schema = yield self.getSchema(client, service, nodeIdentifier) 168 schema = yield self.getSchema(client, service, nodeIdentifier)
168 if schema is None: 169 if schema is None:
169 raise exceptions.DataError( 170 raise exceptions.DataError(
170 _( 171 _(
171 u"no schema specified, and this node has no schema either, we can't construct the data form" 172 "no schema specified, and this node has no schema either, we can't construct the data form"
172 ) 173 )
173 ) 174 )
174 elif isinstance(schema, data_form.Form): 175 elif isinstance(schema, data_form.Form):
175 if copy_form: 176 if copy_form:
176 schema = copy.deepcopy(schema) 177 schema = copy.deepcopy(schema)
177 defer.returnValue(schema) 178 defer.returnValue(schema)
178 179
179 try: 180 try:
180 form = data_form.Form.fromElement(schema) 181 form = data_form.Form.fromElement(schema)
181 except data_form.Error as e: 182 except data_form.Error as e:
182 raise exceptions.DataError(_(u"Invalid Schema: {msg}").format(msg=e)) 183 raise exceptions.DataError(_("Invalid Schema: {msg}").format(msg=e))
183 form.formType = form_type 184 form.formType = form_type
184 defer.returnValue(form) 185 defer.returnValue(form)
185 186
186 def schema2XMLUI(self, schema_elt): 187 def schema2XMLUI(self, schema_elt):
187 form = data_form.Form.fromElement(schema_elt) 188 form = data_form.Form.fromElement(schema_elt)
190 191
191 def _getUISchema(self, service, nodeIdentifier, default_node=None, 192 def _getUISchema(self, service, nodeIdentifier, default_node=None,
192 profile_key=C.PROF_KEY_NONE): 193 profile_key=C.PROF_KEY_NONE):
193 if not nodeIdentifier: 194 if not nodeIdentifier:
194 if not default_node: 195 if not default_node:
195 raise ValueError(_(u"nodeIndentifier needs to be set")) 196 raise ValueError(_("nodeIndentifier needs to be set"))
196 nodeIdentifier = default_node 197 nodeIdentifier = default_node
197 client = self.host.getClient(profile_key) 198 client = self.host.getClient(profile_key)
198 service = None if not service else jid.JID(service) 199 service = None if not service else jid.JID(service)
199 d = self.getUISchema(client, service, nodeIdentifier) 200 d = self.getUISchema(client, service, nodeIdentifier)
200 d.addCallback(lambda xmlui: xmlui.toXml()) 201 d.addCallback(lambda xmlui: xmlui.toXml())
231 item_ids=None, sub_id=None, extra_dict=None, 232 item_ids=None, sub_id=None, extra_dict=None,
232 profile_key=C.PROF_KEY_NONE): 233 profile_key=C.PROF_KEY_NONE):
233 client = self.host.getClient(profile_key) 234 client = self.host.getClient(profile_key)
234 service = jid.JID(service) if service else None 235 service = jid.JID(service) if service else None
235 if not node: 236 if not node:
236 raise exceptions.DataError(_(u"empty node is not allowed")) 237 raise exceptions.DataError(_("empty node is not allowed"))
237 if schema: 238 if schema:
238 schema = generic.parseXml(schema.encode("utf-8")) 239 schema = generic.parseXml(schema.encode("utf-8"))
239 else: 240 else:
240 schema = None 241 schema = None
241 max_items = None if max_items == C.NO_LIMIT else max_items 242 max_items = None if max_items == C.NO_LIMIT else max_items
274 @raise ValueError: one argument is invalid 275 @raise ValueError: one argument is invalid
275 """ 276 """
276 if not nodeIdentifier: 277 if not nodeIdentifier:
277 if not default_node: 278 if not default_node:
278 raise ValueError( 279 raise ValueError(
279 _(u"default_node must be set if nodeIdentifier is not set") 280 _("default_node must be set if nodeIdentifier is not set")
280 ) 281 )
281 nodeIdentifier = default_node 282 nodeIdentifier = default_node
282 # we need the initial form to get options of fields when suitable 283 # we need the initial form to get options of fields when suitable
283 schema_form = yield self.getSchemaForm( 284 schema_form = yield self.getSchemaForm(
284 client, service, nodeIdentifier, schema, form_type="result", copy_form=False 285 client, service, nodeIdentifier, schema, form_type="result", copy_form=False
294 extra, 295 extra,
295 ) 296 )
296 items, metadata = items_data 297 items, metadata = items_data
297 items_xmlui = [] 298 items_xmlui = []
298 for item_elt in items: 299 for item_elt in items:
299 for x_elt in item_elt.elements((data_form.NS_X_DATA, u"x")): 300 for x_elt in item_elt.elements((data_form.NS_X_DATA, "x")):
300 form = data_form.Form.fromElement(x_elt) 301 form = data_form.Form.fromElement(x_elt)
301 if form_ns and form.formNamespace != form_ns: 302 if form_ns and form.formNamespace != form_ns:
302 continue 303 continue
303 xmlui = xml_tools.dataFormResult2XMLUI( 304 xmlui = xml_tools.dataFormResult2XMLUI(
304 form, 305 form,
305 schema_form, 306 schema_form,
306 # FIXME: conflicts with schema (i.e. if "id" or "publisher" already exists) 307 # FIXME: conflicts with schema (i.e. if "id" or "publisher" already exists)
307 # are not checked 308 # are not checked
308 prepend=( 309 prepend=(
309 ("label", "id"), 310 ("label", "id"),
310 ("text", item_elt["id"], u"id"), 311 ("text", item_elt["id"], "id"),
311 ("label", "publisher"), 312 ("label", "publisher"),
312 ("text", item_elt.getAttribute("publisher", ""), u"publisher"), 313 ("text", item_elt.getAttribute("publisher", ""), "publisher"),
313 ), 314 ),
314 filters=filters, 315 filters=filters,
315 read_only=False, 316 read_only=False,
316 ) 317 )
317 items_xmlui.append(xmlui) 318 items_xmlui.append(xmlui)
334 schema, 335 schema,
335 item_id or None, 336 item_id or None,
336 extra, 337 extra,
337 deserialise=True, 338 deserialise=True,
338 ) 339 )
339 d.addCallback(lambda ret: ret or u"") 340 d.addCallback(lambda ret: ret or "")
340 return d 341 return d
341 342
342 @defer.inlineCallbacks 343 @defer.inlineCallbacks
343 def sendDataFormItem(self, client, service, nodeIdentifier, values, schema=None, 344 def sendDataFormItem(self, client, service, nodeIdentifier, values, schema=None,
344 item_id=None, extra=None, deserialise=False): 345 item_id=None, extra=None, deserialise=False):
359 """ 360 """
360 form = yield self.getSchemaForm( 361 form = yield self.getSchemaForm(
361 client, service, nodeIdentifier, schema, form_type="submit" 362 client, service, nodeIdentifier, schema, form_type="submit"
362 ) 363 )
363 364
364 for name, values_list in values.iteritems(): 365 for name, values_list in values.items():
365 try: 366 try:
366 field = form.fields[name] 367 field = form.fields[name]
367 except KeyError: 368 except KeyError:
368 log.warning( 369 log.warning(
369 _(u"field {name} doesn't exist, ignoring it").format(name=name) 370 _("field {name} doesn't exist, ignoring it").format(name=name)
370 ) 371 )
371 continue 372 continue
372 if isinstance(values_list, basestring) or not isinstance( 373 if isinstance(values_list, str) or not isinstance(
373 values_list, Iterable 374 values_list, Iterable
374 ): 375 ):
375 values_list = [values_list] 376 values_list = [values_list]
376 if deserialise: 377 if deserialise:
377 if field.fieldType == u"boolean": 378 if field.fieldType == "boolean":
378 values_list = [C.bool(v) for v in values_list] 379 values_list = [C.bool(v) for v in values_list]
379 elif field.fieldType == u"text-multi": 380 elif field.fieldType == "text-multi":
380 # for text-multi, lines must be put on separate values 381 # for text-multi, lines must be put on separate values
381 values_list = list( 382 values_list = list(
382 itertools.chain(*[v.splitlines() for v in values_list]) 383 itertools.chain(*[v.splitlines() for v in values_list])
383 ) 384 )
384 elif xml_tools.isXHTMLField(field): 385 elif xml_tools.isXHTMLField(field):
385 values_list = [generic.parseXml(v.encode("utf-8")) 386 values_list = [generic.parseXml(v.encode("utf-8"))
386 for v in values_list] 387 for v in values_list]
387 elif u"jid" in (field.fieldType or u""): 388 elif "jid" in (field.fieldType or ""):
388 values_list = [jid.JID(v) for v in values_list] 389 values_list = [jid.JID(v) for v in values_list]
389 if u"list" in (field.fieldType or u""): 390 if "list" in (field.fieldType or ""):
390 # for lists, we check that given values are allowed in form 391 # for lists, we check that given values are allowed in form
391 allowed_values = [o.value for o in field.options] 392 allowed_values = [o.value for o in field.options]
392 values_list = [v for v in values_list if v in allowed_values] 393 values_list = [v for v in values_list if v in allowed_values]
393 if not values_list: 394 if not values_list:
394 # if values don't map to allowed values, we use default ones 395 # if values don't map to allowed values, we use default ones
417 def textbox2ListFilter(self, form_xmlui, widget_type, args, kwargs): 418 def textbox2ListFilter(self, form_xmlui, widget_type, args, kwargs):
418 """Split lines of a textbox in a list 419 """Split lines of a textbox in a list
419 420
420 main use case is using a textbox for labels 421 main use case is using a textbox for labels
421 """ 422 """
422 if widget_type != u"textbox": 423 if widget_type != "textbox":
423 return widget_type, args, kwargs 424 return widget_type, args, kwargs
424 widget_type = u"list" 425 widget_type = "list"
425 options = [o for o in args.pop(0).split(u"\n") if o] 426 options = [o for o in args.pop(0).split("\n") if o]
426 kwargs = { 427 kwargs = {
427 "options": options, 428 "options": options,
428 "name": kwargs.get("name"), 429 "name": kwargs.get("name"),
429 "styles": (u"noselect", u"extensible", u"reducible"), 430 "styles": ("noselect", "extensible", "reducible"),
430 } 431 }
431 return widget_type, args, kwargs 432 return widget_type, args, kwargs
432 433
433 def dateFilter(self, form_xmlui, widget_type, args, kwargs): 434 def dateFilter(self, form_xmlui, widget_type, args, kwargs):
434 """Convert a string with a date to a unix timestamp""" 435 """Convert a string with a date to a unix timestamp"""
435 if widget_type != u"string" or not args[0]: 436 if widget_type != "string" or not args[0]:
436 return widget_type, args, kwargs 437 return widget_type, args, kwargs
437 # we convert XMPP date to timestamp 438 # we convert XMPP date to timestamp
438 try: 439 try:
439 args[0] = unicode(date_utils.date_parse(args[0])) 440 args[0] = str(date_utils.date_parse(args[0]))
440 except Exception as e: 441 except Exception as e:
441 log.warning(_(u"Can't parse date field: {msg}").format(msg=e)) 442 log.warning(_("Can't parse date field: {msg}").format(msg=e))
442 return widget_type, args, kwargs 443 return widget_type, args, kwargs
443 444
444 ## Helper methods ## 445 ## Helper methods ##
445 446
446 def prepareBridgeGet(self, service, node, max_items, sub_id, extra_dict, profile_key): 447 def prepareBridgeGet(self, service, node, max_items, sub_id, extra_dict, profile_key):
477 extra = {} 478 extra = {}
478 # XXX: Q&D way to get list for labels when displaying them, but text when we 479 # XXX: Q&D way to get list for labels when displaying them, but text when we
479 # have to modify them 480 # have to modify them
480 if C.bool(extra.get("labels_as_list", C.BOOL_FALSE)): 481 if C.bool(extra.get("labels_as_list", C.BOOL_FALSE)):
481 filters = filters.copy() 482 filters = filters.copy()
482 filters[u"labels"] = self.textbox2ListFilter 483 filters["labels"] = self.textbox2ListFilter
483 client, service, node, max_items, extra, sub_id = self.prepareBridgeGet( 484 client, service, node, max_items, extra, sub_id = self.prepareBridgeGet(
484 service, node, max_items, sub_id, extra, profile_key 485 service, node, max_items, sub_id, extra, profile_key
485 ) 486 )
486 d = self.getDataFormItems( 487 d = self.getDataFormItems(
487 client, 488 client,
534 client, service, node, item_ids=[item_id] 535 client, service, node, item_ids=[item_id]
535 ) 536 )
536 item_elt = items_data[0][0] 537 item_elt = items_data[0][0]
537 except Exception as e: 538 except Exception as e:
538 log.warning( 539 log.warning(
539 _(u"Can't get previous item, update ignored: {reason}").format( 540 _("Can't get previous item, update ignored: {reason}").format(
540 reason=e 541 reason=e
541 ) 542 )
542 ) 543 )
543 else: 544 else:
544 # and parse it 545 # and parse it
545 form = data_form.findForm(item_elt, form_ns) 546 form = data_form.findForm(item_elt, form_ns)
546 if form is None: 547 if form is None:
547 log.warning( 548 log.warning(
548 _( 549 _(
549 u"Can't parse previous item, update ignored: data form not found" 550 "Can't parse previous item, update ignored: data form not found"
550 ).format(reason=e) 551 ).format(reason=e)
551 ) 552 )
552 else: 553 else:
553 for name, field in form.fields.iteritems(): 554 for name, field in form.fields.items():
554 if name not in values: 555 if name not in values:
555 values[name] = u"\n".join(unicode(v) for v in field.values) 556 values[name] = "\n".join(str(v) for v in field.values)
556 557
557 def _set(self, service, node, values, schema=None, item_id=None, extra=None, 558 def _set(self, service, node, values, schema=None, item_id=None, extra=None,
558 default_node=None, form_ns=None, fill_author=True, 559 default_node=None, form_ns=None, fill_author=True,
559 profile_key=C.PROF_KEY_NONE): 560 profile_key=C.PROF_KEY_NONE):
560 """Bridge method to set item in node with schema 561 """Bridge method to set item in node with schema
576 deserialise=True, 577 deserialise=True,
577 form_ns=form_ns, 578 form_ns=form_ns,
578 default_node=default_node, 579 default_node=default_node,
579 fill_author=fill_author, 580 fill_author=fill_author,
580 ) 581 )
581 d.addCallback(lambda ret: ret or u"") 582 d.addCallback(lambda ret: ret or "")
582 return d 583 return d
583 584
584 @defer.inlineCallbacks 585 @defer.inlineCallbacks
585 def set(self, client, service, node, values, schema, item_id, extra, deserialise, 586 def set(self, client, service, node, values, schema, item_id, extra, deserialise,
586 form_ns, default_node=None, fill_author=True): 587 form_ns, default_node=None, fill_author=True):
603 """ 604 """
604 if extra is None: 605 if extra is None:
605 extra = {} 606 extra = {}
606 if not node: 607 if not node:
607 if default_node is None: 608 if default_node is None:
608 raise ValueError(_(u"default_node must be set if node is not set")) 609 raise ValueError(_("default_node must be set if node is not set"))
609 node = default_node 610 node = default_node
610 now = utils.xmpp_date() 611 now = utils.xmpp_date()
611 if not item_id: 612 if not item_id:
612 values["created"] = now 613 values["created"] = now
613 elif extra.get(u"update", False): 614 elif extra.get("update", False):
614 if item_id is None: 615 if item_id is None:
615 raise exceptions.DataError( 616 raise exceptions.DataError(
616 _(u'if extra["update"] is set, item_id must be set too') 617 _('if extra["update"] is set, item_id must be set too')
617 ) 618 )
618 yield self.copyMissingValues(client, service, node, item_id, form_ns, values) 619 yield self.copyMissingValues(client, service, node, item_id, form_ns, values)
619 620
620 values["updated"] = now 621 values["updated"] = now
621 if fill_author: 622 if fill_author:
628 client, service, node, values, schema, item_id, extra, deserialise 629 client, service, node, values, schema, item_id, extra, deserialise
629 ) 630 )
630 defer.returnValue(item_id) 631 defer.returnValue(item_id)
631 632
632 633
634 @implementer(iwokkel.IDisco)
633 class SchemaHandler(XMPPHandler): 635 class SchemaHandler(XMPPHandler):
634 implements(iwokkel.IDisco)
635 636
636 def getDiscoInfo(self, requestor, service, nodeIdentifier=""): 637 def getDiscoInfo(self, requestor, service, nodeIdentifier=""):
637 return [disco.DiscoFeature(NS_SCHEMA)] 638 return [disco.DiscoFeature(NS_SCHEMA)]
638 639
639 def getDiscoItems(self, requestor, service, nodeIdentifier=""): 640 def getDiscoItems(self, requestor, service, nodeIdentifier=""):