feat(fusion_login_audit): nightly retention GC cron
Adds _fc_retention_gc() that deletes rows older than the configured horizon (default 365 days; 0 = keep forever). Registered as a daily ir.cron. Tests verify both the delete path and the "keep forever" short-circuit. Also documents the Odoo 19 gotcha that ir.cron dropped the numbercall field (the legacy "-1 = run forever" pattern now raises ValueError at install time; just omit the field). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@ bursts. Daily retention cron honours a configurable horizon.
|
||||
'security/ir.model.access.csv',
|
||||
'security/security.xml',
|
||||
'data/mail_template_data.xml',
|
||||
'data/ir_cron_data.xml',
|
||||
'views/fusion_login_audit_views.xml',
|
||||
'views/res_users_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
|
||||
16
fusion_login_audit/data/ir_cron_data.xml
Normal file
16
fusion_login_audit/data/ir_cron_data.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="cron_retention_gc" model="ir.cron">
|
||||
<field name="name">Fusion Login Audit: Retention GC</field>
|
||||
<field name="model_id" ref="model_fusion_login_audit"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._fc_retention_gc()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="active" eval="True"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import fields, models
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class FusionLoginAudit(models.Model):
|
||||
@@ -83,3 +83,23 @@ class FusionLoginAudit(models.Model):
|
||||
_user_time_idx = models.Index('(user_id, event_time DESC)')
|
||||
_login_time_idx = models.Index('(attempted_login, event_time DESC)')
|
||||
_geo_state_idx = models.Index('(geo_lookup_state, event_time)')
|
||||
|
||||
@api.model
|
||||
def _fc_retention_gc(self):
|
||||
"""Delete audit rows older than `fusion_login_audit.retention_days`.
|
||||
Called daily by ir.cron. retention_days=0 means keep forever."""
|
||||
from datetime import timedelta
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
try:
|
||||
days = int(ICP.get_param(
|
||||
'fusion_login_audit.retention_days', 365))
|
||||
except (TypeError, ValueError):
|
||||
days = 365
|
||||
if days <= 0:
|
||||
return 0
|
||||
cutoff = fields.Datetime.now() - timedelta(days=days)
|
||||
old = self.sudo().search([('event_time', '<', cutoff)])
|
||||
count = len(old)
|
||||
if old:
|
||||
old.unlink()
|
||||
return count
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import fields
|
||||
from odoo.tests.common import TransactionCase, tagged
|
||||
|
||||
|
||||
@@ -392,3 +393,50 @@ class TestFusionLoginAuditModel(TransactionCase):
|
||||
pass
|
||||
after = Mail.search_count([('subject', 'ilike', 'disabled@example.com')])
|
||||
self.assertEqual(after, before, "Disabled alerts should queue nothing")
|
||||
|
||||
def test_retention_gc_deletes_old_rows(self):
|
||||
"""The GC method deletes rows older than retention_days."""
|
||||
from datetime import timedelta
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
ICP.set_param('fusion_login_audit.retention_days', '30')
|
||||
|
||||
now = fields.Datetime.now()
|
||||
Audit = self.env['fusion.login.audit'].sudo()
|
||||
old = Audit.create({
|
||||
'attempted_login': 'gc-old@example.com',
|
||||
'result': 'success',
|
||||
'event_time': now - timedelta(days=45),
|
||||
})
|
||||
recent = Audit.create({
|
||||
'attempted_login': 'gc-recent@example.com',
|
||||
'result': 'success',
|
||||
'event_time': now - timedelta(days=5),
|
||||
})
|
||||
old_id, recent_id = old.id, recent.id
|
||||
|
||||
Audit._fc_retention_gc()
|
||||
|
||||
self.assertFalse(Audit.browse(old_id).exists(),
|
||||
"Row older than retention_days should be gone")
|
||||
self.assertTrue(Audit.browse(recent_id).exists(),
|
||||
"Row inside retention_days should survive")
|
||||
|
||||
def test_retention_zero_keeps_forever(self):
|
||||
"""retention_days=0 keeps all rows."""
|
||||
from datetime import timedelta
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
ICP.set_param('fusion_login_audit.retention_days', '0')
|
||||
|
||||
now = fields.Datetime.now()
|
||||
Audit = self.env['fusion.login.audit'].sudo()
|
||||
ancient = Audit.create({
|
||||
'attempted_login': 'forever@example.com',
|
||||
'result': 'success',
|
||||
'event_time': now - timedelta(days=3650),
|
||||
})
|
||||
ancient_id = ancient.id
|
||||
|
||||
Audit._fc_retention_gc()
|
||||
|
||||
self.assertTrue(Audit.browse(ancient_id).exists(),
|
||||
"retention_days=0 must keep everything")
|
||||
|
||||
Reference in New Issue
Block a user