1510 lines
56 KiB
Python
1510 lines
56 KiB
Python
#!/usr/bin/env python2
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# SAT: a jabber client
|
|
# Copyright (C) 2009-2017 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 twisted.internet import defer
|
|
from sat.core import exceptions
|
|
from collections import OrderedDict
|
|
from copy import deepcopy
|
|
import htmlentitydefs
|
|
import re
|
|
|
|
"""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
|
|
html_entity_re = re.compile(r'&([a-zA-Z]+?);')
|
|
XML_ENTITIES = ('quot', 'amp', 'apos', 'lt', 'gt')
|
|
|
|
# TODO: move XMLUI stuff in a separate module
|
|
# TODO: rewrite this with lxml or ElementTree or domish.Element: it's complicated and difficult to maintain with current minidom implementation
|
|
|
|
# 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, prepend=None, filters=None):
|
|
"""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
|
|
@param prepend(iterable, None): widgets to prepend to main LabelContainer
|
|
if not None, must be an iterable of *args for addWidget. Those widgets will
|
|
be added first to the container.
|
|
@param filters(dict, None): if not None, a dictionary of callable:
|
|
key is the name of the widget to filter
|
|
the value is a callable, it will get widget's type, args and kwargs
|
|
and must return the same variable, which it can modify
|
|
This is especially useful to modify well known fields
|
|
@return: the completed XMLUI instance
|
|
"""
|
|
if filters is None:
|
|
filters = {}
|
|
if form.instructions:
|
|
form_ui.addText('\n'.join(form.instructions), 'instructions')
|
|
|
|
form_ui.changeContainer("label")
|
|
|
|
if prepend is not None:
|
|
for widget_args in prepend:
|
|
form_ui.addWidget(*widget_args)
|
|
|
|
for field in form.fieldList:
|
|
widget_type, widget_args, widget_kwargs = _dataFormField2XMLUIData(field, read_only)
|
|
try:
|
|
widget_filter = filters[widget_kwargs['name']]
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
widget_type, widget_args, widget_kwargs = widget_filter(widget_type, widget_args, widget_kwargs)
|
|
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 (Wokkel's XEP-0004 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 dataFormEltResult2XMLUIData(form_xml):
|
|
"""Parse a data form result (not parsed by Wokkel's XEP-0004 implementation).
|
|
|
|
The raw data form is used because Wokkel doesn't manage result items parsing yet.
|
|
@param form_xml (domish.Element): element of the data form
|
|
@return: a couple (headers, result_list):
|
|
- headers (dict{unicode: unicode}): form headers (field labels and types)
|
|
- xmlui_data (list[tuple]): list of (widget_type, widget_args, widget_kwargs)
|
|
"""
|
|
headers = OrderedDict()
|
|
try:
|
|
reported_elt = form_xml.elements('jabber:x:data', 'reported').next()
|
|
except StopIteration:
|
|
raise exceptions.DataError("Couldn't find expected <reported> tag in %s" % form_xml.toXml())
|
|
|
|
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)")
|
|
|
|
xmlui_data = []
|
|
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)
|
|
|
|
xmlui_data.append(_dataFormField2XMLUIData(field))
|
|
|
|
return headers, xmlui_data
|
|
|
|
|
|
def XMLUIData2AdvancedList(xmlui, headers, xmlui_data):
|
|
"""Take a raw data form result (not parsed by Wokkel's XEP-0004 implementation) 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 headers (dict{unicode: unicode}): form headers (field labels and types)
|
|
@param xmlui_data (list[tuple]): list of (widget_type, widget_args, widget_kwargs)
|
|
@return: the completed XMLUI instance
|
|
"""
|
|
adv_list = AdvancedListContainer(xmlui, headers=headers, columns=len(headers), parent=xmlui.current_container)
|
|
xmlui.changeContainer(adv_list)
|
|
|
|
for widget_type, widget_args, widget_kwargs in xmlui_data:
|
|
xmlui.addWidget(widget_type, *widget_args, **widget_kwargs)
|
|
|
|
return xmlui
|
|
|
|
|
|
def dataFormResult2AdvancedList(xmlui, form_xml):
|
|
"""Take a raw data form result (not parsed by Wokkel's XEP-0004 implementation) 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: the completed XMLUI instance
|
|
"""
|
|
headers, xmlui_data = dataFormEltResult2XMLUIData(form_xml)
|
|
XMLUIData2AdvancedList(xmlui, headers, xmlui_data)
|
|
|
|
|
|
def dataFormEltResult2XMLUI(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 dataFormResult2XMLUI(result_form, base_form, session_id=None, prepend=None, filters=None):
|
|
"""Convert data form result to SàT XMLUI.
|
|
|
|
@param result_form (data_form.Form): result form to convert
|
|
@param base_form (data_form.Form): initial form (i.e. of form type "form")
|
|
this one is necessary to reconstruct options when needed (e.g. list elements)
|
|
@param session_id (unicode): session id to return with the data
|
|
@param prepend: same as for [dataForm2Widgets]
|
|
@param filters: same as for [dataForm2Widgets]
|
|
@return: XMLUI instance
|
|
"""
|
|
form = deepcopy(result_form)
|
|
for name, field in form.fields.iteritems():
|
|
try:
|
|
base_field = base_form.fields[name]
|
|
except KeyError:
|
|
continue
|
|
field.options = base_field.options[:]
|
|
xml_ui = XMLUI("window", "vertical", session_id=session_id)
|
|
dataForm2Widgets(xml_ui, form, read_only=True, prepend=prepend, filters=filters)
|
|
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=LabelContainer)
|
|
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
|
|
widget_kwargs['styles'] = ['extensible']
|
|
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
|
|
self.children = []
|
|
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):
|
|
"""Append a child to this element.
|
|
|
|
@param child (Element): child element
|
|
@return: the added child Element
|
|
"""
|
|
self.elem.appendChild(child.elem)
|
|
child.parent = self
|
|
self.children.append(child)
|
|
return child
|
|
|
|
|
|
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, selected=False):
|
|
"""
|
|
|
|
@param parent (TabsContainer): parent container
|
|
@param name (unicode): tab name
|
|
@param label (unicode): tab label
|
|
@param selected (bool): set to True to select this tab
|
|
"""
|
|
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)
|
|
if selected:
|
|
self.setSelected(selected)
|
|
|
|
def setSelected(self, selected=False):
|
|
"""Set the tab selected.
|
|
|
|
@param selected (bool): set to True to select this tab
|
|
"""
|
|
self.elem.setAttribute('selected', 'true' if selected else 'false')
|
|
|
|
|
|
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 LabelContainer(Container):
|
|
type = "label"
|
|
|
|
|
|
class TabsContainer(Container):
|
|
type = "tabs"
|
|
|
|
def addTab(self, name, label=None, selected=None, container=VerticalContainer):
|
|
"""Add a tab.
|
|
|
|
@param name (unicode): tab name
|
|
@param label (unicode): tab label
|
|
@param selected (bool): set to True to select this tab
|
|
@param container (class): container class, inheriting from Container
|
|
@return: the container for the new tab
|
|
"""
|
|
if not label:
|
|
label = name
|
|
tab_elt = TabElement(self, name, label, selected)
|
|
new_container = container(self.xmlui, tab_elt)
|
|
return 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)
|
|
if name in xmlui.named_widgets:
|
|
raise exceptions.ConflictError(_(u'A widget with the name "{name}" already exists.').format(name=name))
|
|
xmlui.named_widgets[name] = self
|
|
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)
|
|
|
|
@property
|
|
def value(self):
|
|
return self.elem.firstChild.firstChild.wholeText
|
|
|
|
|
|
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
|
|
@param 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'
|
|
STYLES = (u'multi', u'noselect', u'extensible', u'reducible', u'inline')
|
|
|
|
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
|
|
can be:
|
|
- multi: multiple selection is allowed
|
|
- noselect: no selection is allowed
|
|
useful when only the list itself is needed
|
|
- extensible: can be extended by user (i.e. new options can be added)
|
|
- reducible: can be reduced by user (i.e. options can be removed)
|
|
- inline: hint that this list should be displayed on a single line (e.g. list of labels)
|
|
@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 u'noselect' in styles and (u'multi' in styles or selected):
|
|
raise exceptions.DataError(_(u'"multi" flag and "selected" option are not compatible with "noselect" flag'))
|
|
if not options:
|
|
# we can have no options if we get a submitted data form
|
|
# but we can't use submitted values directly,
|
|
# because we would not have the labels
|
|
log.warning(_('empty "options" list'))
|
|
super(ListWidget, self).__init__(xmlui, name, parent)
|
|
self.addOptions(options, selected)
|
|
self.setStyles(styles)
|
|
|
|
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)
|
|
|
|
def setStyles(self, styles):
|
|
if not styles.issubset(self.STYLES):
|
|
raise exceptions.DataError(_(u"invalid styles"))
|
|
for style in styles:
|
|
self.elem.setAttribute(style, 'yes')
|
|
# TODO: check flags incompatibily (noselect and multi) like in __init__
|
|
|
|
def setStyle(self, style):
|
|
self.setStyles([style])
|
|
|
|
|
|
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)
|
|
- label: associations of one LabelWidget or EmptyWidget with an other widget
|
|
similar to pairs but specialized in LabelWidget, and not necessarily arranged in 2 columns
|
|
- 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
|
|
- C.XMLUI_DATA_FILETYPE: only used for file dialogs, one of:
|
|
- C.XMLUI_DATA_FILETYPE_FILE: a file path is requested
|
|
- C.XMLUI_DATA_FILETYPE_DIR: a dir path is requested
|
|
- C.XMLUI_DATA_FILETYPE_DEFAULT: same as C.XMLUI_DATA_FILETYPE_FILE
|
|
|
|
@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
|
|
@attribute named_widgets(dict): map from name to widget
|
|
"""
|
|
self._introspect() # FIXME: why doing that on each XMLUI ? should be done once
|
|
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
|
|
self.named_widgets = {}
|
|
|
|
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
|
|
if not top_element.hasAttribute("submit"):
|
|
# getAttribute never return None (it return empty string it attribute doesn't exists)
|
|
# so we have to manage None here
|
|
return None
|
|
value = top_element.getAttribute("submit")
|
|
return value
|
|
|
|
@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
|
|
else: # 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(unicode): body of the note
|
|
@param title(unicode): title of the note
|
|
@param level(unicode): one of C.XMLUI_DATA_LVL_*
|
|
@return(XMLUI): instance of XMLUI
|
|
"""
|
|
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
|
|
|
|
|
|
def quickNote(host, client, message, title='', level=C.XMLUI_DATA_LVL_INFO):
|
|
"""more sugar to do the whole note process"""
|
|
note_ui = note(message, title, level)
|
|
host.actionNew({'xmlui': note_ui.toXml()}, profile=client.profile)
|
|
|
|
|
|
def deferredUI(host, xmlui, chained=False):
|
|
"""create a deferred linked to XMLUI
|
|
|
|
@param xmlui(XMLUI): instance of the XMLUI
|
|
Must be an XMLUI that you can submit, with submit_id set to ''
|
|
@param chained(bool): True if the Deferred result must be returned to the frontend
|
|
useful when backend is in a series of dialogs with an ui
|
|
@return (D(data)): a deferred which fire the data
|
|
"""
|
|
assert xmlui.submit_id == ''
|
|
xmlui_d = defer.Deferred()
|
|
|
|
def onSubmit(data, profile):
|
|
xmlui_d.callback(data)
|
|
return xmlui_d if chained else {}
|
|
|
|
xmlui.submit_id = host.registerCallback(onSubmit, with_data=True, one_shot=True)
|
|
return xmlui_d
|
|
|
|
def deferXMLUI(host, xmlui, action_extra=None, security_limit=C.NO_SECURITY_LIMIT, chained=False, profile=C.PROF_KEY_NONE):
|
|
"""Create a deferred linked to XMLUI
|
|
|
|
@param xmlui(XMLUI): instance of the XMLUI
|
|
Must be an XMLUI that you can submit, with submit_id set to ''
|
|
@param profile: %(doc_profile)s
|
|
@param action_extra(None, dict): extra action to merge with xmlui
|
|
mainly used to add meta informations (see actionNew doc)
|
|
@param security_limit: %(doc_security_limit)s
|
|
@param chained(bool): True if the Deferred result must be returned to the frontend
|
|
useful when backend is in a series of dialogs with an ui
|
|
@return (data): a deferred which fire the data
|
|
"""
|
|
xmlui_d = deferredUI(host, xmlui, chained)
|
|
action_data = {'xmlui': xmlui.toXml()}
|
|
if action_extra is not None:
|
|
action_data.update(action_extra)
|
|
host.actionNew(action_data, security_limit=security_limit, keep_id=xmlui.submit_id, profile=profile)
|
|
return xmlui_d
|
|
|
|
def deferDialog(host, message, title=u'Please confirm', type_=C.XMLUI_DIALOG_CONFIRM, options=None,
|
|
action_extra=None, security_limit=C.NO_SECURITY_LIMIT, chained=False, profile=C.PROF_KEY_NONE):
|
|
"""Create a submitable dialog and manage it with a deferred
|
|
|
|
@param message(unicode): message to display
|
|
@param title(unicode): title of the dialog
|
|
@param type(unicode): dialog type (C.XMLUI_DIALOG_*)
|
|
@param options(None, dict): if not None, will be used to update (extend) dialog_opt arguments of XMLUI
|
|
@param action_extra(None, dict): extra action to merge with xmlui
|
|
mainly used to add meta informations (see actionNew doc)
|
|
@param security_limit: %(doc_security_limit)s
|
|
@param chained(bool): True if the Deferred result must be returned to the frontend
|
|
useful when backend is in a series of dialogs with an ui
|
|
@param profile: %(doc_profile)s
|
|
@return (dict): Deferred dict
|
|
"""
|
|
assert profile is not None
|
|
dialog_opt = {'type': type_, 'message': message}
|
|
if options is not None:
|
|
dialog_opt.update(options)
|
|
dialog = XMLUI(C.XMLUI_DIALOG, title=title, dialog_opt=dialog_opt, submit_id='')
|
|
return deferXMLUI(host, dialog, action_extra, security_limit, chained, profile)
|
|
|
|
def deferConfirm(*args, **kwargs):
|
|
"""call deferDialog and return a boolean instead of the whole data dict"""
|
|
d = deferDialog(*args, **kwargs)
|
|
d.addCallback(lambda data: C.bool(data['answer']))
|
|
return d
|
|
|
|
# Misc other funtions
|
|
|
|
class ElementParser(object):
|
|
"""callable class to parse XML string into Element"""
|
|
# XXX: Found at http://stackoverflow.com/questions/2093400/how-to-create-twisted-words-xish-domish-element-entirely-from-raw-xml/2095942#2095942
|
|
|
|
def _escapeHTML(self, matchobj):
|
|
entity = matchobj.group(1)
|
|
if entity in XML_ENTITIES:
|
|
# we don't escape XML entities
|
|
return matchobj.group(0)
|
|
else:
|
|
try:
|
|
return unichr(htmlentitydefs.name2codepoint[entity])
|
|
except KeyError:
|
|
log.warning(u"removing unknown entity {}".format(entity))
|
|
return u''
|
|
|
|
def __call__(self, raw_xml, force_spaces=False, namespace=None):
|
|
"""
|
|
@param raw_xml(unicode): the raw XML
|
|
@param force_spaces (bool): if True, replace occurrences of '\n' and '\t' with ' '.
|
|
@param namespace(unicode, None): if set, use this namespace for the wrapping element
|
|
"""
|
|
# we need to wrap element in case
|
|
# there is not a unique one on the top
|
|
if namespace is not None:
|
|
raw_xml = u"<div xmlns='{}'>{}</div>".format(namespace, raw_xml)
|
|
else:
|
|
raw_xml = u"<div>{}</div>".format(raw_xml)
|
|
|
|
# avoid ParserError on HTML escaped chars
|
|
raw_xml = html_entity_re.sub(self._escapeHTML, raw_xml)
|
|
|
|
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'))
|
|
top_elt = self.result.firstChildElement()
|
|
# we now can check if there was a unique element on the top
|
|
# and remove our wrapping <div/> is this was the case
|
|
if len(top_elt.children) == 1 and domish.IElement.providedBy(top_elt.children[0]):
|
|
top_elt = top_elt.firstChildElement()
|
|
return top_elt
|
|
|
|
|
|
# FIXME: this method is duplicated from frontends.tools.xmlui.getText
|
|
def getText(node):
|
|
"""Get child text nodes of a domish.Element.
|
|
|
|
@param node (domish.Element)
|
|
@return: joined unicode text of all nodes
|
|
"""
|
|
data = []
|
|
for child in node.childNodes:
|
|
if child.nodeType == child.TEXT_NODE:
|
|
data.append(child.wholeText)
|
|
return u"".join(data)
|
|
|
|
def findAll(elt, namespaces=None, names=None, ):
|
|
"""Find child element at any depth matching criteria
|
|
|
|
@param elt(domish.Element): top parent of the elements to find
|
|
@param names(iterable[unicode], basestring, None): names to match
|
|
None to accept every names
|
|
@param namespace(iterable[unicode], basestring, None): URIs to match
|
|
None to accept every namespaces
|
|
@return ((G)domish.Element): found elements
|
|
"""
|
|
if isinstance(namespaces, basestring):
|
|
namespaces=tuple((namespaces,))
|
|
if isinstance(names, basestring):
|
|
names=tuple((names,))
|
|
|
|
for child in elt.elements():
|
|
if (domish.IElement.providedBy(child) and
|
|
(not names or child.name in names) and
|
|
(not namespaces or child.uri in namespaces)):
|
|
yield child
|
|
for found in findAll(child, namespaces, names):
|
|
yield found
|