[IMP] auditlog: black, isort, prettier

pull/2466/head
Raf Ven 2019-11-08 20:16:33 +01:00 committed by Stefan Rijnhart
parent 1b1f827c1b
commit 732654f95f
12 changed files with 708 additions and 562 deletions

View File

@ -2,22 +2,20 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{ {
'name': "Audit Log", "name": "Audit Log",
'version': "13.0.1.0.0", "version": "13.0.1.0.0",
'author': "ABF OSIELL,Odoo Community Association (OCA)", "author": "ABF OSIELL,Odoo Community Association (OCA)",
'license': "AGPL-3", "license": "AGPL-3",
'website': "https://github.com/OCA/server-tools/", "website": "https://github.com/OCA/server-tools/",
'category': "Tools", "category": "Tools",
'depends': [ "depends": ["base"],
'base', "data": [
"security/ir.model.access.csv",
"data/ir_cron.xml",
"views/auditlog_view.xml",
"views/http_session_view.xml",
"views/http_request_view.xml",
], ],
'data': [ "application": True,
'security/ir.model.access.csv', "installable": True,
'data/ir_cron.xml',
'views/auditlog_view.xml',
'views/http_session_view.xml',
'views/http_request_view.xml',
],
'application': True,
'installable': True,
} }

View File

@ -1,16 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1"> <odoo noupdate="1">
<record id="ir_cron_auditlog_autovacuum" model="ir.cron">
<record id="ir_cron_auditlog_autovacuum" model="ir.cron"> <field name='name'>Auto-vacuum audit logs</field>
<field name='name'>Auto-vacuum audit logs</field> <field name='interval_number'>1</field>
<field name='interval_number'>1</field> <field name='interval_type'>days</field>
<field name='interval_type'>days</field> <field name="numbercall">-1</field>
<field name="numbercall">-1</field> <field name="active" eval="False" />
<field name="active" eval="False"/> <field name="doall" eval="False" />
<field name="doall" eval="False"/> <field name="code">model.autovacuum(180)</field>
<field name="code">model.autovacuum(180)</field> <field name="state">code</field>
<field name="state">code</field> <field name="model_id" ref="model_auditlog_autovacuum" />
<field name="model_id" ref="model_auditlog_autovacuum"/> </record>
</record>
</odoo> </odoo>

View File

@ -3,14 +3,13 @@
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from odoo import models, fields, api from odoo import api, fields, models
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class AuditlogAutovacuum(models.TransientModel): class AuditlogAutovacuum(models.TransientModel):
_name = 'auditlog.autovacuum' _name = "auditlog.autovacuum"
_description = "Auditlog - Delete old logs" _description = "Auditlog - Delete old logs"
@api.model @api.model
@ -24,17 +23,12 @@ class AuditlogAutovacuum(models.TransientModel):
""" """
days = (days > 0) and int(days) or 0 days = (days > 0) and int(days) or 0
deadline = datetime.now() - timedelta(days=days) deadline = datetime.now() - timedelta(days=days)
data_models = ( data_models = ("auditlog.log", "auditlog.http.request", "auditlog.http.session")
'auditlog.log',
'auditlog.http.request',
'auditlog.http.session',
)
for data_model in data_models: for data_model in data_models:
records = self.env[data_model].search( records = self.env[data_model].search(
[('create_date', '<=', fields.Datetime.to_string(deadline))]) [("create_date", "<=", fields.Datetime.to_string(deadline))]
)
nb_records = len(records) nb_records = len(records)
records.unlink() records.unlink()
_logger.info( _logger.info("AUTOVACUUM - %s '%s' records deleted", nb_records, data_model)
"AUTOVACUUM - %s '%s' records deleted",
nb_records, data_model)
return True return True

View File

@ -3,36 +3,31 @@
from psycopg2.extensions import AsIs from psycopg2.extensions import AsIs
from odoo import models, fields, api from odoo import api, fields, models
from odoo.http import request from odoo.http import request
class AuditlogHTTPRequest(models.Model): class AuditlogHTTPRequest(models.Model):
_name = 'auditlog.http.request' _name = "auditlog.http.request"
_description = "Auditlog - HTTP request log" _description = "Auditlog - HTTP request log"
_order = "create_date DESC" _order = "create_date DESC"
display_name = fields.Char( display_name = fields.Char("Name", compute="_compute_display_name", store=True)
"Name", compute="_compute_display_name", store=True)
name = fields.Char("Path") name = fields.Char("Path")
root_url = fields.Char("Root URL") root_url = fields.Char("Root URL")
user_id = fields.Many2one( user_id = fields.Many2one("res.users", string="User")
'res.users', string="User") http_session_id = fields.Many2one("auditlog.http.session", string="Session")
http_session_id = fields.Many2one(
'auditlog.http.session', string="Session")
user_context = fields.Char("Context") user_context = fields.Char("Context")
log_ids = fields.One2many( log_ids = fields.One2many("auditlog.log", "http_request_id", string="Logs")
'auditlog.log', 'http_request_id', string="Logs")
@api.depends('create_date', 'name') @api.depends("create_date", "name")
def _compute_display_name(self): def _compute_display_name(self):
for httprequest in self: for httprequest in self:
create_date = fields.Datetime.from_string(httprequest.create_date) create_date = fields.Datetime.from_string(httprequest.create_date)
tz_create_date = fields.Datetime.context_timestamp( tz_create_date = fields.Datetime.context_timestamp(httprequest, create_date)
httprequest, create_date) httprequest.display_name = "{} ({})".format(
httprequest.display_name = "%s (%s)" % ( httprequest.name or "?", fields.Datetime.to_string(tz_create_date)
httprequest.name or '?', )
fields.Datetime.to_string(tz_create_date))
def name_get(self): def name_get(self):
return [(request.id, request.display_name) for request in self] return [(request.id, request.display_name) for request in self]
@ -47,24 +42,24 @@ class AuditlogHTTPRequest(models.Model):
""" """
if not request: if not request:
return False return False
http_session_model = self.env['auditlog.http.session'] http_session_model = self.env["auditlog.http.session"]
httprequest = request.httprequest httprequest = request.httprequest
if httprequest: if httprequest:
if hasattr(httprequest, 'auditlog_http_request_id'): if hasattr(httprequest, "auditlog_http_request_id"):
# Verify existence. Could have been rolled back after a # Verify existence. Could have been rolled back after a
# concurrency error # concurrency error
self.env.cr.execute( self.env.cr.execute(
"SELECT id FROM %s WHERE id = %s", ( "SELECT id FROM %s WHERE id = %s",
AsIs(self._table), (AsIs(self._table), httprequest.auditlog_http_request_id),
httprequest.auditlog_http_request_id)) )
if self.env.cr.fetchone(): if self.env.cr.fetchone():
return httprequest.auditlog_http_request_id return httprequest.auditlog_http_request_id
vals = { vals = {
'name': httprequest.path, "name": httprequest.path,
'root_url': httprequest.url_root, "root_url": httprequest.url_root,
'user_id': request.uid, "user_id": request.uid,
'http_session_id': http_session_model.current_http_session(), "http_session_id": http_session_model.current_http_session(),
'user_context': request.context, "user_context": request.context,
} }
httprequest.auditlog_http_request_id = self.create(vals).id httprequest.auditlog_http_request_id = self.create(vals).id
return httprequest.auditlog_http_request_id return httprequest.auditlog_http_request_id

View File

@ -1,32 +1,31 @@
# Copyright 2015 ABF OSIELL <https://osiell.com> # Copyright 2015 ABF OSIELL <https://osiell.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import models, fields, api from odoo import api, fields, models
from odoo.http import request from odoo.http import request
class AuditlogtHTTPSession(models.Model): class AuditlogtHTTPSession(models.Model):
_name = 'auditlog.http.session' _name = "auditlog.http.session"
_description = "Auditlog - HTTP User session log" _description = "Auditlog - HTTP User session log"
_order = "create_date DESC" _order = "create_date DESC"
display_name = fields.Char( display_name = fields.Char("Name", compute="_compute_display_name", store=True)
"Name", compute="_compute_display_name", store=True)
name = fields.Char("Session ID", index=True) name = fields.Char("Session ID", index=True)
user_id = fields.Many2one( user_id = fields.Many2one("res.users", string="User", index=True)
'res.users', string="User", index=True)
http_request_ids = fields.One2many( http_request_ids = fields.One2many(
'auditlog.http.request', 'http_session_id', string="HTTP Requests") "auditlog.http.request", "http_session_id", string="HTTP Requests"
)
@api.depends('create_date', 'user_id') @api.depends("create_date", "user_id")
def _compute_display_name(self): def _compute_display_name(self):
for httpsession in self: for httpsession in self:
create_date = fields.Datetime.from_string(httpsession.create_date) create_date = fields.Datetime.from_string(httpsession.create_date)
tz_create_date = fields.Datetime.context_timestamp( tz_create_date = fields.Datetime.context_timestamp(httpsession, create_date)
httpsession, create_date) httpsession.display_name = "{} ({})".format(
httpsession.display_name = "%s (%s)" % ( httpsession.user_id and httpsession.user_id.name or "?",
httpsession.user_id and httpsession.user_id.name or '?', fields.Datetime.to_string(tz_create_date),
fields.Datetime.to_string(tz_create_date)) )
def name_get(self): def name_get(self):
return [(session.id, session.display_name) for session in self] return [(session.id, session.display_name) for session in self]
@ -44,15 +43,11 @@ class AuditlogtHTTPSession(models.Model):
httpsession = request.session httpsession = request.session
if httpsession: if httpsession:
existing_session = self.search( existing_session = self.search(
[('name', '=', httpsession.sid), [("name", "=", httpsession.sid), ("user_id", "=", request.uid)], limit=1
('user_id', '=', request.uid)], )
limit=1)
if existing_session: if existing_session:
return existing_session.id return existing_session.id
vals = { vals = {"name": httpsession.sid, "user_id": request.uid}
'name': httpsession.sid,
'user_id': request.uid,
}
httpsession.auditlog_http_session_id = self.create(vals).id httpsession.auditlog_http_session_id = self.create(vals).id
return httpsession.auditlog_http_session_id return httpsession.auditlog_http_session_id
return False return False

View File

@ -1,45 +1,39 @@
# Copyright 2015 ABF OSIELL <https://osiell.com> # Copyright 2015 ABF OSIELL <https://osiell.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import models, fields from odoo import fields, models
class AuditlogLog(models.Model): class AuditlogLog(models.Model):
_name = 'auditlog.log' _name = "auditlog.log"
_description = "Auditlog - Log" _description = "Auditlog - Log"
_order = "create_date desc" _order = "create_date desc"
name = fields.Char("Resource Name", size=64) name = fields.Char("Resource Name", size=64)
model_id = fields.Many2one( model_id = fields.Many2one("ir.model", string="Model")
'ir.model', string="Model")
res_id = fields.Integer("Resource ID") res_id = fields.Integer("Resource ID")
user_id = fields.Many2one( user_id = fields.Many2one("res.users", string="User")
'res.users', string="User")
method = fields.Char(size=64) method = fields.Char(size=64)
line_ids = fields.One2many( line_ids = fields.One2many("auditlog.log.line", "log_id", string="Fields updated")
'auditlog.log.line', 'log_id', string="Fields updated") http_session_id = fields.Many2one("auditlog.http.session", string="Session")
http_session_id = fields.Many2one( http_request_id = fields.Many2one("auditlog.http.request", string="HTTP Request")
'auditlog.http.session', string="Session")
http_request_id = fields.Many2one(
'auditlog.http.request', string="HTTP Request")
log_type = fields.Selection( log_type = fields.Selection(
[('full', "Full log"), [("full", "Full log"), ("fast", "Fast log")], string="Type"
('fast', "Fast log"), )
],
string="Type")
class AuditlogLogLine(models.Model): class AuditlogLogLine(models.Model):
_name = 'auditlog.log.line' _name = "auditlog.log.line"
_description = "Auditlog - Log details (fields updated)" _description = "Auditlog - Log details (fields updated)"
field_id = fields.Many2one( field_id = fields.Many2one(
'ir.model.fields', ondelete='cascade', string="Field", required=True) "ir.model.fields", ondelete="cascade", string="Field", required=True
)
log_id = fields.Many2one( log_id = fields.Many2one(
'auditlog.log', string="Log", ondelete='cascade', index=True) "auditlog.log", string="Log", ondelete="cascade", index=True
)
old_value = fields.Text() old_value = fields.Text()
new_value = fields.Text() new_value = fields.Text()
old_value_text = fields.Text("Old value Text") old_value_text = fields.Text("Old value Text")
new_value_text = fields.Text("New value Text") new_value_text = fields.Text("New value Text")
field_name = fields.Char("Technical name", related='field_id.name') field_name = fields.Char("Technical name", related="field_id.name")
field_description = fields.Char( field_description = fields.Char("Description", related="field_id.field_description")
"Description", related='field_id.field_description')

View File

@ -1,11 +1,16 @@
# Copyright 2015 ABF OSIELL <https://osiell.com> # Copyright 2015 ABF OSIELL <https://osiell.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import models, fields, api, modules, _ from odoo import _, api, fields, models, modules
FIELDS_BLACKLIST = [ FIELDS_BLACKLIST = [
'id', 'create_uid', 'create_date', 'write_uid', 'write_date', "id",
'display_name', '__last_update', "create_uid",
"create_date",
"write_uid",
"write_date",
"display_name",
"__last_update",
] ]
# Used for performance, to avoid a dictionary instanciation when we need an # Used for performance, to avoid a dictionary instanciation when we need an
# empty dict to simplify algorithms # empty dict to simplify algorithms
@ -19,6 +24,7 @@ class DictDiffer(object):
(3) keys same in both but changed values (3) keys same in both but changed values
(4) keys same in both and unchanged values (4) keys same in both and unchanged values
""" """
def __init__(self, current_dict, past_dict): def __init__(self, current_dict, past_dict):
self.current_dict, self.past_dict = current_dict, past_dict self.current_dict, self.past_dict = current_dict, past_dict
self.set_current = set(current_dict) self.set_current = set(current_dict)
@ -32,62 +38,82 @@ class DictDiffer(object):
return self.set_past - self.intersect return self.set_past - self.intersect
def changed(self): def changed(self):
return set(o for o in self.intersect return {o for o in self.intersect if self.past_dict[o] != self.current_dict[o]}
if self.past_dict[o] != self.current_dict[o])
def unchanged(self): def unchanged(self):
return set(o for o in self.intersect return {o for o in self.intersect if self.past_dict[o] == self.current_dict[o]}
if self.past_dict[o] == self.current_dict[o])
class AuditlogRule(models.Model): class AuditlogRule(models.Model):
_name = 'auditlog.rule' _name = "auditlog.rule"
_description = "Auditlog - Rule" _description = "Auditlog - Rule"
name = fields.Char( name = fields.Char(required=True, states={"subscribed": [("readonly", True)]})
required=True, states={'subscribed': [('readonly', True)]})
model_id = fields.Many2one( model_id = fields.Many2one(
'ir.model', "Model", required=True, "ir.model",
"Model",
required=True,
help="Select model for which you want to generate log.", help="Select model for which you want to generate log.",
states={'subscribed': [('readonly', True)]}) states={"subscribed": [("readonly", True)]},
)
user_ids = fields.Many2many( user_ids = fields.Many2many(
'res.users', "res.users",
'audittail_rules_users', "audittail_rules_users",
'user_id', 'rule_id', "user_id",
"rule_id",
string="Users", string="Users",
help="if User is not added then it will applicable for all users", help="if User is not added then it will applicable for all users",
states={'subscribed': [('readonly', True)]}) states={"subscribed": [("readonly", True)]},
)
log_read = fields.Boolean( log_read = fields.Boolean(
"Log Reads", "Log Reads",
help=("Select this if you want to keep track of read/open on any " help=(
"record of the model of this rule"), "Select this if you want to keep track of read/open on any "
states={'subscribed': [('readonly', True)]}) "record of the model of this rule"
),
states={"subscribed": [("readonly", True)]},
)
log_write = fields.Boolean( log_write = fields.Boolean(
"Log Writes", default=True, "Log Writes",
help=("Select this if you want to keep track of modification on any " default=True,
"record of the model of this rule"), help=(
states={'subscribed': [('readonly', True)]}) "Select this if you want to keep track of modification on any "
"record of the model of this rule"
),
states={"subscribed": [("readonly", True)]},
)
log_unlink = fields.Boolean( log_unlink = fields.Boolean(
"Log Deletes", default=True, "Log Deletes",
help=("Select this if you want to keep track of deletion on any " default=True,
"record of the model of this rule"), help=(
states={'subscribed': [('readonly', True)]}) "Select this if you want to keep track of deletion on any "
"record of the model of this rule"
),
states={"subscribed": [("readonly", True)]},
)
log_create = fields.Boolean( log_create = fields.Boolean(
"Log Creates", default=True, "Log Creates",
help=("Select this if you want to keep track of creation on any " default=True,
"record of the model of this rule"), help=(
states={'subscribed': [('readonly', True)]}) "Select this if you want to keep track of creation on any "
"record of the model of this rule"
),
states={"subscribed": [("readonly", True)]},
)
log_type = fields.Selection( log_type = fields.Selection(
[('full', "Full log"), [("full", "Full log"), ("fast", "Fast log")],
('fast', "Fast log"), string="Type",
], required=True,
string="Type", required=True, default='full', default="full",
help=("Full log: make a diff between the data before and after " help=(
"the operation (log more info like computed fields which were " "Full log: make a diff between the data before and after "
"updated, but it is slower)\n" "the operation (log more info like computed fields which were "
"Fast log: only log the changes made through the create and " "updated, but it is slower)\n"
"write operations (less information, but it is faster)"), "Fast log: only log the changes made through the create and "
states={'subscribed': [('readonly', True)]}) "write operations (less information, but it is faster)"
),
states={"subscribed": [("readonly", True)]},
)
# log_action = fields.Boolean( # log_action = fields.Boolean(
# "Log Action", # "Log Action",
# help=("Select this if you want to keep track of actions on the " # help=("Select this if you want to keep track of actions on the "
@ -97,27 +123,36 @@ class AuditlogRule(models.Model):
# help=("Select this if you want to keep track of workflow on any " # help=("Select this if you want to keep track of workflow on any "
# "record of the model of this rule")) # "record of the model of this rule"))
state = fields.Selection( state = fields.Selection(
[('draft', "Draft"), ('subscribed', "Subscribed")], [("draft", "Draft"), ("subscribed", "Subscribed")],
required=True, default='draft') required=True,
default="draft",
)
action_id = fields.Many2one( action_id = fields.Many2one(
'ir.actions.act_window', string="Action", "ir.actions.act_window",
states={'subscribed': [('readonly', True)]}) string="Action",
states={"subscribed": [("readonly", True)]},
)
_sql_constraints = [ _sql_constraints = [
('model_uniq', 'unique(model_id)', (
("There is already a rule defined on this model\n" "model_uniq",
"You cannot define another: please edit the existing one.")) "unique(model_id)",
(
"There is already a rule defined on this model\n"
"You cannot define another: please edit the existing one."
),
)
] ]
def _register_hook(self): def _register_hook(self):
"""Get all rules and apply them to log method calls.""" """Get all rules and apply them to log method calls."""
super(AuditlogRule, self)._register_hook() super(AuditlogRule, self)._register_hook()
if not hasattr(self.pool, '_auditlog_field_cache'): if not hasattr(self.pool, "_auditlog_field_cache"):
self.pool._auditlog_field_cache = {} self.pool._auditlog_field_cache = {}
if not hasattr(self.pool, '_auditlog_model_cache'): if not hasattr(self.pool, "_auditlog_model_cache"):
self.pool._auditlog_model_cache = {} self.pool._auditlog_model_cache = {}
if not self: if not self:
self = self.search([('state', '=', 'subscribed')]) self = self.search([("state", "=", "subscribed")])
return self._patch_methods() return self._patch_methods()
def _patch_methods(self): def _patch_methods(self):
@ -125,7 +160,7 @@ class AuditlogRule(models.Model):
updated = False updated = False
model_cache = self.pool._auditlog_model_cache model_cache = self.pool._auditlog_model_cache
for rule in self: for rule in self:
if rule.state != 'subscribed': if rule.state != "subscribed":
continue continue
if not self.pool.get(rule.model_id.model): if not self.pool.get(rule.model_id.model):
# ignore rules for models not loadable currently # ignore rules for models not loadable currently
@ -134,31 +169,27 @@ class AuditlogRule(models.Model):
model_model = self.env[rule.model_id.model] model_model = self.env[rule.model_id.model]
# CRUD # CRUD
# -> create # -> create
check_attr = 'auditlog_ruled_create' check_attr = "auditlog_ruled_create"
if getattr(rule, 'log_create') \ if rule.log_create and not hasattr(model_model, check_attr):
and not hasattr(model_model, check_attr): model_model._patch_method("create", rule._make_create())
model_model._patch_method('create', rule._make_create())
setattr(type(model_model), check_attr, True) setattr(type(model_model), check_attr, True)
updated = True updated = True
# -> read # -> read
check_attr = 'auditlog_ruled_read' check_attr = "auditlog_ruled_read"
if getattr(rule, 'log_read') \ if rule.log_read and not hasattr(model_model, check_attr):
and not hasattr(model_model, check_attr): model_model._patch_method("read", rule._make_read())
model_model._patch_method('read', rule._make_read())
setattr(type(model_model), check_attr, True) setattr(type(model_model), check_attr, True)
updated = True updated = True
# -> write # -> write
check_attr = 'auditlog_ruled_write' check_attr = "auditlog_ruled_write"
if getattr(rule, 'log_write') \ if rule.log_write and not hasattr(model_model, check_attr):
and not hasattr(model_model, check_attr): model_model._patch_method("write", rule._make_write())
model_model._patch_method('write', rule._make_write())
setattr(type(model_model), check_attr, True) setattr(type(model_model), check_attr, True)
updated = True updated = True
# -> unlink # -> unlink
check_attr = 'auditlog_ruled_unlink' check_attr = "auditlog_ruled_unlink"
if getattr(rule, 'log_unlink') \ if rule.log_unlink and not hasattr(model_model, check_attr):
and not hasattr(model_model, check_attr): model_model._patch_method("unlink", rule._make_unlink())
model_model._patch_method('unlink', rule._make_unlink())
setattr(type(model_model), check_attr, True) setattr(type(model_model), check_attr, True)
updated = True updated = True
return updated return updated
@ -168,11 +199,12 @@ class AuditlogRule(models.Model):
updated = False updated = False
for rule in self: for rule in self:
model_model = self.env[rule.model_id.model] model_model = self.env[rule.model_id.model]
for method in ['create', 'read', 'write', 'unlink']: for method in ["create", "read", "write", "unlink"]:
if getattr(rule, 'log_%s' % method) and hasattr( if getattr(rule, "log_%s" % method) and hasattr(
getattr(model_model, method), 'origin'): getattr(model_model, method), "origin"
):
model_model._revert_method(method) model_model._revert_method(method)
delattr(type(model_model), 'auditlog_ruled_%s' % method) delattr(type(model_model), "auditlog_ruled_%s" % method)
updated = True updated = True
if updated: if updated:
modules.registry.Registry(self.env.cr.dbname).signal_changes() modules.registry.Registry(self.env.cr.dbname).signal_changes()
@ -203,61 +235,83 @@ class AuditlogRule(models.Model):
log_type = self.log_type log_type = self.log_type
@api.model @api.model
@api.returns('self', lambda value: value.id) @api.returns("self", lambda value: value.id)
def create_full(self, vals, **kwargs): def create_full(self, vals, **kwargs):
self = self.with_context(auditlog_disabled=True) self = self.with_context(auditlog_disabled=True)
rule_model = self.env['auditlog.rule'] rule_model = self.env["auditlog.rule"]
new_record = create_full.origin(self, vals, **kwargs) new_record = create_full.origin(self, vals, **kwargs)
new_values = dict( new_values = {
(d['id'], d) for d in new_record.sudo() d["id"]: d
.with_context(prefetch_fields=False).read(list(self._fields))) for d in new_record.sudo()
.with_context(prefetch_fields=False)
.read(list(self._fields))
}
rule_model.sudo().create_logs( rule_model.sudo().create_logs(
self.env.uid, self._name, new_record.ids, self.env.uid,
'create', None, new_values, {'log_type': log_type}) self._name,
new_record.ids,
"create",
None,
new_values,
{"log_type": log_type},
)
return new_record return new_record
@api.model @api.model
@api.returns('self', lambda value: value.id) @api.returns("self", lambda value: value.id)
def create_fast(self, vals, **kwargs): def create_fast(self, vals, **kwargs):
self = self.with_context(auditlog_disabled=True) self = self.with_context(auditlog_disabled=True)
rule_model = self.env['auditlog.rule'] rule_model = self.env["auditlog.rule"]
vals2 = dict(vals) vals2 = dict(vals)
new_record = create_fast.origin(self, vals, **kwargs) new_record = create_fast.origin(self, vals, **kwargs)
new_values = {new_record.id: vals2} new_values = {new_record.id: vals2}
rule_model.sudo().create_logs( rule_model.sudo().create_logs(
self.env.uid, self._name, new_record.ids, self.env.uid,
'create', None, new_values, {'log_type': log_type}) self._name,
new_record.ids,
"create",
None,
new_values,
{"log_type": log_type},
)
return new_record return new_record
return create_full if self.log_type == 'full' else create_fast return create_full if self.log_type == "full" else create_fast
def _make_read(self): def _make_read(self):
"""Instanciate a read method that log its calls.""" """Instanciate a read method that log its calls."""
self.ensure_one() self.ensure_one()
log_type = self.log_type log_type = self.log_type
def read(self, fields=None, load='_classic_read', **kwargs): def read(self, fields=None, load="_classic_read", **kwargs):
result = read.origin(self, fields, load, **kwargs) result = read.origin(self, fields, load, **kwargs)
# Sometimes the result is not a list but a dictionary # Sometimes the result is not a list but a dictionary
# Also, we can not modify the current result as it will break calls # Also, we can not modify the current result as it will break calls
result2 = result result2 = result
if not isinstance(result2, list): if not isinstance(result2, list):
result2 = [result] result2 = [result]
read_values = dict((d['id'], d) for d in result2) read_values = {d["id"]: d for d in result2}
# Old API # Old API
# If the call came from auditlog itself, skip logging: # If the call came from auditlog itself, skip logging:
# avoid logs on `read` produced by auditlog during internal # avoid logs on `read` produced by auditlog during internal
# processing: read data of relevant records, 'ir.model', # processing: read data of relevant records, 'ir.model',
# 'ir.model.fields'... (no interest in logging such operations) # 'ir.model.fields'... (no interest in logging such operations)
if self.env.context.get('auditlog_disabled'): if self.env.context.get("auditlog_disabled"):
return result return result
self = self.with_context(auditlog_disabled=True) self = self.with_context(auditlog_disabled=True)
rule_model = self.env['auditlog.rule'] rule_model = self.env["auditlog.rule"]
rule_model.sudo().create_logs( rule_model.sudo().create_logs(
self.env.uid, self._name, self.ids, self.env.uid,
'read', read_values, None, {'log_type': log_type}) self._name,
self.ids,
"read",
read_values,
None,
{"log_type": log_type},
)
return result return result
return read return read
def _make_write(self): def _make_write(self):
@ -267,36 +321,54 @@ class AuditlogRule(models.Model):
def write_full(self, vals, **kwargs): def write_full(self, vals, **kwargs):
self = self.with_context(auditlog_disabled=True) self = self.with_context(auditlog_disabled=True)
rule_model = self.env['auditlog.rule'] rule_model = self.env["auditlog.rule"]
old_values = dict( old_values = {
(d['id'], d) for d in self.sudo() d["id"]: d
.with_context(prefetch_fields=False).read(list(self._fields))) for d in self.sudo()
.with_context(prefetch_fields=False)
.read(list(self._fields))
}
result = write_full.origin(self, vals, **kwargs) result = write_full.origin(self, vals, **kwargs)
new_values = dict( new_values = {
(d['id'], d) for d in self.sudo() d["id"]: d
.with_context(prefetch_fields=False).read(list(self._fields))) for d in self.sudo()
.with_context(prefetch_fields=False)
.read(list(self._fields))
}
rule_model.sudo().create_logs( rule_model.sudo().create_logs(
self.env.uid, self._name, self.ids, self.env.uid,
'write', old_values, new_values, {'log_type': log_type}) self._name,
self.ids,
"write",
old_values,
new_values,
{"log_type": log_type},
)
return result return result
def write_fast(self, vals, **kwargs): def write_fast(self, vals, **kwargs):
self = self.with_context(auditlog_disabled=True) self = self.with_context(auditlog_disabled=True)
rule_model = self.env['auditlog.rule'] rule_model = self.env["auditlog.rule"]
# Log the user input only, no matter if the `vals` is updated # Log the user input only, no matter if the `vals` is updated
# afterwards as it could not represent the real state # afterwards as it could not represent the real state
# of the data in the database # of the data in the database
vals2 = dict(vals) vals2 = dict(vals)
old_vals2 = dict.fromkeys(list(vals2.keys()), False) old_vals2 = dict.fromkeys(list(vals2.keys()), False)
old_values = dict((id_, old_vals2) for id_ in self.ids) old_values = {id_: old_vals2 for id_ in self.ids}
new_values = dict((id_, vals2) for id_ in self.ids) new_values = {id_: vals2 for id_ in self.ids}
result = write_fast.origin(self, vals, **kwargs) result = write_fast.origin(self, vals, **kwargs)
rule_model.sudo().create_logs( rule_model.sudo().create_logs(
self.env.uid, self._name, self.ids, self.env.uid,
'write', old_values, new_values, {'log_type': log_type}) self._name,
self.ids,
"write",
old_values,
new_values,
{"log_type": log_type},
)
return result return result
return write_full if self.log_type == 'full' else write_fast return write_full if self.log_type == "full" else write_fast
def _make_unlink(self): def _make_unlink(self):
"""Instanciate an unlink method that log its calls.""" """Instanciate an unlink method that log its calls."""
@ -305,28 +377,50 @@ class AuditlogRule(models.Model):
def unlink_full(self, **kwargs): def unlink_full(self, **kwargs):
self = self.with_context(auditlog_disabled=True) self = self.with_context(auditlog_disabled=True)
rule_model = self.env['auditlog.rule'] rule_model = self.env["auditlog.rule"]
old_values = dict( old_values = {
(d['id'], d) for d in self.sudo() d["id"]: d
.with_context(prefetch_fields=False).read(list(self._fields))) for d in self.sudo()
.with_context(prefetch_fields=False)
.read(list(self._fields))
}
rule_model.sudo().create_logs( rule_model.sudo().create_logs(
self.env.uid, self._name, self.ids, 'unlink', old_values, None, self.env.uid,
{'log_type': log_type}) self._name,
self.ids,
"unlink",
old_values,
None,
{"log_type": log_type},
)
return unlink_full.origin(self, **kwargs) return unlink_full.origin(self, **kwargs)
def unlink_fast(self, **kwargs): def unlink_fast(self, **kwargs):
self = self.with_context(auditlog_disabled=True) self = self.with_context(auditlog_disabled=True)
rule_model = self.env['auditlog.rule'] rule_model = self.env["auditlog.rule"]
rule_model.sudo().create_logs( rule_model.sudo().create_logs(
self.env.uid, self._name, self.ids, 'unlink', None, None, self.env.uid,
{'log_type': log_type}) self._name,
self.ids,
"unlink",
None,
None,
{"log_type": log_type},
)
return unlink_fast.origin(self, **kwargs) return unlink_fast.origin(self, **kwargs)
return unlink_full if self.log_type == 'full' else unlink_fast return unlink_full if self.log_type == "full" else unlink_fast
def create_logs(self, uid, res_model, res_ids, method, def create_logs(
old_values=None, new_values=None, self,
additional_log_values=None): uid,
res_model,
res_ids,
method,
old_values=None,
new_values=None,
additional_log_values=None,
):
"""Create logs. `old_values` and `new_values` are dictionaries, e.g: """Create logs. `old_values` and `new_values` are dictionaries, e.g:
{RES_ID: {'FIELD': VALUE, ...}} {RES_ID: {'FIELD': VALUE, ...}}
""" """
@ -334,37 +428,37 @@ class AuditlogRule(models.Model):
old_values = EMPTY_DICT old_values = EMPTY_DICT
if new_values is None: if new_values is None:
new_values = EMPTY_DICT new_values = EMPTY_DICT
log_model = self.env['auditlog.log'] log_model = self.env["auditlog.log"]
http_request_model = self.env['auditlog.http.request'] http_request_model = self.env["auditlog.http.request"]
http_session_model = self.env['auditlog.http.session'] http_session_model = self.env["auditlog.http.session"]
for res_id in res_ids: for res_id in res_ids:
model_model = self.env[res_model] model_model = self.env[res_model]
name = model_model.browse(res_id).name_get() name = model_model.browse(res_id).name_get()
res_name = name and name[0] and name[0][1] res_name = name and name[0] and name[0][1]
vals = { vals = {
'name': res_name, "name": res_name,
'model_id': self.pool._auditlog_model_cache[res_model], "model_id": self.pool._auditlog_model_cache[res_model],
'res_id': res_id, "res_id": res_id,
'method': method, "method": method,
'user_id': uid, "user_id": uid,
'http_request_id': http_request_model.current_http_request(), "http_request_id": http_request_model.current_http_request(),
'http_session_id': http_session_model.current_http_session(), "http_session_id": http_session_model.current_http_session(),
} }
vals.update(additional_log_values or {}) vals.update(additional_log_values or {})
log = log_model.create(vals) log = log_model.create(vals)
diff = DictDiffer( diff = DictDiffer(
new_values.get(res_id, EMPTY_DICT), new_values.get(res_id, EMPTY_DICT), old_values.get(res_id, EMPTY_DICT)
old_values.get(res_id, EMPTY_DICT)) )
if method == 'create': if method == "create":
self._create_log_line_on_create(log, diff.added(), new_values) self._create_log_line_on_create(log, diff.added(), new_values)
elif method == 'read': elif method == "read":
self._create_log_line_on_read( self._create_log_line_on_read(
log, log, list(old_values.get(res_id, EMPTY_DICT).keys()), old_values
list(old_values.get(res_id, EMPTY_DICT).keys()), old_values
) )
elif method == 'write': elif method == "write":
self._create_log_line_on_write( self._create_log_line_on_write(
log, diff.changed(), old_values, new_values) log, diff.changed(), old_values, new_values
)
def _get_field(self, model, field_name): def _get_field(self, model, field_name):
cache = self.pool._auditlog_field_cache cache = self.pool._auditlog_field_cache
@ -373,32 +467,31 @@ class AuditlogRule(models.Model):
# - we use 'search()' then 'read()' instead of the 'search_read()' # - we use 'search()' then 'read()' instead of the 'search_read()'
# to take advantage of the 'classic_write' loading # to take advantage of the 'classic_write' loading
# - search the field in the current model and those it inherits # - search the field in the current model and those it inherits
field_model = self.env['ir.model.fields'] field_model = self.env["ir.model.fields"]
all_model_ids = [model.id] all_model_ids = [model.id]
all_model_ids.extend(model.inherited_model_ids.ids) all_model_ids.extend(model.inherited_model_ids.ids)
field = field_model.search( field = field_model.search(
[('model_id', 'in', all_model_ids), ('name', '=', field_name)]) [("model_id", "in", all_model_ids), ("name", "=", field_name)]
)
# The field can be a dummy one, like 'in_group_X' on 'res.users' # The field can be a dummy one, like 'in_group_X' on 'res.users'
# As such we can't log it (field_id is required to create a log) # As such we can't log it (field_id is required to create a log)
if not field: if not field:
cache[model.model][field_name] = False cache[model.model][field_name] = False
else: else:
field_data = field.read(load='_classic_write')[0] field_data = field.read(load="_classic_write")[0]
cache[model.model][field_name] = field_data cache[model.model][field_name] = field_data
return cache[model.model][field_name] return cache[model.model][field_name]
def _create_log_line_on_read( def _create_log_line_on_read(self, log, fields_list, read_values):
self, log, fields_list, read_values):
"""Log field filled on a 'read' operation.""" """Log field filled on a 'read' operation."""
log_line_model = self.env['auditlog.log.line'] log_line_model = self.env["auditlog.log.line"]
for field_name in fields_list: for field_name in fields_list:
if field_name in FIELDS_BLACKLIST: if field_name in FIELDS_BLACKLIST:
continue continue
field = self._get_field(log.model_id, field_name) field = self._get_field(log.model_id, field_name)
# not all fields have an ir.models.field entry (ie. related fields) # not all fields have an ir.models.field entry (ie. related fields)
if field: if field:
log_vals = self._prepare_log_line_vals_on_read( log_vals = self._prepare_log_line_vals_on_read(log, field, read_values)
log, field, read_values)
log_line_model.create(log_vals) log_line_model.create(log_vals)
def _prepare_log_line_vals_on_read(self, log, field, read_values): def _prepare_log_line_vals_on_read(self, log, field, read_values):
@ -406,23 +499,23 @@ class AuditlogRule(models.Model):
'read' operation. 'read' operation.
""" """
vals = { vals = {
'field_id': field['id'], "field_id": field["id"],
'log_id': log.id, "log_id": log.id,
'old_value': read_values[log.res_id][field['name']], "old_value": read_values[log.res_id][field["name"]],
'old_value_text': read_values[log.res_id][field['name']], "old_value_text": read_values[log.res_id][field["name"]],
'new_value': False, "new_value": False,
'new_value_text': False, "new_value_text": False,
} }
if field['relation'] and '2many' in field['ttype']: if field["relation"] and "2many" in field["ttype"]:
old_value_text = self.env[field['relation']].browse( old_value_text = (
vals['old_value']).name_get() self.env[field["relation"]].browse(vals["old_value"]).name_get()
vals['old_value_text'] = old_value_text )
vals["old_value_text"] = old_value_text
return vals return vals
def _create_log_line_on_write( def _create_log_line_on_write(self, log, fields_list, old_values, new_values):
self, log, fields_list, old_values, new_values):
"""Log field updated on a 'write' operation.""" """Log field updated on a 'write' operation."""
log_line_model = self.env['auditlog.log.line'] log_line_model = self.env["auditlog.log.line"]
for field_name in fields_list: for field_name in fields_list:
if field_name in FIELDS_BLACKLIST: if field_name in FIELDS_BLACKLIST:
continue continue
@ -430,55 +523,55 @@ class AuditlogRule(models.Model):
# not all fields have an ir.models.field entry (ie. related fields) # not all fields have an ir.models.field entry (ie. related fields)
if field: if field:
log_vals = self._prepare_log_line_vals_on_write( log_vals = self._prepare_log_line_vals_on_write(
log, field, old_values, new_values) log, field, old_values, new_values
)
log_line_model.create(log_vals) log_line_model.create(log_vals)
def _prepare_log_line_vals_on_write( def _prepare_log_line_vals_on_write(self, log, field, old_values, new_values):
self, log, field, old_values, new_values):
"""Prepare the dictionary of values used to create a log line on a """Prepare the dictionary of values used to create a log line on a
'write' operation. 'write' operation.
""" """
vals = { vals = {
'field_id': field['id'], "field_id": field["id"],
'log_id': log.id, "log_id": log.id,
'old_value': old_values[log.res_id][field['name']], "old_value": old_values[log.res_id][field["name"]],
'old_value_text': old_values[log.res_id][field['name']], "old_value_text": old_values[log.res_id][field["name"]],
'new_value': new_values[log.res_id][field['name']], "new_value": new_values[log.res_id][field["name"]],
'new_value_text': new_values[log.res_id][field['name']], "new_value_text": new_values[log.res_id][field["name"]],
} }
# for *2many fields, log the name_get # for *2many fields, log the name_get
if log.log_type == 'full' and field['relation'] \ if log.log_type == "full" and field["relation"] and "2many" in field["ttype"]:
and '2many' in field['ttype']:
# Filter IDs to prevent a 'name_get()' call on deleted resources # Filter IDs to prevent a 'name_get()' call on deleted resources
existing_ids = self.env[field['relation']]._search( existing_ids = self.env[field["relation"]]._search(
[('id', 'in', vals['old_value'])]) [("id", "in", vals["old_value"])]
)
old_value_text = [] old_value_text = []
if existing_ids: if existing_ids:
existing_values = self.env[field['relation']].browse( existing_values = (
existing_ids).name_get() self.env[field["relation"]].browse(existing_ids).name_get()
)
old_value_text.extend(existing_values) old_value_text.extend(existing_values)
# Deleted resources will have a 'DELETED' text representation # Deleted resources will have a 'DELETED' text representation
deleted_ids = set(vals['old_value']) - set(existing_ids) deleted_ids = set(vals["old_value"]) - set(existing_ids)
for deleted_id in deleted_ids: for deleted_id in deleted_ids:
old_value_text.append((deleted_id, 'DELETED')) old_value_text.append((deleted_id, "DELETED"))
vals['old_value_text'] = old_value_text vals["old_value_text"] = old_value_text
new_value_text = self.env[field['relation']].browse( new_value_text = (
vals['new_value']).name_get() self.env[field["relation"]].browse(vals["new_value"]).name_get()
vals['new_value_text'] = new_value_text )
vals["new_value_text"] = new_value_text
return vals return vals
def _create_log_line_on_create( def _create_log_line_on_create(self, log, fields_list, new_values):
self, log, fields_list, new_values):
"""Log field filled on a 'create' operation.""" """Log field filled on a 'create' operation."""
log_line_model = self.env['auditlog.log.line'] log_line_model = self.env["auditlog.log.line"]
for field_name in fields_list: for field_name in fields_list:
if field_name in FIELDS_BLACKLIST: if field_name in FIELDS_BLACKLIST:
continue continue
field = self._get_field(log.model_id, field_name) field = self._get_field(log.model_id, field_name)
# not all fields have an ir.models.field entry (ie. related fields) # not all fields have an ir.models.field entry (ie. related fields)
if field: if field:
log_vals = self._prepare_log_line_vals_on_create( log_vals = self._prepare_log_line_vals_on_create(log, field, new_values)
log, field, new_values)
log_line_model.create(log_vals) log_line_model.create(log_vals)
def _prepare_log_line_vals_on_create(self, log, field, new_values): def _prepare_log_line_vals_on_create(self, log, field, new_values):
@ -486,37 +579,38 @@ class AuditlogRule(models.Model):
'create' operation. 'create' operation.
""" """
vals = { vals = {
'field_id': field['id'], "field_id": field["id"],
'log_id': log.id, "log_id": log.id,
'old_value': False, "old_value": False,
'old_value_text': False, "old_value_text": False,
'new_value': new_values[log.res_id][field['name']], "new_value": new_values[log.res_id][field["name"]],
'new_value_text': new_values[log.res_id][field['name']], "new_value_text": new_values[log.res_id][field["name"]],
} }
if log.log_type == 'full' and field['relation'] \ if log.log_type == "full" and field["relation"] and "2many" in field["ttype"]:
and '2many' in field['ttype']: new_value_text = (
new_value_text = self.env[field['relation']].browse( self.env[field["relation"]].browse(vals["new_value"]).name_get()
vals['new_value']).name_get() )
vals['new_value_text'] = new_value_text vals["new_value_text"] = new_value_text
return vals return vals
def subscribe(self): def subscribe(self):
"""Subscribe Rule for auditing changes on model and apply shortcut """Subscribe Rule for auditing changes on model and apply shortcut
to view logs on that model. to view logs on that model.
""" """
act_window_model = self.env['ir.actions.act_window'] act_window_model = self.env["ir.actions.act_window"]
for rule in self: for rule in self:
# Create a shortcut to view logs # Create a shortcut to view logs
domain = "[('model_id', '=', %s), ('res_id', '=', active_id)]" % ( domain = "[('model_id', '=', %s), ('res_id', '=', active_id)]" % (
rule.model_id.id) rule.model_id.id
)
vals = { vals = {
'name': _("View logs"), "name": _("View logs"),
'res_model': 'auditlog.log', "res_model": "auditlog.log",
'binding_model_id': rule.model_id.id, "binding_model_id": rule.model_id.id,
'domain': domain, "domain": domain,
} }
act_window = act_window_model.sudo().create(vals) act_window = act_window_model.sudo().create(vals)
rule.write({'state': 'subscribed', 'action_id': act_window.id}) rule.write({"state": "subscribed", "action_id": act_window.id})
return True return True
def unsubscribe(self): def unsubscribe(self):
@ -528,5 +622,5 @@ class AuditlogRule(models.Model):
act_window = rule.action_id act_window = rule.action_id
if act_window: if act_window:
act_window.unlink() act_window.unlink()
self.write({'state': 'draft'}) self.write({"state": "draft"})
return True return True

View File

@ -4,48 +4,59 @@ from odoo.tests.common import TransactionCase
class AuditlogCommon(object): class AuditlogCommon(object):
def test_LogCreation(self): def test_LogCreation(self):
"""First test, caching some data.""" """First test, caching some data."""
self.groups_rule.subscribe() self.groups_rule.subscribe()
auditlog_log = self.env['auditlog.log'] auditlog_log = self.env["auditlog.log"]
group = self.env['res.groups'].create({ group = self.env["res.groups"].create({"name": "testgroup1"})
'name': 'testgroup1', self.assertTrue(
}) auditlog_log.search(
self.assertTrue(auditlog_log.search([ [
('model_id', '=', self.groups_model_id), ("model_id", "=", self.groups_model_id),
('method', '=', 'create'), ("method", "=", "create"),
('res_id', '=', group.id), ("res_id", "=", group.id),
]).ensure_one()) ]
group.write({'name': 'Testgroup1'}) ).ensure_one()
self.assertTrue(auditlog_log.search([ )
('model_id', '=', self.groups_model_id), group.write({"name": "Testgroup1"})
('method', '=', 'write'), self.assertTrue(
('res_id', '=', group.id), auditlog_log.search(
]).ensure_one()) [
("model_id", "=", self.groups_model_id),
("method", "=", "write"),
("res_id", "=", group.id),
]
).ensure_one()
)
group.unlink() group.unlink()
self.assertTrue(auditlog_log.search([ self.assertTrue(
('model_id', '=', self.groups_model_id), auditlog_log.search(
('method', '=', 'unlink'), [
('res_id', '=', group.id), ("model_id", "=", self.groups_model_id),
]).ensure_one()) ("method", "=", "unlink"),
("res_id", "=", group.id),
]
).ensure_one()
)
def test_LogCreation2(self): def test_LogCreation2(self):
"""Second test, using cached data of the first one.""" """Second test, using cached data of the first one."""
self.groups_rule.subscribe() self.groups_rule.subscribe()
auditlog_log = self.env['auditlog.log'] auditlog_log = self.env["auditlog.log"]
testgroup2 = self.env['res.groups'].create({ testgroup2 = self.env["res.groups"].create({"name": "testgroup2"})
'name': 'testgroup2', self.assertTrue(
}) auditlog_log.search(
self.assertTrue(auditlog_log.search([ [
('model_id', '=', self.groups_model_id), ("model_id", "=", self.groups_model_id),
('method', '=', 'create'), ("method", "=", "create"),
('res_id', '=', testgroup2.id), ("res_id", "=", testgroup2.id),
]).ensure_one()) ]
).ensure_one()
)
def test_LogCreation3(self): def test_LogCreation3(self):
"""Third test, two groups, the latter being the parent of the former. """Third test, two groups, the latter being the parent of the former.
@ -55,46 +66,56 @@ class AuditlogCommon(object):
""" """
self.groups_rule.subscribe() self.groups_rule.subscribe()
auditlog_log = self.env['auditlog.log'] auditlog_log = self.env["auditlog.log"]
testgroup3 = testgroup3 = self.env['res.groups'].create({ testgroup3 = testgroup3 = self.env["res.groups"].create({"name": "testgroup3"})
'name': 'testgroup3', testgroup4 = self.env["res.groups"].create(
}) {"name": "testgroup4", "implied_ids": [(4, testgroup3.id)]}
testgroup4 = self.env['res.groups'].create({ )
'name': 'testgroup4', testgroup4.write({"implied_ids": [(2, testgroup3.id)]})
'implied_ids': [(4, testgroup3.id)], self.assertTrue(
}) auditlog_log.search(
testgroup4.write({'implied_ids': [(2, testgroup3.id)]}) [
self.assertTrue(auditlog_log.search([ ("model_id", "=", self.groups_model_id),
('model_id', '=', self.groups_model_id), ("method", "=", "create"),
('method', '=', 'create'), ("res_id", "=", testgroup3.id),
('res_id', '=', testgroup3.id), ]
]).ensure_one()) ).ensure_one()
self.assertTrue(auditlog_log.search([ )
('model_id', '=', self.groups_model_id), self.assertTrue(
('method', '=', 'create'), auditlog_log.search(
('res_id', '=', testgroup4.id), [
]).ensure_one()) ("model_id", "=", self.groups_model_id),
self.assertTrue(auditlog_log.search([ ("method", "=", "create"),
('model_id', '=', self.groups_model_id), ("res_id", "=", testgroup4.id),
('method', '=', 'write'), ]
('res_id', '=', testgroup4.id), ).ensure_one()
]).ensure_one()) )
self.assertTrue(
auditlog_log.search(
[
("model_id", "=", self.groups_model_id),
("method", "=", "write"),
("res_id", "=", testgroup4.id),
]
).ensure_one()
)
class TestAuditlogFull(TransactionCase, AuditlogCommon): class TestAuditlogFull(TransactionCase, AuditlogCommon):
def setUp(self): def setUp(self):
super(TestAuditlogFull, self).setUp() super(TestAuditlogFull, self).setUp()
self.groups_model_id = self.env.ref('base.model_res_groups').id self.groups_model_id = self.env.ref("base.model_res_groups").id
self.groups_rule = self.env['auditlog.rule'].create({ self.groups_rule = self.env["auditlog.rule"].create(
'name': 'testrule for groups', {
'model_id': self.groups_model_id, "name": "testrule for groups",
'log_read': True, "model_id": self.groups_model_id,
'log_create': True, "log_read": True,
'log_write': True, "log_create": True,
'log_unlink': True, "log_write": True,
'log_type': 'full', "log_unlink": True,
}) "log_type": "full",
}
)
def tearDown(self): def tearDown(self):
self.groups_rule.unlink() self.groups_rule.unlink()
@ -102,19 +123,20 @@ class TestAuditlogFull(TransactionCase, AuditlogCommon):
class TestAuditlogFast(TransactionCase, AuditlogCommon): class TestAuditlogFast(TransactionCase, AuditlogCommon):
def setUp(self): def setUp(self):
super(TestAuditlogFast, self).setUp() super(TestAuditlogFast, self).setUp()
self.groups_model_id = self.env.ref('base.model_res_groups').id self.groups_model_id = self.env.ref("base.model_res_groups").id
self.groups_rule = self.env['auditlog.rule'].create({ self.groups_rule = self.env["auditlog.rule"].create(
'name': 'testrule for groups', {
'model_id': self.groups_model_id, "name": "testrule for groups",
'log_read': True, "model_id": self.groups_model_id,
'log_create': True, "log_read": True,
'log_write': True, "log_create": True,
'log_unlink': True, "log_write": True,
'log_type': 'fast', "log_unlink": True,
}) "log_type": "fast",
}
)
def tearDown(self): def tearDown(self):
self.groups_rule.unlink() self.groups_rule.unlink()

View File

@ -6,42 +6,39 @@ from odoo.tests.common import TransactionCase
class TestAuditlogAutovacuum(TransactionCase): class TestAuditlogAutovacuum(TransactionCase):
def setUp(self): def setUp(self):
super(TestAuditlogAutovacuum, self).setUp() super(TestAuditlogAutovacuum, self).setUp()
self.groups_model_id = self.env.ref('base.model_res_groups').id self.groups_model_id = self.env.ref("base.model_res_groups").id
self.groups_rule = self.env['auditlog.rule'].create({ self.groups_rule = self.env["auditlog.rule"].create(
'name': 'testrule for groups', {
'model_id': self.groups_model_id, "name": "testrule for groups",
'log_read': True, "model_id": self.groups_model_id,
'log_create': True, "log_read": True,
'log_write': True, "log_create": True,
'log_unlink': True, "log_write": True,
'state': 'subscribed', "log_unlink": True,
'log_type': 'full', "state": "subscribed",
}) "log_type": "full",
}
)
def tearDown(self): def tearDown(self):
self.groups_rule.unlink() self.groups_rule.unlink()
super(TestAuditlogAutovacuum, self).tearDown() super(TestAuditlogAutovacuum, self).tearDown()
def test_autovacuum(self): def test_autovacuum(self):
log_model = self.env['auditlog.log'] log_model = self.env["auditlog.log"]
autovacuum_model = self.env['auditlog.autovacuum'] autovacuum_model = self.env["auditlog.autovacuum"]
group = self.env['res.groups'].create({ group = self.env["res.groups"].create({"name": "testgroup1"})
'name': 'testgroup1', nb_logs = log_model.search_count(
}) [("model_id", "=", self.groups_model_id), ("res_id", "=", group.id)]
nb_logs = log_model.search_count([ )
('model_id', '=', self.groups_model_id),
('res_id', '=', group.id),
])
self.assertGreater(nb_logs, 0) self.assertGreater(nb_logs, 0)
# Milliseconds are ignored by autovacuum, waiting 1s ensure that # Milliseconds are ignored by autovacuum, waiting 1s ensure that
# the logs generated will be processed by the vacuum # the logs generated will be processed by the vacuum
time.sleep(1) time.sleep(1)
autovacuum_model.autovacuum(days=0) autovacuum_model.autovacuum(days=0)
nb_logs = log_model.search_count([ nb_logs = log_model.search_count(
('model_id', '=', self.groups_model_id), [("model_id", "=", self.groups_model_id), ("res_id", "=", group.id)]
('res_id', '=', group.id), )
])
self.assertEqual(nb_logs, 0) self.assertEqual(nb_logs, 0)

View File

@ -1,94 +1,118 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8" ?>
<odoo> <odoo>
<menuitem id="menu_audit" name="Audit" <menuitem
parent="base.menu_custom" sequence="50" id="menu_audit"
groups="base.group_system"/> name="Audit"
parent="base.menu_custom"
sequence="50"
groups="base.group_system"
/>
<!-- auditlog.rule --> <!-- auditlog.rule -->
<record model="ir.ui.view" id="view_auditlog_rule_form"> <record model="ir.ui.view" id="view_auditlog_rule_form">
<field name="name">auditlog.rule.form</field> <field name="name">auditlog.rule.form</field>
<field name="model">auditlog.rule</field> <field name="model">auditlog.rule</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Rule"> <form string="Rule">
<header> <header>
<button string="Subscribe" name="subscribe" <button
type="object" states="draft" class="oe_highlight"/> string="Subscribe"
<button string="Unsubscribe" name="unsubscribe" name="subscribe"
type="object" states="subscribed"/> type="object"
<field name="state" widget="statusbar"/> states="draft"
class="oe_highlight"
/>
<button
string="Unsubscribe"
name="unsubscribe"
type="object"
states="subscribed"
/>
<field name="state" widget="statusbar" />
</header> </header>
<sheet> <sheet>
<group string="Rule"> <group string="Rule">
<group colspan="1"> <group colspan="1">
<field name="name" required="1"/> <field name="name" required="1" />
<field name="model_id"/> <field name="model_id" />
<field name="log_type"/> <field name="log_type" />
<field name="action_id" readonly="1" groups="base.group_no_one"/> <field
name="action_id"
readonly="1"
groups="base.group_no_one"
/>
</group> </group>
<group colspan="1"> <group colspan="1">
<field name="log_read"/> <field name="log_read" />
<field name="log_write"/> <field name="log_write" />
<field name="log_unlink"/> <field name="log_unlink" />
<field name="log_create"/> <field name="log_create" />
</group> </group>
</group> </group>
</sheet> </sheet>
</form> </form>
</field> </field>
</record> </record>
<record model="ir.ui.view" id="view_auditlog_rule_tree"> <record model="ir.ui.view" id="view_auditlog_rule_tree">
<field name="name">auditlog.rule.tree</field> <field name="name">auditlog.rule.tree</field>
<field name="model">auditlog.rule</field> <field name="model">auditlog.rule</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree decoration-info="state == 'draft'" decoration-bf="state == 'subscribed'"> <tree
<field name="name"/> decoration-info="state == 'draft'"
<field name="model_id"/> decoration-bf="state == 'subscribed'"
<field name="log_type"/> >
<field name="log_read"/> <field name="name" />
<field name="log_write"/> <field name="model_id" />
<field name="log_unlink"/> <field name="log_type" />
<field name="log_create"/> <field name="log_read" />
<field name="state"/> <field name="log_write" />
<field name="log_unlink" />
<field name="log_create" />
<field name="state" />
</tree> </tree>
</field> </field>
</record> </record>
<record id="view_auditlog_rule_search" model="ir.ui.view"> <record id="view_auditlog_rule_search" model="ir.ui.view">
<field name="name">auditlog.rule.search</field> <field name="name">auditlog.rule.search</field>
<field name="model">auditlog.rule</field> <field name="model">auditlog.rule</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<search string="Rules"> <search string="Rules">
<field name="name"/> <field name="name" />
<filter name="state_draft" <filter
domain="[('state','=','draft')]" string="Draft"/> name="state_draft"
<filter name="state_subscribed" domain="[('state','=','draft')]"
domain="[('state','=','subscribed')]" string="Subscribed"/> string="Draft"
<field name="model_id"/> />
<filter
name="state_subscribed"
domain="[('state','=','subscribed')]"
string="Subscribed"
/>
<field name="model_id" />
<group expand="0" string="Group By..."> <group expand="0" string="Group By...">
<filter name="group_by_state" string="State" <filter
domain="[]" context="{'group_by':'state'}"/> name="group_by_state"
string="State"
domain="[]"
context="{'group_by':'state'}"
/>
</group> </group>
</search> </search>
</field> </field>
</record> </record>
<record model="ir.actions.act_window" id="action_auditlog_rule_tree"> <record model="ir.actions.act_window" id="action_auditlog_rule_tree">
<field name="name">Rules</field> <field name="name">Rules</field>
<field name="res_model">auditlog.rule</field> <field name="res_model">auditlog.rule</field>
<field name="type">ir.actions.act_window</field> <field name="type">ir.actions.act_window</field>
<field name="view_mode">tree,form</field> <field name="view_mode">tree,form</field>
<field name="context">{}</field> <field name="context">{}</field>
<field name="search_view_id" ref="view_auditlog_rule_search"/> <field name="search_view_id" ref="view_auditlog_rule_search" />
</record> </record>
<menuitem
<menuitem id="menu_action_auditlog_rule_tree" parent="menu_audit" action="action_auditlog_rule_tree"/> id="menu_action_auditlog_rule_tree"
parent="menu_audit"
action="action_auditlog_rule_tree"
/>
<!-- auditlog.log --> <!-- auditlog.log -->
<record model="ir.ui.view" id="view_auditlog_log_form"> <record model="ir.ui.view" id="view_auditlog_log_form">
<field name="name">auditlog.log.form</field> <field name="name">auditlog.log.form</field>
<field name="model">auditlog.log</field> <field name="model">auditlog.log</field>
@ -97,41 +121,41 @@
<sheet> <sheet>
<group string="Log"> <group string="Log">
<group colspan="1"> <group colspan="1">
<field name="create_date" readonly="1"/> <field name="create_date" readonly="1" />
<field name="user_id" readonly="1"/> <field name="user_id" readonly="1" />
<field name="method" readonly="1"/> <field name="method" readonly="1" />
<field name="log_type" readonly="1"/> <field name="log_type" readonly="1" />
</group> </group>
<group colspan="1"> <group colspan="1">
<field name="model_id" readonly="1"/> <field name="model_id" readonly="1" />
<field name="res_id" readonly="1"/> <field name="res_id" readonly="1" />
<field name="name" readonly="1"/> <field name="name" readonly="1" />
</group> </group>
</group> </group>
<group string="HTTP Context"> <group string="HTTP Context">
<field name="http_session_id"/> <field name="http_session_id" />
<field name="http_request_id"/> <field name="http_request_id" />
</group> </group>
<group string="Fields updated"> <group string="Fields updated">
<field name="line_ids" readonly="1" nolabel="1"> <field name="line_ids" readonly="1" nolabel="1">
<form string="Log - Field updated"> <form string="Log - Field updated">
<group> <group>
<field name="field_id" readonly="1"/> <field name="field_id" readonly="1" />
</group> </group>
<group string="Values" col="4"> <group string="Values" col="4">
<field name="old_value" readonly="1"/> <field name="old_value" readonly="1" />
<field name="new_value" readonly="1"/> <field name="new_value" readonly="1" />
<field name="old_value_text" readonly="1"/> <field name="old_value_text" readonly="1" />
<field name="new_value_text" readonly="1"/> <field name="new_value_text" readonly="1" />
</group> </group>
</form> </form>
<tree> <tree>
<field name="field_description"/> <field name="field_description" />
<field name="field_name"/> <field name="field_name" />
<!--<field name="old_value"/>--> <!--<field name="old_value"/>-->
<field name="old_value_text"/> <field name="old_value_text" />
<!--<field name="new_value"/>--> <!--<field name="new_value"/>-->
<field name="new_value_text"/> <field name="new_value_text" />
</tree> </tree>
</field> </field>
</group> </group>
@ -139,61 +163,79 @@
</form> </form>
</field> </field>
</record> </record>
<record model="ir.ui.view" id="view_auditlog_log_tree"> <record model="ir.ui.view" id="view_auditlog_log_tree">
<field name="name">auditlog.log.tree</field> <field name="name">auditlog.log.tree</field>
<field name="model">auditlog.log</field> <field name="model">auditlog.log</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree create="false"> <tree create="false">
<field name="create_date"/> <field name="create_date" />
<field name="name"/> <field name="name" />
<field name="model_id"/> <field name="model_id" />
<field name="res_id"/> <field name="res_id" />
<field name="method"/> <field name="method" />
<field name="user_id"/> <field name="user_id" />
</tree> </tree>
</field> </field>
</record> </record>
<record id="view_auditlog_log_search" model="ir.ui.view">
<record id="view_auditlog_log_search" model="ir.ui.view">
<field name="name">auditlog.log.search</field> <field name="name">auditlog.log.search</field>
<field name="model">auditlog.log</field> <field name="model">auditlog.log</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<search string="Logs"> <search string="Logs">
<field name="name"/> <field name="name" />
<field name="model_id"/> <field name="model_id" />
<field name="res_id"/> <field name="res_id" />
<field name="user_id"/> <field name="user_id" />
<group expand="0" string="Group By..."> <group expand="0" string="Group By...">
<filter name="group_by_user_id" <filter
name="group_by_user_id"
string="User" string="User"
domain="[]" context="{'group_by':'user_id'}"/> domain="[]"
<filter name="group_by_model_id" context="{'group_by':'user_id'}"
/>
<filter
name="group_by_model_id"
string="Model" string="Model"
domain="[]" context="{'group_by':'model_id'}"/> domain="[]"
<filter name="group_by_res_id" context="{'group_by':'model_id'}"
/>
<filter
name="group_by_res_id"
string="Resource ID" string="Resource ID"
domain="[]" context="{'group_by':'res_id'}"/> domain="[]"
<filter name="group_by_create_date" context="{'group_by':'res_id'}"
/>
<filter
name="group_by_create_date"
string="Date" string="Date"
domain="[]" context="{'group_by':'create_date'}"/> domain="[]"
<filter name="group_by_http_session" context="{'group_by':'create_date'}"
/>
<filter
name="group_by_http_session"
string="User session" string="User session"
domain="[]" context="{'group_by':'http_session_id'}"/> domain="[]"
<filter name="group_by_http_request" context="{'group_by':'http_session_id'}"
/>
<filter
name="group_by_http_request"
string="HTTP Request" string="HTTP Request"
domain="[]" context="{'group_by':'http_request_id'}"/> domain="[]"
context="{'group_by':'http_request_id'}"
/>
</group> </group>
</search> </search>
</field> </field>
</record> </record>
<record model="ir.actions.act_window" id="action_auditlog_log_tree"> <record model="ir.actions.act_window" id="action_auditlog_log_tree">
<field name="name">Logs</field> <field name="name">Logs</field>
<field name="res_model">auditlog.log</field> <field name="res_model">auditlog.log</field>
<field name="search_view_id" ref="view_auditlog_log_search"/> <field name="search_view_id" ref="view_auditlog_log_search" />
</record> </record>
<menuitem
<menuitem id="menu_audit_logs" name="Logs" id="menu_audit_logs"
parent="menu_audit" action="action_auditlog_log_tree"/> name="Logs"
parent="menu_audit"
action="action_auditlog_log_tree"
/>
</odoo> </odoo>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8" ?>
<odoo> <odoo>
<record id="view_auditlog_http_request_form" model="ir.ui.view"> <record id="view_auditlog_http_request_form" model="ir.ui.view">
<field name="name">auditlog.http.request.form</field> <field name="name">auditlog.http.request.form</field>
@ -7,71 +7,84 @@
<form string="HTTP Request"> <form string="HTTP Request">
<sheet> <sheet>
<group string="HTTP Request"> <group string="HTTP Request">
<field name="root_url"/> <field name="root_url" />
<field name="name"/> <field name="name" />
<field name="create_date"/> <field name="create_date" />
<field name="user_context"/> <field name="user_context" />
<field name="http_session_id"/> <field name="http_session_id" />
</group> </group>
<group string="Logs"> <group string="Logs">
<field name="log_ids" nolabel="1"/> <field name="log_ids" nolabel="1" />
</group> </group>
</sheet> </sheet>
</form> </form>
</field> </field>
</record> </record>
<record id="view_auditlog_http_request_tree" model="ir.ui.view"> <record id="view_auditlog_http_request_tree" model="ir.ui.view">
<field name="name">auditlog.http.request.tree</field> <field name="name">auditlog.http.request.tree</field>
<field name="model">auditlog.http.request</field> <field name="model">auditlog.http.request</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree> <tree>
<field name="name"/> <field name="name" />
<field name="create_date"/> <field name="create_date" />
<field name="http_session_id"/> <field name="http_session_id" />
</tree> </tree>
</field> </field>
</record> </record>
<record id="view_auditlog_http_request_search" model="ir.ui.view"> <record id="view_auditlog_http_request_search" model="ir.ui.view">
<field name="name">auditlog.http.request.search</field> <field name="name">auditlog.http.request.search</field>
<field name="model">auditlog.http.request</field> <field name="model">auditlog.http.request</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<search string="HTTP Requests"> <search string="HTTP Requests">
<field name="create_date"/> <field name="create_date" />
<field name="root_url"/> <field name="root_url" />
<field name="name"/> <field name="name" />
<field name="user_id"/> <field name="user_id" />
<field name="http_session_id"/> <field name="http_session_id" />
<group expand="0" string="Group By..."> <group expand="0" string="Group By...">
<filter name="group_by_root_url" <filter
name="group_by_root_url"
string="Root URL" string="Root URL"
domain="[]" context="{'group_by':'root_url'}"/> domain="[]"
<filter name="group_by_name" context="{'group_by':'root_url'}"
/>
<filter
name="group_by_name"
string="Path" string="Path"
domain="[]" context="{'group_by':'name'}"/> domain="[]"
<filter name="group_by_create_date" context="{'group_by':'name'}"
/>
<filter
name="group_by_create_date"
string="Created on" string="Created on"
domain="[]" context="{'group_by':'create_date'}"/> domain="[]"
<filter name="group_by_user_id" context="{'group_by':'create_date'}"
/>
<filter
name="group_by_user_id"
string="User" string="User"
domain="[]" context="{'group_by':'user_id'}"/> domain="[]"
<filter name="group_by_http_session_id" context="{'group_by':'user_id'}"
/>
<filter
name="group_by_http_session_id"
string="User session" string="User session"
domain="[]" context="{'group_by':'http_session_id'}"/> domain="[]"
context="{'group_by':'http_session_id'}"
/>
</group> </group>
</search> </search>
</field> </field>
</record> </record>
<record model="ir.actions.act_window" id="action_auditlog_http_request_tree"> <record model="ir.actions.act_window" id="action_auditlog_http_request_tree">
<field name="name">HTTP Requests</field> <field name="name">HTTP Requests</field>
<field name="type">ir.actions.act_window</field> <field name="type">ir.actions.act_window</field>
<field name="res_model">auditlog.http.request</field> <field name="res_model">auditlog.http.request</field>
<field name="view_id" ref="view_auditlog_http_request_tree"/> <field name="view_id" ref="view_auditlog_http_request_tree" />
</record> </record>
<menuitem
<menuitem id="menu_action_auditlog_http_request_tree" id="menu_action_auditlog_http_request_tree"
parent="menu_audit" parent="menu_audit"
action="action_auditlog_http_request_tree"/> action="action_auditlog_http_request_tree"
/>
</odoo> </odoo>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8" ?>
<odoo> <odoo>
<record id="view_auditlog_http_session_form" model="ir.ui.view"> <record id="view_auditlog_http_session_form" model="ir.ui.view">
<field name="name">auditlog.http.session.form</field> <field name="name">auditlog.http.session.form</field>
@ -7,58 +7,62 @@
<form string="User session"> <form string="User session">
<sheet> <sheet>
<group string="User session"> <group string="User session">
<field name="user_id"/> <field name="user_id" />
<field name="create_date"/> <field name="create_date" />
<field name="name"/> <field name="name" />
</group> </group>
<group string="HTTP Requests"> <group string="HTTP Requests">
<field name="http_request_ids" nolabel="1"/> <field name="http_request_ids" nolabel="1" />
</group> </group>
</sheet> </sheet>
</form> </form>
</field> </field>
</record> </record>
<record id="view_auditlog_http_session_tree" model="ir.ui.view"> <record id="view_auditlog_http_session_tree" model="ir.ui.view">
<field name="name">auditlog.http.session.tree</field> <field name="name">auditlog.http.session.tree</field>
<field name="model">auditlog.http.session</field> <field name="model">auditlog.http.session</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree> <tree>
<field name="user_id"/> <field name="user_id" />
<field name="create_date"/> <field name="create_date" />
<field name="name"/> <field name="name" />
</tree> </tree>
</field> </field>
</record> </record>
<record id="view_auditlog_http_session_search" model="ir.ui.view"> <record id="view_auditlog_http_session_search" model="ir.ui.view">
<field name="name">auditlog.http.session.search</field> <field name="name">auditlog.http.session.search</field>
<field name="model">auditlog.http.session</field> <field name="model">auditlog.http.session</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<search string="User sessions"> <search string="User sessions">
<field name="user_id"/> <field name="user_id" />
<field name="name"/> <field name="name" />
<field name="create_date"/> <field name="create_date" />
<group expand="0" string="Group By..."> <group expand="0" string="Group By...">
<filter name="group_by_user_id" <filter
name="group_by_user_id"
string="User" string="User"
domain="[]" context="{'group_by':'user_id'}"/> domain="[]"
<filter name="group_by_create_date" context="{'group_by':'user_id'}"
/>
<filter
name="group_by_create_date"
string="Created on" string="Created on"
domain="[]" context="{'group_by':'create_date'}"/> domain="[]"
context="{'group_by':'create_date'}"
/>
</group> </group>
</search> </search>
</field> </field>
</record> </record>
<record model="ir.actions.act_window" id="action_auditlog_http_session_tree"> <record model="ir.actions.act_window" id="action_auditlog_http_session_tree">
<field name="name">User sessions</field> <field name="name">User sessions</field>
<field name="type">ir.actions.act_window</field> <field name="type">ir.actions.act_window</field>
<field name="res_model">auditlog.http.session</field> <field name="res_model">auditlog.http.session</field>
<field name="view_id" ref="view_auditlog_http_session_tree"/> <field name="view_id" ref="view_auditlog_http_session_tree" />
</record> </record>
<menuitem
<menuitem id="menu_action_auditlog_http_session_tree" id="menu_action_auditlog_http_session_tree"
parent="menu_audit" parent="menu_audit"
action="action_auditlog_http_session_tree"/> action="action_auditlog_http_session_tree"
/>
</odoo> </odoo>