comparison src/tools/xml_tools.py @ 802:9007bb133009

core, frontends: XMLUI refactoring: - XMLUI now use objects with 2 main classes: widgets (button, label, etc), and container which contain widgets according to a layout - widgets and containers classes are found through introspection, thereby it's really easy to add a new one - there is still the AddWidgetName helper, for example AddText('jid', 'test@example.net') will add a StringWidget with name "jid" and default value "test@example.net" - container can be inside other containers. changeContainer change the first parent container
author Goffi <goffi@goffi.org>
date Tue, 04 Feb 2014 18:19:00 +0100
parents e0770d977d58
children f100fd8d279f
comparison
equal deleted inserted replaced
801:02ee9ef95277 802:9007bb133009
27 """This library help manage XML used in SàT (parameters, registration, etc) """ 27 """This library help manage XML used in SàT (parameters, registration, etc) """
28 28
29 SAT_FORM_PREFIX = "SAT_FORM_" 29 SAT_FORM_PREFIX = "SAT_FORM_"
30 SAT_PARAM_SEPARATOR = "_XMLUI_PARAM_" # used to have unique elements names 30 SAT_PARAM_SEPARATOR = "_XMLUI_PARAM_" # used to have unique elements names
31 31
32
33 # Helper functions
34
35 def _dataFormField2XMLUIData(field):
36 """ Get data needed to create an XMLUI's Widget from Wokkel's data_form's Field
37 @param field: data_form.Field (it uses field.value, field.fieldType, field.label and field.var)
38 @return: widget_type, widget_args, widget_kwargs
39
40 """
41 widget_args = [field.value]
42 widget_kwargs = {}
43 if field.fieldType == 'fixed' or field.fieldType is None:
44 widget_type = 'text'
45 elif field.fieldType == 'text-single':
46 widget_type = "string"
47 elif field.fieldType == 'text-private':
48 widget_type = "password"
49 elif field.fieldType == 'boolean':
50 widget_type = "bool"
51 if widget_args[0] is None:
52 widget_args[0] = 'false'
53 elif field.fieldType == 'list-single':
54 widget_type = "list"
55 del widget_args[0]
56 widget_kwargs["options"] = [option.value for option in field.options]
57 else:
58 error(u"FIXME FIXME FIXME: Type [%s] is not managed yet by SàT" % field.fieldType)
59 widget_type = "string"
60
61 if field.var:
62 widget_kwargs["name"] = field.var
63
64 return widget_type, widget_args, widget_kwargs
65
66
32 def dataForm2XMLUI(form, submit_id, session_id=None): 67 def dataForm2XMLUI(form, submit_id, session_id=None):
33 """Take a data form (xep-0004, Wokkel's implementation) and convert it to a SàT xml""" 68 """Take a data form (xep-0004, Wokkel's implementation) and convert it to a SàT XML
34 69 @param submit_id: callback id to call when submitting form
70 @param session_id: id to return with the data
71
72 """
35 form_ui = XMLUI("form", "vertical", submit_id=submit_id, session_id=session_id) 73 form_ui = XMLUI("form", "vertical", submit_id=submit_id, session_id=session_id)
36 74
37 if form.instructions: 75 if form.instructions:
38 form_ui.addText('\n'.join(form.instructions), 'instructions') 76 form_ui.addText('\n'.join(form.instructions), 'instructions')
39 77
40 labels = [field for field in form.fieldList if field.label] 78 labels = [field for field in form.fieldList if field.label]
41 if labels: 79 if labels:
42 # if there is no label, we don't need to use pairs 80 # if there is no label, we don't need to use pairs
43 form_ui.changeLayout("pairs") 81 form_ui.changeContainer("pairs")
44 82
45 for field in form.fieldList: 83 for field in form.fieldList:
46 if field.fieldType == 'fixed': 84 widget_type, widget_args, widget_kwargs = _dataFormField2XMLUIData(field)
47 __field_type = 'text'
48 elif field.fieldType == 'text-single':
49 __field_type = "string"
50 elif field.fieldType == 'text-private':
51 __field_type = "password"
52 elif field.fieldType == 'boolean':
53 __field_type = "bool"
54 if field.value is None:
55 field.value = "false"
56 elif field.fieldType == 'list-single':
57 __field_type = "list"
58 else:
59 error(u"FIXME FIXME FIXME: Type [%s] is not managed yet by SàT" % field.fieldType)
60 __field_type = "string"
61
62 if labels: 85 if labels:
63 if field.label: 86 if field.label:
64 form_ui.addLabel(field.label) 87 form_ui.addLabel(field.label)
65 else: 88 else:
66 form_ui.addEmpty() 89 form_ui.addEmpty()
67 90
68 form_ui.addElement(__field_type, field.var, field.value, [option.value for option in field.options]) 91 form_ui.addWidget(widget_type, *widget_args, **widget_kwargs)
92
69 return form_ui 93 return form_ui
70 94
71 def dataFormResult2AdvancedList(form_ui, form_xml): 95 def dataFormResult2AdvancedList(form_ui, form_xml):
72 """Take a raw data form (not parsed by XEP-0004) and convert it to an advanced list 96 """Take a raw data form (not parsed by XEP-0004) and convert it to an advanced list
73 raw data form is used because Wokkel doesn't manage result items parsing yet 97 raw data form is used because Wokkel doesn't manage result items parsing yet
74 @param form_ui: the XMLUI where the AdvancedList will be added 98 @param form_ui: the XMLUI where the AdvancedList will be added
75 @param form_xml: domish.Element of the data form 99 @param form_xml: domish.Element of the data form
76 @return: AdvancedList element 100 @return: AdvancedList element
77 """ 101 """
78 headers = [] 102 headers = {}
79 items = []
80 try: 103 try:
81 reported_elt = form_xml.elements('jabber:x:data', 'reported').next() 104 reported_elt = form_xml.elements('jabber:x:data', 'reported').next()
82 except StopIteration: 105 except StopIteration:
83 raise exceptions.DataError("Couldn't find expected <reported> tag") 106 raise exceptions.DataError("Couldn't find expected <reported> tag")
84 107
85 for elt in reported_elt.elements(): 108 for elt in reported_elt.elements():
86 if elt.name != "field": 109 if elt.name != "field":
87 raise exceptions.DataError("Unexpected tag") 110 raise exceptions.DataError("Unexpected tag")
88 name = elt["var"] 111 name = elt["var"]
89 label = elt.attributes.get('label','') 112 label = elt.attributes.get('label','')
90 type_ = elt.attributes.get('type','') # TODO 113 type_ = elt.attributes.get('type')
91 headers.append(Header(name, label)) 114 headers[name] = (label, type_)
92 115
93 if not headers: 116 if not headers:
94 raise exceptions.DataError("No reported fields (see XEP-0004 §3.4)") 117 raise exceptions.DataError("No reported fields (see XEP-0004 §3.4)")
95 118
119 adv_list = AdvancedListContainer(form_ui, headers=headers, columns=len(headers), parent=form_ui.current_container)
120 form_ui.changeContainer(adv_list)
121
96 item_elts = form_xml.elements('jabber:x:data', 'item') 122 item_elts = form_xml.elements('jabber:x:data', 'item')
97 123
98 for item_elt in item_elts: 124 for item_elt in item_elts:
99 fields = []
100 for elt in item_elt.elements(): 125 for elt in item_elt.elements():
101 if elt.name != 'field': 126 if elt.name != 'field':
102 warning("Unexpected tag (%s)" % elt.name) 127 warning("Unexpected tag (%s)" % elt.name)
103 continue 128 continue
104 name = elt['var'] 129 field = data_form.Field.fromElement(elt)
105 child_elt = elt.firstChildElement() 130
106 if child_elt.name != "value": 131 widget_type, widget_args, widget_kwargs = _dataFormField2XMLUIData(field)
107 raise exceptions.DataError('Was expecting <value> tag') 132 form_ui.addWidget(widget_type, *widget_args, **widget_kwargs)
108 value = unicode(child_elt) 133
109 fields.append(Field(name, value)) 134 return form_ui
110 items.append(Item(' | '.join((field.value for field in fields if field)), fields))
111
112 return form_ui.addAdvancedList(None, headers, items)
113
114 135
115 def dataFormResult2XMLUI(form_xml, session_id=None): 136 def dataFormResult2XMLUI(form_xml, session_id=None):
116 """Take a raw data form (not parsed by XEP-0004) and convert it to a SàT XMLUI 137 """Take a raw data form (not parsed by XEP-0004) and convert it to a SàT XMLUI
117 raw data form is used because Wokkel doesn't manage result items parsing yet 138 raw data form is used because Wokkel doesn't manage result items parsing yet
118 @param form_xml: domish.Element of the data form 139 @param form_xml: domish.Element of the data form
145 field = data_form.Field(var=value[0], value=value[1]) 166 field = data_form.Field(var=value[0], value=value[1])
146 form.addField(field) 167 form.addField(field)
147 168
148 return form 169 return form
149 170
150 def paramsXml2xmlUI(xml): 171 def paramsXML2XMLUI(xml):
151 """Convert the xml for parameter to a SàT XML User Interface""" 172 """Convert the xml for parameter to a SàT XML User Interface"""
152 params_doc = minidom.parseString(xml.encode('utf-8')) 173 params_doc = minidom.parseString(xml.encode('utf-8'))
153 top = params_doc.documentElement 174 top = params_doc.documentElement
154 if top.nodeName != 'params': 175 if top.nodeName != 'params':
155 error(_('INTERNAL ERROR: parameters xml not valid')) 176 raise exceptions.DataError(_('INTERNAL ERROR: parameters xml not valid'))
156 assert(False) 177
157 param_ui = XMLUI("param", "tabs") 178 param_ui = XMLUI("param", "tabs")
179 tabs_cont = param_ui.current_container
180
158 for category in top.getElementsByTagName("category"): 181 for category in top.getElementsByTagName("category"):
159 category_name = category.getAttribute('name') 182 category_name = category.getAttribute('name')
160 label = category.getAttribute('label') 183 label = category.getAttribute('label')
161 if not category_name: 184 if not category_name:
162 error(_('INTERNAL ERROR: params categories must have a name')) 185 raise exceptions.DataError(_('INTERNAL ERROR: params categories must have a name'))
163 assert(False) 186 tabs_cont.addTab(category_name, label=label, container=PairsContainer)
164 param_ui.addCategory(category_name, 'pairs', label=label)
165 for param in category.getElementsByTagName("param"): 187 for param in category.getElementsByTagName("param"):
188 widget_kwargs = {}
189
166 param_name = param.getAttribute('name') 190 param_name = param.getAttribute('name')
167 label = param.getAttribute('label') 191 param_label = param.getAttribute('label')
168 if not param_name: 192 if not param_name:
169 error(_('INTERNAL ERROR: params must have a name')) 193 raise exceptions.DataError(_('INTERNAL ERROR: params must have a name'))
170 assert(False) 194
171 type_ = param.getAttribute('type') 195 type_ = param.getAttribute('type')
172 value = param.getAttribute('value') or None 196 value = param.getAttribute('value') or None
173 options = getOptions(param)
174 callback_id = param.getAttribute('callback_id') or None 197 callback_id = param.getAttribute('callback_id') or None
198
199 if type_ == 'list':
200 options = _getParamListOptions(param)
201 widget_kwargs['options'] = options
202
175 if type_ == "button": 203 if type_ == "button":
176 param_ui.addEmpty() 204 param_ui.addEmpty()
177 else: 205 else:
178 param_ui.addLabel(label or param_name) 206 param_ui.addLabel(param_label or param_name)
179 param_ui.addElement(name="%s%s%s" % (category_name, SAT_PARAM_SEPARATOR, param_name), type_=type_, value=value, options=options, callback_id=callback_id) 207
208 if value:
209 widget_kwargs["value"] = value
210
211 if callback_id:
212 widget_kwargs['callback_id'] = callback_id
213
214 widget_kwargs['name'] = "%s%s%s" % (category_name, SAT_PARAM_SEPARATOR, param_name)
215
216 param_ui.addWidget(type_, **widget_kwargs)
180 217
181 return param_ui.toXml() 218 return param_ui.toXml()
182 219
183 220
184 def getOptions(param): 221 def _getParamListOptions(param):
185 """Retrieve the options for list element. Allow listing the <option/> 222 """Retrieve the options for list element. Allow listing the <option/>
186 tags directly in <param/> or in an intermediate <options/> tag.""" 223 tags directly in <param/> or in an intermediate <options/> tag."""
187 elems = param.getElementsByTagName("options") 224 elems = param.getElementsByTagName("options")
188 if len(elems) == 0: 225 if len(elems) == 0:
189 elems = param.getElementsByTagName("option") 226 elems = param.getElementsByTagName("option")
192 if len(elems) == 0: 229 if len(elems) == 0:
193 return [] 230 return []
194 return [elem.getAttribute("value") for elem in elems] 231 return [elem.getAttribute("value") for elem in elems]
195 232
196 233
197 class Header(object): 234 ## XMLUI Elements
198 """AdvandeList's header""" 235
199 236
200 def __init__(self, field_name, label, description=None, type_=None): 237 class Element(object):
201 """ 238 """ Base XMLUI element """
202 @param field_name: name of the field referenced 239 type = None
240
241 def __init__(self, xmlui, parent=None):
242 """Create a container element
243 @param xmlui: XMLUI instance
244 @parent: parent element
245 """
246 assert(self.type) is not None
247 if not hasattr(self, 'elem'):
248 self.elem = parent.xmlui.doc.createElement(self.type)
249 self.xmlui = xmlui
250 if parent is not None:
251 parent.append(self)
252 else:
253 self.parent = parent
254
255 def append(self, child):
256 self.elem.appendChild(child.elem)
257 child.parent = self
258
259
260 class TopElement(Element):
261 """ Main XML Element """
262 type = 'top'
263
264 def __init__(self, xmlui):
265 self.elem = xmlui.doc.documentElement
266 super(TopElement, self).__init__(xmlui)
267
268
269 class TabElement(Element):
270 """ Used by TabsContainer to give name and label to tabs """
271 type = 'tab'
272
273 def __init__(self, parent, name, label):
274 if not isinstance(parent, TabsContainer):
275 raise exceptions.DataError(_("TabElement must be a child of TabsContainer"))
276 super(TabElement, self).__init__(parent.xmlui, parent)
277 self.elem.setAttribute('name', name)
278 self.elem.setAttribute('label', label)
279
280
281 class FieldBackElement(Element):
282 """ Used by ButtonWidget to indicate which field have to be sent back """
283 type = 'field_back'
284
285 def __init__(self, parent, name):
286 assert(isinstance(parent, ButtonWidget))
287 super(FieldBackElement, self).__init__(parent.xmlui, parent)
288 self.elem.setAttribute('name', name)
289
290
291 class OptionElement(Element):
292 """" Used by ListWidget to specify options """
293 type = 'option'
294
295 def __init__(self, parent, option):
296 assert(isinstance(parent, ListWidget))
297 super(OptionElement, self).__init__(parent.xmlui, parent)
298 if isinstance(option, basestring):
299 value, label = option, option
300 elif isinstance(option, tuple):
301 value, label = option
302 self.elem.setAttribute('value', value)
303 self.elem.setAttribute('label', label)
304
305
306 class RowElement(Element):
307 """" Used by AdvancedListContainer """
308 type = 'row'
309
310 def __init__(self, parent):
311 assert(isinstance(parent, AdvancedListContainer))
312 super(RowElement, self).__init__(parent.xmlui, parent)
313
314
315 class HeaderElement(Element):
316 """" Used by AdvancedListContainer """
317 type = 'header'
318
319 def __init__(self, parent, name=None, Label=None, description=None):
320 """
321 @param parent: AdvancedListContainer instance
322 @param name: name of the container
203 @param label: label to be displayed in columns 323 @param label: label to be displayed in columns
204 @param description: long descriptive text 324 @param description: long descriptive text
205 @param type_: TODO 325
206 326 """
207 """ 327 assert(isinstance(parent, AdvancedListContainer))
208 self.field_name = field_name 328 super(HeaderElement, self).__init__(parent.xmlui, parent)
209 self.label = label 329 if name:
210 self.description = description 330 field_elt.setAttribute('name', name)
211 self.type = type_ 331 if label:
212 if type_ is not None: 332 field_elt.setAttribute('label', label)
213 raise NotImplementedError # TODO: 333 if description:
214 334 field_elt.setAttribute('description', description)
215 335
216 class Item(object): 336
217 """Item used in AdvancedList""" 337 class Container(Element):
218 338 """ And Element which contains other ones and has a layout """
219 def __init__(self, text=None, fields=None): 339 type = None
220 """ 340
221 @param text: Optional textual representation, when fields are not showed individually 341 def __init__(self, xmlui, parent=None):
222 @param fields: list of Field instances² 342 """Create a container element
223 """ 343 @param xmlui: XMLUI instance
224 self.text = text 344 @parent: parent element or None
225 self.fields = fields if fields is not None else [] 345 """
226 346 self.elem = xmlui.doc.createElement('container')
227 347 super(Container, self).__init__(xmlui, parent)
228 class Field(object): 348 self.elem.setAttribute('type', self.type)
229 """Field used in AdvancedList (in items)""" 349
230 350 def getParentContainer(self):
231 def __init__(self, name, value): 351 """ Return first parent container
232 """ 352 @return: parent container or None
233 @param name: name of the field, used to identify the field in headers 353
234 @param value: actual content of the field 354 """
235 """ 355 current = self.parent
236 self.name = name 356 while(not isinstance(current, (Container)) and
237 self.value = value 357 current is not None):
238 358 current = current.parent
359 return current
360
361 class VerticalContainer(Container):
362 type = "vertical"
363
364
365 class HorizontalContainer(Container):
366 type = "horizontal"
367
368
369 class PairsContainer(Container):
370 type = "pairs"
371
372
373 class TabsContainer(Container):
374 type = "tabs"
375
376 def addTab(self, name, label=None, container=VerticalContainer):
377 """Add a tab"""
378 if not label:
379 label = name
380 tab_elt = TabElement(self, name, label)
381 new_container = container(self.xmlui, tab_elt)
382 self.xmlui.changeContainer(new_container)
383
384 def end(self):
385 """ Called when we have finished tabs
386 change current container to first container parent
387
388 """
389 parent_container = self.getParentContainer()
390 self.xmlui.changeContainer(parent_container)
391
392
393 class AdvancedListContainer(Container):
394 type = "advanced_list"
395
396 def __init__(self, xmlui, name=None, headers=None, items=None, columns=None, parent=None):
397 """Create an advanced list
398 @param headers: optional headers informations
399 @param items: list of Item instances
400 @return: created element
401 """
402 if not items and columns is None:
403 raise DataError(_("either items or columns need do be filled"))
404 if headers is None:
405 headers = []
406 if items is None:
407 items = []
408 super(AdvancedListContainer, self).__init__(xmlui, parent)
409 if columns is None:
410 columns = len(items[0])
411 self._columns = columns
412 self._current_column = 0
413 self.current_row = None
414 if headers:
415 if len(headers) != self._columns:
416 raise exceptions.DataError(_("Headers lenght doesn't correspond to columns"))
417 self.addHeaders(headers)
418 if items:
419 self.addItems(items)
420
421 def addHeaders(self, headers):
422 for header in headers:
423 self.addHeader(header)
424
425 def addHeader(self, header):
426 pass # TODO
427
428 def addItems(self, items):
429 for item in items:
430 self.addItem(item)
431
432 def addItem(self, item):
433 if self._current_column % self._columns == 0:
434 self.current_row = RowElement(self)
435 self.current_row.append(item)
436
437 def end(self):
438 """ Called when we have finished list
439 change current container to first container parent
440
441 """
442 if self._current_colum % self._columns != 0:
443 raise exceptions.DataError(_("Incorrect number of items in list"))
444 parent_container = self.getParentContainer()
445 self.xmlui.changeContainer(parent_container)
446
447
448 class Widget(Element):
449 type = None
450
451 def __init__(self, xmlui, name=None, parent=None):
452 """Create an element
453 @param xmlui: XMLUI instance
454 @param name: name of the element or None
455 @param parent: parent element or None
456 """
457 self.elem = xmlui.doc.createElement('widget')
458 super(Widget, self).__init__(xmlui, parent)
459 if name:
460 self.elem.setAttribute('name', name)
461 self.elem.setAttribute('type', self.type)
462
463
464 class InputWidget(Widget):
465 pass
466
467
468 class EmptyWidget(Widget):
469 type = 'empty'
470
471
472 class TextWidget(Widget):
473 type = 'text'
474
475 def __init__(self, xmlui, text, name=None, parent=None):
476 super(TextWidget, self).__init__(xmlui, name, parent)
477 text = self.xmlui.doc.createTextNode(text)
478 self.elem.appendChild(text)
479
480
481 class LabelWidget(Widget):
482 type='label'
483
484 def __init__(self, xmlui, label, name=None, parent=None):
485 super(LabelWidget, self).__init__(xmlui, name, parent)
486 self.elem.setAttribute('value', label)
487
488
489 class StringWidget(InputWidget):
490 type = 'string'
491
492 def __init__(self, xmlui, value=None, name=None, parent=None):
493 super(StringWidget, self).__init__(xmlui, name, parent)
494 if value:
495 self.elem.setAttribute('value', value)
496
497
498 class PasswordWidget(StringWidget):
499 type = 'password'
500
501
502 class TextBoxWidget(StringWidget):
503 type = 'textbox'
504
505
506 class BoolWidget(InputWidget):
507 type = 'bool'
508
509 def __init__(self, xmlui, value='false', name=None, parent=None):
510 if value == '0':
511 value='false'
512 elif value == '1':
513 value='true'
514 if not value in ('true', 'false'):
515 raise exceptions.DataError(_("Value must be 0, 1, false or true"))
516 super(BoolWidget, self).__init__(xmlui, name, parent)
517 self.elem.setAttribute('value', value)
518
519
520 class ButtonWidget(InputWidget):
521 type = 'button'
522
523 def __init__(self, xmlui, callback_id, value=None, fields_back=None, name=None, parent=None):
524 """Add a button
525 @param callback_id: callback which will be called if button is pressed
526 @param value: label of the button
527 @fields_back: list of names of field to give back when pushing the button
528 @param name: name
529 @param parent: parent container
530 """
531 if fields_back is None:
532 fields_back = []
533 super(ButtonWidget, self).__init__(xmlui, name, parent)
534 self.elem.setAttribute('callback', callback_id)
535 if value:
536 self.elem.setAttribute('value', value)
537 for field in fields_back:
538 fback_el = FieldBackElement(self, field)
539
540
541 class ListWidget(InputWidget):
542 type = 'list'
543
544 def __init__(self, xmlui, options, value=None, style=None, name=None, parent=None):
545 if style is None:
546 style = set()
547 styles = set(style)
548 assert options
549 if not styles.issubset(['multi']):
550 raise exceptions.DataError(_("invalid styles"))
551 super(ListWidget, self).__init__(xmlui, name, parent)
552 self.addOptions(options)
553 if value:
554 self.elem.setAttribute('value', value)
555 for style in styles:
556 self.elem.setAttribute(style, 'yes')
557
558 def addOptions(self, options):
559 """i Add options to a multi-values element (e.g. list) """
560 for option in options:
561 OptionElement(self, option)
562
563
564 ## XMLUI main class
239 565
240 566
241 class XMLUI(object): 567 class XMLUI(object):
242 """This class is used to create a user interface (form/window/parameters/etc) using SàT XML""" 568 """This class is used to create a user interface (form/window/parameters/etc) using SàT XML"""
243 569
244 def __init__(self, panel_type, layout="vertical", title=None, submit_id=None, session_id=None): 570 def __init__(self, panel_type, container="vertical", title=None, submit_id=None, session_id=None):
245 """Init SàT XML Panel 571 """Init SàT XML Panel
246 @param panel_type: one of 572 @param panel_type: one of
247 - window (new window) 573 - window (new window)
248 - form (formulaire, depend of the frontend, usually a panel with cancel/submit buttons) 574 - form (formulaire, depend of the frontend, usually a panel with cancel/submit buttons)
249 - param (parameters, presentatio depend of the frontend) 575 - param (parameters, presentation depend of the frontend)
250 @param layout: disposition of elements, one of: 576 @param container: disposition of elements, one of:
251 - vertical: elements are disposed up to bottom 577 - vertical: elements are disposed up to bottom
252 - horizontal: elements are disposed left to right 578 - horizontal: elements are disposed left to right
253 - pairs: elements come on two aligned columns 579 - pairs: elements come on two aligned columns
254 (usually one for a label, the next for the element) 580 (usually one for a label, the next for the element)
255 - tabs: elemens are in categories with tabs (notebook) 581 - tabs: elemens are in categories with tabs (notebook)
256 @param title: title or default if None 582 @param title: title or default if None
257 @param submit_id: callback id to call for panel_type we can submit (form, param) 583 @param submit_id: callback id to call for panel_type we can submit (form, param)
258 """ 584 """
585 self._introspect()
259 if panel_type not in ['window', 'form', 'param']: 586 if panel_type not in ['window', 'form', 'param']:
260 raise exceptions.DataError(_("Unknown panel type [%s]") % panel_type) 587 raise exceptions.DataError(_("Unknown panel type [%s]") % panel_type)
261 if panel_type == 'form' and submit_id is None: 588 if panel_type == 'form' and submit_id is None:
262 raise exceptions.DataError(_("form XMLUI need a submit_id")) 589 raise exceptions.DataError(_("form XMLUI need a submit_id"))
263 self.type_ = panel_type 590 if not isinstance(container, basestring):
591 raise exceptions.DataError(_("container argument must be a string"))
592 self.type = panel_type
264 impl = minidom.getDOMImplementation() 593 impl = minidom.getDOMImplementation()
265 594
266 self.doc = impl.createDocument(None, "sat_xmlui", None) 595 self.doc = impl.createDocument(None, "sat_xmlui", None)
267 top_element = self.doc.documentElement 596 top_element = self.doc.documentElement
268 top_element.setAttribute("type", panel_type) 597 top_element.setAttribute("type", panel_type)
269 if title: 598 if title:
270 top_element.setAttribute("title", title) 599 top_element.setAttribute("title", title)
271 self.submit_id = submit_id 600 self.submit_id = submit_id
272 self.session_id = session_id 601 self.session_id = session_id
273 self.parentTabsLayout = None # used only we have 'tabs' layout 602 self.main_container = self._createContainer(container, TopElement(self))
274 self.currentCategory = None # used only we have 'tabs' layout 603 self.current_container = self.main_container
275 self.currentLayout = None 604
276 self.changeLayout(layout) 605 def _introspect(self):
606 """ Introspect module to find Widgets and Containers """
607 self._containers = {}
608 self._widgets = {}
609 for obj in globals().values():
610 try:
611 if issubclass(obj, Widget):
612 if obj.__name__ == 'Widget':
613 continue
614 self._widgets[obj.type] = obj
615 elif issubclass(obj, Container):
616 if obj.__name__ == 'Container':
617 continue
618 self._containers[obj.type] = obj
619 except TypeError:
620 pass
277 621
278 def __del__(self): 622 def __del__(self):
279 self.doc.unlink() 623 self.doc.unlink()
624
625 def __getattr__(self, name):
626 if name.startswith("add") and name not in ('addWidget',): # addWidgetName(...) create an instance of WidgetName
627 class_name = name[3:]+"Widget"
628 if class_name in globals():
629 cls = globals()[class_name]
630 if issubclass(cls, Widget):
631 def createWidget(*args, **kwargs):
632 if "parent" not in kwargs:
633 kwargs["parent"] = self.current_container
634 if "name" not in kwargs and issubclass(cls, InputWidget): # name can be given as first argument or in keyword arguments for InputWidgets
635 args = list(args)
636 kwargs["name"] = args.pop(0)
637 return cls(self, *args, **kwargs)
638 return createWidget
639 return object.__getattribute__(self, name)
280 640
281 @property 641 @property
282 def submit_id(self): 642 def submit_id(self):
283 top_element = self.doc.documentElement 643 top_element = self.doc.documentElement
284 value = top_element.getAttribute("submit") 644 value = top_element.getAttribute("submit")
312 elif value: 672 elif value:
313 top_element.setAttribute("session_id", value) 673 top_element.setAttribute("session_id", value)
314 else: 674 else:
315 raise exceptions.DataError("session_id can't be empty") 675 raise exceptions.DataError("session_id can't be empty")
316 676
317 def _createLayout(self, layout, parent=None): 677 def _createContainer(self, container, parent=None, **kwargs):
318 """Create a layout element 678 """Create a container element
319 @param type: layout type (cf init doc) 679 @param type: container type (cf init doc)
320 @parent: parent element or None 680 @parent: parent element or None
321 """ 681 """
322 if not layout in ['vertical', 'horizontal', 'pairs', 'tabs']: 682 if container not in self._containers:
323 error(_("Unknown layout type [%s]") % layout) 683 raise exceptions.DataError(_("Unknown container type [%s]") % container)
324 assert False 684 cls = self._containers[container]
325 layout_elt = self.doc.createElement('layout') 685 new_container = cls(self, parent, **kwargs)
326 layout_elt.setAttribute('type', layout) 686 return new_container
327 if parent is not None: 687
328 parent.appendChild(layout_elt) 688 def changeContainer(self, container, **kwargs):
329 return layout_elt 689 """Change the current container
330 690 @param container: either container type (container it then created),
331 def _createElem(self, type_, name=None, parent=None): 691 or an Container instance"""
332 """Create an element 692 if isinstance(container, basestring):
333 @param type_: one of 693 self.current_container = self._createContainer(container, self.current_container.getParentContainer() or self.main_container, **kwargs)
334 - empty: empty element (usefull to skip something in a layout, e.g. skip first element in a PAIRS layout) 694 else:
335 - text: text to be displayed in an multi-line area, e.g. instructions 695 self.current_container = self.main_container if container is None else container
336 @param name: name of the element or None 696 assert(isinstance(self.current_container, Container))
337 @param parent: parent element or None 697 return self.current_container
338 @return: created element 698
339 """ 699 def addWidget(self, type_, *args, **kwargs):
340 elem = self.doc.createElement('elem') 700 """Convenience method to add an element"""
341 if name: 701 if type_ not in self._widgets:
342 elem.setAttribute('name', name) 702 raise exceptions.DataError(_("Invalid type [%s]") % type_)
343 elem.setAttribute('type', type_) 703 if "parent" not in kwargs:
344 if parent is not None: 704 kwargs["parent"] = self.current_container
345 parent.appendChild(elem) 705 cls = self._widgets[type_]
346 return elem 706 return cls(self, *args, **kwargs)
347
348 def changeLayout(self, layout):
349 """Change the current layout"""
350 self.currentLayout = self._createLayout(layout, self.currentCategory if self.currentCategory else self.doc.documentElement)
351 if layout == "tabs":
352 self.parentTabsLayout = self.currentLayout
353
354 def addEmpty(self, name=None):
355 """Add a multi-lines text"""
356 return self._createElem('empty', name, self.currentLayout)
357
358 def addText(self, text, name=None):
359 """Add a multi-lines text"""
360 elem = self._createElem('text', name, self.currentLayout)
361 text = self.doc.createTextNode(text)
362 elem.appendChild(text)
363 return elem
364
365 def addLabel(self, text, name=None):
366 """Add a single line text, mainly useful as label before element"""
367 elem = self._createElem('label', name, self.currentLayout)
368 elem.setAttribute('value', text)
369 return elem
370
371 def addString(self, name=None, value=None):
372 """Add a string box"""
373 elem = self._createElem('string', name, self.currentLayout)
374 if value:
375 elem.setAttribute('value', value)
376 return elem
377
378 def addPassword(self, name=None, value=None):
379 """Add a password box"""
380 elem = self._createElem('password', name, self.currentLayout)
381 if value:
382 elem.setAttribute('value', value)
383 return elem
384
385 def addTextBox(self, name=None, value=None):
386 """Add a string box"""
387 elem = self._createElem('textbox', name, self.currentLayout)
388 if value:
389 elem.setAttribute('value', value)
390 return elem
391
392 def addBool(self, name=None, value="true"):
393 """Add a string box"""
394 if value=="0":
395 value="false"
396 elif value=="1":
397 value="true"
398 assert value in ["true", "false"]
399 elem = self._createElem('bool', name, self.currentLayout)
400 elem.setAttribute('value', value)
401 return elem
402
403 def addList(self, options, name=None, value=None, style=None):
404 """Add a list of choices"""
405 if style is None:
406 style = set()
407 styles = set(style)
408 assert options
409 assert styles.issubset(['multi'])
410 elem = self._createElem('list', name, self.currentLayout)
411 self.addOptions(options, elem)
412 if value:
413 elem.setAttribute('value', value)
414 for style in styles:
415 elem.setAttribute(style, 'yes')
416 return elem
417
418 def addAdvancedList(self, name=None, headers=None, items=None):
419 """Create an advanced list
420 @param headers: optional headers informations
421 @param items: list of Item instances
422 @return: created element
423 """
424 elem = self._createElem('advanced_list', name, self.currentLayout)
425 self.addHeaders(headers, elem)
426 if items:
427 self.addItems(items, elem)
428 return elem
429
430 def addButton(self, callback_id, name, value, fields_back=[]):
431 """Add a button
432 @param callback: callback which will be called if button is pressed
433 @param name: name
434 @param value: label of the button
435 @fields_back: list of names of field to give back when pushing the button
436 """
437 elem = self._createElem('button', name, self.currentLayout)
438 elem.setAttribute('callback', callback_id)
439 elem.setAttribute('value', value)
440 for field in fields_back:
441 fback_el = self.doc.createElement('field_back')
442 fback_el.setAttribute('name', field)
443 elem.appendChild(fback_el)
444 return elem
445
446 def addElement(self, type_, name=None, value=None, options=None, callback_id=None, headers=None, available=None):
447 """Convenience method to add element, the params correspond to the ones in addSomething methods"""
448 if type_ == 'empty':
449 return self.addEmpty(name)
450 elif type_ == 'text':
451 assert value is not None
452 return self.addText(value, name)
453 elif type_ == 'label':
454 assert(value)
455 return self.addLabel(value)
456 elif type_ == 'string':
457 return self.addString(name, value)
458 elif type_ == 'password':
459 return self.addPassword(name, value)
460 elif type_ == 'textbox':
461 return self.addTextBox(name, value)
462 elif type_ == 'bool':
463 if not value:
464 value = "true"
465 return self.addBool(name, value)
466 elif type_ == 'list':
467 return self.addList(options, name, value)
468 elif type_ == 'advancedlist':
469 return self.addAdvancedList(name, headers, available)
470 elif type_ == 'button':
471 assert(callback_id and value)
472 return self.addButton(callback_id, name, value)
473
474 # List
475
476 def addOptions(self, options, parent):
477 """Add options to a multi-values element (e.g. list)
478 @param parent: multi-values element"""
479 for option in options:
480 opt = self.doc.createElement('option')
481 if isinstance(option, basestring):
482 value, label = option, option
483 elif isinstance(option, tuple):
484 value, label = option
485 opt.setAttribute('value', value)
486 opt.setAttribute('label', label)
487 parent.appendChild(opt)
488
489 # Advanced list
490
491 def addHeaders(self, headers, parent):
492 headers_elt = self.doc.createElement('headers')
493 for header in headers:
494 field_elt = self.doc.createElement('field')
495 field_elt.setAttribute('field_name', header.field_name)
496 field_elt.setAttribute('label', header.label)
497 if header.description:
498 field_elt.setAttribute('description', header.description)
499 if header.type:
500 field_elt.setAttribute('type', header.type)
501 headers_elt.appendChild(field_elt)
502 parent.appendChild(headers_elt)
503
504 def addItems(self, items, parent):
505 """Add items to an AdvancedList
506 @param items: list of Item instances
507 @param parent: parent element (should be addAdvancedList)
508
509 """
510 items_elt = self.doc.createElement('items')
511 for item in items:
512 item_elt = self.doc.createElement('item')
513 if item.text is not None:
514 text_elt = self.doc.createElement('text')
515 text_elt.appendChild(self.doc.createTextNode(item.text))
516 item_elt.appendChild(text_elt)
517
518 for field in item.fields:
519 field_elt = self.doc.createElement('field')
520 field_elt.setAttribute('name', field.name)
521 field_elt.setAttribute('value', field.value)
522 item_elt.appendChild(field_elt)
523
524 items_elt.appendChild(item_elt)
525
526 parent.appendChild(items_elt)
527
528 # Tabs
529
530 def addCategory(self, name, layout, label=None):
531 """Add a category to current layout (must be a tabs layout)"""
532 assert(layout != 'tabs')
533 if not self.parentTabsLayout:
534 error(_("Trying to add a category without parent tabs layout"))
535 assert(False)
536 if self.parentTabsLayout.getAttribute('type') != 'tabs':
537 error(_("parent layout of a category is not tabs"))
538 assert(False)
539
540 if not label:
541 label = name
542 self.currentCategory = cat = self.doc.createElement('category')
543 cat.setAttribute('name', name)
544 cat.setAttribute('label', label)
545 self.changeLayout(layout)
546 self.parentTabsLayout.appendChild(cat)
547 707
548 def toXml(self): 708 def toXml(self):
549 """return the XML representation of the panel""" 709 """return the XML representation of the panel"""
550 return self.doc.toxml() 710 return self.doc.toxml()
711
712
713 # Misc other funtions
551 714
552 715
553 class ElementParser(object): 716 class ElementParser(object):
554 """callable class to parse XML string into Element 717 """callable class to parse XML string into Element
555 Found at http://stackoverflow.com/questions/2093400/how-to-create-twisted-words-xish-domish-element-entirely-from-raw-xml/2095942#2095942 718 Found at http://stackoverflow.com/questions/2093400/how-to-create-twisted-words-xish-domish-element-entirely-from-raw-xml/2095942#2095942