← Back to team overview

gtg team mailing list archive

[Merge] lp:~toolpart/gtg/fix-858762 into lp:gtg

 

ViktorNagy has proposed merging lp:~toolpart/gtg/fix-858762 into lp:gtg.

Requested reviews:
  Gtg developers (gtg)
Related bugs:
  Bug #858762 in Getting Things GNOME!: "Task.remove tag might fail with AttributeError"
  https://bugs.launchpad.net/gtg/+bug/858762

For more details, see:
https://code.launchpad.net/~toolpart/gtg/fix-858762/+merge/76893

fixes 858762
-- 
https://code.launchpad.net/~toolpart/gtg/fix-858762/+merge/76893
Your team Gtg developers is requested to review the proposed merge of lp:~toolpart/gtg/fix-858762 into lp:gtg.
=== added file 'GTG/backends/backend_openerp.py'
--- GTG/backends/backend_openerp.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/backend_openerp.py	2011-09-25 09:20:29 +0000
@@ -0,0 +1,400 @@
+# -*- encoding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+'''
+OpenERP backend
+'''
+
+import os
+import uuid
+import datetime
+import openerplib
+
+from xdg.BaseDirectory import xdg_cache_home
+
+from GTG import _
+from GTG.backends.genericbackend        import GenericBackend
+from GTG.backends.periodicimportbackend import PeriodicImportBackend
+from GTG.backends.backendsignals import BackendSignals
+from GTG.backends.syncengine     import SyncEngine, SyncMeme
+from GTG.core.task               import Task
+from GTG.tools.dates import RealDate, no_date
+from GTG.tools.interruptible            import interruptible
+from GTG.tools.logger import Log
+
+def as_datetime(datestr):
+    if not datestr:
+        return no_date
+
+    return RealDate(datetime.datetime.strptime(datestr[:10], "%Y-%m-%d").date())
+
+class Backend(PeriodicImportBackend):
+
+    _general_description = { \
+        GenericBackend.BACKEND_NAME:       "backend_openerp", \
+        GenericBackend.BACKEND_HUMAN_NAME: _("OpenERP"), \
+        GenericBackend.BACKEND_AUTHORS:    ["Viktor Nagy"], \
+        GenericBackend.BACKEND_TYPE:       GenericBackend.TYPE_READWRITE, \
+        GenericBackend.BACKEND_DESCRIPTION: \
+            _("This backend synchronizes your tasks with an OpenERP server"),
+        }
+
+    _static_parameters = {
+        "username": {
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING,
+            GenericBackend.PARAM_DEFAULT_VALUE: "insert your username here"
+        },
+        "password": {
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_PASSWORD,
+            GenericBackend.PARAM_DEFAULT_VALUE: "",
+        },
+        "server_host": {
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING,
+            GenericBackend.PARAM_DEFAULT_VALUE: "erp.toolpart.hu",
+        },
+        "protocol": {
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING,
+            GenericBackend.PARAM_DEFAULT_VALUE: "xmlrpcs"
+        },
+        "server_port": {
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT,
+            GenericBackend.PARAM_DEFAULT_VALUE: 8071,
+        },
+        "database": {
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING,
+            GenericBackend.PARAM_DEFAULT_VALUE: "ToolPartTeam",
+        },
+        "period": {
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT,
+            GenericBackend.PARAM_DEFAULT_VALUE: 10,
+        },
+        "is-first-run": {
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL,
+            GenericBackend.PARAM_DEFAULT_VALUE: True,
+        },
+        GenericBackend.KEY_ATTACHED_TAGS: {
+             GenericBackend.PARAM_TYPE: GenericBackend.TYPE_LIST_OF_STRINGS,
+             GenericBackend.PARAM_DEFAULT_VALUE: ['@OpenERP'],
+        }
+    }
+
+###############################################################################
+### 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/openerp/', \
+                                      "sync_engine-" + self.get_id())
+        self.sync_engine = self._load_pickled_file(self.sync_engine_path, \
+                                                   SyncEngine())
+
+    def do_periodic_import(self):
+        if not self._check_server():
+            return
+
+        self.cancellation_point()
+        self._sync_tasks()
+
+    def save_state(self):
+        """
+        See GenericBackend for an explanation of this function.
+        """
+        self._store_pickled_file(self.sync_engine_path, self.sync_engine)
+
+    @interruptible
+    def remove_task(self, tid):
+        """
+        See GenericBackend for an explanation of this function.
+        """
+        self.cancellation_point()
+        try:
+            oerp_id = self.sync_engine.get_remote_id(tid)
+            Log.debug("removing task %s from OpenERP" % oerp_id)
+            self._unlink_task(oerp_id)
+        except KeyError:
+            pass
+        try:
+            self.sync_engine.break_relationship(local_id = tid)
+        except:
+            pass
+
+    @interruptible
+    def set_task(self, task):
+        """
+        TODO: write set_task method
+        """
+        self.cancellation_point()
+        tid = task.get_id()
+        is_syncable = self._gtg_task_is_syncable_per_attached_tags(task)
+        action, oerp_id = self.sync_engine.analyze_local_id( \
+                                tid, \
+                                self.datastore.has_task, \
+                                self._erp_has_task, \
+                                is_syncable)
+        Log.debug("GTG->OERP set task (%s, %s)" % (action, is_syncable))
+
+        if action == None:
+            return
+
+        if action == SyncEngine.ADD:
+            Log.debug('Adding task')
+            return # raise NotImplementedError
+
+        elif action == SyncEngine.UPDATE:
+            # we deal only with updating openerp state
+            by_status = {
+                Task.STA_ACTIVE: lambda oerp_id: \
+                    self._set_open(oerp_id),
+                Task.STA_DISMISSED: lambda oerp_id: \
+                    self._set_state(oerp_id,'cancel'),
+                Task.STA_DONE: lambda oerp_id: \
+                    self._set_state(oerp_id,'close'),
+            }
+            try:
+                by_status[task.get_status()](oerp_id)
+            except:
+                # the given state transition might not be available
+                raise
+
+        elif action == SyncEngine.REMOVE:
+            self.datastore.request_task_deletion(tid)
+            try:
+                self.sync_engine.break_relationship(local_id = tid)
+            except KeyError:
+                pass
+
+        elif action == SyncEngine.LOST_SYNCABILITY:
+            pass
+
+###################################
+### OpenERP related
+###################################
+
+    def _check_server(self):
+        """connect to server"""
+        Log.debug( 'checking server connection' )
+        try:
+            self.server = openerplib.get_connection(
+                hostname=self._parameters["server_host"],
+                port=self._parameters["server_port"],
+                protocol=self._parameters["protocol"],
+                database=self._parameters["database"],
+                login=self._parameters["username"],
+                password=self._parameters["password"])
+        except:
+            self.server = None
+            BackendSignals().backend_failed(self.get_id(), \
+                            BackendSignals.ERRNO_NETWORK)
+            return False
+
+        try:
+            self.server.check_login()
+        except:
+            self.server = None
+            BackendSignals().backend_failed(self.get_id(), \
+                            BackendSignals.ERRNO_AUTHENTICATION)
+            return False
+
+        return True
+
+    def _get_model(self):
+        if not self.server:
+            self._check_server()
+        return self.server.get_model('project.task')
+
+    def _sync_tasks(self):
+        '''
+        Download tasks from the server and register them in GTG
+
+        Existing tasks should not be registered.
+        '''
+        task_model = self._get_model()
+        task_ids = task_model.search([("user_id","=", self.server.user_id)])
+        tasks = task_model.read(task_ids,
+                    ['name', 'description', 'context_id',
+                     'date_deadline', 'notes', 'priority',
+                    'timebox_id', 'project_id', 'state',
+                    'date_start', 'date_end'])
+        self.cancellation_point()
+        # merge last modified date with generic task data
+        logs = task_model.perm_read(task_ids, {}, False)
+        self.cancellation_point()
+        def get_task_id(id):
+            return '%s$%d' % (self._parameters['server_host'], id)
+
+        def adjust_task(task):
+            id = task['id']
+            task['rid'] = get_task_id(id)
+            return (id, task)
+
+        tasks = dict(map(adjust_task, tasks))
+        map(lambda l: tasks[l['id']].update(l), logs)
+        Log.debug(str(tasks))
+
+        for task in tasks.values():
+            self._process_openerp_task(task)
+
+        #removing the old ones
+        last_task_list = self.sync_engine.get_all_remote()
+        new_task_keys = map(get_task_id, tasks.keys())
+        for task_link in set(last_task_list).difference(set(new_task_keys)):
+            self.cancellation_point()
+            #we make sure that the other backends are not modifying the task
+            # set
+            with self.datastore.get_backend_mutex():
+                tid = self.sync_engine.get_local_id(task_link)
+                self.datastore.request_task_deletion(tid)
+                try:
+                    self.sync_engine.break_relationship(remote_id = task_link)
+                except KeyError:
+                    pass
+
+    def _process_openerp_task(self, task):
+        '''
+        From the task data find out if this task already exists or should be
+        updated.
+        '''
+        Log.debug("Processing task %s (%d)" % (task['name'], task['id']))
+        action, tid = self.sync_engine.analyze_remote_id(task['rid'],
+                self.datastore.has_task, lambda b: True)
+
+        if action == None:
+            return
+
+        self.cancellation_point()
+        with self.datastore.get_backend_mutex():
+            if action == SyncEngine.ADD:
+                tid = str(uuid.uuid4())
+                gtg = self.datastore.task_factory(tid)
+                self._populate_task(gtg, task)
+                self.sync_engine.record_relationship(local_id = tid,\
+                            remote_id = task['rid'], \
+                            meme = SyncMeme(\
+                                        gtg.get_modified(), \
+                                        as_datetime(task['write_date']), \
+                                        self.get_id()))
+                self.datastore.push_task(gtg)
+
+            elif action == SyncEngine.UPDATE:
+                gtg = self.datastore.get_task(tid)
+                self._populate_task(gtg, task)
+                meme = self.sync_engine.get_meme_from_remote_id( \
+                                                    task['rid'])
+                meme.set_local_last_modified(gtg.get_modified())
+                meme.set_remote_last_modified(as_datetime(task['write_date']))
+        self.save_state()
+
+    def _populate_task(self, gtg, oerp):
+        '''
+        Fills a GTG task with the data from a launchpad bug.
+
+        @param gtg: a Task in GTG
+        @param oerp: a Task in OpenERP
+        '''
+        # draft, open, pending, cancelled, done
+        if oerp["state"] in ['draft', 'open', 'pending']:
+            gtg.set_status(Task.STA_ACTIVE)
+        elif oerp['state'] == 'done':
+            gtg.set_status(Task.STA_DONE)
+        else:
+            gtg.set_status(Task.STA_DISMISSED)
+        if gtg.get_title() != oerp['name']:
+            gtg.set_title(oerp['name'])
+
+        text = ''
+        if oerp['description']:
+            text += oerp['description']
+        if oerp['notes']:
+            text += '\n\n' + oerp['notes']
+        if gtg.get_excerpt() != text:
+            gtg.set_text(text)
+
+        if oerp['date_deadline'] and \
+           gtg.get_due_date() != as_datetime(oerp['date_deadline']):
+            gtg.set_due_date(as_datetime(oerp['date_deadline']))
+        if oerp['date_start'] and \
+           gtg.get_start_date() != as_datetime(oerp['date_start']):
+            gtg.set_start_date(as_datetime(oerp['date_start']))
+        if oerp['date_end'] and \
+           gtg.get_closed_date() != as_datetime(oerp['date_end']):
+            gtg.set_closed_date(as_datetime(oerp['date_end']))
+
+        tags = [oerp['project_id'][1].replace(' ', '_')]
+        if self._parameters[GenericBackend.KEY_ATTACHED_TAGS]:
+            tags.extend(self._parameters[GenericBackend.KEY_ATTACHED_TAGS])
+        # priority
+        priorities = {
+            '4': _('VeryLow'),
+            '3': _('Low'),
+            '2': _('Medium'),
+            '1': _('Urgent'),
+            '0': _('VeryUrgent'),
+        }
+        tags.append(priorities[oerp['priority']])
+        if oerp.has_key('context_id'):
+            tags.append(oerp['context_id'] \
+                    and oerp['context_id'][1].replace(' ', '_') or "NoContext")
+        if oerp.has_key('timebox_id'):
+            tags.append(oerp['timebox_id'] \
+                    and oerp['timebox_id'][1].replace(' ', '_') or 'NoTimebox')
+        new_tags = set(['@' + str(tag) for tag in filter(None, tags)])
+
+        current_tags = set(gtg.get_tags_name())
+        #remove the lost tags
+        for tag in current_tags.difference(new_tags):
+            gtg.remove_tag(tag)
+        #add the new ones
+        for tag in new_tags.difference(current_tags):
+            gtg.add_tag(tag)
+        gtg.add_remote_id(self.get_id(), oerp['rid'])
+
+    def _unlink_task(self, task_id):
+        """Delete a task on the server"""
+        task_model = self._get_model()
+        task_id = int(task_id.split('$')[1])
+        task_model.unlink(task_id)
+
+    def _erp_has_task(self, task_id):
+        Log.debug('Checking task %d' % int(task_id.split('$')[1]))
+        task_model = self._get_model()
+        if task_model.read(int(task_id.split('$')[1]), ['id']):
+            return True
+        return False
+
+    def _set_state(self, oerp_id, state):
+        Log.debug('Setting task %s to %s' % (oerp_id, state))
+        task_model = self._get_model()
+        oerp_id = int(oerp_id.split('$')[1])
+        getattr(task_model, 'do_%s' % state)([oerp_id])
+
+    def _set_open(self, oerp_id):
+        ''' this might mean reopen or open '''
+        task_model = self._get_model()
+        tid = int(oerp_id.split('$')[1])
+        if task_model.read(tid, ['state'])['state'] == 'draft':
+            self._set_state(oerp_id, 'open')
+        else:
+            self._set_state(oerp_id, 'reopen')
+

=== modified file 'GTG/backends/genericbackend.py'
--- GTG/backends/genericbackend.py	2010-08-09 14:09:14 +0000
+++ GTG/backends/genericbackend.py	2011-09-25 09:20:29 +0000
@@ -87,8 +87,8 @@
         '''
         Called each time it is enabled (including on backend creation).
         Please note that a class instance for each disabled backend *is*
-        created, but it's not initialized. 
-        Optional. 
+        created, but it's not initialized.
+        Optional.
         NOTE: make sure to call super().initialize()
         '''
         #NOTE: I'm disabling this since support for runtime checking of the
@@ -124,7 +124,7 @@
     def remove_task(self, tid):
         ''' This function is called from GTG core whenever a task must be
         removed from the backend. Note that the task could be not present here.
-        
+
         @param tid: the id of the task to delete
         '''
         pass
@@ -136,7 +136,7 @@
         This function is needed only in the default backend (XML localfile,
         currently).
         The xml parameter is an object containing GTG default tasks.
-        
+
         @param xml: an xml object containing the default tasks.
         '''
         pass
@@ -159,7 +159,7 @@
     def quit(self, disable = False):
         '''
         Called when GTG quits or the user wants to disable the backend.
-        
+
         @param disable: If disable is True, the backend won't
                         be automatically loaded when GTG starts
         '''
@@ -209,7 +209,7 @@
     # For an example, see the GTG/backends/backend_localfile.py file
     # Each dictionary contains the keys:
     PARAM_DEFAULT_VALUE = "default_value" # its default value
-    PARAM_TYPE = "type"  
+    PARAM_TYPE = "type"
     #PARAM_TYPE is one of the following (changing this changes the way
     # the user can configure the parameter)
     TYPE_PASSWORD = "password" #the real password is stored in the GNOME
@@ -264,7 +264,7 @@
                                          PARAM_TYPE: TYPE_LIST_OF_STRINGS, \
                                          PARAM_DEFAULT_VALUE: [ALLTASKS_TAG], \
                                     }}
-    
+
     #Handy dictionary used in type conversion (from string to type)
     _type_converter = {TYPE_STRING: str,
                        TYPE_INT: int,
@@ -286,7 +286,7 @@
                 temp_dic[key] = value
         for key, value in cls._static_parameters.iteritems():
             temp_dic[key] = value
-        return temp_dic 
+        return temp_dic
 
     def __init__(self, parameters):
         """
@@ -313,7 +313,7 @@
         if Log.is_debugging_mode():
             self.timer_timestep = 5
         else:
-            self.timer_timestep = 1 
+            self.timer_timestep = 1
         self.to_set_timer = None
         self.please_quit = False
         self.cancellation_point = lambda: _cancellation_point(\
@@ -560,7 +560,7 @@
         try:
             os.makedirs(os.path.dirname(path))
         except OSError, exception:
-            if exception.errno != errno.EEXIST: 
+            if exception.errno != errno.EEXIST:
                 raise
         #saving
         with open(path, 'wb') as file:
@@ -619,7 +619,7 @@
     def launch_setting_thread(self, bypass_quit_request = False):
         '''
         This function is launched as a separate thread. Its job is to perform
-        the changes that have been issued from GTG core. 
+        the changes that have been issued from GTG core.
         In particular, for each task in the self.to_set queue, a task
         has to be modified or to be created (if the tid is new), and for
         each task in the self.to_remove queue, a task has to be deleted
@@ -651,7 +651,7 @@
         ''' Save the task in the backend. In particular, it just enqueues the
         task in the self.to_set queue. A thread will shortly run to apply the
         requested changes.
-        
+
         @param task: the task that should be saved
         '''
         tid = task.get_id()

=== added directory 'GTG/backends/openerplib'
=== added file 'GTG/backends/openerplib/__init__.py'
--- GTG/backends/openerplib/__init__.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/openerplib/__init__.py	2011-09-25 09:20:29 +0000
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright (C) Stephane Wirtel
+# Copyright (C) 2011 Nicolas Vanhoren
+# Copyright (C) 2011 OpenERP s.a. (<http://openerp.com>).
+# All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met: 
+# 
+# 1. Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer. 
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution. 
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+# 
+##############################################################################
+
+from main import *
+

=== added file 'GTG/backends/openerplib/dates.py'
--- GTG/backends/openerplib/dates.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/openerplib/dates.py	2011-09-25 09:20:29 +0000
@@ -0,0 +1,97 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright (C) Stephane Wirtel
+# Copyright (C) 2011 Nicolas Vanhoren
+# Copyright (C) 2011 OpenERP s.a. (<http://openerp.com>).
+# All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met: 
+# 
+# 1. Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer. 
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution. 
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+# 
+##############################################################################
+
+import datetime
+
+DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"
+DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S"
+DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % (
+    DEFAULT_SERVER_DATE_FORMAT,
+    DEFAULT_SERVER_TIME_FORMAT)
+
+def str_to_datetime(str):
+    """
+    Converts a string to a datetime object using OpenERP's
+    datetime string format (exemple: '2011-12-01 15:12:35').
+    
+    No timezone information is added, the datetime is a naive instance, but
+    according to OpenERP 6.1 specification the timezone is always UTC.
+    """
+    if not str:
+        return str
+    return datetime.datetime.strptime(str, DEFAULT_SERVER_DATETIME_FORMAT)
+
+def str_to_date(str):
+    """
+    Converts a string to a date object using OpenERP's
+    date string format (exemple: '2011-12-01').
+    """
+    if not str:
+        return str
+    return datetime.datetime.strptime(str, DEFAULT_SERVER_DATE_FORMAT).date()
+
+def str_to_time(str):
+    """
+    Converts a string to a time object using OpenERP's
+    time string format (exemple: '15:12:35').
+    """
+    if not str:
+        return str
+    return datetime.datetime.strptime(str, DEFAULT_SERVER_TIME_FORMAT).time()
+
+def datetime_to_str(obj):
+    """
+    Converts a datetime object to a string using OpenERP's
+    datetime string format (exemple: '2011-12-01 15:12:35').
+    
+    The datetime instance should not have an attached timezone and be in UTC.
+    """
+    if not obj:
+        return False
+    return obj.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
+
+def date_to_str(obj):
+    """
+    Converts a date object to a string using OpenERP's
+    date string format (exemple: '2011-12-01').
+    """
+    if not obj:
+        return False
+    return obj.strftime(DEFAULT_SERVER_DATE_FORMAT)
+
+def time_to_str(obj):
+    """
+    Converts a time object to a string using OpenERP's
+    time string format (exemple: '15:12:35').
+    """
+    if not obj:
+        return False
+    return obj.strftime(DEFAULT_SERVER_TIME_FORMAT)
+

=== added file 'GTG/backends/openerplib/main.py'
--- GTG/backends/openerplib/main.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/openerplib/main.py	2011-09-25 09:20:29 +0000
@@ -0,0 +1,413 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright (C) Stephane Wirtel
+# Copyright (C) 2011 Nicolas Vanhoren
+# Copyright (C) 2011 OpenERP s.a. (<http://openerp.com>).
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+##############################################################################
+
+"""
+OpenERP Client Library
+
+Home page: http://pypi.python.org/pypi/openerp-client-lib
+Code repository: https://code.launchpad.net/~niv-openerp/openerp-client-lib/trunk
+"""
+
+import xmlrpclib
+import logging
+import socket
+
+try:
+    import cPickle as pickle
+except ImportError:
+    import pickle
+
+try:
+    import cStringIO as StringIO
+except ImportError:
+    import StringIO
+
+_logger = logging.getLogger(__name__)
+
+def _getChildLogger(logger, subname):
+    return logging.getLogger(logger.name + "." + subname)
+
+class Connector(object):
+    """
+    The base abstract class representing a connection to an OpenERP Server.
+    """
+
+    __logger = _getChildLogger(_logger, 'connector')
+
+    def __init__(self, hostname, port):
+        """
+        Initilize by specifying an hostname and a port.
+        :param hostname: Host name of the server.
+        :param port: Port for the connection to the server.
+        """
+        self.hostname = hostname
+        self.port = port
+
+class XmlRPCConnector(Connector):
+    """
+    A type of connector that uses the XMLRPC protocol.
+    """
+    PROTOCOL = 'xmlrpc'
+
+    __logger = _getChildLogger(_logger, 'connector.xmlrpc')
+
+    def __init__(self, hostname, port=8069):
+        """
+        Initialize by specifying the hostname and the port.
+        :param hostname: The hostname of the computer holding the instance of OpenERP.
+        :param port: The port used by the OpenERP instance for XMLRPC (default to 8069).
+        """
+        Connector.__init__(self, hostname, port)
+        self.url = 'http://%s:%d/xmlrpc' % (self.hostname, self.port)
+
+    def send(self, service_name, method, *args):
+        url = '%s/%s' % (self.url, service_name)
+        service = xmlrpclib.ServerProxy(url)
+        return getattr(service, method)(*args)
+
+class XmlRPCSConnector(XmlRPCConnector):
+    """
+    A type of connector that uses the secured XMLRPC protocol.
+    """
+    PROTOCOL = 'xmlrpcs'
+
+    __logger = _getChildLogger(_logger, 'connector.xmlrpcs')
+
+    def __init__(self, hostname, port=8071):
+        super(XmlRPCSConnector, self).__init__(hostname, port)
+        self.url = 'https://%s:%d/xmlrpc' % (self.hostname, self.port)
+
+class NetRPC_Exception(Exception):
+    """
+    Exception for NetRPC errors.
+    """
+    def __init__(self, faultCode, faultString):
+        self.faultCode = faultCode
+        self.faultString = faultString
+        self.args = (faultCode, faultString)
+
+class NetRPC(object):
+    """
+    Low level class for NetRPC protocol.
+    """
+    def __init__(self, sock=None):
+        if sock is None:
+            self.sock = socket.socket(
+            socket.AF_INET, socket.SOCK_STREAM)
+        else:
+            self.sock = sock
+        self.sock.settimeout(120)
+    def connect(self, host, port=False):
+        if not port:
+            buf = host.split('//')[1]
+            host, port = buf.split(':')
+        self.sock.connect((host, int(port)))
+
+    def disconnect(self):
+        self.sock.shutdown(socket.SHUT_RDWR)
+        self.sock.close()
+
+    def mysend(self, msg, exception=False, traceback=None):
+        msg = pickle.dumps([msg,traceback])
+        size = len(msg)
+        self.sock.send('%8d' % size)
+        self.sock.send(exception and "1" or "0")
+        totalsent = 0
+        while totalsent < size:
+            sent = self.sock.send(msg[totalsent:])
+            if sent == 0:
+                raise RuntimeError, "socket connection broken"
+            totalsent = totalsent + sent
+
+    def myreceive(self):
+        buf=''
+        while len(buf) < 8:
+            chunk = self.sock.recv(8 - len(buf))
+            if chunk == '':
+                raise RuntimeError, "socket connection broken"
+            buf += chunk
+        size = int(buf)
+        buf = self.sock.recv(1)
+        if buf != "0":
+            exception = buf
+        else:
+            exception = False
+        msg = ''
+        while len(msg) < size:
+            chunk = self.sock.recv(size-len(msg))
+            if chunk == '':
+                raise RuntimeError, "socket connection broken"
+            msg = msg + chunk
+        msgio = StringIO.StringIO(msg)
+        unpickler = pickle.Unpickler(msgio)
+        unpickler.find_global = None
+        res = unpickler.load()
+
+        if isinstance(res[0],Exception):
+            if exception:
+                raise NetRPC_Exception(str(res[0]), str(res[1]))
+            raise res[0]
+        else:
+            return res[0]
+
+class NetRPCConnector(Connector):
+    """
+    A type of connector that uses the NetRPC protocol.
+    """
+
+    PROTOCOL = 'netrpc'
+
+    __logger = _getChildLogger(_logger, 'connector.netrpc')
+
+    def __init__(self, hostname, port=8070):
+        """
+        Initialize by specifying the hostname and the port.
+        :param hostname: The hostname of the computer holding the instance of OpenERP.
+        :param port: The port used by the OpenERP instance for NetRPC (default to 8070).
+        """
+        Connector.__init__(self, hostname, port)
+
+    def send(self, service_name, method, *args):
+        socket = NetRPC()
+        socket.connect(self.hostname, self.port)
+        socket.mysend((service_name, method, )+args)
+        result = socket.myreceive()
+        socket.disconnect()
+        return result
+
+class Service(object):
+    """
+    A class to execute RPC calls on a specific service of the remote server.
+    """
+    def __init__(self, connector, service_name):
+        """
+        :param connector: A valid Connector instance.
+        :param service_name: The name of the service on the remote server.
+        """
+        self.connector = connector
+        self.service_name = service_name
+        self.__logger = _getChildLogger(_getChildLogger(_logger, 'service'),service_name)
+
+    def __getattr__(self, method):
+        """
+        :param method: The name of the method to execute on the service.
+        """
+        self.__logger.debug('method: %r', method)
+        def proxy(*args):
+            """
+            :param args: A list of values for the method
+            """
+            self.__logger.debug('args: %r', args)
+            result = self.connector.send(self.service_name, method, *args)
+            self.__logger.debug('result: %r', result)
+            return result
+        return proxy
+
+class Connection(object):
+    """
+    A class to represent a connection with authentication to an OpenERP Server.
+    It also provides utility methods to interact with the server more easily.
+    """
+    __logger = _getChildLogger(_logger, 'connection')
+
+    def __init__(self, connector,
+                 database=None,
+                 login=None,
+                 password=None,
+                 user_id=None):
+        """
+        Initialize with login information. The login information is facultative to allow specifying
+        it after the initialization of this object.
+
+        :param connector: A valid Connector instance to send messages to the remote server.
+        :param database: The name of the database to work on.
+        :param login: The login of the user.
+        :param password: The password of the user.
+        :param user_id: The user id is a number identifying the user. This is only useful if you
+        already know it, in most cases you don't need to specify it.
+        """
+        self.connector = connector
+
+        self.set_login_info(database, login, password, user_id)
+
+    def set_login_info(self, database, login, password, user_id=None):
+        """
+        Set login information after the initialisation of this object.
+
+        :param connector: A valid Connector instance to send messages to the remote server.
+        :param database: The name of the database to work on.
+        :param login: The login of the user.
+        :param password: The password of the user.
+        :param user_id: The user id is a number identifying the user. This is only useful if you
+        already know it, in most cases you don't need to specify it.
+        """
+        self.database, self.login, self.password = database, login, password
+
+        self.user_id = user_id
+
+    def check_login(self, force=True):
+        """
+        Checks that the login information is valid. Throws an AuthenticationError if the
+        authentication fails.
+
+        :param force: Force to re-check even if this Connection was already validated previously.
+        Default to True.
+        """
+        if self.user_id and not force:
+            return
+
+        if not self.database or not self.login or self.password is None:
+            raise AuthenticationError("Creditentials not provided")
+
+        self.user_id = self.get_service("common").login(self.database, self.login, self.password)
+        if not self.user_id:
+            raise AuthenticationError("Authentication failure")
+        self.__logger.debug("Authenticated with user id %s", self.user_id)
+
+    def get_model(self, model_name):
+        """
+        Returns a Model instance to allow easy remote manipulation of an OpenERP model.
+
+        :param model_name: The name of the model.
+        """
+        return Model(self, model_name)
+
+    def get_service(self, service_name):
+        """
+        Returns a Service instance to allow easy manipulation of one of the services offered by the remote server.
+        Please note this Connection instance does not need to have valid authentication information since authentication
+        is only necessary for the "object" service that handles models.
+
+        :param service_name: The name of the service.
+        """
+        return Service(self.connector, service_name)
+
+class AuthenticationError(Exception):
+    """
+    An error thrown when an authentication to an OpenERP server failed.
+    """
+    pass
+
+class Model(object):
+    """
+    Useful class to dialog with one of the models provided by an OpenERP server.
+    An instance of this class depends on a Connection instance with valid authentication information.
+    """
+
+    def __init__(self, connection, model_name):
+        """
+        :param connection: A valid Connection instance with correct authentication information.
+        :param model_name: The name of the model.
+        """
+        self.connection = connection
+        self.model_name = model_name
+        self.__logger = _getChildLogger(_getChildLogger(_logger, 'object'), model_name)
+
+    def __getattr__(self, method):
+        """
+        Provides proxy methods that will forward calls to the model on the remote OpenERP server.
+
+        :param method: The method for the linked model (search, read, write, unlink, create, ...)
+        """
+        def proxy(*args):
+            """
+            :param args: A list of values for the method
+            """
+            self.connection.check_login(False)
+            self.__logger.debug(args)
+            result = self.connection.get_service('object').execute(
+                                                    self.connection.database,
+                                                    self.connection.user_id,
+                                                    self.connection.password,
+                                                    self.model_name,
+                                                    method,
+                                                    *args)
+            if method == "read":
+                if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
+                    index = {}
+                    for r in result:
+                        index[r['id']] = r
+                    result = [index[x] for x in args[0] if x in index]
+            self.__logger.debug('result: %r', result)
+            return result
+        return proxy
+
+    def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None, context=None):
+        """
+        A shortcut method to combine a search() and a read().
+
+        :param domain: The domain for the search.
+        :param fields: The fields to extract (can be None or [] to extract all fields).
+        :param offset: The offset for the rows to read.
+        :param limit: The maximum number of rows to read.
+        :param order: The order to class the rows.
+        :param context: The context.
+        :return: A list of dictionaries containing all the specified fields.
+        """
+        record_ids = self.search(domain or [], offset, limit or False, order or False, context or {})
+        records = self.read(record_ids, fields or [], context or {})
+        return records
+
+def get_connector(hostname, protocol="xmlrpc", port="auto"):
+    """
+    A shortcut method to easily create a connector to a remote server using XMLRPC or NetRPC.
+
+    :param hostname: The hostname to the remote server.
+    :param protocol: The name of the protocol, must be "xmlrpc" or "netrpc".
+    :param port: The number of the port. Defaults to auto.
+    """
+    if port == 'auto':
+        port = 8069 if protocol=="xmlrpc" else 8070
+    if protocol == "xmlrpc":
+        return XmlRPCConnector(hostname, port)
+    elif protocol == "xmlrpcs":
+        return XmlRPCSConnector(hostname, port)
+    elif protocol == "netrpc":
+        return NetRPCConnector(hostname, port)
+    else:
+        raise ValueError("You must choose xmlrpc or netrpc")
+
+def get_connection(hostname, protocol="xmlrpc", port='auto', database=None,
+                 login=None, password=None, user_id=None):
+    """
+    A shortcut method to easily create a connection to a remote OpenERP server.
+
+    :param hostname: The hostname to the remote server.
+    :param protocol: The name of the protocol, must be "xmlrpc" or "netrpc".
+    :param port: The number of the port. Defaults to auto.
+    :param connector: A valid Connector instance to send messages to the remote server.
+    :param database: The name of the database to work on.
+    :param login: The login of the user.
+    :param password: The password of the user.
+    :param user_id: The user id is a number identifying the user. This is only useful if you
+    already know it, in most cases you don't need to specify it.
+    """
+    return Connection(get_connector(hostname, protocol, port), database, login, password, user_id)
+

=== modified file 'GTG/core/task.py'
--- GTG/core/task.py	2011-08-17 09:57:00 +0000
+++ GTG/core/task.py	2011-09-25 09:20:29 +0000
@@ -143,7 +143,7 @@
             return True
         else:
             return False
-            
+
     #TODO : should we merge this function with set_title ?
     def set_complex_title(self,text,tags=[]):
         if tags:
@@ -151,9 +151,9 @@
         due_date = no_date
         defer_date = no_date
         if text:
-            
+
             # Get tags in the title
-            #NOTE: the ?: tells regexp that the first one is 
+            #NOTE: the ?: tells regexp that the first one is
             # a non-capturing group, so it must not be returned
             # to findall. http://www.amk.ca/python/howto/regex/regex.html
             # ~~~~Invernizzi
@@ -262,7 +262,7 @@
             pardate = self.req.get_task(par).get_due_date()
             if pardate and zedate > pardate:
                 zedate = pardate
-        
+
         return zedate
 
     def set_start_date(self, fulldate):
@@ -277,7 +277,7 @@
         assert(isinstance(fulldate, Date))
         self.closed_date = fulldate
         self.sync()
-        
+
     def get_closed_date(self):
         return self.closed_date
 
@@ -286,7 +286,7 @@
         if due_date == no_date:
             return None
         return due_date.days_left()
-    
+
     def get_days_late(self):
         due_date = self.get_due_date()
         if due_date == no_date:
@@ -375,7 +375,7 @@
         #we use the inherited childrens
         self.add_child(subt.get_id())
         return subt
-        
+
     def add_child(self, tid):
         """Add a subtask to this task
 
@@ -393,7 +393,7 @@
                 child.add_tag(t.get_name())
         self.sync()
         return True
-            
+
     def remove_child(self,tid):
         """Removed a subtask from the task.
 
@@ -407,8 +407,8 @@
             return True
         else:
             return False
-            
-            
+
+
     #FIXME: remove this function and use liblarch instead.
     def get_subtasks(self):
         tree = self.get_tree()
@@ -462,7 +462,7 @@
         val = unicode(str(att_value), "UTF-8")
         self.attributes[(namespace, att_name)] = val
         self.sync()
-    
+
     def get_attribute(self, att_name, namespace=""):
         """Get the attribute C{att_name}.
 
@@ -479,13 +479,13 @@
             return True
         else:
             return False
-          
-#   the following is not currently needed  
+
+#   the following is not currently needed
 #    def modified(self):
 #        TreeNode.modified(self)
 #        for t in self.get_tags():
 #            gobject.idle_add(t.modified)
-        
+
 
     def _modified_update(self):
         '''
@@ -508,7 +508,7 @@
                 tag = self.req.new_tag(tname)
             l.append(tag)
         return l
-        
+
     def rename_tag(self, old, new):
         eold = saxutils.escape(saxutils.unescape(old))
         enew = saxutils.escape(saxutils.unescape(new))
@@ -535,25 +535,25 @@
                 for child in self.get_subtasks():
                     if child.can_be_deleted:
                         child.add_tag(t)
-            
+
                 tag = self.req.get_tag(t)
                 if not tag:
                     tag = self.req.new_tag(t)
                 tag.modified()
             return True
-    
+
     def add_tag(self, tagname):
         "Add a tag to the task and insert '@tag' into the task's content"
 #        print "add tag %s to task %s" %(tagname,self.get_title())
         if self.tag_added(tagname):
             c = self.content
-            
+
             #strip <content>...</content> tags
             if c.startswith('<content>'):
                 c = c[len('<content>'):]
             if c.endswith('</content>'):
                 c = c[:-len('</content>')]
-            
+
             if not c:
                 # don't need a separator if it's the only text
                 sep = ''
@@ -563,7 +563,7 @@
             else:
                 # other text at the beginning, so put the tag on its own line
                 sep = '\n\n'
-            
+
             self.content = "<content><tag>%s</tag>%s%s</content>" % (
                 tagname, sep, c)
             #we modify the task internal state, thus we have to call for a sync
@@ -581,8 +581,9 @@
         self.content = self._strip_tag(self.content, tagname)
         if modified:
             tag = self.req.get_tag(tagname)
-            tag.modified()
-    
+            if tag:
+                tag.modified()
+
     def set_only_these_tags(self, tags_list):
         '''
         Given a list of strings representing tags, it makes sure that
@@ -602,12 +603,12 @@
                     .replace('<tag>%s</tag>, '%(tagname), newtag) #trail comma
                     .replace('<tag>%s</tag>'%(tagname), newtag)
                     #in case XML is missing (bug #504899)
-                    .replace('%s\n\n'%(tagname), newtag) 
-                    .replace('%s, '%(tagname), newtag) 
+                    .replace('%s\n\n'%(tagname), newtag)
+                    .replace('%s, '%(tagname), newtag)
                     #don't forget a space a the end
                     .replace('%s '%(tagname), newtag)
                )
-     
+
 
     #tag_list is a list of tags names
     #return true if at least one of the list is in the task
@@ -623,7 +624,7 @@
                     if not toreturn:
                         toreturn = children_tag(tagc_name)
             return toreturn
-                
+
         #We want to see if the task has no tags
         toreturn = False
         if notag_only:
@@ -635,7 +636,7 @@
         elif tag_list:
             for tagname in tag_list:
                 if not toreturn:
-                    toreturn = children_tag(tagname) 
+                    toreturn = children_tag(tagname)
         else:
             #Well, if we don't filter on tags or notag, it's true, of course
             toreturn = True

=== modified file 'GTG/plugins/hamster/hamster.py'
--- GTG/plugins/hamster/hamster.py	2011-01-16 21:07:21 +0000
+++ GTG/plugins/hamster/hamster.py	2011-09-25 09:20:29 +0000
@@ -33,39 +33,39 @@
         "description": "title",
         "tags": "existing",
     }
-    
+
     def __init__(self):
         #task editor widget
         self.vbox = None
         self.button=gtk.ToolButton()
-    
+
     #### Interaction with Hamster
     def sendTask(self, task):
         """Send a gtg task to hamster-applet"""
         if task is None: return
         gtg_title = task.get_title()
         gtg_tags = tags=[t.lstrip('@').lower() for t in task.get_tags_name()]
-        
+
         activity = "Other"
         if self.preferences['activity'] == 'tag':
             hamster_activities=set([unicode(x[0]).lower() for x in self.hamster.GetActivities()])
             activity_candidates=hamster_activities.intersection(set(gtg_tags))
             if len(activity_candidates)>=1:
-                activity=list(activity_candidates)[0] 
+                activity=list(activity_candidates)[0]
         elif self.preferences['activity'] == 'title':
             activity = gtg_title
         # hamster can't handle ',' or '@' in activity name
         activity = activity.replace(',', '')
         activity = re.sub('\ +@.*', '', activity)
-        
+
         category = ""
         if self.preferences['category'] == 'auto_tag':
             hamster_activities=dict([(unicode(x[0]), unicode(x[1])) for x in self.hamster.GetActivities()])
             if (gtg_title in hamster_activities
                 or gtg_title.replace(",", "") in hamster_activities):
                     category = "%s" % hamster_activities[gtg_title]
-        
-        if (self.preferences['category'] == 'tag' or 
+
+        if (self.preferences['category'] == 'tag' or
           (self.preferences['category'] == 'auto_tag' and not category)):
             # See if any of the tags match existing categories
             categories = dict([(unicode(x[1]).lower(), unicode(x[1])) for x in self.hamster.GetCategories()])
@@ -81,26 +81,30 @@
            description = gtg_title
         elif self.preferences['description'] == 'contents':
            description = task.get_excerpt(strip_tags=True, strip_subtasks=True)
-        
+
         tag_candidates = []
         try:
             if self.preferences['tags'] == 'existing':
                 hamster_tags = set([unicode(x) for x in self.hamster.GetTags()])
                 tag_candidates = list(hamster_tags.intersection(set(gtg_tags)))
             elif self.preferences['tags'] == 'all':
-                tag_candidates = gtg_tags 
+                tag_candidates = gtg_tags
         except dbus.exceptions.DBusException:
             # old hamster version, doesn't support tags
             pass
         tag_str = "".join([" #" + x for x in tag_candidates])
-            
-        #print '%s%s,%s%s'%(activity, category, description, tag_str)
-        hamster_id=self.hamster.AddFact(activity, tag_str, 0, 0, category, description)
-        
+
+        try:
+            hamster_id=self.hamster.AddFact(activity, tag_str, 0, 0, category, description)
+        except dbus.exceptions.DBusException:
+            fact = '%s, %s %s' % (activity, description, tag_str)
+            hamster_id=self.hamster.AddFact(fact, 0, 0)
+        #print "hamster_id", hamster_id
+
         ids=self.get_hamster_ids(task)
         ids.append(str(hamster_id))
         self.set_hamster_ids(task, ids)
-        
+
     def get_records(self, task):
         """Get a list of hamster facts for a task"""
         ids = self.get_hamster_ids(task)
@@ -110,7 +114,7 @@
         for i in ids:
             try:
                 d=self.hamster.GetFactById(i)
-                if d.get("id", None) and i not in valid_ids: 
+                if d.get("id", None) and i not in valid_ids:
                     records.append(d)
                     valid_ids.append(i)
                     continue
@@ -120,35 +124,35 @@
         if modified:
             self.set_hamster_ids(task, valid_ids)
         return records
-    
+
     def get_active_id(self):
         f = self.hamster.GetCurrentFact()
         if f: return f['id']
         else: return None
-            
+
     def is_task_active(self, task):
         records = self.get_records(task)
         ids = [record['id'] for record in records]
         return self.get_active_id() in ids
-    
+
     def stop_task(self, task):
         if self.is_task_active(self, task):
             self.hamster.StopTracking()
-    
-    #### Datastore  
+
+    #### Datastore
     def get_hamster_ids(self, task):
         a = task.get_attribute("id-list", namespace=self.PLUGIN_NAMESPACE)
         if not a: return []
         else: return a.split(',')
-        
+
     def set_hamster_ids(self, task, ids):
         task.set_attribute("id-list", ",".join(ids), namespace=self.PLUGIN_NAMESPACE)
 
-    #### Plugin api methods   
+    #### Plugin api methods
     def activate(self, plugin_api):
         self.plugin_api = plugin_api
         self.hamster=dbus.SessionBus().get_object('org.gnome.Hamster', '/org/gnome/Hamster')
-        
+
         # add menu item
         if plugin_api.is_browser():
             self.menu_item = gtk.MenuItem(_("Start task in Hamster"))
@@ -177,10 +181,10 @@
         self.taskbutton.connect('clicked', self.task_cb, plugin_api)
         self.taskbutton.show()
         plugin_api.add_toolbar_item(self.taskbutton)
-        
+
         task = plugin_api.get_ui().get_task()
         records = self.get_records(task)
-        
+
         if len(records):
             # add section to bottom of window
             vbox = gtk.VBox()
@@ -195,42 +199,42 @@
                 s.set_size_request(-1, 150)
             else:
                 s=inner_table
-            
+
             outer_table = gtk.Table(rows=1, columns=2)
             vbox.pack_start(s)
             vbox.pack_start(outer_table)
             vbox.pack_end(gtk.HSeparator())
-            
+
             total = 0
-            
+
             def add(w, a, b, offset, active=False):
                 if active:
                     a = "<span color='red'>%s</span>"%a
                     b = "<span color='red'>%s</span>"%b
-                
+
                 dateLabel=gtk.Label(a)
                 dateLabel.set_use_markup(True)
                 dateLabel.set_alignment(xalign=0.0, yalign=0.5)
                 dateLabel.set_size_request(200, -1)
-                w.attach(dateLabel, left_attach=0, right_attach=1, top_attach=offset, 
+                w.attach(dateLabel, left_attach=0, right_attach=1, top_attach=offset,
                     bottom_attach=offset+1, xoptions=gtk.FILL, xpadding=20, yoptions=0)
-                
+
                 durLabel=gtk.Label(b)
                 durLabel.set_use_markup(True)
                 durLabel.set_alignment(xalign=0.0, yalign=0.5)
-                w.attach(durLabel, left_attach=1, right_attach=2, top_attach=offset, 
+                w.attach(durLabel, left_attach=1, right_attach=2, top_attach=offset,
                 bottom_attach=offset+1, xoptions=gtk.FILL, yoptions=0)
-            
+
             active_id = self.get_active_id()
             for offset,i in enumerate(records):
-                t = calc_duration(i)    
+                t = calc_duration(i)
                 total += t
                 add(inner_table, format_date(i), format_duration(t), offset, i['id'] == active_id)
-                
+
             add(outer_table, "<big><b>Total</b></big>", "<big><b>%s</b></big>"%format_duration(total), 1)
-            
+
             self.vbox = plugin_api.add_widget_to_taskeditor(vbox)
-        
+
     def deactivate(self, plugin_api):
         if plugin_api.is_browser():
             plugin_api.remove_menu_item(self.menu_item)
@@ -238,18 +242,18 @@
         else:
             plugin_api.remove_toolbar_item(self.taskbutton)
             plugin_api.remove_widget_from_taskeditor(self.vbox)
-        
+
     def browser_cb(self, widget, plugin_api):
         task_id = plugin_api.get_ui().get_selected_task()
         self.sendTask(plugin_api.get_requester().get_task(task_id))
-        
+
     def task_cb(self, widget, plugin_api):
         task = plugin_api.get_ui().get_task()
         self.sendTask(task)
-        
-        
+
+
     #### Preference Handling
-        
+
     def is_configurable(self):
         """A configurable plugin should have this method and return True"""
         return True
@@ -257,16 +261,16 @@
     def configure_dialog(self, manager_dialog):
         self.preferences_load()
         self.preferences_dialog.set_transient_for(manager_dialog)
-        
+
         def pref_to_dialog(pref):
             self.builder.get_object(pref+"_"+self.preferences[pref]) \
                 .set_active(True)
-                
+
         pref_to_dialog("activity")
         pref_to_dialog("category")
         pref_to_dialog("description")
         pref_to_dialog("tags")
-        
+
         self.preferences_dialog.show_all()
 
     def on_preferences_close(self, widget = None, data = None):
@@ -275,7 +279,7 @@
                 if self.builder.get_object(pref+"_"+val).get_active():
                     self.preferences[pref] = val
                     break
-                
+
         dialog_to_pref("activity", ["tag", "title"])
         dialog_to_pref("category", ["auto", "tag", "auto_tag"])
         dialog_to_pref("description", ["title", "contents", "none"])
@@ -290,7 +294,7 @@
                                                          "preferences")
         self.preferences = {}
         self.preferences.update(self.DEFAULT_PREFERENCES)
-        
+
         if type(data) == type (dict()):
             self.preferences.update(data)
 
@@ -299,7 +303,7 @@
                                                   "preferences", \
                                                   self.preferences)
 
-    def preference_dialog_init(self): 
+    def preference_dialog_init(self):
         self.builder = gtk.Builder()
         self.builder.add_from_file(os.path.dirname(os.path.abspath(__file__)) +\
                                    "/prefs.ui")
@@ -309,11 +313,11 @@
                 self.on_preferences_close,
         }
         self.builder.connect_signals(SIGNAL_CONNECTIONS_DIC)
-        
-#### Helper Functions  
+
+#### Helper Functions
 def format_date(task):
     return time.strftime("<b>%A, %b %e</b> %l:%M %p", time.gmtime(task['start_time']))
-    
+
 def calc_duration(fact):
     start=fact['start_time']
     end=fact['end_time']
@@ -321,18 +325,18 @@
     return end-start
 
 def format_duration(seconds):
-    # Based on hamster-applet code -  hamster/stuff.py   
+    # Based on hamster-applet code -  hamster/stuff.py
     """formats duration in a human readable format."""
-    
+
     minutes = seconds / 60
-        
+
     if not minutes:
         return "0min"
-    
+
     hours = minutes / 60
     minutes = minutes % 60
     formatted_duration = ""
-    
+
     if minutes % 60 == 0:
         # duration in round hours
         formatted_duration += "%dh" % (hours)
@@ -344,4 +348,4 @@
         formatted_duration += "%dh %dmin" % (hours, minutes % 60)
 
     return formatted_duration
-   
+

=== added file 'data/icons/hicolor/scalable/apps/backend_openerp.png'
Binary files data/icons/hicolor/scalable/apps/backend_openerp.png	1970-01-01 00:00:00 +0000 and data/icons/hicolor/scalable/apps/backend_openerp.png	2011-09-25 09:20:29 +0000 differ