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
=== modified file 'GTG/backends/__init__.py'
--- GTG/backends/__init__.py 2010-03-01 01:55:12 +0000
+++ GTG/backends/__init__.py 2010-06-23 01:19:23 +0000
@@ -23,14 +23,173 @@
23(like on the hard disk or on the internet)23(like on the hard disk or on the internet)
24and to read projects from this medium24and to read projects from this medium
25"""25"""
26#26
27#Current backends are :27import sys
28#28import uuid
29# localfile.py : store and read a local XML file29import os.path
30#30
31#31from GTG.tools.logger import Log
32#32from GTG.tools.borg import Borg
33# this __init__.py should not be empty. It should list the available backends33from GTG.backends.genericbackend import GenericBackend
34#34from GTG.core import firstrun_tasks
35#http://www.faqts.com/knowledge_base/view.phtml/aid/4221/fid/53835from GTG.tools import cleanxml
36#http://www.python.org/doc/2.1.3/tut/node8.html36from GTG.core import CoreConfig
37
38
39
40class BackendFactory(Borg):
41 '''
42 This class holds the information about the backend types.
43 Since it's about types, all information is static. The instantiated
44 backends are handled in the Datastore.
45 It is a Borg for what matters its only state (_backend_modules),
46 since it makes no sense of keeping multiple instances of this.
47 '''
48
49
50 BACKEND_PREFIX = "backend_"
51
52 def __init__(self):
53 """
54 Creates a dictionary of the currently available backend modules
55 """
56 super(BackendFactory, self).__init__()
57 if hasattr(self, "backend_modules"):
58 #This object has already been constructed
59 return
60 self.backend_modules = {}
61 #Look for backends in the GTG/backends dir
62 this_dir = os.path.dirname(__file__)
63 backend_files = filter(lambda f: f.endswith(".py") and \
64 f[ : len(self.BACKEND_PREFIX)] == self.BACKEND_PREFIX , \
65 os.listdir(this_dir))
66 #Create module names
67 module_names = map(lambda f: f.replace(".py",""), backend_files)
68 Log.debug("Backends found: " + str(module_names))
69 #Load backend modules
70 for module_name in module_names:
71 extended_module_name = "GTG.backends." + module_name
72 try:
73 __import__(extended_module_name)
74 except ImportError, exception:
75 #Something is wrong with this backend, skipping
76 Log.debug("Backend %s could not be loaded: %s" % \
77 (module_name, str(exception)))
78 continue
79 self.backend_modules[module_name] = \
80 sys.modules[extended_module_name]
81
82 def get_backend(self, backend_name):
83 '''
84 Returns the backend module for the backend matching
85 backend_name. Else, returns none
86 '''
87 if backend_name in self.backend_modules:
88 return self.backend_modules[backend_name]
89 else:
90 Log.debug("Trying to load backend %s, but failed!" % backend_name)
91 return None
92
93 def get_all_backends(self):
94 '''
95 Returns a dictionary containing all the backends types
96 '''
97 return self.backend_modules
98
99 def get_new_backend_dict(self, backend_name, additional_parameters = {}):
100 '''
101 Constructs a new backend initialization dictionary. In more
102 exact terms, creates a dictionary, containing all the necessary
103 entries to initialize a backend.
104 '''
105 if not self.backend_modules.has_key(backend_name):
106 return None
107 dic = {}
108 module = self.get_backend(backend_name)
109 #Different pids are necessary to discern between backends of the same
110 # type
111 parameters = module.Backend.get_static_parameters()
112 #we all the parameters and their default values in dic
113 for param_name, param_dic in parameters.iteritems():
114 dic[param_name] = param_dic[GenericBackend.PARAM_DEFAULT_VALUE]
115 dic["pid"] = str(uuid.uuid4())
116 dic["module"] = module.Backend.get_name()
117 for param_name, param_value in additional_parameters.iteritems():
118 dic[param_name] = param_value
119 dic["backend"] = module.Backend(dic)
120 return dic
121
122 def restore_backend_from_xml(self, dic):
123 '''
124 Function restoring a backend from its xml description.
125 dic should be a dictionary containing at least the key
126 - "module", with the module name
127 - "xmlobject", with its xml description.
128 Every other key is passed as-is to the backend, as parameter.
129
130 Returns the backend instance, or None is something goes wrong
131 '''
132 if not "module" in dic or not "xmlobject" in dic:
133 Log.debug ("Malformed backend configuration found! %s" % \
134 dic)
135 module = self.get_backend(dic["module"])
136 if module == None:
137 Log.debug ("could not load module for backend %s" % \
138 dic["module"])
139 return None
140 #we pop the xml object, as it will be redundant when the parameters
141 # are set directly in the dict
142 xp = dic.pop("xmlobject")
143 #Building the dictionary
144 parameters_specs = module.Backend.get_static_parameters()
145 dic["pid"] = str(xp.getAttribute("pid"))
146 for param_name, param_dic in parameters_specs.iteritems():
147 if xp.hasAttribute(param_name):
148 #we need to convert the parameter to the right format.
149 # we fetch the format from the static_parameters
150 param_type = param_dic[GenericBackend.PARAM_TYPE]
151 param_value = GenericBackend.cast_param_type_from_string( \
152 xp.getAttribute(param_name), param_type)
153 dic[param_name] = param_value
154 #We put the backend itself in the dict
155 dic["backend"] = module.Backend(dic)
156 return dic["backend"]
157
158 def get_saved_backends_list(self):
159 backends_dic = self._read_backend_configuration_file()
160
161 #Retrocompatibility: default backend has changed name
162 for dic in backends_dic:
163 if dic["module"] == "localfile":
164 dic["module"] = "backend_localfile"
165 dic["pid"] = str(uuid.uuid4())
166 dic["need_conversion"] = \
167 dic["xmlobject"].getAttribute("filename")
168
169 #Now that the backend list is build, we will construct them
170 for dic in backends_dic:
171 self.restore_backend_from_xml(dic)
172 #If no backend available, we create a new using localfile. Xmlobject
173 # will be filled in by the backend
174 if len(backends_dic) == 0:
175 dic = BackendFactory().get_new_backend_dict( \
176 "backend_localfile")
177 dic["backend"].this_is_the_first_run(firstrun_tasks.populate())
178 backends_dic.append(dic)
179 return backends_dic
180
181 def _read_backend_configuration_file(self):
182 '''
183 Reads the file describing the current backend configuration (project.xml)
184 and returns a list of dictionaries, each containing:
185 - the xml object defining the backend characteristics under
186 "xmlobject"
187 - the name of the backend under "module"
188 '''
189 # Read configuration file, if it does not exist, create one
190 datafile = os.path.join(CoreConfig().get_data_dir(), CoreConfig.DATA_FILE)
191 doc, configxml = cleanxml.openxmlfile(datafile, "config")
192 xmlproject = doc.getElementsByTagName("backend")
193 # collect configured backends
194 return [{"xmlobject": xp, \
195 "module": xp.getAttribute("module")} for xp in xmlproject]
37196
=== added file 'GTG/backends/backend_localfile.py'
--- GTG/backends/backend_localfile.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/backend_localfile.py 2010-06-23 01:19:23 +0000
@@ -0,0 +1,181 @@
1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Gettings Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#
6# This program is free software: you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# -----------------------------------------------------------------------------
19
20'''
21Localfile is a read/write backend that will store your tasks in an XML file
22This file will be in your $XDG_DATA_DIR/gtg folder.
23'''
24
25import os
26import uuid
27
28from GTG.backends.genericbackend import GenericBackend
29from GTG.core import CoreConfig
30from GTG.tools import cleanxml, taskxml
31from GTG import _
32
33
34
35class Backend(GenericBackend):
36
37
38 DEFAULT_PATH = CoreConfig().get_data_dir() #default path for filenames
39
40
41 #Description of the backend (mainly it's data we show the user, only the
42 # name is used internally. Please note that BACKEND_NAME and
43 # BACKEND_ICON_NAME should *not* be translated.
44 _general_description = { \
45 GenericBackend.BACKEND_NAME: "backend_localfile", \
46 GenericBackend.BACKEND_HUMAN_NAME: _("Local File"), \
47 GenericBackend.BACKEND_AUTHORS: ["Lionel Dricot", \
48 "Luca Invernizzi"], \
49 GenericBackend.BACKEND_TYPE: GenericBackend.TYPE_READWRITE, \
50 GenericBackend.BACKEND_DESCRIPTION: \
51 _("Your tasks are saved in a text file (XML format). " + \
52 " This is the most basic and the default way " + \
53 "for GTG to save your tasks."),\
54 }
55
56 #parameters to configure a new backend of this type.
57 #NOTE: should we always give back a different default filename? it can be
58 # done, but I'd like to keep this backend simple, so that it can be
59 # used as example (invernizzi)
60 _static_parameters = { \
61 "path": { \
62 GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING, \
63 GenericBackend.PARAM_DEFAULT_VALUE: \
64 os.path.join(DEFAULT_PATH, "gtg_tasks-%s.xml" %(uuid.uuid4()))
65 }}
66
67 def _get_default_filename_path(self, filename = None):
68 '''
69 Generates a default path with a random filename
70 @param filename: specify a filename
71 '''
72 if not filename:
73 filename = "gtg_tasks-%s.xml" % (uuid.uuid4())
74 return os.path.join(self.DEFAULT_PATH, filename)
75
76 def __init__(self, parameters):
77 """
78 Instantiates a new backend.
79
80 @param parameters: should match the dictionary returned in
81 get_parameters. Anyway, the backend should care if one expected
82 value is None or does not exist in the dictionary.
83 @firstrun: only needed for the default backend. It should be
84 omitted for all other backends.
85 """
86 super(Backend, self).__init__(parameters)
87 self.tids = []
88 #####RETROCOMPATIBILIY
89 #NOTE: retrocompatibility. We convert "filename" to "path"
90 # and we forget about "filename"
91 if "need_conversion" in parameters:
92 parameters["path"] = os.path.join(self.DEFAULT_PATH, \
93 parameters["need_conversion"])
94 del parameters["need_conversion"]
95 if not self.KEY_DEFAULT_BACKEND in parameters:
96 parameters[self.KEY_DEFAULT_BACKEND] = True
97 ####
98 self.doc, self.xmlproj = cleanxml.openxmlfile( \
99 self._parameters["path"], "project")
100
101 def initialize(self):
102 super(Backend, self).initialize()
103 self.doc, self.xmlproj = cleanxml.openxmlfile( \
104 self._parameters["path"], "project")
105
106 def this_is_the_first_run(self, xml):
107 #Create the default tasks for the first run.
108 #We write the XML object in a file
109 self._parameters[self.KEY_DEFAULT_BACKEND] = True
110 cleanxml.savexml(self._parameters["path"], xml)
111 self.doc, self.xmlproj = cleanxml.openxmlfile(\
112 self._parameters["path"], "project")
113 self._parameters[self.KEY_DEFAULT_BACKEND] = True
114
115 def start_get_tasks(self):
116 '''
117 Once this function is launched, the backend can start pushing
118 tasks to gtg parameters.
119
120 @return: start_get_tasks() might not return or finish
121 '''
122 tid_list = []
123 for node in self.xmlproj.childNodes:
124 tid = node.getAttribute("id")
125 if tid not in self.tids:
126 self.tids.append(tid)
127 task = self.datastore.task_factory(tid)
128 if task:
129 task = taskxml.task_from_xml(task, node)
130 self.datastore.push_task(task)
131
132 def set_task(self, task):
133 tid = task.get_id()
134 existing = None
135 #First, we find the existing task from the treenode
136 for node in self.xmlproj.childNodes:
137 if node.getAttribute("id") == tid:
138 existing = node
139 t_xml = taskxml.task_to_xml(self.doc, task)
140 modified = False
141 #We then replace the existing node
142 if existing and t_xml:
143 #We will write only if the task has changed
144 if t_xml.toxml() != existing.toxml():
145 self.xmlproj.replaceChild(t_xml, existing)
146 modified = True
147 #If the node doesn't exist, we create it
148 # (it might not be the case in all backends
149 else:
150 self.xmlproj.appendChild(t_xml)
151 modified = True
152 #In this particular backend, we write all the tasks
153 #This is inherent to the XML file backend
154 if modified and self._parameters["path"] and self.doc :
155 cleanxml.savexml(self._parameters["path"], self.doc)
156
157 def remove_task(self, tid):
158 ''' Completely remove the task with ID = tid '''
159 for node in self.xmlproj.childNodes:
160 if node.getAttribute("id") == tid:
161 self.xmlproj.removeChild(node)
162 if tid in self.tids:
163 self.tids.remove(tid)
164 cleanxml.savexml(self._parameters["path"], self.doc)
165
166
167 def quit(self, disable = False):
168 '''
169 Called when GTG quits or disconnects the backend.
170 '''
171 super(Backend, self).quit(disable)
172
173 def save_state(self):
174 cleanxml.savexml(self._parameters["path"], self.doc, backup=True)
175
176 def get_number_of_tasks(self):
177 '''
178 Returns the number of tasks stored in the backend. Doesn't need to be a
179 fast function, is called just for the UI
180 '''
181 return len(self.tids)
0182
=== added file 'GTG/backends/backendsignals.py'
--- GTG/backends/backendsignals.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/backendsignals.py 2010-06-23 01:19:23 +0000
@@ -0,0 +1,125 @@
1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Getting Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#
6# This program is free software: you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# -----------------------------------------------------------------------------
19
20import gobject
21
22from GTG.tools.borg import Borg
23
24
25
26class BackendSignals(Borg):
27 '''
28 This class handles the signals that involve backends.
29 In particular, it's a wrapper Borg class around a _BackendSignalsGObject
30 class, and all method of the wrapped class can be used as if they were part
31 of this class
32 '''
33
34 #error codes to send along with the BACKEND_FAILED signal
35 ERRNO_AUTHENTICATION = "authentication failed"
36 ERRNO_NETWORK = "network is down"
37 ERRNO_DBUS = "Dbus interface cannot be connected"
38
39 def __init__(self):
40 super(BackendSignals, self).__init__()
41 if hasattr(self, "_gobject"):
42 return
43 self._gobject = _BackendSignalsGObject()
44
45 def __getattr__(self, attr):
46 return getattr(self._gobject, attr)
47
48
49class _BackendSignalsGObject(gobject.GObject):
50
51 #signal name constants
52 BACKEND_STATE_TOGGLED = 'backend-state-toggled' #emitted when a
53 #backend is
54 #enabled or disabled
55 BACKEND_RENAMED = 'backend-renamed' #emitted when a backend is renamed
56 BACKEND_ADDED = 'backend-added'
57 BACKEND_REMOVED = 'backend-added' #when a backend is deleted
58 DEFAULT_BACKEND_LOADED = 'default-backend-loaded' #emitted after all
59 # tasks have been
60 # loaded from the
61 # default backend
62 BACKEND_FAILED = 'backend-failed' #something went wrong with a backend
63 BACKEND_SYNC_STARTED = 'backend-sync-started'
64 BACKEND_SYNC_ENDED = 'backend-sync-ended'
65
66 __string_signal__ = (gobject.SIGNAL_RUN_FIRST, \
67 gobject.TYPE_NONE, (str, ))
68 __none_signal__ = (gobject.SIGNAL_RUN_FIRST, \
69 gobject.TYPE_NONE, ( ))
70 __string_string_signal__ = (gobject.SIGNAL_RUN_FIRST, \
71 gobject.TYPE_NONE, (str, str, ))
72
73 __gsignals__ = {BACKEND_STATE_TOGGLED : __string_signal__, \
74 BACKEND_RENAMED : __string_signal__, \
75 BACKEND_ADDED : __string_signal__, \
76 BACKEND_REMOVED : __string_signal__, \
77 BACKEND_SYNC_STARTED : __string_signal__, \
78 BACKEND_SYNC_ENDED : __string_signal__, \
79 DEFAULT_BACKEND_LOADED: __none_signal__, \
80 BACKEND_FAILED : __string_string_signal__}
81
82 def __init__(self):
83 super(_BackendSignalsGObject, self).__init__()
84 self.backends_currently_syncing = []
85
86 ############# Signals #########
87 #connecting to signals is fine, but keep an eye if you should emit them.
88 #As a general rule, signals should only be emitted in the GenericBackend
89 #class
90
91 def _emit_signal(self, signal, backend_id):
92 gobject.idle_add(self.emit, signal, backend_id)
93
94 def backend_state_changed(self, backend_id):
95 self._emit_signal(self.BACKEND_STATE_TOGGLED, backend_id)
96
97 def backend_renamed(self, backend_id):
98 self._emit_signal(self.BACKEND_RENAMED, backend_id)
99
100 def backend_added(self, backend_id):
101 self._emit_signal(self.BACKEND_ADDED, backend_id)
102
103 def backend_removed(self, backend_id):
104 self._emit_signal(self.BACKEND_REMOVED, backend_id)
105
106 def default_backend_loaded(self):
107 gobject.idle_add(self.emit, self.DEFAULT_BACKEND_LOADED)
108
109 def backend_failed(self, backend_id, error_code):
110 gobject.idle_add(self.emit, self.BACKEND_FAILED, backend_id, \
111 error_code)
112
113 def backend_sync_started(self, backend_id):
114 self._emit_signal(self.BACKEND_SYNC_STARTED, backend_id)
115 self.backends_currently_syncing.append(backend_id)
116
117 def backend_sync_ended(self, backend_id):
118 self._emit_signal(self.BACKEND_SYNC_ENDED, backend_id)
119 try:
120 self.backends_currently_syncing.remove(backend_id)
121 except:
122 pass
123
124 def is_backend_syncing(self, backend_id):
125 return backend_id in self.backends_currently_syncing
0126
=== added file 'GTG/backends/genericbackend.py'
--- GTG/backends/genericbackend.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/genericbackend.py 2010-06-23 01:19:23 +0000
@@ -0,0 +1,571 @@
1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Gettings Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#
6# This program is free software: you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# -----------------------------------------------------------------------------
19
20'''
21FIXME: document!
22'''
23
24import os
25import sys
26import errno
27import pickle
28import threading
29from collections import deque
30
31from GTG.backends.backendsignals import BackendSignals
32from GTG.tools.keyring import Keyring
33from GTG.core import CoreConfig
34from GTG.tools.logger import Log
35
36
37
38
39class GenericBackend(object):
40 '''
41 Base class for every backend. It's a little more than an interface which
42 methods have to be redefined in order for the backend to run.
43 '''
44
45
46 #BACKEND TYPE DESCRIPTION
47 #"_general_description" is a dictionary that holds the values for the
48 # following keys:
49 BACKEND_NAME = "name" #the backend gtg internal name (doesn't change in
50 # translations, *must be unique*)
51 BACKEND_HUMAN_NAME = "human-friendly-name" #The name shown to the user
52 BACKEND_DESCRIPTION = "description" #A short description of the backend
53 BACKEND_AUTHORS = "authors" #a list of strings
54 BACKEND_TYPE = "type"
55 #BACKEND_TYPE is one of:
56 TYPE_READWRITE = "readwrite"
57 TYPE_READONLY = "readonly"
58 TYPE_IMPORT = "import"
59 TYPE_EXPORT = "export"
60 _general_description = {}
61
62
63 #"static_parameters" is a dictionary of dictionaries, each of which
64 #representing a parameter needed to configure the backend.
65 #each "sub-dictionary" is identified by this a key representing its name.
66 #"static_parameters" will be part of the definition of each
67 #particular backend.
68 # Each dictionary contains the keys:
69 #PARAM_DESCRIPTION = "description" #short description (shown to the user
70 # during configuration)
71 PARAM_DEFAULT_VALUE = "default_value" # its default value
72 PARAM_TYPE = "type"
73 #PARAM_TYPE is one of the following (changing this changes the way
74 # the user can configure the parameter)
75 TYPE_PASSWORD = "password" #the real password is stored in the GNOME
76 # keyring
77 # This is just a key to find it there
78 TYPE_STRING = "string" #generic string, nothing fancy is done
79 TYPE_INT = "int" #edit box can contain only integers
80 TYPE_BOOL = "bool" #checkbox is shown
81 TYPE_LIST_OF_STRINGS = "liststring" #list of strings. the "," character is
82 # prohibited in strings
83 _static_parameters = {}
84
85 def initialize(self):
86 '''
87 Called each time it is enabled again (including on backend creation).
88 Please note that a class instance for each disabled backend *is*
89 created, but it's not initialized.
90 Optional.
91 NOTE: make sure to call super().initialize()
92 '''
93 for module_name in self.get_required_modules():
94 sys.modules[module_name]= __import__(module_name)
95 self._parameters[self.KEY_ENABLED] = True
96 self._is_initialized = True
97 #we signal that the backend has been enabled
98 self._signal_manager.backend_state_changed(self.get_id())
99
100 def start_get_tasks(self):
101 '''
102 Once this function is launched, the backend can start pushing
103 tasks to gtg parameters.
104
105 @return: start_get_tasks() might not return or finish
106 '''
107 raise NotImplemented()
108
109 def set_task(self, task):
110 '''
111 Save the task in the backend. If the task id is new for the
112 backend, then a new task must be created.
113 '''
114 pass
115
116 def remove_task(self, tid):
117 ''' Completely remove the task with ID = tid '''
118 pass
119
120 def has_task(self, tid):
121 '''Returns true if the backend has an internal idea
122 of the task corresponding to the tid. False otherwise'''
123 raise NotImplemented()
124
125 def new_task_id(self):
126 '''
127 Returns an available ID for a new task so that a task with this ID
128 can be saved with set_task later.
129 '''
130 raise NotImplemented()
131
132 def this_is_the_first_run(self, xml):
133 '''
134 Steps to execute if it's the first time the backend is run. Optional.
135 '''
136 pass
137
138 def purge(self):
139 '''
140 Called when a backend will be removed from GTG. Useful for removing
141 configuration files. Optional.
142 '''
143 pass
144
145 def get_number_of_tasks(self):
146 '''
147 Returns the number of tasks stored in the backend. Doesn't need to be a
148 fast function, is called just for the UI
149 '''
150 raise NotImplemented()
151
152 @staticmethod
153 def get_required_modules():
154 return []
155
156 def quit(self, disable = False):
157 '''
158 Called when GTG quits or disconnects the backend. Remember to execute
159 also this function when quitting. If disable is True, the backend won't
160 be automatically loaded at next GTG start
161 '''
162 self._is_initialized = False
163 if disable:
164 self._parameters[self.KEY_ENABLED] = False
165 #we signal that we have been disabled
166 self._signal_manager.backend_state_changed(self.get_id())
167 self._signal_manager.backend_sync_ended(self.get_id())
168 syncing_thread = threading.Thread(target = self.sync).run()
169
170 def save_state(self):
171 '''
172 It's the last function executed on a quitting backend, after the
173 pending actions have been done.
174 Useful to ensure that the state is saved in a consistent manner
175 '''
176 pass
177
178###############################################################################
179###### You don't need to reimplement the functions below this line ############
180###############################################################################
181
182 #These parameters are common to all backends and necessary.
183 # They will be added automatically to your _static_parameters list
184 #NOTE: for now I'm disabling changing the default backend. Once it's all
185 # set up, we will see about that (invernizzi)
186 KEY_DEFAULT_BACKEND = "Default"
187 KEY_ENABLED = "Enabled"
188 KEY_HUMAN_NAME = BACKEND_HUMAN_NAME
189 KEY_ATTACHED_TAGS = "attached-tags"
190 KEY_USER = "user"
191 KEY_PID = "pid"
192 ALLTASKS_TAG = "gtg-tags-all" #IXME: moved here to avoid circular imports
193
194 _static_parameters_obligatory = { \
195 KEY_DEFAULT_BACKEND: { \
196 PARAM_TYPE: TYPE_BOOL, \
197 PARAM_DEFAULT_VALUE: False, \
198 }, \
199 KEY_HUMAN_NAME: { \
200 PARAM_TYPE: TYPE_STRING, \
201 PARAM_DEFAULT_VALUE: "", \
202 }, \
203 KEY_USER: { \
204 PARAM_TYPE: TYPE_STRING, \
205 PARAM_DEFAULT_VALUE: "", \
206 }, \
207 KEY_PID: { \
208 PARAM_TYPE: TYPE_STRING, \
209 PARAM_DEFAULT_VALUE: "", \
210 }, \
211 KEY_ENABLED: { \
212 PARAM_TYPE: TYPE_BOOL, \
213 PARAM_DEFAULT_VALUE: False, \
214 }}
215
216 _static_parameters_obligatory_for_rw = { \
217 KEY_ATTACHED_TAGS: {\
218 PARAM_TYPE: TYPE_LIST_OF_STRINGS, \
219 PARAM_DEFAULT_VALUE: [ALLTASKS_TAG], \
220 }}
221
222 #Handy dictionary used in type conversion (from string to type)
223 _type_converter = {TYPE_STRING: str,
224 TYPE_INT: int,
225 }
226
227 @classmethod
228 def _get_static_parameters(cls):
229 '''
230 Helper method, used to obtain the full list of the static_parameters
231 (user configured and default ones)
232 '''
233 if hasattr(cls, "_static_parameters"):
234 temp_dic = cls._static_parameters_obligatory.copy()
235 if cls._general_description[cls.BACKEND_TYPE] == cls.TYPE_READWRITE:
236 for key, value in \
237 cls._static_parameters_obligatory_for_rw.iteritems():
238 temp_dic[key] = value
239 for key, value in cls._static_parameters.iteritems():
240 temp_dic[key] = value
241 return temp_dic
242 else:
243 raise NotImplemented("_static_parameters not implemented for " + \
244 "backend %s" % type(cls))
245
246 def __init__(self, parameters):
247 """
248 Instantiates a new backend. Please note that this is called also for
249 disabled backends. Those are not initialized, so you might want to check
250 out the initialize() function.
251 """
252 if self.KEY_DEFAULT_BACKEND not in parameters:
253 parameters[self.KEY_DEFAULT_BACKEND] = True
254 if parameters[self.KEY_DEFAULT_BACKEND] or \
255 (not self.KEY_ATTACHED_TAGS in parameters and \
256 self._general_description[self.BACKEND_TYPE] \
257 == self.TYPE_READWRITE):
258 parameters[self.KEY_ATTACHED_TAGS] = [self.ALLTASKS_TAG]
259 self._parameters = parameters
260 self._signal_manager = BackendSignals()
261 self._is_initialized = False
262 if Log.is_debugging_mode():
263 self.timer_timestep = 5
264 else:
265 self.timer_timestep = 1
266 self.to_set_timer = None
267 self.please_quit = False
268 self.to_set = deque()
269 self.to_remove = deque()
270
271 def get_attached_tags(self):
272 '''
273 Returns the list of tags which are handled by this backend
274 '''
275 if hasattr(self._parameters, self.KEY_DEFAULT_BACKEND) and \
276 self._parameters[self.KEY_DEFAULT_BACKEND]:
277 return [self.ALLTASKS_TAG]
278 try:
279 return self._parameters[self.KEY_ATTACHED_TAGS]
280 except:
281 return []
282
283 def set_attached_tags(self, tags):
284 '''
285 Changes the set of attached tags
286 '''
287 self._parameters[self.KEY_ATTACHED_TAGS] = tags
288
289 @classmethod
290 def get_static_parameters(cls):
291 """
292 Returns a dictionary of parameters necessary to create a backend.
293 """
294 return cls._get_static_parameters()
295
296 def get_parameters(self):
297 """
298 Returns a dictionary of the current parameters.
299 """
300 return self._parameters
301
302 def set_parameter(self, parameter, value):
303 self._parameters[parameter] = value
304
305 @classmethod
306 def get_name(cls):
307 """
308 Returns the name of the backend as it should be displayed in the UI
309 """
310 return cls._get_from_general_description(cls.BACKEND_NAME)
311
312 @classmethod
313 def get_description(cls):
314 """Returns a description of the backend"""
315 return cls._get_from_general_description(cls.BACKEND_DESCRIPTION)
316
317 @classmethod
318 def get_type(cls):
319 """Returns the backend type(readonly, r/w, import, export) """
320 return cls._get_from_general_description(cls.BACKEND_TYPE)
321
322 @classmethod
323 def get_authors(cls):
324 '''
325 returns the backend author(s)
326 '''
327 return cls._get_from_general_description(cls.BACKEND_AUTHORS)
328
329 @classmethod
330 def _get_from_general_description(cls, key):
331 '''
332 Helper method to extract values from cls._general_description.
333 Raises an exception if the key is missing (helpful for developers
334 adding new backends).
335 '''
336 if key in cls._general_description:
337 return cls._general_description[key]
338 else:
339 raise NotImplemented("Key %s is missing from " +\
340 "'self._general_description' of a backend (%s). " +
341 "Please add the corresponding value" % (key, type(cls)))
342
343 @classmethod
344 def cast_param_type_from_string(cls, param_value, param_type):
345 '''
346 Parameters are saved in a text format, so we have to cast them to the
347 appropriate type on loading. This function does exactly that.
348 '''
349 #FIXME: we could use pickle (dumps and loads), at least in some cases
350 # (invernizzi)
351 if param_type in cls._type_converter:
352 return cls._type_converter[param_type](param_value)
353 elif param_type == cls.TYPE_BOOL:
354 if param_value == "True":
355 return True
356 elif param_value == "False":
357 return False
358 else:
359 raise Exception("Unrecognized bool value '%s'" %
360 param_type)
361 elif param_type == cls.TYPE_PASSWORD:
362 if param_value == -1:
363 return None
364 return Keyring().get_password(int(param_value))
365 elif param_type == cls.TYPE_LIST_OF_STRINGS:
366 the_list = param_value.split(",")
367 if not isinstance(the_list, list):
368 the_list = [the_list]
369 return the_list
370 else:
371 raise NotImplemented("I don't know what type is '%s'" %
372 param_type)
373
374 def cast_param_type_to_string(self, param_type, param_value):
375 '''
376 Inverse of cast_param_type_from_string
377 '''
378 if param_type == GenericBackend.TYPE_PASSWORD:
379 if param_value == None:
380 return str(-1)
381 else:
382 return str(Keyring().set_password(
383 "GTG stored password -" + self.get_id(), param_value))
384 elif param_type == GenericBackend.TYPE_LIST_OF_STRINGS:
385 if param_value == []:
386 return ""
387 return reduce(lambda a, b: a + "," + b, param_value)
388 else:
389 return str(param_value)
390
391 def get_id(self):
392 '''
393 returns the backends id, used in the datastore for indexing backends
394 '''
395 return self.get_name() + "@" + self._parameters["pid"]
396
397 @classmethod
398 def get_human_default_name(cls):
399 '''
400 returns the user friendly default backend name.
401 '''
402 return cls._general_description[cls.BACKEND_HUMAN_NAME]
403
404 def get_human_name(self):
405 '''
406 returns the user customized backend name. If the user hasn't
407 customized it, returns the default one
408 '''
409 if self.KEY_HUMAN_NAME in self._parameters and \
410 self._parameters[self.KEY_HUMAN_NAME] != "":
411 return self._parameters[self.KEY_HUMAN_NAME]
412 else:
413 return self.get_human_default_name()
414
415 def set_human_name(self, name):
416 '''
417 sets a custom name for the backend
418 '''
419 self._parameters[self.KEY_HUMAN_NAME] = name
420 #we signal the change
421 self._signal_manager.backend_renamed(self.get_id())
422
423 def is_enabled(self):
424 '''
425 Returns if the backend is enabled
426 '''
427 return self.get_parameters()[GenericBackend.KEY_ENABLED] or \
428 self.is_default()
429
430 def is_default(self):
431 '''
432 Returns if the backend is enabled
433 '''
434 return self.get_parameters()[GenericBackend.KEY_DEFAULT_BACKEND]
435
436 def is_initialized(self):
437 '''
438 Returns if the backend is up and running
439 '''
440 return self._is_initialized
441
442 def get_parameter_type(self, param_name):
443 try:
444 return self.get_static_parameters()[param_name][self.PARAM_TYPE]
445 except KeyError:
446 return None
447
448 def register_datastore(self, datastore):
449 self.datastore = datastore
450
451###############################################################################
452### HELPER FUNCTIONS ##########################################################
453###############################################################################
454
455 def _store_pickled_file(self, path, data):
456 '''
457 A helper function to save some object in a file.
458 @param path: a relative path. A good choice is
459 "backend_name/object_name"
460 @param data: the object
461 '''
462 path = os.path.join(CoreConfig().get_data_dir(), path)
463 #mkdir -p
464 try:
465 os.makedirs(os.path.dirname(path))
466 except OSError, exception:
467 if exception.errno != errno.EEXIST:
468 raise
469 #saving
470 #try:
471 with open(path, 'wb') as file:
472 pickle.dump(data, file)
473 #except pickle.PickleError:
474 #pass
475
476 def _load_pickled_file(self, path, default_value = None):
477 '''
478 A helper function to load some object from a file.
479 @param path: the relative path of the file
480 @param default_value: the value to return if the file is missing or
481 corrupt
482 @returns object: the needed object, or default_value
483 '''
484 path = os.path.join(CoreConfig().get_data_dir(), path)
485 if not os.path.exists(path):
486 return default_value
487 else:
488 try:
489 with open(path, 'r') as file:
490 return pickle.load(file)
491 except pickle.PickleError:
492 return default_value
493
494###############################################################################
495### THREADING #################################################################
496###############################################################################
497
498 def __try_launch_setting_thread(self):
499 '''
500 Helper function to launch the setting thread, if it's not running.
501 '''
502 if self.to_set_timer == None and self.is_enabled():
503 self.to_set_timer = threading.Timer(self.timer_timestep, \
504 self.launch_setting_thread)
505 self.to_set_timer.start()
506
507 def launch_setting_thread(self):
508 '''
509 This function is launched as a separate thread. Its job is to perform
510 the changes that have been issued from GTG core. In particular, for
511 each task in the self.to_set queue, a task has to be modified or to be
512 created (if the tid is new), and for each task in the self.to_remove
513 queue, a task has to be deleted
514 '''
515 while not self.please_quit:
516 try:
517 task = self.to_set.pop()
518 except IndexError:
519 break
520 #time.sleep(4)
521 tid = task.get_id()
522 if tid not in self.to_remove:
523 self.set_task(task)
524
525 while not self.please_quit:
526 try:
527 tid = self.to_remove.pop()
528 except IndexError:
529 break
530 self.remove_task(tid)
531 #we release the weak lock
532 self.to_set_timer = None
533
534 def queue_set_task(self, task):
535 ''' Save the task in the backend. '''
536 tid = task.get_id()
537 if task not in self.to_set and tid not in self.to_remove:
538 self.to_set.appendleft(task)
539 self.__try_launch_setting_thread()
540
541 def queue_remove_task(self, tid):
542 '''
543 Queues task to be removed.
544 @param tid: The Task ID of the task to be removed
545 '''
546 if tid not in self.to_remove:
547 self.to_remove.appendleft(tid)
548 self.__try_launch_setting_thread()
549 return None
550
551 def sync(self):
552 '''
553 Helper method. Forces the backend to perform all the pending changes.
554 It is usually called upon quitting the backend.
555 '''
556 #FIXME: this function should become part of the r/w r/o generic class
557 # for backends
558 if self.to_set_timer != None:
559 self.please_quit = True
560 try:
561 self.to_set_timer.cancel()
562 except:
563 pass
564 try:
565 self.to_set_timer.join(5)
566 except:
567 pass
568 self.please_quit = False
569 self.launch_setting_thread()
570 self.save_state()
571
0572
=== removed file 'GTG/backends/localfile.py'
--- GTG/backends/localfile.py 2010-04-26 23:12:57 +0000
+++ GTG/backends/localfile.py 1970-01-01 00:00:00 +0000
@@ -1,176 +0,0 @@
1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Gettings Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#
6# This program is free software: you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# -----------------------------------------------------------------------------
19
20'''
21Localfile is a read/write backend that will store your tasks in an XML file
22This file will be in your $XDG_DATA_DIR/gtg folder.
23'''
24
25import os
26import uuid
27
28from GTG.core import CoreConfig
29from GTG.tools import cleanxml, taskxml
30
31def get_name():
32 """Returns the name of the backend as it should be displayed in the UI"""
33 return "Local File"
34
35def get_description():
36 """Returns a description of the backend"""
37 return "Your tasks are saved in an XML file located in your HOME folder"
38
39def get_parameters():
40 """
41 Returns a dictionary of parameters. Keys should be strings and
42 are the name of the parameter.
43 Values are string with value : string, password, int, bool
44 and are an information about the type of the parameter
45 Currently, only string is supported.
46 """
47 dic = {}
48 dic["filename"] = "string"
49 return dic
50
51def get_features():
52 """Returns a dict of features supported by this backend"""
53 return {}
54
55def get_type():
56 """Type is one of : readwrite, readonly, import, export"""
57 return "readwrite"
58
59class Backend:
60 def __init__(self, parameters, firstrunxml=None):
61 """
62 Instantiates a new backend.
63
64 @param parameters: should match the dictionary returned in
65 get_parameters. Anyway, the backend should care if one expected value is
66 None or does not exist in the dictionary.
67 @firstrun: only needed for the default backend. It should be omitted for
68 all other backends.
69 """
70 self.tids = []
71 self.pid = 1
72 if "filename" in parameters:
73 zefile = parameters["filename"]
74 #If zefile is None, we create a new file
75 else:
76 zefile = "%s.xml" %(uuid.uuid4())
77 parameters["filename"] = zefile
78 #For the day we want to open files somewhere else
79 default_folder = True
80 if default_folder:
81 self.zefile = os.path.join(CoreConfig.DATA_DIR, zefile)
82 self.filename = zefile
83 else:
84 self.zefile = zefile
85 self.filename = zefile
86 #Create the default tasks for the first run.
87 #We write the XML object in a file
88 if firstrunxml and not os.path.exists(zefile):
89 #shutil.copy(firstrunfile,self.zefile)
90 cleanxml.savexml(self.zefile, firstrunxml)
91 self.doc, self.xmlproj = cleanxml.openxmlfile(self.zefile, "project")
92
93 def start_get_tasks(self,push_task_func,task_factory_func):
94 '''
95 Once this function is launched, the backend can start pushing
96 tasks to gtg parameters.
97
98 @push_task_func: a function that takes a Task as parameter
99 and pushes it into GTG.
100 @task_factory_func: a function that takes a tid as parameter
101 and returns a Task object with the given pid.
102
103 @return: start_get_tasks() might not return or finish
104 '''
105 tid_list = []
106 for node in self.xmlproj.childNodes:
107 #time.sleep(2)
108 tid = node.getAttribute("id")
109 if tid not in self.tids:
110 self.tids.append(tid)
111 task = task_factory_func(tid)
112 task = taskxml.task_from_xml(task,node)
113 push_task_func(task)
114 #print "#### finishing pushing tasks"
115
116 def set_task(self, task):
117 ''' Save the task in the backend '''
118 #time.sleep(4)
119 tid = task.get_id()
120 if tid not in self.tids:
121 self.tids.append(tid)
122 existing = None
123 #First, we find the existing task from the treenode
124 for node in self.xmlproj.childNodes:
125 if node.getAttribute("id") == tid:
126 existing = node
127 t_xml = taskxml.task_to_xml(self.doc, task)
128 modified = False
129 #We then replace the existing node
130 if existing and t_xml:
131 #We will write only if the task has changed
132 if t_xml.toxml() != existing.toxml():
133 self.xmlproj.replaceChild(t_xml, existing)
134 modified = True
135 #If the node doesn't exist, we create it
136 # (it might not be the case in all backends
137 else:
138 self.xmlproj.appendChild(t_xml)
139 modified = True
140 #In this particular backend, we write all the tasks
141 #This is inherent to the XML file backend
142 if modified and self.zefile and self.doc :
143 cleanxml.savexml(self.zefile, self.doc)
144 return None
145
146 def remove_task(self, tid):
147 ''' Completely remove the task with ID = tid '''
148 for node in self.xmlproj.childNodes:
149 if node.getAttribute("id") == tid:
150 self.xmlproj.removeChild(node)
151 if tid in self.tids:
152 self.tids.remove(tid)
153 cleanxml.savexml(self.zefile, self.doc)
154
155 def new_task_id(self):
156 '''
157 Returns an available ID for a new task so that a task with this ID
158 can be saved with set_task later.
159 If None, then GTG will create a new ID by itself.
160 The ID cannot contain the character "@".
161 '''
162 k = 0
163 pid = self.pid
164 newid = "%s@%s" %(k, pid)
165 while str(newid) in self.tids:
166 k += 1
167 newid = "%s@%s" %(k, pid)
168 self.tids.append(newid)
169 return newid
170
171 def quit(self):
172 '''
173 Called when GTG quits or disconnects the backend.
174 (Subclasses might pass here)
175 '''
176 cleanxml.savexml(self.zefile, self.doc, backup=True)
1770
=== modified file 'GTG/core/__init__.py'
--- GTG/core/__init__.py 2010-05-26 09:54:42 +0000
+++ GTG/core/__init__.py 2010-06-23 01:19:23 +0000
@@ -41,136 +41,80 @@
41#=== IMPORT ====================================================================41#=== IMPORT ====================================================================
42import os42import os
43from xdg.BaseDirectory import xdg_data_home, xdg_config_home43from xdg.BaseDirectory import xdg_data_home, xdg_config_home
44from GTG.tools import cleanxml
45from configobj import ConfigObj44from configobj import ConfigObj
4645from GTG.tools.testingmode import TestingMode
47from GTG.core import firstrun_tasks46
4847import GTG
49class CoreConfig:48from GTG.tools.logger import Log
49from GTG.tools.borg import Borg
50
51
52
53class CoreConfig(Borg):
50 54
55
51 #The projects and tasks are of course DATA !56 #The projects and tasks are of course DATA !
52 #We then use XDG_DATA for them57 #We then use XDG_DATA for them
53 #Don't forget the "/" at the end.58 #Don't forget the "/" at the end.
54 DATA_DIR = os.path.join(xdg_data_home,'gtg/')
55 DATA_FILE = "projects.xml"59 DATA_FILE = "projects.xml"
56 CONF_DIR = os.path.join(xdg_config_home,'gtg/')
57 CONF_FILE = "gtg.conf"60 CONF_FILE = "gtg.conf"
58 TASK_CONF_FILE = "tasks.conf"61 TASK_CONF_FILE = "tasks.conf"
59 conf_dict = None62 conf_dict = None
60 #DBUS63 #DBUS
61 BUSNAME = "org.GTG"64 BUSNAME = "org.GTG"
62 BUSINTERFACE = "/org/GTG"65 BUSINTERFACE = "/org/GTG"
66 #TAGS
67 ALLTASKS_TAG = "gtg-tags-all"
6368
64 def __init__(self):69 def __init__(self):
65 if not os.path.exists(self.CONF_DIR):70 if hasattr(self, 'data_dir'):
66 os.makedirs(self.CONF_DIR)71 #Borg has already been initialized
67 if not os.path.exists(self.DATA_DIR):72 return
68 os.makedirs(self.DATA_DIR)73 if TestingMode().get_testing_mode():
69 if not os.path.exists(self.CONF_DIR + self.CONF_FILE):74 #we avoid running tests in the user data dir
70 f = open(self.CONF_DIR + self.CONF_FILE, "w")75 self.data_dir = '/tmp/GTG_TESTS/data'
71 f.close()76 self.conf_dir = '/tmp/GTG_TESTS/conf'
72 if not os.path.exists(self.CONF_DIR + self.TASK_CONF_FILE):77 else:
73 f = open(self.CONF_DIR + self.TASK_CONF_FILE, "w")78 self.data_dir = os.path.join(xdg_data_home,'gtg/')
74 f.close()79 self.conf_dir = os.path.join(xdg_config_home,'gtg/')
75 for file in [self.CONF_DIR + self.CONF_FILE,80 if not os.path.exists(self.conf_dir):
76 self.CONF_DIR + self.TASK_CONF_FILE]:81 os.makedirs(self.conf_dir)
82 if not os.path.exists(self.data_dir):
83 os.makedirs(self.data_dir)
84 if not os.path.exists(self.conf_dir + self.CONF_FILE):
85 f = open(self.conf_dir + self.CONF_FILE, "w")
86 f.close()
87 if not os.path.exists(self.conf_dir + self.TASK_CONF_FILE):
88 f = open(self.conf_dir + self.TASK_CONF_FILE, "w")
89 f.close()
90 for file in [self.conf_dir + self.CONF_FILE,
91 self.conf_dir + self.TASK_CONF_FILE]:
77 if not ((file, os.R_OK) and os.access(file, os.W_OK)):92 if not ((file, os.R_OK) and os.access(file, os.W_OK)):
78 raise Exception("File " + file + \93 raise Exception("File " + file + \
79 " is a configuration file for gtg, but it " + \94 " is a configuration file for gtg, but it "
80 "cannot be read or written. Please check it")95 "cannot be read or written. Please check it")
81 self.conf_dict = ConfigObj(self.CONF_DIR + self.CONF_FILE)96 self.conf_dict = ConfigObj(self.conf_dir + self.CONF_FILE)
82 self.task_conf_dict = ConfigObj(self.CONF_DIR + self.TASK_CONF_FILE)97 self.task_conf_dict = ConfigObj(self.conf_dir + self.TASK_CONF_FILE)
83 98
84 def save_config(self):99 def save(self):
100 ''' Saves the configuration of CoreConfig '''
85 self.conf_dict.write()101 self.conf_dict.write()
86 self.task_conf_dict.write()102 self.task_conf_dict.write()
87 103
88 def get_backends_list(self):104 def get_icons_directories(self):
89 backend_fn = []105 '''
90106 Returns the directories containing the icons
91 # Check if config dir exists, if not create it107 '''
92 if not os.path.exists(self.DATA_DIR):108 return [GTG.DATA_DIR, os.path.join(GTG.DATA_DIR, "icons")]
93 os.makedirs(self.DATA_DIR)109
94110 def get_data_dir(self):
95 # Read configuration file, if it does not exist, create one111 return self.data_dir
96 datafile = self.DATA_DIR + self.DATA_FILE112
97 doc, configxml = cleanxml.openxmlfile(datafile,"config") #pylint: disable-msg=W0612113 def set_data_dir(self, path):
98 xmlproject = doc.getElementsByTagName("backend")114 self.data_dir = path
99 # collect configred backends115
100 pid = 1116 def get_conf_dir(self):
101 for xp in xmlproject:117 return self.conf_dir
102 dic = {}118
103 #We have some retrocompatibility code119 def set_conf_dir(self, path):
104 #A backend without the module attribute is pre-rev.105120 self.conf_dir = path
105 #and is considered as "filename"
106 if xp.hasAttribute("module"):
107 dic["module"] = str(xp.getAttribute("module"))
108 dic["pid"] = str(xp.getAttribute("pid"))
109 #The following "else" could be removed later
110 else:
111 dic["module"] = "localfile"
112 dic["pid"] = str(pid)
113
114 dic["xmlobject"] = xp
115 pid += 1
116 backend_fn.append(dic)
117
118 firstrun = False
119 #If no backend available, we create a new using localfile
120 if len(backend_fn) == 0:
121 dic = {}
122 dic["module"] = "localfile"
123 dic["pid"] = "1"
124 backend_fn.append(dic)
125 firstrun = True
126
127 #Now that the backend list is build, we will construct them
128 #Remember that b is a dictionnary
129 for b in backend_fn:
130 #We dynamically import modules needed
131 module_name = "GTG.backends.%s"%b["module"]
132 #FIXME : we should throw an error if the backend is not importable
133 module = __import__(module_name)
134 module = getattr(module, "backends")
135 classobj = getattr(module, b["module"])
136 b["parameters"] = classobj.get_parameters()
137 #If creating the default backend, we don't have the xmlobject yet
138 if "xmlobject" in b:
139 xp = b.pop("xmlobject")
140 #We will try to get the parameters
141 for key in b["parameters"]:
142 if xp.hasAttribute(key):
143 b[key] = str(xp.getAttribute(key))
144 if firstrun:
145 frx = firstrun_tasks.populate()
146 back = classobj.Backend(b,firstrunxml=frx)
147 else:
148 back = classobj.Backend(b)
149 #We put the backend itself in the dic
150 b["backend"] = back
151
152 return backend_fn
153
154
155 #If initial save, we don't close stuffs.
156 def save_datastore(self,ds,initial_save=False):
157 doc,xmlconfig = cleanxml.emptydoc("config")
158 for b in ds.get_all_backends():
159 param = b.get_parameters()
160 t_xml = doc.createElement("backend")
161 for key in param:
162 #We dont want parameters,backend,xmlobject
163 if key not in ["backend","parameters","xmlobject"]:
164 t_xml.setAttribute(str(key),str(param[key]))
165 #Saving all the projects at close
166 xmlconfig.appendChild(t_xml)
167 if not initial_save:
168 b.quit()
169
170 datafile = self.DATA_DIR + self.DATA_FILE
171 cleanxml.savexml(datafile,doc,backup=True)
172
173 #Saving the tagstore
174 if not initial_save:
175 ts = ds.get_tagstore()
176 ts.save()
177121
=== modified file 'GTG/core/datastore.py'
--- GTG/core/datastore.py 2010-05-26 08:55:45 +0000
+++ GTG/core/datastore.py 2010-06-23 01:19:23 +0000
@@ -18,267 +18,560 @@
18# -----------------------------------------------------------------------------18# -----------------------------------------------------------------------------
1919
20"""20"""
21datastore contains a list of TagSource objects, which are proxies between a backend and the datastore itself21The DaataStore contains a list of TagSource objects, which are proxies
22between a backend and the datastore itself
22"""23"""
2324
24import threading25import threading
25import gobject26import uuid
26import time27import os.path
2728from collections import deque
28from GTG.core import tagstore, requester29
29from GTG.core.task import Task30from GTG.core import tagstore, requester
30from GTG.core.tree import Tree31from GTG.core.task import Task
3132from GTG.core.tree import Tree
3233from GTG.core import CoreConfig
33#Only the datastore should access to the backend34from GTG.tools.logger import Log
34DEFAULT_BACKEND = "1"35from GTG.backends.genericbackend import GenericBackend
35#If you want to debug a backend, it can be useful to disable the threads36from GTG.tools import cleanxml
36#Currently, it's python threads (and not idle_add, which is not useful)37from GTG.tools.keyring import Keyring
37THREADING = True38from GTG.backends.backendsignals import BackendSignals
3839from GTG.tools.synchronized import synchronized
3940from GTG.tools.borg import Borg
40class DataStore:41
41 """ A wrapper around a backend that provides an API for adding/removing tasks """42
43class DataStore(object):
44 '''
45 A wrapper around all backends that is responsible for keeping the backend
46 instances. It can enable, disable, register and destroy backends, and acts
47 as interface between the backends and GTG core.
48 You should not interface yourself directly with the DataStore: use the
49 Requester instead (which also sends signals as you issue commands).
50 '''
51
52
42 def __init__(self):53 def __init__(self):
43 """ Initializes a DataStore object """54 '''
44 self.backends = {}55 Initializes a DataStore object
56 '''
57 self.backends = {} #dictionary {backend_name_string: Backend instance}
45 self.open_tasks = Tree()58 self.open_tasks = Tree()
46# self.closed_tasks = Tree()
47 self.requester = requester.Requester(self)59 self.requester = requester.Requester(self)
48 self.tagstore = tagstore.TagStore(self.requester)60 self.tagstore = tagstore.TagStore(self.requester)
4961 self._backend_signals = BackendSignals()
50 def all_tasks(self):62 self.mutex = threading.RLock()
51 """63 self.is_default_backend_loaded = False
64 self._backend_signals.connect('default-backend-loaded', \
65 self._activate_non_default_backends)
66 self.filtered_datastore = FilteredDataStore(self)
67
68 ##########################################################################
69 ### Helper functions (get_ methods for Datastore embedded objects)
70 ##########################################################################
71
72 def get_tagstore(self):
73 '''
74 Helper function to obtain the Tagstore associated with this DataStore
75 @return GTG.core.tagstore.TagStore: the tagstore object
76 '''
77 return self.tagstore
78
79 def get_requester(self):
80 '''
81 Helper function to get the Requester associate with this DataStore
82 @returns GTG.core.requester.Requester: the requester associated with
83 this datastore
84 '''
85 return self.requester
86
87 def get_tasks_tree(self):
88 '''
89 Helper function to get a Tree with all the tasks contained in this
90 Datastore
91 @returns GTG.core.tree.Tree: a task tree (the main one)
92 '''
93 return self.open_tasks
94
95 ##########################################################################
96 ### Tasks functions
97 ##########################################################################
98
99 def get_all_tasks(self):
100 '''
52 Returns list of all keys of open tasks101 Returns list of all keys of open tasks
53 """102 @return a list of strings: a list of task ids
103 '''
54 return self.open_tasks.get_all_keys()104 return self.open_tasks.get_all_keys()
55105
56 def has_task(self, tid):106 def has_task(self, tid):
57 """107 '''
58 Returns true if the tid is among the open or closed tasks for108 Returns true if the tid is among the open or closed tasks for
59 this DataStore, False otherwise.109 this DataStore, False otherwise.
60 param tid: Task ID to search for110 @param tid: Task ID to search for
61 """111 @return bool: True if the task is present
62 return self.open_tasks.has_node(tid) #or self.closed_tasks.has_node(tid)112 '''
113 return self.open_tasks.has_node(tid)
63114
64 def get_task(self, tid):115 def get_task(self, tid):
65 """116 '''
66 Returns the internal task object for the given tid, or None if the117 Returns the internal task object for the given tid, or None if the
67 tid is not present in this DataStore.118 tid is not present in this DataStore.
68 @param tid: Task ID to retrieve119 @param tid: Task ID to retrieve
69 """120 @returns GTG.core.task.Task or None: whether the Task is present
70 if tid:121 or not
71 if self.has_task(tid):122 '''
72 task = self.__internal_get_task(tid)123 if self.has_task(tid):
73 else:124 return self.open_tasks.get_node(tid)
74 #print "no task %s" %tid
75 task = None
76 return task
77 else:125 else:
78 print "get_task should take a tid"126 Log.debug("requested non-existent task")
79 return None127 return None
80 128
81 def __internal_get_task(self, tid):129 def task_factory(self, tid, newtask = False):
82 return self.open_tasks.get_node(tid)130 '''
83# if toreturn == None:131 Instantiates the given task id as a Task object.
84# self.closed_tasks.get_node(tid)132 @param tid: a task id. Must be unique
85 #else:133 @param newtask: True if the task has never been seen before
86 #print "error : this task doesn't exist in either tree"134 @return Task: a Task instance
87 #pass135 '''
88 #we return None if the task doesn't exist136 return Task(tid, self.requester, newtask)
89# return toreturn137
90138 def new_task(self):
91 def delete_task(self, tid):
92 """
93 Deletes the given task entirely from this DataStore, and unlinks
94 it from the task's parent.
95 @return: True if task was deleted, or False if the tid was not
96 present in this DataStore.
97 """
98 if not tid or not self.has_task(tid):
99 return False
100
101 self.__internal_get_task(tid).delete()
102 uid, pid = tid.split('@') #pylint: disable-msg=W0612
103 back = self.backends[pid]
104 #Check that the task still exist. It might have been deleted
105 #by its parent a few line earlier :
106 if self.has_task(tid):
107 self.open_tasks.remove_node(tid)
108# self.closed_tasks.remove_node(tid)
109 back.remove_task(tid)
110 return True
111
112 def new_task(self,pid=None):
113 """139 """
114 Creates a blank new task in this DataStore.140 Creates a blank new task in this DataStore.
115 @param pid: (Optional) parent ID that this task should be a child of.141 New task is created in all the backends that collect all tasks (among
116 If not specified, the task will be a child of the default backend.142 them, the default backend). The default backend uses the same task id
143 in its own internal representation.
117 @return: The task object that was created.144 @return: The task object that was created.
118 """145 """
119 if not pid:146 task = self.task_factory(uuid.uuid4(), True)
120 pid = DEFAULT_BACKEND
121 newtid = self.backends[pid].new_task_id()
122 while self.has_task(newtid):
123 print "error : tid already exists"
124 newtid = self.backends[pid].new_task_id()
125 task = Task(newtid, self.requester,newtask=True)
126 self.open_tasks.add_node(task)147 self.open_tasks.add_node(task)
127 task.set_sync_func(self.backends[pid].set_task,callsync=False)
128 return task148 return task
129149
130 def get_tagstore(self):150 @synchronized
131 return self.tagstore151 def push_task(self, task, backend_capabilities = 'bypass for now'):
132152 '''
133 def get_requester(self):153 Adds the given task object to the task tree. In other words, registers
134 return self.requester154 the given task in the GTG task set.
135 155 @param task: A valid task object (a GTG.core.task.Task)
136 def get_tasks_tree(self):156 @return bool: True if the task has been accepted
137 """ return: Open tasks tree """157 '''
138 return self.open_tasks158
139 159 if self.has_task(task.get_id()):
140 def push_task(self,task):160 return False
141 """
142 Adds the given task object as a node to the open tasks tree.
143 @param task: A valid task object
144 """
145 tid = task.get_id()
146 if self.has_task(tid):
147 print "pushing an existing task. We should care about modifications"
148 else:161 else:
149 uid, pid = tid.split('@')
150 self.open_tasks.add_node(task)162 self.open_tasks.add_node(task)
151 task.set_loaded()163 task.set_loaded()
152 task.set_sync_func(self.backends[pid].set_task,callsync=False)164 if self.is_default_backend_loaded:
153 165 task.sync()
154 def task_factory(self,tid):166 return True
155 """167
156 Instantiates the given task id as a Task object.168 ##########################################################################
157 @param tid: The id of the task to instantiate169 ### Backends functions
158 @return: The task object instantiated for tid170 ##########################################################################
159 """171
160 task = None172 def get_all_backends(self, disabled = False):
161 if self.has_task(tid):173 """
162 print "error : tid already exists"174 returns list of all registered backends for this DataStore.
175 @param disabled: If disabled is True, attaches also the list of disabled backends
176 @return list: a list of TaskSource objects
177 """
178 #NOTE: consider cashing this result for speed.
179 result = []
180 for backend in self.backends.itervalues():
181 if backend.is_enabled() or disabled:
182 result.append(backend)
183 return result
184
185 def get_backend(self, backend_id):
186 '''
187 Returns a backend given its id
188 @param backend_id: a backend id
189 @returns GTG.core.datastore.TaskSource or None: the requested backend,
190 or none
191 '''
192 if backend_id in self.backends:
193 return self.backends[backend_id]
163 else:194 else:
164 task = Task(tid, self.requester, newtask=False)195 return None
165 return task
166
167196
168 def register_backend(self, dic):197 def register_backend(self, backend_dic):
169 """198 """
170 Registers a TaskSource as a backend for this DataStore199 Registers a TaskSource as a backend for this DataStore
171 @param dic: Dictionary object with a "backend" and "pid"200 @param backend_dic: Dictionary object containing all the
172 specified. dic["pid"] should be the parent ID to use201 parameters to initialize the backend (filename...). It should
173 with the backend specified in dic["backend"].202 also contain the backend class (under "backend"), and its unique
203 id (under "pid")
174 """204 """
175 if "backend" in dic:205 if "backend" in backend_dic:
176 pid = dic["pid"]206 if "pid" not in backend_dic:
177 backend = dic["backend"]207 Log.debug("registering a backend without pid.")
178 source = TaskSource(backend, dic)208 return None
179 self.backends[pid] = source209 backend = backend_dic["backend"]
180 #Filling the backend210 #Checking that is a new backend
181 #Doing this at start is more efficient than211 if backend.get_id() in self.backends:
182 #after the GUI is launched212 Log.debug("registering already registered backend")
183 source.start_get_tasks(self.push_task,self.task_factory)213 return None
214 source = TaskSource(requester = self.requester,
215 backend = backend,
216 datastore = self.filtered_datastore)
217 self.backends[backend.get_id()] = source
218 #we notify that a new backend is present
219 self._backend_signals.backend_added(backend.get_id())
220 #saving the backend in the correct dictionary (backends for enabled
221 # backends, disabled_backends for the disabled ones)
222 #this is useful for retro-compatibility
223 if not GenericBackend.KEY_ENABLED in backend_dic:
224 source.set_parameter(GenericBackend.KEY_ENABLED, True)
225 if not GenericBackend.KEY_DEFAULT_BACKEND in backend_dic:
226 source.set_parameter(GenericBackend.KEY_DEFAULT_BACKEND, True)
227 #if it's enabled, we initialize it
228 if source.is_enabled() and \
229 (self.is_default_backend_loaded or source.is_default()):
230 source.initialize(connect_signals = False)
231 #Filling the backend
232 #Doing this at start is more efficient than
233 #after the GUI is launched
234 source.start_get_tasks()
235 return source
184 else:236 else:
185 print "Register a dic without backend key: BUG"237 Log.debug("Tried to register a backend without a pid")
186238
187 def unregister_backend(self, backend):239 def _activate_non_default_backends(self, sender = None):
188 """ Unimplemented """240 '''
189 print "unregister backend %s not implemented" %backend241 Non-default backends have to wait until the default loads before
190242 being activated. This function is called after the first default
191 def get_all_backends(self):243 backend has loaded all its tasks.
192 """ returns list of all registered backends for this DataStore """244 '''
193 l = []245 if self.is_default_backend_loaded:
194 for key in self.backends:246 Log.debug("spurious call")
195 l.append(self.backends[key])247 return
196 return l248 self.is_default_backend_loaded = True
249 for backend in self.backends.itervalues():
250 if backend.is_enabled() and not backend.is_default():
251 backend.initialize()
252 backend.start_get_tasks()
253 self.flush_all_tasks(backend.get_id())
254
255 def set_backend_enabled(self, backend_id, state):
256 """
257 The backend corresponding to backend_id is enabled or disabled
258 according to "state".
259 Disable:
260 Quits a backend and disables it (which means it won't be
261 automatically loaded next time GTG is started)
262 Enable:
263 Reloads a disabled backend. Backend must be already known by the
264 Datastore
265 @parma backend_id: a backend id
266 @param state: True to enable, False to disable
267 """
268 if backend_id in self.backends:
269 backend = self.backends[backend_id]
270 current_state = backend.is_enabled()
271 if current_state == True and state == False:
272 #we disable the backend
273 backend.quit(disable = True)
274 elif current_state == False and state == True:
275 if self.is_default_backend_loaded == True:
276 backend.initialize()
277 self.flush_all_tasks(backend_id)
278 else:
279 #will be activated afterwards
280 backend.set_parameter(GenericBackend.KEY_ENABLED,
281 True)
282
283 def remove_backend(self, backend_id):
284 '''
285 Removes a backend, and forgets it ever existed.
286 @param backend_id: a backend id
287 '''
288 if backend_id in self.backends:
289 backend = self.backends[backend_id]
290 if backend.is_enabled():
291 self.set_backend_enabled(backend_id, False)
292 backend.purge()
293 #we notify that the backend has been deleted
294 self._backend_signals.backend_removed(backend.get_id())
295 del self.backends[backend_id]
296
297 def backend_change_attached_tags(self, backend_id, tag_names):
298 '''
299 Changes the tags for which a backend should store a task
300 @param backend_id: a backend_id
301 @param tag_names: the new set of tags. This should not be a tag object,
302 just the tag name.
303 '''
304 backend = self.backends[backend_id]
305 backend.set_attached_tags(tag_names)
306
307 def flush_all_tasks(self, backend_id):
308 '''
309 This function will cause all tasks to be checked against the backend
310 identified with backend_id. If tasks need to be added or removed, it
311 will be done here.
312 It has to be run after the creation of a new backend (or an alteration
313 of its "attached tags"), so that the tasks which are already loaded in
314 the Tree will be saved in the proper backends
315 @param backend_id: a backend id
316 '''
317 def _internal_flush_all_tasks():
318 backend = self.backends[backend_id]
319 for task_id in self.requester.get_all_tasks_list():
320 backend.queue_set_task(None, task_id)
321 t = threading.Thread(target = _internal_flush_all_tasks).start()
322 self.backends[backend_id].start_get_tasks()
323
324 def save(self, quit = False):
325 '''
326 Saves the backends parameters.
327 @param quit: If quit is true, backends are shut down
328 '''
329 doc,xmlconfig = cleanxml.emptydoc("config")
330 #we ask all the backends to quit first.
331 if quit:
332 for b in self.get_all_backends():
333 #NOTE:we could do this in parallel. Maybe a quit and
334 #has_quit would be faster (invernizzi)
335 b.quit()
336 #we save the parameters
337 for b in self.get_all_backends(disabled = True):
338 t_xml = doc.createElement("backend")
339 for key, value in b.get_parameters().iteritems():
340 if key in ["backend", "xmlobject"]:
341 #We don't want parameters,backend,xmlobject
342 continue
343 param_type = b.get_parameter_type(key)
344 value = b.cast_param_type_to_string(param_type, value)
345 t_xml.setAttribute(str(key), value)
346 #Saving all the projects at close
347 xmlconfig.appendChild(t_xml)
348
349 datafile = os.path.join(CoreConfig().get_data_dir(), CoreConfig.DATA_FILE)
350 cleanxml.savexml(datafile,doc,backup=True)
351
352 #Saving the tagstore
353 ts = self.get_tagstore()
354 ts.save()
355
356 def request_task_deletion(self, tid):
357 '''
358 This is a proxy function to request a task deletion from a backend
359 @param tid: the tid of the task to remove
360 '''
361 self.requester.delete_task(tid)
362
197363
198class TaskSource():364class TaskSource():
199 """ transparent interface between the real backend and the datastore """365 '''
200 def __init__(self, backend, parameters):366 Transparent interface between the real backend and the DataStore.
367 Is in charge of connecting and disconnecting to signals
368 '''
369 def __init__(self, requester, backend, datastore):
201 """370 """
202 Instantiates a TaskSource object.371 Instantiates a TaskSource object.
203 @param backend: (Required) Task Backend being wrapperized372 @param requester: a Requester
204 @param parameters: Dictionary of custom parameters.373 @param backend: the backend being wrapped
374 @param datastore: a FilteredDatastore
205 """375 """
206 self.backend = backend376 self.backend = backend
207 self.dic = parameters377 self.req = requester
208 self.to_set = []378 self.backend.register_datastore(datastore)
209 self.to_remove = []379 self.to_set = deque()
210 self.lock = threading.Lock()380 self.to_remove = deque()
211 self.count_set = 0381 self.task_filter = self.get_task_filter_for_backend()
382 if Log.is_debugging_mode():
383 self.timer_timestep = 5
384 else:
385 self.timer_timestep = 1
386 self.set_task_handle = None
387 self.remove_task_handle = None
388 self.to_set_timer = None
212 389
213 def start_get_tasks(self,push_task,task_factory):390 def start_get_tasks(self):
214 """391 ''''
215 Maps the TaskSource to the backend and starts threading.392 Maps the TaskSource to the backend and starts threading.
216 This must be called before the DataStore is usable.393 '''
217 """394 threading.Thread(target = self.__start_get_tasks).start()
218 func = self.backend.start_get_tasks395
219 t = threading.Thread(target=func,args=(push_task,task_factory))396 def __start_get_tasks(self):
220 t.start()397 '''
221 398 Loads all task from the backend and connects its signals afterwards.
222 def set_task(self, task):399 Launched as a thread by start_get_tasks
400 '''
401 self.backend.start_get_tasks()
402 self._connect_signals()
403 if self.backend.is_default():
404 BackendSignals().default_backend_loaded()
405
406 def get_task_filter_for_backend(self):
407 '''
408 Fiter that checks if the task should be stored in this backend.
409
410 @returns function: a function that accepts a task and returns True/False
411 whether the task should be stored or not
412 '''
413 raw_filter = self.req.get_filter("backend_filter").get_function()
414 return lambda task: raw_filter(task, \
415 set(self.backend.get_attached_tags()))
416
417 def should_task_id_be_stored(self, task_id):
418 '''
419 Helper function: Checks if a task should be stored in this backend
420 @param task_id: a task id
421 @returns bool: True if the task should be stored
422 '''
423 task = self.req.get_task(task_id)
424 return self.task_filter(task)
425
426 def queue_set_task(self, sender, tid):
223 """427 """
224 Updates the task in the DataStore. Actually, it adds the task to a428 Updates the task in the DataStore. Actually, it adds the task to a
225 queue to be updated asynchronously.429 queue to be updated asynchronously.
430 @param sender: not used, any value will do.
226 @param task: The Task object to be updated.431 @param task: The Task object to be updated.
227 """432 """
228 tid = task.get_id()433 if self.should_task_id_be_stored(tid):
229 if task not in self.to_set and tid not in self.to_remove:434 if tid not in self.to_set and tid not in self.to_remove:
230 self.to_set.append(task)435 self.to_set.appendleft(tid)
231 if self.lock.acquire(False):436 self.__try_launch_setting_thread()
232 func = self.setting_thread437 else:
233 t = threading.Thread(target=func)438 self.queue_remove_task(None, tid)
234 t.start()
235# else:
236# print "cannot acquire lock : not a problem, just for debug purpose"
237 439
238 def setting_thread(self):440 def launch_setting_thread(self):
239 """441 '''
240 Operates the threads to set and remove tasks.442 Operates the threads to set and remove tasks.
241 Releases the lock when it is done.443 Releases the lock when it is done.
242 """444 '''
243 try:445 #FIXME: the lock should be general for all backends. Therefore, it
244 while len(self.to_set) > 0:446 #should be handled in the datastore
245 t = self.to_set.pop(0)447 while True:
246 tid = t.get_id()448 try:
247 if tid not in self.to_remove:449 tid = self.to_set.pop()
248 self.count_set += 1450 except IndexError:
249 #print "saving task %s (%s saves)" %(tid,self.count_set)451 break
250 self.backend.set_task(t)452 #we check that the task is not already marked for deletion
251 while len(self.to_remove) > 0:453 #and that it's still to be stored in this backend
252 tid = self.to_remove.pop(0)454 #NOTE: no need to lock, we're reading
253 self.backend.remove_task(tid)455 if tid not in self.to_remove and \
254 finally:456 self.should_task_id_be_stored(tid) and \
255 self.lock.release()457 self.req.has_task(tid):
458 task = self.req.get_task(tid)
459 self.backend.queue_set_task(task)
460 while True:
461 try:
462 tid = self.to_remove.pop()
463 except IndexError:
464 break
465 self.backend.queue_remove_task(tid)
466 #we release the weak lock
467 self.to_set_timer = None
256 468
257 def remove_task(self, tid):469 def queue_remove_task(self, sender, tid):
258 """470 '''
259 Queues task to be removed.471 Queues task to be removed.
472 @param sender: not used, any value will do
260 @param tid: The Task ID of the task to be removed473 @param tid: The Task ID of the task to be removed
261 """474 '''
262 if tid not in self.to_remove:475 if tid not in self.to_remove:
263 self.to_remove.append(tid)476 self.to_remove.appendleft(tid)
264 if self.lock.acquire(False):477 self.__try_launch_setting_thread()
265 func = self.setting_thread478
266 t = threading.Thread(target=func)479 def __try_launch_setting_thread(self):
267 t.start()480 '''
268 481 Helper function to launch the setting thread, if it's not running
269 def new_task_id(self):482 '''
270 """483 if self.to_set_timer == None:
271 returns a new ID created by the backend.484 self.to_set_timer = threading.Timer(self.timer_timestep, \
272 """485 self.launch_setting_thread)
273 return self.backend.new_task_id()486 self.to_set_timer.start()
274 487
275 def quit(self):488 def initialize(self, connect_signals = True):
276 """ Quits the backend """489 '''
277 self.backend.quit()490 Initializes the backend and starts looking for signals.
278 491 @param connect_signals: if True, it starts listening for signals
279 #Those functions are only for TaskSource492 '''
280 def get_parameters(self):493 self.backend.initialize()
281 """494 if connect_signals:
282 Returns the parameters specified during creation of the DataStore495 self._connect_signals()
283 """496
284 return self.dic497 def _connect_signals(self):
498 '''
499 Helper function to connect signals
500 '''
501 if not self.set_task_handle:
502 self.set_task_handle = self.req.connect('task-modified', \
503 self.queue_set_task)
504 if not self.remove_task_handle:
505 self.remove_task_handle = self.req.connect('task-deleted',\
506 self.queue_remove_task)
507
508 def _disconnect_signals(self):
509 '''
510 Helper function to disconnect signals
511 '''
512 if self.set_task_handle:
513 self.req.disconnect(self.set_task_handle)
514 self.set_task_handle = None
515 if self.remove_task_handle:
516 self.req.disconnect(self.remove_task_handle)
517 self.remove_task_handle = None
518
519 def sync(self):
520 '''
521 Forces the TaskSource to sync all the pending tasks
522 '''
523 if self.to_set_timer != None:
524 try:
525 self.to_set_timer.cancel()
526 except:
527 pass
528 try:
529 self.to_set_timer.join(5)
530 except:
531 pass
532 self.launch_setting_thread()
533
534 def quit(self, disable = False):
535 '''
536 Quits the backend and disconnect the signals
537 @param disable: if True, the backend is disabled.
538 '''
539 self._disconnect_signals()
540 self.sync()
541 self.backend.quit(disable)
542
543 def __getattr__(self, attr):
544 '''
545 Delegates all the functions not defined here to the real backend
546 (standard python function)
547 @param attr: attribute to get
548 '''
549 if attr in self.__dict__:
550 return self.__dict__[attr]
551 else:
552 return getattr(self.backend, attr)
553
554
555
556class FilteredDataStore(Borg):
557 '''
558 This class acts as an interface to the Datastore.
559 It is used to hide most of the methods of the Datastore.
560 The backends can safely use the remaining methods.
561 '''
562
563
564 def __init__(self, datastore):
565 super(FilteredDataStore, self).__init__()
566 self.datastore = datastore
567
568 def __getattr__(self, attr):
569 if attr in ['task_factory', \
570 'push_task',
571 'get_task',
572 'has_task',
573 'request_task_deletion']:
574 return getattr(self.datastore, attr)
575 else:
576 raise AttributeError
577
285578
=== modified file 'GTG/core/filters_bank.py'
--- GTG/core/filters_bank.py 2010-06-14 19:30:50 +0000
+++ GTG/core/filters_bank.py 2010-06-23 01:19:23 +0000
@@ -35,6 +35,10 @@
3535
36 def set_parameters(self,dic):36 def set_parameters(self,dic):
37 self.dic = dic37 self.dic = dic
38
39 def get_function(self):
40 '''Returns the filtering function'''
41 return self.func
38 42
39 def is_displayed(self,tid):43 def is_displayed(self,tid):
40 task = self.req.get_task(tid)44 task = self.req.get_task(tid)
@@ -152,6 +156,9 @@
152 #worklate156 #worklate
153 filt_obj = Filter(self.worklate,self.req)157 filt_obj = Filter(self.worklate,self.req)
154 self.available_filters['worklate'] = filt_obj158 self.available_filters['worklate'] = filt_obj
159 #backend filter
160 filt_obj = Filter(self.backend_filter, self.req)
161 self.available_filters['backend_filter'] = filt_obj
155 #no_disabled_tag162 #no_disabled_tag
156 filt_obj = Filter(self.no_disabled_tag,self.req)163 filt_obj = Filter(self.no_disabled_tag,self.req)
157 param = {}164 param = {}
@@ -234,6 +241,19 @@
234 """ Filter of tasks which are closed """241 """ Filter of tasks which are closed """
235 ret = task.get_status() in [Task.STA_DISMISSED, Task.STA_DONE]242 ret = task.get_status() in [Task.STA_DISMISSED, Task.STA_DONE]
236 return ret243 return ret
244
245 def backend_filter(self, task, tags_to_match_set):
246 '''
247 Filter that checks if two tags sets intersect. It is used to check if a
248 task should be stored inside a backend
249 @param task: a task object
250 @oaram tags_to_match_set: a *set* of tag names
251 '''
252 all_tasks_tag = self.req.get_alltag_tag().get_name()
253 if all_tasks_tag in tags_to_match_set:
254 return True
255 task_tags = set(task.get_tags_name())
256 return task_tags.intersection(tags_to_match_set)
237 257
238 def no_disabled_tag(self,task,parameters=None):258 def no_disabled_tag(self,task,parameters=None):
239 """Filter of task that don't have any disabled/nonworkview tag"""259 """Filter of task that don't have any disabled/nonworkview tag"""
240260
=== modified file 'GTG/core/requester.py'
--- GTG/core/requester.py 2010-06-21 12:34:23 +0000
+++ GTG/core/requester.py 2010-06-23 01:19:23 +0000
@@ -1,6 +1,6 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------2# -----------------------------------------------------------------------------
3# Gettings Things Gnome! - a personal organizer for the GNOME desktop3# Getting Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#5#
6# This program is free software: you can redistribute it and/or modify it under6# This program is free software: you can redistribute it and/or modify it under
@@ -39,18 +39,18 @@
39 Multiple L{Requester}s can exist on the same datastore, so they should39 Multiple L{Requester}s can exist on the same datastore, so they should
40 never have state of their own.40 never have state of their own.
41 """41 """
42 __gsignals__ = {'task-added': (gobject.SIGNAL_RUN_FIRST, \42
43 gobject.TYPE_NONE, (str, )),43 __string_signal__ = (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (str, ))
44 'task-deleted': (gobject.SIGNAL_RUN_FIRST, \44
45 gobject.TYPE_NONE, (str, )),45 __gsignals__ = {'task-added' : __string_signal__, \
46 'task-modified': (gobject.SIGNAL_RUN_FIRST, \46 'task-deleted' : __string_signal__, \
47 gobject.TYPE_NONE, (str, )),47 'task-modified' : __string_signal__, \
48 'tag-added': (gobject.SIGNAL_RUN_FIRST, \48 'task-tagged' : __string_signal__, \
49 gobject.TYPE_NONE, (str, )),49 'task-untagged' : __string_signal__, \
50 'tag-deleted': (gobject.SIGNAL_RUN_FIRST, \50 'tag-added' : __string_signal__, \
51 gobject.TYPE_NONE, (str, )),51 'tag-deleted' : __string_signal__, \
52 'tag-modified': (gobject.SIGNAL_RUN_FIRST, \52 'tag-path-deleted' : __string_signal__, \
53 gobject.TYPE_NONE, (str, ))}53 'tag-modified' : __string_signal__}
5454
55 def __init__(self, datastore):55 def __init__(self, datastore):
56 """Construct a L{Requester}."""56 """Construct a L{Requester}."""
@@ -72,12 +72,19 @@
72 self.counter_call += 172 self.counter_call += 1
73 #print "signal task_modified %s (%s modifications)" %(tid,self.counter_call)73 #print "signal task_modified %s (%s modifications)" %(tid,self.counter_call)
74 gobject.idle_add(self.emit, "task-modified", tid)74 gobject.idle_add(self.emit, "task-modified", tid)
75 75
76 def _task_deleted(self, tid):
77 #when this is emitted, task has *already* been deleted
78 gobject.idle_add(self.emit, "task-deleted", tid)
79
76 def _tag_added(self,tagname):80 def _tag_added(self,tagname):
77 gobject.idle_add(self.emit, "tag-added", tagname)81 gobject.idle_add(self.emit, "tag-added", tagname)
7882
79 def _tag_modified(self,tagname):83 def _tag_modified(self,tagname):
80 gobject.idle_add(self.emit, "tag-modified", tagname)84 gobject.idle_add(self.emit, "tag-modified", tagname)
85
86 def _tag_path_deleted(self, path):
87 gobject.idle_add(self.emit, "tag-path-deleted", path)
81 88
82 def _tag_deleted(self,tagname):89 def _tag_deleted(self,tagname):
83 gobject.idle_add(self.emit, "tag-deleted", tagname)90 gobject.idle_add(self.emit, "tag-deleted", tagname)
@@ -126,6 +133,7 @@
126133
127 ######### Filters bank #######################134 ######### Filters bank #######################
128 # Get the filter object for a given name135 # Get the filter object for a given name
136
129 def get_filter(self,filter_name):137 def get_filter(self,filter_name):
130 return self.filters.get_filter(filter_name)138 return self.filters.get_filter(filter_name)
131 139
@@ -162,7 +170,7 @@
162 task = self.ds.get_task(tid)170 task = self.ds.get_task(tid)
163 return task171 return task
164172
165 def new_task(self, pid=None, tags=None, newtask=True):173 def new_task(self, tags=None, newtask=True):
166 """Create a new task.174 """Create a new task.
167175
168 Note: this modifies the datastore.176 Note: this modifies the datastore.
@@ -175,11 +183,12 @@
175 existed, C{False} if importing an existing task from a backend.183 existed, C{False} if importing an existing task from a backend.
176 @return: A task from the data store184 @return: A task from the data store
177 """185 """
178 task = self.ds.new_task(pid=pid)186 task = self.ds.new_task()
179 if tags:187 if tags:
180 for t in tags:188 for t in tags:
181 assert(isinstance(t, Tag) == False)189 assert(isinstance(t, Tag) == False)
182 task.tag_added(t)190 task.tag_added(t)
191 self._task_loaded(task.get_id())
183 return task192 return task
184193
185 def delete_task(self, tid):194 def delete_task(self, tid):
@@ -196,11 +205,11 @@
196 for tag in task.get_tags():205 for tag in task.get_tags():
197 self.emit('tag-modified', tag.get_name())206 self.emit('tag-modified', tag.get_name())
198 self.emit('task-deleted', tid)207 self.emit('task-deleted', tid)
199 #return True208 return self.basetree.remove_node(tid)
200 return self.ds.delete_task(tid)
201209
202 ############### Tags ##########################210 ############### Tags ##########################
203 ###############################################211 ###############################################
212
204 def get_tag_tree(self):213 def get_tag_tree(self):
205 return self.ds.get_tagstore()214 return self.ds.get_tagstore()
206215
@@ -251,3 +260,27 @@
251 l.append(t.get_name())260 l.append(t.get_name())
252 l.sort(cmp=lambda x, y: cmp(x.lower(),y.lower()))261 l.sort(cmp=lambda x, y: cmp(x.lower(),y.lower()))
253 return l262 return l
263
264 ############## Backends #######################
265 ###############################################
266
267 def get_all_backends(self, disabled = False):
268 return self.ds.get_all_backends(disabled)
269
270 def register_backend(self, dic):
271 return self.ds.register_backend(dic)
272
273 def flush_all_tasks(self, backend_id):
274 return self.ds.flush_all_tasks(backend_id)
275
276 def get_backend(self, backend_id):
277 return self.ds.get_backend(backend_id)
278
279 def set_backend_enabled(self, backend_id, state):
280 return self.ds.set_backend_enabled(backend_id, state)
281
282 def remove_backend(self, backend_id):
283 return self.ds.remove_backend(backend_id)
284
285 def backend_change_attached_tags(self, backend_id, tags):
286 return self.ds.backend_change_attached_tags(backend_id, tags)
254287
=== modified file 'GTG/core/tagstore.py'
--- GTG/core/tagstore.py 2010-06-12 13:31:18 +0000
+++ GTG/core/tagstore.py 2010-06-23 01:19:23 +0000
@@ -40,6 +40,7 @@
40# There's only one Tag store by user. It will store all the tag used40# There's only one Tag store by user. It will store all the tag used
41# and their attribute.41# and their attribute.
42class TagStore(Tree):42class TagStore(Tree):
43
43 44
44 def __init__(self,requester):45 def __init__(self,requester):
45 Tree.__init__(self)46 Tree.__init__(self)
@@ -50,7 +51,7 @@
50 51
51 ### building the initial tags52 ### building the initial tags
52 # Build the "all tasks tag"53 # Build the "all tasks tag"
53 self.alltag_tag = self.new_tag("gtg-tags-all")54 self.alltag_tag = self.new_tag(CoreConfig.ALLTASKS_TAG)
54 self.alltag_tag.set_attribute("special","all")55 self.alltag_tag.set_attribute("special","all")
55 self.alltag_tag.set_attribute("label","<span weight='bold'>%s</span>"\56 self.alltag_tag.set_attribute("label","<span weight='bold'>%s</span>"\
56 % _("All tasks"))57 % _("All tasks"))
@@ -68,7 +69,7 @@
68 self.sep_tag.set_attribute("special","sep")69 self.sep_tag.set_attribute("special","sep")
69 self.sep_tag.set_attribute("order",2)70 self.sep_tag.set_attribute("order",2)
7071
71 self.filename = os.path.join(CoreConfig.DATA_DIR, XMLFILE)72 self.filename = os.path.join(CoreConfig().get_data_dir(), XMLFILE)
72 doc, self.xmlstore = cleanxml.openxmlfile(self.filename,73 doc, self.xmlstore = cleanxml.openxmlfile(self.filename,
73 XMLROOT) #pylint: disable-msg=W061274 XMLROOT) #pylint: disable-msg=W0612
74 for t in self.xmlstore.childNodes:75 for t in self.xmlstore.childNodes:
@@ -121,7 +122,7 @@
121 if tagname[0] != "@":122 if tagname[0] != "@":
122 tagname = "@" + tagname123 tagname = "@" + tagname
123 return self.get_node(tagname)124 return self.get_node(tagname)
124 125
125 #FIXME : also add a new filter126 #FIXME : also add a new filter
126 def rename_tag(self, oldname, newname):127 def rename_tag(self, oldname, newname):
127 if len(newname) > 0 and \128 if len(newname) > 0 and \
@@ -316,9 +317,12 @@
316 def add_task(self, tid):317 def add_task(self, tid):
317 if tid not in self.tasks:318 if tid not in self.tasks:
318 self.tasks.append(tid) 319 self.tasks.append(tid)
319 def remove_task(self,tid):320
321 def remove_task(self, tid):
320 if tid in self.tasks:322 if tid in self.tasks:
321 self.tasks.remove(tid) 323 self.tasks.remove(tid)
324 self.req._tag_modified(self.get_name())
325
322 def get_tasks(self):326 def get_tasks(self):
323 #return a copy of the list327 #return a copy of the list
324 toreturn = self.tasks[:]328 toreturn = self.tasks[:]
325329
=== modified file 'GTG/core/task.py'
--- GTG/core/task.py 2010-06-18 16:36:17 +0000
+++ GTG/core/task.py 2010-06-23 01:19:23 +0000
@@ -48,10 +48,10 @@
48 #tid is a string ! (we have to choose a type and stick to it)48 #tid is a string ! (we have to choose a type and stick to it)
49 self.tid = str(ze_id)49 self.tid = str(ze_id)
50 self.set_uuid(uuid.uuid4())50 self.set_uuid(uuid.uuid4())
51 self.remote_ids = {}
51 self.content = ""52 self.content = ""
52 #self.content = \53 #self.content = \
53 # "<content>Press Escape or close this task to save it</content>"54 # "<content>Press Escape or close this task to save it</content>"
54 self.sync_func = None
55 self.title = _("My new task")55 self.title = _("My new task")
56 #available status are: Active - Done - Dismiss - Note56 #available status are: Active - Done - Dismiss - Note
57 self.status = self.STA_ACTIVE57 self.status = self.STA_ACTIVE
@@ -78,7 +78,6 @@
78 self.loaded = True78 self.loaded = True
79 if signal:79 if signal:
80 self.req._task_loaded(self.tid)80 self.req._task_loaded(self.tid)
81 #not sure the following is necessary
82 #self.req._task_modified(self.tid)81 #self.req._task_modified(self.tid)
8382
84 def set_to_keep(self):83 def set_to_keep(self):
@@ -102,6 +101,25 @@
102 self.sync()101 self.sync()
103 return self.uuid102 return self.uuid
104103
104 def get_remote_ids(self):
105 '''
106 A task usually has a different id in all the different backends.
107 This function returns a dictionary backend_id->the id the task has
108 in that backend
109 @returns dict: dictionary backend_id->task remote id
110 '''
111 return self.remote_ids
112
113 def add_remote_id(self, backend_id, task_remote_id):
114 '''
115 A task usually has a different id in all the different backends.
116 This function adds a relationship backend_id-> remote_id that can be
117 retrieved using get_remote_ids
118 @param backend_id: string representing the backend id
119 @param task_remote_id: the id for this task in the backend backend_id
120 '''
121 self.remote_ids[str(backend_id)] = str(task_remote_id)
122
105 def get_title(self):123 def get_title(self):
106 return self.title124 return self.title
107125
@@ -115,7 +133,7 @@
115 self.title = title.strip('\t\n')133 self.title = title.strip('\t\n')
116 else:134 else:
117 self.title = "(no title task)"135 self.title = "(no title task)"
118 #Avoid unecessary sync136 #Avoid unnecessary sync
119 if self.title != old_title:137 if self.title != old_title:
120 self.sync()138 self.sync()
121 return True139 return True
@@ -294,8 +312,7 @@
294 """Add a newly created subtask to this task. Return the task added as312 """Add a newly created subtask to this task. Return the task added as
295 a subtask313 a subtask
296 """314 """
297 uid, pid = self.get_id().split('@') #pylint: disable-msg=W0612315 subt = self.req.new_task(newtask=True)
298 subt = self.req.new_task(pid=pid, newtask=True)
299 #we use the inherited childrens316 #we use the inherited childrens
300 self.add_child(subt.get_id())317 self.add_child(subt.get_id())
301 return subt318 return subt
@@ -427,37 +444,32 @@
427 #This method is called by the datastore and should not be called directly444 #This method is called by the datastore and should not be called directly
428 #Use the requester445 #Use the requester
429 def delete(self):446 def delete(self):
430 self.set_sync_func(None, callsync=False)447 #we issue a delete for all the children
431 for task in self.get_subtasks():448 for task in self.get_subtasks():
432 task.remove_parent(self.get_id())449 #I think it's superfluous (invernizzi)
433 self.req.delete_task(task.get_id())450 #task.remove_parent(self.get_id())
451 task.delete()
452 #we tell the parents we have to go
434 for i in self.get_parents():453 for i in self.get_parents():
435 task = self.req.get_task(i)454 task = self.req.get_task(i)
436 task.remove_child(self.get_id())455 task.remove_child(self.get_id())
456 #we tell the tags about the deletion
437 for tagname in self.tags:457 for tagname in self.tags:
438 tag = self.req.get_tag(tagname)458 tag = self.req.get_tag(tagname)
439 tag.remove_task(self.get_id())459 tag.remove_task(self.get_id())
440 #then we remove effectively the task460 #then we signal the we are ready to be removed
441 #self.req.delete_task(self.get_id())461 self.req._task_deleted(self.get_id())
442
443 #This is a callback. The "sync" function has to be set
444 def set_sync_func(self, sync, callsync=True):
445 self.sync_func = sync
446 #We call it immediatly to save stuffs that were set before this
447 if callsync and self.is_loaded():
448 self.sync()
449462
450 def sync(self):463 def sync(self):
451 self._modified_update()464 self._modified_update()
452 if self.sync_func and self.is_loaded():465 if self.is_loaded():
453 self.sync_func(self)
454 self.call_modified()466 self.call_modified()
455 return True467 return True
456 else:468 else:
457 return False469 return False
458 470
459 #This function send the modified signals for the tasks, 471 #This function send the modified signals for the tasks,
460 #parents and childrens 472 #parents and children
461 def call_modified(self):473 def call_modified(self):
462 #we first modify children474 #we first modify children
463 for s in self.get_children():475 for s in self.get_children():
@@ -469,10 +481,11 @@
469 self.req._task_modified(p)481 self.req._task_modified(p)
470482
471 def _modified_update(self):483 def _modified_update(self):
484 '''
485 Updates the modified timestamp
486 '''
472 self.modified = datetime.now()487 self.modified = datetime.now()
473488
474
475
476### TAG FUNCTIONS ############################################################489### TAG FUNCTIONS ############################################################
477#490#
478 def get_tags_name(self):491 def get_tags_name(self):
@@ -509,6 +522,8 @@
509 #Do not add the same tag twice522 #Do not add the same tag twice
510 if not t in self.tags:523 if not t in self.tags:
511 self.tags.append(t)524 self.tags.append(t)
525 #we notify the backends
526 #self.req.tag_was_added_to_task(self, tagname)
512 for child in self.get_subtasks():527 for child in self.get_subtasks():
513 if child.can_be_deleted:528 if child.can_be_deleted:
514 child.add_tag(t)529 child.add_tag(t)
515530
=== modified file 'GTG/gtg.py'
--- GTG/gtg.py 2010-06-18 11:55:03 +0000
+++ GTG/gtg.py 2010-06-23 01:19:23 +0000
@@ -45,18 +45,17 @@
45"""This is the top-level exec script for running GTG"""45"""This is the top-level exec script for running GTG"""
4646
47#=== IMPORT ===================================================================47#=== IMPORT ===================================================================
48from contextlib import contextmanager
49import os48import os
50import logging49import logging
51import signal
5250
53import dbus51import dbus
5452
55#our own imports53#our own imports
56from GTG import _, info54from GTG.backends import BackendFactory
55from GTG import _
57from GTG.core import CoreConfig56from GTG.core import CoreConfig
58from GTG.core.datastore import DataStore57from GTG.core.datastore import DataStore
59from GTG.gtk import crashhandler58from GTG.gtk.crashhandler import signal_catcher
60from GTG.gtk.manager import Manager59from GTG.gtk.manager import Manager
61from GTG.tools.logger import Log60from GTG.tools.logger import Log
6261
@@ -93,54 +92,55 @@
93#=== MAIN CLASS ===============================================================92#=== MAIN CLASS ===============================================================
9493
95def main(options=None, args=None):94def main(options=None, args=None):
95 '''
96 Calling this starts the full GTG experience ( :-D )
97 '''
98 config, ds, req = core_main_init(options, args)
99 # Launch task browser
100 manager = Manager(req, config)
101 #main loop
102 #To be more user friendly and get the logs of crashes, we show an apport
103 # hooked window upon crashes
104 with signal_catcher(manager.close_browser):
105 manager.main()
106 core_main_quit(config, ds)
107
108def core_main_init(options = None, args = None):
109 '''
110 Part of the main function prior to the UI initialization.
111 '''
96 # Debugging subsystem initialization112 # Debugging subsystem initialization
97 if options.debug:113 if options.debug:
98 Log.setLevel(logging.DEBUG)114 Log.setLevel(logging.DEBUG)
99 Log.debug("Debug output enabled.")115 Log.debug("Debug output enabled.")
100 116 Log.set_debugging_mode(True)
101 config = CoreConfig()117 config = CoreConfig()
102 check_instance(config.DATA_DIR)118 check_instance(config.get_data_dir())
103 backends_list = config.get_backends_list()119 backends_list = BackendFactory().get_saved_backends_list()
104
105 #initialize Apport hook for crash handling
106 crashhandler.initialize(app_name = "Getting Things GNOME!", message="GTG"
107 + info.VERSION + _(" has crashed. Please report the bug on <a href=\""
108 "http://bugs.edge.launchpad.net/gtg\">our Launchpad page</a>. If you "
109 "have Apport installed, it will be started for you."), use_apport = True)
110
111 # Load data store120 # Load data store
112 ds = DataStore()121 ds = DataStore()
113 122 # Register backends
114 for backend_dic in backends_list:123 for backend_dic in backends_list:
115 ds.register_backend(backend_dic)124 ds.register_backend(backend_dic)
116
117 #save directly the backends to be sure to write projects.xml125 #save directly the backends to be sure to write projects.xml
118 config.save_datastore(ds,initial_save=True)126 ds.save(quit = False)
119 127
120 # Launch task browser128 # Launch task browser
121 req = ds.get_requester()129 req = ds.get_requester()
122 manager = Manager(req, config)130 return config, ds, req
123
124 #we listen for signals from the system in order to save our configuration
125 # if GTG is forcefully terminated (e.g.: on shutdown).
126 @contextmanager
127 def signal_catcher():
128 #if TERM or ABORT are caught, we close the browser
129 for s in [signal.SIGABRT, signal.SIGTERM]:
130 signal.signal(s, lambda a,b: manager.close_browser())
131 yield
132131
133 #main loop132def core_main_quit(config, ds):
134 with signal_catcher():133 '''
135 manager.main()134 Last bits of code executed in GTG, after the UI has been shut off.
136 135 Currently, it's just saving everything.
136 '''
137 # Ideally we should load window geometry configuration from a config.137 # Ideally we should load window geometry configuration from a config.
138 # backend like gconf at some point, and restore the appearance of the138 # backend like gconf at some point, and restore the appearance of the
139 # application as the user last exited it.139 # application as the user last exited it.
140140 #
141 # Ending the application: we save configuration141 # Ending the application: we save configuration
142 config.save_config()142 config.save()
143 config.save_datastore(ds)143 ds.save(quit = True)
144144
145#=== EXECUTION ================================================================145#=== EXECUTION ================================================================
146146
147147
=== modified file 'GTG/gtk/browser/browser.py'
--- GTG/gtk/browser/browser.py 2010-06-21 12:34:23 +0000
+++ GTG/gtk/browser/browser.py 2010-06-23 01:19:23 +0000
@@ -23,7 +23,6 @@
23#=== IMPORT ===================================================================23#=== IMPORT ===================================================================
24#system imports24#system imports
25import locale25import locale
26import os
27import re26import re
28import time27import time
29import webbrowser28import webbrowser
@@ -35,18 +34,16 @@
3534
36#our own imports35#our own imports
37import GTG36import GTG
37from GTG.core import CoreConfig
38from GTG import _, info, ngettext38from GTG import _, info, ngettext
39from GTG.core.task import Task39from GTG.core.task import Task
40#from GTG.core.tagstore import Tag
41from GTG.gtk.browser import GnomeConfig, tasktree, tagtree40from GTG.gtk.browser import GnomeConfig, tasktree, tagtree
42#from GTG.taskbrowser.preferences import PreferencesDialog
43from GTG.gtk.browser.tasktree import TaskTreeModel,\41from GTG.gtk.browser.tasktree import TaskTreeModel,\
44 ActiveTaskTreeView,\42 ActiveTaskTreeView,\
45 ClosedTaskTreeView43 ClosedTaskTreeView
46from GTG.gtk.browser.tagtree import TagTree44from GTG.gtk.browser.tagtree import TagTree
47from GTG.tools import openurl45from GTG.tools import openurl
48from GTG.tools.dates import strtodate,\46from GTG.tools.dates import no_date,\
49 no_date,\
50 FuzzyDate, \47 FuzzyDate, \
51 get_canonical_date48 get_canonical_date
52from GTG.tools.logger import Log49from GTG.tools.logger import Log
@@ -159,7 +156,7 @@
159 self.priv['quick_add_cbs'] = []156 self.priv['quick_add_cbs'] = []
160157
161 def _init_icon_theme(self):158 def _init_icon_theme(self):
162 icon_dirs = [GTG.DATA_DIR, os.path.join(GTG.DATA_DIR, "icons")]159 icon_dirs = CoreConfig().get_icons_directories()
163 for i in icon_dirs:160 for i in icon_dirs:
164 gtk.icon_theme_get_default().prepend_search_path(i)161 gtk.icon_theme_get_default().prepend_search_path(i)
165 gtk.window_set_default_icon_name("gtg")162 gtk.window_set_default_icon_name("gtg")
@@ -429,7 +426,7 @@
429### HELPER FUNCTIONS ########################################################426### HELPER FUNCTIONS ########################################################
430427
431 def open_preferences(self,widget):428 def open_preferences(self,widget):
432 self.vmanager.show_preferences(self.priv)429 self.vmanager.open_preferences(self.priv)
433 430
434 def quit(self,widget=None):431 def quit(self,widget=None):
435 self.vmanager.close_browser()432 self.vmanager.close_browser()
436433
=== modified file 'GTG/gtk/browser/tagtree.py'
--- GTG/gtk/browser/tagtree.py 2010-06-18 16:46:10 +0000
+++ GTG/gtk/browser/tagtree.py 2010-06-23 01:19:23 +0000
@@ -72,7 +72,8 @@
72 task = self.req.get_task(tid)72 task = self.req.get_task(tid)
73 if task:73 if task:
74 for tag in task.get_tags():74 for tag in task.get_tags():
75 self.tagrefresh(sender=sender,tagname=tag.get_name())75 if tag:
76 self.tagrefresh(sender=sender,tagname=tag.get_name())
7677
77 def tagrefresh(self,sender=None,tagname=None):78 def tagrefresh(self,sender=None,tagname=None):
78 if tagname:79 if tagname:
7980
=== modified file 'GTG/gtk/crashhandler.py'
--- GTG/gtk/crashhandler.py 2010-06-07 21:14:45 +0000
+++ GTG/gtk/crashhandler.py 2010-06-23 01:19:23 +0000
@@ -33,6 +33,12 @@
33import sys33import sys
34import os34import os
35import time35import time
36import signal
37from contextlib import contextmanager
38
39from GTG import info
40
41
36try:42try:
37 import pygtk43 import pygtk
38 pygtk.require("2.0") # not tested on earlier versions44 pygtk.require("2.0") # not tested on earlier versions
@@ -297,3 +303,21 @@
297 return "gtkcrashhandler.py should imported, not run"303 return "gtkcrashhandler.py should imported, not run"
298 raise DoNotRunException()304 raise DoNotRunException()
299305
306
307## We handle initialization directly here, since this module will be used as a
308# singleton
309 #we listen for signals from the system in order to save our configuration
310 # if GTG is forcefully terminated (e.g.: on shutdown).
311@contextmanager
312def signal_catcher(callback):
313 #if TERM or ABORT are caught, we execute the callback function
314 for s in [signal.SIGABRT, signal.SIGTERM]:
315 signal.signal(s, lambda a,b: callback())
316 yield
317
318initialize(app_name = "Getting Things GNOME!",
319 message = "GTG" + info.VERSION +
320 _(" has crashed. Please report the bug on <a "\
321 "href=\"http://bugs.edge.launchpad.net/gtg\">our Launchpad page</a>."\
322 " If you have Apport installed, it will be started for you."), \
323 use_apport = True)
300324
=== modified file 'GTG/gtk/delete_dialog.py'
--- GTG/gtk/delete_dialog.py 2010-06-23 00:38:13 +0000
+++ GTG/gtk/delete_dialog.py 2010-06-23 01:19:23 +0000
@@ -43,7 +43,11 @@
43 """if we pass a tid as a parameter, we delete directly43 """if we pass a tid as a parameter, we delete directly
44 otherwise, we will look which tid is selected"""44 otherwise, we will look which tid is selected"""
45 for tid in self.tids_todelete:45 for tid in self.tids_todelete:
46 self.req.delete_task(tid)46 task = self.req.get_task(tid)
47 if task:
48 task.delete()
49 else:
50 print "trying to delete task already deleted"
47 self.tids_todelete = []51 self.tids_todelete = []
4852
49 def delete_tasks(self, tids=None):53 def delete_tasks(self, tids=None):
5054
=== modified file 'GTG/gtk/editor/editor.py'
--- GTG/gtk/editor/editor.py 2010-06-07 21:14:45 +0000
+++ GTG/gtk/editor/editor.py 2010-06-23 01:19:23 +0000
@@ -39,7 +39,7 @@
39from GTG import _39from GTG import _
40from GTG import ngettext40from GTG import ngettext
41from GTG import PLUGIN_DIR41from GTG import PLUGIN_DIR
42from GTG import DATA_DIR42from GTG.core import CoreConfig
43from GTG.gtk.editor import GnomeConfig43from GTG.gtk.editor import GnomeConfig
44from GTG.gtk.editor.taskview import TaskView44from GTG.gtk.editor.taskview import TaskView
45from GTG.core.plugins.engine import PluginEngine45from GTG.core.plugins.engine import PluginEngine
@@ -176,7 +176,7 @@
176 self.pengine = PluginEngine(PLUGIN_DIR)176 self.pengine = PluginEngine(PLUGIN_DIR)
177 self.te_plugin_api = PluginAPI(window = self.window,177 self.te_plugin_api = PluginAPI(window = self.window,
178 config = None,178 config = None,
179 data_dir = DATA_DIR,179 data_dir = CoreConfig().get_data_dir(),
180 builder = self.builder, 180 builder = self.builder,
181 requester = self.req,181 requester = self.req,
182 tagpopup = None,182 tagpopup = None,
183183
=== modified file 'GTG/gtk/manager.py'
--- GTG/gtk/manager.py 2010-06-10 14:45:36 +0000
+++ GTG/gtk/manager.py 2010-06-23 01:19:23 +0000
@@ -40,7 +40,11 @@
40from GTG.core.plugins.api import PluginAPI40from GTG.core.plugins.api import PluginAPI
41from GTG.tools.logger import Log41from GTG.tools.logger import Log
4242
43
44
43class Manager:45class Manager:
46
47
44 ############## init #####################################################48 ############## init #####################################################
45 def __init__(self, req, config):49 def __init__(self, req, config):
46 self.config_obj = config50 self.config_obj = config
@@ -72,9 +76,9 @@
72 #Deletion UI76 #Deletion UI
73 self.delete_dialog = None77 self.delete_dialog = None
74 78
75 #Preferences windows79 #Preferences and Backends windows
76 # Initialize "Preferences" dialog80 # Initialize dialogs
77 self.preferences = None81 self.preferences_dialog = None
78 82
79 #DBus83 #DBus
80 DBusTaskWrapper(self.req, self)84 DBusTaskWrapper(self.req, self)
@@ -89,7 +93,7 @@
89 # initializes the plugin api class93 # initializes the plugin api class
90 self.plugin_api = PluginAPI(window = self.browser.window,94 self.plugin_api = PluginAPI(window = self.browser.window,
91 config = self.config,95 config = self.config,
92 data_dir = GTG.DATA_DIR,96 data_dir = self.config_obj.get_data_dir(),
93 builder = self.browser.builder,97 builder = self.browser.builder,
94 requester = self.req,98 requester = self.req,
95 tagpopup = self.browser.tagpopup,99 tagpopup = self.browser.tagpopup,
@@ -189,8 +193,8 @@
189 193
190################ Others dialog ############################################194################ Others dialog ############################################
191195
192 def show_preferences(self, config_priv, sender=None):196 def open_preferences(self, config_priv, sender=None):
193 if not self.preferences:197 if not hasattr(self, "preferences"):
194 self.preferences = PreferencesDialog(self.pengine, self.p_apis, \198 self.preferences = PreferencesDialog(self.pengine, self.p_apis, \
195 self.config_obj)199 self.config_obj)
196 self.preferences.activate(config_priv)200 self.preferences.activate(config_priv)
197201
=== modified file 'GTG/gtk/preferences.glade'
--- GTG/gtk/preferences.glade 2010-06-02 18:12:23 +0000
+++ GTG/gtk/preferences.glade 2010-06-23 01:19:23 +0000
@@ -213,63 +213,10 @@
213 </packing>213 </packing>
214 </child>214 </child>
215 <child>215 <child>
216 <object class="GtkAlignment" id="prefs-alignment3">216 <placeholder/>
217 <property name="visible">True</property>
218 <property name="top_padding">10</property>
219 <property name="bottom_padding">10</property>
220 <property name="left_padding">10</property>
221 <property name="right_padding">10</property>
222 <child>
223 <object class="GtkVBox" id="prefs-vbox5">
224 <property name="visible">True</property>
225 <property name="spacing">6</property>
226 <child>
227 <object class="GtkLabel" id="prefs-label6">
228 <property name="visible">True</property>
229 <property name="xalign">0</property>
230 <property name="label" translatable="yes">Task _Backends:</property>
231 <property name="use_underline">True</property>
232 </object>
233 <packing>
234 <property name="expand">False</property>
235 <property name="position">0</property>
236 </packing>
237 </child>
238 <child>
239 <object class="GtkScrolledWindow" id="prefs-scrolledwindow1">
240 <property name="visible">True</property>
241 <property name="sensitive">False</property>
242 <property name="can_focus">True</property>
243 <property name="hscrollbar_policy">never</property>
244 <property name="vscrollbar_policy">automatic</property>
245 <child>
246 <object class="GtkTreeView" id="BackendTree">
247 <property name="visible">True</property>
248 <property name="sensitive">False</property>
249 <property name="can_focus">True</property>
250 </object>
251 </child>
252 </object>
253 <packing>
254 <property name="position">1</property>
255 </packing>
256 </child>
257 </object>
258 </child>
259 </object>
260 <packing>
261 <property name="position">1</property>
262 </packing>
263 </child>217 </child>
264 <child type="tab">218 <child type="tab">
265 <object class="GtkLabel" id="prefs-label2">219 <placeholder/>
266 <property name="visible">True</property>
267 <property name="label" translatable="yes">Storage</property>
268 </object>
269 <packing>
270 <property name="position">1</property>
271 <property name="tab_fill">False</property>
272 </packing>
273 </child>220 </child>
274 <child>221 <child>
275 <object class="GtkAlignment" id="prefs-alignment4">222 <object class="GtkAlignment" id="prefs-alignment4">
276223
=== modified file 'GTG/gtk/preferences.py'
--- GTG/gtk/preferences.py 2010-06-10 14:45:36 +0000
+++ GTG/gtk/preferences.py 2010-06-23 01:19:23 +0000
@@ -270,7 +270,7 @@
270 self.config["plugins"]["enabled"] = \270 self.config["plugins"]["enabled"] = \
271 self.pengine.enabled_plugins().keys()271 self.pengine.enabled_plugins().keys()
272272
273 self.config_obj.save_config()273 self.config_obj.save()
274274
275 self.dialog.hide()275 self.dialog.hide()
276 return True276 return True
277277
=== modified file 'GTG/tests/__init__.py'
--- GTG/tests/__init__.py 2010-06-22 09:43:55 +0000
+++ GTG/tests/__init__.py 2010-06-23 01:19:23 +0000
@@ -19,22 +19,31 @@
1919
20"""Unit tests for GTG."""20"""Unit tests for GTG."""
2121
22from GTG.tools.testingmode import TestingMode
23TestingMode().set_testing_mode(True)
24
25
22import unittest26import unittest
2327
28
24from GTG.tests import (29from GTG.tests import (
25 test_tagstore,30 test_tagstore,
26 test_taskviewserial,31 test_taskviewserial,
27 test_tree,32 test_tree,
28 test_apidocs,33 test_apidocs,
34 test_backends,
35 test_datastore,
29 test_filteredtree,36 test_filteredtree,
30 )37 )
3138
32
33def test_suite():39def test_suite():
34 return unittest.TestSuite([40 return unittest.TestSuite([
35 test_tagstore.test_suite(),41 test_tagstore.test_suite(),
36 test_taskviewserial.test_suite(),42 test_taskviewserial.test_suite(),
37 test_tree.test_suite(),43 test_tree.test_suite(),
38 test_apidocs.test_suite(),44 test_apidocs.test_suite(),
45 test_backends.test_suite(),
46 test_datastore.test_suite(),
39 test_filteredtree.test_suite(),47 test_filteredtree.test_suite(),
40 ])48 ])
49
4150
=== modified file 'GTG/tests/test_apidocs.py'
--- GTG/tests/test_apidocs.py 2010-05-29 14:39:28 +0000
+++ GTG/tests/test_apidocs.py 2010-06-23 01:19:23 +0000
@@ -27,6 +27,8 @@
27import shutil27import shutil
28import uuid28import uuid
2929
30from GTG.core import CoreConfig
31
3032
3133
32class TestApiDocs(unittest.TestCase):34class TestApiDocs(unittest.TestCase):
@@ -50,4 +52,6 @@
50 shutil.rmtree(api_dir)52 shutil.rmtree(api_dir)
5153
52def test_suite():54def test_suite():
55 CoreConfig().set_data_dir("./test_data")
56 CoreConfig().set_conf_dir("./test_data")
53 return unittest.TestLoader().loadTestsFromTestCase(TestApiDocs)57 return unittest.TestLoader().loadTestsFromTestCase(TestApiDocs)
5458
=== added file 'GTG/tests/test_backends.py'
--- GTG/tests/test_backends.py 1970-01-01 00:00:00 +0000
+++ GTG/tests/test_backends.py 2010-06-23 01:19:23 +0000
@@ -0,0 +1,191 @@
1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Gettings Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#
6# This program is free software: you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# -----------------------------------------------------------------------------
19
20"""Tests for GTG backends.
21
22Some of these tests will generate files in
23xdg.BaseDirectory.xdg_data_home/gtg directory.
24"""
25
26# Standard imports
27import unittest
28import os
29import xdg
30
31# GTG imports
32from GTG.backends import backend_localfile as localfile
33from GTG.core import datastore
34from GTG.tools import cleanxml
35from GTG.core import CoreConfig
36
37
38class GtgBackendsUniTests(unittest.TestCase):
39 """Tests for GTG backends."""
40
41 def __init__(self, test):
42 unittest.TestCase.__init__(self, test)
43 self.taskfile = ''
44 self.datafile = ''
45 self.taskpath = ''
46 self.datapath = ''
47
48 def SetUp(self):
49 CoreConfig().set_data_dir("./test_data")
50 CoreConfig().set_conf_dir("./test_data")
51
52 def test_localfile_get_name(self):
53 """Tests for localfile/get_name function :
54 - a string is expected.
55 """
56 res = localfile.Backend.get_name()
57 expectedres = "backend_localfile"
58 self.assertEqual(res, expectedres)
59
60 def test_localfile_get_description(self):
61 """Tests for localfile/get_description function :
62 - a string is expected.
63 """
64 res = localfile.Backend.get_description()
65 expectedres = "Your tasks are saved"
66 self.assertEqual(res[:len(expectedres)], expectedres)
67
68
69 def test_localfile_get_static_parameters(self):
70 """Tests for localfile/get_static_parameters function:
71 - a string is expected.
72 """
73 res = localfile.Backend.get_static_parameters()
74 self.assertEqual(res['path']['type'], "string")
75
76 def test_localfile_get_type(self):
77 """Tests for localfile/get_type function:
78 - a string is expected.
79 """
80 res = localfile.Backend.get_type()
81 expectedres = "readwrite"
82 self.assertEqual(res, expectedres)
83
84
85 def test_localfile_backend_method3(self):
86 """Tests for localfile/Backend/remove_task method:
87 - parse task file to check if task has been removed.
88 """
89 self.create_test_environment()
90 doc, configxml = cleanxml.openxmlfile(self.datapath, 'config')
91 xmlproject = doc.getElementsByTagName('backend')
92 for domobj in xmlproject:
93 dic = {}
94 if domobj.hasAttribute("module"):
95 dic["module"] = str(domobj.getAttribute("module"))
96 dic["pid"] = str(domobj.getAttribute("pid"))
97 dic["xmlobject"] = domobj
98 dic["Enabled"] = True
99 dic["path"] = self.taskpath
100 beobj = localfile.Backend(dic)
101 expectedres = True
102 beobj.remove_task("0@1")
103 beobj.quit()
104 dataline = open(self.taskpath, 'r').read()
105 print dataline
106 if "0@1" in dataline:
107 res = False
108 else:
109 res = True
110 expectedres = True
111 self.assertEqual(res, expectedres)
112
113# def test_localfile_backend_method4(self):
114# """Tests for localfile/Backend/get_task method:
115# - Compares task titles to check if method works.
116# """
117# self.create_test_environment()
118# doc, configxml = cleanxml.openxmlfile(self.datapath, 'config')
119# xmlproject = doc.getElementsByTagName('backend')
120# for domobj in xmlproject:
121# dic = {}
122# if domobj.hasAttribute("module"):
123# dic["module"] = str(domobj.getAttribute("module"))
124# dic["pid"] = str(domobj.getAttribute("pid"))
125# dic["xmlobject"] = domobj
126# dic["filename"] = self.taskfile
127# beobj = localfile.Backend(dic)
128# dstore = datastore.DataStore()
129# newtask = dstore.new_task(tid="0@2", pid="1", newtask=True)
130# beobj.get_task(newtask, "0@1")
131# self.assertEqual(newtask.get_title(), u"Ceci est un test")
132
133# def test_localfile_backend_method5(self):
134# """Tests for localfile/Backend/set_task method:
135# - parses task file to check if new task has been stored.
136# """
137# self.create_test_environment()
138# doc, configxml = cleanxml.openxmlfile(self.datapath, 'config')
139# xmlproject = doc.getElementsByTagName('backend')
140# for domobj in xmlproject:
141# dic = {}
142# if domobj.hasAttribute("module"):
143# dic["module"] = str(domobj.getAttribute("module"))
144# dic["pid"] = str(domobj.getAttribute("pid"))
145# dic["xmlobject"] = domobj
146# dic["filename"] = self.taskfile
147# beobj = localfile.Backend(dic)
148# dstore = datastore.DataStore()
149# newtask = dstore.new_task(tid="0@2", pid="1", newtask=True)
150# beobj.set_task(newtask)
151# dataline = open(self.taskpath, 'r').read()
152# if "0@2" in dataline:
153# res = True
154# else:
155# res = False
156# expectedres = True
157# self.assertEqual(res, expectedres)
158
159 def create_test_environment(self):
160 """Create the test environment"""
161 self.taskfile = 'test.xml'
162 self.datafile = 'projectstest.xml'
163 tasks = [
164 '<?xml version="1.0" ?>\n',
165 '<project>\n',
166 '\t<task id="0@1" status="Active" tags="">\n',
167 '\t\t<title>\n',
168 '\t\t\tCeci est un test\n',
169 '\t\t</title>\n',
170 '\t</task>\n',
171 '</project>\n',
172 ]
173 data = [
174 '<?xml version="1.0" ?>\n',
175 '<config>\n',
176 '\t<backend filename="test.xml" module="localfile" pid="1"/>\n',
177 '</config>\n',
178 ]
179 self.testdir = os.path.join(xdg.BaseDirectory.xdg_data_home, 'gtg')
180 if not os.path.exists(self.testdir):
181 os.makedirs(self.testdir)
182 self.taskpath = os.path.join(self.testdir, self.taskfile)
183 self.datapath = os.path.join(self.testdir, self.datafile)
184 open(self.taskpath, 'w').writelines(tasks)
185 open(self.datapath, 'w').writelines(data)
186
187
188def test_suite():
189 CoreConfig().set_data_dir("./test_data")
190 CoreConfig().set_conf_dir("./test_data")
191 return unittest.TestLoader().loadTestsFromName(__name__)
0192
=== added file 'GTG/tests/test_datastore.py'
--- GTG/tests/test_datastore.py 1970-01-01 00:00:00 +0000
+++ GTG/tests/test_datastore.py 2010-06-23 01:19:23 +0000
@@ -0,0 +1,360 @@
1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Gettings Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#
6# This program is free software: you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# -----------------------------------------------------------------------------
19
20'''
21Tests for the datastore
22'''
23
24import unittest
25import uuid
26import random
27import time
28
29import GTG
30from GTG.core.datastore import DataStore
31from GTG.backends.genericbackend import GenericBackend
32from GTG.core import CoreConfig
33
34
35
36class TestDatastore(unittest.TestCase):
37 '''
38 Tests for the DataStore object.
39 '''
40
41
42 def setUp(self):
43 '''
44 Creates the environment for the tests
45 @returns None
46 '''
47 self.datastore = DataStore()
48 self.requester = self.datastore.get_requester()
49
50 def test_task_factory(self):
51 '''
52 Test for the task_factory function
53 '''
54 #generate a Task with a random id
55 tid = str(uuid.uuid4())
56 task = self.datastore.task_factory(tid, newtask = True)
57 self.assertTrue(isinstance(task, GTG.core.task.Task))
58 self.assertEqual(task.get_id(), tid)
59 self.assertEqual(task.is_new(), True)
60 tid = str(uuid.uuid4())
61 task = self.datastore.task_factory(tid, newtask = False)
62 self.assertEqual(task.is_new(), False)
63
64 def test_new_task_and_has_task(self):
65 '''
66 Tests the new_task function
67 '''
68 task = self.datastore.new_task()
69 tid = task.get_id()
70 self.assertTrue(isinstance(tid, str))
71 self.assertTrue(tid != '')
72 self.assertTrue(task.is_new())
73 self.assertTrue(self.datastore.has_task(tid))
74 self.assertTrue(len(self.datastore.get_all_tasks()) == 1)
75
76 def test_get_all_tasks(self):
77 '''
78 Tests the get_all_tasks function
79 '''
80 task_ids = []
81 for i in xrange(1, 10):
82 task = self.datastore.new_task()
83 task_ids.append(task.get_id())
84 return_list =self.datastore.get_all_tasks()
85 self.assertEqual(len(return_list), i)
86 task_ids.sort()
87 return_list.sort()
88 self.assertEqual(task_ids, return_list)
89
90 def test_get_task(self):
91 '''
92 Tests the get_task function
93 '''
94 self.assertEqual(self.datastore.get_task(str(uuid.uuid4())), None)
95 task = self.datastore.new_task()
96 self.assertTrue(isinstance(self.datastore.get_task(task.get_id()),
97 GTG.core.task.Task))
98 self.assertEqual(self.datastore.get_task(task.get_id()), task)
99
100
101 def test_get_tagstore(self):
102 '''
103 Tests the get_tagstore function
104 '''
105 tagstore = self.datastore.get_tagstore()
106 self.assertTrue(isinstance(tagstore, GTG.core.tagstore.TagStore))
107
108 def test_get_requester(self):
109 '''
110 Tests the get_requester function
111 '''
112 requester = self.datastore.get_requester()
113 self.assertTrue(isinstance(requester, GTG.core.requester.Requester))
114
115 def test_get_tasks_tree(self):
116 '''
117 Tests the get_tasks_tree function
118 '''
119 tasks_tree = self.datastore.get_tasks_tree()
120 self.assertTrue(isinstance(tasks_tree, GTG.core.tree.Tree))
121
122 def test_push_task(self):
123 '''
124 Tests the push_task function
125 '''
126 task_ids = []
127 for i in xrange(1, 10):
128 tid = str(uuid.uuid4())
129 if tid not in task_ids:
130 task_ids.append(tid)
131 task = self.datastore.task_factory(tid)
132 return_value1 = self.datastore.push_task(task)
133 self.assertTrue(return_value1)
134 #we do it twice, but it should be pushed only once if it's
135 # working correctly (the second should be discarded)
136 return_value2 = self.datastore.push_task(task)
137 self.assertFalse(return_value2)
138 stored_tasks = self.datastore.get_all_tasks()
139 task_ids.sort()
140 stored_tasks.sort()
141 self.assertEqual(task_ids, stored_tasks)
142
143 def test_register_backend(self):
144 '''
145 Tests the register_backend function. It also tests the
146 get_all_backends and get_backend function as a side effect
147 '''
148 #create a simple backend dictionary
149 backend = FakeBackend(enabled = True)
150 tasks_in_backend_count = int(random.random() * 20)
151 for temp in xrange(0, tasks_in_backend_count):
152 backend.fake_add_random_task()
153 backend_dic = {'backend': backend, 'pid': 'a'}
154 self.datastore.register_backend(backend_dic)
155 all_backends = self.datastore.get_all_backends(disabled = True)
156 self.assertEqual(len(all_backends), 1)
157 registered_backend = self.datastore.get_backend(backend.get_id())
158 self.assertEqual(backend.get_id(), registered_backend.get_id())
159 self.assertTrue(isinstance(registered_backend, \
160 GTG.core.datastore.TaskSource))
161 self.assertTrue(registered_backend.is_enabled())
162 self.assertEqual(registered_backend.fake_get_initialized_count(), 1)
163 #we give some time for the backend to push all its tasks
164 time.sleep(1)
165 self.assertEqual(len(self.datastore.get_all_tasks()), \
166 tasks_in_backend_count)
167
168 #same test, disabled backend
169 backend = FakeBackend(enabled = False)
170 for temp in xrange(1, int(random.random() * 20)):
171 backend.fake_add_random_task()
172 backend_dic = {'backend': backend, 'pid':'b'}
173 self.datastore.register_backend(backend_dic)
174 all_backends = self.datastore.get_all_backends(disabled = True)
175 self.assertEqual(len(all_backends), 2)
176 all_backends = self.datastore.get_all_backends(disabled = False)
177 self.assertEqual(len(all_backends), 1)
178 registered_backend = self.datastore.get_backend(backend.get_id())
179 self.assertEqual(backend.get_id(), registered_backend.get_id())
180 self.assertTrue(isinstance(registered_backend, \
181 GTG.core.datastore.TaskSource))
182 self.assertFalse(registered_backend.is_enabled())
183 self.assertEqual(registered_backend.fake_get_initialized_count(), 0)
184 #we give some time for the backend to push all its tasks (is
185 #shouldn't, since it's disabled, but we give time anyway
186 time.sleep(1)
187 self.assertEqual(len(self.datastore.get_all_tasks()), \
188 tasks_in_backend_count)
189
190 def test_set_backend_enabled(self):
191 '''
192 Tests the set_backend_enabled function
193 '''
194 enabled_backend = FakeBackend(enabled = True)
195 disabled_backend = FakeBackend(enabled = False)
196 self.datastore.register_backend({'backend': enabled_backend, \
197 'pid': str(uuid.uuid4()), \
198 GenericBackend.KEY_DEFAULT_BACKEND: False})
199 self.datastore.register_backend({'backend': disabled_backend,\
200 'pid': str(uuid.uuid4()), \
201 GenericBackend.KEY_DEFAULT_BACKEND: False})
202 #enabling an enabled backend
203 self.datastore.set_backend_enabled(enabled_backend.get_id(), True)
204 self.assertEqual(enabled_backend.fake_get_initialized_count(), 1)
205 self.assertTrue(enabled_backend.is_enabled())
206 #disabling a disabled backend
207 self.datastore.set_backend_enabled(disabled_backend.get_id(), False)
208 self.assertEqual(disabled_backend.fake_get_initialized_count(), 0)
209 self.assertFalse(disabled_backend.is_enabled())
210 #disabling an enabled backend
211 self.datastore.set_backend_enabled(enabled_backend.get_id(), False)
212 self.assertEqual(enabled_backend.fake_get_initialized_count(), 1)
213 self.assertFalse(enabled_backend.is_enabled())
214 time.sleep(1)
215# #enabling a disabled backend
216# self.datastore.set_backend_enabled(disabled_backend.get_id(), True)
217# self.assertEqual(disabled_backend.fake_get_initialized_count(), 1)
218# self.assertTrue(disabled_backend.is_enabled())
219
220 def test_remove_backend(self):
221 '''
222 Tests the remove_backend function
223 '''
224 enabled_backend = FakeBackend(enabled = True)
225 disabled_backend = FakeBackend(enabled = False)
226 self.datastore.register_backend({'backend': enabled_backend, \
227 'pid': str(uuid.uuid4()), \
228 GenericBackend.KEY_DEFAULT_BACKEND: False})
229 self.datastore.register_backend({'backend': disabled_backend,\
230 'pid': str(uuid.uuid4()), \
231 GenericBackend.KEY_DEFAULT_BACKEND: False})
232 #removing an enabled backend
233 self.datastore.remove_backend(enabled_backend.get_id())
234 self.assertFalse(enabled_backend.is_enabled())
235 self.assertTrue(enabled_backend.fake_is_purged())
236 self.assertEqual( \
237 len(self.datastore.get_all_backends(disabled = True)), 1)
238 #removing a disabled backend
239 self.datastore.remove_backend(disabled_backend.get_id())
240 self.assertFalse(disabled_backend.is_enabled())
241 self.assertTrue(disabled_backend.fake_is_purged())
242 self.assertEqual( \
243 len(self.datastore.get_all_backends(disabled = True)), 0)
244
245 def test_flush_all_tasks(self):
246 '''
247 Tests the flush_all_tasks function
248 '''
249 #we add some tasks in the datastore
250 tasks_in_datastore_count = 10 #int(random.random() * 20)
251 for temp in xrange(0, tasks_in_datastore_count):
252 self.datastore.new_task()
253 datastore_stored_tids = self.datastore.get_all_tasks()
254 self.assertEqual(tasks_in_datastore_count, len(datastore_stored_tids))
255
256 #we enable a backend
257 backend = FakeBackend(enabled = True)
258 self.datastore.register_backend({'backend': backend, 'pid': 'a'})
259 #we wait for the signal storm to wear off
260 time.sleep(5)
261 #we sync
262 self.datastore.get_backend(backend.get_id()).sync()
263 #and we inject task in the backend
264 tasks_in_backend_count = 5 #int(random.random() * 20)
265 for temp in xrange(0, tasks_in_backend_count):
266 backend.fake_add_random_task()
267 backend_stored_tids = backend.fake_get_task_ids()
268 self.assertEqual(tasks_in_backend_count, len(backend_stored_tids))
269 self.datastore.flush_all_tasks(backend.get_id())
270 #we wait for the signal storm to wear off
271 time.sleep(2)
272 #we sync
273 self.datastore.get_backend(backend.get_id()).sync()
274 all_tasks_count = tasks_in_backend_count + tasks_in_datastore_count
275 new_datastore_stored_tids = self.datastore.get_all_tasks()
276 new_backend_stored_tids = backend.fake_get_task_ids()
277 self.assertEqual(len(new_backend_stored_tids), all_tasks_count)
278 self.assertEqual(len(new_datastore_stored_tids), all_tasks_count)
279 new_datastore_stored_tids.sort()
280 new_backend_stored_tids.sort()
281 self.assertEqual(new_backend_stored_tids, new_datastore_stored_tids)
282
283
284
285def test_suite():
286 CoreConfig().set_data_dir("./test_data")
287 CoreConfig().set_conf_dir("./test_data")
288 return unittest.TestLoader().loadTestsFromTestCase(TestDatastore)
289
290
291
292class FakeBackend(unittest.TestCase):
293 '''
294 Mimics the behavior of a simple backend. Just used for testing
295 '''
296
297 def __init__(self, enabled = True):
298 self.enabled = enabled
299 self.initialized_count = 0
300 self.tasks_ids = []
301 self.backend_id = str(uuid.uuid4())
302 self.purged = False
303
304 def is_enabled(self):
305 return self.enabled
306
307 def initialize(self):
308 self.initialized_count += 1
309 self.enabled = True
310
311 def queue_set_task(self, task):
312 self.tasks_ids.append(task.get_id())
313
314 def has_task(self, task_id):
315 return task_id in self.tasks_ids
316
317 def queue_remove_task(self, task_id):
318 self.tasks_ids.remove(task_id)
319
320 def get_id(self):
321 return self.backend_id
322
323 def start_get_tasks(self):
324 for task_id in self.tasks_ids:
325 self.datastore.push_task(self.datastore.task_factory(task_id))
326
327 def quit(self, disabled = False):
328 self.enabled = not disabled
329
330 def purge(self):
331 self.purged = True
332
333 def is_default(self):
334 return True
335
336 def set_parameter(self, param_name, param_value):
337 pass
338
339 def get_attached_tags(self):
340 return [CoreConfig.ALLTASKS_TAG]
341
342 def register_datastore(self, datastore):
343 self.datastore = datastore
344
345 ##########################################################################
346 # The following are used just for testing, they're not present inside a
347 # normal backend
348 ##########################################################################
349
350 def fake_get_initialized_count(self):
351 return self.initialized_count
352
353 def fake_get_task_ids(self):
354 return self.tasks_ids
355
356 def fake_add_random_task(self):
357 self.tasks_ids.append(str(uuid.uuid4()))
358
359 def fake_is_purged(self):
360 return self.purged
0361
=== modified file 'GTG/tests/test_tagstore.py'
--- GTG/tests/test_tagstore.py 2010-05-29 12:50:20 +0000
+++ GTG/tests/test_tagstore.py 2010-06-23 01:19:23 +0000
@@ -23,6 +23,7 @@
2323
24from GTG.core.tagstore import Tag24from GTG.core.tagstore import Tag
25from GTG.core.datastore import DataStore25from GTG.core.datastore import DataStore
26from GTG.core import CoreConfig
2627
2728
2829
@@ -117,4 +118,6 @@
117 self.assertEqual(0, len(save_calls))118 self.assertEqual(0, len(save_calls))
118119
119def test_suite():120def test_suite():
121 CoreConfig().set_data_dir("./test_data")
122 CoreConfig().set_conf_dir("./test_data")
120 return unittest.TestLoader().loadTestsFromTestCase(TestTag)123 return unittest.TestLoader().loadTestsFromTestCase(TestTag)
121124
=== modified file 'GTG/tests/test_taskviewserial.py'
--- GTG/tests/test_taskviewserial.py 2010-06-11 14:30:12 +0000
+++ GTG/tests/test_taskviewserial.py 2010-06-23 01:19:23 +0000
@@ -27,6 +27,7 @@
27import unittest27import unittest
2828
29from GTG.gtk.editor import taskviewserial29from GTG.gtk.editor import taskviewserial
30from GTG.core import CoreConfig
30 31
31class GtgBackendsUniTests(unittest.TestCase):32class GtgBackendsUniTests(unittest.TestCase):
32 """Tests for GTG backends."""33 """Tests for GTG backends."""
@@ -47,4 +48,6 @@
47 48
48 49
49def test_suite():50def test_suite():
51 CoreConfig().set_data_dir("./test_data")
52 CoreConfig().set_conf_dir("./test_data")
50 return unittest.TestLoader().loadTestsFromName(__name__)53 return unittest.TestLoader().loadTestsFromName(__name__)
5154
=== modified file 'GTG/tests/test_tree.py'
--- GTG/tests/test_tree.py 2010-02-21 19:21:18 +0000
+++ GTG/tests/test_tree.py 2010-06-23 01:19:23 +0000
@@ -22,6 +22,9 @@
22import unittest22import unittest
2323
24from GTG.core.tree import Tree,TreeNode24from GTG.core.tree import Tree,TreeNode
25from GTG.core import CoreConfig
26
27
2528
26class TestTree(unittest.TestCase):29class TestTree(unittest.TestCase):
27 """Tests for `Tree`."""30 """Tests for `Tree`."""
@@ -119,4 +122,6 @@
119122
120123
121def test_suite():124def test_suite():
125 CoreConfig().set_data_dir("./test_data")
126 CoreConfig().set_conf_dir("./test_data")
122 return unittest.TestLoader().loadTestsFromName(__name__)127 return unittest.TestLoader().loadTestsFromName(__name__)
123128
=== added file 'GTG/tools/borg.py'
--- GTG/tools/borg.py 1970-01-01 00:00:00 +0000
+++ GTG/tools/borg.py 2010-06-23 01:19:23 +0000
@@ -0,0 +1,33 @@
1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Gettings Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#
6# This program is free software: you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# -----------------------------------------------------------------------------
19
20
21
22class Borg(object):
23 """
24 This pattern ensures that all instances of a particular class share
25 the same state (just inherit this class to have it working)
26 """
27
28 _borg_state = {}
29
30 def __init__(self):
31 self.__dict__ = self._borg_state
32
33
034
=== added file 'GTG/tools/keyring.py'
--- GTG/tools/keyring.py 1970-01-01 00:00:00 +0000
+++ GTG/tools/keyring.py 2010-06-23 01:19:23 +0000
@@ -0,0 +1,48 @@
1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Gettings Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#
6# This program is free software: you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# -----------------------------------------------------------------------------
19
20import gnomekeyring
21
22from GTG.tools.borg import Borg
23
24
25
26class Keyring(Borg):
27
28
29 def __init__(self):
30 super(Keyring, self).__init__()
31 if not hasattr(self, "keyring"):
32 self.keyring = gnomekeyring.get_default_keyring_sync()
33
34 def set_password(self, name, password, userid = ""):
35 return gnomekeyring.item_create_sync(
36 self.keyring,
37 gnomekeyring.ITEM_GENERIC_SECRET,
38 name,
39 {"backend": name},
40 password,
41 True)
42
43 def get_password(self, item_id):
44 try:
45 item_info = gnomekeyring.item_get_info_sync(self.keyring, item_id)
46 return item_info.get_secret()
47 except (gnomekeyring.DeniedError, gnomekeyring.NoMatchError):
48 return ""
049
=== modified file 'GTG/tools/logger.py'
--- GTG/tools/logger.py 2010-03-02 06:32:31 +0000
+++ GTG/tools/logger.py 2010-06-23 01:19:23 +0000
@@ -41,6 +41,7 @@
41 #Shouldn't be needed, but the following line makes sure that41 #Shouldn't be needed, but the following line makes sure that
42 # this is a Singleton.42 # this is a Singleton.
43 self.__dict__['_Debug__logger'] = Debug.__logger43 self.__dict__['_Debug__logger'] = Debug.__logger
44 self.debugging_mode = False
4445
45 def __init_logger(self):46 def __init_logger(self):
46 Debug.__logger = logging.getLogger('gtg_logger')47 Debug.__logger = logging.getLogger('gtg_logger')
@@ -60,5 +61,10 @@
60 """ Delegates to the real logger """61 """ Delegates to the real logger """
61 return setattr(Debug.__logger, attr, value)62 return setattr(Debug.__logger, attr, value)
6263
64 def set_debugging_mode(self, value):
65 self.debugging_mode = value
66 def is_debugging_mode(self):
67 return self.debugging_mode
68
63#The singleton itself69#The singleton itself
64Log = Debug()70Log = Debug()
6571
=== added file 'GTG/tools/synchronized.py'
--- GTG/tools/synchronized.py 1970-01-01 00:00:00 +0000
+++ GTG/tools/synchronized.py 2010-06-23 01:19:23 +0000
@@ -0,0 +1,14 @@
1from __future__ import with_statement
2from threading import Lock
3
4def synchronized(fun):
5 the_lock = Lock()
6
7 def fwrap(function):
8 def newFunction(*args, **kw):
9 with the_lock:
10 return function(*args, **kw)
11
12 return newFunction
13
14 return fwrap(fun)
015
=== modified file 'GTG/tools/taskxml.py'
--- GTG/tools/taskxml.py 2010-06-18 16:36:17 +0000
+++ GTG/tools/taskxml.py 2010-06-23 01:19:23 +0000
@@ -60,7 +60,15 @@
60 cur_tags = xmlnode.getAttribute("tags").replace(' ','').split(",")60 cur_tags = xmlnode.getAttribute("tags").replace(' ','').split(",")
61 if "" in cur_tags: cur_tags.remove("")61 if "" in cur_tags: cur_tags.remove("")
62 for tag in cur_tags: cur_task.tag_added(saxutils.unescape(tag))62 for tag in cur_tags: cur_task.tag_added(saxutils.unescape(tag))
63 63
64 #REMOTE TASK IDS
65 remote_ids_list = xmlnode.getElementsByTagName("task-remote-ids")
66 for remote_id in remote_ids_list:
67 if remote_id.childNodes:
68 node = remote_id.childNodes[0]
69 backend_id = node.firstChild.nodeValue
70 remote_task_id = node.childNodes[1].firstChild.nodeValue
71 task.add_remote_id(backend_id, remote_task_id)
64 return cur_task72 return cur_task
6573
66#Task as parameter the doc where to put the XML node74#Task as parameter the doc where to put the XML node
@@ -99,4 +107,18 @@
99 #t_xml.appendChild(element.firstChild)107 #t_xml.appendChild(element.firstChild)
100 cleanxml.addTextNode(doc,t_xml,"content",desc)108 cleanxml.addTextNode(doc,t_xml,"content",desc)
101 #self.__write_textnode(doc,t_xml,"content",t.get_text())109 #self.__write_textnode(doc,t_xml,"content",t.get_text())
110
111 #REMOTE TASK IDS
112 remote_ids_element = doc.createElement("task-remote-ids")
113 t_xml.appendChild(remote_ids_element)
114 remote_ids_dict = task.get_remote_ids()
115 for backend_id, task_id in remote_ids_dict.iteritems():
116 backend_element = doc.createElement('backend')
117 remote_ids_element.appendChild(backend_element)
118 backend_element.appendChild(doc.createTextNode(backend_id))
119 task_element = doc.createElement('task-id')
120 backend_element.appendChild(task_element)
121 task_element.appendChild(doc.createTextNode(task_id))
122
123
102 return t_xml124 return t_xml
103125
=== added file 'GTG/tools/testingmode.py'
--- GTG/tools/testingmode.py 1970-01-01 00:00:00 +0000
+++ GTG/tools/testingmode.py 2010-06-23 01:19:23 +0000
@@ -0,0 +1,16 @@
1from GTG.tools.borg import Borg
2
3
4
5class TestingMode(Borg):
6
7
8 def set_testing_mode(self, value):
9 self._testing_mode = value
10
11 def get_testing_mode(self):
12 try:
13 return self._testing_mode
14 except:
15 return False
16
017
=== modified file 'Makefile'
--- Makefile 2010-03-01 01:43:33 +0000
+++ Makefile 2010-06-23 01:19:23 +0000
@@ -35,7 +35,7 @@
35# Check for coding standard violations & flakes.35# Check for coding standard violations & flakes.
36lint: pyflakes pep836lint: pyflakes pep8
3737
38.PHONY: check lint pyflakes pep8 apidocs38.PHONY: check lint pyflakes pep8 apidocs edit-apidocs clean
3939
40#Ignore the exit code in pyflakes, so that pep8 is always run when "make lint"40#Ignore the exit code in pyflakes, so that pep8 is always run when "make lint"
41.IGNORE: pyflakes41.IGNORE: pyflakes
4242
=== modified file 'scripts/debug.sh'
--- scripts/debug.sh 2010-06-21 09:44:23 +0000
+++ scripts/debug.sh 2010-06-23 01:19:23 +0000
@@ -42,6 +42,7 @@
42if [ $norun -eq 0 ]; then42if [ $norun -eq 0 ]; then
43 if [ $profile -eq 1 ]; then43 if [ $profile -eq 1 ]; then
44 python -m cProfile -o gtg.prof ./gtg44 python -m cProfile -o gtg.prof ./gtg
45 python ./scripts/profile_interpret.sh
45 else46 else
46 ./gtg $args47 ./gtg $args
47 fi48 fi
4849
=== added file 'scripts/profile_interpret.sh'
--- scripts/profile_interpret.sh 1970-01-01 00:00:00 +0000
+++ scripts/profile_interpret.sh 2010-06-23 01:19:23 +0000
@@ -0,0 +1,4 @@
1#!/usr/bin/env python
2import pstats
3p = pstats.Stats('gtg.prof')
4p.strip_dirs().sort_stats("cumulative").print_stats(20)

Subscribers

People subscribed via source and target branches

to status/vote changes: