fix: apply permlevel restrictions on datafields

This commit is contained in:
Shariq Ansari 2025-01-02 23:13:29 +05:30
parent 7f2ccef7c8
commit 321751ab5a
18 changed files with 184 additions and 99 deletions

View File

@ -565,70 +565,6 @@ def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False):
return fields_meta return fields_meta
@frappe.whitelist()
def get_sidebar_fields(doctype, name):
if not frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": "Side Panel"}):
return []
layout = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": "Side Panel"}).layout
if not layout:
return []
layout = json.loads(layout)
not_allowed_fieldtypes = [
"Tab Break",
"Section Break",
"Column Break",
]
fields = frappe.get_meta(doctype).fields
fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes]
doc = frappe.get_cached_doc(doctype, name)
has_high_permlevel_fields = any(df.permlevel > 0 for df in fields)
if has_high_permlevel_fields:
has_read_access_to_permlevels = doc.get_permlevel_access("read")
has_write_access_to_permlevels = doc.get_permlevel_access("write")
for section in layout:
section["name"] = section.get("name") or section.get("label")
for column in section.get("columns") if section.get("columns") else []:
for field in column.get("fields") if column.get("fields") else []:
field_obj = next((f for f in fields if f.fieldname == field), None)
if field_obj:
if field_obj.permlevel > 0:
field_has_write_access = field_obj.permlevel in has_write_access_to_permlevels
field_has_read_access = field_obj.permlevel in has_read_access_to_permlevels
if not field_has_write_access and field_has_read_access:
field_obj.read_only = 1
if not field_has_read_access and not field_has_write_access:
field_obj.hidden = 1
column["fields"][column.get("fields").index(field)] = get_field_obj(field_obj)
fields_meta = {}
for field in fields:
fields_meta[field.fieldname] = field
return layout
def get_field_obj(field):
field = field.as_dict()
field["placeholder"] = field.get("placeholder") or "Add " + field.label + "..."
if field.fieldtype == "Link":
field["placeholder"] = field.get("placeholder") or "Select " + field.label + "..."
elif field.fieldtype == "Select" and field.options:
field["placeholder"] = field.get("placeholder") or "Select " + field.label + "..."
field["options"] = [{"label": option, "value": option} for option in field.options.split("\n")]
if field.read_only:
field["tooltip"] = "This field is read only and cannot be edited."
return field
def get_assigned_users(doctype, name, default_assigned_to=None): def get_assigned_users(doctype, name, default_assigned_to=None):
assigned_users = frappe.get_all( assigned_users = frappe.get_all(
"ToDo", "ToDo",

View File

@ -46,7 +46,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2025-01-01 20:42:29.862890", "modified": "2025-01-02 22:12:51.663011",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "FCRM", "module": "FCRM",
"name": "CRM Fields Layout", "name": "CRM Fields Layout",
@ -64,6 +64,15 @@
"role": "System Manager", "role": "System Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1
} }
], ],
"sort_field": "creation", "sort_field": "creation",

View File

@ -14,7 +14,7 @@ class CRMFieldsLayout(Document):
@frappe.whitelist() @frappe.whitelist()
def get_fields_layout(doctype: str, type: str): def get_fields_layout(doctype: str, type: str, parent_doctype: str | None = None):
tabs = [] tabs = []
layout = None layout = None
@ -52,11 +52,94 @@ def get_fields_layout(doctype: str, type: str):
field = next((f for f in fields if f.fieldname == field), None) field = next((f for f in fields if f.fieldname == field), None)
if field: if field:
field = field.as_dict() field = field.as_dict()
handle_perm_level_restrictions(field, doctype, parent_doctype)
column["fields"][column.get("fields").index(field["fieldname"])] = field column["fields"][column.get("fields").index(field["fieldname"])] = field
return tabs or [] return tabs or []
@frappe.whitelist()
def get_sidepanel_sections(doctype):
if not frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": "Side Panel"}):
return []
layout = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": "Side Panel"}).layout
if not layout:
return []
layout = json.loads(layout)
not_allowed_fieldtypes = [
"Tab Break",
"Section Break",
"Column Break",
]
fields = frappe.get_meta(doctype).fields
fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes]
for section in layout:
section["name"] = section.get("name") or section.get("label")
for column in section.get("columns") if section.get("columns") else []:
for field in column.get("fields") if column.get("fields") else []:
field_obj = next((f for f in fields if f.fieldname == field), None)
if field_obj:
field_obj = field_obj.as_dict()
handle_perm_level_restrictions(field_obj, doctype)
column["fields"][column.get("fields").index(field)] = get_field_obj(field_obj)
fields_meta = {}
for field in fields:
fields_meta[field.fieldname] = field
return layout
def handle_perm_level_restrictions(field, doctype, parent_doctype=None):
if field.permlevel == 0:
return
field_has_write_access = field.permlevel in get_permlevel_access("write", doctype, parent_doctype)
field_has_read_access = field.permlevel in get_permlevel_access("read", doctype, parent_doctype)
if not field_has_write_access and field_has_read_access:
field.read_only = 1
if not field_has_read_access and not field_has_write_access:
field.hidden = 1
def get_permlevel_access(permission_type="write", doctype=None, parent_doctype=None):
allowed_permlevels = []
roles = frappe.get_roles()
meta = frappe.get_meta(doctype)
if meta.istable and parent_doctype:
meta = frappe.get_meta(parent_doctype)
elif meta.istable and not parent_doctype:
return [1, 0]
for perm in meta.permissions:
if perm.role in roles and perm.get(permission_type) and perm.permlevel not in allowed_permlevels:
allowed_permlevels.append(perm.permlevel)
return allowed_permlevels
def get_field_obj(field):
field["placeholder"] = field.get("placeholder") or "Add " + field.label + "..."
if field.fieldtype == "Link":
field["placeholder"] = field.get("placeholder") or "Select " + field.label + "..."
elif field.fieldtype == "Select" and field.options:
field["placeholder"] = field.get("placeholder") or "Select " + field.label + "..."
field["options"] = [{"label": option, "value": option} for option in field.options.split("\n")]
if field.read_only:
field["tooltip"] = "This field is read only and cannot be edited."
return field
@frappe.whitelist() @frappe.whitelist()
def save_fields_layout(doctype: str, type: str, layout: str): def save_fields_layout(doctype: str, type: str, layout: str):
if frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": type}): if frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": type}):

View File

@ -21,7 +21,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2024-01-19 21:57:02.025918", "modified": "2025-01-02 22:14:28.686821",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "FCRM", "module": "FCRM",
"name": "CRM Industry", "name": "CRM Industry",
@ -51,6 +51,15 @@
"role": "Sales Manager", "role": "Sales Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,

View File

@ -290,7 +290,7 @@
"image_field": "image", "image_field": "image",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2024-09-17 18:36:57.289897", "modified": "2025-01-02 22:14:01.991054",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "FCRM", "module": "FCRM",
"name": "CRM Lead", "name": "CRM Lead",
@ -320,6 +320,15 @@
"role": "Sales Manager", "role": "Sales Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1
} }
], ],
"sender_field": "email", "sender_field": "email",

View File

@ -29,7 +29,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2024-01-19 21:56:04.702254", "modified": "2025-01-02 22:13:30.498404",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "FCRM", "module": "FCRM",
"name": "CRM Lead Source", "name": "CRM Lead Source",
@ -59,6 +59,15 @@
"role": "Sales Manager", "role": "Sales Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,

View File

@ -37,7 +37,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2024-01-19 21:56:16.872924", "modified": "2025-01-02 22:13:43.038656",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "FCRM", "module": "FCRM",
"name": "CRM Lead Status", "name": "CRM Lead Status",
@ -67,6 +67,15 @@
"role": "Sales Manager", "role": "Sales Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1
} }
], ],
"sort_field": "modified", "sort_field": "modified",

View File

@ -170,6 +170,7 @@
:index="index" :index="index"
:data="row" :data="row"
:doctype="doctype" :doctype="doctype"
:parentDoctype="parentDoctype"
/> />
</div> </div>
</template> </template>
@ -198,6 +199,7 @@
v-if="showGridRowFieldsModal" v-if="showGridRowFieldsModal"
v-model="showGridRowFieldsModal" v-model="showGridRowFieldsModal"
:doctype="doctype" :doctype="doctype"
:parentDoctype="parentDoctype"
/> />
<GridFieldsEditorModal <GridFieldsEditorModal
v-if="showGridFieldsEditorModal" v-if="showGridFieldsEditorModal"

View File

@ -36,7 +36,13 @@
:tabs="tabs.data" :tabs="tabs.data"
:doctype="_doctype" :doctype="_doctype"
/> />
<FieldLayout v-else :tabs="tabs.data" :data="{}" :modal="true" :preview="true"/> <FieldLayout
v-else
:tabs="tabs.data"
:data="{}"
:modal="true"
:preview="true"
/>
</div> </div>
</div> </div>
</template> </template>
@ -55,6 +61,10 @@ const props = defineProps({
type: String, type: String,
default: 'CRM Lead', default: 'CRM Lead',
}, },
parentDoctype: {
type: String,
default: '',
},
}) })
const emit = defineEmits(['reload']) const emit = defineEmits(['reload'])
@ -66,12 +76,16 @@ const dirty = ref(false)
const preview = ref(false) const preview = ref(false)
function getParams() { function getParams() {
return { doctype: _doctype.value, type: 'Grid Row' } return {
doctype: _doctype.value,
type: 'Grid Row',
parent_doctype: props.parentDoctype,
}
} }
const tabs = createResource({ const tabs = createResource({
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout', url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
cache: ['GridRowFieldsModal', _doctype.value], cache: ['GridRowFieldsModal', _doctype.value, props.parentDoctype],
params: getParams(), params: getParams(),
onSuccess(data) { onSuccess(data) {
tabs.originalData = JSON.parse(JSON.stringify(data)) tabs.originalData = JSON.parse(JSON.stringify(data))

View File

@ -41,6 +41,7 @@ const props = defineProps({
index: Number, index: Number,
data: Object, data: Object,
doctype: String, doctype: String,
parentDoctype: String,
}) })
const { isManager } = usersStore() const { isManager } = usersStore()
@ -50,8 +51,12 @@ const showGridRowFieldsModal = defineModel('showGridRowFieldsModal')
const tabs = createResource({ const tabs = createResource({
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout', url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
cache: ['Grid Row', props.doctype], cache: ['Grid Row', props.doctype, props.parentDoctype],
params: { doctype: props.doctype, type: 'Grid Row' }, params: {
doctype: props.doctype,
type: 'Grid Row',
parent_doctype: props.parentDoctype,
},
auto: true, auto: true,
}) })

View File

@ -336,9 +336,9 @@ const rows = computed(() => {
}) })
const sections = createResource({ const sections = createResource({
url: 'crm.api.doc.get_sidebar_fields', url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_sidepanel_sections',
cache: ['sidePanelSections', props.contactId], cache: ['sidePanelSections', 'Contact'],
params: { doctype: 'Contact', name: props.contactId }, params: { doctype: 'Contact' },
auto: true, auto: true,
transform: (data) => getParsedSections(data), transform: (data) => getParsedSections(data),
}) })

View File

@ -530,9 +530,9 @@ const tabs = computed(() => {
const { tabIndex } = useActiveTabManager(tabs, 'lastDealTab') const { tabIndex } = useActiveTabManager(tabs, 'lastDealTab')
const sections = createResource({ const sections = createResource({
url: 'crm.api.doc.get_sidebar_fields', url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_sidepanel_sections',
cache: ['sidePanelSections', props.dealId], cache: ['sidePanelSections', 'CRM Deal'],
params: { doctype: 'CRM Deal', name: props.dealId }, params: { doctype: 'CRM Deal' },
auto: true, auto: true,
transform: (data) => getParsedSections(data), transform: (data) => getParsedSections(data),
}) })

View File

@ -531,9 +531,9 @@ function validateFile(file) {
} }
const sections = createResource({ const sections = createResource({
url: 'crm.api.doc.get_sidebar_fields', url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_sidepanel_sections',
cache: ['sidePanelSections', props.leadId], cache: ['sidePanelSections', 'CRM Lead'],
params: { doctype: 'CRM Lead', name: props.leadId }, params: { doctype: 'CRM Lead' },
auto: true, auto: true,
}) })

View File

@ -348,9 +348,9 @@ const rows = computed(() => {
}) })
const fieldsLayout = createResource({ const fieldsLayout = createResource({
url: 'crm.api.doc.get_sidebar_fields', url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_sidepanel_sections',
cache: ['fieldsLayout', props.contactId], cache: ['sidePanelSections', 'Contact'],
params: { doctype: 'Contact', name: props.contactId }, params: { doctype: 'Contact' },
auto: true, auto: true,
transform: (data) => getParsedFields(data), transform: (data) => getParsedFields(data),
}) })

View File

@ -501,9 +501,9 @@ const tabs = computed(() => {
const { tabIndex } = useActiveTabManager(tabs, 'lastDealTab') const { tabIndex } = useActiveTabManager(tabs, 'lastDealTab')
const fieldsLayout = createResource({ const fieldsLayout = createResource({
url: 'crm.api.doc.get_sidebar_fields', url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_sidepanel_sections',
cache: ['fieldsLayout', props.dealId], cache: ['sidePanelSections', 'CRM Deal'],
params: { doctype: 'CRM Deal', name: props.dealId }, params: { doctype: 'CRM Deal' },
auto: true, auto: true,
transform: (data) => getParsedFields(data), transform: (data) => getParsedFields(data),
}) })

View File

@ -421,9 +421,9 @@ watch(tabs, (value) => {
}) })
const fieldsLayout = createResource({ const fieldsLayout = createResource({
url: 'crm.api.doc.get_sidebar_fields', url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_sidepanel_sections',
cache: ['fieldsLayout', props.leadId], cache: ['sidePanelSections', 'CRM Lead'],
params: { doctype: 'CRM Lead', name: props.leadId }, params: { doctype: 'CRM Lead' },
auto: true, auto: true,
}) })

View File

@ -327,9 +327,9 @@ const _organization = ref({})
const _address = ref({}) const _address = ref({})
const fieldsLayout = createResource({ const fieldsLayout = createResource({
url: 'crm.api.doc.get_sidebar_fields', url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_sidepanel_sections',
cache: ['fieldsLayout', props.organizationId], cache: ['sidePanelSections', 'CRM Organization'],
params: { doctype: 'CRM Organization', name: props.organizationId }, params: { doctype: 'CRM Organization' },
auto: true, auto: true,
transform: (data) => getParsedFields(data), transform: (data) => getParsedFields(data),
}) })

View File

@ -336,9 +336,9 @@ const _organization = ref({})
const _address = ref({}) const _address = ref({})
const sections = createResource({ const sections = createResource({
url: 'crm.api.doc.get_sidebar_fields', url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_sidepanel_sections',
cache: ['sidePanelSections', props.organizationId], cache: ['sidePanelSections', 'CRM Organization'],
params: { doctype: 'CRM Organization', name: props.organizationId }, params: { doctype: 'CRM Organization' },
auto: true, auto: true,
transform: (data) => getParsedSections(data), transform: (data) => getParsedSections(data),
}) })