From e12a6c24f2c0c54eb2c2bc01eda979528125e60a Mon Sep 17 00:00:00 2001 From: conncampbell Date: Sat, 8 May 2021 21:32:18 -0600 Subject: [PATCH 01/75] fix: OTP login verification fails --- frappe/templates/includes/login/login.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index 52ac70513d..93bf95bdb7 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -291,17 +291,10 @@ var verify_token = function (event) { } var request_otp = function (r) { - $('.login-content').empty().append($('
').attr({ 'id': 'twofactor_div' }).html( - '
\ -
\ - {{ _("Verification") }}\ -
\ -
\ - \ - \ -
')); - // add event handler for submit button - verify_token(); + $('.login-content').empty(); + $('.login-content:visible').append($('
').attr({ 'id': 'twofactor_div' }).html('{{ _("Verification") }}{{ _("Verify") }}')); + // add event handler for submit button + verify_token(); } var continue_otp_app = function (setup, qrcode) { From 2b51641799c60ae9b16a1d29e4306d6c35e66070 Mon Sep 17 00:00:00 2001 From: conncampbell Date: Mon, 10 May 2021 16:14:51 -0600 Subject: [PATCH 02/75] fix: changed translate entries to javascript rather than python to resolve semgrep linter translate checks --- frappe/templates/includes/login/login.js | 56 ++++++++++++++---------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index 93bf95bdb7..4bc17cb14b 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -21,7 +21,7 @@ login.bind_events = function () { args.pwd = $("#login_password").val(); args.device = "desktop"; if (!args.usr || !args.pwd) { - frappe.msgprint('{{ _("Both login and password required") }}'); + frappe.msgprint(__("Both login and password required")); return false; } login.call(args); @@ -36,7 +36,7 @@ login.bind_events = function () { args.redirect_to = frappe.utils.sanitise_redirect(frappe.utils.get_url_arg("redirect-to")); args.full_name = frappe.utils.xss_sanitise(($("#signup_fullname").val() || "").trim()); if (!args.email || !validate_email(args.email) || !args.full_name) { - login.set_status('{{ _("Valid email and name required") }}', 'red'); + login.set_status(__("Valid email and name required"), 'red'); return false; } login.call(args); @@ -49,7 +49,7 @@ login.bind_events = function () { args.cmd = "frappe.core.doctype.user.user.reset_password"; args.user = ($("#forgot_email").val() || "").trim(); if (!args.user) { - login.set_status('{{ _("Valid Login id required.") }}', 'red'); + login.set_status(__("Valid Login id required."), 'red'); return false; } login.call(args); @@ -60,10 +60,10 @@ login.bind_events = function () { var input = $($(this).attr("toggle")); if (input.attr("type") == "password") { input.attr("type", "text"); - $(this).text('{{ _("Hide") }}') + $(this).text(__("Hide")) } else { input.attr("type", "password"); - $(this).text('{{ _("Show") }}') + $(this).text(__("Show")) } }); @@ -75,7 +75,7 @@ login.bind_events = function () { args.pwd = $("#login_password").val(); args.device = "desktop"; if (!args.usr || !args.pwd) { - login.set_status('{{ _("Both login and password required") }}', 'red'); + login.set_status(__("Both login and password required"), 'red'); return false; } login.call(args); @@ -136,7 +136,7 @@ login.signup = function () { // Login login.call = function (args, callback) { - login.set_status('{{ _("Verifying...") }}', 'blue'); + login.set_status(__("Verifying..."), 'blue'); return frappe.call({ type: "POST", @@ -194,7 +194,7 @@ login.login_handlers = (function () { var login_handlers = { 200: function (data) { if (data.message == 'Logged In') { - login.set_status('{{ _("Success") }}', 'green'); + login.set_status(__("Success"), 'green'); window.location.href = frappe.utils.sanitise_redirect(frappe.utils.get_url_arg("redirect-to")) || data.home_page; } else if (data.message == 'Password Reset') { window.location.href = frappe.utils.sanitise_redirect(data.redirect_to); @@ -218,13 +218,13 @@ login.login_handlers = (function () { } } else if (window.location.hash === '#forgot') { if (data.message === 'not found') { - login.set_status('{{ _("Not a valid user") }}', 'red'); + login.set_status(__("Not a valid user"), 'red'); } else if (data.message == 'not allowed') { - login.set_status('{{ _("Not Allowed") }}', 'red'); + login.set_status(__("Not Allowed"), 'red'); } else if (data.message == 'disabled') { - login.set_status('{{ _("Not Allowed: Disabled User") }}', 'red'); + login.set_status(__("Not Allowed: Disabled User"), 'red'); } else { - login.set_status('{{ _("Instructions Emailed") }}', 'green'); + login.set_status(__("Instructions Emailed"), 'green'); } @@ -232,7 +232,7 @@ login.login_handlers = (function () { if (cint(data.message[0]) == 0) { login.set_status(data.message[1], 'red'); } else { - login.set_status('{{ _("Success") }}', 'green'); + login.set_status(__("Success"), 'green'); frappe.msgprint(data.message[1]) } //login.set_status(__(data.message), 'green'); @@ -240,7 +240,7 @@ login.login_handlers = (function () { //OTP verification if (data.verification && data.message != 'Logged In') { - login.set_status('{{ _("Success") }}', 'green'); + login.set_status(__("Success"), 'green'); document.cookie = "tmp_id=" + data.tmp_id; @@ -253,8 +253,8 @@ login.login_handlers = (function () { } } }, - 401: get_error_handler('{{ _("Invalid Login. Try again.") }}'), - 417: get_error_handler('{{ _("Oops! Something went wrong") }}') + 401: get_error_handler(__("Invalid Login. Try again.")), + 417: get_error_handler(__("Oops! Something went wrong")) }; return login_handlers; @@ -282,7 +282,7 @@ var verify_token = function (event) { args.otp = $("#login_token").val(); args.tmp_id = frappe.get_cookie('tmp_id'); if (!args.otp) { - frappe.msgprint('{{ _("Login token required") }}'); + frappe.msgprint(__("Login token required")); return false; } login.call(args); @@ -292,9 +292,17 @@ var verify_token = function (event) { var request_otp = function (r) { $('.login-content').empty(); - $('.login-content:visible').append($('
').attr({ 'id': 'twofactor_div' }).html('{{ _("Verification") }}{{ _("Verify") }}')); - // add event handler for submit button - verify_token(); + $('.login-content:visible').append($('
').attr({ 'id': 'twofactor_div' }).html( + '
\ +
\ + {{ _("Verification") }}\ +
\ +
\ + \ + \ +
')); + // add event handler for submit button + verify_token(); } var continue_otp_app = function (setup, qrcode) { @@ -302,11 +310,11 @@ var continue_otp_app = function (setup, qrcode) { var qrcode_div = $('
'); if (setup) { - direction = $('
').attr('id', 'qr_info').text('{{ _("Enter Code displayed in OTP App.") }}'); + direction = $('
').attr('id', 'qr_info').text(__("Enter Code displayed in OTP App.")); qrcode_div.append(direction); $('#otp_div').prepend(qrcode_div); } else { - direction = $('
').attr('id', 'qr_info').text('{{ _("OTP setup using OTP App was not completed. Please contact Administrator.") }}'); + direction = $('
').attr('id', 'qr_info').text(__("OTP setup using OTP App was not completed. Please contact Administrator.")); qrcode_div.append(direction); $('#otp_div').prepend(qrcode_div); } @@ -320,7 +328,7 @@ var continue_sms = function (setup, prompt) { sms_div.append(prompt) $('#otp_div').prepend(sms_div); } else { - direction = $('
').attr('id', 'qr_info').text(prompt || '{{ _("SMS was not sent. Please contact Administrator.") }}'); + direction = $('
').attr('id', 'qr_info').text(prompt || __("SMS was not sent. Please contact Administrator.")); sms_div.append(direction); $('#otp_div').prepend(sms_div) } @@ -334,7 +342,7 @@ var continue_email = function (setup, prompt) { email_div.append(prompt) $('#otp_div').prepend(email_div); } else { - var direction = $('
').attr('id', 'qr_info').text(prompt || '{{ _("Verification code email not sent. Please contact Administrator.") }}'); + var direction = $('
').attr('id', 'qr_info').text(prompt || __("Verification code email not sent. Please contact Administrator.")); email_div.append(direction); $('#otp_div').prepend(email_div); } From 1c4e1bc1df7e0788e235b4f735194d8567de6e58 Mon Sep 17 00:00:00 2001 From: prssanna Date: Wed, 14 Apr 2021 14:48:15 +0530 Subject: [PATCH 03/75] refactor: set amended docname to original docname --- frappe/core/doctype/doctype/doctype.py | 16 ++++++ frappe/model/document.py | 10 +++- frappe/model/naming.py | 53 +++++++++++++++++-- frappe/patches.txt | 1 + ...l_name_docfield_to_submittable_doctypes.py | 11 ++++ frappe/public/js/frappe/form/form.js | 24 +++++---- frappe/public/js/frappe/router.js | 6 +++ 7 files changed, 105 insertions(+), 16 deletions(-) create mode 100644 frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 84673f990a..3890ab3a32 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -74,6 +74,7 @@ class DocType(Document): if not self.istable: validate_permissions(self) + self.make_cancellable() self.make_amendable() self.make_repeatable() self.validate_nestedset() @@ -589,6 +590,21 @@ class DocType(Document): "no_copy": 1 }) + def make_cancellable(self): + """If is_submittable is set, add original_name docfield.""" + if self.is_submittable: + if not frappe.db.sql("""select name from tabDocField + where fieldname = 'original_name' and parent = %s""", self.name): + self.append("fields", { + "label": "Original Name", + "fieldtype": "Text", + "fieldname": "original_name", + "read_only": 1, + "hidden": 1, + "print_hide": 1, + "no_copy": 1 + }) + def make_repeatable(self): """If allow_auto_repeat is set, add auto_repeat custom field.""" if self.allow_auto_repeat: diff --git a/frappe/model/document.py b/frappe/model/document.py index 623916597e..2bb4f1f3c0 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -7,7 +7,7 @@ import time from frappe import _, msgprint, is_whitelisted from frappe.utils import flt, cstr, now, get_datetime_str, file_lock, date_diff from frappe.model.base_document import BaseDocument, get_controller -from frappe.model.naming import set_new_name +from frappe.model.naming import set_new_name, rename_cancelled_doc from six import iteritems, string_types from werkzeug.exceptions import NotFound, Forbidden import hashlib, json @@ -919,6 +919,14 @@ class Document(BaseDocument): @whitelist.__func__ def _cancel(self): """Cancel the document. Sets `docstatus` = 2, then saves.""" + + # for backward compatibility + if self.amended_from and not self.original_name: + self.original_name = None + else: + self.original_name = self.name + + self.name = rename_cancelled_doc(self) self.docstatus = 2 self.save() diff --git a/frappe/model/naming.py b/frappe/model/naming.py index b8d6a6f8d7..33a09c659a 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -223,7 +223,16 @@ def revert_series_if_last(key, name, doc=None): * prefix = #### and hashes = 2021 (hash doesn't exist) * will search hash in key then accordingly get prefix = "" """ - if ".#" in key: + + # do not revert if doc is amended, since cancelled docs still exist + if doc.docstatus != 2 and doc.amended_from: + return + + # for first cancelled doc + if doc.docstatus == 2 and not doc.amended_from and doc.original_name: + name = doc.original_name + + if ".#" in key: prefix, hashes = key.rsplit(".", 1) if "#" not in hashes: # get the hash part from the key @@ -306,14 +315,48 @@ def append_number_if_name_exists(doctype, value, fieldname="name", separator="-" def _set_amended_name(doc): + if doc.original_name: + doc.name = doc.original_name + else: + original_name = get_original_name(doc) + doc.name = doc.amended_from + + # rename original doc to next name in series, and set amended doc name as original name + next_name_in_series = get_new_name_from_amended_from(doc) + frappe.rename_doc(doc.doctype, original_name, next_name_in_series, force=True, show_alert=False) + doc.name = original_name + doc.amended_from = next_name_in_series + doc.original_name = original_name + + return doc.name + +def get_original_name(doc): + # get original doc name from chain of amended docs + amended_from = original_name = doc.amended_from + while amended_from: + original_name = amended_from + amended_from = frappe.db.get_value(doc.doctype, amended_from, "amended_from") + + return original_name + +def get_new_name_from_amended_from(doc): am_id = 1 - am_prefix = doc.amended_from - if frappe.db.get_value(doc.doctype, doc.amended_from, "amended_from"): + am_prefix = doc.name + if frappe.db.get_value(doc.doctype, doc.name, "amended_from"): am_id = cint(doc.amended_from.split("-")[-1]) + 1 am_prefix = "-".join(doc.amended_from.split("-")[:-1]) # except the last hyphen - doc.name = am_prefix + "-" + str(am_id) - return doc.name + new_name = am_prefix + "-" + str(am_id) + if new_name == doc.name: + am_id += 1 + new_name = am_prefix + "-" + str(am_id) + return new_name + +def rename_cancelled_doc(doc): + doc = frappe.parse_json(doc) + new_name = get_new_name_from_amended_from(doc) + frappe.rename_doc(doc.doctype, doc.name, new_name, force=True, show_alert=False) + return new_name def _field_autoname(autoname, doc, skip_slicing=None): diff --git a/frappe/patches.txt b/frappe/patches.txt index e70be0a37b..a722bb39fa 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -337,3 +337,4 @@ frappe.patches.v12_0.rename_uploaded_files_with_proper_name frappe.patches.v13_0.queryreport_columns frappe.patches.v13_0.jinja_hook frappe.patches.v13_0.update_notification_channel_if_empty +frappe.patches.v13_0.add_original_name_docfield_to_submittable_doctypes ####### diff --git a/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py b/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py new file mode 100644 index 0000000000..725d37fd4a --- /dev/null +++ b/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py @@ -0,0 +1,11 @@ +import frappe +from frappe.database.schema import add_column + +def execute(): + for doctype in frappe.db.get_all('DocType'): + doctype = frappe.get_doc("DocType", doctype.name) + if doctype.is_submittable and frappe.db.table_exists(doctype.name): + doctype.make_cancellable() + if not frappe.db.has_column(doctype.name, 'original_name'): + add_column(doctype.name, 'original_name', 'Text') + doctype.db_update_all() diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 6d8a6b1cb4..f9d9de967e 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -764,32 +764,36 @@ frappe.ui.form.Form = class FrappeForm { } _cancel(btn, callback, on_error, skip_confirm) { - const me = this; const cancel_doc = () => { frappe.validated = true; - me.script_manager.trigger("before_cancel").then(() => { + this.script_manager.trigger("before_cancel").then(() => { if (!frappe.validated) { - return me.handle_save_fail(btn, on_error); + return this.handle_save_fail(btn, on_error); } - var after_cancel = function(r) { + const original_name = this.docname; + const after_cancel = (r) => { if (r.exc) { - me.handle_save_fail(btn, on_error); + this.handle_save_fail(btn, on_error); } else { frappe.utils.play_sound("cancel"); - me.refresh(); callback && callback(); - me.script_manager.trigger("after_cancel"); + this.script_manager.trigger("after_cancel"); + frappe.run_serially([ + () => this.rename_notify(this.doctype, original_name, r.docs[0].name), + () => frappe.router.clear_re_route(this.doctype, original_name), + () => this.refresh(), + ]); } }; - frappe.ui.form.save(me, "cancel", after_cancel, btn); + frappe.ui.form.save(this, "cancel", after_cancel, btn); }); } if (skip_confirm) { cancel_doc(); } else { - frappe.confirm(__("Permanently Cancel {0}?", [this.docname]), cancel_doc, me.handle_save_fail(btn, on_error)); + frappe.confirm(__("Permanently Cancel {0}?", [this.docname]), cancel_doc, this.handle_save_fail(btn, on_error)); } }; @@ -811,7 +815,7 @@ frappe.ui.form.Form = class FrappeForm { 'docname': this.doc.name }).then(is_amended => { if (is_amended) { - frappe.throw(__('This document is already amended, you cannot ammend it again')); + frappe.throw(__('This document is already amended, you cannot amend it again')); } this.validate_form_action("Amend"); var me = this; diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js index 5378294855..0d5231260c 100644 --- a/frappe/public/js/frappe/router.js +++ b/frappe/public/js/frappe/router.js @@ -235,6 +235,12 @@ frappe.router = { } }, + clear_re_route(doctype, docname) { + delete frappe.re_route[ + `${encodeURIComponent(frappe.router.slug(doctype))}/${encodeURIComponent(docname)}` + ]; + }, + set_title(sub_path) { if (frappe.route_titles[sub_path]) { frappe.utils.set_title(frappe.route_titles[sub_path]); From 251f878009f8f0e5574307ffd4275cd6ff36e3bc Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 15 Apr 2021 11:20:16 +0530 Subject: [PATCH 04/75] test: test naming for cancelled and amended docs --- frappe/tests/test_naming.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/frappe/tests/test_naming.py b/frappe/tests/test_naming.py index 66d48e3612..765ead275f 100644 --- a/frappe/tests/test_naming.py +++ b/frappe/tests/test_naming.py @@ -117,3 +117,36 @@ class TestNaming(unittest.TestCase): self.assertEqual(current_index.get('current'), 2) frappe.db.sql("""delete from `tabSeries` where name = %s""", series) + + def test_naming_for_cancelled_and_amended_doc(self): + if not frappe.db.exists('DocType', 'Submittable Doctype'): + frappe.get_doc({ + "doctype": "DocType", + "module": "Core", + "custom": 1, + "is_submittable": 1, + "permissions": [{ + "role": "System Manager", + "read": 1 + }], + "name": 'Submittable Doctype' + }).insert() + + doc = frappe.new_doc('Submittable DocType') + doc.save() + original_name = doc.name + + doc.submit() + doc.cancel() + cancelled_name = doc.name + self.assertEqual(cancelled_name, "{}-1".format(original_name)) + + amended_doc = frappe.copy_doc(doc) + amended_doc.docstatus = 0 + amended_doc.amended_from = doc.name + amended_doc.save() + self.assertEqual(amended_doc.name, original_name) + + amended_doc.submit() + amended_doc.cancel() + self.assertEqual(amended_doc.name, "{}-2".format(original_name)) From 304a771ba2221aa15cb9f186799a0603cb5ee425 Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 15 Apr 2021 11:21:48 +0530 Subject: [PATCH 05/75] fix: check if doc has attribute amended_from --- frappe/model/naming.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/frappe/model/naming.py b/frappe/model/naming.py index 33a09c659a..b685886c58 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -224,13 +224,14 @@ def revert_series_if_last(key, name, doc=None): * will search hash in key then accordingly get prefix = "" """ - # do not revert if doc is amended, since cancelled docs still exist - if doc.docstatus != 2 and doc.amended_from: - return + if hasattr(doc, 'amended_from'): + # do not revert if doc is amended, since cancelled docs still exist + if doc.docstatus != 2 and doc.amended_from: + return - # for first cancelled doc - if doc.docstatus == 2 and not doc.amended_from and doc.original_name: - name = doc.original_name + # for first cancelled doc + if doc.docstatus == 2 and not doc.amended_from and doc.original_name: + name = doc.original_name if ".#" in key: prefix, hashes = key.rsplit(".", 1) From 3614ca2a4d9ed0831cd3bd7080e977a5c2622149 Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 15 Apr 2021 12:17:53 +0530 Subject: [PATCH 06/75] style: fix indent --- frappe/core/doctype/doctype/doctype.py | 18 +++++++++--------- frappe/tests/test_naming.py | 23 +++++++++++------------ 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 3890ab3a32..0d478015bb 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -580,15 +580,15 @@ class DocType(Document): if self.is_submittable: if not frappe.db.sql("""select name from tabDocField where fieldname = 'amended_from' and parent = %s""", self.name): - self.append("fields", { - "label": "Amended From", - "fieldtype": "Link", - "fieldname": "amended_from", - "options": self.name, - "read_only": 1, - "print_hide": 1, - "no_copy": 1 - }) + self.append("fields", { + "label": "Amended From", + "fieldtype": "Link", + "fieldname": "amended_from", + "options": self.name, + "read_only": 1, + "print_hide": 1, + "no_copy": 1 + }) def make_cancellable(self): """If is_submittable is set, add original_name docfield.""" diff --git a/frappe/tests/test_naming.py b/frappe/tests/test_naming.py index 765ead275f..afaabcc805 100644 --- a/frappe/tests/test_naming.py +++ b/frappe/tests/test_naming.py @@ -119,18 +119,17 @@ class TestNaming(unittest.TestCase): frappe.db.sql("""delete from `tabSeries` where name = %s""", series) def test_naming_for_cancelled_and_amended_doc(self): - if not frappe.db.exists('DocType', 'Submittable Doctype'): - frappe.get_doc({ - "doctype": "DocType", - "module": "Core", - "custom": 1, - "is_submittable": 1, - "permissions": [{ - "role": "System Manager", - "read": 1 - }], - "name": 'Submittable Doctype' - }).insert() + frappe.get_doc({ + "doctype": "DocType", + "module": "Core", + "custom": 1, + "is_submittable": 1, + "permissions": [{ + "role": "System Manager", + "read": 1 + }], + "name": 'Submittable Doctype' + }).insert() doc = frappe.new_doc('Submittable DocType') doc.save() From ba267b6e628ea57009aa310645ba2f539708fe76 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 16 Apr 2021 13:03:21 +0530 Subject: [PATCH 07/75] fix: update patch --- frappe/patches.txt | 2 +- .../add_original_name_docfield_to_submittable_doctypes.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/frappe/patches.txt b/frappe/patches.txt index a722bb39fa..1db775f17c 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -337,4 +337,4 @@ frappe.patches.v12_0.rename_uploaded_files_with_proper_name frappe.patches.v13_0.queryreport_columns frappe.patches.v13_0.jinja_hook frappe.patches.v13_0.update_notification_channel_if_empty -frappe.patches.v13_0.add_original_name_docfield_to_submittable_doctypes ####### +frappe.patches.v13_0.add_original_name_docfield_to_submittable_doctypes diff --git a/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py b/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py index 725d37fd4a..d870e63305 100644 --- a/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py +++ b/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py @@ -1,11 +1,9 @@ import frappe -from frappe.database.schema import add_column def execute(): for doctype in frappe.db.get_all('DocType'): doctype = frappe.get_doc("DocType", doctype.name) if doctype.is_submittable and frappe.db.table_exists(doctype.name): doctype.make_cancellable() - if not frappe.db.has_column(doctype.name, 'original_name'): - add_column(doctype.name, 'original_name', 'Text') + frappe.reload_doctype(doctype.name) doctype.db_update_all() From 26801f215cf03f9cb921752456ca6117bbb987f3 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 16 Apr 2021 13:10:14 +0530 Subject: [PATCH 08/75] fix: use orm --- frappe/core/doctype/doctype/doctype.py | 48 +++++++++++++------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 0d478015bb..aad0321fa0 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -577,33 +577,33 @@ class DocType(Document): def make_amendable(self): """If is_submittable is set, add amended_from docfields.""" - if self.is_submittable: - if not frappe.db.sql("""select name from tabDocField - where fieldname = 'amended_from' and parent = %s""", self.name): - self.append("fields", { - "label": "Amended From", - "fieldtype": "Link", - "fieldname": "amended_from", - "options": self.name, - "read_only": 1, - "print_hide": 1, - "no_copy": 1 - }) + if self.is_submittable and\ + not frappe.db.get_value('DocField', {'fieldname': 'amended_from', 'parent': self.name}): + + self.append("fields", { + "label": "Amended From", + "fieldtype": "Link", + "fieldname": "amended_from", + "options": self.name, + "read_only": 1, + "print_hide": 1, + "no_copy": 1 + }) def make_cancellable(self): """If is_submittable is set, add original_name docfield.""" - if self.is_submittable: - if not frappe.db.sql("""select name from tabDocField - where fieldname = 'original_name' and parent = %s""", self.name): - self.append("fields", { - "label": "Original Name", - "fieldtype": "Text", - "fieldname": "original_name", - "read_only": 1, - "hidden": 1, - "print_hide": 1, - "no_copy": 1 - }) + if self.is_submittable and\ + not frappe.db.get_value('DocField', {'fieldname': 'amended_from', 'parent': self.name}): + + self.append("fields", { + "label": "Original Name", + "fieldtype": "Text", + "fieldname": "original_name", + "read_only": 1, + "hidden": 1, + "print_hide": 1, + "no_copy": 1 + }) def make_repeatable(self): """If allow_auto_repeat is set, add auto_repeat custom field.""" From 413383bf5cbbe27fb2b93c23e761e0a5b8b7f1f4 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 16 Apr 2021 13:38:06 +0530 Subject: [PATCH 09/75] fix: condition in make_cancellable --- frappe/core/doctype/doctype/doctype.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index aad0321fa0..c6f6b628cc 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -593,7 +593,7 @@ class DocType(Document): def make_cancellable(self): """If is_submittable is set, add original_name docfield.""" if self.is_submittable and\ - not frappe.db.get_value('DocField', {'fieldname': 'amended_from', 'parent': self.name}): + not frappe.db.get_value('DocField', {'fieldname': 'original_name', 'parent': self.name}): self.append("fields", { "label": "Original Name", From 2cf5915eee428e026de8b4ad9b8c75c60d756eee Mon Sep 17 00:00:00 2001 From: Prssanna Desai Date: Fri, 16 Apr 2021 13:38:33 +0530 Subject: [PATCH 10/75] fix: use get_meta Co-authored-by: Faris Ansari --- .../v13_0/add_original_name_docfield_to_submittable_doctypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py b/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py index d870e63305..c5080cc692 100644 --- a/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py +++ b/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py @@ -2,7 +2,7 @@ import frappe def execute(): for doctype in frappe.db.get_all('DocType'): - doctype = frappe.get_doc("DocType", doctype.name) + doctype = frappe.get_meta(doctype.name) if doctype.is_submittable and frappe.db.table_exists(doctype.name): doctype.make_cancellable() frappe.reload_doctype(doctype.name) From 209ece8b6cc23fce390d8bb04d5463a85320dd34 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 16 Apr 2021 14:03:36 +0530 Subject: [PATCH 11/75] fix: delete test submittable doctype --- frappe/tests/test_naming.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/tests/test_naming.py b/frappe/tests/test_naming.py index afaabcc805..7be2cc3ef7 100644 --- a/frappe/tests/test_naming.py +++ b/frappe/tests/test_naming.py @@ -119,7 +119,7 @@ class TestNaming(unittest.TestCase): frappe.db.sql("""delete from `tabSeries` where name = %s""", series) def test_naming_for_cancelled_and_amended_doc(self): - frappe.get_doc({ + submittable_doctype = frappe.get_doc({ "doctype": "DocType", "module": "Core", "custom": 1, @@ -149,3 +149,5 @@ class TestNaming(unittest.TestCase): amended_doc.submit() amended_doc.cancel() self.assertEqual(amended_doc.name, "{}-2".format(original_name)) + + submittable_doctype.delete() \ No newline at end of file From 0c603f0d769536d3351386cb2cd7aaccd99797a5 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 16 Apr 2021 17:27:12 +0530 Subject: [PATCH 12/75] test: fix cancel docs tests --- frappe/core/doctype/doctype/test_doctype.py | 4 ++++ .../add_original_name_docfield_to_submittable_doctypes.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py index 9c492d2c36..5a53600fff 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -350,6 +350,7 @@ class TestDocType(unittest.TestCase): dump_docs = json.dumps(docs.get('docs')) cancel_all_linked_docs(dump_docs) data_link_doc.cancel() + data_doc.name = '{}-1'.format(data_doc.name) data_doc.load_from_db() self.assertEqual(data_link_doc.docstatus, 2) self.assertEqual(data_doc.docstatus, 2) @@ -435,7 +436,10 @@ class TestDocType(unittest.TestCase): self.assertRaises(frappe.LinkExistsError, data_link_doc_1.cancel) data_doc.load_from_db() + + data_doc_2.name = '{}-1'.format(data_doc_2.name) data_doc_2.load_from_db() + self.assertEqual(data_link_doc_1.docstatus, 2) #linked doc is canceled diff --git a/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py b/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py index c5080cc692..eccff2334f 100644 --- a/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py +++ b/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py @@ -2,7 +2,7 @@ import frappe def execute(): for doctype in frappe.db.get_all('DocType'): - doctype = frappe.get_meta(doctype.name) + doctype = frappe.get_doc('DocType', doctype.name) if doctype.is_submittable and frappe.db.table_exists(doctype.name): doctype.make_cancellable() frappe.reload_doctype(doctype.name) From b550e54f15d02bbb14d79c70344a89f0f73d9c33 Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 29 Apr 2021 17:31:56 +0530 Subject: [PATCH 13/75] fix: add column in patch --- .../add_original_name_docfield_to_submittable_doctypes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py b/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py index eccff2334f..d17242e90a 100644 --- a/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py +++ b/frappe/patches/v13_0/add_original_name_docfield_to_submittable_doctypes.py @@ -1,9 +1,11 @@ import frappe +from frappe.database.schema import add_column def execute(): for doctype in frappe.db.get_all('DocType'): doctype = frappe.get_doc('DocType', doctype.name) if doctype.is_submittable and frappe.db.table_exists(doctype.name): doctype.make_cancellable() - frappe.reload_doctype(doctype.name) + if not frappe.db.has_column(doctype.name, 'original_name'): + add_column(doctype.name, 'original_name', 'Text') doctype.db_update_all() From 081677c6c4bf6190c804fb432ee2e83d14c74dda Mon Sep 17 00:00:00 2001 From: Prssanna Desai Date: Fri, 7 May 2021 11:30:35 +0530 Subject: [PATCH 14/75] fix: typo in test --- frappe/tests/test_naming.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/tests/test_naming.py b/frappe/tests/test_naming.py index 7be2cc3ef7..e7933ec7b3 100644 --- a/frappe/tests/test_naming.py +++ b/frappe/tests/test_naming.py @@ -131,7 +131,7 @@ class TestNaming(unittest.TestCase): "name": 'Submittable Doctype' }).insert() - doc = frappe.new_doc('Submittable DocType') + doc = frappe.new_doc('Submittable Doctype') doc.save() original_name = doc.name From df6c5cd3e2ea6c750ae13debb533ea5062ebd52f Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 10 Jun 2021 16:27:59 +0530 Subject: [PATCH 15/75] Revert "fix: changed translate entries to javascript rather than python to resolve semgrep linter translate checks" This reverts commit 2b51641799c60ae9b16a1d29e4306d6c35e66070. --- frappe/templates/includes/login/login.js | 56 ++++++++++-------------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index 4bc17cb14b..93bf95bdb7 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -21,7 +21,7 @@ login.bind_events = function () { args.pwd = $("#login_password").val(); args.device = "desktop"; if (!args.usr || !args.pwd) { - frappe.msgprint(__("Both login and password required")); + frappe.msgprint('{{ _("Both login and password required") }}'); return false; } login.call(args); @@ -36,7 +36,7 @@ login.bind_events = function () { args.redirect_to = frappe.utils.sanitise_redirect(frappe.utils.get_url_arg("redirect-to")); args.full_name = frappe.utils.xss_sanitise(($("#signup_fullname").val() || "").trim()); if (!args.email || !validate_email(args.email) || !args.full_name) { - login.set_status(__("Valid email and name required"), 'red'); + login.set_status('{{ _("Valid email and name required") }}', 'red'); return false; } login.call(args); @@ -49,7 +49,7 @@ login.bind_events = function () { args.cmd = "frappe.core.doctype.user.user.reset_password"; args.user = ($("#forgot_email").val() || "").trim(); if (!args.user) { - login.set_status(__("Valid Login id required."), 'red'); + login.set_status('{{ _("Valid Login id required.") }}', 'red'); return false; } login.call(args); @@ -60,10 +60,10 @@ login.bind_events = function () { var input = $($(this).attr("toggle")); if (input.attr("type") == "password") { input.attr("type", "text"); - $(this).text(__("Hide")) + $(this).text('{{ _("Hide") }}') } else { input.attr("type", "password"); - $(this).text(__("Show")) + $(this).text('{{ _("Show") }}') } }); @@ -75,7 +75,7 @@ login.bind_events = function () { args.pwd = $("#login_password").val(); args.device = "desktop"; if (!args.usr || !args.pwd) { - login.set_status(__("Both login and password required"), 'red'); + login.set_status('{{ _("Both login and password required") }}', 'red'); return false; } login.call(args); @@ -136,7 +136,7 @@ login.signup = function () { // Login login.call = function (args, callback) { - login.set_status(__("Verifying..."), 'blue'); + login.set_status('{{ _("Verifying...") }}', 'blue'); return frappe.call({ type: "POST", @@ -194,7 +194,7 @@ login.login_handlers = (function () { var login_handlers = { 200: function (data) { if (data.message == 'Logged In') { - login.set_status(__("Success"), 'green'); + login.set_status('{{ _("Success") }}', 'green'); window.location.href = frappe.utils.sanitise_redirect(frappe.utils.get_url_arg("redirect-to")) || data.home_page; } else if (data.message == 'Password Reset') { window.location.href = frappe.utils.sanitise_redirect(data.redirect_to); @@ -218,13 +218,13 @@ login.login_handlers = (function () { } } else if (window.location.hash === '#forgot') { if (data.message === 'not found') { - login.set_status(__("Not a valid user"), 'red'); + login.set_status('{{ _("Not a valid user") }}', 'red'); } else if (data.message == 'not allowed') { - login.set_status(__("Not Allowed"), 'red'); + login.set_status('{{ _("Not Allowed") }}', 'red'); } else if (data.message == 'disabled') { - login.set_status(__("Not Allowed: Disabled User"), 'red'); + login.set_status('{{ _("Not Allowed: Disabled User") }}', 'red'); } else { - login.set_status(__("Instructions Emailed"), 'green'); + login.set_status('{{ _("Instructions Emailed") }}', 'green'); } @@ -232,7 +232,7 @@ login.login_handlers = (function () { if (cint(data.message[0]) == 0) { login.set_status(data.message[1], 'red'); } else { - login.set_status(__("Success"), 'green'); + login.set_status('{{ _("Success") }}', 'green'); frappe.msgprint(data.message[1]) } //login.set_status(__(data.message), 'green'); @@ -240,7 +240,7 @@ login.login_handlers = (function () { //OTP verification if (data.verification && data.message != 'Logged In') { - login.set_status(__("Success"), 'green'); + login.set_status('{{ _("Success") }}', 'green'); document.cookie = "tmp_id=" + data.tmp_id; @@ -253,8 +253,8 @@ login.login_handlers = (function () { } } }, - 401: get_error_handler(__("Invalid Login. Try again.")), - 417: get_error_handler(__("Oops! Something went wrong")) + 401: get_error_handler('{{ _("Invalid Login. Try again.") }}'), + 417: get_error_handler('{{ _("Oops! Something went wrong") }}') }; return login_handlers; @@ -282,7 +282,7 @@ var verify_token = function (event) { args.otp = $("#login_token").val(); args.tmp_id = frappe.get_cookie('tmp_id'); if (!args.otp) { - frappe.msgprint(__("Login token required")); + frappe.msgprint('{{ _("Login token required") }}'); return false; } login.call(args); @@ -292,17 +292,9 @@ var verify_token = function (event) { var request_otp = function (r) { $('.login-content').empty(); - $('.login-content:visible').append($('
').attr({ 'id': 'twofactor_div' }).html( - '
\ -
\ - {{ _("Verification") }}\ -
\ -
\ - \ - \ -
')); - // add event handler for submit button - verify_token(); + $('.login-content:visible').append($('
').attr({ 'id': 'twofactor_div' }).html('{{ _("Verification") }}{{ _("Verify") }}')); + // add event handler for submit button + verify_token(); } var continue_otp_app = function (setup, qrcode) { @@ -310,11 +302,11 @@ var continue_otp_app = function (setup, qrcode) { var qrcode_div = $('
'); if (setup) { - direction = $('
').attr('id', 'qr_info').text(__("Enter Code displayed in OTP App.")); + direction = $('
').attr('id', 'qr_info').text('{{ _("Enter Code displayed in OTP App.") }}'); qrcode_div.append(direction); $('#otp_div').prepend(qrcode_div); } else { - direction = $('
').attr('id', 'qr_info').text(__("OTP setup using OTP App was not completed. Please contact Administrator.")); + direction = $('
').attr('id', 'qr_info').text('{{ _("OTP setup using OTP App was not completed. Please contact Administrator.") }}'); qrcode_div.append(direction); $('#otp_div').prepend(qrcode_div); } @@ -328,7 +320,7 @@ var continue_sms = function (setup, prompt) { sms_div.append(prompt) $('#otp_div').prepend(sms_div); } else { - direction = $('
').attr('id', 'qr_info').text(prompt || __("SMS was not sent. Please contact Administrator.")); + direction = $('
').attr('id', 'qr_info').text(prompt || '{{ _("SMS was not sent. Please contact Administrator.") }}'); sms_div.append(direction); $('#otp_div').prepend(sms_div) } @@ -342,7 +334,7 @@ var continue_email = function (setup, prompt) { email_div.append(prompt) $('#otp_div').prepend(email_div); } else { - var direction = $('
').attr('id', 'qr_info').text(prompt || __("Verification code email not sent. Please contact Administrator.")); + var direction = $('
').attr('id', 'qr_info').text(prompt || '{{ _("Verification code email not sent. Please contact Administrator.") }}'); email_div.append(direction); $('#otp_div').prepend(email_div); } From cfde23535854270dd00dcf1b265f60c8fd20b25b Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Sat, 12 Jun 2021 15:31:17 +0530 Subject: [PATCH 16/75] fix: Email sent from wrong account when user has access to only one account When user has access to only to one email account, they won't able to select 'From', causing the mail to be sent from default outgoing email account. --- frappe/public/js/frappe/views/communication.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index e2aaec553d..26fd8d667d 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -145,7 +145,7 @@ frappe.views.CommunicationComposer = class { ); }); - if (email_accounts.length > 1) { + if (email_accounts.length) { fields.unshift({ label: __("From"), fieldtype: "Select", From 1bf90d18ee8159554b152c72a5a0341d2560f462 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Tue, 15 Jun 2021 17:17:08 +0530 Subject: [PATCH 17/75] perf-fix: Remove meta from pickling overhead --- frappe/model/base_document.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index af696e116d..9447e60529 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -83,11 +83,15 @@ class BaseDocument(object): @property def meta(self): - if not hasattr(self, "_meta"): + if not getattr(self, "_meta", None): self._meta = frappe.get_meta(self.doctype) return self._meta + def __getstate__(self): + self._meta = None + return self.__dict__ + def update(self, d): """ Update multiple fields of a doctype using a dictionary of key-value pairs. From 23231fcbba400a0ad0af7b2f1cf9a5e10c49f83e Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Tue, 15 Jun 2021 19:01:15 +0200 Subject: [PATCH 18/75] fix: translation of email salutation --- frappe/public/js/frappe/views/communication.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index e2aaec553d..6c1885c458 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -728,7 +728,7 @@ frappe.views.CommunicationComposer = class { const SALUTATION_END_COMMENT = ""; if (this.real_name && !message.includes(SALUTATION_END_COMMENT)) { this.message = ` -

${__('Dear')} ${this.real_name},

+

${__('Dear {0},', [this.real_name], 'Salutation in new email')},

${SALUTATION_END_COMMENT}
${message} `; From ff9a99d7f85724ec77bfb8d84a45cac3e0566be6 Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Wed, 16 Jun 2021 22:04:44 +0530 Subject: [PATCH 19/75] fix: route to orignal form after creating a new document --- frappe/public/js/frappe/form/form.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 35ebf9274d..1001c2c411 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -446,6 +446,8 @@ frappe.ui.form.Form = class FrappeForm { () => this.refresh_fields(), // call trigger () => this.script_manager.trigger("refresh"), + // route back to the parent document + () => frappe.router.route(), // call onload post render for callbacks to be fired () => { if(this.cscript.is_onload) { From 3a943b5426b88e9b25b0f8462d0c5168be0b31ec Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 5 Jun 2021 14:51:07 +0530 Subject: [PATCH 20/75] refactor: Form Tour Class --- .../form_tour_step/form_tour_step.json | 4 +- frappe/public/js/frappe/form/form.js | 52 ++---------- frappe/public/js/frappe/form/form_tour.js | 79 +++++++++++++++++++ 3 files changed, 86 insertions(+), 49 deletions(-) create mode 100644 frappe/public/js/frappe/form/form_tour.js diff --git a/frappe/desk/doctype/form_tour_step/form_tour_step.json b/frappe/desk/doctype/form_tour_step/form_tour_step.json index a772a2498a..8b36b54db7 100644 --- a/frappe/desk/doctype/form_tour_step/form_tour_step.json +++ b/frappe/desk/doctype/form_tour_step/form_tour_step.json @@ -11,7 +11,7 @@ "position", "fieldname", "label", - "condition" + "next_step_condition" ], "fields": [ { @@ -73,7 +73,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-05-26 19:44:48.737453", + "modified": "2021-05-27 19:44:48.737453", "modified_by": "Administrator", "module": "Desk", "name": "Form Tour Step", diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index a24c6ab0d6..6ddbd089fa 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -12,6 +12,7 @@ import './script_manager'; import './script_helpers'; import './sidebar/form_sidebar'; import './footer/footer'; +import './form_tour'; frappe.ui.form.Controller = class FormController { constructor(opts) { @@ -152,6 +153,10 @@ frappe.ui.form.Form = class FrappeForm { parent: $('
').insertAfter(this.layout.wrapper.find('.form-message')) }); + this.tour = new frappe.ui.form.FormTour({ + frm: this + }); + // workflow state this.states = new frappe.ui.form.States({ frm: this @@ -1606,53 +1611,6 @@ frappe.ui.form.Form = class FrappeForm { }, 1000); } - show_tour(on_finish) { - const tour_info = frappe.tour[this.doctype]; - - if (!Array.isArray(tour_info)) { - return; - } - - const driver = new frappe.Driver({ - className: 'frappe-driver', - allowClose: false, - padding: 10, - overlayClickNext: true, - keyboardControl: true, - nextBtnText: 'Next', - prevBtnText: 'Previous', - opacity: 0.25 - }); - - this.layout.sections.forEach(section => section.collapse(false)); - - let steps = tour_info.map(step => { - let field = this.get_docfield(step.fieldname); - return { - element: `.frappe-control[data-fieldname='${step.fieldname}']`, - popover: { - title: step.title || field.label, - description: step.description, - position: step.position || 'bottom' - }, - onNext: () => { - const next_condition_satisfied = this.layout.evaluate_depends_on_value(step.next_step_condition || true); - if (!next_condition_satisfied) { - driver.preventMove(); - } - - if (!driver.hasNextStep()) { - on_finish && on_finish(); - } - } - }; - }); - - driver.defineSteps(steps); - frappe.router.on('change', () => driver.reset()); - driver.start(); - } - setup_docinfo_change_listener() { let doctype = this.doctype; let docname = this.docname; diff --git a/frappe/public/js/frappe/form/form_tour.js b/frappe/public/js/frappe/form/form_tour.js new file mode 100644 index 0000000000..e0cf694a5b --- /dev/null +++ b/frappe/public/js/frappe/form/form_tour.js @@ -0,0 +1,79 @@ +frappe.ui.form.FormTour = class FormTour { + constructor({ frm }) { + this.frm = frm; + this.driver_steps = []; + + this.init_driver(); + } + + init_driver() { + this.driver = new frappe.Driver({ + className: 'frappe-driver', + allowClose: false, + padding: 10, + overlayClickNext: true, + keyboardControl: true, + nextBtnText: 'Next', + prevBtnText: 'Previous', + opacity: 0.25 + }); + + frappe.router.on('change', () => this.driver.reset()); + this.frm.layout.sections.forEach(section => section.collapse(false)); + } + + async set_tour(tour_name, on_finish) { + this.tour = await frappe.db.get_doc('Form Tour', tour_name); + this.on_finish = on_finish; + this.build_steps(); + this.define_steps(); + } + + build_steps() { + this.tour.steps.forEach((step, idx) => { + const me = this; + const on_next = () => { + const next_condition_satisfied = this.frm.layout.evaluate_depends_on_value(step.next_step_condition || true); + + if (!next_condition_satisfied) { + me.driver.preventMove(); + } + + if (!me.driver.hasNextStep()) { + me.on_finish && me.on_finish(); + } + } + + const driver_step = this.get_step(step, on_next); + this.driver_steps.push(driver_step); + }); + } + + get_step(step_info, on_next) { + const field = this.frm.get_field(step_info.fieldname); + // if field is a child table field, `field` will be undefined + const element = field ? field.wrapper : `.frappe-control[data-fieldname='${step_info.fieldname}']`; + const title = step_info.title || field.df.label; + const description = step_info.description; + const position = step_info.position || 'bottom'; + return { + element, + popover: { title, description, position }, + onNext: on_next + }; + } + + define_steps(steps = []) { + if (steps.length == 0) { + steps = this.driver_steps; + } + this.driver.defineSteps(steps); + } + + start_tour() { + if (this.driver_steps.length == 0) { + return; + } + this.driver.start(); + } +}; \ No newline at end of file From 052ae6b53c80b5f8456dcc3022df3e39c04e8fbc Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 5 Jun 2021 15:08:40 +0530 Subject: [PATCH 21/75] feat: backward compatibility --- frappe/public/js/frappe/form/form_tour.js | 14 ++++++++++---- .../public/js/frappe/widgets/onboarding_widget.js | 14 ++++++++++---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/frappe/public/js/frappe/form/form_tour.js b/frappe/public/js/frappe/form/form_tour.js index e0cf694a5b..32f348163e 100644 --- a/frappe/public/js/frappe/form/form_tour.js +++ b/frappe/public/js/frappe/form/form_tour.js @@ -22,9 +22,15 @@ frappe.ui.form.FormTour = class FormTour { this.frm.layout.sections.forEach(section => section.collapse(false)); } - async set_tour(tour_name, on_finish) { - this.tour = await frappe.db.get_doc('Form Tour', tour_name); - this.on_finish = on_finish; + async init({ tour_name, on_finish }) { + if (tour_name) { + this.tour = await frappe.db.get_doc('Form Tour', tour_name); + } else { + this.tour = { steps: frappe.tour[this.frm.doctype] } + } + + if (on_finish) this.on_finish = on_finish; + this.build_steps(); this.define_steps(); } @@ -70,7 +76,7 @@ frappe.ui.form.FormTour = class FormTour { this.driver.defineSteps(steps); } - start_tour() { + start() { if (this.driver_steps.length == 0) { return; } diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js index e552a7dd55..b487c0134f 100644 --- a/frappe/public/js/frappe/widgets/onboarding_widget.js +++ b/frappe/public/js/frappe/widgets/onboarding_widget.js @@ -203,7 +203,7 @@ export default class OnboardingWidget extends Widget { frappe.route_hooks = {}; frappe.route_hooks.after_load = (frm) => { - frm.show_tour(() => { + const on_finish = () => { let msg_dialog = frappe.msgprint({ message: __("Let's take you back to onboarding"), title: __("Great Job"), @@ -217,7 +217,10 @@ export default class OnboardingWidget extends Widget { label: () => __("Continue"), }, }); - }); + }; + frm.tour + .init({ on_finish }) + .then(() => frm.tour.start()); }; frappe.set_route(route); @@ -290,12 +293,15 @@ export default class OnboardingWidget extends Widget { frappe.route_hooks = {}; frappe.route_hooks.after_load = (frm) => { - frm.show_tour(() => { + const on_finish = () => { frappe.msgprint({ message: __("Awesome, now try making an entry yourself"), title: __("Great"), }); - }); + }; + frm.tour + .init({ on_finish }) + .then(() => frm.tour.start()); }; let callback = () => { From a5250ccd82920938ae26cc4d7ccc9cbe542d1c61 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 5 Jun 2021 22:35:21 +0530 Subject: [PATCH 22/75] feat: Form Tour for Grids --- frappe/desk/doctype/form_tour/form_tour.js | 55 ++++ frappe/desk/doctype/form_tour/form_tour.json | 13 +- frappe/desk/doctype/form_tour/form_tour.py | 14 +- .../form_tour_step/form_tour_step.json | 63 +++- frappe/public/js/frappe/desk.js | 4 +- frappe/public/js/frappe/form/form_tour.js | 288 ++++++++++++++---- frappe/public/js/frappe/utils/utils.js | 10 +- frappe/public/scss/desk/driver.scss | 12 + 8 files changed, 381 insertions(+), 78 deletions(-) diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js index 94c6806b50..2a7050d5e8 100644 --- a/frappe/desk/doctype/form_tour/form_tour.js +++ b/frappe/desk/doctype/form_tour/form_tour.js @@ -20,5 +20,60 @@ frappe.ui.form.on('Form Tour', { } }; }); + + frm.set_query("parent_field", "steps", function() { + return { + query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list", + filters: { + doctype: frm.doc.reference_doctype, + fieldtype: "Table", + hidden: 0, + } + }; + }); + + frm.trigger('reference_doctype'); + }, + + reference_doctype(frm) { + if (!frm.doc.reference_doctype) return; + + frappe.db.get_list('DocField', { + filters: { + parent: frm.doc.reference_doctype, + parenttype: 'DocType', + fieldtype: 'Table' + }, + fields: ['options'] + }).then(res => { + if (Array.isArray(res)) { + frm.child_doctypes = res.map(r => r.options); + } + }); + } }); + +frappe.ui.form.on('Form Tour Step', { + parent_field(frm, cdt, cdn) { + const child_row = locals[cdt][cdn]; + frappe.model.set_value(cdt, cdn, 'field', ''); + const field_control = get_child_field("steps", cdn, "field"); + field_control.get_query = function() { + return { + query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list", + filters: { + doctype: child_row.child_doctype, + hidden: 0 + } + }; + } + } +}); + +function get_child_field(child_table, child_name, fieldname) { + // gets the field from grid row form + const grid = cur_frm.fields_dict[child_table].grid; + const grid_row = grid.grid_rows_by_docname[child_name]; + return grid_row.grid_form.fields_dict[fieldname]; +} \ No newline at end of file diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json index 8e09a5d63a..c28dd5762c 100644 --- a/frappe/desk/doctype/form_tour/form_tour.json +++ b/frappe/desk/doctype/form_tour/form_tour.json @@ -9,6 +9,7 @@ "title", "reference_doctype", "completed", + "save_on_complete", "section_break_3", "steps" ], @@ -19,10 +20,10 @@ "in_list_view": 1, "label": "Reference Document", "options": "DocType", - "reqd": 1, - "unique": 1 + "reqd": 1 }, { + "depends_on": "reference_doctype", "fieldname": "steps", "fieldtype": "Table", "label": "Steps", @@ -46,11 +47,17 @@ "label": "Title", "reqd": 1, "unique": 1 + }, + { + "default": "0", + "fieldname": "save_on_complete", + "fieldtype": "Check", + "label": "Save on Completion" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-05-26 19:36:59.093753", + "modified": "2021-06-05 21:39:52.416111", "modified_by": "Administrator", "module": "Desk", "name": "Form Tour", diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py index dd762395c4..8d6b1b2a7c 100644 --- a/frappe/desk/doctype/form_tour/form_tour.py +++ b/frappe/desk/doctype/form_tour/form_tour.py @@ -16,17 +16,23 @@ def get_docfield_list(doctype, txt, searchfield, start, page_len, filters): ['fieldtype', 'like', '%' + txt + '%'] ] - parent_doctype = filters.pop('doctype') - excluded_fieldtypes = ['Column Break'] - excluded_fieldtypes += filters.get('excluded_fieldtypes', []) + parent_doctype = filters.get('doctype') + fieldtype = filters.get('fieldtype') + if not fieldtype: + excluded_fieldtypes = ['Column Break'] + excluded_fieldtypes += filters.get('excluded_fieldtypes', []) + fieldtype_filter = ['not in', excluded_fieldtypes] + else: + fieldtype_filter = fieldtype docfields = frappe.get_all( doctype, fields=["name as value", "label", "fieldtype"], - filters={'parent': parent_doctype, 'fieldtype': ['not in', excluded_fieldtypes]}, + filters={'parent': parent_doctype, 'fieldtype': fieldtype_filter}, or_filters=or_filters, limit_start=start, limit_page_length=page_len, + order_by="idx", as_list=1, ) return docfields diff --git a/frappe/desk/doctype/form_tour_step/form_tour_step.json b/frappe/desk/doctype/form_tour_step/form_tour_step.json index 8b36b54db7..de2ec18f6e 100644 --- a/frappe/desk/doctype/form_tour_step/form_tour_step.json +++ b/frappe/desk/doctype/form_tour_step/form_tour_step.json @@ -4,6 +4,9 @@ "doctype": "DocType", "engine": "InnoDB", "field_order": [ + "is_table_field", + "section_break_2", + "parent_field", "field", "title", "description", @@ -11,7 +14,11 @@ "position", "fieldname", "label", - "next_step_condition" + "has_next_condition", + "next_step_condition", + "section_break_13", + "fieldtype", + "child_doctype" ], "fields": [ { @@ -30,6 +37,7 @@ "reqd": 1 }, { + "depends_on": "eval: (!doc.is_table_field || (doc.is_table_field && doc.parent_field))", "fieldname": "field", "fieldtype": "Link", "label": "Field", @@ -64,16 +72,65 @@ "options": "Left\nLeft Center\nLeft Bottom\nTop\nTop Center\nTop Right\nRight\nRight Center\nRight Bottom\nBottom\nBottom Center\nBottom Right\nMid Center" }, { + "depends_on": "has_next_condition", "fieldname": "next_step_condition", "fieldtype": "Code", "label": "Next Step Condition", + "oldfieldname": "condition", "options": "JS" + }, + { + "default": "0", + "fieldname": "has_next_condition", + "fieldtype": "Check", + "label": "Has Next Condition" + }, + { + "default": "0", + "fetch_from": "field.fieldtype", + "fieldname": "fieldtype", + "fieldtype": "Data", + "hidden": 1, + "label": "Fieldtype", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_table_field", + "fieldtype": "Check", + "label": "Is Table Field" + }, + { + "fieldname": "section_break_2", + "fieldtype": "Section Break" + }, + { + "depends_on": "is_table_field", + "fieldname": "parent_field", + "fieldtype": "Link", + "label": "Parent Field", + "mandatory_depends_on": "is_table_field", + "options": "DocField" + }, + { + "fieldname": "section_break_13", + "fieldtype": "Section Break", + "hidden": 1, + "label": "Hidden Fields" + }, + { + "fetch_from": "parent_field.options", + "fieldname": "child_doctype", + "fieldtype": "Data", + "hidden": 1, + "label": "Child Doctype", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-05-27 19:44:48.737453", + "modified": "2021-06-05 21:45:19.938002", "modified_by": "Administrator", "module": "Desk", "name": "Form Tour Step", @@ -82,4 +139,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 46812f5fb6..65c0139b65 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -607,9 +607,7 @@ frappe.Application = class Application { let doc = JSON.parse(pasted_data); if (doc.doctype) { e.preventDefault(); - let sleep = (time) => { - return new Promise((resolve) => setTimeout(resolve, time)); - }; + const sleep = frappe.utils.sleep; frappe.dom.freeze(__('Creating {0}', [doc.doctype]) + '...'); // to avoid abrupt UX diff --git a/frappe/public/js/frappe/form/form_tour.js b/frappe/public/js/frappe/form/form_tour.js index 32f348163e..148b6a91b0 100644 --- a/frappe/public/js/frappe/form/form_tour.js +++ b/frappe/public/js/frappe/form/form_tour.js @@ -1,13 +1,13 @@ frappe.ui.form.FormTour = class FormTour { - constructor({ frm }) { - this.frm = frm; - this.driver_steps = []; + constructor({ frm }) { + this.frm = frm; + this.driver_steps = []; - this.init_driver(); - } + this.init_driver(); + } - init_driver() { - this.driver = new frappe.Driver({ + init_driver() { + this.driver = new frappe.Driver({ className: 'frappe-driver', allowClose: false, padding: 10, @@ -15,71 +15,233 @@ frappe.ui.form.FormTour = class FormTour { keyboardControl: true, nextBtnText: 'Next', prevBtnText: 'Previous', - opacity: 0.25 + opacity: 0.25, + onHighlighted: (step) => { + // if last step is to save, then attach a listener to save button + if (step.options.is_save_step) { + $(step.options.element).one('click', () => this.driver.reset()); + } + + // focus on input + const $input = $(step.node).find('input').get(0); + if ($input) + frappe.utils.sleep(200).then(() => $input.focus()); + } }); - frappe.router.on('change', () => this.driver.reset()); - this.frm.layout.sections.forEach(section => section.collapse(false)); - } + frappe.router.on('change', () => this.driver.reset()); + this.frm.layout.sections.forEach(section => section.collapse(false)); + } - async init({ tour_name, on_finish }) { - if (tour_name) { - this.tour = await frappe.db.get_doc('Form Tour', tour_name); - } else { - this.tour = { steps: frappe.tour[this.frm.doctype] } - } - - if (on_finish) this.on_finish = on_finish; + async init({ tour_name, on_finish }) { + if (tour_name) { + this.tour = await frappe.db.get_doc('Form Tour', tour_name); + } else { + this.tour = { steps: frappe.tour[this.frm.doctype] } + } + + if (on_finish) this.on_finish = on_finish; - this.build_steps(); - this.define_steps(); - } + this.build_steps(); + this.update_driver_steps(); + } - build_steps() { - this.tour.steps.forEach((step, idx) => { - const me = this; - const on_next = () => { - const next_condition_satisfied = this.frm.layout.evaluate_depends_on_value(step.next_step_condition || true); + build_steps() { + this.driver_steps = []; + this.tour.steps.forEach((step) => { + const on_next = () => { + if (!this.is_next_condition_satisfied(step)) { + this.driver.preventMove(); + } - if (!next_condition_satisfied) { - me.driver.preventMove(); - } + if (!this.driver.hasNextStep()) { + this.on_finish && this.on_finish(); + } + } - if (!me.driver.hasNextStep()) { - me.on_finish && me.on_finish(); - } - } - - const driver_step = this.get_step(step, on_next); + const driver_step = this.get_step(step, on_next); this.driver_steps.push(driver_step); - }); - } + + if (step.fieldtype == 'Table') this.handle_table_step(step); + if (step.is_table_field) this.handle_child_table_step(step); + }); - get_step(step_info, on_next) { - const field = this.frm.get_field(step_info.fieldname); - // if field is a child table field, `field` will be undefined - const element = field ? field.wrapper : `.frappe-control[data-fieldname='${step_info.fieldname}']`; - const title = step_info.title || field.df.label; - const description = step_info.description; - const position = step_info.position || 'bottom'; - return { - element, - popover: { title, description, position }, - onNext: on_next - }; - } + if (this.tour.save_on_complete) { + this.add_step_to_save(); + } + } - define_steps(steps = []) { - if (steps.length == 0) { - steps = this.driver_steps; - } - this.driver.defineSteps(steps); - } + is_next_condition_satisfied(step) { + const form = step.is_table_field ? this.frm.cur_grid.grid_form : this.frm; + return form.layout.evaluate_depends_on_value(step.next_step_condition || true); + } - start() { - if (this.driver_steps.length == 0) { - return; - } - this.driver.start(); - } + get_step(step_info, on_next) { + const { name, fieldname, title, description, position } = step_info; + const field = this.frm.get_field(fieldname); + // if field is a child table field, `field` will be undefined + const element = field ? field.wrapper : `.frappe-control[data-fieldname='${fieldname}']`; + + return { + element, + name, + popover: { title, description, position }, + onNext: on_next + }; + } + + update_driver_steps(steps = []) { + if (steps.length == 0) { + steps = this.driver_steps; + } + this.driver.defineSteps(steps); + } + + start(idx = 0) { + if (this.driver_steps.length == 0) { + return; + } + this.driver.start(idx); + } + + get_next_step() { + // returns the next step only if driver is active + if (this.driver.isActivated & this.driver.hasNextStep()) { + const current_step = this.driver.currentStep; + return this.driver.steps[current_step + 1]; + } + return; + } + + handle_table_step(step_info) { + const is_last_step = step_info.idx == this.tour.steps.length; + + if (!is_last_step) { + // if next step field is inside currently highlighted table field + // then check if there is a row -> if not, then prompt to add row + // then edit the first row and hightlight next step + + const curr_step = step_info; + const next_step = this.tour.steps[curr_step.idx]; + const is_next_field_in_curr_table = next_step.parent_field == curr_step.field; + + if (!is_next_field_in_curr_table) return; + + const table_has_rows = this.frm.doc[curr_step.fieldname].length > 0; + if (table_has_rows) { + // table already has rows + // then just edit the first one on next step + const curr_driver_step = this.driver_steps.find(s => s.name == curr_step.name); + curr_driver_step.onNext = () => { + if (this.is_next_condition_satisfied(curr_step)) { + this.expand_row_and_proceed(curr_step, curr_step.idx); + } else { + this.driver.preventMove(); + } + }; + this.update_driver_steps(); + + } else { + this.add_new_row_step(curr_step); + } + } + } + + add_new_row_step(step) { + const $add_row = `.frappe-control[data-fieldname='${step.fieldname}'] .grid-add-row`; + const add_row_step = { + element: $add_row, + popover: { title: __("Add a Row"), description: "" }, + onNext: () => { + if (!cur_frm.cur_grid) { + this.driver.preventMove(); + } + } + }; + this.driver_steps.push(add_row_step); + + // setup a listener on add row button + // so, once the row is added, move to next step automatically + $($add_row).one('click', () => { + this.expand_row_and_proceed(step, step.idx + 1); // +1 since add row step is added + }); + } + + expand_row_and_proceed(step, start_from) { + this.open_first_row_of(step.fieldname); + this.update_driver_steps(); // need to define again, since driver.js only considers steps which are inside DOM + frappe.utils.sleep(300).then(() => this.driver.start(start_from)) + } + + open_first_row_of(fieldname) { + this.frm.fields_dict[fieldname].grid.grid_rows[0].toggle_view(); + + // setup a listener on close row button + // so, once the row is closed, move to next step automatically + const $close_row = '.grid-row-open .grid-collapse-row'; + $($close_row).one('click', () => { + const next_step = this.get_next_step(); + const next_element = next_step.options.is_save_step ? null : next_step.node; + + frappe.utils.scroll_to(next_element, true, 150, null, () => { + this.driver.moveNext(); + frappe.flags.disable_auto_scroll = false; + }); + frappe.flags.disable_auto_scroll = true; + }); + } + + handle_child_table_step(step_info) { + const is_last_step = step_info.idx == this.tour.steps.length; + + if (!is_last_step) { + const curr_step = step_info; + const next_step = this.tour.steps[curr_step.idx]; + const field = this.frm.get_field(next_step.fieldname); + + if (!field) return; + + // next step highlights parent field + // so, add a step to prompt user to collapse grid form + this.add_collapse_row_step(); + + } else if (this.tour.save_on_complete) { + // if last step & save on complete is checked + // add a step to prompt user to collapse grid form + // to be able to save as a last step + this.add_collapse_row_step(); + } + } + + add_collapse_row_step() { + const $close_row = '.grid-row-open .grid-collapse-row'; + const close_row_step = { + element: $close_row, + popover: { title: __("Collapse"), description: "", position: "left" }, + onNext: () => { + if (cur_frm.cur_grid) { + this.driver.preventMove(); + } + } + }; + this.driver_steps.push(close_row_step); + } + + add_step_to_save() { + const $save_btn = '.standard-actions .primary-action'; + const save_step = { + element: $save_btn, + is_save_step: true, + allowClose: false, + overlayClickNext: false, + popover: { + title: __("Save"), + description: "", + position: "left", + doneBtnText: __("Save") + } + }; + this.driver_steps.push(save_step); + frappe.ui.form.on(this.frm.doctype, 'after_save', () => this.on_finish && this.on_finish()); + } }; \ No newline at end of file diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 484d9c65f1..8d3d0edd53 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -268,7 +268,9 @@ Object.assign(frappe.utils, {

'); return content.html(); }, - scroll_to: function(element, animate=true, additional_offset, element_to_be_scrolled) { + scroll_to: function(element, animate=true, additional_offset, element_to_be_scrolled, callback) { + if (frappe.flags.disable_auto_scroll) return; + element_to_be_scrolled = element_to_be_scrolled || $("html, body"); let scroll_top = 0; if (element) { @@ -289,7 +291,7 @@ Object.assign(frappe.utils, { } if (animate) { - element_to_be_scrolled.animate({ scrollTop: scroll_top }); + element_to_be_scrolled.animate({ scrollTop: scroll_top }).promise().then(callback); } else { element_to_be_scrolled.scrollTop(scroll_top); } @@ -1319,5 +1321,9 @@ Object.assign(frappe.utils, { let e = clipboard_paste_event; let clipboard_data = e.clipboardData || window.clipboardData || e.originalEvent.clipboardData; return clipboard_data.getData('Text'); + }, + + sleep(time) { + return new Promise((resolve) => setTimeout(resolve, time)); } }); diff --git a/frappe/public/scss/desk/driver.scss b/frappe/public/scss/desk/driver.scss index ddf594393f..4135d9667b 100644 --- a/frappe/public/scss/desk/driver.scss +++ b/frappe/public/scss/desk/driver.scss @@ -64,4 +64,16 @@ div#driver-popover-item { input.driver-highlighted-element { background-color: var(--fg-color); +} + +.driver-fix-stacking { + z-index: auto !important; + position: unset !important; + opacity: 1.0 !important; + transform: none !important; + filter: none !important; + perspective: none !important; + transform-style: flat !important; + transform-box: border-box !important; + will-change: unset !important; } \ No newline at end of file From 17b3c701ed0555c0a16717d3663c1d99497997c0 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sun, 6 Jun 2021 21:13:45 +0530 Subject: [PATCH 23/75] feat: standard form tours --- frappe/desk/doctype/form_tour/form_tour.js | 22 +++++++++++++++ frappe/desk/doctype/form_tour/form_tour.json | 27 ++++++++++++------- frappe/desk/doctype/form_tour/form_tour.py | 25 ++++++++++++++++- .../form_tour_step/form_tour_step.json | 13 +++++++-- frappe/model/sync.py | 2 +- 5 files changed, 76 insertions(+), 13 deletions(-) diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js index 2a7050d5e8..3f1e983868 100644 --- a/frappe/desk/doctype/form_tour/form_tour.js +++ b/frappe/desk/doctype/form_tour/form_tour.js @@ -3,6 +3,28 @@ frappe.ui.form.on('Form Tour', { setup: function(frm) { + if (!frm.doc.is_standard) { + frm.trigger('setup_queries'); + } + }, + + refresh(frm) { + if (frm.doc.is_standard && !frappe.boot.developer_mode) { + frm.trigger("disable_form"); + } + }, + + disable_form: function(frm) { + frm.set_read_only(); + frm.fields + .filter((field) => field.has_input) + .forEach((field) => { + frm.set_df_property(field.df.fieldname, "read_only", "1"); + }); + frm.disable_save(); + }, + + setup_queries(frm) { frm.set_query("reference_doctype", function() { return { filters: { diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json index c28dd5762c..e4ea528fcc 100644 --- a/frappe/desk/doctype/form_tour/form_tour.json +++ b/frappe/desk/doctype/form_tour/form_tour.json @@ -8,7 +8,8 @@ "field_order": [ "title", "reference_doctype", - "completed", + "module", + "is_standard", "save_on_complete", "section_break_3", "steps" @@ -30,13 +31,6 @@ "options": "Form Tour Step", "reqd": 1 }, - { - "default": "0", - "depends_on": "eval: doc.__islocal != 1", - "fieldname": "completed", - "fieldtype": "Check", - "label": "Mark as Completed" - }, { "fieldname": "section_break_3", "fieldtype": "Section Break" @@ -53,11 +47,26 @@ "fieldname": "save_on_complete", "fieldtype": "Check", "label": "Save on Completion" + }, + { + "default": "0", + "fieldname": "is_standard", + "fieldtype": "Check", + "label": "Is Standard" + }, + { + "fetch_from": "reference_doctype.module", + "fieldname": "module", + "fieldtype": "Link", + "hidden": 1, + "label": "Module", + "options": "Module Def", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-06-05 21:39:52.416111", + "modified": "2021-06-06 20:32:54.068774", "modified_by": "Administrator", "module": "Desk", "name": "Form Tour", diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py index 8d6b1b2a7c..7d1cd4637e 100644 --- a/frappe/desk/doctype/form_tour/form_tour.py +++ b/frappe/desk/doctype/form_tour/form_tour.py @@ -3,9 +3,32 @@ import frappe from frappe.model.document import Document +from frappe.modules.export_file import export_to_files class FormTour(Document): - pass + def before_insert(self): + if not self.is_standard: return + + # while syncing, set proper docfield reference + for d in self.steps: + if not frappe.db.exists('DocField', d.field): + d.field = frappe.db.get_value('DocField', { + 'fieldname': d.fieldname, 'parent': self.reference_doctype, 'fieldtype': d.fieldtype + }, "name") + + if d.is_table_field and not frappe.db.exists('DocField', d.parent_field): + d.parent_field = frappe.db.get_value('DocField', { + 'fieldname': d.parent_fieldname, 'parent': self.reference_doctype, 'fieldtype': 'Table' + }, "name") + + def on_update(self): + if frappe.conf.developer_mode and self.is_standard: + export_to_files([['Form Tour', self.name]], self.module) + + def before_export(self, doc): + for d in doc.steps: + d.field = "" + d.parent_field = "" @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs diff --git a/frappe/desk/doctype/form_tour_step/form_tour_step.json b/frappe/desk/doctype/form_tour_step/form_tour_step.json index de2ec18f6e..3b6c91a208 100644 --- a/frappe/desk/doctype/form_tour_step/form_tour_step.json +++ b/frappe/desk/doctype/form_tour_step/form_tour_step.json @@ -12,11 +12,12 @@ "description", "column_break_2", "position", - "fieldname", "label", "has_next_condition", "next_step_condition", "section_break_13", + "fieldname", + "parent_fieldname", "fieldtype", "child_doctype" ], @@ -125,12 +126,20 @@ "hidden": 1, "label": "Child Doctype", "read_only": 1 + }, + { + "fetch_from": "parent_field.fieldname", + "fieldname": "parent_fieldname", + "fieldtype": "Data", + "hidden": 1, + "label": "Parent Fieldname", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-06-05 21:45:19.938002", + "modified": "2021-06-06 20:52:21.076972", "modified_by": "Administrator", "module": "Desk", "name": "Form Tour Step", diff --git a/frappe/model/sync.py b/frappe/model/sync.py index 28f9deb25d..836f70dd55 100644 --- a/frappe/model/sync.py +++ b/frappe/model/sync.py @@ -82,7 +82,7 @@ def get_doc_files(files, start_path): document_types = ['doctype', 'page', 'report', 'dashboard_chart_source', 'print_format', 'website_theme', 'web_form', 'web_template', 'notification', 'print_style', 'data_migration_mapping', 'data_migration_plan', 'workspace', - 'onboarding_step', 'module_onboarding'] + 'onboarding_step', 'module_onboarding', 'form_tour'] for doctype in document_types: doctype_path = os.path.join(start_path, doctype) From a81d4fd4ebf638ada949541359feb68a4a28ee22 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 16 Jun 2021 13:32:55 +0530 Subject: [PATCH 24/75] fix: sider issues --- frappe/desk/doctype/form_tour/form_tour.js | 2 +- frappe/desk/doctype/form_tour/form_tour.py | 3 ++- frappe/public/js/frappe/form/dashboard.js | 2 +- frappe/public/js/frappe/form/form_tour.js | 8 ++++---- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js index 3f1e983868..d394a10757 100644 --- a/frappe/desk/doctype/form_tour/form_tour.js +++ b/frappe/desk/doctype/form_tour/form_tour.js @@ -89,7 +89,7 @@ frappe.ui.form.on('Form Tour Step', { hidden: 0 } }; - } + }; } }); diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py index 7d1cd4637e..dbc667ce28 100644 --- a/frappe/desk/doctype/form_tour/form_tour.py +++ b/frappe/desk/doctype/form_tour/form_tour.py @@ -7,7 +7,8 @@ from frappe.modules.export_file import export_to_files class FormTour(Document): def before_insert(self): - if not self.is_standard: return + if not self.is_standard: + return # while syncing, set proper docfield reference for d in self.steps: diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index eb7a6edc5d..6833f68073 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -179,7 +179,7 @@ frappe.ui.form.Dashboard = class FormDashboard { return; } this.render_links(); - this.set_open_count(); + // this.set_open_count(); show = true; } diff --git a/frappe/public/js/frappe/form/form_tour.js b/frappe/public/js/frappe/form/form_tour.js index 148b6a91b0..f84dc6a5ec 100644 --- a/frappe/public/js/frappe/form/form_tour.js +++ b/frappe/public/js/frappe/form/form_tour.js @@ -37,7 +37,7 @@ frappe.ui.form.FormTour = class FormTour { if (tour_name) { this.tour = await frappe.db.get_doc('Form Tour', tour_name); } else { - this.tour = { steps: frappe.tour[this.frm.doctype] } + this.tour = { steps: frappe.tour[this.frm.doctype] }; } if (on_finish) this.on_finish = on_finish; @@ -51,13 +51,13 @@ frappe.ui.form.FormTour = class FormTour { this.tour.steps.forEach((step) => { const on_next = () => { if (!this.is_next_condition_satisfied(step)) { - this.driver.preventMove(); + this.driver.preventMove(); } if (!this.driver.hasNextStep()) { this.on_finish && this.on_finish(); } - } + }; const driver_step = this.get_step(step, on_next); this.driver_steps.push(driver_step); @@ -170,7 +170,7 @@ frappe.ui.form.FormTour = class FormTour { expand_row_and_proceed(step, start_from) { this.open_first_row_of(step.fieldname); this.update_driver_steps(); // need to define again, since driver.js only considers steps which are inside DOM - frappe.utils.sleep(300).then(() => this.driver.start(start_from)) + frappe.utils.sleep(300).then(() => this.driver.start(start_from)); } open_first_row_of(fieldname) { From a754d451769ab0a237f19597c09392466577dfc2 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 19 Jun 2021 19:59:03 +0530 Subject: [PATCH 25/75] feat: custom button to jump to tour --- frappe/desk/doctype/form_tour/form_tour.js | 24 +++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js index d394a10757..e5ea373c4c 100644 --- a/frappe/desk/doctype/form_tour/form_tour.js +++ b/frappe/desk/doctype/form_tour/form_tour.js @@ -3,7 +3,7 @@ frappe.ui.form.on('Form Tour', { setup: function(frm) { - if (!frm.doc.is_standard) { + if (!frm.doc.is_standard || frappe.boot.developer_mode) { frm.trigger('setup_queries'); } }, @@ -12,6 +12,23 @@ frappe.ui.form.on('Form Tour', { if (frm.doc.is_standard && !frappe.boot.developer_mode) { frm.trigger("disable_form"); } + + frm.add_custom_button(__('Show Tour'), async () => { + const issingle = await check_if_single(frm.doc.reference_doctype); + + if (issingle) { + frappe.set_route('Form', frm.doc.reference_doctype) + } else { + const new_name = 'new-' + frappe.scrub(frm.doc.reference_doctype) + '-1'; + frappe.set_route('Form', frm.doc.reference_doctype, new_name); + } + frappe.utils.sleep(500).then(() => { + const tour_name = frm.doc.name; + cur_frm.tour + .init({ tour_name }) + .then(() => cur_frm.tour.start()); + }); + }); }, disable_form: function(frm) { @@ -98,4 +115,9 @@ function get_child_field(child_table, child_name, fieldname) { const grid = cur_frm.fields_dict[child_table].grid; const grid_row = grid.grid_rows_by_docname[child_name]; return grid_row.grid_form.fields_dict[fieldname]; +} + +async function check_if_single(doctype) { + const { message } = await frappe.db.get_value('DocType', doctype, 'issingle'); + return message.issingle || 0; } \ No newline at end of file From 300a05e015c911e2d713d8f59eeb767acee5cfdf Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Mon, 21 Jun 2021 22:31:39 +0530 Subject: [PATCH 26/75] fix: rename should reroute only when --- frappe/public/js/frappe/form/form.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 1001c2c411..698902e209 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -446,8 +446,6 @@ frappe.ui.form.Form = class FrappeForm { () => this.refresh_fields(), // call trigger () => this.script_manager.trigger("refresh"), - // route back to the parent document - () => frappe.router.route(), // call onload post render for callbacks to be fired () => { if(this.cscript.is_onload) { @@ -989,7 +987,7 @@ frappe.ui.form.Form = class FrappeForm { } frappe.re_route[frappe.router.get_sub_path()] = `${encodeURIComponent(frappe.router.slug(this.doctype))}/${encodeURIComponent(name)}`; - frappe.set_route('Form', this.doctype, name); + !frappe._from_link && frappe.set_route('Form', this.doctype, name); } // ACTIONS From 6ad15231617579b5b62c292884a2a0a039eb6886 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 23 Jun 2021 14:18:38 +0530 Subject: [PATCH 27/75] fix: table field & primary action selector --- frappe/public/js/frappe/form/form_tour.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/frappe/public/js/frappe/form/form_tour.js b/frappe/public/js/frappe/form/form_tour.js index f84dc6a5ec..7f7ec9ce4f 100644 --- a/frappe/public/js/frappe/form/form_tour.js +++ b/frappe/public/js/frappe/form/form_tour.js @@ -77,15 +77,18 @@ frappe.ui.form.FormTour = class FormTour { } get_step(step_info, on_next) { - const { name, fieldname, title, description, position } = step_info; + const { name, fieldname, title, description, position, is_table_field } = step_info; const field = this.frm.get_field(fieldname); - // if field is a child table field, `field` will be undefined - const element = field ? field.wrapper : `.frappe-control[data-fieldname='${fieldname}']`; + let element = field ? field.wrapper : `.frappe-control[data-fieldname='${fieldname}']`; + + if (is_table_field) { + element = `.grid-row-open .frappe-control[data-fieldname='${fieldname}']`; + } return { element, name, - popover: { title, description, position }, + popover: { title, description, position: frappe.router.slug(position) }, onNext: on_next }; } @@ -126,8 +129,9 @@ frappe.ui.form.FormTour = class FormTour { const is_next_field_in_curr_table = next_step.parent_field == curr_step.field; if (!is_next_field_in_curr_table) return; - - const table_has_rows = this.frm.doc[curr_step.fieldname].length > 0; + + const rows = this.frm.doc[curr_step.fieldname]; + const table_has_rows = rows && rows.length > 0; if (table_has_rows) { // table already has rows // then just edit the first one on next step @@ -228,7 +232,8 @@ frappe.ui.form.FormTour = class FormTour { } add_step_to_save() { - const $save_btn = '.standard-actions .primary-action'; + const page_id = `#page-${this.frm.doctype}`; + const $save_btn = `${page_id} .standard-actions .primary-action`; const save_step = { element: $save_btn, is_save_step: true, From ef4c6223a8e3f5de847e567fce096eaad9c750c9 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 23 Jun 2021 14:19:06 +0530 Subject: [PATCH 28/75] feat: add ui tests --- cypress/integration/form_tour.js | 88 ++++++++++++++++++++++++++++++++ frappe/tests/ui_test_helpers.py | 49 ++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 cypress/integration/form_tour.js diff --git a/cypress/integration/form_tour.js b/cypress/integration/form_tour.js new file mode 100644 index 0000000000..e1544a4cdf --- /dev/null +++ b/cypress/integration/form_tour.js @@ -0,0 +1,88 @@ +context('Form Tour', () => { + before(() => { + cy.login(); + cy.visit('/app/form-tour'); + return cy.window().its('frappe').then(frappe => { + return frappe.call("frappe.tests.ui_test_helpers.create_form_tour"); + }); + }); + + const open_test_form_tour = () => { + cy.visit('/app/form-tour/Test Form Tour'); + cy.get('button[data-label="Show%20Tour"]').should('be.visible').and('contain', 'Show Tour').as('show_tour'); + cy.get('@show_tour').click(); + cy.wait(500); + cy.url().should('include', '/app/contact'); + }; + + it('jump to a form tour', open_test_form_tour); + + it('navigates a form tour', () => { + open_test_form_tour(); + + cy.get('#driver-popover-item').should('be.visible'); + cy.get('.frappe-control[data-fieldname="first_name"]').as('first_name'); + cy.get('@first_name').should('have.class', 'driver-highlighted-element'); + cy.get('.driver-next-btn').as('next_btn'); + + // next btn shouldn't move to next step, if first name is not entered + cy.get('@next_btn').click(); + cy.wait(500); + cy.get('@first_name').should('have.class', 'driver-highlighted-element'); + + // after filling the field, next step should be highlighted + cy.fill_field('first_name', 'Test Name', 'Data') + cy.wait(500); + cy.get('@next_btn').click(); + cy.wait(500); + + // assert field is highlighted + cy.get('.frappe-control[data-fieldname="last_name"]').as('last_name'); + cy.get('@last_name').should('have.class', 'driver-highlighted-element'); + + // after filling the field, next step should be highlighted + cy.fill_field('last_name', 'Test Last Name', 'Data') + cy.wait(500); + cy.get('@next_btn').click(); + cy.wait(500); + + // assert field is highlighted + cy.get('.frappe-control[data-fieldname="phone_nos"]').as('phone_nos'); + cy.get('@phone_nos').should('have.class', 'driver-highlighted-element'); + + // move to next step + cy.wait(500); + cy.get('@next_btn').click(); + cy.wait(500); + + // assert add row btn is highlighted + cy.get('@phone_nos').find('.grid-add-row').as('add_row'); + cy.get('@add_row').should('have.class', 'driver-highlighted-element'); + + // add a row & move to next step + cy.wait(500); + cy.get('@add_row').click(); + cy.wait(500); + + // assert table field is highlighted + cy.get('.grid-row-open .frappe-control[data-fieldname="phone"]').as('phone'); + cy.get('@phone').should('have.class', 'driver-highlighted-element'); + // enter value in a table field + cy.fill_table_field('phone_nos', '1', 'phone', '1234567890'); + + // move to collapse row step + cy.wait(500); + cy.get('@next_btn').click(); + cy.wait(500); + + // collapse row + cy.get('.grid-row-open .grid-collapse-row').click(); + cy.wait(500); + + // assert save btn is highlighted + cy.get('.primary-action').should('have.class', 'driver-highlighted-element'); + cy.get('@next_btn').should('contain', 'Save'); + + }); +}); + \ No newline at end of file diff --git a/frappe/tests/ui_test_helpers.py b/frappe/tests/ui_test_helpers.py index 7670f99698..cd441f8f10 100644 --- a/frappe/tests/ui_test_helpers.py +++ b/frappe/tests/ui_test_helpers.py @@ -131,3 +131,52 @@ def insert_contact(first_name, phone_number): }) doc.append('phone_nos', {'phone': phone_number}) doc.insert() + +@frappe.whitelist() +def create_form_tour(): + if frappe.db.exists('Form Tour', {'name': 'Test Form Tour'}): + return + + def get_docfield_name(filters): + return frappe.db.get_value('DocField', filters, "name") + + tour = frappe.get_doc({ + 'doctype': 'Form Tour', + 'title': 'Test Form Tour', + 'reference_doctype': 'Contact', + 'save_on_complete': 1, + 'steps': [{ + "title": "Test Title 1", + "description": "Test Description 1", + "has_next_condition": 1, + "next_step_condition": "eval: doc.first_name", + "field": get_docfield_name({'parent': 'Contact', 'fieldname': 'first_name'}), + "fieldname": "first_name", + "fieldtype": "Data" + },{ + "title": "Test Title 2", + "description": "Test Description 2", + "has_next_condition": 1, + "next_step_condition": "eval: doc.last_name", + "field": get_docfield_name({'parent': 'Contact', 'fieldname': 'last_name'}), + "fieldname": "last_name", + "fieldtype": "Data" + },{ + "title": "Test Title 3", + "description": "Test Description 3", + "field": get_docfield_name({'parent': 'Contact', 'fieldname': 'phone_nos'}), + "fieldname": "phone_nos", + "fieldtype": "Table" + },{ + "title": "Test Title 4", + "description": "Test Description 4", + "is_table_field": 1, + "parent_field": get_docfield_name({'parent': 'Contact', 'fieldname': 'phone_nos'}), + "field": get_docfield_name({'parent': 'Contact Phone', 'fieldname': 'phone'}), + "next_step_condition": "eval: doc.phone", + "has_next_condition": 1, + "fieldname": "phone", + "fieldtype": "Data" + }] + }) + tour.insert() \ No newline at end of file From 1f687a13edddadb7253f9cca7fcb8621b32a2768 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 23 Jun 2021 14:27:50 +0530 Subject: [PATCH 29/75] fix: add missing semicolons --- cypress/integration/form_tour.js | 4 ++-- frappe/desk/doctype/form_tour/form_tour.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cypress/integration/form_tour.js b/cypress/integration/form_tour.js index e1544a4cdf..d12be63f3b 100644 --- a/cypress/integration/form_tour.js +++ b/cypress/integration/form_tour.js @@ -31,7 +31,7 @@ context('Form Tour', () => { cy.get('@first_name').should('have.class', 'driver-highlighted-element'); // after filling the field, next step should be highlighted - cy.fill_field('first_name', 'Test Name', 'Data') + cy.fill_field('first_name', 'Test Name', 'Data'); cy.wait(500); cy.get('@next_btn').click(); cy.wait(500); @@ -41,7 +41,7 @@ context('Form Tour', () => { cy.get('@last_name').should('have.class', 'driver-highlighted-element'); // after filling the field, next step should be highlighted - cy.fill_field('last_name', 'Test Last Name', 'Data') + cy.fill_field('last_name', 'Test Last Name', 'Data'); cy.wait(500); cy.get('@next_btn').click(); cy.wait(500); diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js index e5ea373c4c..efb853cfa5 100644 --- a/frappe/desk/doctype/form_tour/form_tour.js +++ b/frappe/desk/doctype/form_tour/form_tour.js @@ -17,7 +17,7 @@ frappe.ui.form.on('Form Tour', { const issingle = await check_if_single(frm.doc.reference_doctype); if (issingle) { - frappe.set_route('Form', frm.doc.reference_doctype) + frappe.set_route('Form', frm.doc.reference_doctype); } else { const new_name = 'new-' + frappe.scrub(frm.doc.reference_doctype) + '-1'; frappe.set_route('Form', frm.doc.reference_doctype, new_name); From 9293807ef31d25fb30b6d5bbd692427808ec9633 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 24 Jun 2021 10:02:30 +0530 Subject: [PATCH 30/75] fix: Restore ability use get_context without params - https://github.com/frappe/frappe/pull/12609 The feature got removed during the website routing refactor https://github.com/frappe/frappe/pull/12334 --- frappe/website/page_renderers/template_page.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/frappe/website/page_renderers/template_page.py b/frappe/website/page_renderers/template_page.py index 5e6e57e33a..49496c4cbe 100644 --- a/frappe/website/page_renderers/template_page.py +++ b/frappe/website/page_renderers/template_page.py @@ -185,10 +185,15 @@ class TemplatePage(BaseTemplatePage): click.echo(f'\n⚠️ DEPRECATION WARNING: {comment_tag} will be deprecated on 2021-12-31.') click.echo(f'Please remove it from {self.template_path} in {self.app}') - def run_pymodule_method(self, method): - if hasattr(self.pymodule, method): + def run_pymodule_method(self, method_name): + if hasattr(self.pymodule, method_name): try: - return getattr(self.pymodule, method)(self.context) + import inspect + method = getattr(self.pymodule, method_name) + if inspect.getargspec(method).args: + return method(self.context) + else: + return method() except (frappe.PermissionError, frappe.DoesNotExistError, frappe.Redirect): raise except Exception: From 6f3e6fee82f021b90e66e0a2e272697285363a78 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 24 Jun 2021 10:22:27 +0530 Subject: [PATCH 31/75] test: Add test to check ability to use get_context without context object --- frappe/tests/test_website.py | 4 ++++ frappe/www/_test/_test_no_context.html | 1 + frappe/www/_test/_test_no_context.py | 7 +++++++ 3 files changed, 12 insertions(+) create mode 100644 frappe/www/_test/_test_no_context.html create mode 100644 frappe/www/_test/_test_no_context.py diff --git a/frappe/tests/test_website.py b/frappe/tests/test_website.py index 6f265d9b94..0c04ed001c 100644 --- a/frappe/tests/test_website.py +++ b/frappe/tests/test_website.py @@ -201,6 +201,10 @@ class TestWebsite(unittest.TestCase): self.assertIn('