← Back to team overview

gtg-user team mailing list archive

[Merge] lp:~gtg-user/gtg/evolution-backend into lp:gtg

 

Luca Invernizzi has proposed merging lp:~gtg-user/gtg/evolution-backend into lp:gtg.

Requested reviews:
  Gtg developers (gtg)


Evolution backend (and removal of the plugin).
This is not at the level of the other backends because the evolution api is not complete (that is, evolution start_date and categories are not exposed). However, it's the best we can have for now.

With this merge, backends are covering all the synchronization plugins used to make.
-- 
https://code.launchpad.net/~gtg-user/gtg/evolution-backend/+merge/33854
Your team Gtg users is subscribed to branch lp:~gtg-user/gtg/evolution-backend.
=== added file 'GTG/backends/backend_evolution.py'
--- GTG/backends/backend_evolution.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/backend_evolution.py	2010-08-26 23:46:43 +0000
@@ -0,0 +1,348 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting 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/>.
+# -----------------------------------------------------------------------------
+
+'''
+Backend for storing/loading tasks in Evolution Tasks
+'''
+
+import os
+import time
+import uuid
+import datetime
+import evolution
+
+from GTG.backends.genericbackend        import GenericBackend
+from GTG                                import _
+from GTG.backends.syncengine            import SyncEngine, SyncMeme
+from GTG.backends.periodicimportbackend import PeriodicImportBackend
+from GTG.tools.dates                    import RealDate, NoDate
+from GTG.core.task                      import Task
+from GTG.tools.interruptible            import interruptible
+from GTG.tools.logger                   import Log
+from GTG.tools.tags                     import extract_tags_from_text
+
+#Dictionaries to translate GTG tasks in Evolution ones
+_GTG_TO_EVOLUTION_STATUS = \
+        {Task.STA_ACTIVE:    evolution.ecal.ICAL_STATUS_CONFIRMED,
+         Task.STA_DONE:      evolution.ecal.ICAL_STATUS_COMPLETED,
+         Task.STA_DISMISSED: evolution.ecal.ICAL_STATUS_CANCELLED}
+
+_EVOLUTION_TO_GTG_STATUS = \
+        {evolution.ecal.ICAL_STATUS_CONFIRMED:   Task.STA_ACTIVE,
+         evolution.ecal.ICAL_STATUS_DRAFT:       Task.STA_ACTIVE,
+         evolution.ecal.ICAL_STATUS_FINAL:       Task.STA_ACTIVE,
+         evolution.ecal.ICAL_STATUS_INPROCESS:   Task.STA_ACTIVE,
+         evolution.ecal.ICAL_STATUS_NEEDSACTION: Task.STA_ACTIVE,
+         evolution.ecal.ICAL_STATUS_NONE:        Task.STA_ACTIVE,
+         evolution.ecal.ICAL_STATUS_TENTATIVE:   Task.STA_ACTIVE,
+         evolution.ecal.ICAL_STATUS_X:           Task.STA_ACTIVE,
+         evolution.ecal.ICAL_STATUS_COMPLETED:   Task.STA_DONE,
+         evolution.ecal.ICAL_STATUS_CANCELLED:   Task.STA_DISMISSED}
+
+
+
+class Backend(PeriodicImportBackend):
+    '''
+    Evolution backend
+    '''
+
+
+    _general_description = { \
+        GenericBackend.BACKEND_NAME:       "backend_evolution", \
+        GenericBackend.BACKEND_HUMAN_NAME: _("Evolution tasks"), \
+        GenericBackend.BACKEND_AUTHORS:    ["Luca Invernizzi"], \
+        GenericBackend.BACKEND_TYPE:       GenericBackend.TYPE_READWRITE, \
+        GenericBackend.BACKEND_DESCRIPTION: \
+            _("Lets you synchronize your GTG tasks with Evolution tasks"),\
+        }
+
+    _static_parameters = { \
+        "period": { \
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT, \
+            GenericBackend.PARAM_DEFAULT_VALUE: 10, },
+        }
+
+###############################################################################
+### Backend standard methods ##################################################
+###############################################################################
+
+    def __init__(self, parameters):
+        '''
+        See GenericBackend for an explanation of this function.
+        Loads the saved state of the sync, if any
+        '''
+        super(Backend, self).__init__(parameters)
+        #loading the saved state of the synchronization, if any
+        self.sync_engine_path = os.path.join('backends/evolution/', \
+                                      "sync_engine-" + self.get_id())
+        self.sync_engine = self._load_pickled_file(self.sync_engine_path, \
+                                                   SyncEngine())
+        #sets up the connection to the evolution api
+        task_personal = evolution.ecal.list_task_sources()[0][1]
+        self._evolution_tasks = evolution.ecal.open_calendar_source( \
+                                    task_personal,
+                                    evolution.ecal.CAL_SOURCE_TYPE_TODO)
+
+    def do_periodic_import(self):
+        """
+        See PeriodicImportBackend for an explanation of this function.
+        """
+        stored_evolution_task_ids = set(self.sync_engine.get_all_remote())
+        current_evolution_task_ids = set([task.get_uid() \
+               for task in self._evolution_tasks.get_all_objects()])
+        for evo_task_id in current_evolution_task_ids:
+            #Adding and updating
+            self.cancellation_point()
+            self._process_evo_task(evo_task_id)
+
+        for evo_task_id in stored_evolution_task_ids.difference(\
+                                current_evolution_task_ids):
+            #Removing the old ones
+            self.cancellation_point()
+            tid = self.sync_engine.get_local_id(evo_task_id)
+            self.datastore.request_task_deletion(tid)
+            try:
+                self.sync_engine.break_relationship(remote_id = \
+                                                    evo_task_id)
+            except KeyError:
+                pass
+
+    def save_state(self):
+        '''
+        See GenericBackend for an explanation of this function.
+        '''
+        self._store_pickled_file(self.sync_engine_path, self.sync_engine)
+
+###############################################################################
+### Process tasks #############################################################
+###############################################################################
+
+    @interruptible
+    def remove_task(self, tid):
+        '''
+        See GenericBackend for an explanation of this function.
+        '''
+        try:
+            evo_task_id = self.sync_engine.get_remote_id(tid)
+            self._delete_evolution_task(self._evo_get_task(evo_task_id))
+        except KeyError:
+            pass
+        try:
+            self.sync_engine.break_relationship(local_id = tid)
+        except:
+            pass
+
+    @interruptible
+    def set_task(self, task):
+        '''
+        See GenericBackend for an explanation of this function.
+        '''
+        tid = task.get_id()
+        is_syncable = self._gtg_task_is_syncable_per_attached_tags(task)
+        action, evo_task_id = self.sync_engine.analyze_local_id(
+                                tid,
+                                self.datastore.has_task,
+                                self._evo_has_task,
+                                is_syncable)
+        Log.debug('GTG->Evo set task (%s, %s)' % (action, is_syncable))
+
+        if action == None:
+            return
+
+        if action == SyncEngine.ADD:
+            evo_task = evolution.ecal.ECalComponent( \
+                        ical = evolution.ecal.CAL_COMPONENT_TODO)
+            self._evolution_tasks.add_object(evo_task)
+            self._populate_evo_task(task, evo_task)
+            meme = SyncMeme(task.get_modified(),
+                            self._evo_get_modified(evo_task),
+                            "GTG")
+            self.sync_engine.record_relationship( \
+                local_id = tid, remote_id = evo_task.get_uid(), meme = meme)
+
+        elif action == SyncEngine.UPDATE:
+            evo_task = self._evo_get_task(evo_task_id)
+            meme = self.sync_engine.get_meme_from_local_id(task.get_id())
+            newest = meme.which_is_newest(task.get_modified(),
+                                 self._evo_get_modified(evo_task))
+            if newest == "local":
+                self._populate_evo_task(task, evo_task)
+                meme.set_remote_last_modified( \
+                            self._evo_get_modified(evo_task))
+                meme.set_local_last_modified(task.get_modified())
+            else:
+                #we skip saving the state
+                return
+
+        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:
+            evo_task = self._evo_get_task(evo_task_id)
+            self._exec_lost_syncability(tid, evo_task)
+        self.save_state()
+
+    def _process_evo_task(self, evo_task_id):
+        '''
+        Takes an evolution task id and carries out the necessary operations to
+        refresh the sync state
+        '''
+        self.cancellation_point()
+        evo_task = self._evo_get_task(evo_task_id)
+        is_syncable = self._evo_task_is_syncable(evo_task)
+        action, tid = self.sync_engine.analyze_remote_id( \
+                         evo_task_id,
+                         self.datastore.has_task,
+                         self._evo_has_task,
+                         is_syncable)
+        Log.debug('GTG<-Evo set task (%s, %s)' % (action, is_syncable))
+
+        if action == SyncEngine.ADD:
+            tid = str(uuid.uuid4())
+            task = self.datastore.task_factory(tid)
+            self._populate_task(task, evo_task)
+            meme = SyncMeme(task.get_modified(),
+                            self._evo_get_modified(evo_task),
+                            "GTG")
+            self.sync_engine.record_relationship(local_id = tid,
+                                                 remote_id = evo_task_id,
+                                                 meme = meme)
+            self.datastore.push_task(task)
+
+        elif action == SyncEngine.UPDATE:
+            task = self.datastore.get_task(tid)
+            meme = self.sync_engine.get_meme_from_remote_id(evo_task_id)
+            newest = meme.which_is_newest(task.get_modified(),
+                                self._evo_get_modified(evo_task))
+            if newest == "remote":
+                with self.datastore.get_backend_mutex():
+                    self._populate_task(task, evo_task)
+                    meme.set_remote_last_modified( \
+                            self._evo_get_modified(evo_task))
+                    meme.set_local_last_modified(task.get_modified())
+
+        elif action == SyncEngine.REMOVE:
+            return
+            try:
+                evo_task = self._evo_get_task(evo_task_id)
+                self._delete_evolution_task(evo_task)
+                self.sync_engine.break_relationship(remote_id = evo_task)
+            except KeyError:
+                pass
+
+        elif action == SyncEngine.LOST_SYNCABILITY:
+            self._exec_lost_syncability(tid, evo_task)
+        self.save_state()
+
+###############################################################################
+### Helper methods ############################################################
+###############################################################################
+
+    def _evo_has_task(self, evo_task_id):
+        '''Returns true if Evolution has that task'''
+        return bool(self._evo_get_task(evo_task_id))
+
+    def _evo_get_task(self, evo_task_id):
+        '''Returns an Evolution task, given its uid'''
+        return self._evolution_tasks.get_object(evo_task_id, "")
+
+    def _evo_get_modified(self, evo_task):
+        '''Returns the modified time of an Evolution task'''
+        return datetime.datetime.fromtimestamp(evo_task.get_modified())
+
+    def _delete_evolution_task(self, evo_task):
+        '''Deletes an Evolution task, given the task object'''
+        self._evolution_tasks.remove_object(evo_task)
+        self._evolution_tasks.update_object(evo_task)
+
+    def _populate_task(self, task, evo_task):
+        '''
+        Updates the attributes of a GTG task copying the ones of an Evolution
+        task
+        '''
+        task.set_title(evo_task.get_summary())
+        text = evo_task.get_description()
+        if text == None:
+            text = ""
+        task.set_text(text)
+        due_date_timestamp = evo_task.get_due()
+        if isinstance(due_date_timestamp, (int, float)):
+            datetime_due_date = RealDate(datetime.datetime.fromtimestamp(\
+                        due_date_timestamp + time.timezone).date())
+        else:
+            datetime_due_date = NoDate()
+        task.set_due_date(datetime_due_date)
+        status = evo_task.get_status()
+        if task.get_status() != _EVOLUTION_TO_GTG_STATUS[status]:
+            task.set_status(_EVOLUTION_TO_GTG_STATUS[status])
+        task.set_only_these_tags(extract_tags_from_text(text))
+
+    def _populate_evo_task(self, task, evo_task):
+        evo_task.set_summary(task.get_title())
+        text = task.get_excerpt(strip_tags = True, strip_subtasks = True)
+        if evo_task.get_description() != text:
+            evo_task.set_description(text)
+        due_date = task.get_due_date()
+        if isinstance(due_date, NoDate):
+            #FIXME: how to unset due dates 
+            evo_task.set_due(None)
+            print "DUE DATE UNSETTING UNSUPPORTED"
+        else:
+            #FIXME: this is strange - review
+            due_date_for_evo = int(time.mktime(\
+                        due_date.to_py_date().timetuple()) - \
+                        time.timezone) - time.timezone
+            evo_task.set_due(due_date_for_evo)
+        status = task.get_status()
+        if _EVOLUTION_TO_GTG_STATUS[evo_task.get_status()] != status:
+            evo_task.set_status(_GTG_TO_EVOLUTION_STATUS[status])
+        self._evolution_tasks.update_object(evo_task)
+
+    def _exec_lost_syncability(self, tid, evo_task):
+        '''
+        Executed when a relationship between tasks loses its syncability
+        property. See SyncEngine for an explanation of that.
+        This function finds out which object is the original one
+        and which is the copy, and deletes the copy.
+        '''
+        meme = self.sync_engine.get_meme_from_local_id(tid)
+        self.sync_engine.break_relationship(local_id = tid)
+        if meme.get_origin() == "GTG":
+            evo_task = self._evo_get_task(evo_task.get_uid())
+            self._delete_evolution_task(evo_task)
+        else:
+            self.datastore.request_task_deletion(tid)
+
+    def _evo_task_is_syncable(self, evo_task):
+        '''
+        Returns True if this Evolution task should be synced into GTG tasks.
+
+        @param evo_task: an Evolution task
+        @returns Boolean
+        '''
+        attached_tags = set(self.get_attached_tags())
+        if GenericBackend.ALLTASKS_TAG in attached_tags:
+            return True
+        evo_tags = set(extract_tags_from_text(evo_task.get_description()))
+        return evo_task.is_disjoint(attached_tags)
+

=== removed file 'GTG/plugins/evolution-sync.gtg-plugin'
--- GTG/plugins/evolution-sync.gtg-plugin	2010-07-29 10:36:24 +0000
+++ GTG/plugins/evolution-sync.gtg-plugin	1970-01-01 00:00:00 +0000
@@ -1,9 +0,0 @@
-[GTG Plugin]
-Module=evolution_sync
-Name=Evolution
-Short-description="Plugin for synchronising GTG with Gnome Evolution Tasks."
-Description="""Plugin for synchronising GTG with Gnome Evolution Tasks"""
-Authors='Luca Invernizzi <invernizzi.l@xxxxxxxxx>'
-Version=0.1.1
-Dependencies=evolution,
-Enabled=False

=== removed directory 'GTG/plugins/evolution_sync'
=== removed file 'GTG/plugins/evolution_sync/__init__.py'
--- GTG/plugins/evolution_sync/__init__.py	2010-03-16 02:16:14 +0000
+++ GTG/plugins/evolution_sync/__init__.py	1970-01-01 00:00:00 +0000
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@xxxxxxxxx>
-#
-# 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/>.
-
-from GTG.plugins.evolution_sync.evolutionSync import EvolutionSync
-
-#needed to keep pyflakes quiet
-if False == True: EvolutionSync()

=== removed file 'GTG/plugins/evolution_sync/evolutionProxy.py'
--- GTG/plugins/evolution_sync/evolutionProxy.py	2010-03-16 02:16:14 +0000
+++ GTG/plugins/evolution_sync/evolutionProxy.py	1970-01-01 00:00:00 +0000
@@ -1,73 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@xxxxxxxxx>
-#
-# 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/>.
-
-import evolution
-
-from GTG.core.task import Task
-from GTG.plugins.evolution_sync.evolutionTask import EvolutionTask
-from GTG.plugins.evolution_sync.genericProxy import GenericProxy
-
-
-class EvolutionProxy(GenericProxy):
-    
-    __GTG_STATUSES = [Task.STA_ACTIVE,
-                   Task.STA_DONE,
-                   Task.STA_DISMISSED]
-
-    __EVO_STATUSES = [evolution.ecal.ICAL_STATUS_CONFIRMED,
-                   evolution.ecal.ICAL_STATUS_COMPLETED,
-                   evolution.ecal.ICAL_STATUS_CANCELLED]
-
-    def __init__(self):
-        super(EvolutionProxy, self).__init__()
-
-    def generateTaskList(self):
-        task_personal = evolution.ecal.list_task_sources()[0][1]
-        self._evolution_tasks = evolution.ecal.open_calendar_source(task_personal,
-                                   evolution.ecal.CAL_SOURCE_TYPE_TODO)
-        self._gtg_to_evo_status = dict(zip(self.__GTG_STATUSES,
-                                            self.__EVO_STATUSES))
-        self._evo_to_gtg_status = dict(zip(self.__EVO_STATUSES,
-                                            self.__GTG_STATUSES))
-        #Need to find a solution for the statuses GTG doesn't expect:
-        for evo_status in [evolution.ecal.ICAL_STATUS_DRAFT,
-                           evolution.ecal.ICAL_STATUS_FINAL,
-                           evolution.ecal.ICAL_STATUS_INPROCESS,
-                           evolution.ecal.ICAL_STATUS_NEEDSACTION,
-                           evolution.ecal.ICAL_STATUS_NONE,
-                           evolution.ecal.ICAL_STATUS_TENTATIVE,
-                           evolution.ecal.ICAL_STATUS_X]:
-            self._evo_to_gtg_status[evo_status] = Task.STA_ACTIVE
-        self._tasks_list = []
-        for task in self._evolution_tasks.get_all_objects():
-            self._tasks_list.append(EvolutionTask(task, self))
-
-    def create_new_task(self, title):
-        task = evolution.ecal.ECalComponent(ical=evolution.ecal.CAL_COMPONENT_TODO)
-        self._evolution_tasks.add_object(task)
-        new_task = EvolutionTask(task, self)
-        new_task.title = title
-        self._tasks_list.append(new_task)
-        return new_task
-
-    def delete_task(self, task):
-        evo_task = task.get_evolution_task()
-        self._evolution_tasks.remove_object(evo_task)
-        self._evolution_tasks.update_object(evo_task)
-
-    def update_task(self, task):
-        evo_task = task.get_evolution_task()
-        self._evolution_tasks.update_object(evo_task)

=== removed file 'GTG/plugins/evolution_sync/evolutionSync.py'
--- GTG/plugins/evolution_sync/evolutionSync.py	2010-03-16 02:16:14 +0000
+++ GTG/plugins/evolution_sync/evolutionSync.py	1970-01-01 00:00:00 +0000
@@ -1,45 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@xxxxxxxxx>
-#                    - Paulo Cabido <paulo.cabido@xxxxxxxxx> (example file)
-#
-# 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/>.
-
-import gtk
-from threading import Thread
-
-from GTG import _
-from GTG.plugins.evolution_sync.syncEngine import SyncEngine
-
-class EvolutionSync:
-
-    def __init__(self):
-        #drop down menu
-        self.menu_item = gtk.MenuItem(_("Synchronize with Evolution"))
-        self.menu_item.connect('activate', self.onTesteMenu)
-
-    def activate(self, plugin_api):
-        self.plugin_api = plugin_api
-        # add a menu item to the menu bar
-        plugin_api.add_menu_item(self.menu_item)
-        self.sync_engine = SyncEngine(self)
-
-    def deactivate(self, plugin_api):
-        plugin_api.remove_menu_item(self.menu_item)
-
-    def onTesteMenu(self, widget):
-        self.worker_thread = Thread(target = \
-                                self.sync_engine.synchronize).start()
-
-    def onTaskOpened(self, plugin_api):
-        pass

=== removed file 'GTG/plugins/evolution_sync/evolutionTask.py'
--- GTG/plugins/evolution_sync/evolutionTask.py	2010-03-16 02:16:14 +0000
+++ GTG/plugins/evolution_sync/evolutionTask.py	1970-01-01 00:00:00 +0000
@@ -1,110 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@xxxxxxxxx>
-#
-# 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/>.
-
-import time
-import datetime
-
-from GTG.plugins.evolution_sync.genericTask import GenericTask
-
-
-class EvolutionTask(GenericTask):
-
-    def __init__(self, evo_task, evolution_proxy):
-        super(EvolutionTask, self).__init__(evolution_proxy)
-        self._evo_task = evo_task
-
-    def _get_title(self):
-        return self._evo_task.get_summary()
-
-    def _set_title(self, title):
-        self._evo_task.set_summary(title)
-        self.get_proxy().update_task(self)
-
-    def _get_id(self):
-        return self._evo_task.get_uid()
-
-    def _get_tags(self):
-        #We could use Evolution's "Categories" as tags
-        raise NotImplementedError()
-
-    def _set_tags(self, tags):
-        raise NotImplementedError()
-        self.get_proxy().update_task(self)
-
-    def _get_text(self):
-        desc = self._evo_task.get_description()
-        if desc == None:
-            return ""
-        return desc
-
-    def _set_text(self, text):
-        self._evo_task.set_description(text)
-        self.get_proxy().update_task(self)
-
-    def _set_status(self, status):
-        #Since Evolution's statuses are fare more than GTG's,
-        # we need to check that the current status is not one of the various
-        # statuses translated in the same gtg status, passed by argument.
-        # This way, we avoid to force a status change when it's not needed 
-        # (and not wanted)
-        current_status_in_gtg_terms = self.get_proxy()._evo_to_gtg_status[\
-                                                   self._evo_task.get_status()]
-        if current_status_in_gtg_terms != status:
-            new_evo_status = self.get_proxy()._gtg_to_evo_status[status]
-            self._evo_task.set_status(new_evo_status)
-            self.get_proxy().update_task(self)
-
-    def _get_status(self):
-        status = self._evo_task.get_status()
-        return self.get_proxy()._evo_to_gtg_status[status]
-
-    def _get_due_date(self):
-        due = self._evo_task.get_due()
-        if isinstance(due, (int, float)):
-            return self.__time_evo_to_date(due)
-
-    def _set_due_date(self, due):
-        if due == None:
-            #TODO: I haven't find a way to reset the due date
-            # We could copy the task, but that would lose all the attributes
-            # currently not supported by the library used (and they're a lot)
-            pass
-        else:
-            self._evo_task.set_due(self.__time_date_to_evo(due))
-        self.get_proxy().update_task(self)
-
-    def _get_modified(self):
-        return self.__time_evo_to_datetime(self._evo_task.get_modified())
-
-    def __time_evo_to_date(self, timestamp):
-        return datetime.datetime.fromtimestamp(timestamp + time.timezone).date()
-
-    def __time_evo_to_datetime(self, timestamp):
-        return datetime.datetime.fromtimestamp(timestamp)
-
-    def __time_datetime_to_evo(self, timeobject):
-        return int(time.mktime(timeobject.timetuple()))
-
-    def __time_date_to_evo(self, timeobject):
-        #NOTE: need to substract the timezone to avoid the "one day before bug
-        # (at the airport => no internet now, need to fill bug number in)
-        #Explanation: gtg date format is converted to datetime in date/00:00 in 
-        # local time, and time.mktime considers that when converting to UNIX
-        # time. Evolution, however, doesn't convert back to local time.
-        return self.__time_datetime_to_evo(timeobject) - time.timezone
-
-    def get_evolution_task(self):
-        return self._evo_task

=== removed file 'GTG/plugins/evolution_sync/genericProxy.py'
--- GTG/plugins/evolution_sync/genericProxy.py	2010-01-31 10:16:17 +0000
+++ GTG/plugins/evolution_sync/genericProxy.py	1970-01-01 00:00:00 +0000
@@ -1,34 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@xxxxxxxxx>
-#
-# 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/>.
-
-class GenericProxy(object):
-
-    def __init__(self):
-        super(GenericProxy, self).__init__()
-        self._tasks_list = []
-
-    def get_tasks_list(self):
-        return self._tasks_list
-
-    def generateTaskList(self):
-        raise NotImplementedError()
-
-    def create_new_task(self, title):
-        raise NotImplementedError()
-
-    def delete_task(self, task):
-        raise NotImplementedError()
-

=== removed file 'GTG/plugins/evolution_sync/genericTask.py'
--- GTG/plugins/evolution_sync/genericTask.py	2010-01-31 10:16:17 +0000
+++ GTG/plugins/evolution_sync/genericTask.py	1970-01-01 00:00:00 +0000
@@ -1,68 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@xxxxxxxxx>
-#
-# 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/>.
-
-
-class GenericTask(object):
-    """GenericTask is the abstract interface that represents a generic task."""
-
-    def __init__(self, proxy):
-        self.__proxy = proxy
-
-    def __str__(self):
-        return "Task " + self.title + "(" + self.id + ")"
-
-    def toString(self):
-        return "Task:\n" + \
-                "\t - Title:    " + self.title         + "\n" + \
-                "\t - ID:       " + self.id            + "\n" + \
-                "\t - Modified: " + str(self.modified) + "\n" + \
-                "\t - Due:      " + str(self.due_date) + "\n" 
-
-    def copy(self, task):
-        #Minimizing the number of actions will allow a faster RTM plugin
-        # (where GET is fast, but SET is slow)
-        if self.title != task.title:
-            self.title = task.title
-        if self.text != task.text:
-            self.text = task.text
-        if self.status != task.status:
-            self.status = task.status
-        if self.due_date != task.due_date:
-            self.due_date = task.due_date
-
-    def get_proxy(self):
-        return self.__proxy
-
-    title = property(lambda self: self._get_title(),
-                     lambda self, arg: self._set_title(arg))
-
-    id = property(lambda self: self._get_id())
-
-    text = property(lambda self: self._get_text(),
-                     lambda self, arg: self._set_text(arg))
-
-    status = property(lambda self: self._get_status(),
-                     lambda self, arg: self._set_status(arg))
-
-    modified = property(lambda self: self._get_modified())
-
-    due_date = property(lambda self: self._get_due_date(),
-                     lambda self, arg: self._set_due_date(arg))
-
-    tags = property(lambda self: self._get_tags(),
-                     lambda self, arg: self._set_tags(arg))
-
-    self = property(lambda self: self)

=== removed file 'GTG/plugins/evolution_sync/gtgProxy.py'
--- GTG/plugins/evolution_sync/gtgProxy.py	2010-03-16 02:16:14 +0000
+++ GTG/plugins/evolution_sync/gtgProxy.py	1970-01-01 00:00:00 +0000
@@ -1,49 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@xxxxxxxxx>
-#
-# 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/>.
-
-from GTG.core.task import Task
-from GTG.plugins.evolution_sync.gtgTask import GtgTask
-from GTG.plugins.evolution_sync.genericProxy import GenericProxy
-
-
-class GtgProxy(GenericProxy):
-
-    def __init__(self, plugin_api):
-        super(GtgProxy, self).__init__()
-        self.plugin_api = plugin_api
-        self.requester = self.plugin_api.get_requester()
-
-    def generateTaskList(self):
-        self._tasks_list = []
-        requester = self.plugin_api.get_requester()
-        statuses = [Task.STA_ACTIVE, Task.STA_DISMISSED, Task.STA_DONE]
-        tasks = map(self.plugin_api.get_task, \
-                     requester.get_tasks_list(status = statuses))
-        map(lambda task: self._tasks_list.append(GtgTask(task, \
-                                        self.plugin_api, self)), tasks)
-
-    def create_new_task(self, title, never_seen_before = True):
-        new_gtg_local_task = self.requester.new_task(newtask=never_seen_before)
-        new_task = GtgTask(new_gtg_local_task, self.plugin_api, self)
-        new_task.title = title
-        self._tasks_list.append(new_task)
-        return new_task
-
-    def delete_task(self, task):
-        #NOTE: delete_task wants the internal gtg id, not the uuid
-        id = task.get_gtg_task().get_id()
-        self.requester.delete_task(id)
-

=== removed file 'GTG/plugins/evolution_sync/gtgTask.py'
--- GTG/plugins/evolution_sync/gtgTask.py	2010-03-16 02:16:14 +0000
+++ GTG/plugins/evolution_sync/gtgTask.py	1970-01-01 00:00:00 +0000
@@ -1,91 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@xxxxxxxxx>
-#
-# 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/>.
-
-import datetime
-
-from GTG.tools.dates import NoDate, RealDate
-from GTG.plugins.evolution_sync.genericTask import GenericTask
-
-class GtgTask(GenericTask):
-
-    def __init__(self, gtg_task, plugin_api, gtg_proxy):
-        super(GtgTask, self).__init__(gtg_proxy)
-        self._gtg_task = gtg_task
-        self.plugin_api = plugin_api
-
-    def _get_title(self):
-        return self._gtg_task.get_title()
-
-    def _set_title(self, title):
-        self._gtg_task.set_title(title)
-
-    def _get_id(self):
-        return self._gtg_task.get_uuid()
-
-    def _get_tags(self):
-        raise NotImplemented()
-        return self._gtg_task.get_tags()
-
-    def _set_tags(self, tags):
-        raise NotImplemented()
-        #NOTE: isn't there a better mode than removing all tags?
-        #      need to add function in GTG/core/task.py
-        old_tags = self.tags
-        for tag in old_tags:
-            self._gtg_task.remove_tag(tag)
-        map(lambda tag: self._gtg_task.add_tag('@'+tag), tags)
-
-    def _get_text(self):
-        return self._gtg_task.get_excerpt()
-
-    def _set_text(self, text):
-        self._gtg_task.set_text(text)
-
-    def _set_status(self, status):
-        self._gtg_task.set_status(status)
-
-    def _get_status(self):
-        return self._gtg_task.get_status()
-
-    def _get_due_date(self):
-        due_date = self._gtg_task.get_due_date()
-        if due_date == NoDate():
-                return None
-        return due_date.to_py_date()
-
-    def _set_due_date(self, due):
-        if due == None:
-            gtg_due = NoDate()
-        else:
-            gtg_due = RealDate(due)
-        self._gtg_task.set_due_date(gtg_due)
-
-    def _get_modified(self):
-        modified = self._gtg_task.get_modified()
-        if modified == None or modified == "":
-            return None
-        return self.__time_gtg_to_datetime(modified)
-
-    def get_gtg_task(self):
-        return self._gtg_task
-
-    def __time_gtg_to_datetime(self, string):
-        #FIXME: need to handle time with TIMEZONES!
-        string = string.split('.')[0].split('Z')[0]
-        if string.find('T') == -1:
-            return datetime.datetime.strptime(string.split(".")[0], "%Y-%m-%d")
-        return datetime.datetime.strptime(string.split(".")[0], \
-                                          "%Y-%m-%dT%H:%M:%S")

=== removed file 'GTG/plugins/evolution_sync/syncEngine.py'
--- GTG/plugins/evolution_sync/syncEngine.py	2010-03-16 02:16:14 +0000
+++ GTG/plugins/evolution_sync/syncEngine.py	1970-01-01 00:00:00 +0000
@@ -1,208 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@xxxxxxxxx>
-#
-# 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/>.
-
-import datetime
-from xdg.BaseDirectory import xdg_data_home
-
-from GTG.plugins.evolution_sync.gtgProxy       import GtgProxy
-from GTG.plugins.evolution_sync.evolutionProxy import EvolutionProxy
-
-class TaskPair(object):
-        def __init__(self, \
-                     local_task,
-                     remote_task):
-            self.local_id = local_task.id
-            self.remote_id = remote_task.id
-            self.__remote_synced_until = remote_task.modified
-            self.local_synced_until = local_task.modified
-            self.__remote_first_seen = datetime.datetime.now()
-
-        self = property(lambda self: self)
-
-        remote_synced_until = property (\
-                lambda self: self.__get_remote_synced_until(),\
-                lambda self, t: self.__set_remote_synced_until(t))
-
-        def __get_remote_synced_until(self):
-            if self.__remote_synced_until > self.__remote_first_seen:
-                return self.__remote_synced_until
-            else:
-                return self.__remote_first_seen
-
-        def __set_remote_synced_until(self, datetime_object):
-            self.__remote_synced_until = datetime_object
-
-
-class SyncEngine(object):
-
-    def __init__(self, this_plugin):
-        super(SyncEngine, self).__init__()
-        self.this_plugin = this_plugin
-        self.plugin_api = this_plugin.plugin_api
-        self.local_proxy = GtgProxy(self.this_plugin.plugin_api)
-        self.remote_proxy = EvolutionProxy()
-
-    def synchronize(self):
-        self.synchronizeWorker()
-
-    def synchronizeWorker(self):
-        #Generate the two list of tasks from the local and remote task source 
-        self.remote_proxy.generateTaskList()
-        self.local_proxy.generateTaskList()
-        remote_tasks = self.remote_proxy.get_tasks_list()
-        local_tasks = self.local_proxy.get_tasks_list()
-
-        #Load the list of known links between tasks (tasks that are the same 
-        # one saved in different task sources)
-        self.taskpairs = self.plugin_api.load_configuration_object( \
-                            "evolution-sync", \
-                            "taskpairs", \
-                            xdg_data_home)
-        if self.taskpairs == None:
-        #We have no previous knowledge of links, this must be the first
-        #attempt to synchronize. So, we try to infer some by
-        # linking tasks with the same title
-            self._link_same_title(local_tasks, remote_tasks) 
-        
-        #We'll use some sets to see what is new and what was deleted
-        old_local_ids = set(map(lambda tp: tp.local_id, self.taskpairs))
-        old_remote_ids = set(map(lambda tp: tp.remote_id, self.taskpairs))
-        current_local_ids = set(map(lambda t: t.id, local_tasks))
-        current_remote_ids = set(map(lambda t: t.id, remote_tasks))
-        #Tasks that have been added
-        new_local_ids = current_local_ids.difference(old_local_ids)
-        new_remote_ids = current_remote_ids.difference(old_remote_ids)
-        #Tasks that have been deleted
-        deleted_local_ids = old_local_ids.difference(current_local_ids)
-        deleted_remote_ids = old_remote_ids.difference(current_remote_ids)
-        #Local tasks with the remote task still existing (could need to be
-        #updated)
-        updatable_local_ids = current_local_ids.difference(new_local_ids)
-        
-        #Add tasks to the remote proxy
-        [new_local_tasks, new_remote_tasks] = self._process_new_tasks(\
-                                                        new_local_ids,\
-                                                        local_tasks, \
-                                                        self.remote_proxy)
-        self._append_to_taskpairs(new_local_tasks, new_remote_tasks)
-        #Add tasks to the local proxy
-        [new_remote_tasks, new_local_tasks] = self._process_new_tasks(\
-                                                        new_remote_ids,\
-                                                        remote_tasks,\
-                                                        self.local_proxy)
-        self._append_to_taskpairs(new_local_tasks, new_remote_tasks)
-        
-        #Delete tasks from the remote proxy
-        taskpairs_deleted = filter(lambda tp: tp.local_id in deleted_local_ids,\
-                                    self.taskpairs)
-        remote_ids_to_delete = map( lambda tp: tp.remote_id, taskpairs_deleted)
-        self._process_deleted_tasks(remote_ids_to_delete, remote_tasks,\
-                                    self.remote_proxy)
-        map(lambda tp: self.taskpairs.remove(tp), taskpairs_deleted)
-
-        #Delete tasks from the local proxy
-        taskpairs_deleted = filter(lambda tp: tp.remote_id in deleted_remote_ids,\
-                                    self.taskpairs)
-        local_ids_to_delete = map( lambda tp: tp.local_id, taskpairs_deleted)
-        self._process_deleted_tasks(local_ids_to_delete, local_tasks, self.local_proxy)
-        map(lambda tp: self.taskpairs.remove(tp), taskpairs_deleted)
-
-        #Update tasks
-        local_to_taskpair = self._list_to_dict(self.taskpairs, \
-                                                  "local_id", \
-                                                  "self")
-        local_id_to_task = self._list_to_dict(local_tasks, \
-                                                  "id", \
-                                                  "self")
-        remote_id_to_task = self._list_to_dict(remote_tasks, \
-                                                  "id", \
-                                                  "self")
-        for local_id in updatable_local_ids:
-            taskpair = local_to_taskpair[local_id]
-            local_task = local_id_to_task[local_id]
-            remote_task = remote_id_to_task[taskpair.remote_id]
-            local_was_updated = local_task.modified > \
-                                    taskpair.local_synced_until
-            remote_was_updated = remote_task.modified > \
-                                    taskpair.remote_synced_until
-
-            if local_was_updated and remote_was_updated:
-                if local_task.modified > remote_task.modified:
-                    remote_task.copy(local_task)
-                else: 
-                #If the update time is the same one, we have to
-                # arbitrary decide which gets copied
-                    local_task.copy(remote_task)
-            elif local_was_updated:
-                remote_task.copy(local_task)
-            elif remote_was_updated:
-                local_task.copy(remote_task)
-
-            taskpair.remote_synced_until = remote_task.modified
-            taskpair.local_synced_until = local_task.modified
-
-        #Lastly, save the list of known links
-        self.plugin_api.save_configuration_object( \
-                            "evolution-sync", \
-                            "taskpairs", \
-                            self.taskpairs,
-                            xdg_data_home)
-
-    def _append_to_taskpairs(self, local_tasks, remote_tasks):
-        for local, remote in zip(local_tasks, remote_tasks):
-            self.taskpairs.append(TaskPair( \
-                                local_task = local,
-                                remote_task = remote))
-
-    def _task_ids_to_tasks(self, id_list, task_list):
-        #TODO: this is not the quickest way to do this
-        id_to_task = self._list_to_dict(task_list, "id", "self")
-        return map(lambda id: id_to_task[id], id_list)
-        
-
-
-    def _process_new_tasks(self, new_ids, all_tasks, proxy):
-        new_tasks = self._task_ids_to_tasks(new_ids, all_tasks)
-        created_tasks = []
-        for task in new_tasks:
-            created_task = proxy.create_new_task(task.title)
-            created_task.copy(task)
-            created_tasks.append(created_task)
-        return new_tasks, created_tasks
-
-    def _process_deleted_tasks(self, ids_to_remove, all_tasks, proxy):
-        tasks_to_remove = self._task_ids_to_tasks(ids_to_remove, all_tasks)
-        for task in tasks_to_remove:
-            proxy.delete_task(task)
-
-    def _list_to_dict(self, source_list, fun1, fun2):
-        list1 = map(lambda elem: getattr(elem, fun1), source_list)
-        list2 = map(lambda elem: getattr(elem, fun2), source_list)
-        return dict(zip(list1, list2))
-
-    def _link_same_title(self, local_list, remote_list):
-        self.taskpairs = []
-        local_title_to_task = self._list_to_dict(local_list, \
-                                                "title", "self")
-        remote_title_to_task = self._list_to_dict(remote_list, \
-                                                 "title", "self")
-        local_titles = map(lambda t: t.title, local_list)
-        remote_titles = map(lambda t: t.title, remote_list)
-        common_titles = set(local_titles).intersection(set(remote_titles))
-        for title in common_titles:
-            self.taskpairs.append(TaskPair( \
-                                local_task = local_title_to_task[title],
-                                remote_task = remote_title_to_task[title]))

=== added file 'data/icons/hicolor/scalable/apps/backend_evolution.png'
Binary files data/icons/hicolor/scalable/apps/backend_evolution.png	1970-01-01 00:00:00 +0000 and data/icons/hicolor/scalable/apps/backend_evolution.png	2010-08-26 23:46:43 +0000 differ

Follow ups