Merge lp:~camptocamp/openerp-connector/7.0-next-release-related-action into lp:~openerp-connector-core-editors/openerp-connector/7.0-next-release

Proposed by Guewen Baconnier @ Camptocamp
Status: Merged
Approved by: Guewen Baconnier @ Camptocamp
Approved revision: 635
Merged at revision: 630
Proposed branch: lp:~camptocamp/openerp-connector/7.0-next-release-related-action
Merge into: lp:~openerp-connector-core-editors/openerp-connector/7.0-next-release
Diff against target: 462 lines (+346/-4)
9 files modified
connector/connector.py (+19/-0)
connector/queue/job.py (+70/-3)
connector/queue/model.py (+16/-0)
connector/queue/model_view.xml (+4/-0)
connector/related_action.py (+75/-0)
connector/session.py (+1/-1)
connector/tests/__init__.py (+2/-0)
connector/tests/test_related_action.py (+143/-0)
connector/tests/test_session.py (+16/-0)
To merge this branch: bzr merge lp:~camptocamp/openerp-connector/7.0-next-release-related-action
Reviewer Review Type Date Requested Status
OpenERP Connector Core Editors Pending
Review via email: mp+218944@code.launchpad.net

Commit message

Add Related Actions.

A related action can be attached to a job. It is shown to the user as a button on the form view of the jobs.
When the button is used, the related action is called and must return an OpenERP "client action".

Examples of related actions:
* Open the form view of the record that is concerned by the job.
* Open a browser on the URL of the record on an external system.

Description of the change

Implements Related Actions
==========================

A related action can be attached to a job. It is shown to the user as a button on the form view of the jobs.
When the button is used, the related action is called and must return an OpenERP "client action".

Examples of related actions:
* Open the form view of the record that is concerned by the job.
* Open a browser on the URL of the record on an external system.

See this branch for the implementation of related actions on the Magento connector: https://code.launchpad.net/~camptocamp/openerp-connector-magento/7.0-next-release-related-actions/

I would like to include this branch in the upcoming release, please test and give me your feedback!

To post a comment you must log in.
635. By Guewen Baconnier @ Camptocamp

rename unwrap_record to unwrap_binding

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'connector/connector.py'
2--- connector/connector.py 2013-10-30 15:42:11 +0000
3+++ connector/connector.py 2014-05-19 11:41:42 +0000
4@@ -304,3 +304,22 @@
5 :type binding_id: int
6 """
7 raise NotImplementedError
8+
9+ def unwrap_binding(self, binding_id, browse=False):
10+ """ For a binding record, gives the normal record.
11+
12+ Example: when called with a ``magento.product.product`` id,
13+ it will return the corresponding ``product.product`` id.
14+
15+ :param browse: when True, returns a browse_record instance
16+ rather than an ID
17+ """
18+ raise NotImplementedError
19+
20+ def unwrap_model(self):
21+ """ For a binding model, gives the normal model.
22+
23+ Example: when called on a binder for ``magento.product.product``,
24+ it will return ``product.product``.
25+ """
26+ raise NotImplementedError
27
28=== modified file 'connector/queue/job.py'
29--- connector/queue/job.py 2014-05-01 11:33:33 +0000
30+++ connector/queue/job.py 2014-05-19 11:41:42 +0000
31@@ -19,10 +19,11 @@
32 #
33 ##############################################################################
34
35-import sys
36+import inspect
37+import functools
38 import logging
39-import inspect
40 import uuid
41+import sys
42 from datetime import datetime, timedelta, MINYEAR
43 from pickle import loads, dumps, UnpicklingError
44
45@@ -598,6 +599,11 @@
46 if result is not None:
47 self.result = result
48
49+ def related_action(self, session):
50+ if not hasattr(self.func, 'related_action'):
51+ return None
52+ return self.func.related_action(session, self)
53+
54
55 def job(func):
56 """ Decorator for jobs.
57@@ -630,7 +636,7 @@
58 infinite retries. Default is 5.
59 * eta: the job can be executed only after this datetime
60 (or now + timedelta if a timedelta or integer is given)
61-
62+
63 * description : a human description of the job,
64 intended to discriminate job instances
65 (Default is the func.__doc__ or 'Function %s' % func.__name__)
66@@ -655,6 +661,9 @@
67 # => the job will be executed with a low priority and not before a
68 # delay of 5 hours from now
69
70+ See also: :py:func:`related_action` a related action can be attached
71+ to a job
72+
73 """
74 def delay(session, model_name, *args, **kwargs):
75 """Enqueue the function. Return the uuid of the created job."""
76@@ -665,3 +674,61 @@
77 **kwargs)
78 func.delay = delay
79 return func
80+
81+
82+def related_action(action=lambda session, job: None, **kwargs):
83+ """ Attach a *Related Action* to a job.
84+
85+ A *Related Action* will appear as a button on the OpenERP view.
86+ The button will execute the action, usually it will open the
87+ form view of the record related to the job.
88+
89+ The ``action`` must be a callable that responds to arguments::
90+
91+ session, job, **kwargs
92+
93+ Example usage:
94+
95+ .. code-block:: python
96+
97+ def related_action_partner(session, job):
98+ model = job.args[0]
99+ partner_id = job.args[1]
100+ # eventually get the real ID if partner_id is a binding ID
101+ action = {
102+ 'name': _("Partner"),
103+ 'type': 'ir.actions.act_window',
104+ 'res_model': model,
105+ 'view_type': 'form',
106+ 'view_mode': 'form',
107+ 'res_id': partner_id,
108+ }
109+ return action
110+
111+ @job
112+ @related_action(action=related_action_partner)
113+ def export_partner(session, model_name, partner_id):
114+ # ...
115+
116+ The kwargs are transmitted to the action:
117+
118+ .. code-block:: python
119+
120+ def related_action_product(session, job, extra_arg=1):
121+ assert extra_arg == 2
122+ model = job.args[0]
123+ product_id = job.args[1]
124+
125+ @job
126+ @related_action(action=related_action_product, extra_arg=2)
127+ def export_product(session, model_name, product_id):
128+ # ...
129+
130+ """
131+ def decorate(func):
132+ if kwargs:
133+ func.related_action = functools.partial(action, **kwargs)
134+ else:
135+ func.related_action = action
136+ return func
137+ return decorate
138
139=== modified file 'connector/queue/model.py'
140--- connector/queue/model.py 2014-05-01 11:33:33 +0000
141+++ connector/queue/model.py 2014-05-19 11:41:42 +0000
142@@ -80,6 +80,22 @@
143 'active': True,
144 }
145
146+ def open_related_action(self, cr, uid, ids, context=None):
147+ """ Open the related action associated to the job """
148+ if hasattr(ids, '__iter__'):
149+ assert len(ids) == 1, "1 ID expected, got %s" % ids
150+ ids = ids[0]
151+ session = ConnectorSession(cr, uid, context=context)
152+ storage = OpenERPJobStorage(session)
153+ job = self.browse(cr, uid, ids, context=context)
154+ job = storage.load(job.uuid)
155+ action = job.related_action(session)
156+ if action is None:
157+ raise orm.except_orm(
158+ _('Error'),
159+ _('No action available for this job'))
160+ return action
161+
162 def _change_job_state(self, cr, uid, ids, state, result=None, context=None):
163 """ Change the state of the `Job` object itself so it
164 will change the other fields (date, result, ...)
165
166=== modified file 'connector/queue/model_view.xml'
167--- connector/queue/model_view.xml 2014-05-01 11:33:33 +0000
168+++ connector/queue/model_view.xml 2014-05-19 11:41:42 +0000
169@@ -66,6 +66,10 @@
170 string="Set to 'Done'"
171 type="object"
172 groups="connector.group_connector_manager"/>
173+ <button name="open_related_action"
174+ string="Related"
175+ type="object"
176+ />
177 <field name="state"
178 widget="statusbar"
179 statusbar_visible="pending,enqueued,started,done"
180
181=== added file 'connector/related_action.py'
182--- connector/related_action.py 1970-01-01 00:00:00 +0000
183+++ connector/related_action.py 2014-05-19 11:41:42 +0000
184@@ -0,0 +1,75 @@
185+# -*- coding: utf-8 -*-
186+##############################################################################
187+#
188+# Author: Guewen Baconnier
189+# Copyright 2014 Camptocamp SA
190+#
191+# This program is free software: you can redistribute it and/or modify
192+# it under the terms of the GNU Affero General Public License as
193+# published by the Free Software Foundation, either version 3 of the
194+# License, or (at your option) any later version.
195+#
196+# This program is distributed in the hope that it will be useful,
197+# but WITHOUT ANY WARRANTY; without even the implied warranty of
198+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
199+# GNU Affero General Public License for more details.
200+#
201+# You should have received a copy of the GNU Affero General Public License
202+# along with this program. If not, see <http://www.gnu.org/licenses/>.
203+#
204+##############################################################################
205+
206+"""
207+Related Actions
208+
209+Related actions are associated with jobs.
210+When called on a job, they will return an action to the client.
211+
212+"""
213+
214+from openerp.tools.translate import _
215+from .connector import Environment, Binder
216+
217+
218+def unwrap_binding(session, job, id_pos=2, binder_class=Binder):
219+ """ Open a form view with the unwrapped record.
220+
221+ For instance, for a job on a ``magento.product.product``,
222+ it will open a ``product.product`` form view with the unwrapped
223+ record.
224+
225+ :param id_pos: position of the binding ID in the args
226+ :param binder_class: base class to search for the binder
227+ """
228+ binding_model = job.args[0]
229+ # shift one to the left because session is not in job.args
230+ binding_id = job.args[id_pos - 1]
231+ action = {
232+ 'name': _('Related Record'),
233+ 'type': 'ir.actions.act_window',
234+ 'view_type': 'form',
235+ 'view_mode': 'form',
236+ }
237+ # try to get an unwrapped record
238+ binding = session.browse(binding_model, binding_id)
239+ if not binding.exists():
240+ # it has been deleted
241+ return None
242+ env = Environment(binding.backend_id, session, binding_model)
243+ binder = env.get_connector_unit(binder_class)
244+ try:
245+ model = binder.unwrap_model()
246+ record_id = binder.unwrap_binding(binding_id)
247+ except ValueError:
248+ # the binding record will be displayed
249+ action.update({
250+ 'res_model': binding_model,
251+ 'res_id': binding_id,
252+ })
253+ else:
254+ # the unwrapped record will be displayed
255+ action.update({
256+ 'res_model': model,
257+ 'res_id': record_id,
258+ })
259+ return action
260
261=== modified file 'connector/session.py'
262--- connector/session.py 2013-09-13 13:22:49 +0000
263+++ connector/session.py 2014-05-19 11:41:42 +0000
264@@ -138,7 +138,7 @@
265 :param values: values to apply on the context
266 :type values: dict
267 """
268- original_context = self._context
269+ original_context = self.context
270 self._context = original_context.copy()
271 self._context.update(values)
272 yield
273
274=== modified file 'connector/tests/__init__.py'
275--- connector/tests/__init__.py 2013-02-21 12:57:12 +0000
276+++ connector/tests/__init__.py 2014-05-19 11:41:42 +0000
277@@ -28,6 +28,7 @@
278 import test_producer
279 import test_connector
280 import test_mapper
281+import test_related_action
282
283 fast_suite = [
284 ]
285@@ -42,4 +43,5 @@
286 test_producer,
287 test_connector,
288 test_mapper,
289+ test_related_action,
290 ]
291
292=== added file 'connector/tests/test_related_action.py'
293--- connector/tests/test_related_action.py 1970-01-01 00:00:00 +0000
294+++ connector/tests/test_related_action.py 2014-05-19 11:41:42 +0000
295@@ -0,0 +1,143 @@
296+# -*- coding: utf-8 -*-
297+
298+import mock
299+import unittest2
300+
301+import openerp
302+import openerp.tests.common as common
303+from ..backend import Backend, BACKENDS
304+from ..connector import Binder
305+from ..queue.job import (Job,
306+ OpenERPJobStorage,
307+ related_action)
308+from ..session import ConnectorSession
309+from ..related_action import unwrap_binding
310+
311+
312+def task_no_related(session, model_name):
313+ pass
314+
315+
316+def task_related_none(session, model_name):
317+ pass
318+
319+
320+def task_related_return(session, model_name):
321+ pass
322+
323+
324+def task_related_return_kwargs(session, model_name):
325+ pass
326+
327+
328+def open_url(session, job, url=None):
329+ subject = job.args[0]
330+ return {
331+ 'type': 'ir.actions.act_url',
332+ 'target': 'new',
333+ 'url': url.format(subject=subject),
334+ }
335+
336+
337+@related_action(action=open_url, url='https://en.wikipedia.org/wiki/{subject}')
338+def task_wikipedia(session, subject):
339+ pass
340+
341+
342+@related_action(action=unwrap_binding)
343+def test_unwrap_binding(session, model_name, binding_id):
344+ pass
345+
346+
347+class test_related_action(unittest2.TestCase):
348+ """ Test Related Actions """
349+
350+ def setUp(self):
351+ super(test_related_action, self).setUp()
352+ self.session = mock.MagicMock()
353+
354+ def test_no_related_action(self):
355+ """ Job without related action """
356+ job = Job(func=task_no_related)
357+ self.assertIsNone(job.related_action(self.session))
358+
359+ def test_return_none(self):
360+ """ Job with related action returning None """
361+ # default action returns None
362+ job = Job(func=related_action()(task_related_none))
363+ self.assertIsNone(job.related_action(self.session))
364+
365+ def test_return(self):
366+ """ Job with related action check if action returns correctly """
367+ def action(session, job):
368+ return session, job
369+ job = Job(func=related_action(action=action)(task_related_return))
370+ act_session, act_job = job.related_action(self.session)
371+ self.assertEqual(act_session, self.session)
372+ self.assertEqual(act_job, job)
373+
374+ def test_kwargs(self):
375+ """ Job with related action check if action propagates kwargs """
376+ def action(session, job, a=1, b=2):
377+ return a, b
378+ job_func = related_action(action=action, b=4)(task_related_return_kwargs)
379+ job = Job(func=job_func)
380+ self.assertEqual(job.related_action(self.session), (1, 4))
381+
382+ def test_unwrap_binding(self):
383+ """ Call the unwrap binding related action """
384+ class TestBinder(Binder):
385+ _model_name = 'binding.res.users'
386+
387+ def unwrap_binding(self, binding_id, browse=False):
388+ return 42
389+
390+ def unwrap_model(self):
391+ return 'res.users'
392+
393+ job = Job(func=test_unwrap_binding, args=('res.users', 555))
394+ session = mock.Mock(name='session')
395+ backend_record = mock.Mock(name='backend_record')
396+ backend = mock.Mock(name='backend')
397+ browse_record = mock.Mock(name='browse_record')
398+ backend.get_class.return_value = TestBinder
399+ backend_record.get_backend.return_value = backend
400+ browse_record.exists.return_value = True
401+ browse_record.backend_id = backend_record
402+ session.browse.return_value = browse_record
403+ action = unwrap_binding(session, job)
404+ expected = {
405+ 'name': mock.ANY,
406+ 'type': 'ir.actions.act_window',
407+ 'view_type': 'form',
408+ 'view_mode': 'form',
409+ 'res_id': 42,
410+ 'res_model': 'res.users',
411+ }
412+ self.assertEquals(action, expected)
413+
414+
415+
416+class test_related_action_storage(common.TransactionCase):
417+ """ Test related actions on stored jobs """
418+
419+ def setUp(self):
420+ super(test_related_action_storage, self).setUp()
421+ self.pool = openerp.modules.registry.RegistryManager.get(common.DB)
422+ self.session = ConnectorSession(self.cr, self.uid)
423+ self.queue_job = self.registry('queue.job')
424+
425+ def test_store_related_action(self):
426+ """ Call the related action on the model """
427+ job = Job(func=task_wikipedia, args=('Discworld',))
428+ storage = OpenERPJobStorage(self.session)
429+ storage.store(job)
430+ stored_ids = self.queue_job.search(self.cr, self.uid,
431+ [('uuid', '=', job.uuid)])
432+ self.assertEqual(len(stored_ids), 1)
433+ stored = self.queue_job.browse(self.cr, self.uid, stored_ids[0])
434+ expected = {'type': 'ir.actions.act_url',
435+ 'target': 'new',
436+ 'url': 'https://en.wikipedia.org/wiki/Discworld',
437+ }
438+ self.assertEquals(stored.open_related_action(), expected)
439
440=== modified file 'connector/tests/test_session.py'
441--- connector/tests/test_session.py 2013-05-02 14:01:27 +0000
442+++ connector/tests/test_session.py 2014-05-19 11:41:42 +0000
443@@ -85,3 +85,19 @@
444 res_users = self.registry('res.users')
445
446 self.assertEqual(self.session.pool.get('res.users'), res_users)
447+
448+ def test_change_context(self):
449+ """
450+ Change the context and check if it is reverted correctly at the end
451+ """
452+ test_key = 'test_key'
453+ self.assertNotIn(test_key, self.session.context)
454+ with self.session.change_context({test_key: 'value'}):
455+ self.assertIn(test_key, self.session.context)
456+ self.assertNotIn(test_key, self.session.context)
457+
458+ #change the context on a session not initialized with a context
459+ session = ConnectorSession(self.cr, self.uid)
460+ with session.change_context({test_key: 'value'}):
461+ self.assertIn(test_key, session.context)
462+ self.assertNotIn(test_key, session.context)

Subscribers

People subscribed via source and target branches

to all changes: