GTG

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

Proposed by Luca Invernizzi
Status: Merged
Merged at revision: 825
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+28258@code.launchpad.net

This proposal supersedes a proposal from 2010-06-22.

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.
Revision history for this message
Lionel Dricot (ploum-deactivatedaccount) wrote : Posted in a previous version of this proposal

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
Revision history for this message
Lionel Dricot (ploum-deactivatedaccount) :
review: Approve
Revision history for this message
Lionel Dricot (ploum-deactivatedaccount) wrote :

just realized that the unit test for datastore isn't working for me :

Exception RuntimeError: 'maximum recursion depth exceeded while calling a Python object' in <type 'exceptions.AttributeError'> ignored
......................<?xml version="1.0" ?>
<project/>

Revision history for this message
Luca Invernizzi (invernizzi) wrote :

On Wed, Jun 23, 2010 at 2:07 PM, Lionel Dricot <email address hidden> wrote:
> just realized that the unit test for datastore isn't working for me :
>
>
> Exception RuntimeError: 'maximum recursion depth exceeded while calling a Python object' in <type 'exceptions.AttributeError'> ignored
> ......................<?xml version="1.0" ?>
> <project/>
>
Fixed!
> --
> https://code.launchpad.net/~gtg-user/gtg/multibackends-halfgsoc_merge/+merge/28258
> You proposed lp:~gtg-user/gtg/multibackends-halfgsoc_merge for merging.
>

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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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:19:23 +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: