GTG

Merge lp:~gtg-user/gtg/backends-main into lp:~gtg/gtg/old-trunk

Proposed by Luca Invernizzi
Status: Superseded
Proposed branch: lp:~gtg-user/gtg/backends-main
Merge into: lp:~gtg/gtg/old-trunk
Diff against target: 2430 lines (+1276/-312)
14 files modified
CHANGELOG (+1/-0)
GTG/backends/backend_localfile.py (+99/-71)
GTG/backends/genericbackend.py (+260/-138)
GTG/core/datastore.py (+156/-51)
GTG/core/filteredtree.py (+4/-1)
GTG/core/filters_bank.py (+2/-0)
GTG/core/requester.py (+3/-0)
GTG/core/task.py (+3/-1)
GTG/gtk/browser/browser.py (+3/-1)
GTG/tests/test_backends.py (+6/-48)
GTG/tests/test_interruptible.py (+61/-0)
GTG/tools/interruptible.py (+62/-0)
GTG/tools/taskxml.py (+6/-1)
data/icons/hicolor/scalable/apps/backend_localfile.svg (+610/-0)
To merge this branch: bzr merge lp:~gtg-user/gtg/backends-main
Reviewer Review Type Date Requested Status
Gtg developers Pending
Review via email: mp+32583@code.launchpad.net

This proposal has been superseded by a proposal from 2010-08-13.

Description of the change

Update of the already merged general backends framework and the localfile backend.
Adds some bug fixes, but the main contribution is documentation.

To post a comment you must log in.
lp:~gtg-user/gtg/backends-main updated
879. By Luca Invernizzi

added localfile png icon

880. By Luca Invernizzi

added localfile svg icon

881. By Luca Invernizzi

Icon is now in the correct place

882. By Luca Invernizzi

updated changelog

883. By Luca Invernizzi

merge with trunk

884. By Luca Invernizzi

backends can auto-flush themselves

885. By Luca Invernizzi

merge w/ trunk

886. By Luca Invernizzi

cherrypicking from my development branch

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'CHANGELOG'
--- CHANGELOG 2010-08-04 00:30:22 +0000
+++ CHANGELOG 2010-08-13 23:43:04 +0000
@@ -4,6 +4,7 @@
4 * Fixed bug with data consistency #579189, by Marko Kevac4 * Fixed bug with data consistency #579189, by Marko Kevac
5 * Added samba bugzilla to the bugzilla plugin, by Jelmer Vernoij5 * Added samba bugzilla to the bugzilla plugin, by Jelmer Vernoij
6 * Fixed bug #532392, a start date is later than a due date, by Volodymyr Floreskul6 * Fixed bug #532392, a start date is later than a due date, by Volodymyr Floreskul
7 * Extended backend system to support multiple backends by Luca Invernizzi
78
82010-03-01 Getting Things GNOME! 0.2.292010-03-01 Getting Things GNOME! 0.2.2
9 * Autostart on login, by Luca Invernizzi10 * Autostart on login, by Luca Invernizzi
1011
=== 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:43:04 +0000
@@ -20,6 +20,9 @@
20'''20'''
21Localfile is a read/write backend that will store your tasks in an XML file21Localfile is a read/write backend that will store your tasks in an XML file
22This file will be in your $XDG_DATA_DIR/gtg folder.22This file will be in your $XDG_DATA_DIR/gtg folder.
23
24This backend contains comments that are meant as a reference, in case someone
25wants to write a backend.
23'''26'''
2427
25import os28import os
@@ -29,18 +32,30 @@
29from GTG.core import CoreConfig32from GTG.core import CoreConfig
30from GTG.tools import cleanxml, taskxml33from GTG.tools import cleanxml, taskxml
31from GTG import _34from GTG import _
35from GTG.tools.logger import Log
3236
3337
3438
35class Backend(GenericBackend):39class Backend(GenericBackend):
40 '''
41 Localfile backend, which stores your tasks in a XML file in the standard
42 XDG_DATA_DIR/gtg folder (the path is configurable).
43 An instance of this class is used as the default backend for GTG.
44 This backend loads all the tasks stored in the localfile after it's enabled,
45 and from that point on just writes the changes to the file: it does not
46 listen for eventual file changes
47 '''
36 48
3749
38 DEFAULT_PATH = CoreConfig().get_data_dir() #default path for filenames50 DEFAULT_PATH = CoreConfig().get_data_dir() #default path for filenames
3951
4052
41 #Description of the backend (mainly it's data we show the user, only the53 #General description of the backend: these are used to show a description of
42 # name is used internally. Please note that BACKEND_NAME and54 # the backend to the user when s/he is considering adding it.
43 # BACKEND_ICON_NAME should *not* be translated.55 # BACKEND_NAME is the name of the backend used internally (it must be
56 # unique).
57 #Please note that BACKEND_NAME and BACKEND_ICON_NAME should *not* be
58 #translated.
44 _general_description = { \59 _general_description = { \
45 GenericBackend.BACKEND_NAME: "backend_localfile", \60 GenericBackend.BACKEND_NAME: "backend_localfile", \
46 GenericBackend.BACKEND_HUMAN_NAME: _("Local File"), \61 GenericBackend.BACKEND_HUMAN_NAME: _("Local File"), \
@@ -53,10 +68,14 @@
53 "for GTG to save your tasks."),\68 "for GTG to save your tasks."),\
54 }69 }
5570
56 #parameters to configure a new backend of this type.71 #These are the parameters to configure a new backend of this type. A
57 #NOTE: should we always give back a different default filename? it can be72 # parameter has a name, a type and a default value.
58 # done, but I'd like to keep this backend simple, so that it can be73 # Here, we define a parameter "path", which is a string, and has a default
59 # used as example (invernizzi)74 # value as a random file in the default path
75 #NOTE: to keep this simple, the filename default path is the same until GTG
76 # is restarted. I consider this a minor annoyance, and we can avoid
77 # coding the change of the path each time a backend is
78 # created (invernizzi)
60 _static_parameters = { \79 _static_parameters = { \
61 "path": { \80 "path": { \
62 GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING, \81 GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING, \
@@ -64,30 +83,22 @@
64 os.path.join(DEFAULT_PATH, "gtg_tasks-%s.xml" %(uuid.uuid4()))83 os.path.join(DEFAULT_PATH, "gtg_tasks-%s.xml" %(uuid.uuid4()))
65 }}84 }}
6685
67 def _get_default_filename_path(self, filename = None):
68 '''
69 Generates a default path with a random filename
70 @param filename: specify a filename
71 '''
72 if not filename:
73 filename = "gtg_tasks-%s.xml" % (uuid.uuid4())
74 return os.path.join(self.DEFAULT_PATH, filename)
75
76 def __init__(self, parameters):86 def __init__(self, parameters):
77 """87 """
78 Instantiates a new backend.88 Instantiates a new backend.
7989
80 @param parameters: should match the dictionary returned in90 @param parameters: A dictionary of parameters, generated from
81 get_parameters. Anyway, the backend should care if one expected91 _static_parameters. A few parameters are added to those, the list of
82 value is None or does not exist in the dictionary. 92 these is in the "DefaultBackend" class, look for the KEY_* constants.
83 @firstrun: only needed for the default backend. It should be93
84 omitted for all other backends.94 The backend should take care if one expected value is None or
95 does not exist in the dictionary.
85 """96 """
86 super(Backend, self).__init__(parameters)97 super(Backend, self).__init__(parameters)
87 self.tids = []98 self.tids = [] #we keep the list of loaded task ids here
88 #####RETROCOMPATIBILIY99 #####RETROCOMPATIBILIY
89 #NOTE: retrocompatibility. We convert "filename" to "path"100 #NOTE: retrocompatibility from the 0.2 series to 0.3.
90 # and we forget about "filename"101 # We convert "filename" to "path and we forget about "filename "
91 if "need_conversion" in parameters:102 if "need_conversion" in parameters:
92 parameters["path"] = os.path.join(self.DEFAULT_PATH, \103 parameters["path"] = os.path.join(self.DEFAULT_PATH, \
93 parameters["need_conversion"])104 parameters["need_conversion"])
@@ -99,24 +110,30 @@
99 self._parameters["path"], "project")110 self._parameters["path"], "project")
100111
101 def initialize(self):112 def initialize(self):
113 """This is called when a backend is enabled"""
102 super(Backend, self).initialize()114 super(Backend, self).initialize()
103 self.doc, self.xmlproj = cleanxml.openxmlfile( \115 self.doc, self.xmlproj = cleanxml.openxmlfile( \
104 self._parameters["path"], "project")116 self._parameters["path"], "project")
105117
106 def this_is_the_first_run(self, xml):118 def this_is_the_first_run(self, xml):
107 #Create the default tasks for the first run.119 """
108 #We write the XML object in a file120 Called upon the very first GTG startup.
121 This function is needed only in this backend, because it can be used as
122 default one.
123 The xml parameter is an object containing GTG default tasks. It will be
124 saved to a file, and the backend will be set as default.
125 @param xml: an xml object containing the default tasks.
126 """
109 self._parameters[self.KEY_DEFAULT_BACKEND] = True127 self._parameters[self.KEY_DEFAULT_BACKEND] = True
110 cleanxml.savexml(self._parameters["path"], xml)128 cleanxml.savexml(self._parameters["path"], xml)
111 self.doc, self.xmlproj = cleanxml.openxmlfile(\129 self.doc, self.xmlproj = cleanxml.openxmlfile(\
112 self._parameters["path"], "project")130 self._parameters["path"], "project")
113 self._parameters[self.KEY_DEFAULT_BACKEND] = True
114131
115 def start_get_tasks(self):132 def start_get_tasks(self):
116 '''133 '''
117 Once this function is launched, the backend can start pushing134 This function starts submitting the tasks from the XML file into GTG core.
118 tasks to gtg parameters.135 It's run as a separate thread.
119 136
120 @return: start_get_tasks() might not return or finish137 @return: start_get_tasks() might not return or finish
121 '''138 '''
122 tid_list = []139 tid_list = []
@@ -130,52 +147,63 @@
130 self.datastore.push_task(task)147 self.datastore.push_task(task)
131148
132 def set_task(self, task):149 def set_task(self, task):
133 tid = task.get_id()150 '''
134 existing = None151 This function is called from GTG core whenever a task should be
135 #First, we find the existing task from the treenode152 saved, either because it's a new one or it has been modified.
136 for node in self.xmlproj.childNodes:153 This function will look into the loaded XML object if the task is
137 if node.getAttribute("id") == tid:154 present, and if it's not, it will create it. Then, it will save the
138 existing = node155 task data in the XML object.
139 t_xml = taskxml.task_to_xml(self.doc, task)156
140 modified = False157 @param task: the task object to save
141 #We then replace the existing node158 '''
142 if existing and t_xml:159 tid = task.get_id()
143 #We will write only if the task has changed160 #We create an XML representation of the task
144 if t_xml.toxml() != existing.toxml():161 t_xml = taskxml.task_to_xml(self.doc, task)
145 self.xmlproj.replaceChild(t_xml, existing)162
146 modified = True163 #we find if the task exists in the XML treenode.
147 #If the node doesn't exist, we create it164 existing = None
148 # (it might not be the case in all backends165 for node in self.xmlproj.childNodes:
149 else:166 if node.getAttribute("id") == tid:
150 self.xmlproj.appendChild(t_xml)167 existing = node
168
169 modified = False
170 #We then replace the existing node
171 if existing and t_xml:
172 #We will write only if the task has changed
173 if t_xml.toxml() != existing.toxml():
174 self.xmlproj.replaceChild(t_xml, existing)
151 modified = True175 modified = True
152 #In this particular backend, we write all the tasks176 #If the node doesn't exist, we create it
153 #This is inherent to the XML file backend177 else:
154 if modified and self._parameters["path"] and self.doc :178 self.xmlproj.appendChild(t_xml)
155 cleanxml.savexml(self._parameters["path"], self.doc)179 modified = True
180
181 #if the XML object has changed, we save it to file
182 if modified and self._parameters["path"] and self.doc :
183 cleanxml.savexml(self._parameters["path"], self.doc)
156184
157 def remove_task(self, tid):185 def remove_task(self, tid):
158 ''' Completely remove the task with ID = tid '''186 ''' This function is called from GTG core whenever a task must be
187 removed from the backend. Note that the task could be not present here.
188
189 @param tid: the id of the task to delete
190 '''
191 modified = False
159 for node in self.xmlproj.childNodes:192 for node in self.xmlproj.childNodes:
160 if node.getAttribute("id") == tid:193 if node.getAttribute("id") == tid:
194 modified = True
161 self.xmlproj.removeChild(node)195 self.xmlproj.removeChild(node)
162 if tid in self.tids:196 if tid in self.tids:
163 self.tids.remove(tid)197 self.tids.remove(tid)
164 cleanxml.savexml(self._parameters["path"], self.doc)198
165199 #We save the XML file only if it's necessary
166200 if modified:
167 def quit(self, disable = False):201 cleanxml.savexml(self._parameters["path"], self.doc)
168 '''202
169 Called when GTG quits or disconnects the backend.203#NOTE: This is not used currently. Therefore, I'm disabling it (invernizzi)
170 '''204# def get_number_of_tasks(self):
171 super(Backend, self).quit(disable)205# '''
172206# Returns the number of tasks stored in the backend. Doesn't need to be a
173 def save_state(self):207# fast function, is called just for the UI
174 cleanxml.savexml(self._parameters["path"], self.doc, backup=True)208# '''
175209# return len(self.tids)
176 def get_number_of_tasks(self):
177 '''
178 Returns the number of tasks stored in the backend. Doesn't need to be a
179 fast function, is called just for the UI
180 '''
181 return len(self.tids)
182210
=== 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:43:04 +0000
@@ -18,7 +18,8 @@
18# -----------------------------------------------------------------------------18# -----------------------------------------------------------------------------
1919
20'''20'''
21FIXME: document!21This file contains the most generic representation of a backend, the
22GenericBackend class
22'''23'''
2324
24import os25import os
@@ -29,69 +30,71 @@
29from collections import deque30from collections import deque
3031
31from GTG.backends.backendsignals import BackendSignals32from GTG.backends.backendsignals import BackendSignals
32from GTG.tools.keyring import Keyring33from GTG.tools.keyring import Keyring
33from GTG.core import CoreConfig34from GTG.core import CoreConfig
34from GTG.tools.logger import Log35from GTG.tools.logger import Log
3536from GTG.tools.interruptible import _cancellation_point
3637
3738
3839
39class GenericBackend(object):40class GenericBackend(object):
40 '''41 '''
41 Base class for every backend. It's a little more than an interface which42 Base class for every backend.
42 methods have to be redefined in order for the backend to run.43 It defines the interface a backend must have and takes care of all the
44 operations common to all backends.
45 A particular backend should redefine all the methods marked as such.
43 '''46 '''
4447
4548
46 #BACKEND TYPE DESCRIPTION49 ###########################################################################
47 #"_general_description" is a dictionary that holds the values for the50 ### BACKEND INTERFACE #####################################################
48 # following keys:51 ###########################################################################
49 BACKEND_NAME = "name" #the backend gtg internal name (doesn't change in52
50 # translations, *must be unique*)53 #General description of the backend: these parameters are used
51 BACKEND_HUMAN_NAME = "human-friendly-name" #The name shown to the user54 #to show a description of the backend to the user when s/he is
52 BACKEND_DESCRIPTION = "description" #A short description of the backend55 #considering adding it.
53 BACKEND_AUTHORS = "authors" #a list of strings56 # For an example, see the GTG/backends/backend_localfile.py file
54 BACKEND_TYPE = "type"57 #_general_description has this format:
55 #BACKEND_TYPE is one of:58 #_general_description = {
56 TYPE_READWRITE = "readwrite"59 # GenericBackend.BACKEND_NAME: "backend_unique_identifier", \
57 TYPE_READONLY = "readonly"60 # GenericBackend.BACKEND_HUMAN_NAME: _("Human friendly name"), \
58 TYPE_IMPORT = "import"61 # GenericBackend.BACKEND_AUTHORS: ["First author", \
59 TYPE_EXPORT = "export"62 # "Chuck Norris"], \
63 # GenericBackend.BACKEND_TYPE: GenericBackend.TYPE_READWRITE, \
64 # GenericBackend.BACKEND_DESCRIPTION: \
65 # _("Short description of the backend"),\
66 # }
67 # The complete list of constants and their meaning is given below.
60 _general_description = {}68 _general_description = {}
6169
6270 #These are the parameters to configure a new backend of this type. A
63 #"static_parameters" is a dictionary of dictionaries, each of which71 # parameter has a name, a type and a default value.
64 #representing a parameter needed to configure the backend.72 # For an example, see the GTG/backends/backend_localfile.py file
65 #each "sub-dictionary" is identified by this a key representing its name.73 #_static_parameters has this format:
66 #"static_parameters" will be part of the definition of each74 #_static_parameters = { \
67 #particular backend.75 # "param1_name": { \
68 # Each dictionary contains the keys:76 # GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING,
69 #PARAM_DESCRIPTION = "description" #short description (shown to the user77 # GenericBackend.PARAM_DEFAULT_VALUE: "my default value",
70 # during configuration)78 # },
71 PARAM_DEFAULT_VALUE = "default_value" # its default value79 # "param2_name": {
72 PARAM_TYPE = "type" 80 # GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT,
73 #PARAM_TYPE is one of the following (changing this changes the way81 # GenericBackend.PARAM_DEFAULT_VALUE: 42,
74 # the user can configure the parameter)82 # }}
75 TYPE_PASSWORD = "password" #the real password is stored in the GNOME83 # The complete list of constants and their meaning is given below.
76 # keyring
77 # This is just a key to find it there
78 TYPE_STRING = "string" #generic string, nothing fancy is done
79 TYPE_INT = "int" #edit box can contain only integers
80 TYPE_BOOL = "bool" #checkbox is shown
81 TYPE_LIST_OF_STRINGS = "liststring" #list of strings. the "," character is
82 # prohibited in strings
83 _static_parameters = {}84 _static_parameters = {}
8485
85 def initialize(self):86 def initialize(self):
86 '''87 '''
87 Called each time it is enabled again (including on backend creation).88 Called each time it is enabled (including on backend creation).
88 Please note that a class instance for each disabled backend *is*89 Please note that a class instance for each disabled backend *is*
89 created, but it's not initialized. 90 created, but it's not initialized.
90 Optional. 91 Optional.
91 NOTE: make sure to call super().initialize()92 NOTE: make sure to call super().initialize()
92 '''93 '''
93 for module_name in self.get_required_modules():94 #NOTE: I'm disabling this since support for runtime checking of the
94 sys.modules[module_name]= __import__(module_name)95 # presence of the necessary modules is disabled. (invernizzi)
96# for module_name in self.get_required_modules():
97# sys.modules[module_name]= __import__(module_name)
95 self._parameters[self.KEY_ENABLED] = True98 self._parameters[self.KEY_ENABLED] = True
96 self._is_initialized = True99 self._is_initialized = True
97 #we signal that the backend has been enabled100 #we signal that the backend has been enabled
@@ -99,65 +102,66 @@
99102
100 def start_get_tasks(self):103 def start_get_tasks(self):
101 '''104 '''
102 Once this function is launched, the backend can start pushing105 This function starts submitting the tasks from the backend into GTG
103 tasks to gtg parameters.106 core.
107 It's run as a separate thread.
104108
105 @return: start_get_tasks() might not return or finish109 @return: start_get_tasks() might not return or finish
106 '''110 '''
107 raise NotImplemented()111 return
108112
109 def set_task(self, task):113 def set_task(self, task):
110 '''114 '''
111 Save the task in the backend. If the task id is new for the 115 This function is called from GTG core whenever a task should be
112 backend, then a new task must be created.116 saved, either because it's a new one or it has been modified.
117 If the task id is new for the backend, then a new task must be
118 created. No special notification that the task is a new one is given.
119
120 @param task: the task object to save
113 '''121 '''
114 pass122 pass
115123
116 def remove_task(self, tid):124 def remove_task(self, tid):
117 ''' Completely remove the task with ID = tid '''125 ''' This function is called from GTG core whenever a task must be
126 removed from the backend. Note that the task could be not present here.
127
128 @param tid: the id of the task to delete
129 '''
118 pass130 pass
119131
120 def has_task(self, tid):
121 '''Returns true if the backend has an internal idea
122 of the task corresponding to the tid. False otherwise'''
123 raise NotImplemented()
124
125 def new_task_id(self):
126 '''
127 Returns an available ID for a new task so that a task with this ID
128 can be saved with set_task later.
129 '''
130 raise NotImplemented()
131
132 def this_is_the_first_run(self, xml):132 def this_is_the_first_run(self, xml):
133 '''133 '''
134 Steps to execute if it's the first time the backend is run. Optional.134 Optional, and almost surely not needed.
135 '''135 Called upon the very first GTG startup.
136 pass136 This function is needed only in the default backend (XML localfile,
137137 currently).
138 def purge(self):138 The xml parameter is an object containing GTG default tasks.
139 '''139
140 Called when a backend will be removed from GTG. Useful for removing140 @param xml: an xml object containing the default tasks.
141 configuration files. Optional.141 '''
142 '''142 pass
143 pass143
144144#NOTE: task counting is disabled in the UI, so I've disabled it here
145 def get_number_of_tasks(self):145# (invernizzi)
146 '''146# def get_number_of_tasks(self):
147 Returns the number of tasks stored in the backend. Doesn't need to be a147# '''
148 fast function, is called just for the UI148# Returns the number of tasks stored in the backend. Doesn't need
149 '''149# to be a fast function, is called just for the UI
150 raise NotImplemented()150# '''
151151# raise NotImplemented()
152 @staticmethod152
153 def get_required_modules():153#NOTE: I'm disabling this since support for runtime checking of the
154 return []154# presence of the necessary modules is disabled. (invernizzi)
155# @staticmethod
156# def get_required_modules():
157# return []
155158
156 def quit(self, disable = False):159 def quit(self, disable = False):
157 '''160 '''
158 Called when GTG quits or disconnects the backend. Remember to execute161 Called when GTG quits or the user wants to disable the backend.
159 also this function when quitting. If disable is True, the backend won't162
160 be automatically loaded at next GTG start163 @param disable: If disable is True, the backend won't
164 be automatically loaded when GTG starts
161 '''165 '''
162 self._is_initialized = False166 self._is_initialized = False
163 if disable:167 if disable:
@@ -179,6 +183,44 @@
179###### You don't need to reimplement the functions below this line ############183###### You don't need to reimplement the functions below this line ############
180###############################################################################184###############################################################################
181185
186 ###########################################################################
187 ### CONSTANTS #############################################################
188 ###########################################################################
189 #BACKEND TYPE DESCRIPTION
190 # Each backend must have a "_general_description" attribute, which
191 # is a dictionary that holds the values for the following keys.
192 BACKEND_NAME = "name" #the backend gtg internal name (doesn't change in
193 # translations, *must be unique*)
194 BACKEND_HUMAN_NAME = "human-friendly-name" #The name shown to the user
195 BACKEND_DESCRIPTION = "description" #A short description of the backend
196 BACKEND_AUTHORS = "authors" #a list of strings
197 BACKEND_TYPE = "type"
198 #BACKEND_TYPE is one of:
199 TYPE_READWRITE = "readwrite"
200 TYPE_READONLY = "readonly"
201 TYPE_IMPORT = "import"
202 TYPE_EXPORT = "export"
203
204
205 #"static_parameters" is a dictionary of dictionaries, each of which
206 # are a description of a parameter needed to configure the backend and
207 # is identified in the outer dictionary by a key which is the name of the
208 # parameter.
209 # For an example, see the GTG/backends/backend_localfile.py file
210 # Each dictionary contains the keys:
211 PARAM_DEFAULT_VALUE = "default_value" # its default value
212 PARAM_TYPE = "type"
213 #PARAM_TYPE is one of the following (changing this changes the way
214 # the user can configure the parameter)
215 TYPE_PASSWORD = "password" #the real password is stored in the GNOME
216 # keyring
217 # This is just a key to find it there
218 TYPE_STRING = "string" #generic string, nothing fancy is done
219 TYPE_INT = "int" #edit box can contain only integers
220 TYPE_BOOL = "bool" #checkbox is shown
221 TYPE_LIST_OF_STRINGS = "liststring" #list of strings. the "," character is
222 # prohibited in strings
223
182 #These parameters are common to all backends and necessary.224 #These parameters are common to all backends and necessary.
183 # They will be added automatically to your _static_parameters list225 # They will be added automatically to your _static_parameters list
184 #NOTE: for now I'm disabling changing the default backend. Once it's all226 #NOTE: for now I'm disabling changing the default backend. Once it's all
@@ -189,7 +231,11 @@
189 KEY_ATTACHED_TAGS = "attached-tags"231 KEY_ATTACHED_TAGS = "attached-tags"
190 KEY_USER = "user"232 KEY_USER = "user"
191 KEY_PID = "pid"233 KEY_PID = "pid"
192 ALLTASKS_TAG = "gtg-tags-all" #IXME: moved here to avoid circular imports234 ALLTASKS_TAG = "gtg-tags-all" #NOTE: this has been moved here to avoid
235 # circular imports. It's the same as in
236 # the CoreConfig class, because it's the
237 # same thing conceptually. It doesn't
238 # matter it the naming diverges.
193239
194 _static_parameters_obligatory = { \240 _static_parameters_obligatory = { \
195 KEY_DEFAULT_BACKEND: { \241 KEY_DEFAULT_BACKEND: { \
@@ -229,28 +275,30 @@
229 '''275 '''
230 Helper method, used to obtain the full list of the static_parameters276 Helper method, used to obtain the full list of the static_parameters
231 (user configured and default ones)277 (user configured and default ones)
278
279 @returns dict: the dict containing all the static parameters
232 '''280 '''
233 if hasattr(cls, "_static_parameters"):281 temp_dic = cls._static_parameters_obligatory.copy()
234 temp_dic = cls._static_parameters_obligatory.copy()282 if cls._general_description[cls.BACKEND_TYPE] == \
235 if cls._general_description[cls.BACKEND_TYPE] == cls.TYPE_READWRITE:283 cls.TYPE_READWRITE:
236 for key, value in \284 for key, value in \
237 cls._static_parameters_obligatory_for_rw.iteritems():285 cls._static_parameters_obligatory_for_rw.iteritems():
238 temp_dic[key] = value
239 for key, value in cls._static_parameters.iteritems():
240 temp_dic[key] = value286 temp_dic[key] = value
241 return temp_dic 287 for key, value in cls._static_parameters.iteritems():
242 else:288 temp_dic[key] = value
243 raise NotImplemented("_static_parameters not implemented for " + \289 return temp_dic
244 "backend %s" % type(cls))
245290
246 def __init__(self, parameters):291 def __init__(self, parameters):
247 """292 """
248 Instantiates a new backend. Please note that this is called also for293 Instantiates a new backend. Please note that this is called also
249 disabled backends. Those are not initialized, so you might want to check294 for disabled backends. Those are not initialized, so you might
250 out the initialize() function.295 want to check out the initialize() function.
251 """296 """
252 if self.KEY_DEFAULT_BACKEND not in parameters:297 if self.KEY_DEFAULT_BACKEND not in parameters:
298 #if it's not specified, then this is the default backend
299 #(for retro-compatibility with the GTG 0.2 series)
253 parameters[self.KEY_DEFAULT_BACKEND] = True300 parameters[self.KEY_DEFAULT_BACKEND] = True
301 #default backends should get all the tasks
254 if parameters[self.KEY_DEFAULT_BACKEND] or \302 if parameters[self.KEY_DEFAULT_BACKEND] or \
255 (not self.KEY_ATTACHED_TAGS in parameters and \303 (not self.KEY_ATTACHED_TAGS in parameters and \
256 self._general_description[self.BACKEND_TYPE] \304 self._general_description[self.BACKEND_TYPE] \
@@ -259,12 +307,17 @@
259 self._parameters = parameters307 self._parameters = parameters
260 self._signal_manager = BackendSignals()308 self._signal_manager = BackendSignals()
261 self._is_initialized = False309 self._is_initialized = False
310 #if debugging mode is enabled, tasks should be saved as soon as they're
311 # marked as modified. If in normal mode, we prefer speed over easier
312 # debugging.
262 if Log.is_debugging_mode():313 if Log.is_debugging_mode():
263 self.timer_timestep = 5314 self.timer_timestep = 5
264 else:315 else:
265 self.timer_timestep = 1 316 self.timer_timestep = 1
266 self.to_set_timer = None317 self.to_set_timer = None
267 self.please_quit = False318 self.please_quit = False
319 self.cancellation_point = lambda: _cancellation_point(\
320 lambda: self.please_quit)
268 self.to_set = deque()321 self.to_set = deque()
269 self.to_remove = deque()322 self.to_remove = deque()
270323
@@ -274,6 +327,9 @@
274 '''327 '''
275 if hasattr(self._parameters, self.KEY_DEFAULT_BACKEND) and \328 if hasattr(self._parameters, self.KEY_DEFAULT_BACKEND) and \
276 self._parameters[self.KEY_DEFAULT_BACKEND]:329 self._parameters[self.KEY_DEFAULT_BACKEND]:
330 #default backends should get all the tasks
331 #NOTE: this shouldn't be needed, but it doesn't cost anything and it
332 # could avoid potential tasks losses.
277 return [self.ALLTASKS_TAG]333 return [self.ALLTASKS_TAG]
278 try:334 try:
279 return self._parameters[self.KEY_ATTACHED_TAGS]335 return self._parameters[self.KEY_ATTACHED_TAGS]
@@ -283,6 +339,8 @@
283 def set_attached_tags(self, tags):339 def set_attached_tags(self, tags):
284 '''340 '''
285 Changes the set of attached tags341 Changes the set of attached tags
342
343 @param tags: the new attached_tags set
286 '''344 '''
287 self._parameters[self.KEY_ATTACHED_TAGS] = tags345 self._parameters[self.KEY_ATTACHED_TAGS] = tags
288346
@@ -300,6 +358,12 @@
300 return self._parameters358 return self._parameters
301359
302 def set_parameter(self, parameter, value):360 def set_parameter(self, parameter, value):
361 '''
362 Change a parameter for this backend
363
364 @param parameter: the parameter name
365 @param value: the new value
366 '''
303 self._parameters[parameter] = value367 self._parameters[parameter] = value
304368
305 @classmethod369 @classmethod
@@ -330,24 +394,22 @@
330 def _get_from_general_description(cls, key):394 def _get_from_general_description(cls, key):
331 '''395 '''
332 Helper method to extract values from cls._general_description.396 Helper method to extract values from cls._general_description.
333 Raises an exception if the key is missing (helpful for developers397
334 adding new backends).398 @param key: the key to extract
335 '''399 '''
336 if key in cls._general_description:400 return cls._general_description[key]
337 return cls._general_description[key]
338 else:
339 raise NotImplemented("Key %s is missing from " +\
340 "'self._general_description' of a backend (%s). " +
341 "Please add the corresponding value" % (key, type(cls)))
342401
343 @classmethod402 @classmethod
344 def cast_param_type_from_string(cls, param_value, param_type):403 def cast_param_type_from_string(cls, param_value, param_type):
345 '''404 '''
346 Parameters are saved in a text format, so we have to cast them to the405 Parameters are saved in a text format, so we have to cast them to the
347 appropriate type on loading. This function does exactly that.406 appropriate type on loading. This function does exactly that.
407
408 @param param_value: the actual value of the parameter, in a string
409 format
410 @param param_type: the wanted type
411 @returns something: the casted param_value
348 '''412 '''
349 #FIXME: we could use pickle (dumps and loads), at least in some cases
350 # (invernizzi)
351 if param_type in cls._type_converter:413 if param_type in cls._type_converter:
352 return cls._type_converter[param_type](param_value)414 return cls._type_converter[param_type](param_value)
353 elif param_type == cls.TYPE_BOOL:415 elif param_type == cls.TYPE_BOOL:
@@ -374,6 +436,10 @@
374 def cast_param_type_to_string(self, param_type, param_value):436 def cast_param_type_to_string(self, param_type, param_value):
375 '''437 '''
376 Inverse of cast_param_type_from_string438 Inverse of cast_param_type_from_string
439
440 @param param_value: the actual value of the parameter
441 @param param_type: the type of the parameter (password...)
442 @returns something: param_value casted to string
377 '''443 '''
378 if param_type == GenericBackend.TYPE_PASSWORD:444 if param_type == GenericBackend.TYPE_PASSWORD:
379 if param_value == None:445 if param_value == None:
@@ -391,20 +457,27 @@
391 def get_id(self):457 def get_id(self):
392 '''458 '''
393 returns the backends id, used in the datastore for indexing backends459 returns the backends id, used in the datastore for indexing backends
460
461 @returns string: the backend id
394 '''462 '''
395 return self.get_name() + "@" + self._parameters["pid"]463 return self.get_name() + "@" + self._parameters["pid"]
396464
397 @classmethod465 @classmethod
398 def get_human_default_name(cls):466 def get_human_default_name(cls):
399 '''467 '''
400 returns the user friendly default backend name. 468 returns the user friendly default backend name, without eventual user
469 modifications.
470
471 @returns string: the default "human name"
401 '''472 '''
402 return cls._general_description[cls.BACKEND_HUMAN_NAME]473 return cls._general_description[cls.BACKEND_HUMAN_NAME]
403474
404 def get_human_name(self):475 def get_human_name(self):
405 '''476 '''
406 returns the user customized backend name. If the user hasn't477 returns the user customized backend name. If the user hasn't
407 customized it, returns the default one478 customized it, returns the default one.
479
480 @returns string: the "human name" of this backend
408 '''481 '''
409 if self.KEY_HUMAN_NAME in self._parameters and \482 if self.KEY_HUMAN_NAME in self._parameters and \
410 self._parameters[self.KEY_HUMAN_NAME] != "":483 self._parameters[self.KEY_HUMAN_NAME] != "":
@@ -415,6 +488,8 @@
415 def set_human_name(self, name):488 def set_human_name(self, name):
416 '''489 '''
417 sets a custom name for the backend490 sets a custom name for the backend
491
492 @param name: the new name
418 '''493 '''
419 self._parameters[self.KEY_HUMAN_NAME] = name494 self._parameters[self.KEY_HUMAN_NAME] = name
420 #we signal the change495 #we signal the change
@@ -423,29 +498,49 @@
423 def is_enabled(self):498 def is_enabled(self):
424 '''499 '''
425 Returns if the backend is enabled500 Returns if the backend is enabled
501
502 @returns bool
426 '''503 '''
427 return self.get_parameters()[GenericBackend.KEY_ENABLED] or \504 return self.get_parameters()[GenericBackend.KEY_ENABLED] or \
428 self.is_default()505 self.is_default()
429506
430 def is_default(self):507 def is_default(self):
431 '''508 '''
432 Returns if the backend is enabled509 Returns if the backend is enabled
510
511 @returns bool
433 '''512 '''
434 return self.get_parameters()[GenericBackend.KEY_DEFAULT_BACKEND]513 return self.get_parameters()[GenericBackend.KEY_DEFAULT_BACKEND]
435514
436 def is_initialized(self):515 def is_initialized(self):
437 '''516 '''
438 Returns if the backend is up and running517 Returns if the backend is up and running
518
519 @returns is_initialized
439 '''520 '''
440 return self._is_initialized521 return self._is_initialized
441522
442 def get_parameter_type(self, param_name):523 def get_parameter_type(self, param_name):
524 '''
525 Given the name of a parameter, returns its type. If the parameter is one
526 of the default ones, it does not have a type: in that case, it returns
527 None
528
529 @param param_name: the name of the parameter
530 @returns string: the type, or None
531 '''
443 try:532 try:
444 return self.get_static_parameters()[param_name][self.PARAM_TYPE]533 return self.get_static_parameters()[param_name][self.PARAM_TYPE]
445 except KeyError:534 except:
446 return None535 return None
447536
448 def register_datastore(self, datastore):537 def register_datastore(self, datastore):
538 '''
539 Setter function to inform the backend about the datastore that's loading
540 it.
541
542 @param datastore: a Datastore
543 '''
449 self.datastore = datastore544 self.datastore = datastore
450545
451###############################################################################546###############################################################################
@@ -455,6 +550,7 @@
455 def _store_pickled_file(self, path, data):550 def _store_pickled_file(self, path, data):
456 '''551 '''
457 A helper function to save some object in a file.552 A helper function to save some object in a file.
553
458 @param path: a relative path. A good choice is554 @param path: a relative path. A good choice is
459 "backend_name/object_name"555 "backend_name/object_name"
460 @param data: the object556 @param data: the object
@@ -467,15 +563,13 @@
467 if exception.errno != errno.EEXIST: 563 if exception.errno != errno.EEXIST:
468 raise564 raise
469 #saving565 #saving
470 #try:
471 with open(path, 'wb') as file:566 with open(path, 'wb') as file:
472 pickle.dump(data, file)567 pickle.dump(data, file)
473 #except pickle.PickleError:
474 #pass
475568
476 def _load_pickled_file(self, path, default_value = None):569 def _load_pickled_file(self, path, default_value = None):
477 '''570 '''
478 A helper function to load some object from a file.571 A helper function to load some object from a file.
572
479 @param path: the relative path of the file573 @param path: the relative path of the file
480 @param default_value: the value to return if the file is missing or574 @param default_value: the value to return if the file is missing or
481 corrupt575 corrupt
@@ -485,11 +579,29 @@
485 if not os.path.exists(path):579 if not os.path.exists(path):
486 return default_value580 return default_value
487 else:581 else:
488 try:582 with open(path, 'r') as file:
489 with open(path, 'r') as file:583 try:
490 return pickle.load(file)584 return pickle.load(file)
491 except pickle.PickleError:585 except pickle.PickleError:
492 return default_value586 Log.error("PICKLE ERROR")
587 return default_value
588
589 def _gtg_task_is_syncable_per_attached_tags(self, task):
590 '''
591 Helper function which checks if the given task satisfies the filtering
592 imposed by the tags attached to the backend.
593 That means, if a user wants a backend to sync only tasks tagged @works,
594 this function should be used to check if that is verified.
595
596 @returns bool: True if the task should be synced
597 '''
598 attached_tags = self.get_attached_tags()
599 if GenericBackend.ALLTASKS_TAG in attached_tags:
600 return True
601 for tag in task.get_tags_name():
602 if tag in attached_tags:
603 return True
604 return False
493605
494###############################################################################606###############################################################################
495### THREADING #################################################################607### THREADING #################################################################
@@ -504,25 +616,29 @@
504 self.launch_setting_thread)616 self.launch_setting_thread)
505 self.to_set_timer.start()617 self.to_set_timer.start()
506618
507 def launch_setting_thread(self):619 def launch_setting_thread(self, bypass_quit_request = False):
508 '''620 '''
509 This function is launched as a separate thread. Its job is to perform621 This function is launched as a separate thread. Its job is to perform
510 the changes that have been issued from GTG core. In particular, for622 the changes that have been issued from GTG core.
511 each task in the self.to_set queue, a task has to be modified or to be623 In particular, for each task in the self.to_set queue, a task
512 created (if the tid is new), and for each task in the self.to_remove624 has to be modified or to be created (if the tid is new), and for
513 queue, a task has to be deleted625 each task in the self.to_remove queue, a task has to be deleted
626
627 @param bypass_quit_request: if True, the thread should not be stopped
628 even if asked by self.please_quit = True.
629 It's used when the backend quits, to finish
630 syncing all pending tasks
514 '''631 '''
515 while not self.please_quit:632 while not self.please_quit or bypass_quit_request:
516 try:633 try:
517 task = self.to_set.pop()634 task = self.to_set.pop()
518 except IndexError:635 except IndexError:
519 break636 break
520 #time.sleep(4)
521 tid = task.get_id()637 tid = task.get_id()
522 if tid not in self.to_remove:638 if tid not in self.to_remove:
523 self.set_task(task)639 self.set_task(task)
524640
525 while not self.please_quit:641 while not self.please_quit or bypass_quit_request:
526 try:642 try:
527 tid = self.to_remove.pop()643 tid = self.to_remove.pop()
528 except IndexError:644 except IndexError:
@@ -532,7 +648,12 @@
532 self.to_set_timer = None648 self.to_set_timer = None
533649
534 def queue_set_task(self, task):650 def queue_set_task(self, task):
535 ''' Save the task in the backend. '''651 ''' Save the task in the backend. In particular, it just enqueues the
652 task in the self.to_set queue. A thread will shortly run to apply the
653 requested changes.
654
655 @param task: the task that should be saved
656 '''
536 tid = task.get_id()657 tid = task.get_id()
537 if task not in self.to_set and tid not in self.to_remove:658 if task not in self.to_set and tid not in self.to_remove:
538 self.to_set.appendleft(task)659 self.to_set.appendleft(task)
@@ -540,7 +661,10 @@
540661
541 def queue_remove_task(self, tid):662 def queue_remove_task(self, tid):
542 '''663 '''
543 Queues task to be removed.664 Queues task to be removed. In particular, it just enqueues the
665 task in the self.to_remove queue. A thread will shortly run to apply the
666 requested changes.
667
544 @param tid: The Task ID of the task to be removed668 @param tid: The Task ID of the task to be removed
545 '''669 '''
546 if tid not in self.to_remove:670 if tid not in self.to_remove:
@@ -553,8 +677,6 @@
553 Helper method. Forces the backend to perform all the pending changes.677 Helper method. Forces the backend to perform all the pending changes.
554 It is usually called upon quitting the backend.678 It is usually called upon quitting the backend.
555 '''679 '''
556 #FIXME: this function should become part of the r/w r/o generic class
557 # for backends
558 if self.to_set_timer != None:680 if self.to_set_timer != None:
559 self.please_quit = True681 self.please_quit = True
560 try:682 try:
@@ -562,10 +684,10 @@
562 except:684 except:
563 pass685 pass
564 try:686 try:
565 self.to_set_timer.join(5)687 self.to_set_timer.join()
566 except:688 except:
567 pass689 pass
568 self.please_quit = False690 self.launch_setting_thread(bypass_quit_request = True)
569 self.launch_setting_thread()
570 self.save_state()691 self.save_state()
571692
693
572694
=== 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:43:04 +0000
@@ -18,8 +18,8 @@
18# -----------------------------------------------------------------------------18# -----------------------------------------------------------------------------
1919
20"""20"""
21The DaataStore contains a list of TagSource objects, which are proxies21Contains the Datastore object, which is the manager of all the active backends
22between a backend and the datastore itself22(both enabled and disabled ones)
23"""23"""
2424
25import threading25import threading
@@ -34,7 +34,6 @@
34from GTG.tools.logger import Log34from GTG.tools.logger import Log
35from GTG.backends.genericbackend import GenericBackend35from GTG.backends.genericbackend import GenericBackend
36from GTG.tools import cleanxml36from GTG.tools import cleanxml
37from GTG.tools.keyring import Keyring
38from GTG.backends.backendsignals import BackendSignals37from GTG.backends.backendsignals import BackendSignals
39from GTG.tools.synchronized import synchronized38from GTG.tools.synchronized import synchronized
40from GTG.tools.borg import Borg39from GTG.tools.borg import Borg
@@ -59,11 +58,17 @@
59 self.requester = requester.Requester(self)58 self.requester = requester.Requester(self)
60 self.tagstore = tagstore.TagStore(self.requester)59 self.tagstore = tagstore.TagStore(self.requester)
61 self._backend_signals = BackendSignals()60 self._backend_signals = BackendSignals()
62 self.mutex = threading.RLock()61 self.please_quit = False #when turned to true, all pending operation
63 self.is_default_backend_loaded = False62 # should be completed and then GTG should quit
63 self.is_default_backend_loaded = False #the default backend must be
64 # loaded before anyone else.
65 # This turns to True when the
66 # default backend loading has
67 # finished.
64 self._backend_signals.connect('default-backend-loaded', \68 self._backend_signals.connect('default-backend-loaded', \
65 self._activate_non_default_backends)69 self._activate_non_default_backends)
66 self.filtered_datastore = FilteredDataStore(self)70 self.filtered_datastore = FilteredDataStore(self)
71 self._backend_mutex = threading.Lock()
6772
68 ##########################################################################73 ##########################################################################
69 ### Helper functions (get_ methods for Datastore embedded objects)74 ### Helper functions (get_ methods for Datastore embedded objects)
@@ -72,6 +77,7 @@
72 def get_tagstore(self):77 def get_tagstore(self):
73 '''78 '''
74 Helper function to obtain the Tagstore associated with this DataStore79 Helper function to obtain the Tagstore associated with this DataStore
80
75 @return GTG.core.tagstore.TagStore: the tagstore object81 @return GTG.core.tagstore.TagStore: the tagstore object
76 '''82 '''
77 return self.tagstore83 return self.tagstore
@@ -79,15 +85,17 @@
79 def get_requester(self):85 def get_requester(self):
80 '''86 '''
81 Helper function to get the Requester associate with this DataStore87 Helper function to get the Requester associate with this DataStore
88
82 @returns GTG.core.requester.Requester: the requester associated with89 @returns GTG.core.requester.Requester: the requester associated with
83 this datastore90 this datastore
84 '''91 '''
85 return self.requester92 return self.requester
86 93
87 def get_tasks_tree(self):94 def get_tasks_tree(self):
88 '''95 '''
89 Helper function to get a Tree with all the tasks contained in this96 Helper function to get a Tree with all the tasks contained in this
90 Datastore97 Datastore.
98
91 @returns GTG.core.tree.Tree: a task tree (the main one)99 @returns GTG.core.tree.Tree: a task tree (the main one)
92 '''100 '''
93 return self.open_tasks101 return self.open_tasks
@@ -99,6 +107,7 @@
99 def get_all_tasks(self):107 def get_all_tasks(self):
100 '''108 '''
101 Returns list of all keys of open tasks109 Returns list of all keys of open tasks
110
102 @return a list of strings: a list of task ids111 @return a list of strings: a list of task ids
103 '''112 '''
104 return self.open_tasks.get_all_keys()113 return self.open_tasks.get_all_keys()
@@ -107,6 +116,7 @@
107 '''116 '''
108 Returns true if the tid is among the open or closed tasks for117 Returns true if the tid is among the open or closed tasks for
109 this DataStore, False otherwise.118 this DataStore, False otherwise.
119
110 @param tid: Task ID to search for120 @param tid: Task ID to search for
111 @return bool: True if the task is present121 @return bool: True if the task is present
112 '''122 '''
@@ -116,6 +126,7 @@
116 '''126 '''
117 Returns the internal task object for the given tid, or None if the127 Returns the internal task object for the given tid, or None if the
118 tid is not present in this DataStore.128 tid is not present in this DataStore.
129
119 @param tid: Task ID to retrieve130 @param tid: Task ID to retrieve
120 @returns GTG.core.task.Task or None: whether the Task is present131 @returns GTG.core.task.Task or None: whether the Task is present
121 or not132 or not
@@ -123,12 +134,13 @@
123 if self.has_task(tid):134 if self.has_task(tid):
124 return self.open_tasks.get_node(tid)135 return self.open_tasks.get_node(tid)
125 else:136 else:
126 Log.debug("requested non-existent task")137 Log.error("requested non-existent task")
127 return None138 return None
128 139
129 def task_factory(self, tid, newtask = False):140 def task_factory(self, tid, newtask = False):
130 '''141 '''
131 Instantiates the given task id as a Task object.142 Instantiates the given task id as a Task object.
143
132 @param tid: a task id. Must be unique144 @param tid: a task id. Must be unique
133 @param newtask: True if the task has never been seen before145 @param newtask: True if the task has never been seen before
134 @return Task: a Task instance146 @return Task: a Task instance
@@ -141,6 +153,7 @@
141 New task is created in all the backends that collect all tasks (among153 New task is created in all the backends that collect all tasks (among
142 them, the default backend). The default backend uses the same task id154 them, the default backend). The default backend uses the same task id
143 in its own internal representation.155 in its own internal representation.
156
144 @return: The task object that was created.157 @return: The task object that was created.
145 """158 """
146 task = self.task_factory(uuid.uuid4(), True)159 task = self.task_factory(uuid.uuid4(), True)
@@ -148,10 +161,13 @@
148 return task161 return task
149 162
150 @synchronized163 @synchronized
151 def push_task(self, task, backend_capabilities = 'bypass for now'):164 def push_task(self, task):
152 '''165 '''
153 Adds the given task object to the task tree. In other words, registers166 Adds the given task object to the task tree. In other words, registers
154 the given task in the GTG task set.167 the given task in the GTG task set.
168 This function is used in mutual exclusion: only a backend at a time is
169 allowed to push tasks.
170
155 @param task: A valid task object (a GTG.core.task.Task)171 @param task: A valid task object (a GTG.core.task.Task)
156 @return bool: True if the task has been accepted172 @return bool: True if the task has been accepted
157 '''173 '''
@@ -172,10 +188,10 @@
172 def get_all_backends(self, disabled = False):188 def get_all_backends(self, disabled = False):
173 """ 189 """
174 returns list of all registered backends for this DataStore.190 returns list of all registered backends for this DataStore.
191
175 @param disabled: If disabled is True, attaches also the list of disabled backends192 @param disabled: If disabled is True, attaches also the list of disabled backends
176 @return list: a list of TaskSource objects193 @return list: a list of TaskSource objects
177 """194 """
178 #NOTE: consider cashing this result for speed.
179 result = []195 result = []
180 for backend in self.backends.itervalues():196 for backend in self.backends.itervalues():
181 if backend.is_enabled() or disabled:197 if backend.is_enabled() or disabled:
@@ -184,10 +200,11 @@
184200
185 def get_backend(self, backend_id):201 def get_backend(self, backend_id):
186 '''202 '''
187 Returns a backend given its id203 Returns a backend given its id.
204
188 @param backend_id: a backend id205 @param backend_id: a backend id
189 @returns GTG.core.datastore.TaskSource or None: the requested backend,206 @returns GTG.core.datastore.TaskSource or None: the requested backend,
190 or none207 or None
191 '''208 '''
192 if backend_id in self.backends:209 if backend_id in self.backends:
193 return self.backends[backend_id]210 return self.backends[backend_id]
@@ -197,20 +214,24 @@
197 def register_backend(self, backend_dic):214 def register_backend(self, backend_dic):
198 """215 """
199 Registers a TaskSource as a backend for this DataStore216 Registers a TaskSource as a backend for this DataStore
217
200 @param backend_dic: Dictionary object containing all the218 @param backend_dic: Dictionary object containing all the
201 parameters to initialize the backend (filename...). It should219 parameters to initialize the backend
202 also contain the backend class (under "backend"), and its unique220 (filename...). It should also contain the
203 id (under "pid")221 backend class (under "backend"), and its
222 unique id (under "pid")
204 """223 """
205 if "backend" in backend_dic:224 if "backend" in backend_dic:
206 if "pid" not in backend_dic:225 if "pid" not in backend_dic:
207 Log.debug("registering a backend without pid.")226 Log.error("registering a backend without pid.")
208 return None227 return None
209 backend = backend_dic["backend"]228 backend = backend_dic["backend"]
210 #Checking that is a new backend229 #Checking that is a new backend
211 if backend.get_id() in self.backends:230 if backend.get_id() in self.backends:
212 Log.debug("registering already registered backend")231 Log.error("registering already registered backend")
213 return None232 return None
233 #creating the TaskSource which will wrap the backend,
234 # filtering the tasks that should hit the backend.
214 source = TaskSource(requester = self.requester,235 source = TaskSource(requester = self.requester,
215 backend = backend,236 backend = backend,
216 datastore = self.filtered_datastore)237 datastore = self.filtered_datastore)
@@ -234,23 +255,46 @@
234 source.start_get_tasks()255 source.start_get_tasks()
235 return source256 return source
236 else:257 else:
237 Log.debug("Tried to register a backend without a pid")258 Log.error("Tried to register a backend without a pid")
238259
239 def _activate_non_default_backends(self, sender = None):260 def _activate_non_default_backends(self, sender = None):
240 '''261 '''
241 Non-default backends have to wait until the default loads before262 Non-default backends have to wait until the default loads before
242 being activated. This function is called after the first default263 being activated. This function is called after the first default
243 backend has loaded all its tasks.264 backend has loaded all its tasks.
265
266 @param sender: not used, just here for signal compatibility
244 '''267 '''
245 if self.is_default_backend_loaded:268 if self.is_default_backend_loaded:
246 Log.debug("spurious call")269 Log.debug("spurious call")
247 return270 return
271
272
248 self.is_default_backend_loaded = True273 self.is_default_backend_loaded = True
249 for backend in self.backends.itervalues():274 for backend in self.backends.itervalues():
250 if backend.is_enabled() and not backend.is_default():275 if backend.is_enabled() and not backend.is_default():
251 backend.initialize()276 self._backend_startup(backend)
252 backend.start_get_tasks()277
253 self.flush_all_tasks(backend.get_id())278 def _backend_startup(self, backend):
279 '''
280 Helper function to launch a thread that starts a backend.
281
282 @param backend: the backend object
283 '''
284 def __backend_startup(self, backend):
285 '''
286 Helper function to start a backend
287
288 @param backend: the backend object
289 '''
290 backend.initialize()
291 backend.start_get_tasks()
292 self.flush_all_tasks(backend.get_id())
293
294 thread = threading.Thread(target = __backend_startup,
295 args = (self, backend))
296 thread.setDaemon(True)
297 thread.start()
254298
255 def set_backend_enabled(self, backend_id, state):299 def set_backend_enabled(self, backend_id, state):
256 """300 """
@@ -262,6 +306,7 @@
262 Enable:306 Enable:
263 Reloads a disabled backend. Backend must be already known by the307 Reloads a disabled backend. Backend must be already known by the
264 Datastore308 Datastore
309
265 @parma backend_id: a backend id310 @parma backend_id: a backend id
266 @param state: True to enable, False to disable311 @param state: True to enable, False to disable
267 """312 """
@@ -270,11 +315,12 @@
270 current_state = backend.is_enabled()315 current_state = backend.is_enabled()
271 if current_state == True and state == False:316 if current_state == True and state == False:
272 #we disable the backend317 #we disable the backend
273 backend.quit(disable = True)318 #FIXME!!!
319 threading.Thread(target = backend.quit, \
320 kwargs = {'disable': True}).start()
274 elif current_state == False and state == True:321 elif current_state == False and state == True:
275 if self.is_default_backend_loaded == True:322 if self.is_default_backend_loaded == True:
276 backend.initialize()323 self._backend_startup(backend)
277 self.flush_all_tasks(backend_id)
278 else:324 else:
279 #will be activated afterwards325 #will be activated afterwards
280 backend.set_parameter(GenericBackend.KEY_ENABLED,326 backend.set_parameter(GenericBackend.KEY_ENABLED,
@@ -283,13 +329,19 @@
283 def remove_backend(self, backend_id):329 def remove_backend(self, backend_id):
284 '''330 '''
285 Removes a backend, and forgets it ever existed.331 Removes a backend, and forgets it ever existed.
332
286 @param backend_id: a backend id333 @param backend_id: a backend id
287 '''334 '''
288 if backend_id in self.backends:335 if backend_id in self.backends:
289 backend = self.backends[backend_id]336 backend = self.backends[backend_id]
290 if backend.is_enabled():337 if backend.is_enabled():
291 self.set_backend_enabled(backend_id, False)338 self.set_backend_enabled(backend_id, False)
292 backend.purge()339 #FIXME: to keep things simple, backends are not notified that they
340 # are completely removed (they think they're just
341 # deactivated). We should add a "purge" call to backend to let
342 # them know that they're removed, so that they can remove all
343 # the various files they've created. (invernizzi)
344
293 #we notify that the backend has been deleted345 #we notify that the backend has been deleted
294 self._backend_signals.backend_removed(backend.get_id())346 self._backend_signals.backend_removed(backend.get_id())
295 del self.backends[backend_id]347 del self.backends[backend_id]
@@ -297,6 +349,7 @@
297 def backend_change_attached_tags(self, backend_id, tag_names):349 def backend_change_attached_tags(self, backend_id, tag_names):
298 '''350 '''
299 Changes the tags for which a backend should store a task351 Changes the tags for which a backend should store a task
352
300 @param backend_id: a backend_id353 @param backend_id: a backend_id
301 @param tag_names: the new set of tags. This should not be a tag object,354 @param tag_names: the new set of tags. This should not be a tag object,
302 just the tag name.355 just the tag name.
@@ -312,43 +365,59 @@
312 It has to be run after the creation of a new backend (or an alteration365 It has to be run after the creation of a new backend (or an alteration
313 of its "attached tags"), so that the tasks which are already loaded in 366 of its "attached tags"), so that the tasks which are already loaded in
314 the Tree will be saved in the proper backends367 the Tree will be saved in the proper backends
368
315 @param backend_id: a backend id369 @param backend_id: a backend id
316 '''370 '''
317 def _internal_flush_all_tasks():371 def _internal_flush_all_tasks():
318 backend = self.backends[backend_id]372 backend = self.backends[backend_id]
319 for task_id in self.requester.get_all_tasks_list():373 for task_id in self.requester.get_all_tasks_list():
374 if self.please_quit:
375 break
320 backend.queue_set_task(None, task_id)376 backend.queue_set_task(None, task_id)
321 t = threading.Thread(target = _internal_flush_all_tasks).start()377 t = threading.Thread(target = _internal_flush_all_tasks)
378 t.start()
322 self.backends[backend_id].start_get_tasks()379 self.backends[backend_id].start_get_tasks()
323380
324 def save(self, quit = False):381 def save(self, quit = False):
325 '''382 '''
326 Saves the backends parameters. 383 Saves the backends parameters.
384
327 @param quit: If quit is true, backends are shut down385 @param quit: If quit is true, backends are shut down
328 '''386 '''
387 try:
388 self.start_get_tasks_thread.join()
389 except Exception, e:
390 pass
329 doc,xmlconfig = cleanxml.emptydoc("config")391 doc,xmlconfig = cleanxml.emptydoc("config")
330 #we ask all the backends to quit first.392 #we ask all the backends to quit first.
331 if quit:393 if quit:
394 #we quit backends in parallel
395 threads_dic = {}
332 for b in self.get_all_backends():396 for b in self.get_all_backends():
333 #NOTE:we could do this in parallel. Maybe a quit and397 thread = threading.Thread(target = b.quit)
334 #has_quit would be faster (invernizzi)398 threads_dic[b.get_id()] = thread
335 b.quit()399 thread.start()
400 for backend_id, thread in threads_dic.iteritems():
401 #after 20 seconds, we give up
402 thread.join(20)
403 if thread.isAlive():
404 Log.error("The %s backend stalled while quitting",
405 backend_id)
336 #we save the parameters406 #we save the parameters
337 for b in self.get_all_backends(disabled = True):407 for b in self.get_all_backends(disabled = True):
338 t_xml = doc.createElement("backend")408 t_xml = doc.createElement("backend")
339 for key, value in b.get_parameters().iteritems():409 for key, value in b.get_parameters().iteritems():
340 if key in ["backend", "xmlobject"]:410 if key in ["backend", "xmlobject"]:
341 #We don't want parameters,backend,xmlobject411 #We don't want parameters, backend, xmlobject: we'll create
412 # them at next startup
342 continue413 continue
343 param_type = b.get_parameter_type(key)414 param_type = b.get_parameter_type(key)
344 value = b.cast_param_type_to_string(param_type, value)415 value = b.cast_param_type_to_string(param_type, value)
345 t_xml.setAttribute(str(key), value)416 t_xml.setAttribute(str(key), value)
346 #Saving all the projects at close417 #Saving all the projects at close
347 xmlconfig.appendChild(t_xml)418 xmlconfig.appendChild(t_xml)
348
349 datafile = os.path.join(CoreConfig().get_data_dir(), CoreConfig.DATA_FILE)419 datafile = os.path.join(CoreConfig().get_data_dir(), CoreConfig.DATA_FILE)
350 cleanxml.savexml(datafile,doc,backup=True)420 cleanxml.savexml(datafile,doc,backup=True)
351
352 #Saving the tagstore421 #Saving the tagstore
353 ts = self.get_tagstore()422 ts = self.get_tagstore()
354 ts.save()423 ts.save()
@@ -356,9 +425,21 @@
356 def request_task_deletion(self, tid):425 def request_task_deletion(self, tid):
357 ''' 426 '''
358 This is a proxy function to request a task deletion from a backend427 This is a proxy function to request a task deletion from a backend
428
359 @param tid: the tid of the task to remove429 @param tid: the tid of the task to remove
360 '''430 '''
361 self.requester.delete_task(tid)431 self.requester.delete_task(tid)
432
433 def get_backend_mutex(self):
434 '''
435 Returns the mutex object used by backends to avoid modifying a task
436 at the same time.
437
438 @returns threading.Lock
439 '''
440 return self._backend_mutex
441
442
362443
363444
364class TaskSource():445class TaskSource():
@@ -366,9 +447,12 @@
366 Transparent interface between the real backend and the DataStore.447 Transparent interface between the real backend and the DataStore.
367 Is in charge of connecting and disconnecting to signals448 Is in charge of connecting and disconnecting to signals
368 '''449 '''
450
451
369 def __init__(self, requester, backend, datastore):452 def __init__(self, requester, backend, datastore):
370 """453 """
371 Instantiates a TaskSource object.454 Instantiates a TaskSource object.
455
372 @param requester: a Requester456 @param requester: a Requester
373 @param backend: the backend being wrapped457 @param backend: the backend being wrapped
374 @param datastore: a FilteredDatastore458 @param datastore: a FilteredDatastore
@@ -378,6 +462,7 @@
378 self.backend.register_datastore(datastore)462 self.backend.register_datastore(datastore)
379 self.to_set = deque()463 self.to_set = deque()
380 self.to_remove = deque()464 self.to_remove = deque()
465 self.please_quit = False
381 self.task_filter = self.get_task_filter_for_backend()466 self.task_filter = self.get_task_filter_for_backend()
382 if Log.is_debugging_mode():467 if Log.is_debugging_mode():
383 self.timer_timestep = 5468 self.timer_timestep = 5
@@ -391,7 +476,10 @@
391 ''''476 ''''
392 Maps the TaskSource to the backend and starts threading.477 Maps the TaskSource to the backend and starts threading.
393 '''478 '''
394 threading.Thread(target = self.__start_get_tasks).start()479 self.start_get_tasks_thread = \
480 threading.Thread(target = self.__start_get_tasks)
481 self.start_get_tasks_thread.setDaemon(True)
482 self.start_get_tasks_thread.start()
395483
396 def __start_get_tasks(self):484 def __start_get_tasks(self):
397 '''485 '''
@@ -405,7 +493,7 @@
405493
406 def get_task_filter_for_backend(self):494 def get_task_filter_for_backend(self):
407 '''495 '''
408 Fiter that checks if the task should be stored in this backend.496 Filter that checks if the task should be stored in this backend.
409497
410 @returns function: a function that accepts a task and returns True/False498 @returns function: a function that accepts a task and returns True/False
411 whether the task should be stored or not499 whether the task should be stored or not
@@ -417,6 +505,7 @@
417 def should_task_id_be_stored(self, task_id):505 def should_task_id_be_stored(self, task_id):
418 '''506 '''
419 Helper function: Checks if a task should be stored in this backend507 Helper function: Checks if a task should be stored in this backend
508
420 @param task_id: a task id509 @param task_id: a task id
421 @returns bool: True if the task should be stored510 @returns bool: True if the task should be stored
422 '''511 '''
@@ -427,6 +516,7 @@
427 """516 """
428 Updates the task in the DataStore. Actually, it adds the task to a517 Updates the task in the DataStore. Actually, it adds the task to a
429 queue to be updated asynchronously.518 queue to be updated asynchronously.
519
430 @param sender: not used, any value will do.520 @param sender: not used, any value will do.
431 @param task: The Task object to be updated.521 @param task: The Task object to be updated.
432 """522 """
@@ -437,14 +527,17 @@
437 else:527 else:
438 self.queue_remove_task(None, tid)528 self.queue_remove_task(None, tid)
439 529
440 def launch_setting_thread(self):530 def launch_setting_thread(self, bypass_please_quit = False):
441 '''531 '''
442 Operates the threads to set and remove tasks.532 Operates the threads to set and remove tasks.
443 Releases the lock when it is done.533 Releases the lock when it is done.
534
535 @param bypass_please_quit: if True, the self.please_quit "quit condition"
536 is ignored. Currently, it's turned to true
537 after the quit condition has been issued, to
538 execute eventual pending operations.
444 '''539 '''
445 #FIXME: the lock should be general for all backends. Therefore, it540 while not self.please_quit or bypass_please_quit:
446 #should be handled in the datastore
447 while True:
448 try:541 try:
449 tid = self.to_set.pop()542 tid = self.to_set.pop()
450 except IndexError:543 except IndexError:
@@ -457,7 +550,7 @@
457 self.req.has_task(tid):550 self.req.has_task(tid):
458 task = self.req.get_task(tid)551 task = self.req.get_task(tid)
459 self.backend.queue_set_task(task)552 self.backend.queue_set_task(task)
460 while True:553 while not self.please_quit or bypass_please_quit:
461 try:554 try:
462 tid = self.to_remove.pop()555 tid = self.to_remove.pop()
463 except IndexError:556 except IndexError:
@@ -469,6 +562,7 @@
469 def queue_remove_task(self, sender, tid):562 def queue_remove_task(self, sender, tid):
470 '''563 '''
471 Queues task to be removed.564 Queues task to be removed.
565
472 @param sender: not used, any value will do566 @param sender: not used, any value will do
473 @param tid: The Task ID of the task to be removed567 @param tid: The Task ID of the task to be removed
474 '''568 '''
@@ -480,14 +574,16 @@
480 '''574 '''
481 Helper function to launch the setting thread, if it's not running575 Helper function to launch the setting thread, if it's not running
482 '''576 '''
483 if self.to_set_timer == None:577 if self.to_set_timer == None and not self.please_quit:
484 self.to_set_timer = threading.Timer(self.timer_timestep, \578 self.to_set_timer = threading.Timer(self.timer_timestep, \
485 self.launch_setting_thread)579 self.launch_setting_thread)
580 self.to_set_timer.setDaemon(True)
486 self.to_set_timer.start()581 self.to_set_timer.start()
487582
488 def initialize(self, connect_signals = True):583 def initialize(self, connect_signals = True):
489 '''584 '''
490 Initializes the backend and starts looking for signals.585 Initializes the backend and starts looking for signals.
586
491 @param connect_signals: if True, it starts listening for signals587 @param connect_signals: if True, it starts listening for signals
492 '''588 '''
493 self.backend.initialize()589 self.backend.initialize()
@@ -520,23 +616,28 @@
520 '''616 '''
521 Forces the TaskSource to sync all the pending tasks617 Forces the TaskSource to sync all the pending tasks
522 '''618 '''
523 if self.to_set_timer != None:619 try:
524 try:620 self.to_set_timer.cancel()
525 self.to_set_timer.cancel()621 except Exception, e:
526 except:622 pass
527 pass623 try:
528 try:624 self.to_set_timer.join(3)
529 self.to_set_timer.join(5)625 except Exception, e:
530 except:626 pass
531 pass627 try:
532 self.launch_setting_thread()628 self.start_get_tasks_thread.join(3)
629 except:
630 pass
631 self.launch_setting_thread(bypass_please_quit = True)
533632
534 def quit(self, disable = False):633 def quit(self, disable = False):
535 '''634 '''
536 Quits the backend and disconnect the signals635 Quits the backend and disconnect the signals
636
537 @param disable: if True, the backend is disabled.637 @param disable: if True, the backend is disabled.
538 '''638 '''
539 self._disconnect_signals()639 self._disconnect_signals()
640 self.please_quit = True
540 self.sync()641 self.sync()
541 self.backend.quit(disable)642 self.backend.quit(disable)
542 643
@@ -544,6 +645,7 @@
544 '''645 '''
545 Delegates all the functions not defined here to the real backend646 Delegates all the functions not defined here to the real backend
546 (standard python function)647 (standard python function)
648
547 @param attr: attribute to get649 @param attr: attribute to get
548 '''650 '''
549 if attr in self.__dict__: 651 if attr in self.__dict__:
@@ -566,12 +668,15 @@
566 self.datastore = datastore668 self.datastore = datastore
567669
568 def __getattr__(self, attr):670 def __getattr__(self, attr):
569 if attr in ['task_factory', \671 if attr in ['task_factory',
570 'push_task',672 'push_task',
571 'get_task',673 'get_task',
572 'has_task',674 'has_task',
675 'get_backend_mutex',
573 'request_task_deletion']:676 'request_task_deletion']:
574 return getattr(self.datastore, attr)677 return getattr(self.datastore, attr)
678 elif attr in ['get_all_tags']:
679 return self.datastore.requester.get_all_tags
575 else:680 else:
576 raise AttributeError681 raise AttributeError
577682
578683
=== 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:43:04 +0000
@@ -791,7 +791,10 @@
791 self.node_to_remove.append(tid)791 self.node_to_remove.append(tid)
792 isroot = False792 isroot = False
793 if tid in self.displayed_nodes:793 if tid in self.displayed_nodes:
794 isroot = self.__is_root(self.get_node(tid))794 node = self.get_node(tid)
795 if not node:
796 return
797 isroot = self.__is_root(node)
795 self.remove_count += 1798 self.remove_count += 1
796 self.__nodes_count -= 1799 self.__nodes_count -= 1
797 self.emit('task-deleted-inview',tid)800 self.emit('task-deleted-inview',tid)
798801
=== 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:43:04 +0000
@@ -256,6 +256,8 @@
256 @param task: a task object256 @param task: a task object
257 @oaram tags_to_match_set: a *set* of tag names257 @oaram tags_to_match_set: a *set* of tag names
258 '''258 '''
259 if task == None:
260 return []
259 try:261 try:
260 tags_to_match_set = parameters['tags']262 tags_to_match_set = parameters['tags']
261 except KeyError:263 except KeyError:
262264
=== 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:43:04 +0000
@@ -284,3 +284,6 @@
284284
285 def backend_change_attached_tags(self, backend_id, tags):285 def backend_change_attached_tags(self, backend_id, tags):
286 return self.ds.backend_change_attached_tags(backend_id, tags)286 return self.ds.backend_change_attached_tags(backend_id, tags)
287
288 def save_datastore(self):
289 return self.ds.save()
287290
=== 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:43:04 +0000
@@ -505,7 +505,9 @@
505 def get_tags(self):505 def get_tags(self):
506 l = []506 l = []
507 for tname in self.tags:507 for tname in self.tags:
508 l.append(self.req.get_tag(tname))508 tag = self.req.get_tag(tname)
509 if tag:
510 l.append(tag)
509 return l511 return l
510 512
511 def rename_tag(self, old, new):513 def rename_tag(self, old, new):
512514
=== 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:43:04 +0000
@@ -953,7 +953,9 @@
953 text = \953 text = \
954 text.replace("%s%s:%s" % (spaces, attribute, args), "")954 text.replace("%s%s:%s" % (spaces, attribute, args), "")
955 # Create the new task955 # Create the new task
956 task = self.req.new_task(tags=[t.get_name() for t in tags], newtask=True)956 task = self.req.new_task( newtask=True)
957 for tag in tags:
958 task.add_tag(tag.get_name())
957 if text != "":959 if text != "":
958 task.set_title(text.strip())960 task.set_title(text.strip())
959 task.set_to_keep()961 task.set_to_keep()
960962
=== 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:43:04 +0000
@@ -1,6 +1,6 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------2# -----------------------------------------------------------------------------
3# Gettings Things Gnome! - a personal organizer for the GNOME desktop3# Getting Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#5#
6# This program is free software: you can redistribute it and/or modify it under6# This program is free software: you can redistribute it and/or modify it under
@@ -30,8 +30,8 @@
3030
31# GTG imports31# GTG imports
32from GTG.backends import backend_localfile as localfile32from GTG.backends import backend_localfile as localfile
33from GTG.core import datastore
34from GTG.tools import cleanxml33from GTG.tools import cleanxml
34from GTG.core import CoreConfig
3535
3636
37class GtgBackendsUniTests(unittest.TestCase):37class GtgBackendsUniTests(unittest.TestCase):
@@ -44,6 +44,10 @@
44 self.taskpath = ''44 self.taskpath = ''
45 self.datapath = ''45 self.datapath = ''
46 46
47 def SetUp(self):
48 CoreConfig().set_data_dir("./test_data")
49 CoreConfig().set_conf_dir("./test_data")
50
47 def test_localfile_get_name(self):51 def test_localfile_get_name(self):
48 """Tests for localfile/get_name function :52 """Tests for localfile/get_name function :
49 - a string is expected.53 - a string is expected.
@@ -105,52 +109,6 @@
105 expectedres = True109 expectedres = True
106 self.assertEqual(res, expectedres)110 self.assertEqual(res, expectedres)
107111
108# def test_localfile_backend_method4(self):
109# """Tests for localfile/Backend/get_task method:
110# - Compares task titles to check if method works.
111# """
112# self.create_test_environment()
113# doc, configxml = cleanxml.openxmlfile(self.datapath, 'config')
114# xmlproject = doc.getElementsByTagName('backend')
115# for domobj in xmlproject:
116# dic = {}
117# if domobj.hasAttribute("module"):
118# dic["module"] = str(domobj.getAttribute("module"))
119# dic["pid"] = str(domobj.getAttribute("pid"))
120# dic["xmlobject"] = domobj
121# dic["filename"] = self.taskfile
122# beobj = localfile.Backend(dic)
123# dstore = datastore.DataStore()
124# newtask = dstore.new_task(tid="0@2", pid="1", newtask=True)
125# beobj.get_task(newtask, "0@1")
126# self.assertEqual(newtask.get_title(), u"Ceci est un test")
127
128# def test_localfile_backend_method5(self):
129# """Tests for localfile/Backend/set_task method:
130# - parses task file to check if new task has been stored.
131# """
132# self.create_test_environment()
133# doc, configxml = cleanxml.openxmlfile(self.datapath, 'config')
134# xmlproject = doc.getElementsByTagName('backend')
135# for domobj in xmlproject:
136# dic = {}
137# if domobj.hasAttribute("module"):
138# dic["module"] = str(domobj.getAttribute("module"))
139# dic["pid"] = str(domobj.getAttribute("pid"))
140# dic["xmlobject"] = domobj
141# dic["filename"] = self.taskfile
142# beobj = localfile.Backend(dic)
143# dstore = datastore.DataStore()
144# newtask = dstore.new_task(tid="0@2", pid="1", newtask=True)
145# beobj.set_task(newtask)
146# dataline = open(self.taskpath, 'r').read()
147# if "0@2" in dataline:
148# res = True
149# else:
150# res = False
151# expectedres = True
152# self.assertEqual(res, expectedres)
153
154 def create_test_environment(self):112 def create_test_environment(self):
155 """Create the test environment"""113 """Create the test environment"""
156 self.taskfile = 'test.xml'114 self.taskfile = 'test.xml'
157115
=== 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:43:04 +0000
@@ -0,0 +1,61 @@
1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Gettings Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#
6# This program is free software: you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# -----------------------------------------------------------------------------
19
20'''
21Tests for interrupting cooperative threads
22'''
23
24import unittest
25import time
26from threading import Thread, Event
27
28from GTG.tools.interruptible import interruptible, _cancellation_point
29
30
31class TestInterruptible(unittest.TestCase):
32 '''
33 Tests for interrupting cooperative threads
34 '''
35
36 def test_interruptible_decorator(self):
37 '''Tests for the @interruptible decorator.'''
38 self.quit_condition = False
39 cancellation_point = lambda: _cancellation_point(\
40 lambda: self.quit_condition)
41 self.thread_started = Event()
42 @interruptible
43 def never_ending(cancellation_point):
44 self.thread_started.set()
45 while True:
46 time.sleep(0.1)
47 cancellation_point()
48 thread = Thread(target = never_ending, args = (cancellation_point, ))
49 thread.start()
50 self.thread_started.wait()
51 self.quit_condition = True
52 countdown = 10
53 while thread.is_alive() and countdown > 0:
54 time.sleep(0.1)
55 countdown -= 1
56 self.assertFalse(thread.is_alive())
57
58
59def test_suite():
60 return unittest.TestLoader().loadTestsFromTestCase(TestInterruptible)
61
062
=== 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:43:04 +0000
@@ -0,0 +1,62 @@
1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Gettings Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#
6# This program is free software: you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# -----------------------------------------------------------------------------
19
20'''
21Utils to stop and quit gracefully a thread, issuing the command from
22another one
23'''
24
25
26
27class Interrupted(Exception):
28 '''Exception raised when a thread should be interrupted'''
29
30
31 pass
32
33
34
35
36def interruptible(fn):
37 '''
38 A decorator that makes a function interruptible. It should be applied only
39 to the function which is the target of a Thread object.
40 '''
41
42
43 def new(*args):
44 try:
45 return fn(*args)
46 except Interrupted:
47 return
48 return new
49
50def _cancellation_point(test_function):
51 '''
52 This function checks a test_function and, if it evaluates to True, makes the
53 thread quit (similar to pthread_cancel() in C)
54 It starts with a _ as it's mostly used in a specialized form, as:
55 cancellation_point = functools.partial(_cancellation_point,
56 lambda: quit_condition == True)
57
58 @param test_function: the function to test before cancelling
59 '''
60 if test_function():
61 raise Interrupted
62
063
=== 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:43:04 +0000
@@ -20,6 +20,7 @@
20#Functions to convert a Task object to an XML string and back20#Functions to convert a Task object to an XML string and back
21import xml.dom.minidom21import xml.dom.minidom
22import xml.sax.saxutils as saxutils22import xml.sax.saxutils as saxutils
23import datetime
2324
24from GTG.tools import cleanxml25from GTG.tools import cleanxml
25from GTG.tools import dates26from GTG.tools import dates
@@ -55,7 +56,6 @@
55 content = xml.dom.minidom.parseString(tas)56 content = xml.dom.minidom.parseString(tas)
56 cur_task.set_text(content.firstChild.toxml()) #pylint: disable-msg=E1103 57 cur_task.set_text(content.firstChild.toxml()) #pylint: disable-msg=E1103
57 cur_task.set_due_date(dates.strtodate(cleanxml.readTextNode(xmlnode,"duedate")))58 cur_task.set_due_date(dates.strtodate(cleanxml.readTextNode(xmlnode,"duedate")))
58 cur_task.set_modified(cleanxml.readTextNode(xmlnode,"modified"))
59 cur_task.set_start_date(dates.strtodate(cleanxml.readTextNode(xmlnode,"startdate")))59 cur_task.set_start_date(dates.strtodate(cleanxml.readTextNode(xmlnode,"startdate")))
60 cur_tags = xmlnode.getAttribute("tags").replace(' ','').split(",")60 cur_tags = xmlnode.getAttribute("tags").replace(' ','').split(",")
61 if "" in cur_tags: cur_tags.remove("")61 if "" in cur_tags: cur_tags.remove("")
@@ -69,6 +69,11 @@
69 backend_id = node.firstChild.nodeValue69 backend_id = node.firstChild.nodeValue
70 remote_task_id = node.childNodes[1].firstChild.nodeValue70 remote_task_id = node.childNodes[1].firstChild.nodeValue
71 task.add_remote_id(backend_id, remote_task_id)71 task.add_remote_id(backend_id, remote_task_id)
72 modified_string = cleanxml.readTextNode(xmlnode,"modified")
73 if modified_string:
74 modified_datetime = datetime.datetime.strptime(modified_string,\
75 "%Y-%m-%dT%H:%M:%S")
76 cur_task.set_modified(modified_datetime)
72 return cur_task77 return cur_task
7378
74#Task as parameter the doc where to put the XML node79#Task as parameter the doc where to put the XML node
7580
=== 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:43:04 +0000
@@ -0,0 +1,610 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
4<svg
5 xmlns:dc="http://purl.org/dc/elements/1.1/"
6 xmlns:cc="http://creativecommons.org/ns#"
7 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
8 xmlns:svg="http://www.w3.org/2000/svg"
9 xmlns="http://www.w3.org/2000/svg"
10 xmlns:xlink="http://www.w3.org/1999/xlink"
11 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
12 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
13 inkscape:export-ydpi="600"
14 inkscape:export-xdpi="600"
15 inkscape:export-filename="/home/luca/Projects/gtg/Google/GSoC/data/icons/hicolor/scalable/apps/backend_localfile.png"
16 sodipodi:docname="backend_localfile.svg"
17 inkscape:version="0.47 r22583"
18 sodipodi:version="0.32"
19 id="svg249"
20 height="48.000000px"
21 width="48.000000px"
22 inkscape:output_extension="org.inkscape.output.svg.inkscape"
23 version="1.1">
24 <defs
25 id="defs3">
26 <inkscape:perspective
27 sodipodi:type="inkscape:persp3d"
28 inkscape:vp_x="0 : 24 : 1"
29 inkscape:vp_y="0 : 1000 : 0"
30 inkscape:vp_z="48 : 24 : 1"
31 inkscape:persp3d-origin="24 : 16 : 1"
32 id="perspective80" />
33 <linearGradient
34 inkscape:collect="always"
35 xlink:href="#linearGradient3656"
36 id="linearGradient5816"
37 gradientUnits="userSpaceOnUse"
38 x1="-26.753757"
39 y1="11.566258"
40 x2="-24.75"
41 y2="9.687501" />
42 <linearGradient
43 inkscape:collect="always"
44 xlink:href="#linearGradient3520"
45 id="linearGradient5836"
46 gradientUnits="userSpaceOnUse"
47 gradientTransform="matrix(0.9223058,0,0,0.9185751,-92.447368,61.3257)"
48 x1="-18.588562"
49 y1="11.052948"
50 x2="-28.789402"
51 y2="14.069944" />
52 <radialGradient
53 inkscape:collect="always"
54 xlink:href="#linearGradient3671"
55 id="radialGradient5839"
56 gradientUnits="userSpaceOnUse"
57 gradientTransform="matrix(0.4073362,-0.2798276,0.7510293,1.0932492,-115.18484,51.56213)"
58 cx="-26.305403"
59 cy="10.108011"
60 fx="-26.305403"
61 fy="10.108011"
62 r="7.0421038" />
63 <linearGradient
64 inkscape:collect="always"
65 id="linearGradient6469">
66 <stop
67 style="stop-color:#000000;stop-opacity:1;"
68 offset="0"
69 id="stop6471" />
70 <stop
71 style="stop-color:#000000;stop-opacity:0;"
72 offset="1"
73 id="stop6473" />
74 </linearGradient>
75 <linearGradient
76 inkscape:collect="always"
77 xlink:href="#linearGradient6469"
78 id="linearGradient6475"
79 x1="58.282169"
80 y1="70.751839"
81 x2="61.181217"
82 y2="67.799171"
83 gradientUnits="userSpaceOnUse"
84 gradientTransform="translate(-180,0)" />
85 <radialGradient
86 inkscape:collect="always"
87 xlink:href="#linearGradient3741"
88 id="radialGradient5810"
89 gradientUnits="userSpaceOnUse"
90 gradientTransform="matrix(1.8860258,0,0,1.1764706,-3.5441033,-4.2352941)"
91 cx="4"
92 cy="5.2999997"
93 fx="4"
94 fy="5.2999997"
95 r="17" />
96 <linearGradient
97 inkscape:collect="always"
98 xlink:href="#linearGradient3613"
99 id="linearGradient5845"
100 gradientUnits="userSpaceOnUse"
101 gradientTransform="translate(-90,60)"
102 x1="-47.5"
103 y1="49.020683"
104 x2="-62.75"
105 y2="-22.502075" />
106 <radialGradient
107 inkscape:collect="always"
108 xlink:href="#linearGradient3683"
109 id="radialGradient5843"
110 gradientUnits="userSpaceOnUse"
111 gradientTransform="matrix(3.9957492,0,0,1.9350367,0.62141,28.832578)"
112 cx="-30.249996"
113 cy="35.357208"
114 fx="-30.249996"
115 fy="35.357208"
116 r="18.000002" />
117 <linearGradient
118 inkscape:collect="always"
119 xlink:href="#linearGradient3702"
120 id="linearGradient5804"
121 gradientUnits="userSpaceOnUse"
122 x1="25.058096"
123 y1="47.027729"
124 x2="25.058096"
125 y2="39.999443" />
126 <radialGradient
127 inkscape:collect="always"
128 xlink:href="#linearGradient3688"
129 id="radialGradient5802"
130 gradientUnits="userSpaceOnUse"
131 gradientTransform="matrix(2.003784,0,0,1.4,-20.01187,-104.4)"
132 cx="4.9929786"
133 cy="43.5"
134 fx="4.9929786"
135 fy="43.5"
136 r="2.5" />
137 <radialGradient
138 inkscape:collect="always"
139 xlink:href="#linearGradient3688"
140 id="radialGradient5800"
141 gradientUnits="userSpaceOnUse"
142 gradientTransform="matrix(2.003784,0,0,1.4,27.98813,-17.4)"
143 cx="4.9929786"
144 cy="43.5"
145 fx="4.9929786"
146 fy="43.5"
147 r="2.5" />
148 <linearGradient
149 inkscape:collect="always"
150 xlink:href="#linearGradient3702"
151 id="linearGradient5798"
152 gradientUnits="userSpaceOnUse"
153 x1="25.058096"
154 y1="47.027729"
155 x2="25.058096"
156 y2="39.999443" />
157 <radialGradient
158 inkscape:collect="always"
159 xlink:href="#linearGradient3688"
160 id="radialGradient5796"
161 gradientUnits="userSpaceOnUse"
162 gradientTransform="matrix(2.003784,0,0,1.4,-20.01187,-104.4)"
163 cx="4.9929786"
164 cy="43.5"
165 fx="4.9929786"
166 fy="43.5"
167 r="2.5" />
168 <radialGradient
169 inkscape:collect="always"
170 xlink:href="#linearGradient3688"
171 id="radialGradient5794"
172 gradientUnits="userSpaceOnUse"
173 gradientTransform="matrix(2.003784,0,0,1.4,27.98813,-17.4)"
174 cx="4.9929786"
175 cy="43.5"
176 fx="4.9929786"
177 fy="43.5"
178 r="2.5" />
179 <linearGradient
180 inkscape:collect="always"
181 id="linearGradient3656">
182 <stop
183 style="stop-color:#ffffff;stop-opacity:1;"
184 offset="0"
185 id="stop3658" />
186 <stop
187 style="stop-color:#ffffff;stop-opacity:0;"
188 offset="1"
189 id="stop3660" />
190 </linearGradient>
191 <linearGradient
192 inkscape:collect="always"
193 id="linearGradient3520">
194 <stop
195 style="stop-color:#000000;stop-opacity:0.41295547"
196 offset="0"
197 id="stop3522" />
198 <stop
199 style="stop-color:#000000;stop-opacity:0;"
200 offset="1"
201 id="stop3524" />
202 </linearGradient>
203 <linearGradient
204 id="linearGradient3671">
205 <stop
206 style="stop-color:#ffffff;stop-opacity:1;"
207 offset="0"
208 id="stop3673" />
209 <stop
210 id="stop3691"
211 offset="0.47533694"
212 style="stop-color:#ffffff;stop-opacity:1;" />
213 <stop
214 style="stop-color:#ffffff;stop-opacity:0;"
215 offset="1"
216 id="stop3675" />
217 </linearGradient>
218 <linearGradient
219 inkscape:collect="always"
220 id="linearGradient3741">
221 <stop
222 style="stop-color:#ffffff;stop-opacity:1;"
223 offset="0"
224 id="stop3743" />
225 <stop
226 style="stop-color:#ffffff;stop-opacity:0;"
227 offset="1"
228 id="stop3745" />
229 </linearGradient>
230 <linearGradient
231 inkscape:collect="always"
232 id="linearGradient3613">
233 <stop
234 style="stop-color:#888a85;stop-opacity:1"
235 offset="0"
236 id="stop3615" />
237 <stop
238 style="stop-color:#babdb6;stop-opacity:1"
239 offset="1"
240 id="stop3617" />
241 </linearGradient>
242 <linearGradient
243 id="linearGradient3683">
244 <stop
245 id="stop3685"
246 offset="0"
247 style="stop-color:#f6f6f5;stop-opacity:1;" />
248 <stop
249 id="stop3689"
250 offset="1"
251 style="stop-color:#d3d7cf;stop-opacity:1" />
252 </linearGradient>
253 <linearGradient
254 id="linearGradient3688"
255 inkscape:collect="always">
256 <stop
257 id="stop3690"
258 offset="0"
259 style="stop-color:black;stop-opacity:1;" />
260 <stop
261 id="stop3692"
262 offset="1"
263 style="stop-color:black;stop-opacity:0;" />
264 </linearGradient>
265 <linearGradient
266 id="linearGradient3702">
267 <stop
268 id="stop3704"
269 offset="0"
270 style="stop-color:black;stop-opacity:0;" />
271 <stop
272 style="stop-color:black;stop-opacity:1;"
273 offset="0.5"
274 id="stop3710" />
275 <stop
276 id="stop3706"
277 offset="1"
278 style="stop-color:black;stop-opacity:0;" />
279 </linearGradient>
280 </defs>
281 <sodipodi:namedview
282 inkscape:window-y="26"
283 inkscape:window-x="230"
284 inkscape:window-height="742"
285 inkscape:window-width="1048"
286 inkscape:document-units="px"
287 inkscape:grid-bbox="true"
288 showgrid="false"
289 inkscape:current-layer="layer6"
290 inkscape:cy="9.7451355"
291 inkscape:cx="46.371834"
292 inkscape:zoom="1"
293 inkscape:pageshadow="2"
294 inkscape:pageopacity="0.0"
295 borderopacity="0.25490196"
296 bordercolor="#666666"
297 pagecolor="#ffffff"
298 id="base"
299 inkscape:showpageshadow="false"
300 inkscape:window-maximized="0" />
301 <metadata
302 id="metadata4">
303 <rdf:RDF>
304 <cc:Work
305 rdf:about="">
306 <dc:format>image/svg+xml</dc:format>
307 <dc:type
308 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
309 <dc:title>Generic Text</dc:title>
310 <dc:subject>
311 <rdf:Bag>
312 <rdf:li>text</rdf:li>
313 <rdf:li>plaintext</rdf:li>
314 <rdf:li>regular</rdf:li>
315 <rdf:li>document</rdf:li>
316 </rdf:Bag>
317 </dc:subject>
318 <cc:license
319 rdf:resource="http://creativecommons.org/licenses/GPL/2.0/" />
320 <dc:creator>
321 <cc:Agent>
322 <dc:title>Lapo Calamandrei</dc:title>
323 </cc:Agent>
324 </dc:creator>
325 <dc:source />
326 <dc:date />
327 <dc:rights>
328 <cc:Agent>
329 <dc:title />
330 </cc:Agent>
331 </dc:rights>
332 <dc:publisher>
333 <cc:Agent>
334 <dc:title />
335 </cc:Agent>
336 </dc:publisher>
337 <dc:identifier />
338 <dc:relation />
339 <dc:language />
340 <dc:coverage />
341 <dc:description />
342 <dc:contributor>
343 <cc:Agent>
344 <dc:title />
345 </cc:Agent>
346 </dc:contributor>
347 </cc:Work>
348 <cc:License
349 rdf:about="http://creativecommons.org/licenses/GPL/2.0/">
350 <cc:permits
351 rdf:resource="http://web.resource.org/cc/Reproduction" />
352 <cc:permits
353 rdf:resource="http://web.resource.org/cc/Distribution" />
354 <cc:requires
355 rdf:resource="http://web.resource.org/cc/Notice" />
356 <cc:permits
357 rdf:resource="http://web.resource.org/cc/DerivativeWorks" />
358 <cc:requires
359 rdf:resource="http://web.resource.org/cc/ShareAlike" />
360 <cc:requires
361 rdf:resource="http://web.resource.org/cc/SourceCode" />
362 </cc:License>
363 </rdf:RDF>
364 </metadata>
365 <g
366 inkscape:groupmode="layer"
367 id="layer6"
368 inkscape:label="Shadow">
369 <g
370 style="display:inline"
371 id="g6015"
372 transform="translate(150,-60)">
373 <rect
374 y="60"
375 x="-150"
376 height="48"
377 width="48"
378 id="rect5504"
379 style="opacity:0;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;display:inline" />
380 <g
381 transform="matrix(1.0464281,0,0,0.8888889,-151.18572,65.72224)"
382 inkscape:label="Shadow"
383 id="g5508"
384 style="opacity:0.65587045;display:inline">
385 <g
386 transform="matrix(1.052632,0,0,1.285713,-1.263158,-13.42854)"
387 style="opacity:0.4"
388 id="g5511">
389 <rect
390 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"
391 id="rect5513"
392 width="5"
393 height="7"
394 x="38"
395 y="40" />
396 <rect
397 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"
398 id="rect5515"
399 width="5"
400 height="7"
401 x="-10"
402 y="-47"
403 transform="scale(-1,-1)" />
404 <rect
405 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"
406 id="rect5517"
407 width="28"
408 height="7.0000005"
409 x="10"
410 y="40" />
411 </g>
412 </g>
413 <g
414 transform="matrix(0.9548466,0,0,0.5555562,-148.98776,79.888875)"
415 inkscape:label="Shadow"
416 id="g5519"
417 style="display:inline">
418 <g
419 transform="matrix(1.052632,0,0,1.285713,-1.263158,-13.42854)"
420 style="opacity:0.4"
421 id="g5521">
422 <rect
423 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"
424 id="rect5523"
425 width="5"
426 height="7"
427 x="38"
428 y="40" />
429 <rect
430 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"
431 id="rect5525"
432 width="5"
433 height="7"
434 x="-10"
435 y="-47"
436 transform="scale(-1,-1)" />
437 <rect
438 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"
439 id="rect5527"
440 width="28"
441 height="7.0000005"
442 x="10"
443 y="40" />
444 </g>
445 </g>
446 <path
447 sodipodi:nodetypes="ccsccccccc"
448 id="path5529"
449 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"
450 style="fill:url(#radialGradient5843);fill-opacity:1;stroke:url(#linearGradient5845);stroke-width:1;stroke-miterlimit:4;display:inline" />
451 <path
452 transform="translate(-150,60)"
453 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"
454 id="path5531"
455 style="opacity:0.68016196;fill:url(#radialGradient5810);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;display:inline"
456 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 "
457 inkscape:radius="-0.4861359"
458 sodipodi:type="inkscape:offset" />
459 <path
460 id="rect5857"
461 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"
462 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" />
463 <path
464 sodipodi:nodetypes="ccccczc"
465 id="path5533"
466 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"
467 style="fill:url(#radialGradient5839);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;display:inline" />
468 <path
469 sodipodi:nodetypes="ccccc"
470 id="path5535"
471 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"
472 style="opacity:0.87854249;fill:url(#linearGradient5836);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;display:inline" />
473 <path
474 transform="translate(-90,60)"
475 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"
476 id="path5537"
477 style="fill:none;fill-opacity:1;stroke:url(#linearGradient5816);stroke-width:1;stroke-miterlimit:4;display:inline"
478 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 "
479 inkscape:radius="-0.99436891"
480 sodipodi:type="inkscape:offset" />
481 <g
482 inkscape:r_cy="true"
483 inkscape:r_cx="true"
484 transform="matrix(0.928889,0,0,1,-148.28889,60)"
485 style="opacity:0.15;fill:#000000;display:inline"
486 id="g5539">
487 <rect
488 ry="0.46875"
489 rx="0.50463516"
490 inkscape:r_cy="true"
491 inkscape:r_cx="true"
492 y="19.0625"
493 x="10"
494 height="0.9375"
495 width="28.125"
496 id="rect5549"
497 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" />
498 <rect
499 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"
500 id="rect5553"
501 width="28.125"
502 height="0.9375"
503 x="10"
504 y="21.0625"
505 inkscape:r_cx="true"
506 inkscape:r_cy="true"
507 rx="0.50463516"
508 ry="0.46875" />
509 <rect
510 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"
511 id="rect5555"
512 width="28.125"
513 height="0.9375"
514 x="10"
515 y="23.0625"
516 inkscape:r_cx="true"
517 inkscape:r_cy="true"
518 rx="0.50463516"
519 ry="0.46875" />
520 <rect
521 ry="0.46875"
522 rx="0.50463516"
523 inkscape:r_cy="true"
524 inkscape:r_cx="true"
525 y="25.0625"
526 x="10"
527 height="0.9375"
528 width="28.125"
529 id="rect5557"
530 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" />
531 <rect
532 ry="0.46875"
533 rx="0.50463516"
534 inkscape:r_cy="true"
535 inkscape:r_cx="true"
536 y="27.0625"
537 x="10"
538 height="0.9375"
539 width="28.125"
540 id="rect5559"
541 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" />
542 <rect
543 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"
544 id="rect5561"
545 width="28.125"
546 height="0.9375"
547 x="10"
548 y="29.0625"
549 inkscape:r_cx="true"
550 inkscape:r_cy="true"
551 rx="0.50463516"
552 ry="0.46875" />
553 <rect
554 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"
555 id="rect5563"
556 width="28.125"
557 height="0.9375"
558 x="10"
559 y="31.0625"
560 inkscape:r_cx="true"
561 inkscape:r_cy="true"
562 rx="0.50463516"
563 ry="0.46875" />
564 <rect
565 ry="0.46875"
566 rx="0.50463516"
567 inkscape:r_cy="true"
568 inkscape:r_cx="true"
569 y="33.0625"
570 x="10"
571 height="0.9375"
572 width="28.125"
573 id="rect5565"
574 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" />
575 <rect
576 ry="0.46875"
577 rx="0.50463516"
578 inkscape:r_cy="true"
579 inkscape:r_cx="true"
580 y="35.0625"
581 x="10"
582 height="0.9375"
583 width="28.125"
584 id="rect5567"
585 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" />
586 <rect
587 ry="0.46875"
588 rx="0.50463516"
589 inkscape:r_cy="true"
590 inkscape:r_cx="true"
591 y="37.0625"
592 x="10"
593 height="0.9375"
594 width="13.8125"
595 id="rect5569"
596 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" />
597 </g>
598 </g>
599 </g>
600 <g
601 style="display:inline"
602 inkscape:groupmode="layer"
603 inkscape:label="Base"
604 id="layer1" />
605 <g
606 inkscape:groupmode="layer"
607 id="layer5"
608 inkscape:label="Text"
609 style="display:inline" />
610</svg>

Subscribers

People subscribed via source and target branches

to status/vote changes: