comparison sat/tools/xml_tools.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/tools/xml_tools.py@65695b9343d3
children 56f94936df1e
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # SAT: a jabber client
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.log import getLogger
23 log = getLogger(__name__)
24
25 from xml.dom import minidom, NotFoundErr
26 from wokkel import data_form
27 from twisted.words.xish import domish
28 from twisted.words.protocols.jabber import jid
29 from twisted.internet import defer
30 from sat.core import exceptions
31 from collections import OrderedDict
32 from copy import deepcopy
33 import htmlentitydefs
34 import re
35
36 """This library help manage XML used in SàT (parameters, registration, etc)"""
37
38 SAT_FORM_PREFIX = "SAT_FORM_"
39 SAT_PARAM_SEPARATOR = "_XMLUI_PARAM_" # used to have unique elements names
40 html_entity_re = re.compile(r'&([a-zA-Z]+?);')
41 XML_ENTITIES = ('quot', 'amp', 'apos', 'lt', 'gt')
42
43 # TODO: move XMLUI stuff in a separate module
44 # TODO: rewrite this with lxml or ElementTree or domish.Element: it's complicated and difficult to maintain with current minidom implementation
45
46 # Helper functions
47
48 def _dataFormField2XMLUIData(field, read_only=False):
49 """Get data needed to create an XMLUI's Widget from Wokkel's data_form's Field.
50
51 The attribute field can be modified (if it's fixed and it has no value).
52 @param field (data_form.Field): a field with attributes "value", "fieldType", "label" and "var"
53 @param read_only (bool): if True and it makes sense, create a read only input widget
54 @return: a tuple (widget_type, widget_args, widget_kwargs)
55 """
56 widget_args = [field.value]
57 widget_kwargs = {}
58 if field.fieldType == 'fixed' or field.fieldType is None:
59 widget_type = 'text'
60 if field.value is None:
61 if field.label is None:
62 log.warning(_("Fixed field has neither value nor label, ignoring it"))
63 field.value = ""
64 else:
65 field.value = field.label
66 field.label = None
67 widget_args[0] = field.value
68 elif field.fieldType == 'text-single':
69 widget_type = "string"
70 widget_kwargs['read_only'] = read_only
71 elif field.fieldType == 'jid-single':
72 widget_type = "jid_input"
73 widget_kwargs['read_only'] = read_only
74 elif field.fieldType == 'text-multi':
75 widget_type = "textbox"
76 widget_args[0] = u'\n'.join(field.values)
77 widget_kwargs['read_only'] = read_only
78 elif field.fieldType == 'text-private':
79 widget_type = "password"
80 widget_kwargs['read_only'] = read_only
81 elif field.fieldType == 'boolean':
82 widget_type = "bool"
83 if widget_args[0] is None:
84 widget_args[0] = 'false'
85 widget_kwargs['read_only'] = read_only
86 elif field.fieldType == 'integer':
87 widget_type = "integer"
88 widget_kwargs['read_only'] = read_only
89 elif field.fieldType == 'list-single':
90 widget_type = "list"
91 widget_kwargs["options"] = [(option.value, option.label or option.value) for option in field.options]
92 widget_kwargs["selected"] = widget_args
93 widget_args = []
94 else:
95 log.error(u"FIXME FIXME FIXME: Type [%s] is not managed yet by SàT" % field.fieldType)
96 widget_type = "string"
97 widget_kwargs['read_only'] = read_only
98
99 if field.var:
100 widget_kwargs["name"] = field.var
101
102 return widget_type, widget_args, widget_kwargs
103
104
105 def dataForm2Widgets(form_ui, form, read_only=False, prepend=None, filters=None):
106 """Complete an existing XMLUI with widget converted from XEP-0004 data forms.
107
108 @param form_ui (XMLUI): XMLUI instance
109 @param form (data_form.Form): Wokkel's implementation of data form
110 @param read_only (bool): if True and it makes sense, create a read only input widget
111 @param prepend(iterable, None): widgets to prepend to main LabelContainer
112 if not None, must be an iterable of *args for addWidget. Those widgets will
113 be added first to the container.
114 @param filters(dict, None): if not None, a dictionary of callable:
115 key is the name of the widget to filter
116 the value is a callable, it will get form's XMLUI, widget's type, args and kwargs
117 and must return widget's type, args and kwargs (which can be modified)
118 This is especially useful to modify well known fields
119 @return: the completed XMLUI instance
120 """
121 if filters is None:
122 filters = {}
123 if form.instructions:
124 form_ui.addText('\n'.join(form.instructions), 'instructions')
125
126 form_ui.changeContainer("label")
127
128 if prepend is not None:
129 for widget_args in prepend:
130 form_ui.addWidget(*widget_args)
131
132 for field in form.fieldList:
133 widget_type, widget_args, widget_kwargs = _dataFormField2XMLUIData(field, read_only)
134 try:
135 widget_filter = filters[widget_kwargs['name']]
136 except KeyError:
137 pass
138 else:
139 widget_type, widget_args, widget_kwargs = widget_filter(form_ui, widget_type, widget_args, widget_kwargs)
140 label = field.label or field.var
141 if label:
142 form_ui.addLabel(label)
143 else:
144 form_ui.addEmpty()
145
146 form_ui.addWidget(widget_type, *widget_args, **widget_kwargs)
147
148 return form_ui
149
150
151 def dataForm2XMLUI(form, submit_id, session_id=None, read_only=False):
152 """Take a data form (Wokkel's XEP-0004 implementation) and convert it to a SàT XMLUI.
153
154 @param form (data_form.Form): a Form instance
155 @param submit_id (unicode): callback id to call when submitting form
156 @param session_id (unicode): session id to return with the data
157 @param read_only (bool): if True and it makes sense, create a read only input widget
158 @return: XMLUI instance
159 """
160 form_ui = XMLUI("form", "vertical", submit_id=submit_id, session_id=session_id)
161 return dataForm2Widgets(form_ui, form, read_only=read_only)
162
163
164 def dataFormEltResult2XMLUIData(form_xml):
165 """Parse a data form result (not parsed by Wokkel's XEP-0004 implementation).
166
167 The raw data form is used because Wokkel doesn't manage result items parsing yet.
168 @param form_xml (domish.Element): element of the data form
169 @return: a couple (headers, result_list):
170 - headers (dict{unicode: unicode}): form headers (field labels and types)
171 - xmlui_data (list[tuple]): list of (widget_type, widget_args, widget_kwargs)
172 """
173 headers = OrderedDict()
174 try:
175 reported_elt = form_xml.elements('jabber:x:data', 'reported').next()
176 except StopIteration:
177 raise exceptions.DataError("Couldn't find expected <reported> tag in %s" % form_xml.toXml())
178
179 for elt in reported_elt.elements():
180 if elt.name != "field":
181 raise exceptions.DataError("Unexpected tag")
182 name = elt["var"]
183 label = elt.attributes.get('label', '')
184 type_ = elt.attributes.get('type')
185 headers[name] = (label, type_)
186
187 if not headers:
188 raise exceptions.DataError("No reported fields (see XEP-0004 §3.4)")
189
190 xmlui_data = []
191 item_elts = form_xml.elements('jabber:x:data', 'item')
192
193 for item_elt in item_elts:
194 for elt in item_elt.elements():
195 if elt.name != 'field':
196 log.warning(u"Unexpected tag (%s)" % elt.name)
197 continue
198 field = data_form.Field.fromElement(elt)
199
200 xmlui_data.append(_dataFormField2XMLUIData(field))
201
202 return headers, xmlui_data
203
204
205 def XMLUIData2AdvancedList(xmlui, headers, xmlui_data):
206 """Take a raw data form result (not parsed by Wokkel's XEP-0004 implementation) and convert it to an advanced list.
207
208 The raw data form is used because Wokkel doesn't manage result items parsing yet.
209 @param xmlui (XMLUI): the XMLUI where the AdvancedList will be added
210 @param headers (dict{unicode: unicode}): form headers (field labels and types)
211 @param xmlui_data (list[tuple]): list of (widget_type, widget_args, widget_kwargs)
212 @return: the completed XMLUI instance
213 """
214 adv_list = AdvancedListContainer(xmlui, headers=headers, columns=len(headers), parent=xmlui.current_container)
215 xmlui.changeContainer(adv_list)
216
217 for widget_type, widget_args, widget_kwargs in xmlui_data:
218 xmlui.addWidget(widget_type, *widget_args, **widget_kwargs)
219
220 return xmlui
221
222
223 def dataFormResult2AdvancedList(xmlui, form_xml):
224 """Take a raw data form result (not parsed by Wokkel's XEP-0004 implementation) and convert it to an advanced list.
225
226 The raw data form is used because Wokkel doesn't manage result items parsing yet.
227 @param xmlui (XMLUI): the XMLUI where the AdvancedList will be added
228 @param form_xml (domish.Element): element of the data form
229 @return: the completed XMLUI instance
230 """
231 headers, xmlui_data = dataFormEltResult2XMLUIData(form_xml)
232 XMLUIData2AdvancedList(xmlui, headers, xmlui_data)
233
234
235 def dataFormEltResult2XMLUI(form_elt, session_id=None):
236 """Take a raw data form (not parsed by XEP-0004) and convert it to a SàT XMLUI.
237
238 The raw data form is used because Wokkel doesn't manage result items parsing yet.
239 @param form_elt (domish.Element): element of the data form
240 @param session_id (unicode): session id to return with the data
241 @return: XMLUI instance
242 """
243 xml_ui = XMLUI("window", "vertical", session_id=session_id)
244 try:
245 dataFormResult2AdvancedList(xml_ui, form_elt)
246 except exceptions.DataError:
247 parsed_form = data_form.Form.fromElement(form_elt)
248 dataForm2Widgets(xml_ui, parsed_form, read_only=True)
249 return xml_ui
250
251 def dataFormResult2XMLUI(result_form, base_form, session_id=None, prepend=None, filters=None):
252 """Convert data form result to SàT XMLUI.
253
254 @param result_form (data_form.Form): result form to convert
255 @param base_form (data_form.Form): initial form (i.e. of form type "form")
256 this one is necessary to reconstruct options when needed (e.g. list elements)
257 @param session_id (unicode): session id to return with the data
258 @param prepend: same as for [dataForm2Widgets]
259 @param filters: same as for [dataForm2Widgets]
260 @return: XMLUI instance
261 """
262 form = deepcopy(result_form)
263 for name, field in form.fields.iteritems():
264 try:
265 base_field = base_form.fields[name]
266 except KeyError:
267 continue
268 field.options = base_field.options[:]
269 xml_ui = XMLUI("window", "vertical", session_id=session_id)
270 dataForm2Widgets(xml_ui, form, read_only=True, prepend=prepend, filters=filters)
271 return xml_ui
272
273
274 def _cleanValue(value):
275 """Workaround method to avoid DBus types with D-Bus bridge.
276
277 @param value: value to clean
278 @return: value in a non DBus type (only clean string yet)
279 """
280 # XXX: must be removed when DBus types will no cause problems anymore
281 # FIXME: should be cleaned inside D-Bus bridge itself
282 if isinstance(value, basestring):
283 return unicode(value)
284 return value
285
286
287 def XMLUIResult2DataFormResult(xmlui_data):
288 """ Extract form data from a XMLUI return.
289
290 @param xmlui_data (dict): data returned by frontends for XMLUI form
291 @return: dict of data usable by Wokkel's data form
292 """
293 return {key[len(SAT_FORM_PREFIX):]: _cleanValue(value) for key, value in xmlui_data.iteritems() if key.startswith(SAT_FORM_PREFIX)}
294
295
296 def formEscape(name):
297 """Return escaped name for forms.
298
299 @param name (unicode): form name
300 @return: unicode
301 """
302 return u"%s%s" % (SAT_FORM_PREFIX, name)
303
304
305 def XMLUIResultToElt(xmlui_data):
306 """Construct result domish.Element from XMLUI result.
307
308 @param xmlui_data (dict): data returned by frontends for XMLUI form
309 @return: domish.Element
310 """
311 form = data_form.Form('submit')
312 form.makeFields(XMLUIResult2DataFormResult(xmlui_data))
313 return form.toElement()
314
315
316 def tupleList2dataForm(values):
317 """Convert a list of tuples (name, value) to a wokkel submit data form.
318
319 @param values (list): list of tuples
320 @return: data_form.Form
321 """
322 form = data_form.Form('submit')
323 for value in values:
324 field = data_form.Field(var=value[0], value=value[1])
325 form.addField(field)
326
327 return form
328
329
330 def paramsXML2XMLUI(xml):
331 """Convert the XML for parameter to a SàT XML User Interface.
332
333 @param xml (unicode)
334 @return: XMLUI
335 """
336 # TODO: refactor params and use Twisted directly to parse XML
337 params_doc = minidom.parseString(xml.encode('utf-8'))
338 top = params_doc.documentElement
339 if top.nodeName != 'params':
340 raise exceptions.DataError(_('INTERNAL ERROR: parameters xml not valid'))
341
342 param_ui = XMLUI("param", "tabs")
343 tabs_cont = param_ui.current_container
344
345 for category in top.getElementsByTagName("category"):
346 category_name = category.getAttribute('name')
347 label = category.getAttribute('label')
348 if not category_name:
349 raise exceptions.DataError(_('INTERNAL ERROR: params categories must have a name'))
350 tabs_cont.addTab(category_name, label=label, container=LabelContainer)
351 for param in category.getElementsByTagName("param"):
352 widget_kwargs = {}
353
354 param_name = param.getAttribute('name')
355 param_label = param.getAttribute('label')
356 type_ = param.getAttribute('type')
357 if not param_name and type_ != 'text':
358 raise exceptions.DataError(_('INTERNAL ERROR: params must have a name'))
359
360 value = param.getAttribute('value') or None
361 callback_id = param.getAttribute('callback_id') or None
362
363 if type_ == 'list':
364 options, selected = _paramsGetListOptions(param)
365 widget_kwargs['options'] = options
366 widget_kwargs['selected'] = selected
367 widget_kwargs['styles'] = ['extensible']
368 elif type_ == 'jids_list':
369 widget_kwargs['jids'] = _paramsGetListJids(param)
370
371 if type_ in ("button", "text"):
372 param_ui.addEmpty()
373 value = param_label
374 else:
375 param_ui.addLabel(param_label or param_name)
376
377 if value:
378 widget_kwargs["value"] = value
379
380 if callback_id:
381 widget_kwargs['callback_id'] = callback_id
382 others = ["%s%s%s" % (category_name, SAT_PARAM_SEPARATOR, other.getAttribute('name'))
383 for other in category.getElementsByTagName('param')
384 if other.getAttribute('type') != 'button']
385 widget_kwargs['fields_back'] = others
386
387 widget_kwargs['name'] = "%s%s%s" % (category_name, SAT_PARAM_SEPARATOR, param_name)
388
389 param_ui.addWidget(type_, **widget_kwargs)
390
391 return param_ui.toXml()
392
393
394 def _paramsGetListOptions(param):
395 """Retrieve the options for list element.
396
397 The <option/> tags must be direct children of <param/>.
398 @param param (domish.Element): element
399 @return: a tuple (options, selected_value)
400 """
401 if len(param.getElementsByTagName("options")) > 0:
402 raise exceptions.DataError(_("The 'options' tag is not allowed in parameter of type 'list'!"))
403 elems = param.getElementsByTagName("option")
404 if len(elems) == 0:
405 return []
406 options = [elem.getAttribute("value") for elem in elems]
407 selected = [elem.getAttribute("value") for elem in elems if elem.getAttribute("selected") == 'true']
408 return (options, selected)
409
410 def _paramsGetListJids(param):
411 """Retrive jids from a jids_list element.
412
413 the <jid/> tags must be direct children of <param/>
414 @param param (domish.Element): element
415 @return: a list of jids
416 """
417 elems = param.getElementsByTagName("jid")
418 jids = [elem.firstChild.data for elem in elems
419 if elem.firstChild is not None
420 and elem.firstChild.nodeType == elem.TEXT_NODE]
421 return jids
422
423
424 ### XMLUI Elements ###
425
426
427 class Element(object):
428 """ Base XMLUI element """
429 type = None
430
431 def __init__(self, xmlui, parent=None):
432 """Create a container element
433
434 @param xmlui: XMLUI instance
435 @parent: parent element
436 """
437 assert self.type is not None
438 self.children = []
439 if not hasattr(self, 'elem'):
440 self.elem = parent.xmlui.doc.createElement(self.type)
441 self.xmlui = xmlui
442 if parent is not None:
443 parent.append(self)
444 self.parent = parent
445
446 def append(self, child):
447 """Append a child to this element.
448
449 @param child (Element): child element
450 @return: the added child Element
451 """
452 self.elem.appendChild(child.elem)
453 child.parent = self
454 self.children.append(child)
455 return child
456
457
458 class TopElement(Element):
459 """ Main XML Element """
460 type = 'top'
461
462 def __init__(self, xmlui):
463 self.elem = xmlui.doc.documentElement
464 super(TopElement, self).__init__(xmlui)
465
466
467 class TabElement(Element):
468 """ Used by TabsContainer to give name and label to tabs."""
469 type = 'tab'
470
471 def __init__(self, parent, name, label, selected=False):
472 """
473
474 @param parent (TabsContainer): parent container
475 @param name (unicode): tab name
476 @param label (unicode): tab label
477 @param selected (bool): set to True to select this tab
478 """
479 if not isinstance(parent, TabsContainer):
480 raise exceptions.DataError(_("TabElement must be a child of TabsContainer"))
481 super(TabElement, self).__init__(parent.xmlui, parent)
482 self.elem.setAttribute('name', name)
483 self.elem.setAttribute('label', label)
484 if selected:
485 self.setSelected(selected)
486
487 def setSelected(self, selected=False):
488 """Set the tab selected.
489
490 @param selected (bool): set to True to select this tab
491 """
492 self.elem.setAttribute('selected', 'true' if selected else 'false')
493
494
495 class FieldBackElement(Element):
496 """ Used by ButtonWidget to indicate which field have to be sent back """
497 type = 'field_back'
498
499 def __init__(self, parent, name):
500 assert isinstance(parent, ButtonWidget)
501 super(FieldBackElement, self).__init__(parent.xmlui, parent)
502 self.elem.setAttribute('name', name)
503
504
505 class InternalFieldElement(Element):
506 """ Used by internal callbacks to indicate which fields are manipulated """
507 type = 'internal_field'
508
509 def __init__(self, parent, name):
510 super(InternalFieldElement, self).__init__(parent.xmlui, parent)
511 self.elem.setAttribute('name', name)
512
513
514 class InternalDataElement(Element):
515 """ Used by internal callbacks to retrieve extra data """
516 type = 'internal_data'
517
518 def __init__(self, parent, children):
519 super(InternalDataElement, self).__init__(parent.xmlui, parent)
520 assert isinstance(children, list)
521 for child in children:
522 self.elem.childNodes.append(child)
523
524
525 class OptionElement(Element):
526 """" Used by ListWidget to specify options """
527 type = 'option'
528
529 def __init__(self, parent, option, selected=False):
530 """
531
532 @param parent
533 @param option (string, tuple)
534 @param selected (boolean)
535 """
536 assert isinstance(parent, ListWidget)
537 super(OptionElement, self).__init__(parent.xmlui, parent)
538 if isinstance(option, basestring):
539 value, label = option, option
540 elif isinstance(option, tuple):
541 value, label = option
542 else:
543 raise NotImplementedError
544 self.elem.setAttribute('value', value)
545 self.elem.setAttribute('label', label)
546 if selected:
547 self.elem.setAttribute('selected', 'true')
548
549
550 class JidElement(Element):
551 """" Used by JidsListWidget to specify jids"""
552 type = 'jid'
553
554 def __init__(self, parent, jid_):
555 """
556 @param jid_(jid.JID, unicode): jid to append
557 """
558 assert isinstance(parent, JidsListWidget)
559 super(JidElement, self).__init__(parent.xmlui, parent)
560 if isinstance(jid_, jid.JID):
561 value = jid_.full()
562 elif isinstance(jid_, basestring):
563 value = unicode(jid_)
564 else:
565 raise NotImplementedError
566 jid_txt = self.xmlui.doc.createTextNode(value)
567 self.elem.appendChild(jid_txt)
568
569
570 class RowElement(Element):
571 """" Used by AdvancedListContainer """
572 type = 'row'
573
574 def __init__(self, parent):
575 assert isinstance(parent, AdvancedListContainer)
576 super(RowElement, self).__init__(parent.xmlui, parent)
577 if parent.next_row_idx is not None:
578 if parent.auto_index:
579 raise exceptions.DataError(_("Can't set row index if auto_index is True"))
580 self.elem.setAttribute('index', parent.next_row_idx)
581 parent.next_row_idx = None
582
583
584 class HeaderElement(Element):
585 """" Used by AdvancedListContainer """
586 type = 'header'
587
588 def __init__(self, parent, name=None, label=None, description=None):
589 """
590 @param parent: AdvancedListContainer instance
591 @param name: name of the container
592 @param label: label to be displayed in columns
593 @param description: long descriptive text
594 """
595 assert isinstance(parent, AdvancedListContainer)
596 super(HeaderElement, self).__init__(parent.xmlui, parent)
597 if name:
598 self.elem.setAttribute('name', name)
599 if label:
600 self.elem.setAttribute('label', label)
601 if description:
602 self.elem.setAttribute('description', description)
603
604
605 ## Containers ##
606
607
608 class Container(Element):
609 """ And Element which contains other ones and has a layout """
610 type = None
611
612 def __init__(self, xmlui, parent=None):
613 """Create a container element
614
615 @param xmlui: XMLUI instance
616 @parent: parent element or None
617 """
618 self.elem = xmlui.doc.createElement('container')
619 super(Container, self).__init__(xmlui, parent)
620 self.elem.setAttribute('type', self.type)
621
622 def getParentContainer(self):
623 """ Return first parent container
624
625 @return: parent container or None
626 """
627 current = self.parent
628 while(not isinstance(current, (Container)) and
629 current is not None):
630 current = current.parent
631 return current
632
633
634 class VerticalContainer(Container):
635 type = "vertical"
636
637
638 class HorizontalContainer(Container):
639 type = "horizontal"
640
641
642 class PairsContainer(Container):
643 type = "pairs"
644
645
646 class LabelContainer(Container):
647 type = "label"
648
649
650 class TabsContainer(Container):
651 type = "tabs"
652
653 def addTab(self, name, label=None, selected=None, container=VerticalContainer):
654 """Add a tab.
655
656 @param name (unicode): tab name
657 @param label (unicode): tab label
658 @param selected (bool): set to True to select this tab
659 @param container (class): container class, inheriting from Container
660 @return: the container for the new tab
661 """
662 if not label:
663 label = name
664 tab_elt = TabElement(self, name, label, selected)
665 new_container = container(self.xmlui, tab_elt)
666 return self.xmlui.changeContainer(new_container)
667
668 def end(self):
669 """ Called when we have finished tabs
670
671 change current container to first container parent
672 """
673 parent_container = self.getParentContainer()
674 self.xmlui.changeContainer(parent_container)
675
676
677 class AdvancedListContainer(Container):
678 """A list which can contain other widgets, headers, etc"""
679 type = "advanced_list"
680
681 def __init__(self, xmlui, callback_id=None, name=None, headers=None, items=None, columns=None, selectable='no', auto_index=False, parent=None):
682 """Create an advanced list
683
684 @param headers: optional headers information
685 @param callback_id: id of the method to call when selection is done
686 @param items: list of widgets to add (just the first row)
687 @param columns: number of columns in this table, or None to autodetect
688 @param selectable: one of:
689 'no': nothing is done
690 'single': one row can be selected
691 @param auto_index: if True, indexes will be generated by frontends, starting from 0
692 @return: created element
693 """
694 assert selectable in ('no', 'single')
695 if not items and columns is None:
696 raise exceptions.DataError(_("either items or columns need do be filled"))
697 if headers is None:
698 headers = []
699 if items is None:
700 items = []
701 super(AdvancedListContainer, self).__init__(xmlui, parent)
702 if columns is None:
703 columns = len(items[0])
704 self._columns = columns
705 self._item_idx = 0
706 self.current_row = None
707 if headers:
708 if len(headers) != self._columns:
709 raise exceptions.DataError(_("Headers lenght doesn't correspond to columns"))
710 self.addHeaders(headers)
711 if items:
712 self.addItems(items)
713 self.elem.setAttribute('columns', str(self._columns))
714 if callback_id is not None:
715 self.elem.setAttribute('callback', callback_id)
716 self.elem.setAttribute('selectable', selectable)
717 self.auto_index = auto_index
718 if auto_index:
719 self.elem.setAttribute('auto_index', 'true')
720 self.next_row_idx = None
721
722 def addHeaders(self, headers):
723 for header in headers:
724 self.addHeader(header)
725
726 def addHeader(self, header):
727 pass # TODO
728
729 def addItems(self, items):
730 for item in items:
731 self.append(item)
732
733 def setRowIndex(self, idx):
734 """ Set index for next row
735
736 index are returned when a row is selected, in data's "index" key
737 @param idx: string index to associate to the next row
738 """
739 self.next_row_idx = idx
740
741 def append(self, child):
742 if isinstance(child, RowElement):
743 return super(AdvancedListContainer, self).append(child)
744 if self._item_idx % self._columns == 0:
745 self.current_row = RowElement(self)
746 self.current_row.append(child)
747 self._item_idx += 1
748
749 def end(self):
750 """ Called when we have finished list
751
752 change current container to first container parent
753 """
754 if self._item_idx % self._columns != 0:
755 raise exceptions.DataError(_("Incorrect number of items in list"))
756 parent_container = self.getParentContainer()
757 self.xmlui.changeContainer(parent_container)
758
759
760 ## Widgets ##
761
762
763 class Widget(Element):
764 type = None
765
766 def __init__(self, xmlui, name=None, parent=None):
767 """Create an element
768
769 @param xmlui: XMLUI instance
770 @param name: name of the element or None
771 @param parent: parent element or None
772 """
773 self.elem = xmlui.doc.createElement('widget')
774 super(Widget, self).__init__(xmlui, parent)
775 if name:
776 self.elem.setAttribute('name', name)
777 if name in xmlui.named_widgets:
778 raise exceptions.ConflictError(_(u'A widget with the name "{name}" already exists.').format(name=name))
779 xmlui.named_widgets[name] = self
780 self.elem.setAttribute('type', self.type)
781
782 def setInternalCallback(self, callback, fields, data_elts=None):
783 """Set an internal UI callback when the widget value is changed.
784
785 The internal callbacks are NO callback ids, they are strings from
786 a predefined set of actions that are running in the scope of XMLUI.
787 @param callback (string): a value from:
788 - 'copy': process the widgets given in 'fields' two by two, by
789 copying the values of one widget to the other. Target widgets
790 of type List do not accept the empty value.
791 - 'move': same than copy but moves the values if the source widget
792 is not a List.
793 - 'groups_of_contact': process the widgets two by two, assume A is
794 is a list of JID and B a list of groups, select in B the groups
795 to which the JID selected in A belongs.
796 - more operation to be added when necessary...
797 @param fields (list): a list of widget names (string)
798 @param data_elts (list[Element]): extra data elements
799 """
800 self.elem.setAttribute('internal_callback', callback)
801 if fields:
802 for field in fields:
803 InternalFieldElement(self, field)
804 if data_elts:
805 InternalDataElement(self, data_elts)
806
807
808 class EmptyWidget(Widget):
809 """Place holder widget"""
810 type = 'empty'
811
812
813 class TextWidget(Widget):
814 """Used for blob of text"""
815 type = 'text'
816
817 def __init__(self, xmlui, value, name=None, parent=None):
818 super(TextWidget, self).__init__(xmlui, name, parent)
819 value_elt = self.xmlui.doc.createElement('value')
820 text = self.xmlui.doc.createTextNode(value)
821 value_elt.appendChild(text)
822 self.elem.appendChild(value_elt)
823
824 @property
825 def value(self):
826 return self.elem.firstChild.firstChild.wholeText
827
828
829 class LabelWidget(Widget):
830 """One line blob of text
831
832 used most of time to display the desciption or name of the next widget
833 """
834 type = 'label'
835
836 def __init__(self, xmlui, label, name=None, parent=None):
837 super(LabelWidget, self).__init__(xmlui, name, parent)
838 self.elem.setAttribute('value', label)
839
840
841 class JidWidget(Widget):
842 """Used to display a Jabber ID, some specific methods can be added"""
843 type = 'jid'
844
845 def __init__(self, xmlui, jid, name=None, parent=None):
846 super(JidWidget, self).__init__(xmlui, name, parent)
847 try:
848 self.elem.setAttribute('value', jid.full())
849 except AttributeError:
850 self.elem.setAttribute('value', unicode(jid))
851
852
853 class DividerWidget(Widget):
854 type = 'divider'
855
856 def __init__(self, xmlui, style='line', name=None, parent=None):
857 """ Create a divider
858
859 @param xmlui: XMLUI instance
860 @param style: one of:
861 - line: a simple line
862 - dot: a line of dots
863 - dash: a line of dashes
864 - plain: a full thick line
865 - blank: a blank line/space
866 @param name: name of the widget
867 @param parent: parent container
868
869 """
870 super(DividerWidget, self).__init__(xmlui, name, parent)
871 self.elem.setAttribute('style', style)
872
873
874 ### Inputs ###
875
876
877 class InputWidget(Widget):
878 """Widget which can accept user inputs
879
880 used mainly in forms
881 """
882 def __init__(self, xmlui, name=None, parent=None, read_only=False):
883 super(InputWidget, self).__init__(xmlui, name, parent)
884 if read_only:
885 self.elem.setAttribute('read_only', 'true')
886
887
888 class StringWidget(InputWidget):
889 type = 'string'
890
891 def __init__(self, xmlui, value=None, name=None, parent=None, read_only=False):
892 super(StringWidget, self).__init__(xmlui, name, parent, read_only=read_only)
893 if value:
894 value_elt = self.xmlui.doc.createElement('value')
895 text = self.xmlui.doc.createTextNode(value)
896 value_elt.appendChild(text)
897 self.elem.appendChild(value_elt)
898
899 @property
900 def value(self):
901 return self.elem.firstChild.firstChild.wholeText
902
903
904 class PasswordWidget(StringWidget):
905 type = 'password'
906
907
908 class TextBoxWidget(StringWidget):
909 type = 'textbox'
910
911
912 class JidInputWidget(StringWidget):
913 type = 'jid_input'
914
915
916 # TODO handle min and max values
917 class IntWidget(StringWidget):
918 type = 'int'
919
920 def __init__(self, xmlui, value=0, name=None, parent=None, read_only=False):
921 try:
922 int(value)
923 except ValueError:
924 raise exceptions.DataError(_("Value must be an integer"))
925 super(IntWidget, self).__init__(xmlui, value, name, parent, read_only=read_only)
926
927
928 class BoolWidget(InputWidget):
929 type = 'bool'
930
931 def __init__(self, xmlui, value='false', name=None, parent=None, read_only=False):
932 if isinstance(value, bool):
933 value = 'true' if value else 'false'
934 elif value == '0':
935 value = 'false'
936 elif value == '1':
937 value = 'true'
938 if value not in ('true', 'false'):
939 raise exceptions.DataError(_("Value must be 0, 1, false or true"))
940 super(BoolWidget, self).__init__(xmlui, name, parent, read_only=read_only)
941 self.elem.setAttribute('value', value)
942
943
944 class ButtonWidget(Widget):
945 type = 'button'
946
947 def __init__(self, xmlui, callback_id, value=None, fields_back=None, name=None, parent=None):
948 """Add a button
949
950 @param callback_id: callback which will be called if button is pressed
951 @param value: label of the button
952 @param fields_back: list of names of field to give back when pushing the button
953 @param name: name
954 @param parent: parent container
955 """
956 if fields_back is None:
957 fields_back = []
958 super(ButtonWidget, self).__init__(xmlui, name, parent)
959 self.elem.setAttribute('callback', callback_id)
960 if value:
961 self.elem.setAttribute('value', value)
962 for field in fields_back:
963 FieldBackElement(self, field)
964
965
966 class ListWidget(InputWidget):
967 type = 'list'
968 STYLES = (u'multi', u'noselect', u'extensible', u'reducible', u'inline')
969
970 def __init__(self, xmlui, options, selected=None, styles=None, name=None, parent=None):
971 """
972
973 @param xmlui
974 @param options (list[option]): each option can be given as:
975 - a single string if the label and the value are the same
976 - a tuple with a couple of string (value,label) if the label and the value differ
977 @param selected (list[string]): list of the selected values
978 @param styles (iterable[string]): flags to set the behaviour of the list
979 can be:
980 - multi: multiple selection is allowed
981 - noselect: no selection is allowed
982 useful when only the list itself is needed
983 - extensible: can be extended by user (i.e. new options can be added)
984 - reducible: can be reduced by user (i.e. options can be removed)
985 - inline: hint that this list should be displayed on a single line (e.g. list of labels)
986 @param name (string)
987 @param parent
988 """
989 styles = set() if styles is None else set(styles)
990 if styles is None:
991 styles = set()
992 else:
993 styles = set(styles)
994 if u'noselect' in styles and (u'multi' in styles or selected):
995 raise exceptions.DataError(_(u'"multi" flag and "selected" option are not compatible with "noselect" flag'))
996 if not options:
997 # we can have no options if we get a submitted data form
998 # but we can't use submitted values directly,
999 # because we would not have the labels
1000 log.warning(_('empty "options" list'))
1001 super(ListWidget, self).__init__(xmlui, name, parent)
1002 self.addOptions(options, selected)
1003 self.setStyles(styles)
1004
1005 def addOptions(self, options, selected=None):
1006 """Add options to a multi-values element (e.g. list) """
1007 if selected:
1008 if isinstance(selected, basestring):
1009 selected = [selected]
1010 else:
1011 selected = []
1012 for option in options:
1013 assert isinstance(option, basestring) or isinstance(option, tuple)
1014 value = option if isinstance(option, basestring) else option[0]
1015 OptionElement(self, option, value in selected)
1016
1017 def setStyles(self, styles):
1018 if not styles.issubset(self.STYLES):
1019 raise exceptions.DataError(_(u"invalid styles"))
1020 for style in styles:
1021 self.elem.setAttribute(style, 'yes')
1022 # TODO: check flags incompatibily (noselect and multi) like in __init__
1023
1024 def setStyle(self, style):
1025 self.setStyles([style])
1026
1027 @property
1028 def value(self):
1029 """Return the value of first selected option"""
1030 for child in self.elem.childNodes:
1031 if child.tagName == u'option' and child.getAttribute(u'selected') == u'true':
1032 return child.getAttribute(u'value')
1033 return u''
1034
1035 class JidsListWidget(InputWidget):
1036 """A list of text or jids where elements can be added/removed or modified"""
1037 type = 'jids_list'
1038
1039 def __init__(self, xmlui, jids, styles=None, name=None, parent=None):
1040 """
1041
1042 @param xmlui
1043 @param jids (list[jid.JID]): base jids
1044 @param styles (iterable[string]): flags to set the behaviour of the list
1045 @param name (string)
1046 @param parent
1047 """
1048 super(JidsListWidget, self).__init__(xmlui, name, parent)
1049 styles = set() if styles is None else set(styles)
1050 if not styles.issubset([]): # TODO
1051 raise exceptions.DataError(_("invalid styles"))
1052 for style in styles:
1053 self.elem.setAttribute(style, 'yes')
1054 if not jids:
1055 log.debug('empty jids list')
1056 else:
1057 self.addJids(jids)
1058
1059 def addJids(self, jids):
1060 for jid_ in jids:
1061 JidElement(self, jid_)
1062
1063
1064 ## Dialog Elements ##
1065
1066
1067 class DialogElement(Element):
1068 """Main dialog element """
1069 type = 'dialog'
1070
1071 def __init__(self, parent, type_, level=None):
1072 if not isinstance(parent, TopElement):
1073 raise exceptions.DataError(_("DialogElement must be a direct child of TopElement"))
1074 super(DialogElement, self).__init__(parent.xmlui, parent)
1075 self.elem.setAttribute(C.XMLUI_DATA_TYPE, type_)
1076 self.elem.setAttribute(C.XMLUI_DATA_LVL, level or C.XMLUI_DATA_LVL_DEFAULT)
1077
1078
1079 class MessageElement(Element):
1080 """Element with the instruction message"""
1081 type = C.XMLUI_DATA_MESS
1082
1083 def __init__(self, parent, message):
1084 if not isinstance(parent, DialogElement):
1085 raise exceptions.DataError(_("MessageElement must be a direct child of DialogElement"))
1086 super(MessageElement, self).__init__(parent.xmlui, parent)
1087 message_txt = self.xmlui.doc.createTextNode(message)
1088 self.elem.appendChild(message_txt)
1089
1090
1091 class ButtonsElement(Element):
1092 """Buttons element which indicate which set to use"""
1093 type = 'buttons'
1094
1095 def __init__(self, parent, set_):
1096 if not isinstance(parent, DialogElement):
1097 raise exceptions.DataError(_("ButtonsElement must be a direct child of DialogElement"))
1098 super(ButtonsElement, self).__init__(parent.xmlui, parent)
1099 self.elem.setAttribute('set', set_)
1100
1101
1102 class FileElement(Element):
1103 """File element used for FileDialog"""
1104 type = 'file'
1105
1106 def __init__(self, parent, type_):
1107 if not isinstance(parent, DialogElement):
1108 raise exceptions.DataError(_("FileElement must be a direct child of DialogElement"))
1109 super(FileElement, self).__init__(parent.xmlui, parent)
1110 self.elem.setAttribute('type', type_)
1111
1112
1113 ## XMLUI main class
1114
1115
1116 class XMLUI(object):
1117 """This class is used to create a user interface (form/window/parameters/etc) using SàT XML"""
1118
1119 def __init__(self, panel_type="window", container="vertical", dialog_opt=None, title=None, submit_id=None, session_id=None):
1120 """Init SàT XML Panel
1121
1122 @param panel_type: one of
1123 - C.XMLUI_WINDOW (new window)
1124 - C.XMLUI_POPUP
1125 - C.XMLUI_FORM (form, depend of the frontend, usually a panel with cancel/submit buttons)
1126 - C.XMLUI_PARAM (parameters, presentation depend of the frontend)
1127 - C.XMLUI_DIALOG (one common dialog, presentation depend of frontend)
1128 @param container: disposition of elements, one of:
1129 - vertical: elements are disposed up to bottom
1130 - horizontal: elements are disposed left to right
1131 - pairs: elements come on two aligned columns
1132 (usually one for a label, the next for the element)
1133 - label: associations of one LabelWidget or EmptyWidget with an other widget
1134 similar to pairs but specialized in LabelWidget, and not necessarily arranged in 2 columns
1135 - tabs: elemens are in categories with tabs (notebook)
1136 @param dialog_opt: only used if panel_type == C.XMLUI_DIALOG. Dictionnary (string/string) where key can be:
1137 - C.XMLUI_DATA_TYPE: type of dialog, value can be:
1138 - C.XMLUI_DIALOG_MESSAGE (default): an information/error message. Action of user is necessary to close the dialog. Usually the frontend display a classic popup
1139 - C.XMLUI_DIALOG_NOTE: like a C.XMLUI_DIALOG_MESSAGE, but action of user is not necessary to close, at frontend choice (it can be closed after a timeout). Usually the frontend display as a timed out notification
1140 - C.XMLUI_DIALOG_CONFIRM: dialog with 2 choices (usualy "Ok"/"Cancel").
1141 returned data can contain:
1142 - "answer": "true" if answer is "ok", "yes" or equivalent, "false" else
1143 - C.XLMUI_DIALOG_FILE: a file selection dialog
1144 returned data can contain:
1145 - "cancelled": "true" if dialog has been cancelled, not present or "false" else
1146 - "path": path of the choosed file/dir
1147 - C.XMLUI_DATA_MESS: message shown in dialog
1148 - C.XMLUI_DATA_LVL: one of:
1149 - C.XMLUI_DATA_LVL_INFO (default): normal message
1150 - C.XMLUI_DATA_LVL_WARNING: attention of user is important
1151 - C.XMLUI_DATA_LVL_ERROR: something went wrong
1152 - C.XMLUI_DATA_BTNS_SET: one of:
1153 - C.XMLUI_DATA_BTNS_SET_OKCANCEL (default): classical "OK" and "Cancel" set
1154 - C.XMLUI_DATA_BTNS_SET_YESNO: a translated "yes" for OK, and "no" for Cancel
1155 - C.XMLUI_DATA_FILETYPE: only used for file dialogs, one of:
1156 - C.XMLUI_DATA_FILETYPE_FILE: a file path is requested
1157 - C.XMLUI_DATA_FILETYPE_DIR: a dir path is requested
1158 - C.XMLUI_DATA_FILETYPE_DEFAULT: same as C.XMLUI_DATA_FILETYPE_FILE
1159
1160 @param title: title or default if None
1161 @param submit_id: callback id to call for panel_type we can submit (form, param, dialog)
1162 @param session_id: use to keep a session attached to the dialog, must be returned by frontends
1163 @attribute named_widgets(dict): map from name to widget
1164 """
1165 self._introspect() # FIXME: why doing that on each XMLUI ? should be done once
1166 if panel_type not in [C.XMLUI_WINDOW, C.XMLUI_FORM, C.XMLUI_PARAM, C.XMLUI_POPUP, C.XMLUI_DIALOG]:
1167 raise exceptions.DataError(_("Unknown panel type [%s]") % panel_type)
1168 if panel_type == C.XMLUI_FORM and submit_id is None:
1169 raise exceptions.DataError(_("form XMLUI need a submit_id"))
1170 if not isinstance(container, basestring):
1171 raise exceptions.DataError(_("container argument must be a string"))
1172 if dialog_opt is not None and panel_type != C.XMLUI_DIALOG:
1173 raise exceptions.DataError(_("dialog_opt can only be used with dialog panels"))
1174 self.type = panel_type
1175 impl = minidom.getDOMImplementation()
1176
1177 self.doc = impl.createDocument(None, "sat_xmlui", None)
1178 top_element = self.doc.documentElement
1179 top_element.setAttribute("type", panel_type)
1180 if title:
1181 top_element.setAttribute("title", title)
1182 self.submit_id = submit_id
1183 self.session_id = session_id
1184 if panel_type == C.XMLUI_DIALOG:
1185 if dialog_opt is None:
1186 dialog_opt = {}
1187 self._createDialog(dialog_opt)
1188 return
1189 self.main_container = self._createContainer(container, TopElement(self))
1190 self.current_container = self.main_container
1191 self.named_widgets = {}
1192
1193 def _introspect(self):
1194 """ Introspect module to find Widgets and Containers """
1195 self._containers = {}
1196 self._widgets = {}
1197 for obj in globals().values():
1198 try:
1199 if issubclass(obj, Widget):
1200 if obj.__name__ == 'Widget':
1201 continue
1202 self._widgets[obj.type] = obj
1203 elif issubclass(obj, Container):
1204 if obj.__name__ == 'Container':
1205 continue
1206 self._containers[obj.type] = obj
1207 except TypeError:
1208 pass
1209
1210 def __del__(self):
1211 self.doc.unlink()
1212
1213 def __getattr__(self, name):
1214 if name.startswith("add") and name not in ('addWidget',): # addWidgetName(...) create an instance of WidgetName
1215 if self.type == C.XMLUI_DIALOG:
1216 raise exceptions.InternalError(_("addXXX can't be used with dialogs"))
1217 class_name = name[3:] + "Widget"
1218 if class_name in globals():
1219 cls = globals()[class_name]
1220 if issubclass(cls, Widget):
1221 def createWidget(*args, **kwargs):
1222 if "parent" not in kwargs:
1223 kwargs["parent"] = self.current_container
1224 if "name" not in kwargs and issubclass(cls, InputWidget): # name can be given as first argument or in keyword arguments for InputWidgets
1225 args = list(args)
1226 kwargs["name"] = args.pop(0)
1227 return cls(self, *args, **kwargs)
1228 return createWidget
1229 return object.__getattribute__(self, name)
1230
1231 @property
1232 def submit_id(self):
1233 top_element = self.doc.documentElement
1234 if not top_element.hasAttribute("submit"):
1235 # getAttribute never return None (it return empty string it attribute doesn't exists)
1236 # so we have to manage None here
1237 return None
1238 value = top_element.getAttribute("submit")
1239 return value
1240
1241 @submit_id.setter
1242 def submit_id(self, value):
1243 top_element = self.doc.documentElement
1244 if value is None:
1245 try:
1246 top_element.removeAttribute("submit")
1247 except NotFoundErr:
1248 pass
1249 else: # submit_id can be the empty string to bypass form restriction
1250 top_element.setAttribute("submit", value)
1251
1252 @property
1253 def session_id(self):
1254 top_element = self.doc.documentElement
1255 value = top_element.getAttribute("session_id")
1256 return value or None
1257
1258 @session_id.setter
1259 def session_id(self, value):
1260 top_element = self.doc.documentElement
1261 if value is None:
1262 try:
1263 top_element.removeAttribute("session_id")
1264 except NotFoundErr:
1265 pass
1266 elif value:
1267 top_element.setAttribute("session_id", value)
1268 else:
1269 raise exceptions.DataError("session_id can't be empty")
1270
1271 def _createDialog(self, dialog_opt):
1272 dialog_type = dialog_opt.setdefault(C.XMLUI_DATA_TYPE, C.XMLUI_DIALOG_MESSAGE)
1273 if dialog_type in [C.XMLUI_DIALOG_CONFIRM, C.XMLUI_DIALOG_FILE] and self.submit_id is None:
1274 raise exceptions.InternalError(_("Submit ID must be filled for this kind of dialog"))
1275 top_element = TopElement(self)
1276 level = dialog_opt.get(C.XMLUI_DATA_LVL)
1277 dialog_elt = DialogElement(top_element, dialog_type, level)
1278
1279 try:
1280 MessageElement(dialog_elt, dialog_opt[C.XMLUI_DATA_MESS])
1281 except KeyError:
1282 pass
1283
1284 try:
1285 ButtonsElement(dialog_elt, dialog_opt[C.XMLUI_DATA_BTNS_SET])
1286 except KeyError:
1287 pass
1288
1289 try:
1290 FileElement(dialog_elt, dialog_opt[C.XMLUI_DATA_FILETYPE])
1291 except KeyError:
1292 pass
1293
1294 def _createContainer(self, container, parent=None, **kwargs):
1295 """Create a container element
1296
1297 @param type: container type (cf init doc)
1298 @parent: parent element or None
1299 """
1300 if container not in self._containers:
1301 raise exceptions.DataError(_("Unknown container type [%s]") % container)
1302 cls = self._containers[container]
1303 new_container = cls(self, parent=parent, **kwargs)
1304 return new_container
1305
1306 def changeContainer(self, container, **kwargs):
1307 """Change the current container
1308
1309 @param container: either container type (container it then created),
1310 or an Container instance"""
1311 if isinstance(container, basestring):
1312 self.current_container = self._createContainer(container, self.current_container.getParentContainer() or self.main_container, **kwargs)
1313 else:
1314 self.current_container = self.main_container if container is None else container
1315 assert isinstance(self.current_container, Container)
1316 return self.current_container
1317
1318 def addWidget(self, type_, *args, **kwargs):
1319 """Convenience method to add an element"""
1320 if type_ not in self._widgets:
1321 raise exceptions.DataError(_("Invalid type [%s]") % type_)
1322 if "parent" not in kwargs:
1323 kwargs["parent"] = self.current_container
1324 cls = self._widgets[type_]
1325 return cls(self, *args, **kwargs)
1326
1327 def toXml(self):
1328 """return the XML representation of the panel"""
1329 return self.doc.toxml()
1330
1331
1332 # Some sugar for XMLUI dialogs
1333
1334 def note(message, title='', level=C.XMLUI_DATA_LVL_INFO):
1335 """sugar to easily create a Note Dialog
1336
1337 @param message(unicode): body of the note
1338 @param title(unicode): title of the note
1339 @param level(unicode): one of C.XMLUI_DATA_LVL_*
1340 @return(XMLUI): instance of XMLUI
1341 """
1342 note_xmlui = XMLUI(C.XMLUI_DIALOG, dialog_opt={
1343 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE,
1344 C.XMLUI_DATA_MESS: message,
1345 C.XMLUI_DATA_LVL: level},
1346 title=title
1347 )
1348 return note_xmlui
1349
1350
1351 def quickNote(host, client, message, title='', level=C.XMLUI_DATA_LVL_INFO):
1352 """more sugar to do the whole note process"""
1353 note_ui = note(message, title, level)
1354 host.actionNew({'xmlui': note_ui.toXml()}, profile=client.profile)
1355
1356
1357 def deferredUI(host, xmlui, chained=False):
1358 """create a deferred linked to XMLUI
1359
1360 @param xmlui(XMLUI): instance of the XMLUI
1361 Must be an XMLUI that you can submit, with submit_id set to ''
1362 @param chained(bool): True if the Deferred result must be returned to the frontend
1363 useful when backend is in a series of dialogs with an ui
1364 @return (D(data)): a deferred which fire the data
1365 """
1366 assert xmlui.submit_id == ''
1367 xmlui_d = defer.Deferred()
1368
1369 def onSubmit(data, profile):
1370 xmlui_d.callback(data)
1371 return xmlui_d if chained else {}
1372
1373 xmlui.submit_id = host.registerCallback(onSubmit, with_data=True, one_shot=True)
1374 return xmlui_d
1375
1376 def deferXMLUI(host, xmlui, action_extra=None, security_limit=C.NO_SECURITY_LIMIT, chained=False, profile=C.PROF_KEY_NONE):
1377 """Create a deferred linked to XMLUI
1378
1379 @param xmlui(XMLUI): instance of the XMLUI
1380 Must be an XMLUI that you can submit, with submit_id set to ''
1381 @param profile: %(doc_profile)s
1382 @param action_extra(None, dict): extra action to merge with xmlui
1383 mainly used to add meta informations (see actionNew doc)
1384 @param security_limit: %(doc_security_limit)s
1385 @param chained(bool): True if the Deferred result must be returned to the frontend
1386 useful when backend is in a series of dialogs with an ui
1387 @return (data): a deferred which fire the data
1388 """
1389 xmlui_d = deferredUI(host, xmlui, chained)
1390 action_data = {'xmlui': xmlui.toXml()}
1391 if action_extra is not None:
1392 action_data.update(action_extra)
1393 host.actionNew(action_data, security_limit=security_limit, keep_id=xmlui.submit_id, profile=profile)
1394 return xmlui_d
1395
1396 def deferDialog(host, message, title=u'Please confirm', type_=C.XMLUI_DIALOG_CONFIRM, options=None,
1397 action_extra=None, security_limit=C.NO_SECURITY_LIMIT, chained=False, profile=C.PROF_KEY_NONE):
1398 """Create a submitable dialog and manage it with a deferred
1399
1400 @param message(unicode): message to display
1401 @param title(unicode): title of the dialog
1402 @param type(unicode): dialog type (C.XMLUI_DIALOG_*)
1403 @param options(None, dict): if not None, will be used to update (extend) dialog_opt arguments of XMLUI
1404 @param action_extra(None, dict): extra action to merge with xmlui
1405 mainly used to add meta informations (see actionNew doc)
1406 @param security_limit: %(doc_security_limit)s
1407 @param chained(bool): True if the Deferred result must be returned to the frontend
1408 useful when backend is in a series of dialogs with an ui
1409 @param profile: %(doc_profile)s
1410 @return (dict): Deferred dict
1411 """
1412 assert profile is not None
1413 dialog_opt = {'type': type_, 'message': message}
1414 if options is not None:
1415 dialog_opt.update(options)
1416 dialog = XMLUI(C.XMLUI_DIALOG, title=title, dialog_opt=dialog_opt, submit_id='')
1417 return deferXMLUI(host, dialog, action_extra, security_limit, chained, profile)
1418
1419 def deferConfirm(*args, **kwargs):
1420 """call deferDialog and return a boolean instead of the whole data dict"""
1421 d = deferDialog(*args, **kwargs)
1422 d.addCallback(lambda data: C.bool(data['answer']))
1423 return d
1424
1425 # Misc other funtions
1426
1427 class ElementParser(object):
1428 """callable class to parse XML string into Element"""
1429 # XXX: Found at http://stackoverflow.com/questions/2093400/how-to-create-twisted-words-xish-domish-element-entirely-from-raw-xml/2095942#2095942
1430
1431 def _escapeHTML(self, matchobj):
1432 entity = matchobj.group(1)
1433 if entity in XML_ENTITIES:
1434 # we don't escape XML entities
1435 return matchobj.group(0)
1436 else:
1437 try:
1438 return unichr(htmlentitydefs.name2codepoint[entity])
1439 except KeyError:
1440 log.warning(u"removing unknown entity {}".format(entity))
1441 return u''
1442
1443 def __call__(self, raw_xml, force_spaces=False, namespace=None):
1444 """
1445 @param raw_xml(unicode): the raw XML
1446 @param force_spaces (bool): if True, replace occurrences of '\n' and '\t' with ' '.
1447 @param namespace(unicode, None): if set, use this namespace for the wrapping element
1448 """
1449 # we need to wrap element in case
1450 # there is not a unique one on the top
1451 if namespace is not None:
1452 raw_xml = u"<div xmlns='{}'>{}</div>".format(namespace, raw_xml)
1453 else:
1454 raw_xml = u"<div>{}</div>".format(raw_xml)
1455
1456 # avoid ParserError on HTML escaped chars
1457 raw_xml = html_entity_re.sub(self._escapeHTML, raw_xml)
1458
1459 self.result = None
1460
1461 def onStart(elem):
1462 self.result = elem
1463
1464 def onEnd():
1465 pass
1466
1467 def onElement(elem):
1468 self.result.addChild(elem)
1469
1470 parser = domish.elementStream()
1471 parser.DocumentStartEvent = onStart
1472 parser.ElementEvent = onElement
1473 parser.DocumentEndEvent = onEnd
1474 tmp = domish.Element((None, "s"))
1475 if force_spaces:
1476 raw_xml = raw_xml.replace('\n', ' ').replace('\t', ' ')
1477 tmp.addRawXml(raw_xml)
1478 parser.parse(tmp.toXml().encode('utf-8'))
1479 top_elt = self.result.firstChildElement()
1480 # we now can check if there was a unique element on the top
1481 # and remove our wrapping <div/> is this was the case
1482 if len(top_elt.children) == 1 and domish.IElement.providedBy(top_elt.children[0]):
1483 top_elt = top_elt.firstChildElement()
1484 return top_elt
1485
1486
1487 # FIXME: this method is duplicated from frontends.tools.xmlui.getText
1488 def getText(node):
1489 """Get child text nodes of a domish.Element.
1490
1491 @param node (domish.Element)
1492 @return: joined unicode text of all nodes
1493 """
1494 data = []
1495 for child in node.childNodes:
1496 if child.nodeType == child.TEXT_NODE:
1497 data.append(child.wholeText)
1498 return u"".join(data)
1499
1500 def findAll(elt, namespaces=None, names=None, ):
1501 """Find child element at any depth matching criteria
1502
1503 @param elt(domish.Element): top parent of the elements to find
1504 @param names(iterable[unicode], basestring, None): names to match
1505 None to accept every names
1506 @param namespace(iterable[unicode], basestring, None): URIs to match
1507 None to accept every namespaces
1508 @return ((G)domish.Element): found elements
1509 """
1510 if isinstance(namespaces, basestring):
1511 namespaces=tuple((namespaces,))
1512 if isinstance(names, basestring):
1513 names=tuple((names,))
1514
1515 for child in elt.elements():
1516 if (domish.IElement.providedBy(child) and
1517 (not names or child.name in names) and
1518 (not namespaces or child.uri in namespaces)):
1519 yield child
1520 for found in findAll(child, namespaces, names):
1521 yield found