changeset 759:93bd868b8fb6

backend, frontends: callbacks refactoring: - launchAction is now async, and return a dictionary for its result - no more action_id, actionResult* are deprecated - callback system is about to be unified
author Goffi <goffi@goffi.org>
date Tue, 24 Dec 2013 15:19:08 +0100 (2013-12-24)
parents 86224a13cc1d
children 73a0077f80cc
files frontends/src/bridge/DBus.py frontends/src/primitivus/primitivus frontends/src/primitivus/xmlui.py frontends/src/wix/main_window.py frontends/src/wix/param.py frontends/src/wix/xmlui.py src/bridge/DBus.py src/bridge/bridge_constructor/bridge_template.ini src/core/sat_main.py src/memory/memory.py src/plugins/plugin_misc_quiz.py src/tools/xml_tools.py
diffstat 12 files changed, 151 insertions(+), 78 deletions(-) [+]
line wrap: on
line diff
--- a/frontends/src/bridge/DBus.py	Tue Dec 24 15:19:08 2013 +0100
+++ b/frontends/src/bridge/DBus.py	Tue Dec 24 15:19:08 2013 +0100
@@ -190,8 +190,8 @@
     def isConnected(self, profile_key="@DEFAULT@"):
         return self.db_core_iface.isConnected(profile_key)
 
-    def launchAction(self, action_type, data, profile_key="@DEFAULT@"):
-        return unicode(self.db_core_iface.launchAction(action_type, data, profile_key))
+    def launchAction(self, callback_id, data, profile_key="@DEFAULT@", callback=None, errback=None):
+        return self.db_core_iface.launchAction(callback_id, data, profile_key, reply_handler=callback, error_handler=lambda err:errback(err._dbus_error_name[len(const_ERROR_PREFIX)+1:]))
 
     def registerNewAccount(self, login, password, email, host, port=5222):
         return unicode(self.db_core_iface.registerNewAccount(login, password, email, host, port))
--- a/frontends/src/primitivus/primitivus	Tue Dec 24 15:19:08 2013 +0100
+++ b/frontends/src/primitivus/primitivus	Tue Dec 24 15:19:08 2013 +0100
@@ -282,7 +282,6 @@
             return input
 
     def _dynamicMenuCb(self, xmlui):
-        misc = {}
         ui = XMLUI(self, xml_data = xmlui)
         ui.show('popup')
 
@@ -448,6 +447,29 @@
                 #No notification left, we can hide the bar
                 self.main_widget.footer = self.editBar
 
+    def launchAction(self, callback_id, data=None, profile_key="@NONE@"):
+        """ Launch a dynamic action
+        @param callback_id: id of the action to launch
+        @param data: data needed only for certain actions
+        @param profile_key: %(doc_profile_key)s
+
+        """
+        if data is None:
+            data = dict()
+        def action_cb(data):
+            if not data:
+                # action was a one shot, nothing to do
+                pass
+            elif "xmlui" in data:
+                ui = XMLUI(self, xml_data = data['xmlui'])
+                ui.show('popup')
+            else:
+                self.showPopUp(sat_widgets.Alert(_("Error"), _(u"Unmanaged action result"), ok_cb=self.removePopUp))
+        def action_eb(failure):
+            self.showPopUp(sat_widgets.Alert(_("Error"), unicode(failure), ok_cb=self.removePopUp))
+
+        self.bridge.launchAction(callback_id, data, profile_key, callback=action_cb, errback=action_eb)
+
     def askConfirmation(self, confirmation_id, confirmation_type, data, profile):
         if not self.check_profile(profile):
             return
--- a/frontends/src/primitivus/xmlui.py	Tue Dec 24 15:19:08 2013 +0100
+++ b/frontends/src/primitivus/xmlui.py	Tue Dec 24 15:19:08 2013 +0100
@@ -109,7 +109,7 @@
                 ctrl.selectValue(elem.getAttribute("value"))
                 self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
             elif type_=="button":
-                callback_id = elem.getAttribute("callback_id")
+                callback_id = elem.getAttribute("callback")
                 ctrl = sat_widgets.CustomButton(value, on_press=self.onButtonPress)
                 ctrl.param_id = (callback_id,[field.getAttribute('name') for field in elem.getElementsByTagName("field_back")])
             elif type_=="advanced_list":
@@ -178,6 +178,7 @@
         top=cat_dom.documentElement
         self.type = top.getAttribute("type")
         self.title = top.getAttribute("title") or self.title
+        self.submit_id = top.getAttribute("submit") or None
         if top.nodeName != "sat_xmlui" or not self.type in ['form', 'param', 'window']:
             raise InvalidXMLUI
 
@@ -234,7 +235,7 @@
 
     def onButtonPress(self, button):
         callback_id, fields = button.param_id
-        data = {"callback_id":callback_id}
+        data = {}
         for field in fields:
             ctrl = self.ctrl_list[field]
             if isinstance(ctrl['control'],sat_widgets.List):
@@ -242,8 +243,7 @@
             else:
                 data[field] = ctrl['control'].getValue()
 
-        id = self.host.bridge.launchAction("button", data, profile_key = self.host.profile)
-        self.host.current_action_ids.add(id)
+        self.host.launchAction(callback_id, data, profile_key = self.host.profile)
 
     def onParamChange(self, widget, extra=None):
         """Called when type is param and a widget to save is modified"""
@@ -262,12 +262,13 @@
                 data.append((ctrl_name, ctrl['control'].get_edit_text()))
         if self.misc.has_key('action_back'): #FIXME FIXME FIXME: WTF ! Must be cleaned
             raise NotImplementedError
-            self.host.debug()
-        elif 'callback' in self.misc:
+        elif 'callback' in self.misc: # FIXME: this part is not needed anymore
             try:
-                self.misc['callback'](data, *self.misc['callback_args'])
+                self.misc['callback'](data, submit_id=self.submit_id, *self.misc['callback_args'])
             except KeyError:
-                self.misc['callback'](data)
+                self.misc['callback'](data, submit_id=self.submit_id)
+        elif self.submit_id is not None:
+            self.host.launchAction(self.submit_id, dict(data), profile_key=self.host.profile)
 
         else:
             warning (_("The form data is not sent back, the type is not managed properly"))
--- a/frontends/src/wix/main_window.py	Tue Dec 24 15:19:08 2013 +0100
+++ b/frontends/src/wix/main_window.py	Tue Dec 24 15:19:08 2013 +0100
@@ -238,6 +238,39 @@
             self.tools.Disable()
         return
 
+    def launchAction(self, callback_id, data=None, profile_key="@NONE@"):
+        """ Launch a dynamic action
+        @param callback_id: id of the action to launch
+        @param data: data needed only for certain actions
+        @param profile_key: %(doc_profile_key)s
+
+        """
+        if data is None:
+            data = dict()
+        def action_cb(data):
+            if not data:
+                # action was a one shot, nothing to do
+                pass
+            elif "xmlui" in data:
+                debug (_("XML user interface received"))
+                XMLUI(self, xml_data = data['xmlui'])
+            else:
+                dlg = wx.MessageDialog(self, _(u"Unmanaged action result"),
+                                       _('Error'),
+                                       wx.OK | wx.ICON_ERROR
+                                      )
+                dlg.ShowModal()
+                dlg.Destroy()
+        def action_eb(failure):
+            dlg = wx.MessageDialog(self, unicode(failure),
+                                   _('Error'),
+                                   wx.OK | wx.ICON_ERROR
+                                  )
+            dlg.ShowModal()
+            dlg.Destroy()
+
+        self.bridge.launchAction(callback_id, data, profile_key, callback=action_cb, errback=action_eb)
+
     def askConfirmation(self, confirmation_id, confirmation_type, data, profile):
         #TODO: refactor this in QuickApp
         if not self.check_profile(profile):
--- a/frontends/src/wix/param.py	Tue Dec 24 15:19:08 2013 +0100
+++ b/frontends/src/wix/param.py	Tue Dec 24 15:19:08 2013 +0100
@@ -129,9 +129,7 @@
         self.__save_parameters()
         name, category = event.GetEventObject().param_id
         callback_id = event.GetEventObject().callback_id
-        data = {"name":name, "category":category, "callback_id":callback_id}
-        id = self.host.bridge.launchAction("button", data, profile_key = self.host.profile)
-        self.host.current_action_ids.add(id)
+        self.host.launchAction(callback_id, None, profile_key = self.host.profile)
         event.Skip()
 
     def __save_parameters(self):
--- a/frontends/src/wix/xmlui.py	Tue Dec 24 15:19:08 2013 +0100
+++ b/frontends/src/wix/xmlui.py	Tue Dec 24 15:19:08 2013 +0100
@@ -30,11 +30,13 @@
     """Create an user interface from a SàT xml"""
 
     def __init__(self, host, xml_data='', title="Form", options = None, misc = None):
-        style = wx.DEFAULT_FRAME_STYLE & ~wx.CLOSE_BOX if 'NO_CANCEL' in options else wx.DEFAULT_FRAME_STYLE #FIXME: gof: Q&D tmp hack
+        if options is None:
+            options = []
+        style = wx.DEFAULT_FRAME_STYLE & ~wx.CLOSE_BOX if 'NO_CANCEL' in options else wx.DEFAULT_FRAME_STYLE #FIXME: Q&D tmp hack
         super(XMLUI, self).__init__(None, title=title, style=style)
 
         self.host = host
-        self.options = options or []
+        self.options = options
         self.misc = misc or {}
         self.ctrl_list = {}  # usefull to access ctrl
 
@@ -165,6 +167,7 @@
         top= cat_dom.documentElement
         self.type = top.getAttribute("type")
         self.title = top .getAttribute("title")
+        self.submit_id = top.getAttribute("submit") or None
         if self.title:
             self.SetTitle(self.title)
         if top.nodeName != "sat_xmlui" or not self.type in ['form', 'param', 'window']:
@@ -195,7 +198,6 @@
     def onButtonClicked(self, event):
         """Called when a button is pushed"""
         callback_id, fields = event.GetEventObject().param_id
-        data = {"callback_id":callback_id}
         for field in fields:
             ctrl = self.ctrl_list[field]
             if isinstance(ctrl['control'], wx.ListBox):
@@ -203,8 +205,7 @@
             else:
                 data[field] = ctrl['control'].GetValue()
 
-        id = self.host.bridge.launchAction("button", data, profile_key = self.host.profile)
-        self.host.current_action_ids.add(id)
+        self.host.launchAction(callback_id, None, profile_key = self.host.profile)
         event.Skip()
 
     def onFormSubmitted(self, event):
@@ -224,6 +225,10 @@
             self.host.current_action_ids.add(id)
         elif self.misc.has_key('callback'):
             self.misc['callback'](data)
+
+        elif self.submit_id is not None:
+            data = dict(selected_values)
+            self.host.launchAction(self.submit_id, data, profile_key=self.host.profile)
         else:
             warning (_("The form data is not sent back, the type is not managed properly"))
         self.MakeModal(False)
--- a/src/bridge/DBus.py	Tue Dec 24 15:19:08 2013 +0100
+++ b/src/bridge/DBus.py	Tue Dec 24 15:19:08 2013 +0100
@@ -378,10 +378,10 @@
         return self._callback("isConnected", unicode(profile_key))
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
-                         in_signature='sa{ss}s', out_signature='s',
-                         async_callbacks=None)
-    def launchAction(self, action_type, data, profile_key="@DEFAULT@"):
-        return self._callback("launchAction", unicode(action_type), data, unicode(profile_key))
+                         in_signature='sa{ss}s', out_signature='a{ss}',
+                         async_callbacks=('callback', 'errback'))
+    def launchAction(self, callback_id, data, profile_key="@DEFAULT@", callback=None, errback=None):
+        return self._callback("launchAction", unicode(callback_id), data, unicode(profile_key), callback=callback, errback=errback)
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='ssssi', out_signature='s',
--- a/src/bridge/bridge_constructor/bridge_template.ini	Tue Dec 24 15:19:08 2013 +0100
+++ b/src/bridge/bridge_constructor/bridge_template.ini	Tue Dec 24 15:19:08 2013 +0100
@@ -104,6 +104,7 @@
 doc_param_1=%(doc_profile)s
 
 [askConfirmation]
+deprecated=
 type=signal
 category=core
 sig_in=ssa{ss}s
@@ -116,6 +117,7 @@
 doc_param_3=%(doc_profile)s
 
 [actionResult]
+deprecated=
 type=signal
 category=core
 sig_in=ssa{ss}s
@@ -130,6 +132,7 @@
 doc_param_3=%(doc_profile)s
 
 [actionResultExt]
+deprecated=
 type=signal
 category=core
 sig_in=ssa{sa{ss}}s
@@ -533,18 +536,21 @@
 doc_param_1=%(doc_profile_key)s
 
 [launchAction]
+async=
 type=method
 category=core
 sig_in=sa{ss}s
-sig_out=s
+sig_out=a{ss}
 param_2_default="@DEFAULT@"
-doc=Launch a specific action
-doc_param_0=action_type: type of the action which can be:
- - button: A button is pushed
-doc_param_1=data: action_type dependant data
+doc=Launch a registred action
+doc_param_0=callback_id: id of the registred callback
+doc_param_1=data: optional data
 doc_param_2=%(doc_profile_key)s
+doc_return=dict where key can be:
+    - xmlui: a XMLUI need to be displayed
 
 [confirmationAnswer]
+deprecated=
 type=method
 category=core
 sig_in=sba{ss}s
--- a/src/core/sat_main.py	Tue Dec 24 15:19:08 2013 +0100
+++ b/src/core/sat_main.py	Tue Dec 24 15:19:08 2013 +0100
@@ -42,6 +42,7 @@
 from sat.tools.xml_tools import tupleList2dataForm
 from sat.tools.misc import TriggerManager
 from glob import glob
+from uuid import uuid4
 
 try:
     from twisted.words.protocols.xmlstream import XMPPHandler
@@ -103,10 +104,8 @@
         CONST[name] = value
 
     def __init__(self):
-        #TODO: standardize callback system
-
-        self.__general_cb_map = {}  # callback called for general reasons (key = name)
-        self.__private_data = {}  # used for internal callbacks (key = id)
+        self._cb_map = {}  # map from callback_id to callbacks
+        self.__private_data = {}  # used for internal callbacks (key = id) FIXME: to be removed
         self.profiles = {}
         self.plugins = {}
         self.menus = {}  # dynamic menus. key: (type, category, name), value: menu data (dictionnary)
@@ -157,7 +156,7 @@
         self.bridge.register("updateContact", self.updateContact)
         self.bridge.register("delContact", self.delContact)
         self.bridge.register("isConnected", self.isConnected)
-        self.bridge.register("launchAction", self.launchAction)
+        self.bridge.register("launchAction", self.launchCallback)
         self.bridge.register("confirmationAnswer", self.confirmationAnswer)
         self.bridge.register("getProgress", self.getProgress)
         self.bridge.register("getMenus", self.getMenus)
@@ -388,7 +387,8 @@
 
         return next_id
 
-    def registerNewAccountCB(self, id, data, profile):
+    def registerNewAccountCB(self, data, profile):
+        # FIXME: to be removed/redone elsewhere
         user = jid.parse(self.memory.getParamA("JabberID", "Connection", profile_key=profile))[0]
         password = self.memory.getParamA("Password", "Connection", profile_key=profile)
         server = self.memory.getParamA("Server", "Connection", profile_key=profile)
@@ -407,8 +407,6 @@
             {"message": _("Are you sure to register new account [%(user)s] to server %(server)s ?") % {'user': user, 'server': server, 'profile': profile}},
             self.regisConfirmCB, profile)
         print "===============+++++++++++ REGISTER NEW ACCOUNT++++++++++++++============"
-        print "id=", id
-        print "data=", data
 
     def regisConfirmCB(self, id, accepted, data, profile):
         print _("register Confirmation CB ! (%s)") % str(accepted)
@@ -428,6 +426,7 @@
         @param fields: list of tuples (name, value)
         @return: tuple: (id, deferred)
         """
+        # FIXME: to be removed
 
         profile = self.memory.getProfileName(profile_key)
         assert(profile)
@@ -469,29 +468,6 @@
             return False
         return self.profiles[profile].isConnected()
 
-    def launchAction(self, type, data, profile_key):
-        """Launch a specific action asked by client
-        @param type: action type (button)
-        @param data: needed data to launch the action
-
-        @return: action id for result, or empty string in case or error
-        """
-        profile = self.memory.getProfileName(profile_key)
-        if not profile:
-            error(_('trying to launch action with a non-existant profile'))
-            raise Exception  # TODO: raise a proper exception
-        if type == "button":
-            try:
-                cb_name = data['callback_id']
-            except KeyError:
-                error(_("Incomplete data"))
-                return ""
-            id = sat_next_id()
-            self.callGeneralCB(cb_name, id, data, profile=profile)
-            return id
-        else:
-            error(_("Unknown action type"))
-            return ""
 
     ## jabber methods ##
 
@@ -808,6 +784,7 @@
         @param data: data (depend of confirmation conf_type)
         @param cb: callback called with the answer
         """
+        # FIXME: use XMLUI and *callback methods for dialog
         client = self.getClient(profile)
         if not client:
             raise exceptions.ProfileUnknownError(_("Asking confirmation a non-existant profile"))
@@ -863,24 +840,53 @@
             #debug("Requested progress for unknown progress_id")
         return data
 
-    def registerGeneralCB(self, name, CB):
-        """Register a callback called for general reason"""
-        self.__general_cb_map[name] = CB
-
-    def removeGeneralCB(self, name):
-        """Remove a general callback"""
-        if name not in self.__general_cb_map:
-            error(_("Trying to remove an unknow general callback"))
+    def registerCallback(self, callback, *args, **kwargs):
+        """ Register a callback.
+        Use with_data=True in kwargs if the callback use the optional data dict
+        use force_id=id to avoid generated id. Can lead to name conflict, avoid if possible
+        @param callback: any callable
+        @return: id of the registered callback
+        """
+        callback_id = kwargs.pop('force_id', None)
+        if callback_id is None:
+            callback_id = str(uuid4())
         else:
-            del self.__general_cb_map[name]
+            if callback_id in self._cb_map:
+                raise exceptions.ConflictError(_(u"id already registered"))
+        self._cb_map[callback_id] = (callback, args, kwargs)
+        return callback_id
+
+    def removeCallback(self, callback_id):
+        """ Remove a previously registered callback
+        @param callback_id: id returned by [registerCallback] """
+        del self._cb_map[callback_id]
 
-    def callGeneralCB(self, name, *args, **kwargs):
-        """Call general function back"""
+    def launchCallback(self, callback_id, data=None, profile_key="@NONE@"):
+        """Launch a specific callback
+        @param callback_id: id of the action (callback) to launch
+        @param data: optional data
+        @profile_key: %(doc_profile_key)s
+        @return: a deferred which fire a dict where key can be:
+            - xmlui: a XMLUI need to be displayed
+        """
+        profile = self.memory.getProfileName(profile_key)
+        if not profile:
+            raise exceptions.ProfileUnknownError(_('trying to launch action with a non-existant profile'))
+
         try:
-            return self.__general_cb_map[name](*args, **kwargs)
+            callback, args, kwargs = self._cb_map[callback_id]
         except KeyError:
-            error(_("Trying to call unknown function (%s)") % name)
-            return None
+            raise exceptions.DataError("Unknown callback id")
+
+        if kwargs.get("with_data", False):
+            if data is None:
+                raise exceptions.DataError("Required data for this callback is missing")
+            args,kwargs=list(args)[:],kwargs.copy() # we don't want to modify the original (kw)args
+            args.insert(0, data)
+            kwargs["profile"] = profile
+            del kwargs["with_data"]
+
+        return defer.maybeDeferred(callback, *args, **kwargs)
 
     #Menus management
 
@@ -925,6 +931,7 @@
         @param profile_key: %(doc_profile_key)s
         @return: XMLUI or empty string if it's a one shot menu
         """
+        # TODO: menus should use launchCallback
         profile = self.memory.getProfileName(profile_key)
         if not profile:
             raise exceptions.ProfileUnknownError
--- a/src/memory/memory.py	Tue Dec 24 15:19:08 2013 +0100
+++ b/src/memory/memory.py	Tue Dec 24 15:19:08 2013 +0100
@@ -212,7 +212,7 @@
         self.default_profile = None
         self.params = {}
         self.params_gen = {}
-        host.registerGeneralCB("registerNewAccount", host.registerNewAccountCB)
+        host.registerCallback(host.registerNewAccountCB, force_id="registerNewAccount")
 
     def createProfile(self, profile):
         """Create a new profile
--- a/src/plugins/plugin_misc_quiz.py	Tue Dec 24 15:19:08 2013 +0100
+++ b/src/plugins/plugin_misc_quiz.py	Tue Dec 24 15:19:08 2013 +0100
@@ -26,8 +26,6 @@
 import random
 
 from wokkel import data_form
-from sat.tools.xml_tools import dataForm2XML
-from sat_frontends.tools.games import TarotCard
 from time import time
 
 
--- a/src/tools/xml_tools.py	Tue Dec 24 15:19:08 2013 +0100
+++ b/src/tools/xml_tools.py	Tue Dec 24 15:19:08 2013 +0100
@@ -219,7 +219,7 @@
 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, layout="vertical", title=None):
+    def __init__(self, panel_type, layout="vertical", title=None, submit_id=None):
         """Init SàT XML Panel
         @param panel_type: one of
             - window (new window)
@@ -232,6 +232,7 @@
               (usually one for a label, the next for the element)
             - tabs: elemens are in categories with tabs (notebook)
         @param title: title or default if None
+        @param submit_id: callback id to call for panel_type we can submit (form, param)
         """
         if not panel_type in ['window', 'form', 'param']:
             error(_("Unknown panel type [%s]") % panel_type)
@@ -244,6 +245,8 @@
         top_element.setAttribute("type", panel_type)
         if title:
             top_element.setAttribute("title", title)
+        if submit_id:
+            top_element.setAttribute("submit", submit_id)
         self.parentTabsLayout = None  # used only we have 'tabs' layout
         self.currentCategory = None  # used only we have 'tabs' layout
         self.currentLayout = None
@@ -369,7 +372,7 @@
         @fields_back: list of names of field to give back when pushing the button
         """
         elem = self._createElem('button', name, self.currentLayout)
-        elem.setAttribute('callback_id', callback_id)
+        elem.setAttribute('callback', callback_id)
         elem.setAttribute('value', value)
         for field in fields_back:
             fback_el = self.doc.createElement('field_back')