← Back to team overview

gtg-user team mailing list archive

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

 

This is the first time I've done something like this. How do I initiate this
for testing in my GTG?

-- Tina


On Thu, Aug 26, 2010 at 18:46, Luca Invernizzi <invernizzi.l@xxxxxxxxx>wrote:

> 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
>
> _______________________________________________
> Mailing list: https://launchpad.net/~gtg-user
> Post to     : gtg-user@xxxxxxxxxxxxxxxxxxx
> Unsubscribe : https://launchpad.net/~gtg-user
> More help   : https://help.launchpad.net/ListHelp
>
>

-- 
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.



References