GTG

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

Proposed by phiamo
Status: Rejected
Rejected by: Izidor Matušov
Proposed branch: lp:~gtg-user/gtg/google-tasks-backend
Merge into: lp:~gtg/gtg/old-trunk
Diff against target: 1507 lines (+1323/-77)
7 files modified
GTG/backends/backend_gtasks.py (+815/-0)
GTG/backends/genericbackend.py (+6/-0)
GTG/backends/gtasks/client_secrets.json (+1/-0)
GTG/backends/gtasks/gtasks.py (+170/-0)
GTG/gtk/backends_dialog/parameters_ui/__init__.py (+55/-77)
GTG/gtk/backends_dialog/parameters_ui/comboboxui.py (+102/-0)
GTG/gtk/backends_dialog/parameters_ui/gtasklistsui.py (+174/-0)
To merge this branch: bzr merge lp:~gtg-user/gtg/google-tasks-backend
Reviewer Review Type Date Requested Status
phiamo (community) plan Needs Resubmitting
Izidor Matušov code,run Needs Fixing
Review via email: mp+150416@code.launchpad.net

Description of the change

Added the google tasks api as a backend.

TODO's:
- the client_secrets.json provides a needed api key. Already emailed google to get info on howto manage this in another fashion, no response yet
- under certain circumstances i get duplicates cause sync_engine telle it does not know things
- would like to change the dropdown in the backend properties to an editable listview so you can tell which list to sync and add per list tags (currently in progress local)

To post a comment you must log in.
Revision history for this message
Izidor Matušov (izidor) wrote :

For beginning couple of formal things:

1, get rid of print statements
2, have you written the backend from the beginning or you used
lp:~gtg-contributors/gtg/google_tasks for inspiration? In the later case you need to put credits into Author sections.
3, Your code have to satisfy PEP8 standard and PyFlakes check <use `make check` command>
4, Get rid of _bak.py file

I managed to get some traceback in console during update

Exception in thread Thread-10:
Traceback (most recent call last):
  File "/usr/lib/python2.7/threading.py", line 552, in __bootstrap_inner
    self.run()
  File "/usr/lib/python2.7/threading.py", line 760, in run
    self.function(*self.args, **self.kwargs)
  File "/home/izidor/projects/gtg/trunk/GTG/backends/genericbackend.py", line 653, in launch_setting_thread
    self.set_task(task)
  File "/home/izidor/projects/gtg/trunk/GTG/tools/interruptible.py", line 39, in new
    return fn(*args)
  File "/home/izidor/projects/gtg/trunk/GTG/backends/backend_gtasks.py", line 275, in set_task
    self._populate_gtask(task, gtask)
  File "/home/izidor/projects/gtg/trunk/GTG/backends/backend_gtasks.py", line 452, in _populate_gtask
    self.gtasks_proxy.update(gtask)
  File "/home/izidor/projects/gtg/trunk/GTG/backends/backend_gtasks.py", line 493, in update
    self.gtasks.task_update(gtask.get_tasklist(), gtask.get_id(), body)
  File "/home/izidor/projects/gtg/trunk/GTG/backends/gtasks/gtasks.py", line 169, in task_update
    return self.execute(self.get_service().tasks().update(tasklist=tasklist,task=task, body=body))
  File "/home/izidor/projects/gtg/trunk/GTG/backends/gtasks/gtasks.py", line 113, in execute
    return request.execute(http=self.http)
  File "/usr/local/lib/python2.7/dist-packages/google_api_python_client-1.0-py2.7.egg/oauth2client/util.py", line 120, in positional_wrapper
    return wrapped(*args, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/google_api_python_client-1.0-py2.7.egg/apiclient/http.py", line 678, in execute
    raise HttpError(resp, content, uri=self.uri)
HttpError: <HttpError 400 when requesting https://www.googleapis.com/tasks/v1/lists/MTM0OTg2OTgwNTUxMDYzOTQ5MTU6MDow/tasks/MTM0OTg2OTgwNTUxMDYzOTQ5MTU6MDoxNzU4ODcwNTY1?alt=json returned "Invalid Value">

Wouldn't it be better to create a list for every tag and then copy tasks to each list?

Please, address those things so we can continue in reviewing your patch.

review: Needs Fixing (code,run)
Revision history for this message
phiamo (phiamo) wrote :

Hello Izidor,
Thanks for your comment.

1, get rid of print statements
> yes asap, i was a bit too entouthiastic yesterday evening...

2, have you written the backend from the beginning or you used
lp:~gtg-contributors/gtg/google_tasks for inspiration? In the later case you need to put credits into Author sections.
no i started with the rtm plugin as a base, and the rest was coded either myself, or given by google (sample for gtasks api usage)

3, Your code have to satisfy PEP8 standard and PyFlakes check <use `make check` command>
i am going further with pylint and pep8 lateron this day
4, Get rid of _bak.py file
of course

Wouldn't it be better to create a list for every tag and then copy tasks to each list?

yes, it would and thats exactly what i am working on atm

1294. By phiamo

Removed print statements

1295. By phiamo

Removed _bak file

1296. By phiamo

Added possibility to save dicts in backend params safely

1297. By phiamo

Changed the way howto config the backed to be more userfriendly and configurable

Revision history for this message
phiamo (phiamo) wrote :

Hi,
i would like to split up the current development into some more steps and hopefully get the fist one into gtg mainline soon, then go further (i thought i could do these steps in one, but i failed, and dont have too much sparetime to do this now at once):

1.st step:
break down the current solution to only support one list in google tasks, configurable in the backend, (see latest commits, there go into this direction but are not finished yet) via the treeview already implemented, but with a radio toggle to select active list only

the goal would be to have the lists selectable in the backend and just tell the backen, sync gtg with this list and back
this should be a reliable mechanism so my google lists (also updated by some android app) are everywhere in sync.

2.nd step:
add the possibility to manage multiple lists. making the backend treeview as it is currently you select which lists to sync via a checkbox and select the default list where new tasks from gtg should get added

3.rd step:
add a tags field to this backend per list, so you configure tags per lists which should end up with a good solution, that i can create a gtg task with a tag lets say @private and this tag is added to the google tasks lists which is configured in the backend with @private
this config should support multiple tags and probably furthermore there also should be a default option toggable, bt not mandatory, so not every task in gtg ends up in google tasks, but only the ones i configure manually

I am not so familiar with launchpad at all, do i need to revoke the code review, and add this plan somewhere?or would you just say, good idea, try it commit it and we will review it?

review: Needs Resubmitting (plan)
Revision history for this message
Izidor Matušov (izidor) wrote :

Hi phiamo,

I'll close this merge request as "Rejected" and feel free to request a merge again.

I think the first step could be just "workable" tasks (those which are showed in WorkView). Also I think you could set 1 tag = 1 list later.

Unmerged revisions

1297. By phiamo

Changed the way howto config the backed to be more userfriendly and configurable

1296. By phiamo

Added possibility to save dicts in backend params safely

1295. By phiamo

Removed _bak file

1294. By phiamo

Removed print statements

1293. By phiamo

Added google tasks backend

1292. By phiamo

Added google tasks backend

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'GTG/backends/backend_gtasks.py'
2--- GTG/backends/backend_gtasks.py 1970-01-01 00:00:00 +0000
3+++ GTG/backends/backend_gtasks.py 2013-02-26 14:40:31 +0000
4@@ -0,0 +1,815 @@
5+# -*- coding: utf-8 -*-
6+# -----------------------------------------------------------------------------
7+# Getting Things GNOME! - a personal organizer for the GNOME desktop
8+# Copyright (c) 2008-2012 - 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+google tasks backend
26+'''
27+
28+import os
29+import cgi
30+import uuid
31+import time
32+import threading
33+import datetime
34+import subprocess
35+import exceptions
36+from dateutil.tz import tzutc, tzlocal
37+
38+from GTG.backends.genericbackend import GenericBackend
39+from GTG import _
40+from GTG.backends.backendsignals import BackendSignals
41+from GTG.backends.syncengine import SyncEngine, SyncMeme
42+from GTG.backends.gtasks.gtasks import GTasks, GTasksError, GTasksAPIError
43+from GTG.backends.periodicimportbackend import PeriodicImportBackend
44+from GTG.tools.dates import Date
45+from GTG.core.task import Task
46+from GTG.tools.interruptible import interruptible
47+from GTG.tools.logger import Log
48+from xdg.BaseDirectory import xdg_config_home
49+
50+
51+
52+
53+
54+class Backend(PeriodicImportBackend):
55+
56+ _general_description = { \
57+ GenericBackend.BACKEND_NAME: "backend_gtasks", \
58+ GenericBackend.BACKEND_HUMAN_NAME: _("gTasks Backend"), \
59+ GenericBackend.BACKEND_AUTHORS: ["Philipp A. Mohrenweiser"], \
60+ GenericBackend.BACKEND_TYPE: GenericBackend.TYPE_READWRITE, \
61+ GenericBackend.BACKEND_DESCRIPTION: \
62+ _("This service synchronizes your tasks with your google online tasks"),\
63+ }
64+
65+ _static_parameters = {
66+ "period": {
67+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT,
68+ GenericBackend.PARAM_DEFAULT_VALUE: 10
69+ },
70+ "gtask-tag-and-list-selection": {
71+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_DICT,
72+ GenericBackend.PARAM_DEFAULT_VALUE: {}
73+ },
74+ "is-first-run": {
75+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL,
76+ GenericBackend.PARAM_DEFAULT_VALUE: True
77+ }
78+ }
79+###############################################################################
80+### Backend standard methods ##################################################
81+###############################################################################
82+
83+ def __init__(self, parameters):
84+ '''
85+ See GenericBackend for an explanation of this function.
86+ Loads the saved state of the sync, if any
87+ '''
88+ super(Backend, self).__init__(parameters)
89+ #loading the saved state of the synchronization, if any
90+ self.sync_engine_path = os.path.join('backends/gtasks/', \
91+ "sync_engine-" + self.get_id())
92+ self.sync_engine = self._load_pickled_file(self.sync_engine_path, \
93+ SyncEngine())
94+ self._this_is_the_first_loop = True
95+ self.gtasks_proxy = GTasksProxy(self)
96+
97+ def get_tasklists(self):
98+ return self.gtasks_proxy.get_tasklists()
99+
100+ def get_liststore_data(self, parameter):
101+ """Needed by the combobox ui to supply liststore data"""
102+ data = []
103+ if "default-list-to-sync-to" == parameter:
104+ for k, v in self.gtasks_proxy.get_tasklists().iteritems():
105+ data.append((k,v))
106+ return data
107+
108+ def initialize(self):
109+ """
110+ See GenericBackend for an explanation of this function.
111+ """
112+ self._parameters[self.KEY_ENABLED] = self.has_valid_config()
113+ self._is_initialized = True
114+ # we signal that the backend has been enabled
115+ self._signal_manager.backend_state_changed(self.get_id())
116+
117+ def has_valid_config(self):
118+ d = self.get_parameters()['gtask-tag-and-list-selection']
119+ for iid, conf in d.iteritems():
120+ if conf.get('sync', False) == True:
121+ have_at_least_one_list_to_sync = True
122+ if conf.get('default', False) == True:
123+ return True
124+ return False
125+
126+ def save_state(self):
127+ """
128+ See GenericBackend for an explanation of this function.
129+ """
130+ self._store_pickled_file(self.sync_engine_path, self.sync_engine)
131+
132+###############################################################################
133+### TWO WAY SYNC ##############################################################
134+###############################################################################
135+
136+ def do_periodic_import(self):
137+ """
138+ See PeriodicImportBackend for an explanation of this function.
139+ """
140+
141+ #we get the old list of synced tasks, and compare with the new tasks set
142+ stored_gtasks_ids = self.sync_engine.get_all_remote()
143+ current_gtask_ids = [gtask_id for gtask_id in \
144+ self.gtasks_proxy.get_gtasks_dict().iterkeys()]
145+ if self._this_is_the_first_loop:
146+ self._on_successful_authentication()
147+
148+ #If it's the very first time the backend is run, it's possible that the
149+ # user already synced his tasks in some way (but we don't know that).
150+ # Therefore, we attempt to induce those tasks relationships matching the
151+ # titles.
152+ if self._parameters["is-first-run"] and False:
153+ gtg_titles_dic = {}
154+ for tid in self.datastore.get_all_tasks():
155+ gtg_task = self.datastore.get_task(tid)
156+ if not self._gtg_task_is_syncable_per_attached_tags(gtg_task):
157+ continue
158+ gtg_title = gtg_task.get_title()
159+ if gtg_titles_dic.has_key(gtg_title):
160+ gtg_titles_dic[gtg_task.get_title()].append(tid)
161+ else:
162+ gtg_titles_dic[gtg_task.get_title()] = [tid]
163+ for gtask_id in current_gtask_ids:
164+ gtask = self.gtasks_proxy.get_task(gtask_id)
165+ try:
166+ tids = gtg_titles_dic[gtask.get_title()]
167+ #we remove the tid, so that it can't be linked to two
168+ # different gtasks tasks
169+ tid = tids.pop()
170+ gtg_task = self.datastore.get_task(tid)
171+ meme = SyncMeme(gtg_task.get_modified(),
172+ gtask.get_modified(),
173+ "GTG")
174+ self.sync_engine.record_relationship( \
175+ local_id = tid,
176+ remote_id = gtask.get_id(),
177+ meme = meme)
178+ except KeyError:
179+ pass
180+ except IndexError:
181+ pass
182+ #a first run has been completed successfully
183+ self._parameters["is-first-run"] = False
184+
185+ for gtask_id in current_gtask_ids:
186+ self.cancellation_point()
187+ #Adding and updating
188+ self._process_gtask(gtask_id)
189+
190+ for gtask_id in set(stored_gtasks_ids).difference(\
191+ set(current_gtask_ids)):
192+ self.cancellation_point()
193+ if not self.please_quit:
194+ tid = self.sync_engine.get_local_id(gtask_id)
195+ self.datastore.request_task_deletion(tid)
196+ try:
197+ self.sync_engine.break_relationship(remote_id = \
198+ gtask_id)
199+ self.save_state()
200+ except KeyError:
201+ pass
202+
203+ def _on_successful_authentication(self):
204+ '''
205+ Saves the token and requests a full flush on first autentication
206+ '''
207+ self._this_is_the_first_loop = False
208+ #we ask the Datastore to flush all the tasks on us
209+ threading.Timer(10,
210+ self.datastore.flush_all_tasks,
211+ args =(self.get_id(),)).start()
212+
213+ @interruptible
214+ def remove_task(self, tid):
215+ """
216+ See GenericBackend for an explanation of this function.
217+ """
218+ if not self.gtasks_proxy.is_authenticated():
219+ return
220+ self.cancellation_point()
221+ try:
222+ gtask_id = self.sync_engine.get_remote_id(tid)
223+ if gtask_id not in self.gtasks_proxy.get_gtasks_dict():
224+ #we might need to refresh our task cache
225+ self.gtasks_proxy.refresh_gtasks_dict()
226+ gtask = self.gtasks_proxy.get_task(gtask_id)
227+ self.gtasks_proxy.delete(gtask)
228+ Log.debug("removing task %s from GTasks" % gtask)
229+ except KeyError:
230+ try:
231+ self.sync_engine.break_relationship(local_id = tid)
232+ self.save_state()
233+ except:
234+ pass
235+
236+ def delete(self, gtask):
237+ self.remove_task(gtask.get_id())
238+
239+###############################################################################
240+### Process tasks #############################################################
241+###############################################################################
242+
243+ @interruptible
244+ def set_task(self, task):
245+ """
246+ See GenericBackend for an explanation of this function.
247+ """
248+ if not self.gtasks_proxy.is_authenticated():
249+ return
250+ self.cancellation_point()
251+ tid = task.get_id()
252+ is_syncable = self._gtg_task_is_syncable_per_attached_tags(task)
253+ action, gtask_id = self.sync_engine.analyze_local_id( \
254+ tid, \
255+ self.datastore.has_task, \
256+ self.gtasks_proxy.has_gtask, \
257+ is_syncable)
258+ if action == None:
259+ return
260+
261+ if action == SyncEngine.ADD:
262+ Log.debug("GTG->GTasks set task (%s, %s) ADD" % (action, is_syncable))
263+ if task.get_status() != Task.STA_ACTIVE:
264+ #OPTIMIZATION:
265+ #we don't sync tasks that have already been closed before we
266+ # even synced them once
267+ return
268+ gtask = self.gtasks_proxy.create_new_gtask(task.get_title())
269+ gtask.set_text("added via set task")
270+ try:
271+ self._populate_gtask(task, gtask)
272+ except:
273+ self.gtasks_proxy.delete(gtask)
274+ raise
275+ meme = SyncMeme(task.get_modified(),
276+ gtask.get_modified(),
277+ "GTG")
278+ self.sync_engine.record_relationship( \
279+ local_id = tid, remote_id = gtask.get_id(), meme = meme)
280+ self.gtasks_proxy.update(gtask)
281+
282+ elif action == SyncEngine.UPDATE:
283+ Log.debug("GTG->GTasks set task (%s, %s) UPDATE" % (action, is_syncable))
284+ gtask = self.gtasks_proxy.get_task(gtask_id)
285+ with self.datastore.get_backend_mutex():
286+ meme = self.sync_engine.get_meme_from_local_id(task.get_id())
287+ newest = meme.which_is_newest(task.get_modified(),
288+ gtask.get_modified())
289+ if newest == "local":
290+ self._populate_gtask(task, gtask)
291+ meme.set_remote_last_modified(gtask.get_modified())
292+ meme.set_local_last_modified(task.get_modified())
293+ else:
294+ #we skip saving the state
295+ return
296+
297+ elif action == SyncEngine.REMOVE:
298+ Log.debug("GTG->GTasks set task (%s, %s) REMOVE" % (action, is_syncable))
299+ self.datastore.request_task_deletion(tid)
300+ try:
301+ self.sync_engine.break_relationship(local_id = tid)
302+ except KeyError:
303+ pass
304+ gtask = self.gtasks_proxy.get_task(gtask_id)
305+ self.gtasks_proxy.task_delete(gtask.get_tasklist(), gtask.get_id())
306+
307+ elif action == SyncEngine.LOST_SYNCABILITY:
308+ try:
309+ gtask = self.gtasks_proxy.get_gtasks_dict()[gtask]
310+ except KeyError:
311+ #in this case, we don't have yet the task in our local cache
312+ # of what's on the gtasks website
313+ self.gtasks_proxy.refresh_gtasks_dict()
314+ gtask = self.gtasks_proxy.get_gtasks_dict()[gtask]
315+ self._exec_lost_syncability(tid, gtask)
316+
317+ self.save_state()
318+
319+ def _exec_lost_syncability(self, tid, gtask):
320+ '''
321+ Executed when a relationship between tasks loses its syncability
322+ property. See SyncEngine for an explanation of that.
323+
324+ @param tid: a GTG task tid
325+ @param note: a GTasks task
326+ '''
327+ self.cancellation_point()
328+ meme = self.sync_engine.get_meme_from_local_id(tid)
329+ #First of all, the relationship is lost
330+ self.sync_engine.break_relationship(local_id = tid)
331+ if meme.get_origin() == "GTG":
332+ self.delete(gtask)
333+ else:
334+ self.datastore.request_task_deletion(tid)
335+
336+ def _process_gtask(self, gtask_id):
337+ '''
338+ Takes a gtasks task id and carries out the necessary operations to
339+ refresh the sync state
340+ '''
341+ self.cancellation_point()
342+ if not self.gtasks_proxy.is_authenticated():
343+ return
344+ gtask = self.gtasks_proxy.get_task(gtask_id)
345+ is_syncable = self._gtask_is_syncable_per_attached_tags(gtask)
346+
347+ action, tid = self.sync_engine.analyze_remote_id( \
348+ gtask_id,
349+ self.datastore.has_task,
350+ self.gtasks_proxy.has_gtask,
351+ is_syncable)
352+
353+ if action == None:
354+ return
355+
356+ Log.debug("GTG<-GTasks set task (%s, %s)" % (action, is_syncable))
357+
358+ if action == SyncEngine.ADD:
359+ if gtask.get_status() != Task.STA_ACTIVE:
360+ #OPTIMIZATION:
361+ #we don't sync tasks that have already been closed before we
362+ # even saw them
363+ return
364+ tid = str(uuid.uuid4())
365+ task = self.datastore.task_factory(tid)
366+ self._populate_task(task, gtask)
367+ meme = SyncMeme(task.get_modified(),
368+ gtask.get_modified(),
369+ "GTasks")
370+ self.sync_engine.record_relationship( \
371+ local_id = tid,
372+ remote_id = gtask.get_id(),
373+ meme = meme)
374+ self.datastore.push_task(task)
375+
376+ elif action == SyncEngine.UPDATE:
377+ task = self.datastore.get_task(tid)
378+ with self.datastore.get_backend_mutex():
379+ meme = self.sync_engine.get_meme_from_remote_id(gtask.get_id())
380+ newest = meme.which_is_newest(task.get_modified(),
381+ gtask.get_modified())
382+ if newest == "remote":
383+ self._populate_task(task, gtask)
384+ meme.set_remote_last_modified(gtask.get_modified())
385+ meme.set_local_last_modified(task.get_modified())
386+ else:
387+ #we skip saving the state
388+ return
389+
390+ elif action == SyncEngine.REMOVE:
391+ try:
392+ self.delete(gtask)
393+ self.sync_engine.break_relationship(remote_id = gtask.get_id())
394+ except KeyError:
395+ pass
396+
397+ elif action == SyncEngine.LOST_SYNCABILITY:
398+ self._exec_lost_syncability(tid, gtask)
399+
400+ self.save_state()
401+
402+###############################################################################
403+### Helper methods ############################################################
404+###############################################################################
405+
406+ def _populate_task(self, task, gtask):
407+ '''
408+ Copies the content of a GTasksTask in a Task
409+ '''
410+ task.set_title(gtask.get_title())
411+ task.set_text(gtask.get_text())
412+ task.set_due_date(gtask.get_due_date())
413+ status = gtask.get_status()
414+ if GTG_TO_GTasks_STATUS[task.get_status()] != status:
415+ task.set_status(gtask.get_status())
416+ #tags
417+ tags = set(['@%s' % tag for tag in gtask.get_tags()])
418+ gtg_tags_lower = set([t.get_name().lower() for t in task.get_tags()])
419+ #tags to remove
420+ for tag in gtg_tags_lower.difference(tags):
421+ task.remove_tag(tag)
422+ #tags to add
423+ for tag in tags.difference(gtg_tags_lower):
424+ gtg_all_tags = self.datastore.get_all_tags()
425+ matching_tags = filter(lambda t: t.lower() == tag, gtg_all_tags)
426+ if len(matching_tags) != 0:
427+ tag = matching_tags[0]
428+ task.add_tag(tag)
429+
430+ def _populate_gtask(self, task, gtask):
431+ '''
432+ Copies the content of a Task into a GTasksTask
433+
434+ @param task: a GTG Task
435+ @param gtask: an GTasksTask
436+ '''
437+ #Get methods of an gtask are fast, set are slow: therefore,
438+ # we try to use set as rarely as possible
439+
440+ #first thing: the status. This way, if we are syncing a completed
441+ # task it doesn't linger for ten seconds in the GTasks Inbox
442+ status = task.get_status()
443+ if gtask.get_status() != status:
444+ gtask.set_status(status)
445+ title = task.get_title()
446+ if gtask.get_title() != title:
447+ gtask.set_title(title)
448+ text = task.get_excerpt(strip_tags = True, strip_subtasks = True)
449+ if gtask.get_text() != text:
450+ gtask.set_text(text)
451+ tags = task.get_tags_name()
452+ gtask_tags = []
453+ for tag in gtask.get_tags():
454+ if tag[0] != '@':
455+ tag = '@' + tag
456+ gtask_tags.append(tag)
457+ #gtasks tags are lowercase only
458+ if gtask_tags != [t.lower() for t in tags]:
459+ gtask.set_tags(tags)
460+ due_date = task.get_due_date()
461+ if gtask.get_due_date() != due_date:
462+ gtask.set_due_date(due_date)
463+ self.gtasks_proxy.update(gtask)
464+
465+ def _gtask_is_syncable_per_attached_tags(self, gtask):
466+ '''
467+ Helper function which checks if the given task satisfies the filtering
468+ imposed by the tags attached to the backend.
469+ That means, if a user wants a backend to sync only tasks tagged @works,
470+ this function should be used to check if that is verified.
471+
472+ @returns bool: True if the task should be synced
473+ '''
474+ attached_tags = self.get_attached_tags()
475+ if GenericBackend.ALLTASKS_TAG in attached_tags:
476+ return True
477+ for tag in gtask.get_tags():
478+ if "@" + tag in attached_tags:
479+ return True
480+ return False
481+
482+###############################################################################
483+### GTasks PROXY #################################################################
484+###############################################################################
485+
486+class GTasksProxy(object):
487+ '''
488+ The purpose of this class is producing an updated list of GTasksTasks.
489+ To do that, it handles:
490+ - authentication to GTasks
491+ - keeping the list fresh
492+ - downloading the list
493+ '''
494+
495+ def __init__(self, backend):
496+ self.backend = backend
497+ self.authenticated = threading.Event()
498+ self.is_not_refreshing = threading.Event()
499+ self.is_not_refreshing.set()
500+ self.gtasks = GTasks(storage_dir = os.path.join(xdg_config_home, 'gtg/'))
501+
502+ ##########################################################################
503+ ### AUTHENTICATION #######################################################
504+ ##########################################################################
505+
506+ def start_authentication(self):
507+ '''
508+ Launches the authentication process
509+ '''
510+ initialize_thread = threading.Thread(target = self._authenticate)
511+ initialize_thread.setDaemon(True)
512+ initialize_thread.start()
513+
514+ def is_authenticated(self):
515+ '''
516+ Returns true if we've autheticated to GTasks
517+ '''
518+ return self.authenticated.isSet()
519+
520+ def wait_for_authentication(self):
521+ '''
522+ Inhibits the thread until authentication occours
523+ '''
524+ self.authenticated.wait()
525+
526+ def _authenticate(self):
527+ '''
528+ authentication main function
529+ '''
530+ self.authenticated.clear()
531+ while not self.authenticated.isSet():
532+ if not self.gtasks.is_valid():
533+ self.gtasks.run_flow()
534+ if self.gtasks.is_valid():
535+ self.authenticated.set()
536+ else:
537+ BackendSignals().backend_failed(self.get_id(), \
538+ BackendSignals.ERRNO_NETWORK)
539+
540+ ##########################################################################
541+ ### GTasks Tasklist HANDLING #############################################
542+ ##########################################################################
543+
544+ def get_target_list(self, task):
545+ """Find out if we confed one of the lists with tags,
546+ and if the current task has this tag add it to the first list matching
547+ otherwise use default list
548+ """
549+ for iid, conf in self.backend.get_parameters()['gtask-tag-and-list-selection'].iteritems():
550+ gtags = self._get_tags_from_string(conf.get('tags', ""))
551+ for gtag in gtags:
552+ if gtag in task.get_tags():
553+ return iid
554+ return self.get_default_tasklist()
555+
556+ def _get_tags_from_string(self, string):
557+ tags = string.split(",")
558+ # stripping spaces
559+ tags = map(lambda t: t.strip(), tags)
560+ # removing empty tags
561+ return filter(lambda t: t, tags)
562+
563+ def get_default_tasklist(self):
564+ for iid, conf in self.backend.get_parameters()['gtask-tag-and-list-selection'].iteritems():
565+ if conf.get('default', False) == True:
566+ return iid
567+ raise Exception('Could not find a default list')
568+
569+ def get_tasklists(self):
570+ self.tasklists = {}
571+ for item in self.gtasks.tasklists_list().get('items'):
572+ self.tasklists[str(item.get('id'))] = str(item.get('title'))
573+ return self.tasklists
574+
575+ def list_tasks(self, tasklist):
576+ return self.gtasks.tasks_list(tasklist).get('items')
577+
578+ ##########################################################################
579+ ### GTasks Task HANDLING #############################################
580+ ##########################################################################
581+
582+
583+ def update(self, gtask):
584+ body = gtask.gtask_dict
585+ self.gtasks.task_update(gtask.get_tasklist(), gtask.get_id(), body)
586+
587+ def get_task(self, gtask_id):
588+ try:
589+ gtask = self.get_gtasks_dict()[gtask_id]
590+ except KeyError:
591+ #in this case, we don't have yet the task in our local cache
592+ # of what's on the gtasks website
593+ self.refresh_gtasks_dict()
594+ gtask = self.get_gtasks_dict()[gtask_id]
595+ return gtask
596+
597+ def get_gtasks_dict(self):
598+ '''
599+ Returns a dict of GTasksTasks. It will start authentication if necessary.
600+ The dict is kept updated automatically.
601+ '''
602+ if not hasattr(self, '_gtask_dict'):
603+ self.refresh_gtasks_dict()
604+ else:
605+ time_difference = datetime.datetime.now() - \
606+ self.__gtask_dict_timestamp
607+ if time_difference.seconds > 60:
608+ self.refresh_gtasks_dict()
609+ return self._gtask_dict.copy()
610+
611+ def refresh_gtasks_dict(self):
612+ '''
613+ Builds a list of GTasksTasks fetched from GTasks
614+ '''
615+ if not self.is_authenticated():
616+ self.start_authentication()
617+ self.wait_for_authentication()
618+
619+ if not self.is_not_refreshing.isSet():
620+ #if we're already refreshing, we just wait for that to happen and
621+ # then we immediately return
622+ self.is_not_refreshing.wait()
623+ return
624+
625+ self.is_not_refreshing.clear()
626+ Log.debug('refreshing gtasks')
627+ #our purpose is to fill this with "tasks_id: GTasksTask" items
628+ gtasks_dict = {}
629+
630+ gtasklists_list = self.get_tasklists()
631+ #for each gtasks list, we retrieve all the tasks in it
632+ for gtasklist in gtasklists_list:
633+ gtasks_list = self.list_tasks(gtasklist)
634+ for gtask in gtasks_list:
635+ gtasks_dict[gtask.get('id')] = GTasksTask(gtask,
636+ gtasklist)
637+
638+ #we're done: we store the dict in this class and we annotate the time we
639+ # got it
640+ self._gtask_dict = gtasks_dict
641+ self.__gtask_dict_timestamp = datetime.datetime.now()
642+ self.is_not_refreshing.set()
643+
644+ def has_gtask(self, gtask_id):
645+ '''
646+ Returns True if we have seen that task id
647+ '''
648+ cache_result = gtask_id in self.get_gtasks_dict()
649+ return cache_result
650+
651+ def create_new_gtask(self, title):
652+ '''
653+ Creates a new gtasks task
654+ '''
655+
656+ result = self.gtasks.task_create(tasklist=self.get_default_tasklist(), title=title)
657+ gtask = GTasksTask(result,
658+ self.get_default_tasklist())
659+ #adding to the dict right away
660+ if hasattr(self, '_gtask_dict'):
661+ #if the list hasn't been downloaded yet, we do not create a list,
662+ # because the fact that the list is created is used to keep track of
663+ # list updates
664+ self._gtask_dict[gtask.get_id()] = gtask
665+ return gtask
666+
667+ def delete(self, gtask):
668+ self.gtasks.task_delete(gtask.get_tasklist(), gtask.get_id())
669+
670+###############################################################################
671+### GTasks TASK ##################################################################
672+###############################################################################
673+
674+#dictionaries to translate a GTasks status into a GTG one (and back)
675+GTG_TO_GTasks_STATUS = {Task.STA_ACTIVE: 'needsAction',
676+ Task.STA_DONE: 'completed',
677+ Task.STA_DISMISSED: 'deleted'}
678+
679+GTasks_TO_GTG_STATUS = {'needsAction': Task.STA_ACTIVE,
680+ 'completed': Task.STA_DONE}
681+
682+
683+
684+class GTasksTask(object):
685+ '''
686+ A proxy object that encapsulates a GTasks task, giving an easier API to access
687+ and modify its attributes.
688+ This backend already uses a library to interact with GTasks, but that is just a
689+ thin proxy for HTML gets and posts.
690+ The meaning of all "special words"
691+
692+ http://www.rememberthemilk.com/services/api/tasks.gtasks
693+ '''
694+
695+
696+ def __init__(self, gtask, gtasklist):
697+ '''
698+ sets up the various parameters needed to interact with a task.
699+
700+ @param task: the task object given by the underlying library
701+ @param gtasklist: the gtasks list the task resides in.
702+ @param gtasks: a handle of the gtasks object, to be able to speak with gtasks.
703+ Authentication should have already been done.
704+ '''
705+ self.gtask_dict = gtask
706+ self.gtasklist = gtasklist
707+
708+ def get_tasklist(self):
709+ return self.gtasklist
710+
711+ def get_title(self):
712+ '''Returns the title of the task, if any'''
713+ title = self.gtask_dict.get('title')
714+ if "(no title task)" == title:
715+ return ""
716+ return title
717+
718+ def set_title(self, title):
719+ '''Sets the task title'''
720+ title = cgi.escape(title)
721+ self.gtask_dict['title'] = title
722+
723+ def get_id(self):
724+ '''Return the task id. The taskseries id is *different*'''
725+ return self.gtask_dict.get('id')
726+
727+ def get_status(self):
728+ '''Returns the task status, in GTG terminology'''
729+ return GTasks_TO_GTG_STATUS[self.gtask_dict.get('status')]
730+
731+ def set_status(self, gtg_status):
732+ '''Sets the task status, in GTG terminology'''
733+ status = GTG_TO_GTasks_STATUS[gtg_status]
734+ self.gtask_dict['status'] = status
735+
736+ def get_tags(self):
737+ '''Returns the task tags
738+ TODO: implement parsing of txt tot get tags... gtasks does not support tags natively'''
739+ return []
740+
741+ def set_tags(self, tags):
742+ '''
743+ Sets a new set of tags to a task. Old tags are deleted.
744+ TODO: implement parsing of txt tot get tags... gtasks does not support tags natively
745+ '''
746+ pass
747+
748+ def get_text(self):
749+ '''
750+ Gets the content of GTasks notes, aggregated in a single string
751+ '''
752+ return self.gtask_dict.get('notes', '')
753+
754+ def set_text(self, notes):
755+ '''
756+ deletes all the old notes in a task and sets a single note with the
757+ given text
758+ '''
759+ notes = cgi.escape(notes)
760+ self.gtask_dict['notes'] = notes
761+
762+ def get_due_date(self):
763+ '''
764+ Gets the task due date
765+ '''
766+ due = self.gtask_dict.get('due')
767+ if not due:
768+ return Date.no_date()
769+ date = self.__time_gtasks_to_datetime(due).date()
770+ return Date(date)
771+
772+ def set_due_date(self, due):
773+ '''
774+ Sets the task due date
775+ '''
776+ due = self.__time_date_to_gtasks(due)
777+ self.gtask_dict['due'] = due
778+
779+ def get_modified(self):
780+ '''
781+ Gets the task modified time, in local time
782+ TODO: #correct
783+ '''
784+ return self.__time_gtasks_to_datetime(self.gtask_dict.get('updated'))
785+
786+ def __tz_utc_to_local(self, dt):
787+ dt = dt.replace(tzinfo = tzutc())
788+ dt = dt.astimezone(tzlocal())
789+ return dt.replace(tzinfo = None)
790+
791+ def __tz_local_to_utc(self, dt):
792+ dt = dt.replace(tzinfo = tzlocal())
793+ dt = dt.astimezone(tzutc())
794+ return dt.replace(tzinfo = None)
795+
796+ def __time_gtasks_to_datetime(self, string):
797+ string = string.split('.')[0].split('Z')[0]
798+ dt = datetime.datetime.strptime(string.split(".")[0], \
799+ "%Y-%m-%dT%H:%M:%S")
800+ return self.__tz_utc_to_local(dt)
801+
802+ def __time_gtasks_to_date(self, string):
803+ string = string.split('.')[0].split('Z')[0]
804+ dt = datetime.datetime.strptime(string.split(".")[0], "%Y-%m-%d")
805+ return self.__tz_utc_to_local(dt)
806+
807+ def __time_datetime_to_gtasks(self, timeobject):
808+ if not timeobject:
809+ return ""
810+ timeobject = self.__tz_local_to_utc(timeobject)
811+ return timeobject.strftime("%Y-%m-%dT%H:%M:%S")
812+
813+ def __time_date_to_gtasks(self, timeobject):
814+ if not timeobject:
815+ return ""
816+ return timeobject.strftime("%Y-%m-%dT00:00:00.000Z")
817+
818+ def __str__(self):
819+ return "Task %s (%s)" % (self.get_title(), self.get_id())
820
821=== modified file 'GTG/backends/genericbackend.py'
822--- GTG/backends/genericbackend.py 2013-02-25 08:12:02 +0000
823+++ GTG/backends/genericbackend.py 2013-02-26 14:40:31 +0000
824@@ -25,6 +25,7 @@
825 import os
826 import errno
827 import pickle
828+import base64
829 import threading
830 from collections import deque
831
832@@ -195,6 +196,7 @@
833 # keyring
834 # This is just a key to find it there
835 TYPE_STRING = "string" # generic string, nothing fancy is done
836+ TYPE_DICT = "dict" # generic string, nothing fancy is done
837 TYPE_INT = "int" # edit box can contain only integers
838 TYPE_BOOL = "bool" # checkbox is shown
839 TYPE_LIST_OF_STRINGS = "liststring" # list of strings. the "," character
840@@ -408,6 +410,8 @@
841 if not isinstance(the_list, list):
842 the_list = [the_list]
843 return the_list
844+ elif param_type == cls.TYPE_DICT:
845+ return pickle.loads(base64.b64decode(param_value))
846 else:
847 raise NotImplemented("I don't know what type is '%s'" %
848 param_type)
849@@ -430,6 +434,8 @@
850 if param_value == []:
851 return ""
852 return reduce(lambda a, b: a + "," + b, param_value)
853+ elif param_type == GenericBackend.TYPE_DICT:
854+ return base64.b64encode(pickle.dumps(param_value))
855 else:
856 return str(param_value)
857
858
859=== added directory 'GTG/backends/gtasks'
860=== added file 'GTG/backends/gtasks/__init__.py'
861=== added file 'GTG/backends/gtasks/client_secrets.json'
862--- GTG/backends/gtasks/client_secrets.json 1970-01-01 00:00:00 +0000
863+++ GTG/backends/gtasks/client_secrets.json 2013-02-26 14:40:31 +0000
864@@ -0,0 +1,1 @@
865+{"installed":{"auth_uri":"https://accounts.google.com/o/oauth2/auth","client_secret":"CTOygFfphbQvJUiC6GiKFXFx","token_uri":"https://accounts.google.com/o/oauth2/token","client_email":"","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","oob"],"client_x509_cert_url":"","client_id":"475130774009-9e3ddktfbfh2retat461nqr9ra4s42ad.apps.googleusercontent.com","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs"}}
866
867=== added file 'GTG/backends/gtasks/gtasks.py'
868--- GTG/backends/gtasks/gtasks.py 1970-01-01 00:00:00 +0000
869+++ GTG/backends/gtasks/gtasks.py 2013-02-26 14:40:31 +0000
870@@ -0,0 +1,170 @@
871+
872+# -*- coding: utf-8 -*-
873+#
874+# Copyright (C) 2012 Google Inc.
875+#
876+# Licensed under the Apache License, Version 2.0 (the "License");
877+# you may not use this file except in compliance with the License.
878+# You may obtain a copy of the License at
879+#
880+# http://www.apache.org/licenses/LICENSE-2.0
881+#
882+# Unless required by applicable law or agreed to in writing, software
883+# distributed under the License is distributed on an "AS IS" BASIS,
884+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
885+# See the License for the specific language governing permissions and
886+# limitations under the License.
887+
888+# Python library for Google Tasks
889+# @author: Philipp A. Mohrenweiser
890+# <phiamo@googlemail.com>
891+# http://gtasks.mohrenweiserpartner.de
892+
893+# For more information on the Tasks API API you can visit:
894+#
895+# http://code.google.com/apis/tasks/v1/using.html
896+#
897+# For more information on the Tasks API API python library surface you
898+# can visit:
899+#
900+# https://google-api-client-libraries.appspot.com/documentation/tasks/v1/python/latest/
901+#
902+# For information on the Python Client Library visit:
903+#
904+# https://developers.google.com/api-client-library/python/start/get_started
905+
906+import gflags
907+import httplib2
908+import logging
909+import os
910+import pprint
911+import sys
912+
913+from apiclient.discovery import build
914+from oauth2client.file import Storage
915+from oauth2client.client import AccessTokenRefreshError
916+from oauth2client.client import flow_from_clientsecrets
917+from oauth2client.tools import run
918+from xdg.BaseDirectory import xdg_config_home
919+
920+
921+FLAGS = gflags.FLAGS
922+
923+# support user credentials
924+user_credentials = os.path.join(xdg_config_home, 'gtg/backends/gtasks/client_secrets.json')
925+if os.path.exists(user_credentials):
926+ CLIENT_SECRETS = user_credentials
927+else:
928+ CLIENT_SECRETS = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'client_secrets.json')
929+
930+# Set up a Flow object to be used for authentication.
931+# Add one or more of the following scopes. PLEASE ONLY ADD THE SCOPES YOU
932+# NEED. For more information on using scopes please see
933+# <https://developers.google.com/+/best-practices>.
934+FLOW = flow_from_clientsecrets(CLIENT_SECRETS,
935+ scope=[
936+ 'https://www.googleapis.com/auth/tasks',
937+ 'https://www.googleapis.com/auth/tasks.readonly',
938+ ],
939+ message="""You can add a own credentials file to:
940+ %s
941+ Get it from https://code.google.com/apis/console/
942+ """ % (user_credentials))
943+
944+# Filename for token
945+FILENAME = 'gmail_tags.dat'
946+
947+class GTasksError(Exception): pass
948+
949+class GTasksAPIError(GTasksError): pass
950+
951+class GTasks:
952+ service = None
953+
954+ def __init__(self, storage_dir=None):
955+ if not storage_dir or not os.path.exists(storage_dir):
956+ storage_dir = "."
957+ path = os.path.join(storage_dir, FILENAME)
958+ self.storage = Storage(path)
959+ self.credentials = self.storage.get()
960+
961+ def get_service(self):
962+ if not self.service:
963+ if not self.is_valid():
964+ self.run_flow()
965+ http = httplib2.Http()
966+ self.http = self.credentials.authorize(http)
967+ self.service = build('tasks', 'v1', http=self.http)
968+ return self.service
969+
970+ def is_valid(self):
971+ if self.credentials is None or self.credentials.invalid:
972+ return False
973+ return True
974+
975+ def run_flow(self):
976+ self.credentials = run(FLOW, self.storage)
977+
978+ def refresh(self):
979+ self.credentials.refresh()
980+
981+ def execute(self, request):
982+ try:
983+ return request.execute(http=self.http)
984+ except AccessTokenRefreshError:
985+ self.auth()
986+
987+ def tasklists_list(self):
988+ return self.execute(self.get_service().tasklists().list())
989+
990+ def task_set_title(self, tasklist, task, title):
991+ return self.execute(self.get_service().tasks().patch(tasklist=tasklist,task=task,body={title:title}))
992+
993+ def tasks_list(self, tasklist, showCompleted=None, dueMin=None, dueMax=None, showDeleted=None, updatedMin=None, pageToken=None, completedMax=None, maxResults=None, completedMin=None, showHidden=None):
994+ return self.execute(self.get_service().tasks().list(tasklist=tasklist, showCompleted=showCompleted, dueMin=dueMin, dueMax=dueMax, showDeleted=showDeleted, updatedMin=updatedMin, pageToken=pageToken, completedMax=completedMax, maxResults=maxResults, completedMin=completedMin, showHidden=showHidden))
995+
996+ def task_create(self, tasklist, title, notes=""):
997+ body = {
998+ "status": "needsAction", # Status of the task. This is either "needsAction" or "completed".
999+ "kind": "tasks#task", # Type of the resource. This is always "tasks#task".
1000+# "parent": "A String", # Parent task identifier. This field is omitted if it is a top-level task. This field is read-only. Use the "move" method to move the task under a different parent or to the top level.
1001+ "title": title, # Title of the task.
1002+ "deleted": False, # Flag indicating whether the task has been deleted. The default if False.
1003+# "completed": "A String", # Completion date of the task (as a RFC 3339 timestamp). This field is omitted if the task has not been completed.
1004+# "updated": "A String", # Last modification time of the task (as a RFC 3339 timestamp).
1005+# "due": "A String", # Due date of the task (as a RFC 3339 timestamp). Optional.
1006+# "etag": "A String", # ETag of the resource.
1007+# "id": "A String", # Task identifier.
1008+# "position": "A String", # String indicating the position of the task among its sibling tasks under the same parent task or at the top level. If this string is greater than another task's corresponding position string according to lexicographical ordering, the task is positioned after the other task under the same parent task (or at the top level). This field is read-only. Use the "move" method to move the task to another position.
1009+# "hidden": True or False, # Flag indicating whether the task is hidden. This is the case if the task had been marked completed when the task list was last cleared. The default is False. This field is read-only.
1010+ "notes": notes, # Notes describing the task. Optional.
1011+# "selfLink": "A String", # URL pointing to this task. Used to retrieve, update, or delete this task.
1012+ }
1013+ return self.execute(self.get_service().tasks().insert(tasklist=tasklist,body=body))
1014+
1015+ def task_set_status(self, tasklist, task, status):
1016+ body = {
1017+ "status": status, # Status of the task. This is either "needsAction" or "completed".
1018+ }
1019+ return self.execute(self.get_service().tasks().patch(tasklist=tasklist,task=task, body=body))
1020+
1021+ def task_set_notes(self, tasklist, task, notes):
1022+ body = {
1023+ "notes": notes, # Status of the task. This is either "needsAction" or "completed".
1024+ }
1025+ return self.execute(self.get_service().tasks().patch(tasklist=tasklist,task=task, body=body))
1026+
1027+
1028+ def task_delete(self, tasklist, task):
1029+ return self.execute(self.get_service().tasks().delete(tasklist=tasklist,task=task))
1030+
1031+ def task_set_due(self, tasklist, task, due):
1032+ body = {
1033+ "due": due, # Status of the task. This is either "needsAction" or "completed".
1034+ }
1035+ return self.execute(self.get_service().tasks().patch(tasklist=tasklist,task=task, body=body))
1036+
1037+ def task_update(self, tasklist, task, body):
1038+ print body
1039+ return self.execute(self.get_service().tasks().update(tasklist=tasklist,task=task, body=body))
1040+
1041
1042=== modified file 'GTG/gtk/backends_dialog/parameters_ui/__init__.py'
1043--- GTG/gtk/backends_dialog/parameters_ui/__init__.py 2013-02-25 07:35:07 +0000
1044+++ GTG/gtk/backends_dialog/parameters_ui/__init__.py 2013-02-26 14:40:31 +0000
1045@@ -28,13 +28,15 @@
1046 import functools
1047
1048 from GTG import _
1049-from GTG.backends.genericbackend import GenericBackend
1050+from GTG.backends.genericbackend import GenericBackend
1051 from GTG.gtk.backends_dialog.parameters_ui.importtagsui import ImportTagsUI
1052-from GTG.gtk.backends_dialog.parameters_ui.textui import TextUI
1053-from GTG.gtk.backends_dialog.parameters_ui.passwordui import PasswordUI
1054-from GTG.gtk.backends_dialog.parameters_ui.periodui import PeriodUI
1055-from GTG.gtk.backends_dialog.parameters_ui.checkboxui import CheckBoxUI
1056-from GTG.gtk.backends_dialog.parameters_ui.pathui import PathUI
1057+from GTG.gtk.backends_dialog.parameters_ui.textui import TextUI
1058+from GTG.gtk.backends_dialog.parameters_ui.passwordui import PasswordUI
1059+from GTG.gtk.backends_dialog.parameters_ui.periodui import PeriodUI
1060+from GTG.gtk.backends_dialog.parameters_ui.checkboxui import CheckBoxUI
1061+from GTG.gtk.backends_dialog.parameters_ui.comboboxui import ComboBoxUI
1062+from GTG.gtk.backends_dialog.parameters_ui.gtasklistsui import ConfigGtasksUI
1063+from GTG.gtk.backends_dialog.parameters_ui.pathui import PathUI
1064
1065
1066 class ParametersUI(gtk.VBox):
1067@@ -43,6 +45,7 @@
1068 widgets to view and edit a backend configuration
1069 '''
1070
1071+
1072 COMMON_WIDTH = 170
1073
1074 def __init__(self, requester):
1075@@ -54,75 +57,50 @@
1076 self.req = requester
1077 self.set_spacing(10)
1078
1079- # builds a list of widget generators. More precisely, it's a
1080+ #builds a list of widget generators. More precisely, it's a
1081 # list of tuples: (backend_parameter_name, widget_generator)
1082 self.parameter_widgets = (
1083- ("import-tags", self.UI_generator(ImportTagsUI,
1084- {"title": _("Import tags"),
1085- "anybox_text": _("All tags"),
1086- "somebox_text": _("Just these \
1087- tags:"),
1088- "parameter_name": "import-tags\
1089- "})),
1090- ("attached-tags", self.UI_generator(ImportTagsUI,
1091- {"title": _("Tags to sync"),
1092- "anybox_text": _("All tasks"),
1093- "somebox_text": _("Tasks with\
1094- these tags:"),
1095- "parameter_name":
1096- "attached-tags"})),
1097- ("path", self.UI_generator(PathUI)),
1098- ("username", self.UI_generator(TextUI,
1099- {"description": _("Username"),
1100- "parameter_name": "username"})),
1101- ("password", self.UI_generator(PasswordUI)),
1102- ("period", self.UI_generator(PeriodUI)),
1103- ("service-url", self.UI_generator(TextUI,
1104- {"description": _("Service URL"),
1105- "parameter_name": "service-url"
1106- })),
1107- ("import-from-replies", self.UI_generator(CheckBoxUI,
1108- {"text":
1109- _("Import tasks\
1110- from @ replies " +
1111+ ("import-tags", self.UI_generator(ImportTagsUI,
1112+ {"title": _("Import tags"),
1113+ "anybox_text": _("All tags"),
1114+ "somebox_text": _("Just these tags:"),
1115+ "parameter_name": "import-tags"})),
1116+ ("attached-tags", self.UI_generator(ImportTagsUI,
1117+ {"title": _("Tags to sync"),
1118+ "anybox_text": _("All tasks"),
1119+ "somebox_text": _("Tasks with these tags:"),
1120+ "parameter_name": "attached-tags"})),
1121+ ("path", self.UI_generator(PathUI)),
1122+ ("username", self.UI_generator(TextUI,
1123+ {"description": _("Username"),
1124+ "parameter_name": "username"})),
1125+ ("password", self.UI_generator(PasswordUI)),
1126+ ("period", self.UI_generator(PeriodUI)),
1127+ ("service-url", self.UI_generator(TextUI,
1128+ {"description": _("Service URL"),
1129+ "parameter_name": "service-url"})),
1130+ ("import-from-replies", self.UI_generator(CheckBoxUI,
1131+ {"text": _("Import tasks from @ replies " + \
1132 "directed to you"),
1133- "parameter":
1134- "import-from-replies"
1135- })),
1136- ("import-from-direct-messages", self.UI_generator(CheckBoxUI,
1137- {"text":
1138- _("Import tasks"
1139- "from direct "
1140- "messages"),
1141- "parameter":
1142- "import-from-\
1143- direct-messages\
1144- "})),
1145- ("import-from-my-tweets", self.UI_generator(CheckBoxUI,
1146- {"text": _("Import \
1147- tasks from \
1148- your tweets"),
1149- "parameter":
1150- "import-from-my-\
1151- tweets"})),
1152- ("import-bug-tags", self.UI_generator(CheckBoxUI,
1153- {"text": _("Tag your GTG "
1154- "tasks with the "
1155- "bug tags"),
1156- "parameter": "import-bug-"
1157- "tags"})),
1158- ("tag-with-project-name", self.UI_generator(CheckBoxUI,
1159- {"text":
1160- _("Tag your "
1161- "GTG tasks with "
1162- "the project"
1163- "targeted by the"
1164- " bug"),
1165- "parameter":
1166- "tag-with-project-"
1167- "name"})), )
1168+ "parameter": "import-from-replies"})),
1169+ ("import-from-direct-messages", self.UI_generator(CheckBoxUI,
1170+ {"text": _("Import tasks from direct messages"),
1171+ "parameter": "import-from-direct-messages"})),
1172+ ("import-from-my-tweets", self.UI_generator(CheckBoxUI,
1173+ {"text": _("Import tasks from your tweets"),
1174+ "parameter": "import-from-my-tweets"})),
1175+ ("import-bug-tags", self.UI_generator(CheckBoxUI,
1176+ {"text": _("Tag your GTG tasks with the bug tags"),
1177+ "parameter": "import-bug-tags"})),
1178+ ("tag-with-project-name", self.UI_generator(CheckBoxUI,
1179+ {"text": _("Tag your GTG tasks with the project "
1180+ "targeted by the bug"),
1181+ "parameter": "tag-with-project-name"})),
1182+ ("gtask-tag-and-list-selection", self.UI_generator(ConfigGtasksUI,
1183+ {"title": _("Please select lists to sync"),
1184+ "parameter": "gtask-tag-and-list-selection"})), )
1185
1186- def UI_generator(self, param_type, special_arguments={}):
1187+ def UI_generator(self, param_type, special_arguments = {}):
1188 '''A helper function to build a widget type from a template.
1189 It passes to the created widget generator a series of common
1190 parameters, plus the ones needed to specialize the given template
1191@@ -134,9 +112,9 @@
1192 @return function: return a widget generator, not a widget. the widget
1193 can be obtained by calling widget_generator(backend)
1194 '''
1195- return lambda backend: param_type(req=self.req,
1196- backend=backend,
1197- width=self.COMMON_WIDTH,
1198+ return lambda backend: param_type(req = self.req,
1199+ backend = backend,
1200+ width = self.COMMON_WIDTH,
1201 **special_arguments)
1202
1203 def refresh(self, backend):
1204@@ -145,14 +123,14 @@
1205
1206 @param backend: the backend that is being configured
1207 '''
1208- # remove the old parameters UIs
1209+ #remove the old parameters UIs
1210 def _remove_child(self, child):
1211 self.remove(child)
1212 self.foreach(functools.partial(_remove_child, self))
1213- # add new widgets
1214+ #add new widgets
1215 backend_parameters = backend.get_parameters()
1216 if backend_parameters[GenericBackend.KEY_DEFAULT_BACKEND]:
1217- # if it's the default backend, the user should not mess with it
1218+ #if it's the default backend, the user should not mess with it
1219 return
1220 for parameter_name, widget in self.parameter_widgets:
1221 if parameter_name in backend_parameters:
1222
1223=== added file 'GTG/gtk/backends_dialog/parameters_ui/comboboxui.py'
1224--- GTG/gtk/backends_dialog/parameters_ui/comboboxui.py 1970-01-01 00:00:00 +0000
1225+++ GTG/gtk/backends_dialog/parameters_ui/comboboxui.py 2013-02-26 14:40:31 +0000
1226@@ -0,0 +1,102 @@
1227+# -*- coding: utf-8 -*-
1228+# -----------------------------------------------------------------------------
1229+# Getting Things GNOME! - a personal organizer for the GNOME desktop
1230+# Copyright (c) 2008-2012 - Lionel Dricot & Bertrand Rousseau
1231+#
1232+# This program is free software: you can redistribute it and/or modify it under
1233+# the terms of the GNU General Public License as published by the Free Software
1234+# Foundation, either version 3 of the License, or (at your option) any later
1235+# version.
1236+#
1237+# This program is distributed in the hope that it will be useful, but WITHOUT
1238+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
1239+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
1240+# details.
1241+#
1242+# You should have received a copy of the GNU General Public License along with
1243+# this program. If not, see <http://www.gnu.org/licenses/>.
1244+# -----------------------------------------------------------------------------
1245+
1246+import gtk
1247+import unicodedata
1248+
1249+
1250+class ComboBoxUI(gtk.HBox):
1251+ '''
1252+ It's a widget displaying a simple combobox, with some text to explain its
1253+ meaning
1254+ '''
1255+
1256+ def __init__(self, req, backend, width, text, parameter):
1257+ '''
1258+ Creates the combobox and the related label.
1259+
1260+ @param req: a Requester
1261+ @param backend: a backend object
1262+ @param width: the width of the gtk.Label object
1263+ @param parameter: the backend parameter this combobox should display
1264+ and modify
1265+ '''
1266+ super(ComboBoxUI, self).__init__()
1267+ self.backend = backend
1268+ self.req = req
1269+ self.text = text
1270+ self.parameter = parameter
1271+ self._populate_gtk(width)
1272+
1273+ def _populate_gtk(self, width):
1274+ '''Creates the combobox and the related label
1275+
1276+ @param width: the width of the gtk.Label object
1277+ '''
1278+ # we expect a list of tuples, formed the same way all!s
1279+ data = self.backend.get_liststore_data(self.parameter)
1280+ types = [type(p) for p in data[0]]
1281+ name_store = gtk.ListStore(*types)
1282+ for d in data:
1283+ name_store.append(d)
1284+ self.combobox = gtk.ComboBox()
1285+ self.combobox.set_model(name_store)
1286+ self.combobox.set_entry_text_column(1)
1287+ self.combobox.connect("changed", self.on_modified)
1288+ renderer_text = gtk.CellRendererText()
1289+ self.combobox.pack_start(renderer_text, True)
1290+ self.combobox.add_attribute(renderer_text, "text", 1)
1291+ d_val = self.backend.get_parameters()[self.parameter]
1292+ if d_val != None:
1293+ i = 0
1294+ for mid, name in name_store:
1295+ if mid == d_val:
1296+ self.combobox.set_active(i)
1297+ i=i+1
1298+ else:
1299+ #activate default
1300+ self.combobox.set_active(0)
1301+ self.box = gtk.HBox(spacing=6)
1302+ self.add(self.box)
1303+ self.label = gtk.Label("Please select a list where to sync gtg tasks to")
1304+ self.box.pack_start(self.label)
1305+ self.box.pack_start(self.combobox)
1306+ self.commit_changes() # have a default value set while asap
1307+
1308+ def commit_changes(self):
1309+ '''Saves the changes to the backend parameter'''
1310+
1311+ tree_iter = self.combobox.get_active_iter()
1312+ if tree_iter != None:
1313+ model = self.combobox.get_model()
1314+ glist = model[tree_iter][0]
1315+ self.backend.set_parameter(self.parameter,
1316+ glist)
1317+
1318+
1319+ def on_modified(self, sender = None):
1320+ ''' Signal callback, executed when the user changes on the combobox.
1321+ Disables the backend. The user will re-enable it to confirm the changes
1322+ (s)he made.
1323+
1324+ @param sender: not used, only here for signal compatibility
1325+ '''
1326+ if self.backend.is_enabled() and not self.backend.is_default():
1327+ self.req.set_backend_enabled(self.backend.get_id(), False)
1328+ self.commit_changes()
1329
1330=== added file 'GTG/gtk/backends_dialog/parameters_ui/gtasklistsui.py'
1331--- GTG/gtk/backends_dialog/parameters_ui/gtasklistsui.py 1970-01-01 00:00:00 +0000
1332+++ GTG/gtk/backends_dialog/parameters_ui/gtasklistsui.py 2013-02-26 14:40:31 +0000
1333@@ -0,0 +1,174 @@
1334+# -*- coding: utf-8 -*-
1335+# -----------------------------------------------------------------------------
1336+# Getting Things GNOME! - a personal organizer for the GNOME desktop
1337+# Copyright (c) 2008-2012 - Lionel Dricot & Bertrand Rousseau
1338+#
1339+# This program is free software: you can redistribute it and/or modify it under
1340+# the terms of the GNU General Public License as published by the Free Software
1341+# Foundation, either version 3 of the License, or (at your option) any later
1342+# version.
1343+#
1344+# This program is distributed in the hope that it will be useful, but WITHOUT
1345+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
1346+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
1347+# details.
1348+#
1349+# You should have received a copy of the GNU General Public License along with
1350+# this program. If not, see <http://www.gnu.org/licenses/>.
1351+# -----------------------------------------------------------------------------
1352+
1353+import gtk
1354+
1355+from GTG.backends.genericbackend import GenericBackend
1356+
1357+
1358+class ConfigGtasksUI(gtk.VBox):
1359+ '''
1360+ It's a widget displaying a couple of radio buttons, a label and a textbox
1361+ to let the user change the attached tags (or imported)
1362+ '''
1363+
1364+ def __init__(self, req, backend, width, title,
1365+ parameter):
1366+ '''Populates the widgets and refresh the tags to display
1367+
1368+ @param req: a requester
1369+ @param backend: the backend to configure
1370+ @param width: the length of the radio buttons
1371+ @param title: the text for the label describing what this collection
1372+ of gtk widgets is used for
1373+ @param anybox_text: the text for the "Any tag matches" radio button
1374+ @param somebox_text: the text for the "only this set of tags matches"
1375+ radio button
1376+ @param parameter_name: the backend parameter this widget should modify
1377+ '''
1378+ super(ConfigGtasksUI, self).__init__()
1379+ self.backend = backend
1380+ self.req = req
1381+ self.title = title
1382+ self.parameter = parameter
1383+ self._refresh_tasklists()
1384+ self._populate_gtk(width)
1385+ self.check_config()
1386+
1387+ def _populate_gtk(self, width):
1388+ '''
1389+ Populates the widgets
1390+
1391+ @param width: the length of the radio buttons
1392+ '''
1393+ self.treeview_tasklists = gtk.TreeView(self.liststore)
1394+
1395+ # list names
1396+ renderer_editabletext = gtk.CellRendererText()
1397+ renderer_editabletext.set_property("editable", True)
1398+
1399+ column_editabletext = gtk.TreeViewColumn("Listname",
1400+ renderer_editabletext, text=1)
1401+ self.treeview_tasklists.append_column(column_editabletext)
1402+
1403+ renderer_editabletext.connect("edited", self.title_edited)
1404+
1405+ # sync_list
1406+ sync_toggle = gtk.CellRendererToggle()
1407+ sync_toggle.connect("toggled", self.on_sync_toggled)
1408+
1409+ column_toggle = gtk.TreeViewColumn("Sync", sync_toggle, active=2)
1410+ self.treeview_tasklists.append_column(column_toggle)
1411+
1412+ # default_list
1413+ default_toggle = gtk.CellRendererToggle()
1414+ default_toggle.connect("toggled", self.on_default_toggled)
1415+ default_toggle.set_radio(True)
1416+
1417+ column_toggle = gtk.TreeViewColumn("Default", default_toggle, active=3)
1418+ self.treeview_tasklists.append_column(column_toggle)
1419+
1420+ ### title
1421+ title_label = gtk.Label()
1422+ title_label.set_alignment(xalign=0, yalign=0)
1423+ title_label.set_markup("<big><b>%s</b></big>" % self.title)
1424+ self.pack_start(title_label, True)
1425+
1426+ align = gtk.Alignment(xalign=0, yalign=0, xscale=1)
1427+ align.set_padding(0, 0, 10, 0)
1428+ self.pack_start(align, True)
1429+ vbox = gtk.VBox()
1430+ align.add(vbox)
1431+ vbox.pack_start(self.treeview_tasklists, True)
1432+ # # error label
1433+ self.error_label = gtk.Label()
1434+ self.error_label.set_alignment(xalign=0, yalign=0)
1435+ self.error_label.set_markup('<span color="red">%s</span>' % 'You have to select at least one list to sync\n and make a syncing list the default list\nto add new entrys to')
1436+ self.error_label.hide()
1437+ self.pack_start(self.error_label, True)
1438+
1439+ def check_config(self):
1440+ if self.backend.has_valid_config():
1441+ self.error_label.hide()
1442+ else:
1443+ self.error_label.show()
1444+
1445+ def on_sync_toggled(self, radio, data=None):
1446+ ''' Signal callback, executed when the user modifies something.
1447+ Disables the backend. The user will re-enable it to confirm the changes
1448+ (s)he made.
1449+
1450+ @param sender: not used, only here for signal compatibility
1451+ @param data: not used, only here for signal compatibility
1452+ '''
1453+ self.req.set_backend_enabled(self.backend.get_id(), False)
1454+ self.liststore[data][2] = not self.liststore[data][2]
1455+ self._set_list_parameters(self.liststore[data][0], 'sync', self.liststore[data][2])
1456+
1457+ def on_default_toggled(self, radio, data=None):
1458+ ''' Signal callback, executed when the user modifies something.
1459+ Disables the backend. The user will re-enable it to confirm the changes
1460+ (s)he made.
1461+
1462+ @param sender: not used, only here for signal compatibility
1463+ @param data: not used, only here for signal compatibility
1464+ '''
1465+ self.req.set_backend_enabled(self.backend.get_id(), False)
1466+ i=0
1467+ for row in self.liststore:
1468+ row[3] = (i == int(data))
1469+ i=i+1
1470+ print "TYPE", type(row[3])
1471+ self._set_list_parameters(row[0], 'default', row[3])
1472+
1473+ def _set_list_parameters(self, iid, key, value):
1474+ d = self.backend.get_parameters()[self.parameter]
1475+ listconfig = d.get(iid, {})
1476+ listconfig[key] = value
1477+ d[iid] = listconfig
1478+ self.backend.set_parameter(self.parameter, d)
1479+ self.check_config()
1480+
1481+ def title_edited(self, radio, data=None):
1482+ ''' Signal callback, executed when the user modifies something.
1483+ Disables the backend. The user will re-enable it to confirm the changes
1484+ (s)he made.
1485+
1486+ @param sender: not used, only here for signal compatibility
1487+ @param data: not used, only here for signal compatibility
1488+ '''
1489+ # every change in the config disables the backend
1490+ self.req.set_backend_enabled(self.backend.get_id(), False)
1491+ #self._refresh_textbox_state()
1492+
1493+ def commit_changes(self):
1494+ '''Saves the changes to the backend parameter'''
1495+ self.check_config()
1496+
1497+ def _refresh_tasklists(self):
1498+ '''
1499+ Refreshes the list of tasklists to display
1500+ '''
1501+ self.liststore = gtk.ListStore(str,str,bool,bool)
1502+ for iid, title in self.backend.get_tasklists().iteritems():
1503+ entry = self.backend.get_parameters()[self.parameter].get(iid, {})
1504+ self.liststore.append([iid,
1505+ title,
1506+ entry.get('sync', False),
1507+ entry.get('default', False)])

Subscribers

People subscribed via source and target branches

to status/vote changes: