gtg team mailing list archive
-
gtg team
-
Mailing list archive
-
Message #03411
[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