← Back to team overview

gtg-user team mailing list archive

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

 

phiamo has proposed merging lp:~gtg-user/gtg/google-tasks-backend into lp:gtg.

Requested reviews:
  Gtg developers (gtg)

For more details, see:
https://code.launchpad.net/~gtg-user/gtg/google-tasks-backend/+merge/150416

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)
-- 
https://code.launchpad.net/~gtg-user/gtg/google-tasks-backend/+merge/150416
Your team Gtg users is subscribed to branch lp:~gtg-user/gtg/google-tasks-backend.
=== added file 'GTG/backends/backend_gtasks.py'
--- GTG/backends/backend_gtasks.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/backend_gtasks.py	2013-02-25 19:21:24 +0000
@@ -0,0 +1,790 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things GNOME! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2012 - 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/>.
+# -----------------------------------------------------------------------------
+
+'''
+google tasks backend
+'''
+
+import os
+import cgi
+import uuid
+import time
+import threading
+import datetime
+import subprocess
+import exceptions
+from dateutil.tz                        import tzutc, tzlocal
+
+from GTG.backends.genericbackend        import GenericBackend
+from GTG                                import _
+from GTG.backends.backendsignals        import BackendSignals
+from GTG.backends.syncengine            import SyncEngine, SyncMeme
+from GTG.backends.gtasks.gtasks         import GTasks, GTasksError, GTasksAPIError
+from GTG.backends.periodicimportbackend import PeriodicImportBackend
+from GTG.tools.dates                    import Date
+from GTG.core.task                      import Task
+from GTG.tools.interruptible            import interruptible
+from GTG.tools.logger                   import Log
+from xdg.BaseDirectory                  import xdg_config_home
+
+
+
+
+
+class Backend(PeriodicImportBackend):
+
+    _general_description = { \
+        GenericBackend.BACKEND_NAME:       "backend_gtasks", \
+        GenericBackend.BACKEND_HUMAN_NAME: _("gTasks Backend"), \
+        GenericBackend.BACKEND_AUTHORS:    ["Philipp A. Mohrenweiser"], \
+        GenericBackend.BACKEND_TYPE:       GenericBackend.TYPE_READWRITE, \
+        GenericBackend.BACKEND_DESCRIPTION: \
+            _("This service synchronizes your tasks with your google online tasks"),\
+        }
+
+    _static_parameters = { 
+        "period": { 
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT, 
+            GenericBackend.PARAM_DEFAULT_VALUE: 10
+        },
+        "default-list-to-sync-to": { 
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING, 
+            GenericBackend.PARAM_DEFAULT_VALUE: None
+        },
+        "is-first-run": { 
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL, 
+            GenericBackend.PARAM_DEFAULT_VALUE: True
+        }
+    }
+###############################################################################
+### Backend standard methods ##################################################
+###############################################################################
+
+    def __init__(self, parameters):
+        '''
+        See GenericBackend for an explanation of this function.
+        Loads the saved state of the sync, if any
+        '''
+        super(Backend, self).__init__(parameters)
+        #loading the saved state of the synchronization, if any
+        self.sync_engine_path = os.path.join('backends/gtasks/', \
+                                      "sync_engine-" + self.get_id())
+        self.sync_engine = self._load_pickled_file(self.sync_engine_path, \
+                                                   SyncEngine())
+        self._this_is_the_first_loop = True
+        self.gtasks_proxy = GTasksProxy(self)
+            
+    def get_liststore_data(self, parameter):
+        """Needed by the combobox ui to supply liststore data"""
+        data = []
+        if "default-list-to-sync-to" == parameter:
+            for k, v in self.gtasks_proxy.get_tasklists().iteritems():
+                data.append((k,v))
+        return data
+        
+    def initialize(self):
+        """
+        See GenericBackend for an explanation of this function.
+        """
+        super(Backend, self).initialize()
+
+    def save_state(self):
+        """
+        See GenericBackend for an explanation of this function.
+        """
+        self._store_pickled_file(self.sync_engine_path, self.sync_engine)
+
+###############################################################################
+### TWO WAY SYNC ##############################################################
+###############################################################################
+
+    def do_periodic_import(self):
+        """
+        See PeriodicImportBackend for an explanation of this function.
+        """
+
+        #we get the old list of synced tasks, and compare with the new tasks set
+        stored_gtasks_ids = self.sync_engine.get_all_remote()
+        current_gtask_ids = [gtask_id for gtask_id in \
+                            self.gtasks_proxy.get_gtasks_dict().iterkeys()]
+        if self._this_is_the_first_loop:
+            self._on_successful_authentication()
+
+        #If it's the very first time the backend is run, it's possible that the
+        # user already synced his tasks in some way (but we don't know that).
+        # Therefore, we attempt to induce those tasks relationships matching the
+        # titles.
+        if self._parameters["is-first-run"] and False:
+            gtg_titles_dic = {}
+            for tid in self.datastore.get_all_tasks():
+                gtg_task = self.datastore.get_task(tid)
+                if not self._gtg_task_is_syncable_per_attached_tags(gtg_task):
+                    continue
+                gtg_title = gtg_task.get_title()
+                if gtg_titles_dic.has_key(gtg_title):
+                    gtg_titles_dic[gtg_task.get_title()].append(tid)
+                else:
+                    gtg_titles_dic[gtg_task.get_title()] = [tid]
+            for gtask_id in current_gtask_ids:
+                gtask = self.gtasks_proxy.get_task(gtask_id)
+                try:
+                    tids = gtg_titles_dic[gtask.get_title()]
+                    #we remove the tid, so that it can't be linked to two
+                    # different gtasks tasks
+                    tid = tids.pop()
+                    gtg_task = self.datastore.get_task(tid)
+                    meme = SyncMeme(gtg_task.get_modified(),
+                                    gtask.get_modified(),
+                                    "GTG")
+                    self.sync_engine.record_relationship( \
+                         local_id = tid,
+                         remote_id = gtask.get_id(),
+                         meme = meme)
+                except KeyError:
+                    pass
+                except IndexError:
+                    pass
+            #a first run has been completed successfully
+            self._parameters["is-first-run"] = False
+        
+        for gtask_id in current_gtask_ids:
+            self.cancellation_point()
+            #Adding and updating
+            self._process_gtask(gtask_id)
+        
+        print "DIFFE", set(stored_gtasks_ids).difference(\
+                                        set(current_gtask_ids))
+        for gtask_id in set(stored_gtasks_ids).difference(\
+                                        set(current_gtask_ids)):
+            self.cancellation_point()
+            if not self.please_quit:
+                tid = self.sync_engine.get_local_id(gtask_id)
+                self.datastore.request_task_deletion(tid)
+                try:
+                    print "BREAKING HEEEEERE"
+                    self.sync_engine.break_relationship(remote_id = \
+                                                        gtask_id)
+                    self.save_state()
+                except KeyError:
+                    pass
+
+    def _on_successful_authentication(self):
+        '''
+        Saves the token and requests a full flush on first autentication
+        '''
+        self._this_is_the_first_loop = False
+        #we ask the Datastore to flush all the tasks on us
+        threading.Timer(10,
+                        self.datastore.flush_all_tasks,
+                        args =(self.get_id(),)).start()
+
+    @interruptible
+    def remove_task(self, tid):
+        """
+        See GenericBackend for an explanation of this function.
+        """
+        if not self.gtasks_proxy.is_authenticated():
+            return
+        self.cancellation_point()
+        try:
+            gtask_id = self.sync_engine.get_remote_id(tid)
+            if gtask_id not in self.gtasks_proxy.get_gtasks_dict():
+                #we might need to refresh our task cache
+                self.gtasks_proxy.refresh_gtasks_dict()
+            gtask = self.gtasks_proxy.get_task(gtask_id)
+            self.gtasks_proxy.delete(gtask)
+            Log.debug("removing task %s from GTasks" % gtask)
+        except KeyError:
+            try:
+                print "BREAKINNNNG"
+                self.sync_engine.break_relationship(local_id = tid)
+                self.save_state()
+            except:
+                pass
+                
+    def delete(self, gtask):
+        self.remove_task(gtask.get_id())
+
+###############################################################################
+### Process tasks #############################################################
+###############################################################################
+
+    @interruptible
+    def set_task(self, task):
+        """
+        See GenericBackend for an explanation of this function.
+        """
+        if not self.gtasks_proxy.is_authenticated():
+            return
+        self.cancellation_point()
+        tid = task.get_id()
+        is_syncable = self._gtg_task_is_syncable_per_attached_tags(task)
+        action, gtask_id = self.sync_engine.analyze_local_id( \
+                                tid, \
+                                self.datastore.has_task, \
+                                self.gtasks_proxy.has_gtask, \
+                                is_syncable)
+        if action == None:
+            return
+
+        if action == SyncEngine.ADD:
+            Log.debug("GTG->GTasks set task (%s, %s) ADD" % (action, is_syncable))
+            if task.get_status() != Task.STA_ACTIVE:
+                #OPTIMIZATION:
+                #we don't sync tasks that have already been closed before we
+                # even synced them once
+                return
+            gtask = self.gtasks_proxy.create_new_gtask(task.get_title())
+            gtask.set_text("added via set task")
+            try:
+                self._populate_gtask(task, gtask)
+            except:
+                self.gtasks_proxy.delete(gtask)
+                raise
+            meme = SyncMeme(task.get_modified(),
+                            gtask.get_modified(),
+                            "GTG")
+            self.sync_engine.record_relationship( \
+                local_id = tid, remote_id = gtask.get_id(), meme = meme)
+            self.gtasks_proxy.update(gtask)
+
+        elif action == SyncEngine.UPDATE:
+            Log.debug("GTG->GTasks set task (%s, %s) UPDATE" % (action, is_syncable))
+            gtask = self.gtasks_proxy.get_task(gtask_id)
+            with self.datastore.get_backend_mutex():
+                meme = self.sync_engine.get_meme_from_local_id(task.get_id())
+                newest = meme.which_is_newest(task.get_modified(),
+                                              gtask.get_modified())
+                if newest == "local":
+                    self._populate_gtask(task, gtask)
+                    meme.set_remote_last_modified(gtask.get_modified())
+                    meme.set_local_last_modified(task.get_modified())
+                else:
+                    #we skip saving the state
+                    return
+
+        elif action == SyncEngine.REMOVE:
+            Log.debug("GTG->GTasks set task (%s, %s) REMOVE" % (action, is_syncable))
+            self.datastore.request_task_deletion(tid)
+            try:
+                self.sync_engine.break_relationship(local_id = tid)
+            except KeyError:
+                pass
+            gtask = self.gtasks_proxy.get_task(gtask_id)
+            self.gtasks_proxy.task_delete(gtask.get_tasklist(), gtask.get_id())
+
+        elif action == SyncEngine.LOST_SYNCABILITY:
+            try:
+                gtask = self.gtasks_proxy.get_gtasks_dict()[gtask]
+            except KeyError:
+                #in this case, we don't have yet the task in our local cache
+                # of what's on the gtasks website
+                self.gtasks_proxy.refresh_gtasks_dict()
+                gtask = self.gtasks_proxy.get_gtasks_dict()[gtask]
+            self._exec_lost_syncability(tid, gtask)
+
+            self.save_state()
+
+    def _exec_lost_syncability(self, tid, gtask):
+        '''
+        Executed when a relationship between tasks loses its syncability
+        property. See SyncEngine for an explanation of that.
+
+        @param tid: a GTG task tid
+        @param note: a GTasks task
+        '''
+        self.cancellation_point()
+        meme = self.sync_engine.get_meme_from_local_id(tid)
+        #First of all, the relationship is lost
+        self.sync_engine.break_relationship(local_id = tid)
+        if meme.get_origin() == "GTG":
+            self.delete(gtask)
+        else:
+            self.datastore.request_task_deletion(tid)
+            
+    def _process_gtask(self, gtask_id):
+        '''
+        Takes a gtasks task id and carries out the necessary operations to
+        refresh the sync state
+        '''
+        self.cancellation_point()
+        if not self.gtasks_proxy.is_authenticated():
+            return
+        gtask = self.gtasks_proxy.get_task(gtask_id)
+        is_syncable = self._gtask_is_syncable_per_attached_tags(gtask)
+        
+        action, tid = self.sync_engine.analyze_remote_id( \
+                                             gtask_id,
+                                             self.datastore.has_task,
+                                             self.gtasks_proxy.has_gtask,
+                                             is_syncable)
+        
+        print "PROCESS ENGINE SAYS", action, tid, gtask_id, self.gtasks_proxy.get_task(gtask_id).get_title()
+        
+        if action == None:
+            return        
+        
+        Log.debug("GTG<-GTasks set task (%s, %s)" % (action, is_syncable))
+        
+        if action == SyncEngine.ADD:
+            if gtask.get_status() != Task.STA_ACTIVE:
+                #OPTIMIZATION:
+                #we don't sync tasks that have already been closed before we
+                # even saw them
+                return
+            tid = str(uuid.uuid4())
+            task = self.datastore.task_factory(tid)
+            self._populate_task(task, gtask)
+            meme = SyncMeme(task.get_modified(),
+                            gtask.get_modified(),
+                            "GTasks")
+            self.sync_engine.record_relationship( \
+                    local_id = tid,
+                    remote_id = gtask.get_id(),
+                    meme = meme)
+            self.datastore.push_task(task)
+
+        elif action == SyncEngine.UPDATE:
+            task = self.datastore.get_task(tid)
+            with self.datastore.get_backend_mutex():
+                meme = self.sync_engine.get_meme_from_remote_id(gtask.get_id())
+                newest = meme.which_is_newest(task.get_modified(),
+                                              gtask.get_modified())
+                if newest == "remote":
+                    self._populate_task(task, gtask)
+                    meme.set_remote_last_modified(gtask.get_modified())
+                    meme.set_local_last_modified(task.get_modified())
+                else:
+                    #we skip saving the state
+                    return
+
+        elif action == SyncEngine.REMOVE:
+            try:
+                self.delete(gtask)
+                self.sync_engine.break_relationship(remote_id = gtask.get_id())
+            except KeyError:
+                pass
+
+        elif action == SyncEngine.LOST_SYNCABILITY:
+            print "LOST", tid,gtask
+            self._exec_lost_syncability(tid, gtask)
+
+        self.save_state()
+
+###############################################################################
+### Helper methods ############################################################
+###############################################################################
+
+    def _populate_task(self, task, gtask):
+        '''
+        Copies the content of a GTasksTask in a Task
+        '''
+        task.set_title(gtask.get_title())
+        task.set_text(gtask.get_text())
+        task.set_due_date(gtask.get_due_date())
+        status = gtask.get_status()
+        if GTG_TO_GTasks_STATUS[task.get_status()] != status:
+            task.set_status(gtask.get_status())
+        #tags
+        tags = set(['@%s' % tag for tag in gtask.get_tags()])
+        gtg_tags_lower = set([t.get_name().lower() for t in task.get_tags()])
+        #tags to remove
+        for tag in gtg_tags_lower.difference(tags):
+            task.remove_tag(tag)
+        #tags to add
+        for tag in tags.difference(gtg_tags_lower):
+            gtg_all_tags = self.datastore.get_all_tags()
+            matching_tags = filter(lambda t: t.lower() == tag, gtg_all_tags)
+            if len(matching_tags) !=  0:
+                tag = matching_tags[0]
+            task.add_tag(tag)
+
+    def _populate_gtask(self, task, gtask):
+        '''
+        Copies the content of a Task into a GTasksTask
+
+        @param task: a GTG Task
+        @param gtask: an GTasksTask
+        '''
+        #Get methods of an gtask are fast, set are slow: therefore,
+        # we try to use set as rarely as possible
+
+        #first thing: the status. This way, if we are syncing a completed
+        # task it doesn't linger for ten seconds in the GTasks Inbox
+        status = task.get_status()
+        if gtask.get_status() != status:
+            gtask.set_status(status)
+        title = task.get_title()
+        if gtask.get_title() != title:
+           gtask.set_title(title)
+        text = task.get_excerpt(strip_tags = True, strip_subtasks = True)
+        if gtask.get_text() != text:
+            gtask.set_text(text)
+        tags = task.get_tags_name()
+        gtask_tags = []
+        for tag in gtask.get_tags():
+            if tag[0] != '@':
+                tag = '@' + tag
+            gtask_tags.append(tag)
+        #gtasks tags are lowercase only
+        if gtask_tags != [t.lower() for t in tags]:
+            gtask.set_tags(tags)
+        due_date = task.get_due_date()
+        if gtask.get_due_date() != due_date:
+            print due_date
+            gtask.set_due_date(due_date)
+        self.gtasks_proxy.update(gtask)
+
+    def _gtask_is_syncable_per_attached_tags(self, gtask):
+        '''
+        Helper function which checks if the given task satisfies the filtering
+        imposed by the tags attached to the backend.
+        That means, if a user wants a backend to sync only tasks tagged @works,
+        this function should be used to check if that is verified.
+
+        @returns bool: True if the task should be synced
+        '''
+        attached_tags = self.get_attached_tags()
+        if GenericBackend.ALLTASKS_TAG in attached_tags:
+            return True
+        for tag in gtask.get_tags():
+            if "@" + tag in attached_tags:
+                return  True
+        return False
+
+###############################################################################
+### GTasks PROXY #################################################################
+###############################################################################
+
+class GTasksProxy(object):
+    '''
+    The purpose of this class is producing an updated list of GTasksTasks.
+    To do that, it handles:
+        - authentication to GTasks
+        - keeping the list fresh
+        - downloading the list
+    '''
+
+    def __init__(self, backend):
+        self.backend = backend
+        self.authenticated = threading.Event()
+        self.is_not_refreshing = threading.Event()
+        self.is_not_refreshing.set()
+        self.gtasks = GTasks(storage_dir = os.path.join(xdg_config_home, 'gtg/'))
+
+    def update(self, gtask):
+        body = gtask.gtask_dict
+        self.gtasks.task_update(gtask.get_tasklist(), gtask.get_id(), body)
+        
+    def get_task(self, gtask_id):
+        try:
+            gtask = self.get_gtasks_dict()[gtask_id]
+        except KeyError:
+            #in this case, we don't have yet the task in our local cache
+            # of what's on the gtasks website
+            self.refresh_gtasks_dict()
+            gtask = self.get_gtasks_dict()[gtask_id]
+        return gtask
+        
+    def get_tasklists(self):
+        self.tasklists = {}
+        for item in self.gtasks.tasklists_list().get('items'):
+            self.tasklists[str(item.get('id'))] = str(item.get('title'))
+        return self.tasklists
+        
+    def list_tasks(self, tasklist):
+        return self.gtasks.tasks_list(tasklist).get('items')
+        
+    ##########################################################################
+    ### AUTHENTICATION #######################################################
+    ##########################################################################
+
+    def start_authentication(self):
+        '''
+        Launches the authentication process
+        '''
+        initialize_thread = threading.Thread(target = self._authenticate)
+        initialize_thread.setDaemon(True)
+        initialize_thread.start()
+    
+    def is_authenticated(self):
+        '''
+        Returns true if we've autheticated to GTasks
+        '''
+        return self.authenticated.isSet()
+
+    def wait_for_authentication(self):
+        '''
+        Inhibits the thread until authentication occours
+        '''
+        self.authenticated.wait()
+
+    def get_auth_token(self):
+        '''
+        Returns the oauth token, or none
+        '''
+        try:
+            return self.token
+        except:
+            return None
+
+    def _authenticate(self):
+        '''
+        authentication main function
+        '''
+        self.authenticated.clear()
+        while not self.authenticated.isSet():
+            if not self.gtasks.is_valid():
+                self.gtasks.run_flow()
+            if self.gtasks.is_valid():
+                self.authenticated.set()
+            else:
+                BackendSignals().backend_failed(self.get_id(), \
+                            BackendSignals.ERRNO_NETWORK)
+
+    ##########################################################################
+    ### GTasks TASKS HANDLING ###################################################
+    ##########################################################################
+
+    def get_gtasks_dict(self):
+        '''
+        Returns a dict of GTasksTasks. It will start authentication if necessary.
+        The dict is kept updated automatically.
+        '''
+        if not hasattr(self, '_gtask_dict'):
+            self.refresh_gtasks_dict()
+        else:
+            time_difference = datetime.datetime.now() - \
+                          self.__gtask_dict_timestamp
+            if time_difference.seconds > 60:
+                self.refresh_gtasks_dict()
+        return self._gtask_dict.copy()
+
+    def refresh_gtasks_dict(self):
+        '''
+        Builds a list of GTasksTasks fetched from GTasks
+        '''
+        if not self.is_authenticated():
+            self.start_authentication()
+            self.wait_for_authentication()
+
+        if not self.is_not_refreshing.isSet():
+            #if we're already refreshing, we just wait for that to happen and
+            # then we immediately return
+            self.is_not_refreshing.wait()
+            return
+            
+        self.is_not_refreshing.clear()
+        Log.debug('refreshing gtasks')
+        #our purpose is to fill this with "tasks_id: GTasksTask" items
+        gtasks_dict = {}
+
+        gtasklists_list = self.get_tasklists()
+        #for each gtasks list, we retrieve all the tasks in it
+        for gtasklist in gtasklists_list:
+                gtasks_list = self.list_tasks(gtasklist)
+                for gtask in gtasks_list:
+                    gtasks_dict[gtask.get('id')] = GTasksTask(gtask,
+                                                          gtasklist)
+
+        #we're done: we store the dict in this class and we annotate the time we
+        # got it
+        self._gtask_dict = gtasks_dict
+        self.__gtask_dict_timestamp = datetime.datetime.now()
+        self.is_not_refreshing.set()
+
+    def has_gtask(self, gtask_id):
+        '''
+        Returns True if we have seen that task id
+        '''
+        cache_result = gtask_id in self.get_gtasks_dict()
+        return cache_result
+
+    def create_new_gtask(self, title):
+        '''
+        Creates a new gtasks task
+        '''
+        
+        result = self.gtasks.task_create(tasklist=self.get_default_tasklist(), title=title)
+        gtask = GTasksTask(result,
+                            self.get_default_tasklist())
+        #adding to the dict right away
+        if hasattr(self, '_gtask_dict'):
+            #if the list hasn't been downloaded yet, we do not create a list,
+            # because the fact that the list is created is used to keep track of
+            # list updates
+            self._gtask_dict[gtask.get_id()] = gtask
+        return gtask
+    
+    def get_default_tasklist(self):
+        return self.backend.get_parameters()['default-list-to-sync-to']
+        
+    def delete(self, gtask):
+        self.gtasks.task_delete(gtask.get_tasklist(), gtask.get_id())
+
+###############################################################################
+### GTasks TASK ##################################################################
+###############################################################################
+
+#dictionaries to translate a GTasks status into a GTG one (and back)
+GTG_TO_GTasks_STATUS = {Task.STA_ACTIVE: 'needsAction',
+                     Task.STA_DONE: 'completed',
+                     Task.STA_DISMISSED: 'deleted'}
+
+GTasks_TO_GTG_STATUS = {'needsAction': Task.STA_ACTIVE,
+                     'completed': Task.STA_DONE}
+
+
+
+class GTasksTask(object):
+    '''
+    A proxy object that encapsulates a GTasks task, giving an easier API to access
+    and modify its attributes.
+    This backend already uses a library to interact with GTasks, but that is just a
+    thin proxy for HTML gets and posts.
+    The meaning of all "special words"
+    
+    http://www.rememberthemilk.com/services/api/tasks.gtasks
+    '''
+    
+
+    def __init__(self, gtask, gtasklist):
+        '''
+        sets up the various parameters needed to interact with a task.
+
+        @param task: the task object given by the underlying library
+        @param gtasklist: the gtasks list the task resides in.
+        @param gtasks: a handle of the gtasks object, to be able to speak with gtasks.
+                    Authentication should have already been done.
+        '''
+        self.gtask_dict = gtask
+        self.gtasklist = gtasklist
+        
+    def get_tasklist(self):
+        return self.gtasklist
+        
+    def get_title(self):
+        '''Returns the title of the task, if any'''
+        title = self.gtask_dict.get('title')
+        if "(no title task)" == title:
+            return ""
+        return title
+
+    def set_title(self, title):
+        '''Sets the task title'''
+        title = cgi.escape(title)
+        self.gtask_dict['title'] = title
+
+    def get_id(self):
+        '''Return the task id. The taskseries id is *different*'''
+        return self.gtask_dict.get('id')
+
+    def get_status(self):
+        '''Returns the task status, in GTG terminology'''
+        return GTasks_TO_GTG_STATUS[self.gtask_dict.get('status')]
+
+    def set_status(self, gtg_status):
+        '''Sets the task status, in GTG terminology'''
+        status = GTG_TO_GTasks_STATUS[gtg_status]
+        self.gtask_dict['status'] = status
+        
+    def get_tags(self):
+        '''Returns the task tags 
+        TODO: implement parsing of txt tot get tags... gtasks does not support tags natively'''
+        return []
+        
+    def set_tags(self, tags):
+        '''
+        Sets a new set of tags to a task. Old tags are deleted.
+        TODO: implement parsing of txt tot get tags... gtasks does not support tags natively
+        '''
+        pass
+        
+    def get_text(self):
+        '''
+        Gets the content of GTasks notes, aggregated in a single string
+        '''
+        return self.gtask_dict.get('notes', '')
+
+    def set_text(self, notes):
+        '''
+        deletes all the old notes in a task and sets a single note with the
+        given text
+        '''
+        notes = cgi.escape(notes)
+        self.gtask_dict['notes'] = notes
+
+    def get_due_date(self):
+        '''
+        Gets the task due date
+        '''
+        due = self.gtask_dict.get('due')
+        if not due:
+            return Date.no_date()
+        date = self.__time_gtasks_to_datetime(due).date()
+        return Date(date)
+
+    def set_due_date(self, due):
+        '''
+        Sets the task due date
+        '''
+        due = self.__time_date_to_gtasks(due)
+        self.gtask_dict['due'] = due
+
+    def get_modified(self):
+        '''
+        Gets the task modified time, in local time
+        TODO: #correct
+        '''
+        return self.__time_gtasks_to_datetime(self.gtask_dict.get('updated'))
+
+    def __tz_utc_to_local(self, dt):
+        dt = dt.replace(tzinfo = tzutc())
+        dt = dt.astimezone(tzlocal())
+        return dt.replace(tzinfo = None)
+
+    def __tz_local_to_utc(self, dt):
+        dt = dt.replace(tzinfo = tzlocal())
+        dt = dt.astimezone(tzutc())
+        return dt.replace(tzinfo = None)
+
+    def __time_gtasks_to_datetime(self, string):
+        string = string.split('.')[0].split('Z')[0]
+        dt = datetime.datetime.strptime(string.split(".")[0], \
+                                          "%Y-%m-%dT%H:%M:%S")
+        return self.__tz_utc_to_local(dt)
+        
+    def __time_gtasks_to_date(self, string):
+        string = string.split('.')[0].split('Z')[0]
+        dt = datetime.datetime.strptime(string.split(".")[0], "%Y-%m-%d")
+        return self.__tz_utc_to_local(dt)
+
+    def __time_datetime_to_gtasks(self, timeobject):
+        if not timeobject:
+            return ""
+        timeobject = self.__tz_local_to_utc(timeobject)
+        return timeobject.strftime("%Y-%m-%dT%H:%M:%S")
+
+    def __time_date_to_gtasks(self, timeobject):
+        if not timeobject:
+            return ""
+        return timeobject.strftime("%Y-%m-%dT00:00:00.000Z")
+
+    def __str__(self):
+        return "Task %s (%s)" % (self.get_title(), self.get_id())

=== added directory 'GTG/backends/gtasks'
=== added file 'GTG/backends/gtasks/__init__.py'
=== added file 'GTG/backends/gtasks/client_secrets.json'
--- GTG/backends/gtasks/client_secrets.json	1970-01-01 00:00:00 +0000
+++ GTG/backends/gtasks/client_secrets.json	2013-02-25 19:21:24 +0000
@@ -0,0 +1,1 @@
+{"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"}}

=== added file 'GTG/backends/gtasks/gtasks.py'
--- GTG/backends/gtasks/gtasks.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/gtasks/gtasks.py	2013-02-25 19:21:24 +0000
@@ -0,0 +1,170 @@
+
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2012 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Python library for Google Tasks
+# @author: Philipp A. Mohrenweiser
+# <phiamo@xxxxxxxxxxxxxx>
+# http://gtasks.mohrenweiserpartner.de
+
+# For more information on the Tasks API API you can visit:
+#
+#   http://code.google.com/apis/tasks/v1/using.html
+#
+# For more information on the Tasks API API python library surface you
+# can visit:
+#
+#   https://google-api-client-libraries.appspot.com/documentation/tasks/v1/python/latest/
+#
+# For information on the Python Client Library visit:
+#
+#   https://developers.google.com/api-client-library/python/start/get_started
+
+import gflags
+import httplib2
+import logging
+import os
+import pprint
+import sys
+
+from apiclient.discovery import build
+from oauth2client.file import Storage
+from oauth2client.client import AccessTokenRefreshError
+from oauth2client.client import flow_from_clientsecrets
+from oauth2client.tools import run
+from xdg.BaseDirectory import xdg_config_home
+
+
+FLAGS = gflags.FLAGS
+
+# support user credentials
+user_credentials = os.path.join(xdg_config_home, 'gtg/backends/gtasks/client_secrets.json')
+if os.path.exists(user_credentials):
+    CLIENT_SECRETS = user_credentials
+else:
+    CLIENT_SECRETS = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'client_secrets.json')
+
+# Set up a Flow object to be used for authentication.
+# Add one or more of the following scopes. PLEASE ONLY ADD THE SCOPES YOU
+# NEED. For more information on using scopes please see
+# <https://developers.google.com/+/best-practices>.
+FLOW = flow_from_clientsecrets(CLIENT_SECRETS,
+    scope=[
+      'https://www.googleapis.com/auth/tasks',
+      'https://www.googleapis.com/auth/tasks.readonly',
+    ],
+    message="""You can add a own credentials file to:
+    %s
+    Get it from https://code.google.com/apis/console/
+    """ % (user_credentials))
+
+# Filename for token
+FILENAME = 'gmail_tags.dat'
+
+class GTasksError(Exception): pass
+
+class GTasksAPIError(GTasksError): pass
+
+class GTasks:
+    service = None
+    
+    def __init__(self, storage_dir=None):
+        if not storage_dir or not os.path.exists(storage_dir):
+            storage_dir = "."
+        path = os.path.join(storage_dir, FILENAME)
+        self.storage = Storage(path)
+        self.credentials = self.storage.get()
+    
+    def get_service(self):        
+        if not self.service:
+            if not self.is_valid():
+                self.run_flow()
+            http = httplib2.Http()
+            self.http = self.credentials.authorize(http)
+            self.service = build('tasks', 'v1', http=self.http)
+        return self.service
+        
+    def is_valid(self):
+        if self.credentials is None or self.credentials.invalid:
+            return False
+        return True
+        
+    def run_flow(self):
+        self.credentials = run(FLOW, self.storage)
+        
+    def refresh(self):
+        self.credentials.refresh()
+    
+    def execute(self, request):
+        try:
+            return request.execute(http=self.http)
+        except AccessTokenRefreshError:
+            self.auth()
+        
+    def tasklists_list(self):
+        return self.execute(self.get_service().tasklists().list())  
+        
+    def task_set_title(self, tasklist, task, title):
+        return self.execute(self.get_service().tasks().patch(tasklist=tasklist,task=task,body={title:title}))  
+    
+    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):
+        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))
+
+    def task_create(self, tasklist, title, notes=""):
+        body = {
+            "status": "needsAction", # Status of the task. This is either "needsAction" or "completed".
+            "kind": "tasks#task", # Type of the resource. This is always "tasks#task".
+#            "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.
+            "title": title, # Title of the task.
+            "deleted": False, # Flag indicating whether the task has been deleted. The default if False.
+#            "completed": "A String", # Completion date of the task (as a RFC 3339 timestamp). This field is omitted if the task has not been completed.
+#            "updated": "A String", # Last modification time of the task (as a RFC 3339 timestamp).
+#            "due": "A String", # Due date of the task (as a RFC 3339 timestamp). Optional.
+#            "etag": "A String", # ETag of the resource.
+#            "id": "A String", # Task identifier.
+#            "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.
+#            "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.
+            "notes": notes, # Notes describing the task. Optional.
+#            "selfLink": "A String", # URL pointing to this task. Used to retrieve, update, or delete this task.
+        }
+        return self.execute(self.get_service().tasks().insert(tasklist=tasklist,body=body))  
+        
+    def task_set_status(self, tasklist, task, status):
+        body = {
+            "status": status, # Status of the task. This is either "needsAction" or "completed".
+        }
+        return self.execute(self.get_service().tasks().patch(tasklist=tasklist,task=task, body=body))  
+        
+    def task_set_notes(self, tasklist, task, notes):
+        body = {
+            "notes": notes, # Status of the task. This is either "needsAction" or "completed".
+        }
+        return self.execute(self.get_service().tasks().patch(tasklist=tasklist,task=task, body=body))  
+        
+
+    def task_delete(self, tasklist, task):
+        return self.execute(self.get_service().tasks().delete(tasklist=tasklist,task=task))  
+        
+    def task_set_due(self, tasklist, task, due):
+        body = {
+            "due": due, # Status of the task. This is either "needsAction" or "completed".
+        }
+        return self.execute(self.get_service().tasks().patch(tasklist=tasklist,task=task, body=body))  
+        
+    def task_update(self, tasklist, task, body):
+        print body
+        return self.execute(self.get_service().tasks().update(tasklist=tasklist,task=task, body=body))  
+        

=== added file 'GTG/backends/gtasks/gtasks_bak.py'
--- GTG/backends/gtasks/gtasks_bak.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/gtasks/gtasks_bak.py	2013-02-25 19:21:24 +0000
@@ -0,0 +1,395 @@
+# Python library for Google Tasks
+# @author: Philipp A. Mohrenweiser
+# <phiamo@xxxxxxxxxxxxxx>
+# http://gtasks.mohrenweiserpartner.de
+
+import urllib
+import time
+from hashlib import md5
+
+from GTG import _
+from GTG.tools.logger import Log
+
+_use_jsonlib = False
+try:
+    import simplejson as json
+    _use_jsonlib = True
+except ImportError:
+    try:
+        import json as json
+        _use_jsonlib = True
+    except ImportError:
+        try:
+            from django.utils import simplejson as json
+            _use_jsonlib = True
+        except ImportError:
+            pass
+    
+if not _use_jsonlib:
+    Log.warning("simplejson module is not available, "
+             "falling back to the internal JSON parser. "
+             "Please consider installing the simplejson module from "
+             "http://pypi.python.org/pypi/simplejson.";)
+
+SERVICE_URL = 'http://api.rememberthemilk.com/services/rest/'
+AUTH_SERVICE_URL = 'http://www.rememberthemilk.com/services/auth/'
+
+
+class RTMError(Exception): pass
+
+class RTMAPIError(RTMError): pass
+
+class AuthStateMachine(object):
+
+    class NoData(RTMError): pass
+
+    def __init__(self, states):
+        self.states = states
+        self.data = {}
+
+    def dataReceived(self, state, datum):
+        if state not in self.states:
+            error_string = _("Invalid state")+" <%s>" 
+
+            raise RTMError, error_string % state
+        self.data[state] = datum
+
+    def get(self, state):
+        if state in self.data:
+            return self.data[state]
+        else:
+            raise AuthStateMachine.NoData, 'No data for <%s>' % state
+
+
+class RTM(object):
+
+    def __init__(self, apiKey, secret, token=None):
+        self.apiKey = apiKey
+        self.secret = secret
+        self.authInfo = AuthStateMachine(['frob', 'token'])
+
+        # this enables one to do 'rtm.tasks.getList()', for example
+        for prefix, methods in API.items():
+            setattr(self, prefix,
+                    RTMAPICategory(self, prefix, methods))
+
+        if token:
+            self.authInfo.dataReceived('token', token)
+
+    def _sign(self, params):
+        "Sign the parameters with MD5 hash"
+        pairs = ''.join(['%s%s' % (k,v) for k,v in sortedItems(params)])
+        return md5(self.secret+pairs).hexdigest()
+
+    def get(self, **params):
+        "Get the XML response for the passed `params`."
+        params['api_key'] = self.apiKey
+        params['format'] = 'json'
+        params['api_sig'] = self._sign(params)
+
+        json_data = openURL(SERVICE_URL, params).read()
+
+        #LOG.debug("JSON response: \n%s" % json)
+        if _use_jsonlib:
+            data = dottedDict('ROOT', json.loads(json_data))
+        else:
+            data = dottedJSON(json_data)
+        rsp = data.rsp
+
+        if rsp.stat == 'fail':
+            raise RTMAPIError, 'API call failed - %s (%s)' % (
+                rsp.err.msg, rsp.err.code)
+        else:
+            return rsp
+
+    def getNewFrob(self):
+        rsp = self.get(method='rtm.auth.getFrob')
+        self.authInfo.dataReceived('frob', rsp.frob)
+        return rsp.frob
+
+    def getAuthURL(self):
+        try:
+            frob = self.authInfo.get('frob')
+        except AuthStateMachine.NoData:
+            frob = self.getNewFrob()
+
+        params = {
+            'api_key': self.apiKey,
+            'perms'  : 'delete',
+            'frob'   : frob
+            }
+        params['api_sig'] = self._sign(params)
+        return AUTH_SERVICE_URL + '?' + urllib.urlencode(params)
+
+    def getToken(self):
+        frob = self.authInfo.get('frob')
+        rsp = self.get(method='rtm.auth.getToken', frob=frob)
+        self.authInfo.dataReceived('token', rsp.auth.token)
+        return rsp.auth.token
+
+class RTMAPICategory:
+    "See the `API` structure and `RTM.__init__`"
+
+    def __init__(self, rtm, prefix, methods):
+        self.rtm = rtm
+        self.prefix = prefix
+        self.methods = methods
+
+    def __getattr__(self, attr):
+        if attr in self.methods:
+            rargs, oargs = self.methods[attr]
+            if self.prefix == 'tasksNotes':
+                aname = 'rtm.tasks.notes.%s' % attr
+            else:
+                aname = 'rtm.%s.%s' % (self.prefix, attr)
+            return lambda **params: self.callMethod(
+                aname, rargs, oargs, **params)
+        else:
+            raise AttributeError, 'No such attribute: %s' % attr
+
+    def callMethod(self, aname, rargs, oargs, **params):
+        # Sanity checks
+        for requiredArg in rargs:
+            if requiredArg not in params:
+                raise TypeError, 'Required parameter (%s) missing' % requiredArg
+
+        for param in params:
+            if param not in rargs + oargs:
+                Log.error('Invalid parameter (%s)' % param)
+
+        return self.rtm.get(method=aname,
+                            auth_token=self.rtm.authInfo.get('token'),
+                            **params)
+
+
+
+# Utility functions
+
+def sortedItems(dictionary):
+    "Return a list of (key, value) sorted based on keys"
+    keys = dictionary.keys()
+    keys.sort()
+    for key in keys:
+        yield key, dictionary[key]
+
+def openURL(url, queryArgs=None):
+    if queryArgs:
+        url = url + '?' + urllib.urlencode(queryArgs)
+    #LOG.debug("URL> %s", url)
+    return urllib.urlopen(url)
+
+class dottedDict(object):
+    """Make dictionary items accessible via the object-dot notation."""
+
+    def __init__(self, name, dictionary):
+        self._name = name
+
+        if type(dictionary) is dict:
+            for key, value in dictionary.items():
+                if type(value) is dict:
+                    value = dottedDict(key, value)
+                elif type(value) in (list, tuple) and key != 'tag':
+                    value = [dottedDict('%s_%d' % (key, i), item)
+                             for i, item in indexed(value)]
+                setattr(self, key, value)
+        else:
+            raise ValueError, 'not a dict: %s' % dictionary
+
+    def __repr__(self):
+        children = [c for c in dir(self) if not c.startswith('_')]
+        return 'dotted <%s> : %s' % (
+            self._name,
+            ', '.join(children))
+
+
+def safeEval(string):
+    return eval(string, {}, {})
+
+def dottedJSON(json):
+    return dottedDict('ROOT', safeEval(json))
+
+def indexed(seq):
+    index = 0
+    for item in seq:
+        yield index, item
+        index += 1
+
+
+# API spec
+
+API = {
+   'auth': {
+       'checkToken':
+           [('auth_token',), ()],
+       'getFrob':
+           [(), ()],
+       'getToken':
+           [('frob',), ()]
+       },
+    'contacts': {
+        'add':
+            [('timeline', 'contact'), ()],
+        'delete':
+            [('timeline', 'contact_id'), ()],
+        'getList':
+            [(), ()]
+        },
+    'groups': {
+        'add':
+            [('timeline', 'group'), ()],
+        'addContact':
+            [('timeline', 'group_id', 'contact_id'), ()],
+        'delete':
+            [('timeline', 'group_id'), ()],
+        'getList':
+            [(), ()],
+        'removeContact':
+            [('timeline', 'group_id', 'contact_id'), ()],
+        },
+    'lists': {
+        'add':
+            [('timeline', 'name',), ('filter',)],
+        'archive':
+            [('timeline', 'list_id'), ()],
+        'delete':
+            [('timeline', 'list_id'), ()],
+        'getList':
+            [(), ()],
+        'setDefaultList':
+            [('timeline'), ('list_id')],
+        'setName':
+            [('timeline', 'list_id', 'name'), ()],
+        'unarchive':
+            [('timeline',), ('list_id',)]
+        },
+    'locations': {
+        'getList':
+            [(), ()]
+        },
+    'reflection': {
+        'getMethodInfo':
+            [('methodName',), ()],
+        'getMethods':
+            [(), ()]
+        },
+    'settings': {
+        'getList':
+            [(), ()]
+        },
+    'tasks': {
+        'add':
+            [('timeline', 'name',), ('list_id', 'parse',)],
+        'addTags':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
+             ()],
+        'complete':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id',), ()],
+        'delete':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'), ()],
+        'getList':
+            [(),
+             ('list_id', 'filter', 'last_sync')],
+        'movePriority':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'direction'),
+             ()],
+        'moveTo':
+            [('timeline', 'from_list_id', 'to_list_id', 'taskseries_id', 'task_id'),
+             ()],
+        'postpone':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ()],
+        'removeTags':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
+             ()],
+        'setDueDate':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ('due', 'has_due_time', 'parse')],
+        'setEstimate':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ('estimate',)],
+        'setLocation':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ('location_id',)],
+        'setName':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'name'),
+             ()],
+        'setPriority':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ('priority',)],
+        'setRecurrence':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ('repeat',)],
+        'setTags':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ('tags',)],
+        'setURL':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ('url',)],
+        'uncomplete':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ()],
+        },
+    'tasksNotes': {
+        'add':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'note_title', 'note_text'), ()],
+        'delete':
+            [('timeline', 'note_id'), ()],
+        'edit':
+            [('timeline', 'note_id', 'note_title', 'note_text'), ()]
+        },
+    'test': {
+        'echo':
+            [(), ()],
+        'login':
+            [(), ()]
+        },
+    'time': {
+        'convert':
+            [('to_timezone',), ('from_timezone', 'to_timezone', 'time')],
+        'parse':
+            [('text',), ('timezone', 'dateformat')]
+        },
+    'timelines': {
+        'create':
+            [(), ()]
+        },
+    'timezones': {
+        'getList':
+            [(), ()]
+        },
+    'transactions': {
+        'undo':
+            [('timeline', 'transaction_id'), ()]
+        },
+    }
+
+def createRTM(apiKey, secret, token=None):
+    rtm = RTM(apiKey, secret, token)
+#    if token is None:
+#        print 'No token found'
+#        print 'Give me access here:', rtm.getAuthURL()
+#        raw_input('Press enter once you gave access')
+#        print 'Note down this token for future use:', rtm.getToken()
+
+    return rtm
+
+def test(apiKey, secret, token=None):
+    rtm = createRTM(apiKey, secret, token)
+
+    rspTasks = rtm.tasks.getList(filter='dueWithin:"1 week of today"')
+    print [t.name for t in rspTasks.tasks.list.taskseries]
+    print rspTasks.tasks.list.id
+
+    rspLists = rtm.lists.getList()
+    # print rspLists.lists.list
+    print [(x.name, x.id) for x in rspLists.lists.list]
+
+def set_log_level(level):
+    '''Sets the log level of the logger used by the module.
+    
+    >>> import rtm
+    >>> import logging
+    >>> rtm.set_log_level(logging.INFO)
+    '''
+    
+    #LOG.setLevel(level)

=== modified file 'GTG/gtk/backends_dialog/parameters_ui/__init__.py'
--- GTG/gtk/backends_dialog/parameters_ui/__init__.py	2013-02-25 07:35:07 +0000
+++ GTG/gtk/backends_dialog/parameters_ui/__init__.py	2013-02-25 19:21:24 +0000
@@ -28,13 +28,14 @@
 import functools
 
 from GTG import _
-from GTG.backends.genericbackend import GenericBackend
+from GTG.backends.genericbackend                        import GenericBackend
 from GTG.gtk.backends_dialog.parameters_ui.importtagsui import ImportTagsUI
-from GTG.gtk.backends_dialog.parameters_ui.textui import TextUI
-from GTG.gtk.backends_dialog.parameters_ui.passwordui import PasswordUI
-from GTG.gtk.backends_dialog.parameters_ui.periodui import PeriodUI
-from GTG.gtk.backends_dialog.parameters_ui.checkboxui import CheckBoxUI
-from GTG.gtk.backends_dialog.parameters_ui.pathui import PathUI
+from GTG.gtk.backends_dialog.parameters_ui.textui       import TextUI
+from GTG.gtk.backends_dialog.parameters_ui.passwordui   import PasswordUI
+from GTG.gtk.backends_dialog.parameters_ui.periodui     import PeriodUI
+from GTG.gtk.backends_dialog.parameters_ui.checkboxui   import CheckBoxUI
+from GTG.gtk.backends_dialog.parameters_ui.comboboxui   import ComboBoxUI
+from GTG.gtk.backends_dialog.parameters_ui.pathui       import PathUI
 
 
 class ParametersUI(gtk.VBox):
@@ -43,6 +44,7 @@
     widgets to view and edit a backend configuration
     '''
 
+
     COMMON_WIDTH = 170
 
     def __init__(self, requester):
@@ -54,75 +56,50 @@
         self.req = requester
         self.set_spacing(10)
 
-        # builds a list of widget generators. More precisely, it's a
+        #builds a list of widget generators. More precisely, it's a
         # list of tuples: (backend_parameter_name, widget_generator)
         self.parameter_widgets = (
-            ("import-tags", self.UI_generator(ImportTagsUI,
-                                              {"title": _("Import tags"),
-                                               "anybox_text": _("All tags"),
-                                               "somebox_text": _("Just these \
-                                                                      tags:"),
-                                               "parameter_name": "import-tags\
-                                                                         "})),
-            ("attached-tags", self.UI_generator(ImportTagsUI,
-                                                {"title": _("Tags to sync"),
-                                                 "anybox_text": _("All tasks"),
-                                                 "somebox_text": _("Tasks with\
-                                                                 these tags:"),
-                                                 "parameter_name":
-                                                 "attached-tags"})),
-            ("path", self.UI_generator(PathUI)),
-            ("username", self.UI_generator(TextUI,
-                                           {"description": _("Username"),
-                                            "parameter_name": "username"})),
-            ("password", self.UI_generator(PasswordUI)),
-            ("period", self.UI_generator(PeriodUI)),
-            ("service-url", self.UI_generator(TextUI,
-                                              {"description": _("Service URL"),
-                                               "parameter_name": "service-url"
-                                               })),
-            ("import-from-replies", self.UI_generator(CheckBoxUI,
-                                                      {"text":
-                                                       _("Import tasks\
-                                                         from @ replies " +
+               ("import-tags", self.UI_generator(ImportTagsUI,
+                            {"title": _("Import tags"),
+                             "anybox_text": _("All tags"),
+                             "somebox_text": _("Just these tags:"),
+                             "parameter_name": "import-tags"})),
+               ("attached-tags", self.UI_generator(ImportTagsUI,
+                            {"title": _("Tags to sync"),
+                             "anybox_text": _("All tasks"),
+                             "somebox_text": _("Tasks with these tags:"),
+                             "parameter_name": "attached-tags"})),
+               ("path", self.UI_generator(PathUI)),
+               ("username", self.UI_generator(TextUI,
+                            {"description": _("Username"),
+                             "parameter_name": "username"})),
+               ("password", self.UI_generator(PasswordUI)),
+               ("period", self.UI_generator(PeriodUI)),
+               ("service-url", self.UI_generator(TextUI,
+                            {"description": _("Service URL"),
+                             "parameter_name": "service-url"})),
+               ("import-from-replies", self.UI_generator(CheckBoxUI,
+                            {"text": _("Import tasks from @ replies " + \
                                                          "directed to you"),
-                                                       "parameter":
-                                                       "import-from-replies"
-                                                       })),
-            ("import-from-direct-messages", self.UI_generator(CheckBoxUI,
-                                                              {"text":
-                                                               _("Import tasks"
-                                                                 "from direct "
-                                                                 "messages"),
-                                                               "parameter":
-                                                               "import-from-\
-                                                               direct-messages\
-                                                               "})),
-            ("import-from-my-tweets", self.UI_generator(CheckBoxUI,
-                                                        {"text": _("Import \
-                                                                 tasks from \
-                                                                 your tweets"),
-                                                         "parameter":
-                                                         "import-from-my-\
-                                                         tweets"})),
-            ("import-bug-tags", self.UI_generator(CheckBoxUI,
-                                                  {"text": _("Tag your GTG "
-                                                             "tasks with the "
-                                                             "bug tags"),
-                                                   "parameter": "import-bug-"
-                                                                "tags"})),
-            ("tag-with-project-name", self.UI_generator(CheckBoxUI,
-                                                        {"text":
-                                                         _("Tag your "
-                                                           "GTG tasks with "
-                                                           "the project"
-                                                           "targeted by the"
-                                                           " bug"),
-                                                         "parameter":
-                                                         "tag-with-project-"
-                                                         "name"})), )
+                             "parameter": "import-from-replies"})),
+               ("import-from-direct-messages", self.UI_generator(CheckBoxUI,
+                            {"text": _("Import tasks from direct messages"),
+                             "parameter": "import-from-direct-messages"})),
+               ("import-from-my-tweets", self.UI_generator(CheckBoxUI,
+                            {"text": _("Import tasks from your tweets"),
+                             "parameter": "import-from-my-tweets"})),
+               ("import-bug-tags", self.UI_generator(CheckBoxUI,
+                           {"text": _("Tag your GTG tasks with the bug tags"),
+                             "parameter": "import-bug-tags"})),
+               ("tag-with-project-name", self.UI_generator(CheckBoxUI,
+                            {"text": _("Tag your GTG tasks with the project "
+                                       "targeted by the bug"),
+                             "parameter": "tag-with-project-name"})), 
+               ("default-list-to-sync-to", self.UI_generator(ComboBoxUI,
+                            {"text": _("The list to sync back to:"),
+                             "parameter": "default-list-to-sync-to"})), )
 
-    def UI_generator(self, param_type, special_arguments={}):
+    def UI_generator(self, param_type, special_arguments = {}):
         '''A helper function to build a widget type from a template.
         It passes to the created widget generator a series of common
          parameters, plus the ones needed to specialize the given template
@@ -134,9 +111,9 @@
         @return function: return a widget generator, not a widget. the widget
                            can be obtained by calling widget_generator(backend)
         '''
-        return lambda backend: param_type(req=self.req,
-                                          backend=backend,
-                                          width=self.COMMON_WIDTH,
+        return lambda backend: param_type(req = self.req,
+                                          backend = backend,
+                                          width = self.COMMON_WIDTH,
                                           **special_arguments)
 
     def refresh(self, backend):
@@ -145,14 +122,14 @@
 
         @param backend: the backend that is being configured
         '''
-        # remove the old parameters UIs
+        #remove the old parameters UIs
         def _remove_child(self, child):
             self.remove(child)
         self.foreach(functools.partial(_remove_child, self))
-        # add new widgets
+        #add new widgets
         backend_parameters = backend.get_parameters()
         if backend_parameters[GenericBackend.KEY_DEFAULT_BACKEND]:
-            # if it's the default backend, the user should not mess with it
+            #if it's the default backend, the user should not mess with it
             return
         for parameter_name, widget in self.parameter_widgets:
             if parameter_name in backend_parameters:

=== added file 'GTG/gtk/backends_dialog/parameters_ui/comboboxui.py'
--- GTG/gtk/backends_dialog/parameters_ui/comboboxui.py	1970-01-01 00:00:00 +0000
+++ GTG/gtk/backends_dialog/parameters_ui/comboboxui.py	2013-02-25 19:21:24 +0000
@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things GNOME! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2012 - 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/>.
+# -----------------------------------------------------------------------------
+
+import gtk
+import unicodedata
+
+
+class ComboBoxUI(gtk.HBox):
+    '''
+    It's a widget displaying a simple combobox, with some text to explain its
+    meaning
+    '''
+
+    def __init__(self, req, backend, width, text, parameter):
+        '''
+        Creates the combobox and the related label.
+
+        @param req: a Requester
+        @param backend: a backend object
+        @param width: the width of the gtk.Label object
+        @param parameter: the backend parameter this combobox should display
+                           and modify
+        '''
+        super(ComboBoxUI, self).__init__()
+        self.backend = backend
+        self.req = req
+        self.text = text
+        self.parameter = parameter
+        self._populate_gtk(width)
+
+    def _populate_gtk(self, width):
+        '''Creates the combobox and the related label
+
+        @param width: the width of the gtk.Label object
+        '''
+        # we expect a list of tuples, formed the same way all!s
+        data = self.backend.get_liststore_data(self.parameter)        
+        types = [type(p) for p in data[0]]
+        name_store = gtk.ListStore(*types)
+        for d in data:
+            name_store.append(d)
+        self.combobox = gtk.ComboBox()
+        self.combobox.set_model(name_store)
+        self.combobox.set_entry_text_column(1)
+        self.combobox.connect("changed", self.on_modified)
+        renderer_text = gtk.CellRendererText()
+        self.combobox.pack_start(renderer_text, True)
+        self.combobox.add_attribute(renderer_text, "text", 1)
+        d_val = self.backend.get_parameters()[self.parameter]
+        if d_val != None:
+            i = 0
+            for mid, name in name_store:
+                if mid == d_val:
+                    self.combobox.set_active(i)
+                i=i+1
+        else:
+            #activate default
+            self.combobox.set_active(0)
+        self.box = gtk.HBox(spacing=6)
+        self.add(self.box)
+        self.label = gtk.Label("Please select a list where to sync gtg tasks to")
+        self.box.pack_start(self.label)
+        self.box.pack_start(self.combobox)
+        self.commit_changes() # have a default value set while asap
+
+    def commit_changes(self):
+        '''Saves the changes to the backend parameter'''
+        
+        tree_iter = self.combobox.get_active_iter()
+        if tree_iter != None:
+            model = self.combobox.get_model()
+            glist = model[tree_iter][0]
+            self.backend.set_parameter(self.parameter,
+                        glist)
+            
+
+    def on_modified(self, sender = None):
+        ''' Signal callback, executed when the user changes on the combobox.
+        Disables the backend. The user will re-enable it to confirm the changes
+        (s)he made.
+
+        @param sender: not used, only here for signal compatibility
+        '''
+        if self.backend.is_enabled() and not self.backend.is_default():
+            self.req.set_backend_enabled(self.backend.get_id(), False)
+        self.commit_changes()


Follow ups