Merge lp:~openerp-dev/openobject-addons/trunk-bug-832635-nch into lp:openobject-addons

Proposed by Naresh(OpenERP)
Status: Merged
Merged at revision: 5754
Proposed branch: lp:~openerp-dev/openobject-addons/trunk-bug-832635-nch
Merge into: lp:openobject-addons
Diff against target: 591 lines (+267/-240)
1 file modified
audittrail/audittrail.py (+267/-240)
To merge this branch: bzr merge lp:~openerp-dev/openobject-addons/trunk-bug-832635-nch
Reviewer Review Type Date Requested Status
qdp (OpenERP) Needs Fixing
Antony Lesuisse (OpenERP) Pending
Vo Minh Thu Pending
Olivier Dony (Odoo) Pending
Review via email: mp+77861@code.launchpad.net

This proposal supersedes a proposal from 2011-09-07.

Description of the change

Hello,

This merge proposal contains fix for the linked bug as well as refactoring and improvement of the audit trail module.

please provide your feedback,

Thanks,

To post a comment you must log in.
Revision history for this message
Antony Lesuisse (OpenERP) (al-openerp) wrote : Posted in a previous version of this proposal

Could you update your merge proposal to the lastest trunk i committed a refactoring to
- override *_cr method to reuse the cursor.
- defer any database access after checking that auditrail is actually installed.

review: Needs Fixing
Revision history for this message
Antony Lesuisse (OpenERP) (al-openerp) wrote : Posted in a previous version of this proposal

Please use trunk version of

check_rules
execute_cr
exec_workflow_cr

instead of

check_rule_subscription
audit_log_call

because the former use _cr, are shorter and much faster (no useless db hits).
Remove all cr.commit().

review: Needs Fixing
Revision history for this message
Naresh(OpenERP) (nch-openerp) wrote : Posted in a previous version of this proposal

hello,

made compatible to new signatures...

Thanks,

Revision history for this message
qdp (OpenERP) (qdp) wrote :

Hi Naresh,

i've tested but i found a problem (Not sure i didn't break it myself during the merge, though):
*given you have an audittrail rule for res.partner and res.partner.address
*given you have a partner Agrolait with 4 existing addresses
*if you create a new address for this partner, it will create you the following logs
   - one write on contatcs fields of res.partner (correct)
   - four empty writes on res.partner.address where no changes are logged (not correct)
   - one write on res.partner.address with the data you just created (incorrect, the method logged should be 'create' instead of 'write', otherwise it's ok)

i've commited in your branch my merge with trunk, not to loose my work. You should starts from it. Also, let me know if i need to assign someone else on this task, as i'm not aware of your current load (but as you made this merge prop you'll probably be faster than anyone for it ;-) ).

Btw, i'm setting this merge prop as 'work in progress' so that it's not anymore on the pending merge we need to review. Reset its state back to 'needs review' when you're work is over.

Thanks,
Quentin

review: Needs Fixing
Revision history for this message
Naresh(OpenERP) (nch-openerp) wrote :

Hi Qdp,

Thanks for the review ! from your review I just found 1 bug(fixed it in the last commit) and for the rest I made gave explanation. Correct me If I missed here something.

> Hi Naresh,
>
> i've tested but i found a problem (Not sure i didn't break it myself during
> the merge, though):
> *given you have an audittrail rule for res.partner and res.partner.address
> *given you have a partner Agrolait with 4 existing addresses
> *if you create a new address for this partner, it will create you the
> following logs
> - one write on contatcs fields of res.partner (correct)

> - four empty writes on res.partner.address where no changes are logged (not
> correct)

yes, actual bug Fixed it in the last commit.

> - one write on res.partner.address with the data you just created
> (incorrect, the method logged should be 'create' instead of 'write', otherwise
> it's ok)

 according to our architecture the relational table like o2m (create,unlink,write,read) operations are handled by the server itself.i.e we pass an arg at the start of the tuple with values as '0':create, '1':write,'2':unlink etc which is used by server to perform the specific operation on the relational model.now as to log we need to hack the execution of the method before the server actually executes. So if you create a parent record along with a child the client issues a 'create' call for the parent record along with the values which will be logged as create but if you add a child record in an already saved parent record the client just issues a 'write' call for the parent record(which is correct) and the server handles the creation of the child records internally. Same is the case for *unlink*.

>
> i've commited in your branch my merge with trunk, not to loose my work. You
> should starts from it. Also, let me know if i need to assign someone else on
> this task, as i'm not aware of your current load (but as you made this merge
> prop you'll probably be faster than anyone for it ;-) ).

Nop, I managed it...:)
>
> Btw, i'm setting this merge prop as 'work in progress' so that it's not
> anymore on the pending merge we need to review. Reset its state back to 'needs
> review' when you're work is over.
>
 Needs review...work over !
> Thanks,
> Quentin

Regards,
Naresh

Revision history for this message
qdp (OpenERP) (qdp) wrote :

> - four empty writes on res.partner.address where no changes are logged (not
> correct)
yes but no, you didn't implemented it in the right way: what you should do is avoid creating the audittrail log if there is no log line, instead of creating it then deleting it if there is no line. It will avoid useless operations.

For that, you need to do a small change:
=> replace the function
     self.create_log_line(cr, uid, log_id, model, lines)
   by
     self.create_log(cr, uid, model, lines)

def create_log(self, cr, uid, model, lines=[]):
    if not lines:
        return True
    vals = { ... }
    vals['lines] = [(0, 0, { ... }), ...]
    return self.pool.get('audittrail.log').create(cr, uid, vals, context)

> - one write on res.partner.address with the data you just created
> (incorrect, the method logged should be 'create' instead of 'write', otherwise
> it's ok)
i know, but it's not a argument. You can force the method to use. For example, try to add those 2 lines (it seems doing the trick but i didn't test completly):
=== modified file 'audittrail/audittrail.py'
--- audittrail/audittrail.py 2011-11-08 16:55:34 +0000
+++ audittrail/audittrail.py 2011-11-09 09:43:22 +0000
@@ -354,6 +357,8 @@
                     vals = {'method': method,'object_id': model.id,'user_id': uid_orig }
                     for resource in resource_data:
                         resource_id = resource['id']
+ if resource_id not in dict_to_use.keys():
+ vals.update({'method': 'create'})
                         vals.update({'res_id': resource_id})
                         log_id = log_pool.create(cr, uid, vals)
                         lines = []

i set it back to 'work in progress' ;-)
Quentin

review: Needs Fixing
Revision history for this message
Naresh(OpenERP) (nch-openerp) wrote :

> For that, you need to do a small change:
> => replace the function
> self.create_log_line(cr, uid, log_id, model, lines)
> by
> self.create_log(cr, uid, model, lines)
>
> def create_log(self, cr, uid, model, lines=[]):
> if not lines:
> return True
> vals = { ... }
> vals['lines] = [(0, 0, { ... }), ...]
> return self.pool.get('audittrail.log').create(cr, uid, vals, context)
>

hmm...cannot do that because lines will always be there just the difference is whether the new values is equal to the old value or not if its equal don't create log line ignore it else create it. but of-cause there is a scope of improvement here by checking the condition before appending the lines. and also create the log entry only if there is atleast 1 line.

>
> > - one write on res.partner.address with the data you just created
> > (incorrect, the method logged should be 'create' instead of 'write',
> otherwise
> > it's ok)
> i know, but it's not a argument. You can force the method to use. For example,
> try to add those 2 lines (it seems doing the trick but i didn't test
> completly):
> === modified file 'audittrail/audittrail.py'
> --- audittrail/audittrail.py 2011-11-08 16:55:34 +0000
> +++ audittrail/audittrail.py 2011-11-09 09:43:22 +0000
> @@ -354,6 +357,8 @@
> vals = {'method': method,'object_id': model.id,'user_id':
> uid_orig }
> for resource in resource_data:
> resource_id = resource['id']
> + if resource_id not in dict_to_use.keys():
> + vals.update({'method': 'create'})
> vals.update({'res_id': resource_id})
> log_id = log_pool.create(cr, uid, vals)
> lines = []

yup, didn't think of this hack !

Regards,
Naresh

Revision history for this message
qdp (OpenERP) (qdp) wrote :

hi again Naresh,

before proceeding, i still have few remarks:
1) in the docstring of get_value_text() please add the meaning of the params and returned value
2) please document the function start_log_process()
3) the code of inline_process_old_data() and inline_process_new_data() seems really similar. I wonder if we couldn't factorize that code. Morover, having those functions inside another function make the code a bit unreadable.
4) a general effort of documenting/commenting is needed because the code is really difficult to understand.

Next time will be the good one, i'm pretty sure of it! (otherwise, the bug is totally fixed by now and the module seems to run perfectly!)

Regards,
Quentin

review: Needs Fixing
Revision history for this message
Naresh(OpenERP) (nch-openerp) wrote :

Hello Quentin,

Thanks for reviewing ! I had made the changes as required and also I have rewritten the method to process and log for the actions and workflow. So you will be required to perform again a test before merge. Fix few bugs too like useless logs were created for X2M models when a 'read' operation was performed.

hope now it want come back....Thanks again...

Regards,

Revision history for this message
qdp (OpenERP) (qdp) wrote :

Hi Naresh,

i finished the refactoring myself. Explain what should have been done would have taken so much time and iteration...

you can check: it's merged in revision 5754.

Thanks

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'audittrail/audittrail.py'
--- audittrail/audittrail.py 2011-10-16 01:28:00 +0000
+++ audittrail/audittrail.py 2011-11-21 16:41:25 +0000
@@ -43,22 +43,17 @@
43 "log_create": fields.boolean("Log Creates",help="Select this if you want to keep track of creation on any record of the object of this rule"),43 "log_create": fields.boolean("Log Creates",help="Select this if you want to keep track of creation on any record of the object of this rule"),
44 "log_action": fields.boolean("Log Action",help="Select this if you want to keep track of actions on the object of this rule"),44 "log_action": fields.boolean("Log Action",help="Select this if you want to keep track of actions on the object of this rule"),
45 "log_workflow": fields.boolean("Log Workflow",help="Select this if you want to keep track of workflow on any record of the object of this rule"),45 "log_workflow": fields.boolean("Log Workflow",help="Select this if you want to keep track of workflow on any record of the object of this rule"),
46 "state": fields.selection((("draft", "Draft"),46 "state": fields.selection((("draft", "Draft"), ("subscribed", "Subscribed")), "State", required=True),
47 ("subscribed", "Subscribed")),
48 "State", required=True),
49 "action_id": fields.many2one('ir.actions.act_window', "Action ID"),47 "action_id": fields.many2one('ir.actions.act_window', "Action ID"),
50
51 }48 }
52
53 _defaults = {49 _defaults = {
54 'state': lambda *a: 'draft',50 'state': 'draft',
55 'log_create': lambda *a: 1,51 'log_create': 1,
56 'log_unlink': lambda *a: 1,52 'log_unlink': 1,
57 'log_write': lambda *a: 1,53 'log_write': 1,
58 }54 }
59
60 _sql_constraints = [55 _sql_constraints = [
61 ('model_uniq', 'unique (object_id)', """There is a rule defined on this object\n You cannot define another one the same object!""")56 ('model_uniq', 'unique (object_id)', """There is already a rule defined on this object\n You cannot define another: please edit the existing one.""")
62 ]57 ]
63 __functions = {}58 __functions = {}
6459
@@ -178,54 +173,33 @@
178class audittrail_objects_proxy(object_proxy):173class audittrail_objects_proxy(object_proxy):
179 """ Uses Object proxy for auditing changes on object of subscribed Rules"""174 """ Uses Object proxy for auditing changes on object of subscribed Rules"""
180175
181 def get_value_text(self, cr, uid, field_name, values, model, context=None):176 def get_value_text(self, cr, uid, pool, resource_pool, method, field, value):
182 """177 """
183 Gets textual values for the fields178 Gets textual values for the fields.
184 e.g.: For field of type many2one it gives its name value instead of id179 If the field is a many2one, it returns the name.
180 If it's a one2many or a many2many, it returns a list of name.
181 In other cases, it just returns the value.
182 :param cr: the current row, from the database cursor,
183 :param uid: the current user’s ID for security checks,
184 :param pool: current db's pooler object.
185 :param resource_pool: pooler object of the model which values are being changed.
186 :param field: for which the text value is to be returned.
187 :param value: value of the field.
188 :param recursive: True or False, True will repeat the process recursively
189 :return: string value or a list of values(for O2M/M2M)
190 """
185191
186 @param cr: the current row, from the database cursor,192 field_obj = (resource_pool._all_columns.get(field)).column
187 @param uid: the current user’s ID for security checks,193 if field_obj._type in ('one2many','many2many'):
188 @param field_name: List of fields for text values194 data = pool.get(field_obj._obj).name_get(cr, uid, value)
189 @param values: Values for field to be converted into textual values195 #return the modifications on x2many fields as a list of names
190 @return: values: List of textual values for given fields196 res = map(lambda x:x[1], data)
191 """197 elif field_obj._type == 'many2one':
192 if not context:198 #return the modifications on a many2one field as its value returned by name_get()
193 context = {}199 res = value and value[1] or value
194 if field_name in('__last_update','id'):
195 return values
196 pool = pooler.get_pool(cr.dbname)
197 field_pool = pool.get('ir.model.fields')
198 model_pool = pool.get('ir.model')
199 obj_pool = pool.get(model.model)
200 if obj_pool._inherits:
201 inherits_ids = model_pool.search(cr, uid, [('model', '=', obj_pool._inherits.keys()[0])])
202 field_ids = field_pool.search(cr, uid, [('name', '=', field_name), ('model_id', 'in', (model.id, inherits_ids[0]))])
203 else:200 else:
204 field_ids = field_pool.search(cr, uid, [('name', '=', field_name), ('model_id', '=', model.id)])201 res = value
205 field_id = field_ids and field_ids[0] or False202 return res
206 assert field_id, _("'%s' field does not exist in '%s' model" %(field_name, model.model))
207
208 field = field_pool.read(cr, uid, field_id)
209 relation_model = field['relation']
210 relation_model_pool = relation_model and pool.get(relation_model) or False
211
212 if field['ttype'] == 'many2one':
213 res = False
214 relation_id = False
215 if values and type(values) == tuple:
216 relation_id = values[0]
217 if relation_id and relation_model_pool:
218 relation_model_object = relation_model_pool.read(cr, uid, relation_id, [relation_model_pool._rec_name])
219 res = relation_model_object[relation_model_pool._rec_name]
220 return res
221
222 elif field['ttype'] in ('many2many','one2many'):
223 res = []
224 for relation_model_object in relation_model_pool.read(cr, uid, values, [relation_model_pool._rec_name]):
225 res.append(relation_model_object[relation_model_pool._rec_name])
226 return res
227
228 return values
229203
230 def create_log_line(self, cr, uid, log_id, model, lines=[]):204 def create_log_line(self, cr, uid, log_id, model, lines=[]):
231 """205 """
@@ -233,7 +207,7 @@
233207
234 @param cr: the current row, from the database cursor,208 @param cr: the current row, from the database cursor,
235 @param uid: the current user’s ID for security checks,209 @param uid: the current user’s ID for security checks,
236 @param model: Object who's values are being changed210 @param model: Object which values are being changed
237 @param lines: List of values for line is to be created211 @param lines: List of values for line is to be created
238 """212 """
239 pool = pooler.get_pool(cr.dbname)213 pool = pooler.get_pool(cr.dbname)
@@ -241,216 +215,273 @@
241 model_pool = pool.get('ir.model')215 model_pool = pool.get('ir.model')
242 field_pool = pool.get('ir.model.fields')216 field_pool = pool.get('ir.model.fields')
243 log_line_pool = pool.get('audittrail.log.line')217 log_line_pool = pool.get('audittrail.log.line')
244 #start Loop
245 for line in lines:218 for line in lines:
246 if line['name'] in('__last_update','id'):219 field_obj = obj_pool._all_columns.get(line['name'])
247 continue220 assert field_obj, _("'%s' field does not exist in '%s' model" %(line['name'], model.model))
221 field_obj = field_obj.column
222 old_value = line.get('old_value', '')
223 new_value = line.get('new_value', '')
224 search_models = [model.id]
248 if obj_pool._inherits:225 if obj_pool._inherits:
249 inherits_ids = model_pool.search(cr, uid, [('model', '=', obj_pool._inherits.keys()[0])])226 search_models += model_pool.search(cr, uid, [('model', 'in', obj_pool._inherits.keys())])
250 field_ids = field_pool.search(cr, uid, [('name', '=', line['name']), ('model_id', 'in', (model.id, inherits_ids[0]))])227 field_id = field_pool.search(cr, uid, [('name', '=', line['name']), ('model_id', 'in', search_models)])
251 else:228 if field_obj._type == 'many2one':
252 field_ids = field_pool.search(cr, uid, [('name', '=', line['name']), ('model_id', '=', model.id)])229 old_value = old_value and old_value[0] or old_value
253 field_id = field_ids and field_ids[0] or False230 new_value = new_value and new_value[0] or new_value
254 assert field_id, _("'%s' field does not exist in '%s' model" %(line['name'], model.model))
255
256 field = field_pool.read(cr, uid, field_id)
257 old_value = 'old_value' in line and line['old_value'] or ''
258 new_value = 'new_value' in line and line['new_value'] or ''
259 old_value_text = 'old_value_text' in line and line['old_value_text'] or ''
260 new_value_text = 'new_value_text' in line and line['new_value_text'] or ''
261
262 if old_value_text == new_value_text:
263 continue
264 if field['ttype'] == 'many2one':
265 if type(old_value) == tuple:
266 old_value = old_value[0]
267 if type(new_value) == tuple:
268 new_value = new_value[0]
269 vals = {231 vals = {
270 "log_id": log_id,232 "log_id": log_id,
271 "field_id": field_id,233 "field_id": field_id and field_id[0] or False,
272 "old_value": old_value,234 "old_value": old_value,
273 "new_value": new_value,235 "new_value": new_value,
274 "old_value_text": old_value_text,236 "old_value_text": line.get('old_value_text', ''),
275 "new_value_text": new_value_text,237 "new_value_text": line.get('new_value_text', ''),
276 "field_description": field['field_description']238 "field_description": field_obj.string
277 }239 }
278 line_id = log_line_pool.create(cr, uid, vals)240 line_id = log_line_pool.create(cr, uid, vals)
279 cr.commit()
280 #End Loop
281 return True241 return True
282242
283 def log_fct(self, cr, uid, model, method, fct_src, *args):243 def log_fct(self, cr, uid_orig, model, method, fct_src, *args):
284 """244 """
285 Logging function: This function is performs logging oprations according to method245 Logging function: This function is performing the logging operation
286 @param db: the current database246 @param model: Object whose values are being changed
287 @param uid: the current user’s ID for security checks,247 @param method: method to log: create, read, write, unlink, action or workflow action
288 @param object: Object who's values are being changed
289 @param method: method to log: create, read, write, unlink
290 @param fct_src: execute method of Object proxy248 @param fct_src: execute method of Object proxy
291249
292 @return: Returns result as per method of Object proxy250 @return: Returns result as per method of Object proxy
293 """251 """
294 uid_orig = uid
295 uid = 1
296 res2 = args
297 pool = pooler.get_pool(cr.dbname)252 pool = pooler.get_pool(cr.dbname)
298 resource_pool = pool.get(model)253 resource_pool = pool.get(model)
299 log_pool = pool.get('audittrail.log')
300 model_pool = pool.get('ir.model')254 model_pool = pool.get('ir.model')
301255 model_ids = model_pool.search(cr, 1, [('model', '=', model)])
302 model_ids = model_pool.search(cr, uid, [('model', '=', model)])
303 model_id = model_ids and model_ids[0] or False256 model_id = model_ids and model_ids[0] or False
304 assert model_id, _("'%s' Model does not exist..." %(model))257 assert model_id, _("'%s' Model does not exist..." %(model))
305 model = model_pool.browse(cr, uid, model_id)258 model = model_pool.browse(cr, 1, model_id)
306259
307 if method in ('create'):260 # fields to log. currently only used by log on read()
308 res_id = fct_src(cr, uid_orig, model.model, method, *args)261 field_list = []
309 resource = resource_pool.read(cr, uid, res_id, args[0].keys())262 old_values = new_values = {}
310 vals = {263
311 "method": method,264 if method == 'create':
312 "object_id": model.id,265 res = fct_src(cr, uid_orig, model.model, method, *args)
313 "user_id": uid_orig,266 if res:
314 "res_id": resource['id'],267 res_ids = [res]
315 }268 new_values = self.get_data(cr, uid_orig, pool, res_ids, model, method)
316 if 'id' in resource:269 elif method == 'read':
317 del resource['id']270 res = fct_src(cr, uid_orig, model.model, method, *args)
318 log_id = log_pool.create(cr, uid, vals)271 # build the res_ids and the old_values dict. Here we don't use get_data() to
319 lines = []272 # avoid performing an additional read()
320 for field in resource:273 res_ids = []
321 line = {274 for record in res:
322 'name': field,275 res_ids.append(record['id'])
323 'new_value': resource[field],276 old_values[(model.id, record['id'])] = {'value': record, 'text': record}
324 'new_value_text': self.get_value_text(cr, uid, field, resource[field], model)277 # log only the fields read
325 }278 field_list = args[1]
326 lines.append(line)279 elif method == 'unlink':
327 self.create_log_line(cr, uid, log_id, model, lines)280 res_ids = args[0]
328281 old_values = self.get_data(cr, uid_orig, pool, res_ids, model, method)
329 return res_id282 res = fct_src(cr, uid_orig, model.model, method, *args)
330283 else: # method is write, action or workflow action
331 elif method in ('read'):284 res_ids = []
332 res_ids = args[0]
333 old_values = {}
334 res = fct_src(cr, uid_orig, model.model, method, *args)
335 if type(res) == list:
336 for v in res:
337 old_values[v['id']] = v
338 else:
339 old_values[res['id']] = res
340 for res_id in old_values:
341 vals = {
342 "method": method,
343 "object_id": model.id,
344 "user_id": uid_orig,
345 "res_id": res_id,
346
347 }
348 log_id = log_pool.create(cr, uid, vals)
349 lines = []
350 for field in old_values[res_id]:
351 line = {
352 'name': field,
353 'old_value': old_values[res_id][field],
354 'old_value_text': self.get_value_text(cr, uid, field, old_values[res_id][field], model)
355 }
356 lines.append(line)
357
358 self.create_log_line(cr, uid, log_id, model, lines)
359 return res
360
361 elif method in ('unlink'):
362 res_ids = args[0]
363 old_values = {}
364 for res_id in res_ids:
365 old_values[res_id] = resource_pool.read(cr, uid, res_id)
366
367 for res_id in res_ids:
368 vals = {
369 "method": method,
370 "object_id": model.id,
371 "user_id": uid_orig,
372 "res_id": res_id,
373
374 }
375 log_id = log_pool.create(cr, uid, vals)
376 lines = []
377 for field in old_values[res_id]:
378 if field in ('id'):
379 continue
380 line = {
381 'name': field,
382 'old_value': old_values[res_id][field],
383 'old_value_text': self.get_value_text(cr, uid, field, old_values[res_id][field], model)
384 }
385 lines.append(line)
386
387 self.create_log_line(cr, uid, log_id, model, lines)
388 res = fct_src(cr, uid_orig, model.model, method, *args)
389 return res
390 else:
391 res_ids = []
392 res = True
393 if args:285 if args:
394 res_ids = args[0]286 res_ids = args[0]
395 old_values = {}287 if isinstance(res_ids, (long, int)):
396 fields = []
397 if len(args)>1 and type(args[1]) == dict:
398 fields = args[1].keys()
399 if type(res_ids) in (long, int):
400 res_ids = [res_ids]288 res_ids = [res_ids]
401 if res_ids:289 if res_ids:
402 for resource in resource_pool.read(cr, uid, res_ids):290 # store the old values into a dictionary
403 resource_id = resource['id']291 old_values = self.get_data(cr, uid_orig, pool, res_ids, model, method)
404 if 'id' in resource:292 # process the original function, workflow trigger...
405 del resource['id']
406 old_values_text = {}
407 old_value = {}
408 for field in resource.keys():
409 old_value[field] = resource[field]
410 old_values_text[field] = self.get_value_text(cr, uid, field, resource[field], model)
411 old_values[resource_id] = {'text':old_values_text, 'value': old_value}
412
413 res = fct_src(cr, uid_orig, model.model, method, *args)293 res = fct_src(cr, uid_orig, model.model, method, *args)
414294 if method == 'copy':
295 res_ids = [res]
415 if res_ids:296 if res_ids:
416 for resource in resource_pool.read(cr, uid, res_ids):297 # check the new values and store them into a dictionary
417 resource_id = resource['id']298 new_values = self.get_data(cr, uid_orig, pool, res_ids, model, method)
418 if 'id' in resource:299 # compare the old and new values and create audittrail log if needed
419 del resource['id']300 self.process_data(cr, uid_orig, pool, res_ids, model, method, old_values, new_values, field_list)
420 vals = {301 return res
421 "method": method,302
422 "object_id": model.id,303 def get_data(self, cr, uid, pool, res_ids, model, method):
423 "user_id": uid_orig,304 """
424 "res_id": resource_id,305 This function simply read all the fields of the given res_ids, and also recurisvely on
425 }306 all records of a x2m fields read that need to be logged. Then it returns the result in
426307 convenient structure that will be used as comparison basis.
427308
428 log_id = log_pool.create(cr, uid, vals)309 :param cr: the current row, from the database cursor,
429 lines = []310 :param uid: the current user’s ID. This parameter is currently not used as every
430 for field in resource.keys():311 operation to get data is made as super admin. Though, it could be usefull later.
431 line = {312 :param pool: current db's pooler object.
432 'name': field,313 :param res_ids: Id's of resource to be logged/compared.
433 'new_value': resource[field],314 :param model: Object whose values are being changed
434 'old_value': old_values[resource_id]['value'][field],315 :param method: method to log: create, read, unlink, write, actions, workflow actions
435 'new_value_text': self.get_value_text(cr, uid, field, resource[field], model),316 :return: dict mapping a tuple (model_id, resource_id) with its value and textual value
436 'old_value_text': old_values[resource_id]['text'][field]317 { (model_id, resource_id): { 'value': ...
437 }318 'textual_value': ...
438 lines.append(line)319 },
439320 }
440 self.create_log_line(cr, uid, log_id, model, lines)321 """
441 return res322 data = {}
323 resource_pool = pool.get(model.model)
324 # read all the fields of the given resources in super admin mode
325 for resource in resource_pool.read(cr, 1, res_ids):
326 values = {}
327 values_text = {}
328 resource_id = resource['id']
329 # loop on each field on the res_ids we just have read
330 for field in resource:
331 if field in ('__last_update', 'id'):
332 continue
333 values[field] = resource[field]
334 # get the textual value of that field for this record
335 values_text[field] = self.get_value_text(cr, 1, pool, resource_pool, method, field, resource[field])
336
337 field_obj = resource_pool._all_columns.get(field).column
338 if field_obj._type in ('one2many','many2many'):
339 # check if an audittrail rule apply in super admin mode
340 if self.check_rules(cr, 1, field_obj._obj, method):
341 # check if the model associated to a *2m field exists, in super admin mode
342 x2m_model_ids = pool.get('ir.model').search(cr, 1, [('model', '=', field_obj._obj)])
343 x2m_model_id = x2m_model_ids and x2m_model_ids[0] or False
344 assert x2m_model_id, _("'%s' Model does not exist..." %(field_obj._obj))
345 x2m_model = pool.get('ir.model').browse(cr, 1, x2m_model_id)
346 #recursive call on x2m fields that need to be checked too
347 data.update(self.get_data(cr, 1, pool, resource[field], x2m_model, method))
348 data[(model.id, resource_id)] = {'text':values_text, 'value': values}
349 return data
350
351 def prepare_audittrail_log_line(self, cr, uid, pool, model, resource_id, method, old_values, new_values, field_list=[]):
352 """
353 This function compares the old data (i.e before the method was executed) and the new data
354 (after the method was executed) and returns a structure with all the needed information to
355 log those differences.
356
357 :param cr: the current row, from the database cursor,
358 :param uid: the current user’s ID. This parameter is currently not used as every
359 operation to get data is made as super admin. Though, it could be usefull later.
360 :param pool: current db's pooler object.
361 :param model: model object which values are being changed
362 :param resource_id: ID of record to which values are being changed
363 :param method: method to log: create, read, unlink, write, actions, workflow actions
364 :param old_values: dict of values read before execution of the method
365 :param new_values: dict of values read after execution of the method
366 :param field_list: optional argument containing the list of fields to log. Currently only
367 used when performing a read, it could be usefull later on if we want to log the write
368 on specific fields only.
369
370 :return: dictionary with
371 * keys: tuples build as ID of model object to log and ID of resource to log
372 * values: list of all the changes in field values for this couple (model, resource)
373 return {
374 (model.id, resource_id): []
375 }
376
377 The reason why the structure returned is build as above is because when modifying an existing
378 record (res.partner, for example), we may have to log a change done in a x2many field (on
379 res.partner.address, for example)
380 """
381 key = (model.id, resource_id)
382 lines = {
383 key: []
384 }
385 # loop on all the fields
386 for field_name, field_definition in pool.get(model.model)._all_columns.items():
387 #if the field_list param is given, skip all the fields not in that list
388 if field_list and field_name not in field_list:
389 continue
390 field_obj = field_definition.column
391 if field_obj._type in ('one2many','many2many'):
392 # checking if an audittrail rule apply in super admin mode
393 if self.check_rules(cr, 1, field_obj._obj, method):
394 # checking if the model associated to a *2m field exists, in super admin mode
395 x2m_model_ids = pool.get('ir.model').search(cr, 1, [('model', '=', field_obj._obj)])
396 x2m_model_id = x2m_model_ids and x2m_model_ids[0] or False
397 assert x2m_model_id, _("'%s' Model does not exist..." %(field_obj._obj))
398 x2m_model = pool.get('ir.model').browse(cr, 1, x2m_model_id)
399 # the resource_ids that need to be checked are the sum of both old and previous values (because we
400 # need to log also creation or deletion in those lists).
401 x2m_old_values_ids = old_values.get(key, {'value': {}})['value'].get(field_name, [])
402 x2m_new_values_ids = new_values.get(key, {'value': {}})['value'].get(field_name, [])
403 # We use list(set(...)) to remove duplicates.
404 res_ids = list(set(x2m_old_values_ids + x2m_new_values_ids))
405 for res_id in res_ids:
406 lines.update(self.prepare_audittrail_log_line(cr, 1, pool, x2m_model, res_id, method, old_values, new_values, field_list))
407 # if the value value is different than the old value: record the change
408 if key not in old_values or key not in new_values or old_values[key]['value'][field_name] != new_values[key]['value'][field_name]:
409 data = {
410 'name': field_name,
411 'new_value': key in new_values and new_values[key]['value'].get(field_name),
412 'old_value': key in old_values and old_values[key]['value'].get(field_name),
413 'new_value_text': key in new_values and new_values[key]['text'].get(field_name),
414 'old_value_text': key in old_values and old_values[key]['text'].get(field_name)
415 }
416 lines[key].append(data)
417 return lines
418
419 def process_data(self, cr, uid, pool, res_ids, model, method, old_values={}, new_values={}, field_list=[]):
420 """
421 This function processes and iterates recursively to log the difference between the old
422 data (i.e before the method was executed) and the new data and creates audittrail log
423 accordingly.
424
425 :param cr: the current row, from the database cursor,
426 :param uid: the current user’s ID,
427 :param pool: current db's pooler object.
428 :param res_ids: Id's of resource to be logged/compared.
429 :param model: model object which values are being changed
430 :param method: method to log: create, read, unlink, write, actions, workflow actions
431 :param old_values: dict of values read before execution of the method
432 :param new_values: dict of values read after execution of the method
433 :param field_list: optional argument containing the list of fields to log. Currently only
434 used when performing a read, it could be usefull later on if we want to log the write
435 on specific fields only.
436 :return: True
437 """
438 # loop on all the given ids
439 for res_id in res_ids:
440 # compare old and new values and get audittrail log lines accordingly
441 lines = self.prepare_audittrail_log_line(cr, uid, pool, model, res_id, method, old_values, new_values, field_list)
442
443 # if at least one modification has been found
444 for model_id, resource_id in lines:
445 vals = {
446 'method': method,
447 'object_id': model_id,
448 'user_id': uid,
449 'res_id': resource_id,
450 }
451 if (model_id, resource_id) not in old_values and method not in ('copy', 'read'):
452 # the resource was not existing so we are forcing the method to 'create'
453 # (because it could also come with the value 'write' if we are creating
454 # new record through a one2many field)
455 vals.update({'method': 'create'})
456 if (model_id, resource_id) not in new_values and method not in ('copy', 'read'):
457 # the resource is not existing anymore so we are forcing the method to 'unlink'
458 # (because it could also come with the value 'write' if we are deleting the
459 # record through a one2many field)
460 vals.update({'method': 'unlink'})
461 # create the audittrail log in super admin mode, only if a change has been detected
462 if lines[(model_id, resource_id)]:
463 log_id = pool.get('audittrail.log').create(cr, 1, vals)
464 model = pool.get('ir.model').browse(cr, uid, model_id)
465 self.create_log_line(cr, 1, log_id, model, lines[(model_id, resource_id)])
442 return True466 return True
443467
444 def check_rules(self, cr, uid, model, method):468 def check_rules(self, cr, uid, model, method):
469 """
470 Checks if auditrails is installed for that db and then if one rule match
471 @param cr: the current row, from the database cursor,
472 @param uid: the current user’s ID,
473 @param model: value of _name of the object which values are being changed
474 @param method: method to log: create, read, unlink,write,actions,workflow actions
475 @return: True or False
476 """
445 pool = pooler.get_pool(cr.dbname)477 pool = pooler.get_pool(cr.dbname)
446 # Check if auditrails is installed for that db and then if one rule match
447 if 'audittrail.rule' in pool.models:478 if 'audittrail.rule' in pool.models:
448 model_ids = pool.get('ir.model').search(cr, 1, [('model', '=', model)])479 model_ids = pool.get('ir.model').search(cr, 1, [('model', '=', model)])
449 model_id = model_ids and model_ids[0] or False480 model_id = model_ids and model_ids[0] or False
450 if model_id:481 if model_id:
451 rule_ids = pool.get('audittrail.rule').search(cr, 1, [('object_id', '=', model_id), ('state', '=', 'subscribed')])482 rule_ids = pool.get('audittrail.rule').search(cr, 1, [('object_id', '=', model_id), ('state', '=', 'subscribed')])
452 for rule in pool.get('audittrail.rule').read(cr, 1, rule_ids, ['user_id','log_read','log_write','log_create','log_unlink','log_action','log_workflow']):483 for rule in pool.get('audittrail.rule').read(cr, 1, rule_ids, ['user_id','log_read','log_write','log_create','log_unlink','log_action','log_workflow']):
453 if len(rule['user_id'])==0 or uid in rule['user_id']:484 if len(rule['user_id']) == 0 or uid in rule['user_id']:
454 if rule.get('log_'+method,0):485 if rule.get('log_'+method,0):
455 return True486 return True
456 elif method not in ('default_get','read','fields_view_get','fields_get','search','search_count','name_search','name_get','get','request_get', 'get_sc', 'unlink', 'write', 'create'):487 elif method not in ('default_get','read','fields_view_get','fields_get','search','search_count','name_search','name_get','get','request_get', 'get_sc', 'unlink', 'write', 'create'):
@@ -460,18 +491,14 @@
460 def execute_cr(self, cr, uid, model, method, *args, **kw):491 def execute_cr(self, cr, uid, model, method, *args, **kw):
461 fct_src = super(audittrail_objects_proxy, self).execute_cr492 fct_src = super(audittrail_objects_proxy, self).execute_cr
462 if self.check_rules(cr,uid,model,method):493 if self.check_rules(cr,uid,model,method):
463 res = self.log_fct(cr, uid, model, method, fct_src, *args)494 return self.log_fct(cr, uid, model, method, fct_src, *args)
464 else:495 return fct_src(cr, uid, model, method, *args)
465 res = fct_src(cr, uid, model, method, *args)
466 return res
467496
468 def exec_workflow_cr(self, cr, uid, model, method, *args, **argv):497 def exec_workflow_cr(self, cr, uid, model, method, *args, **argv):
469 fct_src = super(audittrail_objects_proxy, self).exec_workflow_cr498 fct_src = super(audittrail_objects_proxy, self).exec_workflow_cr
470 if self.check_rules(cr,uid,model,'workflow'):499 if self.check_rules(cr,uid,model,'workflow'):
471 res = self.log_fct(cr, uid, model, method, fct_src, *args)500 return self.log_fct(cr, uid, model, method, fct_src, *args)
472 else:501 return fct_src(cr, uid, model, method, *args)
473 res = fct_src(cr, uid, model, method, *args)
474 return res
475502
476audittrail_objects_proxy()503audittrail_objects_proxy()
477504

Subscribers

People subscribed via source and target branches

to all changes: