Mercurial > libervia-backend
view frontends/src/tools/xmlui.py @ 1005:b4af31a8a4f2
core (logs): added formatting, name filter and outputs management:
- formatting is inspired from, and use when possible, standard logging. "message", "levelname", and "name" are the only format managed, depending on backend more can be managed (standard backend formats are specified in official python logging doc)
- name filter use regular expressions. It's possible to log only plugins with SAT_LOG_LOGGER="^sat.plugins". To log only XEPs 96 & 65, we can use SAT_LOG_LOGGER='(xep_0095|xep_0065)'
- output management use a particular syntax:
- output handler are name with "//", so far there are "//default" (most of time stderr), "//memory" and "//file"
- options can be specified in parenthesis, e.g. "//memory(50)" mean a 50 lines memory buffer (50 is the current default, so that's equivalent to "//memory")
- several handlers can be specified: "//file(/tmp/sat.log)//default" will use the default logging + a the /tmp/sat.log file
- if there is only one handler, it use the file handler: "/tmp/sat.log" is the same as "//file(/tmp/sat.log)"
- not finished, need more work for twisted and basic backends
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 05 May 2014 18:58:34 +0200 |
parents | d2e612a45e76 |
children | 7a39ae3950f7 |
line wrap: on
line source
#!/usr/bin/python # -*- coding: utf-8 -*- # SàT frontend tools # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 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_frontends.constants import Const from sat.core.exceptions import DataError class InvalidXMLUI(Exception): pass def getText(node): """Get child text nodes @param node: dom Node @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) class Widget(object): """ base Widget """ pass class EmptyWidget(Widget): """ Just a placeholder widget """ pass class TextWidget(Widget): """ Non interactive text """ pass class LabelWidget(Widget): """ Non interactive text """ pass class JidWidget(Widget): """ Jabber ID """ pass class DividerWidget(Widget): """ Separator """ pass class StringWidget(Widget): """ Input widget with require a string often called Edit in toolkits """ class PasswordWidget(Widget): """ Input widget with require a masked string """ class TextBoxWidget(Widget): """ Input widget with require a long, possibly multilines string often called TextArea in toolkits """ class BoolWidget(Widget): """ Input widget with require a boolean value often called CheckBox in toolkits """ class ButtonWidget(Widget): """ A clickable widget """ class ListWidget(Widget): """ A widget able to show/choose one or several strings in a list """ class Container(Widget): """ Widget which can contain other ones with a specific layout """ @classmethod def _xmluiAdapt(cls, instance): """ Make cls as instance.__class__ cls must inherit from original instance class Usefull when you get a class from UI toolkit """ assert instance.__class__ in cls.__bases__ instance.__class__ = type(cls.__name__, cls.__bases__, dict(cls.__dict__)) class PairsContainer(Container): """ Widgets are disposed in rows of two (usually label/input) """ pass class TabsContainer(Container): """ A container which several other containers in tabs Often called Notebook in toolkits """ class VerticalContainer(Container): """ Widgets are disposed vertically """ pass class AdvancedListContainer(Container): """ Widgets are disposed in rows with advaned features """ pass class XMLUI(object): """ Base class to construct SàT XML User Interface New frontends can inherite this class to easily implement XMLUI @property widget_factory: factory to create frontend-specific widgets @proporety dialog_factory: factory to create frontend-specific dialogs """ widget_factory = None dialog_factory = None # TODO def __init__(self, host, xml_data, title = None, flags = None, dom_parse=None, dom_free=None): """ Initialise the XMLUI instance @param host: %(doc_host)s @param xml_data: the raw XML containing the UI @param title: force the title, or use XMLUI one if None @param flags: list of string which can be: - NO_CANCEL: the UI can't be cancelled @param dom_parse: methode equivalent to minidom.parseString (but which must manage unicode), or None to use default one @param dom_free: method used to free the parsed DOM """ if dom_parse is None: from xml.dom import minidom self.dom_parse = lambda xml_data: minidom.parseString(xml_data.encode('utf-8')) self.dom_free = lambda cat_dom: cat_dom.unlink() else: self.dom_parse = dom_parse self.dom_free = dom_free or (lambda cat_dom: None) self.host = host self.title = title or "" if flags is None: flags = [] self.flags = flags self.ctrl_list = {} # usefull to access ctrl self._main_cont = None self.constructUI(xml_data) def escape(self, name): """ return escaped name for forms """ return u"%s%s" % (Const.SAT_FORM_PREFIX, name) @property def main_cont(self): return self._main_cont @main_cont.setter def main_cont(self, value): if self._main_cont is not None: raise ValueError(_("XMLUI can have only one main container")) self._main_cont = value def _parseChilds(self, parent, current_node, wanted = ('container',), data = None): """ Recursively parse childNodes of an elemen @param parent: widget container with '_xmluiAppend' method @param current_node: element from which childs will be parsed @param wanted: list of tag names that can be present in the childs to be SàT XMLUI compliant @param data: additionnal data which are needed in some cases """ for node in current_node.childNodes: if wanted and not node.nodeName in wanted: raise InvalidXMLUI('Unexpected node: [%s]' % node.nodeName) if node.nodeName == "container": type_ = node.getAttribute('type') if parent is self and type_ != 'vertical': # main container is not a VerticalContainer and we want one, so we create one to wrap it parent = self.widget_factory.createVerticalContainer(self) self.main_cont = parent if type_ == "tabs": cont = self.widget_factory.createTabsContainer(parent) self._parseChilds(parent, node, ('tab',), cont) elif type_ == "vertical": cont = self.widget_factory.createVerticalContainer(parent) self._parseChilds(cont, node, ('widget', 'container')) elif type_ == "pairs": cont = self.widget_factory.createPairsContainer(parent) self._parseChilds(cont, node, ('widget', 'container')) elif type_ == "advanced_list": try: columns = int(node.getAttribute('columns')) except (TypeError, ValueError): raise DataError("Invalid columns") selectable = node.getAttribute('selectable') or 'no' auto_index = node.getAttribute('auto_index') == 'true' data = {'index': 0} if auto_index else None cont = self.widget_factory.createAdvancedListContainer(parent, columns, selectable) callback_id = node.getAttribute("callback") or None if callback_id is not None: if selectable == 'no': raise ValueError("can't have selectable=='no' and callback_id at the same time") cont._xmlui_callback_id = callback_id cont._xmluiOnSelect(self.onAdvListSelect) self._parseChilds(cont, node, ('row',), data) else: print(_("Unknown container [%s], using default one") % type_) cont = self.widget_factory.createVerticalContainer(parent) self._parseChilds(cont, node, ('widget', 'container')) try: parent._xmluiAppend(cont) except (AttributeError, TypeError): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError if parent is self: self.main_cont = cont else: raise Exception(_("Internal Error, container has not _xmluiAppend method")) elif node.nodeName == 'tab': name = node.getAttribute('name') label = node.getAttribute('label') if not name or not isinstance(data, TabsContainer): raise InvalidXMLUI if self.type == 'param': self._current_category = name #XXX: awful hack because params need category and we don't keep parent tab_cont = data new_tab = tab_cont._xmluiAddTab(label or name) self._parseChilds(new_tab, node, ('widget', 'container')) elif node.nodeName == 'row': try: index = str(data['index']) data['index'] += 1 except TypeError: index = node.getAttribute('index') or None parent._xmluiAddRow(index) self._parseChilds(parent, node, ('widget', 'container')) elif node.nodeName == "widget": id_ = node.getAttribute("id") name = node.getAttribute("name") type_ = node.getAttribute("type") value = node.getAttribute("value") if node.hasAttribute('value') else u'' if type_=="empty": ctrl = self.widget_factory.createEmptyWidget(parent) elif type_=="text": try: value = node.childNodes[0].wholeText except IndexError: # warning (_("text node has no child !")) pass # FIXME proper warning (this one is not OK with Libervia) ctrl = self.widget_factory.createTextWidget(parent, value) elif type_=="label": ctrl = self.widget_factory.createLabelWidget(parent, value) elif type_=="jid": ctrl = self.widget_factory.createJidWidget(parent, value) elif type_=="divider": style = node.getAttribute("style") or 'line' ctrl = self.widget_factory.createDividerWidget(parent, style) elif type_=="string": ctrl = self.widget_factory.createStringWidget(parent, value) self.ctrl_list[name] = ({'type':type_, 'control':ctrl}) elif type_=="password": ctrl = self.widget_factory.createPasswordWidget(parent, value) self.ctrl_list[name] = ({'type':type_, 'control':ctrl}) elif type_=="textbox": ctrl = self.widget_factory.createTextBoxWidget(parent, value) self.ctrl_list[name] = ({'type':type_, 'control':ctrl}) elif type_=="bool": ctrl = self.widget_factory.createBoolWidget(parent, value=='true') self.ctrl_list[name] = ({'type':type_, 'control':ctrl}) elif type_ == "list": style = [] if node.getAttribute("multi") == 'yes' else ['single'] _options = [(option.getAttribute("value"), option.getAttribute("label")) for option in node.getElementsByTagName("option")] _selected = [option.getAttribute("value") for option in node.getElementsByTagName("option") if option.getAttribute('selected') == 'true'] ctrl = self.widget_factory.createListWidget(parent, _options, _selected, style) self.ctrl_list[name] = ({'type': type_, 'control': ctrl}) elif type_=="button": callback_id = node.getAttribute("callback") ctrl = self.widget_factory.createButtonWidget(parent, value, self.onButtonPress) ctrl._xmlui_param_id = (callback_id, [field.getAttribute('name') for field in node.getElementsByTagName("field_back")]) else: print(_("FIXME FIXME FIXME: widget type [%s] is not implemented") % type_) raise NotImplementedError(_("FIXME FIXME FIXME: type [%s] is not implemented") % type_) if self.type == 'param' and type_ not in ('text', 'button'): try: ctrl._xmluiOnChange(self.onParamChange) ctrl._param_category = self._current_category except (AttributeError, TypeError): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError if not isinstance(ctrl, (EmptyWidget, TextWidget, LabelWidget, JidWidget)): print(_("No change listener on [%s]") % ctrl) if type_ != 'text': callback = node.getAttribute("internal_callback") or None if callback: fields = [field.getAttribute('name') for field in node.getElementsByTagName("internal_field")] data = self.getInternalCallbackData(callback, node) ctrl._xmlui_param_internal = (callback, fields, data) if type_ == 'button': ctrl._xmluiOnClick(self.onChangeInternal) else: ctrl._xmluiOnChange(self.onChangeInternal) ctrl._xmlui_name = name parent._xmluiAppend(ctrl) else: raise NotImplementedError(_('Unknown tag [%s]') % node.nodeName) def constructUI(self, xml_data, post_treat=None): """ Actually construct the UI @param xml_data: raw XMLUI @param post_treat: frontend specific treatments to do once the UI is constructed @return: constructed widget """ cat_dom = self.dom_parse(xml_data) top=cat_dom.documentElement self.type = top.getAttribute("type") self.title = self.title or top.getAttribute("title") or u"" self.session_id = top.getAttribute("session_id") or None self.submit_id = top.getAttribute("submit") or None if top.nodeName != "sat_xmlui" or not self.type in ['form', 'param', 'window', 'popup']: raise InvalidXMLUI if self.type == 'param': self.param_changed = set() self._parseChilds(self, cat_dom.documentElement) if post_treat is not None: post_treat() self.dom_free(cat_dom) def _xmluiClose(self): """ Close the window/popup/... where the constructeur XMLUI is this method must be overrided """ raise NotImplementedError def _xmluiLaunchAction(self, action_id, data): self.host.launchAction(action_id, data, profile_key = self.host.profile) def _xmluiSetParam(self, name, value, category): self.host.bridge.setParam(name, value, category, profile_key=self.host.profile) ##EVENTS## def onParamChange(self, ctrl): """ Called when type is param and a widget to save is modified @param ctrl: widget modified """ assert(self.type == "param") self.param_changed.add(ctrl) def onAdvListSelect(self, ctrl): data = {} widgets = ctrl._xmluiGetSelectedWidgets() for wid in widgets: try: name = self.escape(wid._xmlui_name) value = wid._xmluiGetValue() data[name] = value except (AttributeError, TypeError): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError pass idx = ctrl._xmluiGetSelectedIndex() if idx is not None: data['index'] = idx callback_id = ctrl._xmlui_callback_id if callback_id is None: print(_("No callback_id found")) return self._xmluiLaunchAction(callback_id, data) def onButtonPress(self, button): """ Called when an XMLUI button is clicked Launch the action associated to the button @param button: the button clicked """ callback_id, fields = button._xmlui_param_id if not callback_id: # the button is probably bound to an internal action return data = {} for field in fields: escaped = self.escape(field) ctrl = self.ctrl_list[field] if isinstance(ctrl['control'], ListWidget): data[escaped] = u'\t'.join(ctrl['control']._xmluiGetSelectedValues()) else: data[escaped] = ctrl['control']._xmluiGetValue() self._xmluiLaunchAction(callback_id, data) def onChangeInternal(self, ctrl): """ Called when a widget that has been bound to an internal callback is changed. This is used to perform some UI actions without communicating with the backend. See sat.tools.xml_tools.Widget.setInternalCallback for more details. @param ctrl: widget modified """ action, fields, data = ctrl._xmlui_param_internal if action not in ('copy', 'move', 'groups_of_contact'): raise NotImplementedError(_("FIXME: XMLUI internal action [%s] is not implemented") % action) def copy_move(source, target): """Depending of 'action' value, copy or move from source to target.""" if isinstance(target, ListWidget): if isinstance(source, ListWidget): values = source._xmluiGetSelectedValues() else: values = [source._xmluiGetValue()] if action == 'move': source._xmluiSetValue('') values = [value for value in values if value] if values: target._xmluiAddValues(values, select=True) else: if isinstance(source, ListWidget): value = u', '.join(source._xmluiGetSelectedValues()) else: value = source._xmluiGetValue() if action == 'move': source._xmluiSetValue('') target._xmluiSetValue(value) def groups_of_contact(source, target): """Select in target the groups of the contact which is selected in source.""" assert(isinstance(source, ListWidget)) assert(isinstance(target, ListWidget)) try: contact_jid_s = source._xmluiGetSelectedValues()[0] except IndexError: return target._xmluiSelectValues(data[contact_jid_s]) pass source = None for field in fields: widget = self.ctrl_list[field]['control'] if not source: source = widget continue if action in ('copy', 'move'): copy_move(source, widget) elif action == 'groups_of_contact': groups_of_contact(source, widget) source = None def getInternalCallbackData(self, action, node): """Retrieve from node the data needed to perform given action. TODO: it would be better to not have a specific way to retrieve data for each action, but instead to have a generic method to extract any kind of data structure from the 'internal_data' element. @param action (string): a value from the one that can be passed to the 'callback' parameter of sat.tools.xml_tools.Widget.setInternalCallback @param node (DOM Element): the node of the widget that triggers the callback """ try: # data is stored in the first 'internal_data' element of the node data_elts = node.getElementsByTagName('internal_data')[0].childNodes except IndexError: return None data = {} if action == 'groups_of_contact': # return a dict(key: string, value: list[string]) for elt in data_elts: jid_s = elt.getAttribute('name') data[jid_s] = [] for value_elt in elt.childNodes: data[jid_s].append(value_elt.getAttribute('name')) return data def onFormSubmitted(self, ignore=None): """ An XMLUI form has been submited call the submit action associated with this form """ selected_values = [] for ctrl_name in self.ctrl_list: escaped = self.escape(ctrl_name) ctrl = self.ctrl_list[ctrl_name] if isinstance(ctrl['control'], ListWidget): selected_values.append((escaped, u'\t'.join(ctrl['control']._xmluiGetSelectedValues()))) else: selected_values.append((escaped, ctrl['control']._xmluiGetValue())) if self.submit_id is not None: data = dict(selected_values) if self.session_id is not None: data["session_id"] = self.session_id self._xmluiLaunchAction(self.submit_id, data) else: print(_("The form data is not sent back, the type is not managed properly")) self._xmluiClose() def onFormCancelled(self, ignore=None): """ Called when a form is cancelled """ print(_("Cancelling form")) self._xmluiClose() def onSaveParams(self, ignore=None): """ Params are saved, we send them to backend self.type must be param """ assert(self.type == 'param') for ctrl in self.param_changed: if isinstance(ctrl, ListWidget): value = u'\t'.join(ctrl._xmluiGetSelectedValues()) else: value = ctrl._xmluiGetValue() param_name = ctrl._xmlui_name.split(Const.SAT_PARAM_SEPARATOR)[1] self._xmluiSetParam(param_name, value, ctrl._param_category) self._xmluiClose()