Merge lp:~toolpart/gtg/fix-858762 into lp:~gtg/gtg/old-trunk
- fix-858762
- Merge into 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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Izidor Matušov | Approve | ||
Review via email: mp+76893@code.launchpad.net |
Commit message
Description of the change
fixes 858762
To post a comment you must log in.
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' |
1597 | 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 |
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.