feat: protect attached files (#31855)
* feat: protect attached files * fix: protection does not apply to draft documents * chore: update descriptions * feat: hide delete button when file is protected
This commit is contained in:
parent
fd783f07de
commit
3f5da98b27
6 changed files with 78 additions and 10 deletions
|
|
@ -15,6 +15,7 @@
|
|||
"restrict_to_domain",
|
||||
"read_only",
|
||||
"in_create",
|
||||
"protect_attached_files",
|
||||
"sb1",
|
||||
"naming_rule",
|
||||
"autoname",
|
||||
|
|
@ -683,14 +684,22 @@
|
|||
"options": "Dynamic\nCompressed"
|
||||
},
|
||||
{
|
||||
"default": "50",
|
||||
"depends_on": "istable",
|
||||
"fieldname": "grid_page_length",
|
||||
"fieldtype": "Int",
|
||||
"label": "Grid Page Length",
|
||||
"non_negative": 1
|
||||
"default": "50",
|
||||
"depends_on": "istable",
|
||||
"fieldname": "grid_page_length",
|
||||
"fieldtype": "Int",
|
||||
"label": "Grid Page Length",
|
||||
"non_negative": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Users are only able to delete attached files if the document is either in draft or if the document is canceled and they are also able to delete the document.",
|
||||
"fieldname": "protect_attached_files",
|
||||
"fieldtype": "Check",
|
||||
"label": "Protect Attached Files"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "fa fa-bolt",
|
||||
"idx": 6,
|
||||
"index_web_pages_for_search": 1,
|
||||
|
|
@ -771,7 +780,7 @@
|
|||
"link_fieldname": "reference_doctype"
|
||||
}
|
||||
],
|
||||
"modified": "2025-02-20 19:05:52.119679",
|
||||
"modified": "2025-03-27 18:16:53.286909",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocType",
|
||||
|
|
@ -801,6 +810,7 @@
|
|||
}
|
||||
],
|
||||
"route": "doctype",
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "module",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "creation",
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ class DocType(Document):
|
|||
]
|
||||
nsm_parent_field: DF.Data | None
|
||||
permissions: DF.Table[DocPerm]
|
||||
protect_attached_files: DF.Check
|
||||
queue_in_background: DF.Check
|
||||
quick_entry: DF.Check
|
||||
read_only: DF.Check
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from PIL import Image, ImageFile, ImageOps
|
|||
import frappe
|
||||
from frappe import _
|
||||
from frappe.database.schema import SPECIAL_CHAR_PATTERN
|
||||
from frappe.exceptions import DoesNotExistError
|
||||
from frappe.model.document import Document
|
||||
from frappe.permissions import SYSTEM_USER_ROLE, get_doctypes_with_read
|
||||
from frappe.utils import call_hook_method, cint, get_files_path, get_hook_method, get_url
|
||||
|
|
@ -151,6 +152,7 @@ class File(Document):
|
|||
if self.is_home_folder or self.is_attachments_folder:
|
||||
frappe.throw(_("Cannot delete Home and Attachments folders"))
|
||||
self.validate_empty_folder()
|
||||
self.validate_protected_file()
|
||||
self._delete_file_on_disk()
|
||||
if not self.is_folder:
|
||||
self.add_comment_in_reference_doc("Attachment Removed", _("Removed {0}").format(self.file_name))
|
||||
|
|
@ -469,6 +471,36 @@ class File(Document):
|
|||
if self.is_folder and frappe.get_all("File", filters={"folder": self.name}, limit=1):
|
||||
frappe.throw(_("Folder {0} is not empty").format(self.name), FolderNotEmpty)
|
||||
|
||||
def validate_protected_file(self):
|
||||
"""Throw an exception if this file is attached to a doctype that protects files.
|
||||
|
||||
Allows deleting the attached file if the linked document is in draft. If submitted,
|
||||
deletion is not allowed. If canceled, requires delete permissions on the linked document.
|
||||
"""
|
||||
if not (self.attached_to_doctype and self.attached_to_name):
|
||||
return
|
||||
|
||||
try:
|
||||
ref_doc = frappe.get_doc(self.attached_to_doctype, self.attached_to_name)
|
||||
except DoesNotExistError:
|
||||
return
|
||||
|
||||
if ref_doc.docstatus == 0:
|
||||
# If the document is not submitted yet, users can correct wrong attachments
|
||||
return
|
||||
|
||||
if not ref_doc.meta.protect_attached_files:
|
||||
return
|
||||
|
||||
if ref_doc.docstatus == 2 and ref_doc.has_permission("delete"):
|
||||
# Deletion must still be possible if users have the permission to delete the linked document
|
||||
return
|
||||
|
||||
frappe.throw(
|
||||
msg=_("This file is attached to a protected document and cannot be deleted."),
|
||||
title=_("Protected File"),
|
||||
)
|
||||
|
||||
def _delete_file_on_disk(self):
|
||||
"""If file not attached to any other record, delete it"""
|
||||
on_disk_file_not_shared = self.content_hash and not frappe.get_all(
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
"column_break_21",
|
||||
"allow_copy",
|
||||
"make_attachments_public",
|
||||
"protect_attached_files",
|
||||
"view_settings_section",
|
||||
"title_field",
|
||||
"show_title_field_in_link",
|
||||
|
|
@ -407,6 +408,13 @@
|
|||
"fieldtype": "Int",
|
||||
"label": "Grid Page Length",
|
||||
"non_negative": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Users are only able to delete attached files if the document is either in draft or if the document is canceled and they are also able to delete the document.",
|
||||
"fieldname": "protect_attached_files",
|
||||
"fieldtype": "Check",
|
||||
"label": "Protect Attached Files"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
|
|
@ -415,7 +423,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-02-21 20:16:50.501895",
|
||||
"modified": "2025-03-27 18:22:32.618603",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form",
|
||||
|
|
@ -439,4 +447,4 @@
|
|||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ class CustomizeForm(Document):
|
|||
"Random",
|
||||
"By script",
|
||||
]
|
||||
protect_attached_files: DF.Check
|
||||
queue_in_background: DF.Check
|
||||
quick_entry: DF.Check
|
||||
search_fields: DF.Data | None
|
||||
|
|
@ -728,6 +729,7 @@ doctype_properties = {
|
|||
"editable_grid": "Check",
|
||||
"max_attachments": "Int",
|
||||
"make_attachments_public": "Check",
|
||||
"protect_attached_files": "Check",
|
||||
"track_changes": "Check",
|
||||
"track_views": "Check",
|
||||
"allow_auto_repeat": "Check",
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ frappe.ui.form.Attachments = class Attachments {
|
|||
</a>`;
|
||||
|
||||
let remove_action = null;
|
||||
if (frappe.model.can_write(this.frm.doctype, this.frm.name)) {
|
||||
if (this.can_delete_attachment()) {
|
||||
remove_action = function (target_id) {
|
||||
frappe.confirm(__("Are you sure you want to delete the attachment?"), function () {
|
||||
let target_attachment = me
|
||||
|
|
@ -156,6 +156,21 @@ frappe.ui.form.Attachments = class Attachments {
|
|||
.insertAfter(this.add_attachment_wrapper);
|
||||
}
|
||||
|
||||
can_delete_attachment() {
|
||||
if (this.frm.meta.protect_attached_files) {
|
||||
switch (this.frm.doc.docstatus) {
|
||||
case 0:
|
||||
return this.frm.has_perm("write");
|
||||
case 2:
|
||||
return this.frm.has_perm("write") && this.frm.has_perm("delete");
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return this.frm.has_perm("write");
|
||||
}
|
||||
|
||||
get_file_url(attachment) {
|
||||
var file_url = attachment.file_url;
|
||||
if (!file_url) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue