GTG

Merge lp:~gtg-user/gtg/multibackends-halfgsoc_merge into lp:~gtg/gtg/old-trunk

Proposed by Luca Invernizzi
Status: Superseded
Proposed branch: lp:~gtg-user/gtg/multibackends-halfgsoc_merge
Merge into: lp:~gtg/gtg/old-trunk
Diff against target: 3899 lines (+2527/-662)
36 files modified
GTG/backends/__init__.py (+170/-11)
GTG/backends/backend_localfile.py (+181/-0)
GTG/backends/backendsignals.py (+125/-0)
GTG/backends/genericbackend.py (+571/-0)
GTG/backends/localfile.py (+0/-176)
GTG/core/__init__.py (+57/-113)
GTG/core/datastore.py (+501/-208)
GTG/core/filters_bank.py (+20/-0)
GTG/core/requester.py (+51/-18)
GTG/core/tagstore.py (+8/-4)
GTG/core/task.py (+37/-22)
GTG/gtg.py (+34/-34)
GTG/gtk/browser/browser.py (+4/-7)
GTG/gtk/browser/tagtree.py (+2/-1)
GTG/gtk/crashhandler.py (+24/-0)
GTG/gtk/delete_dialog.py (+5/-1)
GTG/gtk/editor/editor.py (+2/-2)
GTG/gtk/manager.py (+10/-6)
GTG/gtk/preferences.glade (+2/-55)
GTG/gtk/preferences.py (+1/-1)
GTG/tests/__init__.py (+10/-1)
GTG/tests/test_apidocs.py (+4/-0)
GTG/tests/test_backends.py (+191/-0)
GTG/tests/test_datastore.py (+360/-0)
GTG/tests/test_tagstore.py (+3/-0)
GTG/tests/test_taskviewserial.py (+3/-0)
GTG/tests/test_tree.py (+5/-0)
GTG/tools/borg.py (+33/-0)
GTG/tools/keyring.py (+48/-0)
GTG/tools/logger.py (+6/-0)
GTG/tools/synchronized.py (+14/-0)
GTG/tools/taskxml.py (+23/-1)
GTG/tools/testingmode.py (+16/-0)
Makefile (+1/-1)
scripts/debug.sh (+1/-0)
scripts/profile_interpret.sh (+4/-0)
To merge this branch: bzr merge lp:~gtg-user/gtg/multibackends-halfgsoc_merge
Reviewer Review Type Date Requested Status
Lionel Dricot (community) Approve
Review via email: mp+28245@code.launchpad.net

This proposal has been superseded by a proposal from 2010-06-23.

Description of the change

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

[0] https://code.edge.launchpad.net/~gtg-user/gtg/backends-first-merge/+merge/26532

To post a comment you must log in.
827. By Luca Invernizzi

small fix

828. By Luca Invernizzi

small fix in filters_bank

829. By Luca Invernizzi

wrong function signature

830. By Luca Invernizzi

other small fix in the datastore (doesn't affect the localfile backend)

Revision history for this message
Lionel Dricot (ploum-deactivatedaccount) wrote :

It looks really good. There's plenty of good stuffs everywhere.

There's only one issue I want you to fix :
def backend_filter(self, task, tags_to_match_set): should respect the filter template and, thus, the second parameter should be a "paramenter={}". Then, it's up to you to pass the tags [] in a dic.

I believe that it will also allow you to get rid of the ugly "get_filter_func" that I prefer to avoid.

I trust you to fix that then, according to me, you can do the merge.

review: Approve

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'GTG/backends/__init__.py'
2--- GTG/backends/__init__.py 2010-03-01 01:55:12 +0000
3+++ GTG/backends/__init__.py 2010-06-23 01:06:26 +0000
4@@ -23,14 +23,173 @@
5 (like on the hard disk or on the internet)
6 and to read projects from this medium
7 """
8-#
9-#Current backends are :
10-#
11-# localfile.py : store and read a local XML file
12-#
13-#
14-#
15-# this __init__.py should not be empty. It should list the available backends
16-#
17-#http://www.faqts.com/knowledge_base/view.phtml/aid/4221/fid/538
18-#http://www.python.org/doc/2.1.3/tut/node8.html
19+
20+import sys
21+import uuid
22+import os.path
23+
24+from GTG.tools.logger import Log
25+from GTG.tools.borg import Borg
26+from GTG.backends.genericbackend import GenericBackend
27+from GTG.core import firstrun_tasks
28+from GTG.tools import cleanxml
29+from GTG.core import CoreConfig
30+
31+
32+
33+class BackendFactory(Borg):
34+ '''
35+ This class holds the information about the backend types.
36+ Since it's about types, all information is static. The instantiated
37+ backends are handled in the Datastore.
38+ It is a Borg for what matters its only state (_backend_modules),
39+ since it makes no sense of keeping multiple instances of this.
40+ '''
41+
42+
43+ BACKEND_PREFIX = "backend_"
44+
45+ def __init__(self):
46+ """
47+ Creates a dictionary of the currently available backend modules
48+ """
49+ super(BackendFactory, self).__init__()
50+ if hasattr(self, "backend_modules"):
51+ #This object has already been constructed
52+ return
53+ self.backend_modules = {}
54+ #Look for backends in the GTG/backends dir
55+ this_dir = os.path.dirname(__file__)
56+ backend_files = filter(lambda f: f.endswith(".py") and \
57+ f[ : len(self.BACKEND_PREFIX)] == self.BACKEND_PREFIX , \
58+ os.listdir(this_dir))
59+ #Create module names
60+ module_names = map(lambda f: f.replace(".py",""), backend_files)
61+ Log.debug("Backends found: " + str(module_names))
62+ #Load backend modules
63+ for module_name in module_names:
64+ extended_module_name = "GTG.backends." + module_name
65+ try:
66+ __import__(extended_module_name)
67+ except ImportError, exception:
68+ #Something is wrong with this backend, skipping
69+ Log.debug("Backend %s could not be loaded: %s" % \
70+ (module_name, str(exception)))
71+ continue
72+ self.backend_modules[module_name] = \
73+ sys.modules[extended_module_name]
74+
75+ def get_backend(self, backend_name):
76+ '''
77+ Returns the backend module for the backend matching
78+ backend_name. Else, returns none
79+ '''
80+ if backend_name in self.backend_modules:
81+ return self.backend_modules[backend_name]
82+ else:
83+ Log.debug("Trying to load backend %s, but failed!" % backend_name)
84+ return None
85+
86+ def get_all_backends(self):
87+ '''
88+ Returns a dictionary containing all the backends types
89+ '''
90+ return self.backend_modules
91+
92+ def get_new_backend_dict(self, backend_name, additional_parameters = {}):
93+ '''
94+ Constructs a new backend initialization dictionary. In more
95+ exact terms, creates a dictionary, containing all the necessary
96+ entries to initialize a backend.
97+ '''
98+ if not self.backend_modules.has_key(backend_name):
99+ return None
100+ dic = {}
101+ module = self.get_backend(backend_name)
102+ #Different pids are necessary to discern between backends of the same
103+ # type
104+ parameters = module.Backend.get_static_parameters()
105+ #we all the parameters and their default values in dic
106+ for param_name, param_dic in parameters.iteritems():
107+ dic[param_name] = param_dic[GenericBackend.PARAM_DEFAULT_VALUE]
108+ dic["pid"] = str(uuid.uuid4())
109+ dic["module"] = module.Backend.get_name()
110+ for param_name, param_value in additional_parameters.iteritems():
111+ dic[param_name] = param_value
112+ dic["backend"] = module.Backend(dic)
113+ return dic
114+
115+ def restore_backend_from_xml(self, dic):
116+ '''
117+ Function restoring a backend from its xml description.
118+ dic should be a dictionary containing at least the key
119+ - "module", with the module name
120+ - "xmlobject", with its xml description.
121+ Every other key is passed as-is to the backend, as parameter.
122+
123+ Returns the backend instance, or None is something goes wrong
124+ '''
125+ if not "module" in dic or not "xmlobject" in dic:
126+ Log.debug ("Malformed backend configuration found! %s" % \
127+ dic)
128+ module = self.get_backend(dic["module"])
129+ if module == None:
130+ Log.debug ("could not load module for backend %s" % \
131+ dic["module"])
132+ return None
133+ #we pop the xml object, as it will be redundant when the parameters
134+ # are set directly in the dict
135+ xp = dic.pop("xmlobject")
136+ #Building the dictionary
137+ parameters_specs = module.Backend.get_static_parameters()
138+ dic["pid"] = str(xp.getAttribute("pid"))
139+ for param_name, param_dic in parameters_specs.iteritems():
140+ if xp.hasAttribute(param_name):
141+ #we need to convert the parameter to the right format.
142+ # we fetch the format from the static_parameters
143+ param_type = param_dic[GenericBackend.PARAM_TYPE]
144+ param_value = GenericBackend.cast_param_type_from_string( \
145+ xp.getAttribute(param_name), param_type)
146+ dic[param_name] = param_value
147+ #We put the backend itself in the dict
148+ dic["backend"] = module.Backend(dic)
149+ return dic["backend"]
150+
151+ def get_saved_backends_list(self):
152+ backends_dic = self._read_backend_configuration_file()
153+
154+ #Retrocompatibility: default backend has changed name
155+ for dic in backends_dic:
156+ if dic["module"] == "localfile":
157+ dic["module"] = "backend_localfile"
158+ dic["pid"] = str(uuid.uuid4())
159+ dic["need_conversion"] = \
160+ dic["xmlobject"].getAttribute("filename")
161+
162+ #Now that the backend list is build, we will construct them
163+ for dic in backends_dic:
164+ self.restore_backend_from_xml(dic)
165+ #If no backend available, we create a new using localfile. Xmlobject
166+ # will be filled in by the backend
167+ if len(backends_dic) == 0:
168+ dic = BackendFactory().get_new_backend_dict( \
169+ "backend_localfile")
170+ dic["backend"].this_is_the_first_run(firstrun_tasks.populate())
171+ backends_dic.append(dic)
172+ return backends_dic
173+
174+ def _read_backend_configuration_file(self):
175+ '''
176+ Reads the file describing the current backend configuration (project.xml)
177+ and returns a list of dictionaries, each containing:
178+ - the xml object defining the backend characteristics under
179+ "xmlobject"
180+ - the name of the backend under "module"
181+ '''
182+ # Read configuration file, if it does not exist, create one
183+ datafile = os.path.join(CoreConfig().get_data_dir(), CoreConfig.DATA_FILE)
184+ doc, configxml = cleanxml.openxmlfile(datafile, "config")
185+ xmlproject = doc.getElementsByTagName("backend")
186+ # collect configured backends
187+ return [{"xmlobject": xp, \
188+ "module": xp.getAttribute("module")} for xp in xmlproject]
189
190=== added file 'GTG/backends/backend_localfile.py'
191--- GTG/backends/backend_localfile.py 1970-01-01 00:00:00 +0000
192+++ GTG/backends/backend_localfile.py 2010-06-23 01:06:26 +0000
193@@ -0,0 +1,181 @@
194+# -*- coding: utf-8 -*-
195+# -----------------------------------------------------------------------------
196+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
197+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
198+#
199+# This program is free software: you can redistribute it and/or modify it under
200+# the terms of the GNU General Public License as published by the Free Software
201+# Foundation, either version 3 of the License, or (at your option) any later
202+# version.
203+#
204+# This program is distributed in the hope that it will be useful, but WITHOUT
205+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
206+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
207+# details.
208+#
209+# You should have received a copy of the GNU General Public License along with
210+# this program. If not, see <http://www.gnu.org/licenses/>.
211+# -----------------------------------------------------------------------------
212+
213+'''
214+Localfile is a read/write backend that will store your tasks in an XML file
215+This file will be in your $XDG_DATA_DIR/gtg folder.
216+'''
217+
218+import os
219+import uuid
220+
221+from GTG.backends.genericbackend import GenericBackend
222+from GTG.core import CoreConfig
223+from GTG.tools import cleanxml, taskxml
224+from GTG import _
225+
226+
227+
228+class Backend(GenericBackend):
229+
230+
231+ DEFAULT_PATH = CoreConfig().get_data_dir() #default path for filenames
232+
233+
234+ #Description of the backend (mainly it's data we show the user, only the
235+ # name is used internally. Please note that BACKEND_NAME and
236+ # BACKEND_ICON_NAME should *not* be translated.
237+ _general_description = { \
238+ GenericBackend.BACKEND_NAME: "backend_localfile", \
239+ GenericBackend.BACKEND_HUMAN_NAME: _("Local File"), \
240+ GenericBackend.BACKEND_AUTHORS: ["Lionel Dricot", \
241+ "Luca Invernizzi"], \
242+ GenericBackend.BACKEND_TYPE: GenericBackend.TYPE_READWRITE, \
243+ GenericBackend.BACKEND_DESCRIPTION: \
244+ _("Your tasks are saved in a text file (XML format). " + \
245+ " This is the most basic and the default way " + \
246+ "for GTG to save your tasks."),\
247+ }
248+
249+ #parameters to configure a new backend of this type.
250+ #NOTE: should we always give back a different default filename? it can be
251+ # done, but I'd like to keep this backend simple, so that it can be
252+ # used as example (invernizzi)
253+ _static_parameters = { \
254+ "path": { \
255+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING, \
256+ GenericBackend.PARAM_DEFAULT_VALUE: \
257+ os.path.join(DEFAULT_PATH, "gtg_tasks-%s.xml" %(uuid.uuid4()))
258+ }}
259+
260+ def _get_default_filename_path(self, filename = None):
261+ '''
262+ Generates a default path with a random filename
263+ @param filename: specify a filename
264+ '''
265+ if not filename:
266+ filename = "gtg_tasks-%s.xml" % (uuid.uuid4())
267+ return os.path.join(self.DEFAULT_PATH, filename)
268+
269+ def __init__(self, parameters):
270+ """
271+ Instantiates a new backend.
272+
273+ @param parameters: should match the dictionary returned in
274+ get_parameters. Anyway, the backend should care if one expected
275+ value is None or does not exist in the dictionary.
276+ @firstrun: only needed for the default backend. It should be
277+ omitted for all other backends.
278+ """
279+ super(Backend, self).__init__(parameters)
280+ self.tids = []
281+ #####RETROCOMPATIBILIY
282+ #NOTE: retrocompatibility. We convert "filename" to "path"
283+ # and we forget about "filename"
284+ if "need_conversion" in parameters:
285+ parameters["path"] = os.path.join(self.DEFAULT_PATH, \
286+ parameters["need_conversion"])
287+ del parameters["need_conversion"]
288+ if not self.KEY_DEFAULT_BACKEND in parameters:
289+ parameters[self.KEY_DEFAULT_BACKEND] = True
290+ ####
291+ self.doc, self.xmlproj = cleanxml.openxmlfile( \
292+ self._parameters["path"], "project")
293+
294+ def initialize(self):
295+ super(Backend, self).initialize()
296+ self.doc, self.xmlproj = cleanxml.openxmlfile( \
297+ self._parameters["path"], "project")
298+
299+ def this_is_the_first_run(self, xml):
300+ #Create the default tasks for the first run.
301+ #We write the XML object in a file
302+ self._parameters[self.KEY_DEFAULT_BACKEND] = True
303+ cleanxml.savexml(self._parameters["path"], xml)
304+ self.doc, self.xmlproj = cleanxml.openxmlfile(\
305+ self._parameters["path"], "project")
306+ self._parameters[self.KEY_DEFAULT_BACKEND] = True
307+
308+ def start_get_tasks(self):
309+ '''
310+ Once this function is launched, the backend can start pushing
311+ tasks to gtg parameters.
312+
313+ @return: start_get_tasks() might not return or finish
314+ '''
315+ tid_list = []
316+ for node in self.xmlproj.childNodes:
317+ tid = node.getAttribute("id")
318+ if tid not in self.tids:
319+ self.tids.append(tid)
320+ task = self.datastore.task_factory(tid)
321+ if task:
322+ task = taskxml.task_from_xml(task, node)
323+ self.datastore.push_task(task)
324+
325+ def set_task(self, task):
326+ tid = task.get_id()
327+ existing = None
328+ #First, we find the existing task from the treenode
329+ for node in self.xmlproj.childNodes:
330+ if node.getAttribute("id") == tid:
331+ existing = node
332+ t_xml = taskxml.task_to_xml(self.doc, task)
333+ modified = False
334+ #We then replace the existing node
335+ if existing and t_xml:
336+ #We will write only if the task has changed
337+ if t_xml.toxml() != existing.toxml():
338+ self.xmlproj.replaceChild(t_xml, existing)
339+ modified = True
340+ #If the node doesn't exist, we create it
341+ # (it might not be the case in all backends
342+ else:
343+ self.xmlproj.appendChild(t_xml)
344+ modified = True
345+ #In this particular backend, we write all the tasks
346+ #This is inherent to the XML file backend
347+ if modified and self._parameters["path"] and self.doc :
348+ cleanxml.savexml(self._parameters["path"], self.doc)
349+
350+ def remove_task(self, tid):
351+ ''' Completely remove the task with ID = tid '''
352+ for node in self.xmlproj.childNodes:
353+ if node.getAttribute("id") == tid:
354+ self.xmlproj.removeChild(node)
355+ if tid in self.tids:
356+ self.tids.remove(tid)
357+ cleanxml.savexml(self._parameters["path"], self.doc)
358+
359+
360+ def quit(self, disable = False):
361+ '''
362+ Called when GTG quits or disconnects the backend.
363+ '''
364+ super(Backend, self).quit(disable)
365+
366+ def save_state(self):
367+ cleanxml.savexml(self._parameters["path"], self.doc, backup=True)
368+
369+ def get_number_of_tasks(self):
370+ '''
371+ Returns the number of tasks stored in the backend. Doesn't need to be a
372+ fast function, is called just for the UI
373+ '''
374+ return len(self.tids)
375
376=== added file 'GTG/backends/backendsignals.py'
377--- GTG/backends/backendsignals.py 1970-01-01 00:00:00 +0000
378+++ GTG/backends/backendsignals.py 2010-06-23 01:06:26 +0000
379@@ -0,0 +1,125 @@
380+# -*- coding: utf-8 -*-
381+# -----------------------------------------------------------------------------
382+# Getting Things Gnome! - a personal organizer for the GNOME desktop
383+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
384+#
385+# This program is free software: you can redistribute it and/or modify it under
386+# the terms of the GNU General Public License as published by the Free Software
387+# Foundation, either version 3 of the License, or (at your option) any later
388+# version.
389+#
390+# This program is distributed in the hope that it will be useful, but WITHOUT
391+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
392+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
393+# details.
394+#
395+# You should have received a copy of the GNU General Public License along with
396+# this program. If not, see <http://www.gnu.org/licenses/>.
397+# -----------------------------------------------------------------------------
398+
399+import gobject
400+
401+from GTG.tools.borg import Borg
402+
403+
404+
405+class BackendSignals(Borg):
406+ '''
407+ This class handles the signals that involve backends.
408+ In particular, it's a wrapper Borg class around a _BackendSignalsGObject
409+ class, and all method of the wrapped class can be used as if they were part
410+ of this class
411+ '''
412+
413+ #error codes to send along with the BACKEND_FAILED signal
414+ ERRNO_AUTHENTICATION = "authentication failed"
415+ ERRNO_NETWORK = "network is down"
416+ ERRNO_DBUS = "Dbus interface cannot be connected"
417+
418+ def __init__(self):
419+ super(BackendSignals, self).__init__()
420+ if hasattr(self, "_gobject"):
421+ return
422+ self._gobject = _BackendSignalsGObject()
423+
424+ def __getattr__(self, attr):
425+ return getattr(self._gobject, attr)
426+
427+
428+class _BackendSignalsGObject(gobject.GObject):
429+
430+ #signal name constants
431+ BACKEND_STATE_TOGGLED = 'backend-state-toggled' #emitted when a
432+ #backend is
433+ #enabled or disabled
434+ BACKEND_RENAMED = 'backend-renamed' #emitted when a backend is renamed
435+ BACKEND_ADDED = 'backend-added'
436+ BACKEND_REMOVED = 'backend-added' #when a backend is deleted
437+ DEFAULT_BACKEND_LOADED = 'default-backend-loaded' #emitted after all
438+ # tasks have been
439+ # loaded from the
440+ # default backend
441+ BACKEND_FAILED = 'backend-failed' #something went wrong with a backend
442+ BACKEND_SYNC_STARTED = 'backend-sync-started'
443+ BACKEND_SYNC_ENDED = 'backend-sync-ended'
444+
445+ __string_signal__ = (gobject.SIGNAL_RUN_FIRST, \
446+ gobject.TYPE_NONE, (str, ))
447+ __none_signal__ = (gobject.SIGNAL_RUN_FIRST, \
448+ gobject.TYPE_NONE, ( ))
449+ __string_string_signal__ = (gobject.SIGNAL_RUN_FIRST, \
450+ gobject.TYPE_NONE, (str, str, ))
451+
452+ __gsignals__ = {BACKEND_STATE_TOGGLED : __string_signal__, \
453+ BACKEND_RENAMED : __string_signal__, \
454+ BACKEND_ADDED : __string_signal__, \
455+ BACKEND_REMOVED : __string_signal__, \
456+ BACKEND_SYNC_STARTED : __string_signal__, \
457+ BACKEND_SYNC_ENDED : __string_signal__, \
458+ DEFAULT_BACKEND_LOADED: __none_signal__, \
459+ BACKEND_FAILED : __string_string_signal__}
460+
461+ def __init__(self):
462+ super(_BackendSignalsGObject, self).__init__()
463+ self.backends_currently_syncing = []
464+
465+ ############# Signals #########
466+ #connecting to signals is fine, but keep an eye if you should emit them.
467+ #As a general rule, signals should only be emitted in the GenericBackend
468+ #class
469+
470+ def _emit_signal(self, signal, backend_id):
471+ gobject.idle_add(self.emit, signal, backend_id)
472+
473+ def backend_state_changed(self, backend_id):
474+ self._emit_signal(self.BACKEND_STATE_TOGGLED, backend_id)
475+
476+ def backend_renamed(self, backend_id):
477+ self._emit_signal(self.BACKEND_RENAMED, backend_id)
478+
479+ def backend_added(self, backend_id):
480+ self._emit_signal(self.BACKEND_ADDED, backend_id)
481+
482+ def backend_removed(self, backend_id):
483+ self._emit_signal(self.BACKEND_REMOVED, backend_id)
484+
485+ def default_backend_loaded(self):
486+ gobject.idle_add(self.emit, self.DEFAULT_BACKEND_LOADED)
487+
488+ def backend_failed(self, backend_id, error_code):
489+ gobject.idle_add(self.emit, self.BACKEND_FAILED, backend_id, \
490+ error_code)
491+
492+ def backend_sync_started(self, backend_id):
493+ self._emit_signal(self.BACKEND_SYNC_STARTED, backend_id)
494+ self.backends_currently_syncing.append(backend_id)
495+
496+ def backend_sync_ended(self, backend_id):
497+ self._emit_signal(self.BACKEND_SYNC_ENDED, backend_id)
498+ try:
499+ self.backends_currently_syncing.remove(backend_id)
500+ except:
501+ pass
502+
503+ def is_backend_syncing(self, backend_id):
504+ return backend_id in self.backends_currently_syncing
505
506=== added file 'GTG/backends/genericbackend.py'
507--- GTG/backends/genericbackend.py 1970-01-01 00:00:00 +0000
508+++ GTG/backends/genericbackend.py 2010-06-23 01:06:26 +0000
509@@ -0,0 +1,571 @@
510+# -*- coding: utf-8 -*-
511+# -----------------------------------------------------------------------------
512+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
513+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
514+#
515+# This program is free software: you can redistribute it and/or modify it under
516+# the terms of the GNU General Public License as published by the Free Software
517+# Foundation, either version 3 of the License, or (at your option) any later
518+# version.
519+#
520+# This program is distributed in the hope that it will be useful, but WITHOUT
521+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
522+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
523+# details.
524+#
525+# You should have received a copy of the GNU General Public License along with
526+# this program. If not, see <http://www.gnu.org/licenses/>.
527+# -----------------------------------------------------------------------------
528+
529+'''
530+FIXME: document!
531+'''
532+
533+import os
534+import sys
535+import errno
536+import pickle
537+import threading
538+from collections import deque
539+
540+from GTG.backends.backendsignals import BackendSignals
541+from GTG.tools.keyring import Keyring
542+from GTG.core import CoreConfig
543+from GTG.tools.logger import Log
544+
545+
546+
547+
548+class GenericBackend(object):
549+ '''
550+ Base class for every backend. It's a little more than an interface which
551+ methods have to be redefined in order for the backend to run.
552+ '''
553+
554+
555+ #BACKEND TYPE DESCRIPTION
556+ #"_general_description" is a dictionary that holds the values for the
557+ # following keys:
558+ BACKEND_NAME = "name" #the backend gtg internal name (doesn't change in
559+ # translations, *must be unique*)
560+ BACKEND_HUMAN_NAME = "human-friendly-name" #The name shown to the user
561+ BACKEND_DESCRIPTION = "description" #A short description of the backend
562+ BACKEND_AUTHORS = "authors" #a list of strings
563+ BACKEND_TYPE = "type"
564+ #BACKEND_TYPE is one of:
565+ TYPE_READWRITE = "readwrite"
566+ TYPE_READONLY = "readonly"
567+ TYPE_IMPORT = "import"
568+ TYPE_EXPORT = "export"
569+ _general_description = {}
570+
571+
572+ #"static_parameters" is a dictionary of dictionaries, each of which
573+ #representing a parameter needed to configure the backend.
574+ #each "sub-dictionary" is identified by this a key representing its name.
575+ #"static_parameters" will be part of the definition of each
576+ #particular backend.
577+ # Each dictionary contains the keys:
578+ #PARAM_DESCRIPTION = "description" #short description (shown to the user
579+ # during configuration)
580+ PARAM_DEFAULT_VALUE = "default_value" # its default value
581+ PARAM_TYPE = "type"
582+ #PARAM_TYPE is one of the following (changing this changes the way
583+ # the user can configure the parameter)
584+ TYPE_PASSWORD = "password" #the real password is stored in the GNOME
585+ # keyring
586+ # This is just a key to find it there
587+ TYPE_STRING = "string" #generic string, nothing fancy is done
588+ TYPE_INT = "int" #edit box can contain only integers
589+ TYPE_BOOL = "bool" #checkbox is shown
590+ TYPE_LIST_OF_STRINGS = "liststring" #list of strings. the "," character is
591+ # prohibited in strings
592+ _static_parameters = {}
593+
594+ def initialize(self):
595+ '''
596+ Called each time it is enabled again (including on backend creation).
597+ Please note that a class instance for each disabled backend *is*
598+ created, but it's not initialized.
599+ Optional.
600+ NOTE: make sure to call super().initialize()
601+ '''
602+ for module_name in self.get_required_modules():
603+ sys.modules[module_name]= __import__(module_name)
604+ self._parameters[self.KEY_ENABLED] = True
605+ self._is_initialized = True
606+ #we signal that the backend has been enabled
607+ self._signal_manager.backend_state_changed(self.get_id())
608+
609+ def start_get_tasks(self):
610+ '''
611+ Once this function is launched, the backend can start pushing
612+ tasks to gtg parameters.
613+
614+ @return: start_get_tasks() might not return or finish
615+ '''
616+ raise NotImplemented()
617+
618+ def set_task(self, task):
619+ '''
620+ Save the task in the backend. If the task id is new for the
621+ backend, then a new task must be created.
622+ '''
623+ pass
624+
625+ def remove_task(self, tid):
626+ ''' Completely remove the task with ID = tid '''
627+ pass
628+
629+ def has_task(self, tid):
630+ '''Returns true if the backend has an internal idea
631+ of the task corresponding to the tid. False otherwise'''
632+ raise NotImplemented()
633+
634+ def new_task_id(self):
635+ '''
636+ Returns an available ID for a new task so that a task with this ID
637+ can be saved with set_task later.
638+ '''
639+ raise NotImplemented()
640+
641+ def this_is_the_first_run(self, xml):
642+ '''
643+ Steps to execute if it's the first time the backend is run. Optional.
644+ '''
645+ pass
646+
647+ def purge(self):
648+ '''
649+ Called when a backend will be removed from GTG. Useful for removing
650+ configuration files. Optional.
651+ '''
652+ pass
653+
654+ def get_number_of_tasks(self):
655+ '''
656+ Returns the number of tasks stored in the backend. Doesn't need to be a
657+ fast function, is called just for the UI
658+ '''
659+ raise NotImplemented()
660+
661+ @staticmethod
662+ def get_required_modules():
663+ return []
664+
665+ def quit(self, disable = False):
666+ '''
667+ Called when GTG quits or disconnects the backend. Remember to execute
668+ also this function when quitting. If disable is True, the backend won't
669+ be automatically loaded at next GTG start
670+ '''
671+ self._is_initialized = False
672+ if disable:
673+ self._parameters[self.KEY_ENABLED] = False
674+ #we signal that we have been disabled
675+ self._signal_manager.backend_state_changed(self.get_id())
676+ self._signal_manager.backend_sync_ended(self.get_id())
677+ syncing_thread = threading.Thread(target = self.sync).run()
678+
679+ def save_state(self):
680+ '''
681+ It's the last function executed on a quitting backend, after the
682+ pending actions have been done.
683+ Useful to ensure that the state is saved in a consistent manner
684+ '''
685+ pass
686+
687+###############################################################################
688+###### You don't need to reimplement the functions below this line ############
689+###############################################################################
690+
691+ #These parameters are common to all backends and necessary.
692+ # They will be added automatically to your _static_parameters list
693+ #NOTE: for now I'm disabling changing the default backend. Once it's all
694+ # set up, we will see about that (invernizzi)
695+ KEY_DEFAULT_BACKEND = "Default"
696+ KEY_ENABLED = "Enabled"
697+ KEY_HUMAN_NAME = BACKEND_HUMAN_NAME
698+ KEY_ATTACHED_TAGS = "attached-tags"
699+ KEY_USER = "user"
700+ KEY_PID = "pid"
701+ ALLTASKS_TAG = "gtg-tags-all" #IXME: moved here to avoid circular imports
702+
703+ _static_parameters_obligatory = { \
704+ KEY_DEFAULT_BACKEND: { \
705+ PARAM_TYPE: TYPE_BOOL, \
706+ PARAM_DEFAULT_VALUE: False, \
707+ }, \
708+ KEY_HUMAN_NAME: { \
709+ PARAM_TYPE: TYPE_STRING, \
710+ PARAM_DEFAULT_VALUE: "", \
711+ }, \
712+ KEY_USER: { \
713+ PARAM_TYPE: TYPE_STRING, \
714+ PARAM_DEFAULT_VALUE: "", \
715+ }, \
716+ KEY_PID: { \
717+ PARAM_TYPE: TYPE_STRING, \
718+ PARAM_DEFAULT_VALUE: "", \
719+ }, \
720+ KEY_ENABLED: { \
721+ PARAM_TYPE: TYPE_BOOL, \
722+ PARAM_DEFAULT_VALUE: False, \
723+ }}
724+
725+ _static_parameters_obligatory_for_rw = { \
726+ KEY_ATTACHED_TAGS: {\
727+ PARAM_TYPE: TYPE_LIST_OF_STRINGS, \
728+ PARAM_DEFAULT_VALUE: [ALLTASKS_TAG], \
729+ }}
730+
731+ #Handy dictionary used in type conversion (from string to type)
732+ _type_converter = {TYPE_STRING: str,
733+ TYPE_INT: int,
734+ }
735+
736+ @classmethod
737+ def _get_static_parameters(cls):
738+ '''
739+ Helper method, used to obtain the full list of the static_parameters
740+ (user configured and default ones)
741+ '''
742+ if hasattr(cls, "_static_parameters"):
743+ temp_dic = cls._static_parameters_obligatory.copy()
744+ if cls._general_description[cls.BACKEND_TYPE] == cls.TYPE_READWRITE:
745+ for key, value in \
746+ cls._static_parameters_obligatory_for_rw.iteritems():
747+ temp_dic[key] = value
748+ for key, value in cls._static_parameters.iteritems():
749+ temp_dic[key] = value
750+ return temp_dic
751+ else:
752+ raise NotImplemented("_static_parameters not implemented for " + \
753+ "backend %s" % type(cls))
754+
755+ def __init__(self, parameters):
756+ """
757+ Instantiates a new backend. Please note that this is called also for
758+ disabled backends. Those are not initialized, so you might want to check
759+ out the initialize() function.
760+ """
761+ if self.KEY_DEFAULT_BACKEND not in parameters:
762+ parameters[self.KEY_DEFAULT_BACKEND] = True
763+ if parameters[self.KEY_DEFAULT_BACKEND] or \
764+ (not self.KEY_ATTACHED_TAGS in parameters and \
765+ self._general_description[self.BACKEND_TYPE] \
766+ == self.TYPE_READWRITE):
767+ parameters[self.KEY_ATTACHED_TAGS] = [self.ALLTASKS_TAG]
768+ self._parameters = parameters
769+ self._signal_manager = BackendSignals()
770+ self._is_initialized = False
771+ if Log.is_debugging_mode():
772+ self.timer_timestep = 5
773+ else:
774+ self.timer_timestep = 1
775+ self.to_set_timer = None
776+ self.please_quit = False
777+ self.to_set = deque()
778+ self.to_remove = deque()
779+
780+ def get_attached_tags(self):
781+ '''
782+ Returns the list of tags which are handled by this backend
783+ '''
784+ if hasattr(self._parameters, self.KEY_DEFAULT_BACKEND) and \
785+ self._parameters[self.KEY_DEFAULT_BACKEND]:
786+ return [self.ALLTASKS_TAG]
787+ try:
788+ return self._parameters[self.KEY_ATTACHED_TAGS]
789+ except:
790+ return []
791+
792+ def set_attached_tags(self, tags):
793+ '''
794+ Changes the set of attached tags
795+ '''
796+ self._parameters[self.KEY_ATTACHED_TAGS] = tags
797+
798+ @classmethod
799+ def get_static_parameters(cls):
800+ """
801+ Returns a dictionary of parameters necessary to create a backend.
802+ """
803+ return cls._get_static_parameters()
804+
805+ def get_parameters(self):
806+ """
807+ Returns a dictionary of the current parameters.
808+ """
809+ return self._parameters
810+
811+ def set_parameter(self, parameter, value):
812+ self._parameters[parameter] = value
813+
814+ @classmethod
815+ def get_name(cls):
816+ """
817+ Returns the name of the backend as it should be displayed in the UI
818+ """
819+ return cls._get_from_general_description(cls.BACKEND_NAME)
820+
821+ @classmethod
822+ def get_description(cls):
823+ """Returns a description of the backend"""
824+ return cls._get_from_general_description(cls.BACKEND_DESCRIPTION)
825+
826+ @classmethod
827+ def get_type(cls):
828+ """Returns the backend type(readonly, r/w, import, export) """
829+ return cls._get_from_general_description(cls.BACKEND_TYPE)
830+
831+ @classmethod
832+ def get_authors(cls):
833+ '''
834+ returns the backend author(s)
835+ '''
836+ return cls._get_from_general_description(cls.BACKEND_AUTHORS)
837+
838+ @classmethod
839+ def _get_from_general_description(cls, key):
840+ '''
841+ Helper method to extract values from cls._general_description.
842+ Raises an exception if the key is missing (helpful for developers
843+ adding new backends).
844+ '''
845+ if key in cls._general_description:
846+ return cls._general_description[key]
847+ else:
848+ raise NotImplemented("Key %s is missing from " +\
849+ "'self._general_description' of a backend (%s). " +
850+ "Please add the corresponding value" % (key, type(cls)))
851+
852+ @classmethod
853+ def cast_param_type_from_string(cls, param_value, param_type):
854+ '''
855+ Parameters are saved in a text format, so we have to cast them to the
856+ appropriate type on loading. This function does exactly that.
857+ '''
858+ #FIXME: we could use pickle (dumps and loads), at least in some cases
859+ # (invernizzi)
860+ if param_type in cls._type_converter:
861+ return cls._type_converter[param_type](param_value)
862+ elif param_type == cls.TYPE_BOOL:
863+ if param_value == "True":
864+ return True
865+ elif param_value == "False":
866+ return False
867+ else:
868+ raise Exception("Unrecognized bool value '%s'" %
869+ param_type)
870+ elif param_type == cls.TYPE_PASSWORD:
871+ if param_value == -1:
872+ return None
873+ return Keyring().get_password(int(param_value))
874+ elif param_type == cls.TYPE_LIST_OF_STRINGS:
875+ the_list = param_value.split(",")
876+ if not isinstance(the_list, list):
877+ the_list = [the_list]
878+ return the_list
879+ else:
880+ raise NotImplemented("I don't know what type is '%s'" %
881+ param_type)
882+
883+ def cast_param_type_to_string(self, param_type, param_value):
884+ '''
885+ Inverse of cast_param_type_from_string
886+ '''
887+ if param_type == GenericBackend.TYPE_PASSWORD:
888+ if param_value == None:
889+ return str(-1)
890+ else:
891+ return str(Keyring().set_password(
892+ "GTG stored password -" + self.get_id(), param_value))
893+ elif param_type == GenericBackend.TYPE_LIST_OF_STRINGS:
894+ if param_value == []:
895+ return ""
896+ return reduce(lambda a, b: a + "," + b, param_value)
897+ else:
898+ return str(param_value)
899+
900+ def get_id(self):
901+ '''
902+ returns the backends id, used in the datastore for indexing backends
903+ '''
904+ return self.get_name() + "@" + self._parameters["pid"]
905+
906+ @classmethod
907+ def get_human_default_name(cls):
908+ '''
909+ returns the user friendly default backend name.
910+ '''
911+ return cls._general_description[cls.BACKEND_HUMAN_NAME]
912+
913+ def get_human_name(self):
914+ '''
915+ returns the user customized backend name. If the user hasn't
916+ customized it, returns the default one
917+ '''
918+ if self.KEY_HUMAN_NAME in self._parameters and \
919+ self._parameters[self.KEY_HUMAN_NAME] != "":
920+ return self._parameters[self.KEY_HUMAN_NAME]
921+ else:
922+ return self.get_human_default_name()
923+
924+ def set_human_name(self, name):
925+ '''
926+ sets a custom name for the backend
927+ '''
928+ self._parameters[self.KEY_HUMAN_NAME] = name
929+ #we signal the change
930+ self._signal_manager.backend_renamed(self.get_id())
931+
932+ def is_enabled(self):
933+ '''
934+ Returns if the backend is enabled
935+ '''
936+ return self.get_parameters()[GenericBackend.KEY_ENABLED] or \
937+ self.is_default()
938+
939+ def is_default(self):
940+ '''
941+ Returns if the backend is enabled
942+ '''
943+ return self.get_parameters()[GenericBackend.KEY_DEFAULT_BACKEND]
944+
945+ def is_initialized(self):
946+ '''
947+ Returns if the backend is up and running
948+ '''
949+ return self._is_initialized
950+
951+ def get_parameter_type(self, param_name):
952+ try:
953+ return self.get_static_parameters()[param_name][self.PARAM_TYPE]
954+ except KeyError:
955+ return None
956+
957+ def register_datastore(self, datastore):
958+ self.datastore = datastore
959+
960+###############################################################################
961+### HELPER FUNCTIONS ##########################################################
962+###############################################################################
963+
964+ def _store_pickled_file(self, path, data):
965+ '''
966+ A helper function to save some object in a file.
967+ @param path: a relative path. A good choice is
968+ "backend_name/object_name"
969+ @param data: the object
970+ '''
971+ path = os.path.join(CoreConfig().get_data_dir(), path)
972+ #mkdir -p
973+ try:
974+ os.makedirs(os.path.dirname(path))
975+ except OSError, exception:
976+ if exception.errno != errno.EEXIST:
977+ raise
978+ #saving
979+ #try:
980+ with open(path, 'wb') as file:
981+ pickle.dump(data, file)
982+ #except pickle.PickleError:
983+ #pass
984+
985+ def _load_pickled_file(self, path, default_value = None):
986+ '''
987+ A helper function to load some object from a file.
988+ @param path: the relative path of the file
989+ @param default_value: the value to return if the file is missing or
990+ corrupt
991+ @returns object: the needed object, or default_value
992+ '''
993+ path = os.path.join(CoreConfig().get_data_dir(), path)
994+ if not os.path.exists(path):
995+ return default_value
996+ else:
997+ try:
998+ with open(path, 'r') as file:
999+ return pickle.load(file)
1000+ except pickle.PickleError:
1001+ return default_value
1002+
1003+###############################################################################
1004+### THREADING #################################################################
1005+###############################################################################
1006+
1007+ def __try_launch_setting_thread(self):
1008+ '''
1009+ Helper function to launch the setting thread, if it's not running.
1010+ '''
1011+ if self.to_set_timer == None and self.is_enabled():
1012+ self.to_set_timer = threading.Timer(self.timer_timestep, \
1013+ self.launch_setting_thread)
1014+ self.to_set_timer.start()
1015+
1016+ def launch_setting_thread(self):
1017+ '''
1018+ This function is launched as a separate thread. Its job is to perform
1019+ the changes that have been issued from GTG core. In particular, for
1020+ each task in the self.to_set queue, a task has to be modified or to be
1021+ created (if the tid is new), and for each task in the self.to_remove
1022+ queue, a task has to be deleted
1023+ '''
1024+ while not self.please_quit:
1025+ try:
1026+ task = self.to_set.pop()
1027+ except IndexError:
1028+ break
1029+ #time.sleep(4)
1030+ tid = task.get_id()
1031+ if tid not in self.to_remove:
1032+ self.set_task(task)
1033+
1034+ while not self.please_quit:
1035+ try:
1036+ tid = self.to_remove.pop()
1037+ except IndexError:
1038+ break
1039+ self.remove_task(tid)
1040+ #we release the weak lock
1041+ self.to_set_timer = None
1042+
1043+ def queue_set_task(self, task):
1044+ ''' Save the task in the backend. '''
1045+ tid = task.get_id()
1046+ if task not in self.to_set and tid not in self.to_remove:
1047+ self.to_set.appendleft(task)
1048+ self.__try_launch_setting_thread()
1049+
1050+ def queue_remove_task(self, tid):
1051+ '''
1052+ Queues task to be removed.
1053+ @param tid: The Task ID of the task to be removed
1054+ '''
1055+ if tid not in self.to_remove:
1056+ self.to_remove.appendleft(tid)
1057+ self.__try_launch_setting_thread()
1058+ return None
1059+
1060+ def sync(self):
1061+ '''
1062+ Helper method. Forces the backend to perform all the pending changes.
1063+ It is usually called upon quitting the backend.
1064+ '''
1065+ #FIXME: this function should become part of the r/w r/o generic class
1066+ # for backends
1067+ if self.to_set_timer != None:
1068+ self.please_quit = True
1069+ try:
1070+ self.to_set_timer.cancel()
1071+ except:
1072+ pass
1073+ try:
1074+ self.to_set_timer.join(5)
1075+ except:
1076+ pass
1077+ self.please_quit = False
1078+ self.launch_setting_thread()
1079+ self.save_state()
1080+
1081
1082=== removed file 'GTG/backends/localfile.py'
1083--- GTG/backends/localfile.py 2010-04-26 23:12:57 +0000
1084+++ GTG/backends/localfile.py 1970-01-01 00:00:00 +0000
1085@@ -1,176 +0,0 @@
1086-# -*- coding: utf-8 -*-
1087-# -----------------------------------------------------------------------------
1088-# Gettings Things Gnome! - a personal organizer for the GNOME desktop
1089-# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
1090-#
1091-# This program is free software: you can redistribute it and/or modify it under
1092-# the terms of the GNU General Public License as published by the Free Software
1093-# Foundation, either version 3 of the License, or (at your option) any later
1094-# version.
1095-#
1096-# This program is distributed in the hope that it will be useful, but WITHOUT
1097-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
1098-# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
1099-# details.
1100-#
1101-# You should have received a copy of the GNU General Public License along with
1102-# this program. If not, see <http://www.gnu.org/licenses/>.
1103-# -----------------------------------------------------------------------------
1104-
1105-'''
1106-Localfile is a read/write backend that will store your tasks in an XML file
1107-This file will be in your $XDG_DATA_DIR/gtg folder.
1108-'''
1109-
1110-import os
1111-import uuid
1112-
1113-from GTG.core import CoreConfig
1114-from GTG.tools import cleanxml, taskxml
1115-
1116-def get_name():
1117- """Returns the name of the backend as it should be displayed in the UI"""
1118- return "Local File"
1119-
1120-def get_description():
1121- """Returns a description of the backend"""
1122- return "Your tasks are saved in an XML file located in your HOME folder"
1123-
1124-def get_parameters():
1125- """
1126- Returns a dictionary of parameters. Keys should be strings and
1127- are the name of the parameter.
1128- Values are string with value : string, password, int, bool
1129- and are an information about the type of the parameter
1130- Currently, only string is supported.
1131- """
1132- dic = {}
1133- dic["filename"] = "string"
1134- return dic
1135-
1136-def get_features():
1137- """Returns a dict of features supported by this backend"""
1138- return {}
1139-
1140-def get_type():
1141- """Type is one of : readwrite, readonly, import, export"""
1142- return "readwrite"
1143-
1144-class Backend:
1145- def __init__(self, parameters, firstrunxml=None):
1146- """
1147- Instantiates a new backend.
1148-
1149- @param parameters: should match the dictionary returned in
1150- get_parameters. Anyway, the backend should care if one expected value is
1151- None or does not exist in the dictionary.
1152- @firstrun: only needed for the default backend. It should be omitted for
1153- all other backends.
1154- """
1155- self.tids = []
1156- self.pid = 1
1157- if "filename" in parameters:
1158- zefile = parameters["filename"]
1159- #If zefile is None, we create a new file
1160- else:
1161- zefile = "%s.xml" %(uuid.uuid4())
1162- parameters["filename"] = zefile
1163- #For the day we want to open files somewhere else
1164- default_folder = True
1165- if default_folder:
1166- self.zefile = os.path.join(CoreConfig.DATA_DIR, zefile)
1167- self.filename = zefile
1168- else:
1169- self.zefile = zefile
1170- self.filename = zefile
1171- #Create the default tasks for the first run.
1172- #We write the XML object in a file
1173- if firstrunxml and not os.path.exists(zefile):
1174- #shutil.copy(firstrunfile,self.zefile)
1175- cleanxml.savexml(self.zefile, firstrunxml)
1176- self.doc, self.xmlproj = cleanxml.openxmlfile(self.zefile, "project")
1177-
1178- def start_get_tasks(self,push_task_func,task_factory_func):
1179- '''
1180- Once this function is launched, the backend can start pushing
1181- tasks to gtg parameters.
1182-
1183- @push_task_func: a function that takes a Task as parameter
1184- and pushes it into GTG.
1185- @task_factory_func: a function that takes a tid as parameter
1186- and returns a Task object with the given pid.
1187-
1188- @return: start_get_tasks() might not return or finish
1189- '''
1190- tid_list = []
1191- for node in self.xmlproj.childNodes:
1192- #time.sleep(2)
1193- tid = node.getAttribute("id")
1194- if tid not in self.tids:
1195- self.tids.append(tid)
1196- task = task_factory_func(tid)
1197- task = taskxml.task_from_xml(task,node)
1198- push_task_func(task)
1199- #print "#### finishing pushing tasks"
1200-
1201- def set_task(self, task):
1202- ''' Save the task in the backend '''
1203- #time.sleep(4)
1204- tid = task.get_id()
1205- if tid not in self.tids:
1206- self.tids.append(tid)
1207- existing = None
1208- #First, we find the existing task from the treenode
1209- for node in self.xmlproj.childNodes:
1210- if node.getAttribute("id") == tid:
1211- existing = node
1212- t_xml = taskxml.task_to_xml(self.doc, task)
1213- modified = False
1214- #We then replace the existing node
1215- if existing and t_xml:
1216- #We will write only if the task has changed
1217- if t_xml.toxml() != existing.toxml():
1218- self.xmlproj.replaceChild(t_xml, existing)
1219- modified = True
1220- #If the node doesn't exist, we create it
1221- # (it might not be the case in all backends
1222- else:
1223- self.xmlproj.appendChild(t_xml)
1224- modified = True
1225- #In this particular backend, we write all the tasks
1226- #This is inherent to the XML file backend
1227- if modified and self.zefile and self.doc :
1228- cleanxml.savexml(self.zefile, self.doc)
1229- return None
1230-
1231- def remove_task(self, tid):
1232- ''' Completely remove the task with ID = tid '''
1233- for node in self.xmlproj.childNodes:
1234- if node.getAttribute("id") == tid:
1235- self.xmlproj.removeChild(node)
1236- if tid in self.tids:
1237- self.tids.remove(tid)
1238- cleanxml.savexml(self.zefile, self.doc)
1239-
1240- def new_task_id(self):
1241- '''
1242- Returns an available ID for a new task so that a task with this ID
1243- can be saved with set_task later.
1244- If None, then GTG will create a new ID by itself.
1245- The ID cannot contain the character "@".
1246- '''
1247- k = 0
1248- pid = self.pid
1249- newid = "%s@%s" %(k, pid)
1250- while str(newid) in self.tids:
1251- k += 1
1252- newid = "%s@%s" %(k, pid)
1253- self.tids.append(newid)
1254- return newid
1255-
1256- def quit(self):
1257- '''
1258- Called when GTG quits or disconnects the backend.
1259- (Subclasses might pass here)
1260- '''
1261- cleanxml.savexml(self.zefile, self.doc, backup=True)
1262
1263=== modified file 'GTG/core/__init__.py'
1264--- GTG/core/__init__.py 2010-05-26 09:54:42 +0000
1265+++ GTG/core/__init__.py 2010-06-23 01:06:26 +0000
1266@@ -41,136 +41,80 @@
1267 #=== IMPORT ====================================================================
1268 import os
1269 from xdg.BaseDirectory import xdg_data_home, xdg_config_home
1270-from GTG.tools import cleanxml
1271 from configobj import ConfigObj
1272-
1273-from GTG.core import firstrun_tasks
1274-
1275-class CoreConfig:
1276+from GTG.tools.testingmode import TestingMode
1277+
1278+import GTG
1279+from GTG.tools.logger import Log
1280+from GTG.tools.borg import Borg
1281+
1282+
1283+
1284+class CoreConfig(Borg):
1285
1286+
1287 #The projects and tasks are of course DATA !
1288 #We then use XDG_DATA for them
1289 #Don't forget the "/" at the end.
1290- DATA_DIR = os.path.join(xdg_data_home,'gtg/')
1291 DATA_FILE = "projects.xml"
1292- CONF_DIR = os.path.join(xdg_config_home,'gtg/')
1293 CONF_FILE = "gtg.conf"
1294 TASK_CONF_FILE = "tasks.conf"
1295 conf_dict = None
1296 #DBUS
1297 BUSNAME = "org.GTG"
1298 BUSINTERFACE = "/org/GTG"
1299+ #TAGS
1300+ ALLTASKS_TAG = "gtg-tags-all"
1301
1302 def __init__(self):
1303- if not os.path.exists(self.CONF_DIR):
1304- os.makedirs(self.CONF_DIR)
1305- if not os.path.exists(self.DATA_DIR):
1306- os.makedirs(self.DATA_DIR)
1307- if not os.path.exists(self.CONF_DIR + self.CONF_FILE):
1308- f = open(self.CONF_DIR + self.CONF_FILE, "w")
1309- f.close()
1310- if not os.path.exists(self.CONF_DIR + self.TASK_CONF_FILE):
1311- f = open(self.CONF_DIR + self.TASK_CONF_FILE, "w")
1312- f.close()
1313- for file in [self.CONF_DIR + self.CONF_FILE,
1314- self.CONF_DIR + self.TASK_CONF_FILE]:
1315+ if hasattr(self, 'data_dir'):
1316+ #Borg has already been initialized
1317+ return
1318+ if TestingMode().get_testing_mode():
1319+ #we avoid running tests in the user data dir
1320+ self.data_dir = '/tmp/GTG_TESTS/data'
1321+ self.conf_dir = '/tmp/GTG_TESTS/conf'
1322+ else:
1323+ self.data_dir = os.path.join(xdg_data_home,'gtg/')
1324+ self.conf_dir = os.path.join(xdg_config_home,'gtg/')
1325+ if not os.path.exists(self.conf_dir):
1326+ os.makedirs(self.conf_dir)
1327+ if not os.path.exists(self.data_dir):
1328+ os.makedirs(self.data_dir)
1329+ if not os.path.exists(self.conf_dir + self.CONF_FILE):
1330+ f = open(self.conf_dir + self.CONF_FILE, "w")
1331+ f.close()
1332+ if not os.path.exists(self.conf_dir + self.TASK_CONF_FILE):
1333+ f = open(self.conf_dir + self.TASK_CONF_FILE, "w")
1334+ f.close()
1335+ for file in [self.conf_dir + self.CONF_FILE,
1336+ self.conf_dir + self.TASK_CONF_FILE]:
1337 if not ((file, os.R_OK) and os.access(file, os.W_OK)):
1338 raise Exception("File " + file + \
1339- " is a configuration file for gtg, but it " + \
1340+ " is a configuration file for gtg, but it "
1341 "cannot be read or written. Please check it")
1342- self.conf_dict = ConfigObj(self.CONF_DIR + self.CONF_FILE)
1343- self.task_conf_dict = ConfigObj(self.CONF_DIR + self.TASK_CONF_FILE)
1344+ self.conf_dict = ConfigObj(self.conf_dir + self.CONF_FILE)
1345+ self.task_conf_dict = ConfigObj(self.conf_dir + self.TASK_CONF_FILE)
1346
1347- def save_config(self):
1348+ def save(self):
1349+ ''' Saves the configuration of CoreConfig '''
1350 self.conf_dict.write()
1351 self.task_conf_dict.write()
1352-
1353- def get_backends_list(self):
1354- backend_fn = []
1355-
1356- # Check if config dir exists, if not create it
1357- if not os.path.exists(self.DATA_DIR):
1358- os.makedirs(self.DATA_DIR)
1359-
1360- # Read configuration file, if it does not exist, create one
1361- datafile = self.DATA_DIR + self.DATA_FILE
1362- doc, configxml = cleanxml.openxmlfile(datafile,"config") #pylint: disable-msg=W0612
1363- xmlproject = doc.getElementsByTagName("backend")
1364- # collect configred backends
1365- pid = 1
1366- for xp in xmlproject:
1367- dic = {}
1368- #We have some retrocompatibility code
1369- #A backend without the module attribute is pre-rev.105
1370- #and is considered as "filename"
1371- if xp.hasAttribute("module"):
1372- dic["module"] = str(xp.getAttribute("module"))
1373- dic["pid"] = str(xp.getAttribute("pid"))
1374- #The following "else" could be removed later
1375- else:
1376- dic["module"] = "localfile"
1377- dic["pid"] = str(pid)
1378-
1379- dic["xmlobject"] = xp
1380- pid += 1
1381- backend_fn.append(dic)
1382-
1383- firstrun = False
1384- #If no backend available, we create a new using localfile
1385- if len(backend_fn) == 0:
1386- dic = {}
1387- dic["module"] = "localfile"
1388- dic["pid"] = "1"
1389- backend_fn.append(dic)
1390- firstrun = True
1391-
1392- #Now that the backend list is build, we will construct them
1393- #Remember that b is a dictionnary
1394- for b in backend_fn:
1395- #We dynamically import modules needed
1396- module_name = "GTG.backends.%s"%b["module"]
1397- #FIXME : we should throw an error if the backend is not importable
1398- module = __import__(module_name)
1399- module = getattr(module, "backends")
1400- classobj = getattr(module, b["module"])
1401- b["parameters"] = classobj.get_parameters()
1402- #If creating the default backend, we don't have the xmlobject yet
1403- if "xmlobject" in b:
1404- xp = b.pop("xmlobject")
1405- #We will try to get the parameters
1406- for key in b["parameters"]:
1407- if xp.hasAttribute(key):
1408- b[key] = str(xp.getAttribute(key))
1409- if firstrun:
1410- frx = firstrun_tasks.populate()
1411- back = classobj.Backend(b,firstrunxml=frx)
1412- else:
1413- back = classobj.Backend(b)
1414- #We put the backend itself in the dic
1415- b["backend"] = back
1416-
1417- return backend_fn
1418-
1419-
1420- #If initial save, we don't close stuffs.
1421- def save_datastore(self,ds,initial_save=False):
1422- doc,xmlconfig = cleanxml.emptydoc("config")
1423- for b in ds.get_all_backends():
1424- param = b.get_parameters()
1425- t_xml = doc.createElement("backend")
1426- for key in param:
1427- #We dont want parameters,backend,xmlobject
1428- if key not in ["backend","parameters","xmlobject"]:
1429- t_xml.setAttribute(str(key),str(param[key]))
1430- #Saving all the projects at close
1431- xmlconfig.appendChild(t_xml)
1432- if not initial_save:
1433- b.quit()
1434-
1435- datafile = self.DATA_DIR + self.DATA_FILE
1436- cleanxml.savexml(datafile,doc,backup=True)
1437-
1438- #Saving the tagstore
1439- if not initial_save:
1440- ts = ds.get_tagstore()
1441- ts.save()
1442+
1443+ def get_icons_directories(self):
1444+ '''
1445+ Returns the directories containing the icons
1446+ '''
1447+ return [GTG.DATA_DIR, os.path.join(GTG.DATA_DIR, "icons")]
1448+
1449+ def get_data_dir(self):
1450+ return self.data_dir
1451+
1452+ def set_data_dir(self, path):
1453+ self.data_dir = path
1454+
1455+ def get_conf_dir(self):
1456+ return self.conf_dir
1457+
1458+ def set_conf_dir(self, path):
1459+ self.conf_dir = path
1460
1461=== modified file 'GTG/core/datastore.py'
1462--- GTG/core/datastore.py 2010-05-26 08:55:45 +0000
1463+++ GTG/core/datastore.py 2010-06-23 01:06:26 +0000
1464@@ -18,267 +18,560 @@
1465 # -----------------------------------------------------------------------------
1466
1467 """
1468-datastore contains a list of TagSource objects, which are proxies between a backend and the datastore itself
1469+The DaataStore contains a list of TagSource objects, which are proxies
1470+between a backend and the datastore itself
1471 """
1472
1473 import threading
1474-import gobject
1475-import time
1476-
1477-from GTG.core import tagstore, requester
1478-from GTG.core.task import Task
1479-from GTG.core.tree import Tree
1480-
1481-
1482-#Only the datastore should access to the backend
1483-DEFAULT_BACKEND = "1"
1484-#If you want to debug a backend, it can be useful to disable the threads
1485-#Currently, it's python threads (and not idle_add, which is not useful)
1486-THREADING = True
1487-
1488-
1489-class DataStore:
1490- """ A wrapper around a backend that provides an API for adding/removing tasks """
1491+import uuid
1492+import os.path
1493+from collections import deque
1494+
1495+from GTG.core import tagstore, requester
1496+from GTG.core.task import Task
1497+from GTG.core.tree import Tree
1498+from GTG.core import CoreConfig
1499+from GTG.tools.logger import Log
1500+from GTG.backends.genericbackend import GenericBackend
1501+from GTG.tools import cleanxml
1502+from GTG.tools.keyring import Keyring
1503+from GTG.backends.backendsignals import BackendSignals
1504+from GTG.tools.synchronized import synchronized
1505+from GTG.tools.borg import Borg
1506+
1507+
1508+class DataStore(object):
1509+ '''
1510+ A wrapper around all backends that is responsible for keeping the backend
1511+ instances. It can enable, disable, register and destroy backends, and acts
1512+ as interface between the backends and GTG core.
1513+ You should not interface yourself directly with the DataStore: use the
1514+ Requester instead (which also sends signals as you issue commands).
1515+ '''
1516+
1517+
1518 def __init__(self):
1519- """ Initializes a DataStore object """
1520- self.backends = {}
1521+ '''
1522+ Initializes a DataStore object
1523+ '''
1524+ self.backends = {} #dictionary {backend_name_string: Backend instance}
1525 self.open_tasks = Tree()
1526-# self.closed_tasks = Tree()
1527 self.requester = requester.Requester(self)
1528 self.tagstore = tagstore.TagStore(self.requester)
1529-
1530- def all_tasks(self):
1531- """
1532+ self._backend_signals = BackendSignals()
1533+ self.mutex = threading.RLock()
1534+ self.is_default_backend_loaded = False
1535+ self._backend_signals.connect('default-backend-loaded', \
1536+ self._activate_non_default_backends)
1537+ self.filtered_datastore = FilteredDataStore(self)
1538+
1539+ ##########################################################################
1540+ ### Helper functions (get_ methods for Datastore embedded objects)
1541+ ##########################################################################
1542+
1543+ def get_tagstore(self):
1544+ '''
1545+ Helper function to obtain the Tagstore associated with this DataStore
1546+ @return GTG.core.tagstore.TagStore: the tagstore object
1547+ '''
1548+ return self.tagstore
1549+
1550+ def get_requester(self):
1551+ '''
1552+ Helper function to get the Requester associate with this DataStore
1553+ @returns GTG.core.requester.Requester: the requester associated with
1554+ this datastore
1555+ '''
1556+ return self.requester
1557+
1558+ def get_tasks_tree(self):
1559+ '''
1560+ Helper function to get a Tree with all the tasks contained in this
1561+ Datastore
1562+ @returns GTG.core.tree.Tree: a task tree (the main one)
1563+ '''
1564+ return self.open_tasks
1565+
1566+ ##########################################################################
1567+ ### Tasks functions
1568+ ##########################################################################
1569+
1570+ def get_all_tasks(self):
1571+ '''
1572 Returns list of all keys of open tasks
1573- """
1574+ @return a list of strings: a list of task ids
1575+ '''
1576 return self.open_tasks.get_all_keys()
1577
1578 def has_task(self, tid):
1579- """
1580+ '''
1581 Returns true if the tid is among the open or closed tasks for
1582 this DataStore, False otherwise.
1583- param tid: Task ID to search for
1584- """
1585- return self.open_tasks.has_node(tid) #or self.closed_tasks.has_node(tid)
1586+ @param tid: Task ID to search for
1587+ @return bool: True if the task is present
1588+ '''
1589+ return self.open_tasks.has_node(tid)
1590
1591 def get_task(self, tid):
1592- """
1593+ '''
1594 Returns the internal task object for the given tid, or None if the
1595 tid is not present in this DataStore.
1596 @param tid: Task ID to retrieve
1597- """
1598- if tid:
1599- if self.has_task(tid):
1600- task = self.__internal_get_task(tid)
1601- else:
1602- #print "no task %s" %tid
1603- task = None
1604- return task
1605+ @returns GTG.core.task.Task or None: whether the Task is present
1606+ or not
1607+ '''
1608+ if self.has_task(tid):
1609+ return self.open_tasks.get_node(tid)
1610 else:
1611- print "get_task should take a tid"
1612+ Log.debug("requested non-existent task")
1613 return None
1614
1615- def __internal_get_task(self, tid):
1616- return self.open_tasks.get_node(tid)
1617-# if toreturn == None:
1618-# self.closed_tasks.get_node(tid)
1619- #else:
1620- #print "error : this task doesn't exist in either tree"
1621- #pass
1622- #we return None if the task doesn't exist
1623-# return toreturn
1624-
1625- def delete_task(self, tid):
1626- """
1627- Deletes the given task entirely from this DataStore, and unlinks
1628- it from the task's parent.
1629- @return: True if task was deleted, or False if the tid was not
1630- present in this DataStore.
1631- """
1632- if not tid or not self.has_task(tid):
1633- return False
1634-
1635- self.__internal_get_task(tid).delete()
1636- uid, pid = tid.split('@') #pylint: disable-msg=W0612
1637- back = self.backends[pid]
1638- #Check that the task still exist. It might have been deleted
1639- #by its parent a few line earlier :
1640- if self.has_task(tid):
1641- self.open_tasks.remove_node(tid)
1642-# self.closed_tasks.remove_node(tid)
1643- back.remove_task(tid)
1644- return True
1645-
1646- def new_task(self,pid=None):
1647+ def task_factory(self, tid, newtask = False):
1648+ '''
1649+ Instantiates the given task id as a Task object.
1650+ @param tid: a task id. Must be unique
1651+ @param newtask: True if the task has never been seen before
1652+ @return Task: a Task instance
1653+ '''
1654+ return Task(tid, self.requester, newtask)
1655+
1656+ def new_task(self):
1657 """
1658 Creates a blank new task in this DataStore.
1659- @param pid: (Optional) parent ID that this task should be a child of.
1660- If not specified, the task will be a child of the default backend.
1661+ New task is created in all the backends that collect all tasks (among
1662+ them, the default backend). The default backend uses the same task id
1663+ in its own internal representation.
1664 @return: The task object that was created.
1665 """
1666- if not pid:
1667- pid = DEFAULT_BACKEND
1668- newtid = self.backends[pid].new_task_id()
1669- while self.has_task(newtid):
1670- print "error : tid already exists"
1671- newtid = self.backends[pid].new_task_id()
1672- task = Task(newtid, self.requester,newtask=True)
1673+ task = self.task_factory(uuid.uuid4(), True)
1674 self.open_tasks.add_node(task)
1675- task.set_sync_func(self.backends[pid].set_task,callsync=False)
1676 return task
1677-
1678- def get_tagstore(self):
1679- return self.tagstore
1680-
1681- def get_requester(self):
1682- return self.requester
1683-
1684- def get_tasks_tree(self):
1685- """ return: Open tasks tree """
1686- return self.open_tasks
1687-
1688- def push_task(self,task):
1689- """
1690- Adds the given task object as a node to the open tasks tree.
1691- @param task: A valid task object
1692- """
1693- tid = task.get_id()
1694- if self.has_task(tid):
1695- print "pushing an existing task. We should care about modifications"
1696+
1697+ @synchronized
1698+ def push_task(self, task, backend_capabilities = 'bypass for now'):
1699+ '''
1700+ Adds the given task object to the task tree. In other words, registers
1701+ the given task in the GTG task set.
1702+ @param task: A valid task object (a GTG.core.task.Task)
1703+ @return bool: True if the task has been accepted
1704+ '''
1705+
1706+ if self.has_task(task.get_id()):
1707+ return False
1708 else:
1709- uid, pid = tid.split('@')
1710 self.open_tasks.add_node(task)
1711 task.set_loaded()
1712- task.set_sync_func(self.backends[pid].set_task,callsync=False)
1713-
1714- def task_factory(self,tid):
1715- """
1716- Instantiates the given task id as a Task object.
1717- @param tid: The id of the task to instantiate
1718- @return: The task object instantiated for tid
1719- """
1720- task = None
1721- if self.has_task(tid):
1722- print "error : tid already exists"
1723+ if self.is_default_backend_loaded:
1724+ task.sync()
1725+ return True
1726+
1727+ ##########################################################################
1728+ ### Backends functions
1729+ ##########################################################################
1730+
1731+ def get_all_backends(self, disabled = False):
1732+ """
1733+ returns list of all registered backends for this DataStore.
1734+ @param disabled: If disabled is True, attaches also the list of disabled backends
1735+ @return list: a list of TaskSource objects
1736+ """
1737+ #NOTE: consider cashing this result for speed.
1738+ result = []
1739+ for backend in self.backends.itervalues():
1740+ if backend.is_enabled() or disabled:
1741+ result.append(backend)
1742+ return result
1743+
1744+ def get_backend(self, backend_id):
1745+ '''
1746+ Returns a backend given its id
1747+ @param backend_id: a backend id
1748+ @returns GTG.core.datastore.TaskSource or None: the requested backend,
1749+ or none
1750+ '''
1751+ if backend_id in self.backends:
1752+ return self.backends[backend_id]
1753 else:
1754- task = Task(tid, self.requester, newtask=False)
1755- return task
1756-
1757+ return None
1758
1759- def register_backend(self, dic):
1760+ def register_backend(self, backend_dic):
1761 """
1762 Registers a TaskSource as a backend for this DataStore
1763- @param dic: Dictionary object with a "backend" and "pid"
1764- specified. dic["pid"] should be the parent ID to use
1765- with the backend specified in dic["backend"].
1766+ @param backend_dic: Dictionary object containing all the
1767+ parameters to initialize the backend (filename...). It should
1768+ also contain the backend class (under "backend"), and its unique
1769+ id (under "pid")
1770 """
1771- if "backend" in dic:
1772- pid = dic["pid"]
1773- backend = dic["backend"]
1774- source = TaskSource(backend, dic)
1775- self.backends[pid] = source
1776- #Filling the backend
1777- #Doing this at start is more efficient than
1778- #after the GUI is launched
1779- source.start_get_tasks(self.push_task,self.task_factory)
1780+ if "backend" in backend_dic:
1781+ if "pid" not in backend_dic:
1782+ Log.debug("registering a backend without pid.")
1783+ return None
1784+ backend = backend_dic["backend"]
1785+ #Checking that is a new backend
1786+ if backend.get_id() in self.backends:
1787+ Log.debug("registering already registered backend")
1788+ return None
1789+ source = TaskSource(requester = self.requester,
1790+ backend = backend,
1791+ datastore = self.filtered_datastore)
1792+ self.backends[backend.get_id()] = source
1793+ #we notify that a new backend is present
1794+ self._backend_signals.backend_added(backend.get_id())
1795+ #saving the backend in the correct dictionary (backends for enabled
1796+ # backends, disabled_backends for the disabled ones)
1797+ #this is useful for retro-compatibility
1798+ if not GenericBackend.KEY_ENABLED in backend_dic:
1799+ source.set_parameter(GenericBackend.KEY_ENABLED, True)
1800+ if not GenericBackend.KEY_DEFAULT_BACKEND in backend_dic:
1801+ source.set_parameter(GenericBackend.KEY_DEFAULT_BACKEND, True)
1802+ #if it's enabled, we initialize it
1803+ if source.is_enabled() and \
1804+ (self.is_default_backend_loaded or source.is_default()):
1805+ source.initialize(connect_signals = False)
1806+ #Filling the backend
1807+ #Doing this at start is more efficient than
1808+ #after the GUI is launched
1809+ source.start_get_tasks()
1810+ return source
1811 else:
1812- print "Register a dic without backend key: BUG"
1813-
1814- def unregister_backend(self, backend):
1815- """ Unimplemented """
1816- print "unregister backend %s not implemented" %backend
1817-
1818- def get_all_backends(self):
1819- """ returns list of all registered backends for this DataStore """
1820- l = []
1821- for key in self.backends:
1822- l.append(self.backends[key])
1823- return l
1824+ Log.debug("Tried to register a backend without a pid")
1825+
1826+ def _activate_non_default_backends(self, sender = None):
1827+ '''
1828+ Non-default backends have to wait until the default loads before
1829+ being activated. This function is called after the first default
1830+ backend has loaded all its tasks.
1831+ '''
1832+ if self.is_default_backend_loaded:
1833+ Log.debug("spurious call")
1834+ return
1835+ self.is_default_backend_loaded = True
1836+ for backend in self.backends.itervalues():
1837+ if backend.is_enabled() and not backend.is_default():
1838+ backend.initialize()
1839+ backend.start_get_tasks()
1840+ self.flush_all_tasks(backend.get_id())
1841+
1842+ def set_backend_enabled(self, backend_id, state):
1843+ """
1844+ The backend corresponding to backend_id is enabled or disabled
1845+ according to "state".
1846+ Disable:
1847+ Quits a backend and disables it (which means it won't be
1848+ automatically loaded next time GTG is started)
1849+ Enable:
1850+ Reloads a disabled backend. Backend must be already known by the
1851+ Datastore
1852+ @parma backend_id: a backend id
1853+ @param state: True to enable, False to disable
1854+ """
1855+ if backend_id in self.backends:
1856+ backend = self.backends[backend_id]
1857+ current_state = backend.is_enabled()
1858+ if current_state == True and state == False:
1859+ #we disable the backend
1860+ backend.quit(disable = True)
1861+ elif current_state == False and state == True:
1862+ if self.is_default_backend_loaded == True:
1863+ backend.initialize()
1864+ self.flush_all_tasks(backend_id)
1865+ else:
1866+ #will be activated afterwards
1867+ backend.set_parameter(GenericBackend.KEY_ENABLED,
1868+ True)
1869+
1870+ def remove_backend(self, backend_id):
1871+ '''
1872+ Removes a backend, and forgets it ever existed.
1873+ @param backend_id: a backend id
1874+ '''
1875+ if backend_id in self.backends:
1876+ backend = self.backends[backend_id]
1877+ if backend.is_enabled():
1878+ self.set_backend_enabled(backend_id, False)
1879+ backend.purge()
1880+ #we notify that the backend has been deleted
1881+ self._backend_signals.backend_removed(backend.get_id())
1882+ del self.backends[backend_id]
1883+
1884+ def backend_change_attached_tags(self, backend_id, tag_names):
1885+ '''
1886+ Changes the tags for which a backend should store a task
1887+ @param backend_id: a backend_id
1888+ @param tag_names: the new set of tags. This should not be a tag object,
1889+ just the tag name.
1890+ '''
1891+ backend = self.backends[backend_id]
1892+ backend.set_attached_tags(tag_names)
1893+
1894+ def flush_all_tasks(self, backend_id):
1895+ '''
1896+ This function will cause all tasks to be checked against the backend
1897+ identified with backend_id. If tasks need to be added or removed, it
1898+ will be done here.
1899+ It has to be run after the creation of a new backend (or an alteration
1900+ of its "attached tags"), so that the tasks which are already loaded in
1901+ the Tree will be saved in the proper backends
1902+ @param backend_id: a backend id
1903+ '''
1904+ def _internal_flush_all_tasks():
1905+ backend = self.backends[backend_id]
1906+ for task_id in self.requester.get_all_tasks_list():
1907+ backend.queue_set_task(None, task_id)
1908+ t = threading.Thread(target = _internal_flush_all_tasks).start()
1909+ self.backends[backend_id].start_get_tasks()
1910+
1911+ def save(self, quit = False):
1912+ '''
1913+ Saves the backends parameters.
1914+ @param quit: If quit is true, backends are shut down
1915+ '''
1916+ doc,xmlconfig = cleanxml.emptydoc("config")
1917+ #we ask all the backends to quit first.
1918+ if quit:
1919+ for b in self.get_all_backends():
1920+ #NOTE:we could do this in parallel. Maybe a quit and
1921+ #has_quit would be faster (invernizzi)
1922+ b.quit()
1923+ #we save the parameters
1924+ for b in self.get_all_backends(disabled = True):
1925+ t_xml = doc.createElement("backend")
1926+ for key, value in b.get_parameters().iteritems():
1927+ if key in ["backend", "xmlobject"]:
1928+ #We don't want parameters,backend,xmlobject
1929+ continue
1930+ param_type = b.get_parameter_type(key)
1931+ value = b.cast_param_type_to_string(param_type, value)
1932+ t_xml.setAttribute(str(key), value)
1933+ #Saving all the projects at close
1934+ xmlconfig.appendChild(t_xml)
1935+
1936+ datafile = os.path.join(CoreConfig().get_data_dir(), CoreConfig.DATA_FILE)
1937+ cleanxml.savexml(datafile,doc,backup=True)
1938+
1939+ #Saving the tagstore
1940+ ts = self.get_tagstore()
1941+ ts.save()
1942+
1943+ def request_task_deletion(self, tid):
1944+ '''
1945+ This is a proxy function to request a task deletion from a backend
1946+ @param tid: the tid of the task to remove
1947+ '''
1948+ self.requester.delete_task(tid)
1949+
1950
1951 class TaskSource():
1952- """ transparent interface between the real backend and the datastore """
1953- def __init__(self, backend, parameters):
1954+ '''
1955+ Transparent interface between the real backend and the DataStore.
1956+ Is in charge of connecting and disconnecting to signals
1957+ '''
1958+ def __init__(self, requester, backend, datastore):
1959 """
1960 Instantiates a TaskSource object.
1961- @param backend: (Required) Task Backend being wrapperized
1962- @param parameters: Dictionary of custom parameters.
1963+ @param requester: a Requester
1964+ @param backend: the backend being wrapped
1965+ @param datastore: a FilteredDatastore
1966 """
1967 self.backend = backend
1968- self.dic = parameters
1969- self.to_set = []
1970- self.to_remove = []
1971- self.lock = threading.Lock()
1972- self.count_set = 0
1973+ self.req = requester
1974+ self.backend.register_datastore(datastore)
1975+ self.to_set = deque()
1976+ self.to_remove = deque()
1977+ self.task_filter = self.get_task_filter_for_backend()
1978+ if Log.is_debugging_mode():
1979+ self.timer_timestep = 5
1980+ else:
1981+ self.timer_timestep = 1
1982+ self.set_task_handle = None
1983+ self.remove_task_handle = None
1984+ self.to_set_timer = None
1985
1986- def start_get_tasks(self,push_task,task_factory):
1987- """
1988+ def start_get_tasks(self):
1989+ ''''
1990 Maps the TaskSource to the backend and starts threading.
1991- This must be called before the DataStore is usable.
1992- """
1993- func = self.backend.start_get_tasks
1994- t = threading.Thread(target=func,args=(push_task,task_factory))
1995- t.start()
1996-
1997- def set_task(self, task):
1998+ '''
1999+ threading.Thread(target = self.__start_get_tasks).start()
2000+
2001+ def __start_get_tasks(self):
2002+ '''
2003+ Loads all task from the backend and connects its signals afterwards.
2004+ Launched as a thread by start_get_tasks
2005+ '''
2006+ self.backend.start_get_tasks()
2007+ self._connect_signals()
2008+ if self.backend.is_default():
2009+ BackendSignals().default_backend_loaded()
2010+
2011+ def get_task_filter_for_backend(self):
2012+ '''
2013+ Fiter that checks if the task should be stored in this backend.
2014+
2015+ @returns function: a function that accepts a task and returns True/False
2016+ whether the task should be stored or not
2017+ '''
2018+ raw_filter = self.req.get_filter("backend_filter").get_function()
2019+ return lambda task: raw_filter(task, \
2020+ set(self.backend.get_attached_tags()))
2021+
2022+ def should_task_id_be_stored(self, task_id):
2023+ '''
2024+ Helper function: Checks if a task should be stored in this backend
2025+ @param task_id: a task id
2026+ @returns bool: True if the task should be stored
2027+ '''
2028+ task = self.req.get_task(task_id)
2029+ return self.task_filter(task)
2030+
2031+ def queue_set_task(self, sender, tid):
2032 """
2033 Updates the task in the DataStore. Actually, it adds the task to a
2034 queue to be updated asynchronously.
2035+ @param sender: not used, any value will do.
2036 @param task: The Task object to be updated.
2037 """
2038- tid = task.get_id()
2039- if task not in self.to_set and tid not in self.to_remove:
2040- self.to_set.append(task)
2041- if self.lock.acquire(False):
2042- func = self.setting_thread
2043- t = threading.Thread(target=func)
2044- t.start()
2045-# else:
2046-# print "cannot acquire lock : not a problem, just for debug purpose"
2047+ if self.should_task_id_be_stored(tid):
2048+ if tid not in self.to_set and tid not in self.to_remove:
2049+ self.to_set.appendleft(tid)
2050+ self.__try_launch_setting_thread()
2051+ else:
2052+ self.queue_remove_task(None, tid)
2053
2054- def setting_thread(self):
2055- """
2056+ def launch_setting_thread(self):
2057+ '''
2058 Operates the threads to set and remove tasks.
2059 Releases the lock when it is done.
2060- """
2061- try:
2062- while len(self.to_set) > 0:
2063- t = self.to_set.pop(0)
2064- tid = t.get_id()
2065- if tid not in self.to_remove:
2066- self.count_set += 1
2067- #print "saving task %s (%s saves)" %(tid,self.count_set)
2068- self.backend.set_task(t)
2069- while len(self.to_remove) > 0:
2070- tid = self.to_remove.pop(0)
2071- self.backend.remove_task(tid)
2072- finally:
2073- self.lock.release()
2074+ '''
2075+ #FIXME: the lock should be general for all backends. Therefore, it
2076+ #should be handled in the datastore
2077+ while True:
2078+ try:
2079+ tid = self.to_set.pop()
2080+ except IndexError:
2081+ break
2082+ #we check that the task is not already marked for deletion
2083+ #and that it's still to be stored in this backend
2084+ #NOTE: no need to lock, we're reading
2085+ if tid not in self.to_remove and \
2086+ self.should_task_id_be_stored(tid) and \
2087+ self.req.has_task(tid):
2088+ task = self.req.get_task(tid)
2089+ self.backend.queue_set_task(task)
2090+ while True:
2091+ try:
2092+ tid = self.to_remove.pop()
2093+ except IndexError:
2094+ break
2095+ self.backend.queue_remove_task(tid)
2096+ #we release the weak lock
2097+ self.to_set_timer = None
2098
2099- def remove_task(self, tid):
2100- """
2101+ def queue_remove_task(self, sender, tid):
2102+ '''
2103 Queues task to be removed.
2104+ @param sender: not used, any value will do
2105 @param tid: The Task ID of the task to be removed
2106- """
2107+ '''
2108 if tid not in self.to_remove:
2109- self.to_remove.append(tid)
2110- if self.lock.acquire(False):
2111- func = self.setting_thread
2112- t = threading.Thread(target=func)
2113- t.start()
2114-
2115- def new_task_id(self):
2116- """
2117- returns a new ID created by the backend.
2118- """
2119- return self.backend.new_task_id()
2120-
2121- def quit(self):
2122- """ Quits the backend """
2123- self.backend.quit()
2124-
2125- #Those functions are only for TaskSource
2126- def get_parameters(self):
2127- """
2128- Returns the parameters specified during creation of the DataStore
2129- """
2130- return self.dic
2131+ self.to_remove.appendleft(tid)
2132+ self.__try_launch_setting_thread()
2133+
2134+ def __try_launch_setting_thread(self):
2135+ '''
2136+ Helper function to launch the setting thread, if it's not running
2137+ '''
2138+ if self.to_set_timer == None:
2139+ self.to_set_timer = threading.Timer(self.timer_timestep, \
2140+ self.launch_setting_thread)
2141+ self.to_set_timer.start()
2142+
2143+ def initialize(self, connect_signals = True):
2144+ '''
2145+ Initializes the backend and starts looking for signals.
2146+ @param connect_signals: if True, it starts listening for signals
2147+ '''
2148+ self.backend.initialize()
2149+ if connect_signals:
2150+ self._connect_signals()
2151+
2152+ def _connect_signals(self):
2153+ '''
2154+ Helper function to connect signals
2155+ '''
2156+ if not self.set_task_handle:
2157+ self.set_task_handle = self.req.connect('task-modified', \
2158+ self.queue_set_task)
2159+ if not self.remove_task_handle:
2160+ self.remove_task_handle = self.req.connect('task-deleted',\
2161+ self.queue_remove_task)
2162+
2163+ def _disconnect_signals(self):
2164+ '''
2165+ Helper function to disconnect signals
2166+ '''
2167+ if self.set_task_handle:
2168+ self.req.disconnect(self.set_task_handle)
2169+ self.set_task_handle = None
2170+ if self.remove_task_handle:
2171+ self.req.disconnect(self.remove_task_handle)
2172+ self.remove_task_handle = None
2173+
2174+ def sync(self):
2175+ '''
2176+ Forces the TaskSource to sync all the pending tasks
2177+ '''
2178+ if self.to_set_timer != None:
2179+ try:
2180+ self.to_set_timer.cancel()
2181+ except:
2182+ pass
2183+ try:
2184+ self.to_set_timer.join(5)
2185+ except:
2186+ pass
2187+ self.launch_setting_thread()
2188+
2189+ def quit(self, disable = False):
2190+ '''
2191+ Quits the backend and disconnect the signals
2192+ @param disable: if True, the backend is disabled.
2193+ '''
2194+ self._disconnect_signals()
2195+ self.sync()
2196+ self.backend.quit(disable)
2197+
2198+ def __getattr__(self, attr):
2199+ '''
2200+ Delegates all the functions not defined here to the real backend
2201+ (standard python function)
2202+ @param attr: attribute to get
2203+ '''
2204+ if attr in self.__dict__:
2205+ return self.__dict__[attr]
2206+ else:
2207+ return getattr(self.backend, attr)
2208+
2209+
2210+
2211+class FilteredDataStore(Borg):
2212+ '''
2213+ This class acts as an interface to the Datastore.
2214+ It is used to hide most of the methods of the Datastore.
2215+ The backends can safely use the remaining methods.
2216+ '''
2217+
2218+
2219+ def __init__(self, datastore):
2220+ super(FilteredDataStore, self).__init__()
2221+ self.datastore = datastore
2222+
2223+ def __getattr__(self, attr):
2224+ if attr in ['task_factory', \
2225+ 'push_task',
2226+ 'get_task',
2227+ 'has_task',
2228+ 'request_task_deletion']:
2229+ return getattr(self.datastore, attr)
2230+ else:
2231+ raise AttributeError
2232+
2233
2234=== modified file 'GTG/core/filters_bank.py'
2235--- GTG/core/filters_bank.py 2010-06-14 19:30:50 +0000
2236+++ GTG/core/filters_bank.py 2010-06-23 01:06:26 +0000
2237@@ -35,6 +35,10 @@
2238
2239 def set_parameters(self,dic):
2240 self.dic = dic
2241+
2242+ def get_function(self):
2243+ '''Returns the filtering function'''
2244+ return self.func
2245
2246 def is_displayed(self,tid):
2247 task = self.req.get_task(tid)
2248@@ -152,6 +156,9 @@
2249 #worklate
2250 filt_obj = Filter(self.worklate,self.req)
2251 self.available_filters['worklate'] = filt_obj
2252+ #backend filter
2253+ filt_obj = Filter(self.backend_filter, self.req)
2254+ self.available_filters['backend_filter'] = filt_obj
2255 #no_disabled_tag
2256 filt_obj = Filter(self.no_disabled_tag,self.req)
2257 param = {}
2258@@ -234,6 +241,19 @@
2259 """ Filter of tasks which are closed """
2260 ret = task.get_status() in [Task.STA_DISMISSED, Task.STA_DONE]
2261 return ret
2262+
2263+ def backend_filter(self, task, tags_to_match_set):
2264+ '''
2265+ Filter that checks if two tags sets intersect. It is used to check if a
2266+ task should be stored inside a backend
2267+ @param task: a task object
2268+ @oaram tags_to_match_set: a *set* of tag names
2269+ '''
2270+ all_tasks_tag = self.req.get_alltag_tag().get_name()
2271+ if all_tasks_tag in tags_to_match_set:
2272+ return True
2273+ task_tags = set(task.get_tags_name())
2274+ return task_tags.intersection(tags_to_match_set)
2275
2276 def no_disabled_tag(self,task,parameters=None):
2277 """Filter of task that don't have any disabled/nonworkview tag"""
2278
2279=== modified file 'GTG/core/requester.py'
2280--- GTG/core/requester.py 2010-06-21 12:34:23 +0000
2281+++ GTG/core/requester.py 2010-06-23 01:06:26 +0000
2282@@ -1,6 +1,6 @@
2283 # -*- coding: utf-8 -*-
2284 # -----------------------------------------------------------------------------
2285-# Gettings Things Gnome! - a personal organizer for the GNOME desktop
2286+# Getting Things Gnome! - a personal organizer for the GNOME desktop
2287 # Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
2288 #
2289 # This program is free software: you can redistribute it and/or modify it under
2290@@ -39,18 +39,18 @@
2291 Multiple L{Requester}s can exist on the same datastore, so they should
2292 never have state of their own.
2293 """
2294- __gsignals__ = {'task-added': (gobject.SIGNAL_RUN_FIRST, \
2295- gobject.TYPE_NONE, (str, )),
2296- 'task-deleted': (gobject.SIGNAL_RUN_FIRST, \
2297- gobject.TYPE_NONE, (str, )),
2298- 'task-modified': (gobject.SIGNAL_RUN_FIRST, \
2299- gobject.TYPE_NONE, (str, )),
2300- 'tag-added': (gobject.SIGNAL_RUN_FIRST, \
2301- gobject.TYPE_NONE, (str, )),
2302- 'tag-deleted': (gobject.SIGNAL_RUN_FIRST, \
2303- gobject.TYPE_NONE, (str, )),
2304- 'tag-modified': (gobject.SIGNAL_RUN_FIRST, \
2305- gobject.TYPE_NONE, (str, ))}
2306+
2307+ __string_signal__ = (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (str, ))
2308+
2309+ __gsignals__ = {'task-added' : __string_signal__, \
2310+ 'task-deleted' : __string_signal__, \
2311+ 'task-modified' : __string_signal__, \
2312+ 'task-tagged' : __string_signal__, \
2313+ 'task-untagged' : __string_signal__, \
2314+ 'tag-added' : __string_signal__, \
2315+ 'tag-deleted' : __string_signal__, \
2316+ 'tag-path-deleted' : __string_signal__, \
2317+ 'tag-modified' : __string_signal__}
2318
2319 def __init__(self, datastore):
2320 """Construct a L{Requester}."""
2321@@ -72,12 +72,19 @@
2322 self.counter_call += 1
2323 #print "signal task_modified %s (%s modifications)" %(tid,self.counter_call)
2324 gobject.idle_add(self.emit, "task-modified", tid)
2325-
2326+
2327+ def _task_deleted(self, tid):
2328+ #when this is emitted, task has *already* been deleted
2329+ gobject.idle_add(self.emit, "task-deleted", tid)
2330+
2331 def _tag_added(self,tagname):
2332 gobject.idle_add(self.emit, "tag-added", tagname)
2333
2334 def _tag_modified(self,tagname):
2335 gobject.idle_add(self.emit, "tag-modified", tagname)
2336+
2337+ def _tag_path_deleted(self, path):
2338+ gobject.idle_add(self.emit, "tag-path-deleted", path)
2339
2340 def _tag_deleted(self,tagname):
2341 gobject.idle_add(self.emit, "tag-deleted", tagname)
2342@@ -126,6 +133,7 @@
2343
2344 ######### Filters bank #######################
2345 # Get the filter object for a given name
2346+
2347 def get_filter(self,filter_name):
2348 return self.filters.get_filter(filter_name)
2349
2350@@ -162,7 +170,7 @@
2351 task = self.ds.get_task(tid)
2352 return task
2353
2354- def new_task(self, pid=None, tags=None, newtask=True):
2355+ def new_task(self, tags=None, newtask=True):
2356 """Create a new task.
2357
2358 Note: this modifies the datastore.
2359@@ -175,11 +183,12 @@
2360 existed, C{False} if importing an existing task from a backend.
2361 @return: A task from the data store
2362 """
2363- task = self.ds.new_task(pid=pid)
2364+ task = self.ds.new_task()
2365 if tags:
2366 for t in tags:
2367 assert(isinstance(t, Tag) == False)
2368 task.tag_added(t)
2369+ self._task_loaded(task.get_id())
2370 return task
2371
2372 def delete_task(self, tid):
2373@@ -196,11 +205,11 @@
2374 for tag in task.get_tags():
2375 self.emit('tag-modified', tag.get_name())
2376 self.emit('task-deleted', tid)
2377- #return True
2378- return self.ds.delete_task(tid)
2379+ return self.basetree.remove_node(tid)
2380
2381 ############### Tags ##########################
2382 ###############################################
2383+
2384 def get_tag_tree(self):
2385 return self.ds.get_tagstore()
2386
2387@@ -251,3 +260,27 @@
2388 l.append(t.get_name())
2389 l.sort(cmp=lambda x, y: cmp(x.lower(),y.lower()))
2390 return l
2391+
2392+ ############## Backends #######################
2393+ ###############################################
2394+
2395+ def get_all_backends(self, disabled = False):
2396+ return self.ds.get_all_backends(disabled)
2397+
2398+ def register_backend(self, dic):
2399+ return self.ds.register_backend(dic)
2400+
2401+ def flush_all_tasks(self, backend_id):
2402+ return self.ds.flush_all_tasks(backend_id)
2403+
2404+ def get_backend(self, backend_id):
2405+ return self.ds.get_backend(backend_id)
2406+
2407+ def set_backend_enabled(self, backend_id, state):
2408+ return self.ds.set_backend_enabled(backend_id, state)
2409+
2410+ def remove_backend(self, backend_id):
2411+ return self.ds.remove_backend(backend_id)
2412+
2413+ def backend_change_attached_tags(self, backend_id, tags):
2414+ return self.ds.backend_change_attached_tags(backend_id, tags)
2415
2416=== modified file 'GTG/core/tagstore.py'
2417--- GTG/core/tagstore.py 2010-06-12 13:31:18 +0000
2418+++ GTG/core/tagstore.py 2010-06-23 01:06:26 +0000
2419@@ -40,6 +40,7 @@
2420 # There's only one Tag store by user. It will store all the tag used
2421 # and their attribute.
2422 class TagStore(Tree):
2423+
2424
2425 def __init__(self,requester):
2426 Tree.__init__(self)
2427@@ -50,7 +51,7 @@
2428
2429 ### building the initial tags
2430 # Build the "all tasks tag"
2431- self.alltag_tag = self.new_tag("gtg-tags-all")
2432+ self.alltag_tag = self.new_tag(CoreConfig.ALLTASKS_TAG)
2433 self.alltag_tag.set_attribute("special","all")
2434 self.alltag_tag.set_attribute("label","<span weight='bold'>%s</span>"\
2435 % _("All tasks"))
2436@@ -68,7 +69,7 @@
2437 self.sep_tag.set_attribute("special","sep")
2438 self.sep_tag.set_attribute("order",2)
2439
2440- self.filename = os.path.join(CoreConfig.DATA_DIR, XMLFILE)
2441+ self.filename = os.path.join(CoreConfig().get_data_dir(), XMLFILE)
2442 doc, self.xmlstore = cleanxml.openxmlfile(self.filename,
2443 XMLROOT) #pylint: disable-msg=W0612
2444 for t in self.xmlstore.childNodes:
2445@@ -121,7 +122,7 @@
2446 if tagname[0] != "@":
2447 tagname = "@" + tagname
2448 return self.get_node(tagname)
2449-
2450+
2451 #FIXME : also add a new filter
2452 def rename_tag(self, oldname, newname):
2453 if len(newname) > 0 and \
2454@@ -316,9 +317,12 @@
2455 def add_task(self, tid):
2456 if tid not in self.tasks:
2457 self.tasks.append(tid)
2458- def remove_task(self,tid):
2459+
2460+ def remove_task(self, tid):
2461 if tid in self.tasks:
2462 self.tasks.remove(tid)
2463+ self.req._tag_modified(self.get_name())
2464+
2465 def get_tasks(self):
2466 #return a copy of the list
2467 toreturn = self.tasks[:]
2468
2469=== modified file 'GTG/core/task.py'
2470--- GTG/core/task.py 2010-06-18 16:36:17 +0000
2471+++ GTG/core/task.py 2010-06-23 01:06:26 +0000
2472@@ -48,10 +48,10 @@
2473 #tid is a string ! (we have to choose a type and stick to it)
2474 self.tid = str(ze_id)
2475 self.set_uuid(uuid.uuid4())
2476+ self.remote_ids = {}
2477 self.content = ""
2478 #self.content = \
2479 # "<content>Press Escape or close this task to save it</content>"
2480- self.sync_func = None
2481 self.title = _("My new task")
2482 #available status are: Active - Done - Dismiss - Note
2483 self.status = self.STA_ACTIVE
2484@@ -78,7 +78,6 @@
2485 self.loaded = True
2486 if signal:
2487 self.req._task_loaded(self.tid)
2488- #not sure the following is necessary
2489 #self.req._task_modified(self.tid)
2490
2491 def set_to_keep(self):
2492@@ -102,6 +101,25 @@
2493 self.sync()
2494 return self.uuid
2495
2496+ def get_remote_ids(self):
2497+ '''
2498+ A task usually has a different id in all the different backends.
2499+ This function returns a dictionary backend_id->the id the task has
2500+ in that backend
2501+ @returns dict: dictionary backend_id->task remote id
2502+ '''
2503+ return self.remote_ids
2504+
2505+ def add_remote_id(self, backend_id, task_remote_id):
2506+ '''
2507+ A task usually has a different id in all the different backends.
2508+ This function adds a relationship backend_id-> remote_id that can be
2509+ retrieved using get_remote_ids
2510+ @param backend_id: string representing the backend id
2511+ @param task_remote_id: the id for this task in the backend backend_id
2512+ '''
2513+ self.remote_ids[str(backend_id)] = str(task_remote_id)
2514+
2515 def get_title(self):
2516 return self.title
2517
2518@@ -115,7 +133,7 @@
2519 self.title = title.strip('\t\n')
2520 else:
2521 self.title = "(no title task)"
2522- #Avoid unecessary sync
2523+ #Avoid unnecessary sync
2524 if self.title != old_title:
2525 self.sync()
2526 return True
2527@@ -294,8 +312,7 @@
2528 """Add a newly created subtask to this task. Return the task added as
2529 a subtask
2530 """
2531- uid, pid = self.get_id().split('@') #pylint: disable-msg=W0612
2532- subt = self.req.new_task(pid=pid, newtask=True)
2533+ subt = self.req.new_task(newtask=True)
2534 #we use the inherited childrens
2535 self.add_child(subt.get_id())
2536 return subt
2537@@ -427,37 +444,32 @@
2538 #This method is called by the datastore and should not be called directly
2539 #Use the requester
2540 def delete(self):
2541- self.set_sync_func(None, callsync=False)
2542+ #we issue a delete for all the children
2543 for task in self.get_subtasks():
2544- task.remove_parent(self.get_id())
2545- self.req.delete_task(task.get_id())
2546+ #I think it's superfluous (invernizzi)
2547+ #task.remove_parent(self.get_id())
2548+ task.delete()
2549+ #we tell the parents we have to go
2550 for i in self.get_parents():
2551 task = self.req.get_task(i)
2552 task.remove_child(self.get_id())
2553+ #we tell the tags about the deletion
2554 for tagname in self.tags:
2555 tag = self.req.get_tag(tagname)
2556 tag.remove_task(self.get_id())
2557- #then we remove effectively the task
2558- #self.req.delete_task(self.get_id())
2559-
2560- #This is a callback. The "sync" function has to be set
2561- def set_sync_func(self, sync, callsync=True):
2562- self.sync_func = sync
2563- #We call it immediatly to save stuffs that were set before this
2564- if callsync and self.is_loaded():
2565- self.sync()
2566+ #then we signal the we are ready to be removed
2567+ self.req._task_deleted(self.get_id())
2568
2569 def sync(self):
2570 self._modified_update()
2571- if self.sync_func and self.is_loaded():
2572- self.sync_func(self)
2573+ if self.is_loaded():
2574 self.call_modified()
2575 return True
2576 else:
2577 return False
2578
2579 #This function send the modified signals for the tasks,
2580- #parents and childrens
2581+ #parents and children
2582 def call_modified(self):
2583 #we first modify children
2584 for s in self.get_children():
2585@@ -469,10 +481,11 @@
2586 self.req._task_modified(p)
2587
2588 def _modified_update(self):
2589+ '''
2590+ Updates the modified timestamp
2591+ '''
2592 self.modified = datetime.now()
2593
2594-
2595-
2596 ### TAG FUNCTIONS ############################################################
2597 #
2598 def get_tags_name(self):
2599@@ -509,6 +522,8 @@
2600 #Do not add the same tag twice
2601 if not t in self.tags:
2602 self.tags.append(t)
2603+ #we notify the backends
2604+ #self.req.tag_was_added_to_task(self, tagname)
2605 for child in self.get_subtasks():
2606 if child.can_be_deleted:
2607 child.add_tag(t)
2608
2609=== modified file 'GTG/gtg.py'
2610--- GTG/gtg.py 2010-06-18 11:55:03 +0000
2611+++ GTG/gtg.py 2010-06-23 01:06:26 +0000
2612@@ -45,18 +45,17 @@
2613 """This is the top-level exec script for running GTG"""
2614
2615 #=== IMPORT ===================================================================
2616-from contextlib import contextmanager
2617 import os
2618 import logging
2619-import signal
2620
2621 import dbus
2622
2623 #our own imports
2624-from GTG import _, info
2625+from GTG.backends import BackendFactory
2626+from GTG import _
2627 from GTG.core import CoreConfig
2628 from GTG.core.datastore import DataStore
2629-from GTG.gtk import crashhandler
2630+from GTG.gtk.crashhandler import signal_catcher
2631 from GTG.gtk.manager import Manager
2632 from GTG.tools.logger import Log
2633
2634@@ -93,54 +92,55 @@
2635 #=== MAIN CLASS ===============================================================
2636
2637 def main(options=None, args=None):
2638+ '''
2639+ Calling this starts the full GTG experience ( :-D )
2640+ '''
2641+ config, ds, req = core_main_init(options, args)
2642+ # Launch task browser
2643+ manager = Manager(req, config)
2644+ #main loop
2645+ #To be more user friendly and get the logs of crashes, we show an apport
2646+ # hooked window upon crashes
2647+ with signal_catcher(manager.close_browser):
2648+ manager.main()
2649+ core_main_quit(config, ds)
2650+
2651+def core_main_init(options = None, args = None):
2652+ '''
2653+ Part of the main function prior to the UI initialization.
2654+ '''
2655 # Debugging subsystem initialization
2656 if options.debug:
2657 Log.setLevel(logging.DEBUG)
2658 Log.debug("Debug output enabled.")
2659-
2660+ Log.set_debugging_mode(True)
2661 config = CoreConfig()
2662- check_instance(config.DATA_DIR)
2663- backends_list = config.get_backends_list()
2664-
2665- #initialize Apport hook for crash handling
2666- crashhandler.initialize(app_name = "Getting Things GNOME!", message="GTG"
2667- + info.VERSION + _(" has crashed. Please report the bug on <a href=\""
2668- "http://bugs.edge.launchpad.net/gtg\">our Launchpad page</a>. If you "
2669- "have Apport installed, it will be started for you."), use_apport = True)
2670-
2671+ check_instance(config.get_data_dir())
2672+ backends_list = BackendFactory().get_saved_backends_list()
2673 # Load data store
2674 ds = DataStore()
2675-
2676+ # Register backends
2677 for backend_dic in backends_list:
2678 ds.register_backend(backend_dic)
2679-
2680 #save directly the backends to be sure to write projects.xml
2681- config.save_datastore(ds,initial_save=True)
2682+ ds.save(quit = False)
2683
2684 # Launch task browser
2685 req = ds.get_requester()
2686- manager = Manager(req, config)
2687-
2688- #we listen for signals from the system in order to save our configuration
2689- # if GTG is forcefully terminated (e.g.: on shutdown).
2690- @contextmanager
2691- def signal_catcher():
2692- #if TERM or ABORT are caught, we close the browser
2693- for s in [signal.SIGABRT, signal.SIGTERM]:
2694- signal.signal(s, lambda a,b: manager.close_browser())
2695- yield
2696+ return config, ds, req
2697
2698- #main loop
2699- with signal_catcher():
2700- manager.main()
2701-
2702+def core_main_quit(config, ds):
2703+ '''
2704+ Last bits of code executed in GTG, after the UI has been shut off.
2705+ Currently, it's just saving everything.
2706+ '''
2707 # Ideally we should load window geometry configuration from a config.
2708 # backend like gconf at some point, and restore the appearance of the
2709 # application as the user last exited it.
2710-
2711+ #
2712 # Ending the application: we save configuration
2713- config.save_config()
2714- config.save_datastore(ds)
2715+ config.save()
2716+ ds.save(quit = True)
2717
2718 #=== EXECUTION ================================================================
2719
2720
2721=== modified file 'GTG/gtk/browser/browser.py'
2722--- GTG/gtk/browser/browser.py 2010-06-21 12:34:23 +0000
2723+++ GTG/gtk/browser/browser.py 2010-06-23 01:06:26 +0000
2724@@ -23,7 +23,6 @@
2725 #=== IMPORT ===================================================================
2726 #system imports
2727 import locale
2728-import os
2729 import re
2730 import time
2731 import webbrowser
2732@@ -35,18 +34,16 @@
2733
2734 #our own imports
2735 import GTG
2736+from GTG.core import CoreConfig
2737 from GTG import _, info, ngettext
2738 from GTG.core.task import Task
2739-#from GTG.core.tagstore import Tag
2740 from GTG.gtk.browser import GnomeConfig, tasktree, tagtree
2741-#from GTG.taskbrowser.preferences import PreferencesDialog
2742 from GTG.gtk.browser.tasktree import TaskTreeModel,\
2743 ActiveTaskTreeView,\
2744 ClosedTaskTreeView
2745 from GTG.gtk.browser.tagtree import TagTree
2746 from GTG.tools import openurl
2747-from GTG.tools.dates import strtodate,\
2748- no_date,\
2749+from GTG.tools.dates import no_date,\
2750 FuzzyDate, \
2751 get_canonical_date
2752 from GTG.tools.logger import Log
2753@@ -159,7 +156,7 @@
2754 self.priv['quick_add_cbs'] = []
2755
2756 def _init_icon_theme(self):
2757- icon_dirs = [GTG.DATA_DIR, os.path.join(GTG.DATA_DIR, "icons")]
2758+ icon_dirs = CoreConfig().get_icons_directories()
2759 for i in icon_dirs:
2760 gtk.icon_theme_get_default().prepend_search_path(i)
2761 gtk.window_set_default_icon_name("gtg")
2762@@ -429,7 +426,7 @@
2763 ### HELPER FUNCTIONS ########################################################
2764
2765 def open_preferences(self,widget):
2766- self.vmanager.show_preferences(self.priv)
2767+ self.vmanager.open_preferences(self.priv)
2768
2769 def quit(self,widget=None):
2770 self.vmanager.close_browser()
2771
2772=== modified file 'GTG/gtk/browser/tagtree.py'
2773--- GTG/gtk/browser/tagtree.py 2010-06-18 16:46:10 +0000
2774+++ GTG/gtk/browser/tagtree.py 2010-06-23 01:06:26 +0000
2775@@ -72,7 +72,8 @@
2776 task = self.req.get_task(tid)
2777 if task:
2778 for tag in task.get_tags():
2779- self.tagrefresh(sender=sender,tagname=tag.get_name())
2780+ if tag:
2781+ self.tagrefresh(sender=sender,tagname=tag.get_name())
2782
2783 def tagrefresh(self,sender=None,tagname=None):
2784 if tagname:
2785
2786=== modified file 'GTG/gtk/crashhandler.py'
2787--- GTG/gtk/crashhandler.py 2010-06-07 21:14:45 +0000
2788+++ GTG/gtk/crashhandler.py 2010-06-23 01:06:26 +0000
2789@@ -33,6 +33,12 @@
2790 import sys
2791 import os
2792 import time
2793+import signal
2794+from contextlib import contextmanager
2795+
2796+from GTG import info
2797+
2798+
2799 try:
2800 import pygtk
2801 pygtk.require("2.0") # not tested on earlier versions
2802@@ -297,3 +303,21 @@
2803 return "gtkcrashhandler.py should imported, not run"
2804 raise DoNotRunException()
2805
2806+
2807+## We handle initialization directly here, since this module will be used as a
2808+# singleton
2809+ #we listen for signals from the system in order to save our configuration
2810+ # if GTG is forcefully terminated (e.g.: on shutdown).
2811+@contextmanager
2812+def signal_catcher(callback):
2813+ #if TERM or ABORT are caught, we execute the callback function
2814+ for s in [signal.SIGABRT, signal.SIGTERM]:
2815+ signal.signal(s, lambda a,b: callback())
2816+ yield
2817+
2818+initialize(app_name = "Getting Things GNOME!",
2819+ message = "GTG" + info.VERSION +
2820+ _(" has crashed. Please report the bug on <a "\
2821+ "href=\"http://bugs.edge.launchpad.net/gtg\">our Launchpad page</a>."\
2822+ " If you have Apport installed, it will be started for you."), \
2823+ use_apport = True)
2824
2825=== modified file 'GTG/gtk/delete_dialog.py'
2826--- GTG/gtk/delete_dialog.py 2010-06-23 00:38:13 +0000
2827+++ GTG/gtk/delete_dialog.py 2010-06-23 01:06:26 +0000
2828@@ -43,7 +43,11 @@
2829 """if we pass a tid as a parameter, we delete directly
2830 otherwise, we will look which tid is selected"""
2831 for tid in self.tids_todelete:
2832- self.req.delete_task(tid)
2833+ task = self.req.get_task(tid)
2834+ if task:
2835+ task.delete()
2836+ else:
2837+ print "trying to delete task already deleted"
2838 self.tids_todelete = []
2839
2840 def delete_tasks(self, tids=None):
2841
2842=== modified file 'GTG/gtk/editor/editor.py'
2843--- GTG/gtk/editor/editor.py 2010-06-07 21:14:45 +0000
2844+++ GTG/gtk/editor/editor.py 2010-06-23 01:06:26 +0000
2845@@ -39,7 +39,7 @@
2846 from GTG import _
2847 from GTG import ngettext
2848 from GTG import PLUGIN_DIR
2849-from GTG import DATA_DIR
2850+from GTG.core import CoreConfig
2851 from GTG.gtk.editor import GnomeConfig
2852 from GTG.gtk.editor.taskview import TaskView
2853 from GTG.core.plugins.engine import PluginEngine
2854@@ -176,7 +176,7 @@
2855 self.pengine = PluginEngine(PLUGIN_DIR)
2856 self.te_plugin_api = PluginAPI(window = self.window,
2857 config = None,
2858- data_dir = DATA_DIR,
2859+ data_dir = CoreConfig().get_data_dir(),
2860 builder = self.builder,
2861 requester = self.req,
2862 tagpopup = None,
2863
2864=== modified file 'GTG/gtk/manager.py'
2865--- GTG/gtk/manager.py 2010-06-10 14:45:36 +0000
2866+++ GTG/gtk/manager.py 2010-06-23 01:06:26 +0000
2867@@ -40,7 +40,11 @@
2868 from GTG.core.plugins.api import PluginAPI
2869 from GTG.tools.logger import Log
2870
2871+
2872+
2873 class Manager:
2874+
2875+
2876 ############## init #####################################################
2877 def __init__(self, req, config):
2878 self.config_obj = config
2879@@ -72,9 +76,9 @@
2880 #Deletion UI
2881 self.delete_dialog = None
2882
2883- #Preferences windows
2884- # Initialize "Preferences" dialog
2885- self.preferences = None
2886+ #Preferences and Backends windows
2887+ # Initialize dialogs
2888+ self.preferences_dialog = None
2889
2890 #DBus
2891 DBusTaskWrapper(self.req, self)
2892@@ -89,7 +93,7 @@
2893 # initializes the plugin api class
2894 self.plugin_api = PluginAPI(window = self.browser.window,
2895 config = self.config,
2896- data_dir = GTG.DATA_DIR,
2897+ data_dir = self.config_obj.get_data_dir(),
2898 builder = self.browser.builder,
2899 requester = self.req,
2900 tagpopup = self.browser.tagpopup,
2901@@ -189,8 +193,8 @@
2902
2903 ################ Others dialog ############################################
2904
2905- def show_preferences(self, config_priv, sender=None):
2906- if not self.preferences:
2907+ def open_preferences(self, config_priv, sender=None):
2908+ if not hasattr(self, "preferences"):
2909 self.preferences = PreferencesDialog(self.pengine, self.p_apis, \
2910 self.config_obj)
2911 self.preferences.activate(config_priv)
2912
2913=== modified file 'GTG/gtk/preferences.glade'
2914--- GTG/gtk/preferences.glade 2010-06-02 18:12:23 +0000
2915+++ GTG/gtk/preferences.glade 2010-06-23 01:06:26 +0000
2916@@ -213,63 +213,10 @@
2917 </packing>
2918 </child>
2919 <child>
2920- <object class="GtkAlignment" id="prefs-alignment3">
2921- <property name="visible">True</property>
2922- <property name="top_padding">10</property>
2923- <property name="bottom_padding">10</property>
2924- <property name="left_padding">10</property>
2925- <property name="right_padding">10</property>
2926- <child>
2927- <object class="GtkVBox" id="prefs-vbox5">
2928- <property name="visible">True</property>
2929- <property name="spacing">6</property>
2930- <child>
2931- <object class="GtkLabel" id="prefs-label6">
2932- <property name="visible">True</property>
2933- <property name="xalign">0</property>
2934- <property name="label" translatable="yes">Task _Backends:</property>
2935- <property name="use_underline">True</property>
2936- </object>
2937- <packing>
2938- <property name="expand">False</property>
2939- <property name="position">0</property>
2940- </packing>
2941- </child>
2942- <child>
2943- <object class="GtkScrolledWindow" id="prefs-scrolledwindow1">
2944- <property name="visible">True</property>
2945- <property name="sensitive">False</property>
2946- <property name="can_focus">True</property>
2947- <property name="hscrollbar_policy">never</property>
2948- <property name="vscrollbar_policy">automatic</property>
2949- <child>
2950- <object class="GtkTreeView" id="BackendTree">
2951- <property name="visible">True</property>
2952- <property name="sensitive">False</property>
2953- <property name="can_focus">True</property>
2954- </object>
2955- </child>
2956- </object>
2957- <packing>
2958- <property name="position">1</property>
2959- </packing>
2960- </child>
2961- </object>
2962- </child>
2963- </object>
2964- <packing>
2965- <property name="position">1</property>
2966- </packing>
2967+ <placeholder/>
2968 </child>
2969 <child type="tab">
2970- <object class="GtkLabel" id="prefs-label2">
2971- <property name="visible">True</property>
2972- <property name="label" translatable="yes">Storage</property>
2973- </object>
2974- <packing>
2975- <property name="position">1</property>
2976- <property name="tab_fill">False</property>
2977- </packing>
2978+ <placeholder/>
2979 </child>
2980 <child>
2981 <object class="GtkAlignment" id="prefs-alignment4">
2982
2983=== modified file 'GTG/gtk/preferences.py'
2984--- GTG/gtk/preferences.py 2010-06-10 14:45:36 +0000
2985+++ GTG/gtk/preferences.py 2010-06-23 01:06:26 +0000
2986@@ -270,7 +270,7 @@
2987 self.config["plugins"]["enabled"] = \
2988 self.pengine.enabled_plugins().keys()
2989
2990- self.config_obj.save_config()
2991+ self.config_obj.save()
2992
2993 self.dialog.hide()
2994 return True
2995
2996=== modified file 'GTG/tests/__init__.py'
2997--- GTG/tests/__init__.py 2010-06-22 09:43:55 +0000
2998+++ GTG/tests/__init__.py 2010-06-23 01:06:26 +0000
2999@@ -19,22 +19,31 @@
3000
3001 """Unit tests for GTG."""
3002
3003+from GTG.tools.testingmode import TestingMode
3004+TestingMode().set_testing_mode(True)
3005+
3006+
3007 import unittest
3008
3009+
3010 from GTG.tests import (
3011 test_tagstore,
3012 test_taskviewserial,
3013 test_tree,
3014 test_apidocs,
3015+ test_backends,
3016+ test_datastore,
3017 test_filteredtree,
3018 )
3019
3020-
3021 def test_suite():
3022 return unittest.TestSuite([
3023 test_tagstore.test_suite(),
3024 test_taskviewserial.test_suite(),
3025 test_tree.test_suite(),
3026 test_apidocs.test_suite(),
3027+ test_backends.test_suite(),
3028+ test_datastore.test_suite(),
3029 test_filteredtree.test_suite(),
3030 ])
3031+
3032
3033=== modified file 'GTG/tests/test_apidocs.py'
3034--- GTG/tests/test_apidocs.py 2010-05-29 14:39:28 +0000
3035+++ GTG/tests/test_apidocs.py 2010-06-23 01:06:26 +0000
3036@@ -27,6 +27,8 @@
3037 import shutil
3038 import uuid
3039
3040+from GTG.core import CoreConfig
3041+
3042
3043
3044 class TestApiDocs(unittest.TestCase):
3045@@ -50,4 +52,6 @@
3046 shutil.rmtree(api_dir)
3047
3048 def test_suite():
3049+ CoreConfig().set_data_dir("./test_data")
3050+ CoreConfig().set_conf_dir("./test_data")
3051 return unittest.TestLoader().loadTestsFromTestCase(TestApiDocs)
3052
3053=== added file 'GTG/tests/test_backends.py'
3054--- GTG/tests/test_backends.py 1970-01-01 00:00:00 +0000
3055+++ GTG/tests/test_backends.py 2010-06-23 01:06:26 +0000
3056@@ -0,0 +1,191 @@
3057+# -*- coding: utf-8 -*-
3058+# -----------------------------------------------------------------------------
3059+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
3060+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
3061+#
3062+# This program is free software: you can redistribute it and/or modify it under
3063+# the terms of the GNU General Public License as published by the Free Software
3064+# Foundation, either version 3 of the License, or (at your option) any later
3065+# version.
3066+#
3067+# This program is distributed in the hope that it will be useful, but WITHOUT
3068+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
3069+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
3070+# details.
3071+#
3072+# You should have received a copy of the GNU General Public License along with
3073+# this program. If not, see <http://www.gnu.org/licenses/>.
3074+# -----------------------------------------------------------------------------
3075+
3076+"""Tests for GTG backends.
3077+
3078+Some of these tests will generate files in
3079+xdg.BaseDirectory.xdg_data_home/gtg directory.
3080+"""
3081+
3082+# Standard imports
3083+import unittest
3084+import os
3085+import xdg
3086+
3087+# GTG imports
3088+from GTG.backends import backend_localfile as localfile
3089+from GTG.core import datastore
3090+from GTG.tools import cleanxml
3091+from GTG.core import CoreConfig
3092+
3093+
3094+class GtgBackendsUniTests(unittest.TestCase):
3095+ """Tests for GTG backends."""
3096+
3097+ def __init__(self, test):
3098+ unittest.TestCase.__init__(self, test)
3099+ self.taskfile = ''
3100+ self.datafile = ''
3101+ self.taskpath = ''
3102+ self.datapath = ''
3103+
3104+ def SetUp(self):
3105+ CoreConfig().set_data_dir("./test_data")
3106+ CoreConfig().set_conf_dir("./test_data")
3107+
3108+ def test_localfile_get_name(self):
3109+ """Tests for localfile/get_name function :
3110+ - a string is expected.
3111+ """
3112+ res = localfile.Backend.get_name()
3113+ expectedres = "backend_localfile"
3114+ self.assertEqual(res, expectedres)
3115+
3116+ def test_localfile_get_description(self):
3117+ """Tests for localfile/get_description function :
3118+ - a string is expected.
3119+ """
3120+ res = localfile.Backend.get_description()
3121+ expectedres = "Your tasks are saved"
3122+ self.assertEqual(res[:len(expectedres)], expectedres)
3123+
3124+
3125+ def test_localfile_get_static_parameters(self):
3126+ """Tests for localfile/get_static_parameters function:
3127+ - a string is expected.
3128+ """
3129+ res = localfile.Backend.get_static_parameters()
3130+ self.assertEqual(res['path']['type'], "string")
3131+
3132+ def test_localfile_get_type(self):
3133+ """Tests for localfile/get_type function:
3134+ - a string is expected.
3135+ """
3136+ res = localfile.Backend.get_type()
3137+ expectedres = "readwrite"
3138+ self.assertEqual(res, expectedres)
3139+
3140+
3141+ def test_localfile_backend_method3(self):
3142+ """Tests for localfile/Backend/remove_task method:
3143+ - parse task file to check if task has been removed.
3144+ """
3145+ self.create_test_environment()
3146+ doc, configxml = cleanxml.openxmlfile(self.datapath, 'config')
3147+ xmlproject = doc.getElementsByTagName('backend')
3148+ for domobj in xmlproject:
3149+ dic = {}
3150+ if domobj.hasAttribute("module"):
3151+ dic["module"] = str(domobj.getAttribute("module"))
3152+ dic["pid"] = str(domobj.getAttribute("pid"))
3153+ dic["xmlobject"] = domobj
3154+ dic["Enabled"] = True
3155+ dic["path"] = self.taskpath
3156+ beobj = localfile.Backend(dic)
3157+ expectedres = True
3158+ beobj.remove_task("0@1")
3159+ beobj.quit()
3160+ dataline = open(self.taskpath, 'r').read()
3161+ print dataline
3162+ if "0@1" in dataline:
3163+ res = False
3164+ else:
3165+ res = True
3166+ expectedres = True
3167+ self.assertEqual(res, expectedres)
3168+
3169+# def test_localfile_backend_method4(self):
3170+# """Tests for localfile/Backend/get_task method:
3171+# - Compares task titles to check if method works.
3172+# """
3173+# self.create_test_environment()
3174+# doc, configxml = cleanxml.openxmlfile(self.datapath, 'config')
3175+# xmlproject = doc.getElementsByTagName('backend')
3176+# for domobj in xmlproject:
3177+# dic = {}
3178+# if domobj.hasAttribute("module"):
3179+# dic["module"] = str(domobj.getAttribute("module"))
3180+# dic["pid"] = str(domobj.getAttribute("pid"))
3181+# dic["xmlobject"] = domobj
3182+# dic["filename"] = self.taskfile
3183+# beobj = localfile.Backend(dic)
3184+# dstore = datastore.DataStore()
3185+# newtask = dstore.new_task(tid="0@2", pid="1", newtask=True)
3186+# beobj.get_task(newtask, "0@1")
3187+# self.assertEqual(newtask.get_title(), u"Ceci est un test")
3188+
3189+# def test_localfile_backend_method5(self):
3190+# """Tests for localfile/Backend/set_task method:
3191+# - parses task file to check if new task has been stored.
3192+# """
3193+# self.create_test_environment()
3194+# doc, configxml = cleanxml.openxmlfile(self.datapath, 'config')
3195+# xmlproject = doc.getElementsByTagName('backend')
3196+# for domobj in xmlproject:
3197+# dic = {}
3198+# if domobj.hasAttribute("module"):
3199+# dic["module"] = str(domobj.getAttribute("module"))
3200+# dic["pid"] = str(domobj.getAttribute("pid"))
3201+# dic["xmlobject"] = domobj
3202+# dic["filename"] = self.taskfile
3203+# beobj = localfile.Backend(dic)
3204+# dstore = datastore.DataStore()
3205+# newtask = dstore.new_task(tid="0@2", pid="1", newtask=True)
3206+# beobj.set_task(newtask)
3207+# dataline = open(self.taskpath, 'r').read()
3208+# if "0@2" in dataline:
3209+# res = True
3210+# else:
3211+# res = False
3212+# expectedres = True
3213+# self.assertEqual(res, expectedres)
3214+
3215+ def create_test_environment(self):
3216+ """Create the test environment"""
3217+ self.taskfile = 'test.xml'
3218+ self.datafile = 'projectstest.xml'
3219+ tasks = [
3220+ '<?xml version="1.0" ?>\n',
3221+ '<project>\n',
3222+ '\t<task id="0@1" status="Active" tags="">\n',
3223+ '\t\t<title>\n',
3224+ '\t\t\tCeci est un test\n',
3225+ '\t\t</title>\n',
3226+ '\t</task>\n',
3227+ '</project>\n',
3228+ ]
3229+ data = [
3230+ '<?xml version="1.0" ?>\n',
3231+ '<config>\n',
3232+ '\t<backend filename="test.xml" module="localfile" pid="1"/>\n',
3233+ '</config>\n',
3234+ ]
3235+ self.testdir = os.path.join(xdg.BaseDirectory.xdg_data_home, 'gtg')
3236+ if not os.path.exists(self.testdir):
3237+ os.makedirs(self.testdir)
3238+ self.taskpath = os.path.join(self.testdir, self.taskfile)
3239+ self.datapath = os.path.join(self.testdir, self.datafile)
3240+ open(self.taskpath, 'w').writelines(tasks)
3241+ open(self.datapath, 'w').writelines(data)
3242+
3243+
3244+def test_suite():
3245+ CoreConfig().set_data_dir("./test_data")
3246+ CoreConfig().set_conf_dir("./test_data")
3247+ return unittest.TestLoader().loadTestsFromName(__name__)
3248
3249=== added file 'GTG/tests/test_datastore.py'
3250--- GTG/tests/test_datastore.py 1970-01-01 00:00:00 +0000
3251+++ GTG/tests/test_datastore.py 2010-06-23 01:06:26 +0000
3252@@ -0,0 +1,360 @@
3253+# -*- coding: utf-8 -*-
3254+# -----------------------------------------------------------------------------
3255+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
3256+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
3257+#
3258+# This program is free software: you can redistribute it and/or modify it under
3259+# the terms of the GNU General Public License as published by the Free Software
3260+# Foundation, either version 3 of the License, or (at your option) any later
3261+# version.
3262+#
3263+# This program is distributed in the hope that it will be useful, but WITHOUT
3264+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
3265+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
3266+# details.
3267+#
3268+# You should have received a copy of the GNU General Public License along with
3269+# this program. If not, see <http://www.gnu.org/licenses/>.
3270+# -----------------------------------------------------------------------------
3271+
3272+'''
3273+Tests for the datastore
3274+'''
3275+
3276+import unittest
3277+import uuid
3278+import random
3279+import time
3280+
3281+import GTG
3282+from GTG.core.datastore import DataStore
3283+from GTG.backends.genericbackend import GenericBackend
3284+from GTG.core import CoreConfig
3285+
3286+
3287+
3288+class TestDatastore(unittest.TestCase):
3289+ '''
3290+ Tests for the DataStore object.
3291+ '''
3292+
3293+
3294+ def setUp(self):
3295+ '''
3296+ Creates the environment for the tests
3297+ @returns None
3298+ '''
3299+ self.datastore = DataStore()
3300+ self.requester = self.datastore.get_requester()
3301+
3302+ def test_task_factory(self):
3303+ '''
3304+ Test for the task_factory function
3305+ '''
3306+ #generate a Task with a random id
3307+ tid = str(uuid.uuid4())
3308+ task = self.datastore.task_factory(tid, newtask = True)
3309+ self.assertTrue(isinstance(task, GTG.core.task.Task))
3310+ self.assertEqual(task.get_id(), tid)
3311+ self.assertEqual(task.is_new(), True)
3312+ tid = str(uuid.uuid4())
3313+ task = self.datastore.task_factory(tid, newtask = False)
3314+ self.assertEqual(task.is_new(), False)
3315+
3316+ def test_new_task_and_has_task(self):
3317+ '''
3318+ Tests the new_task function
3319+ '''
3320+ task = self.datastore.new_task()
3321+ tid = task.get_id()
3322+ self.assertTrue(isinstance(tid, str))
3323+ self.assertTrue(tid != '')
3324+ self.assertTrue(task.is_new())
3325+ self.assertTrue(self.datastore.has_task(tid))
3326+ self.assertTrue(len(self.datastore.get_all_tasks()) == 1)
3327+
3328+ def test_get_all_tasks(self):
3329+ '''
3330+ Tests the get_all_tasks function
3331+ '''
3332+ task_ids = []
3333+ for i in xrange(1, 10):
3334+ task = self.datastore.new_task()
3335+ task_ids.append(task.get_id())
3336+ return_list =self.datastore.get_all_tasks()
3337+ self.assertEqual(len(return_list), i)
3338+ task_ids.sort()
3339+ return_list.sort()
3340+ self.assertEqual(task_ids, return_list)
3341+
3342+ def test_get_task(self):
3343+ '''
3344+ Tests the get_task function
3345+ '''
3346+ self.assertEqual(self.datastore.get_task(str(uuid.uuid4())), None)
3347+ task = self.datastore.new_task()
3348+ self.assertTrue(isinstance(self.datastore.get_task(task.get_id()),
3349+ GTG.core.task.Task))
3350+ self.assertEqual(self.datastore.get_task(task.get_id()), task)
3351+
3352+
3353+ def test_get_tagstore(self):
3354+ '''
3355+ Tests the get_tagstore function
3356+ '''
3357+ tagstore = self.datastore.get_tagstore()
3358+ self.assertTrue(isinstance(tagstore, GTG.core.tagstore.TagStore))
3359+
3360+ def test_get_requester(self):
3361+ '''
3362+ Tests the get_requester function
3363+ '''
3364+ requester = self.datastore.get_requester()
3365+ self.assertTrue(isinstance(requester, GTG.core.requester.Requester))
3366+
3367+ def test_get_tasks_tree(self):
3368+ '''
3369+ Tests the get_tasks_tree function
3370+ '''
3371+ tasks_tree = self.datastore.get_tasks_tree()
3372+ self.assertTrue(isinstance(tasks_tree, GTG.core.tree.Tree))
3373+
3374+ def test_push_task(self):
3375+ '''
3376+ Tests the push_task function
3377+ '''
3378+ task_ids = []
3379+ for i in xrange(1, 10):
3380+ tid = str(uuid.uuid4())
3381+ if tid not in task_ids:
3382+ task_ids.append(tid)
3383+ task = self.datastore.task_factory(tid)
3384+ return_value1 = self.datastore.push_task(task)
3385+ self.assertTrue(return_value1)
3386+ #we do it twice, but it should be pushed only once if it's
3387+ # working correctly (the second should be discarded)
3388+ return_value2 = self.datastore.push_task(task)
3389+ self.assertFalse(return_value2)
3390+ stored_tasks = self.datastore.get_all_tasks()
3391+ task_ids.sort()
3392+ stored_tasks.sort()
3393+ self.assertEqual(task_ids, stored_tasks)
3394+
3395+ def test_register_backend(self):
3396+ '''
3397+ Tests the register_backend function. It also tests the
3398+ get_all_backends and get_backend function as a side effect
3399+ '''
3400+ #create a simple backend dictionary
3401+ backend = FakeBackend(enabled = True)
3402+ tasks_in_backend_count = int(random.random() * 20)
3403+ for temp in xrange(0, tasks_in_backend_count):
3404+ backend.fake_add_random_task()
3405+ backend_dic = {'backend': backend, 'pid': 'a'}
3406+ self.datastore.register_backend(backend_dic)
3407+ all_backends = self.datastore.get_all_backends(disabled = True)
3408+ self.assertEqual(len(all_backends), 1)
3409+ registered_backend = self.datastore.get_backend(backend.get_id())
3410+ self.assertEqual(backend.get_id(), registered_backend.get_id())
3411+ self.assertTrue(isinstance(registered_backend, \
3412+ GTG.core.datastore.TaskSource))
3413+ self.assertTrue(registered_backend.is_enabled())
3414+ self.assertEqual(registered_backend.fake_get_initialized_count(), 1)
3415+ #we give some time for the backend to push all its tasks
3416+ time.sleep(1)
3417+ self.assertEqual(len(self.datastore.get_all_tasks()), \
3418+ tasks_in_backend_count)
3419+
3420+ #same test, disabled backend
3421+ backend = FakeBackend(enabled = False)
3422+ for temp in xrange(1, int(random.random() * 20)):
3423+ backend.fake_add_random_task()
3424+ backend_dic = {'backend': backend, 'pid':'b'}
3425+ self.datastore.register_backend(backend_dic)
3426+ all_backends = self.datastore.get_all_backends(disabled = True)
3427+ self.assertEqual(len(all_backends), 2)
3428+ all_backends = self.datastore.get_all_backends(disabled = False)
3429+ self.assertEqual(len(all_backends), 1)
3430+ registered_backend = self.datastore.get_backend(backend.get_id())
3431+ self.assertEqual(backend.get_id(), registered_backend.get_id())
3432+ self.assertTrue(isinstance(registered_backend, \
3433+ GTG.core.datastore.TaskSource))
3434+ self.assertFalse(registered_backend.is_enabled())
3435+ self.assertEqual(registered_backend.fake_get_initialized_count(), 0)
3436+ #we give some time for the backend to push all its tasks (is
3437+ #shouldn't, since it's disabled, but we give time anyway
3438+ time.sleep(1)
3439+ self.assertEqual(len(self.datastore.get_all_tasks()), \
3440+ tasks_in_backend_count)
3441+
3442+ def test_set_backend_enabled(self):
3443+ '''
3444+ Tests the set_backend_enabled function
3445+ '''
3446+ enabled_backend = FakeBackend(enabled = True)
3447+ disabled_backend = FakeBackend(enabled = False)
3448+ self.datastore.register_backend({'backend': enabled_backend, \
3449+ 'pid': str(uuid.uuid4()), \
3450+ GenericBackend.KEY_DEFAULT_BACKEND: False})
3451+ self.datastore.register_backend({'backend': disabled_backend,\
3452+ 'pid': str(uuid.uuid4()), \
3453+ GenericBackend.KEY_DEFAULT_BACKEND: False})
3454+ #enabling an enabled backend
3455+ self.datastore.set_backend_enabled(enabled_backend.get_id(), True)
3456+ self.assertEqual(enabled_backend.fake_get_initialized_count(), 1)
3457+ self.assertTrue(enabled_backend.is_enabled())
3458+ #disabling a disabled backend
3459+ self.datastore.set_backend_enabled(disabled_backend.get_id(), False)
3460+ self.assertEqual(disabled_backend.fake_get_initialized_count(), 0)
3461+ self.assertFalse(disabled_backend.is_enabled())
3462+ #disabling an enabled backend
3463+ self.datastore.set_backend_enabled(enabled_backend.get_id(), False)
3464+ self.assertEqual(enabled_backend.fake_get_initialized_count(), 1)
3465+ self.assertFalse(enabled_backend.is_enabled())
3466+ time.sleep(1)
3467+# #enabling a disabled backend
3468+# self.datastore.set_backend_enabled(disabled_backend.get_id(), True)
3469+# self.assertEqual(disabled_backend.fake_get_initialized_count(), 1)
3470+# self.assertTrue(disabled_backend.is_enabled())
3471+
3472+ def test_remove_backend(self):
3473+ '''
3474+ Tests the remove_backend function
3475+ '''
3476+ enabled_backend = FakeBackend(enabled = True)
3477+ disabled_backend = FakeBackend(enabled = False)
3478+ self.datastore.register_backend({'backend': enabled_backend, \
3479+ 'pid': str(uuid.uuid4()), \
3480+ GenericBackend.KEY_DEFAULT_BACKEND: False})
3481+ self.datastore.register_backend({'backend': disabled_backend,\
3482+ 'pid': str(uuid.uuid4()), \
3483+ GenericBackend.KEY_DEFAULT_BACKEND: False})
3484+ #removing an enabled backend
3485+ self.datastore.remove_backend(enabled_backend.get_id())
3486+ self.assertFalse(enabled_backend.is_enabled())
3487+ self.assertTrue(enabled_backend.fake_is_purged())
3488+ self.assertEqual( \
3489+ len(self.datastore.get_all_backends(disabled = True)), 1)
3490+ #removing a disabled backend
3491+ self.datastore.remove_backend(disabled_backend.get_id())
3492+ self.assertFalse(disabled_backend.is_enabled())
3493+ self.assertTrue(disabled_backend.fake_is_purged())
3494+ self.assertEqual( \
3495+ len(self.datastore.get_all_backends(disabled = True)), 0)
3496+
3497+ def test_flush_all_tasks(self):
3498+ '''
3499+ Tests the flush_all_tasks function
3500+ '''
3501+ #we add some tasks in the datastore
3502+ tasks_in_datastore_count = 10 #int(random.random() * 20)
3503+ for temp in xrange(0, tasks_in_datastore_count):
3504+ self.datastore.new_task()
3505+ datastore_stored_tids = self.datastore.get_all_tasks()
3506+ self.assertEqual(tasks_in_datastore_count, len(datastore_stored_tids))
3507+
3508+ #we enable a backend
3509+ backend = FakeBackend(enabled = True)
3510+ self.datastore.register_backend({'backend': backend, 'pid': 'a'})
3511+ #we wait for the signal storm to wear off
3512+ time.sleep(5)
3513+ #we sync
3514+ self.datastore.get_backend(backend.get_id()).sync()
3515+ #and we inject task in the backend
3516+ tasks_in_backend_count = 5 #int(random.random() * 20)
3517+ for temp in xrange(0, tasks_in_backend_count):
3518+ backend.fake_add_random_task()
3519+ backend_stored_tids = backend.fake_get_task_ids()
3520+ self.assertEqual(tasks_in_backend_count, len(backend_stored_tids))
3521+ self.datastore.flush_all_tasks(backend.get_id())
3522+ #we wait for the signal storm to wear off
3523+ time.sleep(2)
3524+ #we sync
3525+ self.datastore.get_backend(backend.get_id()).sync()
3526+ all_tasks_count = tasks_in_backend_count + tasks_in_datastore_count
3527+ new_datastore_stored_tids = self.datastore.get_all_tasks()
3528+ new_backend_stored_tids = backend.fake_get_task_ids()
3529+ self.assertEqual(len(new_backend_stored_tids), all_tasks_count)
3530+ self.assertEqual(len(new_datastore_stored_tids), all_tasks_count)
3531+ new_datastore_stored_tids.sort()
3532+ new_backend_stored_tids.sort()
3533+ self.assertEqual(new_backend_stored_tids, new_datastore_stored_tids)
3534+
3535+
3536+
3537+def test_suite():
3538+ CoreConfig().set_data_dir("./test_data")
3539+ CoreConfig().set_conf_dir("./test_data")
3540+ return unittest.TestLoader().loadTestsFromTestCase(TestDatastore)
3541+
3542+
3543+
3544+class FakeBackend(unittest.TestCase):
3545+ '''
3546+ Mimics the behavior of a simple backend. Just used for testing
3547+ '''
3548+
3549+ def __init__(self, enabled = True):
3550+ self.enabled = enabled
3551+ self.initialized_count = 0
3552+ self.tasks_ids = []
3553+ self.backend_id = str(uuid.uuid4())
3554+ self.purged = False
3555+
3556+ def is_enabled(self):
3557+ return self.enabled
3558+
3559+ def initialize(self):
3560+ self.initialized_count += 1
3561+ self.enabled = True
3562+
3563+ def queue_set_task(self, task):
3564+ self.tasks_ids.append(task.get_id())
3565+
3566+ def has_task(self, task_id):
3567+ return task_id in self.tasks_ids
3568+
3569+ def queue_remove_task(self, task_id):
3570+ self.tasks_ids.remove(task_id)
3571+
3572+ def get_id(self):
3573+ return self.backend_id
3574+
3575+ def start_get_tasks(self):
3576+ for task_id in self.tasks_ids:
3577+ self.datastore.push_task(self.datastore.task_factory(task_id))
3578+
3579+ def quit(self, disabled = False):
3580+ self.enabled = not disabled
3581+
3582+ def purge(self):
3583+ self.purged = True
3584+
3585+ def is_default(self):
3586+ return True
3587+
3588+ def set_parameter(self, param_name, param_value):
3589+ pass
3590+
3591+ def get_attached_tags(self):
3592+ return [CoreConfig.ALLTASKS_TAG]
3593+
3594+ def register_datastore(self, datastore):
3595+ self.datastore = datastore
3596+
3597+ ##########################################################################
3598+ # The following are used just for testing, they're not present inside a
3599+ # normal backend
3600+ ##########################################################################
3601+
3602+ def fake_get_initialized_count(self):
3603+ return self.initialized_count
3604+
3605+ def fake_get_task_ids(self):
3606+ return self.tasks_ids
3607+
3608+ def fake_add_random_task(self):
3609+ self.tasks_ids.append(str(uuid.uuid4()))
3610+
3611+ def fake_is_purged(self):
3612+ return self.purged
3613
3614=== modified file 'GTG/tests/test_tagstore.py'
3615--- GTG/tests/test_tagstore.py 2010-05-29 12:50:20 +0000
3616+++ GTG/tests/test_tagstore.py 2010-06-23 01:06:26 +0000
3617@@ -23,6 +23,7 @@
3618
3619 from GTG.core.tagstore import Tag
3620 from GTG.core.datastore import DataStore
3621+from GTG.core import CoreConfig
3622
3623
3624
3625@@ -117,4 +118,6 @@
3626 self.assertEqual(0, len(save_calls))
3627
3628 def test_suite():
3629+ CoreConfig().set_data_dir("./test_data")
3630+ CoreConfig().set_conf_dir("./test_data")
3631 return unittest.TestLoader().loadTestsFromTestCase(TestTag)
3632
3633=== modified file 'GTG/tests/test_taskviewserial.py'
3634--- GTG/tests/test_taskviewserial.py 2010-06-11 14:30:12 +0000
3635+++ GTG/tests/test_taskviewserial.py 2010-06-23 01:06:26 +0000
3636@@ -27,6 +27,7 @@
3637 import unittest
3638
3639 from GTG.gtk.editor import taskviewserial
3640+from GTG.core import CoreConfig
3641
3642 class GtgBackendsUniTests(unittest.TestCase):
3643 """Tests for GTG backends."""
3644@@ -47,4 +48,6 @@
3645
3646
3647 def test_suite():
3648+ CoreConfig().set_data_dir("./test_data")
3649+ CoreConfig().set_conf_dir("./test_data")
3650 return unittest.TestLoader().loadTestsFromName(__name__)
3651
3652=== modified file 'GTG/tests/test_tree.py'
3653--- GTG/tests/test_tree.py 2010-02-21 19:21:18 +0000
3654+++ GTG/tests/test_tree.py 2010-06-23 01:06:26 +0000
3655@@ -22,6 +22,9 @@
3656 import unittest
3657
3658 from GTG.core.tree import Tree,TreeNode
3659+from GTG.core import CoreConfig
3660+
3661+
3662
3663 class TestTree(unittest.TestCase):
3664 """Tests for `Tree`."""
3665@@ -119,4 +122,6 @@
3666
3667
3668 def test_suite():
3669+ CoreConfig().set_data_dir("./test_data")
3670+ CoreConfig().set_conf_dir("./test_data")
3671 return unittest.TestLoader().loadTestsFromName(__name__)
3672
3673=== added file 'GTG/tools/borg.py'
3674--- GTG/tools/borg.py 1970-01-01 00:00:00 +0000
3675+++ GTG/tools/borg.py 2010-06-23 01:06:26 +0000
3676@@ -0,0 +1,33 @@
3677+# -*- coding: utf-8 -*-
3678+# -----------------------------------------------------------------------------
3679+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
3680+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
3681+#
3682+# This program is free software: you can redistribute it and/or modify it under
3683+# the terms of the GNU General Public License as published by the Free Software
3684+# Foundation, either version 3 of the License, or (at your option) any later
3685+# version.
3686+#
3687+# This program is distributed in the hope that it will be useful, but WITHOUT
3688+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
3689+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
3690+# details.
3691+#
3692+# You should have received a copy of the GNU General Public License along with
3693+# this program. If not, see <http://www.gnu.org/licenses/>.
3694+# -----------------------------------------------------------------------------
3695+
3696+
3697+
3698+class Borg(object):
3699+ """
3700+ This pattern ensures that all instances of a particular class share
3701+ the same state (just inherit this class to have it working)
3702+ """
3703+
3704+ _borg_state = {}
3705+
3706+ def __init__(self):
3707+ self.__dict__ = self._borg_state
3708+
3709+
3710
3711=== added file 'GTG/tools/keyring.py'
3712--- GTG/tools/keyring.py 1970-01-01 00:00:00 +0000
3713+++ GTG/tools/keyring.py 2010-06-23 01:06:26 +0000
3714@@ -0,0 +1,48 @@
3715+# -*- coding: utf-8 -*-
3716+# -----------------------------------------------------------------------------
3717+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
3718+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
3719+#
3720+# This program is free software: you can redistribute it and/or modify it under
3721+# the terms of the GNU General Public License as published by the Free Software
3722+# Foundation, either version 3 of the License, or (at your option) any later
3723+# version.
3724+#
3725+# This program is distributed in the hope that it will be useful, but WITHOUT
3726+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
3727+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
3728+# details.
3729+#
3730+# You should have received a copy of the GNU General Public License along with
3731+# this program. If not, see <http://www.gnu.org/licenses/>.
3732+# -----------------------------------------------------------------------------
3733+
3734+import gnomekeyring
3735+
3736+from GTG.tools.borg import Borg
3737+
3738+
3739+
3740+class Keyring(Borg):
3741+
3742+
3743+ def __init__(self):
3744+ super(Keyring, self).__init__()
3745+ if not hasattr(self, "keyring"):
3746+ self.keyring = gnomekeyring.get_default_keyring_sync()
3747+
3748+ def set_password(self, name, password, userid = ""):
3749+ return gnomekeyring.item_create_sync(
3750+ self.keyring,
3751+ gnomekeyring.ITEM_GENERIC_SECRET,
3752+ name,
3753+ {"backend": name},
3754+ password,
3755+ True)
3756+
3757+ def get_password(self, item_id):
3758+ try:
3759+ item_info = gnomekeyring.item_get_info_sync(self.keyring, item_id)
3760+ return item_info.get_secret()
3761+ except (gnomekeyring.DeniedError, gnomekeyring.NoMatchError):
3762+ return ""
3763
3764=== modified file 'GTG/tools/logger.py'
3765--- GTG/tools/logger.py 2010-03-02 06:32:31 +0000
3766+++ GTG/tools/logger.py 2010-06-23 01:06:26 +0000
3767@@ -41,6 +41,7 @@
3768 #Shouldn't be needed, but the following line makes sure that
3769 # this is a Singleton.
3770 self.__dict__['_Debug__logger'] = Debug.__logger
3771+ self.debugging_mode = False
3772
3773 def __init_logger(self):
3774 Debug.__logger = logging.getLogger('gtg_logger')
3775@@ -60,5 +61,10 @@
3776 """ Delegates to the real logger """
3777 return setattr(Debug.__logger, attr, value)
3778
3779+ def set_debugging_mode(self, value):
3780+ self.debugging_mode = value
3781+ def is_debugging_mode(self):
3782+ return self.debugging_mode
3783+
3784 #The singleton itself
3785 Log = Debug()
3786
3787=== added file 'GTG/tools/synchronized.py'
3788--- GTG/tools/synchronized.py 1970-01-01 00:00:00 +0000
3789+++ GTG/tools/synchronized.py 2010-06-23 01:06:26 +0000
3790@@ -0,0 +1,14 @@
3791+from __future__ import with_statement
3792+from threading import Lock
3793+
3794+def synchronized(fun):
3795+ the_lock = Lock()
3796+
3797+ def fwrap(function):
3798+ def newFunction(*args, **kw):
3799+ with the_lock:
3800+ return function(*args, **kw)
3801+
3802+ return newFunction
3803+
3804+ return fwrap(fun)
3805
3806=== modified file 'GTG/tools/taskxml.py'
3807--- GTG/tools/taskxml.py 2010-06-18 16:36:17 +0000
3808+++ GTG/tools/taskxml.py 2010-06-23 01:06:26 +0000
3809@@ -60,7 +60,15 @@
3810 cur_tags = xmlnode.getAttribute("tags").replace(' ','').split(",")
3811 if "" in cur_tags: cur_tags.remove("")
3812 for tag in cur_tags: cur_task.tag_added(saxutils.unescape(tag))
3813-
3814+
3815+ #REMOTE TASK IDS
3816+ remote_ids_list = xmlnode.getElementsByTagName("task-remote-ids")
3817+ for remote_id in remote_ids_list:
3818+ if remote_id.childNodes:
3819+ node = remote_id.childNodes[0]
3820+ backend_id = node.firstChild.nodeValue
3821+ remote_task_id = node.childNodes[1].firstChild.nodeValue
3822+ task.add_remote_id(backend_id, remote_task_id)
3823 return cur_task
3824
3825 #Task as parameter the doc where to put the XML node
3826@@ -99,4 +107,18 @@
3827 #t_xml.appendChild(element.firstChild)
3828 cleanxml.addTextNode(doc,t_xml,"content",desc)
3829 #self.__write_textnode(doc,t_xml,"content",t.get_text())
3830+
3831+ #REMOTE TASK IDS
3832+ remote_ids_element = doc.createElement("task-remote-ids")
3833+ t_xml.appendChild(remote_ids_element)
3834+ remote_ids_dict = task.get_remote_ids()
3835+ for backend_id, task_id in remote_ids_dict.iteritems():
3836+ backend_element = doc.createElement('backend')
3837+ remote_ids_element.appendChild(backend_element)
3838+ backend_element.appendChild(doc.createTextNode(backend_id))
3839+ task_element = doc.createElement('task-id')
3840+ backend_element.appendChild(task_element)
3841+ task_element.appendChild(doc.createTextNode(task_id))
3842+
3843+
3844 return t_xml
3845
3846=== added file 'GTG/tools/testingmode.py'
3847--- GTG/tools/testingmode.py 1970-01-01 00:00:00 +0000
3848+++ GTG/tools/testingmode.py 2010-06-23 01:06:26 +0000
3849@@ -0,0 +1,16 @@
3850+from GTG.tools.borg import Borg
3851+
3852+
3853+
3854+class TestingMode(Borg):
3855+
3856+
3857+ def set_testing_mode(self, value):
3858+ self._testing_mode = value
3859+
3860+ def get_testing_mode(self):
3861+ try:
3862+ return self._testing_mode
3863+ except:
3864+ return False
3865+
3866
3867=== modified file 'Makefile'
3868--- Makefile 2010-03-01 01:43:33 +0000
3869+++ Makefile 2010-06-23 01:06:26 +0000
3870@@ -35,7 +35,7 @@
3871 # Check for coding standard violations & flakes.
3872 lint: pyflakes pep8
3873
3874-.PHONY: check lint pyflakes pep8 apidocs
3875+.PHONY: check lint pyflakes pep8 apidocs edit-apidocs clean
3876
3877 #Ignore the exit code in pyflakes, so that pep8 is always run when "make lint"
3878 .IGNORE: pyflakes
3879
3880=== modified file 'scripts/debug.sh'
3881--- scripts/debug.sh 2010-06-21 09:44:23 +0000
3882+++ scripts/debug.sh 2010-06-23 01:06:26 +0000
3883@@ -42,6 +42,7 @@
3884 if [ $norun -eq 0 ]; then
3885 if [ $profile -eq 1 ]; then
3886 python -m cProfile -o gtg.prof ./gtg
3887+ python ./scripts/profile_interpret.sh
3888 else
3889 ./gtg $args
3890 fi
3891
3892=== added file 'scripts/profile_interpret.sh'
3893--- scripts/profile_interpret.sh 1970-01-01 00:00:00 +0000
3894+++ scripts/profile_interpret.sh 2010-06-23 01:06:26 +0000
3895@@ -0,0 +1,4 @@
3896+#!/usr/bin/env python
3897+import pstats
3898+p = pstats.Stats('gtg.prof')
3899+p.strip_dirs().sort_stats("cumulative").print_stats(20)

Subscribers

People subscribed via source and target branches

to status/vote changes: