gtg-user team mailing list archive
-
gtg-user team
-
Mailing list archive
-
Message #00303
[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 users is subscribed to branch lp:~gtg-user/gtg/backends-main.
=== 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