changeset 60:9764e027ecc0

SàT: multi-profile parameters, first draft - new bridge methods getProfilesList and createProfile - param xml now use two types: general and individual, general are common parameters, individual parameters are linked to profiles - indidual xml are constructed from param xml and saved values /!\ params values are not saved yet
author Goffi <goffi@goffi.org>
date Thu, 28 Jan 2010 02:32:27 +1100
parents 3e5abe3bbead
children 58d49fc19639
files plugins/plugin_xep_0065.py sat sat.tac sat_bridge/DBus.py tools/memory.py
diffstat 5 files changed, 207 insertions(+), 39 deletions(-) [+]
line wrap: on
line diff
--- a/plugins/plugin_xep_0065.py	Sun Jan 10 18:40:45 2010 +1100
+++ b/plugins/plugin_xep_0065.py	Thu Jan 28 02:32:27 2010 +1100
@@ -455,10 +455,12 @@
     
     params = """
     <params>
+    <general>
     <category name="File Transfert">
         <param name="IP" value='0.0.0.0' default_cb='yes' type="string" />
         <param name="Port" value="28915" type="string" />
     </category>
+    </general>
     </params>
     """
 
--- a/sat	Sun Jan 10 18:40:45 2010 +1100
+++ b/sat	Thu Jan 28 02:32:27 2010 +1100
@@ -1,3 +1,3 @@
 #!/bin/sh
-twistd -oy sat.tac
+twistd -noy sat.tac
 
--- a/sat.tac	Sun Jan 10 18:40:45 2010 +1100
+++ b/sat.tac	Thu Jan 28 02:32:27 2010 +1100
@@ -21,7 +21,7 @@
 
 CONST = {
     'client_name' : u'SàT (Salut à toi)',
-    'client_version' : u'0.0.2',   #Please add 'D' at the end for dev versions
+    'client_version' : u'0.0.2D',   #Please add 'D' at the end for dev versions
     'local_dir' : '~/.sat'
 }
 
@@ -312,6 +312,8 @@
         self.server_features=[]  #XXX: temp dic, need to be transfered into self.memory in the future
 
         self.bridge=DBusBridge()
+        self.bridge.register("getProfilesList", self.memory.getProfilesList)
+        self.bridge.register("createProfile", self.memory.createProfile)
         self.bridge.register("registerNewAccount", self.registerNewAccount)
         self.bridge.register("connect", self.connect)
         self.bridge.register("disconnect", self.disconnect)
@@ -351,14 +353,19 @@
             self.plugins[plug_info['import_name']] = getattr(mod, plug_info['main'])(self)
             #TODO: test xmppclient presence and register handler parent
 
-    def connect(self):
+    def getJidFrProfile(self, profile):
+        """Return jid from source"""
+        if profile == "@DEFAULT@":
+            return  jid.JID(self.memory.getParamA("JabberID", "Connection"))
+
+    def connect(self, profile = '@DEFAULT@'):
         """Connect to jabber server"""
 
         if (self.isConnected()):
             info("already connected !")
             return
         print "connecting..."
-        self.me = jid.JID(self.memory.getParamA("JabberID", "Connection"))
+        self.me = self.getJidFrProfile(profile)
         self.xmppclient = SatXMPPClient(self.bridge, self.me, self.memory.getParamA("Password", "Connection"),
             self.memory.getParamA("Server", "Connection"), 5222)
         self.xmppclient.streamInitialized = self.streamInitialized
--- a/sat_bridge/DBus.py	Sun Jan 10 18:40:45 2010 +1100
+++ b/sat_bridge/DBus.py	Thu Jan 28 02:32:27 2010 +1100
@@ -105,6 +105,18 @@
 
     ### methods ###    
 
+    @dbus.service.method(const_INT_PREFIX+const_REQ_SUFFIX,
+                         in_signature='', out_signature='as')
+    def getProfilesList(self):
+        info ('Profile list asked')
+        return self.cb["getProfilesList"]()
+
+    @dbus.service.method(const_INT_PREFIX+const_REQ_SUFFIX,
+                         in_signature='sb', out_signature='')
+    def createProfile(self, name, default=False):
+        info ('Profile creation asked')
+        return self.cb["createProfile"](name, default)
+
     @dbus.service.method(const_INT_PREFIX+const_COMM_SUFFIX,
                          in_signature='sssi', out_signature='s')
     def registerNewAccount(self, login, password, host, port=5222):
--- a/tools/memory.py	Sun Jan 10 18:40:45 2010 +1100
+++ b/tools/memory.py	Thu Jan 28 02:32:27 2010 +1100
@@ -41,15 +41,19 @@
     #TODO: mettre Watched dans un plugin
     default_xml = u"""
     <params>
-    <category name="Connection">
-        <param name="JabberID" value="goffi@necton2.int/TestScript" type="string" />
-        <param name="Password" value="toto" type="password" />
-        <param name="Server" value="necton2.int" type="string" />
-        <param name="NewAccount" value="Register new account" type="button" callback="registerNewAccount"/>
-    </category>
-    <category name="Misc">
-        <param name="Watched" value="test@Jabber.goffi.int" type="string" />
-    </category>
+    <general>
+    </general>
+    <individual>
+        <category name="Connection">
+            <param name="JabberID" value="goffi@necton2.int/TestScript" type="string" />
+            <param name="Password" value="toto" type="password" />
+            <param name="Server" value="necton2.int" type="string" />
+            <param name="NewAccount" value="Register new account" type="button" callback="registerNewAccount"/>
+        </category>
+        <category name="Misc">
+            <param name="Watched" value="test@Jabber.goffi.int" type="string" />
+        </category>
+    </individual>
     </params>
     """
 
@@ -64,17 +68,47 @@
         """Save parameters to xml file"""
         with open(file, 'wb') as xml_file:
             self.dom.writexml(xml_file)
-    
+
     def __init__(self, host):
         debug("Parameters init")
         self.host = host
+        self.default_profile = None
+        self.params = {'goffi':{}}  #gof:
+        self.params_gen = {}
         host.set_const('savefile_param', SAVEFILE_PARAM)
-        host.set_const('savefile_history', SAVEFILE_HISTORY)
-        host.set_const('savefile_private', SAVEFILE_PRIVATE)
         host.registerGeneralCB("registerNewAccount", host.registerNewAccountCB)
 
+    def getProfilesList(self):
+        return self.params.keys()
+
+    def createProfile(self, name, default=False):
+        """Create a new profile
+        @param name: Name of the profile
+        @param default: True if default value"""
+        if self.params.has_key(name):
+            info ('The profile name already exists')
+            return 1
+        self.params[name]={}
+        return 0
+
+    def getProfileName(self, profile_key):
+        """return profile according to profile_key
+        @param profile_key: profile name or key which can be
+                            @ALL@ for all profiles
+                            @DEFAULT@ for default profile
+        @return: requested profile name or None if it doesn't exist"""
+        if profile_key=='@DEFAULT@':
+            if not self.params:
+                return None
+            info('No default profile, returning first one') #TODO: manage real default profile
+            return self.params.keys()[0]  #FIXME: gof: temporary, must use real default value, and fallback to first one if it doesn't exists
+        if not self.params.has_key(profile_key):
+            error ('Trying to access an unknown profile')
+            return None
+        return profile_key
+
     def __get_unique_node(self, parent, tag, name):
-        """return node with given tag, create a new one if the node doesn't exist
+        """return node with given tag
         @param parent: parent of nodes to check (e.g. documentElement)
         @param tag: tag to check (e.g. "category")
         @param name: name to check (e.g. "JID")
@@ -119,47 +153,133 @@
         @param errback: must manage the error with args failure, name, category
         """
         #TODO: send signal param update if value changed
-        node =  self.__getParamNode(name, category)
+        node =  self.__getParamNode(name, category, '@ALL@')
         if not node:
             error("Requested param [%s] in category [%s] doesn't exist !" , name, category)
             return
-        if node.getAttribute('default_cb') == 'yes':
-            del node.attributes['default_cb']
+        if node[1].getAttribute('default_cb') == 'yes':
+            del node[1].attributes['default_cb']
             d = defer.maybeDeferred(callback)
             d.addCallback(self.__default_ok, name, category)
             d.addErrback(errback or self.__default_ko, name, category)
 
-    def getParamA(self, name, category, attr="value"):
+    def getParamA(self, name, category, attr="value", profile_key="@DEFAULT@"):
         """Helper method to get a specific attribute
            @param name: name of the parameter
            @param category: category of the parameter
            @param attr: name of the attribute (default: "value")
+           @param profile: owner of the param (@ALL@ for everyone)
            
            @return: attribute"""
         node = self.__getParamNode(name, category)
         if not node:
             error("Requested param [%s] in category [%s] doesn't exist !" , name, category)
             return None
-        return node.getAttribute(attr)
+
+        if node[0] == 'general':
+            value = self.__getParam(None, category, name, 'general')
+            return value or node[1].getAttribute(attr)
+        
+        assert(node[0] == 'individual')
+
+        profile = self.getProfileName(profile_key)
+        if not profile:
+            error('Requesting a param for an non-existant profile')
+            return None
+
+        return self.__getParam(profile, category, name) or node[1].getAttribute(attr)
+
 
-    def getParams(self):
-        """Return the whole params XML"""
-        return self.dom.toxml()
+    def __getParam(self, profile, category, name, type='individual'):
+        """Return the param, or None if it doesn't exist
+        @param profile: the profile name (not profile key, i.e. name and not something like @DEFAULT@)
+        @param category: param category
+        @param name: param name
+        """
+        if type == 'general':
+            if self.params_gen.has_key((category, name)):
+                return self.params_gen[(category, name)]
+            return None  #This general param has the default value
+        assert (type == 'individual')
+        if not self.params.has_key(profile) or not self.params[profile].has_key((category, name)):
+            return None
+        return self.params[profile][(category, name)]
 
-    def getParamsForCategory(self, category):
+    def __constructProfileXml(self, profile):
+        """Construct xml for asked profile, filling values when needed
+        /!\ as noticed in doc, don't forget to unlink the minidom.Document
+        @param profile: profile name (not key !)
+        @return: minidom.Document of the profile xml (cf warning above)
+        """
+        prof_xml = minidom.parseString('<params/>')
+        
+        for type_node in self.dom.documentElement.childNodes:
+            if type_node.nodeName == 'general' or type_node.nodeName == 'individual':  #we use all params, general and individual
+                for cat_node in type_node.childNodes:
+                    if cat_node.nodeName == 'category':
+                        category = cat_node.getAttribute('name')
+                        cat_copy = cat_node.cloneNode(True) #we make a copy for the new xml
+                        params = cat_copy.getElementsByTagName("param")
+                        for param_node in params:
+                            name = param_node.getAttribute('name')
+                            profile_value = self.__getParam(profile, category, name, type_node.nodeName)
+                            if profile_value:  #there is a value for this profile, we must change the default
+                                param_node.setAttribute('value', profile_value)
+                        prof_xml.documentElement.appendChild(cat_copy)
+        return prof_xml
+
+
+
+    def getParams(self, profile_key='@DEFAULT@'):
+        """Construct xml for asked profile
+        Take params xml as skeleton"""
+        profile = self.getProfileName(profile_key)
+        if not profile:
+            error("Asking params for inexistant profile")
+            return ""
+        prof_xml = self.__constructProfileXml(profile) 
+        return_xml = prof_xml.toxml()
+        prof_xml.unlink()
+
+        return return_xml
+
+    def getParamsForCategory(self, category, profile_key='@DEFAULT@'):
         """Return node's xml for selected category"""
-        for node in self.dom.documentElement.childNodes:
+        #TODO: manage category of general type (without existant profile)
+        profile = self.getProfileName(profile_key)
+        if not profile:
+            error("Asking params for inexistant profile")
+            return ""
+        prof_xml = self.__constructProfileXml(profile) 
+        
+        for node in prof_xml.getElementsByTagName("category"):
             if node.nodeName == "category" and node.getAttribute("name") == category:
-                return node.toxml()
+                result = node.toxml()
+                prof_xml.unlink()
+                return result
+
+        prof_xml.unlink()
         return "<category />"
 
-    def __getParamNode(self, name, category):
-        for node in self.dom.documentElement.childNodes:
-            if node.nodeName == "category" and node.getAttribute("name") == category:
-                params = node.getElementsByTagName("param")
-                for param in params:
-                    if param.getAttribute("name") == name:
-                        return param
+    def __getParamNode(self, name, category, type="@ALL@"): #FIXME: is type useful ?
+        """Return a node from the param_xml
+        @param name: name of the node
+        @param category: category of the node
+        @type: keyword for search:
+                                    @ALL@ search everywhere
+                                    @GENERAL@ only search in general type
+                                    @INDIVIDUAL@ only search in individual type
+        @return: a tuple with the node type and the the node, or None if not found"""
+
+        for type_node in self.dom.documentElement.childNodes:
+            if ( ((type == "@ALL@" or type == "@GENERAL@") and type_node.nodeName == 'general') 
+            or ( (type == "@ALL@" or type == "@INDIVIDUAL@") and type_node.nodeName == 'individual') ):
+                for node in type_node.getElementsByTagName('category'):
+                    if node.getAttribute("name") == category:
+                        params = node.getElementsByTagName("param")
+                        for param in params:
+                            if param.getAttribute("name") == name:
+                                return (type_node.nodeName, param)
         return None
         
     def getParamsCategories(self):
@@ -169,16 +289,31 @@
             categories.append(cat.getAttribute("name"))
         return categories
 
-    def setParam(self, name, value, category):
-        node = self.__getParamNode(name, category)
+    def setParam(self, name, value, category, profile_key='@DEFAULT@'):
+        """Set a parameter, return None if the parameter is not in param xml"""
+
+        node = self.__getParamNode(name, category, '@ALL@')
         if not node:
+            error('Requesting an unknown parameter (%s/%s)' % (category, name))
+            return
+        
+        if node[0] == 'general':
+            self.params_gen[(category, name)] = value
+            return
+        
+        assert (node[0] == 'individual')
+       
+        profile = self.getProfileName(profile_key)
+        if not profile:
+            error('Trying to set parameter for an unknown profile')
             return #TODO: throw an error
+        
         type = node.getAttribute("type")
         if type=="button":
             print "clique",node.toxml()
         else:
-            node.setAttribute("value", value)
-            self.host.bridge.paramUpdate(name, value, category)
+            self.params[profile][(category, name)] = value
+            self.host.bridge.paramUpdate(name, value, category) #TODO: add profile in signal
 
 class Memory:
     """This class manage all persistent informations"""
@@ -194,6 +329,8 @@
         self.private={}  #used to store private value
         self.disco={}  #XXX: maybe best in a separate class
         self.features={}
+        host.set_const('savefile_history', SAVEFILE_HISTORY)
+        host.set_const('savefile_private', SAVEFILE_PRIVATE)
         self.load()
 
     def load(self):
@@ -254,6 +391,16 @@
             pickle.dump(self.private, private_pickle)
         debug("private values saved")
 
+    def getProfilesList(self):
+        return self.params.getProfilesList()
+
+    def createProfile(self, name, default=False):
+        """Create a new profile
+        @param name: Profile name
+        @param default: True if default profile (replace previous default)
+        """
+        return self.params.createProfile(name, default)
+
     def addToHistory(self, me_jid, from_jid, to_jid, type, message):
         me_short=me_jid.userhost()
         from_short=from_jid.userhost()