Merge lp:~gtg-user/gtg/tomboy-backend into lp:~gtg/gtg/old-trunk
- tomboy-backend
- Merge into old-trunk
Status: | Merged |
---|---|
Merged at revision: | 880 |
Proposed branch: | lp:~gtg-user/gtg/tomboy-backend |
Merge into: | lp:~gtg/gtg/old-trunk |
Prerequisite: | lp:~gtg-user/gtg/multibackends-halfgsoc_merge |
Diff against target: |
1137 lines (+1102/-0) 5 files modified
CHANGELOG (+1/-0) GTG/backends/backend_gnote.py (+61/-0) GTG/backends/backend_tomboy.py (+60/-0) GTG/backends/generictomboy.py (+583/-0) GTG/tests/test_backend_tomboy.py (+397/-0) |
To merge this branch: | bzr merge lp:~gtg-user/gtg/tomboy-backend |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gtg developers | Pending | ||
Lionel Dricot | Pending | ||
Review via email: mp+32715@code.launchpad.net |
This proposal supersedes a proposal from 2010-08-13.
Commit message
Description of the change
The tomboy backend. Any tomboy note matching a particular tag will be inserted (r/w) in GTG.
It handles gracefully the change of the "attached tags", that are the tags to which the backend is looking for. The r/w synchronization engine is included.
It's complemented with a series of testcases and exception handling (when tomboy is put under stress - ~500 notes on my laptop - it begins to drop connections on dbus).
This is just to show the code for a review. I'll need to review the docstrings and use this backend for a couple of weeks before considering it stable - it works pretty well so far-.
Lionel Dricot (ploum-deactivatedaccount) wrote : Posted in a previous version of this proposal | # |
Luca Invernizzi (invernizzi) wrote : | # |
Added testcase for tomboy(forgot to add this). Needs the updated backends framework (merge request submitted) to pass the test.
- 884. By Luca Invernizzi
-
cherrypicking from my development branch
- 885. By Luca Invernizzi
-
merge w/ trunk
- 886. By Luca Invernizzi
-
small typo in docs:
- 887. By Luca Invernizzi
-
merge w/ trunk
- 888. By Luca Invernizzi
-
cherrypicking from my development branch
Preview Diff
1 | === modified file 'CHANGELOG' |
2 | --- CHANGELOG 2010-08-04 00:30:22 +0000 |
3 | +++ CHANGELOG 2010-08-26 23:33:46 +0000 |
4 | @@ -4,6 +4,7 @@ |
5 | * Fixed bug with data consistency #579189, by Marko Kevac |
6 | * Added samba bugzilla to the bugzilla plugin, by Jelmer Vernoij |
7 | * Fixed bug #532392, a start date is later than a due date, by Volodymyr Floreskul |
8 | + * New Tomboy/Gnote backend, by Luca Invernizzi |
9 | |
10 | 2010-03-01 Getting Things GNOME! 0.2.2 |
11 | * Autostart on login, by Luca Invernizzi |
12 | |
13 | === added file 'GTG/backends/backend_gnote.py' |
14 | --- GTG/backends/backend_gnote.py 1970-01-01 00:00:00 +0000 |
15 | +++ GTG/backends/backend_gnote.py 2010-08-26 23:33:46 +0000 |
16 | @@ -0,0 +1,61 @@ |
17 | +# -*- coding: utf-8 -*- |
18 | +# ----------------------------------------------------------------------------- |
19 | +# Gettings Things Gnome! - a personal organizer for the GNOME desktop |
20 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
21 | +# |
22 | +# This program is free software: you can redistribute it and/or modify it under |
23 | +# the terms of the GNU General Public License as published by the Free Software |
24 | +# Foundation, either version 3 of the License, or (at your option) any later |
25 | +# version. |
26 | +# |
27 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
28 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
29 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
30 | +# details. |
31 | +# |
32 | +# You should have received a copy of the GNU General Public License along with |
33 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
34 | +# ----------------------------------------------------------------------------- |
35 | + |
36 | +''' |
37 | +The gnote backend. The actual backend is all in GenericTomboy, since it's |
38 | +shared with the tomboy backend. |
39 | +''' |
40 | +#To introspect tomboy: qdbus org.gnome.Tomboy /org/gnome/Tomboy/RemoteControl |
41 | + |
42 | +from GTG.backends.genericbackend import GenericBackend |
43 | +from GTG import _ |
44 | +from GTG.backends.generictomboy import GenericTomboy |
45 | + |
46 | + |
47 | + |
48 | +class Backend(GenericTomboy): |
49 | + ''' |
50 | + A simple class that adds some description to the GenericTomboy class. |
51 | + It's done this way since Tomboy and Gnote backends have different |
52 | + descriptions and Dbus addresses but the same backend behind them. |
53 | + ''' |
54 | + |
55 | + |
56 | + _general_description = { \ |
57 | + GenericBackend.BACKEND_NAME: "backend_gnote", \ |
58 | + GenericBackend.BACKEND_HUMAN_NAME: _("Gnote"), \ |
59 | + GenericBackend.BACKEND_AUTHORS: ["Luca Invernizzi"], \ |
60 | + GenericBackend.BACKEND_TYPE: GenericBackend.TYPE_READWRITE, \ |
61 | + GenericBackend.BACKEND_DESCRIPTION: \ |
62 | + _("This backend can synchronize all or part of your Gnote" |
63 | + " notes in GTG. If you decide it would be handy to" |
64 | + " have one of your notes in your TODO list, just tag it " |
65 | + "with the tag you have chosen (you'll configure it later" |
66 | + "), and it will appear in GTG."),\ |
67 | + } |
68 | + |
69 | + _static_parameters = { \ |
70 | + GenericBackend.KEY_ATTACHED_TAGS: {\ |
71 | + GenericBackend.PARAM_TYPE: GenericBackend.TYPE_LIST_OF_STRINGS, \ |
72 | + GenericBackend.PARAM_DEFAULT_VALUE: ["@GTG-Gnote"]}, \ |
73 | + } |
74 | + |
75 | + _BUS_ADDRESS = ("org.gnome.Gnote", |
76 | + "/org/gnome/Gnote/RemoteControl", |
77 | + "org.gnome.Gnote.RemoteControl") |
78 | |
79 | === added file 'GTG/backends/backend_tomboy.py' |
80 | --- GTG/backends/backend_tomboy.py 1970-01-01 00:00:00 +0000 |
81 | +++ GTG/backends/backend_tomboy.py 2010-08-26 23:33:46 +0000 |
82 | @@ -0,0 +1,60 @@ |
83 | +# -*- coding: utf-8 -*- |
84 | +# ----------------------------------------------------------------------------- |
85 | +# Getting Things Gnome! - a personal organizer for the GNOME desktop |
86 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
87 | +# |
88 | +# This program is free software: you can redistribute it and/or modify it under |
89 | +# the terms of the GNU General Public License as published by the Free Software |
90 | +# Foundation, either version 3 of the License, or (at your option) any later |
91 | +# version. |
92 | +# |
93 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
94 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
95 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
96 | +# details. |
97 | +# |
98 | +# You should have received a copy of the GNU General Public License along with |
99 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
100 | +# ----------------------------------------------------------------------------- |
101 | + |
102 | +''' |
103 | +The tomboy backend. The actual backend is all in GenericTomboy, since it's |
104 | +shared with the Gnote backend. |
105 | +''' |
106 | + |
107 | +from GTG.backends.genericbackend import GenericBackend |
108 | +from GTG import _ |
109 | +from GTG.backends.generictomboy import GenericTomboy |
110 | + |
111 | + |
112 | + |
113 | +class Backend(GenericTomboy): |
114 | + ''' |
115 | + A simple class that adds some description to the GenericTomboy class. |
116 | + It's done this way since Tomboy and Gnote backends have different |
117 | + descriptions and Dbus addresses but the same backend behind them. |
118 | + ''' |
119 | + |
120 | + |
121 | + _general_description = { \ |
122 | + GenericBackend.BACKEND_NAME: "backend_tomboy", \ |
123 | + GenericBackend.BACKEND_HUMAN_NAME: _("Tomboy"), \ |
124 | + GenericBackend.BACKEND_AUTHORS: ["Luca Invernizzi"], \ |
125 | + GenericBackend.BACKEND_TYPE: GenericBackend.TYPE_READWRITE, \ |
126 | + GenericBackend.BACKEND_DESCRIPTION: \ |
127 | + _("This backend can synchronize all or part of your Tomboy" |
128 | + " notes in GTG. If you decide it would be handy to" |
129 | + " have one of your notes in your TODO list, just tag it " |
130 | + "with the tag you have chosen (you'll configure it later" |
131 | + "), and it will appear in GTG."),\ |
132 | + } |
133 | + |
134 | + _static_parameters = { \ |
135 | + GenericBackend.KEY_ATTACHED_TAGS: {\ |
136 | + GenericBackend.PARAM_TYPE: GenericBackend.TYPE_LIST_OF_STRINGS, \ |
137 | + GenericBackend.PARAM_DEFAULT_VALUE: ["@GTG-Tomboy"]}, \ |
138 | + } |
139 | + |
140 | + _BUS_ADDRESS = ("org.gnome.Tomboy", |
141 | + "/org/gnome/Tomboy/RemoteControl", |
142 | + "org.gnome.Tomboy.RemoteControl") |
143 | |
144 | === added file 'GTG/backends/generictomboy.py' |
145 | --- GTG/backends/generictomboy.py 1970-01-01 00:00:00 +0000 |
146 | +++ GTG/backends/generictomboy.py 2010-08-26 23:33:46 +0000 |
147 | @@ -0,0 +1,583 @@ |
148 | +# -*- coding: utf-8 -*- |
149 | +# ----------------------------------------------------------------------------- |
150 | +# Getting Things Gnome! - a personal organizer for the GNOME desktop |
151 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
152 | +# |
153 | +# This program is free software: you can redistribute it and/or modify it under |
154 | +# the terms of the GNU General Public License as published by the Free Software |
155 | +# Foundation, either version 3 of the License, or (at your option) any later |
156 | +# version. |
157 | +# |
158 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
159 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
160 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
161 | +# details. |
162 | +# |
163 | +# You should have received a copy of the GNU General Public License along with |
164 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
165 | +# ----------------------------------------------------------------------------- |
166 | + |
167 | +''' |
168 | +Contains the Backend class for both Tomboy and Gnote |
169 | +''' |
170 | +#Note: To introspect tomboy, execute: |
171 | +# qdbus org.gnome.Tomboy /org/gnome/Tomboy/RemoteControl |
172 | + |
173 | +import os |
174 | +import threading |
175 | +import uuid |
176 | +import dbus |
177 | +import datetime |
178 | +import unicodedata |
179 | + |
180 | +from GTG.tools.testingmode import TestingMode |
181 | +from GTG.tools.borg import Borg |
182 | +from GTG.backends.genericbackend import GenericBackend |
183 | +from GTG.backends.backendsignals import BackendSignals |
184 | +from GTG.backends.syncengine import SyncEngine, SyncMeme |
185 | +from GTG.tools.logger import Log |
186 | +from GTG.tools.watchdog import Watchdog |
187 | +from GTG.tools.interruptible import interruptible |
188 | +from GTG.tools.tags import extract_tags_from_text |
189 | + |
190 | + |
191 | + |
192 | +class GenericTomboy(GenericBackend): |
193 | + '''Backend class for Tomboy/Gnote''' |
194 | + |
195 | + |
196 | +############################################################################### |
197 | +### Backend standard methods ################################################## |
198 | +############################################################################### |
199 | + |
200 | + def __init__(self, parameters): |
201 | + """ |
202 | + See GenericBackend for an explanation of this function. |
203 | + """ |
204 | + super(GenericTomboy, self).__init__(parameters) |
205 | + #loading the saved state of the synchronization, if any |
206 | + self.data_path = os.path.join('backends/tomboy/', \ |
207 | + "sync_engine-" + self.get_id()) |
208 | + self.sync_engine = self._load_pickled_file(self.data_path, \ |
209 | + SyncEngine()) |
210 | + #if the backend is being tested, we connect to a different DBus |
211 | + # interface to avoid clashing with a running instance of Tomboy |
212 | + if TestingMode().get_testing_mode(): |
213 | + #just used for testing purposes |
214 | + self.BUS_ADDRESS = \ |
215 | + self._parameters["use this fake connection instead"] |
216 | + else: |
217 | + self.BUS_ADDRESS = self._BUS_ADDRESS |
218 | + #we let some time pass before considering a tomboy task for importing, |
219 | + # as the user may still be editing it. Here, we store the Timer objects |
220 | + # that will execute after some time after each tomboy signal. |
221 | + #NOTE: I'm not sure if this is the case anymore (but it shouldn't hurt |
222 | + # anyway). (invernizzi) |
223 | + self._tomboy_setting_timers = {} |
224 | + |
225 | + def initialize(self): |
226 | + ''' |
227 | + See GenericBackend for an explanation of this function. |
228 | + Connects to the session bus and sets the callbacks for bus signals |
229 | + ''' |
230 | + super(GenericTomboy, self).initialize() |
231 | + with self.DbusWatchdog(self): |
232 | + bus = dbus.SessionBus() |
233 | + bus.add_signal_receiver(self.on_note_saved, |
234 | + dbus_interface = self.BUS_ADDRESS[2], |
235 | + signal_name = "NoteSaved") |
236 | + bus.add_signal_receiver(self.on_note_deleted, |
237 | + dbus_interface = self.BUS_ADDRESS[2], |
238 | + signal_name = "NoteDeleted") |
239 | + |
240 | + @interruptible |
241 | + def start_get_tasks(self): |
242 | + ''' |
243 | + See GenericBackend for an explanation of this function. |
244 | + Gets all the notes from Tomboy and sees if they must be added in GTG |
245 | + (and, if so, it adds them). |
246 | + ''' |
247 | + with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy: |
248 | + with self.DbusWatchdog(self): |
249 | + tomboy_notes = [note_id for note_id in \ |
250 | + tomboy.ListAllNotes()] |
251 | + #adding the new ones |
252 | + for note in tomboy_notes: |
253 | + self.cancellation_point() |
254 | + self._process_tomboy_note(note) |
255 | + #checking if some notes have been deleted while GTG was not running |
256 | + stored_notes_ids = self.sync_engine.get_all_remote() |
257 | + for note in set(stored_notes_ids).difference(set(tomboy_notes)): |
258 | + self.on_note_deleted(note, None) |
259 | + |
260 | + def save_state(self): |
261 | + '''Saves the state of the synchronization''' |
262 | + self._store_pickled_file(self.data_path, self.sync_engine) |
263 | + |
264 | + def quit(self, disable = False): |
265 | + ''' |
266 | + See GenericBackend for an explanation of this function. |
267 | + ''' |
268 | + def quit_thread(): |
269 | + while True: |
270 | + try: |
271 | + [key, timer] = \ |
272 | + self._tomboy_setting_timers.iteritems().next() |
273 | + except StopIteration: |
274 | + break |
275 | + timer.cancel() |
276 | + del self._tomboy_setting_timers[key] |
277 | + threading.Thread(target = quit_thread).start() |
278 | + super(GenericTomboy, self).quit(disable) |
279 | + |
280 | +############################################################################### |
281 | +### Something got removed ##################################################### |
282 | +############################################################################### |
283 | + |
284 | + @interruptible |
285 | + def on_note_deleted(self, note, something): |
286 | + ''' |
287 | + Callback, executed when a tomboy note is deleted. |
288 | + Deletes the related GTG task. |
289 | + |
290 | + @param note: the id of the Tomboy note |
291 | + @param something: not used, here for signal callback compatibility |
292 | + ''' |
293 | + with self.datastore.get_backend_mutex(): |
294 | + self.cancellation_point() |
295 | + try: |
296 | + tid = self.sync_engine.get_local_id(note) |
297 | + except KeyError: |
298 | + return |
299 | + if self.datastore.has_task(tid): |
300 | + self.datastore.request_task_deletion(tid) |
301 | + self.break_relationship(remote_id = note) |
302 | + |
303 | + @interruptible |
304 | + def remove_task(self, tid): |
305 | + ''' |
306 | + See GenericBackend for an explanation of this function. |
307 | + ''' |
308 | + with self.datastore.get_backend_mutex(): |
309 | + self.cancellation_point() |
310 | + try: |
311 | + note = self.sync_engine.get_remote_id(tid) |
312 | + except KeyError: |
313 | + return |
314 | + with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy: |
315 | + with self.DbusWatchdog(self): |
316 | + if tomboy.NoteExists(note): |
317 | + tomboy.DeleteNote(note) |
318 | + self.break_relationship(local_id = tid) |
319 | + |
320 | + def _exec_lost_syncability(self, tid, note): |
321 | + ''' |
322 | + Executed when a relationship between tasks loses its syncability |
323 | + property. See SyncEngine for an explanation of that. |
324 | + This function finds out which object (task/note) is the original one |
325 | + and which is the copy, and deletes the copy. |
326 | + |
327 | + @param tid: a GTG task tid |
328 | + @param note: a tomboy note id |
329 | + ''' |
330 | + self.cancellation_point() |
331 | + meme = self.sync_engine.get_meme_from_remote_id(note) |
332 | + #First of all, the relationship is lost |
333 | + self.sync_engine.break_relationship(remote_id = note) |
334 | + if meme.get_origin() == "GTG": |
335 | + with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy: |
336 | + with self.DbusWatchdog(self): |
337 | + tomboy.DeleteNote(note) |
338 | + else: |
339 | + self.datastore.request_task_deletion(tid) |
340 | + |
341 | +############################################################################### |
342 | +### Process tasks ############################################################# |
343 | +############################################################################### |
344 | + |
345 | + def _process_tomboy_note(self, note): |
346 | + ''' |
347 | + Given a tomboy note, finds out if it must be synced to a GTG note and, |
348 | + if so, it carries out the synchronization (by creating or updating a GTG |
349 | + task, or deleting itself if the related task has been deleted) |
350 | + |
351 | + @param note: a Tomboy note id |
352 | + ''' |
353 | + with self.datastore.get_backend_mutex(): |
354 | + self.cancellation_point() |
355 | + is_syncable = self._tomboy_note_is_syncable(note) |
356 | + with self.DbusWatchdog(self): |
357 | + action, tid = self.sync_engine.analyze_remote_id(note, \ |
358 | + self.datastore.has_task, \ |
359 | + self._tomboy_note_exists, is_syncable) |
360 | + Log.debug("processing tomboy (%s, %s)" % (action, is_syncable)) |
361 | + |
362 | + if action == SyncEngine.ADD: |
363 | + tid = str(uuid.uuid4()) |
364 | + task = self.datastore.task_factory(tid) |
365 | + self._populate_task(task, note) |
366 | + self.record_relationship(local_id = tid,\ |
367 | + remote_id = note, \ |
368 | + meme = SyncMeme(task.get_modified(), |
369 | + self.get_modified_for_note(note), |
370 | + self.get_id())) |
371 | + self.datastore.push_task(task) |
372 | + |
373 | + elif action == SyncEngine.UPDATE: |
374 | + task = self.datastore.get_task(tid) |
375 | + meme = self.sync_engine.get_meme_from_remote_id(note) |
376 | + newest = meme.which_is_newest(task.get_modified(), |
377 | + self.get_modified_for_note(note)) |
378 | + if newest == "remote": |
379 | + self._populate_task(task, note) |
380 | + meme.set_local_last_modified(task.get_modified()) |
381 | + meme.set_remote_last_modified(\ |
382 | + self.get_modified_for_note(note)) |
383 | + self.save_state() |
384 | + |
385 | + elif action == SyncEngine.REMOVE: |
386 | + with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy: |
387 | + with self.DbusWatchdog(self): |
388 | + tomboy.DeleteNote(note) |
389 | + try: |
390 | + self.sync_engine.break_relationship(remote_id = note) |
391 | + except KeyError: |
392 | + pass |
393 | + |
394 | + elif action == SyncEngine.LOST_SYNCABILITY: |
395 | + self._exec_lost_syncability(tid, note) |
396 | + |
397 | + @interruptible |
398 | + def set_task(self, task): |
399 | + ''' |
400 | + See GenericBackend for an explanation of this function. |
401 | + ''' |
402 | + self.cancellation_point() |
403 | + is_syncable = self._gtg_task_is_syncable_per_attached_tags(task) |
404 | + tid = task.get_id() |
405 | + with self.datastore.get_backend_mutex(): |
406 | + with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy: |
407 | + with self.DbusWatchdog(self): |
408 | + action, note = self.sync_engine.analyze_local_id(tid, \ |
409 | + self.datastore.has_task, tomboy.NoteExists, \ |
410 | + is_syncable) |
411 | + Log.debug("processing gtg (%s, %d)" % (action, is_syncable)) |
412 | + |
413 | + if action == SyncEngine.ADD: |
414 | + #GTG allows multiple tasks with the same name, |
415 | + #Tomboy doesn't. we need to handle the renaming |
416 | + #manually |
417 | + title = task.get_title() |
418 | + duplicate_counter = 1 |
419 | + with self.DbusWatchdog(self): |
420 | + note = tomboy.CreateNamedNote(title) |
421 | + while note == "": |
422 | + duplicate_counter += 1 |
423 | + note = tomboy.CreateNamedNote(title + "(%d)" % |
424 | + duplicate_counter) |
425 | + if duplicate_counter != 1: |
426 | + #if we needed to rename, we have to rename also |
427 | + # the gtg task |
428 | + task.set_title(title + " (%d)" % duplicate_counter) |
429 | + |
430 | + self._populate_note(note, task) |
431 | + self.record_relationship( \ |
432 | + local_id = tid, remote_id = note, \ |
433 | + meme = SyncMeme(task.get_modified(), |
434 | + self.get_modified_for_note(note), |
435 | + "GTG")) |
436 | + |
437 | + elif action == SyncEngine.UPDATE: |
438 | + meme = self.sync_engine.get_meme_from_local_id(\ |
439 | + task.get_id()) |
440 | + newest = meme.which_is_newest(task.get_modified(), |
441 | + self.get_modified_for_note(note)) |
442 | + if newest == "local": |
443 | + self._populate_note(note, task) |
444 | + meme.set_local_last_modified(task.get_modified()) |
445 | + meme.set_remote_last_modified(\ |
446 | + self.get_modified_for_note(note)) |
447 | + self.save_state() |
448 | + |
449 | + elif action == SyncEngine.REMOVE: |
450 | + self.datastore.request_task_deletion(tid) |
451 | + try: |
452 | + self.sync_engine.break_relationship(local_id = tid) |
453 | + self.save_state() |
454 | + except KeyError: |
455 | + pass |
456 | + |
457 | + elif action == SyncEngine.LOST_SYNCABILITY: |
458 | + self._exec_lost_syncability(tid, note) |
459 | + |
460 | +############################################################################### |
461 | +### Helper methods ############################################################ |
462 | +############################################################################### |
463 | + |
464 | + @interruptible |
465 | + def on_note_saved(self, note): |
466 | + ''' |
467 | + Callback, executed when a tomboy note is saved by Tomboy itself. |
468 | + Updates the related GTG task (or creates one, if necessary). |
469 | + |
470 | + @param note: the id of the Tomboy note |
471 | + ''' |
472 | + self.cancellation_point() |
473 | + #NOTE: we let some seconds pass before executing the real callback, as |
474 | + # the editing of the Tomboy note may still be in progress |
475 | + @interruptible |
476 | + def _execute_on_note_saved(self, note): |
477 | + self.cancellation_point() |
478 | + try: |
479 | + del self._tomboy_setting_timers[note] |
480 | + except: |
481 | + pass |
482 | + self._process_tomboy_note(note) |
483 | + self.save_state() |
484 | + |
485 | + try: |
486 | + self._tomboy_setting_timers[note].cancel() |
487 | + except KeyError: |
488 | + pass |
489 | + finally: |
490 | + timer =threading.Timer(5, _execute_on_note_saved, |
491 | + args = (self, note)) |
492 | + self._tomboy_setting_timers[note] = timer |
493 | + timer.start() |
494 | + |
495 | + def _tomboy_note_is_syncable(self, note): |
496 | + ''' |
497 | + Returns True if this tomboy note should be synced into GTG tasks. |
498 | + |
499 | + @param note: the note id |
500 | + @returns Boolean |
501 | + ''' |
502 | + attached_tags = self.get_attached_tags() |
503 | + if GenericBackend.ALLTASKS_TAG in attached_tags: |
504 | + return True |
505 | + with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy: |
506 | + with self.DbusWatchdog(self): |
507 | + content = tomboy.GetNoteContents(note) |
508 | + syncable = False |
509 | + for tag in attached_tags: |
510 | + try: |
511 | + content.index(tag) |
512 | + syncable = True |
513 | + break |
514 | + except ValueError: |
515 | + pass |
516 | + return syncable |
517 | + |
518 | + def _tomboy_note_exists(self, note): |
519 | + ''' |
520 | + Returns True if a tomboy note exists with the given id. |
521 | + |
522 | + @param note: the note id |
523 | + @returns Boolean |
524 | + ''' |
525 | + with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy: |
526 | + with self.DbusWatchdog(self): |
527 | + return tomboy.NoteExists(note) |
528 | + |
529 | + def get_modified_for_note(self, note): |
530 | + ''' |
531 | + Returns the modification time for the given note id. |
532 | + |
533 | + @param note: the note id |
534 | + @returns datetime.datetime |
535 | + ''' |
536 | + with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy: |
537 | + with self.DbusWatchdog(self): |
538 | + return datetime.datetime.fromtimestamp( \ |
539 | + tomboy.GetNoteChangeDate(note)) |
540 | + |
541 | + def _tomboy_split_title_and_text(self, content): |
542 | + ''' |
543 | + Tomboy does not have a "getTitle" and "getText" functions to get the |
544 | + title and the text of a note separately. Instead, it has a getContent |
545 | + function, that returns both of them. |
546 | + This function splits up the output of getContent into a title string and |
547 | + a text string. |
548 | + |
549 | + @param content: a string, the result of a getContent call |
550 | + @returns list: a list composed by [title, text] |
551 | + ''' |
552 | + try: |
553 | + end_of_title = content.index('\n') |
554 | + except ValueError: |
555 | + return content, unicode("") |
556 | + title = content[: end_of_title] |
557 | + if len(content) > end_of_title: |
558 | + return title, content[end_of_title +1 :] |
559 | + else: |
560 | + return title, unicode("") |
561 | + |
562 | + def _populate_task(self, task, note): |
563 | + ''' |
564 | + Copies the content of a Tomboy note into a task. |
565 | + |
566 | + @param task: a GTG Task |
567 | + @param note: a Tomboy note |
568 | + ''' |
569 | + #add tags objects (it's not enough to have @tag in the text to add a |
570 | + # tag |
571 | + with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy: |
572 | + with self.DbusWatchdog(self): |
573 | + content = tomboy.GetNoteContents(note) |
574 | + #update the tags list |
575 | + task.set_only_these_tags(extract_tags_from_text(content)) |
576 | + #extract title and text |
577 | + [title, text] = self._tomboy_split_title_and_text(unicode(content)) |
578 | + #Tomboy speaks unicode, we don't |
579 | + title = unicodedata.normalize('NFKD', title).encode('ascii', 'ignore') |
580 | + text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore') |
581 | + task.set_title(title) |
582 | + task.set_text(text) |
583 | + task.add_remote_id(self.get_id(), note) |
584 | + |
585 | + def _populate_note(self, note, task): |
586 | + ''' |
587 | + Copies the content of a task into a Tomboy note. |
588 | + |
589 | + @param note: a Tomboy note |
590 | + @param task: a GTG Task |
591 | + ''' |
592 | + title = task.get_title() |
593 | + tested_title = title |
594 | + duplicate_counter = 1 |
595 | + with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy: |
596 | + with self.DbusWatchdog(self): |
597 | + tomboy.SetNoteContents(note, title + '\n' + \ |
598 | + task.get_excerpt(strip_tags = False)) |
599 | + |
600 | + def break_relationship(self, *args, **kwargs): |
601 | + ''' |
602 | + Proxy method for SyncEngine.break_relationship, which also saves the |
603 | + state of the synchronization. |
604 | + ''' |
605 | + #tomboy passes Dbus.String objects, which are not pickable. We convert |
606 | + # those to unicode |
607 | + kwargs["remote_id"] = unicode(kwargs["remote_id"]) |
608 | + try: |
609 | + self.sync_engine.break_relationship(*args, **kwargs) |
610 | + #we try to save the state at each change in the sync_engine: |
611 | + #it's slower, but it should avoid widespread task |
612 | + #duplication |
613 | + self.save_state() |
614 | + except KeyError: |
615 | + pass |
616 | + |
617 | + def record_relationship(self, *args, **kwargs): |
618 | + ''' |
619 | + Proxy method for SyncEngine.break_relationship, which also saves the |
620 | + state of the synchronization. |
621 | + ''' |
622 | + #tomboy passes Dbus.String objects, which are not pickable. We convert |
623 | + # those to unicode |
624 | + kwargs["remote_id"] = unicode(kwargs["remote_id"]) |
625 | + |
626 | + self.sync_engine.record_relationship(*args, **kwargs) |
627 | + #we try to save the state at each change in the sync_engine: |
628 | + #it's slower, but it should avoid widespread task |
629 | + #duplication |
630 | + self.save_state() |
631 | + |
632 | +############################################################################### |
633 | +### Connection handling ####################################################### |
634 | +############################################################################### |
635 | + |
636 | + |
637 | + |
638 | + class TomboyConnection(Borg): |
639 | + ''' |
640 | + TomboyConnection creates a connection to TOMBOY via DBUS and |
641 | + handles all the possible exceptions. |
642 | + It is a class that can be used with a with statement. |
643 | + Example: |
644 | + with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy: |
645 | + #do something |
646 | + ''' |
647 | + |
648 | + |
649 | + def __init__(self, backend, bus_name, bus_path, bus_interface): |
650 | + ''' |
651 | + Sees if a TomboyConnection object already exists. If so, since we |
652 | + are inheriting from a Borg object, the initialization already took |
653 | + place. |
654 | + If not, it tries to connect to Tomboy via Dbus. If the connection |
655 | + is not possible, the user is notified about it. |
656 | + |
657 | + @param backend: a reference to a Backend |
658 | + @param bus_name: the DBUS address of Tomboy |
659 | + @param bus_path: the DBUS path of Tomboy RemoteControl |
660 | + @param bus_interface: the DBUS address of Tomboy RemoteControl |
661 | + ''' |
662 | + super(GenericTomboy.TomboyConnection, self).__init__() |
663 | + if hasattr(self, "tomboy_connection_is_ok") and \ |
664 | + self.tomboy_connection_is_ok: |
665 | + return |
666 | + self.backend = backend |
667 | + with GenericTomboy.DbusWatchdog(backend): |
668 | + bus = dbus.SessionBus() |
669 | + obj = bus.get_object(bus_name, bus_path) |
670 | + self.tomboy = dbus.Interface(obj, bus_interface) |
671 | + self.tomboy_connection_is_ok = True |
672 | + |
673 | + def __enter__(self): |
674 | + ''' |
675 | + Returns the Tomboy connection |
676 | + |
677 | + @returns dbus.Interface |
678 | + ''' |
679 | + return self.tomboy |
680 | + |
681 | + def __exit__(self, exception_type, value, traceback): |
682 | + ''' |
683 | + Checks the state of the connection. |
684 | + If something went wrong for the connection, notifies the user. |
685 | + |
686 | + @param exception_type: the type of exception that occurred, or |
687 | + None |
688 | + @param value: the instance of the exception occurred, or None |
689 | + @param traceback: the traceback of the error |
690 | + @returns: False if some exception must be re-raised. |
691 | + ''' |
692 | + if isinstance(value, dbus.DBusException): |
693 | + self.tomboy_connection_is_ok = False |
694 | + self.backend.quit(disable = True) |
695 | + BackendSignals().backend_failed(self.backend.get_id(), \ |
696 | + BackendSignals.ERRNO_DBUS) |
697 | + else: |
698 | + return False |
699 | + return True |
700 | + |
701 | + |
702 | + |
703 | + class DbusWatchdog(Watchdog): |
704 | + ''' |
705 | + A simple watchdog to detect stale dbus connections |
706 | + ''' |
707 | + |
708 | + |
709 | + def __init__(self, backend): |
710 | + ''' |
711 | + Simple constructor, which sets _when_taking_too_long as the function |
712 | + to run when the connection is taking too long. |
713 | + |
714 | + @param backend: a Backend object |
715 | + ''' |
716 | + self.backend = backend |
717 | + super(GenericTomboy.DbusWatchdog, self).__init__(3, \ |
718 | + self._when_taking_too_long) |
719 | + |
720 | + def _when_taking_too_long(self): |
721 | + ''' |
722 | + Function that is executed when the Dbus connection seems to be |
723 | + hanging. It disables the backend and signals the error to the user. |
724 | + ''' |
725 | + Log.error("Dbus connection is taking too long for the Tomboy/Gnote" |
726 | + "backend!") |
727 | + self.backend.quit(disable = True) |
728 | + BackendSignals().backend_failed(self.backend.get_id(), \ |
729 | + BackendSignals.ERRNO_DBUS) |
730 | + |
731 | |
732 | === added file 'GTG/tests/test_backend_tomboy.py' |
733 | --- GTG/tests/test_backend_tomboy.py 1970-01-01 00:00:00 +0000 |
734 | +++ GTG/tests/test_backend_tomboy.py 2010-08-26 23:33:46 +0000 |
735 | @@ -0,0 +1,397 @@ |
736 | +# -*- coding: utf-8 -*- |
737 | +# ----------------------------------------------------------------------------- |
738 | +# Getting Things Gnome! - a personal organizer for the GNOME desktop |
739 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
740 | +# |
741 | +# This program is free software: you can redistribute it and/or modify it under |
742 | +# the terms of the GNU General Public License as published by the Free Software |
743 | +# Foundation, either version 3 of the License, or (at your option) any later |
744 | +# version. |
745 | +# |
746 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
747 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
748 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
749 | +# details. |
750 | +# |
751 | +# You should have received a copy of the GNU General Public License along with |
752 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
753 | +# ----------------------------------------------------------------------------- |
754 | + |
755 | +''' |
756 | +Tests for the tomboy backend |
757 | +''' |
758 | + |
759 | +import os |
760 | +import sys |
761 | +import errno |
762 | +import unittest |
763 | +import uuid |
764 | +import signal |
765 | +import time |
766 | +import math |
767 | +import dbus |
768 | +import gobject |
769 | +import random |
770 | +import threading |
771 | +import dbus.glib |
772 | +import dbus.service |
773 | +import tempfile |
774 | +from datetime import datetime |
775 | +from dbus.mainloop.glib import DBusGMainLoop |
776 | + |
777 | +from GTG.core.datastore import DataStore |
778 | +from GTG.backends import BackendFactory |
779 | +from GTG.backends.genericbackend import GenericBackend |
780 | + |
781 | + |
782 | + |
783 | +class TestBackendTomboy(unittest.TestCase): |
784 | + ''' |
785 | + Tests for the tomboy backend. |
786 | + ''' |
787 | + |
788 | + |
789 | + def setUp(self): |
790 | + self.spawn_fake_tomboy_server() |
791 | + #only the test process should go further, the dbus server one should |
792 | + #stop here |
793 | + if not self.child_process_pid: return |
794 | + #we create a custom dictionary listening to the server, and register it |
795 | + # in GTG. |
796 | + additional_dic = {} |
797 | + additional_dic["use this fake connection instead"] = \ |
798 | + (FakeTomboy.BUS_NAME, |
799 | + FakeTomboy.BUS_PATH, |
800 | + FakeTomboy.BUS_INTERFACE) |
801 | + additional_dic[GenericBackend.KEY_ATTACHED_TAGS] = \ |
802 | + [GenericBackend.ALLTASKS_TAG] |
803 | + additional_dic[GenericBackend.KEY_DEFAULT_BACKEND] = True |
804 | + dic = BackendFactory().get_new_backend_dict('backend_tomboy', |
805 | + additional_dic) |
806 | + self.datastore = DataStore() |
807 | + self.backend = self.datastore.register_backend(dic) |
808 | + #waiting for the "start_get_tasks" to settle |
809 | + time.sleep(1) |
810 | + #we create a dbus session to speak with the server |
811 | + self.bus = dbus.SessionBus() |
812 | + obj = self.bus.get_object(FakeTomboy.BUS_NAME, FakeTomboy.BUS_PATH) |
813 | + self.tomboy = dbus.Interface(obj, FakeTomboy.BUS_INTERFACE) |
814 | + |
815 | + def spawn_fake_tomboy_server(self): |
816 | + #the fake tomboy server has to be in a different process, |
817 | + #otherwise it will lock on the GIL. |
818 | + #For details, see |
819 | + #http://lists.freedesktop.org/archives/dbus/2007-January/006921.html |
820 | + |
821 | + #we use a lockfile to make sure the server is running before we start |
822 | + # the test |
823 | + lockfile_fd, lockfile_path = tempfile.mkstemp() |
824 | + self.child_process_pid = os.fork() |
825 | + if self.child_process_pid: |
826 | + #we wait in polling that the server has been started |
827 | + while True: |
828 | + try: |
829 | + fd = os.open(lockfile_path, os.O_CREAT | os.O_EXCL | os.O_RDWR) |
830 | + except OSError, e: |
831 | + if e.errno != errno.EEXIST: |
832 | + raise |
833 | + time.sleep(0.3) |
834 | + continue |
835 | + os.close(fd) |
836 | + break |
837 | + else: |
838 | + FakeTomboy() |
839 | + os.close(lockfile_fd) |
840 | + os.unlink(lockfile_path) |
841 | + |
842 | + def tearDown(self): |
843 | + if not self.child_process_pid: return |
844 | + self.datastore.save(quit = True) |
845 | + print "QUIT" |
846 | + time.sleep(0.5) |
847 | + self.tomboy.FakeQuit() |
848 | + self.bus.close() |
849 | + os.kill(self.child_process_pid, signal.SIGKILL) |
850 | + os.waitpid(self.child_process_pid, 0) |
851 | + |
852 | + def test_everything(self): |
853 | + ''' |
854 | + ''' |
855 | + #we cannot use separate test functions because we only want a single |
856 | + # FakeTomboy dbus server running |
857 | + if not self.child_process_pid: return |
858 | + for function in dir(self): |
859 | + if function.startswith("TEST_"): |
860 | + getattr(self, function)() |
861 | + self.tomboy.Reset() |
862 | + for tid in self.datastore.get_all_tasks(): |
863 | + self.datastore.request_task_deletion(tid) |
864 | + time.sleep(0.1) |
865 | + |
866 | + def TEST_processing_tomboy_notes(self): |
867 | + self.backend.set_attached_tags([GenericBackend.ALLTASKS_TAG]) |
868 | + #adding a note |
869 | + note = self.tomboy.CreateNamedNote(str(uuid.uuid4())) |
870 | + self.backend._process_tomboy_note(note) |
871 | + self.assertEqual(len(self.datastore.get_all_tasks()), 1) |
872 | + tid = self.backend.sync_engine.sync_memes.get_local_id(note) |
873 | + task = self.datastore.get_task(tid) |
874 | + #re-adding that (should not change anything) |
875 | + self.backend._process_tomboy_note(note) |
876 | + self.assertEqual(len(self.datastore.get_all_tasks()), 1) |
877 | + self.assertEqual( \ |
878 | + self.backend.sync_engine.sync_memes.get_local_id(note), tid) |
879 | + #removing the note and updating gtg |
880 | + self.tomboy.DeleteNote(note) |
881 | + self.backend.set_task(task) |
882 | + self.assertEqual(len(self.datastore.get_all_tasks()), 0) |
883 | + |
884 | + def TEST_set_task(self): |
885 | + self.backend.set_attached_tags([GenericBackend.ALLTASKS_TAG]) |
886 | + #adding a task |
887 | + task = self.datastore.requester.new_task() |
888 | + task.set_title("title") |
889 | + self.backend.set_task(task) |
890 | + self.assertEqual(len(self.tomboy.ListAllNotes()), 1) |
891 | + note = self.tomboy.ListAllNotes()[0] |
892 | + self.assertEqual(str(self.tomboy.GetNoteTitle(note)), task.get_title()) |
893 | + #re-adding that (should not change anything) |
894 | + self.backend.set_task(task) |
895 | + self.assertEqual(len(self.tomboy.ListAllNotes()), 1) |
896 | + self.assertEqual(note, self.tomboy.ListAllNotes()[0]) |
897 | + #removing the task and updating tomboy |
898 | + self.datastore.request_task_deletion(task.get_id()) |
899 | + self.backend._process_tomboy_note(note) |
900 | + self.assertEqual(len(self.tomboy.ListAllNotes()), 0) |
901 | + |
902 | + def TEST_update_newest(self): |
903 | + self.backend.set_attached_tags([GenericBackend.ALLTASKS_TAG]) |
904 | + task = self.datastore.requester.new_task() |
905 | + task.set_title("title") |
906 | + self.backend.set_task(task) |
907 | + note = self.tomboy.ListAllNotes()[0] |
908 | + gtg_modified = task.get_modified() |
909 | + tomboy_modified = self._modified_string_to_datetime( \ |
910 | + self.tomboy.GetNoteChangeDate(note)) |
911 | + #no-one updated, nothing should happen |
912 | + self.backend.set_task(task) |
913 | + self.assertEqual(gtg_modified, task.get_modified()) |
914 | + self.assertEqual(tomboy_modified, \ |
915 | + self._modified_string_to_datetime( \ |
916 | + self.tomboy.GetNoteChangeDate(note))) |
917 | + #we update the GTG task |
918 | + UPDATED_GTG_TITLE = "UPDATED_GTG_TITLE" |
919 | + task.set_title(UPDATED_GTG_TITLE) |
920 | + self.backend.set_task(task) |
921 | + self.assertTrue(gtg_modified < task.get_modified()) |
922 | + self.assertTrue(tomboy_modified <= |
923 | + self._modified_string_to_datetime( \ |
924 | + self.tomboy.GetNoteChangeDate(note))) |
925 | + self.assertEqual(task.get_title(), UPDATED_GTG_TITLE) |
926 | + self.assertEqual(self.tomboy.GetNoteTitle(note), UPDATED_GTG_TITLE) |
927 | + gtg_modified = task.get_modified() |
928 | + tomboy_modified = self._modified_string_to_datetime( \ |
929 | + self.tomboy.GetNoteChangeDate(note)) |
930 | + #we update the TOMBOY task |
931 | + UPDATED_TOMBOY_TITLE = "UPDATED_TOMBOY_TITLE" |
932 | + #the resolution of tomboy notes changed time is 1 second, so we need |
933 | + # to wait. This *shouldn't* be needed in the actual code because |
934 | + # tomboy signals are always a few seconds late. |
935 | + time.sleep(1) |
936 | + self.tomboy.SetNoteContents(note, UPDATED_TOMBOY_TITLE) |
937 | + self.backend._process_tomboy_note(note) |
938 | + self.assertTrue(gtg_modified <= task.get_modified()) |
939 | + self.assertTrue(tomboy_modified <= |
940 | + self._modified_string_to_datetime( \ |
941 | + self.tomboy.GetNoteChangeDate(note))) |
942 | + self.assertEqual(task.get_title(), UPDATED_TOMBOY_TITLE) |
943 | + self.assertEqual(self.tomboy.GetNoteTitle(note), UPDATED_TOMBOY_TITLE) |
944 | + |
945 | + def TEST_processing_tomboy_notes_with_tags(self): |
946 | + self.backend.set_attached_tags(['@a']) |
947 | + #adding a not syncable note |
948 | + note = self.tomboy.CreateNamedNote("title" + str(uuid.uuid4())) |
949 | + self.backend._process_tomboy_note(note) |
950 | + self.assertEqual(len(self.datastore.get_all_tasks()), 0) |
951 | + #re-adding that (should not change anything) |
952 | + self.backend._process_tomboy_note(note) |
953 | + self.assertEqual(len(self.datastore.get_all_tasks()), 0) |
954 | + #adding a tag to that note |
955 | + self.tomboy.SetNoteContents(note, "something with @a") |
956 | + self.backend._process_tomboy_note(note) |
957 | + self.assertEqual(len(self.datastore.get_all_tasks()), 1) |
958 | + #removing the tag and resyncing |
959 | + self.tomboy.SetNoteContents(note, "something with no tags") |
960 | + self.backend._process_tomboy_note(note) |
961 | + self.assertEqual(len(self.datastore.get_all_tasks()), 0) |
962 | + #adding a syncable note |
963 | + note = self.tomboy.CreateNamedNote("title @a" + str(uuid.uuid4())) |
964 | + self.backend._process_tomboy_note(note) |
965 | + self.assertEqual(len(self.datastore.get_all_tasks()), 1) |
966 | + tid = self.backend.sync_engine.sync_memes.get_local_id(note) |
967 | + task = self.datastore.get_task(tid) |
968 | + #re-adding that (should not change anything) |
969 | + self.backend._process_tomboy_note(note) |
970 | + self.assertEqual(len(self.datastore.get_all_tasks()), 1) |
971 | + self.assertEqual( \ |
972 | + self.backend.sync_engine.sync_memes.get_local_id(note), tid) |
973 | + #removing the note and updating gtg |
974 | + self.tomboy.DeleteNote(note) |
975 | + self.backend.set_task(task) |
976 | + self.assertEqual(len(self.datastore.get_all_tasks()), 0) |
977 | + |
978 | + def TEST_set_task_with_tags(self): |
979 | + self.backend.set_attached_tags(['@a']) |
980 | + #adding a not syncable task |
981 | + task = self.datastore.requester.new_task() |
982 | + task.set_title("title") |
983 | + self.backend.set_task(task) |
984 | + self.assertEqual(len(self.tomboy.ListAllNotes()), 0) |
985 | + #making that task syncable |
986 | + task.set_title("something else") |
987 | + task.add_tag("@a") |
988 | + self.backend.set_task(task) |
989 | + self.assertEqual(len(self.tomboy.ListAllNotes()), 1) |
990 | + note = self.tomboy.ListAllNotes()[0] |
991 | + self.assertEqual(str(self.tomboy.GetNoteTitle(note)), task.get_title()) |
992 | + #re-adding that (should not change anything) |
993 | + self.backend.set_task(task) |
994 | + self.assertEqual(len(self.tomboy.ListAllNotes()), 1) |
995 | + self.assertEqual(note, self.tomboy.ListAllNotes()[0]) |
996 | + #removing the syncable property and updating tomboy |
997 | + task.remove_tag("@a") |
998 | + self.backend.set_task(task) |
999 | + self.assertEqual(len(self.tomboy.ListAllNotes()), 0) |
1000 | + |
1001 | + def TEST_multiple_task_same_title(self): |
1002 | + self.backend.set_attached_tags(['@a']) |
1003 | + how_many_tasks = int(math.ceil(20 * random.random())) |
1004 | + for iteration in xrange(0, how_many_tasks): |
1005 | + task = self.datastore.requester.new_task() |
1006 | + task.set_title("title") |
1007 | + task.add_tag('@a') |
1008 | + self.backend.set_task(task) |
1009 | + self.assertEqual(len(self.tomboy.ListAllNotes()), how_many_tasks) |
1010 | + |
1011 | + def _modified_string_to_datetime(self, modified_string): |
1012 | + return datetime.fromtimestamp(modified_string) |
1013 | + |
1014 | + |
1015 | +def test_suite(): |
1016 | + return unittest.TestLoader().loadTestsFromTestCase(TestBackendTomboy) |
1017 | + |
1018 | + |
1019 | + |
1020 | +class FakeTomboy(dbus.service.Object): |
1021 | + """ |
1022 | + D-Bus service object that mimics TOMBOY |
1023 | + """ |
1024 | + |
1025 | + |
1026 | + #We don't directly use the tomboy dbus path to avoid conflicts |
1027 | + # if tomboy is running during the test |
1028 | + |
1029 | + BUS_NAME = "Fake.Tomboy" |
1030 | + BUS_PATH = "/Fake/Tomboy" |
1031 | + BUS_INTERFACE = "Fake.Tomboy.RemoteControl" |
1032 | + |
1033 | + def __init__(self): |
1034 | + # Attach the object to D-Bus |
1035 | + DBusGMainLoop(set_as_default=True) |
1036 | + self.bus = dbus.SessionBus() |
1037 | + bus_name = dbus.service.BusName(self.BUS_NAME, bus = self.bus) |
1038 | + dbus.service.Object.__init__(self, bus_name, self.BUS_PATH) |
1039 | + self.notes = {} |
1040 | + threading.Thread(target = self.fake_main_loop).start() |
1041 | + |
1042 | + @dbus.service.method(BUS_INTERFACE, in_signature="s", out_signature="s") |
1043 | + def GetNoteContents(self, note): |
1044 | + return self.notes[note]['content'] |
1045 | + |
1046 | + @dbus.service.method(BUS_INTERFACE, in_signature="s", out_signature="b") |
1047 | + def NoteExists(self, note): |
1048 | + return self.notes.has_key(note) |
1049 | + |
1050 | + @dbus.service.method(BUS_INTERFACE, in_signature="s", out_signature="d") |
1051 | + def GetNoteChangeDate(self, note): |
1052 | + return self.notes[note]['changed'] |
1053 | + |
1054 | + @dbus.service.method(BUS_INTERFACE, in_signature="ss") |
1055 | + def SetNoteContents(self, note, text): |
1056 | + self.fake_update_note(note) |
1057 | + self.notes[note]['content'] = text |
1058 | + |
1059 | + @dbus.service.method(BUS_INTERFACE, in_signature="s", out_signature="s") |
1060 | + def GetNoteTitle(self, note): |
1061 | + return self._GetNoteTitle(note) |
1062 | + |
1063 | + def _GetNoteTitle(self, note): |
1064 | + content = self.notes[note]['content'] |
1065 | + try: |
1066 | + end_of_title = content.index('\n') |
1067 | + except ValueError: |
1068 | + return content |
1069 | + return content[:end_of_title] |
1070 | + |
1071 | + @dbus.service.method(BUS_INTERFACE, in_signature="s") |
1072 | + def DeleteNote(self, note): |
1073 | + del self.notes[note] |
1074 | + |
1075 | + @dbus.service.method(BUS_INTERFACE, in_signature="s", out_signature="s") |
1076 | + def CreateNamedNote(self, title): |
1077 | + #this is to mimic the way tomboy handles title clashes |
1078 | + if self._FindNote(title) != '': |
1079 | + return '' |
1080 | + note = str(uuid.uuid4()) |
1081 | + self.notes[note] = {'content': title} |
1082 | + self.fake_update_note(note) |
1083 | + return note |
1084 | + |
1085 | + @dbus.service.method(BUS_INTERFACE, in_signature="s", out_signature="s") |
1086 | + def FindNote(self, title): |
1087 | + return self._FindNote(title) |
1088 | + |
1089 | + def _FindNote(self, title): |
1090 | + for note in self.notes: |
1091 | + if self._GetNoteTitle(note) == title: |
1092 | + return note |
1093 | + return '' |
1094 | + |
1095 | + @dbus.service.method(BUS_INTERFACE, out_signature = "as") |
1096 | + def ListAllNotes(self): |
1097 | + return list(self.notes) |
1098 | + |
1099 | + @dbus.service.signal(BUS_INTERFACE, signature='s') |
1100 | + def NoteSaved(self, note): |
1101 | + pass |
1102 | + |
1103 | + @dbus.service.signal(BUS_INTERFACE, signature='s') |
1104 | + def NoteDeleted(self, note): |
1105 | + pass |
1106 | + |
1107 | +############################################################################### |
1108 | +### Function with the fake_ prefix are here to assist in testing, they do not |
1109 | +### need to be present in the real class |
1110 | +############################################################################### |
1111 | + |
1112 | + def fake_update_note(self, note): |
1113 | + self.notes[note]['changed'] = time.mktime(datetime.now().timetuple()) |
1114 | + |
1115 | + def fake_main_loop(self): |
1116 | + gobject.threads_init() |
1117 | + dbus.glib.init_threads() |
1118 | + self.main_loop = gobject.MainLoop() |
1119 | + self.main_loop.run() |
1120 | + |
1121 | + @dbus.service.method(BUS_INTERFACE) |
1122 | + def Reset(self): |
1123 | + self.notes = {} |
1124 | + |
1125 | + @dbus.service.method(BUS_INTERFACE) |
1126 | + def FakeQuit(self): |
1127 | + threading.Timer(0.2, self._fake_quit).start() |
1128 | + |
1129 | + def _fake_quit(self): |
1130 | + self.main_loop.quit() |
1131 | + sys.exit(0) |
1132 | + |
1133 | |
1134 | === added file 'data/icons/hicolor/scalable/apps/backend_gnote.png' |
1135 | Binary files data/icons/hicolor/scalable/apps/backend_gnote.png 1970-01-01 00:00:00 +0000 and data/icons/hicolor/scalable/apps/backend_gnote.png 2010-08-26 23:33:46 +0000 differ |
1136 | === added file 'data/icons/hicolor/scalable/apps/backend_tomboy.png' |
1137 | Binary files data/icons/hicolor/scalable/apps/backend_tomboy.png 1970-01-01 00:00:00 +0000 and data/icons/hicolor/scalable/apps/backend_tomboy.png 2010-08-26 23:33:46 +0000 differ |
Not touching anything in GTG so, obviously, you can merge it. Very good work !