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