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
1=== modified file 'CHANGELOG'
2--- CHANGELOG 2010-08-04 00:30:22 +0000
3+++ CHANGELOG 2010-08-13 23:43:04 +0000
4@@ -4,6 +4,7 @@
5 * Fixed bug with data consistency #579189, by Marko Kevac
6 * Added samba bugzilla to the bugzilla plugin, by Jelmer Vernoij
7 * Fixed bug #532392, a start date is later than a due date, by Volodymyr Floreskul
8+ * Extended backend system to support multiple backends by Luca Invernizzi
9
10 2010-03-01 Getting Things GNOME! 0.2.2
11 * Autostart on login, by Luca Invernizzi
12
13=== modified file 'GTG/backends/backend_localfile.py'
14--- GTG/backends/backend_localfile.py 2010-06-22 20:24:01 +0000
15+++ GTG/backends/backend_localfile.py 2010-08-13 23:43:04 +0000
16@@ -20,6 +20,9 @@
17 '''
18 Localfile is a read/write backend that will store your tasks in an XML file
19 This file will be in your $XDG_DATA_DIR/gtg folder.
20+
21+This backend contains comments that are meant as a reference, in case someone
22+wants to write a backend.
23 '''
24
25 import os
26@@ -29,18 +32,30 @@
27 from GTG.core import CoreConfig
28 from GTG.tools import cleanxml, taskxml
29 from GTG import _
30+from GTG.tools.logger import Log
31
32
33
34 class Backend(GenericBackend):
35+ '''
36+ Localfile backend, which stores your tasks in a XML file in the standard
37+ XDG_DATA_DIR/gtg folder (the path is configurable).
38+ An instance of this class is used as the default backend for GTG.
39+ This backend loads all the tasks stored in the localfile after it's enabled,
40+ and from that point on just writes the changes to the file: it does not
41+ listen for eventual file changes
42+ '''
43
44
45 DEFAULT_PATH = CoreConfig().get_data_dir() #default path for filenames
46
47
48- #Description of the backend (mainly it's data we show the user, only the
49- # name is used internally. Please note that BACKEND_NAME and
50- # BACKEND_ICON_NAME should *not* be translated.
51+ #General description of the backend: these are used to show a description of
52+ # the backend to the user when s/he is considering adding it.
53+ # BACKEND_NAME is the name of the backend used internally (it must be
54+ # unique).
55+ #Please note that BACKEND_NAME and BACKEND_ICON_NAME should *not* be
56+ #translated.
57 _general_description = { \
58 GenericBackend.BACKEND_NAME: "backend_localfile", \
59 GenericBackend.BACKEND_HUMAN_NAME: _("Local File"), \
60@@ -53,10 +68,14 @@
61 "for GTG to save your tasks."),\
62 }
63
64- #parameters to configure a new backend of this type.
65- #NOTE: should we always give back a different default filename? it can be
66- # done, but I'd like to keep this backend simple, so that it can be
67- # used as example (invernizzi)
68+ #These are the parameters to configure a new backend of this type. A
69+ # parameter has a name, a type and a default value.
70+ # Here, we define a parameter "path", which is a string, and has a default
71+ # value as a random file in the default path
72+ #NOTE: to keep this simple, the filename default path is the same until GTG
73+ # is restarted. I consider this a minor annoyance, and we can avoid
74+ # coding the change of the path each time a backend is
75+ # created (invernizzi)
76 _static_parameters = { \
77 "path": { \
78 GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING, \
79@@ -64,30 +83,22 @@
80 os.path.join(DEFAULT_PATH, "gtg_tasks-%s.xml" %(uuid.uuid4()))
81 }}
82
83- def _get_default_filename_path(self, filename = None):
84- '''
85- Generates a default path with a random filename
86- @param filename: specify a filename
87- '''
88- if not filename:
89- filename = "gtg_tasks-%s.xml" % (uuid.uuid4())
90- return os.path.join(self.DEFAULT_PATH, filename)
91-
92 def __init__(self, parameters):
93 """
94 Instantiates a new backend.
95
96- @param parameters: should match the dictionary returned in
97- get_parameters. Anyway, the backend should care if one expected
98- value is None or does not exist in the dictionary.
99- @firstrun: only needed for the default backend. It should be
100- omitted for all other backends.
101+ @param parameters: A dictionary of parameters, generated from
102+ _static_parameters. A few parameters are added to those, the list of
103+ these is in the "DefaultBackend" class, look for the KEY_* constants.
104+
105+ The backend should take care if one expected value is None or
106+ does not exist in the dictionary.
107 """
108 super(Backend, self).__init__(parameters)
109- self.tids = []
110+ self.tids = [] #we keep the list of loaded task ids here
111 #####RETROCOMPATIBILIY
112- #NOTE: retrocompatibility. We convert "filename" to "path"
113- # and we forget about "filename"
114+ #NOTE: retrocompatibility from the 0.2 series to 0.3.
115+ # We convert "filename" to "path and we forget about "filename "
116 if "need_conversion" in parameters:
117 parameters["path"] = os.path.join(self.DEFAULT_PATH, \
118 parameters["need_conversion"])
119@@ -99,24 +110,30 @@
120 self._parameters["path"], "project")
121
122 def initialize(self):
123+ """This is called when a backend is enabled"""
124 super(Backend, self).initialize()
125 self.doc, self.xmlproj = cleanxml.openxmlfile( \
126 self._parameters["path"], "project")
127
128 def this_is_the_first_run(self, xml):
129- #Create the default tasks for the first run.
130- #We write the XML object in a file
131+ """
132+ Called upon the very first GTG startup.
133+ This function is needed only in this backend, because it can be used as
134+ default one.
135+ The xml parameter is an object containing GTG default tasks. It will be
136+ saved to a file, and the backend will be set as default.
137+ @param xml: an xml object containing the default tasks.
138+ """
139 self._parameters[self.KEY_DEFAULT_BACKEND] = True
140 cleanxml.savexml(self._parameters["path"], xml)
141 self.doc, self.xmlproj = cleanxml.openxmlfile(\
142 self._parameters["path"], "project")
143- self._parameters[self.KEY_DEFAULT_BACKEND] = True
144
145 def start_get_tasks(self):
146 '''
147- Once this function is launched, the backend can start pushing
148- tasks to gtg parameters.
149-
150+ This function starts submitting the tasks from the XML file into GTG core.
151+ It's run as a separate thread.
152+
153 @return: start_get_tasks() might not return or finish
154 '''
155 tid_list = []
156@@ -130,52 +147,63 @@
157 self.datastore.push_task(task)
158
159 def set_task(self, task):
160- tid = task.get_id()
161- existing = None
162- #First, we find the existing task from the treenode
163- for node in self.xmlproj.childNodes:
164- if node.getAttribute("id") == tid:
165- existing = node
166- t_xml = taskxml.task_to_xml(self.doc, task)
167- modified = False
168- #We then replace the existing node
169- if existing and t_xml:
170- #We will write only if the task has changed
171- if t_xml.toxml() != existing.toxml():
172- self.xmlproj.replaceChild(t_xml, existing)
173- modified = True
174- #If the node doesn't exist, we create it
175- # (it might not be the case in all backends
176- else:
177- self.xmlproj.appendChild(t_xml)
178+ '''
179+ This function is called from GTG core whenever a task should be
180+ saved, either because it's a new one or it has been modified.
181+ This function will look into the loaded XML object if the task is
182+ present, and if it's not, it will create it. Then, it will save the
183+ task data in the XML object.
184+
185+ @param task: the task object to save
186+ '''
187+ tid = task.get_id()
188+ #We create an XML representation of the task
189+ t_xml = taskxml.task_to_xml(self.doc, task)
190+
191+ #we find if the task exists in the XML treenode.
192+ existing = None
193+ for node in self.xmlproj.childNodes:
194+ if node.getAttribute("id") == tid:
195+ existing = node
196+
197+ modified = False
198+ #We then replace the existing node
199+ if existing and t_xml:
200+ #We will write only if the task has changed
201+ if t_xml.toxml() != existing.toxml():
202+ self.xmlproj.replaceChild(t_xml, existing)
203 modified = True
204- #In this particular backend, we write all the tasks
205- #This is inherent to the XML file backend
206- if modified and self._parameters["path"] and self.doc :
207- cleanxml.savexml(self._parameters["path"], self.doc)
208+ #If the node doesn't exist, we create it
209+ else:
210+ self.xmlproj.appendChild(t_xml)
211+ modified = True
212+
213+ #if the XML object has changed, we save it to file
214+ if modified and self._parameters["path"] and self.doc :
215+ cleanxml.savexml(self._parameters["path"], self.doc)
216
217 def remove_task(self, tid):
218- ''' Completely remove the task with ID = tid '''
219+ ''' This function is called from GTG core whenever a task must be
220+ removed from the backend. Note that the task could be not present here.
221+
222+ @param tid: the id of the task to delete
223+ '''
224+ modified = False
225 for node in self.xmlproj.childNodes:
226 if node.getAttribute("id") == tid:
227+ modified = True
228 self.xmlproj.removeChild(node)
229 if tid in self.tids:
230 self.tids.remove(tid)
231- cleanxml.savexml(self._parameters["path"], self.doc)
232-
233-
234- def quit(self, disable = False):
235- '''
236- Called when GTG quits or disconnects the backend.
237- '''
238- super(Backend, self).quit(disable)
239-
240- def save_state(self):
241- cleanxml.savexml(self._parameters["path"], self.doc, backup=True)
242-
243- def get_number_of_tasks(self):
244- '''
245- Returns the number of tasks stored in the backend. Doesn't need to be a
246- fast function, is called just for the UI
247- '''
248- return len(self.tids)
249+
250+ #We save the XML file only if it's necessary
251+ if modified:
252+ cleanxml.savexml(self._parameters["path"], self.doc)
253+
254+#NOTE: This is not used currently. Therefore, I'm disabling it (invernizzi)
255+# def get_number_of_tasks(self):
256+# '''
257+# Returns the number of tasks stored in the backend. Doesn't need to be a
258+# fast function, is called just for the UI
259+# '''
260+# return len(self.tids)
261
262=== modified file 'GTG/backends/genericbackend.py'
263--- GTG/backends/genericbackend.py 2010-06-22 20:44:26 +0000
264+++ GTG/backends/genericbackend.py 2010-08-13 23:43:04 +0000
265@@ -18,7 +18,8 @@
266 # -----------------------------------------------------------------------------
267
268 '''
269-FIXME: document!
270+This file contains the most generic representation of a backend, the
271+GenericBackend class
272 '''
273
274 import os
275@@ -29,69 +30,71 @@
276 from collections import deque
277
278 from GTG.backends.backendsignals import BackendSignals
279-from GTG.tools.keyring import Keyring
280-from GTG.core import CoreConfig
281-from GTG.tools.logger import Log
282-
283+from GTG.tools.keyring import Keyring
284+from GTG.core import CoreConfig
285+from GTG.tools.logger import Log
286+from GTG.tools.interruptible import _cancellation_point
287
288
289
290 class GenericBackend(object):
291 '''
292- Base class for every backend. It's a little more than an interface which
293- methods have to be redefined in order for the backend to run.
294+ Base class for every backend.
295+ It defines the interface a backend must have and takes care of all the
296+ operations common to all backends.
297+ A particular backend should redefine all the methods marked as such.
298 '''
299
300
301- #BACKEND TYPE DESCRIPTION
302- #"_general_description" is a dictionary that holds the values for the
303- # following keys:
304- BACKEND_NAME = "name" #the backend gtg internal name (doesn't change in
305- # translations, *must be unique*)
306- BACKEND_HUMAN_NAME = "human-friendly-name" #The name shown to the user
307- BACKEND_DESCRIPTION = "description" #A short description of the backend
308- BACKEND_AUTHORS = "authors" #a list of strings
309- BACKEND_TYPE = "type"
310- #BACKEND_TYPE is one of:
311- TYPE_READWRITE = "readwrite"
312- TYPE_READONLY = "readonly"
313- TYPE_IMPORT = "import"
314- TYPE_EXPORT = "export"
315+ ###########################################################################
316+ ### BACKEND INTERFACE #####################################################
317+ ###########################################################################
318+
319+ #General description of the backend: these parameters are used
320+ #to show a description of the backend to the user when s/he is
321+ #considering adding it.
322+ # For an example, see the GTG/backends/backend_localfile.py file
323+ #_general_description has this format:
324+ #_general_description = {
325+ # GenericBackend.BACKEND_NAME: "backend_unique_identifier", \
326+ # GenericBackend.BACKEND_HUMAN_NAME: _("Human friendly name"), \
327+ # GenericBackend.BACKEND_AUTHORS: ["First author", \
328+ # "Chuck Norris"], \
329+ # GenericBackend.BACKEND_TYPE: GenericBackend.TYPE_READWRITE, \
330+ # GenericBackend.BACKEND_DESCRIPTION: \
331+ # _("Short description of the backend"),\
332+ # }
333+ # The complete list of constants and their meaning is given below.
334 _general_description = {}
335
336-
337- #"static_parameters" is a dictionary of dictionaries, each of which
338- #representing a parameter needed to configure the backend.
339- #each "sub-dictionary" is identified by this a key representing its name.
340- #"static_parameters" will be part of the definition of each
341- #particular backend.
342- # Each dictionary contains the keys:
343- #PARAM_DESCRIPTION = "description" #short description (shown to the user
344- # during configuration)
345- PARAM_DEFAULT_VALUE = "default_value" # its default value
346- PARAM_TYPE = "type"
347- #PARAM_TYPE is one of the following (changing this changes the way
348- # the user can configure the parameter)
349- TYPE_PASSWORD = "password" #the real password is stored in the GNOME
350- # keyring
351- # This is just a key to find it there
352- TYPE_STRING = "string" #generic string, nothing fancy is done
353- TYPE_INT = "int" #edit box can contain only integers
354- TYPE_BOOL = "bool" #checkbox is shown
355- TYPE_LIST_OF_STRINGS = "liststring" #list of strings. the "," character is
356- # prohibited in strings
357+ #These are the parameters to configure a new backend of this type. A
358+ # parameter has a name, a type and a default value.
359+ # For an example, see the GTG/backends/backend_localfile.py file
360+ #_static_parameters has this format:
361+ #_static_parameters = { \
362+ # "param1_name": { \
363+ # GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING,
364+ # GenericBackend.PARAM_DEFAULT_VALUE: "my default value",
365+ # },
366+ # "param2_name": {
367+ # GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT,
368+ # GenericBackend.PARAM_DEFAULT_VALUE: 42,
369+ # }}
370+ # The complete list of constants and their meaning is given below.
371 _static_parameters = {}
372
373 def initialize(self):
374 '''
375- Called each time it is enabled again (including on backend creation).
376+ Called each time it is enabled (including on backend creation).
377 Please note that a class instance for each disabled backend *is*
378 created, but it's not initialized.
379 Optional.
380 NOTE: make sure to call super().initialize()
381 '''
382- for module_name in self.get_required_modules():
383- sys.modules[module_name]= __import__(module_name)
384+ #NOTE: I'm disabling this since support for runtime checking of the
385+ # presence of the necessary modules is disabled. (invernizzi)
386+# for module_name in self.get_required_modules():
387+# sys.modules[module_name]= __import__(module_name)
388 self._parameters[self.KEY_ENABLED] = True
389 self._is_initialized = True
390 #we signal that the backend has been enabled
391@@ -99,65 +102,66 @@
392
393 def start_get_tasks(self):
394 '''
395- Once this function is launched, the backend can start pushing
396- tasks to gtg parameters.
397+ This function starts submitting the tasks from the backend into GTG
398+ core.
399+ It's run as a separate thread.
400
401 @return: start_get_tasks() might not return or finish
402 '''
403- raise NotImplemented()
404+ return
405
406 def set_task(self, task):
407 '''
408- Save the task in the backend. If the task id is new for the
409- backend, then a new task must be created.
410+ This function is called from GTG core whenever a task should be
411+ saved, either because it's a new one or it has been modified.
412+ If the task id is new for the backend, then a new task must be
413+ created. No special notification that the task is a new one is given.
414+
415+ @param task: the task object to save
416 '''
417 pass
418
419 def remove_task(self, tid):
420- ''' Completely remove the task with ID = tid '''
421+ ''' This function is called from GTG core whenever a task must be
422+ removed from the backend. Note that the task could be not present here.
423+
424+ @param tid: the id of the task to delete
425+ '''
426 pass
427
428- def has_task(self, tid):
429- '''Returns true if the backend has an internal idea
430- of the task corresponding to the tid. False otherwise'''
431- raise NotImplemented()
432-
433- def new_task_id(self):
434- '''
435- Returns an available ID for a new task so that a task with this ID
436- can be saved with set_task later.
437- '''
438- raise NotImplemented()
439-
440 def this_is_the_first_run(self, xml):
441 '''
442- Steps to execute if it's the first time the backend is run. Optional.
443- '''
444- pass
445-
446- def purge(self):
447- '''
448- Called when a backend will be removed from GTG. Useful for removing
449- configuration files. Optional.
450- '''
451- pass
452-
453- def get_number_of_tasks(self):
454- '''
455- Returns the number of tasks stored in the backend. Doesn't need to be a
456- fast function, is called just for the UI
457- '''
458- raise NotImplemented()
459-
460- @staticmethod
461- def get_required_modules():
462- return []
463+ Optional, and almost surely not needed.
464+ Called upon the very first GTG startup.
465+ This function is needed only in the default backend (XML localfile,
466+ currently).
467+ The xml parameter is an object containing GTG default tasks.
468+
469+ @param xml: an xml object containing the default tasks.
470+ '''
471+ pass
472+
473+#NOTE: task counting is disabled in the UI, so I've disabled it here
474+# (invernizzi)
475+# def get_number_of_tasks(self):
476+# '''
477+# Returns the number of tasks stored in the backend. Doesn't need
478+# to be a fast function, is called just for the UI
479+# '''
480+# raise NotImplemented()
481+
482+#NOTE: I'm disabling this since support for runtime checking of the
483+# presence of the necessary modules is disabled. (invernizzi)
484+# @staticmethod
485+# def get_required_modules():
486+# return []
487
488 def quit(self, disable = False):
489 '''
490- Called when GTG quits or disconnects the backend. Remember to execute
491- also this function when quitting. If disable is True, the backend won't
492- be automatically loaded at next GTG start
493+ Called when GTG quits or the user wants to disable the backend.
494+
495+ @param disable: If disable is True, the backend won't
496+ be automatically loaded when GTG starts
497 '''
498 self._is_initialized = False
499 if disable:
500@@ -179,6 +183,44 @@
501 ###### You don't need to reimplement the functions below this line ############
502 ###############################################################################
503
504+ ###########################################################################
505+ ### CONSTANTS #############################################################
506+ ###########################################################################
507+ #BACKEND TYPE DESCRIPTION
508+ # Each backend must have a "_general_description" attribute, which
509+ # is a dictionary that holds the values for the following keys.
510+ BACKEND_NAME = "name" #the backend gtg internal name (doesn't change in
511+ # translations, *must be unique*)
512+ BACKEND_HUMAN_NAME = "human-friendly-name" #The name shown to the user
513+ BACKEND_DESCRIPTION = "description" #A short description of the backend
514+ BACKEND_AUTHORS = "authors" #a list of strings
515+ BACKEND_TYPE = "type"
516+ #BACKEND_TYPE is one of:
517+ TYPE_READWRITE = "readwrite"
518+ TYPE_READONLY = "readonly"
519+ TYPE_IMPORT = "import"
520+ TYPE_EXPORT = "export"
521+
522+
523+ #"static_parameters" is a dictionary of dictionaries, each of which
524+ # are a description of a parameter needed to configure the backend and
525+ # is identified in the outer dictionary by a key which is the name of the
526+ # parameter.
527+ # For an example, see the GTG/backends/backend_localfile.py file
528+ # Each dictionary contains the keys:
529+ PARAM_DEFAULT_VALUE = "default_value" # its default value
530+ PARAM_TYPE = "type"
531+ #PARAM_TYPE is one of the following (changing this changes the way
532+ # the user can configure the parameter)
533+ TYPE_PASSWORD = "password" #the real password is stored in the GNOME
534+ # keyring
535+ # This is just a key to find it there
536+ TYPE_STRING = "string" #generic string, nothing fancy is done
537+ TYPE_INT = "int" #edit box can contain only integers
538+ TYPE_BOOL = "bool" #checkbox is shown
539+ TYPE_LIST_OF_STRINGS = "liststring" #list of strings. the "," character is
540+ # prohibited in strings
541+
542 #These parameters are common to all backends and necessary.
543 # They will be added automatically to your _static_parameters list
544 #NOTE: for now I'm disabling changing the default backend. Once it's all
545@@ -189,7 +231,11 @@
546 KEY_ATTACHED_TAGS = "attached-tags"
547 KEY_USER = "user"
548 KEY_PID = "pid"
549- ALLTASKS_TAG = "gtg-tags-all" #IXME: moved here to avoid circular imports
550+ ALLTASKS_TAG = "gtg-tags-all" #NOTE: this has been moved here to avoid
551+ # circular imports. It's the same as in
552+ # the CoreConfig class, because it's the
553+ # same thing conceptually. It doesn't
554+ # matter it the naming diverges.
555
556 _static_parameters_obligatory = { \
557 KEY_DEFAULT_BACKEND: { \
558@@ -229,28 +275,30 @@
559 '''
560 Helper method, used to obtain the full list of the static_parameters
561 (user configured and default ones)
562+
563+ @returns dict: the dict containing all the static parameters
564 '''
565- if hasattr(cls, "_static_parameters"):
566- temp_dic = cls._static_parameters_obligatory.copy()
567- if cls._general_description[cls.BACKEND_TYPE] == cls.TYPE_READWRITE:
568- for key, value in \
569- cls._static_parameters_obligatory_for_rw.iteritems():
570- temp_dic[key] = value
571- for key, value in cls._static_parameters.iteritems():
572+ temp_dic = cls._static_parameters_obligatory.copy()
573+ if cls._general_description[cls.BACKEND_TYPE] == \
574+ cls.TYPE_READWRITE:
575+ for key, value in \
576+ cls._static_parameters_obligatory_for_rw.iteritems():
577 temp_dic[key] = value
578- return temp_dic
579- else:
580- raise NotImplemented("_static_parameters not implemented for " + \
581- "backend %s" % type(cls))
582+ for key, value in cls._static_parameters.iteritems():
583+ temp_dic[key] = value
584+ return temp_dic
585
586 def __init__(self, parameters):
587 """
588- Instantiates a new backend. Please note that this is called also for
589- disabled backends. Those are not initialized, so you might want to check
590- out the initialize() function.
591+ Instantiates a new backend. Please note that this is called also
592+ for disabled backends. Those are not initialized, so you might
593+ want to check out the initialize() function.
594 """
595 if self.KEY_DEFAULT_BACKEND not in parameters:
596+ #if it's not specified, then this is the default backend
597+ #(for retro-compatibility with the GTG 0.2 series)
598 parameters[self.KEY_DEFAULT_BACKEND] = True
599+ #default backends should get all the tasks
600 if parameters[self.KEY_DEFAULT_BACKEND] or \
601 (not self.KEY_ATTACHED_TAGS in parameters and \
602 self._general_description[self.BACKEND_TYPE] \
603@@ -259,12 +307,17 @@
604 self._parameters = parameters
605 self._signal_manager = BackendSignals()
606 self._is_initialized = False
607+ #if debugging mode is enabled, tasks should be saved as soon as they're
608+ # marked as modified. If in normal mode, we prefer speed over easier
609+ # debugging.
610 if Log.is_debugging_mode():
611 self.timer_timestep = 5
612 else:
613 self.timer_timestep = 1
614 self.to_set_timer = None
615 self.please_quit = False
616+ self.cancellation_point = lambda: _cancellation_point(\
617+ lambda: self.please_quit)
618 self.to_set = deque()
619 self.to_remove = deque()
620
621@@ -274,6 +327,9 @@
622 '''
623 if hasattr(self._parameters, self.KEY_DEFAULT_BACKEND) and \
624 self._parameters[self.KEY_DEFAULT_BACKEND]:
625+ #default backends should get all the tasks
626+ #NOTE: this shouldn't be needed, but it doesn't cost anything and it
627+ # could avoid potential tasks losses.
628 return [self.ALLTASKS_TAG]
629 try:
630 return self._parameters[self.KEY_ATTACHED_TAGS]
631@@ -283,6 +339,8 @@
632 def set_attached_tags(self, tags):
633 '''
634 Changes the set of attached tags
635+
636+ @param tags: the new attached_tags set
637 '''
638 self._parameters[self.KEY_ATTACHED_TAGS] = tags
639
640@@ -300,6 +358,12 @@
641 return self._parameters
642
643 def set_parameter(self, parameter, value):
644+ '''
645+ Change a parameter for this backend
646+
647+ @param parameter: the parameter name
648+ @param value: the new value
649+ '''
650 self._parameters[parameter] = value
651
652 @classmethod
653@@ -330,24 +394,22 @@
654 def _get_from_general_description(cls, key):
655 '''
656 Helper method to extract values from cls._general_description.
657- Raises an exception if the key is missing (helpful for developers
658- adding new backends).
659+
660+ @param key: the key to extract
661 '''
662- if key in cls._general_description:
663- return cls._general_description[key]
664- else:
665- raise NotImplemented("Key %s is missing from " +\
666- "'self._general_description' of a backend (%s). " +
667- "Please add the corresponding value" % (key, type(cls)))
668+ return cls._general_description[key]
669
670 @classmethod
671 def cast_param_type_from_string(cls, param_value, param_type):
672 '''
673 Parameters are saved in a text format, so we have to cast them to the
674 appropriate type on loading. This function does exactly that.
675+
676+ @param param_value: the actual value of the parameter, in a string
677+ format
678+ @param param_type: the wanted type
679+ @returns something: the casted param_value
680 '''
681- #FIXME: we could use pickle (dumps and loads), at least in some cases
682- # (invernizzi)
683 if param_type in cls._type_converter:
684 return cls._type_converter[param_type](param_value)
685 elif param_type == cls.TYPE_BOOL:
686@@ -374,6 +436,10 @@
687 def cast_param_type_to_string(self, param_type, param_value):
688 '''
689 Inverse of cast_param_type_from_string
690+
691+ @param param_value: the actual value of the parameter
692+ @param param_type: the type of the parameter (password...)
693+ @returns something: param_value casted to string
694 '''
695 if param_type == GenericBackend.TYPE_PASSWORD:
696 if param_value == None:
697@@ -391,20 +457,27 @@
698 def get_id(self):
699 '''
700 returns the backends id, used in the datastore for indexing backends
701+
702+ @returns string: the backend id
703 '''
704 return self.get_name() + "@" + self._parameters["pid"]
705
706 @classmethod
707 def get_human_default_name(cls):
708 '''
709- returns the user friendly default backend name.
710+ returns the user friendly default backend name, without eventual user
711+ modifications.
712+
713+ @returns string: the default "human name"
714 '''
715 return cls._general_description[cls.BACKEND_HUMAN_NAME]
716
717 def get_human_name(self):
718 '''
719 returns the user customized backend name. If the user hasn't
720- customized it, returns the default one
721+ customized it, returns the default one.
722+
723+ @returns string: the "human name" of this backend
724 '''
725 if self.KEY_HUMAN_NAME in self._parameters and \
726 self._parameters[self.KEY_HUMAN_NAME] != "":
727@@ -415,6 +488,8 @@
728 def set_human_name(self, name):
729 '''
730 sets a custom name for the backend
731+
732+ @param name: the new name
733 '''
734 self._parameters[self.KEY_HUMAN_NAME] = name
735 #we signal the change
736@@ -423,29 +498,49 @@
737 def is_enabled(self):
738 '''
739 Returns if the backend is enabled
740+
741+ @returns bool
742 '''
743 return self.get_parameters()[GenericBackend.KEY_ENABLED] or \
744- self.is_default()
745+ self.is_default()
746
747 def is_default(self):
748 '''
749 Returns if the backend is enabled
750+
751+ @returns bool
752 '''
753 return self.get_parameters()[GenericBackend.KEY_DEFAULT_BACKEND]
754
755 def is_initialized(self):
756 '''
757 Returns if the backend is up and running
758+
759+ @returns is_initialized
760 '''
761 return self._is_initialized
762
763 def get_parameter_type(self, param_name):
764+ '''
765+ Given the name of a parameter, returns its type. If the parameter is one
766+ of the default ones, it does not have a type: in that case, it returns
767+ None
768+
769+ @param param_name: the name of the parameter
770+ @returns string: the type, or None
771+ '''
772 try:
773 return self.get_static_parameters()[param_name][self.PARAM_TYPE]
774- except KeyError:
775+ except:
776 return None
777
778 def register_datastore(self, datastore):
779+ '''
780+ Setter function to inform the backend about the datastore that's loading
781+ it.
782+
783+ @param datastore: a Datastore
784+ '''
785 self.datastore = datastore
786
787 ###############################################################################
788@@ -455,6 +550,7 @@
789 def _store_pickled_file(self, path, data):
790 '''
791 A helper function to save some object in a file.
792+
793 @param path: a relative path. A good choice is
794 "backend_name/object_name"
795 @param data: the object
796@@ -467,15 +563,13 @@
797 if exception.errno != errno.EEXIST:
798 raise
799 #saving
800- #try:
801 with open(path, 'wb') as file:
802 pickle.dump(data, file)
803- #except pickle.PickleError:
804- #pass
805
806 def _load_pickled_file(self, path, default_value = None):
807 '''
808 A helper function to load some object from a file.
809+
810 @param path: the relative path of the file
811 @param default_value: the value to return if the file is missing or
812 corrupt
813@@ -485,11 +579,29 @@
814 if not os.path.exists(path):
815 return default_value
816 else:
817- try:
818- with open(path, 'r') as file:
819+ with open(path, 'r') as file:
820+ try:
821 return pickle.load(file)
822- except pickle.PickleError:
823- return default_value
824+ except pickle.PickleError:
825+ Log.error("PICKLE ERROR")
826+ return default_value
827+
828+ def _gtg_task_is_syncable_per_attached_tags(self, task):
829+ '''
830+ Helper function which checks if the given task satisfies the filtering
831+ imposed by the tags attached to the backend.
832+ That means, if a user wants a backend to sync only tasks tagged @works,
833+ this function should be used to check if that is verified.
834+
835+ @returns bool: True if the task should be synced
836+ '''
837+ attached_tags = self.get_attached_tags()
838+ if GenericBackend.ALLTASKS_TAG in attached_tags:
839+ return True
840+ for tag in task.get_tags_name():
841+ if tag in attached_tags:
842+ return True
843+ return False
844
845 ###############################################################################
846 ### THREADING #################################################################
847@@ -504,25 +616,29 @@
848 self.launch_setting_thread)
849 self.to_set_timer.start()
850
851- def launch_setting_thread(self):
852+ def launch_setting_thread(self, bypass_quit_request = False):
853 '''
854 This function is launched as a separate thread. Its job is to perform
855- the changes that have been issued from GTG core. In particular, for
856- each task in the self.to_set queue, a task has to be modified or to be
857- created (if the tid is new), and for each task in the self.to_remove
858- queue, a task has to be deleted
859+ the changes that have been issued from GTG core.
860+ In particular, for each task in the self.to_set queue, a task
861+ has to be modified or to be created (if the tid is new), and for
862+ each task in the self.to_remove queue, a task has to be deleted
863+
864+ @param bypass_quit_request: if True, the thread should not be stopped
865+ even if asked by self.please_quit = True.
866+ It's used when the backend quits, to finish
867+ syncing all pending tasks
868 '''
869- while not self.please_quit:
870+ while not self.please_quit or bypass_quit_request:
871 try:
872 task = self.to_set.pop()
873 except IndexError:
874 break
875- #time.sleep(4)
876 tid = task.get_id()
877 if tid not in self.to_remove:
878 self.set_task(task)
879
880- while not self.please_quit:
881+ while not self.please_quit or bypass_quit_request:
882 try:
883 tid = self.to_remove.pop()
884 except IndexError:
885@@ -532,7 +648,12 @@
886 self.to_set_timer = None
887
888 def queue_set_task(self, task):
889- ''' Save the task in the backend. '''
890+ ''' Save the task in the backend. In particular, it just enqueues the
891+ task in the self.to_set queue. A thread will shortly run to apply the
892+ requested changes.
893+
894+ @param task: the task that should be saved
895+ '''
896 tid = task.get_id()
897 if task not in self.to_set and tid not in self.to_remove:
898 self.to_set.appendleft(task)
899@@ -540,7 +661,10 @@
900
901 def queue_remove_task(self, tid):
902 '''
903- Queues task to be removed.
904+ Queues task to be removed. In particular, it just enqueues the
905+ task in the self.to_remove queue. A thread will shortly run to apply the
906+ requested changes.
907+
908 @param tid: The Task ID of the task to be removed
909 '''
910 if tid not in self.to_remove:
911@@ -553,8 +677,6 @@
912 Helper method. Forces the backend to perform all the pending changes.
913 It is usually called upon quitting the backend.
914 '''
915- #FIXME: this function should become part of the r/w r/o generic class
916- # for backends
917 if self.to_set_timer != None:
918 self.please_quit = True
919 try:
920@@ -562,10 +684,10 @@
921 except:
922 pass
923 try:
924- self.to_set_timer.join(5)
925+ self.to_set_timer.join()
926 except:
927 pass
928- self.please_quit = False
929- self.launch_setting_thread()
930+ self.launch_setting_thread(bypass_quit_request = True)
931 self.save_state()
932
933+
934
935=== modified file 'GTG/core/datastore.py'
936--- GTG/core/datastore.py 2010-06-23 10:54:11 +0000
937+++ GTG/core/datastore.py 2010-08-13 23:43:04 +0000
938@@ -18,8 +18,8 @@
939 # -----------------------------------------------------------------------------
940
941 """
942-The DaataStore contains a list of TagSource objects, which are proxies
943-between a backend and the datastore itself
944+Contains the Datastore object, which is the manager of all the active backends
945+(both enabled and disabled ones)
946 """
947
948 import threading
949@@ -34,7 +34,6 @@
950 from GTG.tools.logger import Log
951 from GTG.backends.genericbackend import GenericBackend
952 from GTG.tools import cleanxml
953-from GTG.tools.keyring import Keyring
954 from GTG.backends.backendsignals import BackendSignals
955 from GTG.tools.synchronized import synchronized
956 from GTG.tools.borg import Borg
957@@ -59,11 +58,17 @@
958 self.requester = requester.Requester(self)
959 self.tagstore = tagstore.TagStore(self.requester)
960 self._backend_signals = BackendSignals()
961- self.mutex = threading.RLock()
962- self.is_default_backend_loaded = False
963+ self.please_quit = False #when turned to true, all pending operation
964+ # should be completed and then GTG should quit
965+ self.is_default_backend_loaded = False #the default backend must be
966+ # loaded before anyone else.
967+ # This turns to True when the
968+ # default backend loading has
969+ # finished.
970 self._backend_signals.connect('default-backend-loaded', \
971 self._activate_non_default_backends)
972 self.filtered_datastore = FilteredDataStore(self)
973+ self._backend_mutex = threading.Lock()
974
975 ##########################################################################
976 ### Helper functions (get_ methods for Datastore embedded objects)
977@@ -72,6 +77,7 @@
978 def get_tagstore(self):
979 '''
980 Helper function to obtain the Tagstore associated with this DataStore
981+
982 @return GTG.core.tagstore.TagStore: the tagstore object
983 '''
984 return self.tagstore
985@@ -79,15 +85,17 @@
986 def get_requester(self):
987 '''
988 Helper function to get the Requester associate with this DataStore
989+
990 @returns GTG.core.requester.Requester: the requester associated with
991- this datastore
992+ this datastore
993 '''
994 return self.requester
995
996 def get_tasks_tree(self):
997 '''
998 Helper function to get a Tree with all the tasks contained in this
999- Datastore
1000+ Datastore.
1001+
1002 @returns GTG.core.tree.Tree: a task tree (the main one)
1003 '''
1004 return self.open_tasks
1005@@ -99,6 +107,7 @@
1006 def get_all_tasks(self):
1007 '''
1008 Returns list of all keys of open tasks
1009+
1010 @return a list of strings: a list of task ids
1011 '''
1012 return self.open_tasks.get_all_keys()
1013@@ -107,6 +116,7 @@
1014 '''
1015 Returns true if the tid is among the open or closed tasks for
1016 this DataStore, False otherwise.
1017+
1018 @param tid: Task ID to search for
1019 @return bool: True if the task is present
1020 '''
1021@@ -116,6 +126,7 @@
1022 '''
1023 Returns the internal task object for the given tid, or None if the
1024 tid is not present in this DataStore.
1025+
1026 @param tid: Task ID to retrieve
1027 @returns GTG.core.task.Task or None: whether the Task is present
1028 or not
1029@@ -123,12 +134,13 @@
1030 if self.has_task(tid):
1031 return self.open_tasks.get_node(tid)
1032 else:
1033- Log.debug("requested non-existent task")
1034+ Log.error("requested non-existent task")
1035 return None
1036
1037 def task_factory(self, tid, newtask = False):
1038 '''
1039 Instantiates the given task id as a Task object.
1040+
1041 @param tid: a task id. Must be unique
1042 @param newtask: True if the task has never been seen before
1043 @return Task: a Task instance
1044@@ -141,6 +153,7 @@
1045 New task is created in all the backends that collect all tasks (among
1046 them, the default backend). The default backend uses the same task id
1047 in its own internal representation.
1048+
1049 @return: The task object that was created.
1050 """
1051 task = self.task_factory(uuid.uuid4(), True)
1052@@ -148,10 +161,13 @@
1053 return task
1054
1055 @synchronized
1056- def push_task(self, task, backend_capabilities = 'bypass for now'):
1057+ def push_task(self, task):
1058 '''
1059 Adds the given task object to the task tree. In other words, registers
1060 the given task in the GTG task set.
1061+ This function is used in mutual exclusion: only a backend at a time is
1062+ allowed to push tasks.
1063+
1064 @param task: A valid task object (a GTG.core.task.Task)
1065 @return bool: True if the task has been accepted
1066 '''
1067@@ -172,10 +188,10 @@
1068 def get_all_backends(self, disabled = False):
1069 """
1070 returns list of all registered backends for this DataStore.
1071+
1072 @param disabled: If disabled is True, attaches also the list of disabled backends
1073 @return list: a list of TaskSource objects
1074 """
1075- #NOTE: consider cashing this result for speed.
1076 result = []
1077 for backend in self.backends.itervalues():
1078 if backend.is_enabled() or disabled:
1079@@ -184,10 +200,11 @@
1080
1081 def get_backend(self, backend_id):
1082 '''
1083- Returns a backend given its id
1084+ Returns a backend given its id.
1085+
1086 @param backend_id: a backend id
1087 @returns GTG.core.datastore.TaskSource or None: the requested backend,
1088- or none
1089+ or None
1090 '''
1091 if backend_id in self.backends:
1092 return self.backends[backend_id]
1093@@ -197,20 +214,24 @@
1094 def register_backend(self, backend_dic):
1095 """
1096 Registers a TaskSource as a backend for this DataStore
1097+
1098 @param backend_dic: Dictionary object containing all the
1099- parameters to initialize the backend (filename...). It should
1100- also contain the backend class (under "backend"), and its unique
1101- id (under "pid")
1102+ parameters to initialize the backend
1103+ (filename...). It should also contain the
1104+ backend class (under "backend"), and its
1105+ unique id (under "pid")
1106 """
1107 if "backend" in backend_dic:
1108 if "pid" not in backend_dic:
1109- Log.debug("registering a backend without pid.")
1110+ Log.error("registering a backend without pid.")
1111 return None
1112 backend = backend_dic["backend"]
1113 #Checking that is a new backend
1114 if backend.get_id() in self.backends:
1115- Log.debug("registering already registered backend")
1116+ Log.error("registering already registered backend")
1117 return None
1118+ #creating the TaskSource which will wrap the backend,
1119+ # filtering the tasks that should hit the backend.
1120 source = TaskSource(requester = self.requester,
1121 backend = backend,
1122 datastore = self.filtered_datastore)
1123@@ -234,23 +255,46 @@
1124 source.start_get_tasks()
1125 return source
1126 else:
1127- Log.debug("Tried to register a backend without a pid")
1128+ Log.error("Tried to register a backend without a pid")
1129
1130 def _activate_non_default_backends(self, sender = None):
1131 '''
1132 Non-default backends have to wait until the default loads before
1133 being activated. This function is called after the first default
1134 backend has loaded all its tasks.
1135+
1136+ @param sender: not used, just here for signal compatibility
1137 '''
1138 if self.is_default_backend_loaded:
1139 Log.debug("spurious call")
1140 return
1141+
1142+
1143 self.is_default_backend_loaded = True
1144 for backend in self.backends.itervalues():
1145 if backend.is_enabled() and not backend.is_default():
1146- backend.initialize()
1147- backend.start_get_tasks()
1148- self.flush_all_tasks(backend.get_id())
1149+ self._backend_startup(backend)
1150+
1151+ def _backend_startup(self, backend):
1152+ '''
1153+ Helper function to launch a thread that starts a backend.
1154+
1155+ @param backend: the backend object
1156+ '''
1157+ def __backend_startup(self, backend):
1158+ '''
1159+ Helper function to start a backend
1160+
1161+ @param backend: the backend object
1162+ '''
1163+ backend.initialize()
1164+ backend.start_get_tasks()
1165+ self.flush_all_tasks(backend.get_id())
1166+
1167+ thread = threading.Thread(target = __backend_startup,
1168+ args = (self, backend))
1169+ thread.setDaemon(True)
1170+ thread.start()
1171
1172 def set_backend_enabled(self, backend_id, state):
1173 """
1174@@ -262,6 +306,7 @@
1175 Enable:
1176 Reloads a disabled backend. Backend must be already known by the
1177 Datastore
1178+
1179 @parma backend_id: a backend id
1180 @param state: True to enable, False to disable
1181 """
1182@@ -270,11 +315,12 @@
1183 current_state = backend.is_enabled()
1184 if current_state == True and state == False:
1185 #we disable the backend
1186- backend.quit(disable = True)
1187+ #FIXME!!!
1188+ threading.Thread(target = backend.quit, \
1189+ kwargs = {'disable': True}).start()
1190 elif current_state == False and state == True:
1191 if self.is_default_backend_loaded == True:
1192- backend.initialize()
1193- self.flush_all_tasks(backend_id)
1194+ self._backend_startup(backend)
1195 else:
1196 #will be activated afterwards
1197 backend.set_parameter(GenericBackend.KEY_ENABLED,
1198@@ -283,13 +329,19 @@
1199 def remove_backend(self, backend_id):
1200 '''
1201 Removes a backend, and forgets it ever existed.
1202+
1203 @param backend_id: a backend id
1204 '''
1205 if backend_id in self.backends:
1206 backend = self.backends[backend_id]
1207 if backend.is_enabled():
1208 self.set_backend_enabled(backend_id, False)
1209- backend.purge()
1210+ #FIXME: to keep things simple, backends are not notified that they
1211+ # are completely removed (they think they're just
1212+ # deactivated). We should add a "purge" call to backend to let
1213+ # them know that they're removed, so that they can remove all
1214+ # the various files they've created. (invernizzi)
1215+
1216 #we notify that the backend has been deleted
1217 self._backend_signals.backend_removed(backend.get_id())
1218 del self.backends[backend_id]
1219@@ -297,6 +349,7 @@
1220 def backend_change_attached_tags(self, backend_id, tag_names):
1221 '''
1222 Changes the tags for which a backend should store a task
1223+
1224 @param backend_id: a backend_id
1225 @param tag_names: the new set of tags. This should not be a tag object,
1226 just the tag name.
1227@@ -312,43 +365,59 @@
1228 It has to be run after the creation of a new backend (or an alteration
1229 of its "attached tags"), so that the tasks which are already loaded in
1230 the Tree will be saved in the proper backends
1231+
1232 @param backend_id: a backend id
1233 '''
1234 def _internal_flush_all_tasks():
1235 backend = self.backends[backend_id]
1236 for task_id in self.requester.get_all_tasks_list():
1237+ if self.please_quit:
1238+ break
1239 backend.queue_set_task(None, task_id)
1240- t = threading.Thread(target = _internal_flush_all_tasks).start()
1241+ t = threading.Thread(target = _internal_flush_all_tasks)
1242+ t.start()
1243 self.backends[backend_id].start_get_tasks()
1244
1245 def save(self, quit = False):
1246 '''
1247 Saves the backends parameters.
1248+
1249 @param quit: If quit is true, backends are shut down
1250 '''
1251+ try:
1252+ self.start_get_tasks_thread.join()
1253+ except Exception, e:
1254+ pass
1255 doc,xmlconfig = cleanxml.emptydoc("config")
1256 #we ask all the backends to quit first.
1257 if quit:
1258+ #we quit backends in parallel
1259+ threads_dic = {}
1260 for b in self.get_all_backends():
1261- #NOTE:we could do this in parallel. Maybe a quit and
1262- #has_quit would be faster (invernizzi)
1263- b.quit()
1264+ thread = threading.Thread(target = b.quit)
1265+ threads_dic[b.get_id()] = thread
1266+ thread.start()
1267+ for backend_id, thread in threads_dic.iteritems():
1268+ #after 20 seconds, we give up
1269+ thread.join(20)
1270+ if thread.isAlive():
1271+ Log.error("The %s backend stalled while quitting",
1272+ backend_id)
1273 #we save the parameters
1274 for b in self.get_all_backends(disabled = True):
1275 t_xml = doc.createElement("backend")
1276 for key, value in b.get_parameters().iteritems():
1277 if key in ["backend", "xmlobject"]:
1278- #We don't want parameters,backend,xmlobject
1279+ #We don't want parameters, backend, xmlobject: we'll create
1280+ # them at next startup
1281 continue
1282 param_type = b.get_parameter_type(key)
1283 value = b.cast_param_type_to_string(param_type, value)
1284 t_xml.setAttribute(str(key), value)
1285 #Saving all the projects at close
1286 xmlconfig.appendChild(t_xml)
1287-
1288 datafile = os.path.join(CoreConfig().get_data_dir(), CoreConfig.DATA_FILE)
1289 cleanxml.savexml(datafile,doc,backup=True)
1290-
1291 #Saving the tagstore
1292 ts = self.get_tagstore()
1293 ts.save()
1294@@ -356,9 +425,21 @@
1295 def request_task_deletion(self, tid):
1296 '''
1297 This is a proxy function to request a task deletion from a backend
1298+
1299 @param tid: the tid of the task to remove
1300 '''
1301 self.requester.delete_task(tid)
1302+
1303+ def get_backend_mutex(self):
1304+ '''
1305+ Returns the mutex object used by backends to avoid modifying a task
1306+ at the same time.
1307+
1308+ @returns threading.Lock
1309+ '''
1310+ return self._backend_mutex
1311+
1312+
1313
1314
1315 class TaskSource():
1316@@ -366,9 +447,12 @@
1317 Transparent interface between the real backend and the DataStore.
1318 Is in charge of connecting and disconnecting to signals
1319 '''
1320+
1321+
1322 def __init__(self, requester, backend, datastore):
1323 """
1324 Instantiates a TaskSource object.
1325+
1326 @param requester: a Requester
1327 @param backend: the backend being wrapped
1328 @param datastore: a FilteredDatastore
1329@@ -378,6 +462,7 @@
1330 self.backend.register_datastore(datastore)
1331 self.to_set = deque()
1332 self.to_remove = deque()
1333+ self.please_quit = False
1334 self.task_filter = self.get_task_filter_for_backend()
1335 if Log.is_debugging_mode():
1336 self.timer_timestep = 5
1337@@ -391,7 +476,10 @@
1338 ''''
1339 Maps the TaskSource to the backend and starts threading.
1340 '''
1341- threading.Thread(target = self.__start_get_tasks).start()
1342+ self.start_get_tasks_thread = \
1343+ threading.Thread(target = self.__start_get_tasks)
1344+ self.start_get_tasks_thread.setDaemon(True)
1345+ self.start_get_tasks_thread.start()
1346
1347 def __start_get_tasks(self):
1348 '''
1349@@ -405,7 +493,7 @@
1350
1351 def get_task_filter_for_backend(self):
1352 '''
1353- Fiter that checks if the task should be stored in this backend.
1354+ Filter that checks if the task should be stored in this backend.
1355
1356 @returns function: a function that accepts a task and returns True/False
1357 whether the task should be stored or not
1358@@ -417,6 +505,7 @@
1359 def should_task_id_be_stored(self, task_id):
1360 '''
1361 Helper function: Checks if a task should be stored in this backend
1362+
1363 @param task_id: a task id
1364 @returns bool: True if the task should be stored
1365 '''
1366@@ -427,6 +516,7 @@
1367 """
1368 Updates the task in the DataStore. Actually, it adds the task to a
1369 queue to be updated asynchronously.
1370+
1371 @param sender: not used, any value will do.
1372 @param task: The Task object to be updated.
1373 """
1374@@ -437,14 +527,17 @@
1375 else:
1376 self.queue_remove_task(None, tid)
1377
1378- def launch_setting_thread(self):
1379+ def launch_setting_thread(self, bypass_please_quit = False):
1380 '''
1381 Operates the threads to set and remove tasks.
1382 Releases the lock when it is done.
1383+
1384+ @param bypass_please_quit: if True, the self.please_quit "quit condition"
1385+ is ignored. Currently, it's turned to true
1386+ after the quit condition has been issued, to
1387+ execute eventual pending operations.
1388 '''
1389- #FIXME: the lock should be general for all backends. Therefore, it
1390- #should be handled in the datastore
1391- while True:
1392+ while not self.please_quit or bypass_please_quit:
1393 try:
1394 tid = self.to_set.pop()
1395 except IndexError:
1396@@ -457,7 +550,7 @@
1397 self.req.has_task(tid):
1398 task = self.req.get_task(tid)
1399 self.backend.queue_set_task(task)
1400- while True:
1401+ while not self.please_quit or bypass_please_quit:
1402 try:
1403 tid = self.to_remove.pop()
1404 except IndexError:
1405@@ -469,6 +562,7 @@
1406 def queue_remove_task(self, sender, tid):
1407 '''
1408 Queues task to be removed.
1409+
1410 @param sender: not used, any value will do
1411 @param tid: The Task ID of the task to be removed
1412 '''
1413@@ -480,14 +574,16 @@
1414 '''
1415 Helper function to launch the setting thread, if it's not running
1416 '''
1417- if self.to_set_timer == None:
1418+ if self.to_set_timer == None and not self.please_quit:
1419 self.to_set_timer = threading.Timer(self.timer_timestep, \
1420 self.launch_setting_thread)
1421+ self.to_set_timer.setDaemon(True)
1422 self.to_set_timer.start()
1423
1424 def initialize(self, connect_signals = True):
1425 '''
1426 Initializes the backend and starts looking for signals.
1427+
1428 @param connect_signals: if True, it starts listening for signals
1429 '''
1430 self.backend.initialize()
1431@@ -520,23 +616,28 @@
1432 '''
1433 Forces the TaskSource to sync all the pending tasks
1434 '''
1435- if self.to_set_timer != None:
1436- try:
1437- self.to_set_timer.cancel()
1438- except:
1439- pass
1440- try:
1441- self.to_set_timer.join(5)
1442- except:
1443- pass
1444- self.launch_setting_thread()
1445+ try:
1446+ self.to_set_timer.cancel()
1447+ except Exception, e:
1448+ pass
1449+ try:
1450+ self.to_set_timer.join(3)
1451+ except Exception, e:
1452+ pass
1453+ try:
1454+ self.start_get_tasks_thread.join(3)
1455+ except:
1456+ pass
1457+ self.launch_setting_thread(bypass_please_quit = True)
1458
1459 def quit(self, disable = False):
1460 '''
1461 Quits the backend and disconnect the signals
1462+
1463 @param disable: if True, the backend is disabled.
1464 '''
1465 self._disconnect_signals()
1466+ self.please_quit = True
1467 self.sync()
1468 self.backend.quit(disable)
1469
1470@@ -544,6 +645,7 @@
1471 '''
1472 Delegates all the functions not defined here to the real backend
1473 (standard python function)
1474+
1475 @param attr: attribute to get
1476 '''
1477 if attr in self.__dict__:
1478@@ -566,12 +668,15 @@
1479 self.datastore = datastore
1480
1481 def __getattr__(self, attr):
1482- if attr in ['task_factory', \
1483+ if attr in ['task_factory',
1484 'push_task',
1485 'get_task',
1486 'has_task',
1487+ 'get_backend_mutex',
1488 'request_task_deletion']:
1489 return getattr(self.datastore, attr)
1490+ elif attr in ['get_all_tags']:
1491+ return self.datastore.requester.get_all_tags
1492 else:
1493 raise AttributeError
1494
1495
1496=== modified file 'GTG/core/filteredtree.py'
1497--- GTG/core/filteredtree.py 2010-08-02 19:07:24 +0000
1498+++ GTG/core/filteredtree.py 2010-08-13 23:43:04 +0000
1499@@ -791,7 +791,10 @@
1500 self.node_to_remove.append(tid)
1501 isroot = False
1502 if tid in self.displayed_nodes:
1503- isroot = self.__is_root(self.get_node(tid))
1504+ node = self.get_node(tid)
1505+ if not node:
1506+ return
1507+ isroot = self.__is_root(node)
1508 self.remove_count += 1
1509 self.__nodes_count -= 1
1510 self.emit('task-deleted-inview',tid)
1511
1512=== modified file 'GTG/core/filters_bank.py'
1513--- GTG/core/filters_bank.py 2010-08-02 19:09:17 +0000
1514+++ GTG/core/filters_bank.py 2010-08-13 23:43:04 +0000
1515@@ -256,6 +256,8 @@
1516 @param task: a task object
1517 @oaram tags_to_match_set: a *set* of tag names
1518 '''
1519+ if task == None:
1520+ return []
1521 try:
1522 tags_to_match_set = parameters['tags']
1523 except KeyError:
1524
1525=== modified file 'GTG/core/requester.py'
1526--- GTG/core/requester.py 2010-06-22 19:55:15 +0000
1527+++ GTG/core/requester.py 2010-08-13 23:43:04 +0000
1528@@ -284,3 +284,6 @@
1529
1530 def backend_change_attached_tags(self, backend_id, tags):
1531 return self.ds.backend_change_attached_tags(backend_id, tags)
1532+
1533+ def save_datastore(self):
1534+ return self.ds.save()
1535
1536=== modified file 'GTG/core/task.py'
1537--- GTG/core/task.py 2010-07-01 09:59:33 +0000
1538+++ GTG/core/task.py 2010-08-13 23:43:04 +0000
1539@@ -505,7 +505,9 @@
1540 def get_tags(self):
1541 l = []
1542 for tname in self.tags:
1543- l.append(self.req.get_tag(tname))
1544+ tag = self.req.get_tag(tname)
1545+ if tag:
1546+ l.append(tag)
1547 return l
1548
1549 def rename_tag(self, old, new):
1550
1551=== modified file 'GTG/gtk/browser/browser.py'
1552--- GTG/gtk/browser/browser.py 2010-08-10 17:30:24 +0000
1553+++ GTG/gtk/browser/browser.py 2010-08-13 23:43:04 +0000
1554@@ -953,7 +953,9 @@
1555 text = \
1556 text.replace("%s%s:%s" % (spaces, attribute, args), "")
1557 # Create the new task
1558- task = self.req.new_task(tags=[t.get_name() for t in tags], newtask=True)
1559+ task = self.req.new_task( newtask=True)
1560+ for tag in tags:
1561+ task.add_tag(tag.get_name())
1562 if text != "":
1563 task.set_title(text.strip())
1564 task.set_to_keep()
1565
1566=== modified file 'GTG/tests/test_backends.py'
1567--- GTG/tests/test_backends.py 2010-06-23 12:49:28 +0000
1568+++ GTG/tests/test_backends.py 2010-08-13 23:43:04 +0000
1569@@ -1,6 +1,6 @@
1570 # -*- coding: utf-8 -*-
1571 # -----------------------------------------------------------------------------
1572-# Gettings Things Gnome! - a personal organizer for the GNOME desktop
1573+# Getting Things Gnome! - a personal organizer for the GNOME desktop
1574 # Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
1575 #
1576 # This program is free software: you can redistribute it and/or modify it under
1577@@ -30,8 +30,8 @@
1578
1579 # GTG imports
1580 from GTG.backends import backend_localfile as localfile
1581-from GTG.core import datastore
1582 from GTG.tools import cleanxml
1583+from GTG.core import CoreConfig
1584
1585
1586 class GtgBackendsUniTests(unittest.TestCase):
1587@@ -44,6 +44,10 @@
1588 self.taskpath = ''
1589 self.datapath = ''
1590
1591+ def SetUp(self):
1592+ CoreConfig().set_data_dir("./test_data")
1593+ CoreConfig().set_conf_dir("./test_data")
1594+
1595 def test_localfile_get_name(self):
1596 """Tests for localfile/get_name function :
1597 - a string is expected.
1598@@ -105,52 +109,6 @@
1599 expectedres = True
1600 self.assertEqual(res, expectedres)
1601
1602-# def test_localfile_backend_method4(self):
1603-# """Tests for localfile/Backend/get_task method:
1604-# - Compares task titles to check if method works.
1605-# """
1606-# self.create_test_environment()
1607-# doc, configxml = cleanxml.openxmlfile(self.datapath, 'config')
1608-# xmlproject = doc.getElementsByTagName('backend')
1609-# for domobj in xmlproject:
1610-# dic = {}
1611-# if domobj.hasAttribute("module"):
1612-# dic["module"] = str(domobj.getAttribute("module"))
1613-# dic["pid"] = str(domobj.getAttribute("pid"))
1614-# dic["xmlobject"] = domobj
1615-# dic["filename"] = self.taskfile
1616-# beobj = localfile.Backend(dic)
1617-# dstore = datastore.DataStore()
1618-# newtask = dstore.new_task(tid="0@2", pid="1", newtask=True)
1619-# beobj.get_task(newtask, "0@1")
1620-# self.assertEqual(newtask.get_title(), u"Ceci est un test")
1621-
1622-# def test_localfile_backend_method5(self):
1623-# """Tests for localfile/Backend/set_task method:
1624-# - parses task file to check if new task has been stored.
1625-# """
1626-# self.create_test_environment()
1627-# doc, configxml = cleanxml.openxmlfile(self.datapath, 'config')
1628-# xmlproject = doc.getElementsByTagName('backend')
1629-# for domobj in xmlproject:
1630-# dic = {}
1631-# if domobj.hasAttribute("module"):
1632-# dic["module"] = str(domobj.getAttribute("module"))
1633-# dic["pid"] = str(domobj.getAttribute("pid"))
1634-# dic["xmlobject"] = domobj
1635-# dic["filename"] = self.taskfile
1636-# beobj = localfile.Backend(dic)
1637-# dstore = datastore.DataStore()
1638-# newtask = dstore.new_task(tid="0@2", pid="1", newtask=True)
1639-# beobj.set_task(newtask)
1640-# dataline = open(self.taskpath, 'r').read()
1641-# if "0@2" in dataline:
1642-# res = True
1643-# else:
1644-# res = False
1645-# expectedres = True
1646-# self.assertEqual(res, expectedres)
1647-
1648 def create_test_environment(self):
1649 """Create the test environment"""
1650 self.taskfile = 'test.xml'
1651
1652=== added file 'GTG/tests/test_interruptible.py'
1653--- GTG/tests/test_interruptible.py 1970-01-01 00:00:00 +0000
1654+++ GTG/tests/test_interruptible.py 2010-08-13 23:43:04 +0000
1655@@ -0,0 +1,61 @@
1656+# -*- coding: utf-8 -*-
1657+# -----------------------------------------------------------------------------
1658+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
1659+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
1660+#
1661+# This program is free software: you can redistribute it and/or modify it under
1662+# the terms of the GNU General Public License as published by the Free Software
1663+# Foundation, either version 3 of the License, or (at your option) any later
1664+# version.
1665+#
1666+# This program is distributed in the hope that it will be useful, but WITHOUT
1667+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
1668+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
1669+# details.
1670+#
1671+# You should have received a copy of the GNU General Public License along with
1672+# this program. If not, see <http://www.gnu.org/licenses/>.
1673+# -----------------------------------------------------------------------------
1674+
1675+'''
1676+Tests for interrupting cooperative threads
1677+'''
1678+
1679+import unittest
1680+import time
1681+from threading import Thread, Event
1682+
1683+from GTG.tools.interruptible import interruptible, _cancellation_point
1684+
1685+
1686+class TestInterruptible(unittest.TestCase):
1687+ '''
1688+ Tests for interrupting cooperative threads
1689+ '''
1690+
1691+ def test_interruptible_decorator(self):
1692+ '''Tests for the @interruptible decorator.'''
1693+ self.quit_condition = False
1694+ cancellation_point = lambda: _cancellation_point(\
1695+ lambda: self.quit_condition)
1696+ self.thread_started = Event()
1697+ @interruptible
1698+ def never_ending(cancellation_point):
1699+ self.thread_started.set()
1700+ while True:
1701+ time.sleep(0.1)
1702+ cancellation_point()
1703+ thread = Thread(target = never_ending, args = (cancellation_point, ))
1704+ thread.start()
1705+ self.thread_started.wait()
1706+ self.quit_condition = True
1707+ countdown = 10
1708+ while thread.is_alive() and countdown > 0:
1709+ time.sleep(0.1)
1710+ countdown -= 1
1711+ self.assertFalse(thread.is_alive())
1712+
1713+
1714+def test_suite():
1715+ return unittest.TestLoader().loadTestsFromTestCase(TestInterruptible)
1716+
1717
1718=== added file 'GTG/tools/interruptible.py'
1719--- GTG/tools/interruptible.py 1970-01-01 00:00:00 +0000
1720+++ GTG/tools/interruptible.py 2010-08-13 23:43:04 +0000
1721@@ -0,0 +1,62 @@
1722+# -*- coding: utf-8 -*-
1723+# -----------------------------------------------------------------------------
1724+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
1725+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
1726+#
1727+# This program is free software: you can redistribute it and/or modify it under
1728+# the terms of the GNU General Public License as published by the Free Software
1729+# Foundation, either version 3 of the License, or (at your option) any later
1730+# version.
1731+#
1732+# This program is distributed in the hope that it will be useful, but WITHOUT
1733+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
1734+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
1735+# details.
1736+#
1737+# You should have received a copy of the GNU General Public License along with
1738+# this program. If not, see <http://www.gnu.org/licenses/>.
1739+# -----------------------------------------------------------------------------
1740+
1741+'''
1742+Utils to stop and quit gracefully a thread, issuing the command from
1743+another one
1744+'''
1745+
1746+
1747+
1748+class Interrupted(Exception):
1749+ '''Exception raised when a thread should be interrupted'''
1750+
1751+
1752+ pass
1753+
1754+
1755+
1756+
1757+def interruptible(fn):
1758+ '''
1759+ A decorator that makes a function interruptible. It should be applied only
1760+ to the function which is the target of a Thread object.
1761+ '''
1762+
1763+
1764+ def new(*args):
1765+ try:
1766+ return fn(*args)
1767+ except Interrupted:
1768+ return
1769+ return new
1770+
1771+def _cancellation_point(test_function):
1772+ '''
1773+ This function checks a test_function and, if it evaluates to True, makes the
1774+ thread quit (similar to pthread_cancel() in C)
1775+ It starts with a _ as it's mostly used in a specialized form, as:
1776+ cancellation_point = functools.partial(_cancellation_point,
1777+ lambda: quit_condition == True)
1778+
1779+ @param test_function: the function to test before cancelling
1780+ '''
1781+ if test_function():
1782+ raise Interrupted
1783+
1784
1785=== modified file 'GTG/tools/taskxml.py'
1786--- GTG/tools/taskxml.py 2010-06-22 19:55:15 +0000
1787+++ GTG/tools/taskxml.py 2010-08-13 23:43:04 +0000
1788@@ -20,6 +20,7 @@
1789 #Functions to convert a Task object to an XML string and back
1790 import xml.dom.minidom
1791 import xml.sax.saxutils as saxutils
1792+import datetime
1793
1794 from GTG.tools import cleanxml
1795 from GTG.tools import dates
1796@@ -55,7 +56,6 @@
1797 content = xml.dom.minidom.parseString(tas)
1798 cur_task.set_text(content.firstChild.toxml()) #pylint: disable-msg=E1103
1799 cur_task.set_due_date(dates.strtodate(cleanxml.readTextNode(xmlnode,"duedate")))
1800- cur_task.set_modified(cleanxml.readTextNode(xmlnode,"modified"))
1801 cur_task.set_start_date(dates.strtodate(cleanxml.readTextNode(xmlnode,"startdate")))
1802 cur_tags = xmlnode.getAttribute("tags").replace(' ','').split(",")
1803 if "" in cur_tags: cur_tags.remove("")
1804@@ -69,6 +69,11 @@
1805 backend_id = node.firstChild.nodeValue
1806 remote_task_id = node.childNodes[1].firstChild.nodeValue
1807 task.add_remote_id(backend_id, remote_task_id)
1808+ modified_string = cleanxml.readTextNode(xmlnode,"modified")
1809+ if modified_string:
1810+ modified_datetime = datetime.datetime.strptime(modified_string,\
1811+ "%Y-%m-%dT%H:%M:%S")
1812+ cur_task.set_modified(modified_datetime)
1813 return cur_task
1814
1815 #Task as parameter the doc where to put the XML node
1816
1817=== added file 'data/icons/hicolor/scalable/apps/backend_localfile.svg'
1818--- data/icons/hicolor/scalable/apps/backend_localfile.svg 1970-01-01 00:00:00 +0000
1819+++ data/icons/hicolor/scalable/apps/backend_localfile.svg 2010-08-13 23:43:04 +0000
1820@@ -0,0 +1,610 @@
1821+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
1822+<!-- Created with Inkscape (http://www.inkscape.org/) -->
1823+
1824+<svg
1825+ xmlns:dc="http://purl.org/dc/elements/1.1/"
1826+ xmlns:cc="http://creativecommons.org/ns#"
1827+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
1828+ xmlns:svg="http://www.w3.org/2000/svg"
1829+ xmlns="http://www.w3.org/2000/svg"
1830+ xmlns:xlink="http://www.w3.org/1999/xlink"
1831+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
1832+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
1833+ inkscape:export-ydpi="600"
1834+ inkscape:export-xdpi="600"
1835+ inkscape:export-filename="/home/luca/Projects/gtg/Google/GSoC/data/icons/hicolor/scalable/apps/backend_localfile.png"
1836+ sodipodi:docname="backend_localfile.svg"
1837+ inkscape:version="0.47 r22583"
1838+ sodipodi:version="0.32"
1839+ id="svg249"
1840+ height="48.000000px"
1841+ width="48.000000px"
1842+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
1843+ version="1.1">
1844+ <defs
1845+ id="defs3">
1846+ <inkscape:perspective
1847+ sodipodi:type="inkscape:persp3d"
1848+ inkscape:vp_x="0 : 24 : 1"
1849+ inkscape:vp_y="0 : 1000 : 0"
1850+ inkscape:vp_z="48 : 24 : 1"
1851+ inkscape:persp3d-origin="24 : 16 : 1"
1852+ id="perspective80" />
1853+ <linearGradient
1854+ inkscape:collect="always"
1855+ xlink:href="#linearGradient3656"
1856+ id="linearGradient5816"
1857+ gradientUnits="userSpaceOnUse"
1858+ x1="-26.753757"
1859+ y1="11.566258"
1860+ x2="-24.75"
1861+ y2="9.687501" />
1862+ <linearGradient
1863+ inkscape:collect="always"
1864+ xlink:href="#linearGradient3520"
1865+ id="linearGradient5836"
1866+ gradientUnits="userSpaceOnUse"
1867+ gradientTransform="matrix(0.9223058,0,0,0.9185751,-92.447368,61.3257)"
1868+ x1="-18.588562"
1869+ y1="11.052948"
1870+ x2="-28.789402"
1871+ y2="14.069944" />
1872+ <radialGradient
1873+ inkscape:collect="always"
1874+ xlink:href="#linearGradient3671"
1875+ id="radialGradient5839"
1876+ gradientUnits="userSpaceOnUse"
1877+ gradientTransform="matrix(0.4073362,-0.2798276,0.7510293,1.0932492,-115.18484,51.56213)"
1878+ cx="-26.305403"
1879+ cy="10.108011"
1880+ fx="-26.305403"
1881+ fy="10.108011"
1882+ r="7.0421038" />
1883+ <linearGradient
1884+ inkscape:collect="always"
1885+ id="linearGradient6469">
1886+ <stop
1887+ style="stop-color:#000000;stop-opacity:1;"
1888+ offset="0"
1889+ id="stop6471" />
1890+ <stop
1891+ style="stop-color:#000000;stop-opacity:0;"
1892+ offset="1"
1893+ id="stop6473" />
1894+ </linearGradient>
1895+ <linearGradient
1896+ inkscape:collect="always"
1897+ xlink:href="#linearGradient6469"
1898+ id="linearGradient6475"
1899+ x1="58.282169"
1900+ y1="70.751839"
1901+ x2="61.181217"
1902+ y2="67.799171"
1903+ gradientUnits="userSpaceOnUse"
1904+ gradientTransform="translate(-180,0)" />
1905+ <radialGradient
1906+ inkscape:collect="always"
1907+ xlink:href="#linearGradient3741"
1908+ id="radialGradient5810"
1909+ gradientUnits="userSpaceOnUse"
1910+ gradientTransform="matrix(1.8860258,0,0,1.1764706,-3.5441033,-4.2352941)"
1911+ cx="4"
1912+ cy="5.2999997"
1913+ fx="4"
1914+ fy="5.2999997"
1915+ r="17" />
1916+ <linearGradient
1917+ inkscape:collect="always"
1918+ xlink:href="#linearGradient3613"
1919+ id="linearGradient5845"
1920+ gradientUnits="userSpaceOnUse"
1921+ gradientTransform="translate(-90,60)"
1922+ x1="-47.5"
1923+ y1="49.020683"
1924+ x2="-62.75"
1925+ y2="-22.502075" />
1926+ <radialGradient
1927+ inkscape:collect="always"
1928+ xlink:href="#linearGradient3683"
1929+ id="radialGradient5843"
1930+ gradientUnits="userSpaceOnUse"
1931+ gradientTransform="matrix(3.9957492,0,0,1.9350367,0.62141,28.832578)"
1932+ cx="-30.249996"
1933+ cy="35.357208"
1934+ fx="-30.249996"
1935+ fy="35.357208"
1936+ r="18.000002" />
1937+ <linearGradient
1938+ inkscape:collect="always"
1939+ xlink:href="#linearGradient3702"
1940+ id="linearGradient5804"
1941+ gradientUnits="userSpaceOnUse"
1942+ x1="25.058096"
1943+ y1="47.027729"
1944+ x2="25.058096"
1945+ y2="39.999443" />
1946+ <radialGradient
1947+ inkscape:collect="always"
1948+ xlink:href="#linearGradient3688"
1949+ id="radialGradient5802"
1950+ gradientUnits="userSpaceOnUse"
1951+ gradientTransform="matrix(2.003784,0,0,1.4,-20.01187,-104.4)"
1952+ cx="4.9929786"
1953+ cy="43.5"
1954+ fx="4.9929786"
1955+ fy="43.5"
1956+ r="2.5" />
1957+ <radialGradient
1958+ inkscape:collect="always"
1959+ xlink:href="#linearGradient3688"
1960+ id="radialGradient5800"
1961+ gradientUnits="userSpaceOnUse"
1962+ gradientTransform="matrix(2.003784,0,0,1.4,27.98813,-17.4)"
1963+ cx="4.9929786"
1964+ cy="43.5"
1965+ fx="4.9929786"
1966+ fy="43.5"
1967+ r="2.5" />
1968+ <linearGradient
1969+ inkscape:collect="always"
1970+ xlink:href="#linearGradient3702"
1971+ id="linearGradient5798"
1972+ gradientUnits="userSpaceOnUse"
1973+ x1="25.058096"
1974+ y1="47.027729"
1975+ x2="25.058096"
1976+ y2="39.999443" />
1977+ <radialGradient
1978+ inkscape:collect="always"
1979+ xlink:href="#linearGradient3688"
1980+ id="radialGradient5796"
1981+ gradientUnits="userSpaceOnUse"
1982+ gradientTransform="matrix(2.003784,0,0,1.4,-20.01187,-104.4)"
1983+ cx="4.9929786"
1984+ cy="43.5"
1985+ fx="4.9929786"
1986+ fy="43.5"
1987+ r="2.5" />
1988+ <radialGradient
1989+ inkscape:collect="always"
1990+ xlink:href="#linearGradient3688"
1991+ id="radialGradient5794"
1992+ gradientUnits="userSpaceOnUse"
1993+ gradientTransform="matrix(2.003784,0,0,1.4,27.98813,-17.4)"
1994+ cx="4.9929786"
1995+ cy="43.5"
1996+ fx="4.9929786"
1997+ fy="43.5"
1998+ r="2.5" />
1999+ <linearGradient
2000+ inkscape:collect="always"
2001+ id="linearGradient3656">
2002+ <stop
2003+ style="stop-color:#ffffff;stop-opacity:1;"
2004+ offset="0"
2005+ id="stop3658" />
2006+ <stop
2007+ style="stop-color:#ffffff;stop-opacity:0;"
2008+ offset="1"
2009+ id="stop3660" />
2010+ </linearGradient>
2011+ <linearGradient
2012+ inkscape:collect="always"
2013+ id="linearGradient3520">
2014+ <stop
2015+ style="stop-color:#000000;stop-opacity:0.41295547"
2016+ offset="0"
2017+ id="stop3522" />
2018+ <stop
2019+ style="stop-color:#000000;stop-opacity:0;"
2020+ offset="1"
2021+ id="stop3524" />
2022+ </linearGradient>
2023+ <linearGradient
2024+ id="linearGradient3671">
2025+ <stop
2026+ style="stop-color:#ffffff;stop-opacity:1;"
2027+ offset="0"
2028+ id="stop3673" />
2029+ <stop
2030+ id="stop3691"
2031+ offset="0.47533694"
2032+ style="stop-color:#ffffff;stop-opacity:1;" />
2033+ <stop
2034+ style="stop-color:#ffffff;stop-opacity:0;"
2035+ offset="1"
2036+ id="stop3675" />
2037+ </linearGradient>
2038+ <linearGradient
2039+ inkscape:collect="always"
2040+ id="linearGradient3741">
2041+ <stop
2042+ style="stop-color:#ffffff;stop-opacity:1;"
2043+ offset="0"
2044+ id="stop3743" />
2045+ <stop
2046+ style="stop-color:#ffffff;stop-opacity:0;"
2047+ offset="1"
2048+ id="stop3745" />
2049+ </linearGradient>
2050+ <linearGradient
2051+ inkscape:collect="always"
2052+ id="linearGradient3613">
2053+ <stop
2054+ style="stop-color:#888a85;stop-opacity:1"
2055+ offset="0"
2056+ id="stop3615" />
2057+ <stop
2058+ style="stop-color:#babdb6;stop-opacity:1"
2059+ offset="1"
2060+ id="stop3617" />
2061+ </linearGradient>
2062+ <linearGradient
2063+ id="linearGradient3683">
2064+ <stop
2065+ id="stop3685"
2066+ offset="0"
2067+ style="stop-color:#f6f6f5;stop-opacity:1;" />
2068+ <stop
2069+ id="stop3689"
2070+ offset="1"
2071+ style="stop-color:#d3d7cf;stop-opacity:1" />
2072+ </linearGradient>
2073+ <linearGradient
2074+ id="linearGradient3688"
2075+ inkscape:collect="always">
2076+ <stop
2077+ id="stop3690"
2078+ offset="0"
2079+ style="stop-color:black;stop-opacity:1;" />
2080+ <stop
2081+ id="stop3692"
2082+ offset="1"
2083+ style="stop-color:black;stop-opacity:0;" />
2084+ </linearGradient>
2085+ <linearGradient
2086+ id="linearGradient3702">
2087+ <stop
2088+ id="stop3704"
2089+ offset="0"
2090+ style="stop-color:black;stop-opacity:0;" />
2091+ <stop
2092+ style="stop-color:black;stop-opacity:1;"
2093+ offset="0.5"
2094+ id="stop3710" />
2095+ <stop
2096+ id="stop3706"
2097+ offset="1"
2098+ style="stop-color:black;stop-opacity:0;" />
2099+ </linearGradient>
2100+ </defs>
2101+ <sodipodi:namedview
2102+ inkscape:window-y="26"
2103+ inkscape:window-x="230"
2104+ inkscape:window-height="742"
2105+ inkscape:window-width="1048"
2106+ inkscape:document-units="px"
2107+ inkscape:grid-bbox="true"
2108+ showgrid="false"
2109+ inkscape:current-layer="layer6"
2110+ inkscape:cy="9.7451355"
2111+ inkscape:cx="46.371834"
2112+ inkscape:zoom="1"
2113+ inkscape:pageshadow="2"
2114+ inkscape:pageopacity="0.0"
2115+ borderopacity="0.25490196"
2116+ bordercolor="#666666"
2117+ pagecolor="#ffffff"
2118+ id="base"
2119+ inkscape:showpageshadow="false"
2120+ inkscape:window-maximized="0" />
2121+ <metadata
2122+ id="metadata4">
2123+ <rdf:RDF>
2124+ <cc:Work
2125+ rdf:about="">
2126+ <dc:format>image/svg+xml</dc:format>
2127+ <dc:type
2128+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
2129+ <dc:title>Generic Text</dc:title>
2130+ <dc:subject>
2131+ <rdf:Bag>
2132+ <rdf:li>text</rdf:li>
2133+ <rdf:li>plaintext</rdf:li>
2134+ <rdf:li>regular</rdf:li>
2135+ <rdf:li>document</rdf:li>
2136+ </rdf:Bag>
2137+ </dc:subject>
2138+ <cc:license
2139+ rdf:resource="http://creativecommons.org/licenses/GPL/2.0/" />
2140+ <dc:creator>
2141+ <cc:Agent>
2142+ <dc:title>Lapo Calamandrei</dc:title>
2143+ </cc:Agent>
2144+ </dc:creator>
2145+ <dc:source />
2146+ <dc:date />
2147+ <dc:rights>
2148+ <cc:Agent>
2149+ <dc:title />
2150+ </cc:Agent>
2151+ </dc:rights>
2152+ <dc:publisher>
2153+ <cc:Agent>
2154+ <dc:title />
2155+ </cc:Agent>
2156+ </dc:publisher>
2157+ <dc:identifier />
2158+ <dc:relation />
2159+ <dc:language />
2160+ <dc:coverage />
2161+ <dc:description />
2162+ <dc:contributor>
2163+ <cc:Agent>
2164+ <dc:title />
2165+ </cc:Agent>
2166+ </dc:contributor>
2167+ </cc:Work>
2168+ <cc:License
2169+ rdf:about="http://creativecommons.org/licenses/GPL/2.0/">
2170+ <cc:permits
2171+ rdf:resource="http://web.resource.org/cc/Reproduction" />
2172+ <cc:permits
2173+ rdf:resource="http://web.resource.org/cc/Distribution" />
2174+ <cc:requires
2175+ rdf:resource="http://web.resource.org/cc/Notice" />
2176+ <cc:permits
2177+ rdf:resource="http://web.resource.org/cc/DerivativeWorks" />
2178+ <cc:requires
2179+ rdf:resource="http://web.resource.org/cc/ShareAlike" />
2180+ <cc:requires
2181+ rdf:resource="http://web.resource.org/cc/SourceCode" />
2182+ </cc:License>
2183+ </rdf:RDF>
2184+ </metadata>
2185+ <g
2186+ inkscape:groupmode="layer"
2187+ id="layer6"
2188+ inkscape:label="Shadow">
2189+ <g
2190+ style="display:inline"
2191+ id="g6015"
2192+ transform="translate(150,-60)">
2193+ <rect
2194+ y="60"
2195+ x="-150"
2196+ height="48"
2197+ width="48"
2198+ id="rect5504"
2199+ style="opacity:0;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;display:inline" />
2200+ <g
2201+ transform="matrix(1.0464281,0,0,0.8888889,-151.18572,65.72224)"
2202+ inkscape:label="Shadow"
2203+ id="g5508"
2204+ style="opacity:0.65587045;display:inline">
2205+ <g
2206+ transform="matrix(1.052632,0,0,1.285713,-1.263158,-13.42854)"
2207+ style="opacity:0.4"
2208+ id="g5511">
2209+ <rect
2210+ 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"
2211+ id="rect5513"
2212+ width="5"
2213+ height="7"
2214+ x="38"
2215+ y="40" />
2216+ <rect
2217+ 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"
2218+ id="rect5515"
2219+ width="5"
2220+ height="7"
2221+ x="-10"
2222+ y="-47"
2223+ transform="scale(-1,-1)" />
2224+ <rect
2225+ 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"
2226+ id="rect5517"
2227+ width="28"
2228+ height="7.0000005"
2229+ x="10"
2230+ y="40" />
2231+ </g>
2232+ </g>
2233+ <g
2234+ transform="matrix(0.9548466,0,0,0.5555562,-148.98776,79.888875)"
2235+ inkscape:label="Shadow"
2236+ id="g5519"
2237+ style="display:inline">
2238+ <g
2239+ transform="matrix(1.052632,0,0,1.285713,-1.263158,-13.42854)"
2240+ style="opacity:0.4"
2241+ id="g5521">
2242+ <rect
2243+ 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"
2244+ id="rect5523"
2245+ width="5"
2246+ height="7"
2247+ x="38"
2248+ y="40" />
2249+ <rect
2250+ 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"
2251+ id="rect5525"
2252+ width="5"
2253+ height="7"
2254+ x="-10"
2255+ y="-47"
2256+ transform="scale(-1,-1)" />
2257+ <rect
2258+ 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"
2259+ id="rect5527"
2260+ width="28"
2261+ height="7.0000005"
2262+ x="10"
2263+ y="40" />
2264+ </g>
2265+ </g>
2266+ <path
2267+ sodipodi:nodetypes="ccsccccccc"
2268+ id="path5529"
2269+ 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"
2270+ style="fill:url(#radialGradient5843);fill-opacity:1;stroke:url(#linearGradient5845);stroke-width:1;stroke-miterlimit:4;display:inline" />
2271+ <path
2272+ transform="translate(-150,60)"
2273+ 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"
2274+ id="path5531"
2275+ style="opacity:0.68016196;fill:url(#radialGradient5810);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;display:inline"
2276+ 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 "
2277+ inkscape:radius="-0.4861359"
2278+ sodipodi:type="inkscape:offset" />
2279+ <path
2280+ id="rect5857"
2281+ 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"
2282+ 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" />
2283+ <path
2284+ sodipodi:nodetypes="ccccczc"
2285+ id="path5533"
2286+ 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"
2287+ style="fill:url(#radialGradient5839);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;display:inline" />
2288+ <path
2289+ sodipodi:nodetypes="ccccc"
2290+ id="path5535"
2291+ 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"
2292+ style="opacity:0.87854249;fill:url(#linearGradient5836);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;display:inline" />
2293+ <path
2294+ transform="translate(-90,60)"
2295+ 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"
2296+ id="path5537"
2297+ style="fill:none;fill-opacity:1;stroke:url(#linearGradient5816);stroke-width:1;stroke-miterlimit:4;display:inline"
2298+ 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 "
2299+ inkscape:radius="-0.99436891"
2300+ sodipodi:type="inkscape:offset" />
2301+ <g
2302+ inkscape:r_cy="true"
2303+ inkscape:r_cx="true"
2304+ transform="matrix(0.928889,0,0,1,-148.28889,60)"
2305+ style="opacity:0.15;fill:#000000;display:inline"
2306+ id="g5539">
2307+ <rect
2308+ ry="0.46875"
2309+ rx="0.50463516"
2310+ inkscape:r_cy="true"
2311+ inkscape:r_cx="true"
2312+ y="19.0625"
2313+ x="10"
2314+ height="0.9375"
2315+ width="28.125"
2316+ id="rect5549"
2317+ 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" />
2318+ <rect
2319+ 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"
2320+ id="rect5553"
2321+ width="28.125"
2322+ height="0.9375"
2323+ x="10"
2324+ y="21.0625"
2325+ inkscape:r_cx="true"
2326+ inkscape:r_cy="true"
2327+ rx="0.50463516"
2328+ ry="0.46875" />
2329+ <rect
2330+ 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"
2331+ id="rect5555"
2332+ width="28.125"
2333+ height="0.9375"
2334+ x="10"
2335+ y="23.0625"
2336+ inkscape:r_cx="true"
2337+ inkscape:r_cy="true"
2338+ rx="0.50463516"
2339+ ry="0.46875" />
2340+ <rect
2341+ ry="0.46875"
2342+ rx="0.50463516"
2343+ inkscape:r_cy="true"
2344+ inkscape:r_cx="true"
2345+ y="25.0625"
2346+ x="10"
2347+ height="0.9375"
2348+ width="28.125"
2349+ id="rect5557"
2350+ 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" />
2351+ <rect
2352+ ry="0.46875"
2353+ rx="0.50463516"
2354+ inkscape:r_cy="true"
2355+ inkscape:r_cx="true"
2356+ y="27.0625"
2357+ x="10"
2358+ height="0.9375"
2359+ width="28.125"
2360+ id="rect5559"
2361+ 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" />
2362+ <rect
2363+ 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"
2364+ id="rect5561"
2365+ width="28.125"
2366+ height="0.9375"
2367+ x="10"
2368+ y="29.0625"
2369+ inkscape:r_cx="true"
2370+ inkscape:r_cy="true"
2371+ rx="0.50463516"
2372+ ry="0.46875" />
2373+ <rect
2374+ 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"
2375+ id="rect5563"
2376+ width="28.125"
2377+ height="0.9375"
2378+ x="10"
2379+ y="31.0625"
2380+ inkscape:r_cx="true"
2381+ inkscape:r_cy="true"
2382+ rx="0.50463516"
2383+ ry="0.46875" />
2384+ <rect
2385+ ry="0.46875"
2386+ rx="0.50463516"
2387+ inkscape:r_cy="true"
2388+ inkscape:r_cx="true"
2389+ y="33.0625"
2390+ x="10"
2391+ height="0.9375"
2392+ width="28.125"
2393+ id="rect5565"
2394+ 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" />
2395+ <rect
2396+ ry="0.46875"
2397+ rx="0.50463516"
2398+ inkscape:r_cy="true"
2399+ inkscape:r_cx="true"
2400+ y="35.0625"
2401+ x="10"
2402+ height="0.9375"
2403+ width="28.125"
2404+ id="rect5567"
2405+ 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" />
2406+ <rect
2407+ ry="0.46875"
2408+ rx="0.50463516"
2409+ inkscape:r_cy="true"
2410+ inkscape:r_cx="true"
2411+ y="37.0625"
2412+ x="10"
2413+ height="0.9375"
2414+ width="13.8125"
2415+ id="rect5569"
2416+ 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" />
2417+ </g>
2418+ </g>
2419+ </g>
2420+ <g
2421+ style="display:inline"
2422+ inkscape:groupmode="layer"
2423+ inkscape:label="Base"
2424+ id="layer1" />
2425+ <g
2426+ inkscape:groupmode="layer"
2427+ id="layer5"
2428+ inkscape:label="Text"
2429+ style="display:inline" />
2430+</svg>

Subscribers

People subscribed via source and target branches

to status/vote changes: