Mercurial > libervia-backend
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 |