gtg team mailing list archive
-
gtg team
-
Mailing list archive
-
Message #02743
[Merge] lp:~gtg-user/gtg/tomboy-backend into lp:gtg
Luca Invernizzi has proposed merging lp:~gtg-user/gtg/tomboy-backend into lp:gtg with lp:~gtg-user/gtg/multibackends-halfgsoc_merge as a prerequisite.
Requested reviews:
Lionel Dricot (ploum)
Gtg developers (gtg)
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-.
--
https://code.launchpad.net/~gtg-user/gtg/tomboy-backend/+merge/28257
Your team Gtg developers is requested to review the proposed merge of lp:~gtg-user/gtg/tomboy-backend into lp:gtg.
=== added file 'GTG/backends/backend_tomboy.py'
--- GTG/backends/backend_tomboy.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/backend_tomboy.py 2010-06-23 01:17:32 +0000
@@ -0,0 +1,452 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+'''
+'''
+#To introspect tomboy: qdbus org.gnome.Tomboy /org/gnome/Tomboy/RemoteControl
+
+import os
+import re
+import threading
+import uuid
+import dbus
+import datetime
+
+from GTG.tools.borg import Borg
+from GTG.backends.genericbackend import GenericBackend
+from GTG import _
+from GTG.backends.backendsignals import BackendSignals
+from GTG.backends.syncengine import SyncEngine, SyncMeme
+from GTG.tools.logger import Log
+from GTG.tools.watchdog import Watchdog
+
+
+
+class Backend(GenericBackend):
+
+
+ _general_description = { \
+ GenericBackend.BACKEND_NAME: "backend_tomboy", \
+ GenericBackend.BACKEND_HUMAN_NAME: _("Tomboy/Gnote"), \
+ GenericBackend.BACKEND_AUTHORS: ["Luca Invernizzi"], \
+ GenericBackend.BACKEND_TYPE: GenericBackend.TYPE_READWRITE, \
+ GenericBackend.BACKEND_DESCRIPTION: \
+ _("This backend can synchronize all or part of your Tomboy"
+ "/Gnote notes in GTG. If you decide it would be handy to"
+ " have one of your notes in your TODO list, just tag it "
+ "with the tag you have chosen (you'll configure it later"
+ "), and it will appear in GTG."),\
+ }
+
+ _static_parameters = { \
+ GenericBackend.KEY_ATTACHED_TAGS: {\
+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_LIST_OF_STRINGS, \
+ GenericBackend.PARAM_DEFAULT_VALUE: ["@GTG-Tomboy"]}, \
+ }
+
+###############################################################################
+### Backend standard methods ##################################################
+###############################################################################
+
+ def __init__(self, parameters):
+ """
+ Instantiates a new backend.
+
+ @param parameters: should match the dictionary returned in
+ get_parameters. Anyway, the backend should care if one expected
+ value is None or does not exist in the dictionary.
+ """
+ super(Backend, self).__init__(parameters)
+ #loading the saved state of the synchronization, if any
+ self._tomboy_setting_timers = {}
+ self.data_path = os.path.join('backends/tomboy/', \
+ "sync_engine-" + self.get_id())
+ self.sync_engine = self._load_pickled_file(self.data_path, \
+ SyncEngine())
+ if self._parameters.has_key("use this fake connection instead"):
+ #just used for testing purposes
+ self.BUS_ADDRESS = \
+ self._parameters["use this fake connection instead"]
+ else:
+ self.BUS_ADDRESS = ("org.gnome.Tomboy",
+ "/org/gnome/Tomboy/RemoteControl",
+ "org.gnome.Tomboy.RemoteControl")
+
+ def initialize(self):
+ super(Backend, self).initialize()
+ with self.DbusWatchdog(self):
+ bus = dbus.SessionBus()
+ bus.add_signal_receiver(self.on_note_saved,
+ dbus_interface = self.BUS_ADDRESS[2],
+ signal_name = "NoteSaved")
+ bus.add_signal_receiver(self.on_note_deleted,
+ dbus_interface = self.BUS_ADDRESS[2],
+ signal_name = "NoteDeleted")
+
+ def start_get_tasks(self):
+ '''
+ Once this function is launched, the backend can start pushing
+ tasks to gtg parameters.
+
+ @return: start_get_tasks() might not return or finish
+ '''
+ with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
+ with self.DbusWatchdog(self):
+ tomboy_notes = tomboy.ListAllNotes()
+ for note in tomboy_notes:
+ self._process_tomboy_note(note)
+
+ def save_state(self):
+ self._store_pickled_file(self.data_path, self.sync_engine)
+
+ def get_number_of_tasks(self):
+ '''
+ Returns the number of tasks stored in the backend. Doesn't need to be a
+ fast function, is called just for the UI
+ '''
+ return 42
+
+###############################################################################
+### Something got removed #####################################################
+###############################################################################
+
+ def on_note_deleted(self, note, something):
+ try:
+ tid = self.sync_engine.get_local_id(note)
+ except KeyError:
+ return
+ if self.datastore.has_task(tid):
+ self.datastore.request_task_deletion(tid)
+ try:
+ self.sync_engine.break_relationship(remote_id = note)
+ except:
+ pass
+
+ def remove_task(self, tid):
+ ''' Completely remove the task with ID = tid '''
+ try:
+ note = self.sync_engine.get_remote_id(tid)
+ except KeyError:
+ return
+ with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
+ with self.DbusWatchdog(self):
+ if tomboy.NoteExists(note):
+ tomboy.DeleteNote(note)
+ try:
+ self.sync_engine.break_relationship(local_id = tid)
+ except:
+ pass
+
+ def _exec_lost_syncability(self, tid, note):
+ meme = self.sync_engine.get_meme_from_remote_id(note)
+ if meme.get_origin() == "GTG":
+ with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
+ with self.DbusWatchdog(self):
+ tomboy.DeleteNote(note)
+ else:
+ self.datastore.request_task_deletion(tid)
+ try:
+ self.sync_engine.break_relationship(remote_id = note)
+ except KeyError:
+ pass
+
+###############################################################################
+### Process tasks #############################################################
+###############################################################################
+
+ def _process_tomboy_note(self, note):
+ is_syncable = self._tomboy_note_is_syncable(note)
+ with self.DbusWatchdog(self):
+ action, tid = self.sync_engine.analyze_remote_id(note, \
+ self.datastore.has_task, \
+ self._tomboy_note_exists, is_syncable)
+ Log.debug("processing tomboy (%s, %d)" % (action, is_syncable))
+
+ if action == SyncEngine.ADD:
+ tid = str(uuid.uuid4())
+ task = self.datastore.task_factory(tid)
+ self._populate_task(task, note)
+ self.sync_engine.record_relationship(local_id = tid,\
+ remote_id = str(note), \
+ meme = self._new_meme(task, note, self.get_id()))
+ self.datastore.push_task(task)
+
+ elif action == SyncEngine.UPDATE:
+ task = self.datastore.get_task(tid)
+ meme = self.sync_engine.get_meme_from_remote_id(note)
+ newest = meme.which_is_newest(task.get_modified(),
+ self.get_modified_for_note(note))
+ if newest == "remote":
+ self._populate_task(task, note)
+ self._update_meme(meme, task, note)
+
+ elif action == SyncEngine.REMOVE:
+ with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
+ with self.DbusWatchdog(self):
+ tomboy.DeleteNote(note)
+ try:
+ self.sync_engine.break_relationship(remote_id = note)
+ except KeyError:
+ pass
+
+ elif action == SyncEngine.LOST_SYNCABILITY:
+ self._exec_lost_syncability(tid, note)
+
+ def set_task(self, task):
+ is_syncable = self._task_is_syncable(task)
+ with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
+ tid = task.get_id()
+ with self.DbusWatchdog(self):
+ action, note = self.sync_engine.analyze_local_id(tid, \
+ self.datastore.has_task, tomboy.NoteExists, \
+ is_syncable)
+ Log.debug("processing gtg (%s, %d)" % (action, is_syncable))
+
+ if action == SyncEngine.ADD:
+ #GTG allows multiple tasks with the same name, Tomboy doesn't.
+ #we need to handle the renaming manually
+ title = task.get_title()
+ duplicate_counter = 1
+ with self.DbusWatchdog(self):
+ note = tomboy.CreateNamedNote(title)
+ while note == "":
+ duplicate_counter += 1
+ note = tomboy.CreateNamedNote(title + "(%d)" %
+ duplicate_counter)
+ if duplicate_counter != 1:
+ #if we needed to rename, we have to rename also the gtg task
+ task.set_title(title + " (%d)" % duplicate_counter)
+
+ self.sync_engine.record_relationship( \
+ local_id = tid, remote_id = str(note), \
+ meme = self._new_meme(task, note, "GTG"))
+
+ elif action == SyncEngine.UPDATE:
+ meme = self.sync_engine.get_meme_from_local_id(task.get_id())
+ newest = meme.which_is_newest(task.get_modified(),
+ self.get_modified_for_note(note))
+ if newest == "local":
+ self._populate_note(note, task)
+ self._update_meme(meme, task, note)
+
+ elif action == SyncEngine.REMOVE:
+ self.datastore.request_task_deletion(tid)
+ try:
+ self.sync_engine.break_relationship(local_id = tid)
+ except KeyError:
+ pass
+
+ elif action == SyncEngine.LOST_SYNCABILITY:
+ self._exec_lost_syncability(tid, note)
+
+###############################################################################
+### Helper methods ############################################################
+###############################################################################
+
+ def on_note_saved(self, note):
+ #it is necessary to queue these events as tomboy notes are not edited
+ # atomically: we need to give them some time to be filled with all the
+ # necessary data (an incomplete note might break a relationship with a
+ # task)
+ try:
+ self._tomboy_setting_timers[note].cancel()
+ except KeyError:
+ pass
+ finally:
+ timer =threading.Timer(10, self._execute_on_note_saved, args = [note])
+ self._tomboy_setting_timers[note] = timer
+ timer.start()
+
+
+ def _execute_on_note_saved(self, note):
+ try:
+ del self._tomboy_setting_timers[note]
+ except:
+ pass
+ self._process_tomboy_note(note)
+
+ def _tomboy_note_is_syncable(self, note):
+ attached_tags = self.get_attached_tags()
+ if GenericBackend.ALLTASKS_TAG in attached_tags:
+ return True
+ with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
+ with self.DbusWatchdog(self):
+ content = tomboy.GetNoteContents(note)
+ syncable = False
+ for tag in attached_tags:
+ try:
+ content.index(tag)
+ syncable = True
+ break
+ except ValueError:
+ pass
+ return syncable
+
+ def _task_is_syncable(self, task):
+ attached_tags = self.get_attached_tags()
+ if GenericBackend.ALLTASKS_TAG in attached_tags:
+ return True
+ for tag in task.get_tags_name():
+ if tag in attached_tags:
+ return True
+ return False
+
+ def _tomboy_note_exists(self, note):
+ with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
+ with self.DbusWatchdog(self):
+ return tomboy.NoteExists(note)
+
+ def get_modified_for_note(self, note):
+ with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
+ with self.DbusWatchdog(self):
+ return datetime.datetime.fromtimestamp( \
+ tomboy.GetNoteChangeDate(note))
+
+ def _tomboy_split_title_and_text(self, content):
+ try:
+ end_of_title = content.index('\n')
+ except ValueError:
+ return content, ""
+ title = content[: end_of_title]
+ if len(content) > end_of_title:
+ return title, content[end_of_title +1 :]
+ else:
+ return title, ""
+
+ def _populate_task(self, task, note):
+ #add tags objects (it's not enough to have @tag in the text to add a
+ # tag
+ with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
+ with self.DbusWatchdog(self):
+ content = tomboy.GetNoteContents(note)
+ #update the tags list
+ matches = re.finditer("(?<![^|\s])(@\w+)", content)
+ new_tags_list = [content[g.start() : g.end()] for g in matches]
+ for tag in task.get_tags_name():
+ try:
+ new_tags_list.remove(tag)
+ task.remove_tag(tag)
+ except:
+ task.add_tag(tag)
+ for tag in new_tags_list:
+ task.add_tag(tag)
+ #extract title and text
+ [title, text] = self._tomboy_split_title_and_text(content)
+ task.set_title(title)
+ task.set_text(text)
+ task.add_remote_id(self.get_id(), str(note))
+
+
+ def _populate_note(self, note, task):
+ title = task.get_title()
+ tested_title = title
+ duplicate_counter = 1
+ with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
+ with self.DbusWatchdog(self):
+ while tomboy.FindNote(tested_title) != '':
+ duplicate_counter +=1
+ tested_title = title + " (%d)" % duplicate_counter
+ tomboy.SetNoteContents(note, tested_title + '\n' + \
+ task.get_excerpt())
+
+ def _new_meme(self, task, note, origin):
+ meme = self._update_meme(SyncMeme(), task, note)
+ meme.set_origin(origin)
+ return meme
+
+ def _update_meme(self, meme, task, note):
+ meme.set_local_last_modified(task.get_modified())
+ meme.set_remote_last_modified(self.get_modified_for_note(note))
+ return meme
+
+ def quit(self, disable = False):
+ def quit_thread():
+ while True:
+ try:
+ [key, timer] = \
+ self._tomboy_setting_timers.iteritems().next()
+ except StopIteration:
+ break
+ timer.cancel()
+ del self._tomboy_setting_timers[key]
+ threading.Thread(target = quit_thread).start()
+ super(Backend, self).quit(disable)
+
+###############################################################################
+### Connection handling #######################################################
+###############################################################################
+
+
+
+ class TomboyConnection(Borg):
+ '''
+ TomboyConnection creates a connection to TOMBOY via DBUS and handles all
+ the possible exceptions.
+ It is a class that can be used with a with statement.
+ Example:
+ with self.TomboyConnection(self, *self.BUS_ADDRESS) as tomboy:
+ #do something
+ '''
+
+
+ def __init__(self, backend, bus_name, bus_path, bus_interface):
+ super(Backend.TomboyConnection, self).__init__()
+ if hasattr(self, "tomboy_connection_is_ok") and \
+ self.tomboy_connection_is_ok:
+ return
+ self.backend = backend
+ with Backend.DbusWatchdog(backend):
+ bus = dbus.SessionBus()
+ obj = bus.get_object(bus_name, bus_path)
+ self.tomboy = dbus.Interface(obj, bus_interface)
+ self.tomboy_connection_is_ok = True
+
+ def __enter__(self):
+ return self.tomboy
+
+ def __exit__(self, type, value, traceback):
+ if value == None:
+ return True
+ if isinstance(value, dbus.DBusException):
+ self.tomboy_connection_is_ok = False
+ self.backend.quit(disable = True)
+ BackendSignals().backend_failed(self.backend.get_id(), \
+ BackendSignals.ERRNO_DBUS)
+ else:
+ return False
+ return True
+
+
+
+ class DbusWatchdog(Watchdog):
+ '''
+ A simple watchdog to detect stale dbus connections
+ '''
+
+
+ def __init__(self, backend):
+ super(Backend.DbusWatchdog, self).__init__(10, \
+ self._when_taking_too_long)
+ self.backend = backend
+
+ def _when_taking_too_long(self):
+ self.backend.quit(disable = True)
+ BackendSignals().backend_failed(self.backend.get_id(), \
+ BackendSignals.ERRNO_DBUS)
+
=== added file 'GTG/backends/syncengine.py'
--- GTG/backends/syncengine.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/syncengine.py 2010-06-23 01:17:32 +0000
@@ -0,0 +1,115 @@
+from GTG.tools.twokeydict import TwoKeyDict
+
+
+TYPE_LOCAL = "local"
+TYPE_REMOTE = "remote"
+
+
+
+class SyncMeme(object):
+
+
+ #FIXME: Add checking for crcs to make this stronger
+ def set_local_last_modified(self, modified_datetime):
+ self.local_last_modified = modified_datetime
+
+ def get_local_last_modified(self):
+ return self.local_last_modified
+
+ def set_remote_last_modified(self, modified_datetime):
+ self.remote_last_modified = modified_datetime
+
+ def get_remote_last_modified(self):
+ return self.remote_last_modified
+
+ def which_is_newest(self, local_modified, remote_modified):
+ if local_modified <= self.local_last_modified and \
+ remote_modified <= self.remote_last_modified:
+ return None
+ if local_modified > remote_modified:
+ return "local"
+ else:
+ return "remote"
+
+ def get_origin(self):
+ '''
+ Returns the name of the source that firstly presented the object
+ '''
+ return self.origin
+
+ def set_origin(self, origin):
+ self.origin = origin
+
+
+
+class SyncMemes(TwoKeyDict):
+
+
+ get_remote_id = TwoKeyDict._get_secondary_key
+ get_local_id = TwoKeyDict._get_primary_key
+ remove_local_id = TwoKeyDict._remove_by_primary
+ remove_remote_id = TwoKeyDict._remove_by_secondary
+ get_meme_from_local_id = TwoKeyDict._get_by_primary
+ get_meme_from_remote_id = TwoKeyDict._get_by_secondary
+
+
+
+class SyncEngine(object):
+
+
+ UPDATE = "update"
+ REMOVE = "remove"
+ ADD = "add"
+ LOST_SYNCABILITY = "lost syncability"
+
+ def __init__(self):
+ self.sync_memes = SyncMemes()
+
+ def _analyze_element(self, element_id, is_local, \
+ has_local, has_remote, is_syncable = True):
+ if is_local:
+ get_other_id = self.sync_memes.get_remote_id
+ is_task_present = has_remote
+ else:
+ get_other_id = self.sync_memes.get_local_id
+ is_task_present = has_local
+
+ try:
+ other_id = get_other_id(element_id)
+ if is_task_present(other_id):
+ if is_syncable:
+ return self.UPDATE, other_id
+ else:
+ return self.LOST_SYNCABILITY, other_id
+ else:
+ return self.REMOVE, None
+ except KeyError:
+ if is_syncable:
+ return self.ADD, None
+ return None, None
+
+ def analyze_local_id(self, element_id, *other_args):
+ return self._analyze_element(element_id, True, *other_args)
+
+ def analyze_remote_id(self, element_id, *other_args):
+ return self._analyze_element(element_id, False, *other_args)
+
+ def record_relationship(self, local_id, remote_id, meme):
+ triplet = (local_id, remote_id, meme)
+ self.sync_memes.add(triplet)
+
+ def break_relationship(self, local_id = None, remote_id = None):
+ if local_id:
+ self.sync_memes.remove_local_id(local_id)
+ elif remote_id:
+ self.sync_memes.remove_remote_id(remote_id)
+
+ def __getattr__(self, attr):
+ if attr in ['get_remote_id', \
+ 'get_local_id',
+ 'get_meme_from_local_id',
+ 'get_meme_from_remote_id']:
+ return getattr(self.sync_memes, attr)
+ else:
+ raise AttributeError
+
=== modified file 'GTG/gtk/delete_dialog.py'
--- GTG/gtk/delete_dialog.py 2010-06-23 01:17:32 +0000
+++ GTG/gtk/delete_dialog.py 2010-06-23 01:17:32 +0000
@@ -19,17 +19,12 @@
# -----------------------------------------------------------------------------
import gtk
-from GTG import _, ngettext
+from GTG import _
from GTG.gtk import ViewConfig
-
class DeletionUI():
-
-
- MAXIMUM_TIDS_TO_SHOW = 20
-
- def __init__(self, req):
+ def __init__(self,req):
self.req = req
self.tids_todelete = []
# Load window tree
@@ -62,28 +57,19 @@
cdlabel2 = self.builder.get_object("cd-label2")
cdlabel3 = self.builder.get_object("cd-label3")
cdlabel4 = self.builder.get_object("cd-label4")
- singular = len(self.tids_todelete)
- label_text = ngettext("Deleting a task cannot be undone, "
- "and will delete the following tasks: ",
- "Deleting a task cannot be undone, "
- "and will delete the following task: ",
- singular)
- cdlabel2.set_label(ngettext("Are you sure you want to delete these "
- "tasks?",
- "Are you sure you want to delete this "
- "task?",
- singular))
-
- cdlabel3.set_label(ngettext("Keep selected tasks",
- "Keep selected task",
- singular))
- cdlabel4.set_label(ngettext("Permanently remove tasks",
- "Permanently remove task",
- singular))
+ if len(self.tids_todelete) == 1:
+ label_text = _("Deleting a task cannot be undone, and will delete the following task: ")
+ cdlabel2.set_label(_("Are you sure you want to delete this task?"))
+ cdlabel3.set_label(_("Keep selected task"))
+ cdlabel4.set_label(_("Permanently remove task"))
+ else:
+ label_text = _("Deleting a task cannot be undone, and will delete the following tasks: ")
+ cdlabel2.set_label(_("Are you sure you want to delete these tasks?"))
+ cdlabel3.set_label(_("Keep selected tasks"))
+ cdlabel4.set_label(_("Permanently remove tasks"))
label_text = label_text[0:label_text.find(":") + 1]
- # I find the tasks that are going to be deleted, avoiding to fetch
- # too many titles (no more that the maximum capacity)
+ # I find the tasks that are going to be deleted
tasks = []
for tid in self.tids_todelete:
def recursive_list_tasks(task_list, root):
@@ -91,24 +77,12 @@
their children, recursively"""
if root not in task_list:
task_list.append(root)
- if len(task_list) >= self.MAXIMUM_TIDS_TO_SHOW:
- return
for i in root.get_subtasks():
recursive_list_tasks(task_list, i)
task = self.req.get_task(tid)
recursive_list_tasks(tasks, task)
- len_tasks = len(tasks)
- #we don't want to end with just one task that doesn't fit the
- # screen and a line saying "And one more task", so we go a
- # little over our limit
- if len_tasks > self.MAXIMUM_TIDS_TO_SHOW + 2:
- tasks = tasks[: self.MAXIMUM_TIDS_TO_SHOW + 2]
- break
titles_list = [task.get_title() for task in tasks]
titles = reduce (lambda x, y: x + "\n - " + y, titles_list)
- missing_titles_count = len(self.tids_todelete) - len_tasks
- if missing_titles_count > 2:
- titles += _("\nAnd %d more tasks" % missing_titles_count)
label.set_text("%s %s" % (label_text, "\n - " + titles))
delete_dialog = self.builder.get_object("confirm_delete")
delete_dialog.resize(1, 1)
=== added file 'GTG/tests/test_backend_tomboy.py'
--- GTG/tests/test_backend_tomboy.py 1970-01-01 00:00:00 +0000
+++ GTG/tests/test_backend_tomboy.py 2010-06-23 01:17:32 +0000
@@ -0,0 +1,401 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+'''
+Tests for the tomboy backend
+'''
+
+import os
+import sys
+import errno
+import unittest
+import uuid
+import signal
+import time
+import math
+import dbus
+import gobject
+import random
+import threading
+import dbus.glib
+import dbus.service
+import tempfile
+import datetime
+from datetime import datetime
+from dbus.mainloop.glib import DBusGMainLoop
+
+from GTG.core.datastore import DataStore
+from GTG.backends import BackendFactory
+from GTG.backends.genericbackend import GenericBackend
+from GTG.core import CoreConfig
+
+
+
+class TestBackendTomboy(unittest.TestCase):
+ '''
+ Tests for the tomboy backend.
+ '''
+
+
+ def setUp(self):
+ self.spawn_fake_tomboy_server()
+ #only the test process should go further, the dbus server one should
+ #stop here
+ if not self.child_process_pid: return
+ #we create a custom dictionary listening to the server, and register it
+ # in GTG.
+ additional_dic = {}
+ additional_dic["use this fake connection instead"] = \
+ (FakeTomboy.BUS_NAME,
+ FakeTomboy.BUS_PATH,
+ FakeTomboy.BUS_INTERFACE)
+ additional_dic[GenericBackend.KEY_ATTACHED_TAGS] = \
+ [GenericBackend.ALLTASKS_TAG]
+ additional_dic[GenericBackend.KEY_DEFAULT_BACKEND] = True
+ dic = BackendFactory().get_new_backend_dict('backend_tomboy',
+ additional_dic)
+ self.datastore = DataStore()
+ self.backend = self.datastore.register_backend(dic)
+ #waiting for the "start_get_tasks" to settle
+ time.sleep(1)
+ #we create a dbus session to speak with the server
+ self.bus = dbus.SessionBus()
+ obj = self.bus.get_object(FakeTomboy.BUS_NAME, FakeTomboy.BUS_PATH)
+ self.tomboy = dbus.Interface(obj, FakeTomboy.BUS_INTERFACE)
+
+ def spawn_fake_tomboy_server(self):
+ #the fake tomboy server has to be in a different process,
+ #otherwise it will lock on the GIL.
+ #For details, see
+ #http://lists.freedesktop.org/archives/dbus/2007-January/006921.html
+
+ #we use a lockfile to make sure the server is running before we start
+ # the test
+ lockfile_fd, lockfile_path = tempfile.mkstemp()
+ self.child_process_pid = os.fork()
+ if self.child_process_pid:
+ #we wait in polling that the server has been started
+ while True:
+ try:
+ fd = os.open(lockfile_path, os.O_CREAT | os.O_EXCL | os.O_RDWR)
+ except OSError, e:
+ if e.errno != errno.EEXIST:
+ raise
+ time.sleep(0.3)
+ continue
+ os.close(fd)
+ break
+ else:
+ FakeTomboy()
+ os.close(lockfile_fd)
+ os.unlink(lockfile_path)
+
+ def tearDown(self):
+ if not self.child_process_pid: return
+ self.datastore.save(quit = True)
+ time.sleep(0.5)
+ self.tomboy.FakeQuit()
+ self.bus.close()
+ os.kill(self.child_process_pid, signal.SIGKILL)
+ os.waitpid(self.child_process_pid, 0)
+
+ def test_everything(self):
+ '''
+ '''
+ #we cannot use separate test functions because we only want a single
+ # FakeTomboy dbus server running
+ if not self.child_process_pid: return
+ for function in dir(self):
+ if function.startswith("TEST_"):
+ getattr(self, function)()
+ self.tomboy.Reset()
+ for tid in self.datastore.get_all_tasks():
+ self.datastore.request_task_deletion(tid)
+ time.sleep(0.1)
+
+ def TEST_processing_tomboy_notes(self):
+ self.backend.set_attached_tags([GenericBackend.ALLTASKS_TAG])
+ #adding a note
+ note = self.tomboy.CreateNamedNote(str(uuid.uuid4()))
+ self.backend._process_tomboy_note(note)
+ self.assertEqual(len(self.datastore.get_all_tasks()), 1)
+ tid = self.backend.sync_engine.sync_memes.get_local_id(note)
+ task = self.datastore.get_task(tid)
+ #re-adding that (should not change anything)
+ self.backend._process_tomboy_note(note)
+ self.assertEqual(len(self.datastore.get_all_tasks()), 1)
+ self.assertEqual( \
+ self.backend.sync_engine.sync_memes.get_local_id(note), tid)
+ #removing the note and updating gtg
+ self.tomboy.DeleteNote(note)
+ self.backend.set_task(task)
+ self.assertEqual(len(self.datastore.get_all_tasks()), 0)
+
+ def TEST_set_task(self):
+ self.backend.set_attached_tags([GenericBackend.ALLTASKS_TAG])
+ #adding a task
+ task = self.datastore.requester.new_task()
+ task.set_title("title")
+ self.backend.set_task(task)
+ self.assertEqual(len(self.tomboy.ListAllNotes()), 1)
+ note = self.tomboy.ListAllNotes()[0]
+ self.assertEqual(str(self.tomboy.GetNoteTitle(note)), task.get_title())
+ #re-adding that (should not change anything)
+ self.backend.set_task(task)
+ self.assertEqual(len(self.tomboy.ListAllNotes()), 1)
+ self.assertEqual(note, self.tomboy.ListAllNotes()[0])
+ #removing the task and updating tomboy
+ self.datastore.request_task_deletion(task.get_id())
+ self.backend._process_tomboy_note(note)
+ self.assertEqual(len(self.tomboy.ListAllNotes()), 0)
+
+ def TEST_update_newest(self):
+ self.backend.set_attached_tags([GenericBackend.ALLTASKS_TAG])
+ task = self.datastore.requester.new_task()
+ task.set_title("title")
+ self.backend.set_task(task)
+ note = self.tomboy.ListAllNotes()[0]
+ gtg_modified = task.get_modified()
+ tomboy_modified = self._modified_string_to_datetime( \
+ self.tomboy.GetNoteChangeDate(note))
+ #no-one updated, nothing should happen
+ self.backend.set_task(task)
+ self.assertEqual(gtg_modified, task.get_modified())
+ self.assertEqual(tomboy_modified, \
+ self._modified_string_to_datetime( \
+ self.tomboy.GetNoteChangeDate(note)))
+ #we update the GTG task
+ UPDATED_GTG_TITLE = "UPDATED_GTG_TITLE"
+ task.set_title(UPDATED_GTG_TITLE)
+ self.backend.set_task(task)
+ self.assertTrue(gtg_modified < task.get_modified())
+ self.assertTrue(tomboy_modified <=
+ self._modified_string_to_datetime( \
+ self.tomboy.GetNoteChangeDate(note)))
+ self.assertEqual(task.get_title(), UPDATED_GTG_TITLE)
+ self.assertEqual(self.tomboy.GetNoteTitle(note), UPDATED_GTG_TITLE)
+ gtg_modified = task.get_modified()
+ tomboy_modified = self._modified_string_to_datetime( \
+ self.tomboy.GetNoteChangeDate(note))
+ #we update the TOMBOY task
+ UPDATED_TOMBOY_TITLE = "UPDATED_TOMBOY_TITLE"
+ #the resolution of tomboy notes changed time is 1 second, so we need
+ # to wait. This *shouldn't* be needed in the actual code because
+ # tomboy signals are always a few seconds late.
+ time.sleep(1)
+ self.tomboy.SetNoteContents(note, UPDATED_TOMBOY_TITLE)
+ self.backend._process_tomboy_note(note)
+ self.assertTrue(gtg_modified <= task.get_modified())
+ self.assertTrue(tomboy_modified <=
+ self._modified_string_to_datetime( \
+ self.tomboy.GetNoteChangeDate(note)))
+ self.assertEqual(task.get_title(), UPDATED_TOMBOY_TITLE)
+ self.assertEqual(self.tomboy.GetNoteTitle(note), UPDATED_TOMBOY_TITLE)
+
+ def TEST_processing_tomboy_notes_with_tags(self):
+ self.backend.set_attached_tags(['@a'])
+ #adding a not syncable note
+ note = self.tomboy.CreateNamedNote("title" + str(uuid.uuid4()))
+ self.backend._process_tomboy_note(note)
+ self.assertEqual(len(self.datastore.get_all_tasks()), 0)
+ #re-adding that (should not change anything)
+ self.backend._process_tomboy_note(note)
+ self.assertEqual(len(self.datastore.get_all_tasks()), 0)
+ #adding a tag to that note
+ self.tomboy.SetNoteContents(note, "something with @a")
+ self.backend._process_tomboy_note(note)
+ self.assertEqual(len(self.datastore.get_all_tasks()), 1)
+ #removing the tag and resyncing
+ self.tomboy.SetNoteContents(note, "something with no tags")
+ self.backend._process_tomboy_note(note)
+ self.assertEqual(len(self.datastore.get_all_tasks()), 0)
+ #adding a syncable note
+ note = self.tomboy.CreateNamedNote("title @a" + str(uuid.uuid4()))
+ self.backend._process_tomboy_note(note)
+ self.assertEqual(len(self.datastore.get_all_tasks()), 1)
+ tid = self.backend.sync_engine.sync_memes.get_local_id(note)
+ task = self.datastore.get_task(tid)
+ #re-adding that (should not change anything)
+ self.backend._process_tomboy_note(note)
+ self.assertEqual(len(self.datastore.get_all_tasks()), 1)
+ self.assertEqual( \
+ self.backend.sync_engine.sync_memes.get_local_id(note), tid)
+ #removing the note and updating gtg
+ self.tomboy.DeleteNote(note)
+ self.backend.set_task(task)
+ self.assertEqual(len(self.datastore.get_all_tasks()), 0)
+
+ def TEST_set_task_with_tags(self):
+ self.backend.set_attached_tags(['@a'])
+ #adding a not syncable task
+ task = self.datastore.requester.new_task()
+ task.set_title("title")
+ self.backend.set_task(task)
+ self.assertEqual(len(self.tomboy.ListAllNotes()), 0)
+ #making that task syncable
+ task.set_title("something else")
+ task.add_tag("@a")
+ self.backend.set_task(task)
+ self.assertEqual(len(self.tomboy.ListAllNotes()), 1)
+ note = self.tomboy.ListAllNotes()[0]
+ self.assertEqual(str(self.tomboy.GetNoteTitle(note)), task.get_title())
+ #re-adding that (should not change anything)
+ self.backend.set_task(task)
+ self.assertEqual(len(self.tomboy.ListAllNotes()), 1)
+ self.assertEqual(note, self.tomboy.ListAllNotes()[0])
+ #removing the syncable property and updating tomboy
+ task.remove_tag("@a")
+ self.backend.set_task(task)
+ self.assertEqual(len(self.tomboy.ListAllNotes()), 0)
+
+ def TEST_multiple_task_same_title(self):
+ self.backend.set_attached_tags(['@a'])
+ how_many_tasks = int(math.ceil(20 * random.random()))
+ for iteration in xrange(0, how_many_tasks):
+ task = self.datastore.requester.new_task()
+ task.set_title("title")
+ task.add_tag('@a')
+ self.backend.set_task(task)
+ self.assertEqual(len(self.tomboy.ListAllNotes()), how_many_tasks)
+
+ def _modified_string_to_datetime(self, modified_string):
+ return datetime.fromtimestamp(modified_string)
+
+
+def test_suite():
+ CoreConfig().set_data_dir("./test_data")
+ CoreConfig().set_conf_dir("./test_data")
+ return unittest.TestLoader().loadTestsFromTestCase(TestBackendTomboy)
+
+
+
+
+class FakeTomboy(dbus.service.Object):
+ """
+ D-Bus service object that mimics TOMBOY
+ """
+
+
+ #We don't directly use the tomboy dbus path to avoid conflicts
+ # if tomboy is running during the test
+
+ BUS_NAME = "Fake.Tomboy"
+ BUS_PATH = "/Fake/Tomboy"
+ BUS_INTERFACE = "Fake.Tomboy.RemoteControl"
+
+ def __init__(self):
+ # Attach the object to D-Bus
+ DBusGMainLoop(set_as_default=True)
+ self.bus = dbus.SessionBus()
+ bus_name = dbus.service.BusName(self.BUS_NAME, bus = self.bus)
+ dbus.service.Object.__init__(self, bus_name, self.BUS_PATH)
+ self.notes = {}
+ threading.Thread(target = self.fake_main_loop).start()
+
+ @dbus.service.method(BUS_INTERFACE, in_signature="s", out_signature="s")
+ def GetNoteContents(self, note):
+ return self.notes[note]['content']
+
+ @dbus.service.method(BUS_INTERFACE, in_signature="s", out_signature="b")
+ def NoteExists(self, note):
+ return self.notes.has_key(note)
+
+ @dbus.service.method(BUS_INTERFACE, in_signature="s", out_signature="d")
+ def GetNoteChangeDate(self, note):
+ return self.notes[note]['changed']
+
+ @dbus.service.method(BUS_INTERFACE, in_signature="ss")
+ def SetNoteContents(self, note, text):
+ self.fake_update_note(note)
+ self.notes[note]['content'] = text
+
+ @dbus.service.method(BUS_INTERFACE, in_signature="s", out_signature="s")
+ def GetNoteTitle(self, note):
+ return self._GetNoteTitle(note)
+
+ def _GetNoteTitle(self, note):
+ content = self.notes[note]['content']
+ try:
+ end_of_title = content.index('\n')
+ except ValueError:
+ return content
+ return content[:end_of_title]
+
+ @dbus.service.method(BUS_INTERFACE, in_signature="s")
+ def DeleteNote(self, note):
+ del self.notes[note]
+
+ @dbus.service.method(BUS_INTERFACE, in_signature="s", out_signature="s")
+ def CreateNamedNote(self, title):
+ #this is to mimic the way tomboy handles title clashes
+ if self._FindNote(title) != '':
+ return ''
+ note = str(uuid.uuid4())
+ self.notes[note] = {'content': title}
+ self.fake_update_note(note)
+ return note
+
+ @dbus.service.method(BUS_INTERFACE, in_signature="s", out_signature="s")
+ def FindNote(self, title):
+ return self._FindNote(title)
+
+ def _FindNote(self, title):
+ for note in self.notes:
+ if self._GetNoteTitle(note) == title:
+ return note
+ return ''
+
+ @dbus.service.method(BUS_INTERFACE, out_signature = "as")
+ def ListAllNotes(self):
+ return list(self.notes)
+
+ @dbus.service.signal(BUS_INTERFACE, signature='s')
+ def NoteSaved(self, note):
+ pass
+
+ @dbus.service.signal(BUS_INTERFACE, signature='s')
+ def NoteDeleted(self, note):
+ pass
+
+###############################################################################
+### Function with the fake_ prefix are here to assist in testing, they do not
+### need to be present in the real class
+###############################################################################
+
+ def fake_update_note(self, note):
+ self.notes[note]['changed'] = time.mktime(datetime.now().timetuple())
+
+ def fake_main_loop(self):
+ gobject.threads_init()
+ dbus.glib.init_threads()
+ self.main_loop = gobject.MainLoop()
+ self.main_loop.run()
+
+ @dbus.service.method(BUS_INTERFACE)
+ def Reset(self):
+ self.notes = {}
+
+ @dbus.service.method(BUS_INTERFACE)
+ def FakeQuit(self):
+ threading.Timer(0.2, self._fake_quit).start()
+
+ def _fake_quit(self):
+ self.main_loop.quit()
+ sys.exit(0)
+
=== added file 'GTG/tests/test_bidict.py'
--- GTG/tests/test_bidict.py 1970-01-01 00:00:00 +0000
+++ GTG/tests/test_bidict.py 2010-06-23 01:17:32 +0000
@@ -0,0 +1,81 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+'''
+Tests for the datastore
+'''
+
+import unittest
+import uuid
+
+from GTG.tools.bidict import BiDict
+from GTG.core import CoreConfig
+
+
+
+class TestBiDict(unittest.TestCase):
+ '''
+ Tests for the BiDict object.
+ '''
+
+
+ def test_add_and_gets(self):
+ '''
+ Test for the __init__, _get_by_first, _get_by_second function
+ '''
+ pairs = [(uuid.uuid4(), uuid.uuid4()) for a in xrange(10)]
+ bidict = BiDict(*pairs)
+ for pair in pairs:
+ self.assertEqual(bidict._get_by_first(pair[0]), pair[1])
+ self.assertEqual(bidict._get_by_second(pair[1]), pair[0])
+
+ def test_remove_by_first_or_second(self):
+ pair_first = (1, 'one')
+ pair_second = (2, 'two')
+ bidict = BiDict(pair_first, pair_second)
+ bidict._remove_by_first(pair_first[0])
+ bidict._remove_by_second(pair_second[1])
+ missing_first = 0
+ missing_second = 0
+ try:
+ bidict._get_by_first(pair_first[0])
+ except KeyError:
+ missing_first += 1
+ try:
+ bidict._get_by_first(pair_second[0])
+ except KeyError:
+ missing_first += 1
+ try:
+ bidict._get_by_second(pair_first[1])
+ except KeyError:
+ missing_second += 1
+ try:
+ bidict._get_by_second(pair_second[1])
+ except KeyError:
+ missing_second += 1
+ self.assertEqual(missing_first, 2)
+ self.assertEqual(missing_second, 2)
+
+def test_suite():
+ CoreConfig().set_data_dir("./test_data")
+ CoreConfig().set_conf_dir("./test_data")
+ return unittest.TestLoader().loadTestsFromTestCase(TestBiDict)
+
+
+
=== added file 'GTG/tests/test_syncengine.py'
--- GTG/tests/test_syncengine.py 1970-01-01 00:00:00 +0000
+++ GTG/tests/test_syncengine.py 2010-06-23 01:17:32 +0000
@@ -0,0 +1,192 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+'''
+Tests for the SyncEngine class
+'''
+
+import unittest
+import uuid
+
+from GTG.backends.syncengine import SyncEngine
+from GTG.core import CoreConfig
+
+
+
+class TestSyncEngine(unittest.TestCase):
+ '''
+ Tests for the SyncEngine object.
+ '''
+
+ def setUp(self):
+ self.ftp_local = FakeTaskProvider()
+ self.ftp_remote = FakeTaskProvider()
+ self.sync_engine = SyncEngine()
+
+ def test_analyze_element_and_record_and_break_relationship(self):
+ '''
+ Test for the _analyze_element, analyze_remote_id, analyze_local_id,
+ record_relationship, break_relationship
+ '''
+ #adding a new local task
+ local_id = uuid.uuid4()
+ self.ftp_local.fake_add_task(local_id)
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task), \
+ (SyncEngine.ADD, None))
+ #creating the related remote task
+ remote_id = uuid.uuid4()
+ self.ftp_remote.fake_add_task(remote_id)
+ #informing the sync_engine about that
+ self.sync_engine.record_relationship(local_id, remote_id, object())
+ #verifying that it understood that
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task), \
+ (SyncEngine.UPDATE, remote_id))
+ self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task), \
+ (SyncEngine.UPDATE, local_id))
+ #and not the reverse
+ self.assertEqual(self.sync_engine.analyze_remote_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task), \
+ (SyncEngine.ADD, None))
+ self.assertEqual(self.sync_engine.analyze_local_id(remote_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task), \
+ (SyncEngine.ADD, None))
+ #now we remove the remote task
+ self.ftp_remote.fake_remove_task(remote_id)
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task), \
+ (SyncEngine.REMOVE, None))
+ self.sync_engine.break_relationship(local_id = local_id)
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task), \
+ (SyncEngine.ADD, None))
+ self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task), \
+ (SyncEngine.ADD, None))
+ #we add them back and remove giving the remote id as key to find what to
+ #delete
+ self.ftp_local.fake_add_task(local_id)
+ self.ftp_remote.fake_add_task(remote_id)
+ self.ftp_remote.fake_remove_task(remote_id)
+ self.sync_engine.record_relationship(local_id, remote_id, object)
+ self.sync_engine.break_relationship(remote_id = remote_id)
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task), \
+ (SyncEngine.ADD, None))
+ self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task), \
+ (SyncEngine.ADD, None))
+
+ def test_syncability(self):
+ '''
+ Test for the _analyze_element, analyze_remote_id, analyze_local_id.
+ Checks that the is_syncable parameter is used correctly
+ '''
+ #adding a new local task unsyncable
+ local_id = uuid.uuid4()
+ self.ftp_local.fake_add_task(local_id)
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task,
+ False), \
+ (None, None))
+ #adding a new local task, syncable
+ local_id = uuid.uuid4()
+ self.ftp_local.fake_add_task(local_id)
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task), \
+ (SyncEngine.ADD, None))
+ #creating the related remote task
+ remote_id = uuid.uuid4()
+ self.ftp_remote.fake_add_task(remote_id)
+ #informing the sync_engine about that
+ self.sync_engine.record_relationship(local_id, remote_id, object())
+ #checking that it behaves correctly with established relationships
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task,
+ True), \
+ (SyncEngine.UPDATE, remote_id))
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task,
+ False), \
+ (SyncEngine.LOST_SYNCABILITY, remote_id))
+ self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task,
+ True), \
+ (SyncEngine.UPDATE, local_id))
+ self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task,
+ False), \
+ (SyncEngine.LOST_SYNCABILITY, local_id))
+ #now we remove the remote task
+ self.ftp_remote.fake_remove_task(remote_id)
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task,
+ True), \
+ (SyncEngine.REMOVE, None))
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task,
+ False), \
+ (SyncEngine.REMOVE, None))
+ self.sync_engine.break_relationship(local_id = local_id)
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task,
+ True), \
+ (SyncEngine.ADD, None))
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task,
+ False), \
+ (None, None))
+ self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task,
+ True), \
+ (SyncEngine.ADD, None))
+ self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task,
+ False), \
+ (None, None))
+
+def test_suite():
+ CoreConfig().set_data_dir("./test_data")
+ CoreConfig().set_conf_dir("./test_data")
+ return unittest.TestLoader().loadTestsFromTestCase(TestSyncEngine)
+
+
+class FakeTaskProvider(object):
+
+ def __init__(self):
+ self.dic = {}
+
+ def has_task(self, tid):
+ return self.dic.has_key(tid)
+
+###############################################################################
+### Function with the fake_ prefix are here to assist in testing, they do not
+### need to be present in the real class
+###############################################################################
+
+ def fake_add_task(self, tid):
+ self.dic[tid] = "something"
+
+ def fake_get_task(self, tid):
+ return self.dic[tid]
+
+ def fake_remove_task(self, tid):
+ del self.dic[tid]
=== added file 'GTG/tests/test_syncmeme.py'
--- GTG/tests/test_syncmeme.py 1970-01-01 00:00:00 +0000
+++ GTG/tests/test_syncmeme.py 2010-06-23 01:17:32 +0000
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+'''
+Tests for the SyncMeme class
+'''
+
+import unittest
+import datetime
+
+from GTG.backends.syncengine import SyncMeme
+from GTG.core import CoreConfig
+
+
+
+class TestSyncMeme(unittest.TestCase):
+ '''
+ Tests for the SyncEngine object.
+ '''
+
+ def test_which_is_newest(self):
+ '''
+ test the which_is_newest function
+
+ '''
+ meme = SyncMeme()
+ #tasks have not changed
+ local_modified = datetime.datetime.now()
+ remote_modified = datetime.datetime.now()
+ meme.set_local_last_modified(local_modified)
+ meme.set_remote_last_modified(remote_modified)
+ self.assertEqual(meme.which_is_newest(local_modified, \
+ remote_modified), None)
+ #we update the local
+ local_modified = datetime.datetime.now()
+ self.assertEqual(meme.which_is_newest(local_modified, \
+ remote_modified), 'local')
+ #we update the remote
+ remote_modified = datetime.datetime.now()
+ self.assertEqual(meme.which_is_newest(local_modified, \
+ remote_modified), 'remote')
+def test_suite():
+ CoreConfig().set_data_dir("./test_data")
+ CoreConfig().set_conf_dir("./test_data")
+ return unittest.TestLoader().loadTestsFromTestCase(TestSyncMeme)
+
=== added file 'GTG/tests/test_twokeydict.py'
--- GTG/tests/test_twokeydict.py 1970-01-01 00:00:00 +0000
+++ GTG/tests/test_twokeydict.py 2010-06-23 01:17:32 +0000
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+'''
+Tests for the TwoKeyDict class
+'''
+
+import unittest
+import uuid
+
+from GTG.tools.twokeydict import TwoKeyDict
+from GTG.core import CoreConfig
+
+
+
+class TestTwoKeyDict(unittest.TestCase):
+ '''
+ Tests for the TwoKeyDict object.
+ '''
+
+
+ def test_add_and_gets(self):
+ '''
+ Test for the __init__, _get_by_first, _get_by_second function
+ '''
+ triplets = [(uuid.uuid4(), uuid.uuid4(), uuid.uuid4()) \
+ for a in xrange(10)]
+ tw_dict = TwoKeyDict(*triplets)
+ for triplet in triplets:
+ self.assertEqual(tw_dict._get_by_primary(triplet[0]), triplet[2])
+ self.assertEqual(tw_dict._get_by_secondary(triplet[1]), triplet[2])
+
+ def test_remove_by_first_or_second(self):
+ triplet_first = (1, 'I', 'one')
+ triplet_second = (2, 'II', 'two')
+ tw_dict = TwoKeyDict(triplet_first, triplet_second)
+ tw_dict._remove_by_primary(triplet_first[0])
+ tw_dict._remove_by_secondary(triplet_second[1])
+ missing_first = 0
+ missing_second = 0
+ try:
+ tw_dict._get_by_primary(triplet_first[0])
+ except KeyError:
+ missing_first += 1
+ try:
+ tw_dict._get_by_secondary(triplet_second[0])
+ except KeyError:
+ missing_first += 1
+ try:
+ tw_dict._get_by_secondary(triplet_first[1])
+ except KeyError:
+ missing_second += 1
+ try:
+ tw_dict._get_by_secondary(triplet_second[1])
+ except KeyError:
+ missing_second += 1
+ self.assertEqual(missing_first, 2)
+ self.assertEqual(missing_second, 2)
+ #check for memory leaks
+ dict_len = 0
+ for key in tw_dict._primary_to_value.iterkeys():
+ dict_len += 1
+ self.assertEqual(dict_len, 0)
+
+ def test_get_primary_and_secondary_key(self):
+ triplets = [(uuid.uuid4(), uuid.uuid4(), uuid.uuid4()) \
+ for a in xrange(10)]
+ tw_dict = TwoKeyDict(*triplets)
+ for triplet in triplets:
+ self.assertEqual(tw_dict._get_secondary_key(triplet[0]), \
+ triplet[1])
+ self.assertEqual(tw_dict._get_primary_key(triplet[1]), \
+ triplet[0])
+
+def test_suite():
+ CoreConfig().set_data_dir("./test_data")
+ CoreConfig().set_conf_dir("./test_data")
+ return unittest.TestLoader().loadTestsFromTestCase(TestTwoKeyDict)
+
=== added file 'GTG/tools/bidict.py'
--- GTG/tools/bidict.py 1970-01-01 00:00:00 +0000
+++ GTG/tools/bidict.py 2010-06-23 01:17:32 +0000
@@ -0,0 +1,53 @@
+
+
+
+class BiDict(object):
+
+
+ '''
+ Bidirectional dictionary: the pairs stored can be accessed using either the
+ first or the second element as key.
+ If no special behaviour is coded, it defaults to a normal dict indexed on
+ the first value.
+ You don't need this if there is no clash between the domains of the first
+ and second element of the pairs.
+ '''
+
+ def __init__(self, *pairs):
+ super(BiDict, self).__init__()
+ self._first_to_second = {}
+ self._second_to_first = {}
+ for pair in pairs:
+ self.add(pair)
+
+ def add(self, pair):
+ self._first_to_second[pair[0]] = pair[1]
+ self._second_to_first[pair[1]] = pair[0]
+
+ def _get_by_first(self, key):
+ return self._first_to_second[key]
+
+ def _get_by_second(self, key):
+ return self._second_to_first[key]
+
+ def _remove_by_first(self, first):
+ second = self._first_to_second[first]
+ del self._second_to_first[second]
+ del self._first_to_second[first]
+
+ def _remove_by_second(self, second):
+ first = self._second_to_first[second]
+ del self._first_to_second[first]
+ del self._second_to_first[second]
+
+ def __str__(self):
+ return reduce(lambda text, keys: \
+ str(text) + str(keys),
+ self._first_to_second.iteritems())
+
+ def __getattr__(self, attr):
+ if attr in self.__dict__:
+ return self.__dict__[attr]
+ else:
+ return getattr(self._first_to_second, attr)
+
=== added file 'GTG/tools/networkmanager.py'
--- GTG/tools/networkmanager.py 1970-01-01 00:00:00 +0000
+++ GTG/tools/networkmanager.py 2010-06-23 01:17:32 +0000
@@ -0,0 +1,73 @@
+#!/bin/env python
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# Copyright (C) 2010 Red Hat, Inc.
+#
+
+import dbus
+
+
+def is_connection_up():
+ state = False
+ bus = dbus.SystemBus()
+
+ proxy = bus.get_object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
+ manager = dbus.Interface(proxy, "org.freedesktop.NetworkManager")
+
+# Get device-specific state
+# devices = manager.GetDevices()
+# for d in devices:
+# dev_proxy = bus.get_object("org.freedesktop.NetworkManager", d)
+# prop_iface = dbus.Interface(dev_proxy, "org.freedesktop.DBus.Properties")
+#
+# # Get the device's current state and interface name
+# state = prop_iface.Get("org.freedesktop.NetworkManager.Device", "State")
+# name = prop_iface.Get("org.freedesktop.NetworkManager.Device", "Interface")
+#
+# # and print them out
+# if state == 8: # activated
+# print "Device %s is activated" % name
+# else:
+# print "Device %s is not activated" % name
+
+
+# Get active connection state
+ manager_prop_iface = dbus.Interface(proxy, "org.freedesktop.DBus.Properties")
+ active = manager_prop_iface.Get("org.freedesktop.NetworkManager", "ActiveConnections")
+ for a in active:
+ ac_proxy = bus.get_object("org.freedesktop.NetworkManager", a)
+ prop_iface = dbus.Interface(ac_proxy, "org.freedesktop.DBus.Properties")
+ state = prop_iface.Get("org.freedesktop.NetworkManager.ActiveConnection", "State")
+
+ # Connections in NM are a collection of settings that describe everything
+ # needed to connect to a specific network. Lets get those details so we
+ # can find the user-readable name of the connection.
+ con_path = prop_iface.Get("org.freedesktop.NetworkManager.ActiveConnection", "Connection")
+ con_service = prop_iface.Get("org.freedesktop.NetworkManager.ActiveConnection", "ServiceName")
+
+ # ask the provider of the connection for its details
+ service_proxy = bus.get_object(con_service, con_path)
+ con_iface = dbus.Interface(service_proxy, "org.freedesktop.NetworkManagerSettings.Connection")
+ con_details = con_iface.GetSettings()
+ con_name = con_details['connection']['id']
+
+ if state == 2: # activated
+ #print "Connection '%s' is activated" % con_name
+ state = True
+ #else:
+ #print "Connection '%s' is activating" % con_name
+ return state
+
=== added file 'GTG/tools/twokeydict.py'
--- GTG/tools/twokeydict.py 1970-01-01 00:00:00 +0000
+++ GTG/tools/twokeydict.py 2010-06-23 01:17:32 +0000
@@ -0,0 +1,49 @@
+from GTG.tools.bidict import BiDict
+
+
+class TwoKeyDict(object):
+ '''
+ It's a standard Dictionary with a secondary key.
+ For example, you can add an element ('2', 'II', two'), where the
+ first two arguments are keys and the third is the stored object, and access
+ it as:
+ twokey['2'] ==> 'two'
+ twokey['II'] ==> 'two'
+ You can also request the other key, given one.
+ Function calls start with _ because you'll probably want to rename them when
+ you use this dictionary, for the sake of clarity.
+ '''
+
+
+ def __init__(self, *triplets):
+ super(TwoKeyDict, self).__init__()
+ self._key_to_key_bidict = BiDict()
+ self._primary_to_value = {}
+ for triplet in triplets:
+ self.add(triplet)
+
+ def add(self, triplet):
+ self._key_to_key_bidict.add((triplet[0], triplet[1]))
+ self._primary_to_value[triplet[0]] = triplet[2]
+
+ def _get_by_primary(self, primary):
+ return self._primary_to_value[primary]
+
+ def _get_by_secondary(self, secondary):
+ primary = self._key_to_key_bidict._get_by_second(secondary)
+ return self._get_by_primary(primary)
+
+ def _remove_by_primary(self, primary):
+ del self._primary_to_value[primary]
+ self._key_to_key_bidict._remove_by_first(primary)
+
+ def _remove_by_secondary(self, secondary):
+ primary = self._key_to_key_bidict._get_by_second(secondary)
+ self._remove_by_primary(primary)
+
+ def _get_secondary_key(self, primary):
+ return self._key_to_key_bidict._get_by_first(primary)
+
+ def _get_primary_key(self, secondary):
+ return self._key_to_key_bidict._get_by_second(secondary)
+
=== added file 'GTG/tools/watchdog.py'
--- GTG/tools/watchdog.py 1970-01-01 00:00:00 +0000
+++ GTG/tools/watchdog.py 2010-06-23 01:17:32 +0000
@@ -0,0 +1,26 @@
+import threading
+
+class Watchdog(object):
+ '''
+ a simple thread-safe watchdog.
+ usage:
+ with Watchdod(timeout, error_function):
+ #do something
+ '''
+
+ def __init__(self, timeout, error_function):
+ self.timeout = timeout
+ self.error_function = error_function
+
+ def __enter__(self):
+ self.timer = threading.Timer(self.timeout, self.error_function)
+ self.timer.start()
+
+ def __exit__(self, type, value, traceback):
+ try:
+ self.timer.cancel()
+ except:
+ pass
+ if value == None:
+ return True
+ return False
=== added file 'data/icons/hicolor/scalable/apps/backend_tomboy.png'
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-06-23 01:17:32 +0000 differ
=== added file 'data/icons/hicolor/svg/tomboy.svg'
--- data/icons/hicolor/svg/tomboy.svg 1970-01-01 00:00:00 +0000
+++ data/icons/hicolor/svg/tomboy.svg 2010-06-23 01:17:32 +0000
@@ -0,0 +1,463 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ inkscape:export-ydpi="600"
+ inkscape:export-xdpi="600"
+ inkscape:export-filename="backend_tomboy.png"
+ width="48px"
+ height="48px"
+ id="svg11300"
+ sodipodi:version="0.32"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="tomboy.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ sodipodi:modified="true"
+ version="1.1">
+ <defs
+ id="defs3">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 24 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="48 : 24 : 1"
+ inkscape:persp3d-origin="24 : 16 : 1"
+ id="perspective72" />
+ <linearGradient
+ id="linearGradient2994">
+ <stop
+ id="stop2996"
+ offset="0"
+ style="stop-color:#000000;stop-opacity:1;" />
+ <stop
+ id="stop2998"
+ offset="1"
+ style="stop-color:#c9c9c9;stop-opacity:1;" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2994"
+ id="linearGradient2560"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-5.825542,0.125)"
+ x1="25.71875"
+ y1="31.046875"
+ x2="25.514589"
+ y2="30.703125" />
+ <linearGradient
+ id="linearGradient2984"
+ inkscape:collect="always">
+ <stop
+ id="stop2986"
+ offset="0"
+ style="stop-color:#e7e2b8;stop-opacity:1;" />
+ <stop
+ id="stop2988"
+ offset="1"
+ style="stop-color:#e7e2b8;stop-opacity:0;" />
+ </linearGradient>
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2984"
+ id="radialGradient2558"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(2.923565,0,0,2.029717,-61.55532,-27.88417)"
+ cx="29.053354"
+ cy="27.640751"
+ fx="29.053354"
+ fy="27.640751"
+ r="3.2408544" />
+ <linearGradient
+ id="linearGradient2974">
+ <stop
+ id="stop2976"
+ offset="0"
+ style="stop-color:#c1c1c1;stop-opacity:1;" />
+ <stop
+ id="stop2978"
+ offset="1"
+ style="stop-color:#acacac;stop-opacity:1;" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2974"
+ id="linearGradient2556"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-5.669292,0)"
+ x1="46"
+ y1="19.8125"
+ x2="47.6875"
+ y2="22.625" />
+ <linearGradient
+ id="linearGradient2966">
+ <stop
+ id="stop2968"
+ offset="0"
+ style="stop-color:#ffd1d1;stop-opacity:1;" />
+ <stop
+ style="stop-color:#ff1d1d;stop-opacity:1;"
+ offset="0.5"
+ id="stop3006" />
+ <stop
+ id="stop2970"
+ offset="1"
+ style="stop-color:#6f0000;stop-opacity:1;" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2966"
+ id="linearGradient2554"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-5.669292,0)"
+ x1="48.90625"
+ y1="17.376184"
+ x2="50.988335"
+ y2="22.250591" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2865"
+ id="radialGradient2552"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1,0,0,0.348243,0,26.35543)"
+ cx="23.5625"
+ cy="40.4375"
+ fx="23.5625"
+ fy="40.4375"
+ r="19.5625" />
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient6417">
+ <stop
+ style="stop-color:black;stop-opacity:1;"
+ offset="0"
+ id="stop6419" />
+ <stop
+ style="stop-color:black;stop-opacity:0;"
+ offset="1"
+ id="stop6421" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient6407">
+ <stop
+ style="stop-color:white;stop-opacity:1;"
+ offset="0"
+ id="stop6409" />
+ <stop
+ style="stop-color:white;stop-opacity:0;"
+ offset="1"
+ id="stop6411" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient6377">
+ <stop
+ style="stop-color:#fff27e;stop-opacity:1;"
+ offset="0"
+ id="stop6379" />
+ <stop
+ style="stop-color:#edd400;stop-opacity:1;"
+ offset="1"
+ id="stop6381" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient5609">
+ <stop
+ style="stop-color:white;stop-opacity:1;"
+ offset="0"
+ id="stop5611" />
+ <stop
+ style="stop-color:white;stop-opacity:0;"
+ offset="1"
+ id="stop5613" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient2865">
+ <stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop2867" />
+ <stop
+ style="stop-color:#000000;stop-opacity:0;"
+ offset="1"
+ id="stop2869" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5609"
+ id="linearGradient5615"
+ x1="26.213203"
+ y1="14.08672"
+ x2="26.130388"
+ y2="67.031342"
+ gradientUnits="userSpaceOnUse" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6377"
+ id="radialGradient6405"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.669712,0,1.702451e-8,1.220484,-30.23773,-11.79928)"
+ cx="45.150326"
+ cy="35.915409"
+ fx="45.150326"
+ fy="35.915409"
+ r="21.626934" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6407"
+ id="radialGradient6413"
+ cx="43.875"
+ cy="35.90107"
+ fx="43.875"
+ fy="35.90107"
+ r="20.21875"
+ gradientTransform="matrix(10.88255,-6.454846e-8,0,11.39737,-433.5968,-381.3811)"
+ gradientUnits="userSpaceOnUse" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6417"
+ id="radialGradient6423"
+ cx="39.907337"
+ cy="31.780704"
+ fx="39.907337"
+ fy="31.780704"
+ r="5.2591065"
+ gradientTransform="matrix(1,0,0,0.361345,0,22.29694)"
+ gradientUnits="userSpaceOnUse" />
+ </defs>
+ <sodipodi:namedview
+ stroke="#c4a000"
+ fill="#edd400"
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="0.25490196"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1"
+ inkscape:cx="82.479872"
+ inkscape:cy="36.573128"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:grid-bbox="true"
+ inkscape:document-units="px"
+ inkscape:showpageshadow="false"
+ inkscape:window-width="1154"
+ inkscape:window-height="742"
+ inkscape:window-x="124"
+ inkscape:window-y="26"
+ inkscape:window-maximized="0" />
+ <metadata
+ id="metadata4">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:creator>
+ <cc:Agent>
+ <dc:title>Jakub Steiner</dc:title>
+ </cc:Agent>
+ </dc:creator>
+ <dc:source>http://jimmac.musichall.cz</dc:source>
+ <cc:license
+ rdf:resource="http://creativecommons.org/licenses/GPL/2.0/" />
+ <dc:title>Tomboy</dc:title>
+ </cc:Work>
+ <cc:License
+ rdf:about="http://creativecommons.org/licenses/GPL/2.0/">
+ <cc:permits
+ rdf:resource="http://web.resource.org/cc/Reproduction" />
+ <cc:permits
+ rdf:resource="http://web.resource.org/cc/Distribution" />
+ <cc:requires
+ rdf:resource="http://web.resource.org/cc/Notice" />
+ <cc:permits
+ rdf:resource="http://web.resource.org/cc/DerivativeWorks" />
+ <cc:requires
+ rdf:resource="http://web.resource.org/cc/ShareAlike" />
+ <cc:requires
+ rdf:resource="http://web.resource.org/cc/SourceCode" />
+ </cc:License>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="layer1"
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer">
+ <g
+ style="display:inline"
+ transform="matrix(0.211321,0,0,0.209762,-0.326773,3.523521)"
+ id="g1197">
+ <path
+ d="M 32.706693,164.36026 C 22.319193,164.36026 13.956693,172.72276 13.956693,183.11026 C 13.956693,193.49776 22.319193,201.86026 32.706693,201.86026 L 205.20669,201.86026 C 215.59419,201.86026 223.95669,193.49776 223.95669,183.11026 C 223.95669,172.72276 215.59419,164.36026 205.20669,164.36026 L 32.706693,164.36026 z "
+ style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
+ id="path1196" />
+ <path
+ d="M 32.706693,165.61026 C 23.011693,165.61026 15.206693,173.41526 15.206693,183.11026 C 15.206693,192.80526 23.011693,200.61026 32.706693,200.61026 L 205.20669,200.61026 C 214.90169,200.61026 222.70669,192.80526 222.70669,183.11026 C 222.70669,173.41526 214.90169,165.61026 205.20669,165.61026 L 32.706693,165.61026 z "
+ style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
+ id="path1195" />
+ <path
+ d="M 32.706694,166.86026 C 23.704194,166.86026 16.456694,174.10776 16.456694,183.11026 C 16.456694,192.11276 23.704194,199.36026 32.706694,199.36026 L 205.20669,199.36026 C 214.20919,199.36026 221.45669,192.11276 221.45669,183.11026 C 221.45669,174.10776 214.20919,166.86026 205.20669,166.86026 L 32.706694,166.86026 z "
+ style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
+ id="path1194" />
+ <path
+ d="M 32.706694,168.11026 C 24.396694,168.11026 17.706694,174.80026 17.706694,183.11026 C 17.706694,191.42026 24.396694,198.11026 32.706694,198.11026 L 205.20669,198.11026 C 213.51669,198.11026 220.20669,191.42026 220.20669,183.11026 C 220.20669,174.80026 213.51669,168.11026 205.20669,168.11026 L 32.706694,168.11026 z "
+ style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
+ id="path1193" />
+ <path
+ d="M 32.707764,169.36026 C 25.090264,169.36026 18.957764,175.49276 18.957764,183.11026 C 18.957764,190.72776 25.090264,196.86026 32.707764,196.86026 L 205.20618,196.86026 C 212.82368,196.86026 218.95618,190.72776 218.95618,183.11026 C 218.95618,175.49276 212.82368,169.36026 205.20618,169.36026 L 32.707764,169.36026 z "
+ style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
+ id="path1192" />
+ <path
+ d="M 32.706694,170.61026 C 25.781694,170.61026 20.206694,176.18526 20.206694,183.11026 C 20.206694,190.03526 25.781694,195.61026 32.706694,195.61026 L 205.20669,195.61026 C 212.13169,195.61026 217.70669,190.03526 217.70669,183.11026 C 217.70669,176.18526 212.13169,170.61026 205.20669,170.61026 L 32.706694,170.61026 z "
+ style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
+ id="path1191" />
+ <path
+ d="M 32.706694,171.86026 C 26.474194,171.86026 21.456694,176.87776 21.456694,183.11026 C 21.456694,189.34276 26.474194,194.36026 32.706694,194.36026 L 205.20669,194.36026 C 211.43919,194.36026 216.45669,189.34276 216.45669,183.11026 C 216.45669,176.87776 211.43919,171.86026 205.20669,171.86026 L 32.706694,171.86026 z "
+ style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
+ id="path1190" />
+ <path
+ d="M 32.706694,173.11026 C 27.166694,173.11026 22.706694,177.57026 22.706694,183.11026 C 22.706694,188.65026 27.166694,193.11026 32.706694,193.11026 L 205.20669,193.11026 C 210.74669,193.11026 215.20669,188.65026 215.20669,183.11026 C 215.20669,177.57026 210.74669,173.11026 205.20669,173.11026 L 32.706694,173.11026 z "
+ style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
+ id="path1189" />
+ </g>
+ <path
+ style="color:black;fill:#edd400;fill-opacity:1;fill-rule:evenodd;stroke:#c4a000;stroke-width:0.99999982;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ d="M 10.301452,14.596007 L 38.950705,14.94956 C 39.713282,14.94956 40.194615,15.526206 40.327198,16.143397 C 40.327198,16.143397 44.728528,35.958541 44.728528,35.958541 C 44.728528,35.958541 44.740986,42.306163 44.740986,42.306163 C 44.740986,42.967549 44.127071,43.5 43.364494,43.5 L 5.8876636,43.5 C 5.1250868,43.5 4.5111713,42.967549 4.5111713,42.306163 L 4.4999999,36.139247 L 8.9249601,15.789844 C 9.2343193,15.128458 9.5388757,14.596007 10.301452,14.596007 z "
+ id="rect1975"
+ sodipodi:nodetypes="ccccccccccc" />
+ <rect
+ style="opacity:0.37078654;color:black;fill:#f57900;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ id="rect2851"
+ width="39.048077"
+ height="7.0714951"
+ x="5.1146202"
+ y="35.957905"
+ rx="0.67937863"
+ ry="0.67937863" />
+ <path
+ style="opacity:0.16292138;color:black;fill:white;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ d="M 5.0643333,36.53243 C 5.0643333,36.53243 5.2151951,36.0021 5.7683553,35.957906 L 43.332958,35.957906 C 44.087267,35.957906 44.137554,36.709207 44.137554,36.709207 C 44.137554,36.709207 44.161153,35.089634 42.853683,35.089634 L 6.4417985,35.089634 C 5.4360528,35.178022 5.0643333,35.869517 5.0643333,36.53243 z "
+ id="path2853"
+ sodipodi:nodetypes="ccccccc" />
+ <path
+ style="opacity:0.4831461;color:black;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient5615);stroke-width:0.99999982;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible"
+ d="M 10.3125,15.59375 C 10.161184,15.59375 10.21304,15.567007 10.15625,15.625 C 10.105373,15.676955 10.000439,15.900085 9.875,16.15625 C 9.8604211,16.186022 9.8588161,16.18654 9.84375,16.21875 L 5.5,36.125 L 5.5,36.1875 L 5.5,42.3125 C 5.5,42.356573 5.5723676,42.5 5.875,42.5 L 43.375,42.5 C 43.677632,42.5 43.75,42.356571 43.75,42.3125 C 43.75,42.3125 43.719646,36.244353 43.71875,36.0625 C 43.718499,36.061373 43.719177,35.970668 43.71875,35.96875 C 43.625157,35.547912 39.34375,16.375 39.34375,16.375 C 39.2872,16.111751 39.174175,15.9375 38.9375,15.9375 L 10.3125,15.59375 z "
+ id="path4730" />
+ <path
+ style="opacity:0.46629214;color:black;fill:url(#radialGradient6423);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ d="M 44.194174,35.681088 C 43.814854,34.425759 43.31029,31.880389 43.31029,31.880389 L 34.383068,35.06237 C 40.128311,35.06237 42.956737,34.797205 44.194174,35.681088 z "
+ id="path6415"
+ sodipodi:nodetypes="cccc" />
+ <path
+ sodipodi:nodetypes="cczczcccc"
+ id="path2524"
+ d="M 8.7832195,16.426565 L 4.6483895,34.455844 C 4.6483895,34.455844 26.119997,34.580845 33.923967,34.580845 C 41.904536,34.580845 45.079034,31.612425 45.079034,31.612425 C 45.079034,31.612425 44.072291,31.158937 42.753925,26.645704 C 42.753925,26.645704 40.35014,16.145315 40.35014,16.145315 C 39.886699,14.958914 39.493934,14.481646 38.59375,14.489065 L 10.8125,14.489065 C 9.0672254,14.521152 9.0760813,15.324617 8.7832195,16.426565 z "
+ style="fill:url(#radialGradient6405);fill-opacity:1;fill-rule:evenodd;stroke:#c4a000;stroke-width:0.99999964px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ <path
+ style="opacity:0.46629214;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#radialGradient6413);stroke-width:0.99999964px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 10.8125,15.5 C 10.172211,15.51823 10.104203,15.599785 10.0625,15.65625 C 10.019129,15.714974 9.9124269,16.076338 9.75,16.6875 L 5.90625,33.46875 C 7.6006857,33.478529 26.476894,33.59375 33.9375,33.59375 C 37.803786,33.59375 40.454778,32.879448 42.125,32.1875 C 43.179074,31.750813 43.238325,31.610471 43.5625,31.375 C 43.071872,30.585095 42.457459,29.252403 41.78125,26.9375 C 41.780599,26.916672 41.780599,26.895828 41.78125,26.875 C 41.78125,26.875 39.475321,16.832651 39.40625,16.53125 C 39.192263,15.983448 39.024353,15.680487 38.9375,15.59375 C 38.850647,15.507013 38.865951,15.497757 38.59375,15.5 L 10.84375,15.5 L 10.8125,15.5 z "
+ id="path6403" />
+ <path
+ sodipodi:nodetypes="ccccccc"
+ id="path6359"
+ d="M 5.0643333,38.53243 C 5.0643333,38.53243 5.2151951,38.0021 5.7683553,37.957906 L 43.332958,37.957906 C 44.087267,37.957906 44.137554,38.709207 44.137554,38.709207 C 44.137554,38.709207 44.161153,37.089634 42.853683,37.089634 L 6.4417985,37.089634 C 5.4360528,37.178022 5.0643333,37.869517 5.0643333,38.53243 z "
+ style="opacity:0.26404497;color:black;fill:white;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
+ <path
+ style="opacity:0.26404497;color:black;fill:white;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ d="M 5.0643333,40.53243 C 5.0643333,40.53243 5.2151951,40.0021 5.7683553,39.957906 L 43.332958,39.957906 C 44.087267,39.957906 44.137554,40.709207 44.137554,40.709207 C 44.137554,40.709207 44.161153,39.089634 42.853683,39.089634 L 6.4417985,39.089634 C 5.4360528,39.178022 5.0643333,39.869517 5.0643333,40.53243 z "
+ id="path6361"
+ sodipodi:nodetypes="ccccccc" />
+ <path
+ sodipodi:nodetypes="ccccccc"
+ id="path6363"
+ d="M 5.0643333,42.53243 C 5.0643333,42.53243 5.2151951,42.0021 5.7683553,41.957906 L 43.332958,41.957906 C 44.087267,41.957906 44.137554,42.709207 44.137554,42.709207 C 44.137554,42.709207 44.161153,41.089634 42.853683,41.089634 L 6.4417985,41.089634 C 5.4360528,41.178022 5.0643333,41.869517 5.0643333,42.53243 z "
+ style="opacity:0.26404497;color:black;fill:white;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
+ <path
+ inkscape:r_cy="true"
+ inkscape:r_cx="true"
+ transform="matrix(0.616613,0,0,0.293577,11.48816,14.62848)"
+ d="M 43.125 40.4375 A 19.5625 6.8125 0 1 1 4,40.4375 A 19.5625 6.8125 0 1 1 43.125 40.4375 z"
+ sodipodi:ry="6.8125"
+ sodipodi:rx="19.5625"
+ sodipodi:cy="40.4375"
+ sodipodi:cx="23.5625"
+ id="path3008"
+ style="opacity:0.2;color:black;fill:url(#radialGradient2552);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ sodipodi:type="arc" />
+ <g
+ style="display:inline"
+ id="g1574"
+ transform="matrix(1.033699,-0.276979,0.276979,1.033699,14.81828,-29.04823)"
+ inkscape:r_cx="true"
+ inkscape:r_cy="true">
+ <path
+ transform="translate(-29.75546,19)"
+ sodipodi:nodetypes="cccccc"
+ id="path2960"
+ d="M 17.34116,32.5 L 22.96616,26.875 L 43.059909,17.125 C 46.309909,15.875 48.247409,20.5 45.372409,22.125 L 25.34116,31.5 L 17.34116,32.5 z "
+ style="color:black;fill:#cb9022;fill-opacity:1;fill-rule:evenodd;stroke:#5c410c;stroke-width:0.93443578;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ inkscape:r_cx="true"
+ inkscape:r_cy="true" />
+ <path
+ transform="translate(-29.75546,19)"
+ style="color:black;fill:url(#linearGradient2554);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ d="M 38.330708,20 C 38.330708,20 39.768208,20.09375 40.330708,21.34375 C 40.910201,22.631511 40.330708,24 40.330708,24 L 45.361958,21.53125 C 45.361958,21.53125 46.81399,20.649883 46.018208,18.6875 C 45.233296,16.751923 43.330708,17.53125 43.330708,17.53125 L 38.330708,20 z "
+ id="path2964"
+ sodipodi:nodetypes="czcczcc"
+ inkscape:r_cx="true"
+ inkscape:r_cy="true" />
+ <path
+ transform="translate(-29.75546,19)"
+ sodipodi:nodetypes="czcczcc"
+ id="path2962"
+ d="M 38.330708,20 C 38.330708,20 39.768208,20.09375 40.330708,21.34375 C 40.910201,22.631511 40.330708,24 40.330708,24 L 42.330708,23 C 42.330708,23 43.15774,21.681133 42.549458,20.3125 C 41.924458,18.90625 40.330708,19 40.330708,19 L 38.330708,20 z "
+ style="color:black;fill:url(#linearGradient2556);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ inkscape:r_cx="true"
+ inkscape:r_cy="true" />
+ <path
+ transform="translate(-29.75546,19)"
+ sodipodi:nodetypes="cccc"
+ id="path2982"
+ d="M 18.768208,31.78125 L 23.268208,27.28125 C 24.768208,28.09375 25.549458,29.4375 25.143208,31 L 18.768208,31.78125 z "
+ style="color:black;fill:url(#radialGradient2558);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ inkscape:r_cx="true"
+ inkscape:r_cy="true" />
+ <path
+ transform="translate(-29.75546,19)"
+ sodipodi:nodetypes="cccc"
+ id="path2992"
+ d="M 20.111958,30.375 L 18.486958,31.96875 L 20.830708,31.65625 C 21.049458,30.9375 20.643208,30.59375 20.111958,30.375 z "
+ style="color:black;fill:url(#linearGradient2560);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ inkscape:r_cx="true"
+ inkscape:r_cy="true" />
+ <path
+ transform="translate(-29.75546,19)"
+ sodipodi:nodetypes="ccccc"
+ id="path3002"
+ d="M 23.268208,27.25 L 24.830708,28.5 L 40.218048,21.18133 C 39.773616,20.325286 38.976281,20.096733 38.314669,20.019068 L 23.268208,27.25 z "
+ style="color:black;fill:white;fill-opacity:0.36363639;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ inkscape:r_cx="true"
+ inkscape:r_cy="true" />
+ <path
+ transform="translate(-29.75546,19)"
+ sodipodi:nodetypes="ccccc"
+ id="path3004"
+ d="M 25.143208,31.0625 L 25.330708,30.3125 L 40.561798,23.1829 C 40.561798,23.1829 40.451638,23.796527 40.345919,23.93225 L 25.143208,31.0625 z "
+ style="color:black;fill:black;fill-opacity:0.36363639;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ inkscape:r_cx="true"
+ inkscape:r_cy="true" />
+ </g>
+ </g>
+</svg>
Follow ups