diff --git a/frappe/core/doctype/file_data/file_data.json b/frappe/core/doctype/file_data/file_data.json index d416ebbb3f..cb42e57ad1 100644 --- a/frappe/core/doctype/file_data/file_data.json +++ b/frappe/core/doctype/file_data/file_data.json @@ -1,72 +1,255 @@ { + "_last_update": null, + "_user_tags": null, + "allow_attach": null, + "allow_copy": null, + "allow_email": null, + "allow_import": null, + "allow_print": null, + "allow_rename": null, + "allow_trash": null, "autoname": "File.######", - "creation": "2012-12-12 11:19:22.000000", + "change_log": null, + "client_script": null, + "client_script_core": null, + "client_string": null, + "colour": null, + "creation": "2012-12-12 11:19:22", + "custom": null, + "default_print_format": null, + "description": null, "docstatus": 0, "doctype": "DocType", + "document_type": null, + "dt_template": null, "fields": [ { + "allow_on_submit": null, + "default": null, + "depends_on": null, + "description": null, "fieldname": "file_name", "fieldtype": "Data", + "hidden": null, + "ignore_restrictions": null, + "in_filter": null, + "in_list_view": 1, "label": "File Name", + "no_column": null, + "no_copy": null, "oldfieldname": "file_name", "oldfieldtype": "Data", + "options": null, "permlevel": 0, - "read_only": 1 + "print_hide": null, + "print_width": null, + "read_only": 1, + "report_hide": null, + "reqd": null, + "search_index": null, + "set_only_once": null, + "trigger": null, + "width": null }, { + "allow_on_submit": null, + "default": null, + "depends_on": null, + "description": null, "fieldname": "file_url", "fieldtype": "Data", + "hidden": null, + "ignore_restrictions": null, + "in_filter": null, "in_list_view": 1, "label": "File URL", + "no_column": null, + "no_copy": null, + "oldfieldname": null, + "oldfieldtype": null, + "options": null, "permlevel": 0, - "read_only": 1 + "print_hide": null, + "print_width": null, + "read_only": 1, + "report_hide": null, + "reqd": null, + "search_index": null, + "set_only_once": null, + "trigger": null, + "width": null }, { + "allow_on_submit": null, + "default": null, + "depends_on": null, + "description": null, "fieldname": "attached_to_doctype", "fieldtype": "Link", + "hidden": null, + "ignore_restrictions": null, + "in_filter": null, "in_list_view": 1, "label": "Attached To DocType", + "no_column": null, + "no_copy": null, + "oldfieldname": null, + "oldfieldtype": null, "options": "DocType", "permlevel": 0, + "print_hide": null, + "print_width": null, "read_only": 1, - "search_index": 1 + "report_hide": null, + "reqd": null, + "search_index": 1, + "set_only_once": null, + "trigger": null, + "width": null }, { + "allow_on_submit": null, + "default": null, + "depends_on": null, + "description": null, "fieldname": "attached_to_name", "fieldtype": "Data", + "hidden": null, + "ignore_restrictions": null, + "in_filter": null, "in_list_view": 1, "label": "Attached To Name", + "no_column": null, + "no_copy": null, + "oldfieldname": null, + "oldfieldtype": null, + "options": null, "permlevel": 0, + "print_hide": null, + "print_width": null, "read_only": 1, - "search_index": 1 + "report_hide": null, + "reqd": null, + "search_index": 1, + "set_only_once": null, + "trigger": null, + "width": null }, { + "allow_on_submit": null, + "default": null, + "depends_on": null, + "description": null, "fieldname": "file_size", "fieldtype": "Int", + "hidden": null, + "ignore_restrictions": null, + "in_filter": null, "in_list_view": 1, "label": "File Size", + "no_column": null, + "no_copy": null, + "oldfieldname": null, + "oldfieldtype": null, + "options": null, "permlevel": 0, - "read_only": 1 + "print_hide": null, + "print_width": null, + "read_only": 1, + "report_hide": null, + "reqd": null, + "search_index": null, + "set_only_once": null, + "trigger": null, + "width": null + }, + { + "allow_on_submit": null, + "default": null, + "depends_on": null, + "description": null, + "fieldname": "content_hash", + "fieldtype": "Data", + "hidden": null, + "ignore_restrictions": null, + "in_filter": null, + "in_list_view": null, + "label": "Content Hash", + "no_column": null, + "no_copy": null, + "oldfieldname": null, + "oldfieldtype": null, + "options": null, + "permlevel": 0, + "print_hide": null, + "print_width": null, + "read_only": null, + "report_hide": null, + "reqd": null, + "search_index": 1, + "set_only_once": null, + "trigger": null, + "width": null } ], + "hide_heading": null, + "hide_toolbar": null, "icon": "icon-file", "idx": 1, - "modified": "2014-01-20 17:48:46.000000", + "in_create": null, + "in_dialog": null, + "is_submittable": null, + "is_transaction_doc": null, + "issingle": null, + "istable": null, + "max_attachments": null, + "menu_index": null, + "modified": "2014-04-07 17:01:13.295614", "modified_by": "Administrator", "module": "Core", "name": "File Data", + "name_case": null, "owner": "Administrator", + "parent": null, + "parent_node": null, + "parentfield": null, + "parenttype": null, "permissions": [ { - "cancel": 1, + "amend": null, + "cancel": 0, + "create": null, "delete": 1, "email": 1, + "export": null, + "import": null, + "match": null, "permlevel": 0, "print": 1, "read": 1, + "report": null, + "restrict": null, + "restricted": null, "role": "System Manager", + "submit": null, "write": 1 } ], - "read_only": 0 + "plugin": null, + "print_outline": null, + "read_only": 0, + "read_only_onload": null, + "search_fields": null, + "section_style": null, + "server_code": null, + "server_code_compiled": null, + "server_code_core": null, + "server_code_error": null, + "show_in_menu": null, + "smallicon": null, + "subject": null, + "tag_fields": null, + "title_field": null, + "use_template": null, + "version": null } \ No newline at end of file diff --git a/frappe/core/doctype/file_data/file_data.py b/frappe/core/doctype/file_data/file_data.py index 7b58f9e905..a457a3da5b 100644 --- a/frappe/core/doctype/file_data/file_data.py +++ b/frappe/core/doctype/file_data/file_data.py @@ -11,6 +11,7 @@ naming for same name files: file.gif, file-1.gif, file-2.gif etc import frappe, frappe.utils, os from frappe import conf from frappe.model.document import Document +from frappe.utils.file_manager import delete_file_data_content class FileData(Document): def before_insert(self): @@ -19,10 +20,10 @@ class FileData(Document): def on_update(self): # check duplicate assignement n_records = frappe.db.sql("""select name from `tabFile Data` - where file_name=%s + where content_hash=%s and name!=%s and attached_to_doctype=%s - and attached_to_name=%s""", (self.file_name, self.name, self.attached_to_doctype, + and attached_to_name=%s""", (self.content_hash, self.name, self.attached_to_doctype, self.attached_to_name)) if len(n_records) > 0: self.duplicate_entry = n_records[0][0] @@ -34,7 +35,7 @@ class FileData(Document): if self.attached_to_name: # check persmission try: - if not self.ignore_permissions and \ + if not getattr(self, 'ignore_permissions', False) and \ not frappe.has_permission(self.attached_to_doctype, "write", self.attached_to_name): frappe.msgprint(frappe._("No permission to write / remove."), raise_exception=True) @@ -44,13 +45,8 @@ class FileData(Document): # if file not attached to any other record, delete it if self.file_name and not frappe.db.count("File Data", - {"file_name": self.file_name, "name": ["!=", self.name]}): - if self.file_name.startswith("files/"): - path = frappe.utils.get_site_path("public", self.file_name) - else: - path = frappe.utils.get_site_path("public", "files", self.file_name) - if os.path.exists(path): - os.remove(path) + {"content_hash": self.content_hash, "name": ["!=", self.name]}): + delete_file_data_content(self) def on_rollback(self): - self.on_trash() \ No newline at end of file + self.on_trash() diff --git a/frappe/hooks.txt b/frappe/hooks.txt index c3aff8d520..5feb017e48 100644 --- a/frappe/hooks.txt +++ b/frappe/hooks.txt @@ -48,3 +48,6 @@ doc_event:Website Route Permission:on_update = frappe.templates.generators.websi doc_event:*:on_update = frappe.core.doctype.notification_count.notification_count.clear_doctype_notifications doc_event:*:on_cancel = frappe.core.doctype.notification_count.notification_count.clear_doctype_notifications doc_event:*:on_trash = frappe.core.doctype.notification_count.notification_count.clear_doctype_notifications + +write_file_keys = file_url +write_file_keys = file_name diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 22763351f9..b177708681 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -114,21 +114,14 @@ def update_child_docs(old, new, meta): % (df.options, '%s', '%s'), (new, old)) def update_link_field_values(link_fields, old, new, doctype): - update_list = [] - - # update values for field in link_fields: - # if already updated, do not do it again - if [field['parent'], field['fieldname']] in update_list: - continue - update_list.append([field['parent'], field['fieldname']]) if field['issingle']: frappe.db.sql("""\ update `tabSingles` set value=%s where doctype=%s and field=%s and value=%s""", (new, field['parent'], field['fieldname'], old)) else: - if doctype!='DocType' and field['parent']!=new: + if field['parent']!=new: frappe.db.sql("""\ update `tab%s` set `%s`=%s where `%s`=%s""" \ diff --git a/frappe/patches.txt b/frappe/patches.txt index bcb5b9efa3..b1ac2d88f6 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -16,11 +16,12 @@ frappe.patches.4_0.website_sitemap_hierarchy frappe.patches.4_0.webnotes_to_frappe execute:frappe.reset_perms("Module Def") frappe.patches.4_0.rename_sitemap_to_route +frappe.patches.4_0.rename_profile_to_user frappe.patches.4_0.set_website_route_idx execute:import frappe.installer;frappe.installer.make_site_dirs() #2014-02-19 frappe.patches.4_0.private_backups frappe.patches.4_0.set_module_in_report frappe.patches.4_0.remove_old_parent -frappe.patches.4_0.rename_profile_to_user frappe.patches.4_0.update_datetime frappe.patches.4_0.deprecate_control_panel +frappe.patches.4_0.file_manager_hooks diff --git a/frappe/patches/4_0/file_manager_hooks.py b/frappe/patches/4_0/file_manager_hooks.py new file mode 100644 index 0000000000..5c72e62da3 --- /dev/null +++ b/frappe/patches/4_0/file_manager_hooks.py @@ -0,0 +1,27 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals + +import frappe +import os +from frappe.utils import get_files_path +from frappe.utils.file_manager import get_content_hash, get_file + + +def execute(): + frappe.reload_doc('core', 'doctype', 'file_data') + for name, file_name, file_url in frappe.db.sql( + """select name, file_name, file_url from `tabFile Data` + where file_name is not null"""): + b = frappe.get_doc('File Data', name) + old_file_name = b.file_name + b.file_name = os.path.basename(old_file_name) + if old_file_name.startswith('files/') or old_file_name.startswith('/files/'): + b.file_url = os.path.normpath('/' + old_file_name) + else: + b.file_url = os.path.normpath('/files/' + old_file_name) + _file_name, content = get_file(name) + b.content_hash = get_content_hash(content) + b.save() + diff --git a/frappe/patches/4_0/rename_profile_to_user.py b/frappe/patches/4_0/rename_profile_to_user.py index 6c2b984429..66b8752da6 100644 --- a/frappe/patches/4_0/rename_profile_to_user.py +++ b/frappe/patches/4_0/rename_profile_to_user.py @@ -1,6 +1,7 @@ import frappe from frappe.model import rename_field +from frappe.model.meta import get_table_columns def execute(): tables = frappe.db.sql_list("show tables") @@ -9,6 +10,7 @@ def execute(): if frappe.db.exists("DocType", "Website Route Permission"): frappe.reload_doc("website", "doctype", "website_route_permission") - rename_field("Website Route Permission", "profile", "user") + if "profile" in get_table_columns("Website Route Permission"): + rename_field("Website Route Permission", "profile", "user") frappe.reload_doc("website", "doctype", "blogger") rename_field("Blogger", "profile", "user") diff --git a/frappe/public/js/frappe/form/attachments.js b/frappe/public/js/frappe/form/attachments.js index cbde3e16f5..1d9f7248ec 100644 --- a/frappe/public/js/frappe/form/attachments.js +++ b/frappe/public/js/frappe/form/attachments.js @@ -41,13 +41,14 @@ frappe.ui.form.Attachments = Class.extend({ this.$list.empty(); var attachments = this.get_attachments(); - var file_names = keys(attachments).sort(); + var that = this; + // add attachment objects - if(file_names.length) { - for(var i=0; i' + frappe._("None") + '

').appendTo(this.$list); } @@ -58,18 +59,23 @@ frappe.ui.form.Attachments = Class.extend({ get_attachments: function() { return this.frm.get_docinfo().attachments; }, - add_attachment: function(filename, attachments) { - var fileid = attachments[filename]; + add_attachment: function(attachment) { + var file_name = attachment.file_name; + var file_url = attachment.file_url; + var fileid = attachment.name; + if (!file_name) { + file_name = file_url; + } var me = this; var $attach = $(repl('
\ \ - %(filename)s×\ + %(file_name)s×\
', { - filename: filename, - href: frappe.utils.get_file_link(filename) + file_name: file_name, + file_url: file_url })) .appendTo(this.$list) @@ -83,7 +89,7 @@ frappe.ui.form.Attachments = Class.extend({ me.remove_attachment($(remove_btn).data("fileid")) } ); - return false; + return false }); if(!frappe.model.can_write(this.frm.doctype, this.frm.name)) { @@ -131,9 +137,9 @@ frappe.ui.form.Attachments = Class.extend({ doctype: this.frm.doctype, docname: this.frm.docname, }, - callback: function(fileid, filename, r) { + callback: function(fileid, filename, file_url, r) { me.dialog.hide(); - me.update_attachment(fileid, filename, fieldname, r); + me.update_attachment(fileid, filename, file_url, fieldname, r.message); }, onerror: function() { me.dialog.hide(); @@ -142,26 +148,27 @@ frappe.ui.form.Attachments = Class.extend({ max_height: this.frm.cscript ? this.frm.cscript.attachment_max_height : null, }); }, - update_attachment: function(fileid, filename, fieldname, r) { + update_attachment: function(fileid, filename, fieldname, file_url, attachment) { if(fileid) { - this.add_to_attachments(fileid, filename); + this.add_to_attachments(attachment); this.refresh(); if(fieldname) { - this.frm.set_value(fieldname, frappe.utils.get_file_link(filename)); + this.frm.set_value(fieldname, file_url); this.frm.cscript[fieldname] && this.frm.cscript[fieldname](this.frm.doc); this.frm.toolbar.show_infobar(); } } }, - add_to_attachments: function(fileid, filename) { - this.get_attachments()[filename] = fileid; + add_to_attachments: function (attachment) { + this.get_attachments().push(attachment); }, remove_fileid: function(fileid) { var attachments = this.get_attachments(); - var new_attachments = {}; - $.each(attachments, function(key, value) { - if(value!=fileid) - new_attachments[key] = value; + var new_attachments = []; + $.each(attachments, function(i, attachment) { + if(attachment.name!=fileid) { + new_attachments.push(attachment); + } }); this.frm.get_docinfo().attachments = new_attachments; this.refresh(); @@ -181,4 +188,4 @@ frappe.ui.form.Attachments = Class.extend({ } } } -}); \ No newline at end of file +}); diff --git a/frappe/public/js/frappe/upload.js b/frappe/public/js/frappe/upload.js index 90356fe317..19f49af828 100644 --- a/frappe/public/js/frappe/upload.js +++ b/frappe/public/js/frappe/upload.js @@ -91,9 +91,9 @@ frappe.upload = { opts.onerror ? opts.onerror(r) : opts.callback(null, null, r); return; } - opts.callback(r.message.fid, r.message.filename, r); + opts.callback(r.message.name , r.message.file_name, r.message.file_url, r); $(document).trigger("upload_complete", - [r.message.fid, r.message.filename]); + [r.message.name , r.message.file_name]); } }); } @@ -123,4 +123,4 @@ frappe.upload = { freader.readAsDataURL(fileobj); } } -} \ No newline at end of file +} diff --git a/frappe/tests/test_filemanager.py b/frappe/tests/test_filemanager.py new file mode 100644 index 0000000000..45207d8857 --- /dev/null +++ b/frappe/tests/test_filemanager.py @@ -0,0 +1,80 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +import frappe +import os +import unittest + +from frappe.utils.file_manager import save_file, get_file, get_files_path + +test_content1 = 'Hello' +test_content2 = 'Hello World' + +def make_test_doc(): + d = frappe.new_doc('ToDo') + d.description = 'Test' + d.save() + return d.doctype, d.name + +class TestSimpleFile(unittest.TestCase): + + def setUp(self): + self.attached_to_doctype, self.attached_to_docname = make_test_doc() + self.test_content = test_content1 + self.saved_file = save_file('hello.txt', self.test_content, self.attached_to_doctype, self.attached_to_docname) + self.saved_filename = get_files_path(self.saved_file.file_name) + + def test_save(self): + filename, content = get_file(self.saved_file.name) + self.assertEqual(content, self.test_content) + + def tearDown(self): + # File gets deleted on rollback, so blank + pass + + +class TestSameFileName(unittest.TestCase): + + def setUp(self): + self.attached_to_doctype, self.attached_to_docname = make_test_doc() + self.test_content1 = test_content1 + self.test_content2 = test_content2 + self.saved_file1 = save_file('hello.txt', self.test_content1, self.attached_to_doctype, self.attached_to_docname) + self.saved_file2 = save_file('hello.txt', self.test_content2, self.attached_to_doctype, self.attached_to_docname) + self.saved_filename1 = get_files_path(self.saved_file1.file_name) + self.saved_filename2 = get_files_path(self.saved_file2.file_name) + + def test_saved_content(self): + filename1, content1 = get_file(self.saved_file1.name) + self.assertEqual(content1, self.test_content1) + filename2, content2 = get_file(self.saved_file2.name) + self.assertEqual(content2, self.test_content2) + + def tearDown(self): + # File gets deleted on rollback, so blank + pass + + +class TestSameContent(unittest.TestCase): + + def setUp(self): + self.attached_to_doctype1, self.attached_to_docname1 = make_test_doc() + self.attached_to_doctype2, self.attached_to_docname2 = make_test_doc() + self.test_content1 = test_content1 + self.test_content2 = test_content1 + self.orig_filename = 'hello.txt' + self.dup_filename = 'hello2.txt' + self.saved_file1 = save_file(self.orig_filename, self.test_content1, self.attached_to_doctype1, self.attached_to_docname1) + self.saved_file2 = save_file(self.dup_filename, self.test_content2, self.attached_to_doctype2, self.attached_to_docname2) + self.saved_filename1 = get_files_path(self.saved_file1.file_name) + self.saved_filename2 = get_files_path(self.saved_file2.file_name) + + def test_saved_content(self): + filename1, content1 = get_file(self.saved_file1.name) + filename2, content2 = get_file(self.saved_file2.name) + self.assertEqual(filename1, filename2) + self.assertFalse(os.path.exists(get_files_path(self.dup_filename))) + + def tearDown(self): + # File gets deleted on rollback, so blank + pass diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index abcf2d11c8..d722c13087 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -782,8 +782,8 @@ def get_site_base_path(sites_dir=None, hostname=None): def get_site_path(*path): return get_path(base=get_site_base_path(), *path) -def get_files_path(): - return get_site_path("public", "files") +def get_files_path(*path): + return get_site_path("public", "files", *path) def get_backups_path(): return get_site_path("private", "backups") @@ -913,3 +913,11 @@ def touch_file(path): def get_test_client(): from frappe.app import application return Client(application) + +def get_hook_method(hook_name, fallback=None): + method = (frappe.get_hooks().get(hook_name)) + if method: + method = frappe.get_attr(method[0]) + return method + if fallback: + return fallback diff --git a/frappe/utils/file_manager.py b/frappe/utils/file_manager.py index 767994b712..06dea9ee33 100644 --- a/frappe/utils/file_manager.py +++ b/frappe/utils/file_manager.py @@ -4,9 +4,12 @@ from __future__ import unicode_literals import frappe import os, base64, re -from frappe.utils import cstr, cint, get_site_path +import hashlib +import mimetypes +from frappe.utils import cstr, cint, get_site_path, get_hook_method, get_files_path from frappe import _ from frappe import conf +from copy import copy class MaxFileSizeReachedError(frappe.ValidationError): pass @@ -31,7 +34,11 @@ def upload(): elif file_url: filedata = save_url(file_url, dt, dn) - return {"fid": filedata.name, "filename": filedata.file_name or filedata.file_url } + return { + "name": filedata.name, + "file_name": filedata.file_name, + "file_url": filedata.file_url + } def save_uploaded(dt, dn): fname, content = get_uploaded_content() @@ -76,11 +83,12 @@ def extract_images_from_html(doc, fieldname): data = match.group(1) headers, content = data.split(",") filename = headers.split("filename=")[-1] - filename = save_file(filename, content, doc.doctype, doc.name, decode=True).get("file_name") + # TODO fix this + file_url = save_file(filename, content, doc.doctype, doc.name, decode=True).get("file_url") if not frappe.flags.has_dataurl: frappe.flags.has_dataurl = True - return ' 100: - frappe.msgprint("Too many versions", raise_exception=True) - - return new_fname - -def scrub_file_name(fname): - if '\\' in fname: - fname = fname.split('\\')[-1] - if '/' in fname: - fname = fname.split('/')[-1] - return fname +def save_file_on_filesystem(fname, content, content_type=None): + import filecmp + public_path = os.path.join(frappe.local.site_path, "public") + fpath = write_file(content, get_files_path(), fname) + path = os.path.relpath(fpath, public_path) + return { + 'file_name': os.path.basename(path), + 'file_url': '/' + path + } def check_max_file_size(content): max_file_size = conf.get('max_file_size') or 1000000 @@ -196,17 +154,14 @@ def check_max_file_size(content): return file_size -def write_file(content, files_path): +def write_file(content, file_path, fname): """write file to disk with a random name (to compare)""" - # create account folder (if not exists) - frappe.create_folder(files_path) - fname = os.path.join(files_path, frappe.generate_hash()) - + # create directory (if not exists) + frappe.create_folder(get_files_path()) # write the file - with open(fname, 'w+') as f: + with open(os.path.join(file_path, fname), 'w+') as f: f.write(content) - - return fname + return get_files_path(fname) def remove_all(dt, dn): """remove all files in a transaction""" @@ -220,6 +175,19 @@ def remove_all(dt, dn): def remove_file(fid): """Remove file and File Data entry""" frappe.delete_doc("File Data", fid) + +def delete_file_data_content(doc): + method = get_hook_method('delete_file_data_content', fallback=delete_file_from_filesystem) + method(doc) + +def delete_file_from_filesystem(doc): + path = doc.file_name + if path.startswith("files/"): + path = frappe.utils.get_site_path("public", doc.file_name) + else: + path = frappe.utils.get_site_path("public", "files", doc.file_name) + if os.path.exists(path): + os.remove(path) def get_file(fname): f = frappe.db.sql("""select file_name from `tabFile Data` @@ -237,3 +205,14 @@ def get_file(fname): content = f.read() return [file_name, content] + +def get_content_hash(content): + return hashlib.md5(content).hexdigest() + +def get_file_name(fname, optional_suffix): + n_records = frappe.db.sql("select name from `tabFile Data` where file_name='{}'".format(fname)) + if len(n_records) > 0: + partial, extn = fname.rsplit('.', 1) + return '{partial}{suffix}.{extn}'.format(partial=partial, extn=extn, suffix=optional_suffix) + return fname + diff --git a/frappe/widgets/form/load.py b/frappe/widgets/form/load.py index 67e23369a2..2e26d43ad2 100644 --- a/frappe/widgets/form/load.py +++ b/frappe/widgets/form/load.py @@ -6,6 +6,7 @@ import frappe, json import frappe.utils import frappe.defaults import frappe.widgets.form.meta +from frappe.utils.file_manager import get_file_url @frappe.whitelist() def getdoc(doctype, name, user=None): @@ -88,11 +89,15 @@ def get_restrictions(meta): return out def add_attachments(dt, dn): - attachments = {} + attachments = [] for f in frappe.db.sql("""select name, file_name, file_url from `tabFile Data` where attached_to_name=%s and attached_to_doctype=%s""", (dn, dt), as_dict=True): - attachments[f.file_url or f.file_name] = f.name + attachments.append({ + 'name': f.name, + 'file_url': f.file_url, + 'file_name': f.file_name + }) return attachments @@ -122,4 +127,4 @@ def get_badge_info(doctypes, filters): for doctype in doctypes: out[doctype] = frappe.db.get_value(doctype, filters, "count(*)") - return out \ No newline at end of file + return out