Mercurial > libervia-backend
view src/tools/xml_tools.py @ 1478:90130847a0a8
frontends, primitivus: JidsList XMLUI widget management /!\ urwid SàText must be updated
author | Goffi <goffi@goffi.org> |
---|---|
date | Thu, 20 Aug 2015 18:36:53 +0200 |
parents | 48706f4ff19c |
children | a77217511afd |
line wrap: on
line source
#!/usr/bin/python # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015 Jérôme Poisson (goffi@goffi.org) # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. from sat.core.i18n import _ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) from xml.dom import minidom, NotFoundErr from wokkel import data_form from twisted.words.xish import domish from twisted.words.protocols.jabber import jid from sat.core import exceptions """This library help manage XML used in SàT (parameters, registration, etc)""" SAT_FORM_PREFIX = "SAT_FORM_" SAT_PARAM_SEPARATOR = "_XMLUI_PARAM_" # used to have unique elements names # Helper functions def _dataFormField2XMLUIData(field, read_only=False): """Get data needed to create an XMLUI's Widget from Wokkel's data_form's Field. The attribute field can be modified (if it's fixed and it has no value). @param field (data_form.Field): a field with attributes "value", "fieldType", "label" and "var" @param read_only (bool): if True and it makes sense, create a read only input widget @return: a tuple (widget_type, widget_args, widget_kwargs) """ widget_args = [field.value] widget_kwargs = {} if field.fieldType == 'fixed' or field.fieldType is None: widget_type = 'text' if field.value is None: if field.label is None: log.warning(_("Fixed field has neither value nor label, ignoring it")) field.value = "" else: field.value = field.label field.label = None widget_args[0] = field.value elif field.fieldType == 'text-single': widget_type = "string" widget_kwargs['read_only'] = read_only elif field.fieldType == 'jid-single': widget_type = "jid_input" widget_kwargs['read_only'] = read_only elif field.fieldType == 'text-multi': widget_type = "textbox" widget_args[0] = u'\n'.join(field.values) widget_kwargs['read_only'] = read_only elif field.fieldType == 'text-private': widget_type = "password" widget_kwargs['read_only'] = read_only elif field.fieldType == 'boolean': widget_type = "bool" if widget_args[0] is None: widget_args[0] = 'false' widget_kwargs['read_only'] = read_only elif field.fieldType == 'integer': widget_type = "integer" widget_kwargs['read_only'] = read_only elif field.fieldType == 'list-single': widget_type = "list" widget_kwargs["options"] = [(option.value, option.label or option.value) for option in field.options] widget_kwargs["selected"] = widget_args widget_args = [] else: log.error(u"FIXME FIXME FIXME: Type [%s] is not managed yet by SàT" % field.fieldType) widget_type = "string" widget_kwargs['read_only'] = read_only if field.var: widget_kwargs["name"] = field.var return widget_type, widget_args, widget_kwargs def dataForm2Widgets(form_ui, form, read_only=False): """Complete an existing XMLUI with widget converted from XEP-0004 data forms. @param form_ui (XMLUI): XMLUI instance @param form (data_form.Form): Wokkel's implementation of data form @param read_only (bool): if True and it makes sense, create a read only input widget @return: the completed XMLUI instance """ if form.instructions: form_ui.addText('\n'.join(form.instructions), 'instructions') form_ui.changeContainer("pairs") for field in form.fieldList: widget_type, widget_args, widget_kwargs = _dataFormField2XMLUIData(field, read_only) label = field.label or field.var if label: form_ui.addLabel(label) else: form_ui.addEmpty() form_ui.addWidget(widget_type, *widget_args, **widget_kwargs) return form_ui def dataForm2XMLUI(form, submit_id, session_id=None, read_only=False): """Take a data form (XEP-0004, Wokkel's implementation) and convert it to a SàT XMLUI. @param form (data_form.Form): a Form instance @param submit_id (unicode): callback id to call when submitting form @param session_id (unicode): session id to return with the data @param read_only (bool): if True and it makes sense, create a read only input widget @return: XMLUI instance """ form_ui = XMLUI("form", "vertical", submit_id=submit_id, session_id=session_id) return dataForm2Widgets(form_ui, form, read_only=read_only) def dataFormResult2AdvancedList(xmlui, form_xml): """Take a raw data form (not parsed by XEP-0004) and convert it to an advanced list. The raw data form is used because Wokkel doesn't manage result items parsing yet. @param xmlui (XMLUI): the XMLUI where the AdvancedList will be added @param form_xml (domish.Element): element of the data form @return: AdvancedList element """ headers = {} 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.elements(): if elt.name != "field": raise exceptions.DataError("Unexpected tag") name = elt["var"] label = elt.attributes.get('label', '') type_ = elt.attributes.get('type') headers[name] = (label, type_) if not headers: raise exceptions.DataError("No reported fields (see XEP-0004 §3.4)") adv_list = AdvancedListContainer(xmlui, headers=headers, columns=len(headers), parent=xmlui.current_container) xmlui.changeContainer(adv_list) item_elts = form_xml.elements('jabber:x:data', 'item') for item_elt in item_elts: for elt in item_elt.elements(): if elt.name != 'field': log.warning(u"Unexpected tag (%s)" % elt.name) continue field = data_form.Field.fromElement(elt) widget_type, widget_args, widget_kwargs = _dataFormField2XMLUIData(field) xmlui.addWidget(widget_type, *widget_args, **widget_kwargs) return xmlui def dataFormResult2XMLUI(form_elt, session_id=None): """Take a raw data form (not parsed by XEP-0004) and convert it to a SàT XMLUI. The raw data form is used because Wokkel doesn't manage result items parsing yet. @param form_elt (domish.Element): element of the data form @param session_id (unicode): session id to return with the data @return: XMLUI instance """ xml_ui = XMLUI("window", "vertical", session_id=session_id) try: dataFormResult2AdvancedList(xml_ui, form_elt) except exceptions.DataError: parsed_form = data_form.Form.fromElement(form_elt) dataForm2Widgets(xml_ui, parsed_form, read_only=True) return xml_ui def _cleanValue(value): """Workaround method to avoid DBus types with D-Bus bridge. @param value: value to clean @return: value in a non DBus type (only clean string yet) """ # XXX: must be removed when DBus types will no cause problems anymore # FIXME: should be cleaned inside D-Bus bridge itself if isinstance(value, basestring): return unicode(value) return value def XMLUIResult2DataFormResult(xmlui_data): """ Extract form data from a XMLUI return. @param xmlui_data (dict): data returned by frontends for XMLUI form @return: dict of data usable by Wokkel's data form """ return {key[len(SAT_FORM_PREFIX):]: _cleanValue(value) for key, value in xmlui_data.iteritems() if key.startswith(SAT_FORM_PREFIX)} def formEscape(name): """Return escaped name for forms. @param name (unicode): form name @return: unicode """ return u"%s%s" % (SAT_FORM_PREFIX, name) def XMLUIResultToElt(xmlui_data): """Construct result domish.Element from XMLUI result. @param xmlui_data (dict): data returned by frontends for XMLUI form @return: domish.Element """ form = data_form.Form('submit') form.makeFields(XMLUIResult2DataFormResult(xmlui_data)) return form.toElement() def tupleList2dataForm(values): """Convert a list of tuples (name, value) to a wokkel submit data form. @param values (list): list of tuples @return: data_form.Form """ form = data_form.Form('submit') for value in values: field = data_form.Field(var=value[0], value=value[1]) form.addField(field) return form def paramsXML2XMLUI(xml): """Convert the XML for parameter to a SàT XML User Interface. @param xml (unicode) @return: XMLUI """ # TODO: refactor params and use Twisted directly to parse XML params_doc = minidom.parseString(xml.encode('utf-8')) top = params_doc.documentElement if top.nodeName != 'params': raise exceptions.DataError(_('INTERNAL ERROR: parameters xml not valid')) param_ui = XMLUI("param", "tabs") tabs_cont = param_ui.current_container for category in top.getElementsByTagName("category"): category_name = category.getAttribute('name') label = category.getAttribute('label') if not category_name: raise exceptions.DataError(_('INTERNAL ERROR: params categories must have a name')) tabs_cont.addTab(category_name, label=label, container=PairsContainer) for param in category.getElementsByTagName("param"): widget_kwargs = {} param_name = param.getAttribute('name') param_label = param.getAttribute('label') type_ = param.getAttribute('type') if not param_name and type_ != 'text': raise exceptions.DataError(_('INTERNAL ERROR: params must have a name')) value = param.getAttribute('value') or None callback_id = param.getAttribute('callback_id') or None if type_ == 'list': options, selected = _paramsGetListOptions(param) widget_kwargs['options'] = options widget_kwargs['selected'] = selected elif type_ == 'jids_list': widget_kwargs['jids'] = _paramsGetListJids(param) if type_ in ("button", "text"): param_ui.addEmpty() value = param_label else: param_ui.addLabel(param_label or param_name) if value: widget_kwargs["value"] = value if callback_id: widget_kwargs['callback_id'] = callback_id others = ["%s%s%s" % (category_name, SAT_PARAM_SEPARATOR, other.getAttribute('name')) for other in category.getElementsByTagName('param') if other.getAttribute('type') != 'button'] widget_kwargs['fields_back'] = others widget_kwargs['name'] = "%s%s%s" % (category_name, SAT_PARAM_SEPARATOR, param_name) param_ui.addWidget(type_, **widget_kwargs) return param_ui.toXml() def _paramsGetListOptions(param): """Retrieve the options for list element. The <option/> tags must be direct children of <param/>. @param param (domish.Element): element @return: a tuple (options, selected_value) """ if len(param.getElementsByTagName("options")) > 0: raise exceptions.DataError(_("The 'options' tag is not allowed in parameter of type 'list'!")) elems = param.getElementsByTagName("option") if len(elems) == 0: return [] options = [elem.getAttribute("value") for elem in elems] selected = [elem.getAttribute("value") for elem in elems if elem.getAttribute("selected") == 'true'] return (options, selected) def _paramsGetListJids(param): """Retrive jids from a jids_list element. the <jid/> tags must be direct children of <param/> @param param (domish.Element): element @return: a list of jids """ elems = param.getElementsByTagName("jid") jids = [elem.firstChild.data for elem in elems if elem.firstChild is not None and elem.firstChild.nodeType == elem.TEXT_NODE] return jids ### XMLUI Elements ### class Element(object): """ Base XMLUI element """ type = None def __init__(self, xmlui, parent=None): """Create a container element @param xmlui: XMLUI instance @parent: parent element """ assert self.type is not None if not hasattr(self, 'elem'): self.elem = parent.xmlui.doc.createElement(self.type) self.xmlui = xmlui if parent is not None: parent.append(self) self.parent = parent def append(self, child): self.elem.appendChild(child.elem) child.parent = self class TopElement(Element): """ Main XML Element """ type = 'top' def __init__(self, xmlui): self.elem = xmlui.doc.documentElement super(TopElement, self).__init__(xmlui) class TabElement(Element): """ Used by TabsContainer to give name and label to tabs.""" type = 'tab' def __init__(self, parent, name, label): if not isinstance(parent, TabsContainer): raise exceptions.DataError(_("TabElement must be a child of TabsContainer")) super(TabElement, self).__init__(parent.xmlui, parent) self.elem.setAttribute('name', name) self.elem.setAttribute('label', label) class FieldBackElement(Element): """ Used by ButtonWidget to indicate which field have to be sent back """ type = 'field_back' def __init__(self, parent, name): assert isinstance(parent, ButtonWidget) super(FieldBackElement, self).__init__(parent.xmlui, parent) self.elem.setAttribute('name', name) class InternalFieldElement(Element): """ Used by internal callbacks to indicate which fields are manipulated """ type = 'internal_field' def __init__(self, parent, name): super(InternalFieldElement, self).__init__(parent.xmlui, parent) self.elem.setAttribute('name', name) class InternalDataElement(Element): """ Used by internal callbacks to retrieve extra data """ type = 'internal_data' def __init__(self, parent, children): super(InternalDataElement, self).__init__(parent.xmlui, parent) assert isinstance(children, list) for child in children: self.elem.childNodes.append(child) class OptionElement(Element): """" Used by ListWidget to specify options """ type = 'option' def __init__(self, parent, option, selected=False): """ @param parent @param option (string, tuple) @param selected (boolean) """ assert isinstance(parent, ListWidget) super(OptionElement, self).__init__(parent.xmlui, parent) if isinstance(option, basestring): value, label = option, option elif isinstance(option, tuple): value, label = option else: raise NotImplementedError self.elem.setAttribute('value', value) self.elem.setAttribute('label', label) if selected: self.elem.setAttribute('selected', 'true') class JidElement(Element): """" Used by JidsListWidget to specify jids""" type = 'jid' def __init__(self, parent, jid_): """ @param jid_(jid.JID, unicode): jid to append """ assert isinstance(parent, JidsListWidget) super(JidElement, self).__init__(parent.xmlui, parent) if isinstance(jid_, jid.JID): value = jid.full elif isinstance(jid_, basestring): value = unicode(jid_) else: raise NotImplementedError jid_txt = self.xmlui.doc.createTextNode(value) self.elem.appendChild(jid_txt) class RowElement(Element): """" Used by AdvancedListContainer """ type = 'row' def __init__(self, parent): assert isinstance(parent, AdvancedListContainer) super(RowElement, self).__init__(parent.xmlui, parent) if parent.next_row_idx is not None: if parent.auto_index: raise exceptions.DataError(_("Can't set row index if auto_index is True")) self.elem.setAttribute('index', parent.next_row_idx) parent.next_row_idx = None class HeaderElement(Element): """" Used by AdvancedListContainer """ type = 'header' def __init__(self, parent, name=None, label=None, description=None): """ @param parent: AdvancedListContainer instance @param name: name of the container @param label: label to be displayed in columns @param description: long descriptive text """ assert isinstance(parent, AdvancedListContainer) super(HeaderElement, self).__init__(parent.xmlui, parent) if name: self.elem.setAttribute('name', name) if label: self.elem.setAttribute('label', label) if description: self.elem.setAttribute('description', description) ## Containers ## class Container(Element): """ And Element which contains other ones and has a layout """ type = None def __init__(self, xmlui, parent=None): """Create a container element @param xmlui: XMLUI instance @parent: parent element or None """ self.elem = xmlui.doc.createElement('container') super(Container, self).__init__(xmlui, parent) self.elem.setAttribute('type', self.type) def getParentContainer(self): """ Return first parent container @return: parent container or None """ current = self.parent while(not isinstance(current, (Container)) and current is not None): current = current.parent return current class VerticalContainer(Container): type = "vertical" class HorizontalContainer(Container): type = "horizontal" class PairsContainer(Container): type = "pairs" class TabsContainer(Container): type = "tabs" def addTab(self, name, label=None, container=VerticalContainer): """Add a tab""" if not label: label = name tab_elt = TabElement(self, name, label) new_container = container(self.xmlui, tab_elt) self.xmlui.changeContainer(new_container) def end(self): """ Called when we have finished tabs change current container to first container parent """ parent_container = self.getParentContainer() self.xmlui.changeContainer(parent_container) class AdvancedListContainer(Container): """A list which can contain other widgets, headers, etc""" type = "advanced_list" def __init__(self, xmlui, callback_id=None, name=None, headers=None, items=None, columns=None, selectable='no', auto_index=False, parent=None): """Create an advanced list @param headers: optional headers information @param callback_id: id of the method to call when selection is done @param items: list of widgets to add (just the first row) @param columns: number of columns in this table, or None to autodetect @param selectable: one of: 'no': nothing is done 'single': one row can be selected @param auto_index: if True, indexes will be generated by frontends, starting from 0 @return: created element """ assert selectable in ('no', 'single') if not items and columns is None: raise exceptions.DataError(_("either items or columns need do be filled")) if headers is None: headers = [] if items is None: items = [] super(AdvancedListContainer, self).__init__(xmlui, parent) if columns is None: columns = len(items[0]) self._columns = columns self._item_idx = 0 self.current_row = None if headers: if len(headers) != self._columns: raise exceptions.DataError(_("Headers lenght doesn't correspond to columns")) self.addHeaders(headers) if items: self.addItems(items) self.elem.setAttribute('columns', str(self._columns)) if callback_id is not None: self.elem.setAttribute('callback', callback_id) self.elem.setAttribute('selectable', selectable) self.auto_index = auto_index if auto_index: self.elem.setAttribute('auto_index', 'true') self.next_row_idx = None def addHeaders(self, headers): for header in headers: self.addHeader(header) def addHeader(self, header): pass # TODO def addItems(self, items): for item in items: self.append(item) def setRowIndex(self, idx): """ Set index for next row index are returned when a row is selected, in data's "index" key @param idx: string index to associate to the next row """ self.next_row_idx = idx def append(self, child): if isinstance(child, RowElement): return super(AdvancedListContainer, self).append(child) if self._item_idx % self._columns == 0: self.current_row = RowElement(self) self.current_row.append(child) self._item_idx += 1 def end(self): """ Called when we have finished list change current container to first container parent """ if self._item_idx % self._columns != 0: raise exceptions.DataError(_("Incorrect number of items in list")) parent_container = self.getParentContainer() self.xmlui.changeContainer(parent_container) ## Widgets ## class Widget(Element): type = None def __init__(self, xmlui, name=None, parent=None): """Create an element @param xmlui: XMLUI instance @param name: name of the element or None @param parent: parent element or None """ self.elem = xmlui.doc.createElement('widget') super(Widget, self).__init__(xmlui, parent) if name: self.elem.setAttribute('name', name) self.elem.setAttribute('type', self.type) def setInternalCallback(self, callback, fields, data_elts=None): """Set an internal UI callback when the widget value is changed. The internal callbacks are NO callback ids, they are strings from a predefined set of actions that are running in the scope of XMLUI. @param callback (string): a value from: - 'copy': process the widgets given in 'fields' two by two, by copying the values of one widget to the other. Target widgets of type List do not accept the empty value. - 'move': same than copy but moves the values if the source widget is not a List. - 'groups_of_contact': process the widgets two by two, assume A is is a list of JID and B a list of groups, select in B the groups to which the JID selected in A belongs. - more operation to be added when necessary... @param fields (list): a list of widget names (string) @param data_elts (list[Element]): extra data elements """ self.elem.setAttribute('internal_callback', callback) if fields: for field in fields: InternalFieldElement(self, field) if data_elts: InternalDataElement(self, data_elts) class EmptyWidget(Widget): """Place holder widget""" type = 'empty' class TextWidget(Widget): """Used for blob of text""" type = 'text' def __init__(self, xmlui, value, name=None, parent=None): super(TextWidget, self).__init__(xmlui, name, parent) value_elt = self.xmlui.doc.createElement('value') text = self.xmlui.doc.createTextNode(value) value_elt.appendChild(text) self.elem.appendChild(value_elt) class LabelWidget(Widget): """One line blob of text used most of time to display the desciption or name of the next widget """ type = 'label' def __init__(self, xmlui, label, name=None, parent=None): super(LabelWidget, self).__init__(xmlui, name, parent) self.elem.setAttribute('value', label) class JidWidget(Widget): """Used to display a Jabber ID, some specific methods can be added""" type = 'jid' def __init__(self, xmlui, jid, name=None, parent=None): super(JidWidget, self).__init__(xmlui, name, parent) try: self.elem.setAttribute('value', jid.full()) except AttributeError: self.elem.setAttribute('value', unicode(jid)) class DividerWidget(Widget): type = 'divider' def __init__(self, xmlui, style='line', name=None, parent=None): """ Create a divider @param xmlui: XMLUI instance @param style: one of: - line: a simple line - dot: a line of dots - dash: a line of dashes - plain: a full thick line - blank: a blank line/space @param name: name of the widget @param parent: parent container """ super(DividerWidget, self).__init__(xmlui, name, parent) self.elem.setAttribute('style', style) ### Inputs ### class InputWidget(Widget): """Widget which can accept user inputs used mainly in forms """ def __init__(self, xmlui, name=None, parent=None, read_only=False): super(InputWidget, self).__init__(xmlui, name, parent) if read_only: self.elem.setAttribute('read_only', 'true') class StringWidget(InputWidget): type = 'string' def __init__(self, xmlui, value=None, name=None, parent=None, read_only=False): super(StringWidget, self).__init__(xmlui, name, parent, read_only=read_only) if value: value_elt = self.xmlui.doc.createElement('value') text = self.xmlui.doc.createTextNode(value) value_elt.appendChild(text) self.elem.appendChild(value_elt) class PasswordWidget(StringWidget): type = 'password' class TextBoxWidget(StringWidget): type = 'textbox' class JidInputWidget(StringWidget): type = 'jid_input' # TODO handle min and max values class IntWidget(StringWidget): type = 'int' def __init__(self, xmlui, value=0, name=None, parent=None, read_only=False): try: int(value) except ValueError: raise exceptions.DataError(_("Value must be an integer")) super(IntWidget, self).__init__(xmlui, value, name, parent, read_only=read_only) class BoolWidget(InputWidget): type = 'bool' def __init__(self, xmlui, value='false', name=None, parent=None, read_only=False): if isinstance(value, bool): value = 'true' if value else 'false' elif value == '0': value = 'false' elif value == '1': value = 'true' if value not in ('true', 'false'): raise exceptions.DataError(_("Value must be 0, 1, false or true")) super(BoolWidget, self).__init__(xmlui, name, parent, read_only=read_only) self.elem.setAttribute('value', value) class ButtonWidget(Widget): type = 'button' def __init__(self, xmlui, callback_id, value=None, fields_back=None, name=None, parent=None): """Add a button @param callback_id: callback which will be called if button is pressed @param value: label of the button @fields_back: list of names of field to give back when pushing the button @param name: name @param parent: parent container """ if fields_back is None: fields_back = [] super(ButtonWidget, self).__init__(xmlui, name, parent) self.elem.setAttribute('callback', callback_id) if value: self.elem.setAttribute('value', value) for field in fields_back: FieldBackElement(self, field) class ListWidget(InputWidget): type = 'list' def __init__(self, xmlui, options, selected=None, styles=None, name=None, parent=None): """ @param xmlui @param options (list[option]): each option can be given as: - a single string if the label and the value are the same - a tuple with a couple of string (value,label) if the label and the value differ @param selected (list[string]): list of the selected values @param styles (iterable[string]): flags to set the behaviour of the list @param name (string) @param parent """ styles = set() if styles is None else set(styles) if styles is None: styles = set() else: styles = set(styles) if not options: log.warning(_('empty "options" list')) if not styles.issubset(['multi']): raise exceptions.DataError(_("invalid styles")) super(ListWidget, self).__init__(xmlui, name, parent) self.addOptions(options, selected) for style in styles: self.elem.setAttribute(style, 'yes') def addOptions(self, options, selected=None): """Add options to a multi-values element (e.g. list) """ if selected: if isinstance(selected, basestring): selected = [selected] else: selected = [] for option in options: assert isinstance(option, basestring) or isinstance(option, tuple) value = option if isinstance(option, basestring) else option[0] OptionElement(self, option, value in selected) class JidsListWidget(InputWidget): """A list of text or jids where elements can be added/removed or modified""" type = 'jids_list' def __init__(self, xmlui, jids, styles=None, name=None, parent=None): """ @param xmlui @param jids (list[jid.JID]): base jids @param styles (iterable[string]): flags to set the behaviour of the list @param name (string) @param parent """ super(JidsListWidget, self).__init__(xmlui, name, parent) styles = set() if styles is None else set(styles) if not styles.issubset([]): # TODO raise exceptions.DataError(_("invalid styles")) for style in styles: self.elem.setAttribute(style, 'yes') if not jids: log.debug('empty jids list') else: self.addJids(jids) def addJids(self, jids): for jid_ in jids: JidElement(self, jid_) ## Dialog Elements ## class DialogElement(Element): """Main dialog element """ type = 'dialog' def __init__(self, parent, type_, level=None): if not isinstance(parent, TopElement): raise exceptions.DataError(_("DialogElement must be a direct child of TopElement")) super(DialogElement, self).__init__(parent.xmlui, parent) self.elem.setAttribute(C.XMLUI_DATA_TYPE, type_) self.elem.setAttribute(C.XMLUI_DATA_LVL, level or C.XMLUI_DATA_LVL_DEFAULT) class MessageElement(Element): """Element with the instruction message""" type = C.XMLUI_DATA_MESS def __init__(self, parent, message): if not isinstance(parent, DialogElement): raise exceptions.DataError(_("MessageElement must be a direct child of DialogElement")) super(MessageElement, self).__init__(parent.xmlui, parent) message_txt = self.xmlui.doc.createTextNode(message) self.elem.appendChild(message_txt) class ButtonsElement(Element): """Buttons element which indicate which set to use""" type = 'buttons' def __init__(self, parent, set_): if not isinstance(parent, DialogElement): raise exceptions.DataError(_("ButtonsElement must be a direct child of DialogElement")) super(ButtonsElement, self).__init__(parent.xmlui, parent) self.elem.setAttribute('set', set_) class FileElement(Element): """File element used for FileDialog""" type = 'file' def __init__(self, parent, type_): if not isinstance(parent, DialogElement): raise exceptions.DataError(_("FileElement must be a direct child of DialogElement")) super(FileElement, self).__init__(parent.xmlui, parent) self.elem.setAttribute('type', type_) ## XMLUI main class class XMLUI(object): """This class is used to create a user interface (form/window/parameters/etc) using SàT XML""" def __init__(self, panel_type="window", container="vertical", dialog_opt=None, title=None, submit_id=None, session_id=None): """Init SàT XML Panel @param panel_type: one of - C.XMLUI_WINDOW (new window) - C.XMLUI_POPUP - C.XMLUI_FORM (formulaire, depend of the frontend, usually a panel with cancel/submit buttons) - C.XMLUI_PARAM (parameters, presentation depend of the frontend) - C.XMLUI_DIALOG (one common dialog, presentation depend of frontend) @param container: disposition of elements, one of: - vertical: elements are disposed up to bottom - horizontal: elements are disposed left to right - pairs: elements come on two aligned columns (usually one for a label, the next for the element) - tabs: elemens are in categories with tabs (notebook) @param dialog_opt: only used if panel_type == C.XMLUI_DIALOG. Dictionnary (string/string) where key can be: - C.XMLUI_DATA_TYPE: type of dialog, value can be: - C.XMLUI_DIALOG_MESSAGE (default): an information/error message. Action of user is necessary to close the dialog. Usually the frontend display a classic popup - C.XMLUI_DIALOG_NOTE: like a C.XMLUI_DIALOG_MESSAGE, but action of user is not necessary to close, at frontend choice (it can be closed after a timeout). Usually the frontend display as a timed out notification - C.XMLUI_DIALOG_CONFIRM: dialog with 2 choices (usualy "Ok"/"Cancel"). returned data can contain: - "answer": "true" if answer is "ok", "yes" or equivalent, "false" else - C.XLMUI_DIALOG_FILE: a file selection dialog returned data can contain: - "cancelled": "true" if dialog has been cancelled, not present or "false" else - "path": path of the choosed file/dir - C.XMLUI_DATA_MESS: message shown in dialog - C.XMLUI_DATA_LVL: one of: - C.XMLUI_DATA_LVL_INFO (default): normal message - C.XMLUI_DATA_LVL_WARNING: attention of user is important - C.XMLUI_DATA_LVL_ERROR: something went wrong - C.XMLUI_DATA_BTNS_SET: one of: - C.XMLUI_DATA_BTNS_SET_OKCANCEL (default): classical "OK" and "Cancel" set - C.XMLUI_DATA_BTNS_SET_YESNO: a translated "yes" for OK, and "no" for Cancel @param title: title or default if None @param submit_id: callback id to call for panel_type we can submit (form, param, dialog) @param session_id: use to keep a session attached to the dialog, must be returned by frontends """ self._introspect() if panel_type not in [C.XMLUI_WINDOW, C.XMLUI_FORM, C.XMLUI_PARAM, C.XMLUI_POPUP, C.XMLUI_DIALOG]: raise exceptions.DataError(_("Unknown panel type [%s]") % panel_type) if panel_type == C.XMLUI_FORM and submit_id is None: raise exceptions.DataError(_("form XMLUI need a submit_id")) if not isinstance(container, basestring): raise exceptions.DataError(_("container argument must be a string")) if dialog_opt is not None and panel_type != C.XMLUI_DIALOG: raise exceptions.DataError(_("dialog_opt can only be used with dialog panels")) self.type = panel_type impl = minidom.getDOMImplementation() self.doc = impl.createDocument(None, "sat_xmlui", None) top_element = self.doc.documentElement top_element.setAttribute("type", panel_type) if title: top_element.setAttribute("title", title) self.submit_id = submit_id self.session_id = session_id if panel_type == C.XMLUI_DIALOG: if dialog_opt is None: dialog_opt = {} self._createDialog(dialog_opt) return self.main_container = self._createContainer(container, TopElement(self)) self.current_container = self.main_container def _introspect(self): """ Introspect module to find Widgets and Containers """ self._containers = {} self._widgets = {} for obj in globals().values(): try: if issubclass(obj, Widget): if obj.__name__ == 'Widget': continue self._widgets[obj.type] = obj elif issubclass(obj, Container): if obj.__name__ == 'Container': continue self._containers[obj.type] = obj except TypeError: pass def __del__(self): self.doc.unlink() def __getattr__(self, name): if name.startswith("add") and name not in ('addWidget',): # addWidgetName(...) create an instance of WidgetName if self.type == C.XMLUI_DIALOG: raise exceptions.InternalError(_("addXXX can't be used with dialogs")) class_name = name[3:] + "Widget" if class_name in globals(): cls = globals()[class_name] if issubclass(cls, Widget): def createWidget(*args, **kwargs): if "parent" not in kwargs: kwargs["parent"] = self.current_container if "name" not in kwargs and issubclass(cls, InputWidget): # name can be given as first argument or in keyword arguments for InputWidgets args = list(args) kwargs["name"] = args.pop(0) return cls(self, *args, **kwargs) return createWidget return object.__getattribute__(self, name) @property def submit_id(self): top_element = self.doc.documentElement value = top_element.getAttribute("submit") return value or None @submit_id.setter def submit_id(self, value): top_element = self.doc.documentElement if value is None: try: top_element.removeAttribute("submit") except NotFoundErr: pass elif value: # submit_id can be the empty string to bypass form restriction top_element.setAttribute("submit", value) @property def session_id(self): top_element = self.doc.documentElement value = top_element.getAttribute("session_id") return value or None @session_id.setter def session_id(self, value): top_element = self.doc.documentElement if value is None: try: top_element.removeAttribute("session_id") except NotFoundErr: pass elif value: top_element.setAttribute("session_id", value) else: raise exceptions.DataError("session_id can't be empty") def _createDialog(self, dialog_opt): dialog_type = dialog_opt.setdefault(C.XMLUI_DATA_TYPE, C.XMLUI_DIALOG_MESSAGE) if dialog_type in [C.XMLUI_DIALOG_CONFIRM, C.XMLUI_DIALOG_FILE] and self.submit_id is None: raise exceptions.InternalError(_("Submit ID must be filled for this kind of dialog")) top_element = TopElement(self) level = dialog_opt.get(C.XMLUI_DATA_LVL) dialog_elt = DialogElement(top_element, dialog_type, level) try: MessageElement(dialog_elt, dialog_opt[C.XMLUI_DATA_MESS]) except KeyError: pass try: ButtonsElement(dialog_elt, dialog_opt[C.XMLUI_DATA_BTNS_SET]) except KeyError: pass try: FileElement(dialog_elt, dialog_opt[C.XMLUI_DATA_FILETYPE]) except KeyError: pass def _createContainer(self, container, parent=None, **kwargs): """Create a container element @param type: container type (cf init doc) @parent: parent element or None """ if container not in self._containers: raise exceptions.DataError(_("Unknown container type [%s]") % container) cls = self._containers[container] new_container = cls(self, parent=parent, **kwargs) return new_container def changeContainer(self, container, **kwargs): """Change the current container @param container: either container type (container it then created), or an Container instance""" if isinstance(container, basestring): self.current_container = self._createContainer(container, self.current_container.getParentContainer() or self.main_container, **kwargs) else: self.current_container = self.main_container if container is None else container assert isinstance(self.current_container, Container) return self.current_container def addWidget(self, type_, *args, **kwargs): """Convenience method to add an element""" if type_ not in self._widgets: raise exceptions.DataError(_("Invalid type [%s]") % type_) if "parent" not in kwargs: kwargs["parent"] = self.current_container cls = self._widgets[type_] return cls(self, *args, **kwargs) def toXml(self): """return the XML representation of the panel""" return self.doc.toxml() # Some sugar for XMLUI dialogs def note(message, title='', level=C.XMLUI_DATA_LVL_INFO): """Sugar to easily create a Note Dialog @param message: body of the note @param title: title of the note @param level: one of C.XMLUI_DATA_LVL_* """ note_xmlui = XMLUI(C.XMLUI_DIALOG, dialog_opt={ C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE, C.XMLUI_DATA_MESS: message, C.XMLUI_DATA_LVL: level}, title=title ) return note_xmlui # Misc other funtions class ElementParser(object): """callable class to parse XML string into Element Found at http://stackoverflow.com/questions/2093400/how-to-create-twisted-words-xish-domish-element-entirely-from-raw-xml/2095942#2095942 (c) Karl Anderson """ def __call__(self, raw_xml, force_spaces=False): """ @param raw_xml(unicode): the raw XML @param force_spaces: if True, replace occurrences of '\n' and '\t' with ' '. """ self.result = None def onStart(elem): self.result = elem def onEnd(): pass def onElement(elem): self.result.addChild(elem) parser = domish.elementStream() parser.DocumentStartEvent = onStart parser.ElementEvent = onElement parser.DocumentEndEvent = onEnd tmp = domish.Element((None, "s")) if force_spaces: raw_xml = raw_xml.replace('\n', ' ').replace('\t', ' ') tmp.addRawXml(raw_xml) parser.parse(tmp.toXml().encode('utf-8')) return self.result.firstChildElement()