diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index 454cc89694..19a7c68e19 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -17,7 +17,7 @@ if [ "$TYPE" == "server" ]; then
fi
if [ "$DB" == "mariadb" ];then
- sudo apt install mariadb-client-10.3
+ sudo apt update && sudo apt install mariadb-client-10.3
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'";
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'";
diff --git a/.gitignore b/.gitignore
index c9dd8f38f3..7e3d178630 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,7 @@ dist/
frappe/docs/current
frappe/public/dist
.vscode
+.vs
node_modules
.kdev4/
*.kdev4
diff --git a/codecov.yml b/codecov.yml
index a9f6df0296..ed8b36c45c 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -11,6 +11,13 @@ coverage:
threshold: 0.5%
flags:
- server
+ patch:
+ default: false
+ server:
+ target: auto
+ threshold: 85%
+ flags:
+ - server
comment:
layout: "diff, flags"
diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py
index cd20a5c0f3..28880e7e38 100644
--- a/frappe/core/doctype/data_import/importer.py
+++ b/frappe/core/doctype/data_import/importer.py
@@ -199,7 +199,7 @@ class Importer:
new_doc = frappe.new_doc(self.doctype)
new_doc.update(doc)
- if (meta.autoname or "").lower() != "prompt":
+ if not doc.name and (meta.autoname or "").lower() != "prompt":
# name can only be set directly if autoname is prompt
new_doc.set("name", None)
diff --git a/frappe/core/doctype/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js
index 1c52070063..b907ebc0bc 100644
--- a/frappe/core/doctype/doctype/doctype.js
+++ b/frappe/core/doctype/doctype/doctype.js
@@ -143,11 +143,10 @@ frappe.ui.form.on("DocField", {
curr_value.doctype = doctype;
curr_value.fieldname = fieldname;
}
- let curr_df_link_doctype = row.fieldtype == "Link" ? row.options : null;
let doctypes = frm.doc.fields
.filter(df => df.fieldtype == "Link")
- .filter(df => df.options && df.options != curr_df_link_doctype)
+ .filter(df => df.options && df.fieldname != row.fieldname)
.map(df => ({
label: `${df.options} (${df.fieldname})`,
value: df.fieldname
diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py
index af9c8a48fa..91090bdd77 100755
--- a/frappe/core/doctype/file/file.py
+++ b/frappe/core/doctype/file/file.py
@@ -569,6 +569,24 @@ class File(Document):
frappe.local.rollback_observers.append(self)
self.save()
+ @staticmethod
+ def zip_files(files):
+ from six import string_types
+
+ zip_file = io.BytesIO()
+ zf = zipfile.ZipFile(zip_file, "w", zipfile.ZIP_DEFLATED)
+ for _file in files:
+ if isinstance(_file, string_types):
+ _file = frappe.get_doc("File", _file)
+ if not isinstance(_file, File):
+ continue
+ if _file.is_folder:
+ continue
+ zf.writestr(_file.file_name, _file.get_content())
+ zf.close()
+ return zip_file.getvalue()
+
+
def on_doctype_update():
frappe.db.add_index("File", ["attached_to_doctype", "attached_to_name"])
@@ -612,6 +630,16 @@ def move_file(file_list, new_parent, old_parent):
frappe.get_doc("File", old_parent).save()
frappe.get_doc("File", new_parent).save()
+
+@frappe.whitelist()
+def zip_files(files):
+ files = frappe.parse_json(files)
+ zipped_files = File.zip_files(files)
+ frappe.response["filename"] = "files.zip"
+ frappe.response["filecontent"] = zipped_files
+ frappe.response["type"] = "download"
+
+
def setup_folder_path(filename, new_parent):
file = frappe.get_doc("File", filename)
file.folder = new_parent
diff --git a/frappe/core/doctype/module_profile/module_profile.js b/frappe/core/doctype/module_profile/module_profile.js
index 9c92042dda..3714d31ade 100644
--- a/frappe/core/doctype/module_profile/module_profile.js
+++ b/frappe/core/doctype/module_profile/module_profile.js
@@ -1,19 +1,23 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Module Profile', {
- refresh: function(frm) {
+frappe.ui.form.on("Module Profile", {
+ refresh: function (frm) {
if (has_common(frappe.user_roles, ["Administrator", "System Manager"])) {
if (!frm.module_editor && frm.doc.__onload && frm.doc.__onload.all_modules) {
- let module_area = $('
')
- .appendTo(frm.fields_dict.module_html.wrapper);
-
+ const module_area = $(frm.fields_dict.module_html.wrapper);
frm.module_editor = new frappe.ModuleEditor(frm, module_area);
}
}
if (frm.module_editor) {
- frm.module_editor.refresh();
+ frm.module_editor.show();
+ }
+ },
+
+ validate: function (frm) {
+ if (frm.module_editor) {
+ frm.module_editor.set_modules_in_table();
}
}
});
diff --git a/frappe/core/doctype/module_profile/module_profile.json b/frappe/core/doctype/module_profile/module_profile.json
index 0e4e56962e..32bc757427 100644
--- a/frappe/core/doctype/module_profile/module_profile.json
+++ b/frappe/core/doctype/module_profile/module_profile.json
@@ -34,11 +34,17 @@
}
],
"index_web_pages_for_search": 1,
- "links": [],
- "modified": "2021-01-03 15:36:52.622696",
+ "links": [
+ {
+ "link_doctype": "User",
+ "link_fieldname": "module_profile"
+ }
+ ],
+ "modified": "2021-12-03 15:47:21.296443",
"modified_by": "Administrator",
"module": "Core",
"name": "Module Profile",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/core/doctype/role_profile/role_profile.json b/frappe/core/doctype/role_profile/role_profile.json
index 4b3f35aa57..7cd60a16d1 100644
--- a/frappe/core/doctype/role_profile/role_profile.json
+++ b/frappe/core/doctype/role_profile/role_profile.json
@@ -1,175 +1,80 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "role_profile",
- "beta": 0,
- "creation": "2017-08-31 04:16:38.764465",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "autoname": "role_profile",
+ "creation": "2017-08-31 04:16:38.764465",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "role_profile",
+ "roles_html",
+ "roles"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "role_profile",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Role Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
+ "fieldname": "role_profile",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Role Name",
+ "reqd": 1,
"unique": 1
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "roles_html",
- "fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Roles HTML",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "roles_html",
+ "fieldtype": "HTML",
+ "label": "Roles HTML",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "roles",
- "fieldtype": "Table",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Roles Assigned",
- "length": 0,
- "no_copy": 0,
- "options": "Has Role",
- "permlevel": 1,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "roles",
+ "fieldtype": "Table",
+ "hidden": 1,
+ "label": "Roles Assigned",
+ "options": "Has Role",
+ "permlevel": 1,
+ "print_hide": 1,
+ "read_only": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2017-10-17 11:05:11.183066",
- "modified_by": "Administrator",
- "module": "Core",
- "name": "Role Profile",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "links": [
+ {
+ "link_doctype": "User",
+ "link_fieldname": "role_profile_name"
+ }
+ ],
+ "modified": "2021-12-03 15:45:45.270963",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Role Profile",
+ "naming_rule": "Expression (old style)",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "email": 1,
+ "export": 1,
+ "permlevel": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "title_field": "role_profile",
- "track_changes": 1,
- "track_seen": 0
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "role_profile",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py
index e47846958a..b3c85b22a1 100644
--- a/frappe/core/doctype/user/test_user.py
+++ b/frappe/core/doctype/user/test_user.py
@@ -251,7 +251,7 @@ class TestUser(unittest.TestCase):
c = FrappeClient(url)
res1 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
res2 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
- self.assertEqual(res1.status_code, 200)
+ self.assertEqual(res1.status_code, 400)
self.assertEqual(res2.status_code, 417)
def test_user_rename(self):
diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js
index 2ce7413aa7..5b3a1affd9 100644
--- a/frappe/core/doctype/user/user.js
+++ b/frappe/core/doctype/user/user.js
@@ -50,7 +50,7 @@ frappe.ui.form.on('User', {
let d = frm.add_child("block_modules");
d.module = v.module;
});
- frm.module_editor && frm.module_editor.refresh();
+ frm.module_editor && frm.module_editor.show();
}
});
}
@@ -180,7 +180,7 @@ frappe.ui.form.on('User', {
frm.roles_editor.show();
}
- frm.module_editor && frm.module_editor.refresh();
+ frm.module_editor && frm.module_editor.show();
if(frappe.session.user==doc.name) {
// update display settings
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index f36553593b..6c729901e5 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -808,6 +808,7 @@ def reset_password(user):
return frappe.msgprint(_("Password reset instructions have been sent to your email"))
except frappe.DoesNotExistError:
+ frappe.local.response['http_status_code'] = 400
frappe.clear_messages()
return 'not found'
diff --git a/frappe/database/database.py b/frappe/database/database.py
index f489cea7de..0f325a746e 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -171,10 +171,10 @@ class Database(object):
frappe.errprint(query)
elif self.is_deadlocked(e):
- raise frappe.QueryDeadlockError
+ raise frappe.QueryDeadlockError(e)
elif self.is_timedout(e):
- raise frappe.QueryTimeoutError
+ raise frappe.QueryTimeoutError(e)
if ignore_ddl and (self.is_missing_column(e) or self.is_missing_table(e) or self.cant_drop_field_or_key(e)):
pass
@@ -511,14 +511,10 @@ class Database(object):
# Get coulmn and value of the single doctype Accounts Settings
account_settings = frappe.db.get_singles_dict("Accounts Settings")
"""
- result = self.sql("""
- SELECT field, value
- FROM `tabSingles`
- WHERE doctype = %s
- """, doctype)
-
+ result = self.query.get_sql(
+ "Singles", filters={"doctype": doctype}, fields=["field", "value"]
+ ).run()
dict_ = frappe._dict(result)
-
return dict_
@staticmethod
@@ -547,8 +543,11 @@ class Database(object):
if fieldname in self.value_cache[doctype]:
return self.value_cache[doctype][fieldname]
- val = self.sql("""select `value` from
- `tabSingles` where `doctype`=%s and `field`=%s""", (doctype, fieldname))
+ val = self.query.get_sql(
+ table="Singles",
+ filters={"doctype": doctype, "field": fieldname},
+ fields="value",
+ ).run()
val = val[0][0] if val else None
df = frappe.get_meta(doctype).get_field(fieldname)
@@ -583,7 +582,7 @@ class Database(object):
if not isinstance(fields, Criterion):
for field in fields:
- if "(" in field or " as " in field:
+ if "(" in str(field) or " as " in str(field):
field_objects.append(PseudoColumn(field))
else:
field_objects.append(field)
@@ -842,7 +841,7 @@ class Database(object):
cache_count = frappe.cache().get_value('doctype:count:{}'.format(dt))
if cache_count is not None:
return cache_count
- query = self.query.build_conditions(table=dt, filters=filters).select(Count("*"))
+ query = self.query.get_sql(table=dt, filters=filters, fields=Count("*"))
if filters:
count = self.sql(query, debug=debug)[0][0]
return count
diff --git a/frappe/database/query.py b/frappe/database/query.py
index 69328cb206..6d2be5fa25 100644
--- a/frappe/database/query.py
+++ b/frappe/database/query.py
@@ -286,14 +286,13 @@ class Query:
):
criterion = self.build_conditions(table, filters, **kwargs)
if isinstance(fields, (list, tuple)):
- query = criterion.select(*kwargs.get("field_objects"))
+ query = criterion.select(*kwargs.get("field_objects", fields))
elif isinstance(fields, Criterion):
query = criterion.select(fields)
else:
- if fields=="*":
- query = criterion.select(fields)
+ query = criterion.select(fields)
return query
diff --git a/frappe/desk/doctype/global_search_settings/global_search_settings.py b/frappe/desk/doctype/global_search_settings/global_search_settings.py
index 9ffe9aaf06..e9a47cecd1 100644
--- a/frappe/desk/doctype/global_search_settings/global_search_settings.py
+++ b/frappe/desk/doctype/global_search_settings/global_search_settings.py
@@ -33,7 +33,7 @@ class GlobalSearchSettings(Document):
def get_doctypes_for_global_search():
def get_from_db():
- doctypes = frappe.get_list("Global Search DocType", fields=["document_type"], order_by="idx ASC")
+ doctypes = frappe.get_all("Global Search DocType", fields=["document_type"], order_by="idx ASC")
return [d.document_type for d in doctypes] or []
return frappe.cache().hget("global_search", "search_priorities", get_from_db)
diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py
index cbf459e8ae..14ea2712e2 100644
--- a/frappe/desk/form/linked_with.py
+++ b/frappe/desk/form/linked_with.py
@@ -410,11 +410,11 @@ def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None):
try:
if link.get("filters"):
- ret = frappe.get_list(doctype=dt, fields=fields, filters=link.get("filters"))
+ ret = frappe.get_all(doctype=dt, fields=fields, filters=link.get("filters"))
elif link.get("get_parent"):
if me and me.parent and me.parenttype == dt:
- ret = frappe.get_list(doctype=dt, fields=fields,
+ ret = frappe.get_all(doctype=dt, fields=fields,
filters=[[dt, "name", '=', me.parent]])
else:
ret = None
@@ -426,7 +426,7 @@ def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None):
if link.get("doctype_fieldname"):
filters.append([link.get('child_doctype'), link.get("doctype_fieldname"), "=", doctype])
- ret = frappe.get_list(doctype=dt, fields=fields, filters=filters, or_filters=or_filters, distinct=True)
+ ret = frappe.get_all(doctype=dt, fields=fields, filters=filters, or_filters=or_filters, distinct=True)
else:
link_fieldnames = link.get("fieldname")
@@ -437,7 +437,7 @@ def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None):
# dynamic link
if link.get("doctype_fieldname"):
filters.append([dt, link.get("doctype_fieldname"), "=", doctype])
- ret = frappe.get_list(doctype=dt, fields=fields, filters=filters, or_filters=or_filters)
+ ret = frappe.get_all(doctype=dt, fields=fields, filters=filters, or_filters=or_filters)
else:
ret = None
diff --git a/frappe/desk/page/user_profile/user_profile_controller.js b/frappe/desk/page/user_profile/user_profile_controller.js
index c1a89f316e..40b542d5c3 100644
--- a/frappe/desk/page/user_profile/user_profile_controller.js
+++ b/frappe/desk/page/user_profile/user_profile_controller.js
@@ -17,21 +17,15 @@ class UserProfile {
show() {
let route = frappe.get_route();
this.user_id = route[1] || frappe.session.user;
-
- //validate if user
- if (route.length > 1) {
- frappe.dom.freeze(__('Loading user profile') + '...');
- frappe.db.exists('User', this.user_id).then(exists => {
- frappe.dom.unfreeze();
- if (exists) {
- this.make_user_profile();
- } else {
- frappe.msgprint(__('User does not exist'));
- }
- });
- } else {
- frappe.set_route('user-profile', frappe.session.user);
- }
+ frappe.dom.freeze(__('Loading user profile') + '...');
+ frappe.db.exists('User', this.user_id).then(exists => {
+ frappe.dom.unfreeze();
+ if (exists) {
+ this.make_user_profile();
+ } else {
+ frappe.msgprint(__('User does not exist'));
+ }
+ });
}
make_user_profile() {
@@ -74,8 +68,7 @@ class UserProfile {
primary_action_label: __('Go'),
primary_action: ({ user }) => {
dialog.hide();
- this.user_id = user;
- this.make_user_profile();
+ frappe.set_route('user-profile', user);
}
});
dialog.show();
diff --git a/frappe/desk/page/user_profile/user_profile_sidebar.html b/frappe/desk/page/user_profile/user_profile_sidebar.html
index 4a35c6cf9c..9f8889fd03 100644
--- a/frappe/desk/page/user_profile/user_profile_sidebar.html
+++ b/frappe/desk/page/user_profile/user_profile_sidebar.html
@@ -51,10 +51,10 @@
{%=__("Edit Profile") %}
{%=__("User Settings") %}
- {%=__("Leaderboard") %}
-
\ No newline at end of file
+
diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py
index 4489a68cac..d89a3d83be 100644
--- a/frappe/email/doctype/email_queue/email_queue.py
+++ b/frappe/email/doctype/email_queue/email_queue.py
@@ -18,7 +18,7 @@ from frappe import _, safe_encode, task
from frappe.model.document import Document
from frappe.email.queue import get_unsubcribed_url, get_unsubscribe_message
from frappe.email.email_body import add_attachment, get_formatted_html, get_email
-from frappe.utils import cint, split_emails, add_days, nowdate, cstr
+from frappe.utils import cint, split_emails, add_days, nowdate, cstr, get_hook_method
from frappe.email.doctype.email_account.email_account import EmailAccount
@@ -121,9 +121,13 @@ class EmailQueue(Document):
continue
message = ctx.build_message(recipient.recipient)
- if not frappe.flags.in_test:
- ctx.smtp_session.sendmail(from_addr=self.sender, to_addrs=recipient.recipient, msg=message)
- ctx.add_to_sent_list(recipient)
+ method = get_hook_method('override_email_send')
+ if method:
+ method(self, self.sender, recipient.recipient, message)
+ else:
+ if not frappe.flags.in_test:
+ ctx.smtp_session.sendmail(from_addr=self.sender, to_addrs=recipient.recipient, msg=message)
+ ctx.add_to_sent_list(recipient)
if frappe.flags.in_test:
frappe.flags.sent_mail = message
@@ -283,9 +287,14 @@ class SendMailContext:
if attachment.get('fcontent'):
continue
- fid = attachment.get("fid")
- if fid:
- _file = frappe.get_doc("File", fid)
+ file_filters = {}
+ if attachment.get('fid'):
+ file_filters['name'] = attachment.get('fid')
+ elif attachment.get('file_url'):
+ file_filters['file_url'] = attachment.get('file_url')
+
+ if file_filters:
+ _file = frappe.get_doc("File", file_filters)
fcontent = _file.get_content()
attachment.update({
'fname': _file.file_name,
@@ -293,6 +302,7 @@ class SendMailContext:
'parent': message_obj
})
attachment.pop("fid", None)
+ attachment.pop("file_url", None)
add_attachment(**attachment)
elif attachment.get("print_format_attachment") == 1:
@@ -503,7 +513,7 @@ class QueueBuilder:
if self._attachments:
# store attachments with fid or print format details, to be attached on-demand later
for att in self._attachments:
- if att.get('fid'):
+ if att.get('fid') or att.get('file_url'):
attachments.append(att)
elif att.get("print_format_attachment") == 1:
if not att.get('lang', None):
diff --git a/frappe/email/doctype/newsletter/newsletter.js b/frappe/email/doctype/newsletter/newsletter.js
index 3277d8e9ee..55805ad485 100644
--- a/frappe/email/doctype/newsletter/newsletter.js
+++ b/frappe/email/doctype/newsletter/newsletter.js
@@ -4,69 +4,137 @@
frappe.ui.form.on('Newsletter', {
refresh(frm) {
let doc = frm.doc;
- if (!doc.__islocal && !cint(doc.email_sent) && !doc.__unsaved
- && in_list(frappe.boot.user.can_write, doc.doctype)) {
- frm.add_custom_button(__('Send Now'), function() {
- frappe.confirm(__("Do you really want to send this email newsletter?"), function() {
- frm.call('send_emails').then(() => {
- frm.refresh();
+ let can_write = in_list(frappe.boot.user.can_write, doc.doctype);
+ if (!frm.is_new() && !frm.is_dirty() && !doc.email_sent && can_write) {
+ frm.add_custom_button(__('Send a test email'), () => {
+ frm.events.send_test_email(frm);
+ }, __('Preview'));
+
+ frm.add_custom_button(__('Check broken links'), () => {
+ frm.dashboard.set_headline(__('Checking broken links...'));
+ frm.call('find_broken_links').then(r => {
+ frm.dashboard.set_headline('');
+ let links = r.message;
+ if (links && links.length) {
+ let html = '