Merge lp:~acsone-openerp/openerp-connector/multi-company-job into lp:~openerp-connector-core-editors/openerp-connector/7.0

Proposed by Laurent Mignon (Acsone)
Status: Merged
Approved by: Guewen Baconnier @ Camptocamp
Approved revision: 624
Merged at revision: 628
Proposed branch: lp:~acsone-openerp/openerp-connector/multi-company-job
Merge into: lp:~openerp-connector-core-editors/openerp-connector/7.0
Diff against target: 276 lines (+155/-2)
5 files modified
connector/queue/job.py (+12/-0)
connector/queue/model.py (+6/-1)
connector/queue/model_view.xml (+3/-0)
connector/security/connector_security.xml (+12/-0)
connector/tests/test_job.py (+122/-1)
To merge this branch: bzr merge lp:~acsone-openerp/openerp-connector/multi-company-job
Reviewer Review Type Date Requested Status
Guewen Baconnier @ Camptocamp code review, launched unit test Approve
Review via email: mp+214895@code.launchpad.net

Description of the change

Here is the proposed changes to support multi companies by the connector.

Summary of changes:
-------------------
* add a new field company_id on the queue_job model (optional)
* modify the method OpenERPJobStorage.enqueue to fill the company_id with the default company of the user or the one provided in the context.
* add a record rule on queue.job to restrict the visibility by company
* modify the method QueueJob.subscribe_users to only subscribe users members of the company_id

Motivations and details:
------------------------
Motivations and details of the changes were discussed in https://lists.launchpad.net/openerp-connector-community/msg00253.html,

To post a comment you must log in.
Revision history for this message
Guewen Baconnier @ Camptocamp (gbaconnier-c2c) wrote :

Hi,

This is a really good work. Thanks

I'm wondering, when 'company_id' is not in the context, the job will be assigned to the default company (by a rule or the user's one), so it will never be empty, could it be a problem? Also, it seems to me that the column could be 'not null'.

review: Needs Information
Revision history for this message
Laurent Mignon (Acsone) (lmi) wrote :

Guewen,
>
> This is a really good work. Thanks

Thank you for the review.

> I'm wondering, when 'company_id' is not in the context, the job will be
> assigned to the default company (by a rule or the user's one), so it will
> never be empty, could it be a problem?

This is a point that I wondered for a long time. I think that in some cases, we need to have the possibility to keep the company_id empty by default. In this kind of use cases the developer can subclass the ConnectorSession class to always put an empty company_id in the context.

> Also, it seems to me that the column could be 'not null'.

I do not think so. In my use of the framework, I've some global jobs not linked to a company_id and I think it's safer to let the possibility to not restrict a job to a company_id. Since the ConnectorSession provides a clean way to locally change the context, we have a clean way to put an empty company_id in the context before delaying a job.

 with self.session.change_context({'company_id': None}):
    my_job.delay(...)

Regards,

lmi

Revision history for this message
Laurent Mignon (Acsone) (lmi) wrote :

I wonder if it could be interesting to have a select=True on the company_id to have an SQL index on this column.

Revision history for this message
Guewen Baconnier @ Camptocamp (gbaconnier-c2c) wrote :

On Thu, May 1, 2014 at 9:57 AM, Laurent Mignon (Acsone)
<email address hidden> wrote:
> I wonder if it could be interesting to have a select=True on the company_id to have an SQL index on this column.
> --
> https://code.launchpad.net/~acsone-openerp/openerp-connector/multi-company-job/+merge/214895
> You are reviewing the proposed merge of lp:~acsone-openerp/openerp-connector/multi-company-job into lp:openerp-connector.

Most of the OpenERP implementation will have 1 to a few companies, so
I don't think an index would be efficient with only a few distinct
values.

Revision history for this message
Guewen Baconnier @ Camptocamp (gbaconnier-c2c) wrote :

> Guewen,
> >
> > This is a really good work. Thanks
>
> Thank you for the review.
>
> > I'm wondering, when 'company_id' is not in the context, the job will be
> > assigned to the default company (by a rule or the user's one), so it will
> > never be empty, could it be a problem?
>
> This is a point that I wondered for a long time. I think that in some cases,
> we need to have the possibility to keep the company_id empty by default. In
> this kind of use cases the developer can subclass the ConnectorSession class
> to always put an empty company_id in the context.
>
> > Also, it seems to me that the column could be 'not null'.
>
> I do not think so. In my use of the framework, I've some global jobs not
> linked to a company_id and I think it's safer to let the possibility to not
> restrict a job to a company_id. Since the ConnectorSession provides a clean
> way to locally change the context, we have a clean way to put an empty
> company_id in the context before delaying a job.
>
> with self.session.change_context({'company_id': None}):
> my_job.delay(...)
>
> Regards,
>
> lmi

Well, I agree. That's ok for me.
Thanks again for your work.

review: Approve (code review, launched unit test)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'connector/queue/job.py'
2--- connector/queue/job.py 2014-02-04 14:39:23 +0000
3+++ connector/queue/job.py 2014-04-09 08:17:14 +0000
4@@ -112,6 +112,14 @@
5 job = Job(func=func, model_name=model_name, args=args, kwargs=kwargs,
6 priority=priority, eta=eta, max_retries=max_retries, description=description)
7 job.user_id = self.session.uid
8+ if 'company_id' in self.session.context:
9+ company_id = self.session.context['company_id']
10+ else:
11+ company_id = self.session.pool.get('res.company')._company_default_get(self.session.cr, job.user_id,
12+ object='queue.job',
13+ field='company_id',
14+ context=self.session.context)
15+ job.company_id = company_id
16 self.store(job)
17 return job.uuid
18
19@@ -167,6 +175,7 @@
20 'max_retries': job.max_retries,
21 'exc_info': job.exc_info,
22 'user_id': job.user_id or self.session.uid,
23+ 'company_id': job.company_id,
24 'result': unicode(job.result) if job.result else False,
25 'date_enqueued': False,
26 'date_started': False,
27@@ -266,6 +275,8 @@
28 job.max_retries = stored.max_retries
29 if stored.worker_id:
30 job.worker_uuid = stored.worker_id.uuid
31+ if stored.company_id:
32+ job.company_id = stored.company_id.id
33 return job
34
35
36@@ -443,6 +454,7 @@
37 self.exc_info = None
38
39 self.user_id = None
40+ self.company_id = None
41 self._eta = None
42 self.eta = eta
43 self.canceled = False
44
45=== modified file 'connector/queue/model.py'
46--- connector/queue/model.py 2013-11-19 10:14:40 +0000
47+++ connector/queue/model.py 2014-04-09 08:17:14 +0000
48@@ -51,6 +51,7 @@
49 ondelete='set null', readonly=True),
50 'uuid': fields.char('UUID', readonly=True, select=True, required=True),
51 'user_id': fields.many2one('res.users', string='User ID', required=True),
52+ 'company_id' : fields.many2one('res.company', 'Company'),
53 'name': fields.char('Description', readonly=True),
54 'func_string': fields.char('Task', readonly=True),
55 'func': fields.text('Pickled Function', readonly=True, required=True),
56@@ -132,8 +133,12 @@
57 if not group_ref:
58 return
59 group_id = group_ref[1]
60+ company_ids = [val['company_id'][0] for val in self.read(cr, uid, ids, ['company_id'], context=context) if val['company_id']]
61+ domain = [('groups_id', '=', group_id)]
62+ if company_ids:
63+ domain.append(('company_ids', 'child_of', company_ids))
64 user_ids = self.pool.get('res.users').search(
65- cr, uid, [('groups_id', '=', group_id)], context=context)
66+ cr, uid, domain, context=context)
67 self.message_subscribe_users(cr, uid, ids,
68 user_ids=user_ids,
69 context=context)
70
71=== modified file 'connector/queue/model_view.xml'
72--- connector/queue/model_view.xml 2013-03-22 15:00:58 +0000
73+++ connector/queue/model_view.xml 2014-04-09 08:17:14 +0000
74@@ -80,6 +80,7 @@
75 <field name="func_string"/>
76 <field name="priority"/>
77 <field name="eta"/>
78+ <field name="company_id" groups="base.group_multi_company"/>
79 <field name="user_id"/>
80 <field name="date_created"/>
81 <field name="date_enqueued"/>
82@@ -123,6 +124,7 @@
83 <field name="eta"/>
84 <field name="date_created"/>
85 <field name="date_done"/>
86+ <field name="company_id" groups="base.group_multi_company"/>
87 </tree>
88 </field>
89 </record>
90@@ -135,6 +137,7 @@
91 <field name="uuid"/>
92 <field name="name"/>
93 <field name="func_string"/>
94+ <field name="company_id" groups="base.group_multi_company" widget="selection"/>
95 <filter name="pending" string="Pending"
96 domain="[('state', '=', 'pending')]"/>
97 <filter name="enqueued" string="Enqueued"
98
99=== modified file 'connector/security/connector_security.xml'
100--- connector/security/connector_security.xml 2013-03-20 19:43:49 +0000
101+++ connector/security/connector_security.xml 2014-04-09 08:17:14 +0000
102@@ -14,4 +14,16 @@
103 </record>
104
105 </data>
106+
107+ <data noupdate="1">
108+
109+ <record id="queue_job_comp_rule" model="ir.rule">
110+ <field name="name" >Queue job multi-company</field>
111+ <field name="model_id" ref="model_queue_job"/>
112+ <field name="global" eval="True"/>
113+ <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
114+ </record>
115+
116+ </data>
117+
118 </openerp>
119
120=== modified file 'connector/tests/test_job.py'
121--- connector/tests/test_job.py 2014-01-13 16:07:35 +0000
122+++ connector/tests/test_job.py 2014-04-09 08:17:14 +0000
123@@ -5,6 +5,7 @@
124 from datetime import datetime, timedelta
125
126 import openerp
127+from openerp import SUPERUSER_ID
128 import openerp.tests.common as common
129 from openerp.addons.connector.queue.job import (
130 Job, OpenERPJobStorage, job,
131@@ -133,6 +134,7 @@
132 eta=eta,
133 description="My description")
134 job.user_id = 1
135+ job.company_id = self.ref("base.main_company")
136 storage = OpenERPJobStorage(self.session)
137 storage.store(job)
138 job_read = storage.load(job.uuid)
139@@ -149,13 +151,14 @@
140 self.assertEqual(job.exc_info, job_read.exc_info)
141 self.assertEqual(job.result, job_read.result)
142 self.assertEqual(job.user_id, job_read.user_id)
143+ self.assertEqual(job.company_id, job_read.company_id)
144 delta = timedelta(seconds=1) # DB does not keep milliseconds
145 self.assertAlmostEqual(job.date_created, job_read.date_created,
146 delta=delta)
147 self.assertAlmostEqual(job.date_started, job_read.date_started,
148 delta=delta)
149 self.assertAlmostEqual(job.date_enqueued, job_read.date_enqueued,
150- delta=delta)
151+ delta=delta)
152 self.assertAlmostEqual(job.date_done, job_read.date_done,
153 delta=delta)
154 self.assertAlmostEqual(job.eta, job_read.eta,
155@@ -179,3 +182,121 @@
156 task_a.delay(self.session, 'res.users', 'o', 'k', c='!')
157 stored = self.queue_job.search(self.cr, self.uid, [])
158 self.assertEqual(len(stored), 1)
159+
160+
161+class test_job_storage_multi_company(common.TransactionCase):
162+ """ Test storage of jobs """
163+
164+ def setUp(self):
165+ super(test_job_storage_multi_company, self).setUp()
166+ self.pool = openerp.modules.registry.RegistryManager.get(common.DB)
167+ self.session = ConnectorSession(self.cr, self.uid, context={})
168+ self.queue_job = self.registry('queue.job')
169+ self.other_partner_id_a = self.registry('res.partner').create(self.cr, self.uid, {
170+ "name": "My Company a",
171+ "is_company": True,
172+ "email": "test@tes.ttest",
173+ })
174+ self.other_company_id_a = self.registry('res.company').create(self.cr, self.uid, {
175+ "name": "My Company a",
176+ "partner_id": self.other_partner_id_a,
177+ "rml_header1": "My Company Tagline",
178+ "currency_id": self.ref("base.EUR")
179+ })
180+ self.other_user_id_a = self.registry('res.users').create(self.cr, self.uid, {
181+ "partner_id": self.other_partner_id_a,
182+ "company_id": self.other_company_id_a,
183+ "company_ids": [(4, self.other_company_id_a)],
184+ "login": "my_login a",
185+ "name": "my user",
186+ "groups_id": [
187+ (4, self.ref("connector.group_connector_manager"))]
188+ })
189+ self.other_partner_id_b = self.registry('res.partner').create(self.cr, self.uid, {
190+ "name": "My Company b",
191+ "is_company": True,
192+ "email": "test@tes.ttest",
193+ })
194+ self.other_company_id_b = self.registry('res.company').create(self.cr, self.uid, {
195+ "name": "My Company b",
196+ "partner_id": self.other_partner_id_b,
197+ "rml_header1": "My Company Tagline",
198+ "currency_id": self.ref("base.EUR")
199+ })
200+ self.other_user_id_b = self.registry('res.users').create(self.cr, self.uid, {
201+ "partner_id": self.other_partner_id_b,
202+ "company_id": self.other_company_id_b,
203+ "company_ids": [(4, self.other_company_id_b)],
204+ "login": "my_login_b",
205+ "name": "my user 1",
206+ "groups_id": [
207+ (4, self.ref("connector.group_connector_manager"))]
208+ })
209+
210+ def _create_job(self):
211+ self.cr.execute('delete from queue_job')
212+ job(task_a)
213+ task_a.delay(self.session, 'res.users')
214+ stored = self.queue_job.search(self.cr, self.uid, [])
215+ self.assertEqual(len(stored), 1)
216+ return self.queue_job.browse(self.cr, self.uid, stored[0])
217+
218+ def test_job_default_company_id(self):
219+ """the default company is the one from the current user_id"""
220+ stored_brw = self._create_job()
221+ self.assertEqual(
222+ stored_brw.company_id.id,
223+ self.ref("base.main_company"),
224+ 'Incorrect default company_id')
225+ with self.session.change_user(self.other_user_id_b):
226+ stored_brw = self._create_job()
227+ self.assertEqual(
228+ stored_brw.company_id.id,
229+ self.other_company_id_b,
230+ 'Incorrect default company_id')
231+
232+ def test_job_no_company_id(self):
233+ """ if we put an empty company_id in the context
234+ jobs are created without company_id"""
235+ with self.session.change_context({'company_id': None}):
236+ stored_brw = self._create_job()
237+ self.assertFalse(
238+ stored_brw.company_id,
239+ ' Company_id should be empty')
240+
241+ def test_job_specific_company_id(self):
242+ """If a company_id specified in the context
243+ it's used by default for the job creation"""
244+ with self.session.change_context({'company_id': self.other_company_id_a}):
245+ stored_brw = self._create_job()
246+ self.assertEqual(
247+ stored_brw.company_id.id,
248+ self.other_company_id_a,
249+ 'Incorrect company_id')
250+
251+ def test_job_subscription(self):
252+ # if the job is created without company_id, all members of
253+ # connector.group_connector_manager must be followers
254+ with self.session.change_context({'company_id': None}):
255+ stored_brw = self._create_job()
256+ self.queue_job. _subscribe_users(self.cr, self.uid, [stored_brw.id])
257+ stored_brw.refresh()
258+ user_ids = self.registry('res.users').search(
259+ self.cr, self.uid, [('groups_id', '=', self.ref('connector.group_connector_manager'))])
260+ self.assertEqual(len(stored_brw.message_follower_ids), len(user_ids))
261+ expected_partners = [u.partner_id for u in self.registry("res.users").browse(self.cr, self.uid, user_ids)]
262+ self.assertSetEqual(set(stored_brw.message_follower_ids), set(expected_partners))
263+ followers_id = [f.id for f in stored_brw.message_follower_ids]
264+ self.assertIn(self.other_partner_id_a, followers_id)
265+ self.assertIn(self.other_partner_id_b, followers_id)
266+ # jobs created for a specific company_id are followed only by company's members
267+ with self.session.change_context({'company_id': self.other_company_id_a}):
268+ stored_brw = self._create_job()
269+ self.queue_job. _subscribe_users(self.cr, self.other_user_id_a, [stored_brw.id])
270+ stored_brw.refresh()
271+ self.assertEqual(len(stored_brw.message_follower_ids), 2) # 2 because admin + self.other_partner_id_a
272+ expected_partners = [u.partner_id for u in self.registry("res.users").browse(self.cr, self.uid, [SUPERUSER_ID, self.other_user_id_a])]
273+ self.assertSetEqual(set(stored_brw.message_follower_ids), set(expected_partners))
274+ followers_id = [f.id for f in stored_brw.message_follower_ids]
275+ self.assertIn(self.other_partner_id_a, followers_id)
276+ self.assertNotIn(self.other_partner_id_b, followers_id)