← Back to team overview

gtg team mailing list archive

[Merge] lp:~gtg-user/gtg/multibackends-halfgsoc_merge into lp:gtg

 

Luca Invernizzi has proposed merging lp:~gtg-user/gtg/multibackends-halfgsoc_merge into lp:gtg.

Requested reviews:
  Gtg developers (gtg)


It's the last merge request [0] with all the changes we talked about applied.
It seems quite stable, since I had the need to change it rarely. 
The other backends will come in separate merges, once this one is accepted.

[0] https://code.edge.launchpad.net/~gtg-user/gtg/backends-first-merge/+merge/26532
-- 
https://code.launchpad.net/~gtg-user/gtg/multibackends-halfgsoc_merge/+merge/28258
Your team Gtg developers is requested to review the proposed merge of lp:~gtg-user/gtg/multibackends-halfgsoc_merge into lp:gtg.
=== modified file 'GTG/backends/__init__.py'
--- GTG/backends/__init__.py	2010-03-01 01:55:12 +0000
+++ GTG/backends/__init__.py	2010-06-23 01:19:23 +0000
@@ -23,14 +23,173 @@
 (like on the hard disk or on the internet)
 and to read projects from this medium
 """
-#
-#Current backends are :
-#
-# localfile.py : store and read a local XML file
-#
-#
-#
-# this __init__.py should not be empty. It should list the available backends
-#
-#http://www.faqts.com/knowledge_base/view.phtml/aid/4221/fid/538
-#http://www.python.org/doc/2.1.3/tut/node8.html
+
+import sys
+import uuid
+import os.path
+
+from GTG.tools.logger            import Log
+from GTG.tools.borg              import Borg
+from GTG.backends.genericbackend import GenericBackend
+from GTG.core                    import firstrun_tasks
+from GTG.tools                   import cleanxml
+from GTG.core                    import CoreConfig
+
+
+
+class BackendFactory(Borg):
+    '''
+    This class holds the information about the backend types.
+    Since it's about types, all information is static. The instantiated
+    backends are handled in the Datastore.
+    It is a Borg for what matters its only state (_backend_modules),
+    since it makes no sense of keeping multiple instances of this.
+    '''
+
+
+    BACKEND_PREFIX = "backend_"
+
+    def __init__(self):
+        """
+         Creates a dictionary of the currently available backend modules
+        """
+        super(BackendFactory, self).__init__()
+        if hasattr(self, "backend_modules"):
+            #This object has already been constructed
+            return
+        self.backend_modules = {}
+        #Look for backends in the GTG/backends dir
+        this_dir = os.path.dirname(__file__)
+        backend_files = filter(lambda f: f.endswith(".py") and     \
+               f[ : len(self.BACKEND_PREFIX)] == self.BACKEND_PREFIX , \
+               os.listdir(this_dir))
+        #Create module names
+        module_names = map(lambda f: f.replace(".py",""), backend_files)
+        Log.debug("Backends found: " + str(module_names))
+        #Load backend modules
+        for module_name in module_names:
+            extended_module_name = "GTG.backends." + module_name
+            try:
+                __import__(extended_module_name)
+            except ImportError, exception:
+                #Something is wrong with this backend, skipping
+                Log.debug("Backend %s could not be loaded: %s" % \
+                          (module_name, str(exception)))
+                continue
+            self.backend_modules[module_name] = \
+                    sys.modules[extended_module_name]
+
+    def get_backend(self, backend_name):
+        '''
+        Returns the backend module for the backend matching 
+        backend_name. Else, returns none
+        '''
+        if backend_name in self.backend_modules:
+            return self.backend_modules[backend_name]
+        else:
+            Log.debug("Trying to load backend %s, but failed!" % backend_name)
+            return None
+
+    def get_all_backends(self):
+        '''
+        Returns a dictionary containing all the backends types
+        '''
+        return self.backend_modules
+
+    def get_new_backend_dict(self, backend_name, additional_parameters = {}):
+        '''
+        Constructs a new backend initialization dictionary. In more
+        exact terms, creates a dictionary, containing all the necessary
+        entries to initialize a backend.
+        '''
+        if not self.backend_modules.has_key(backend_name):
+            return None
+        dic = {}
+        module = self.get_backend(backend_name)
+        #Different pids are necessary to discern between backends of the same
+        # type
+        parameters = module.Backend.get_static_parameters()
+        #we all the parameters and their default values in dic
+        for param_name, param_dic in parameters.iteritems():
+            dic[param_name] = param_dic[GenericBackend.PARAM_DEFAULT_VALUE]
+        dic["pid"] = str(uuid.uuid4())
+        dic["module"] = module.Backend.get_name()
+        for param_name, param_value in additional_parameters.iteritems():
+            dic[param_name] = param_value
+        dic["backend"] = module.Backend(dic)
+        return dic
+
+    def restore_backend_from_xml(self, dic):
+        '''
+        Function restoring a backend from its xml description.
+        dic should be a dictionary containing at least the key 
+        - "module", with the module name
+        - "xmlobject", with its xml description.
+        Every other key is passed as-is to the backend, as parameter.
+
+        Returns the backend instance, or None is something goes wrong
+        '''
+        if not "module" in dic or not "xmlobject" in dic:
+            Log.debug ("Malformed backend configuration found! %s" % \
+                       dic)
+        module = self.get_backend(dic["module"])
+        if module == None:
+            Log.debug ("could not load module for backend %s" % \
+                       dic["module"])
+            return None
+        #we pop the xml object, as it will be redundant when the parameters
+        # are set directly in the dict
+        xp = dic.pop("xmlobject")
+        #Building the dictionary
+        parameters_specs = module.Backend.get_static_parameters()
+        dic["pid"] = str(xp.getAttribute("pid"))
+        for param_name, param_dic in parameters_specs.iteritems():
+            if xp.hasAttribute(param_name):
+                #we need to convert the parameter to the right format.
+                # we fetch the format from the static_parameters
+                param_type = param_dic[GenericBackend.PARAM_TYPE]
+                param_value = GenericBackend.cast_param_type_from_string( \
+                                    xp.getAttribute(param_name), param_type)
+                dic[param_name] = param_value
+        #We put the backend itself in the dict
+        dic["backend"] = module.Backend(dic)
+        return dic["backend"]
+
+    def get_saved_backends_list(self):
+        backends_dic = self._read_backend_configuration_file()
+
+        #Retrocompatibility: default backend has changed name
+        for dic in backends_dic:
+            if dic["module"] == "localfile":
+                dic["module"] = "backend_localfile"
+                dic["pid"] = str(uuid.uuid4())
+                dic["need_conversion"] = \
+                    dic["xmlobject"].getAttribute("filename")
+                
+        #Now that the backend list is build, we will construct them
+        for dic in backends_dic:
+            self.restore_backend_from_xml(dic)
+        #If no backend available, we create a new using localfile. Xmlobject
+        # will be filled in by the backend
+        if len(backends_dic) == 0:
+            dic = BackendFactory().get_new_backend_dict( \
+                                                "backend_localfile")
+            dic["backend"].this_is_the_first_run(firstrun_tasks.populate())
+            backends_dic.append(dic)
+        return backends_dic
+
+    def _read_backend_configuration_file(self):
+        '''
+        Reads the file describing the current backend configuration (project.xml)
+        and returns a list of dictionaries, each containing: 
+         - the xml object defining the backend characteristics under
+              "xmlobject"
+         - the name of the backend under "module"
+        '''
+        # Read configuration file, if it does not exist, create one
+        datafile = os.path.join(CoreConfig().get_data_dir(), CoreConfig.DATA_FILE)
+        doc, configxml = cleanxml.openxmlfile(datafile, "config")
+        xmlproject = doc.getElementsByTagName("backend")
+        # collect configured backends
+        return [{"xmlobject": xp, \
+                 "module": xp.getAttribute("module")} for xp in xmlproject]

=== added file 'GTG/backends/backend_localfile.py'
--- GTG/backends/backend_localfile.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/backend_localfile.py	2010-06-23 01:19:23 +0000
@@ -0,0 +1,181 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU 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 General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+'''
+Localfile is a read/write backend that will store your tasks in an XML file
+This file will be in your $XDG_DATA_DIR/gtg folder.
+'''
+
+import os
+import uuid
+
+from GTG.backends.genericbackend import GenericBackend
+from GTG.core                    import CoreConfig
+from GTG.tools                   import cleanxml, taskxml
+from GTG                         import _
+
+
+
+class Backend(GenericBackend):
+    
+
+    DEFAULT_PATH = CoreConfig().get_data_dir() #default path for filenames
+
+
+    #Description of the backend (mainly it's data we show the user, only the
+    # name is used internally. Please note that BACKEND_NAME and
+    # BACKEND_ICON_NAME should *not* be translated.
+    _general_description = { \
+        GenericBackend.BACKEND_NAME:       "backend_localfile", \
+        GenericBackend.BACKEND_HUMAN_NAME: _("Local File"), \
+        GenericBackend.BACKEND_AUTHORS:    ["Lionel Dricot", \
+                                            "Luca Invernizzi"], \
+        GenericBackend.BACKEND_TYPE:       GenericBackend.TYPE_READWRITE, \
+        GenericBackend.BACKEND_DESCRIPTION: \
+            _("Your tasks are saved in a text file (XML format). " + \
+              " This is the most basic and the default way " +   \
+              "for GTG to save your tasks."),\
+        }
+
+    #parameters to configure a new backend of this type.
+    #NOTE: should we always give back a different default filename? it can be
+    #      done, but I'd like to keep this backend simple, so that it can be
+    #      used as example (invernizzi)
+    _static_parameters = { \
+        "path": { \
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING, \
+            GenericBackend.PARAM_DEFAULT_VALUE: \
+                 os.path.join(DEFAULT_PATH, "gtg_tasks-%s.xml" %(uuid.uuid4()))
+        }}
+
+    def _get_default_filename_path(self, filename = None):
+        '''
+        Generates a default path with a random filename
+        @param filename: specify a filename
+        '''
+        if not filename:
+            filename = "gtg_tasks-%s.xml" % (uuid.uuid4())
+        return os.path.join(self.DEFAULT_PATH, filename)
+
+    def __init__(self, parameters):
+        """
+        Instantiates a new backend.
+
+        @param parameters: should match the dictionary returned in
+        get_parameters. Anyway, the backend should care if one expected
+        value is None or does not exist in the dictionary. 
+        @firstrun: only needed for the default backend. It should be
+        omitted for all other backends.
+        """
+        super(Backend, self).__init__(parameters)
+        self.tids = []
+        #####RETROCOMPATIBILIY
+        #NOTE: retrocompatibility. We convert "filename" to "path"
+        #      and we forget about "filename"
+        if "need_conversion" in parameters:
+            parameters["path"] = os.path.join(self.DEFAULT_PATH, \
+                                        parameters["need_conversion"])
+            del parameters["need_conversion"]
+        if not self.KEY_DEFAULT_BACKEND in parameters:
+            parameters[self.KEY_DEFAULT_BACKEND] = True
+        ####
+        self.doc, self.xmlproj = cleanxml.openxmlfile( \
+                                self._parameters["path"], "project")
+
+    def initialize(self):
+        super(Backend, self).initialize()
+        self.doc, self.xmlproj = cleanxml.openxmlfile( \
+                                self._parameters["path"], "project")
+
+    def this_is_the_first_run(self, xml):
+        #Create the default tasks for the first run.
+        #We write the XML object in a file
+        self._parameters[self.KEY_DEFAULT_BACKEND] = True
+        cleanxml.savexml(self._parameters["path"], xml)
+        self.doc, self.xmlproj = cleanxml.openxmlfile(\
+                        self._parameters["path"], "project")
+        self._parameters[self.KEY_DEFAULT_BACKEND] = True
+
+    def start_get_tasks(self):
+        '''
+        Once this function is launched, the backend can start pushing
+        tasks to gtg parameters.
+        
+        @return: start_get_tasks() might not return or finish
+        '''
+        tid_list = []
+        for node in self.xmlproj.childNodes:
+            tid = node.getAttribute("id")
+            if tid not in self.tids:
+                self.tids.append(tid)
+            task = self.datastore.task_factory(tid)
+            if task:
+                task = taskxml.task_from_xml(task, node)
+                self.datastore.push_task(task)
+
+    def set_task(self, task):
+            tid = task.get_id()
+            existing = None
+            #First, we find the existing task from the treenode
+            for node in self.xmlproj.childNodes:
+                if node.getAttribute("id") == tid:
+                    existing = node
+            t_xml = taskxml.task_to_xml(self.doc, task)
+            modified = False
+            #We then replace the existing node
+            if existing and t_xml:
+                #We will write only if the task has changed
+                if t_xml.toxml() != existing.toxml():
+                    self.xmlproj.replaceChild(t_xml, existing)
+                    modified = True
+            #If the node doesn't exist, we create it
+            # (it might not be the case in all backends
+            else:
+                self.xmlproj.appendChild(t_xml)
+                modified = True
+            #In this particular backend, we write all the tasks
+            #This is inherent to the XML file backend
+            if modified and self._parameters["path"] and self.doc :
+                cleanxml.savexml(self._parameters["path"], self.doc)
+
+    def remove_task(self, tid):
+        ''' Completely remove the task with ID = tid '''
+        for node in self.xmlproj.childNodes:
+            if node.getAttribute("id") == tid:
+                self.xmlproj.removeChild(node)
+                if tid in self.tids:
+                    self.tids.remove(tid)
+        cleanxml.savexml(self._parameters["path"], self.doc)
+
+
+    def quit(self, disable = False):
+        '''
+        Called when GTG quits or disconnects the backend.
+        '''
+        super(Backend, self).quit(disable)
+
+    def save_state(self):
+        cleanxml.savexml(self._parameters["path"], self.doc, backup=True)
+
+    def get_number_of_tasks(self):
+        '''
+        Returns the number of tasks stored in the backend. Doesn't need to be a
+        fast function, is called just for the UI
+        '''
+        return len(self.tids)

=== added file 'GTG/backends/backendsignals.py'
--- GTG/backends/backendsignals.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/backendsignals.py	2010-06-23 01:19:23 +0000
@@ -0,0 +1,125 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU 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 General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+import gobject
+
+from GTG.tools.borg import Borg
+
+
+
+class BackendSignals(Borg):
+    '''
+    This class handles the signals that involve backends. 
+    In particular, it's a wrapper Borg class around a _BackendSignalsGObject
+    class, and all method of the wrapped class can be used as if they were part
+    of this class
+    '''
+    
+    #error codes to send along with the BACKEND_FAILED signal
+    ERRNO_AUTHENTICATION = "authentication failed"
+    ERRNO_NETWORK = "network is down"
+    ERRNO_DBUS = "Dbus interface cannot be connected"
+
+    def __init__(self):
+        super(BackendSignals, self).__init__()
+        if  hasattr(self, "_gobject"):
+            return
+        self._gobject = _BackendSignalsGObject()
+
+    def __getattr__(self, attr):
+        return getattr(self._gobject, attr)
+
+
+class _BackendSignalsGObject(gobject.GObject):
+
+    #signal name constants
+    BACKEND_STATE_TOGGLED = 'backend-state-toggled' #emitted when a
+                                                    #backend is
+                                                    #enabled or disabled
+    BACKEND_RENAMED = 'backend-renamed' #emitted when a backend is renamed
+    BACKEND_ADDED = 'backend-added'
+    BACKEND_REMOVED = 'backend-added' #when a backend is deleted
+    DEFAULT_BACKEND_LOADED = 'default-backend-loaded' #emitted after all
+                                                     # tasks have been
+                                                     # loaded from the
+                                                     # default backend
+    BACKEND_FAILED = 'backend-failed' #something went wrong with a backend
+    BACKEND_SYNC_STARTED = 'backend-sync-started'
+    BACKEND_SYNC_ENDED = 'backend-sync-ended'
+
+    __string_signal__ = (gobject.SIGNAL_RUN_FIRST, \
+                     gobject.TYPE_NONE, (str, ))
+    __none_signal__   = (gobject.SIGNAL_RUN_FIRST, \
+                     gobject.TYPE_NONE, ( ))
+    __string_string_signal__ = (gobject.SIGNAL_RUN_FIRST, \
+                    gobject.TYPE_NONE, (str, str, ))
+
+    __gsignals__ = {BACKEND_STATE_TOGGLED : __string_signal__, \
+                    BACKEND_RENAMED       : __string_signal__, \
+                    BACKEND_ADDED         : __string_signal__, \
+                    BACKEND_REMOVED       : __string_signal__, \
+                    BACKEND_SYNC_STARTED  : __string_signal__, \
+                    BACKEND_SYNC_ENDED    : __string_signal__, \
+                    DEFAULT_BACKEND_LOADED: __none_signal__, \
+                    BACKEND_FAILED        : __string_string_signal__}
+
+    def __init__(self):
+        super(_BackendSignalsGObject, self).__init__()
+        self.backends_currently_syncing = []
+
+    ############# Signals #########
+    #connecting to signals is fine, but keep an eye if you should emit them.
+    #As a general rule, signals should only be emitted in the GenericBackend
+    #class
+    
+    def _emit_signal(self, signal, backend_id):
+        gobject.idle_add(self.emit, signal, backend_id)
+
+    def backend_state_changed(self, backend_id):
+        self._emit_signal(self.BACKEND_STATE_TOGGLED, backend_id)
+
+    def backend_renamed(self, backend_id):
+        self._emit_signal(self.BACKEND_RENAMED, backend_id)
+
+    def backend_added(self, backend_id):
+        self._emit_signal(self.BACKEND_ADDED, backend_id)
+
+    def backend_removed(self, backend_id):
+        self._emit_signal(self.BACKEND_REMOVED, backend_id)
+
+    def default_backend_loaded(self):
+        gobject.idle_add(self.emit, self.DEFAULT_BACKEND_LOADED)
+
+    def backend_failed(self, backend_id, error_code):
+        gobject.idle_add(self.emit, self.BACKEND_FAILED, backend_id, \
+                         error_code)
+
+    def backend_sync_started(self, backend_id):
+        self._emit_signal(self.BACKEND_SYNC_STARTED, backend_id)
+        self.backends_currently_syncing.append(backend_id)
+
+    def backend_sync_ended(self, backend_id):
+        self._emit_signal(self.BACKEND_SYNC_ENDED, backend_id)
+        try:
+            self.backends_currently_syncing.remove(backend_id)
+        except:
+            pass
+    
+    def is_backend_syncing(self, backend_id):
+        return backend_id in self.backends_currently_syncing

=== added file 'GTG/backends/genericbackend.py'
--- GTG/backends/genericbackend.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/genericbackend.py	2010-06-23 01:19:23 +0000
@@ -0,0 +1,571 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU 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 General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+'''
+FIXME: document!
+'''
+
+import os
+import sys
+import errno
+import pickle
+import threading
+from collections import deque
+
+from GTG.backends.backendsignals import BackendSignals
+from GTG.tools.keyring import Keyring
+from GTG.core import CoreConfig
+from GTG.tools.logger import Log
+
+
+
+
+class GenericBackend(object):
+    '''
+    Base class for every backend. It's a little more than an interface which
+    methods have to be redefined in order for the backend to run.
+    '''
+
+
+    #BACKEND TYPE DESCRIPTION
+    #"_general_description" is a dictionary that holds the values for the
+    # following keys:
+    BACKEND_NAME = "name" #the backend gtg internal name (doesn't change in
+                          # translations, *must be unique*)
+    BACKEND_HUMAN_NAME = "human-friendly-name" #The name shown to the user
+    BACKEND_DESCRIPTION = "description" #A short description of the backend
+    BACKEND_AUTHORS = "authors" #a list of strings
+    BACKEND_TYPE = "type"
+    #BACKEND_TYPE is one of:
+    TYPE_READWRITE = "readwrite"
+    TYPE_READONLY = "readonly"
+    TYPE_IMPORT = "import"
+    TYPE_EXPORT = "export"
+    _general_description = {}
+
+
+    #"static_parameters" is a dictionary of dictionaries, each of which
+    #representing a parameter needed to configure the backend.
+    #each "sub-dictionary" is identified by this a key representing its name.
+    #"static_parameters" will be part of the definition of each
+    #particular backend.
+    # Each dictionary contains the keys:
+    #PARAM_DESCRIPTION = "description" #short description (shown to the user
+                                      # during configuration)
+    PARAM_DEFAULT_VALUE = "default_value" # its default value
+    PARAM_TYPE = "type"  
+    #PARAM_TYPE is one of the following (changing this changes the way
+    # the user can configure the parameter)
+    TYPE_PASSWORD = "password" #the real password is stored in the GNOME
+                               # keyring
+                               # This is just a key to find it there
+    TYPE_STRING = "string"  #generic string, nothing fancy is done
+    TYPE_INT = "int"  #edit box can contain only integers
+    TYPE_BOOL = "bool" #checkbox is shown
+    TYPE_LIST_OF_STRINGS = "liststring" #list of strings. the "," character is
+                                        # prohibited in strings
+    _static_parameters = {}
+
+    def initialize(self):
+        '''
+        Called each time it is enabled again (including on backend creation).
+        Please note that a class instance for each disabled backend *is*
+        created, but it's not initialized. 
+        Optional. 
+        NOTE: make sure to call super().initialize()
+        '''
+        for module_name in self.get_required_modules():
+            sys.modules[module_name]= __import__(module_name)
+        self._parameters[self.KEY_ENABLED] = True
+        self._is_initialized = True
+        #we signal that the backend has been enabled
+        self._signal_manager.backend_state_changed(self.get_id())
+
+    def start_get_tasks(self):
+        '''
+        Once this function is launched, the backend can start pushing
+        tasks to gtg parameters.
+
+        @return: start_get_tasks() might not return or finish
+        '''
+        raise NotImplemented()
+
+    def set_task(self, task):
+        '''
+        Save the task in the backend. If the task id is new for the 
+        backend, then a new task must be created.
+        '''
+        pass
+
+    def remove_task(self, tid):
+        ''' Completely remove the task with ID = tid '''
+        pass
+
+    def has_task(self, tid):
+        '''Returns true if the backend has an internal idea 
+           of the task corresponding to the tid. False otherwise'''
+        raise NotImplemented()
+
+    def new_task_id(self):
+        '''
+        Returns an available ID for a new task so that a task with this ID
+        can be saved with set_task later.
+        '''
+        raise NotImplemented()
+
+    def this_is_the_first_run(self, xml):
+        '''
+        Steps to execute if it's the first time the backend is run. Optional.
+        '''
+        pass
+
+    def purge(self):
+        '''
+        Called when a backend will be removed from GTG. Useful for removing
+        configuration files. Optional.
+        '''
+        pass
+
+    def get_number_of_tasks(self):
+        '''
+        Returns the number of tasks stored in the backend. Doesn't need to be a
+        fast function, is called just for the UI
+        '''
+        raise NotImplemented()
+
+    @staticmethod
+    def get_required_modules():
+        return []
+
+    def quit(self, disable = False):
+        '''
+        Called when GTG quits or disconnects the backend. Remember to execute
+        also this function when quitting. If disable is True, the backend won't
+        be automatically loaded at next GTG start
+        '''
+        self._is_initialized = False
+        if disable:
+            self._parameters[self.KEY_ENABLED] = False
+            #we signal that we have been disabled
+            self._signal_manager.backend_state_changed(self.get_id())
+            self._signal_manager.backend_sync_ended(self.get_id())
+        syncing_thread = threading.Thread(target = self.sync).run()
+
+    def save_state(self):
+        '''
+        It's the last function executed on a quitting backend, after the
+        pending actions have been done.
+        Useful to ensure that the state is saved in a consistent manner
+        '''
+        pass
+
+###############################################################################
+###### You don't need to reimplement the functions below this line ############
+###############################################################################
+
+    #These parameters are common to all backends and necessary.
+    # They will be added automatically to your _static_parameters list
+    #NOTE: for now I'm disabling changing the default backend. Once it's all
+    #      set up, we will see about that (invernizzi)
+    KEY_DEFAULT_BACKEND = "Default"
+    KEY_ENABLED = "Enabled"
+    KEY_HUMAN_NAME = BACKEND_HUMAN_NAME
+    KEY_ATTACHED_TAGS = "attached-tags"
+    KEY_USER = "user"
+    KEY_PID = "pid"
+    ALLTASKS_TAG = "gtg-tags-all"  #IXME: moved here to avoid circular imports
+
+    _static_parameters_obligatory = { \
+                                    KEY_DEFAULT_BACKEND: { \
+                                         PARAM_TYPE: TYPE_BOOL, \
+                                         PARAM_DEFAULT_VALUE: False, \
+                                    }, \
+                                    KEY_HUMAN_NAME: { \
+                                         PARAM_TYPE: TYPE_STRING, \
+                                         PARAM_DEFAULT_VALUE: "", \
+                                    }, \
+                                    KEY_USER: { \
+                                         PARAM_TYPE: TYPE_STRING, \
+                                         PARAM_DEFAULT_VALUE: "", \
+                                    }, \
+                                    KEY_PID: { \
+                                         PARAM_TYPE: TYPE_STRING, \
+                                         PARAM_DEFAULT_VALUE: "", \
+                                    }, \
+                                    KEY_ENABLED: { \
+                                         PARAM_TYPE: TYPE_BOOL, \
+                                         PARAM_DEFAULT_VALUE: False, \
+                                    }}
+
+    _static_parameters_obligatory_for_rw = { \
+                                    KEY_ATTACHED_TAGS: {\
+                                         PARAM_TYPE: TYPE_LIST_OF_STRINGS, \
+                                         PARAM_DEFAULT_VALUE: [ALLTASKS_TAG], \
+                                    }}
+    
+    #Handy dictionary used in type conversion (from string to type)
+    _type_converter = {TYPE_STRING: str,
+                       TYPE_INT: int,
+                      }
+
+    @classmethod
+    def _get_static_parameters(cls):
+        '''
+        Helper method, used to obtain the full list of the static_parameters
+        (user configured and default ones)
+        '''
+        if hasattr(cls, "_static_parameters"):
+            temp_dic = cls._static_parameters_obligatory.copy()
+            if cls._general_description[cls.BACKEND_TYPE] == cls.TYPE_READWRITE:
+                for key, value in \
+                          cls._static_parameters_obligatory_for_rw.iteritems():
+                    temp_dic[key] = value
+            for key, value in cls._static_parameters.iteritems():
+                temp_dic[key] = value
+            return temp_dic 
+        else:
+            raise NotImplemented("_static_parameters not implemented for " + \
+                                 "backend %s" % type(cls))
+
+    def __init__(self, parameters):
+        """
+        Instantiates a new backend. Please note that this is called also for
+        disabled backends. Those are not initialized, so you might want to check
+        out the initialize() function.
+        """
+        if self.KEY_DEFAULT_BACKEND not in parameters:
+            parameters[self.KEY_DEFAULT_BACKEND] = True
+        if parameters[self.KEY_DEFAULT_BACKEND] or \
+                (not self.KEY_ATTACHED_TAGS in parameters and \
+                self._general_description[self.BACKEND_TYPE] \
+                                        == self.TYPE_READWRITE):
+            parameters[self.KEY_ATTACHED_TAGS] = [self.ALLTASKS_TAG]
+        self._parameters = parameters
+        self._signal_manager = BackendSignals()
+        self._is_initialized = False
+        if Log.is_debugging_mode():
+            self.timer_timestep = 5
+        else:
+            self.timer_timestep = 1 
+        self.to_set_timer = None
+        self.please_quit = False
+        self.to_set = deque()
+        self.to_remove = deque()
+
+    def get_attached_tags(self):
+        '''
+        Returns the list of tags which are handled by this backend
+        '''
+        if hasattr(self._parameters, self.KEY_DEFAULT_BACKEND) and \
+                   self._parameters[self.KEY_DEFAULT_BACKEND]:
+            return [self.ALLTASKS_TAG]
+        try:
+            return self._parameters[self.KEY_ATTACHED_TAGS]
+        except:
+            return []
+
+    def set_attached_tags(self, tags):
+        '''
+        Changes the set of attached tags
+        '''
+        self._parameters[self.KEY_ATTACHED_TAGS] = tags
+
+    @classmethod
+    def get_static_parameters(cls):
+        """
+        Returns a dictionary of parameters necessary to create a backend.
+        """
+        return cls._get_static_parameters()
+
+    def get_parameters(self):
+        """
+        Returns a dictionary of the current parameters.
+        """
+        return self._parameters
+
+    def set_parameter(self, parameter, value):
+        self._parameters[parameter] = value
+
+    @classmethod
+    def get_name(cls):
+        """
+        Returns the name of the backend as it should be displayed in the UI
+        """
+        return cls._get_from_general_description(cls.BACKEND_NAME)
+
+    @classmethod
+    def get_description(cls):
+        """Returns a description of the backend"""
+        return cls._get_from_general_description(cls.BACKEND_DESCRIPTION)
+
+    @classmethod
+    def get_type(cls):
+        """Returns the backend type(readonly, r/w, import, export) """
+        return cls._get_from_general_description(cls.BACKEND_TYPE)
+
+    @classmethod
+    def get_authors(cls):
+        '''
+        returns the backend author(s)
+        '''
+        return cls._get_from_general_description(cls.BACKEND_AUTHORS)
+
+    @classmethod
+    def _get_from_general_description(cls, key):
+        '''
+        Helper method to extract values from cls._general_description.
+        Raises an exception if the key is missing (helpful for developers
+        adding new backends).
+        '''
+        if key in cls._general_description:
+            return cls._general_description[key]
+        else:
+            raise NotImplemented("Key %s is missing from " +\
+                    "'self._general_description' of a backend (%s). " +
+                    "Please add the corresponding value" % (key, type(cls)))
+
+    @classmethod
+    def cast_param_type_from_string(cls, param_value, param_type):
+        '''
+        Parameters are saved in a text format, so we have to cast them to the
+        appropriate type on loading. This function does exactly that.
+        '''
+        #FIXME: we could use pickle (dumps and loads), at least in some cases
+        #       (invernizzi)
+        if param_type in cls._type_converter:
+            return cls._type_converter[param_type](param_value)
+        elif param_type == cls.TYPE_BOOL:
+            if param_value == "True":
+                return True
+            elif param_value == "False":
+                return False
+            else:
+                raise Exception("Unrecognized bool value '%s'" %
+                                 param_type)
+        elif param_type == cls.TYPE_PASSWORD:
+            if param_value == -1:
+                return None
+            return Keyring().get_password(int(param_value))
+        elif param_type == cls.TYPE_LIST_OF_STRINGS:
+            the_list = param_value.split(",")
+            if not isinstance(the_list, list):
+                the_list = [the_list]
+            return the_list
+        else:
+            raise NotImplemented("I don't know what type is '%s'" %
+                                 param_type)
+
+    def cast_param_type_to_string(self, param_type, param_value):
+        '''
+        Inverse of cast_param_type_from_string
+        '''
+        if param_type == GenericBackend.TYPE_PASSWORD:
+            if param_value == None:
+                return str(-1)
+            else:
+                return str(Keyring().set_password(
+                    "GTG stored password -" + self.get_id(), param_value))
+        elif param_type == GenericBackend.TYPE_LIST_OF_STRINGS:
+            if param_value == []:
+                return ""
+            return reduce(lambda a, b: a + "," + b, param_value)
+        else:
+            return str(param_value)
+
+    def get_id(self):
+        '''
+        returns the backends id, used in the datastore for indexing backends
+        '''
+        return self.get_name() + "@" + self._parameters["pid"]
+
+    @classmethod
+    def get_human_default_name(cls):
+        '''
+        returns the user friendly default backend name. 
+        '''
+        return cls._general_description[cls.BACKEND_HUMAN_NAME]
+
+    def get_human_name(self):
+        '''
+        returns the user customized backend name. If the user hasn't
+        customized it, returns the default one
+        '''
+        if self.KEY_HUMAN_NAME in self._parameters and \
+                    self._parameters[self.KEY_HUMAN_NAME] != "":
+            return self._parameters[self.KEY_HUMAN_NAME]
+        else:
+            return self.get_human_default_name()
+
+    def set_human_name(self, name):
+        '''
+        sets a custom name for the backend
+        '''
+        self._parameters[self.KEY_HUMAN_NAME] = name
+        #we signal the change
+        self._signal_manager.backend_renamed(self.get_id())
+
+    def is_enabled(self):
+        '''
+        Returns if the backend is enabled
+        '''
+        return self.get_parameters()[GenericBackend.KEY_ENABLED] or \
+               self.is_default()
+
+    def is_default(self):
+        '''
+        Returns if the backend is enabled
+        '''
+        return self.get_parameters()[GenericBackend.KEY_DEFAULT_BACKEND]
+
+    def is_initialized(self):
+        '''
+        Returns if the backend is up and running
+        '''
+        return self._is_initialized
+
+    def get_parameter_type(self, param_name):
+        try:
+            return self.get_static_parameters()[param_name][self.PARAM_TYPE]
+        except KeyError:
+            return None
+
+    def register_datastore(self, datastore):
+        self.datastore = datastore
+
+###############################################################################
+### HELPER FUNCTIONS ##########################################################
+###############################################################################
+
+    def _store_pickled_file(self, path, data):
+        '''
+        A helper function to save some object in a file.
+        @param path: a relative path. A good choice is
+        "backend_name/object_name"
+        @param data: the object
+        '''
+        path = os.path.join(CoreConfig().get_data_dir(), path)
+        #mkdir -p
+        try:
+            os.makedirs(os.path.dirname(path))
+        except OSError, exception:
+            if exception.errno != errno.EEXIST: 
+                raise
+        #saving
+        #try:
+        with open(path, 'wb') as file:
+                pickle.dump(data, file)
+                #except pickle.PickleError:
+                    #pass
+
+    def _load_pickled_file(self, path, default_value = None):
+        '''
+        A helper function to load some object from a file.
+        @param path: the relative path of the file
+        @param default_value: the value to return if the file is missing or
+        corrupt
+        @returns object: the needed object, or default_value
+        '''
+        path = os.path.join(CoreConfig().get_data_dir(), path)
+        if not os.path.exists(path):
+            return default_value
+        else:
+            try:
+                with open(path, 'r') as file:
+                    return pickle.load(file)
+            except pickle.PickleError:
+                return default_value
+
+###############################################################################
+### THREADING #################################################################
+###############################################################################
+
+    def __try_launch_setting_thread(self):
+        '''
+        Helper function to launch the setting thread, if it's not running.
+        '''
+        if self.to_set_timer == None and self.is_enabled():
+            self.to_set_timer = threading.Timer(self.timer_timestep, \
+                                        self.launch_setting_thread)
+            self.to_set_timer.start()
+
+    def launch_setting_thread(self):
+        '''
+        This function is launched as a separate thread. Its job is to perform
+        the changes that have been issued from GTG core. In particular, for
+        each task in the self.to_set queue, a task has to be modified or to be
+        created (if the tid is new), and for each task in the self.to_remove
+        queue, a task has to be deleted
+        '''
+        while not self.please_quit:
+            try:
+                task = self.to_set.pop()
+            except IndexError:
+                break
+            #time.sleep(4)
+            tid = task.get_id()
+            if tid  not in self.to_remove:
+                self.set_task(task)
+
+        while not self.please_quit:
+            try:
+                tid = self.to_remove.pop()
+            except IndexError:
+                break
+            self.remove_task(tid)
+        #we release the weak lock
+        self.to_set_timer = None
+
+    def queue_set_task(self, task):
+        ''' Save the task in the backend. '''
+        tid = task.get_id()
+        if task not in self.to_set and tid not in self.to_remove:
+            self.to_set.appendleft(task)
+            self.__try_launch_setting_thread()
+
+    def queue_remove_task(self, tid):
+        '''
+        Queues task to be removed.
+        @param tid: The Task ID of the task to be removed
+        '''
+        if tid not in self.to_remove:
+            self.to_remove.appendleft(tid)
+            self.__try_launch_setting_thread()
+            return None
+
+    def sync(self):
+        '''
+        Helper method. Forces the backend to perform all the pending changes.
+        It is usually called upon quitting the backend.
+        '''
+        #FIXME: this function should become part of the r/w r/o generic class
+        #  for backends
+        if self.to_set_timer != None:
+            self.please_quit = True
+            try:
+                self.to_set_timer.cancel()
+            except:
+                pass
+            try:
+                self.to_set_timer.join(5)
+            except:
+                pass
+        self.please_quit = False
+        self.launch_setting_thread()
+        self.save_state()
+

=== removed file 'GTG/backends/localfile.py'
--- GTG/backends/localfile.py	2010-04-26 23:12:57 +0000
+++ GTG/backends/localfile.py	1970-01-01 00:00:00 +0000
@@ -1,176 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Gettings Things Gnome! - a personal organizer for the GNOME desktop
-# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
-#
-# This program is free software: you can redistribute it and/or modify it under
-# the terms of the GNU 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 General Public License for more
-# details.
-#
-# You should have received a copy of the GNU General Public License along with
-# this program.  If not, see <http://www.gnu.org/licenses/>.
-# -----------------------------------------------------------------------------
-
-'''
-Localfile is a read/write backend that will store your tasks in an XML file
-This file will be in your $XDG_DATA_DIR/gtg folder.
-'''
-
-import os
-import uuid
-
-from GTG.core  import CoreConfig
-from GTG.tools import cleanxml, taskxml
-
-def get_name():
-    """Returns the name of the backend as it should be displayed in the UI"""
-    return "Local File"
-
-def get_description():
-    """Returns a description of the backend"""
-    return "Your tasks are saved in an XML file located in your HOME folder"
-
-def get_parameters():
-    """
-    Returns a dictionary of parameters. Keys should be strings and
-    are the name of the parameter.
-    Values are string with value : string, password, int, bool
-    and are an information about the type of the parameter
-    Currently, only string is supported.
-    """
-    dic = {}
-    dic["filename"] = "string"
-    return dic
-
-def get_features():
-    """Returns a dict of features supported by this backend"""
-    return {}
-
-def get_type():
-    """Type is one of : readwrite, readonly, import, export"""
-    return "readwrite"
-
-class Backend:
-    def __init__(self, parameters, firstrunxml=None):
-        """
-        Instantiates a new backend.
-
-        @param parameters: should match the dictionary returned in
-         get_parameters. Anyway, the backend should care if one expected value is
-         None or does not exist in the dictionary.
-        @firstrun: only needed for the default backend. It should be omitted for
-         all other backends.
-        """
-        self.tids = []
-        self.pid = 1
-        if "filename" in parameters:
-            zefile = parameters["filename"]
-        #If zefile is None, we create a new file
-        else:
-            zefile = "%s.xml" %(uuid.uuid4())
-            parameters["filename"] = zefile
-        #For the day we want to open files somewhere else
-        default_folder = True
-        if default_folder:
-            self.zefile = os.path.join(CoreConfig.DATA_DIR, zefile)
-            self.filename = zefile
-        else:
-            self.zefile = zefile
-            self.filename = zefile
-        #Create the default tasks for the first run.
-        #We write the XML object in a file
-        if firstrunxml and not os.path.exists(zefile):
-            #shutil.copy(firstrunfile,self.zefile)
-            cleanxml.savexml(self.zefile, firstrunxml)
-        self.doc, self.xmlproj = cleanxml.openxmlfile(self.zefile, "project")
-
-    def start_get_tasks(self,push_task_func,task_factory_func):
-        '''
-        Once this function is launched, the backend can start pushing
-        tasks to gtg parameters.
-
-        @push_task_func: a function that takes a Task as parameter
-         and pushes it into GTG.
-        @task_factory_func: a function that takes a tid as parameter
-         and returns a Task object with the given pid. 
-
-        @return: start_get_tasks() might not return or finish
-        '''
-        tid_list = []
-        for node in self.xmlproj.childNodes:
-            #time.sleep(2)
-            tid = node.getAttribute("id")
-            if tid not in self.tids:
-                self.tids.append(tid)
-            task = task_factory_func(tid)
-            task = taskxml.task_from_xml(task,node)
-            push_task_func(task)
-        #print "#### finishing pushing tasks"
-
-    def set_task(self, task):
-        ''' Save the task in the backend '''
-        #time.sleep(4)
-        tid = task.get_id()
-        if tid not in self.tids:
-            self.tids.append(tid)
-        existing = None
-        #First, we find the existing task from the treenode
-        for node in self.xmlproj.childNodes:
-            if node.getAttribute("id") == tid:
-                existing = node
-        t_xml = taskxml.task_to_xml(self.doc, task)
-        modified = False
-        #We then replace the existing node
-        if existing and t_xml:
-            #We will write only if the task has changed
-            if t_xml.toxml() != existing.toxml():
-                self.xmlproj.replaceChild(t_xml, existing)
-                modified = True
-        #If the node doesn't exist, we create it
-        # (it might not be the case in all backends
-        else:
-            self.xmlproj.appendChild(t_xml)
-            modified = True
-        #In this particular backend, we write all the tasks
-        #This is inherent to the XML file backend
-        if modified and self.zefile and self.doc :
-            cleanxml.savexml(self.zefile, self.doc)
-        return None
-
-    def remove_task(self, tid):
-        ''' Completely remove the task with ID = tid '''
-        for node in self.xmlproj.childNodes:
-            if node.getAttribute("id") == tid:
-                self.xmlproj.removeChild(node)
-                if tid in self.tids:
-                    self.tids.remove(tid)
-        cleanxml.savexml(self.zefile, self.doc)
-
-    def new_task_id(self):
-        '''
-        Returns an available ID for a new task so that a task with this ID
-        can be saved with set_task later.
-        If None, then GTG will create a new ID by itself.
-        The ID cannot contain the character "@".
-        '''
-        k = 0
-        pid = self.pid
-        newid = "%s@%s" %(k, pid)
-        while str(newid) in self.tids:
-            k += 1
-            newid = "%s@%s" %(k, pid)
-        self.tids.append(newid)
-        return newid
-
-    def quit(self):
-        '''
-        Called when GTG quits or disconnects the backend.
-        (Subclasses might pass here)
-        '''
-        cleanxml.savexml(self.zefile, self.doc, backup=True)

=== modified file 'GTG/core/__init__.py'
--- GTG/core/__init__.py	2010-05-26 09:54:42 +0000
+++ GTG/core/__init__.py	2010-06-23 01:19:23 +0000
@@ -41,136 +41,80 @@
 #=== IMPORT ====================================================================
 import os
 from xdg.BaseDirectory import xdg_data_home, xdg_config_home
-from GTG.tools         import cleanxml
 from configobj         import ConfigObj
-
-from GTG.core          import firstrun_tasks
-
-class CoreConfig:
+from GTG.tools.testingmode import TestingMode
+
+import GTG
+from GTG.tools.logger import Log
+from GTG.tools.borg   import Borg
+
+
+
+class CoreConfig(Borg):
     
+
     #The projects and tasks are of course DATA !
     #We then use XDG_DATA for them
     #Don't forget the "/" at the end.
-    DATA_DIR  = os.path.join(xdg_data_home,'gtg/')
     DATA_FILE = "projects.xml"
-    CONF_DIR = os.path.join(xdg_config_home,'gtg/')
     CONF_FILE = "gtg.conf"
     TASK_CONF_FILE = "tasks.conf"
     conf_dict = None
     #DBUS
     BUSNAME = "org.GTG"
     BUSINTERFACE = "/org/GTG"
+    #TAGS
+    ALLTASKS_TAG = "gtg-tags-all"
 
     def __init__(self):
-        if not os.path.exists(self.CONF_DIR):
-            os.makedirs(self.CONF_DIR)
-        if not os.path.exists(self.DATA_DIR):
-            os.makedirs(self.DATA_DIR)
-        if not os.path.exists(self.CONF_DIR + self.CONF_FILE):
-            f = open(self.CONF_DIR + self.CONF_FILE, "w")
-            f.close()
-        if not os.path.exists(self.CONF_DIR + self.TASK_CONF_FILE):
-            f = open(self.CONF_DIR + self.TASK_CONF_FILE, "w")
-            f.close()
-        for file in [self.CONF_DIR + self.CONF_FILE,
-                     self.CONF_DIR + self.TASK_CONF_FILE]:
+        if  hasattr(self, 'data_dir'):
+            #Borg has already been initialized
+            return
+        if TestingMode().get_testing_mode():
+            #we avoid running tests in the user data dir
+            self.data_dir = '/tmp/GTG_TESTS/data'
+            self.conf_dir = '/tmp/GTG_TESTS/conf'
+        else:
+            self.data_dir = os.path.join(xdg_data_home,'gtg/')
+            self.conf_dir = os.path.join(xdg_config_home,'gtg/')
+        if not os.path.exists(self.conf_dir):
+            os.makedirs(self.conf_dir)
+        if not os.path.exists(self.data_dir):
+            os.makedirs(self.data_dir)
+        if not os.path.exists(self.conf_dir + self.CONF_FILE):
+            f = open(self.conf_dir + self.CONF_FILE, "w")
+            f.close()
+        if not os.path.exists(self.conf_dir + self.TASK_CONF_FILE):
+            f = open(self.conf_dir + self.TASK_CONF_FILE, "w")
+            f.close()
+        for file in [self.conf_dir + self.CONF_FILE,
+                     self.conf_dir + self.TASK_CONF_FILE]:
             if not ((file, os.R_OK) and os.access(file, os.W_OK)):
                 raise Exception("File " + file + \
-                            " is a configuration file for gtg, but it " + \
+                            " is a configuration file for gtg, but it "
                             "cannot be read or written. Please check it")
-        self.conf_dict = ConfigObj(self.CONF_DIR + self.CONF_FILE)
-        self.task_conf_dict = ConfigObj(self.CONF_DIR + self.TASK_CONF_FILE)
+        self.conf_dict = ConfigObj(self.conf_dir + self.CONF_FILE)
+        self.task_conf_dict = ConfigObj(self.conf_dir + self.TASK_CONF_FILE)
     
-    def save_config(self):
+    def save(self):
+        ''' Saves the configuration of CoreConfig '''
         self.conf_dict.write()
         self.task_conf_dict.write()
-    
-    def get_backends_list(self):
-        backend_fn = []
-
-        # Check if config dir exists, if not create it
-        if not os.path.exists(self.DATA_DIR):
-            os.makedirs(self.DATA_DIR)
-
-        # Read configuration file, if it does not exist, create one
-        datafile = self.DATA_DIR + self.DATA_FILE
-        doc, configxml = cleanxml.openxmlfile(datafile,"config") #pylint: disable-msg=W0612
-        xmlproject = doc.getElementsByTagName("backend")
-        # collect configred backends
-        pid = 1
-        for xp in xmlproject:
-            dic = {}
-            #We have some retrocompatibility code
-            #A backend without the module attribute is pre-rev.105
-            #and is considered as "filename"
-            if xp.hasAttribute("module"):
-                dic["module"] = str(xp.getAttribute("module"))
-                dic["pid"] = str(xp.getAttribute("pid"))
-            #The following "else" could be removed later
-            else:
-                dic["module"] = "localfile"
-                dic["pid"] = str(pid)
-            
-            dic["xmlobject"] = xp
-            pid += 1
-            backend_fn.append(dic)
-                
-        firstrun = False
-        #If no backend available, we create a new using localfile
-        if len(backend_fn) == 0:
-            dic = {}
-            dic["module"] = "localfile"
-            dic["pid"] = "1"
-            backend_fn.append(dic)
-            firstrun = True
-            
-        #Now that the backend list is build, we will construct them
-        #Remember that b is a dictionnary
-        for b in backend_fn:
-            #We dynamically import modules needed
-            module_name = "GTG.backends.%s"%b["module"]
-            #FIXME : we should throw an error if the backend is not importable
-            module   = __import__(module_name)
-            module   = getattr(module, "backends")
-            classobj = getattr(module, b["module"])
-            b["parameters"] = classobj.get_parameters()
-            #If creating the default backend, we don't have the xmlobject yet
-            if "xmlobject" in b:
-                xp = b.pop("xmlobject")
-                #We will try to get the parameters
-                for key in b["parameters"]:
-                    if xp.hasAttribute(key):
-                        b[key] = str(xp.getAttribute(key))
-            if firstrun:
-                frx = firstrun_tasks.populate()
-                back = classobj.Backend(b,firstrunxml=frx)
-            else:
-                back = classobj.Backend(b)
-            #We put the backend itself in the dic
-            b["backend"] = back
-            
-        return backend_fn
-        
-
-    #If initial save, we don't close stuffs.
-    def save_datastore(self,ds,initial_save=False):
-        doc,xmlconfig = cleanxml.emptydoc("config")
-        for b in ds.get_all_backends():
-            param = b.get_parameters()
-            t_xml = doc.createElement("backend")
-            for key in param:
-                #We dont want parameters,backend,xmlobject
-                if key not in ["backend","parameters","xmlobject"]:
-                    t_xml.setAttribute(str(key),str(param[key]))
-            #Saving all the projects at close
-            xmlconfig.appendChild(t_xml)
-            if not initial_save:
-                b.quit()
-            
-        datafile = self.DATA_DIR + self.DATA_FILE
-        cleanxml.savexml(datafile,doc,backup=True)
-
-        #Saving the tagstore
-        if not initial_save:
-            ts = ds.get_tagstore()
-            ts.save()
+
+    def get_icons_directories(self):
+        '''
+        Returns the directories containing the icons
+        '''
+        return [GTG.DATA_DIR, os.path.join(GTG.DATA_DIR, "icons")]
+
+    def get_data_dir(self):
+        return self.data_dir
+
+    def set_data_dir(self, path):
+        self.data_dir = path
+
+    def get_conf_dir(self):
+        return self.conf_dir
+
+    def set_conf_dir(self, path):
+        self.conf_dir = path

=== modified file 'GTG/core/datastore.py'
--- GTG/core/datastore.py	2010-05-26 08:55:45 +0000
+++ GTG/core/datastore.py	2010-06-23 01:19:23 +0000
@@ -18,267 +18,560 @@
 # -----------------------------------------------------------------------------
 
 """
-datastore contains a list of TagSource objects, which are proxies between a backend and the datastore itself
+The DaataStore contains a list of TagSource objects, which are proxies
+between a backend and the datastore itself
 """
 
 import threading
-import gobject
-import time
-
-from GTG.core      import tagstore, requester
-from GTG.core.task import Task
-from GTG.core.tree import Tree
-
-
-#Only the datastore should access to the backend
-DEFAULT_BACKEND = "1"
-#If you want to debug a backend, it can be useful to disable the threads
-#Currently, it's python threads (and not idle_add, which is not useful)
-THREADING = True
-
-
-class DataStore:
-    """ A wrapper around a backend that provides an API for adding/removing tasks """
+import uuid
+import os.path
+from collections import deque
+
+from GTG.core                    import tagstore, requester
+from GTG.core.task               import Task
+from GTG.core.tree               import Tree
+from GTG.core                    import CoreConfig
+from GTG.tools.logger            import Log
+from GTG.backends.genericbackend import GenericBackend
+from GTG.tools                   import cleanxml
+from GTG.tools.keyring           import Keyring
+from GTG.backends.backendsignals import BackendSignals
+from GTG.tools.synchronized      import synchronized
+from GTG.tools.borg              import Borg
+
+
+class DataStore(object):
+    '''
+    A wrapper around all backends that is responsible for keeping the backend
+    instances. It can enable, disable, register and destroy backends, and acts
+    as interface between the backends and GTG core.
+    You should not interface yourself directly with the DataStore: use the
+    Requester instead (which also sends signals as you issue commands).
+    '''
+
+
     def __init__(self):
-        """ Initializes a DataStore object """
-        self.backends = {}
+        '''
+        Initializes a DataStore object
+        '''
+        self.backends = {} #dictionary {backend_name_string: Backend instance}
         self.open_tasks = Tree()
-#        self.closed_tasks = Tree()
         self.requester = requester.Requester(self)
         self.tagstore = tagstore.TagStore(self.requester)
-
-    def all_tasks(self):
-        """
+        self._backend_signals = BackendSignals()
+        self.mutex = threading.RLock()
+        self.is_default_backend_loaded = False
+        self._backend_signals.connect('default-backend-loaded', \
+                                      self._activate_non_default_backends)
+        self.filtered_datastore = FilteredDataStore(self)
+
+    ##########################################################################
+    ### Helper functions (get_ methods for Datastore embedded objects)
+    ##########################################################################
+
+    def get_tagstore(self):
+        '''
+        Helper function to obtain the Tagstore associated with this DataStore
+        @return GTG.core.tagstore.TagStore: the tagstore object
+        '''
+        return self.tagstore
+
+    def get_requester(self):
+        '''
+        Helper function to get the Requester associate with this DataStore
+        @returns GTG.core.requester.Requester: the requester associated with
+        this datastore
+        '''
+        return self.requester
+        
+    def get_tasks_tree(self):
+        '''
+        Helper function to get a Tree with all the tasks contained in this
+        Datastore
+        @returns GTG.core.tree.Tree: a task tree (the main one)
+        '''
+        return self.open_tasks
+
+    ##########################################################################
+    ### Tasks functions
+    ##########################################################################
+
+    def get_all_tasks(self):
+        '''
         Returns list of all keys of open tasks
-        """
+        @return a list of strings: a list of task ids
+        '''
         return self.open_tasks.get_all_keys()
 
     def has_task(self, tid):
-        """
+        '''
         Returns true if the tid is among the open or closed tasks for
         this DataStore, False otherwise.
-        param tid: Task ID to search for
-        """
-        return self.open_tasks.has_node(tid) #or self.closed_tasks.has_node(tid)
+        @param tid: Task ID to search for
+        @return bool: True if the task is present
+        '''
+        return self.open_tasks.has_node(tid)
 
     def get_task(self, tid):
-        """
+        '''
         Returns the internal task object for the given tid, or None if the
         tid is not present in this DataStore.
         @param tid: Task ID to retrieve
-        """
-        if tid:
-            if self.has_task(tid):
-                task = self.__internal_get_task(tid)
-            else:
-                #print "no task %s" %tid
-                task = None
-            return task
+        @returns GTG.core.task.Task or None:  whether the Task is present
+        or not
+        '''
+        if self.has_task(tid):
+            return self.open_tasks.get_node(tid)
         else:
-            print "get_task should take a tid"
+            Log.debug("requested non-existent task")
             return None
         
-    def __internal_get_task(self, tid):
-        return self.open_tasks.get_node(tid)
-#        if toreturn == None:
-#            self.closed_tasks.get_node(tid)
-        #else:
-            #print "error : this task doesn't exist in either tree"
-            #pass
-        #we return None if the task doesn't exist
-#        return toreturn
-
-    def delete_task(self, tid):
-        """
-        Deletes the given task entirely from this DataStore, and unlinks
-        it from the task's parent.
-        @return: True if task was deleted, or False if the tid was not
-         present in this DataStore.
-        """
-        if not tid or not self.has_task(tid):
-            return False
-
-        self.__internal_get_task(tid).delete()
-        uid, pid = tid.split('@') #pylint: disable-msg=W0612
-        back = self.backends[pid]
-        #Check that the task still exist. It might have been deleted
-        #by its parent a few line earlier :
-        if self.has_task(tid):
-            self.open_tasks.remove_node(tid)
-#            self.closed_tasks.remove_node(tid)
-        back.remove_task(tid)
-        return True
-            
-    def new_task(self,pid=None):
+    def task_factory(self, tid, newtask = False):
+        '''
+        Instantiates the given task id as a Task object.
+        @param tid: a task id. Must be unique
+        @param newtask: True if the task has never been seen before
+        @return Task: a Task instance
+        '''
+        return Task(tid, self.requester, newtask)
+
+    def new_task(self):
         """
         Creates a blank new task in this DataStore.
-        @param pid: (Optional) parent ID that this task should be a child of.
-         If not specified, the task will be a child of the default backend.
+        New task is created in all the backends that collect all tasks (among
+        them, the default backend). The default backend uses the same task id
+        in its own internal representation.
         @return: The task object that was created.
         """
-        if not pid:
-            pid = DEFAULT_BACKEND
-        newtid = self.backends[pid].new_task_id()
-        while self.has_task(newtid):
-            print "error : tid already exists"
-            newtid = self.backends[pid].new_task_id()
-        task = Task(newtid, self.requester,newtask=True)
+        task = self.task_factory(uuid.uuid4(), True)
         self.open_tasks.add_node(task)
-        task.set_sync_func(self.backends[pid].set_task,callsync=False)
         return task
-
-    def get_tagstore(self):
-        return self.tagstore
-
-    def get_requester(self):
-        return self.requester
-        
-    def get_tasks_tree(self):
-        """ return: Open tasks tree """
-        return self.open_tasks
-        
-    def push_task(self,task):
-        """
-        Adds the given task object as a node to the open tasks tree.
-        @param task: A valid task object
-        """
-        tid = task.get_id()
-        if self.has_task(tid):
-            print "pushing an existing task. We should care about modifications"
+        
+    @synchronized
+    def push_task(self, task, backend_capabilities = 'bypass for now'):
+        '''
+        Adds the given task object to the task tree. In other words, registers
+        the given task in the GTG task set.
+        @param task: A valid task object  (a GTG.core.task.Task)
+        @return bool: True if the task has been accepted
+        '''
+
+        if self.has_task(task.get_id()):
+            return False
         else:
-            uid, pid = tid.split('@')
             self.open_tasks.add_node(task)
             task.set_loaded()
-            task.set_sync_func(self.backends[pid].set_task,callsync=False)
-    
-    def task_factory(self,tid):
-        """
-        Instantiates the given task id as a Task object.
-        @param tid: The id of the task to instantiate
-        @return: The task object instantiated for tid
-        """
-        task = None
-        if self.has_task(tid):
-            print "error : tid already exists"
+            if self.is_default_backend_loaded:
+                task.sync()
+            return True
+
+    ##########################################################################
+    ### Backends functions
+    ##########################################################################
+
+    def get_all_backends(self, disabled = False):
+        """ 
+        returns list of all registered backends for this DataStore.
+        @param disabled: If disabled is True, attaches also the list of disabled backends
+        @return list: a list of TaskSource objects
+        """
+        #NOTE: consider cashing this result for speed.
+        result = []
+        for backend in self.backends.itervalues():
+            if backend.is_enabled() or disabled:
+                result.append(backend)
+        return result
+
+    def get_backend(self, backend_id):
+        '''
+        Returns a backend given its id
+        @param backend_id: a backend id
+        @returns GTG.core.datastore.TaskSource or None: the requested backend,
+        or none
+        '''
+        if backend_id in self.backends:
+            return self.backends[backend_id]
         else:
-            task = Task(tid, self.requester, newtask=False)
-        return task
-            
+            return None
 
-    def register_backend(self, dic):
+    def register_backend(self, backend_dic):
         """
         Registers a TaskSource as a backend for this DataStore
-        @param dic: Dictionary object with a "backend" and "pid"
-         specified.  dic["pid"] should be the parent ID to use
-         with the backend specified in dic["backend"].
+        @param backend_dic: Dictionary object containing all the
+        parameters to initialize the backend (filename...). It should
+        also contain the backend class (under "backend"), and its unique
+        id (under "pid")
         """
-        if "backend" in dic:
-            pid = dic["pid"]
-            backend = dic["backend"]
-            source = TaskSource(backend, dic)
-            self.backends[pid] = source
-            #Filling the backend
-            #Doing this at start is more efficient than
-            #after the GUI is launched
-            source.start_get_tasks(self.push_task,self.task_factory)
+        if "backend" in backend_dic:
+            if "pid" not in backend_dic:
+                Log.debug("registering a backend without pid.")
+                return None
+            backend = backend_dic["backend"]
+            #Checking that is a new backend
+            if backend.get_id() in self.backends:
+                Log.debug("registering already registered backend")
+                return None
+            source = TaskSource(requester = self.requester,
+                                backend = backend,
+                                datastore = self.filtered_datastore)
+            self.backends[backend.get_id()] = source
+            #we notify that a new backend is present
+            self._backend_signals.backend_added(backend.get_id())
+            #saving the backend in the correct dictionary (backends for enabled
+            # backends, disabled_backends for the disabled ones)
+            #this is useful for retro-compatibility 
+            if not GenericBackend.KEY_ENABLED in backend_dic:
+                source.set_parameter(GenericBackend.KEY_ENABLED, True)
+            if not GenericBackend.KEY_DEFAULT_BACKEND in backend_dic:
+                source.set_parameter(GenericBackend.KEY_DEFAULT_BACKEND, True)
+            #if it's enabled, we initialize it
+            if source.is_enabled() and \
+               (self.is_default_backend_loaded or source.is_default()):
+                source.initialize(connect_signals = False)
+                #Filling the backend
+                #Doing this at start is more efficient than
+                #after the GUI is launched
+                source.start_get_tasks()
+            return source
         else:
-            print "Register a dic without backend key:  BUG"
-
-    def unregister_backend(self, backend):
-        """ Unimplemented """
-        print "unregister backend %s not implemented" %backend
-
-    def get_all_backends(self):
-        """ returns list of all registered backends for this DataStore """
-        l = []
-        for key in self.backends:
-            l.append(self.backends[key])
-        return l
+            Log.debug("Tried to register a backend without a  pid")
+
+    def _activate_non_default_backends(self, sender = None):
+        '''
+        Non-default backends have to wait until the default loads before
+        being  activated. This function is called after the first default
+        backend has loaded all its tasks.
+        '''
+        if self.is_default_backend_loaded:
+            Log.debug("spurious call")
+            return
+        self.is_default_backend_loaded = True
+        for backend in self.backends.itervalues():
+            if backend.is_enabled() and not backend.is_default():
+                backend.initialize()
+                backend.start_get_tasks()
+                self.flush_all_tasks(backend.get_id())
+
+    def set_backend_enabled(self, backend_id, state):
+        """
+        The backend corresponding to backend_id is enabled or disabled
+        according to "state".
+        Disable:
+        Quits a backend and disables it (which means it won't be
+        automatically loaded next time GTG is started)
+        Enable:
+        Reloads a disabled backend. Backend must be already known by the
+        Datastore
+        @parma backend_id: a backend id
+        @param state: True to enable, False to disable
+        """
+        if backend_id in self.backends:
+            backend = self.backends[backend_id]
+            current_state = backend.is_enabled()
+            if current_state == True and state == False:
+                #we disable the backend
+                backend.quit(disable = True)
+            elif current_state == False and state == True:
+                if self.is_default_backend_loaded == True:
+                    backend.initialize()
+                    self.flush_all_tasks(backend_id)
+                else:
+                    #will be activated afterwards
+                    backend.set_parameter(GenericBackend.KEY_ENABLED,
+                                       True)
+
+    def remove_backend(self, backend_id):
+        '''
+        Removes a backend, and forgets it ever existed.
+        @param backend_id: a backend id
+        '''
+        if backend_id in self.backends:
+            backend = self.backends[backend_id]
+            if backend.is_enabled():
+                self.set_backend_enabled(backend_id, False)
+            backend.purge()
+            #we notify that the backend has been deleted
+            self._backend_signals.backend_removed(backend.get_id())
+            del self.backends[backend_id]
+
+    def backend_change_attached_tags(self, backend_id, tag_names):
+        '''
+        Changes the tags for which a backend should store a task
+        @param backend_id: a backend_id
+        @param tag_names: the new set of tags. This should not be a tag object,
+                          just the tag name.
+        '''
+        backend = self.backends[backend_id]
+        backend.set_attached_tags(tag_names)
+
+    def flush_all_tasks(self, backend_id):
+        '''
+        This function will cause all tasks to be checked against the backend
+        identified with backend_id. If tasks need to be added or removed, it
+        will be done here.
+        It has to be run after the creation of a new backend (or an alteration
+        of its "attached tags"), so that the tasks which are already loaded in 
+        the Tree will be saved in the proper backends
+        @param backend_id: a backend id
+        '''
+        def _internal_flush_all_tasks():
+            backend = self.backends[backend_id]
+            for task_id in self.requester.get_all_tasks_list():
+                backend.queue_set_task(None, task_id)
+        t = threading.Thread(target = _internal_flush_all_tasks).start()
+        self.backends[backend_id].start_get_tasks()
+
+    def save(self, quit = False):
+        '''
+        Saves the backends parameters. 
+        @param quit: If quit is true, backends are shut down
+        '''
+        doc,xmlconfig = cleanxml.emptydoc("config")
+        #we ask all the backends to quit first.
+        if quit:
+            for b in self.get_all_backends():
+                #NOTE:we could do this in parallel. Maybe a quit and
+                #has_quit would be faster (invernizzi)
+                b.quit()
+        #we save the parameters
+        for b in self.get_all_backends(disabled = True):
+            t_xml = doc.createElement("backend")
+            for key, value in b.get_parameters().iteritems():
+                if key in ["backend", "xmlobject"]:
+                    #We don't want parameters,backend,xmlobject
+                    continue
+                param_type = b.get_parameter_type(key)
+                value = b.cast_param_type_to_string(param_type, value)
+                t_xml.setAttribute(str(key), value)
+            #Saving all the projects at close
+            xmlconfig.appendChild(t_xml)
+            
+        datafile = os.path.join(CoreConfig().get_data_dir(), CoreConfig.DATA_FILE)
+        cleanxml.savexml(datafile,doc,backup=True)
+
+        #Saving the tagstore
+        ts = self.get_tagstore()
+        ts.save()
+
+    def request_task_deletion(self, tid):
+        ''' 
+        This is a proxy function to request a task deletion from a backend
+        @param tid: the tid of the task to remove
+        '''
+        self.requester.delete_task(tid)
+
 
 class TaskSource():
-    """ transparent interface between the real backend and the datastore """
-    def __init__(self, backend, parameters):
+    '''
+    Transparent interface between the real backend and the DataStore.
+    Is in charge of connecting and disconnecting to signals
+    '''
+    def __init__(self, requester, backend, datastore):
         """
         Instantiates a TaskSource object.
-        @param backend: (Required) Task Backend being wrapperized
-        @param parameters: Dictionary of custom parameters.
+        @param requester: a Requester
+        @param backend:  the backend being wrapped
+        @param datastore: a FilteredDatastore
         """
         self.backend = backend
-        self.dic = parameters
-        self.to_set = []
-        self.to_remove = []
-        self.lock = threading.Lock()
-        self.count_set = 0
+        self.req = requester
+        self.backend.register_datastore(datastore)
+        self.to_set = deque()
+        self.to_remove = deque()
+        self.task_filter = self.get_task_filter_for_backend()
+        if Log.is_debugging_mode():
+            self.timer_timestep = 5
+        else:
+            self.timer_timestep = 1 
+        self.set_task_handle = None
+        self.remove_task_handle = None
+        self.to_set_timer = None
         
-    def start_get_tasks(self,push_task,task_factory):
-        """
+    def start_get_tasks(self):
+        ''''
         Maps the TaskSource to the backend and starts threading.
-        This must be called before the DataStore is usable.
-        """
-        func = self.backend.start_get_tasks
-        t = threading.Thread(target=func,args=(push_task,task_factory))
-        t.start()
-    
-    def set_task(self, task):
+        '''
+        threading.Thread(target = self.__start_get_tasks).start()
+
+    def __start_get_tasks(self):
+        '''
+        Loads all task from the backend and connects its signals afterwards.
+        Launched as a thread by start_get_tasks
+        '''
+        self.backend.start_get_tasks()
+        self._connect_signals()
+        if self.backend.is_default():
+            BackendSignals().default_backend_loaded()
+
+    def get_task_filter_for_backend(self):
+        '''
+        Fiter that checks if the task should be stored in this backend.
+
+        @returns function: a function that accepts a task and returns True/False
+                 whether the task should be stored or not
+        '''
+        raw_filter = self.req.get_filter("backend_filter").get_function()
+        return lambda task: raw_filter(task, \
+                              set(self.backend.get_attached_tags()))
+
+    def should_task_id_be_stored(self, task_id):
+        '''
+        Helper function:  Checks if a task should be stored in this backend
+        @param task_id: a task id
+        @returns bool: True if the task should be stored
+        '''
+        task = self.req.get_task(task_id)
+        return self.task_filter(task)
+
+    def queue_set_task(self, sender, tid):
         """
         Updates the task in the DataStore.  Actually, it adds the task to a
         queue to be updated asynchronously.
+        @param sender: not used, any value will do.
         @param task: The Task object to be updated.
         """
-        tid = task.get_id()
-        if task not in self.to_set and tid not in self.to_remove:
-            self.to_set.append(task)
-        if self.lock.acquire(False):
-            func = self.setting_thread
-            t = threading.Thread(target=func)
-            t.start()
-#        else:
-#            print "cannot acquire lock : not a problem, just for debug purpose"
+        if self.should_task_id_be_stored(tid):
+            if tid not in self.to_set and tid not in self.to_remove:
+                self.to_set.appendleft(tid)
+                self.__try_launch_setting_thread()
+        else:
+            self.queue_remove_task(None, tid)
             
-    def setting_thread(self):
-        """
+    def launch_setting_thread(self):
+        '''
         Operates the threads to set and remove tasks.
         Releases the lock when it is done.
-        """
-        try:
-            while len(self.to_set) > 0:
-                t = self.to_set.pop(0)
-                tid = t.get_id()
-                if tid not in self.to_remove:
-                    self.count_set += 1
-                    #print "saving task %s (%s saves)" %(tid,self.count_set)
-                    self.backend.set_task(t)
-            while len(self.to_remove) > 0:
-                tid = self.to_remove.pop(0)
-                self.backend.remove_task(tid)
-        finally:
-            self.lock.release()
+        '''
+        #FIXME: the lock should be general for all backends. Therefore, it
+        #should be handled in the datastore
+        while True:
+            try:
+                tid = self.to_set.pop()
+            except IndexError:
+                break
+            #we check that the task is not already marked for deletion
+            #and that it's still to be stored in this backend
+            #NOTE: no need to lock, we're reading
+            if tid not in self.to_remove and \
+                    self.should_task_id_be_stored(tid) and \
+                   self.req.has_task(tid):
+                task = self.req.get_task(tid)
+                self.backend.queue_set_task(task)
+        while True:
+            try:
+                tid = self.to_remove.pop()
+            except IndexError:
+                break
+            self.backend.queue_remove_task(tid)
+        #we release the weak lock
+        self.to_set_timer = None
     
-    def remove_task(self, tid):
-        """
+    def queue_remove_task(self, sender, tid):
+        '''
         Queues task to be removed.
+        @param sender: not used, any value will do
         @param tid: The Task ID of the task to be removed
-        """
+        '''
         if tid not in self.to_remove:
-            self.to_remove.append(tid)
-        if self.lock.acquire(False):
-            func = self.setting_thread
-            t = threading.Thread(target=func)
-            t.start()
-    
-    def new_task_id(self):
-        """
-        returns a new ID created by the backend.
-        """
-        return self.backend.new_task_id()
-    
-    def quit(self):
-        """ Quits the backend """
-        self.backend.quit()
-        
-    #Those functions are only for TaskSource
-    def get_parameters(self):
-        """
-        Returns the parameters specified during creation of the DataStore
-        """
-        return self.dic
+            self.to_remove.appendleft(tid)
+            self.__try_launch_setting_thread()
+
+    def __try_launch_setting_thread(self):
+        '''
+        Helper function to launch the setting thread, if it's not running
+        '''
+        if self.to_set_timer == None:
+            self.to_set_timer = threading.Timer(self.timer_timestep, \
+                                        self.launch_setting_thread)
+            self.to_set_timer.start()
+
+    def initialize(self, connect_signals = True):
+        '''
+        Initializes the backend and starts looking for signals.
+        @param connect_signals: if True, it starts listening for signals
+        '''
+        self.backend.initialize()
+        if connect_signals:
+            self._connect_signals()
+
+    def _connect_signals(self):
+        '''
+        Helper function to connect signals
+        '''
+        if not self.set_task_handle:
+            self.set_task_handle = self.req.connect('task-modified', \
+                                                    self.queue_set_task)
+        if not self.remove_task_handle:
+            self.remove_task_handle = self.req.connect('task-deleted',\
+                                                   self.queue_remove_task)
+
+    def _disconnect_signals(self):
+        '''
+        Helper function to disconnect signals
+        '''
+        if self.set_task_handle:
+            self.req.disconnect(self.set_task_handle)
+            self.set_task_handle = None
+        if  self.remove_task_handle:
+            self.req.disconnect(self.remove_task_handle)
+            self.remove_task_handle = None
+
+    def sync(self):
+        '''
+        Forces the TaskSource to sync all the pending tasks
+        '''
+        if self.to_set_timer != None:
+            try:
+                self.to_set_timer.cancel()
+            except:
+                pass
+            try:
+                self.to_set_timer.join(5)
+            except:
+                pass
+        self.launch_setting_thread()
+
+    def quit(self, disable = False):
+        '''
+        Quits the backend and disconnect the signals
+        @param disable: if True, the backend is disabled.
+        '''
+        self._disconnect_signals()
+        self.sync()
+        self.backend.quit(disable)
+    
+    def __getattr__(self, attr):
+        '''
+        Delegates all the functions not defined here to the real backend
+        (standard python function)
+        @param attr: attribute to get
+        '''
+        if attr in self.__dict__: 
+            return self.__dict__[attr]
+        else:
+            return getattr(self.backend, attr)
+
+
+
+class FilteredDataStore(Borg):
+    ''' 
+    This class acts as an interface to the Datastore.
+    It is used to hide most of the methods of the Datastore.
+    The backends can safely use the remaining methods.
+    '''
+
+
+    def __init__(self, datastore):
+        super(FilteredDataStore, self).__init__()
+        self.datastore = datastore
+
+    def __getattr__(self, attr):
+        if attr in ['task_factory', \
+                    'push_task',
+                    'get_task',
+                    'has_task',
+                    'request_task_deletion']:
+            return getattr(self.datastore, attr)
+        else:
+            raise AttributeError
+

=== modified file 'GTG/core/filters_bank.py'
--- GTG/core/filters_bank.py	2010-06-14 19:30:50 +0000
+++ GTG/core/filters_bank.py	2010-06-23 01:19:23 +0000
@@ -35,6 +35,10 @@
 
     def set_parameters(self,dic):
         self.dic = dic
+
+    def get_function(self):
+        '''Returns the filtering function'''
+        return self.func
     
     def is_displayed(self,tid):
         task = self.req.get_task(tid)
@@ -152,6 +156,9 @@
         #worklate
         filt_obj = Filter(self.worklate,self.req)
         self.available_filters['worklate'] = filt_obj
+        #backend filter
+        filt_obj = Filter(self.backend_filter, self.req)
+        self.available_filters['backend_filter'] = filt_obj
         #no_disabled_tag
         filt_obj = Filter(self.no_disabled_tag,self.req)
         param = {}
@@ -234,6 +241,19 @@
         """ Filter of tasks which are closed """
         ret = task.get_status() in [Task.STA_DISMISSED, Task.STA_DONE]
         return ret
+
+    def backend_filter(self, task, tags_to_match_set):
+        '''
+        Filter that checks if two tags sets intersect. It is used to check if a
+        task should be stored inside a backend
+        @param task: a task object
+        @oaram tags_to_match_set: a *set* of tag names
+        '''
+        all_tasks_tag = self.req.get_alltag_tag().get_name()
+        if all_tasks_tag in tags_to_match_set:
+            return True
+        task_tags = set(task.get_tags_name())
+        return task_tags.intersection(tags_to_match_set)
         
     def no_disabled_tag(self,task,parameters=None):
         """Filter of task that don't have any disabled/nonworkview tag"""

=== modified file 'GTG/core/requester.py'
--- GTG/core/requester.py	2010-06-21 12:34:23 +0000
+++ GTG/core/requester.py	2010-06-23 01:19:23 +0000
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
 # Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
 #
 # This program is free software: you can redistribute it and/or modify it under
@@ -39,18 +39,18 @@
     Multiple L{Requester}s can exist on the same datastore, so they should
     never have state of their own.
     """
-    __gsignals__ = {'task-added': (gobject.SIGNAL_RUN_FIRST, \
-                                    gobject.TYPE_NONE, (str, )),
-                    'task-deleted': (gobject.SIGNAL_RUN_FIRST, \
-                                    gobject.TYPE_NONE, (str, )),
-                    'task-modified': (gobject.SIGNAL_RUN_FIRST, \
-                                    gobject.TYPE_NONE, (str, )),
-                    'tag-added': (gobject.SIGNAL_RUN_FIRST, \
-                                    gobject.TYPE_NONE, (str, )),
-                    'tag-deleted': (gobject.SIGNAL_RUN_FIRST, \
-                                    gobject.TYPE_NONE, (str, )),
-                    'tag-modified': (gobject.SIGNAL_RUN_FIRST, \
-                                    gobject.TYPE_NONE, (str, ))}
+
+    __string_signal__ = (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (str, ))
+
+    __gsignals__ = {'task-added' : __string_signal__, \
+              'task-deleted'     : __string_signal__, \
+              'task-modified'    : __string_signal__, \
+              'task-tagged'      : __string_signal__, \
+              'task-untagged'    : __string_signal__, \
+              'tag-added'        : __string_signal__, \
+              'tag-deleted'      : __string_signal__, \
+              'tag-path-deleted' : __string_signal__, \
+              'tag-modified'     : __string_signal__}
 
     def __init__(self, datastore):
         """Construct a L{Requester}."""
@@ -72,12 +72,19 @@
         self.counter_call += 1
         #print "signal task_modified %s (%s modifications)" %(tid,self.counter_call)
         gobject.idle_add(self.emit, "task-modified", tid)
-        
+
+    def _task_deleted(self, tid):
+        #when this is emitted, task has *already* been deleted
+        gobject.idle_add(self.emit, "task-deleted", tid)
+
     def _tag_added(self,tagname):
         gobject.idle_add(self.emit, "tag-added", tagname)
 
     def _tag_modified(self,tagname):
         gobject.idle_add(self.emit, "tag-modified", tagname)
+
+    def _tag_path_deleted(self, path):
+        gobject.idle_add(self.emit, "tag-path-deleted", path)
         
     def _tag_deleted(self,tagname):
         gobject.idle_add(self.emit, "tag-deleted", tagname)
@@ -126,6 +133,7 @@
 
     ######### Filters bank #######################
     # Get the filter object for a given name
+
     def get_filter(self,filter_name):
         return self.filters.get_filter(filter_name)
     
@@ -162,7 +170,7 @@
         task = self.ds.get_task(tid)
         return task
 
-    def new_task(self, pid=None, tags=None, newtask=True):
+    def new_task(self, tags=None, newtask=True):
         """Create a new task.
 
         Note: this modifies the datastore.
@@ -175,11 +183,12 @@
             existed, C{False} if importing an existing task from a backend.
         @return: A task from the data store
         """
-        task = self.ds.new_task(pid=pid)
+        task = self.ds.new_task()
         if tags:
             for t in tags:
                 assert(isinstance(t, Tag) == False)
                 task.tag_added(t)
+        self._task_loaded(task.get_id())
         return task
 
     def delete_task(self, tid):
@@ -196,11 +205,11 @@
             for tag in task.get_tags():
                 self.emit('tag-modified', tag.get_name())
         self.emit('task-deleted', tid)
-        #return True
-        return self.ds.delete_task(tid)
+        return self.basetree.remove_node(tid)
 
     ############### Tags ##########################
     ###############################################
+
     def get_tag_tree(self):
         return self.ds.get_tagstore()
 
@@ -251,3 +260,27 @@
                 l.append(t.get_name())
         l.sort(cmp=lambda x, y: cmp(x.lower(),y.lower()))
         return l
+
+    ############## Backends #######################
+    ###############################################
+
+    def get_all_backends(self, disabled = False):
+        return self.ds.get_all_backends(disabled)
+
+    def register_backend(self, dic):
+        return self.ds.register_backend(dic)
+
+    def flush_all_tasks(self, backend_id):
+        return self.ds.flush_all_tasks(backend_id)
+
+    def get_backend(self, backend_id):
+        return self.ds.get_backend(backend_id)
+
+    def set_backend_enabled(self, backend_id, state):
+        return self.ds.set_backend_enabled(backend_id, state)
+
+    def remove_backend(self, backend_id):
+        return self.ds.remove_backend(backend_id)
+
+    def backend_change_attached_tags(self, backend_id, tags):
+        return self.ds.backend_change_attached_tags(backend_id, tags)

=== modified file 'GTG/core/tagstore.py'
--- GTG/core/tagstore.py	2010-06-12 13:31:18 +0000
+++ GTG/core/tagstore.py	2010-06-23 01:19:23 +0000
@@ -40,6 +40,7 @@
 # There's only one Tag store by user. It will store all the tag used
 # and their attribute.
 class TagStore(Tree):
+
     
     def __init__(self,requester):
         Tree.__init__(self)
@@ -50,7 +51,7 @@
         
         ### building the initial tags
         # Build the "all tasks tag"
-        self.alltag_tag = self.new_tag("gtg-tags-all")
+        self.alltag_tag = self.new_tag(CoreConfig.ALLTASKS_TAG)
         self.alltag_tag.set_attribute("special","all")
         self.alltag_tag.set_attribute("label","<span weight='bold'>%s</span>"\
                                              % _("All tasks"))
@@ -68,7 +69,7 @@
         self.sep_tag.set_attribute("special","sep")
         self.sep_tag.set_attribute("order",2)
 
-        self.filename = os.path.join(CoreConfig.DATA_DIR, XMLFILE)
+        self.filename = os.path.join(CoreConfig().get_data_dir(), XMLFILE)
         doc, self.xmlstore = cleanxml.openxmlfile(self.filename,
             XMLROOT) #pylint: disable-msg=W0612
         for t in self.xmlstore.childNodes:
@@ -121,7 +122,7 @@
         if tagname[0] != "@":
             tagname = "@" + tagname
         return self.get_node(tagname)
-        
+
     #FIXME : also add a new filter
     def rename_tag(self, oldname, newname):
         if len(newname) > 0 and \
@@ -316,9 +317,12 @@
     def add_task(self, tid):
         if tid not in self.tasks:
             self.tasks.append(tid)      
-    def remove_task(self,tid):
+
+    def remove_task(self, tid):
         if tid in self.tasks:
             self.tasks.remove(tid)          
+            self.req._tag_modified(self.get_name())
+
     def get_tasks(self):
         #return a copy of the list
         toreturn = self.tasks[:]

=== modified file 'GTG/core/task.py'
--- GTG/core/task.py	2010-06-18 16:36:17 +0000
+++ GTG/core/task.py	2010-06-23 01:19:23 +0000
@@ -48,10 +48,10 @@
         #tid is a string ! (we have to choose a type and stick to it)
         self.tid = str(ze_id)
         self.set_uuid(uuid.uuid4())
+        self.remote_ids = {}
         self.content = ""
         #self.content = \
         #    "<content>Press Escape or close this task to save it</content>"
-        self.sync_func = None
         self.title = _("My new task")
         #available status are: Active - Done - Dismiss - Note
         self.status = self.STA_ACTIVE
@@ -78,7 +78,6 @@
             self.loaded = True
             if signal:
                 self.req._task_loaded(self.tid)
-                #not sure the following is necessary
                 #self.req._task_modified(self.tid)
 
     def set_to_keep(self):
@@ -102,6 +101,25 @@
             self.sync()
         return self.uuid
 
+    def get_remote_ids(self):
+        '''
+        A task usually has a different id in all the different backends.
+        This function returns a dictionary backend_id->the id the task has
+        in that backend
+        @returns dict: dictionary backend_id->task remote id
+        '''
+        return self.remote_ids
+
+    def add_remote_id(self, backend_id, task_remote_id):
+        '''
+        A task usually has a different id in all the different backends.
+        This function adds a relationship backend_id-> remote_id that can be
+        retrieved using get_remote_ids
+        @param backend_id: string representing the backend id
+        @param task_remote_id: the id for this task in the backend backend_id
+        '''
+        self.remote_ids[str(backend_id)] = str(task_remote_id)
+
     def get_title(self):
         return self.title
 
@@ -115,7 +133,7 @@
             self.title = title.strip('\t\n')
         else:
             self.title = "(no title task)"
-        #Avoid unecessary sync
+        #Avoid unnecessary sync
         if self.title != old_title:
             self.sync()
             return True
@@ -294,8 +312,7 @@
         """Add a newly created subtask to this task. Return the task added as
         a subtask
         """
-        uid, pid = self.get_id().split('@') #pylint: disable-msg=W0612
-        subt     = self.req.new_task(pid=pid, newtask=True)
+        subt     = self.req.new_task(newtask=True)
         #we use the inherited childrens
         self.add_child(subt.get_id())
         return subt
@@ -427,37 +444,32 @@
     #This method is called by the datastore and should not be called directly
     #Use the requester
     def delete(self):
-        self.set_sync_func(None, callsync=False)
+        #we issue a delete for all the children
         for task in self.get_subtasks():
-            task.remove_parent(self.get_id())
-            self.req.delete_task(task.get_id())
+            #I think it's superfluous (invernizzi)
+            #task.remove_parent(self.get_id())
+            task.delete()
+        #we tell the parents we have to go
         for i in self.get_parents():
             task = self.req.get_task(i)
             task.remove_child(self.get_id())
+        #we tell the tags about the deletion
         for tagname in self.tags:
             tag = self.req.get_tag(tagname)
             tag.remove_task(self.get_id())
-        #then we remove effectively the task
-        #self.req.delete_task(self.get_id())
-
-    #This is a callback. The "sync" function has to be set
-    def set_sync_func(self, sync, callsync=True):
-        self.sync_func = sync
-        #We call it immediatly to save stuffs that were set before this
-        if callsync and self.is_loaded():
-            self.sync()
+        #then we signal the we are ready to be removed
+        self.req._task_deleted(self.get_id())
 
     def sync(self):
         self._modified_update()
-        if self.sync_func and self.is_loaded():
-            self.sync_func(self)
+        if self.is_loaded():
             self.call_modified()
             return True
         else:
             return False
     
     #This function send the modified signals for the tasks, 
-    #parents and childrens       
+    #parents and children
     def call_modified(self):
         #we first modify children
         for s in self.get_children():
@@ -469,10 +481,11 @@
             self.req._task_modified(p)
 
     def _modified_update(self):
+        '''
+        Updates the modified timestamp
+        '''
         self.modified = datetime.now()
 
-
-
 ### TAG FUNCTIONS ############################################################
 #
     def get_tags_name(self):
@@ -509,6 +522,8 @@
         #Do not add the same tag twice
         if not t in self.tags:
             self.tags.append(t)
+            #we notify the backends
+            #self.req.tag_was_added_to_task(self, tagname)
             for child in self.get_subtasks():
                 if child.can_be_deleted:
                     child.add_tag(t)

=== modified file 'GTG/gtg.py'
--- GTG/gtg.py	2010-06-18 11:55:03 +0000
+++ GTG/gtg.py	2010-06-23 01:19:23 +0000
@@ -45,18 +45,17 @@
 """This is the top-level exec script for running GTG"""
 
 #=== IMPORT ===================================================================
-from contextlib import contextmanager
 import os
 import logging
-import signal
 
 import dbus
 
 #our own imports
-from GTG                import _, info
+from GTG.backends              import BackendFactory
+from GTG                import _
 from GTG.core           import CoreConfig
 from GTG.core.datastore import DataStore
-from GTG.gtk            import crashhandler
+from GTG.gtk.crashhandler import signal_catcher
 from GTG.gtk.manager    import Manager
 from GTG.tools.logger   import Log
 
@@ -93,54 +92,55 @@
 #=== MAIN CLASS ===============================================================
 
 def main(options=None, args=None):
+    '''
+    Calling this starts the full GTG experience  ( :-D )
+    '''
+    config, ds, req = core_main_init(options, args)
+    # Launch task browser
+    manager = Manager(req, config)
+    #main loop
+    #To be more user friendly and get the logs of crashes, we show an apport
+    # hooked window upon crashes
+    with signal_catcher(manager.close_browser):
+        manager.main()
+    core_main_quit(config, ds)
+
+def core_main_init(options = None, args = None):
+    ''' 
+    Part of the main function prior to the UI initialization.
+    '''
     # Debugging subsystem initialization
     if options.debug:
         Log.setLevel(logging.DEBUG)
         Log.debug("Debug output enabled.")
-    
+        Log.set_debugging_mode(True)
     config = CoreConfig()
-    check_instance(config.DATA_DIR)
-    backends_list = config.get_backends_list()
-
-    #initialize Apport hook for crash handling
-    crashhandler.initialize(app_name = "Getting Things GNOME!", message="GTG"
-      + info.VERSION + _(" has crashed. Please report the bug on <a href=\""
-      "http://bugs.edge.launchpad.net/gtg\";>our Launchpad page</a>. If you "
-      "have Apport installed, it will be started for you."), use_apport = True)
-    
+    check_instance(config.get_data_dir())
+    backends_list = BackendFactory().get_saved_backends_list()
     # Load data store
     ds = DataStore()
-    
+    # Register backends 
     for backend_dic in backends_list:
         ds.register_backend(backend_dic)
-    
     #save directly the backends to be sure to write projects.xml
-    config.save_datastore(ds,initial_save=True)
+    ds.save(quit = False)
         
     # Launch task browser
     req = ds.get_requester()
-    manager = Manager(req, config)
- 
-    #we listen for signals from the system in order to save our configuration
-    # if GTG is forcefully terminated (e.g.: on shutdown).
-    @contextmanager
-    def signal_catcher():
-        #if TERM or ABORT are caught, we close the browser
-        for s in [signal.SIGABRT, signal.SIGTERM]:
-            signal.signal(s, lambda a,b: manager.close_browser())
-        yield
+    return config, ds, req
 
-    #main loop
-    with signal_catcher():
-        manager.main()
-      
+def core_main_quit(config, ds):
+    '''
+    Last bits of code executed in GTG, after the UI has been shut off. 
+    Currently, it's just saving everything.
+    '''
     # Ideally we should load window geometry configuration from a config.
     # backend like gconf at some point, and restore the appearance of the
     # application as the user last exited it.
-
+    #
     # Ending the application: we save configuration
-    config.save_config()
-    config.save_datastore(ds)
+    config.save()
+    ds.save(quit = True)
 
 #=== EXECUTION ================================================================
 

=== modified file 'GTG/gtk/browser/browser.py'
--- GTG/gtk/browser/browser.py	2010-06-21 12:34:23 +0000
+++ GTG/gtk/browser/browser.py	2010-06-23 01:19:23 +0000
@@ -23,7 +23,6 @@
 #=== IMPORT ===================================================================
 #system imports
 import locale
-import os
 import re
 import time
 import webbrowser
@@ -35,18 +34,16 @@
 
 #our own imports
 import GTG
+from GTG.core                       import CoreConfig
 from GTG                         import _, info, ngettext
 from GTG.core.task               import Task
-#from GTG.core.tagstore           import Tag
 from GTG.gtk.browser             import GnomeConfig, tasktree, tagtree
-#from GTG.taskbrowser.preferences import PreferencesDialog
 from GTG.gtk.browser.tasktree    import TaskTreeModel,\
                                         ActiveTaskTreeView,\
                                         ClosedTaskTreeView
 from GTG.gtk.browser.tagtree     import TagTree
 from GTG.tools                   import openurl
-from GTG.tools.dates             import strtodate,\
-                                        no_date,\
+from GTG.tools.dates             import no_date,\
                                         FuzzyDate, \
                                         get_canonical_date
 from GTG.tools.logger            import Log
@@ -159,7 +156,7 @@
         self.priv['quick_add_cbs']            = []
 
     def _init_icon_theme(self):
-        icon_dirs = [GTG.DATA_DIR, os.path.join(GTG.DATA_DIR, "icons")]
+        icon_dirs = CoreConfig().get_icons_directories()
         for i in icon_dirs:
             gtk.icon_theme_get_default().prepend_search_path(i)
             gtk.window_set_default_icon_name("gtg")
@@ -429,7 +426,7 @@
 ### HELPER FUNCTIONS ########################################################
 
     def open_preferences(self,widget):
-        self.vmanager.show_preferences(self.priv)
+        self.vmanager.open_preferences(self.priv)
         
     def quit(self,widget=None):
         self.vmanager.close_browser()

=== modified file 'GTG/gtk/browser/tagtree.py'
--- GTG/gtk/browser/tagtree.py	2010-06-18 16:46:10 +0000
+++ GTG/gtk/browser/tagtree.py	2010-06-23 01:19:23 +0000
@@ -72,7 +72,8 @@
             task = self.req.get_task(tid)
             if task:
                 for tag in task.get_tags():
-                    self.tagrefresh(sender=sender,tagname=tag.get_name())
+                    if tag:
+                        self.tagrefresh(sender=sender,tagname=tag.get_name())
 
     def tagrefresh(self,sender=None,tagname=None):
         if tagname:

=== modified file 'GTG/gtk/crashhandler.py'
--- GTG/gtk/crashhandler.py	2010-06-07 21:14:45 +0000
+++ GTG/gtk/crashhandler.py	2010-06-23 01:19:23 +0000
@@ -33,6 +33,12 @@
 import sys
 import os
 import time
+import signal
+from contextlib import contextmanager
+
+from GTG import info
+
+
 try:
     import pygtk
     pygtk.require("2.0") # not tested on earlier versions
@@ -297,3 +303,21 @@
             return "gtkcrashhandler.py should imported, not run"
     raise DoNotRunException()
 
+
+## We handle initialization directly here, since this module will be used as a
+#  singleton
+        #we listen for signals from the system in order to save our configuration
+        # if GTG is forcefully terminated (e.g.: on shutdown).
+@contextmanager
+def signal_catcher(callback):
+    #if TERM or ABORT are caught, we execute the callback function
+    for s in [signal.SIGABRT, signal.SIGTERM]:
+        signal.signal(s, lambda a,b: callback())
+    yield
+
+initialize(app_name = "Getting Things GNOME!",
+           message  =  "GTG" + info.VERSION + 
+           _(" has crashed. Please report the bug on <a "\
+             "href=\"http://bugs.edge.launchpad.net/gtg\";>our Launchpad page</a>."\
+             " If you have Apport installed, it will be started for you."),       \
+          use_apport = True)

=== modified file 'GTG/gtk/delete_dialog.py'
--- GTG/gtk/delete_dialog.py	2010-06-23 00:38:13 +0000
+++ GTG/gtk/delete_dialog.py	2010-06-23 01:19:23 +0000
@@ -43,7 +43,11 @@
         """if we pass a tid as a parameter, we delete directly
         otherwise, we will look which tid is selected"""
         for tid in self.tids_todelete:
-            self.req.delete_task(tid)
+            task = self.req.get_task(tid)
+            if task:
+                task.delete()
+            else:
+                print "trying to delete task already deleted"
         self.tids_todelete = []
 
     def delete_tasks(self, tids=None):

=== modified file 'GTG/gtk/editor/editor.py'
--- GTG/gtk/editor/editor.py	2010-06-07 21:14:45 +0000
+++ GTG/gtk/editor/editor.py	2010-06-23 01:19:23 +0000
@@ -39,7 +39,7 @@
 from GTG                     import _
 from GTG                     import ngettext
 from GTG                     import PLUGIN_DIR
-from GTG                     import DATA_DIR
+from GTG.core                import CoreConfig
 from GTG.gtk.editor          import GnomeConfig
 from GTG.gtk.editor.taskview import TaskView
 from GTG.core.plugins.engine import PluginEngine
@@ -176,7 +176,7 @@
         self.pengine = PluginEngine(PLUGIN_DIR)
         self.te_plugin_api = PluginAPI(window = self.window,
                                        config = None,
-                                       data_dir = DATA_DIR,
+                                       data_dir = CoreConfig().get_data_dir(),
                                        builder = self.builder, 
                                        requester = self.req,
                                        tagpopup = None,

=== modified file 'GTG/gtk/manager.py'
--- GTG/gtk/manager.py	2010-06-10 14:45:36 +0000
+++ GTG/gtk/manager.py	2010-06-23 01:19:23 +0000
@@ -40,7 +40,11 @@
 from GTG.core.plugins.api    import PluginAPI
 from GTG.tools.logger        import Log
 
+
+
 class Manager:
+    
+
     ############## init #####################################################
     def __init__(self, req, config):
         self.config_obj = config
@@ -72,9 +76,9 @@
         #Deletion UI
         self.delete_dialog = None
         
-        #Preferences windows
-        # Initialize "Preferences" dialog
-        self.preferences = None
+        #Preferences and Backends windows
+        # Initialize  dialogs
+        self.preferences_dialog = None
         
         #DBus
         DBusTaskWrapper(self.req, self)
@@ -89,7 +93,7 @@
         # initializes the plugin api class
         self.plugin_api = PluginAPI(window         = self.browser.window,
                                     config         = self.config,
-                                    data_dir       = GTG.DATA_DIR,
+                                    data_dir = self.config_obj.get_data_dir(),
                                     builder        = self.browser.builder,
                                     requester      = self.req,
                                     tagpopup       = self.browser.tagpopup,
@@ -189,8 +193,8 @@
             
 ################ Others dialog ############################################
 
-    def show_preferences(self, config_priv, sender=None):
-        if not self.preferences:
+    def open_preferences(self, config_priv, sender=None):
+        if not hasattr(self, "preferences"):
             self.preferences = PreferencesDialog(self.pengine, self.p_apis, \
                     self.config_obj)
         self.preferences.activate(config_priv)

=== modified file 'GTG/gtk/preferences.glade'
--- GTG/gtk/preferences.glade	2010-06-02 18:12:23 +0000
+++ GTG/gtk/preferences.glade	2010-06-23 01:19:23 +0000
@@ -213,63 +213,10 @@
                   </packing>
                 </child>
                 <child>
-                  <object class="GtkAlignment" id="prefs-alignment3">
-                    <property name="visible">True</property>
-                    <property name="top_padding">10</property>
-                    <property name="bottom_padding">10</property>
-                    <property name="left_padding">10</property>
-                    <property name="right_padding">10</property>
-                    <child>
-                      <object class="GtkVBox" id="prefs-vbox5">
-                        <property name="visible">True</property>
-                        <property name="spacing">6</property>
-                        <child>
-                          <object class="GtkLabel" id="prefs-label6">
-                            <property name="visible">True</property>
-                            <property name="xalign">0</property>
-                            <property name="label" translatable="yes">Task _Backends:</property>
-                            <property name="use_underline">True</property>
-                          </object>
-                          <packing>
-                            <property name="expand">False</property>
-                            <property name="position">0</property>
-                          </packing>
-                        </child>
-                        <child>
-                          <object class="GtkScrolledWindow" id="prefs-scrolledwindow1">
-                            <property name="visible">True</property>
-                            <property name="sensitive">False</property>
-                            <property name="can_focus">True</property>
-                            <property name="hscrollbar_policy">never</property>
-                            <property name="vscrollbar_policy">automatic</property>
-                            <child>
-                              <object class="GtkTreeView" id="BackendTree">
-                                <property name="visible">True</property>
-                                <property name="sensitive">False</property>
-                                <property name="can_focus">True</property>
-                              </object>
-                            </child>
-                          </object>
-                          <packing>
-                            <property name="position">1</property>
-                          </packing>
-                        </child>
-                      </object>
-                    </child>
-                  </object>
-                  <packing>
-                    <property name="position">1</property>
-                  </packing>
+                  <placeholder/>
                 </child>
                 <child type="tab">
-                  <object class="GtkLabel" id="prefs-label2">
-                    <property name="visible">True</property>
-                    <property name="label" translatable="yes">Storage</property>
-                  </object>
-                  <packing>
-                    <property name="position">1</property>
-                    <property name="tab_fill">False</property>
-                  </packing>
+                  <placeholder/>
                 </child>
                 <child>
                   <object class="GtkAlignment" id="prefs-alignment4">

=== modified file 'GTG/gtk/preferences.py'
--- GTG/gtk/preferences.py	2010-06-10 14:45:36 +0000
+++ GTG/gtk/preferences.py	2010-06-23 01:19:23 +0000
@@ -270,7 +270,7 @@
             self.config["plugins"]["enabled"] = \
               self.pengine.enabled_plugins().keys()
 
-        self.config_obj.save_config()
+        self.config_obj.save()
 
         self.dialog.hide()
         return True

=== modified file 'GTG/tests/__init__.py'
--- GTG/tests/__init__.py	2010-06-22 09:43:55 +0000
+++ GTG/tests/__init__.py	2010-06-23 01:19:23 +0000
@@ -19,22 +19,31 @@
 
 """Unit tests for GTG."""
 
+from GTG.tools.testingmode import TestingMode
+TestingMode().set_testing_mode(True)
+
+
 import unittest
 
+
 from GTG.tests import (
     test_tagstore,
     test_taskviewserial,
     test_tree,
     test_apidocs,
+    test_backends,
+    test_datastore,
     test_filteredtree,
     )
 
-
 def test_suite():
     return unittest.TestSuite([
         test_tagstore.test_suite(),
         test_taskviewserial.test_suite(),
     	test_tree.test_suite(),
         test_apidocs.test_suite(),
+        test_backends.test_suite(),
+        test_datastore.test_suite(),
         test_filteredtree.test_suite(),
         ])
+

=== modified file 'GTG/tests/test_apidocs.py'
--- GTG/tests/test_apidocs.py	2010-05-29 14:39:28 +0000
+++ GTG/tests/test_apidocs.py	2010-06-23 01:19:23 +0000
@@ -27,6 +27,8 @@
 import shutil
 import uuid
 
+from GTG.core import CoreConfig
+
 
 
 class TestApiDocs(unittest.TestCase):
@@ -50,4 +52,6 @@
         shutil.rmtree(api_dir)
 
 def test_suite():
+    CoreConfig().set_data_dir("./test_data")
+    CoreConfig().set_conf_dir("./test_data")
     return unittest.TestLoader().loadTestsFromTestCase(TestApiDocs)

=== added file 'GTG/tests/test_backends.py'
--- GTG/tests/test_backends.py	1970-01-01 00:00:00 +0000
+++ GTG/tests/test_backends.py	2010-06-23 01:19:23 +0000
@@ -0,0 +1,191 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU 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 General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+"""Tests for GTG backends.
+
+Some of these tests will generate files in
+xdg.BaseDirectory.xdg_data_home/gtg directory.
+"""
+
+# Standard imports
+import unittest
+import os
+import xdg
+
+# GTG imports
+from GTG.backends import backend_localfile as localfile
+from GTG.core import datastore
+from GTG.tools import cleanxml
+from GTG.core import CoreConfig
+
+
+class GtgBackendsUniTests(unittest.TestCase):
+    """Tests for GTG backends."""
+
+    def __init__(self, test):
+        unittest.TestCase.__init__(self, test)
+        self.taskfile = ''
+        self.datafile = ''
+        self.taskpath = ''
+        self.datapath = ''
+    
+    def SetUp(self):
+        CoreConfig().set_data_dir("./test_data")
+        CoreConfig().set_conf_dir("./test_data")
+
+    def test_localfile_get_name(self):
+        """Tests for localfile/get_name function :
+        - a string is expected.
+        """
+        res = localfile.Backend.get_name()
+        expectedres = "backend_localfile"
+        self.assertEqual(res, expectedres)
+
+    def test_localfile_get_description(self):
+        """Tests for localfile/get_description function :
+        - a string is expected.
+        """
+        res = localfile.Backend.get_description()
+        expectedres = "Your tasks are saved"
+        self.assertEqual(res[:len(expectedres)], expectedres)
+
+
+    def test_localfile_get_static_parameters(self):
+        """Tests for localfile/get_static_parameters function:
+        - a string is expected.
+        """
+        res = localfile.Backend.get_static_parameters()
+        self.assertEqual(res['path']['type'], "string")
+
+    def test_localfile_get_type(self):
+        """Tests for localfile/get_type function:
+        - a string is expected.
+        """
+        res = localfile.Backend.get_type()
+        expectedres = "readwrite"
+        self.assertEqual(res, expectedres)
+
+
+    def test_localfile_backend_method3(self):
+        """Tests for localfile/Backend/remove_task method:
+        - parse task file to check if task has been removed.
+        """
+        self.create_test_environment()
+        doc, configxml = cleanxml.openxmlfile(self.datapath, 'config')
+        xmlproject = doc.getElementsByTagName('backend')
+        for domobj in xmlproject:
+            dic = {}
+            if domobj.hasAttribute("module"):
+                dic["module"] = str(domobj.getAttribute("module"))
+                dic["pid"] = str(domobj.getAttribute("pid"))
+                dic["xmlobject"] = domobj
+                dic["Enabled"] = True
+                dic["path"] = self.taskpath
+        beobj = localfile.Backend(dic)
+        expectedres = True
+        beobj.remove_task("0@1")
+        beobj.quit()
+        dataline = open(self.taskpath, 'r').read()
+        print dataline
+        if "0@1" in dataline:
+            res = False
+        else:
+            res = True
+        expectedres = True
+        self.assertEqual(res, expectedres)
+
+#    def test_localfile_backend_method4(self):
+#        """Tests for localfile/Backend/get_task method:
+#        - Compares task titles to check if method works.
+#        """
+#        self.create_test_environment()
+#        doc, configxml = cleanxml.openxmlfile(self.datapath, 'config')
+#        xmlproject = doc.getElementsByTagName('backend')
+#        for domobj in xmlproject:
+#            dic = {}
+#            if domobj.hasAttribute("module"):
+#                dic["module"] = str(domobj.getAttribute("module"))
+#                dic["pid"] = str(domobj.getAttribute("pid"))
+#                dic["xmlobject"] = domobj
+#                dic["filename"] = self.taskfile
+#        beobj = localfile.Backend(dic)
+#        dstore = datastore.DataStore()
+#        newtask = dstore.new_task(tid="0@2", pid="1", newtask=True)
+#        beobj.get_task(newtask, "0@1")
+#        self.assertEqual(newtask.get_title(), u"Ceci est un test")
+
+#    def test_localfile_backend_method5(self):
+#        """Tests for localfile/Backend/set_task method:
+#        - parses task file to check if new task has been stored.
+#        """
+#        self.create_test_environment()
+#        doc, configxml = cleanxml.openxmlfile(self.datapath, 'config')
+#        xmlproject = doc.getElementsByTagName('backend')
+#        for domobj in xmlproject:
+#            dic = {}
+#            if domobj.hasAttribute("module"):
+#                dic["module"] = str(domobj.getAttribute("module"))
+#                dic["pid"] = str(domobj.getAttribute("pid"))
+#                dic["xmlobject"] = domobj
+#                dic["filename"] = self.taskfile
+#        beobj = localfile.Backend(dic)
+#        dstore = datastore.DataStore()
+#        newtask = dstore.new_task(tid="0@2", pid="1", newtask=True)
+#        beobj.set_task(newtask)
+#        dataline = open(self.taskpath, 'r').read()
+#        if "0@2" in dataline:
+#            res = True
+#        else:
+#            res = False
+#        expectedres = True
+#        self.assertEqual(res, expectedres)
+
+    def create_test_environment(self):
+        """Create the test environment"""
+        self.taskfile = 'test.xml'
+        self.datafile = 'projectstest.xml'
+        tasks = [
+            '<?xml version="1.0" ?>\n',
+            '<project>\n',
+            '\t<task id="0@1" status="Active" tags="">\n',
+            '\t\t<title>\n',
+            '\t\t\tCeci est un test\n',
+            '\t\t</title>\n',
+            '\t</task>\n',
+            '</project>\n',
+            ]
+        data = [
+            '<?xml version="1.0" ?>\n',
+            '<config>\n',
+            '\t<backend filename="test.xml" module="localfile" pid="1"/>\n',
+            '</config>\n',
+            ]
+        self.testdir = os.path.join(xdg.BaseDirectory.xdg_data_home, 'gtg')
+        if not os.path.exists(self.testdir):
+            os.makedirs(self.testdir)
+        self.taskpath = os.path.join(self.testdir, self.taskfile)
+        self.datapath = os.path.join(self.testdir, self.datafile)
+        open(self.taskpath, 'w').writelines(tasks)
+        open(self.datapath, 'w').writelines(data)
+
+
+def test_suite():
+    CoreConfig().set_data_dir("./test_data")
+    CoreConfig().set_conf_dir("./test_data")
+    return unittest.TestLoader().loadTestsFromName(__name__)

=== added file 'GTG/tests/test_datastore.py'
--- GTG/tests/test_datastore.py	1970-01-01 00:00:00 +0000
+++ GTG/tests/test_datastore.py	2010-06-23 01:19:23 +0000
@@ -0,0 +1,360 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU 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 General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+'''
+Tests for the datastore
+'''
+
+import unittest
+import uuid
+import random
+import time
+
+import GTG
+from GTG.core.datastore import DataStore
+from GTG.backends.genericbackend import GenericBackend
+from GTG.core import CoreConfig
+
+
+
+class TestDatastore(unittest.TestCase):
+    '''
+    Tests for the DataStore object.
+    '''
+
+    
+    def setUp(self):
+        '''
+        Creates the environment for the tests
+        @returns None
+        '''
+        self.datastore = DataStore()
+        self.requester = self.datastore.get_requester()
+
+    def test_task_factory(self):
+        '''
+        Test for the task_factory function
+        '''
+        #generate a Task with a random id
+        tid = str(uuid.uuid4())
+        task = self.datastore.task_factory(tid, newtask = True)
+        self.assertTrue(isinstance(task, GTG.core.task.Task))
+        self.assertEqual(task.get_id(), tid)
+        self.assertEqual(task.is_new(), True)
+        tid = str(uuid.uuid4())
+        task = self.datastore.task_factory(tid, newtask = False)
+        self.assertEqual(task.is_new(), False)
+
+    def test_new_task_and_has_task(self):
+        '''
+        Tests the new_task function
+        '''
+        task = self.datastore.new_task()
+        tid = task.get_id()
+        self.assertTrue(isinstance(tid, str))
+        self.assertTrue(tid != '')
+        self.assertTrue(task.is_new())
+        self.assertTrue(self.datastore.has_task(tid))
+        self.assertTrue(len(self.datastore.get_all_tasks()) == 1)
+
+    def test_get_all_tasks(self):
+        '''
+        Tests the get_all_tasks function
+        '''
+        task_ids = []
+        for i in xrange(1, 10):
+            task = self.datastore.new_task()
+            task_ids.append(task.get_id())
+            return_list =self.datastore.get_all_tasks()
+            self.assertEqual(len(return_list), i)
+            task_ids.sort()
+            return_list.sort()
+            self.assertEqual(task_ids, return_list)
+
+    def test_get_task(self):
+        '''
+        Tests the get_task function
+        '''
+        self.assertEqual(self.datastore.get_task(str(uuid.uuid4())), None)
+        task = self.datastore.new_task()
+        self.assertTrue(isinstance(self.datastore.get_task(task.get_id()),
+                                   GTG.core.task.Task))
+        self.assertEqual(self.datastore.get_task(task.get_id()), task)
+
+
+    def test_get_tagstore(self):
+        '''
+        Tests the get_tagstore function
+        '''
+        tagstore = self.datastore.get_tagstore()
+        self.assertTrue(isinstance(tagstore, GTG.core.tagstore.TagStore))
+
+    def test_get_requester(self):
+        '''
+        Tests the get_requester function
+        '''
+        requester = self.datastore.get_requester()
+        self.assertTrue(isinstance(requester, GTG.core.requester.Requester))
+
+    def test_get_tasks_tree(self):
+        '''
+        Tests the get_tasks_tree function
+        '''
+        tasks_tree = self.datastore.get_tasks_tree()
+        self.assertTrue(isinstance(tasks_tree, GTG.core.tree.Tree))
+
+    def test_push_task(self):
+        '''
+        Tests the push_task function
+        '''
+        task_ids = []
+        for i in xrange(1, 10):
+            tid = str(uuid.uuid4())
+            if tid not in task_ids:
+                task_ids.append(tid)
+            task = self.datastore.task_factory(tid)
+            return_value1 = self.datastore.push_task(task)
+            self.assertTrue(return_value1)
+            #we do it twice, but it should be pushed only once if it's
+            # working correctly (the second should be discarded)
+            return_value2 = self.datastore.push_task(task)
+            self.assertFalse(return_value2)
+            stored_tasks = self.datastore.get_all_tasks()
+            task_ids.sort()
+            stored_tasks.sort()
+            self.assertEqual(task_ids, stored_tasks)
+
+    def test_register_backend(self):
+        '''
+        Tests the register_backend function. It also tests the
+        get_all_backends and get_backend function as a side effect
+        '''
+        #create a simple backend dictionary
+        backend = FakeBackend(enabled = True)
+        tasks_in_backend_count = int(random.random() * 20)
+        for temp in xrange(0, tasks_in_backend_count):
+            backend.fake_add_random_task()
+        backend_dic = {'backend': backend, 'pid': 'a'}
+        self.datastore.register_backend(backend_dic)
+        all_backends = self.datastore.get_all_backends(disabled = True)
+        self.assertEqual(len(all_backends), 1)
+        registered_backend = self.datastore.get_backend(backend.get_id())
+        self.assertEqual(backend.get_id(), registered_backend.get_id())
+        self.assertTrue(isinstance(registered_backend, \
+                                   GTG.core.datastore.TaskSource))
+        self.assertTrue(registered_backend.is_enabled())
+        self.assertEqual(registered_backend.fake_get_initialized_count(), 1)
+        #we give some time for the backend to push all its tasks
+        time.sleep(1)
+        self.assertEqual(len(self.datastore.get_all_tasks()), \
+                         tasks_in_backend_count)
+
+        #same test, disabled backend
+        backend = FakeBackend(enabled = False)
+        for temp in xrange(1, int(random.random() * 20)):
+            backend.fake_add_random_task()
+        backend_dic = {'backend': backend, 'pid':'b'}
+        self.datastore.register_backend(backend_dic)
+        all_backends = self.datastore.get_all_backends(disabled = True)
+        self.assertEqual(len(all_backends), 2)
+        all_backends = self.datastore.get_all_backends(disabled = False)
+        self.assertEqual(len(all_backends), 1)
+        registered_backend = self.datastore.get_backend(backend.get_id())
+        self.assertEqual(backend.get_id(), registered_backend.get_id())
+        self.assertTrue(isinstance(registered_backend, \
+                                   GTG.core.datastore.TaskSource))
+        self.assertFalse(registered_backend.is_enabled())
+        self.assertEqual(registered_backend.fake_get_initialized_count(), 0)
+        #we give some time for the backend to push all its tasks (is
+        #shouldn't, since it's disabled, but we give time anyway
+        time.sleep(1)
+        self.assertEqual(len(self.datastore.get_all_tasks()), \
+                         tasks_in_backend_count)
+
+    def test_set_backend_enabled(self):
+        '''
+        Tests the set_backend_enabled function
+        '''
+        enabled_backend = FakeBackend(enabled = True)
+        disabled_backend = FakeBackend(enabled = False)
+        self.datastore.register_backend({'backend': enabled_backend, \
+                                'pid': str(uuid.uuid4()), \
+                                GenericBackend.KEY_DEFAULT_BACKEND: False})
+        self.datastore.register_backend({'backend': disabled_backend,\
+                                'pid': str(uuid.uuid4()), \
+                                GenericBackend.KEY_DEFAULT_BACKEND: False})
+        #enabling an enabled backend
+        self.datastore.set_backend_enabled(enabled_backend.get_id(), True)
+        self.assertEqual(enabled_backend.fake_get_initialized_count(), 1)
+        self.assertTrue(enabled_backend.is_enabled())
+        #disabling a disabled backend
+        self.datastore.set_backend_enabled(disabled_backend.get_id(), False)
+        self.assertEqual(disabled_backend.fake_get_initialized_count(), 0)
+        self.assertFalse(disabled_backend.is_enabled())
+        #disabling an enabled backend
+        self.datastore.set_backend_enabled(enabled_backend.get_id(), False)
+        self.assertEqual(enabled_backend.fake_get_initialized_count(), 1)
+        self.assertFalse(enabled_backend.is_enabled())
+        time.sleep(1)
+#        #enabling a disabled backend
+#        self.datastore.set_backend_enabled(disabled_backend.get_id(), True)
+#        self.assertEqual(disabled_backend.fake_get_initialized_count(), 1)
+#        self.assertTrue(disabled_backend.is_enabled())
+            
+    def test_remove_backend(self):
+        '''
+        Tests the remove_backend function
+        '''
+        enabled_backend = FakeBackend(enabled = True)
+        disabled_backend = FakeBackend(enabled = False)
+        self.datastore.register_backend({'backend': enabled_backend, \
+                                'pid': str(uuid.uuid4()), \
+                                GenericBackend.KEY_DEFAULT_BACKEND: False})
+        self.datastore.register_backend({'backend': disabled_backend,\
+                                'pid': str(uuid.uuid4()), \
+                                GenericBackend.KEY_DEFAULT_BACKEND: False})
+        #removing an enabled backend
+        self.datastore.remove_backend(enabled_backend.get_id())
+        self.assertFalse(enabled_backend.is_enabled())
+        self.assertTrue(enabled_backend.fake_is_purged())
+        self.assertEqual( \
+            len(self.datastore.get_all_backends(disabled = True)), 1)
+        #removing a disabled backend
+        self.datastore.remove_backend(disabled_backend.get_id())
+        self.assertFalse(disabled_backend.is_enabled())
+        self.assertTrue(disabled_backend.fake_is_purged())
+        self.assertEqual( \
+            len(self.datastore.get_all_backends(disabled = True)), 0)
+
+    def test_flush_all_tasks(self):
+        '''
+        Tests the flush_all_tasks function
+        '''
+        #we add some tasks in the datastore
+        tasks_in_datastore_count = 10 #int(random.random() * 20)
+        for temp in xrange(0, tasks_in_datastore_count):
+            self.datastore.new_task()
+        datastore_stored_tids = self.datastore.get_all_tasks()
+        self.assertEqual(tasks_in_datastore_count, len(datastore_stored_tids))
+
+        #we enable a backend
+        backend = FakeBackend(enabled = True)
+        self.datastore.register_backend({'backend': backend, 'pid': 'a'})
+        #we wait for the signal storm to wear off
+        time.sleep(5)
+        #we sync
+        self.datastore.get_backend(backend.get_id()).sync()
+        #and we inject task in the backend
+        tasks_in_backend_count = 5 #int(random.random() * 20)
+        for temp in xrange(0, tasks_in_backend_count):
+            backend.fake_add_random_task()
+        backend_stored_tids = backend.fake_get_task_ids()
+        self.assertEqual(tasks_in_backend_count, len(backend_stored_tids))
+        self.datastore.flush_all_tasks(backend.get_id())
+        #we wait for the signal storm to wear off
+        time.sleep(2)
+        #we sync
+        self.datastore.get_backend(backend.get_id()).sync()
+        all_tasks_count = tasks_in_backend_count + tasks_in_datastore_count
+        new_datastore_stored_tids = self.datastore.get_all_tasks()
+        new_backend_stored_tids = backend.fake_get_task_ids()
+        self.assertEqual(len(new_backend_stored_tids), all_tasks_count)
+        self.assertEqual(len(new_datastore_stored_tids), all_tasks_count)
+        new_datastore_stored_tids.sort()
+        new_backend_stored_tids.sort()
+        self.assertEqual(new_backend_stored_tids, new_datastore_stored_tids)
+
+
+
+def test_suite():
+    CoreConfig().set_data_dir("./test_data")
+    CoreConfig().set_conf_dir("./test_data")
+    return unittest.TestLoader().loadTestsFromTestCase(TestDatastore)
+
+
+
+class FakeBackend(unittest.TestCase):
+    '''
+    Mimics the behavior of a simple backend. Just used for testing
+    '''
+
+    def __init__(self, enabled = True):
+        self.enabled = enabled
+        self.initialized_count = 0
+        self.tasks_ids = []
+        self.backend_id = str(uuid.uuid4())
+        self.purged = False
+
+    def is_enabled(self):
+        return self.enabled
+
+    def initialize(self):
+        self.initialized_count += 1
+        self.enabled = True
+
+    def queue_set_task(self, task):
+        self.tasks_ids.append(task.get_id())
+
+    def has_task(self, task_id):
+        return task_id in self.tasks_ids
+
+    def queue_remove_task(self, task_id):
+        self.tasks_ids.remove(task_id)
+
+    def get_id(self):
+        return self.backend_id
+
+    def start_get_tasks(self):
+        for task_id in self.tasks_ids:
+            self.datastore.push_task(self.datastore.task_factory(task_id))
+
+    def quit(self, disabled = False):
+        self.enabled = not disabled
+
+    def purge(self):
+        self.purged = True
+
+    def is_default(self):
+        return True
+
+    def set_parameter(self, param_name, param_value):
+        pass
+
+    def get_attached_tags(self):
+        return [CoreConfig.ALLTASKS_TAG]
+
+    def register_datastore(self, datastore):
+        self.datastore = datastore
+    
+    ##########################################################################
+    # The following are used just for testing, they're not present inside a
+    # normal backend
+    ##########################################################################
+
+    def fake_get_initialized_count(self):
+        return self.initialized_count
+
+    def fake_get_task_ids(self):
+        return self.tasks_ids
+
+    def fake_add_random_task(self):
+        self.tasks_ids.append(str(uuid.uuid4()))
+
+    def fake_is_purged(self):
+        return self.purged

=== modified file 'GTG/tests/test_tagstore.py'
--- GTG/tests/test_tagstore.py	2010-05-29 12:50:20 +0000
+++ GTG/tests/test_tagstore.py	2010-06-23 01:19:23 +0000
@@ -23,6 +23,7 @@
 
 from GTG.core.tagstore   import Tag
 from GTG.core.datastore import DataStore
+from GTG.core import CoreConfig
 
 
 
@@ -117,4 +118,6 @@
         self.assertEqual(0, len(save_calls))
 
 def test_suite():
+    CoreConfig().set_data_dir("./test_data")
+    CoreConfig().set_conf_dir("./test_data")
     return unittest.TestLoader().loadTestsFromTestCase(TestTag)

=== modified file 'GTG/tests/test_taskviewserial.py'
--- GTG/tests/test_taskviewserial.py	2010-06-11 14:30:12 +0000
+++ GTG/tests/test_taskviewserial.py	2010-06-23 01:19:23 +0000
@@ -27,6 +27,7 @@
 import unittest
 
 from GTG.gtk.editor import taskviewserial
+from GTG.core import CoreConfig
     
 class GtgBackendsUniTests(unittest.TestCase):
     """Tests for GTG backends."""
@@ -47,4 +48,6 @@
         
         
 def test_suite():
+    CoreConfig().set_data_dir("./test_data")
+    CoreConfig().set_conf_dir("./test_data")
     return unittest.TestLoader().loadTestsFromName(__name__)

=== modified file 'GTG/tests/test_tree.py'
--- GTG/tests/test_tree.py	2010-02-21 19:21:18 +0000
+++ GTG/tests/test_tree.py	2010-06-23 01:19:23 +0000
@@ -22,6 +22,9 @@
 import unittest
 
 from GTG.core.tree import Tree,TreeNode
+from GTG.core import CoreConfig
+
+
 
 class TestTree(unittest.TestCase):
     """Tests for `Tree`."""
@@ -119,4 +122,6 @@
 
 
 def test_suite():
+    CoreConfig().set_data_dir("./test_data")
+    CoreConfig().set_conf_dir("./test_data")
     return unittest.TestLoader().loadTestsFromName(__name__)

=== added file 'GTG/tools/borg.py'
--- GTG/tools/borg.py	1970-01-01 00:00:00 +0000
+++ GTG/tools/borg.py	2010-06-23 01:19:23 +0000
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU 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 General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+
+
+class Borg(object):
+    """
+    This pattern ensures that all instances of a particular class share
+    the same state (just inherit this class to have it working)
+    """
+
+    _borg_state = {}
+    
+    def __init__(self):
+        self.__dict__ = self._borg_state
+
+

=== added file 'GTG/tools/keyring.py'
--- GTG/tools/keyring.py	1970-01-01 00:00:00 +0000
+++ GTG/tools/keyring.py	2010-06-23 01:19:23 +0000
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU 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 General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+import gnomekeyring
+
+from GTG.tools.borg import Borg
+
+
+
+class Keyring(Borg):
+
+
+    def __init__(self):
+        super(Keyring, self).__init__()
+        if not hasattr(self, "keyring"):
+            self.keyring = gnomekeyring.get_default_keyring_sync()
+
+    def set_password(self, name, password, userid = ""):
+        return gnomekeyring.item_create_sync(
+                    self.keyring,
+                    gnomekeyring.ITEM_GENERIC_SECRET,
+                    name,
+                    {"backend": name},
+                    password,
+                    True)
+
+    def get_password(self, item_id):
+        try:
+            item_info = gnomekeyring.item_get_info_sync(self.keyring, item_id)
+            return item_info.get_secret()
+        except (gnomekeyring.DeniedError, gnomekeyring.NoMatchError):
+            return ""

=== modified file 'GTG/tools/logger.py'
--- GTG/tools/logger.py	2010-03-02 06:32:31 +0000
+++ GTG/tools/logger.py	2010-06-23 01:19:23 +0000
@@ -41,6 +41,7 @@
         #Shouldn't be needed, but the following line makes sure that
         # this is a Singleton.
         self.__dict__['_Debug__logger'] = Debug.__logger
+        self.debugging_mode = False
 
     def __init_logger(self):
         Debug.__logger = logging.getLogger('gtg_logger')
@@ -60,5 +61,10 @@
         """ Delegates to the real logger """
         return setattr(Debug.__logger, attr, value)
 
+    def set_debugging_mode(self, value):
+        self.debugging_mode = value
+    def is_debugging_mode(self):
+        return self.debugging_mode
+
 #The singleton itself
 Log = Debug()

=== added file 'GTG/tools/synchronized.py'
--- GTG/tools/synchronized.py	1970-01-01 00:00:00 +0000
+++ GTG/tools/synchronized.py	2010-06-23 01:19:23 +0000
@@ -0,0 +1,14 @@
+from __future__ import with_statement
+from threading import Lock
+
+def synchronized(fun):
+    the_lock = Lock()
+
+    def fwrap(function):
+        def newFunction(*args, **kw):
+            with the_lock:
+                return function(*args, **kw)
+
+        return newFunction
+
+    return fwrap(fun)

=== modified file 'GTG/tools/taskxml.py'
--- GTG/tools/taskxml.py	2010-06-18 16:36:17 +0000
+++ GTG/tools/taskxml.py	2010-06-23 01:19:23 +0000
@@ -60,7 +60,15 @@
     cur_tags = xmlnode.getAttribute("tags").replace(' ','').split(",")
     if "" in cur_tags: cur_tags.remove("")
     for tag in cur_tags: cur_task.tag_added(saxutils.unescape(tag))
-    
+
+    #REMOTE TASK IDS
+    remote_ids_list = xmlnode.getElementsByTagName("task-remote-ids")
+    for remote_id in remote_ids_list:
+        if remote_id.childNodes:
+            node = remote_id.childNodes[0]
+            backend_id = node.firstChild.nodeValue
+            remote_task_id = node.childNodes[1].firstChild.nodeValue
+            task.add_remote_id(backend_id, remote_task_id)
     return cur_task
 
 #Task as parameter the doc where to put the XML node
@@ -99,4 +107,18 @@
         #t_xml.appendChild(element.firstChild)
         cleanxml.addTextNode(doc,t_xml,"content",desc)
     #self.__write_textnode(doc,t_xml,"content",t.get_text())
+
+    #REMOTE TASK IDS
+    remote_ids_element = doc.createElement("task-remote-ids")
+    t_xml.appendChild(remote_ids_element)
+    remote_ids_dict = task.get_remote_ids()
+    for backend_id, task_id in remote_ids_dict.iteritems():
+        backend_element = doc.createElement('backend')
+        remote_ids_element.appendChild(backend_element)
+        backend_element.appendChild(doc.createTextNode(backend_id))
+        task_element = doc.createElement('task-id')
+        backend_element.appendChild(task_element)
+        task_element.appendChild(doc.createTextNode(task_id))
+
+
     return t_xml

=== added file 'GTG/tools/testingmode.py'
--- GTG/tools/testingmode.py	1970-01-01 00:00:00 +0000
+++ GTG/tools/testingmode.py	2010-06-23 01:19:23 +0000
@@ -0,0 +1,16 @@
+from GTG.tools.borg import Borg
+
+
+
+class TestingMode(Borg):
+
+
+    def set_testing_mode(self, value):
+        self._testing_mode = value
+
+    def get_testing_mode(self):
+        try:
+            return self._testing_mode
+        except:
+            return False
+

=== modified file 'Makefile'
--- Makefile	2010-03-01 01:43:33 +0000
+++ Makefile	2010-06-23 01:19:23 +0000
@@ -35,7 +35,7 @@
 # Check for coding standard violations & flakes.
 lint: pyflakes pep8
 
-.PHONY: check lint pyflakes pep8 apidocs
+.PHONY: check lint pyflakes pep8 apidocs edit-apidocs clean
 
 #Ignore the exit code in pyflakes, so that pep8 is always run when "make lint"
 .IGNORE: pyflakes

=== modified file 'scripts/debug.sh'
--- scripts/debug.sh	2010-06-21 09:44:23 +0000
+++ scripts/debug.sh	2010-06-23 01:19:23 +0000
@@ -42,6 +42,7 @@
 if [ $norun -eq 0 ]; then
     if [ $profile -eq 1 ]; then
 	python -m cProfile -o gtg.prof ./gtg
+    python ./scripts/profile_interpret.sh
     else
 	./gtg $args
     fi

=== added file 'scripts/profile_interpret.sh'
--- scripts/profile_interpret.sh	1970-01-01 00:00:00 +0000
+++ scripts/profile_interpret.sh	2010-06-23 01:19:23 +0000
@@ -0,0 +1,4 @@
+#!/usr/bin/env python
+import pstats
+p = pstats.Stats('gtg.prof')
+p.strip_dirs().sort_stats("cumulative").print_stats(20)