From 852d264698d54f7ddb405ad090e75bfc3f6dcff1 Mon Sep 17 00:00:00 2001 From: Clayton <118192227+claytonlin1110@users.noreply.github.com> Date: Wed, 4 Mar 2026 05:45:11 -0600 Subject: [PATCH] feat: add activity log support (#37696) --- .../permission_log/permission_log.json | 6 +- .../doctype/permission_log/permission_log.py | 37 +++- .../permission_manager/permission_manager.js | 160 ++++++++++++++++++ .../permission_manager/permission_manager.py | 63 ++++++- 4 files changed, 257 insertions(+), 9 deletions(-) diff --git a/frappe/core/doctype/permission_log/permission_log.json b/frappe/core/doctype/permission_log/permission_log.json index 61c2a8b1b5..a46f15feff 100644 --- a/frappe/core/doctype/permission_log/permission_log.json +++ b/frappe/core/doctype/permission_log/permission_log.json @@ -118,7 +118,7 @@ "fieldtype": "Select", "hidden": 1, "label": "Status", - "options": "Updated\nRemoved\nAdded" + "options": "Updated\nRemoved\nAdded\nReset" } ], "index_web_pages_for_search": 1, @@ -150,6 +150,10 @@ { "color": "Green", "title": "Added" + }, + { + "color": "Blue", + "title": "Reset" } ], "title_field": "changed_by" diff --git a/frappe/core/doctype/permission_log/permission_log.py b/frappe/core/doctype/permission_log/permission_log.py index 5f8d5a4e75..3eb4956297 100644 --- a/frappe/core/doctype/permission_log/permission_log.py +++ b/frappe/core/doctype/permission_log/permission_log.py @@ -23,7 +23,7 @@ class PermissionLog(Document): for_document: DF.DynamicLink reference: DF.DynamicLink | None reference_type: DF.Link | None - status: DF.Literal["Updated", "Removed", "Added"] + status: DF.Literal["Updated", "Removed", "Added", "Reset"] # end: auto-generated types @property @@ -34,6 +34,13 @@ class PermissionLog(Document): def make_perm_log(doc, method=None): if not hasattr(doc, "get_permission_log_options"): return + # During reset we insert a single "Reset" log; skip per-Custom-DocPerm "Removed" logs + if ( + method == "after_delete" + and doc.doctype == "Custom DocPerm" + and getattr(frappe.flags, "skip_perm_log_for_doctype", None) == doc.parent + ): + return params = doc.get_permission_log_options(method) or {} if not getattr(doc, "_no_perm_log", False): @@ -46,16 +53,34 @@ def insert_perm_log( for_doctype: str | None = None, for_document: str | None = None, fields: list | tuple | None = None, + custom_changes: dict | None = None, ): + """Log a permission change. When custom_changes is provided (e.g. for reset-to-standard), + it must be {"from": {...}, "to": {...}} and optionally "status"; doc is used for + reference/owner only.""" if frappe.flags.in_install or frappe.flags.in_migrate: # no need to log changes when migrating or installing app/site return - current, previous = get_changes(doc, doc_before_save, fields) - if not previous and not current: - return - - status = "Updated" if doc_before_save else ("Added" if doc.flags.in_insert else "Removed") + if custom_changes is not None: + previous = custom_changes.get("from", {}) + current = custom_changes.get("to", {}) + status = custom_changes.get("status", "Updated") + else: + current, previous = get_changes(doc, doc_before_save, fields) + if not previous and not current: + return + status = "Updated" if doc_before_save else ("Added" if doc.flags.in_insert else "Removed") + # Ensure role (and parent) are always in changes for Custom DocPerm so the UI can show them + if doc.doctype == "Custom DocPerm": + previous["role"] = previous.get("role") or ( + doc_before_save and getattr(doc_before_save, "role", None) + ) + current["role"] = current.get("role") or getattr(doc, "role", None) + previous["parent"] = previous.get("parent") or ( + doc_before_save and getattr(doc_before_save, "parent", None) + ) + current["parent"] = current.get("parent") or getattr(doc, "parent", None) frappe.get_doc( { diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js index 7136de5dec..79a2d11cb0 100644 --- a/frappe/core/page/permission_manager/permission_manager.js +++ b/frappe/core/page/permission_manager/permission_manager.js @@ -72,6 +72,11 @@ frappe.PermissionEngine = class PermissionEngine { this.page.add_inner_button(__("Set User Permissions"), () => { return frappe.set_route("List", "User Permission"); }); + + this.page.add_inner_button(__("View Activity Log"), () => { + this.show_activity_log(); + }); + this.set_from_route(); } @@ -577,4 +582,159 @@ frappe.PermissionEngine = class PermissionEngine { options: ["not in", ["User", "[Select]"]], }); } + + show_activity_log() { + const PERM_FIELDS = [ + "select", + "read", + "write", + "create", + "delete", + "submit", + "cancel", + "amend", + "print", + "email", + "report", + "import", + "export", + "share", + "mask", + ]; + const STATUS_COLOR = { Added: "green", Removed: "red", Updated: "orange", Reset: "blue" }; + + let doctype = this.get_doctype(); + let show_doctype_column = !doctype; + + let title = doctype + ? __("Activity Log for {0}", [__(doctype)]) + : __("Role Permissions Activity Log"); + + let d = new frappe.ui.Dialog({ title, size: "large" }); + let $body = $(d.body); + $body.html(`
| ${__("Modified By")} | +${__("Action")} | +${__("Role")} | + ${header_doctype} +${__("Changes")} | +${__("Timestamp")} | +
|---|