changeset 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 204930b870e1
children 694f118d0cd5
files src/plugins/plugin_misc_quiz.py src/plugins/plugin_misc_tarot.py src/plugins/plugin_xep_0077.py src/tools/xml_tools.py
diffstat 4 files changed, 196 insertions(+), 31 deletions(-) [+]
line wrap: on
line diff
--- a/src/plugins/plugin_misc_quiz.py	Sun Sep 08 18:05:19 2013 +0200
+++ b/src/plugins/plugin_misc_quiz.py	Sun Sep 08 18:05:19 2013 +0200
@@ -28,7 +28,7 @@
 from zope.interface import implements
 
 from wokkel import disco, iwokkel, data_form
-from sat.tools.xml_tools import dataForm2xml
+from sat.tools.xml_tools import dataForm2XML
 from sat.tools.games import TarotCard
 
 from time import time
--- a/src/plugins/plugin_misc_tarot.py	Sun Sep 08 18:05:19 2013 +0200
+++ b/src/plugins/plugin_misc_tarot.py	Sun Sep 08 18:05:19 2013 +0200
@@ -25,7 +25,7 @@
 from zope.interface import implements
 
 from wokkel import disco, iwokkel, data_form
-from sat.tools.xml_tools import dataForm2xml
+from sat.tools.xml_tools import dataForm2XML
 from sat.tools.games import TarotCard
 
 from time import time
@@ -600,7 +600,7 @@
 
             elif elt.name == 'contrat':  # it's time to choose contrat
                 form = data_form.Form.fromElement(elt.firstChildElement())
-                xml_data = dataForm2xml(form)
+                xml_data = dataForm2XML(form)
                 self.host.bridge.tarotGameChooseContrat(room_jid.userhost(), xml_data, profile)
 
             elif elt.name == 'contrat_choosed':
@@ -753,7 +753,7 @@
                 for looser in elt.elements(name='looser', uri=NS_CG):
                     loosers.append(unicode(looser))
                 form = data_form.Form.fromElement(form_elt)
-                xml_data = dataForm2xml(form)
+                xml_data = dataForm2XML(form)
                 self.host.bridge.tarotGameScore(room_jid.userhost(), xml_data, winners, loosers, profile)
             elif elt.name == 'error':
                 if elt['type'] == 'invalid_cards':
--- a/src/plugins/plugin_xep_0077.py	Sun Sep 08 18:05:19 2013 +0200
+++ b/src/plugins/plugin_xep_0077.py	Sun Sep 08 18:05:19 2013 +0200
@@ -20,7 +20,7 @@
 from logging import debug, info, error
 from twisted.words.protocols.jabber import jid
 from twisted.words.protocols.jabber.xmlstream import IQ
-from sat.tools.xml_tools import dataForm2xml
+from sat.tools.xml_tools import dataForm2XML
 
 from wokkel import data_form
 
@@ -63,7 +63,7 @@
             return
 
         form = data_form.Form.fromElement(x_elem)
-        xml_data = dataForm2xml(form)
+        xml_data = dataForm2XML(form)
         self.host.bridge.actionResult("XMLUI", answer['id'], {"target": answer["from"], "type": "registration", "xml": xml_data}, profile)
 
     def reg_err(self, failure, profile):
--- a/src/tools/xml_tools.py	Sun Sep 08 18:05:19 2013 +0200
+++ b/src/tools/xml_tools.py	Sun Sep 08 18:05:19 2013 +0200
@@ -21,11 +21,12 @@
 from xml.dom import minidom
 from wokkel import data_form
 from twisted.words.xish import domish
+from core import exceptions
 
 """This library help manage XML used in SàT (parameters, registration, etc) """
 
 
-def dataForm2xml(form):
+def dataForm2XML(form):
     """Take a data form (xep-0004, Wokkel's implementation) and convert it to a SàT xml"""
 
     form_ui = XMLUI("form", "vertical")
@@ -60,6 +61,60 @@
         form_ui.addElement(__field_type, field.var, field.value, [option.value for option in field.options])
     return form_ui.toXml()
 
+def dataFormResult2AdvancedList(form_ui, form_xml):
+    """Take a raw data form (not parsed by XEP-0004) and convert it to an advanced list
+    raw data form is used because Wokkel doesn't manage result items parsing yet
+    @param form_ui: the XMLUI where the AdvancedList will be added²
+    @param form_xml: domish.Element of the data form
+    @return: AdvancedList element
+    """
+    headers = []
+    items = []
+    try:
+        reported_elt = form_xml.elements('jabber:x:data', 'reported').next()
+    except StopIteration:
+        raise exceptions.DataError("Couldn't find expected <reported> tag")
+
+    for elt in reported_elt.children:
+        if elt.name != "field":
+            raise exceptions.DataError("Unexpected tag")
+        name = elt["var"]
+        label = elt.attributes.get('label','')
+        type_ = elt.attributes.get('type','') # TODO
+        headers.append(Header(name, label))
+
+    if not headers:
+        raise DataError("No reported fields (see XEP-0004 §3.4)")
+
+    item_elts = form_xml.elements('jabber:x:data', 'item')
+
+    for item_elt in item_elts:
+        fields = []
+        for elt in item_elt.children:
+            if elt.name != 'field':
+                warning("Unexpected tag (%s)" % elt.name)
+                continue
+            name = elt['var']
+            child_elt = elt.firstChildElement()
+            if child_elt.name != "value":
+                raise DataError('Was expecting <value> tag')
+            value = unicode(child_elt)
+            fields.append(Field(name, value))
+        items.append(Item(' | '.join((field.value for field in fields if field)), fields))
+
+    return form_ui.addAdvancedList(None, headers, items)
+
+
+def dataFormResult2XML(form_xml):
+    """Take a raw data form (not parsed by XEP-0004) and convert it to a SàT XMLUI
+    raw data form is used because Wokkel doesn't manage result items parsing yet
+    @param form_xml: domish.Element of the data form
+    @return: XMLUI interface
+    """
+
+    form_ui = XMLUI("window", "vertical")
+    dataFormResult2AdvancedList(form_ui, form_xml)
+    return form_ui.toXml()
 
 def tupleList2dataForm(values):
     """convert a list of tuples (name,value) to a wokkel submit data form"""
@@ -70,7 +125,6 @@
 
     return form
 
-
 def paramsXml2xmlUI(xml):
     """Convert the xml for parameter to a SàT XML User Interface"""
     params_doc = minidom.parseString(xml.encode('utf-8'))
@@ -104,6 +158,50 @@
     return param_ui.toXml()
 
 
+class Header(object):
+    """AdvandeList's header"""
+
+    def __init__(self, field_name, label, description=None, type_=None):
+        """
+        @param field_name: name of the field referenced
+        @param label: label to be displayed in columns
+        @param description: long descriptive text
+        @param type_: TODO
+
+        """
+        self.field_name = field_name
+        self.label = label
+        self.description = description
+        self.type = type_
+        if type_ is not None:
+            raise NotImplementedError # TODO:
+
+
+class Item(object):
+    """Item used in AdvancedList"""
+
+    def __init__(self, text=None, fields=None):
+        """
+        @param text: Optional textual representation, when fields are not showed individually
+        @param fields: list of Field instances²
+        """
+        self.text = text
+        self.fields = fields if fields is not None else []
+
+
+class Field(object):
+    """Field used in AdvancedList (in items)"""
+
+    def __init__(self, name, value):
+        """
+        @param name: name of the field, used to identify the field in headers
+        @param value: actual content of the field
+        """
+        self.name = name
+        self.value = value
+
+
+
 class XMLUI(object):
     """This class is used to create a user interface (form/window/parameters/etc) using SàT XML"""
 
@@ -140,7 +238,7 @@
     def __del__(self):
         self.doc.unlink()
 
-    def __createLayout(self, layout, parent=None):
+    def _createLayout(self, layout, parent=None):
         """Create a layout element
         @param type: layout type (cf init doc)
         @parent: parent element or None
@@ -154,13 +252,14 @@
             parent.appendChild(layout_elt)
         return layout_elt
 
-    def __createElem(self, type_, name=None, parent=None):
+    def _createElem(self, type_, name=None, parent=None):
         """Create an element
         @param type_: one of
             - empty: empty element (usefull to skip something in a layout, e.g. skip first element in a PAIRS layout)
             - text: text to be displayed in an multi-line area, e.g. instructions
         @param name: name of the element or None
         @param parent: parent element or None
+        @return: created element
         """
         elem = self.doc.createElement('elem')
         if name:
@@ -172,100 +271,125 @@
 
     def changeLayout(self, layout):
         """Change the current layout"""
-        self.currentLayout = self.__createLayout(layout, self.currentCategory if self.currentCategory else self.doc.documentElement)
+        self.currentLayout = self._createLayout(layout, self.currentCategory if self.currentCategory else self.doc.documentElement)
         if layout == "tabs":
             self.parentTabsLayout = self.currentLayout
 
     def addEmpty(self, name=None):
         """Add a multi-lines text"""
-        self.__createElem('empty', name, self.currentLayout)
+        return self._createElem('empty', name, self.currentLayout)
 
     def addText(self, text, name=None):
         """Add a multi-lines text"""
-        elem = self.__createElem('text', name, self.currentLayout)
+        elem = self._createElem('text', name, self.currentLayout)
         text = self.doc.createTextNode(text)
         elem.appendChild(text)
+        return elem
 
     def addLabel(self, text, name=None):
         """Add a single line text, mainly useful as label before element"""
-        elem = self.__createElem('label', name, self.currentLayout)
+        elem = self._createElem('label', name, self.currentLayout)
         elem.setAttribute('value', text)
+        return elem
 
     def addString(self, name=None, value=None):
         """Add a string box"""
-        elem = self.__createElem('string', name, self.currentLayout)
+        elem = self._createElem('string', name, self.currentLayout)
         if value:
             elem.setAttribute('value', value)
+        return elem
 
     def addPassword(self, name=None, value=None):
         """Add a password box"""
-        elem = self.__createElem('password', name, self.currentLayout)
+        elem = self._createElem('password', name, self.currentLayout)
         if value:
             elem.setAttribute('value', value)
+        return elem
 
     def addTextBox(self, name=None, value=None):
         """Add a string box"""
-        elem = self.__createElem('textbox', name, self.currentLayout)
+        elem = self._createElem('textbox', name, self.currentLayout)
         if value:
             elem.setAttribute('value', value)
+        return elem
 
     def addBool(self, name=None, value="true"):
         """Add a string box"""
         assert value in ["true", "false"]
-        elem = self.__createElem('bool', name, self.currentLayout)
+        elem = self._createElem('bool', name, self.currentLayout)
         elem.setAttribute('value', value)
+        return elem
 
     def addList(self, options, name=None, value=None, style=set()):
         """Add a list of choices"""
         styles = set(style)
         assert options
         assert styles.issubset(['multi'])
-        elem = self.__createElem('list', name, self.currentLayout)
+        elem = self._createElem('list', name, self.currentLayout)
         self.addOptions(options, elem)
         if value:
             elem.setAttribute('value', value)
         for style in styles:
             elem.setAttribute(style, 'yes')
+        return elem
+
+    def addAdvancedList(self, name=None, headers=None, items=None):
+        """Create an advanced list
+        @param headers: optional headers informations
+        @param items: list of Item instances
+        @return: created element
+        """
+        elem = self._createElem('advanced_list', name, self.currentLayout)
+        self.addHeaders(headers, elem)
+        if items:
+            self.addItems(items, elem)
+        return elem
 
     def addButton(self, callback_id, name, value, fields_back=[]):
         """Add a button
         @param callback: callback which will be called if button is pressed
         @param name: name
         @param value: label of the button
-        @fields_back: list of names of field to give back when pushing the button"""
-        elem = self.__createElem('button', name, self.currentLayout)
+        @fields_back: list of names of field to give back when pushing the button
+        """
+        elem = self._createElem('button', name, self.currentLayout)
         elem.setAttribute('callback_id', callback_id)
         elem.setAttribute('value', value)
         for field in fields_back:
             fback_el = self.doc.createElement('field_back')
             fback_el.setAttribute('name', field)
             elem.appendChild(fback_el)
+        return elem
 
-    def addElement(self, type_, name=None, value=None, options=None, callback_id=None):
+    def addElement(self, type_, name=None, value=None, options=None, callback_id=None, headers=None, available=None):
         """Convenience method to add element, the params correspond to the ones in addSomething methods"""
         if type_ == 'empty':
-            self.addEmpty(name)
+            return self.addEmpty(name)
         elif type_ == 'text':
             assert value is not None
-            self.addText(value, name)
+            return self.addText(value, name)
         elif type_ == 'label':
             assert(value)
-            self.addLabel(value)
+            return self.addLabel(value)
         elif type_ == 'string':
-            self.addString(name, value)
+            return self.addString(name, value)
         elif type_ == 'password':
-            self.addPassword(name, value)
+            return self.addPassword(name, value)
         elif type_ == 'textbox':
-            self.addTextBox(name, value)
+            return self.addTextBox(name, value)
         elif type_ == 'bool':
             if not value:
                 value = "true"
-            self.addBool(name, value)
+            return self.addBool(name, value)
         elif type_ == 'list':
-            self.addList(options, name, value)
+            return self.addList(options, name, value)
+        elif type_ == 'advancedlist':
+            return self.addAdvancedList(name, headers, available)
         elif type_ == 'button':
             assert(callback_id and value)
-            self.addButton(callback_id, name, value)
+            return self.addButton(callback_id, name, value)
+
+    # List
 
     def addOptions(self, options, parent):
         """Add options to a multi-values element (e.g. list)
@@ -275,6 +399,47 @@
             opt.setAttribute('value', option)
             parent.appendChild(opt)
 
+    # Advanced list
+
+    def addHeaders(self, headers, parent):
+        headers_elt = self.doc.createElement('headers')
+        for header in headers:
+            field_elt = self.doc.createElement('field')
+            field_elt.setAttribute('field_name', header.field_name)
+            field_elt.setAttribute('label', header.label)
+            if header.description:
+                field_elt.setAttribute('description', header.description)
+            if header.type:
+                field_elt.setAttribute('type', header.type)
+            headers_elt.appendChild(field_elt)
+        parent.appendChild(headers_elt)
+
+    def addItems(self, items, parent):
+        """Add items to an AdvancedList
+        @param items: list of Item instances
+        @param parent: parent element (should be addAdvancedList)
+
+        """
+        items_elt = self.doc.createElement('items')
+        for item in items:
+            item_elt = self.doc.createElement('item')
+            if item.text is not None:
+                text_elt = self.doc.createElement('text')
+                text_elt.appendChild(self.doc.createTextNode(item.text))
+                item_elt.appendChild(text_elt)
+
+            for field in item.fields:
+                field_elt = self.doc.createElement('field')
+                field_elt.setAttribute('name', field.name)
+                field_elt.setAttribute('value', field.value)
+                item_elt.appendChild(field_elt)
+
+            items_elt.appendChild(item_elt)
+
+        parent.appendChild(items_elt)
+
+    # Tabs
+
     def addCategory(self, name, layout, label=None):
         """Add a category to current layout (must be a tabs layout)"""
         assert(layout != 'tabs')