GTG

Merge lp:~gtg-user/gtg/evolution-backend into lp:~gtg/gtg/old-trunk

Proposed by Luca Invernizzi
Status: Merged
Merged at revision: 880
Proposed branch: lp:~gtg-user/gtg/evolution-backend
Merge into: lp:~gtg/gtg/old-trunk
Diff against target: 1185 lines (+420/-707)
11 files modified
GTG/backends/backend_evolution.py (+420/-0)
GTG/plugins/evolution-sync.gtg-plugin (+0/-9)
GTG/plugins/evolution_sync/__init__.py (+0/-20)
GTG/plugins/evolution_sync/evolutionProxy.py (+0/-73)
GTG/plugins/evolution_sync/evolutionSync.py (+0/-45)
GTG/plugins/evolution_sync/evolutionTask.py (+0/-110)
GTG/plugins/evolution_sync/genericProxy.py (+0/-34)
GTG/plugins/evolution_sync/genericTask.py (+0/-68)
GTG/plugins/evolution_sync/gtgProxy.py (+0/-49)
GTG/plugins/evolution_sync/gtgTask.py (+0/-91)
GTG/plugins/evolution_sync/syncEngine.py (+0/-208)
To merge this branch: bzr merge lp:~gtg-user/gtg/evolution-backend
Reviewer Review Type Date Requested Status
Gtg developers Pending
Review via email: mp+33854@code.launchpad.net

Description of the change

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.

To post a comment you must log in.
Revision history for this message
Martina de la Cruz (tyamar) wrote :
Download full text (49.5 KiB)

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 <email address hidden>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...

Revision history for this message
Luca Invernizzi (invernizzi) wrote :

Hi Tina,
it's easy: you just have to download the code and execute it.
See http://allievi.sssup.it/techblog/?p=569 for the steps, summarized as:

tar czf gtg_backup.tgz .local/share/gtg/ #just in case, backup of your gtg install (if any)
sudo aptitude install bzr
bzr branch lp:~gtg-user/gtg/all_the_backends_merge_requests gtg_backends
cd gtg_backends
./scripts/debug.sh -d #this lauches a debug version of GTG that doesn't touch your own GTG tasks

Then, if you find something that doesn't work, you open a bug and describe the problem. Thanks!

Revision history for this message
Martina de la Cruz (tyamar) wrote :

Thanks!

-- Tina

On Mon, Aug 30, 2010 at 16:03, Luca Invernizzi <email address hidden>wrote:

> Hi Tina,
> it's easy: you just have to download the code and execute it.
> See http://allievi.sssup.it/techblog/?p=569 for the steps, summarized as:
>
> tar czf gtg_backup.tgz .local/share/gtg/ #just in case, backup of your gtg
> install (if any)
> sudo aptitude install bzr
> bzr branch lp:~gtg-user/gtg/all_the_backends_merge_requests gtg_backends
> cd gtg_backends
> ./scripts/debug.sh -d #this lauches a debug version of GTG that doesn't
> touch your own GTG tasks
>
> Then, if you find something that doesn't work, you open a bug and describe
> the problem. Thanks!
> --
> 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.
>
> _______________________________________________
> Mailing list: https://launchpad.net/~gtg-user
> Post to : <email address hidden>
> Unsubscribe : https://launchpad.net/~gtg-user
> More help : https://help.launchpad.net/ListHelp
>

lp:~gtg-user/gtg/evolution-backend updated
878. By Luca Invernizzi

cherrypicking from my development branch

879. By Luca Invernizzi

cherrypicking from my development branch

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'GTG/backends/backend_evolution.py'
2--- GTG/backends/backend_evolution.py 1970-01-01 00:00:00 +0000
3+++ GTG/backends/backend_evolution.py 2010-09-03 23:47:40 +0000
4@@ -0,0 +1,420 @@
5+# -*- coding: utf-8 -*-
6+# -----------------------------------------------------------------------------
7+# Getting Things Gnome! - a personal organizer for the GNOME desktop
8+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
9+#
10+# This program is free software: you can redistribute it and/or modify it under
11+# the terms of the GNU General Public License as published by the Free Software
12+# Foundation, either version 3 of the License, or (at your option) any later
13+# version.
14+#
15+# This program is distributed in the hope that it will be useful, but WITHOUT
16+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
18+# details.
19+#
20+# You should have received a copy of the GNU General Public License along with
21+# this program. If not, see <http://www.gnu.org/licenses/>.
22+# -----------------------------------------------------------------------------
23+
24+'''
25+Backend for storing/loading tasks in Evolution Tasks
26+'''
27+
28+import os
29+import time
30+import uuid
31+import datetime
32+import evolution
33+from dateutil.tz import tzutc, tzlocal
34+
35+from GTG.backends.genericbackend import GenericBackend
36+from GTG import _
37+from GTG.backends.syncengine import SyncEngine, SyncMeme
38+from GTG.backends.periodicimportbackend import PeriodicImportBackend
39+from GTG.tools.dates import RealDate, NoDate
40+from GTG.core.task import Task
41+from GTG.tools.interruptible import interruptible
42+from GTG.tools.logger import Log
43+from GTG.tools.tags import extract_tags_from_text
44+
45+#Dictionaries to translate GTG tasks in Evolution ones
46+_GTG_TO_EVOLUTION_STATUS = \
47+ {Task.STA_ACTIVE: evolution.ecal.ICAL_STATUS_CONFIRMED,
48+ Task.STA_DONE: evolution.ecal.ICAL_STATUS_COMPLETED,
49+ Task.STA_DISMISSED: evolution.ecal.ICAL_STATUS_CANCELLED}
50+
51+_EVOLUTION_TO_GTG_STATUS = \
52+ {evolution.ecal.ICAL_STATUS_CONFIRMED: Task.STA_ACTIVE,
53+ evolution.ecal.ICAL_STATUS_DRAFT: Task.STA_ACTIVE,
54+ evolution.ecal.ICAL_STATUS_FINAL: Task.STA_ACTIVE,
55+ evolution.ecal.ICAL_STATUS_INPROCESS: Task.STA_ACTIVE,
56+ evolution.ecal.ICAL_STATUS_NEEDSACTION: Task.STA_ACTIVE,
57+ evolution.ecal.ICAL_STATUS_NONE: Task.STA_ACTIVE,
58+ evolution.ecal.ICAL_STATUS_TENTATIVE: Task.STA_ACTIVE,
59+ evolution.ecal.ICAL_STATUS_X: Task.STA_ACTIVE,
60+ evolution.ecal.ICAL_STATUS_COMPLETED: Task.STA_DONE,
61+ evolution.ecal.ICAL_STATUS_CANCELLED: Task.STA_DISMISSED}
62+
63+
64+
65+class Backend(PeriodicImportBackend):
66+ '''
67+ Evolution backend
68+ '''
69+
70+
71+ _general_description = { \
72+ GenericBackend.BACKEND_NAME: "backend_evolution", \
73+ GenericBackend.BACKEND_HUMAN_NAME: _("Evolution tasks"), \
74+ GenericBackend.BACKEND_AUTHORS: ["Luca Invernizzi"], \
75+ GenericBackend.BACKEND_TYPE: GenericBackend.TYPE_READWRITE, \
76+ GenericBackend.BACKEND_DESCRIPTION: \
77+ _("Lets you synchronize your GTG tasks with Evolution tasks"),\
78+ }
79+
80+ _static_parameters = { \
81+ "period": { \
82+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT, \
83+ GenericBackend.PARAM_DEFAULT_VALUE: 10, },
84+ "is-first-run": { \
85+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL, \
86+ GenericBackend.PARAM_DEFAULT_VALUE: True, },
87+ }
88+
89+###############################################################################
90+### Backend standard methods ##################################################
91+###############################################################################
92+
93+ def __init__(self, parameters):
94+ '''
95+ See GenericBackend for an explanation of this function.
96+ Loads the saved state of the sync, if any
97+ '''
98+ super(Backend, self).__init__(parameters)
99+ #loading the saved state of the synchronization, if any
100+ self.sync_engine_path = os.path.join('backends/evolution/', \
101+ "sync_engine-" + self.get_id())
102+ self.sync_engine = self._load_pickled_file(self.sync_engine_path, \
103+ SyncEngine())
104+ #sets up the connection to the evolution api
105+ task_personal = evolution.ecal.list_task_sources()[0][1]
106+ self._evolution_tasks = evolution.ecal.open_calendar_source( \
107+ task_personal,
108+ evolution.ecal.CAL_SOURCE_TYPE_TODO)
109+
110+ def do_periodic_import(self):
111+ """
112+ See PeriodicImportBackend for an explanation of this function.
113+ """
114+ stored_evolution_task_ids = set(self.sync_engine.get_all_remote())
115+ current_evolution_task_ids = set([task.get_uid() \
116+ for task in self._evolution_tasks.get_all_objects()])
117+ #If it's the very first time the backend is run, it's possible that the
118+ # user already synced his tasks in some way (but we don't know that).
119+ # Therefore, we attempt to induce those tasks relationships matching the
120+ # titles.
121+ if self._parameters["is-first-run"]:
122+ gtg_titles_dic = {}
123+ for tid in self.datastore.get_all_tasks():
124+ gtg_task = self.datastore.get_task(tid)
125+ if not self._gtg_task_is_syncable_per_attached_tags(gtg_task):
126+ continue
127+ gtg_title = gtg_task.get_title()
128+ if gtg_titles_dic.has_key(gtg_title):
129+ gtg_titles_dic[gtg_task.get_title()].append(tid)
130+ else:
131+ gtg_titles_dic[gtg_task.get_title()] = [tid]
132+ for evo_task_id in current_evolution_task_ids:
133+ evo_task = self._evo_get_task(evo_task_id)
134+ try:
135+ tids = gtg_titles_dic[evo_task.get_summary()]
136+ #we remove the tid, so that it can't be linked to two
137+ # different evolution tasks
138+ tid = tids.pop()
139+ gtg_task = self.datastore.get_task(tid)
140+ meme = SyncMeme(gtg_task.get_modified(),
141+ self._evo_get_modified(evo_task),
142+ "GTG")
143+ self.sync_engine.record_relationship( \
144+ local_id = tid,
145+ remote_id = evo_task.get_uid(),
146+ meme = meme)
147+ except KeyError:
148+ pass
149+ #a first run has been completed successfully
150+ self._parameters["is-first-run"] = False
151+
152+ for evo_task_id in current_evolution_task_ids:
153+ #Adding and updating
154+ self.cancellation_point()
155+ self._process_evo_task(evo_task_id)
156+
157+ for evo_task_id in stored_evolution_task_ids.difference(\
158+ current_evolution_task_ids):
159+ #Removing the old ones
160+ self.cancellation_point()
161+ tid = self.sync_engine.get_local_id(evo_task_id)
162+ self.datastore.request_task_deletion(tid)
163+ try:
164+ self.sync_engine.break_relationship(remote_id = \
165+ evo_task_id)
166+ except KeyError:
167+ pass
168+
169+ def save_state(self):
170+ '''
171+ See GenericBackend for an explanation of this function.
172+ '''
173+ self._store_pickled_file(self.sync_engine_path, self.sync_engine)
174+
175+###############################################################################
176+### Process tasks #############################################################
177+###############################################################################
178+
179+ @interruptible
180+ def remove_task(self, tid):
181+ '''
182+ See GenericBackend for an explanation of this function.
183+ '''
184+ try:
185+ evo_task_id = self.sync_engine.get_remote_id(tid)
186+ self._delete_evolution_task(self._evo_get_task(evo_task_id))
187+ except KeyError:
188+ pass
189+ try:
190+ self.sync_engine.break_relationship(local_id = tid)
191+ except:
192+ pass
193+
194+ @interruptible
195+ def set_task(self, task):
196+ '''
197+ See GenericBackend for an explanation of this function.
198+ '''
199+ tid = task.get_id()
200+ is_syncable = self._gtg_task_is_syncable_per_attached_tags(task)
201+ action, evo_task_id = self.sync_engine.analyze_local_id(
202+ tid,
203+ self.datastore.has_task,
204+ self._evo_has_task,
205+ is_syncable)
206+ Log.debug('GTG->Evo set task (%s, %s)' % (action, is_syncable))
207+
208+ if action == None:
209+ return
210+
211+ if action == SyncEngine.ADD:
212+ evo_task = evolution.ecal.ECalComponent( \
213+ ical = evolution.ecal.CAL_COMPONENT_TODO)
214+ with self.datastore.get_backend_mutex():
215+ self._evolution_tasks.add_object(evo_task)
216+ self._populate_evo_task(task, evo_task)
217+ meme = SyncMeme(task.get_modified(),
218+ self._evo_get_modified(evo_task),
219+ "GTG")
220+ self.sync_engine.record_relationship( \
221+ local_id = tid, remote_id = evo_task.get_uid(), meme = meme)
222+
223+ elif action == SyncEngine.UPDATE:
224+ with self.datastore.get_backend_mutex():
225+ evo_task = self._evo_get_task(evo_task_id)
226+ meme = self.sync_engine.get_meme_from_local_id(task.get_id())
227+ newest = meme.which_is_newest(task.get_modified(),
228+ self._evo_get_modified(evo_task))
229+ if newest == "local":
230+ self._populate_evo_task(task, evo_task)
231+ meme.set_remote_last_modified( \
232+ self._evo_get_modified(evo_task))
233+ meme.set_local_last_modified(task.get_modified())
234+ else:
235+ #we skip saving the state
236+ return
237+
238+ elif action == SyncEngine.REMOVE:
239+ self.datastore.request_task_deletion(tid)
240+ try:
241+ self.sync_engine.break_relationship(local_id = tid)
242+ except KeyError:
243+ pass
244+
245+ elif action == SyncEngine.LOST_SYNCABILITY:
246+ evo_task = self._evo_get_task(evo_task_id)
247+ self._exec_lost_syncability(tid, evo_task)
248+ self.save_state()
249+
250+ def _process_evo_task(self, evo_task_id):
251+ '''
252+ Takes an evolution task id and carries out the necessary operations to
253+ refresh the sync state
254+ '''
255+ self.cancellation_point()
256+ evo_task = self._evo_get_task(evo_task_id)
257+ is_syncable = self._evo_task_is_syncable(evo_task)
258+ action, tid = self.sync_engine.analyze_remote_id( \
259+ evo_task_id,
260+ self.datastore.has_task,
261+ self._evo_has_task,
262+ is_syncable)
263+ Log.debug('GTG<-Evo set task (%s, %s)' % (action, is_syncable))
264+
265+ if action == SyncEngine.ADD:
266+ with self.datastore.get_backend_mutex():
267+ tid = str(uuid.uuid4())
268+ task = self.datastore.task_factory(tid)
269+ self._populate_task(task, evo_task)
270+ meme = SyncMeme(task.get_modified(),
271+ self._evo_get_modified(evo_task),
272+ "GTG")
273+ self.sync_engine.record_relationship(local_id = tid,
274+ remote_id = evo_task_id,
275+ meme = meme)
276+ self.datastore.push_task(task)
277+
278+ elif action == SyncEngine.UPDATE:
279+ with self.datastore.get_backend_mutex():
280+ task = self.datastore.get_task(tid)
281+ meme = self.sync_engine.get_meme_from_remote_id(evo_task_id)
282+ newest = meme.which_is_newest(task.get_modified(),
283+ self._evo_get_modified(evo_task))
284+ if newest == "remote":
285+ self._populate_task(task, evo_task)
286+ meme.set_remote_last_modified( \
287+ self._evo_get_modified(evo_task))
288+ meme.set_local_last_modified(task.get_modified())
289+
290+ elif action == SyncEngine.REMOVE:
291+ return
292+ try:
293+ evo_task = self._evo_get_task(evo_task_id)
294+ self._delete_evolution_task(evo_task)
295+ self.sync_engine.break_relationship(remote_id = evo_task)
296+ except KeyError:
297+ pass
298+
299+ elif action == SyncEngine.LOST_SYNCABILITY:
300+ self._exec_lost_syncability(tid, evo_task)
301+ self.save_state()
302+
303+###############################################################################
304+### Helper methods ############################################################
305+###############################################################################
306+
307+ def _evo_has_task(self, evo_task_id):
308+ '''Returns true if Evolution has that task'''
309+ return bool(self._evo_get_task(evo_task_id))
310+
311+ def _evo_get_task(self, evo_task_id):
312+ '''Returns an Evolution task, given its uid'''
313+ return self._evolution_tasks.get_object(evo_task_id, "")
314+
315+ def _evo_get_modified(self, evo_task):
316+ '''Returns the modified time of an Evolution task'''
317+ return datetime.datetime.fromtimestamp(evo_task.get_modified())
318+
319+ def _delete_evolution_task(self, evo_task):
320+ '''Deletes an Evolution task, given the task object'''
321+ self._evolution_tasks.remove_object(evo_task)
322+ self._evolution_tasks.update_object(evo_task)
323+
324+ def _populate_task(self, task, evo_task):
325+ '''
326+ Updates the attributes of a GTG task copying the ones of an Evolution
327+ task
328+ '''
329+ task.set_title(evo_task.get_summary())
330+ text = evo_task.get_description()
331+ if text == None:
332+ text = ""
333+ task.set_text(text)
334+ due_date_timestamp = evo_task.get_due()
335+ if isinstance(due_date_timestamp, (int, float)):
336+ due_date = self.__date_from_evo_to_gtg(due_date_timestamp)
337+ else:
338+ due_date = NoDate()
339+ task.set_due_date(due_date)
340+ status = evo_task.get_status()
341+ if task.get_status() != _EVOLUTION_TO_GTG_STATUS[status]:
342+ task.set_status(_EVOLUTION_TO_GTG_STATUS[status])
343+ task.set_only_these_tags(extract_tags_from_text(text))
344+
345+ def _populate_evo_task(self, task, evo_task):
346+ evo_task.set_summary(task.get_title())
347+ text = task.get_excerpt(strip_tags = True, strip_subtasks = True)
348+ if evo_task.get_description() != text:
349+ evo_task.set_description(text)
350+ due_date = task.get_due_date()
351+ if isinstance(due_date, NoDate):
352+ evo_task.set_due(None)
353+ else:
354+ evo_task.set_due(self.__date_from_gtg_to_evo(due_date))
355+ status = task.get_status()
356+ if _EVOLUTION_TO_GTG_STATUS[evo_task.get_status()] != status:
357+ evo_task.set_status(_GTG_TO_EVOLUTION_STATUS[status])
358+ #this calls are sometime ignored by evolution. Doing it twice
359+ # is a hackish way to solve the problem. (TODO: send bug report)
360+ self._evolution_tasks.update_object(evo_task)
361+ self._evolution_tasks.update_object(evo_task)
362+
363+ def _exec_lost_syncability(self, tid, evo_task):
364+ '''
365+ Executed when a relationship between tasks loses its syncability
366+ property. See SyncEngine for an explanation of that.
367+ This function finds out which object is the original one
368+ and which is the copy, and deletes the copy.
369+ '''
370+ meme = self.sync_engine.get_meme_from_local_id(tid)
371+ self.sync_engine.break_relationship(local_id = tid)
372+ if meme.get_origin() == "GTG":
373+ evo_task = self._evo_get_task(evo_task.get_uid())
374+ self._delete_evolution_task(evo_task)
375+ else:
376+ self.datastore.request_task_deletion(tid)
377+
378+ def _evo_task_is_syncable(self, evo_task):
379+ '''
380+ Returns True if this Evolution task should be synced into GTG tasks.
381+
382+ @param evo_task: an Evolution task
383+ @returns Boolean
384+ '''
385+ attached_tags = set(self.get_attached_tags())
386+ if GenericBackend.ALLTASKS_TAG in attached_tags:
387+ return True
388+ evo_tags = set(extract_tags_from_text(evo_task.get_description()))
389+ return evo_task.is_disjoint(attached_tags)
390+
391+ def __date_from_evo_to_gtg(self, evo_date_timestamp):
392+ """
393+ Converts an evolution date object into the format understood by GTG
394+
395+ @param evo_date: an int, which represent time from epoch in UTC
396+ convention
397+ """
398+ evo_datetime = datetime.datetime.fromtimestamp(evo_date_timestamp)
399+ #See self.__date_from_gtg_to_evo for an explanation
400+ evo_datetime = evo_datetime.replace(tzinfo = tzlocal())
401+ gtg_datetime = evo_datetime.astimezone(tzutc())
402+ #we strip timezone infos, as they're not used or expected in GTG
403+ gtg_datetime.replace(tzinfo = None)
404+ return RealDate(gtg_datetime.date())
405+
406+ def __date_from_gtg_to_evo(self, gtg_date):
407+ """
408+ Converts a datetime.date object into the format understood by Evolution
409+
410+ @param gtg_date: a GTG Date object
411+ """
412+ #GTG thinks in local time, evolution in utc
413+ #to convert date objects between different timezones, we must convert
414+ #them to datetime objects
415+ gtg_datetime = datetime.datetime.combine(gtg_date.to_py_date(), datetime.time(0))
416+ #We don't want to express GTG date into a UTC equivalent. Instead, we
417+ #want the *same* date in GTG and evolution. Therefore, we must not do
418+ #the conversion Local-> UTC (which would point to the same moment in
419+ #time in different conventions), but do the opposite conversion UTC->
420+ #Local (which will refer to different points in time, but to the same
421+ #written date)
422+ gtg_datetime = gtg_datetime.replace(tzinfo = tzutc())
423+ evo_datetime = gtg_datetime.astimezone(tzlocal())
424+ return int(time.mktime(evo_datetime.timetuple()))
425
426=== removed file 'GTG/plugins/evolution-sync.gtg-plugin'
427--- GTG/plugins/evolution-sync.gtg-plugin 2010-07-29 10:36:24 +0000
428+++ GTG/plugins/evolution-sync.gtg-plugin 1970-01-01 00:00:00 +0000
429@@ -1,9 +0,0 @@
430-[GTG Plugin]
431-Module=evolution_sync
432-Name=Evolution
433-Short-description="Plugin for synchronising GTG with Gnome Evolution Tasks."
434-Description="""Plugin for synchronising GTG with Gnome Evolution Tasks"""
435-Authors='Luca Invernizzi <invernizzi.l@gmail.com>'
436-Version=0.1.1
437-Dependencies=evolution,
438-Enabled=False
439
440=== removed directory 'GTG/plugins/evolution_sync'
441=== removed file 'GTG/plugins/evolution_sync/__init__.py'
442--- GTG/plugins/evolution_sync/__init__.py 2010-03-16 02:16:14 +0000
443+++ GTG/plugins/evolution_sync/__init__.py 1970-01-01 00:00:00 +0000
444@@ -1,20 +0,0 @@
445-# -*- coding: utf-8 -*-
446-# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@gmail.com>
447-#
448-# This program is free software: you can redistribute it and/or modify it under
449-# the terms of the GNU General Public License as published by the Free Software
450-# Foundation, either version 3 of the License, or (at your option) any later
451-# version.
452-#
453-# This program is distributed in the hope that it will be useful, but WITHOUT
454-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
455-# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
456-# details.
457-#
458-# You should have received a copy of the GNU General Public License along with
459-# this program. If not, see <http://www.gnu.org/licenses/>.
460-
461-from GTG.plugins.evolution_sync.evolutionSync import EvolutionSync
462-
463-#needed to keep pyflakes quiet
464-if False == True: EvolutionSync()
465
466=== removed file 'GTG/plugins/evolution_sync/evolutionProxy.py'
467--- GTG/plugins/evolution_sync/evolutionProxy.py 2010-03-16 02:16:14 +0000
468+++ GTG/plugins/evolution_sync/evolutionProxy.py 1970-01-01 00:00:00 +0000
469@@ -1,73 +0,0 @@
470-# -*- coding: utf-8 -*-
471-# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@gmail.com>
472-#
473-# This program is free software: you can redistribute it and/or modify it under
474-# the terms of the GNU General Public License as published by the Free Software
475-# Foundation, either version 3 of the License, or (at your option) any later
476-# version.
477-#
478-# This program is distributed in the hope that it will be useful, but WITHOUT
479-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
480-# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
481-# details.
482-#
483-# You should have received a copy of the GNU General Public License along with
484-# this program. If not, see <http://www.gnu.org/licenses/>.
485-
486-import evolution
487-
488-from GTG.core.task import Task
489-from GTG.plugins.evolution_sync.evolutionTask import EvolutionTask
490-from GTG.plugins.evolution_sync.genericProxy import GenericProxy
491-
492-
493-class EvolutionProxy(GenericProxy):
494-
495- __GTG_STATUSES = [Task.STA_ACTIVE,
496- Task.STA_DONE,
497- Task.STA_DISMISSED]
498-
499- __EVO_STATUSES = [evolution.ecal.ICAL_STATUS_CONFIRMED,
500- evolution.ecal.ICAL_STATUS_COMPLETED,
501- evolution.ecal.ICAL_STATUS_CANCELLED]
502-
503- def __init__(self):
504- super(EvolutionProxy, self).__init__()
505-
506- def generateTaskList(self):
507- task_personal = evolution.ecal.list_task_sources()[0][1]
508- self._evolution_tasks = evolution.ecal.open_calendar_source(task_personal,
509- evolution.ecal.CAL_SOURCE_TYPE_TODO)
510- self._gtg_to_evo_status = dict(zip(self.__GTG_STATUSES,
511- self.__EVO_STATUSES))
512- self._evo_to_gtg_status = dict(zip(self.__EVO_STATUSES,
513- self.__GTG_STATUSES))
514- #Need to find a solution for the statuses GTG doesn't expect:
515- for evo_status in [evolution.ecal.ICAL_STATUS_DRAFT,
516- evolution.ecal.ICAL_STATUS_FINAL,
517- evolution.ecal.ICAL_STATUS_INPROCESS,
518- evolution.ecal.ICAL_STATUS_NEEDSACTION,
519- evolution.ecal.ICAL_STATUS_NONE,
520- evolution.ecal.ICAL_STATUS_TENTATIVE,
521- evolution.ecal.ICAL_STATUS_X]:
522- self._evo_to_gtg_status[evo_status] = Task.STA_ACTIVE
523- self._tasks_list = []
524- for task in self._evolution_tasks.get_all_objects():
525- self._tasks_list.append(EvolutionTask(task, self))
526-
527- def create_new_task(self, title):
528- task = evolution.ecal.ECalComponent(ical=evolution.ecal.CAL_COMPONENT_TODO)
529- self._evolution_tasks.add_object(task)
530- new_task = EvolutionTask(task, self)
531- new_task.title = title
532- self._tasks_list.append(new_task)
533- return new_task
534-
535- def delete_task(self, task):
536- evo_task = task.get_evolution_task()
537- self._evolution_tasks.remove_object(evo_task)
538- self._evolution_tasks.update_object(evo_task)
539-
540- def update_task(self, task):
541- evo_task = task.get_evolution_task()
542- self._evolution_tasks.update_object(evo_task)
543
544=== removed file 'GTG/plugins/evolution_sync/evolutionSync.py'
545--- GTG/plugins/evolution_sync/evolutionSync.py 2010-03-16 02:16:14 +0000
546+++ GTG/plugins/evolution_sync/evolutionSync.py 1970-01-01 00:00:00 +0000
547@@ -1,45 +0,0 @@
548-# -*- coding: utf-8 -*-
549-# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@gmail.com>
550-# - Paulo Cabido <paulo.cabido@gmail.com> (example file)
551-#
552-# This program is free software: you can redistribute it and/or modify it under
553-# the terms of the GNU General Public License as published by the Free Software
554-# Foundation, either version 3 of the License, or (at your option) any later
555-# version.
556-#
557-# This program is distributed in the hope that it will be useful, but WITHOUT
558-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
559-# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
560-# details.
561-#
562-# You should have received a copy of the GNU General Public License along with
563-# this program. If not, see <http://www.gnu.org/licenses/>.
564-
565-import gtk
566-from threading import Thread
567-
568-from GTG import _
569-from GTG.plugins.evolution_sync.syncEngine import SyncEngine
570-
571-class EvolutionSync:
572-
573- def __init__(self):
574- #drop down menu
575- self.menu_item = gtk.MenuItem(_("Synchronize with Evolution"))
576- self.menu_item.connect('activate', self.onTesteMenu)
577-
578- def activate(self, plugin_api):
579- self.plugin_api = plugin_api
580- # add a menu item to the menu bar
581- plugin_api.add_menu_item(self.menu_item)
582- self.sync_engine = SyncEngine(self)
583-
584- def deactivate(self, plugin_api):
585- plugin_api.remove_menu_item(self.menu_item)
586-
587- def onTesteMenu(self, widget):
588- self.worker_thread = Thread(target = \
589- self.sync_engine.synchronize).start()
590-
591- def onTaskOpened(self, plugin_api):
592- pass
593
594=== removed file 'GTG/plugins/evolution_sync/evolutionTask.py'
595--- GTG/plugins/evolution_sync/evolutionTask.py 2010-03-16 02:16:14 +0000
596+++ GTG/plugins/evolution_sync/evolutionTask.py 1970-01-01 00:00:00 +0000
597@@ -1,110 +0,0 @@
598-# -*- coding: utf-8 -*-
599-# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@gmail.com>
600-#
601-# This program is free software: you can redistribute it and/or modify it under
602-# the terms of the GNU General Public License as published by the Free Software
603-# Foundation, either version 3 of the License, or (at your option) any later
604-# version.
605-#
606-# This program is distributed in the hope that it will be useful, but WITHOUT
607-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
608-# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
609-# details.
610-#
611-# You should have received a copy of the GNU General Public License along with
612-# this program. If not, see <http://www.gnu.org/licenses/>.
613-
614-import time
615-import datetime
616-
617-from GTG.plugins.evolution_sync.genericTask import GenericTask
618-
619-
620-class EvolutionTask(GenericTask):
621-
622- def __init__(self, evo_task, evolution_proxy):
623- super(EvolutionTask, self).__init__(evolution_proxy)
624- self._evo_task = evo_task
625-
626- def _get_title(self):
627- return self._evo_task.get_summary()
628-
629- def _set_title(self, title):
630- self._evo_task.set_summary(title)
631- self.get_proxy().update_task(self)
632-
633- def _get_id(self):
634- return self._evo_task.get_uid()
635-
636- def _get_tags(self):
637- #We could use Evolution's "Categories" as tags
638- raise NotImplementedError()
639-
640- def _set_tags(self, tags):
641- raise NotImplementedError()
642- self.get_proxy().update_task(self)
643-
644- def _get_text(self):
645- desc = self._evo_task.get_description()
646- if desc == None:
647- return ""
648- return desc
649-
650- def _set_text(self, text):
651- self._evo_task.set_description(text)
652- self.get_proxy().update_task(self)
653-
654- def _set_status(self, status):
655- #Since Evolution's statuses are fare more than GTG's,
656- # we need to check that the current status is not one of the various
657- # statuses translated in the same gtg status, passed by argument.
658- # This way, we avoid to force a status change when it's not needed
659- # (and not wanted)
660- current_status_in_gtg_terms = self.get_proxy()._evo_to_gtg_status[\
661- self._evo_task.get_status()]
662- if current_status_in_gtg_terms != status:
663- new_evo_status = self.get_proxy()._gtg_to_evo_status[status]
664- self._evo_task.set_status(new_evo_status)
665- self.get_proxy().update_task(self)
666-
667- def _get_status(self):
668- status = self._evo_task.get_status()
669- return self.get_proxy()._evo_to_gtg_status[status]
670-
671- def _get_due_date(self):
672- due = self._evo_task.get_due()
673- if isinstance(due, (int, float)):
674- return self.__time_evo_to_date(due)
675-
676- def _set_due_date(self, due):
677- if due == None:
678- #TODO: I haven't find a way to reset the due date
679- # We could copy the task, but that would lose all the attributes
680- # currently not supported by the library used (and they're a lot)
681- pass
682- else:
683- self._evo_task.set_due(self.__time_date_to_evo(due))
684- self.get_proxy().update_task(self)
685-
686- def _get_modified(self):
687- return self.__time_evo_to_datetime(self._evo_task.get_modified())
688-
689- def __time_evo_to_date(self, timestamp):
690- return datetime.datetime.fromtimestamp(timestamp + time.timezone).date()
691-
692- def __time_evo_to_datetime(self, timestamp):
693- return datetime.datetime.fromtimestamp(timestamp)
694-
695- def __time_datetime_to_evo(self, timeobject):
696- return int(time.mktime(timeobject.timetuple()))
697-
698- def __time_date_to_evo(self, timeobject):
699- #NOTE: need to substract the timezone to avoid the "one day before bug
700- # (at the airport => no internet now, need to fill bug number in)
701- #Explanation: gtg date format is converted to datetime in date/00:00 in
702- # local time, and time.mktime considers that when converting to UNIX
703- # time. Evolution, however, doesn't convert back to local time.
704- return self.__time_datetime_to_evo(timeobject) - time.timezone
705-
706- def get_evolution_task(self):
707- return self._evo_task
708
709=== removed file 'GTG/plugins/evolution_sync/genericProxy.py'
710--- GTG/plugins/evolution_sync/genericProxy.py 2010-01-31 10:16:17 +0000
711+++ GTG/plugins/evolution_sync/genericProxy.py 1970-01-01 00:00:00 +0000
712@@ -1,34 +0,0 @@
713-# -*- coding: utf-8 -*-
714-# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@gmail.com>
715-#
716-# This program is free software: you can redistribute it and/or modify it under
717-# the terms of the GNU General Public License as published by the Free Software
718-# Foundation, either version 3 of the License, or (at your option) any later
719-# version.
720-#
721-# This program is distributed in the hope that it will be useful, but WITHOUT
722-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
723-# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
724-# details.
725-#
726-# You should have received a copy of the GNU General Public License along with
727-# this program. If not, see <http://www.gnu.org/licenses/>.
728-
729-class GenericProxy(object):
730-
731- def __init__(self):
732- super(GenericProxy, self).__init__()
733- self._tasks_list = []
734-
735- def get_tasks_list(self):
736- return self._tasks_list
737-
738- def generateTaskList(self):
739- raise NotImplementedError()
740-
741- def create_new_task(self, title):
742- raise NotImplementedError()
743-
744- def delete_task(self, task):
745- raise NotImplementedError()
746-
747
748=== removed file 'GTG/plugins/evolution_sync/genericTask.py'
749--- GTG/plugins/evolution_sync/genericTask.py 2010-01-31 10:16:17 +0000
750+++ GTG/plugins/evolution_sync/genericTask.py 1970-01-01 00:00:00 +0000
751@@ -1,68 +0,0 @@
752-# -*- coding: utf-8 -*-
753-# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@gmail.com>
754-#
755-# This program is free software: you can redistribute it and/or modify it under
756-# the terms of the GNU General Public License as published by the Free Software
757-# Foundation, either version 3 of the License, or (at your option) any later
758-# version.
759-#
760-# This program is distributed in the hope that it will be useful, but WITHOUT
761-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
762-# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
763-# details.
764-#
765-# You should have received a copy of the GNU General Public License along with
766-# this program. If not, see <http://www.gnu.org/licenses/>.
767-
768-
769-class GenericTask(object):
770- """GenericTask is the abstract interface that represents a generic task."""
771-
772- def __init__(self, proxy):
773- self.__proxy = proxy
774-
775- def __str__(self):
776- return "Task " + self.title + "(" + self.id + ")"
777-
778- def toString(self):
779- return "Task:\n" + \
780- "\t - Title: " + self.title + "\n" + \
781- "\t - ID: " + self.id + "\n" + \
782- "\t - Modified: " + str(self.modified) + "\n" + \
783- "\t - Due: " + str(self.due_date) + "\n"
784-
785- def copy(self, task):
786- #Minimizing the number of actions will allow a faster RTM plugin
787- # (where GET is fast, but SET is slow)
788- if self.title != task.title:
789- self.title = task.title
790- if self.text != task.text:
791- self.text = task.text
792- if self.status != task.status:
793- self.status = task.status
794- if self.due_date != task.due_date:
795- self.due_date = task.due_date
796-
797- def get_proxy(self):
798- return self.__proxy
799-
800- title = property(lambda self: self._get_title(),
801- lambda self, arg: self._set_title(arg))
802-
803- id = property(lambda self: self._get_id())
804-
805- text = property(lambda self: self._get_text(),
806- lambda self, arg: self._set_text(arg))
807-
808- status = property(lambda self: self._get_status(),
809- lambda self, arg: self._set_status(arg))
810-
811- modified = property(lambda self: self._get_modified())
812-
813- due_date = property(lambda self: self._get_due_date(),
814- lambda self, arg: self._set_due_date(arg))
815-
816- tags = property(lambda self: self._get_tags(),
817- lambda self, arg: self._set_tags(arg))
818-
819- self = property(lambda self: self)
820
821=== removed file 'GTG/plugins/evolution_sync/gtgProxy.py'
822--- GTG/plugins/evolution_sync/gtgProxy.py 2010-03-16 02:16:14 +0000
823+++ GTG/plugins/evolution_sync/gtgProxy.py 1970-01-01 00:00:00 +0000
824@@ -1,49 +0,0 @@
825-# -*- coding: utf-8 -*-
826-# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@gmail.com>
827-#
828-# This program is free software: you can redistribute it and/or modify it under
829-# the terms of the GNU General Public License as published by the Free Software
830-# Foundation, either version 3 of the License, or (at your option) any later
831-# version.
832-#
833-# This program is distributed in the hope that it will be useful, but WITHOUT
834-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
835-# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
836-# details.
837-#
838-# You should have received a copy of the GNU General Public License along with
839-# this program. If not, see <http://www.gnu.org/licenses/>.
840-
841-from GTG.core.task import Task
842-from GTG.plugins.evolution_sync.gtgTask import GtgTask
843-from GTG.plugins.evolution_sync.genericProxy import GenericProxy
844-
845-
846-class GtgProxy(GenericProxy):
847-
848- def __init__(self, plugin_api):
849- super(GtgProxy, self).__init__()
850- self.plugin_api = plugin_api
851- self.requester = self.plugin_api.get_requester()
852-
853- def generateTaskList(self):
854- self._tasks_list = []
855- requester = self.plugin_api.get_requester()
856- statuses = [Task.STA_ACTIVE, Task.STA_DISMISSED, Task.STA_DONE]
857- tasks = map(self.plugin_api.get_task, \
858- requester.get_tasks_list(status = statuses))
859- map(lambda task: self._tasks_list.append(GtgTask(task, \
860- self.plugin_api, self)), tasks)
861-
862- def create_new_task(self, title, never_seen_before = True):
863- new_gtg_local_task = self.requester.new_task(newtask=never_seen_before)
864- new_task = GtgTask(new_gtg_local_task, self.plugin_api, self)
865- new_task.title = title
866- self._tasks_list.append(new_task)
867- return new_task
868-
869- def delete_task(self, task):
870- #NOTE: delete_task wants the internal gtg id, not the uuid
871- id = task.get_gtg_task().get_id()
872- self.requester.delete_task(id)
873-
874
875=== removed file 'GTG/plugins/evolution_sync/gtgTask.py'
876--- GTG/plugins/evolution_sync/gtgTask.py 2010-03-16 02:16:14 +0000
877+++ GTG/plugins/evolution_sync/gtgTask.py 1970-01-01 00:00:00 +0000
878@@ -1,91 +0,0 @@
879-# -*- coding: utf-8 -*-
880-# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@gmail.com>
881-#
882-# This program is free software: you can redistribute it and/or modify it under
883-# the terms of the GNU General Public License as published by the Free Software
884-# Foundation, either version 3 of the License, or (at your option) any later
885-# version.
886-#
887-# This program is distributed in the hope that it will be useful, but WITHOUT
888-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
889-# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
890-# details.
891-#
892-# You should have received a copy of the GNU General Public License along with
893-# this program. If not, see <http://www.gnu.org/licenses/>.
894-
895-import datetime
896-
897-from GTG.tools.dates import NoDate, RealDate
898-from GTG.plugins.evolution_sync.genericTask import GenericTask
899-
900-class GtgTask(GenericTask):
901-
902- def __init__(self, gtg_task, plugin_api, gtg_proxy):
903- super(GtgTask, self).__init__(gtg_proxy)
904- self._gtg_task = gtg_task
905- self.plugin_api = plugin_api
906-
907- def _get_title(self):
908- return self._gtg_task.get_title()
909-
910- def _set_title(self, title):
911- self._gtg_task.set_title(title)
912-
913- def _get_id(self):
914- return self._gtg_task.get_uuid()
915-
916- def _get_tags(self):
917- raise NotImplemented()
918- return self._gtg_task.get_tags()
919-
920- def _set_tags(self, tags):
921- raise NotImplemented()
922- #NOTE: isn't there a better mode than removing all tags?
923- # need to add function in GTG/core/task.py
924- old_tags = self.tags
925- for tag in old_tags:
926- self._gtg_task.remove_tag(tag)
927- map(lambda tag: self._gtg_task.add_tag('@'+tag), tags)
928-
929- def _get_text(self):
930- return self._gtg_task.get_excerpt()
931-
932- def _set_text(self, text):
933- self._gtg_task.set_text(text)
934-
935- def _set_status(self, status):
936- self._gtg_task.set_status(status)
937-
938- def _get_status(self):
939- return self._gtg_task.get_status()
940-
941- def _get_due_date(self):
942- due_date = self._gtg_task.get_due_date()
943- if due_date == NoDate():
944- return None
945- return due_date.to_py_date()
946-
947- def _set_due_date(self, due):
948- if due == None:
949- gtg_due = NoDate()
950- else:
951- gtg_due = RealDate(due)
952- self._gtg_task.set_due_date(gtg_due)
953-
954- def _get_modified(self):
955- modified = self._gtg_task.get_modified()
956- if modified == None or modified == "":
957- return None
958- return self.__time_gtg_to_datetime(modified)
959-
960- def get_gtg_task(self):
961- return self._gtg_task
962-
963- def __time_gtg_to_datetime(self, string):
964- #FIXME: need to handle time with TIMEZONES!
965- string = string.split('.')[0].split('Z')[0]
966- if string.find('T') == -1:
967- return datetime.datetime.strptime(string.split(".")[0], "%Y-%m-%d")
968- return datetime.datetime.strptime(string.split(".")[0], \
969- "%Y-%m-%dT%H:%M:%S")
970
971=== removed file 'GTG/plugins/evolution_sync/syncEngine.py'
972--- GTG/plugins/evolution_sync/syncEngine.py 2010-03-16 02:16:14 +0000
973+++ GTG/plugins/evolution_sync/syncEngine.py 1970-01-01 00:00:00 +0000
974@@ -1,208 +0,0 @@
975-# -*- coding: utf-8 -*-
976-# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@gmail.com>
977-#
978-# This program is free software: you can redistribute it and/or modify it under
979-# the terms of the GNU General Public License as published by the Free Software
980-# Foundation, either version 3 of the License, or (at your option) any later
981-# version.
982-#
983-# This program is distributed in the hope that it will be useful, but WITHOUT
984-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
985-# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
986-# details.
987-#
988-# You should have received a copy of the GNU General Public License along with
989-# this program. If not, see <http://www.gnu.org/licenses/>.
990-
991-import datetime
992-from xdg.BaseDirectory import xdg_data_home
993-
994-from GTG.plugins.evolution_sync.gtgProxy import GtgProxy
995-from GTG.plugins.evolution_sync.evolutionProxy import EvolutionProxy
996-
997-class TaskPair(object):
998- def __init__(self, \
999- local_task,
1000- remote_task):
1001- self.local_id = local_task.id
1002- self.remote_id = remote_task.id
1003- self.__remote_synced_until = remote_task.modified
1004- self.local_synced_until = local_task.modified
1005- self.__remote_first_seen = datetime.datetime.now()
1006-
1007- self = property(lambda self: self)
1008-
1009- remote_synced_until = property (\
1010- lambda self: self.__get_remote_synced_until(),\
1011- lambda self, t: self.__set_remote_synced_until(t))
1012-
1013- def __get_remote_synced_until(self):
1014- if self.__remote_synced_until > self.__remote_first_seen:
1015- return self.__remote_synced_until
1016- else:
1017- return self.__remote_first_seen
1018-
1019- def __set_remote_synced_until(self, datetime_object):
1020- self.__remote_synced_until = datetime_object
1021-
1022-
1023-class SyncEngine(object):
1024-
1025- def __init__(self, this_plugin):
1026- super(SyncEngine, self).__init__()
1027- self.this_plugin = this_plugin
1028- self.plugin_api = this_plugin.plugin_api
1029- self.local_proxy = GtgProxy(self.this_plugin.plugin_api)
1030- self.remote_proxy = EvolutionProxy()
1031-
1032- def synchronize(self):
1033- self.synchronizeWorker()
1034-
1035- def synchronizeWorker(self):
1036- #Generate the two list of tasks from the local and remote task source
1037- self.remote_proxy.generateTaskList()
1038- self.local_proxy.generateTaskList()
1039- remote_tasks = self.remote_proxy.get_tasks_list()
1040- local_tasks = self.local_proxy.get_tasks_list()
1041-
1042- #Load the list of known links between tasks (tasks that are the same
1043- # one saved in different task sources)
1044- self.taskpairs = self.plugin_api.load_configuration_object( \
1045- "evolution-sync", \
1046- "taskpairs", \
1047- xdg_data_home)
1048- if self.taskpairs == None:
1049- #We have no previous knowledge of links, this must be the first
1050- #attempt to synchronize. So, we try to infer some by
1051- # linking tasks with the same title
1052- self._link_same_title(local_tasks, remote_tasks)
1053-
1054- #We'll use some sets to see what is new and what was deleted
1055- old_local_ids = set(map(lambda tp: tp.local_id, self.taskpairs))
1056- old_remote_ids = set(map(lambda tp: tp.remote_id, self.taskpairs))
1057- current_local_ids = set(map(lambda t: t.id, local_tasks))
1058- current_remote_ids = set(map(lambda t: t.id, remote_tasks))
1059- #Tasks that have been added
1060- new_local_ids = current_local_ids.difference(old_local_ids)
1061- new_remote_ids = current_remote_ids.difference(old_remote_ids)
1062- #Tasks that have been deleted
1063- deleted_local_ids = old_local_ids.difference(current_local_ids)
1064- deleted_remote_ids = old_remote_ids.difference(current_remote_ids)
1065- #Local tasks with the remote task still existing (could need to be
1066- #updated)
1067- updatable_local_ids = current_local_ids.difference(new_local_ids)
1068-
1069- #Add tasks to the remote proxy
1070- [new_local_tasks, new_remote_tasks] = self._process_new_tasks(\
1071- new_local_ids,\
1072- local_tasks, \
1073- self.remote_proxy)
1074- self._append_to_taskpairs(new_local_tasks, new_remote_tasks)
1075- #Add tasks to the local proxy
1076- [new_remote_tasks, new_local_tasks] = self._process_new_tasks(\
1077- new_remote_ids,\
1078- remote_tasks,\
1079- self.local_proxy)
1080- self._append_to_taskpairs(new_local_tasks, new_remote_tasks)
1081-
1082- #Delete tasks from the remote proxy
1083- taskpairs_deleted = filter(lambda tp: tp.local_id in deleted_local_ids,\
1084- self.taskpairs)
1085- remote_ids_to_delete = map( lambda tp: tp.remote_id, taskpairs_deleted)
1086- self._process_deleted_tasks(remote_ids_to_delete, remote_tasks,\
1087- self.remote_proxy)
1088- map(lambda tp: self.taskpairs.remove(tp), taskpairs_deleted)
1089-
1090- #Delete tasks from the local proxy
1091- taskpairs_deleted = filter(lambda tp: tp.remote_id in deleted_remote_ids,\
1092- self.taskpairs)
1093- local_ids_to_delete = map( lambda tp: tp.local_id, taskpairs_deleted)
1094- self._process_deleted_tasks(local_ids_to_delete, local_tasks, self.local_proxy)
1095- map(lambda tp: self.taskpairs.remove(tp), taskpairs_deleted)
1096-
1097- #Update tasks
1098- local_to_taskpair = self._list_to_dict(self.taskpairs, \
1099- "local_id", \
1100- "self")
1101- local_id_to_task = self._list_to_dict(local_tasks, \
1102- "id", \
1103- "self")
1104- remote_id_to_task = self._list_to_dict(remote_tasks, \
1105- "id", \
1106- "self")
1107- for local_id in updatable_local_ids:
1108- taskpair = local_to_taskpair[local_id]
1109- local_task = local_id_to_task[local_id]
1110- remote_task = remote_id_to_task[taskpair.remote_id]
1111- local_was_updated = local_task.modified > \
1112- taskpair.local_synced_until
1113- remote_was_updated = remote_task.modified > \
1114- taskpair.remote_synced_until
1115-
1116- if local_was_updated and remote_was_updated:
1117- if local_task.modified > remote_task.modified:
1118- remote_task.copy(local_task)
1119- else:
1120- #If the update time is the same one, we have to
1121- # arbitrary decide which gets copied
1122- local_task.copy(remote_task)
1123- elif local_was_updated:
1124- remote_task.copy(local_task)
1125- elif remote_was_updated:
1126- local_task.copy(remote_task)
1127-
1128- taskpair.remote_synced_until = remote_task.modified
1129- taskpair.local_synced_until = local_task.modified
1130-
1131- #Lastly, save the list of known links
1132- self.plugin_api.save_configuration_object( \
1133- "evolution-sync", \
1134- "taskpairs", \
1135- self.taskpairs,
1136- xdg_data_home)
1137-
1138- def _append_to_taskpairs(self, local_tasks, remote_tasks):
1139- for local, remote in zip(local_tasks, remote_tasks):
1140- self.taskpairs.append(TaskPair( \
1141- local_task = local,
1142- remote_task = remote))
1143-
1144- def _task_ids_to_tasks(self, id_list, task_list):
1145- #TODO: this is not the quickest way to do this
1146- id_to_task = self._list_to_dict(task_list, "id", "self")
1147- return map(lambda id: id_to_task[id], id_list)
1148-
1149-
1150-
1151- def _process_new_tasks(self, new_ids, all_tasks, proxy):
1152- new_tasks = self._task_ids_to_tasks(new_ids, all_tasks)
1153- created_tasks = []
1154- for task in new_tasks:
1155- created_task = proxy.create_new_task(task.title)
1156- created_task.copy(task)
1157- created_tasks.append(created_task)
1158- return new_tasks, created_tasks
1159-
1160- def _process_deleted_tasks(self, ids_to_remove, all_tasks, proxy):
1161- tasks_to_remove = self._task_ids_to_tasks(ids_to_remove, all_tasks)
1162- for task in tasks_to_remove:
1163- proxy.delete_task(task)
1164-
1165- def _list_to_dict(self, source_list, fun1, fun2):
1166- list1 = map(lambda elem: getattr(elem, fun1), source_list)
1167- list2 = map(lambda elem: getattr(elem, fun2), source_list)
1168- return dict(zip(list1, list2))
1169-
1170- def _link_same_title(self, local_list, remote_list):
1171- self.taskpairs = []
1172- local_title_to_task = self._list_to_dict(local_list, \
1173- "title", "self")
1174- remote_title_to_task = self._list_to_dict(remote_list, \
1175- "title", "self")
1176- local_titles = map(lambda t: t.title, local_list)
1177- remote_titles = map(lambda t: t.title, remote_list)
1178- common_titles = set(local_titles).intersection(set(remote_titles))
1179- for title in common_titles:
1180- self.taskpairs.append(TaskPair( \
1181- local_task = local_title_to_task[title],
1182- remote_task = remote_title_to_task[title]))
1183
1184=== added file 'data/icons/hicolor/scalable/apps/backend_evolution.png'
1185Binary 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-09-03 23:47:40 +0000 differ

Subscribers

People subscribed via source and target branches

to status/vote changes: