refactor: single perm type for multiple doctypes

problem: if same perm type is defined in two different apps, we'd have to handle the merging of `doc_types` multi-select field
This commit is contained in:
Saqib Ansari 2025-11-14 14:28:20 +05:30
parent c459c7414b
commit 010351cee2
9 changed files with 70 additions and 131 deletions

View file

@ -1,27 +1,26 @@
{
"actions": [],
"autoname": "prompt",
"creation": "2025-07-28 13:12:03.573433",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"module",
"doc_types"
"perm_type",
"doc_type"
],
"fields": [
{
"fieldname": "doc_types",
"fieldtype": "Table MultiSelect",
"label": "Applies To",
"options": "Permission Type DocType",
"fieldname": "perm_type",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Permission Type",
"reqd": 1
},
{
"fieldname": "module",
"fieldname": "doc_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Module",
"options": "Module Def",
"label": "Applies To (DocType)",
"options": "DocType",
"reqd": 1
}
],
@ -29,11 +28,10 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-11-10 12:48:16.863229",
"modified": "2025-11-13 16:17:58.536849",
"modified_by": "Administrator",
"module": "Core",
"name": "Permission Type",
"naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{

View file

@ -20,34 +20,26 @@ class PermissionType(Document):
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.core.doctype.permission_type_doctype.permission_type_doctype import PermissionTypeDocType
from frappe.types import DF
doc_types: DF.TableMultiSelect[PermissionTypeDocType]
module: DF.Link
doc_type: DF.Link
perm_type: DF.Data
# end: auto-generated types
def autoname(self):
self.name = f"{frappe.scrub(self.doc_type)}_{frappe.scrub(self.perm_type)}"
def before_insert(self):
self.name = frappe.scrub(self.name)
self.perm_type = frappe.scrub(self.perm_type)
def validate(self):
from frappe.permissions import std_rights
if self.name in std_rights:
if self.perm_type in std_rights:
frappe.throw(
_("Permission Type '{0}' is reserved. Please choose another name.").format(self.name)
_("Permission Type '{0}' is reserved. Please choose another name.").format(self.perm_type)
)
# remove duplicate doc types
seen = set()
unique_doc_types = []
for dt in self.doc_types:
if dt.doc_type not in seen:
seen.add(dt.doc_type)
unique_doc_types.append(dt)
self.doc_types = unique_doc_types
def can_write(self):
return (
frappe.conf.developer_mode
@ -56,30 +48,51 @@ class PermissionType(Document):
or frappe.flags.in_test
)
def should_export(self):
return (
frappe.conf.developer_mode
and not frappe.flags.in_migrate
and not frappe.flags.in_install
and not frappe.flags.in_test
)
def get_folder_path(self):
app = frappe.get_doctype_app(self.doc_type)
folder = frappe.get_app_source_path(app, app, "permission_types")
return folder
def on_update(self):
if not self.can_write():
frappe.throw(_("Creation of this document is only permitted in developer mode."))
from frappe.modules.export_file import export_to_files
export_to_files(record_list=[["Permission Type", self.name]], record_module=self.module)
for target in CUSTOM_FIELD_TARGET:
self.create_custom_field(target)
if self.should_export():
from frappe.modules.export_file import export_to_files
module = frappe.db.get_value("DocType", self.doc_type, "module")
export_to_files(record_list=[["Permission Type", self.name]], record_module=module)
def before_export(self, export_doc):
del export_doc["idx"]
del export_doc["docstatus"]
for key in list(export_doc.keys()):
if key.startswith("_"):
del export_doc[key]
def create_custom_field(self, target):
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
if not self.custom_field_exists(target):
field = "share_doctype" if target == "DocShare" else "parent"
doc_types = [dt.doc_type for dt in self.doc_types if dt.doc_type]
depends_on = f"eval:{frappe.as_json(doc_types)}.includes(doc.{field})"
depends_on = f"eval:doc.{field} == '{self.doc_type}'"
create_custom_field(
target,
{
"fieldname": self.name,
"label": self.name.replace("_", " ").title(),
"fieldname": self.perm_type,
"label": frappe.unscrub(self.perm_type),
"fieldtype": "Check",
"insert_after": "append",
"depends_on": depends_on,
@ -93,7 +106,9 @@ class PermissionType(Document):
for target in CUSTOM_FIELD_TARGET:
self.delete_custom_field(target)
delete_folder(self.module, "Permission Type", self.name)
if self.should_export():
module = frappe.db.get_value("DocType", self.doc_type, "module")
delete_folder(module, "Permission Type", self.name)
def delete_custom_field(self, target):
if name := self.custom_field_exists(target):
@ -103,7 +118,7 @@ class PermissionType(Document):
return frappe.db.exists(
"Custom Field",
{
"fieldname": self.name,
"fieldname": self.perm_type,
"dt": target,
},
)
@ -111,19 +126,10 @@ class PermissionType(Document):
@site_cache
def get_doctype_ptype_map():
ptypes = frappe.qb.get_query(
"Permission Type",
fields=[
"name",
{"doc_types": ["doc_type"]},
],
order_by="name",
)
ptypes = ptypes.run(as_dict=True)
ptypes = frappe.get_all("Permission Type", fields=["perm_type", "doc_type"], order_by="perm_type")
doctype_ptype_map = defaultdict(list)
for pt in ptypes:
for dt in pt.doc_types:
if pt.name not in doctype_ptype_map[dt.doc_type]:
doctype_ptype_map[dt.doc_type].append(pt.name)
if pt.perm_type not in doctype_ptype_map[pt.doc_type]:
doctype_ptype_map[pt.doc_type].append(pt.perm_type)
return dict(doctype_ptype_map)

View file

@ -52,9 +52,9 @@ class IntegrationTestPermissionType(IntegrationTestCase):
doc = frappe.get_doc(
{
"doctype": "Permission Type",
"name": "read",
"perm_type": "read",
"doc_type": "ToDo",
"module": "Core",
"doc_types": [{"doc_type": "ToDo"}],
}
)
@ -76,9 +76,9 @@ class IntegrationTestPermissionType(IntegrationTestCase):
ptype_doc = frappe.get_doc(
{
"doctype": "Permission Type",
"name": name,
"perm_type": name,
"doc_type": doc_type,
"module": "Core",
"doc_types": [{"doc_type": doc_type}],
}
)
ptype_doc.insert(ignore_if_duplicate=True)
@ -88,9 +88,9 @@ class IntegrationTestPermissionType(IntegrationTestCase):
def _verify_custom_fields_created(self, ptype_doc, doc_type):
"""Verify that custom fields are created for the permission type."""
for target in ["Custom DocPerm", "DocPerm", "DocShare"]:
custom_field = frappe.get_doc("Custom Field", {"dt": target, "fieldname": ptype_doc.name})
custom_field = frappe.get_doc("Custom Field", {"dt": target, "fieldname": ptype_doc.perm_type})
self.assertEqual(custom_field.dt, target)
self.assertEqual(custom_field.fieldname, ptype_doc.name)
self.assertEqual(custom_field.fieldname, ptype_doc.perm_type)
self.assertEqual(custom_field.fieldtype, "Check")
self.assertIn(doc_type, custom_field.depends_on)

View file

@ -1,35 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-11-03 15:51:52.422122",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"doc_type"
],
"fields": [
{
"fieldname": "doc_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "DocType",
"options": "DocType",
"reqd": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-11-03 15:52:16.161580",
"modified_by": "Administrator",
"module": "Core",
"name": "Permission Type DocType",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View file

@ -1,23 +0,0 @@
# Copyright (c) 2025, Frappe Technologies and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class PermissionTypeDocType(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
doc_type: DF.Link
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
# end: auto-generated types
pass

View file

@ -1,16 +0,0 @@
{
"creation": "2025-10-30 19:17:39.833189",
"doc_types": [
{
"doc_type": "User"
}
],
"docstatus": 0,
"doctype": "Permission Type",
"idx": 0,
"modified": "2025-11-03 16:58:56.819971",
"modified_by": "Administrator",
"module": "Core",
"name": "impersonate",
"owner": "Administrator"
}

View file

@ -0,0 +1,10 @@
{
"creation": "2025-11-13 16:34:50.584738",
"doc_type": "User",
"doctype": "Permission Type",
"modified": "2025-11-14 14:21:45.603237",
"modified_by": "Administrator",
"name": "user_impersonate",
"owner": "Administrator",
"perm_type": "impersonate"
}

View file

@ -66,7 +66,6 @@ def sync_for(app_name, force=0, reset_permissions=False):
"role",
"has_role",
"doctype",
"permission_type_doctype",
"permission_type",
]:
files.append(os.path.join(FRAPPE_PATH, "core", "doctype", core_module, f"{core_module}.json"))