comparison src/tools/xml_tools.py @ 630:0b914394e74f

core: added advanced list to XMLUI (need improvment, very basic so far) - the advanced list use headers which can be used as columns - in the future, should provide the ability to highly customize list disposition, in a way similar to Amarok 2's playlist.
author Goffi <goffi@goffi.org>
date Sun, 08 Sep 2013 18:05:19 +0200
parents 6f4c31192c7c
children 17bd09cd1001
comparison
equal deleted inserted replaced
629:204930b870e1 630:0b914394e74f
19 19
20 from logging import debug, info, error 20 from logging import debug, info, error
21 from xml.dom import minidom 21 from xml.dom import minidom
22 from wokkel import data_form 22 from wokkel import data_form
23 from twisted.words.xish import domish 23 from twisted.words.xish import domish
24 from core import exceptions
24 25
25 """This library help manage XML used in SàT (parameters, registration, etc) """ 26 """This library help manage XML used in SàT (parameters, registration, etc) """
26 27
27 28
28 def dataForm2xml(form): 29 def dataForm2XML(form):
29 """Take a data form (xep-0004, Wokkel's implementation) and convert it to a SàT xml""" 30 """Take a data form (xep-0004, Wokkel's implementation) and convert it to a SàT xml"""
30 31
31 form_ui = XMLUI("form", "vertical") 32 form_ui = XMLUI("form", "vertical")
32 33
33 if form.instructions: 34 if form.instructions:
58 form_ui.addEmpty() 59 form_ui.addEmpty()
59 60
60 form_ui.addElement(__field_type, field.var, field.value, [option.value for option in field.options]) 61 form_ui.addElement(__field_type, field.var, field.value, [option.value for option in field.options])
61 return form_ui.toXml() 62 return form_ui.toXml()
62 63
64 def dataFormResult2AdvancedList(form_ui, form_xml):
65 """Take a raw data form (not parsed by XEP-0004) and convert it to an advanced list
66 raw data form is used because Wokkel doesn't manage result items parsing yet
67 @param form_ui: the XMLUI where the AdvancedList will be added²
68 @param form_xml: domish.Element of the data form
69 @return: AdvancedList element
70 """
71 headers = []
72 items = []
73 try:
74 reported_elt = form_xml.elements('jabber:x:data', 'reported').next()
75 except StopIteration:
76 raise exceptions.DataError("Couldn't find expected <reported> tag")
77
78 for elt in reported_elt.children:
79 if elt.name != "field":
80 raise exceptions.DataError("Unexpected tag")
81 name = elt["var"]
82 label = elt.attributes.get('label','')
83 type_ = elt.attributes.get('type','') # TODO
84 headers.append(Header(name, label))
85
86 if not headers:
87 raise DataError("No reported fields (see XEP-0004 §3.4)")
88
89 item_elts = form_xml.elements('jabber:x:data', 'item')
90
91 for item_elt in item_elts:
92 fields = []
93 for elt in item_elt.children:
94 if elt.name != 'field':
95 warning("Unexpected tag (%s)" % elt.name)
96 continue
97 name = elt['var']
98 child_elt = elt.firstChildElement()
99 if child_elt.name != "value":
100 raise DataError('Was expecting <value> tag')
101 value = unicode(child_elt)
102 fields.append(Field(name, value))
103 items.append(Item(' | '.join((field.value for field in fields if field)), fields))
104
105 return form_ui.addAdvancedList(None, headers, items)
106
107
108 def dataFormResult2XML(form_xml):
109 """Take a raw data form (not parsed by XEP-0004) and convert it to a SàT XMLUI
110 raw data form is used because Wokkel doesn't manage result items parsing yet
111 @param form_xml: domish.Element of the data form
112 @return: XMLUI interface
113 """
114
115 form_ui = XMLUI("window", "vertical")
116 dataFormResult2AdvancedList(form_ui, form_xml)
117 return form_ui.toXml()
63 118
64 def tupleList2dataForm(values): 119 def tupleList2dataForm(values):
65 """convert a list of tuples (name,value) to a wokkel submit data form""" 120 """convert a list of tuples (name,value) to a wokkel submit data form"""
66 form = data_form.Form('submit') 121 form = data_form.Form('submit')
67 for value in values: 122 for value in values:
68 field = data_form.Field(var=value[0], value=value[1]) 123 field = data_form.Field(var=value[0], value=value[1])
69 form.addField(field) 124 form.addField(field)
70 125
71 return form 126 return form
72
73 127
74 def paramsXml2xmlUI(xml): 128 def paramsXml2xmlUI(xml):
75 """Convert the xml for parameter to a SàT XML User Interface""" 129 """Convert the xml for parameter to a SàT XML User Interface"""
76 params_doc = minidom.parseString(xml.encode('utf-8')) 130 params_doc = minidom.parseString(xml.encode('utf-8'))
77 top = params_doc.documentElement 131 top = params_doc.documentElement
102 param_ui.addElement(name=name, type_=type_, value=value, callback_id=callback_id) 156 param_ui.addElement(name=name, type_=type_, value=value, callback_id=callback_id)
103 157
104 return param_ui.toXml() 158 return param_ui.toXml()
105 159
106 160
161 class Header(object):
162 """AdvandeList's header"""
163
164 def __init__(self, field_name, label, description=None, type_=None):
165 """
166 @param field_name: name of the field referenced
167 @param label: label to be displayed in columns
168 @param description: long descriptive text
169 @param type_: TODO
170
171 """
172 self.field_name = field_name
173 self.label = label
174 self.description = description
175 self.type = type_
176 if type_ is not None:
177 raise NotImplementedError # TODO:
178
179
180 class Item(object):
181 """Item used in AdvancedList"""
182
183 def __init__(self, text=None, fields=None):
184 """
185 @param text: Optional textual representation, when fields are not showed individually
186 @param fields: list of Field instances²
187 """
188 self.text = text
189 self.fields = fields if fields is not None else []
190
191
192 class Field(object):
193 """Field used in AdvancedList (in items)"""
194
195 def __init__(self, name, value):
196 """
197 @param name: name of the field, used to identify the field in headers
198 @param value: actual content of the field
199 """
200 self.name = name
201 self.value = value
202
203
204
107 class XMLUI(object): 205 class XMLUI(object):
108 """This class is used to create a user interface (form/window/parameters/etc) using SàT XML""" 206 """This class is used to create a user interface (form/window/parameters/etc) using SàT XML"""
109 207
110 def __init__(self, panel_type, layout="vertical", title=None): 208 def __init__(self, panel_type, layout="vertical", title=None):
111 """Init SàT XML Panel 209 """Init SàT XML Panel
138 self.changeLayout(layout) 236 self.changeLayout(layout)
139 237
140 def __del__(self): 238 def __del__(self):
141 self.doc.unlink() 239 self.doc.unlink()
142 240
143 def __createLayout(self, layout, parent=None): 241 def _createLayout(self, layout, parent=None):
144 """Create a layout element 242 """Create a layout element
145 @param type: layout type (cf init doc) 243 @param type: layout type (cf init doc)
146 @parent: parent element or None 244 @parent: parent element or None
147 """ 245 """
148 if not layout in ['vertical', 'horizontal', 'pairs', 'tabs']: 246 if not layout in ['vertical', 'horizontal', 'pairs', 'tabs']:
152 layout_elt.setAttribute('type', layout) 250 layout_elt.setAttribute('type', layout)
153 if parent is not None: 251 if parent is not None:
154 parent.appendChild(layout_elt) 252 parent.appendChild(layout_elt)
155 return layout_elt 253 return layout_elt
156 254
157 def __createElem(self, type_, name=None, parent=None): 255 def _createElem(self, type_, name=None, parent=None):
158 """Create an element 256 """Create an element
159 @param type_: one of 257 @param type_: one of
160 - empty: empty element (usefull to skip something in a layout, e.g. skip first element in a PAIRS layout) 258 - empty: empty element (usefull to skip something in a layout, e.g. skip first element in a PAIRS layout)
161 - text: text to be displayed in an multi-line area, e.g. instructions 259 - text: text to be displayed in an multi-line area, e.g. instructions
162 @param name: name of the element or None 260 @param name: name of the element or None
163 @param parent: parent element or None 261 @param parent: parent element or None
262 @return: created element
164 """ 263 """
165 elem = self.doc.createElement('elem') 264 elem = self.doc.createElement('elem')
166 if name: 265 if name:
167 elem.setAttribute('name', name) 266 elem.setAttribute('name', name)
168 elem.setAttribute('type', type_) 267 elem.setAttribute('type', type_)
170 parent.appendChild(elem) 269 parent.appendChild(elem)
171 return elem 270 return elem
172 271
173 def changeLayout(self, layout): 272 def changeLayout(self, layout):
174 """Change the current layout""" 273 """Change the current layout"""
175 self.currentLayout = self.__createLayout(layout, self.currentCategory if self.currentCategory else self.doc.documentElement) 274 self.currentLayout = self._createLayout(layout, self.currentCategory if self.currentCategory else self.doc.documentElement)
176 if layout == "tabs": 275 if layout == "tabs":
177 self.parentTabsLayout = self.currentLayout 276 self.parentTabsLayout = self.currentLayout
178 277
179 def addEmpty(self, name=None): 278 def addEmpty(self, name=None):
180 """Add a multi-lines text""" 279 """Add a multi-lines text"""
181 self.__createElem('empty', name, self.currentLayout) 280 return self._createElem('empty', name, self.currentLayout)
182 281
183 def addText(self, text, name=None): 282 def addText(self, text, name=None):
184 """Add a multi-lines text""" 283 """Add a multi-lines text"""
185 elem = self.__createElem('text', name, self.currentLayout) 284 elem = self._createElem('text', name, self.currentLayout)
186 text = self.doc.createTextNode(text) 285 text = self.doc.createTextNode(text)
187 elem.appendChild(text) 286 elem.appendChild(text)
287 return elem
188 288
189 def addLabel(self, text, name=None): 289 def addLabel(self, text, name=None):
190 """Add a single line text, mainly useful as label before element""" 290 """Add a single line text, mainly useful as label before element"""
191 elem = self.__createElem('label', name, self.currentLayout) 291 elem = self._createElem('label', name, self.currentLayout)
192 elem.setAttribute('value', text) 292 elem.setAttribute('value', text)
293 return elem
193 294
194 def addString(self, name=None, value=None): 295 def addString(self, name=None, value=None):
195 """Add a string box""" 296 """Add a string box"""
196 elem = self.__createElem('string', name, self.currentLayout) 297 elem = self._createElem('string', name, self.currentLayout)
197 if value: 298 if value:
198 elem.setAttribute('value', value) 299 elem.setAttribute('value', value)
300 return elem
199 301
200 def addPassword(self, name=None, value=None): 302 def addPassword(self, name=None, value=None):
201 """Add a password box""" 303 """Add a password box"""
202 elem = self.__createElem('password', name, self.currentLayout) 304 elem = self._createElem('password', name, self.currentLayout)
203 if value: 305 if value:
204 elem.setAttribute('value', value) 306 elem.setAttribute('value', value)
307 return elem
205 308
206 def addTextBox(self, name=None, value=None): 309 def addTextBox(self, name=None, value=None):
207 """Add a string box""" 310 """Add a string box"""
208 elem = self.__createElem('textbox', name, self.currentLayout) 311 elem = self._createElem('textbox', name, self.currentLayout)
209 if value: 312 if value:
210 elem.setAttribute('value', value) 313 elem.setAttribute('value', value)
314 return elem
211 315
212 def addBool(self, name=None, value="true"): 316 def addBool(self, name=None, value="true"):
213 """Add a string box""" 317 """Add a string box"""
214 assert value in ["true", "false"] 318 assert value in ["true", "false"]
215 elem = self.__createElem('bool', name, self.currentLayout) 319 elem = self._createElem('bool', name, self.currentLayout)
216 elem.setAttribute('value', value) 320 elem.setAttribute('value', value)
321 return elem
217 322
218 def addList(self, options, name=None, value=None, style=set()): 323 def addList(self, options, name=None, value=None, style=set()):
219 """Add a list of choices""" 324 """Add a list of choices"""
220 styles = set(style) 325 styles = set(style)
221 assert options 326 assert options
222 assert styles.issubset(['multi']) 327 assert styles.issubset(['multi'])
223 elem = self.__createElem('list', name, self.currentLayout) 328 elem = self._createElem('list', name, self.currentLayout)
224 self.addOptions(options, elem) 329 self.addOptions(options, elem)
225 if value: 330 if value:
226 elem.setAttribute('value', value) 331 elem.setAttribute('value', value)
227 for style in styles: 332 for style in styles:
228 elem.setAttribute(style, 'yes') 333 elem.setAttribute(style, 'yes')
334 return elem
335
336 def addAdvancedList(self, name=None, headers=None, items=None):
337 """Create an advanced list
338 @param headers: optional headers informations
339 @param items: list of Item instances
340 @return: created element
341 """
342 elem = self._createElem('advanced_list', name, self.currentLayout)
343 self.addHeaders(headers, elem)
344 if items:
345 self.addItems(items, elem)
346 return elem
229 347
230 def addButton(self, callback_id, name, value, fields_back=[]): 348 def addButton(self, callback_id, name, value, fields_back=[]):
231 """Add a button 349 """Add a button
232 @param callback: callback which will be called if button is pressed 350 @param callback: callback which will be called if button is pressed
233 @param name: name 351 @param name: name
234 @param value: label of the button 352 @param value: label of the button
235 @fields_back: list of names of field to give back when pushing the button""" 353 @fields_back: list of names of field to give back when pushing the button
236 elem = self.__createElem('button', name, self.currentLayout) 354 """
355 elem = self._createElem('button', name, self.currentLayout)
237 elem.setAttribute('callback_id', callback_id) 356 elem.setAttribute('callback_id', callback_id)
238 elem.setAttribute('value', value) 357 elem.setAttribute('value', value)
239 for field in fields_back: 358 for field in fields_back:
240 fback_el = self.doc.createElement('field_back') 359 fback_el = self.doc.createElement('field_back')
241 fback_el.setAttribute('name', field) 360 fback_el.setAttribute('name', field)
242 elem.appendChild(fback_el) 361 elem.appendChild(fback_el)
243 362 return elem
244 def addElement(self, type_, name=None, value=None, options=None, callback_id=None): 363
364 def addElement(self, type_, name=None, value=None, options=None, callback_id=None, headers=None, available=None):
245 """Convenience method to add element, the params correspond to the ones in addSomething methods""" 365 """Convenience method to add element, the params correspond to the ones in addSomething methods"""
246 if type_ == 'empty': 366 if type_ == 'empty':
247 self.addEmpty(name) 367 return self.addEmpty(name)
248 elif type_ == 'text': 368 elif type_ == 'text':
249 assert value is not None 369 assert value is not None
250 self.addText(value, name) 370 return self.addText(value, name)
251 elif type_ == 'label': 371 elif type_ == 'label':
252 assert(value) 372 assert(value)
253 self.addLabel(value) 373 return self.addLabel(value)
254 elif type_ == 'string': 374 elif type_ == 'string':
255 self.addString(name, value) 375 return self.addString(name, value)
256 elif type_ == 'password': 376 elif type_ == 'password':
257 self.addPassword(name, value) 377 return self.addPassword(name, value)
258 elif type_ == 'textbox': 378 elif type_ == 'textbox':
259 self.addTextBox(name, value) 379 return self.addTextBox(name, value)
260 elif type_ == 'bool': 380 elif type_ == 'bool':
261 if not value: 381 if not value:
262 value = "true" 382 value = "true"
263 self.addBool(name, value) 383 return self.addBool(name, value)
264 elif type_ == 'list': 384 elif type_ == 'list':
265 self.addList(options, name, value) 385 return self.addList(options, name, value)
386 elif type_ == 'advancedlist':
387 return self.addAdvancedList(name, headers, available)
266 elif type_ == 'button': 388 elif type_ == 'button':
267 assert(callback_id and value) 389 assert(callback_id and value)
268 self.addButton(callback_id, name, value) 390 return self.addButton(callback_id, name, value)
391
392 # List
269 393
270 def addOptions(self, options, parent): 394 def addOptions(self, options, parent):
271 """Add options to a multi-values element (e.g. list) 395 """Add options to a multi-values element (e.g. list)
272 @param parent: multi-values element""" 396 @param parent: multi-values element"""
273 for option in options: 397 for option in options:
274 opt = self.doc.createElement('option') 398 opt = self.doc.createElement('option')
275 opt.setAttribute('value', option) 399 opt.setAttribute('value', option)
276 parent.appendChild(opt) 400 parent.appendChild(opt)
401
402 # Advanced list
403
404 def addHeaders(self, headers, parent):
405 headers_elt = self.doc.createElement('headers')
406 for header in headers:
407 field_elt = self.doc.createElement('field')
408 field_elt.setAttribute('field_name', header.field_name)
409 field_elt.setAttribute('label', header.label)
410 if header.description:
411 field_elt.setAttribute('description', header.description)
412 if header.type:
413 field_elt.setAttribute('type', header.type)
414 headers_elt.appendChild(field_elt)
415 parent.appendChild(headers_elt)
416
417 def addItems(self, items, parent):
418 """Add items to an AdvancedList
419 @param items: list of Item instances
420 @param parent: parent element (should be addAdvancedList)
421
422 """
423 items_elt = self.doc.createElement('items')
424 for item in items:
425 item_elt = self.doc.createElement('item')
426 if item.text is not None:
427 text_elt = self.doc.createElement('text')
428 text_elt.appendChild(self.doc.createTextNode(item.text))
429 item_elt.appendChild(text_elt)
430
431 for field in item.fields:
432 field_elt = self.doc.createElement('field')
433 field_elt.setAttribute('name', field.name)
434 field_elt.setAttribute('value', field.value)
435 item_elt.appendChild(field_elt)
436
437 items_elt.appendChild(item_elt)
438
439 parent.appendChild(items_elt)
440
441 # Tabs
277 442
278 def addCategory(self, name, layout, label=None): 443 def addCategory(self, name, layout, label=None):
279 """Add a category to current layout (must be a tabs layout)""" 444 """Add a category to current layout (must be a tabs layout)"""
280 assert(layout != 'tabs') 445 assert(layout != 'tabs')
281 if not self.parentTabsLayout: 446 if not self.parentTabsLayout: