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
=== 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-09-03 23:47:40 +0000
@@ -0,0 +1,420 @@
1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Getting Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#
6# This program is free software: you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# -----------------------------------------------------------------------------
19
20'''
21Backend for storing/loading tasks in Evolution Tasks
22'''
23
24import os
25import time
26import uuid
27import datetime
28import evolution
29from dateutil.tz import tzutc, tzlocal
30
31from GTG.backends.genericbackend import GenericBackend
32from GTG import _
33from GTG.backends.syncengine import SyncEngine, SyncMeme
34from GTG.backends.periodicimportbackend import PeriodicImportBackend
35from GTG.tools.dates import RealDate, NoDate
36from GTG.core.task import Task
37from GTG.tools.interruptible import interruptible
38from GTG.tools.logger import Log
39from GTG.tools.tags import extract_tags_from_text
40
41#Dictionaries to translate GTG tasks in Evolution ones
42_GTG_TO_EVOLUTION_STATUS = \
43 {Task.STA_ACTIVE: evolution.ecal.ICAL_STATUS_CONFIRMED,
44 Task.STA_DONE: evolution.ecal.ICAL_STATUS_COMPLETED,
45 Task.STA_DISMISSED: evolution.ecal.ICAL_STATUS_CANCELLED}
46
47_EVOLUTION_TO_GTG_STATUS = \
48 {evolution.ecal.ICAL_STATUS_CONFIRMED: Task.STA_ACTIVE,
49 evolution.ecal.ICAL_STATUS_DRAFT: Task.STA_ACTIVE,
50 evolution.ecal.ICAL_STATUS_FINAL: Task.STA_ACTIVE,
51 evolution.ecal.ICAL_STATUS_INPROCESS: Task.STA_ACTIVE,
52 evolution.ecal.ICAL_STATUS_NEEDSACTION: Task.STA_ACTIVE,
53 evolution.ecal.ICAL_STATUS_NONE: Task.STA_ACTIVE,
54 evolution.ecal.ICAL_STATUS_TENTATIVE: Task.STA_ACTIVE,
55 evolution.ecal.ICAL_STATUS_X: Task.STA_ACTIVE,
56 evolution.ecal.ICAL_STATUS_COMPLETED: Task.STA_DONE,
57 evolution.ecal.ICAL_STATUS_CANCELLED: Task.STA_DISMISSED}
58
59
60
61class Backend(PeriodicImportBackend):
62 '''
63 Evolution backend
64 '''
65
66
67 _general_description = { \
68 GenericBackend.BACKEND_NAME: "backend_evolution", \
69 GenericBackend.BACKEND_HUMAN_NAME: _("Evolution tasks"), \
70 GenericBackend.BACKEND_AUTHORS: ["Luca Invernizzi"], \
71 GenericBackend.BACKEND_TYPE: GenericBackend.TYPE_READWRITE, \
72 GenericBackend.BACKEND_DESCRIPTION: \
73 _("Lets you synchronize your GTG tasks with Evolution tasks"),\
74 }
75
76 _static_parameters = { \
77 "period": { \
78 GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT, \
79 GenericBackend.PARAM_DEFAULT_VALUE: 10, },
80 "is-first-run": { \
81 GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL, \
82 GenericBackend.PARAM_DEFAULT_VALUE: True, },
83 }
84
85###############################################################################
86### Backend standard methods ##################################################
87###############################################################################
88
89 def __init__(self, parameters):
90 '''
91 See GenericBackend for an explanation of this function.
92 Loads the saved state of the sync, if any
93 '''
94 super(Backend, self).__init__(parameters)
95 #loading the saved state of the synchronization, if any
96 self.sync_engine_path = os.path.join('backends/evolution/', \
97 "sync_engine-" + self.get_id())
98 self.sync_engine = self._load_pickled_file(self.sync_engine_path, \
99 SyncEngine())
100 #sets up the connection to the evolution api
101 task_personal = evolution.ecal.list_task_sources()[0][1]
102 self._evolution_tasks = evolution.ecal.open_calendar_source( \
103 task_personal,
104 evolution.ecal.CAL_SOURCE_TYPE_TODO)
105
106 def do_periodic_import(self):
107 """
108 See PeriodicImportBackend for an explanation of this function.
109 """
110 stored_evolution_task_ids = set(self.sync_engine.get_all_remote())
111 current_evolution_task_ids = set([task.get_uid() \
112 for task in self._evolution_tasks.get_all_objects()])
113 #If it's the very first time the backend is run, it's possible that the
114 # user already synced his tasks in some way (but we don't know that).
115 # Therefore, we attempt to induce those tasks relationships matching the
116 # titles.
117 if self._parameters["is-first-run"]:
118 gtg_titles_dic = {}
119 for tid in self.datastore.get_all_tasks():
120 gtg_task = self.datastore.get_task(tid)
121 if not self._gtg_task_is_syncable_per_attached_tags(gtg_task):
122 continue
123 gtg_title = gtg_task.get_title()
124 if gtg_titles_dic.has_key(gtg_title):
125 gtg_titles_dic[gtg_task.get_title()].append(tid)
126 else:
127 gtg_titles_dic[gtg_task.get_title()] = [tid]
128 for evo_task_id in current_evolution_task_ids:
129 evo_task = self._evo_get_task(evo_task_id)
130 try:
131 tids = gtg_titles_dic[evo_task.get_summary()]
132 #we remove the tid, so that it can't be linked to two
133 # different evolution tasks
134 tid = tids.pop()
135 gtg_task = self.datastore.get_task(tid)
136 meme = SyncMeme(gtg_task.get_modified(),
137 self._evo_get_modified(evo_task),
138 "GTG")
139 self.sync_engine.record_relationship( \
140 local_id = tid,
141 remote_id = evo_task.get_uid(),
142 meme = meme)
143 except KeyError:
144 pass
145 #a first run has been completed successfully
146 self._parameters["is-first-run"] = False
147
148 for evo_task_id in current_evolution_task_ids:
149 #Adding and updating
150 self.cancellation_point()
151 self._process_evo_task(evo_task_id)
152
153 for evo_task_id in stored_evolution_task_ids.difference(\
154 current_evolution_task_ids):
155 #Removing the old ones
156 self.cancellation_point()
157 tid = self.sync_engine.get_local_id(evo_task_id)
158 self.datastore.request_task_deletion(tid)
159 try:
160 self.sync_engine.break_relationship(remote_id = \
161 evo_task_id)
162 except KeyError:
163 pass
164
165 def save_state(self):
166 '''
167 See GenericBackend for an explanation of this function.
168 '''
169 self._store_pickled_file(self.sync_engine_path, self.sync_engine)
170
171###############################################################################
172### Process tasks #############################################################
173###############################################################################
174
175 @interruptible
176 def remove_task(self, tid):
177 '''
178 See GenericBackend for an explanation of this function.
179 '''
180 try:
181 evo_task_id = self.sync_engine.get_remote_id(tid)
182 self._delete_evolution_task(self._evo_get_task(evo_task_id))
183 except KeyError:
184 pass
185 try:
186 self.sync_engine.break_relationship(local_id = tid)
187 except:
188 pass
189
190 @interruptible
191 def set_task(self, task):
192 '''
193 See GenericBackend for an explanation of this function.
194 '''
195 tid = task.get_id()
196 is_syncable = self._gtg_task_is_syncable_per_attached_tags(task)
197 action, evo_task_id = self.sync_engine.analyze_local_id(
198 tid,
199 self.datastore.has_task,
200 self._evo_has_task,
201 is_syncable)
202 Log.debug('GTG->Evo set task (%s, %s)' % (action, is_syncable))
203
204 if action == None:
205 return
206
207 if action == SyncEngine.ADD:
208 evo_task = evolution.ecal.ECalComponent( \
209 ical = evolution.ecal.CAL_COMPONENT_TODO)
210 with self.datastore.get_backend_mutex():
211 self._evolution_tasks.add_object(evo_task)
212 self._populate_evo_task(task, evo_task)
213 meme = SyncMeme(task.get_modified(),
214 self._evo_get_modified(evo_task),
215 "GTG")
216 self.sync_engine.record_relationship( \
217 local_id = tid, remote_id = evo_task.get_uid(), meme = meme)
218
219 elif action == SyncEngine.UPDATE:
220 with self.datastore.get_backend_mutex():
221 evo_task = self._evo_get_task(evo_task_id)
222 meme = self.sync_engine.get_meme_from_local_id(task.get_id())
223 newest = meme.which_is_newest(task.get_modified(),
224 self._evo_get_modified(evo_task))
225 if newest == "local":
226 self._populate_evo_task(task, evo_task)
227 meme.set_remote_last_modified( \
228 self._evo_get_modified(evo_task))
229 meme.set_local_last_modified(task.get_modified())
230 else:
231 #we skip saving the state
232 return
233
234 elif action == SyncEngine.REMOVE:
235 self.datastore.request_task_deletion(tid)
236 try:
237 self.sync_engine.break_relationship(local_id = tid)
238 except KeyError:
239 pass
240
241 elif action == SyncEngine.LOST_SYNCABILITY:
242 evo_task = self._evo_get_task(evo_task_id)
243 self._exec_lost_syncability(tid, evo_task)
244 self.save_state()
245
246 def _process_evo_task(self, evo_task_id):
247 '''
248 Takes an evolution task id and carries out the necessary operations to
249 refresh the sync state
250 '''
251 self.cancellation_point()
252 evo_task = self._evo_get_task(evo_task_id)
253 is_syncable = self._evo_task_is_syncable(evo_task)
254 action, tid = self.sync_engine.analyze_remote_id( \
255 evo_task_id,
256 self.datastore.has_task,
257 self._evo_has_task,
258 is_syncable)
259 Log.debug('GTG<-Evo set task (%s, %s)' % (action, is_syncable))
260
261 if action == SyncEngine.ADD:
262 with self.datastore.get_backend_mutex():
263 tid = str(uuid.uuid4())
264 task = self.datastore.task_factory(tid)
265 self._populate_task(task, evo_task)
266 meme = SyncMeme(task.get_modified(),
267 self._evo_get_modified(evo_task),
268 "GTG")
269 self.sync_engine.record_relationship(local_id = tid,
270 remote_id = evo_task_id,
271 meme = meme)
272 self.datastore.push_task(task)
273
274 elif action == SyncEngine.UPDATE:
275 with self.datastore.get_backend_mutex():
276 task = self.datastore.get_task(tid)
277 meme = self.sync_engine.get_meme_from_remote_id(evo_task_id)
278 newest = meme.which_is_newest(task.get_modified(),
279 self._evo_get_modified(evo_task))
280 if newest == "remote":
281 self._populate_task(task, evo_task)
282 meme.set_remote_last_modified( \
283 self._evo_get_modified(evo_task))
284 meme.set_local_last_modified(task.get_modified())
285
286 elif action == SyncEngine.REMOVE:
287 return
288 try:
289 evo_task = self._evo_get_task(evo_task_id)
290 self._delete_evolution_task(evo_task)
291 self.sync_engine.break_relationship(remote_id = evo_task)
292 except KeyError:
293 pass
294
295 elif action == SyncEngine.LOST_SYNCABILITY:
296 self._exec_lost_syncability(tid, evo_task)
297 self.save_state()
298
299###############################################################################
300### Helper methods ############################################################
301###############################################################################
302
303 def _evo_has_task(self, evo_task_id):
304 '''Returns true if Evolution has that task'''
305 return bool(self._evo_get_task(evo_task_id))
306
307 def _evo_get_task(self, evo_task_id):
308 '''Returns an Evolution task, given its uid'''
309 return self._evolution_tasks.get_object(evo_task_id, "")
310
311 def _evo_get_modified(self, evo_task):
312 '''Returns the modified time of an Evolution task'''
313 return datetime.datetime.fromtimestamp(evo_task.get_modified())
314
315 def _delete_evolution_task(self, evo_task):
316 '''Deletes an Evolution task, given the task object'''
317 self._evolution_tasks.remove_object(evo_task)
318 self._evolution_tasks.update_object(evo_task)
319
320 def _populate_task(self, task, evo_task):
321 '''
322 Updates the attributes of a GTG task copying the ones of an Evolution
323 task
324 '''
325 task.set_title(evo_task.get_summary())
326 text = evo_task.get_description()
327 if text == None:
328 text = ""
329 task.set_text(text)
330 due_date_timestamp = evo_task.get_due()
331 if isinstance(due_date_timestamp, (int, float)):
332 due_date = self.__date_from_evo_to_gtg(due_date_timestamp)
333 else:
334 due_date = NoDate()
335 task.set_due_date(due_date)
336 status = evo_task.get_status()
337 if task.get_status() != _EVOLUTION_TO_GTG_STATUS[status]:
338 task.set_status(_EVOLUTION_TO_GTG_STATUS[status])
339 task.set_only_these_tags(extract_tags_from_text(text))
340
341 def _populate_evo_task(self, task, evo_task):
342 evo_task.set_summary(task.get_title())
343 text = task.get_excerpt(strip_tags = True, strip_subtasks = True)
344 if evo_task.get_description() != text:
345 evo_task.set_description(text)
346 due_date = task.get_due_date()
347 if isinstance(due_date, NoDate):
348 evo_task.set_due(None)
349 else:
350 evo_task.set_due(self.__date_from_gtg_to_evo(due_date))
351 status = task.get_status()
352 if _EVOLUTION_TO_GTG_STATUS[evo_task.get_status()] != status:
353 evo_task.set_status(_GTG_TO_EVOLUTION_STATUS[status])
354 #this calls are sometime ignored by evolution. Doing it twice
355 # is a hackish way to solve the problem. (TODO: send bug report)
356 self._evolution_tasks.update_object(evo_task)
357 self._evolution_tasks.update_object(evo_task)
358
359 def _exec_lost_syncability(self, tid, evo_task):
360 '''
361 Executed when a relationship between tasks loses its syncability
362 property. See SyncEngine for an explanation of that.
363 This function finds out which object is the original one
364 and which is the copy, and deletes the copy.
365 '''
366 meme = self.sync_engine.get_meme_from_local_id(tid)
367 self.sync_engine.break_relationship(local_id = tid)
368 if meme.get_origin() == "GTG":
369 evo_task = self._evo_get_task(evo_task.get_uid())
370 self._delete_evolution_task(evo_task)
371 else:
372 self.datastore.request_task_deletion(tid)
373
374 def _evo_task_is_syncable(self, evo_task):
375 '''
376 Returns True if this Evolution task should be synced into GTG tasks.
377
378 @param evo_task: an Evolution task
379 @returns Boolean
380 '''
381 attached_tags = set(self.get_attached_tags())
382 if GenericBackend.ALLTASKS_TAG in attached_tags:
383 return True
384 evo_tags = set(extract_tags_from_text(evo_task.get_description()))
385 return evo_task.is_disjoint(attached_tags)
386
387 def __date_from_evo_to_gtg(self, evo_date_timestamp):
388 """
389 Converts an evolution date object into the format understood by GTG
390
391 @param evo_date: an int, which represent time from epoch in UTC
392 convention
393 """
394 evo_datetime = datetime.datetime.fromtimestamp(evo_date_timestamp)
395 #See self.__date_from_gtg_to_evo for an explanation
396 evo_datetime = evo_datetime.replace(tzinfo = tzlocal())
397 gtg_datetime = evo_datetime.astimezone(tzutc())
398 #we strip timezone infos, as they're not used or expected in GTG
399 gtg_datetime.replace(tzinfo = None)
400 return RealDate(gtg_datetime.date())
401
402 def __date_from_gtg_to_evo(self, gtg_date):
403 """
404 Converts a datetime.date object into the format understood by Evolution
405
406 @param gtg_date: a GTG Date object
407 """
408 #GTG thinks in local time, evolution in utc
409 #to convert date objects between different timezones, we must convert
410 #them to datetime objects
411 gtg_datetime = datetime.datetime.combine(gtg_date.to_py_date(), datetime.time(0))
412 #We don't want to express GTG date into a UTC equivalent. Instead, we
413 #want the *same* date in GTG and evolution. Therefore, we must not do
414 #the conversion Local-> UTC (which would point to the same moment in
415 #time in different conventions), but do the opposite conversion UTC->
416 #Local (which will refer to different points in time, but to the same
417 #written date)
418 gtg_datetime = gtg_datetime.replace(tzinfo = tzutc())
419 evo_datetime = gtg_datetime.astimezone(tzlocal())
420 return int(time.mktime(evo_datetime.timetuple()))
0421
=== 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 @@
1[GTG Plugin]
2Module=evolution_sync
3Name=Evolution
4Short-description="Plugin for synchronising GTG with Gnome Evolution Tasks."
5Description="""Plugin for synchronising GTG with Gnome Evolution Tasks"""
6Authors='Luca Invernizzi <invernizzi.l@gmail.com>'
7Version=0.1.1
8Dependencies=evolution,
9Enabled=False
100
=== 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 @@
1# -*- coding: utf-8 -*-
2# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@gmail.com>
3#
4# This program is free software: you can redistribute it and/or modify it under
5# the terms of the GNU General Public License as published by the Free Software
6# Foundation, either version 3 of the License, or (at your option) any later
7# version.
8#
9# This program is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12# details.
13#
14# You should have received a copy of the GNU General Public License along with
15# this program. If not, see <http://www.gnu.org/licenses/>.
16
17from GTG.plugins.evolution_sync.evolutionSync import EvolutionSync
18
19#needed to keep pyflakes quiet
20if False == True: EvolutionSync()
210
=== 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 @@
1# -*- coding: utf-8 -*-
2# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@gmail.com>
3#
4# This program is free software: you can redistribute it and/or modify it under
5# the terms of the GNU General Public License as published by the Free Software
6# Foundation, either version 3 of the License, or (at your option) any later
7# version.
8#
9# This program is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12# details.
13#
14# You should have received a copy of the GNU General Public License along with
15# this program. If not, see <http://www.gnu.org/licenses/>.
16
17import evolution
18
19from GTG.core.task import Task
20from GTG.plugins.evolution_sync.evolutionTask import EvolutionTask
21from GTG.plugins.evolution_sync.genericProxy import GenericProxy
22
23
24class EvolutionProxy(GenericProxy):
25
26 __GTG_STATUSES = [Task.STA_ACTIVE,
27 Task.STA_DONE,
28 Task.STA_DISMISSED]
29
30 __EVO_STATUSES = [evolution.ecal.ICAL_STATUS_CONFIRMED,
31 evolution.ecal.ICAL_STATUS_COMPLETED,
32 evolution.ecal.ICAL_STATUS_CANCELLED]
33
34 def __init__(self):
35 super(EvolutionProxy, self).__init__()
36
37 def generateTaskList(self):
38 task_personal = evolution.ecal.list_task_sources()[0][1]
39 self._evolution_tasks = evolution.ecal.open_calendar_source(task_personal,
40 evolution.ecal.CAL_SOURCE_TYPE_TODO)
41 self._gtg_to_evo_status = dict(zip(self.__GTG_STATUSES,
42 self.__EVO_STATUSES))
43 self._evo_to_gtg_status = dict(zip(self.__EVO_STATUSES,
44 self.__GTG_STATUSES))
45 #Need to find a solution for the statuses GTG doesn't expect:
46 for evo_status in [evolution.ecal.ICAL_STATUS_DRAFT,
47 evolution.ecal.ICAL_STATUS_FINAL,
48 evolution.ecal.ICAL_STATUS_INPROCESS,
49 evolution.ecal.ICAL_STATUS_NEEDSACTION,
50 evolution.ecal.ICAL_STATUS_NONE,
51 evolution.ecal.ICAL_STATUS_TENTATIVE,
52 evolution.ecal.ICAL_STATUS_X]:
53 self._evo_to_gtg_status[evo_status] = Task.STA_ACTIVE
54 self._tasks_list = []
55 for task in self._evolution_tasks.get_all_objects():
56 self._tasks_list.append(EvolutionTask(task, self))
57
58 def create_new_task(self, title):
59 task = evolution.ecal.ECalComponent(ical=evolution.ecal.CAL_COMPONENT_TODO)
60 self._evolution_tasks.add_object(task)
61 new_task = EvolutionTask(task, self)
62 new_task.title = title
63 self._tasks_list.append(new_task)
64 return new_task
65
66 def delete_task(self, task):
67 evo_task = task.get_evolution_task()
68 self._evolution_tasks.remove_object(evo_task)
69 self._evolution_tasks.update_object(evo_task)
70
71 def update_task(self, task):
72 evo_task = task.get_evolution_task()
73 self._evolution_tasks.update_object(evo_task)
740
=== 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 @@
1# -*- coding: utf-8 -*-
2# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@gmail.com>
3# - Paulo Cabido <paulo.cabido@gmail.com> (example file)
4#
5# This program is free software: you can redistribute it and/or modify it under
6# the terms of the GNU General Public License as published by the Free Software
7# Foundation, either version 3 of the License, or (at your option) any later
8# version.
9#
10# This program is distributed in the hope that it will be useful, but WITHOUT
11# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
13# details.
14#
15# You should have received a copy of the GNU General Public License along with
16# this program. If not, see <http://www.gnu.org/licenses/>.
17
18import gtk
19from threading import Thread
20
21from GTG import _
22from GTG.plugins.evolution_sync.syncEngine import SyncEngine
23
24class EvolutionSync:
25
26 def __init__(self):
27 #drop down menu
28 self.menu_item = gtk.MenuItem(_("Synchronize with Evolution"))
29 self.menu_item.connect('activate', self.onTesteMenu)
30
31 def activate(self, plugin_api):
32 self.plugin_api = plugin_api
33 # add a menu item to the menu bar
34 plugin_api.add_menu_item(self.menu_item)
35 self.sync_engine = SyncEngine(self)
36
37 def deactivate(self, plugin_api):
38 plugin_api.remove_menu_item(self.menu_item)
39
40 def onTesteMenu(self, widget):
41 self.worker_thread = Thread(target = \
42 self.sync_engine.synchronize).start()
43
44 def onTaskOpened(self, plugin_api):
45 pass
460
=== 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 @@
1# -*- coding: utf-8 -*-
2# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@gmail.com>
3#
4# This program is free software: you can redistribute it and/or modify it under
5# the terms of the GNU General Public License as published by the Free Software
6# Foundation, either version 3 of the License, or (at your option) any later
7# version.
8#
9# This program is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12# details.
13#
14# You should have received a copy of the GNU General Public License along with
15# this program. If not, see <http://www.gnu.org/licenses/>.
16
17import time
18import datetime
19
20from GTG.plugins.evolution_sync.genericTask import GenericTask
21
22
23class EvolutionTask(GenericTask):
24
25 def __init__(self, evo_task, evolution_proxy):
26 super(EvolutionTask, self).__init__(evolution_proxy)
27 self._evo_task = evo_task
28
29 def _get_title(self):
30 return self._evo_task.get_summary()
31
32 def _set_title(self, title):
33 self._evo_task.set_summary(title)
34 self.get_proxy().update_task(self)
35
36 def _get_id(self):
37 return self._evo_task.get_uid()
38
39 def _get_tags(self):
40 #We could use Evolution's "Categories" as tags
41 raise NotImplementedError()
42
43 def _set_tags(self, tags):
44 raise NotImplementedError()
45 self.get_proxy().update_task(self)
46
47 def _get_text(self):
48 desc = self._evo_task.get_description()
49 if desc == None:
50 return ""
51 return desc
52
53 def _set_text(self, text):
54 self._evo_task.set_description(text)
55 self.get_proxy().update_task(self)
56
57 def _set_status(self, status):
58 #Since Evolution's statuses are fare more than GTG's,
59 # we need to check that the current status is not one of the various
60 # statuses translated in the same gtg status, passed by argument.
61 # This way, we avoid to force a status change when it's not needed
62 # (and not wanted)
63 current_status_in_gtg_terms = self.get_proxy()._evo_to_gtg_status[\
64 self._evo_task.get_status()]
65 if current_status_in_gtg_terms != status:
66 new_evo_status = self.get_proxy()._gtg_to_evo_status[status]
67 self._evo_task.set_status(new_evo_status)
68 self.get_proxy().update_task(self)
69
70 def _get_status(self):
71 status = self._evo_task.get_status()
72 return self.get_proxy()._evo_to_gtg_status[status]
73
74 def _get_due_date(self):
75 due = self._evo_task.get_due()
76 if isinstance(due, (int, float)):
77 return self.__time_evo_to_date(due)
78
79 def _set_due_date(self, due):
80 if due == None:
81 #TODO: I haven't find a way to reset the due date
82 # We could copy the task, but that would lose all the attributes
83 # currently not supported by the library used (and they're a lot)
84 pass
85 else:
86 self._evo_task.set_due(self.__time_date_to_evo(due))
87 self.get_proxy().update_task(self)
88
89 def _get_modified(self):
90 return self.__time_evo_to_datetime(self._evo_task.get_modified())
91
92 def __time_evo_to_date(self, timestamp):
93 return datetime.datetime.fromtimestamp(timestamp + time.timezone).date()
94
95 def __time_evo_to_datetime(self, timestamp):
96 return datetime.datetime.fromtimestamp(timestamp)
97
98 def __time_datetime_to_evo(self, timeobject):
99 return int(time.mktime(timeobject.timetuple()))
100
101 def __time_date_to_evo(self, timeobject):
102 #NOTE: need to substract the timezone to avoid the "one day before bug
103 # (at the airport => no internet now, need to fill bug number in)
104 #Explanation: gtg date format is converted to datetime in date/00:00 in
105 # local time, and time.mktime considers that when converting to UNIX
106 # time. Evolution, however, doesn't convert back to local time.
107 return self.__time_datetime_to_evo(timeobject) - time.timezone
108
109 def get_evolution_task(self):
110 return self._evo_task
1110
=== 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 @@
1# -*- coding: utf-8 -*-
2# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@gmail.com>
3#
4# This program is free software: you can redistribute it and/or modify it under
5# the terms of the GNU General Public License as published by the Free Software
6# Foundation, either version 3 of the License, or (at your option) any later
7# version.
8#
9# This program is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12# details.
13#
14# You should have received a copy of the GNU General Public License along with
15# this program. If not, see <http://www.gnu.org/licenses/>.
16
17class GenericProxy(object):
18
19 def __init__(self):
20 super(GenericProxy, self).__init__()
21 self._tasks_list = []
22
23 def get_tasks_list(self):
24 return self._tasks_list
25
26 def generateTaskList(self):
27 raise NotImplementedError()
28
29 def create_new_task(self, title):
30 raise NotImplementedError()
31
32 def delete_task(self, task):
33 raise NotImplementedError()
34
350
=== 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 @@
1# -*- coding: utf-8 -*-
2# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@gmail.com>
3#
4# This program is free software: you can redistribute it and/or modify it under
5# the terms of the GNU General Public License as published by the Free Software
6# Foundation, either version 3 of the License, or (at your option) any later
7# version.
8#
9# This program is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12# details.
13#
14# You should have received a copy of the GNU General Public License along with
15# this program. If not, see <http://www.gnu.org/licenses/>.
16
17
18class GenericTask(object):
19 """GenericTask is the abstract interface that represents a generic task."""
20
21 def __init__(self, proxy):
22 self.__proxy = proxy
23
24 def __str__(self):
25 return "Task " + self.title + "(" + self.id + ")"
26
27 def toString(self):
28 return "Task:\n" + \
29 "\t - Title: " + self.title + "\n" + \
30 "\t - ID: " + self.id + "\n" + \
31 "\t - Modified: " + str(self.modified) + "\n" + \
32 "\t - Due: " + str(self.due_date) + "\n"
33
34 def copy(self, task):
35 #Minimizing the number of actions will allow a faster RTM plugin
36 # (where GET is fast, but SET is slow)
37 if self.title != task.title:
38 self.title = task.title
39 if self.text != task.text:
40 self.text = task.text
41 if self.status != task.status:
42 self.status = task.status
43 if self.due_date != task.due_date:
44 self.due_date = task.due_date
45
46 def get_proxy(self):
47 return self.__proxy
48
49 title = property(lambda self: self._get_title(),
50 lambda self, arg: self._set_title(arg))
51
52 id = property(lambda self: self._get_id())
53
54 text = property(lambda self: self._get_text(),
55 lambda self, arg: self._set_text(arg))
56
57 status = property(lambda self: self._get_status(),
58 lambda self, arg: self._set_status(arg))
59
60 modified = property(lambda self: self._get_modified())
61
62 due_date = property(lambda self: self._get_due_date(),
63 lambda self, arg: self._set_due_date(arg))
64
65 tags = property(lambda self: self._get_tags(),
66 lambda self, arg: self._set_tags(arg))
67
68 self = property(lambda self: self)
690
=== 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 @@
1# -*- coding: utf-8 -*-
2# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@gmail.com>
3#
4# This program is free software: you can redistribute it and/or modify it under
5# the terms of the GNU General Public License as published by the Free Software
6# Foundation, either version 3 of the License, or (at your option) any later
7# version.
8#
9# This program is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12# details.
13#
14# You should have received a copy of the GNU General Public License along with
15# this program. If not, see <http://www.gnu.org/licenses/>.
16
17from GTG.core.task import Task
18from GTG.plugins.evolution_sync.gtgTask import GtgTask
19from GTG.plugins.evolution_sync.genericProxy import GenericProxy
20
21
22class GtgProxy(GenericProxy):
23
24 def __init__(self, plugin_api):
25 super(GtgProxy, self).__init__()
26 self.plugin_api = plugin_api
27 self.requester = self.plugin_api.get_requester()
28
29 def generateTaskList(self):
30 self._tasks_list = []
31 requester = self.plugin_api.get_requester()
32 statuses = [Task.STA_ACTIVE, Task.STA_DISMISSED, Task.STA_DONE]
33 tasks = map(self.plugin_api.get_task, \
34 requester.get_tasks_list(status = statuses))
35 map(lambda task: self._tasks_list.append(GtgTask(task, \
36 self.plugin_api, self)), tasks)
37
38 def create_new_task(self, title, never_seen_before = True):
39 new_gtg_local_task = self.requester.new_task(newtask=never_seen_before)
40 new_task = GtgTask(new_gtg_local_task, self.plugin_api, self)
41 new_task.title = title
42 self._tasks_list.append(new_task)
43 return new_task
44
45 def delete_task(self, task):
46 #NOTE: delete_task wants the internal gtg id, not the uuid
47 id = task.get_gtg_task().get_id()
48 self.requester.delete_task(id)
49
500
=== 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 @@
1# -*- coding: utf-8 -*-
2# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@gmail.com>
3#
4# This program is free software: you can redistribute it and/or modify it under
5# the terms of the GNU General Public License as published by the Free Software
6# Foundation, either version 3 of the License, or (at your option) any later
7# version.
8#
9# This program is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12# details.
13#
14# You should have received a copy of the GNU General Public License along with
15# this program. If not, see <http://www.gnu.org/licenses/>.
16
17import datetime
18
19from GTG.tools.dates import NoDate, RealDate
20from GTG.plugins.evolution_sync.genericTask import GenericTask
21
22class GtgTask(GenericTask):
23
24 def __init__(self, gtg_task, plugin_api, gtg_proxy):
25 super(GtgTask, self).__init__(gtg_proxy)
26 self._gtg_task = gtg_task
27 self.plugin_api = plugin_api
28
29 def _get_title(self):
30 return self._gtg_task.get_title()
31
32 def _set_title(self, title):
33 self._gtg_task.set_title(title)
34
35 def _get_id(self):
36 return self._gtg_task.get_uuid()
37
38 def _get_tags(self):
39 raise NotImplemented()
40 return self._gtg_task.get_tags()
41
42 def _set_tags(self, tags):
43 raise NotImplemented()
44 #NOTE: isn't there a better mode than removing all tags?
45 # need to add function in GTG/core/task.py
46 old_tags = self.tags
47 for tag in old_tags:
48 self._gtg_task.remove_tag(tag)
49 map(lambda tag: self._gtg_task.add_tag('@'+tag), tags)
50
51 def _get_text(self):
52 return self._gtg_task.get_excerpt()
53
54 def _set_text(self, text):
55 self._gtg_task.set_text(text)
56
57 def _set_status(self, status):
58 self._gtg_task.set_status(status)
59
60 def _get_status(self):
61 return self._gtg_task.get_status()
62
63 def _get_due_date(self):
64 due_date = self._gtg_task.get_due_date()
65 if due_date == NoDate():
66 return None
67 return due_date.to_py_date()
68
69 def _set_due_date(self, due):
70 if due == None:
71 gtg_due = NoDate()
72 else:
73 gtg_due = RealDate(due)
74 self._gtg_task.set_due_date(gtg_due)
75
76 def _get_modified(self):
77 modified = self._gtg_task.get_modified()
78 if modified == None or modified == "":
79 return None
80 return self.__time_gtg_to_datetime(modified)
81
82 def get_gtg_task(self):
83 return self._gtg_task
84
85 def __time_gtg_to_datetime(self, string):
86 #FIXME: need to handle time with TIMEZONES!
87 string = string.split('.')[0].split('Z')[0]
88 if string.find('T') == -1:
89 return datetime.datetime.strptime(string.split(".")[0], "%Y-%m-%d")
90 return datetime.datetime.strptime(string.split(".")[0], \
91 "%Y-%m-%dT%H:%M:%S")
920
=== 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 @@
1# -*- coding: utf-8 -*-
2# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@gmail.com>
3#
4# This program is free software: you can redistribute it and/or modify it under
5# the terms of the GNU General Public License as published by the Free Software
6# Foundation, either version 3 of the License, or (at your option) any later
7# version.
8#
9# This program is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12# details.
13#
14# You should have received a copy of the GNU General Public License along with
15# this program. If not, see <http://www.gnu.org/licenses/>.
16
17import datetime
18from xdg.BaseDirectory import xdg_data_home
19
20from GTG.plugins.evolution_sync.gtgProxy import GtgProxy
21from GTG.plugins.evolution_sync.evolutionProxy import EvolutionProxy
22
23class TaskPair(object):
24 def __init__(self, \
25 local_task,
26 remote_task):
27 self.local_id = local_task.id
28 self.remote_id = remote_task.id
29 self.__remote_synced_until = remote_task.modified
30 self.local_synced_until = local_task.modified
31 self.__remote_first_seen = datetime.datetime.now()
32
33 self = property(lambda self: self)
34
35 remote_synced_until = property (\
36 lambda self: self.__get_remote_synced_until(),\
37 lambda self, t: self.__set_remote_synced_until(t))
38
39 def __get_remote_synced_until(self):
40 if self.__remote_synced_until > self.__remote_first_seen:
41 return self.__remote_synced_until
42 else:
43 return self.__remote_first_seen
44
45 def __set_remote_synced_until(self, datetime_object):
46 self.__remote_synced_until = datetime_object
47
48
49class SyncEngine(object):
50
51 def __init__(self, this_plugin):
52 super(SyncEngine, self).__init__()
53 self.this_plugin = this_plugin
54 self.plugin_api = this_plugin.plugin_api
55 self.local_proxy = GtgProxy(self.this_plugin.plugin_api)
56 self.remote_proxy = EvolutionProxy()
57
58 def synchronize(self):
59 self.synchronizeWorker()
60
61 def synchronizeWorker(self):
62 #Generate the two list of tasks from the local and remote task source
63 self.remote_proxy.generateTaskList()
64 self.local_proxy.generateTaskList()
65 remote_tasks = self.remote_proxy.get_tasks_list()
66 local_tasks = self.local_proxy.get_tasks_list()
67
68 #Load the list of known links between tasks (tasks that are the same
69 # one saved in different task sources)
70 self.taskpairs = self.plugin_api.load_configuration_object( \
71 "evolution-sync", \
72 "taskpairs", \
73 xdg_data_home)
74 if self.taskpairs == None:
75 #We have no previous knowledge of links, this must be the first
76 #attempt to synchronize. So, we try to infer some by
77 # linking tasks with the same title
78 self._link_same_title(local_tasks, remote_tasks)
79
80 #We'll use some sets to see what is new and what was deleted
81 old_local_ids = set(map(lambda tp: tp.local_id, self.taskpairs))
82 old_remote_ids = set(map(lambda tp: tp.remote_id, self.taskpairs))
83 current_local_ids = set(map(lambda t: t.id, local_tasks))
84 current_remote_ids = set(map(lambda t: t.id, remote_tasks))
85 #Tasks that have been added
86 new_local_ids = current_local_ids.difference(old_local_ids)
87 new_remote_ids = current_remote_ids.difference(old_remote_ids)
88 #Tasks that have been deleted
89 deleted_local_ids = old_local_ids.difference(current_local_ids)
90 deleted_remote_ids = old_remote_ids.difference(current_remote_ids)
91 #Local tasks with the remote task still existing (could need to be
92 #updated)
93 updatable_local_ids = current_local_ids.difference(new_local_ids)
94
95 #Add tasks to the remote proxy
96 [new_local_tasks, new_remote_tasks] = self._process_new_tasks(\
97 new_local_ids,\
98 local_tasks, \
99 self.remote_proxy)
100 self._append_to_taskpairs(new_local_tasks, new_remote_tasks)
101 #Add tasks to the local proxy
102 [new_remote_tasks, new_local_tasks] = self._process_new_tasks(\
103 new_remote_ids,\
104 remote_tasks,\
105 self.local_proxy)
106 self._append_to_taskpairs(new_local_tasks, new_remote_tasks)
107
108 #Delete tasks from the remote proxy
109 taskpairs_deleted = filter(lambda tp: tp.local_id in deleted_local_ids,\
110 self.taskpairs)
111 remote_ids_to_delete = map( lambda tp: tp.remote_id, taskpairs_deleted)
112 self._process_deleted_tasks(remote_ids_to_delete, remote_tasks,\
113 self.remote_proxy)
114 map(lambda tp: self.taskpairs.remove(tp), taskpairs_deleted)
115
116 #Delete tasks from the local proxy
117 taskpairs_deleted = filter(lambda tp: tp.remote_id in deleted_remote_ids,\
118 self.taskpairs)
119 local_ids_to_delete = map( lambda tp: tp.local_id, taskpairs_deleted)
120 self._process_deleted_tasks(local_ids_to_delete, local_tasks, self.local_proxy)
121 map(lambda tp: self.taskpairs.remove(tp), taskpairs_deleted)
122
123 #Update tasks
124 local_to_taskpair = self._list_to_dict(self.taskpairs, \
125 "local_id", \
126 "self")
127 local_id_to_task = self._list_to_dict(local_tasks, \
128 "id", \
129 "self")
130 remote_id_to_task = self._list_to_dict(remote_tasks, \
131 "id", \
132 "self")
133 for local_id in updatable_local_ids:
134 taskpair = local_to_taskpair[local_id]
135 local_task = local_id_to_task[local_id]
136 remote_task = remote_id_to_task[taskpair.remote_id]
137 local_was_updated = local_task.modified > \
138 taskpair.local_synced_until
139 remote_was_updated = remote_task.modified > \
140 taskpair.remote_synced_until
141
142 if local_was_updated and remote_was_updated:
143 if local_task.modified > remote_task.modified:
144 remote_task.copy(local_task)
145 else:
146 #If the update time is the same one, we have to
147 # arbitrary decide which gets copied
148 local_task.copy(remote_task)
149 elif local_was_updated:
150 remote_task.copy(local_task)
151 elif remote_was_updated:
152 local_task.copy(remote_task)
153
154 taskpair.remote_synced_until = remote_task.modified
155 taskpair.local_synced_until = local_task.modified
156
157 #Lastly, save the list of known links
158 self.plugin_api.save_configuration_object( \
159 "evolution-sync", \
160 "taskpairs", \
161 self.taskpairs,
162 xdg_data_home)
163
164 def _append_to_taskpairs(self, local_tasks, remote_tasks):
165 for local, remote in zip(local_tasks, remote_tasks):
166 self.taskpairs.append(TaskPair( \
167 local_task = local,
168 remote_task = remote))
169
170 def _task_ids_to_tasks(self, id_list, task_list):
171 #TODO: this is not the quickest way to do this
172 id_to_task = self._list_to_dict(task_list, "id", "self")
173 return map(lambda id: id_to_task[id], id_list)
174
175
176
177 def _process_new_tasks(self, new_ids, all_tasks, proxy):
178 new_tasks = self._task_ids_to_tasks(new_ids, all_tasks)
179 created_tasks = []
180 for task in new_tasks:
181 created_task = proxy.create_new_task(task.title)
182 created_task.copy(task)
183 created_tasks.append(created_task)
184 return new_tasks, created_tasks
185
186 def _process_deleted_tasks(self, ids_to_remove, all_tasks, proxy):
187 tasks_to_remove = self._task_ids_to_tasks(ids_to_remove, all_tasks)
188 for task in tasks_to_remove:
189 proxy.delete_task(task)
190
191 def _list_to_dict(self, source_list, fun1, fun2):
192 list1 = map(lambda elem: getattr(elem, fun1), source_list)
193 list2 = map(lambda elem: getattr(elem, fun2), source_list)
194 return dict(zip(list1, list2))
195
196 def _link_same_title(self, local_list, remote_list):
197 self.taskpairs = []
198 local_title_to_task = self._list_to_dict(local_list, \
199 "title", "self")
200 remote_title_to_task = self._list_to_dict(remote_list, \
201 "title", "self")
202 local_titles = map(lambda t: t.title, local_list)
203 remote_titles = map(lambda t: t.title, remote_list)
204 common_titles = set(local_titles).intersection(set(remote_titles))
205 for title in common_titles:
206 self.taskpairs.append(TaskPair( \
207 local_task = local_title_to_task[title],
208 remote_task = remote_title_to_task[title]))
2090
=== added file 'data/icons/hicolor/scalable/apps/backend_evolution.png'
210Binary 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 differ1Binary 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: