diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 0000000000..26801ebbe8 --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,16 @@ +name: Backport +on: + pull_request: + types: + - closed + - labeled + +jobs: + backport: + runs-on: ubuntu-18.04 + name: Backport + steps: + - name: Backport + uses: tibdex/backport@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/frappe/__init__.py b/frappe/__init__.py index 09140830d7..f942e53018 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -543,7 +543,7 @@ def only_for(roles, message=False): myroles = set(get_roles()) if not roles.intersection(myroles): if message: - msgprint(_('Only for {}').format(', '.join(roles))) + msgprint(_('This action is only allowed for {}').format(bold(', '.join(roles))), _('Not Permitted')) raise PermissionError def get_domain_data(module): diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index c48cd2f7b1..85a71ee2f6 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -523,7 +523,7 @@ def run_ui_tests(context, app, headless=False): # run for headless mode run_or_open = 'run' if headless else 'open' - command = '{site_env} {password_env} yarn run cypress:{run_or_open}' + command = '{site_env} {password_env} yarn run cypress {run_or_open}' formatted_command = command.format(site_env=site_env, password_env=password_env, run_or_open=run_or_open) frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True) diff --git a/frappe/contacts/doctype/address/address.js b/frappe/contacts/doctype/address/address.js index face6a86a0..63574622c0 100644 --- a/frappe/contacts/doctype/address/address.js +++ b/frappe/contacts/doctype/address/address.js @@ -24,6 +24,15 @@ frappe.ui.form.on("Address", { } }); frm.refresh_field("links"); + + if (frm.doc.links) { + for (let i in frm.doc.links) { + let link = frm.doc.links[i]; + frm.add_custom_button(__("{0}: {1}", [__(link.link_doctype), __(link.link_name)]), function() { + frappe.set_route("Form", link.link_doctype, link.link_name); + }, __("Links")); + } + } }, validate: function(frm) { // clear linked customer / supplier / sales partner on saving... @@ -38,9 +47,13 @@ frappe.ui.form.on("Address", { () => frappe.timeout(1), () => { const last_doc = frappe.contacts.get_last_doc(frm); - if(frappe.dynamic_link && frappe.dynamic_link.doc - && frappe.dynamic_link.doc.name == last_doc.docname){ - frappe.set_route('Form', last_doc.doctype, last_doc.docname); + if (frappe.dynamic_link && frappe.dynamic_link.doc && frappe.dynamic_link.doc.name == last_doc.docname) { + for (let i in frm.doc.links) { + let link = frm.doc.links[i]; + if (last_doc.doctype == link.link_doctype && last_doc.docname == link.link_name) { + frappe.set_route('Form', last_doc.doctype, last_doc.docname); + } + } } } ]); diff --git a/frappe/contacts/doctype/contact/contact.js b/frappe/contacts/doctype/contact/contact.js index 7bbbbb1564..5285f8b85c 100644 --- a/frappe/contacts/doctype/contact/contact.js +++ b/frappe/contacts/doctype/contact/contact.js @@ -42,7 +42,7 @@ frappe.ui.form.on("Contact", { }); frm.refresh_field("links"); - if (frm.doc.links.length > 0) { + if (frm.doc.links) { frappe.call({ method: "frappe.contacts.doctype.contact.contact.address_query", args: {links: frm.doc.links}, @@ -58,6 +58,13 @@ frappe.ui.form.on("Contact", { } } }); + + for (let i in frm.doc.links) { + let link = frm.doc.links[i]; + frm.add_custom_button(__("{0}: {1}", [__(link.link_doctype), __(link.link_name)]), function() { + frappe.set_route("Form", link.link_doctype, link.link_name); + }, __("Links")); + } } }, validate: function(frm) { @@ -73,9 +80,13 @@ frappe.ui.form.on("Contact", { () => frappe.timeout(1), () => { const last_doc = frappe.contacts.get_last_doc(frm); - if(frappe.dynamic_link && frappe.dynamic_link.doc - && frappe.dynamic_link.doc.name == last_doc.docname){ - frappe.set_route('Form', last_doc.doctype, last_doc.docname); + if (frappe.dynamic_link && frappe.dynamic_link.doc && frappe.dynamic_link.doc.name == last_doc.docname) { + for (let i in frm.doc.links) { + let link = frm.doc.links[i]; + if (last_doc.doctype == link.link_doctype && last_doc.docname == link.link_name) { + frappe.set_route('Form', last_doc.doctype, last_doc.docname); + } + } } } ]); diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index c4873ee40e..adb98676a4 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -73,7 +73,12 @@ frappe.ui.form.on('User', { if(!frm.is_new()) { if(has_access_to_edit_user()) { - + frm.add_custom_button(__("Resend Welcome Email"), function() { + frm.call('send_welcome_mail_to_user').then(()=>{ + frappe.msgprint(__("Email has been sent to {0}", [frm.doc.email])); + }); + }); + frm.add_custom_button(__("Set User Permissions"), function() { frappe.route_options = { "user": doc.name diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 6bbf042d8a..b581cd7c37 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -98,8 +98,6 @@ class User(Document): clear_notifications(user=self.name) frappe.clear_cache(user=self.name) self.send_password_notification(self.__new_password) - if self.__new_password: - self.reset_password_key = '' create_contact(self, ignore_mandatory=True) if self.name not in ('Administrator', 'Guest') and not self.user_image: frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name) @@ -176,7 +174,6 @@ class User(Document): and name in ({0}) limit 1""".format(', '.join(['%s'] * len(self.roles))), [d.role for d in self.roles])) - def share_with_self(self): if self.user_type=="System User": frappe.share.add(self.doctype, self.name, self.name, write=1, share=1, diff --git a/frappe/core/doctype/version/version.py b/frappe/core/doctype/version/version.py index 05cc102ab9..d7a3b62a2c 100644 --- a/frappe/core/doctype/version/version.py +++ b/frappe/core/doctype/version/version.py @@ -43,6 +43,8 @@ def get_diff(old, new, for_child=False): if not new: return None + blacklisted_fields = ["Markdown Editor", "Text Editor", "Code", "HTML Editor"] + # capture data import if set data_import = new.flags.via_data_import out = frappe._dict(changed = [], added = [], removed = [], row_changed = [], data_import=data_import) @@ -75,12 +77,12 @@ def get_diff(old, new, for_child=False): out.removed.append([df.fieldname, d.as_dict()]) elif (old_value != new_value): - # Check for None values - old_data = old.get_formatted(df.fieldname) if old_value else old_value - new_data = new.get_formatted(df.fieldname) if new_value else new_value + if df.fieldtype not in blacklisted_fields: + old_value = old.get_formatted(df.fieldname) if old_value else old_value + new_value = new.get_formatted(df.fieldname) if new_value else new_value - if old_data != new_data: - out.changed.append((df.fieldname, old_data, new_data)) + if old_value != new_value: + out.changed.append((df.fieldname, old_value, new_value)) # docstatus if not for_child and old.docstatus != new.docstatus: diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 0ecdc13b51..25cf0b6865 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -185,7 +185,7 @@ def run(report_name, filters=None, user=None, ignore_prepared_report=False): else: result = generate_report_result(report, filters, user) - result["add_total_row"] = report.add_total_row and not result['skip_total_row'] + result["add_total_row"] = report.add_total_row and not result.get('skip_total_row', False) return result @@ -303,6 +303,11 @@ def export_query(): if file_format_type == "Excel": data = run(report_name, filters) data = frappe._dict(data) + if not data.columns: + frappe.respond_as_web_page(_("No data to export"), + _("You can try changing the filters of your report.")) + return + columns = get_columns_dict(data.columns) from frappe.utils.xlsxutils import make_xlsx diff --git a/frappe/desk/treeview.py b/frappe/desk/treeview.py index 3b26ecd0a5..2944f20a37 100644 --- a/frappe/desk/treeview.py +++ b/frappe/desk/treeview.py @@ -57,9 +57,6 @@ def add_node(): args = make_tree_args(**frappe.form_dict) doc = frappe.get_doc(args) - if args.doctype == "Sales Person": - doc.employee = frappe.form_dict.get('employee') - doc.save() def make_tree_args(**kwarg): diff --git a/frappe/email/queue.py b/frappe/email/queue.py index 62b0d9ea3f..b35f5944b2 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -256,7 +256,7 @@ def get_emails_sent_this_month(): def get_emails_sent_today(): return frappe.db.sql("""SELECT COUNT(`name`) FROM `tabEmail Queue` WHERE - `status`='Sent' AND `creation` > (NOW() - INTERVAL '24' HOUR)""")[0][0] + `status` in ('Sent', 'Not Sent', 'Sending') AND `creation` > (NOW() - INTERVAL '24' HOUR)""")[0][0] def get_unsubscribe_message(unsubscribe_message, expose_recipients): if unsubscribe_message: diff --git a/frappe/integrations/doctype/slack_webhook_url/slack_webhook_url.py b/frappe/integrations/doctype/slack_webhook_url/slack_webhook_url.py index de96582c64..b28a6c50e6 100644 --- a/frappe/integrations/doctype/slack_webhook_url/slack_webhook_url.py +++ b/frappe/integrations/doctype/slack_webhook_url/slack_webhook_url.py @@ -10,9 +10,20 @@ from frappe import _ import requests import json + +error_messages = { + 400: "400: Invalid Payload or User not found", + 403: "403: Action Prohibited", + 404: "404: Channel not found", + 410: "410: The Channel is Archived", + 500: "500: Rollup Error, Slack seems to be down" +} + + class SlackWebhookURL(Document): pass + def send_slack_message(webhook_url, message, reference_doctype, reference_name): slack_url = frappe.db.get_value("Slack Webhook URL", webhook_url, "webhook_url") doc_url = get_url_to_form(reference_doctype, reference_name) @@ -21,10 +32,10 @@ def send_slack_message(webhook_url, message, reference_doctype, reference_name): "fallback": _("See the document at {0}").format(doc_url), "actions": [ { - "type": "button", - "text": _("Go to the document"), - "url": doc_url, - "style": "primary" + "type": "button", + "text": _("Go to the document"), + "url": doc_url, + "style": "primary" } ] } @@ -32,10 +43,9 @@ def send_slack_message(webhook_url, message, reference_doctype, reference_name): data = {"text": message, "attachments": attachments} r = requests.post(slack_url, data=json.dumps(data)) - - if r.ok == True: - return 'success' - - elif r.ok == False: - frappe.log_error(r.error, _('Slack Webhook Error')) + if not r.ok: + message = error_messages.get(r.status_code, r.status_code) + frappe.log_error(message, _('Slack Webhook Error')) return 'error' + + return 'success' diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index cbd0b1954c..8ebbe21692 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -230,7 +230,7 @@ frappe.views.BaseList = class BaseList { setup_filter_area() { this.filter_area = new FilterArea(this); - if (this.filters.length > 0) { + if (this.filters && this.filters.length > 0) { return this.filter_area.set(this.filters); } } diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 377f668864..10542641c0 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -1044,7 +1044,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { } setup_realtime_updates() { - if (this.list_view_settings.disable_auto_refresh) { + if (this.list_view_settings && this.list_view_settings.disable_auto_refresh) { return; } frappe.realtime.on('list_update', data => { diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 365cbaa9db..22ae5d50c1 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -1022,7 +1022,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } get_menu_items() { - return [ + let items = [ { label: __('Refresh'), action: () => this.refresh(), @@ -1153,6 +1153,18 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { standard: true }, { + label: __('User Permissions'), + action: () => frappe.set_route('List', 'User Permission', { + doctype: 'Report', + name: this.report_name + }), + condition: () => frappe.model.can_set_user_permissions('Report'), + standard: true + } + ]; + + if (frappe.user.is_report_manager()) { + items.push({ label: __('Save'), action: () => { let d = new frappe.ui.Dialog({ @@ -1163,6 +1175,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { fieldname: 'report_name', label: __("Report Name"), default: this.report_doc.is_standard == 'No' ? this.report_name : "", + reqd: true } ], primary_action: (values) => { @@ -1184,17 +1197,10 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { d.show(); }, standard: true - }, - { - label: __('User Permissions'), - action: () => frappe.set_route('List', 'User Permission', { - doctype: 'Report', - name: this.report_name - }), - condition: () => frappe.model.can_set_user_permissions('Report'), - standard: true - } - ]; + }) + } + + return items; } add_portrait_warning(dialog) { diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 866b91ef72..39c8c387eb 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -1026,8 +1026,7 @@ def expand_relative_urls(html): html = re.sub('(href|src){1}([\s]*=[\s]*[\'"]?)((?!http)[^\'" >]+)([\'"]?)', _expand_relative_urls, html) # background-image: url('/assets/...') - html = re.sub('(:[\s]?url)(\([\'"]?)([^\)]*)([\'"]?\))', _expand_relative_urls, html) - + html = re.sub('(:[\s]?url)(\([\'"]?)((?!http)[^\'" >]+)([\'"]?\))', _expand_relative_urls, html) return html def quoted(url): diff --git a/frappe/website/doctype/website_theme/website_theme.py b/frappe/website/doctype/website_theme/website_theme.py index 180d9e876c..954891b4b1 100644 --- a/frappe/website/doctype/website_theme/website_theme.py +++ b/frappe/website/doctype/website_theme/website_theme.py @@ -112,4 +112,4 @@ def generate_theme_files_if_not_exist(): doc.generate_theme_if_not_exist() doc.save() except Exception: - pass + frappe.log_error(frappe.get_traceback(), "Theme File Generation Failed")