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