← Back to team overview

gtg team mailing list archive

[Merge] lp:~gtg-user/gtg/backends-main into lp:gtg

 

Luca Invernizzi has proposed merging lp:~gtg-user/gtg/backends-main into lp:gtg.

Requested reviews:
  Gtg developers (gtg)


Update of the already merged general backends framework and the localfile backend.
Adds some bug fixes, but the main contribution is documentation.
-- 
https://code.launchpad.net/~gtg-user/gtg/backends-main/+merge/32645
Your team Gtg developers is requested to review the proposed merge of lp:~gtg-user/gtg/backends-main into lp:gtg.
=== modified file 'CHANGELOG'
--- CHANGELOG	2010-08-04 00:30:22 +0000
+++ CHANGELOG	2010-08-13 23:45:01 +0000
@@ -4,6 +4,7 @@
     * Fixed bug with data consistency #579189, by Marko Kevac
     * Added samba bugzilla to the bugzilla plugin, by Jelmer Vernoij
     * Fixed bug #532392, a start date is later than a due date, by Volodymyr Floreskul
+    * Extended backend system to support multiple backends by Luca Invernizzi
 
 2010-03-01 Getting Things GNOME! 0.2.2
     * Autostart on login, by Luca Invernizzi

=== modified file 'GTG/backends/backend_localfile.py'
--- GTG/backends/backend_localfile.py	2010-06-22 20:24:01 +0000
+++ GTG/backends/backend_localfile.py	2010-08-13 23:45:01 +0000
@@ -20,6 +20,9 @@
 '''
 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.
+
+This backend contains comments that are meant as a reference, in case someone
+wants to write a backend.
 '''
 
 import os
@@ -29,18 +32,30 @@
 from GTG.core                    import CoreConfig
 from GTG.tools                   import cleanxml, taskxml
 from GTG                         import _
+from GTG.tools.logger            import Log
 
 
 
 class Backend(GenericBackend):
+    '''
+    Localfile backend, which stores your tasks in a XML file in the standard
+    XDG_DATA_DIR/gtg folder (the path is configurable).
+    An instance of this class is used as the default backend for GTG.
+    This backend loads all the tasks stored in the localfile after it's enabled,
+    and from that point on just writes the changes to the file: it does not
+    listen for eventual file changes
+    '''
     
 
     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 of the backend: these are used to show a description of
+    # the backend to the user when s/he is considering adding it.
+    # BACKEND_NAME is the name of the backend used internally (it must be
+    # unique).
+    #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"), \
@@ -53,10 +68,14 @@
               "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)
+    #These are the parameters to configure a new backend of this type. A
+    # parameter has a name, a type and a default value.
+    # Here, we define a parameter "path", which is a string, and has a default
+    # value as a random file in the default path
+    #NOTE: to keep this simple, the filename default path is the same until GTG
+    #      is restarted. I consider this a minor annoyance, and we can avoid
+    #      coding the change of the path each time a backend is
+    #      created (invernizzi)
     _static_parameters = { \
         "path": { \
             GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING, \
@@ -64,30 +83,22 @@
                  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.
+        @param parameters: A dictionary of parameters, generated from
+        _static_parameters. A few parameters are added to those, the list of
+        these is in the "DefaultBackend" class, look for the KEY_* constants.
+    
+        The backend should take care if one expected value is None or
+        does not exist in the dictionary.
         """
         super(Backend, self).__init__(parameters)
-        self.tids = []
+        self.tids = [] #we keep the list of loaded task ids here
         #####RETROCOMPATIBILIY
-        #NOTE: retrocompatibility. We convert "filename" to "path"
-        #      and we forget about "filename"
+        #NOTE: retrocompatibility from the 0.2 series to 0.3.
+        # 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"])
@@ -99,24 +110,30 @@
                                 self._parameters["path"], "project")
 
     def initialize(self):
+        """This is called when a backend is enabled"""
         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
+        """
+        Called upon the very first GTG startup.
+        This function is needed only in this backend, because it can be used as
+        default one.
+        The xml parameter is an object containing GTG default tasks. It will be
+        saved to a file, and the backend will be set as default.
+        @param xml: an xml object containing the default tasks.
+        """
         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.
-        
+        This function starts submitting the tasks from the XML file into GTG core.
+        It's run as a separate thread.
+                
         @return: start_get_tasks() might not return or finish
         '''
         tid_list = []
@@ -130,52 +147,63 @@
                 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)
+        '''
+        This function is called from GTG core whenever a task should be
+        saved, either because it's a new one or it has been modified.
+        This function will look into the loaded XML object if the task is
+        present, and if it's not, it will create it. Then, it will save the
+        task data in the XML object.
+
+        @param task: the task object to save
+        '''
+        tid = task.get_id()
+        #We create an XML representation of the task
+        t_xml = taskxml.task_to_xml(self.doc, task)
+
+        #we find if the task exists in the XML treenode.
+        existing = None
+        for node in self.xmlproj.childNodes:
+            if node.getAttribute("id") == tid:
+                existing = node
+
+        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
-            #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)
+        #If the node doesn't exist, we create it
+        else:
+            self.xmlproj.appendChild(t_xml)
+            modified = True
+
+        #if the XML object has changed, we save it to file
+        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 '''
+        ''' This function is called from GTG core whenever a task must be
+        removed from the backend. Note that the task could be not present here.
+        
+        @param tid: the id of the task to delete
+        '''
+        modified = False
         for node in self.xmlproj.childNodes:
             if node.getAttribute("id") == tid:
+                modified = True
                 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)
+
+        #We save the XML file only if it's necessary
+        if modified:
+            cleanxml.savexml(self._parameters["path"], self.doc)
+
+#NOTE: This is not used currently. Therefore, I'm disabling it (invernizzi)
+#    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)

=== modified file 'GTG/backends/genericbackend.py'
--- GTG/backends/genericbackend.py	2010-06-22 20:44:26 +0000
+++ GTG/backends/genericbackend.py	2010-08-13 23:45:01 +0000
@@ -18,7 +18,8 @@
 # -----------------------------------------------------------------------------
 
 '''
-FIXME: document!
+This file contains the most generic representation of a backend, the
+GenericBackend class
 '''
 
 import os
@@ -29,69 +30,71 @@
 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
-
+from GTG.tools.keyring           import Keyring
+from GTG.core                    import CoreConfig
+from GTG.tools.logger            import Log
+from GTG.tools.interruptible     import _cancellation_point
 
 
 
 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.
+    Base class for every backend.
+    It defines the interface a backend must have and takes care of all the
+    operations common to all backends.
+    A particular backend should redefine all the methods marked as such.
     '''
 
 
-    #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"
+   ###########################################################################
+   ### BACKEND INTERFACE #####################################################
+   ###########################################################################
+
+    #General description of the backend: these parameters are used
+    #to show a description of the backend to the user when s/he is
+    #considering adding it.
+    # For an example, see the GTG/backends/backend_localfile.py file
+    #_general_description has this format:
+    #_general_description = {
+    #    GenericBackend.BACKEND_NAME:       "backend_unique_identifier", \
+    #    GenericBackend.BACKEND_HUMAN_NAME: _("Human friendly name"), \
+    #    GenericBackend.BACKEND_AUTHORS:    ["First author", \
+    #                                        "Chuck Norris"], \
+    #    GenericBackend.BACKEND_TYPE:       GenericBackend.TYPE_READWRITE, \
+    #    GenericBackend.BACKEND_DESCRIPTION: \
+    #        _("Short description of the backend"),\
+    #    }
+    # The complete list of constants and their meaning is given below.
     _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
+    #These are the parameters to configure a new backend of this type. A
+    # parameter has a name, a type and a default value.
+    # For an example, see the GTG/backends/backend_localfile.py file
+    #_static_parameters has this format:
+    #_static_parameters = { \
+    #    "param1_name": { \
+    #        GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING,
+    #        GenericBackend.PARAM_DEFAULT_VALUE: "my default value",
+    #    },
+    #    "param2_name": {
+    #        GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT,
+    #        GenericBackend.PARAM_DEFAULT_VALUE: 42,
+    #        }}
+    # The complete list of constants and their meaning is given below.
     _static_parameters = {}
 
     def initialize(self):
         '''
-        Called each time it is enabled again (including on backend creation).
+        Called each time it is enabled (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)
+        #NOTE: I'm disabling this since support for runtime checking of the
+        #        presence of the necessary modules is disabled. (invernizzi)
+#        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
@@ -99,65 +102,66 @@
 
     def start_get_tasks(self):
         '''
-        Once this function is launched, the backend can start pushing
-        tasks to gtg parameters.
+        This function starts submitting the tasks from the backend into GTG
+        core.
+        It's run as a separate thread.
 
         @return: start_get_tasks() might not return or finish
         '''
-        raise NotImplemented()
+        return
 
     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.
+        This function is called from GTG core whenever a task should be
+        saved, either because it's a new one or it has been modified.
+        If the task id is new for the backend, then a new task must be
+        created. No special notification that the task is a new one is given.
+
+        @param task: the task object to save
         '''
         pass
 
     def remove_task(self, tid):
-        ''' Completely remove the task with ID = tid '''
+        ''' This function is called from GTG core whenever a task must be
+        removed from the backend. Note that the task could be not present here.
+        
+        @param tid: the id of the task to delete
+        '''
         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 []
+        Optional, and almost surely not needed.
+        Called upon the very first GTG startup.
+        This function is needed only in the default backend (XML localfile,
+        currently).
+        The xml parameter is an object containing GTG default tasks.
+        
+        @param xml: an xml object containing the default tasks.
+        '''
+        pass
+
+#NOTE: task counting is disabled in the UI, so I've disabled it here
+#      (invernizzi)
+#    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()
+
+#NOTE: I'm disabling this since support for runtime checking of the
+#        presence of the necessary modules is disabled. (invernizzi)
+#    @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
+        Called when GTG quits or the user wants to disable the backend.
+        
+        @param disable: If disable is True, the backend won't
+                        be automatically loaded when GTG starts
         '''
         self._is_initialized = False
         if disable:
@@ -179,6 +183,44 @@
 ###### You don't need to reimplement the functions below this line ############
 ###############################################################################
 
+   ###########################################################################
+   ### CONSTANTS #############################################################
+   ###########################################################################
+    #BACKEND TYPE DESCRIPTION
+    # Each backend must have a "_general_description" attribute, which
+    # 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"
+
+
+    #"static_parameters" is a dictionary of dictionaries, each of which
+    # are a description of a parameter needed to configure the backend and
+    # is identified in the outer dictionary by a key which is the name of the
+    # parameter.
+    # For an example, see the GTG/backends/backend_localfile.py file
+    # Each dictionary contains the keys:
+    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
+
     #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
@@ -189,7 +231,11 @@
     KEY_ATTACHED_TAGS = "attached-tags"
     KEY_USER = "user"
     KEY_PID = "pid"
-    ALLTASKS_TAG = "gtg-tags-all"  #IXME: moved here to avoid circular imports
+    ALLTASKS_TAG = "gtg-tags-all" #NOTE: this has been moved here to avoid
+                                  #    circular imports. It's the same as in
+                                  #    the CoreConfig class, because it's the
+                                  #    same thing conceptually. It doesn't
+                                  #    matter it the naming diverges.
 
     _static_parameters_obligatory = { \
                                     KEY_DEFAULT_BACKEND: { \
@@ -229,28 +275,30 @@
         '''
         Helper method, used to obtain the full list of the static_parameters
         (user configured and default ones)
+
+        @returns dict: the dict containing all the static parameters
         '''
-        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 = 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
-            return temp_dic 
-        else:
-            raise NotImplemented("_static_parameters not implemented for " + \
-                                 "backend %s" % type(cls))
+        for key, value in cls._static_parameters.iteritems():
+            temp_dic[key] = value
+        return temp_dic 
 
     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.
+        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:
+            #if it's not specified, then this is the default backend
+            #(for retro-compatibility with the GTG 0.2 series)
             parameters[self.KEY_DEFAULT_BACKEND] = True
+        #default backends should get all the tasks
         if parameters[self.KEY_DEFAULT_BACKEND] or \
                 (not self.KEY_ATTACHED_TAGS in parameters and \
                 self._general_description[self.BACKEND_TYPE] \
@@ -259,12 +307,17 @@
         self._parameters = parameters
         self._signal_manager = BackendSignals()
         self._is_initialized = False
+        #if debugging mode is enabled, tasks should be saved as soon as they're
+        # marked as modified. If in normal mode, we prefer speed over easier
+        # debugging.
         if Log.is_debugging_mode():
             self.timer_timestep = 5
         else:
             self.timer_timestep = 1 
         self.to_set_timer = None
         self.please_quit = False
+        self.cancellation_point = lambda: _cancellation_point(\
+                                        lambda: self.please_quit)
         self.to_set = deque()
         self.to_remove = deque()
 
@@ -274,6 +327,9 @@
         '''
         if hasattr(self._parameters, self.KEY_DEFAULT_BACKEND) and \
                    self._parameters[self.KEY_DEFAULT_BACKEND]:
+            #default backends should get all the tasks
+            #NOTE: this shouldn't be needed, but it doesn't cost anything and it
+            #      could avoid potential tasks losses.
             return [self.ALLTASKS_TAG]
         try:
             return self._parameters[self.KEY_ATTACHED_TAGS]
@@ -283,6 +339,8 @@
     def set_attached_tags(self, tags):
         '''
         Changes the set of attached tags
+
+        @param tags: the new attached_tags set
         '''
         self._parameters[self.KEY_ATTACHED_TAGS] = tags
 
@@ -300,6 +358,12 @@
         return self._parameters
 
     def set_parameter(self, parameter, value):
+        '''
+        Change a parameter for this backend
+
+        @param parameter: the parameter name
+        @param value: the new value
+        '''
         self._parameters[parameter] = value
 
     @classmethod
@@ -330,24 +394,22 @@
     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).
+
+        @param key: the key to extract
         '''
-        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)))
+        return cls._general_description[key]
 
     @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.
+
+        @param param_value: the actual value of the parameter, in a string
+                            format
+        @param param_type: the wanted type
+        @returns something: the casted param_value
         '''
-        #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:
@@ -374,6 +436,10 @@
     def cast_param_type_to_string(self, param_type, param_value):
         '''
         Inverse of cast_param_type_from_string
+
+        @param param_value: the actual value of the parameter
+        @param param_type: the type of the parameter (password...)
+        @returns something: param_value casted to string
         '''
         if param_type == GenericBackend.TYPE_PASSWORD:
             if param_value == None:
@@ -391,20 +457,27 @@
     def get_id(self):
         '''
         returns the backends id, used in the datastore for indexing backends
+
+        @returns string: the backend id
         '''
         return self.get_name() + "@" + self._parameters["pid"]
 
     @classmethod
     def get_human_default_name(cls):
         '''
-        returns the user friendly default backend name. 
+        returns the user friendly default backend name, without eventual user
+        modifications.
+
+        @returns string: the default "human 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
+        customized it, returns the default one.
+
+        @returns string: the "human name" of this backend
         '''
         if self.KEY_HUMAN_NAME in self._parameters and \
                     self._parameters[self.KEY_HUMAN_NAME] != "":
@@ -415,6 +488,8 @@
     def set_human_name(self, name):
         '''
         sets a custom name for the backend
+
+        @param name: the new name
         '''
         self._parameters[self.KEY_HUMAN_NAME] = name
         #we signal the change
@@ -423,29 +498,49 @@
     def is_enabled(self):
         '''
         Returns if the backend is enabled
+
+        @returns bool
         '''
         return self.get_parameters()[GenericBackend.KEY_ENABLED] or \
-               self.is_default()
+                self.is_default()
 
     def is_default(self):
         '''
         Returns if the backend is enabled
+
+        @returns bool
         '''
         return self.get_parameters()[GenericBackend.KEY_DEFAULT_BACKEND]
 
     def is_initialized(self):
         '''
         Returns if the backend is up and running
+
+        @returns is_initialized
         '''
         return self._is_initialized
 
     def get_parameter_type(self, param_name):
+        '''
+        Given the name of a parameter, returns its type. If the parameter is one
+        of the default ones, it does not have a type: in that case, it returns
+        None
+
+        @param param_name: the name of the parameter
+        @returns string: the type, or None
+        '''
         try:
             return self.get_static_parameters()[param_name][self.PARAM_TYPE]
-        except KeyError:
+        except:
             return None
 
     def register_datastore(self, datastore):
+        '''
+        Setter function to inform the backend about the datastore that's loading
+        it.
+
+        @param datastore: a Datastore
+        '''
         self.datastore = datastore
 
 ###############################################################################
@@ -455,6 +550,7 @@
     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
@@ -467,15 +563,13 @@
             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
@@ -485,11 +579,29 @@
         if not os.path.exists(path):
             return default_value
         else:
-            try:
-                with open(path, 'r') as file:
+            with open(path, 'r') as file:
+                try:
                     return pickle.load(file)
-            except pickle.PickleError:
-                return default_value
+                except pickle.PickleError:
+                    Log.error("PICKLE ERROR")
+                    return default_value
+
+    def _gtg_task_is_syncable_per_attached_tags(self, task):
+        '''
+        Helper function which checks if the given task satisfies the filtering
+        imposed by the tags attached to the backend.
+        That means, if a user wants a backend to sync only tasks tagged @works,
+        this function should be used to check if that is verified.
+
+        @returns bool: True if the task should be synced
+        '''
+        attached_tags = self.get_attached_tags()
+        if GenericBackend.ALLTASKS_TAG in attached_tags:
+            return True
+        for tag in task.get_tags_name():
+            if tag in attached_tags:
+                return  True
+        return False
 
 ###############################################################################
 ### THREADING #################################################################
@@ -504,25 +616,29 @@
                                         self.launch_setting_thread)
             self.to_set_timer.start()
 
-    def launch_setting_thread(self):
+    def launch_setting_thread(self, bypass_quit_request = False):
         '''
         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
+        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
+
+        @param bypass_quit_request: if True, the thread should not be stopped
+                                    even if asked by self.please_quit = True.
+                                    It's used when the backend quits, to finish
+                                    syncing all pending tasks
         '''
-        while not self.please_quit:
+        while not self.please_quit or bypass_quit_request:
             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:
+        while not self.please_quit or bypass_quit_request:
             try:
                 tid = self.to_remove.pop()
             except IndexError:
@@ -532,7 +648,12 @@
         self.to_set_timer = None
 
     def queue_set_task(self, task):
-        ''' Save the task in the backend. '''
+        ''' Save the task in the backend. In particular, it just enqueues the
+        task in the self.to_set queue. A thread will shortly run to apply the
+        requested changes.
+        
+        @param task: the task that should be saved
+        '''
         tid = task.get_id()
         if task not in self.to_set and tid not in self.to_remove:
             self.to_set.appendleft(task)
@@ -540,7 +661,10 @@
 
     def queue_remove_task(self, tid):
         '''
-        Queues task to be removed.
+        Queues task to be removed. In particular, it just enqueues the
+        task in the self.to_remove queue. A thread will shortly run to apply the
+        requested changes.
+
         @param tid: The Task ID of the task to be removed
         '''
         if tid not in self.to_remove:
@@ -553,8 +677,6 @@
         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:
@@ -562,10 +684,10 @@
             except:
                 pass
             try:
-                self.to_set_timer.join(5)
+                self.to_set_timer.join()
             except:
                 pass
-        self.please_quit = False
-        self.launch_setting_thread()
+        self.launch_setting_thread(bypass_quit_request = True)
         self.save_state()
 
+

=== modified file 'GTG/core/datastore.py'
--- GTG/core/datastore.py	2010-06-23 10:54:11 +0000
+++ GTG/core/datastore.py	2010-08-13 23:45:01 +0000
@@ -18,8 +18,8 @@
 # -----------------------------------------------------------------------------
 
 """
-The DaataStore contains a list of TagSource objects, which are proxies
-between a backend and the datastore itself
+Contains the Datastore object, which is the manager of all the active backends
+(both enabled and disabled ones)
 """
 
 import threading
@@ -34,7 +34,6 @@
 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
@@ -59,11 +58,17 @@
         self.requester = requester.Requester(self)
         self.tagstore = tagstore.TagStore(self.requester)
         self._backend_signals = BackendSignals()
-        self.mutex = threading.RLock()
-        self.is_default_backend_loaded = False
+        self.please_quit = False #when turned to true, all pending operation
+                                 # should be completed and then GTG should quit
+        self.is_default_backend_loaded = False #the default backend must be
+                                               # loaded before anyone else.
+                                               # This turns to True when the
+                                               # default backend loading has
+                                               # finished.
         self._backend_signals.connect('default-backend-loaded', \
                                       self._activate_non_default_backends)
         self.filtered_datastore = FilteredDataStore(self)
+        self._backend_mutex = threading.Lock()
 
     ##########################################################################
     ### Helper functions (get_ methods for Datastore embedded objects)
@@ -72,6 +77,7 @@
     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
@@ -79,15 +85,17 @@
     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
+                                               this datastore
         '''
         return self.requester
         
     def get_tasks_tree(self):
         '''
         Helper function to get a Tree with all the tasks contained in this
-        Datastore
+        Datastore.
+
         @returns GTG.core.tree.Tree: a task tree (the main one)
         '''
         return self.open_tasks
@@ -99,6 +107,7 @@
     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()
@@ -107,6 +116,7 @@
         '''
         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 bool: True if the task is present
         '''
@@ -116,6 +126,7 @@
         '''
         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
         @returns GTG.core.task.Task or None:  whether the Task is present
         or not
@@ -123,12 +134,13 @@
         if self.has_task(tid):
             return self.open_tasks.get_node(tid)
         else:
-            Log.debug("requested non-existent task")
+            Log.error("requested non-existent task")
             return 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
@@ -141,6 +153,7 @@
         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.
         """
         task = self.task_factory(uuid.uuid4(), True)
@@ -148,10 +161,13 @@
         return task
         
     @synchronized
-    def push_task(self, task, backend_capabilities = 'bypass for now'):
+    def push_task(self, task):
         '''
         Adds the given task object to the task tree. In other words, registers
         the given task in the GTG task set.
+        This function is used in mutual exclusion: only a backend at a time is
+        allowed to push tasks.
+
         @param task: A valid task object  (a GTG.core.task.Task)
         @return bool: True if the task has been accepted
         '''
@@ -172,10 +188,10 @@
     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:
@@ -184,10 +200,11 @@
 
     def get_backend(self, backend_id):
         '''
-        Returns a backend given its 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
+                                                        or None
         '''
         if backend_id in self.backends:
             return self.backends[backend_id]
@@ -197,20 +214,24 @@
     def register_backend(self, backend_dic):
         """
         Registers a TaskSource as a backend for this DataStore
+
         @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")
+                            parameters to initialize the backend
+                            (filename...). It should also contain the
+                            backend class (under "backend"), and its
+                            unique id (under "pid")
         """
         if "backend" in backend_dic:
             if "pid" not in backend_dic:
-                Log.debug("registering a backend without pid.")
+                Log.error("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")
+                Log.error("registering already registered backend")
                 return None
+            #creating the TaskSource which will wrap the backend,
+            # filtering the tasks that should hit the backend.
             source = TaskSource(requester = self.requester,
                                 backend = backend,
                                 datastore = self.filtered_datastore)
@@ -234,23 +255,46 @@
                 source.start_get_tasks()
             return source
         else:
-            Log.debug("Tried to register a backend without a  pid")
+            Log.error("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.
+
+        @param sender: not used, just here for signal compatibility
         '''
         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())
+                self._backend_startup(backend)
+
+    def _backend_startup(self, backend):
+        '''
+        Helper function to launch a thread that starts a backend.
+
+        @param backend: the backend object
+        '''
+        def __backend_startup(self, backend):
+            '''
+            Helper function to start a backend
+
+            @param backend: the backend object
+            '''
+            backend.initialize()
+            backend.start_get_tasks()
+            self.flush_all_tasks(backend.get_id())
+
+        thread = threading.Thread(target = __backend_startup,
+                                          args = (self, backend))
+        thread.setDaemon(True)
+        thread.start()
 
     def set_backend_enabled(self, backend_id, state):
         """
@@ -262,6 +306,7 @@
         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
         """
@@ -270,11 +315,12 @@
             current_state = backend.is_enabled()
             if current_state == True and state == False:
                 #we disable the backend
-                backend.quit(disable = True)
+                #FIXME!!!
+                threading.Thread(target = backend.quit, \
+                                 kwargs = {'disable': True}).start()
             elif current_state == False and state == True:
                 if self.is_default_backend_loaded == True:
-                    backend.initialize()
-                    self.flush_all_tasks(backend_id)
+                    self._backend_startup(backend)
                 else:
                     #will be activated afterwards
                     backend.set_parameter(GenericBackend.KEY_ENABLED,
@@ -283,13 +329,19 @@
     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()
+            #FIXME: to keep things simple, backends are not notified that they
+            #       are completely removed (they think they're just
+            #       deactivated). We should add a "purge" call to backend to let
+            #       them know that they're removed, so that they can remove all
+            #       the various files they've created. (invernizzi)
+
             #we notify that the backend has been deleted
             self._backend_signals.backend_removed(backend.get_id())
             del self.backends[backend_id]
@@ -297,6 +349,7 @@
     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.
@@ -312,43 +365,59 @@
         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():
+                if self.please_quit:
+                    break
                 backend.queue_set_task(None, task_id)
-        t = threading.Thread(target = _internal_flush_all_tasks).start()
+        t = threading.Thread(target = _internal_flush_all_tasks)
+        t.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
         '''
+        try:
+            self.start_get_tasks_thread.join()
+        except Exception, e:
+            pass
         doc,xmlconfig = cleanxml.emptydoc("config")
         #we ask all the backends to quit first.
         if quit:
+            #we quit backends in parallel
+            threads_dic = {}
             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()
+                thread = threading.Thread(target = b.quit)
+                threads_dic[b.get_id()] = thread
+                thread.start()
+            for backend_id, thread in threads_dic.iteritems():
+                #after 20 seconds, we give up
+                thread.join(20)
+                if thread.isAlive():
+                    Log.error("The %s backend stalled while quitting", 
+                              backend_id)
         #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
+                    #We don't want parameters, backend, xmlobject: we'll create
+                    # them at next startup
                     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()
@@ -356,9 +425,21 @@
     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)
+    
+    def get_backend_mutex(self):
+        '''
+        Returns the mutex object used by backends to avoid modifying a task
+        at the same time.
+
+        @returns threading.Lock
+        '''
+        return self._backend_mutex
+
+
 
 
 class TaskSource():
@@ -366,9 +447,12 @@
     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 requester: a Requester
         @param backend:  the backend being wrapped
         @param datastore: a FilteredDatastore
@@ -378,6 +462,7 @@
         self.backend.register_datastore(datastore)
         self.to_set = deque()
         self.to_remove = deque()
+        self.please_quit = False
         self.task_filter = self.get_task_filter_for_backend()
         if Log.is_debugging_mode():
             self.timer_timestep = 5
@@ -391,7 +476,10 @@
         ''''
         Maps the TaskSource to the backend and starts threading.
         '''
-        threading.Thread(target = self.__start_get_tasks).start()
+        self.start_get_tasks_thread = \
+             threading.Thread(target = self.__start_get_tasks)
+        self.start_get_tasks_thread.setDaemon(True)
+        self.start_get_tasks_thread.start()
 
     def __start_get_tasks(self):
         '''
@@ -405,7 +493,7 @@
 
     def get_task_filter_for_backend(self):
         '''
-        Fiter that checks if the task should be stored in this backend.
+        Filter 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
@@ -417,6 +505,7 @@
     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
         '''
@@ -427,6 +516,7 @@
         """
         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.
         """
@@ -437,14 +527,17 @@
         else:
             self.queue_remove_task(None, tid)
             
-    def launch_setting_thread(self):
+    def launch_setting_thread(self, bypass_please_quit = False):
         '''
         Operates the threads to set and remove tasks.
         Releases the lock when it is done.
+
+        @param bypass_please_quit: if True, the self.please_quit "quit condition"
+                                   is ignored. Currently, it's turned to true
+                                   after the quit condition has been issued, to
+                                   execute eventual pending operations.
         '''
-        #FIXME: the lock should be general for all backends. Therefore, it
-        #should be handled in the datastore
-        while True:
+        while not self.please_quit or bypass_please_quit:
             try:
                 tid = self.to_set.pop()
             except IndexError:
@@ -457,7 +550,7 @@
                    self.req.has_task(tid):
                 task = self.req.get_task(tid)
                 self.backend.queue_set_task(task)
-        while True:
+        while not self.please_quit or bypass_please_quit:
             try:
                 tid = self.to_remove.pop()
             except IndexError:
@@ -469,6 +562,7 @@
     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
         '''
@@ -480,14 +574,16 @@
         '''
         Helper function to launch the setting thread, if it's not running
         '''
-        if self.to_set_timer == None:
+        if self.to_set_timer == None and not self.please_quit:
             self.to_set_timer = threading.Timer(self.timer_timestep, \
                                         self.launch_setting_thread)
+            self.to_set_timer.setDaemon(True)
             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()
@@ -520,23 +616,28 @@
         '''
         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()
+        try:
+            self.to_set_timer.cancel()
+        except Exception, e:
+            pass
+        try:
+            self.to_set_timer.join(3)
+        except Exception, e:
+            pass
+        try:
+            self.start_get_tasks_thread.join(3)
+        except:
+            pass
+        self.launch_setting_thread(bypass_please_quit = True)
 
     def quit(self, disable = False):
         '''
         Quits the backend and disconnect the signals
+
         @param disable: if True, the backend is disabled.
         '''
         self._disconnect_signals()
+        self.please_quit = True
         self.sync()
         self.backend.quit(disable)
     
@@ -544,6 +645,7 @@
         '''
         Delegates all the functions not defined here to the real backend
         (standard python function)
+
         @param attr: attribute to get
         '''
         if attr in self.__dict__: 
@@ -566,12 +668,15 @@
         self.datastore = datastore
 
     def __getattr__(self, attr):
-        if attr in ['task_factory', \
+        if attr in ['task_factory',
                     'push_task',
                     'get_task',
                     'has_task',
+                    'get_backend_mutex',
                     'request_task_deletion']:
             return getattr(self.datastore, attr)
+        elif attr in ['get_all_tags']:
+            return self.datastore.requester.get_all_tags
         else:
             raise AttributeError
 

=== modified file 'GTG/core/filteredtree.py'
--- GTG/core/filteredtree.py	2010-08-02 19:07:24 +0000
+++ GTG/core/filteredtree.py	2010-08-13 23:45:01 +0000
@@ -791,7 +791,10 @@
             self.node_to_remove.append(tid)
             isroot = False
             if tid in self.displayed_nodes:
-                isroot = self.__is_root(self.get_node(tid))
+                node = self.get_node(tid)
+                if not node:
+                    return
+                isroot = self.__is_root(node)
                 self.remove_count += 1
                 self.__nodes_count -= 1
                 self.emit('task-deleted-inview',tid)

=== modified file 'GTG/core/filters_bank.py'
--- GTG/core/filters_bank.py	2010-08-02 19:09:17 +0000
+++ GTG/core/filters_bank.py	2010-08-13 23:45:01 +0000
@@ -256,6 +256,8 @@
         @param task: a task object
         @oaram tags_to_match_set: a *set* of tag names
         '''
+        if task == None:
+            return []
         try:
             tags_to_match_set = parameters['tags']
         except KeyError:

=== modified file 'GTG/core/requester.py'
--- GTG/core/requester.py	2010-06-22 19:55:15 +0000
+++ GTG/core/requester.py	2010-08-13 23:45:01 +0000
@@ -284,3 +284,6 @@
 
     def backend_change_attached_tags(self, backend_id, tags):
         return self.ds.backend_change_attached_tags(backend_id, tags)
+
+    def save_datastore(self):
+        return self.ds.save()

=== modified file 'GTG/core/task.py'
--- GTG/core/task.py	2010-07-01 09:59:33 +0000
+++ GTG/core/task.py	2010-08-13 23:45:01 +0000
@@ -505,7 +505,9 @@
     def get_tags(self):
         l = []
         for tname in self.tags:
-            l.append(self.req.get_tag(tname))
+            tag = self.req.get_tag(tname)
+            if tag:
+                l.append(tag)
         return l
         
     def rename_tag(self, old, new):

=== modified file 'GTG/gtk/browser/browser.py'
--- GTG/gtk/browser/browser.py	2010-08-10 17:30:24 +0000
+++ GTG/gtk/browser/browser.py	2010-08-13 23:45:01 +0000
@@ -953,7 +953,9 @@
                     text = \
                         text.replace("%s%s:%s" % (spaces, attribute, args), "")
             # Create the new task
-            task = self.req.new_task(tags=[t.get_name() for t in tags], newtask=True)
+            task = self.req.new_task( newtask=True)
+            for tag in tags:
+                task.add_tag(tag.get_name())
             if text != "":
                 task.set_title(text.strip())
                 task.set_to_keep()

=== modified file 'GTG/tests/test_backends.py'
--- GTG/tests/test_backends.py	2010-06-23 12:49:28 +0000
+++ GTG/tests/test_backends.py	2010-08-13 23:45:01 +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
@@ -30,8 +30,8 @@
 
 # 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):
@@ -44,6 +44,10 @@
         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.
@@ -105,52 +109,6 @@
         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'

=== added file 'GTG/tests/test_interruptible.py'
--- GTG/tests/test_interruptible.py	1970-01-01 00:00:00 +0000
+++ GTG/tests/test_interruptible.py	2010-08-13 23:45:01 +0000
@@ -0,0 +1,61 @@
+# -*- 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 interrupting cooperative threads
+'''
+
+import unittest
+import time
+from threading import Thread, Event
+
+from GTG.tools.interruptible import interruptible, _cancellation_point
+
+
+class TestInterruptible(unittest.TestCase):
+    '''
+    Tests for interrupting cooperative threads
+    '''
+
+    def test_interruptible_decorator(self):
+        '''Tests for the @interruptible decorator.'''
+        self.quit_condition = False
+        cancellation_point = lambda: _cancellation_point(\
+                                        lambda: self.quit_condition)
+        self.thread_started = Event()
+        @interruptible
+        def never_ending(cancellation_point):
+            self.thread_started.set()
+            while True:
+                time.sleep(0.1)
+                cancellation_point()
+        thread = Thread(target = never_ending, args = (cancellation_point, ))
+        thread.start()
+        self.thread_started.wait()
+        self.quit_condition = True
+        countdown = 10
+        while thread.is_alive() and countdown > 0:
+            time.sleep(0.1)
+            countdown -= 1
+        self.assertFalse(thread.is_alive())
+
+
+def test_suite():
+    return unittest.TestLoader().loadTestsFromTestCase(TestInterruptible)
+

=== added file 'GTG/tools/interruptible.py'
--- GTG/tools/interruptible.py	1970-01-01 00:00:00 +0000
+++ GTG/tools/interruptible.py	2010-08-13 23:45:01 +0000
@@ -0,0 +1,62 @@
+# -*- 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/>.
+# -----------------------------------------------------------------------------
+
+'''
+Utils to stop and quit gracefully a thread, issuing the command from
+another one
+'''
+
+
+
+class Interrupted(Exception):
+    '''Exception raised when a thread should be interrupted'''
+
+
+    pass
+
+
+
+
+def interruptible(fn):
+    '''
+    A decorator that makes a function interruptible. It should be applied only
+    to the function which is the target of a Thread object.
+    '''
+
+
+    def new(*args):
+        try:
+            return fn(*args)
+        except Interrupted:
+            return
+    return new
+
+def _cancellation_point(test_function):
+    '''
+    This function checks a test_function and, if it evaluates to True, makes the
+    thread quit (similar to pthread_cancel() in C)
+    It starts with a _ as it's mostly used in a specialized form, as:
+    cancellation_point = functools.partial(_cancellation_point,
+                                           lambda: quit_condition == True)
+
+    @param test_function: the function to test before cancelling
+    '''
+    if test_function():
+        raise Interrupted
+

=== modified file 'GTG/tools/taskxml.py'
--- GTG/tools/taskxml.py	2010-06-22 19:55:15 +0000
+++ GTG/tools/taskxml.py	2010-08-13 23:45:01 +0000
@@ -20,6 +20,7 @@
 #Functions to convert a Task object to an XML string and back
 import xml.dom.minidom
 import xml.sax.saxutils as saxutils
+import datetime
 
 from GTG.tools import cleanxml
 from GTG.tools import dates
@@ -55,7 +56,6 @@
             content = xml.dom.minidom.parseString(tas)
             cur_task.set_text(content.firstChild.toxml()) #pylint: disable-msg=E1103 
     cur_task.set_due_date(dates.strtodate(cleanxml.readTextNode(xmlnode,"duedate")))
-    cur_task.set_modified(cleanxml.readTextNode(xmlnode,"modified"))
     cur_task.set_start_date(dates.strtodate(cleanxml.readTextNode(xmlnode,"startdate")))
     cur_tags = xmlnode.getAttribute("tags").replace(' ','').split(",")
     if "" in cur_tags: cur_tags.remove("")
@@ -69,6 +69,11 @@
             backend_id = node.firstChild.nodeValue
             remote_task_id = node.childNodes[1].firstChild.nodeValue
             task.add_remote_id(backend_id, remote_task_id)
+    modified_string = cleanxml.readTextNode(xmlnode,"modified")
+    if modified_string:
+        modified_datetime = datetime.datetime.strptime(modified_string,\
+                                                    "%Y-%m-%dT%H:%M:%S")
+        cur_task.set_modified(modified_datetime)
     return cur_task
 
 #Task as parameter the doc where to put the XML node

=== added file 'data/icons/hicolor/scalable/apps/backend_localfile.svg'
--- data/icons/hicolor/scalable/apps/backend_localfile.svg	1970-01-01 00:00:00 +0000
+++ data/icons/hicolor/scalable/apps/backend_localfile.svg	2010-08-13 23:45:01 +0000
@@ -0,0 +1,610 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:xlink="http://www.w3.org/1999/xlink";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   inkscape:export-ydpi="600"
+   inkscape:export-xdpi="600"
+   inkscape:export-filename="/home/luca/Projects/gtg/Google/GSoC/data/icons/hicolor/scalable/apps/backend_localfile.png"
+   sodipodi:docname="backend_localfile.svg"
+   inkscape:version="0.47 r22583"
+   sodipodi:version="0.32"
+   id="svg249"
+   height="48.000000px"
+   width="48.000000px"
+   inkscape:output_extension="org.inkscape.output.svg.inkscape"
+   version="1.1">
+  <defs
+     id="defs3">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 24 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="48 : 24 : 1"
+       inkscape:persp3d-origin="24 : 16 : 1"
+       id="perspective80" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3656"
+       id="linearGradient5816"
+       gradientUnits="userSpaceOnUse"
+       x1="-26.753757"
+       y1="11.566258"
+       x2="-24.75"
+       y2="9.687501" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3520"
+       id="linearGradient5836"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.9223058,0,0,0.9185751,-92.447368,61.3257)"
+       x1="-18.588562"
+       y1="11.052948"
+       x2="-28.789402"
+       y2="14.069944" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3671"
+       id="radialGradient5839"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.4073362,-0.2798276,0.7510293,1.0932492,-115.18484,51.56213)"
+       cx="-26.305403"
+       cy="10.108011"
+       fx="-26.305403"
+       fy="10.108011"
+       r="7.0421038" />
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient6469">
+      <stop
+         style="stop-color:#000000;stop-opacity:1;"
+         offset="0"
+         id="stop6471" />
+      <stop
+         style="stop-color:#000000;stop-opacity:0;"
+         offset="1"
+         id="stop6473" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient6469"
+       id="linearGradient6475"
+       x1="58.282169"
+       y1="70.751839"
+       x2="61.181217"
+       y2="67.799171"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(-180,0)" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3741"
+       id="radialGradient5810"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.8860258,0,0,1.1764706,-3.5441033,-4.2352941)"
+       cx="4"
+       cy="5.2999997"
+       fx="4"
+       fy="5.2999997"
+       r="17" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3613"
+       id="linearGradient5845"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(-90,60)"
+       x1="-47.5"
+       y1="49.020683"
+       x2="-62.75"
+       y2="-22.502075" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3683"
+       id="radialGradient5843"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(3.9957492,0,0,1.9350367,0.62141,28.832578)"
+       cx="-30.249996"
+       cy="35.357208"
+       fx="-30.249996"
+       fy="35.357208"
+       r="18.000002" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3702"
+       id="linearGradient5804"
+       gradientUnits="userSpaceOnUse"
+       x1="25.058096"
+       y1="47.027729"
+       x2="25.058096"
+       y2="39.999443" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3688"
+       id="radialGradient5802"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(2.003784,0,0,1.4,-20.01187,-104.4)"
+       cx="4.9929786"
+       cy="43.5"
+       fx="4.9929786"
+       fy="43.5"
+       r="2.5" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3688"
+       id="radialGradient5800"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(2.003784,0,0,1.4,27.98813,-17.4)"
+       cx="4.9929786"
+       cy="43.5"
+       fx="4.9929786"
+       fy="43.5"
+       r="2.5" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3702"
+       id="linearGradient5798"
+       gradientUnits="userSpaceOnUse"
+       x1="25.058096"
+       y1="47.027729"
+       x2="25.058096"
+       y2="39.999443" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3688"
+       id="radialGradient5796"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(2.003784,0,0,1.4,-20.01187,-104.4)"
+       cx="4.9929786"
+       cy="43.5"
+       fx="4.9929786"
+       fy="43.5"
+       r="2.5" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3688"
+       id="radialGradient5794"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(2.003784,0,0,1.4,27.98813,-17.4)"
+       cx="4.9929786"
+       cy="43.5"
+       fx="4.9929786"
+       fy="43.5"
+       r="2.5" />
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient3656">
+      <stop
+         style="stop-color:#ffffff;stop-opacity:1;"
+         offset="0"
+         id="stop3658" />
+      <stop
+         style="stop-color:#ffffff;stop-opacity:0;"
+         offset="1"
+         id="stop3660" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient3520">
+      <stop
+         style="stop-color:#000000;stop-opacity:0.41295547"
+         offset="0"
+         id="stop3522" />
+      <stop
+         style="stop-color:#000000;stop-opacity:0;"
+         offset="1"
+         id="stop3524" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3671">
+      <stop
+         style="stop-color:#ffffff;stop-opacity:1;"
+         offset="0"
+         id="stop3673" />
+      <stop
+         id="stop3691"
+         offset="0.47533694"
+         style="stop-color:#ffffff;stop-opacity:1;" />
+      <stop
+         style="stop-color:#ffffff;stop-opacity:0;"
+         offset="1"
+         id="stop3675" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient3741">
+      <stop
+         style="stop-color:#ffffff;stop-opacity:1;"
+         offset="0"
+         id="stop3743" />
+      <stop
+         style="stop-color:#ffffff;stop-opacity:0;"
+         offset="1"
+         id="stop3745" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient3613">
+      <stop
+         style="stop-color:#888a85;stop-opacity:1"
+         offset="0"
+         id="stop3615" />
+      <stop
+         style="stop-color:#babdb6;stop-opacity:1"
+         offset="1"
+         id="stop3617" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3683">
+      <stop
+         id="stop3685"
+         offset="0"
+         style="stop-color:#f6f6f5;stop-opacity:1;" />
+      <stop
+         id="stop3689"
+         offset="1"
+         style="stop-color:#d3d7cf;stop-opacity:1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3688"
+       inkscape:collect="always">
+      <stop
+         id="stop3690"
+         offset="0"
+         style="stop-color:black;stop-opacity:1;" />
+      <stop
+         id="stop3692"
+         offset="1"
+         style="stop-color:black;stop-opacity:0;" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3702">
+      <stop
+         id="stop3704"
+         offset="0"
+         style="stop-color:black;stop-opacity:0;" />
+      <stop
+         style="stop-color:black;stop-opacity:1;"
+         offset="0.5"
+         id="stop3710" />
+      <stop
+         id="stop3706"
+         offset="1"
+         style="stop-color:black;stop-opacity:0;" />
+    </linearGradient>
+  </defs>
+  <sodipodi:namedview
+     inkscape:window-y="26"
+     inkscape:window-x="230"
+     inkscape:window-height="742"
+     inkscape:window-width="1048"
+     inkscape:document-units="px"
+     inkscape:grid-bbox="true"
+     showgrid="false"
+     inkscape:current-layer="layer6"
+     inkscape:cy="9.7451355"
+     inkscape:cx="46.371834"
+     inkscape:zoom="1"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     borderopacity="0.25490196"
+     bordercolor="#666666"
+     pagecolor="#ffffff"
+     id="base"
+     inkscape:showpageshadow="false"
+     inkscape:window-maximized="0" />
+  <metadata
+     id="metadata4">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title>Generic Text</dc:title>
+        <dc:subject>
+          <rdf:Bag>
+            <rdf:li>text</rdf:li>
+            <rdf:li>plaintext</rdf:li>
+            <rdf:li>regular</rdf:li>
+            <rdf:li>document</rdf:li>
+          </rdf:Bag>
+        </dc:subject>
+        <cc:license
+           rdf:resource="http://creativecommons.org/licenses/GPL/2.0/"; />
+        <dc:creator>
+          <cc:Agent>
+            <dc:title>Lapo Calamandrei</dc:title>
+          </cc:Agent>
+        </dc:creator>
+        <dc:source />
+        <dc:date />
+        <dc:rights>
+          <cc:Agent>
+            <dc:title />
+          </cc:Agent>
+        </dc:rights>
+        <dc:publisher>
+          <cc:Agent>
+            <dc:title />
+          </cc:Agent>
+        </dc:publisher>
+        <dc:identifier />
+        <dc:relation />
+        <dc:language />
+        <dc:coverage />
+        <dc:description />
+        <dc:contributor>
+          <cc:Agent>
+            <dc:title />
+          </cc:Agent>
+        </dc:contributor>
+      </cc:Work>
+      <cc:License
+         rdf:about="http://creativecommons.org/licenses/GPL/2.0/";>
+        <cc:permits
+           rdf:resource="http://web.resource.org/cc/Reproduction"; />
+        <cc:permits
+           rdf:resource="http://web.resource.org/cc/Distribution"; />
+        <cc:requires
+           rdf:resource="http://web.resource.org/cc/Notice"; />
+        <cc:permits
+           rdf:resource="http://web.resource.org/cc/DerivativeWorks"; />
+        <cc:requires
+           rdf:resource="http://web.resource.org/cc/ShareAlike"; />
+        <cc:requires
+           rdf:resource="http://web.resource.org/cc/SourceCode"; />
+      </cc:License>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:groupmode="layer"
+     id="layer6"
+     inkscape:label="Shadow">
+    <g
+       style="display:inline"
+       id="g6015"
+       transform="translate(150,-60)">
+      <rect
+         y="60"
+         x="-150"
+         height="48"
+         width="48"
+         id="rect5504"
+         style="opacity:0;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;display:inline" />
+      <g
+         transform="matrix(1.0464281,0,0,0.8888889,-151.18572,65.72224)"
+         inkscape:label="Shadow"
+         id="g5508"
+         style="opacity:0.65587045;display:inline">
+        <g
+           transform="matrix(1.052632,0,0,1.285713,-1.263158,-13.42854)"
+           style="opacity:0.4"
+           id="g5511">
+          <rect
+             style="opacity:1;fill:url(#radialGradient5794);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+             id="rect5513"
+             width="5"
+             height="7"
+             x="38"
+             y="40" />
+          <rect
+             style="opacity:1;fill:url(#radialGradient5796);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+             id="rect5515"
+             width="5"
+             height="7"
+             x="-10"
+             y="-47"
+             transform="scale(-1,-1)" />
+          <rect
+             style="opacity:1;fill:url(#linearGradient5798);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+             id="rect5517"
+             width="28"
+             height="7.0000005"
+             x="10"
+             y="40" />
+        </g>
+      </g>
+      <g
+         transform="matrix(0.9548466,0,0,0.5555562,-148.98776,79.888875)"
+         inkscape:label="Shadow"
+         id="g5519"
+         style="display:inline">
+        <g
+           transform="matrix(1.052632,0,0,1.285713,-1.263158,-13.42854)"
+           style="opacity:0.4"
+           id="g5521">
+          <rect
+             style="opacity:1;fill:url(#radialGradient5800);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+             id="rect5523"
+             width="5"
+             height="7"
+             x="38"
+             y="40" />
+          <rect
+             style="opacity:1;fill:url(#radialGradient5802);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+             id="rect5525"
+             width="5"
+             height="7"
+             x="-10"
+             y="-47"
+             transform="scale(-1,-1)" />
+          <rect
+             style="opacity:1;fill:url(#linearGradient5804);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+             id="rect5527"
+             width="28"
+             height="7.0000005"
+             x="10"
+             y="40" />
+        </g>
+      </g>
+      <path
+         sodipodi:nodetypes="ccsccccccc"
+         id="path5529"
+         d="M -141.47614,63.5 C -141.47614,63.5 -124,63.5 -122.5,63.5 C -118.62295,63.572942 -116,66 -113.5,68.5 C -111,71 -108.89232,73.752625 -108.5,77.5 C -108.5,79 -108.5,102.47614 -108.5,102.47614 C -108.5,103.59736 -109.40264,104.5 -110.52385,104.5 L -141.47614,104.5 C -142.59736,104.5 -143.5,103.59736 -143.5,102.47614 L -143.5,65.523858 C -143.5,64.402641 -142.59736,63.5 -141.47614,63.5 z"
+         style="fill:url(#radialGradient5843);fill-opacity:1;stroke:url(#linearGradient5845);stroke-width:1;stroke-miterlimit:4;display:inline" />
+      <path
+         transform="translate(-150,60)"
+         d="M 8.53125,4 C 7.6730803,4 7,4.6730802 7,5.53125 L 7,42.46875 C 7,43.32692 7.6730802,44 8.53125,44 L 39.46875,44 C 40.326919,44 41,43.326918 41,42.46875 C 41,42.46875 41,19 41,17.5 C 41,16.10803 40.513021,13.200521 38.65625,11.34375 C 36.65625,9.34375 35.65625,8.34375 33.65625,6.34375 C 31.799479,4.4869792 28.89197,4 27.5,4 C 26,4 8.53125,4 8.53125,4 z"
+         id="path5531"
+         style="opacity:0.68016196;fill:url(#radialGradient5810);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;display:inline"
+         inkscape:original="M 8.53125 3.5 C 7.410033 3.5 6.5 4.4100329 6.5 5.53125 L 6.5 42.46875 C 6.5 43.589967 7.4100329 44.5 8.53125 44.5 L 39.46875 44.5 C 40.589967 44.5 41.5 43.589966 41.5 42.46875 C 41.5 42.46875 41.5 19 41.5 17.5 C 41.5 16 41 13 39 11 C 37 9 36 8 34 6 C 32 4 29 3.5 27.5 3.5 C 26 3.5 8.5312499 3.5 8.53125 3.5 z "
+         inkscape:radius="-0.4861359"
+         sodipodi:type="inkscape:offset" />
+      <path
+         id="rect5857"
+         d="M -138.59375,69.125 C -138.81243,69.125 -139,69.312565 -139,69.53125 C -139,69.749934 -138.81243,69.937499 -138.59375,69.9375 L -117.40625,69.9375 C -117.18757,69.9375 -117,69.749934 -117,69.53125 C -117,69.312566 -117.18757,69.125 -117.40625,69.125 L -138.59375,69.125 z M -138.53125,71.0625 C -138.79094,71.0625 -139,71.271563 -139,71.53125 C -139,71.790937 -138.79094,72 -138.53125,72 L -116.46875,72 C -116.20906,72 -116,71.790937 -116,71.53125 C -116,71.271563 -116.20906,71.0625 -116.46875,71.0625 L -138.53125,71.0625 z M -138.53125,73.0625 C -138.79094,73.0625 -139,73.271563 -139,73.53125 C -139,73.790937 -138.79094,74 -138.53125,74 L -113.34375,74 C -113.08406,74 -112.875,73.790937 -112.875,73.53125 C -112.875,73.271563 -113.08406,73.0625 -113.34375,73.0625 L -138.53125,73.0625 z M -138.53125,75.0625 C -138.79094,75.0625 -139,75.271563 -139,75.53125 C -139,75.790937 -138.79094,76 -138.53125,76 L -113.34375,76 C -113.08406,76 -112.875,75.790937 -112.875,75.53125 C -112.875,75.271563 -113.08406,75.0625 -113.34375,75.0625 L -138.53125,75.0625 z M -138.53125,77.0625 C -138.79094,77.0625 -139,77.271563 -139,77.53125 C -139,77.790937 -138.79094,78 -138.53125,78 L -113.34375,78 C -113.08406,78 -112.875,77.790937 -112.875,77.53125 C -112.875,77.271563 -113.08406,77.0625 -113.34375,77.0625 L -138.53125,77.0625 z"
+         style="opacity:0.15;fill:url(#linearGradient6475);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999982px;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
+      <path
+         sodipodi:nodetypes="ccccczc"
+         id="path5533"
+         d="M -122.5,64 C -123.88889,64 -122.54207,64.497088 -121.15625,65.125 C -119.77043,65.752912 -116.18337,68.340052 -117,72 C -112.67669,71.569417 -110.32087,75.122378 -110,76.28125 C -109.67913,77.440122 -109,78.888889 -109,77.5 C -108.97167,73.694419 -111.84543,71.068299 -113.84375,68.84375 C -115.84207,66.619201 -118.84621,64.476761 -122.5,64 z"
+         style="fill:url(#radialGradient5839);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;display:inline" />
+      <path
+         sodipodi:nodetypes="ccccc"
+         id="path5535"
+         d="M -121.39912,65.014353 C -120.47682,65.014353 -118.39068,71.210015 -119.31298,75.343603 C -115.01802,74.915844 -110.4596,75.43178 -110,76.28125 C -110.32087,75.122378 -112.67669,71.569417 -117,72 C -116.13534,68.124761 -120.18657,65.382702 -121.39912,65.014353 z"
+         style="opacity:0.87854249;fill:url(#linearGradient5836);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;display:inline" />
+      <path
+         transform="translate(-90,60)"
+         d="M -51.46875,4.5 C -52.051916,4.5 -52.5,4.9480842 -52.5,5.53125 L -52.5,42.46875 C -52.5,43.051915 -52.051914,43.5 -51.46875,43.5 L -20.53125,43.5 C -19.948085,43.5 -19.5,43.051914 -19.5,42.46875 C -19.5,42.46875 -19.5,19 -19.5,17.5 C -19.5,16.220971 -19.980469,13.394531 -21.6875,11.6875 C -23.6875,9.6875 -24.6875,8.6875 -26.6875,6.6875 C -28.394531,4.9804687 -31.220971,4.5 -32.5,4.5 C -34,4.5 -51.46875,4.5 -51.46875,4.5 z"
+         id="path5537"
+         style="fill:none;fill-opacity:1;stroke:url(#linearGradient5816);stroke-width:1;stroke-miterlimit:4;display:inline"
+         inkscape:original="M -51.46875 3.5 C -52.589967 3.5 -53.5 4.4100329 -53.5 5.53125 L -53.5 42.46875 C -53.5 43.589967 -52.589966 44.5 -51.46875 44.5 L -20.53125 44.5 C -19.410033 44.5 -18.5 43.589966 -18.5 42.46875 C -18.5 42.46875 -18.5 19 -18.5 17.5 C -18.5 16 -19 13 -21 11 C -23 9 -24 8 -26 6 C -28 4 -31 3.5 -32.5 3.5 C -34 3.5 -51.468749 3.5 -51.46875 3.5 z "
+         inkscape:radius="-0.99436891"
+         sodipodi:type="inkscape:offset" />
+      <g
+         inkscape:r_cy="true"
+         inkscape:r_cx="true"
+         transform="matrix(0.928889,0,0,1,-148.28889,60)"
+         style="opacity:0.15;fill:#000000;display:inline"
+         id="g5539">
+        <rect
+           ry="0.46875"
+           rx="0.50463516"
+           inkscape:r_cy="true"
+           inkscape:r_cx="true"
+           y="19.0625"
+           x="10"
+           height="0.9375"
+           width="28.125"
+           id="rect5549"
+           style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999982px;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
+        <rect
+           style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999982px;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+           id="rect5553"
+           width="28.125"
+           height="0.9375"
+           x="10"
+           y="21.0625"
+           inkscape:r_cx="true"
+           inkscape:r_cy="true"
+           rx="0.50463516"
+           ry="0.46875" />
+        <rect
+           style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999982px;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+           id="rect5555"
+           width="28.125"
+           height="0.9375"
+           x="10"
+           y="23.0625"
+           inkscape:r_cx="true"
+           inkscape:r_cy="true"
+           rx="0.50463516"
+           ry="0.46875" />
+        <rect
+           ry="0.46875"
+           rx="0.50463516"
+           inkscape:r_cy="true"
+           inkscape:r_cx="true"
+           y="25.0625"
+           x="10"
+           height="0.9375"
+           width="28.125"
+           id="rect5557"
+           style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999982px;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
+        <rect
+           ry="0.46875"
+           rx="0.50463516"
+           inkscape:r_cy="true"
+           inkscape:r_cx="true"
+           y="27.0625"
+           x="10"
+           height="0.9375"
+           width="28.125"
+           id="rect5559"
+           style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999982px;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
+        <rect
+           style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999982px;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+           id="rect5561"
+           width="28.125"
+           height="0.9375"
+           x="10"
+           y="29.0625"
+           inkscape:r_cx="true"
+           inkscape:r_cy="true"
+           rx="0.50463516"
+           ry="0.46875" />
+        <rect
+           style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999982px;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+           id="rect5563"
+           width="28.125"
+           height="0.9375"
+           x="10"
+           y="31.0625"
+           inkscape:r_cx="true"
+           inkscape:r_cy="true"
+           rx="0.50463516"
+           ry="0.46875" />
+        <rect
+           ry="0.46875"
+           rx="0.50463516"
+           inkscape:r_cy="true"
+           inkscape:r_cx="true"
+           y="33.0625"
+           x="10"
+           height="0.9375"
+           width="28.125"
+           id="rect5565"
+           style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999982px;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
+        <rect
+           ry="0.46875"
+           rx="0.50463516"
+           inkscape:r_cy="true"
+           inkscape:r_cx="true"
+           y="35.0625"
+           x="10"
+           height="0.9375"
+           width="28.125"
+           id="rect5567"
+           style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999982px;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
+        <rect
+           ry="0.46875"
+           rx="0.50463516"
+           inkscape:r_cy="true"
+           inkscape:r_cx="true"
+           y="37.0625"
+           x="10"
+           height="0.9375"
+           width="13.8125"
+           id="rect5569"
+           style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999982px;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
+      </g>
+    </g>
+  </g>
+  <g
+     style="display:inline"
+     inkscape:groupmode="layer"
+     inkscape:label="Base"
+     id="layer1" />
+  <g
+     inkscape:groupmode="layer"
+     id="layer5"
+     inkscape:label="Text"
+     style="display:inline" />
+</svg>


Follow ups