diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index b8bed89a4d..48247860b3 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -93,6 +93,7 @@ class File(Document): self.set_is_private() self.set_file_name() self.validate_duplicate_entry() + self.validate_attachment_limit() self.validate_folder() if not self.file_url and not self.flags.ignore_file_validate: @@ -140,6 +141,26 @@ class File(Document): if self.file_url and (self.is_private != self.file_url.startswith('/private')): frappe.throw(_('Invalid file URL. Please contact System Administrator.')) + def validate_attachment_limit(self): + attachment_limit = 0 + if self.attached_to_doctype and self.attached_to_name: + attachment_limit = cint(frappe.get_meta(self.attached_to_doctype).max_attachments) + + if attachment_limit: + current_attachment_count = len(frappe.get_all('File', filters={ + 'attached_to_doctype': self.attached_to_doctype, + 'attached_to_name': self.attached_to_name, + }, limit=attachment_limit + 1)) + + if current_attachment_count >= attachment_limit: + frappe.throw( + _("Maximum Attachment Limit of {0} has been reached for {1} {2}.").format( + frappe.bold(attachment_limit), self.attached_to_doctype, self.attached_to_name + ), + exc=frappe.exceptions.AttachmentLimitReached, + title=_('Attachment Limit Reached') + ) + def set_folder_name(self): """Make parent folders if not exists based on reference doctype and name""" if self.attached_to_doctype and not self.folder: diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py index 85397ea1ee..e627558680 100644 --- a/frappe/core/doctype/file/test_file.py +++ b/frappe/core/doctype/file/test_file.py @@ -160,6 +160,31 @@ class TestSameContent(unittest.TestCase): def test_saved_content(self): self.assertFalse(os.path.exists(get_files_path(self.dup_filename))) + def test_attachment_limit(self): + doctype, docname = make_test_doc() + from frappe.custom.doctype.property_setter.property_setter import make_property_setter + limit_property = make_property_setter('ToDo', None, 'max_attachments', 1, 'int', for_doctype=True) + file1 = frappe.get_doc({ + "doctype": "File", + "file_name": 'test-attachment', + "attached_to_doctype": doctype, + "attached_to_name": docname, + "content": 'test' + }) + + file1.insert() + + file2 = frappe.get_doc({ + "doctype": "File", + "file_name": 'test-attachment', + "attached_to_doctype": doctype, + "attached_to_name": docname, + "content": 'test2' + }) + + self.assertRaises(frappe.exceptions.AttachmentLimitReached, file2.insert) + limit_property.delete() + frappe.clear_cache(doctype='ToDo') def tearDown(self): # File gets deleted on rollback, so blank diff --git a/frappe/exceptions.py b/frappe/exceptions.py index 9b63b23787..82fbff7a90 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -106,6 +106,7 @@ class InvalidDates(ValidationError): pass class DataTooLongException(ValidationError): pass class FileAlreadyAttachedException(Exception): pass class DocumentAlreadyRestored(Exception): pass +class AttachmentLimitReached(Exception): pass # OAuth exceptions class InvalidAuthorizationHeader(CSRFTokenError): pass class InvalidAuthorizationPrefix(CSRFTokenError): pass diff --git a/frappe/public/js/frappe/file_uploader/index.js b/frappe/public/js/frappe/file_uploader/index.js index 62a7bff822..646f60715a 100644 --- a/frappe/public/js/frappe/file_uploader/index.js +++ b/frappe/public/js/frappe/file_uploader/index.js @@ -15,7 +15,11 @@ export default class FileUploader { allow_multiple, as_dataurl, disable_file_browser, + frm } = {}) { + + frm && frm.attachments.max_reached(true); + if (!wrapper) { this.make_dialog(); } else { diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index bb9e8c22d1..90b628f269 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -232,14 +232,10 @@ frappe.ui.form.Form = class FrappeForm { throw "attach error"; } - if(me.attachments.max_reached()) { - frappe.msgprint(__("Maximum Attachment Limit for this record reached.")); - throw "attach error"; - } - new frappe.ui.FileUploader({ doctype: me.doctype, docname: me.docname, + frm: me, files: dataTransfer.files, folder: 'Home/Attachments', on_success(file_doc) { diff --git a/frappe/public/js/frappe/form/sidebar/attachments.js b/frappe/public/js/frappe/form/sidebar/attachments.js index 165527e281..56b484e7c4 100644 --- a/frappe/public/js/frappe/form/sidebar/attachments.js +++ b/frappe/public/js/frappe/form/sidebar/attachments.js @@ -16,15 +16,19 @@ frappe.ui.form.Attachments = Class.extend({ this.add_attachment_wrapper = this.parent.find(".add_attachment").parent(); this.attachments_label = this.parent.find(".attachments-label"); }, - max_reached: function() { - // no of attachments - var n = Object.keys(this.get_attachments()).length; - - // button if the number of attachments is less than max - if(n < this.frm.meta.max_attachments || !this.frm.meta.max_attachments) { - return false; + max_reached: function(raise_exception=false) { + const attachment_count = Object.keys(this.get_attachments()).length; + const attachment_limit = this.frm.meta.max_attachments; + if (attachment_limit && attachment_count >= attachment_limit) { + if (raise_exception) { + frappe.throw({ + title: __("Attachment Limit Reached"), + message: __("Maximum attachment limit of {0} has been reached.", [cstr(attachment_limit).bold()]), + }); + } + return true; } - return true; + return false; }, refresh: function() { var me = this; @@ -140,7 +144,6 @@ frappe.ui.form.Attachments = Class.extend({ }); }, new_attachment: function(fieldname) { - var me = this; if (this.dialog) { // remove upload dialog this.dialog.$wrapper.remove(); @@ -149,6 +152,7 @@ frappe.ui.form.Attachments = Class.extend({ new frappe.ui.FileUploader({ doctype: this.frm.doctype, docname: this.frm.docname, + frm: this.frm, folder: 'Home/Attachments', on_success: (file_doc) => { this.attachment_uploaded(file_doc);