feat(fusion_login_audit): smart button + Login Activity tab on res.users
Adds four x_fc_* fields on res.users: login_audit_ids (One2many), login_audit_count (compute), last_successful_login (compute, stored), last_login_ip (compute, stored). action_fc_view_login_audit returns a window action scoped to the current user. View inheritance adds a smart button to the button box and a "Login Activity" page to the notebook, both gated by base.group_system on the inner XML nodes (NOT on the view record — Odoo 19 forbids that; see CLAUDE.md rule #11). Tests (2 new, 18 total green): test_computed_last_successful_login — uses registry cursor to commit the audit row so the stored compute picks it up across the TransactionCase boundary. test_action_view_login_audit_returns_window_action — smart-button action shape + domain scoping. CLAUDE.md rule #11 added: inherited ir.ui.view records cannot have groups/group_ids on the record; the gate must be on the inner XML nodes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -227,3 +227,42 @@ class TestFusionLoginAuditModel(TransactionCase):
|
||||
self.assertFalse(row.user_id)
|
||||
self.assertEqual(row.failure_reason, 'unknown_user')
|
||||
self.assertEqual(row.result, 'failure')
|
||||
|
||||
def test_computed_last_successful_login(self):
|
||||
"""x_fc_last_successful_login reflects the latest success row."""
|
||||
user = self.env['res.users'].sudo().create({
|
||||
'name': 'Compute Tester',
|
||||
'login': 'compute-tester@example.com',
|
||||
'password': 'compute-tester-pw-1',
|
||||
})
|
||||
# Use registry cursor so the audit row survives the transactional
|
||||
# boundary the way the auth-time path does.
|
||||
with self.env.registry.cursor() as audit_cr:
|
||||
from odoo import api
|
||||
audit_env = api.Environment(audit_cr, self.env.uid, self.env.context)
|
||||
audit_env['fusion.login.audit'].sudo().create({
|
||||
'user_id': user.id,
|
||||
'attempted_login': user.login,
|
||||
'result': 'success',
|
||||
'database': self.env.cr.dbname,
|
||||
'ip_address': '198.51.100.42',
|
||||
})
|
||||
user.invalidate_recordset(['x_fc_last_successful_login',
|
||||
'x_fc_login_audit_count',
|
||||
'x_fc_last_login_ip'])
|
||||
self.assertTrue(user.x_fc_last_successful_login)
|
||||
self.assertGreaterEqual(user.x_fc_login_audit_count, 1)
|
||||
self.assertEqual(user.x_fc_last_login_ip, '198.51.100.42')
|
||||
|
||||
def test_action_view_login_audit_returns_window_action(self):
|
||||
"""The smart-button action returns an act_window scoped to this user."""
|
||||
user = self.env['res.users'].sudo().create({
|
||||
'name': 'Action Tester',
|
||||
'login': 'action-tester@example.com',
|
||||
'password': 'action-tester-pw-1',
|
||||
})
|
||||
action = user.action_fc_view_login_audit()
|
||||
self.assertEqual(action['res_model'], 'fusion.login.audit')
|
||||
self.assertEqual(action['type'], 'ir.actions.act_window')
|
||||
# Domain must filter to this user
|
||||
self.assertIn(('user_id', '=', user.id), action['domain'])
|
||||
|
||||
Reference in New Issue
Block a user