GTG

Merge lp:~toolpart/gtg/fix-858762 into lp:~gtg/gtg/old-trunk

Proposed by ViktorNagy
Status: Merged
Merged at revision: 1024
Proposed branch: lp:~toolpart/gtg/fix-858762
Merge into: lp:~gtg/gtg/old-trunk
Diff against target: 1597 lines (+1043/-96)
7 files modified
GTG/backends/backend_openerp.py (+400/-0)
GTG/backends/genericbackend.py (+12/-12)
GTG/backends/openerplib/__init__.py (+32/-0)
GTG/backends/openerplib/dates.py (+97/-0)
GTG/backends/openerplib/main.py (+413/-0)
GTG/core/task.py (+28/-27)
GTG/plugins/hamster/hamster.py (+61/-57)
To merge this branch: bzr merge lp:~toolpart/gtg/fix-858762
Reviewer Review Type Date Requested Status
Izidor Matušov Approve
Review via email: mp+76893@code.launchpad.net

Description of the change

fixes 858762

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

I ignored OpenERP backend (because it is not finished), style changes and hamster change. In the end, it was just one line of code to be changed.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'GTG/backends/backend_openerp.py'
2--- GTG/backends/backend_openerp.py 1970-01-01 00:00:00 +0000
3+++ GTG/backends/backend_openerp.py 2011-09-25 09:20:29 +0000
4@@ -0,0 +1,400 @@
5+# -*- encoding: utf-8 -*-
6+# -----------------------------------------------------------------------------
7+# Getting Things Gnome! - a personal organizer for the GNOME desktop
8+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
9+#
10+# This program is free software: you can redistribute it and/or modify it under
11+# the terms of the GNU General Public License as published by the Free Software
12+# Foundation, either version 3 of the License, or (at your option) any later
13+# version.
14+#
15+# This program is distributed in the hope that it will be useful, but WITHOUT
16+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
18+# details.
19+#
20+# You should have received a copy of the GNU General Public License along with
21+# this program. If not, see <http://www.gnu.org/licenses/>.
22+# -----------------------------------------------------------------------------
23+
24+'''
25+OpenERP backend
26+'''
27+
28+import os
29+import uuid
30+import datetime
31+import openerplib
32+
33+from xdg.BaseDirectory import xdg_cache_home
34+
35+from GTG import _
36+from GTG.backends.genericbackend import GenericBackend
37+from GTG.backends.periodicimportbackend import PeriodicImportBackend
38+from GTG.backends.backendsignals import BackendSignals
39+from GTG.backends.syncengine import SyncEngine, SyncMeme
40+from GTG.core.task import Task
41+from GTG.tools.dates import RealDate, no_date
42+from GTG.tools.interruptible import interruptible
43+from GTG.tools.logger import Log
44+
45+def as_datetime(datestr):
46+ if not datestr:
47+ return no_date
48+
49+ return RealDate(datetime.datetime.strptime(datestr[:10], "%Y-%m-%d").date())
50+
51+class Backend(PeriodicImportBackend):
52+
53+ _general_description = { \
54+ GenericBackend.BACKEND_NAME: "backend_openerp", \
55+ GenericBackend.BACKEND_HUMAN_NAME: _("OpenERP"), \
56+ GenericBackend.BACKEND_AUTHORS: ["Viktor Nagy"], \
57+ GenericBackend.BACKEND_TYPE: GenericBackend.TYPE_READWRITE, \
58+ GenericBackend.BACKEND_DESCRIPTION: \
59+ _("This backend synchronizes your tasks with an OpenERP server"),
60+ }
61+
62+ _static_parameters = {
63+ "username": {
64+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING,
65+ GenericBackend.PARAM_DEFAULT_VALUE: "insert your username here"
66+ },
67+ "password": {
68+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_PASSWORD,
69+ GenericBackend.PARAM_DEFAULT_VALUE: "",
70+ },
71+ "server_host": {
72+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING,
73+ GenericBackend.PARAM_DEFAULT_VALUE: "erp.toolpart.hu",
74+ },
75+ "protocol": {
76+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING,
77+ GenericBackend.PARAM_DEFAULT_VALUE: "xmlrpcs"
78+ },
79+ "server_port": {
80+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT,
81+ GenericBackend.PARAM_DEFAULT_VALUE: 8071,
82+ },
83+ "database": {
84+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING,
85+ GenericBackend.PARAM_DEFAULT_VALUE: "ToolPartTeam",
86+ },
87+ "period": {
88+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT,
89+ GenericBackend.PARAM_DEFAULT_VALUE: 10,
90+ },
91+ "is-first-run": {
92+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL,
93+ GenericBackend.PARAM_DEFAULT_VALUE: True,
94+ },
95+ GenericBackend.KEY_ATTACHED_TAGS: {
96+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_LIST_OF_STRINGS,
97+ GenericBackend.PARAM_DEFAULT_VALUE: ['@OpenERP'],
98+ }
99+ }
100+
101+###############################################################################
102+### Backend standard methods ##################################################
103+###############################################################################
104+
105+ def __init__(self, parameters):
106+ '''
107+ See GenericBackend for an explanation of this function.
108+ Loads the saved state of the sync, if any
109+ '''
110+ super(Backend, self).__init__(parameters)
111+ #loading the saved state of the synchronization, if any
112+ self.sync_engine_path = os.path.join('backends/openerp/', \
113+ "sync_engine-" + self.get_id())
114+ self.sync_engine = self._load_pickled_file(self.sync_engine_path, \
115+ SyncEngine())
116+
117+ def do_periodic_import(self):
118+ if not self._check_server():
119+ return
120+
121+ self.cancellation_point()
122+ self._sync_tasks()
123+
124+ def save_state(self):
125+ """
126+ See GenericBackend for an explanation of this function.
127+ """
128+ self._store_pickled_file(self.sync_engine_path, self.sync_engine)
129+
130+ @interruptible
131+ def remove_task(self, tid):
132+ """
133+ See GenericBackend for an explanation of this function.
134+ """
135+ self.cancellation_point()
136+ try:
137+ oerp_id = self.sync_engine.get_remote_id(tid)
138+ Log.debug("removing task %s from OpenERP" % oerp_id)
139+ self._unlink_task(oerp_id)
140+ except KeyError:
141+ pass
142+ try:
143+ self.sync_engine.break_relationship(local_id = tid)
144+ except:
145+ pass
146+
147+ @interruptible
148+ def set_task(self, task):
149+ """
150+ TODO: write set_task method
151+ """
152+ self.cancellation_point()
153+ tid = task.get_id()
154+ is_syncable = self._gtg_task_is_syncable_per_attached_tags(task)
155+ action, oerp_id = self.sync_engine.analyze_local_id( \
156+ tid, \
157+ self.datastore.has_task, \
158+ self._erp_has_task, \
159+ is_syncable)
160+ Log.debug("GTG->OERP set task (%s, %s)" % (action, is_syncable))
161+
162+ if action == None:
163+ return
164+
165+ if action == SyncEngine.ADD:
166+ Log.debug('Adding task')
167+ return # raise NotImplementedError
168+
169+ elif action == SyncEngine.UPDATE:
170+ # we deal only with updating openerp state
171+ by_status = {
172+ Task.STA_ACTIVE: lambda oerp_id: \
173+ self._set_open(oerp_id),
174+ Task.STA_DISMISSED: lambda oerp_id: \
175+ self._set_state(oerp_id,'cancel'),
176+ Task.STA_DONE: lambda oerp_id: \
177+ self._set_state(oerp_id,'close'),
178+ }
179+ try:
180+ by_status[task.get_status()](oerp_id)
181+ except:
182+ # the given state transition might not be available
183+ raise
184+
185+ elif action == SyncEngine.REMOVE:
186+ self.datastore.request_task_deletion(tid)
187+ try:
188+ self.sync_engine.break_relationship(local_id = tid)
189+ except KeyError:
190+ pass
191+
192+ elif action == SyncEngine.LOST_SYNCABILITY:
193+ pass
194+
195+###################################
196+### OpenERP related
197+###################################
198+
199+ def _check_server(self):
200+ """connect to server"""
201+ Log.debug( 'checking server connection' )
202+ try:
203+ self.server = openerplib.get_connection(
204+ hostname=self._parameters["server_host"],
205+ port=self._parameters["server_port"],
206+ protocol=self._parameters["protocol"],
207+ database=self._parameters["database"],
208+ login=self._parameters["username"],
209+ password=self._parameters["password"])
210+ except:
211+ self.server = None
212+ BackendSignals().backend_failed(self.get_id(), \
213+ BackendSignals.ERRNO_NETWORK)
214+ return False
215+
216+ try:
217+ self.server.check_login()
218+ except:
219+ self.server = None
220+ BackendSignals().backend_failed(self.get_id(), \
221+ BackendSignals.ERRNO_AUTHENTICATION)
222+ return False
223+
224+ return True
225+
226+ def _get_model(self):
227+ if not self.server:
228+ self._check_server()
229+ return self.server.get_model('project.task')
230+
231+ def _sync_tasks(self):
232+ '''
233+ Download tasks from the server and register them in GTG
234+
235+ Existing tasks should not be registered.
236+ '''
237+ task_model = self._get_model()
238+ task_ids = task_model.search([("user_id","=", self.server.user_id)])
239+ tasks = task_model.read(task_ids,
240+ ['name', 'description', 'context_id',
241+ 'date_deadline', 'notes', 'priority',
242+ 'timebox_id', 'project_id', 'state',
243+ 'date_start', 'date_end'])
244+ self.cancellation_point()
245+ # merge last modified date with generic task data
246+ logs = task_model.perm_read(task_ids, {}, False)
247+ self.cancellation_point()
248+ def get_task_id(id):
249+ return '%s$%d' % (self._parameters['server_host'], id)
250+
251+ def adjust_task(task):
252+ id = task['id']
253+ task['rid'] = get_task_id(id)
254+ return (id, task)
255+
256+ tasks = dict(map(adjust_task, tasks))
257+ map(lambda l: tasks[l['id']].update(l), logs)
258+ Log.debug(str(tasks))
259+
260+ for task in tasks.values():
261+ self._process_openerp_task(task)
262+
263+ #removing the old ones
264+ last_task_list = self.sync_engine.get_all_remote()
265+ new_task_keys = map(get_task_id, tasks.keys())
266+ for task_link in set(last_task_list).difference(set(new_task_keys)):
267+ self.cancellation_point()
268+ #we make sure that the other backends are not modifying the task
269+ # set
270+ with self.datastore.get_backend_mutex():
271+ tid = self.sync_engine.get_local_id(task_link)
272+ self.datastore.request_task_deletion(tid)
273+ try:
274+ self.sync_engine.break_relationship(remote_id = task_link)
275+ except KeyError:
276+ pass
277+
278+ def _process_openerp_task(self, task):
279+ '''
280+ From the task data find out if this task already exists or should be
281+ updated.
282+ '''
283+ Log.debug("Processing task %s (%d)" % (task['name'], task['id']))
284+ action, tid = self.sync_engine.analyze_remote_id(task['rid'],
285+ self.datastore.has_task, lambda b: True)
286+
287+ if action == None:
288+ return
289+
290+ self.cancellation_point()
291+ with self.datastore.get_backend_mutex():
292+ if action == SyncEngine.ADD:
293+ tid = str(uuid.uuid4())
294+ gtg = self.datastore.task_factory(tid)
295+ self._populate_task(gtg, task)
296+ self.sync_engine.record_relationship(local_id = tid,\
297+ remote_id = task['rid'], \
298+ meme = SyncMeme(\
299+ gtg.get_modified(), \
300+ as_datetime(task['write_date']), \
301+ self.get_id()))
302+ self.datastore.push_task(gtg)
303+
304+ elif action == SyncEngine.UPDATE:
305+ gtg = self.datastore.get_task(tid)
306+ self._populate_task(gtg, task)
307+ meme = self.sync_engine.get_meme_from_remote_id( \
308+ task['rid'])
309+ meme.set_local_last_modified(gtg.get_modified())
310+ meme.set_remote_last_modified(as_datetime(task['write_date']))
311+ self.save_state()
312+
313+ def _populate_task(self, gtg, oerp):
314+ '''
315+ Fills a GTG task with the data from a launchpad bug.
316+
317+ @param gtg: a Task in GTG
318+ @param oerp: a Task in OpenERP
319+ '''
320+ # draft, open, pending, cancelled, done
321+ if oerp["state"] in ['draft', 'open', 'pending']:
322+ gtg.set_status(Task.STA_ACTIVE)
323+ elif oerp['state'] == 'done':
324+ gtg.set_status(Task.STA_DONE)
325+ else:
326+ gtg.set_status(Task.STA_DISMISSED)
327+ if gtg.get_title() != oerp['name']:
328+ gtg.set_title(oerp['name'])
329+
330+ text = ''
331+ if oerp['description']:
332+ text += oerp['description']
333+ if oerp['notes']:
334+ text += '\n\n' + oerp['notes']
335+ if gtg.get_excerpt() != text:
336+ gtg.set_text(text)
337+
338+ if oerp['date_deadline'] and \
339+ gtg.get_due_date() != as_datetime(oerp['date_deadline']):
340+ gtg.set_due_date(as_datetime(oerp['date_deadline']))
341+ if oerp['date_start'] and \
342+ gtg.get_start_date() != as_datetime(oerp['date_start']):
343+ gtg.set_start_date(as_datetime(oerp['date_start']))
344+ if oerp['date_end'] and \
345+ gtg.get_closed_date() != as_datetime(oerp['date_end']):
346+ gtg.set_closed_date(as_datetime(oerp['date_end']))
347+
348+ tags = [oerp['project_id'][1].replace(' ', '_')]
349+ if self._parameters[GenericBackend.KEY_ATTACHED_TAGS]:
350+ tags.extend(self._parameters[GenericBackend.KEY_ATTACHED_TAGS])
351+ # priority
352+ priorities = {
353+ '4': _('VeryLow'),
354+ '3': _('Low'),
355+ '2': _('Medium'),
356+ '1': _('Urgent'),
357+ '0': _('VeryUrgent'),
358+ }
359+ tags.append(priorities[oerp['priority']])
360+ if oerp.has_key('context_id'):
361+ tags.append(oerp['context_id'] \
362+ and oerp['context_id'][1].replace(' ', '_') or "NoContext")
363+ if oerp.has_key('timebox_id'):
364+ tags.append(oerp['timebox_id'] \
365+ and oerp['timebox_id'][1].replace(' ', '_') or 'NoTimebox')
366+ new_tags = set(['@' + str(tag) for tag in filter(None, tags)])
367+
368+ current_tags = set(gtg.get_tags_name())
369+ #remove the lost tags
370+ for tag in current_tags.difference(new_tags):
371+ gtg.remove_tag(tag)
372+ #add the new ones
373+ for tag in new_tags.difference(current_tags):
374+ gtg.add_tag(tag)
375+ gtg.add_remote_id(self.get_id(), oerp['rid'])
376+
377+ def _unlink_task(self, task_id):
378+ """Delete a task on the server"""
379+ task_model = self._get_model()
380+ task_id = int(task_id.split('$')[1])
381+ task_model.unlink(task_id)
382+
383+ def _erp_has_task(self, task_id):
384+ Log.debug('Checking task %d' % int(task_id.split('$')[1]))
385+ task_model = self._get_model()
386+ if task_model.read(int(task_id.split('$')[1]), ['id']):
387+ return True
388+ return False
389+
390+ def _set_state(self, oerp_id, state):
391+ Log.debug('Setting task %s to %s' % (oerp_id, state))
392+ task_model = self._get_model()
393+ oerp_id = int(oerp_id.split('$')[1])
394+ getattr(task_model, 'do_%s' % state)([oerp_id])
395+
396+ def _set_open(self, oerp_id):
397+ ''' this might mean reopen or open '''
398+ task_model = self._get_model()
399+ tid = int(oerp_id.split('$')[1])
400+ if task_model.read(tid, ['state'])['state'] == 'draft':
401+ self._set_state(oerp_id, 'open')
402+ else:
403+ self._set_state(oerp_id, 'reopen')
404+
405
406=== modified file 'GTG/backends/genericbackend.py'
407--- GTG/backends/genericbackend.py 2010-08-09 14:09:14 +0000
408+++ GTG/backends/genericbackend.py 2011-09-25 09:20:29 +0000
409@@ -87,8 +87,8 @@
410 '''
411 Called each time it is enabled (including on backend creation).
412 Please note that a class instance for each disabled backend *is*
413- created, but it's not initialized.
414- Optional.
415+ created, but it's not initialized.
416+ Optional.
417 NOTE: make sure to call super().initialize()
418 '''
419 #NOTE: I'm disabling this since support for runtime checking of the
420@@ -124,7 +124,7 @@
421 def remove_task(self, tid):
422 ''' This function is called from GTG core whenever a task must be
423 removed from the backend. Note that the task could be not present here.
424-
425+
426 @param tid: the id of the task to delete
427 '''
428 pass
429@@ -136,7 +136,7 @@
430 This function is needed only in the default backend (XML localfile,
431 currently).
432 The xml parameter is an object containing GTG default tasks.
433-
434+
435 @param xml: an xml object containing the default tasks.
436 '''
437 pass
438@@ -159,7 +159,7 @@
439 def quit(self, disable = False):
440 '''
441 Called when GTG quits or the user wants to disable the backend.
442-
443+
444 @param disable: If disable is True, the backend won't
445 be automatically loaded when GTG starts
446 '''
447@@ -209,7 +209,7 @@
448 # For an example, see the GTG/backends/backend_localfile.py file
449 # Each dictionary contains the keys:
450 PARAM_DEFAULT_VALUE = "default_value" # its default value
451- PARAM_TYPE = "type"
452+ PARAM_TYPE = "type"
453 #PARAM_TYPE is one of the following (changing this changes the way
454 # the user can configure the parameter)
455 TYPE_PASSWORD = "password" #the real password is stored in the GNOME
456@@ -264,7 +264,7 @@
457 PARAM_TYPE: TYPE_LIST_OF_STRINGS, \
458 PARAM_DEFAULT_VALUE: [ALLTASKS_TAG], \
459 }}
460-
461+
462 #Handy dictionary used in type conversion (from string to type)
463 _type_converter = {TYPE_STRING: str,
464 TYPE_INT: int,
465@@ -286,7 +286,7 @@
466 temp_dic[key] = value
467 for key, value in cls._static_parameters.iteritems():
468 temp_dic[key] = value
469- return temp_dic
470+ return temp_dic
471
472 def __init__(self, parameters):
473 """
474@@ -313,7 +313,7 @@
475 if Log.is_debugging_mode():
476 self.timer_timestep = 5
477 else:
478- self.timer_timestep = 1
479+ self.timer_timestep = 1
480 self.to_set_timer = None
481 self.please_quit = False
482 self.cancellation_point = lambda: _cancellation_point(\
483@@ -560,7 +560,7 @@
484 try:
485 os.makedirs(os.path.dirname(path))
486 except OSError, exception:
487- if exception.errno != errno.EEXIST:
488+ if exception.errno != errno.EEXIST:
489 raise
490 #saving
491 with open(path, 'wb') as file:
492@@ -619,7 +619,7 @@
493 def launch_setting_thread(self, bypass_quit_request = False):
494 '''
495 This function is launched as a separate thread. Its job is to perform
496- the changes that have been issued from GTG core.
497+ the changes that have been issued from GTG core.
498 In particular, for each task in the self.to_set queue, a task
499 has to be modified or to be created (if the tid is new), and for
500 each task in the self.to_remove queue, a task has to be deleted
501@@ -651,7 +651,7 @@
502 ''' Save the task in the backend. In particular, it just enqueues the
503 task in the self.to_set queue. A thread will shortly run to apply the
504 requested changes.
505-
506+
507 @param task: the task that should be saved
508 '''
509 tid = task.get_id()
510
511=== added directory 'GTG/backends/openerplib'
512=== added file 'GTG/backends/openerplib/__init__.py'
513--- GTG/backends/openerplib/__init__.py 1970-01-01 00:00:00 +0000
514+++ GTG/backends/openerplib/__init__.py 2011-09-25 09:20:29 +0000
515@@ -0,0 +1,32 @@
516+# -*- coding: utf-8 -*-
517+##############################################################################
518+#
519+# Copyright (C) Stephane Wirtel
520+# Copyright (C) 2011 Nicolas Vanhoren
521+# Copyright (C) 2011 OpenERP s.a. (<http://openerp.com>).
522+# All rights reserved.
523+#
524+# Redistribution and use in source and binary forms, with or without
525+# modification, are permitted provided that the following conditions are met:
526+#
527+# 1. Redistributions of source code must retain the above copyright notice, this
528+# list of conditions and the following disclaimer.
529+# 2. Redistributions in binary form must reproduce the above copyright notice,
530+# this list of conditions and the following disclaimer in the documentation
531+# and/or other materials provided with the distribution.
532+#
533+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
534+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
535+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
536+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
537+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
538+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
539+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
540+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
541+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
542+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
543+#
544+##############################################################################
545+
546+from main import *
547+
548
549=== added file 'GTG/backends/openerplib/dates.py'
550--- GTG/backends/openerplib/dates.py 1970-01-01 00:00:00 +0000
551+++ GTG/backends/openerplib/dates.py 2011-09-25 09:20:29 +0000
552@@ -0,0 +1,97 @@
553+# -*- coding: utf-8 -*-
554+##############################################################################
555+#
556+# Copyright (C) Stephane Wirtel
557+# Copyright (C) 2011 Nicolas Vanhoren
558+# Copyright (C) 2011 OpenERP s.a. (<http://openerp.com>).
559+# All rights reserved.
560+#
561+# Redistribution and use in source and binary forms, with or without
562+# modification, are permitted provided that the following conditions are met:
563+#
564+# 1. Redistributions of source code must retain the above copyright notice, this
565+# list of conditions and the following disclaimer.
566+# 2. Redistributions in binary form must reproduce the above copyright notice,
567+# this list of conditions and the following disclaimer in the documentation
568+# and/or other materials provided with the distribution.
569+#
570+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
571+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
572+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
573+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
574+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
575+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
576+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
577+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
578+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
579+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
580+#
581+##############################################################################
582+
583+import datetime
584+
585+DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"
586+DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S"
587+DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % (
588+ DEFAULT_SERVER_DATE_FORMAT,
589+ DEFAULT_SERVER_TIME_FORMAT)
590+
591+def str_to_datetime(str):
592+ """
593+ Converts a string to a datetime object using OpenERP's
594+ datetime string format (exemple: '2011-12-01 15:12:35').
595+
596+ No timezone information is added, the datetime is a naive instance, but
597+ according to OpenERP 6.1 specification the timezone is always UTC.
598+ """
599+ if not str:
600+ return str
601+ return datetime.datetime.strptime(str, DEFAULT_SERVER_DATETIME_FORMAT)
602+
603+def str_to_date(str):
604+ """
605+ Converts a string to a date object using OpenERP's
606+ date string format (exemple: '2011-12-01').
607+ """
608+ if not str:
609+ return str
610+ return datetime.datetime.strptime(str, DEFAULT_SERVER_DATE_FORMAT).date()
611+
612+def str_to_time(str):
613+ """
614+ Converts a string to a time object using OpenERP's
615+ time string format (exemple: '15:12:35').
616+ """
617+ if not str:
618+ return str
619+ return datetime.datetime.strptime(str, DEFAULT_SERVER_TIME_FORMAT).time()
620+
621+def datetime_to_str(obj):
622+ """
623+ Converts a datetime object to a string using OpenERP's
624+ datetime string format (exemple: '2011-12-01 15:12:35').
625+
626+ The datetime instance should not have an attached timezone and be in UTC.
627+ """
628+ if not obj:
629+ return False
630+ return obj.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
631+
632+def date_to_str(obj):
633+ """
634+ Converts a date object to a string using OpenERP's
635+ date string format (exemple: '2011-12-01').
636+ """
637+ if not obj:
638+ return False
639+ return obj.strftime(DEFAULT_SERVER_DATE_FORMAT)
640+
641+def time_to_str(obj):
642+ """
643+ Converts a time object to a string using OpenERP's
644+ time string format (exemple: '15:12:35').
645+ """
646+ if not obj:
647+ return False
648+ return obj.strftime(DEFAULT_SERVER_TIME_FORMAT)
649+
650
651=== added file 'GTG/backends/openerplib/main.py'
652--- GTG/backends/openerplib/main.py 1970-01-01 00:00:00 +0000
653+++ GTG/backends/openerplib/main.py 2011-09-25 09:20:29 +0000
654@@ -0,0 +1,413 @@
655+# -*- coding: utf-8 -*-
656+##############################################################################
657+#
658+# Copyright (C) Stephane Wirtel
659+# Copyright (C) 2011 Nicolas Vanhoren
660+# Copyright (C) 2011 OpenERP s.a. (<http://openerp.com>).
661+# All rights reserved.
662+#
663+# Redistribution and use in source and binary forms, with or without
664+# modification, are permitted provided that the following conditions are met:
665+#
666+# 1. Redistributions of source code must retain the above copyright notice, this
667+# list of conditions and the following disclaimer.
668+# 2. Redistributions in binary form must reproduce the above copyright notice,
669+# this list of conditions and the following disclaimer in the documentation
670+# and/or other materials provided with the distribution.
671+#
672+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
673+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
674+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
675+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
676+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
677+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
678+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
679+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
680+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
681+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
682+#
683+##############################################################################
684+
685+"""
686+OpenERP Client Library
687+
688+Home page: http://pypi.python.org/pypi/openerp-client-lib
689+Code repository: https://code.launchpad.net/~niv-openerp/openerp-client-lib/trunk
690+"""
691+
692+import xmlrpclib
693+import logging
694+import socket
695+
696+try:
697+ import cPickle as pickle
698+except ImportError:
699+ import pickle
700+
701+try:
702+ import cStringIO as StringIO
703+except ImportError:
704+ import StringIO
705+
706+_logger = logging.getLogger(__name__)
707+
708+def _getChildLogger(logger, subname):
709+ return logging.getLogger(logger.name + "." + subname)
710+
711+class Connector(object):
712+ """
713+ The base abstract class representing a connection to an OpenERP Server.
714+ """
715+
716+ __logger = _getChildLogger(_logger, 'connector')
717+
718+ def __init__(self, hostname, port):
719+ """
720+ Initilize by specifying an hostname and a port.
721+ :param hostname: Host name of the server.
722+ :param port: Port for the connection to the server.
723+ """
724+ self.hostname = hostname
725+ self.port = port
726+
727+class XmlRPCConnector(Connector):
728+ """
729+ A type of connector that uses the XMLRPC protocol.
730+ """
731+ PROTOCOL = 'xmlrpc'
732+
733+ __logger = _getChildLogger(_logger, 'connector.xmlrpc')
734+
735+ def __init__(self, hostname, port=8069):
736+ """
737+ Initialize by specifying the hostname and the port.
738+ :param hostname: The hostname of the computer holding the instance of OpenERP.
739+ :param port: The port used by the OpenERP instance for XMLRPC (default to 8069).
740+ """
741+ Connector.__init__(self, hostname, port)
742+ self.url = 'http://%s:%d/xmlrpc' % (self.hostname, self.port)
743+
744+ def send(self, service_name, method, *args):
745+ url = '%s/%s' % (self.url, service_name)
746+ service = xmlrpclib.ServerProxy(url)
747+ return getattr(service, method)(*args)
748+
749+class XmlRPCSConnector(XmlRPCConnector):
750+ """
751+ A type of connector that uses the secured XMLRPC protocol.
752+ """
753+ PROTOCOL = 'xmlrpcs'
754+
755+ __logger = _getChildLogger(_logger, 'connector.xmlrpcs')
756+
757+ def __init__(self, hostname, port=8071):
758+ super(XmlRPCSConnector, self).__init__(hostname, port)
759+ self.url = 'https://%s:%d/xmlrpc' % (self.hostname, self.port)
760+
761+class NetRPC_Exception(Exception):
762+ """
763+ Exception for NetRPC errors.
764+ """
765+ def __init__(self, faultCode, faultString):
766+ self.faultCode = faultCode
767+ self.faultString = faultString
768+ self.args = (faultCode, faultString)
769+
770+class NetRPC(object):
771+ """
772+ Low level class for NetRPC protocol.
773+ """
774+ def __init__(self, sock=None):
775+ if sock is None:
776+ self.sock = socket.socket(
777+ socket.AF_INET, socket.SOCK_STREAM)
778+ else:
779+ self.sock = sock
780+ self.sock.settimeout(120)
781+ def connect(self, host, port=False):
782+ if not port:
783+ buf = host.split('//')[1]
784+ host, port = buf.split(':')
785+ self.sock.connect((host, int(port)))
786+
787+ def disconnect(self):
788+ self.sock.shutdown(socket.SHUT_RDWR)
789+ self.sock.close()
790+
791+ def mysend(self, msg, exception=False, traceback=None):
792+ msg = pickle.dumps([msg,traceback])
793+ size = len(msg)
794+ self.sock.send('%8d' % size)
795+ self.sock.send(exception and "1" or "0")
796+ totalsent = 0
797+ while totalsent < size:
798+ sent = self.sock.send(msg[totalsent:])
799+ if sent == 0:
800+ raise RuntimeError, "socket connection broken"
801+ totalsent = totalsent + sent
802+
803+ def myreceive(self):
804+ buf=''
805+ while len(buf) < 8:
806+ chunk = self.sock.recv(8 - len(buf))
807+ if chunk == '':
808+ raise RuntimeError, "socket connection broken"
809+ buf += chunk
810+ size = int(buf)
811+ buf = self.sock.recv(1)
812+ if buf != "0":
813+ exception = buf
814+ else:
815+ exception = False
816+ msg = ''
817+ while len(msg) < size:
818+ chunk = self.sock.recv(size-len(msg))
819+ if chunk == '':
820+ raise RuntimeError, "socket connection broken"
821+ msg = msg + chunk
822+ msgio = StringIO.StringIO(msg)
823+ unpickler = pickle.Unpickler(msgio)
824+ unpickler.find_global = None
825+ res = unpickler.load()
826+
827+ if isinstance(res[0],Exception):
828+ if exception:
829+ raise NetRPC_Exception(str(res[0]), str(res[1]))
830+ raise res[0]
831+ else:
832+ return res[0]
833+
834+class NetRPCConnector(Connector):
835+ """
836+ A type of connector that uses the NetRPC protocol.
837+ """
838+
839+ PROTOCOL = 'netrpc'
840+
841+ __logger = _getChildLogger(_logger, 'connector.netrpc')
842+
843+ def __init__(self, hostname, port=8070):
844+ """
845+ Initialize by specifying the hostname and the port.
846+ :param hostname: The hostname of the computer holding the instance of OpenERP.
847+ :param port: The port used by the OpenERP instance for NetRPC (default to 8070).
848+ """
849+ Connector.__init__(self, hostname, port)
850+
851+ def send(self, service_name, method, *args):
852+ socket = NetRPC()
853+ socket.connect(self.hostname, self.port)
854+ socket.mysend((service_name, method, )+args)
855+ result = socket.myreceive()
856+ socket.disconnect()
857+ return result
858+
859+class Service(object):
860+ """
861+ A class to execute RPC calls on a specific service of the remote server.
862+ """
863+ def __init__(self, connector, service_name):
864+ """
865+ :param connector: A valid Connector instance.
866+ :param service_name: The name of the service on the remote server.
867+ """
868+ self.connector = connector
869+ self.service_name = service_name
870+ self.__logger = _getChildLogger(_getChildLogger(_logger, 'service'),service_name)
871+
872+ def __getattr__(self, method):
873+ """
874+ :param method: The name of the method to execute on the service.
875+ """
876+ self.__logger.debug('method: %r', method)
877+ def proxy(*args):
878+ """
879+ :param args: A list of values for the method
880+ """
881+ self.__logger.debug('args: %r', args)
882+ result = self.connector.send(self.service_name, method, *args)
883+ self.__logger.debug('result: %r', result)
884+ return result
885+ return proxy
886+
887+class Connection(object):
888+ """
889+ A class to represent a connection with authentication to an OpenERP Server.
890+ It also provides utility methods to interact with the server more easily.
891+ """
892+ __logger = _getChildLogger(_logger, 'connection')
893+
894+ def __init__(self, connector,
895+ database=None,
896+ login=None,
897+ password=None,
898+ user_id=None):
899+ """
900+ Initialize with login information. The login information is facultative to allow specifying
901+ it after the initialization of this object.
902+
903+ :param connector: A valid Connector instance to send messages to the remote server.
904+ :param database: The name of the database to work on.
905+ :param login: The login of the user.
906+ :param password: The password of the user.
907+ :param user_id: The user id is a number identifying the user. This is only useful if you
908+ already know it, in most cases you don't need to specify it.
909+ """
910+ self.connector = connector
911+
912+ self.set_login_info(database, login, password, user_id)
913+
914+ def set_login_info(self, database, login, password, user_id=None):
915+ """
916+ Set login information after the initialisation of this object.
917+
918+ :param connector: A valid Connector instance to send messages to the remote server.
919+ :param database: The name of the database to work on.
920+ :param login: The login of the user.
921+ :param password: The password of the user.
922+ :param user_id: The user id is a number identifying the user. This is only useful if you
923+ already know it, in most cases you don't need to specify it.
924+ """
925+ self.database, self.login, self.password = database, login, password
926+
927+ self.user_id = user_id
928+
929+ def check_login(self, force=True):
930+ """
931+ Checks that the login information is valid. Throws an AuthenticationError if the
932+ authentication fails.
933+
934+ :param force: Force to re-check even if this Connection was already validated previously.
935+ Default to True.
936+ """
937+ if self.user_id and not force:
938+ return
939+
940+ if not self.database or not self.login or self.password is None:
941+ raise AuthenticationError("Creditentials not provided")
942+
943+ self.user_id = self.get_service("common").login(self.database, self.login, self.password)
944+ if not self.user_id:
945+ raise AuthenticationError("Authentication failure")
946+ self.__logger.debug("Authenticated with user id %s", self.user_id)
947+
948+ def get_model(self, model_name):
949+ """
950+ Returns a Model instance to allow easy remote manipulation of an OpenERP model.
951+
952+ :param model_name: The name of the model.
953+ """
954+ return Model(self, model_name)
955+
956+ def get_service(self, service_name):
957+ """
958+ Returns a Service instance to allow easy manipulation of one of the services offered by the remote server.
959+ Please note this Connection instance does not need to have valid authentication information since authentication
960+ is only necessary for the "object" service that handles models.
961+
962+ :param service_name: The name of the service.
963+ """
964+ return Service(self.connector, service_name)
965+
966+class AuthenticationError(Exception):
967+ """
968+ An error thrown when an authentication to an OpenERP server failed.
969+ """
970+ pass
971+
972+class Model(object):
973+ """
974+ Useful class to dialog with one of the models provided by an OpenERP server.
975+ An instance of this class depends on a Connection instance with valid authentication information.
976+ """
977+
978+ def __init__(self, connection, model_name):
979+ """
980+ :param connection: A valid Connection instance with correct authentication information.
981+ :param model_name: The name of the model.
982+ """
983+ self.connection = connection
984+ self.model_name = model_name
985+ self.__logger = _getChildLogger(_getChildLogger(_logger, 'object'), model_name)
986+
987+ def __getattr__(self, method):
988+ """
989+ Provides proxy methods that will forward calls to the model on the remote OpenERP server.
990+
991+ :param method: The method for the linked model (search, read, write, unlink, create, ...)
992+ """
993+ def proxy(*args):
994+ """
995+ :param args: A list of values for the method
996+ """
997+ self.connection.check_login(False)
998+ self.__logger.debug(args)
999+ result = self.connection.get_service('object').execute(
1000+ self.connection.database,
1001+ self.connection.user_id,
1002+ self.connection.password,
1003+ self.model_name,
1004+ method,
1005+ *args)
1006+ if method == "read":
1007+ if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
1008+ index = {}
1009+ for r in result:
1010+ index[r['id']] = r
1011+ result = [index[x] for x in args[0] if x in index]
1012+ self.__logger.debug('result: %r', result)
1013+ return result
1014+ return proxy
1015+
1016+ def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None, context=None):
1017+ """
1018+ A shortcut method to combine a search() and a read().
1019+
1020+ :param domain: The domain for the search.
1021+ :param fields: The fields to extract (can be None or [] to extract all fields).
1022+ :param offset: The offset for the rows to read.
1023+ :param limit: The maximum number of rows to read.
1024+ :param order: The order to class the rows.
1025+ :param context: The context.
1026+ :return: A list of dictionaries containing all the specified fields.
1027+ """
1028+ record_ids = self.search(domain or [], offset, limit or False, order or False, context or {})
1029+ records = self.read(record_ids, fields or [], context or {})
1030+ return records
1031+
1032+def get_connector(hostname, protocol="xmlrpc", port="auto"):
1033+ """
1034+ A shortcut method to easily create a connector to a remote server using XMLRPC or NetRPC.
1035+
1036+ :param hostname: The hostname to the remote server.
1037+ :param protocol: The name of the protocol, must be "xmlrpc" or "netrpc".
1038+ :param port: The number of the port. Defaults to auto.
1039+ """
1040+ if port == 'auto':
1041+ port = 8069 if protocol=="xmlrpc" else 8070
1042+ if protocol == "xmlrpc":
1043+ return XmlRPCConnector(hostname, port)
1044+ elif protocol == "xmlrpcs":
1045+ return XmlRPCSConnector(hostname, port)
1046+ elif protocol == "netrpc":
1047+ return NetRPCConnector(hostname, port)
1048+ else:
1049+ raise ValueError("You must choose xmlrpc or netrpc")
1050+
1051+def get_connection(hostname, protocol="xmlrpc", port='auto', database=None,
1052+ login=None, password=None, user_id=None):
1053+ """
1054+ A shortcut method to easily create a connection to a remote OpenERP server.
1055+
1056+ :param hostname: The hostname to the remote server.
1057+ :param protocol: The name of the protocol, must be "xmlrpc" or "netrpc".
1058+ :param port: The number of the port. Defaults to auto.
1059+ :param connector: A valid Connector instance to send messages to the remote server.
1060+ :param database: The name of the database to work on.
1061+ :param login: The login of the user.
1062+ :param password: The password of the user.
1063+ :param user_id: The user id is a number identifying the user. This is only useful if you
1064+ already know it, in most cases you don't need to specify it.
1065+ """
1066+ return Connection(get_connector(hostname, protocol, port), database, login, password, user_id)
1067+
1068
1069=== modified file 'GTG/core/task.py'
1070--- GTG/core/task.py 2011-08-17 09:57:00 +0000
1071+++ GTG/core/task.py 2011-09-25 09:20:29 +0000
1072@@ -143,7 +143,7 @@
1073 return True
1074 else:
1075 return False
1076-
1077+
1078 #TODO : should we merge this function with set_title ?
1079 def set_complex_title(self,text,tags=[]):
1080 if tags:
1081@@ -151,9 +151,9 @@
1082 due_date = no_date
1083 defer_date = no_date
1084 if text:
1085-
1086+
1087 # Get tags in the title
1088- #NOTE: the ?: tells regexp that the first one is
1089+ #NOTE: the ?: tells regexp that the first one is
1090 # a non-capturing group, so it must not be returned
1091 # to findall. http://www.amk.ca/python/howto/regex/regex.html
1092 # ~~~~Invernizzi
1093@@ -262,7 +262,7 @@
1094 pardate = self.req.get_task(par).get_due_date()
1095 if pardate and zedate > pardate:
1096 zedate = pardate
1097-
1098+
1099 return zedate
1100
1101 def set_start_date(self, fulldate):
1102@@ -277,7 +277,7 @@
1103 assert(isinstance(fulldate, Date))
1104 self.closed_date = fulldate
1105 self.sync()
1106-
1107+
1108 def get_closed_date(self):
1109 return self.closed_date
1110
1111@@ -286,7 +286,7 @@
1112 if due_date == no_date:
1113 return None
1114 return due_date.days_left()
1115-
1116+
1117 def get_days_late(self):
1118 due_date = self.get_due_date()
1119 if due_date == no_date:
1120@@ -375,7 +375,7 @@
1121 #we use the inherited childrens
1122 self.add_child(subt.get_id())
1123 return subt
1124-
1125+
1126 def add_child(self, tid):
1127 """Add a subtask to this task
1128
1129@@ -393,7 +393,7 @@
1130 child.add_tag(t.get_name())
1131 self.sync()
1132 return True
1133-
1134+
1135 def remove_child(self,tid):
1136 """Removed a subtask from the task.
1137
1138@@ -407,8 +407,8 @@
1139 return True
1140 else:
1141 return False
1142-
1143-
1144+
1145+
1146 #FIXME: remove this function and use liblarch instead.
1147 def get_subtasks(self):
1148 tree = self.get_tree()
1149@@ -462,7 +462,7 @@
1150 val = unicode(str(att_value), "UTF-8")
1151 self.attributes[(namespace, att_name)] = val
1152 self.sync()
1153-
1154+
1155 def get_attribute(self, att_name, namespace=""):
1156 """Get the attribute C{att_name}.
1157
1158@@ -479,13 +479,13 @@
1159 return True
1160 else:
1161 return False
1162-
1163-# the following is not currently needed
1164+
1165+# the following is not currently needed
1166 # def modified(self):
1167 # TreeNode.modified(self)
1168 # for t in self.get_tags():
1169 # gobject.idle_add(t.modified)
1170-
1171+
1172
1173 def _modified_update(self):
1174 '''
1175@@ -508,7 +508,7 @@
1176 tag = self.req.new_tag(tname)
1177 l.append(tag)
1178 return l
1179-
1180+
1181 def rename_tag(self, old, new):
1182 eold = saxutils.escape(saxutils.unescape(old))
1183 enew = saxutils.escape(saxutils.unescape(new))
1184@@ -535,25 +535,25 @@
1185 for child in self.get_subtasks():
1186 if child.can_be_deleted:
1187 child.add_tag(t)
1188-
1189+
1190 tag = self.req.get_tag(t)
1191 if not tag:
1192 tag = self.req.new_tag(t)
1193 tag.modified()
1194 return True
1195-
1196+
1197 def add_tag(self, tagname):
1198 "Add a tag to the task and insert '@tag' into the task's content"
1199 # print "add tag %s to task %s" %(tagname,self.get_title())
1200 if self.tag_added(tagname):
1201 c = self.content
1202-
1203+
1204 #strip <content>...</content> tags
1205 if c.startswith('<content>'):
1206 c = c[len('<content>'):]
1207 if c.endswith('</content>'):
1208 c = c[:-len('</content>')]
1209-
1210+
1211 if not c:
1212 # don't need a separator if it's the only text
1213 sep = ''
1214@@ -563,7 +563,7 @@
1215 else:
1216 # other text at the beginning, so put the tag on its own line
1217 sep = '\n\n'
1218-
1219+
1220 self.content = "<content><tag>%s</tag>%s%s</content>" % (
1221 tagname, sep, c)
1222 #we modify the task internal state, thus we have to call for a sync
1223@@ -581,8 +581,9 @@
1224 self.content = self._strip_tag(self.content, tagname)
1225 if modified:
1226 tag = self.req.get_tag(tagname)
1227- tag.modified()
1228-
1229+ if tag:
1230+ tag.modified()
1231+
1232 def set_only_these_tags(self, tags_list):
1233 '''
1234 Given a list of strings representing tags, it makes sure that
1235@@ -602,12 +603,12 @@
1236 .replace('<tag>%s</tag>, '%(tagname), newtag) #trail comma
1237 .replace('<tag>%s</tag>'%(tagname), newtag)
1238 #in case XML is missing (bug #504899)
1239- .replace('%s\n\n'%(tagname), newtag)
1240- .replace('%s, '%(tagname), newtag)
1241+ .replace('%s\n\n'%(tagname), newtag)
1242+ .replace('%s, '%(tagname), newtag)
1243 #don't forget a space a the end
1244 .replace('%s '%(tagname), newtag)
1245 )
1246-
1247+
1248
1249 #tag_list is a list of tags names
1250 #return true if at least one of the list is in the task
1251@@ -623,7 +624,7 @@
1252 if not toreturn:
1253 toreturn = children_tag(tagc_name)
1254 return toreturn
1255-
1256+
1257 #We want to see if the task has no tags
1258 toreturn = False
1259 if notag_only:
1260@@ -635,7 +636,7 @@
1261 elif tag_list:
1262 for tagname in tag_list:
1263 if not toreturn:
1264- toreturn = children_tag(tagname)
1265+ toreturn = children_tag(tagname)
1266 else:
1267 #Well, if we don't filter on tags or notag, it's true, of course
1268 toreturn = True
1269
1270=== modified file 'GTG/plugins/hamster/hamster.py'
1271--- GTG/plugins/hamster/hamster.py 2011-01-16 21:07:21 +0000
1272+++ GTG/plugins/hamster/hamster.py 2011-09-25 09:20:29 +0000
1273@@ -33,39 +33,39 @@
1274 "description": "title",
1275 "tags": "existing",
1276 }
1277-
1278+
1279 def __init__(self):
1280 #task editor widget
1281 self.vbox = None
1282 self.button=gtk.ToolButton()
1283-
1284+
1285 #### Interaction with Hamster
1286 def sendTask(self, task):
1287 """Send a gtg task to hamster-applet"""
1288 if task is None: return
1289 gtg_title = task.get_title()
1290 gtg_tags = tags=[t.lstrip('@').lower() for t in task.get_tags_name()]
1291-
1292+
1293 activity = "Other"
1294 if self.preferences['activity'] == 'tag':
1295 hamster_activities=set([unicode(x[0]).lower() for x in self.hamster.GetActivities()])
1296 activity_candidates=hamster_activities.intersection(set(gtg_tags))
1297 if len(activity_candidates)>=1:
1298- activity=list(activity_candidates)[0]
1299+ activity=list(activity_candidates)[0]
1300 elif self.preferences['activity'] == 'title':
1301 activity = gtg_title
1302 # hamster can't handle ',' or '@' in activity name
1303 activity = activity.replace(',', '')
1304 activity = re.sub('\ +@.*', '', activity)
1305-
1306+
1307 category = ""
1308 if self.preferences['category'] == 'auto_tag':
1309 hamster_activities=dict([(unicode(x[0]), unicode(x[1])) for x in self.hamster.GetActivities()])
1310 if (gtg_title in hamster_activities
1311 or gtg_title.replace(",", "") in hamster_activities):
1312 category = "%s" % hamster_activities[gtg_title]
1313-
1314- if (self.preferences['category'] == 'tag' or
1315+
1316+ if (self.preferences['category'] == 'tag' or
1317 (self.preferences['category'] == 'auto_tag' and not category)):
1318 # See if any of the tags match existing categories
1319 categories = dict([(unicode(x[1]).lower(), unicode(x[1])) for x in self.hamster.GetCategories()])
1320@@ -81,26 +81,30 @@
1321 description = gtg_title
1322 elif self.preferences['description'] == 'contents':
1323 description = task.get_excerpt(strip_tags=True, strip_subtasks=True)
1324-
1325+
1326 tag_candidates = []
1327 try:
1328 if self.preferences['tags'] == 'existing':
1329 hamster_tags = set([unicode(x) for x in self.hamster.GetTags()])
1330 tag_candidates = list(hamster_tags.intersection(set(gtg_tags)))
1331 elif self.preferences['tags'] == 'all':
1332- tag_candidates = gtg_tags
1333+ tag_candidates = gtg_tags
1334 except dbus.exceptions.DBusException:
1335 # old hamster version, doesn't support tags
1336 pass
1337 tag_str = "".join([" #" + x for x in tag_candidates])
1338-
1339- #print '%s%s,%s%s'%(activity, category, description, tag_str)
1340- hamster_id=self.hamster.AddFact(activity, tag_str, 0, 0, category, description)
1341-
1342+
1343+ try:
1344+ hamster_id=self.hamster.AddFact(activity, tag_str, 0, 0, category, description)
1345+ except dbus.exceptions.DBusException:
1346+ fact = '%s, %s %s' % (activity, description, tag_str)
1347+ hamster_id=self.hamster.AddFact(fact, 0, 0)
1348+ #print "hamster_id", hamster_id
1349+
1350 ids=self.get_hamster_ids(task)
1351 ids.append(str(hamster_id))
1352 self.set_hamster_ids(task, ids)
1353-
1354+
1355 def get_records(self, task):
1356 """Get a list of hamster facts for a task"""
1357 ids = self.get_hamster_ids(task)
1358@@ -110,7 +114,7 @@
1359 for i in ids:
1360 try:
1361 d=self.hamster.GetFactById(i)
1362- if d.get("id", None) and i not in valid_ids:
1363+ if d.get("id", None) and i not in valid_ids:
1364 records.append(d)
1365 valid_ids.append(i)
1366 continue
1367@@ -120,35 +124,35 @@
1368 if modified:
1369 self.set_hamster_ids(task, valid_ids)
1370 return records
1371-
1372+
1373 def get_active_id(self):
1374 f = self.hamster.GetCurrentFact()
1375 if f: return f['id']
1376 else: return None
1377-
1378+
1379 def is_task_active(self, task):
1380 records = self.get_records(task)
1381 ids = [record['id'] for record in records]
1382 return self.get_active_id() in ids
1383-
1384+
1385 def stop_task(self, task):
1386 if self.is_task_active(self, task):
1387 self.hamster.StopTracking()
1388-
1389- #### Datastore
1390+
1391+ #### Datastore
1392 def get_hamster_ids(self, task):
1393 a = task.get_attribute("id-list", namespace=self.PLUGIN_NAMESPACE)
1394 if not a: return []
1395 else: return a.split(',')
1396-
1397+
1398 def set_hamster_ids(self, task, ids):
1399 task.set_attribute("id-list", ",".join(ids), namespace=self.PLUGIN_NAMESPACE)
1400
1401- #### Plugin api methods
1402+ #### Plugin api methods
1403 def activate(self, plugin_api):
1404 self.plugin_api = plugin_api
1405 self.hamster=dbus.SessionBus().get_object('org.gnome.Hamster', '/org/gnome/Hamster')
1406-
1407+
1408 # add menu item
1409 if plugin_api.is_browser():
1410 self.menu_item = gtk.MenuItem(_("Start task in Hamster"))
1411@@ -177,10 +181,10 @@
1412 self.taskbutton.connect('clicked', self.task_cb, plugin_api)
1413 self.taskbutton.show()
1414 plugin_api.add_toolbar_item(self.taskbutton)
1415-
1416+
1417 task = plugin_api.get_ui().get_task()
1418 records = self.get_records(task)
1419-
1420+
1421 if len(records):
1422 # add section to bottom of window
1423 vbox = gtk.VBox()
1424@@ -195,42 +199,42 @@
1425 s.set_size_request(-1, 150)
1426 else:
1427 s=inner_table
1428-
1429+
1430 outer_table = gtk.Table(rows=1, columns=2)
1431 vbox.pack_start(s)
1432 vbox.pack_start(outer_table)
1433 vbox.pack_end(gtk.HSeparator())
1434-
1435+
1436 total = 0
1437-
1438+
1439 def add(w, a, b, offset, active=False):
1440 if active:
1441 a = "<span color='red'>%s</span>"%a
1442 b = "<span color='red'>%s</span>"%b
1443-
1444+
1445 dateLabel=gtk.Label(a)
1446 dateLabel.set_use_markup(True)
1447 dateLabel.set_alignment(xalign=0.0, yalign=0.5)
1448 dateLabel.set_size_request(200, -1)
1449- w.attach(dateLabel, left_attach=0, right_attach=1, top_attach=offset,
1450+ w.attach(dateLabel, left_attach=0, right_attach=1, top_attach=offset,
1451 bottom_attach=offset+1, xoptions=gtk.FILL, xpadding=20, yoptions=0)
1452-
1453+
1454 durLabel=gtk.Label(b)
1455 durLabel.set_use_markup(True)
1456 durLabel.set_alignment(xalign=0.0, yalign=0.5)
1457- w.attach(durLabel, left_attach=1, right_attach=2, top_attach=offset,
1458+ w.attach(durLabel, left_attach=1, right_attach=2, top_attach=offset,
1459 bottom_attach=offset+1, xoptions=gtk.FILL, yoptions=0)
1460-
1461+
1462 active_id = self.get_active_id()
1463 for offset,i in enumerate(records):
1464- t = calc_duration(i)
1465+ t = calc_duration(i)
1466 total += t
1467 add(inner_table, format_date(i), format_duration(t), offset, i['id'] == active_id)
1468-
1469+
1470 add(outer_table, "<big><b>Total</b></big>", "<big><b>%s</b></big>"%format_duration(total), 1)
1471-
1472+
1473 self.vbox = plugin_api.add_widget_to_taskeditor(vbox)
1474-
1475+
1476 def deactivate(self, plugin_api):
1477 if plugin_api.is_browser():
1478 plugin_api.remove_menu_item(self.menu_item)
1479@@ -238,18 +242,18 @@
1480 else:
1481 plugin_api.remove_toolbar_item(self.taskbutton)
1482 plugin_api.remove_widget_from_taskeditor(self.vbox)
1483-
1484+
1485 def browser_cb(self, widget, plugin_api):
1486 task_id = plugin_api.get_ui().get_selected_task()
1487 self.sendTask(plugin_api.get_requester().get_task(task_id))
1488-
1489+
1490 def task_cb(self, widget, plugin_api):
1491 task = plugin_api.get_ui().get_task()
1492 self.sendTask(task)
1493-
1494-
1495+
1496+
1497 #### Preference Handling
1498-
1499+
1500 def is_configurable(self):
1501 """A configurable plugin should have this method and return True"""
1502 return True
1503@@ -257,16 +261,16 @@
1504 def configure_dialog(self, manager_dialog):
1505 self.preferences_load()
1506 self.preferences_dialog.set_transient_for(manager_dialog)
1507-
1508+
1509 def pref_to_dialog(pref):
1510 self.builder.get_object(pref+"_"+self.preferences[pref]) \
1511 .set_active(True)
1512-
1513+
1514 pref_to_dialog("activity")
1515 pref_to_dialog("category")
1516 pref_to_dialog("description")
1517 pref_to_dialog("tags")
1518-
1519+
1520 self.preferences_dialog.show_all()
1521
1522 def on_preferences_close(self, widget = None, data = None):
1523@@ -275,7 +279,7 @@
1524 if self.builder.get_object(pref+"_"+val).get_active():
1525 self.preferences[pref] = val
1526 break
1527-
1528+
1529 dialog_to_pref("activity", ["tag", "title"])
1530 dialog_to_pref("category", ["auto", "tag", "auto_tag"])
1531 dialog_to_pref("description", ["title", "contents", "none"])
1532@@ -290,7 +294,7 @@
1533 "preferences")
1534 self.preferences = {}
1535 self.preferences.update(self.DEFAULT_PREFERENCES)
1536-
1537+
1538 if type(data) == type (dict()):
1539 self.preferences.update(data)
1540
1541@@ -299,7 +303,7 @@
1542 "preferences", \
1543 self.preferences)
1544
1545- def preference_dialog_init(self):
1546+ def preference_dialog_init(self):
1547 self.builder = gtk.Builder()
1548 self.builder.add_from_file(os.path.dirname(os.path.abspath(__file__)) +\
1549 "/prefs.ui")
1550@@ -309,11 +313,11 @@
1551 self.on_preferences_close,
1552 }
1553 self.builder.connect_signals(SIGNAL_CONNECTIONS_DIC)
1554-
1555-#### Helper Functions
1556+
1557+#### Helper Functions
1558 def format_date(task):
1559 return time.strftime("<b>%A, %b %e</b> %l:%M %p", time.gmtime(task['start_time']))
1560-
1561+
1562 def calc_duration(fact):
1563 start=fact['start_time']
1564 end=fact['end_time']
1565@@ -321,18 +325,18 @@
1566 return end-start
1567
1568 def format_duration(seconds):
1569- # Based on hamster-applet code - hamster/stuff.py
1570+ # Based on hamster-applet code - hamster/stuff.py
1571 """formats duration in a human readable format."""
1572-
1573+
1574 minutes = seconds / 60
1575-
1576+
1577 if not minutes:
1578 return "0min"
1579-
1580+
1581 hours = minutes / 60
1582 minutes = minutes % 60
1583 formatted_duration = ""
1584-
1585+
1586 if minutes % 60 == 0:
1587 # duration in round hours
1588 formatted_duration += "%dh" % (hours)
1589@@ -344,4 +348,4 @@
1590 formatted_duration += "%dh %dmin" % (hours, minutes % 60)
1591
1592 return formatted_duration
1593-
1594+
1595
1596=== added file 'data/icons/hicolor/scalable/apps/backend_openerp.png'
1597Binary 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

Subscribers

People subscribed via source and target branches

to status/vote changes: