From 599e4782c0b34ac3145c1f0ae176a9f2dd2c590d Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 6 Nov 2019 12:01:10 +0530 Subject: [PATCH 01/70] fix(security): dont allow ignore_permissions in whitelisted method: frappe.rename_doc --- frappe/__init__.py | 11 ++++++++--- frappe/public/js/frappe/model/model.js | 6 +++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 6424dcd9b5..4a1cf98042 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -800,10 +800,15 @@ def reload_doc(module, dt=None, dn=None, force=False, reset_permissions=False): import frappe.modules return frappe.modules.reload_doc(module, dt, dn, force=force, reset_permissions=reset_permissions) -def rename_doc(*args, **kwargs): - """Rename a document. Calls `frappe.model.rename_doc.rename_doc`""" +@whitelist() +def rename_doc(doctype, old, new, force=False, merge=False, ignore_if_exists=False): + """ + Renames a doc(dt, old) to doc(dt, new) and updates all linked fields of type "Link" + + Calls `frappe.model.rename_doc.rename_doc` + """ from frappe.model.rename_doc import rename_doc - return rename_doc(*args, **kwargs) + return rename_doc(doctype, old, new, force, merge, ignore_if_exists) def get_module(modulename): """Returns a module object for given Python module name using `importlib.import_module`.""" diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 0c6388c681..7040d8a24d 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -555,12 +555,12 @@ $.extend(frappe.model, { var args = d.get_values(); if(!args) return; return frappe.call({ - method:"frappe.model.rename_doc.rename_doc", + method:"frappe.rename_doc", args: { doctype: doctype, old: docname, - "new": args.new_name, - "merge": args.merge + new: args.new_name, + merge: args.merge }, btn: d.get_primary_btn(), callback: function(r,rt) { From a5de575f23ed0582d6109160fa22ca89292d9a81 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 6 Nov 2019 12:04:53 +0530 Subject: [PATCH 02/70] chore: un-whitelist frappe.model.rename_doc.rename_doc --- frappe/model/rename_doc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 7dc3944750..a8daa66c1b 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -26,7 +26,6 @@ def update_document_title(doctype, docname, title_field=None, old_title=None, ne return docname -@frappe.whitelist() def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=False, ignore_if_exists=False): """ Renames a doc(dt, old) to doc(dt, new) and From 40600f4fbac658b53765616e7b8f5080ade3b107 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 6 Nov 2019 14:57:48 +0530 Subject: [PATCH 03/70] chore: update rename_doc calls --- frappe/__init__.py | 10 +++++----- frappe/core/doctype/file/file.py | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 4a1cf98042..e3620a5f4e 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -6,7 +6,7 @@ globals attached to frappe module """ from __future__ import unicode_literals, print_function -from six import iteritems, binary_type, text_type, string_types +from six import iteritems, binary_type, text_type, string_types, PY2 from werkzeug.local import Local, release_local import os, sys, importlib, inspect, json from past.builtins import cmp @@ -19,7 +19,7 @@ from .utils.jinja import (get_jenv, get_template, render_template, get_email_fro # Harmless for Python 3 # For Python 2 set default encoding to utf-8 -if sys.version[0] == '2': +if PY2: reload(sys) sys.setdefaultencoding("utf-8") @@ -1556,9 +1556,9 @@ def get_version(doctype, name, limit = None, head = False, raise_err = True): >>> [ { - "version": [version.data], # Refer Version DocType get_diff method and data attribute - "user": "admin@gmail.com" # User that created this version - "creation": # Creation timestamp of that object. + "version": [version.data], # Refer Version DocType get_diff method and data attribute + "user": "admin@gmail.com", # User that created this version + "creation": # Creation timestamp of that object. } ] ''' diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index a01f0432bf..cc2041bf41 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -625,7 +625,8 @@ def setup_folder_path(filename, new_parent): file.save() if file.is_folder: - frappe.rename_doc("File", file.name, file.get_name_based_on_parent_folder(), ignore_permissions=True) + from frappe.model.rename_doc import rename_doc + rename_doc("File", file.name, file.get_name_based_on_parent_folder(), ignore_permissions=True) def get_extension(filename, extn, content): mimetype = None From d0a5a9a7ba7bbff9edadb6a306d468d14420983d Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Thu, 14 Nov 2019 00:06:53 +0100 Subject: [PATCH 04/70] :white_check_mark: Add JUnit XML reports to Travis Signed-off-by: mathieu.brunot --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index df66db88a7..0288c01f7e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,12 +21,12 @@ matrix: - name: "Python 3.6 MariaDB" python: 3.6 env: DB=mariadb TYPE=server - script: bench --site test_site run-tests --coverage + script: bench --site test_site run-tests --coverage --junit-xml-output frappe_unit_tests.xml - name: "Python 3.6 PostgreSQL" python: 3.6 env: DB=postgres TYPE=server - script: bench --site test_site run-tests --coverage + script: bench --site test_site run-tests --coverage --junit-xml-output frappe_unit_tests.xml - name: "Cypress" python: 3.6 @@ -37,7 +37,7 @@ matrix: - name: "Python 2.7 MariaDB" python: 2.7 env: DB=mariadb TYPE=server - script: bench --site test_site run-tests --coverage + script: bench --site test_site run-tests --coverage --junit-xml-output frappe_unit_tests.xml install: - cd ~ From 0054206e431d6418447e3a117223aeeba6e80833 Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Mon, 25 Nov 2019 23:23:49 +0100 Subject: [PATCH 05/70] :construction: Testing without TimeLogging Signed-off-by: mathieu.brunot --- frappe/test_runner.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/test_runner.py b/frappe/test_runner.py index 76140e442c..4e5010ed38 100644 --- a/frappe/test_runner.py +++ b/frappe/test_runner.py @@ -131,7 +131,8 @@ def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfa pr = cProfile.Profile() pr.enable() - out = unittest_runner(resultclass=TimeLoggingTestResult, verbosity=1+(verbose and 1 or 0), failfast=failfast).run(test_suite) + # resultclass=TimeLoggingTestResult, + out = unittest_runner(verbosity=1+(verbose and 1 or 0), failfast=failfast).run(test_suite) if profile: pr.disable() From f3717bd7a69f2ab9088e87e481ddd5f64b1c2aaa Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Tue, 26 Nov 2019 03:11:14 +0100 Subject: [PATCH 06/70] :white_check_mark: Test XML reports Signed-off-by: mathieu.brunot --- .travis.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.travis.yml b/.travis.yml index 0288c01f7e..a5c81f9631 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,11 +19,21 @@ cache: matrix: include: - name: "Python 3.6 MariaDB" + python: 3.6 + env: DB=mariadb TYPE=server + script: bench --site test_site run-tests --coverage + + - name: "Python 3.6 MariaDB XML Report" python: 3.6 env: DB=mariadb TYPE=server script: bench --site test_site run-tests --coverage --junit-xml-output frappe_unit_tests.xml - name: "Python 3.6 PostgreSQL" + python: 3.6 + env: DB=postgres TYPE=server + script: bench --site test_site run-tests --coverage + + - name: "Python 3.6 PostgreSQL XML Report" python: 3.6 env: DB=postgres TYPE=server script: bench --site test_site run-tests --coverage --junit-xml-output frappe_unit_tests.xml @@ -35,6 +45,11 @@ matrix: script: bench --site test_site run-ui-tests frappe --headless - name: "Python 2.7 MariaDB" + python: 2.7 + env: DB=mariadb TYPE=server + script: bench --site test_site run-tests --coverage + + - name: "Python 2.7 MariaDB XML Report" python: 2.7 env: DB=mariadb TYPE=server script: bench --site test_site run-tests --coverage --junit-xml-output frappe_unit_tests.xml From 3073515b1c2c40c14aeda5a2e4cb4a3a1b363c12 Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Tue, 26 Nov 2019 03:11:40 +0100 Subject: [PATCH 07/70] :construction: Split runner init from test run Signed-off-by: mathieu.brunot --- frappe/test_runner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/test_runner.py b/frappe/test_runner.py index 4e5010ed38..23ba00789d 100644 --- a/frappe/test_runner.py +++ b/frappe/test_runner.py @@ -131,8 +131,8 @@ def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfa pr = cProfile.Profile() pr.enable() - # resultclass=TimeLoggingTestResult, - out = unittest_runner(verbosity=1+(verbose and 1 or 0), failfast=failfast).run(test_suite) + runner = unittest_runner(resultclass=TimeLoggingTestResult, verbosity=1+(verbose and 1 or 0), failfast=failfast) + out = runner.run(test_suite) if profile: pr.disable() From cf50b52df7b2bf33044a619dbdbf0cb8d2b2c255 Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Tue, 26 Nov 2019 03:21:23 +0100 Subject: [PATCH 08/70] :bug: Allow Unit Test XML to write binary Signed-off-by: mathieu.brunot --- frappe/test_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/test_runner.py b/frappe/test_runner.py index 23ba00789d..86aa1b730e 100644 --- a/frappe/test_runner.py +++ b/frappe/test_runner.py @@ -38,7 +38,7 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(), xmloutput_fh = None if junit_xml_output: - xmloutput_fh = open(junit_xml_output, 'w') + xmloutput_fh = open(junit_xml_output, 'wb') unittest_runner = xmlrunner_wrapper(xmloutput_fh) else: unittest_runner = unittest.TextTestRunner From 469944d2738fd9de1d468c2a9b357c6f65003001 Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Tue, 26 Nov 2019 04:00:14 +0100 Subject: [PATCH 09/70] :bug: Change runner init if XML report on Signed-off-by: mathieu.brunot --- frappe/test_runner.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/frappe/test_runner.py b/frappe/test_runner.py index 86aa1b730e..e7ec13afe6 100644 --- a/frappe/test_runner.py +++ b/frappe/test_runner.py @@ -65,11 +65,11 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(), frappe.get_attr(fn)() if doctype: - ret = run_tests_for_doctype(doctype, verbose, tests, force, profile) + ret = run_tests_for_doctype(doctype, verbose, tests, force, profile, junit_xml_output=junit_xml_output) elif module: - ret = run_tests_for_module(module, verbose, tests, profile) + ret = run_tests_for_module(module, verbose, tests, profile, junit_xml_output=junit_xml_output) else: - ret = run_all_tests(app, verbose, profile, ui_tests, failfast=failfast) + ret = run_all_tests(app, verbose, profile, ui_tests, failfast=failfast, junit_xml_output=junit_xml_output) frappe.db.commit() @@ -106,7 +106,7 @@ class TimeLoggingTestResult(unittest.TextTestResult): super(TimeLoggingTestResult, self).addSuccess(test) -def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfast=False): +def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfast=False, junit_xml_output=False): import os apps = [app] if app else frappe.get_installed_apps() @@ -127,11 +127,15 @@ def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfa _add_test(app, path, filename, verbose, test_suite, ui_tests) + if junit_xml_output: + runner = unittest_runner(verbosity=1+(verbose and 1 or 0), failfast=failfast) + else: + runner = unittest_runner(resultclass=TimeLoggingTestResult, verbosity=1+(verbose and 1 or 0), failfast=failfast) + if profile: pr = cProfile.Profile() pr.enable() - runner = unittest_runner(resultclass=TimeLoggingTestResult, verbosity=1+(verbose and 1 or 0), failfast=failfast) out = runner.run(test_suite) if profile: @@ -143,7 +147,7 @@ def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfa return out -def run_tests_for_doctype(doctypes, verbose=False, tests=(), force=False, profile=False): +def run_tests_for_doctype(doctypes, verbose=False, tests=(), force=False, profile=False, junit_xml_output=False): modules = [] if not isinstance(doctypes, (list, tuple)): doctypes = [doctypes] @@ -161,15 +165,15 @@ def run_tests_for_doctype(doctypes, verbose=False, tests=(), force=False, profil make_test_records(doctype, verbose=verbose, force=force) modules.append(importlib.import_module(test_module)) - return _run_unittest(modules, verbose=verbose, tests=tests, profile=profile) + return _run_unittest(modules, verbose=verbose, tests=tests, profile=profile, junit_xml_output=junit_xml_output) -def run_tests_for_module(module, verbose=False, tests=(), profile=False): +def run_tests_for_module(module, verbose=False, tests=(), profile=False, junit_xml_output=False): module = importlib.import_module(module) if hasattr(module, "test_dependencies"): for doctype in module.test_dependencies: make_test_records(doctype, verbose=verbose) - return _run_unittest(module, verbose=verbose, tests=tests, profile=profile) + return _run_unittest(module, verbose=verbose, tests=tests, profile=profile, junit_xml_output=junit_xml_output) def run_setup_wizard_ui_test(app=None, verbose=False, profile=False): '''Run setup wizard UI test using test_test_runner''' @@ -186,7 +190,7 @@ def run_ui_tests(app=None, test=None, test_list=None, verbose=False, profile=Fal frappe.flags.ui_test_path = test return _run_unittest(module, verbose=verbose, tests=(), profile=profile) -def _run_unittest(modules, verbose=False, tests=(), profile=False): +def _run_unittest(modules, verbose=False, tests=(), profile=False, junit_xml_output=False): test_suite = unittest.TestSuite() if not isinstance(modules, (list, tuple)): @@ -202,13 +206,18 @@ def _run_unittest(modules, verbose=False, tests=(), profile=False): else: test_suite.addTest(module_test_cases) + if junit_xml_output: + runner = unittest_runner(verbosity=1+(verbose and 1 or 0)) + else: + runner = unittest_runner(resultclass=TimeLoggingTestResult, verbosity=1+(verbose and 1 or 0)) + if profile: pr = cProfile.Profile() pr.enable() frappe.flags.tests_verbose = verbose - out = unittest_runner(verbosity=1+(verbose and 1 or 0)).run(test_suite) + out = runner.run(test_suite) if profile: From 28b2ee045536fb4c0fa57f9a763d47b47522553a Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 20 Dec 2019 14:25:47 +0530 Subject: [PATCH 10/70] chore: added rename_doc tests --- frappe/tests/test_document.py | 50 +++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index 4c48ef2811..1e92015602 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -2,9 +2,14 @@ # MIT License. See license.txt from __future__ import unicode_literals -import frappe, unittest, os -from frappe.utils import cint +import os +import unittest + +import frappe +from frappe.utils import cint, add_to_date, now from frappe.model.naming import revert_series_if_last, make_autoname, parse_naming_series +from frappe.exceptions import DoesNotExistError + class TestDocument(unittest.TestCase): def test_get_return_empty_list_for_table_field_if_none(self): @@ -236,3 +241,44 @@ class TestDocument(unittest.TestCase): new_current = cint(frappe.db.get_value('Series', prefix, "current", order_by="name")) self.assertEqual(cint(old_current) - 1, new_current) + + def test_rename_doc(self): + from random import choice, sample + + available_documents = [] + doctype = "ToDo" + + # data generation: 4 todo documents + for num in range(1, 5): + doc = frappe.get_doc({ + "doctype": doctype, + "date": add_to_date(now(), days=num), + "description": "this is todo #{}".format(num) + }).insert() + available_documents.append(doc.name) + + # test 1: document renaming + old_name = choice(available_documents) + new_name = old_name + '.new' + self.assertEqual(new_name, frappe.rename_doc(doctype, old_name, new_name, force=True)) + available_documents.remove(old_name) + available_documents.append(new_name) + + # test 2: merge documents + first_todo, second_todo = sample(available_documents, 2) + + second_todo_doc = frappe.get_doc(doctype, second_todo) + second_todo_doc.priority = "High" + second_todo_doc.save() + + merged_todo = frappe.rename_doc(doctype, first_todo, second_todo, merge=True, force=True) + merged_todo_doc = frappe.get_doc(doctype, merged_todo) + available_documents.remove(first_todo) + + with self.assertRaises(DoesNotExistError): + frappe.get_doc(doctype, first_todo) + + self.assertEqual(merged_todo_doc.priority, second_todo_doc.priority) + + for docname in available_documents: + frappe.delete_doc(doctype, docname) \ No newline at end of file From 173960e50bf4c2b7306b7107aa8f99d60ad385d2 Mon Sep 17 00:00:00 2001 From: prssanna Date: Mon, 30 Dec 2019 16:55:21 +0530 Subject: [PATCH 11/70] fix: update docstatus of documents if docstatus value is changed in workflow --- frappe/workflow/doctype/workflow/workflow.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/frappe/workflow/doctype/workflow/workflow.py b/frappe/workflow/doctype/workflow/workflow.py index c3f0991d85..1de48d348f 100644 --- a/frappe/workflow/doctype/workflow/workflow.py +++ b/frappe/workflow/doctype/workflow/workflow.py @@ -16,6 +16,7 @@ class Workflow(Document): self.validate_docstatus() def on_update(self): + self.update_doc_status() frappe.clear_cache(doctype=self.document_type) frappe.cache().delete_key('workflow_' + self.name) # clear cache created in model/workflow.py @@ -56,6 +57,16 @@ class Workflow(Document): docstatus_map[d.doc_status] = d.state + def update_doc_status(self): + doc_before_save = self.get_doc_before_save() + for current_doc_state, doc_before_save_state in zip(self.states, doc_before_save.states): + if not doc_before_save_state.doc_status == current_doc_state.doc_status: + frappe.db.set_value(self.document_type, + {self.workflow_state_field: doc_before_save_state.state}, + 'docstatus', + current_doc_state.doc_status + ) + def validate_docstatus(self): def get_state(state): for s in self.states: From 764f18e2e9548e80e91d07e024984ffa5364ae46 Mon Sep 17 00:00:00 2001 From: prssanna Date: Tue, 31 Dec 2019 12:24:30 +0530 Subject: [PATCH 12/70] fix: check if doc_before_save exists --- frappe/workflow/doctype/workflow/workflow.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/frappe/workflow/doctype/workflow/workflow.py b/frappe/workflow/doctype/workflow/workflow.py index 1de48d348f..523a7437c4 100644 --- a/frappe/workflow/doctype/workflow/workflow.py +++ b/frappe/workflow/doctype/workflow/workflow.py @@ -59,13 +59,14 @@ class Workflow(Document): def update_doc_status(self): doc_before_save = self.get_doc_before_save() - for current_doc_state, doc_before_save_state in zip(self.states, doc_before_save.states): - if not doc_before_save_state.doc_status == current_doc_state.doc_status: - frappe.db.set_value(self.document_type, - {self.workflow_state_field: doc_before_save_state.state}, - 'docstatus', - current_doc_state.doc_status - ) + if doc_before_save: + for current_doc_state, doc_before_save_state in zip(self.states, doc_before_save.states): + if not doc_before_save_state.doc_status == current_doc_state.doc_status: + frappe.db.set_value(self.document_type, + {self.workflow_state_field: doc_before_save_state.state}, + 'docstatus', + current_doc_state.doc_status + ) def validate_docstatus(self): def get_state(state): From a4749de6a397bcf14fe84961421dc2dc3fb8f816 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 23 Dec 2019 11:34:19 +0530 Subject: [PATCH 13/70] fix(warn): added "Irreversible Change" warnings to merge document via toolbar rename --- frappe/public/js/frappe/form/toolbar.js | 62 ++++++++++++++++--------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 67a04ce13e..0398ed5db1 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -72,6 +72,13 @@ frappe.ui.form.Toolbar = Class.extend({ setup_editable_title: function () { let me = this; + function document_unchanged(){ + frappe.show_alert({ + indicator: "yellow", + message: __("Unchanged") + }) + } + this.page.$title_area.find(".title-text").on("click", () => { let fields = []; let doctype = me.frm.doctype; @@ -118,30 +125,39 @@ frappe.ui.form.Toolbar = Class.extend({ d.set_primary_action(__("Rename"), function () { let args = d.get_values(); if (args.title != me.frm.doc[title_field] || args.name != docname) { - frappe.call({ - method: "frappe.model.rename_doc.update_document_title", - args: { - doctype, - docname, - title_field, - old_title: me.frm.doc[title_field], - new_title: args.title, - new_name: args.name, - merge: args.merge - }, - btn: d.get_primary_btn() - }).then((res) => { - me.frm.reload_doc(); - if (!res.exc && (args.name != docname)) { - $(document).trigger("rename", [doctype, docname, res.message || args.name]); - if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname]; - } - }); + if (args.merge) { + let warning = __("This cannot be undone"); + let message = __("Are you sure you want to merge {0} with {1}?", [docname.bold(), args.name.bold()]) + let confirm_message = message + "
" + warning + ""; + + frappe.confirm( + confirm_message, + function() { + frappe.call({ + method: "frappe.model.rename_doc.update_document_title", + args: { + doctype, + docname, + title_field, + old_title: me.frm.doc[title_field], + new_title: args.title, + new_name: args.name, + merge: args.merge + }, + btn: d.get_primary_btn() + }).then((res) => { + me.frm.reload_doc(); + if (!res.exc && (args.name != docname)) { + $(document).trigger("rename", [doctype, docname, res.message || args.name]); + if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname]; + } + }) + }, + document_unchanged + ); + } } else { - frappe.show_alert({ - indicator: "yellow", - message: __("Unchanged") - }); + document_unchanged() } d.hide(); }); From 5fa802c2036e1ba047a9a572a1caeb9033a88988 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 23 Dec 2019 11:36:20 +0530 Subject: [PATCH 14/70] fix(warn): added "Irreversible Change" warnings to merge document via frappe.model.rename_doc JS API --- frappe/public/js/frappe/model/model.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 48d0d14037..37e087905b 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -550,13 +550,18 @@ $.extend(frappe.model, { }, rename_doc: function(doctype, docname, callback) { + let message = __("Merge with existing"); + let warning = __("This cannot be undone"); + let merge_label = message + " (" + warning + ")"; + var d = new frappe.ui.Dialog({ title: __("Rename {0}", [__(docname)]), fields: [ {label:__("New Name"), fieldname: "new_name", fieldtype:"Data", reqd:1, "default": docname}, - {label:__("Merge with existing"), fieldtype:"Check", fieldname:"merge"}, + {label:merge_label, fieldtype:"Check", fieldname:"merge"}, ] }); + d.set_primary_action(__("Rename"), function() { var args = d.get_values(); if(!args) return; From cbcdc93fd6563cfabb1c9a07181798726b4a40f1 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 23 Dec 2019 22:53:53 +0530 Subject: [PATCH 15/70] refactor: Move "rename document title" code to separate function --- frappe/public/js/frappe/form/toolbar.js | 106 +++++++++++++----------- 1 file changed, 57 insertions(+), 49 deletions(-) diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 0398ed5db1..a00779c69e 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -69,23 +69,61 @@ frappe.ui.form.Toolbar = Class.extend({ can_rename: function() { return this.frm.perm[0].write && this.frm.meta.allow_rename && !this.frm.doc.__islocal; }, + show_unchanged_document_alert: function() { + frappe.show_alert({ + indicator: "yellow", + message: __("Unchanged") + }); + }, + rename_document_title(new_name, new_title, merge=false) { + const docname = this.frm.doc.name; + const title_field = this.frm.meta.title_field || ''; + const doctype = this.frm.doctype; + + const warning = __("This cannot be undone"); + const message = __("Are you sure you want to merge {0} with {1}?", [docname.bold(), new_name.bold()]); + const confirm_message = message + "
" + warning + ""; + + let rename_document = () => { + return frappe.xcall("frappe.model.rename_doc.update_document_title", { + doctype, + docname, + title_field, + old_title: this.frm.doc[title_field], + new_name: new_name, + new_title: new_title, + merge: merge + }).then(new_docname => { + if (new_name != docname) { + $(document).trigger("rename", [doctype, docname, new_docname || new_name]); + if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname]; + } + }); + }; + + return new Promise((resolve, reject) => { + if (new_title === this.frm.doc[title_field] && new_name === docname) { + this.show_unchanged_document_alert(); + resolve(); + } else if (merge) { + frappe.confirm(confirm_message, () => { + rename_document().then(resolve).catch(reject); + }, reject); + } else { + rename_document().then(resolve).catch(reject); + } + }); + + }, setup_editable_title: function () { let me = this; - function document_unchanged(){ - frappe.show_alert({ - indicator: "yellow", - message: __("Unchanged") - }) - } - this.page.$title_area.find(".title-text").on("click", () => { let fields = []; - let doctype = me.frm.doctype; let docname = me.frm.doc.name; let title_field = me.frm.meta.title_field || ''; - // check if title is updateable + // check if title is updatable if (me.is_title_editable()) { let title_field_label = me.frm.get_docfield(title_field).label; @@ -98,7 +136,7 @@ frappe.ui.form.Toolbar = Class.extend({ }); } - // check if docname is updateable + // check if docname is updatable if (me.can_rename()) { fields.push(...[{ label: __("New Name"), @@ -121,45 +159,15 @@ frappe.ui.form.Toolbar = Class.extend({ fields: fields }); d.show(); - - d.set_primary_action(__("Rename"), function () { - let args = d.get_values(); - if (args.title != me.frm.doc[title_field] || args.name != docname) { - if (args.merge) { - let warning = __("This cannot be undone"); - let message = __("Are you sure you want to merge {0} with {1}?", [docname.bold(), args.name.bold()]) - let confirm_message = message + "
" + warning + ""; - - frappe.confirm( - confirm_message, - function() { - frappe.call({ - method: "frappe.model.rename_doc.update_document_title", - args: { - doctype, - docname, - title_field, - old_title: me.frm.doc[title_field], - new_title: args.title, - new_name: args.name, - merge: args.merge - }, - btn: d.get_primary_btn() - }).then((res) => { - me.frm.reload_doc(); - if (!res.exc && (args.name != docname)) { - $(document).trigger("rename", [doctype, docname, res.message || args.name]); - if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname]; - } - }) - }, - document_unchanged - ); - } - } else { - document_unchanged() - } - d.hide(); + d.set_primary_action(__("Rename"), (values) => { + d.disable_primary_action(); + this.rename_document_title(values.name, values.title, values.merge) + .then(() => { + d.hide(); + }) + .catch(() => { + d.enable_primary_action(); + }); }); } }); From 9bf4221d45d1b301b9a0d27a384c937bc73e0e29 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 2 Jan 2020 13:02:46 +0530 Subject: [PATCH 16/70] fix: new_name variable undefined for document which cant be renamed --- frappe/public/js/frappe/form/toolbar.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index a00779c69e..a84432d792 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -80,24 +80,27 @@ frappe.ui.form.Toolbar = Class.extend({ const title_field = this.frm.meta.title_field || ''; const doctype = this.frm.doctype; - const warning = __("This cannot be undone"); - const message = __("Are you sure you want to merge {0} with {1}?", [docname.bold(), new_name.bold()]); - const confirm_message = message + "
" + warning + ""; + if (new_name) { + const warning = __("This cannot be undone"); + const message = __("Are you sure you want to merge {0} with {1}?", [docname.bold(), new_name.bold()]); + const confirm_message = message + "
" + warning + ""; + } let rename_document = () => { return frappe.xcall("frappe.model.rename_doc.update_document_title", { doctype, docname, + new_name, title_field, old_title: this.frm.doc[title_field], - new_name: new_name, - new_title: new_title, - merge: merge + new_title, + merge }).then(new_docname => { if (new_name != docname) { $(document).trigger("rename", [doctype, docname, new_docname || new_name]); if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname]; } + this.frm.reload_doc(); }); }; @@ -106,14 +109,11 @@ frappe.ui.form.Toolbar = Class.extend({ this.show_unchanged_document_alert(); resolve(); } else if (merge) { - frappe.confirm(confirm_message, () => { - rename_document().then(resolve).catch(reject); - }, reject); + frappe.confirm(confirm_message, function() { rename_document().then(resolve).catch(reject) }, reject); } else { rename_document().then(resolve).catch(reject); } }); - }, setup_editable_title: function () { let me = this; From ecfc0714bc00b48b6dcfbf29099d0d1d8f4a56df Mon Sep 17 00:00:00 2001 From: Ben Knowles Date: Thu, 2 Jan 2020 14:33:57 -0600 Subject: [PATCH 17/70] fix: global search index breaking on long titles If you created a doc with a title longer than 140 characters, it's supposed to be truncated to 140. However, the global search index would break for that doctype due to an error in the query. The truncate was happening AFTER it was escaped, causing it to lose the closing quote mark which caused a SQL error and broke the index. --- frappe/utils/global_search.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/utils/global_search.py b/frappe/utils/global_search.py index 7012b737c0..20309559e3 100644 --- a/frappe/utils/global_search.py +++ b/frappe/utils/global_search.py @@ -137,8 +137,8 @@ def rebuild_for_doctype(doctype): "name": frappe.db.escape(doc.name), "content": frappe.db.escape(' ||| '.join(content or '')), "published": published, - "title": frappe.db.escape(title or '')[:int(frappe.db.VARCHAR_LEN)], - "route": frappe.db.escape(route or '')[:int(frappe.db.VARCHAR_LEN)] + "title": frappe.db.escape((title or '')[:int(frappe.db.VARCHAR_LEN)]), + "route": frappe.db.escape((route or '')[:int(frappe.db.VARCHAR_LEN)]) }) if all_contents: insert_values_for_multiple_docs(all_contents) From ce2a0854db58ff0d03e6a89a2e255874fd794e43 Mon Sep 17 00:00:00 2001 From: "Chinmay D. Pai" Date: Fri, 3 Jan 2020 14:56:15 +0530 Subject: [PATCH 18/70] fix(email): safe_encode to avoid smtp ascii encoding issue fixes issue where smtplib fails to encode the mail as ascii Traceback (most recent call last): File "/home/frappe/frappe-bench/apps/frappe/frappe/app.py", line 60, in application response = frappe.api.handle() File "/home/frappe/frappe-bench/apps/frappe/frappe/api.py", line 55, in handle return frappe.handler.handle() File "/home/frappe/frappe-bench/apps/frappe/frappe/handler.py", line 22, in handle data = execute_cmd(cmd) File "/home/frappe/frappe-bench/apps/frappe/frappe/handler.py", line 61, in execute_cmd return frappe.call(method, **frappe.form_dict) File "/home/frappe/frappe-bench/apps/frappe/frappe/__init__.py", line 1042, in call return fn(*args, **newargs) File "/home/frappe/frappe-bench/apps/frappe/frappe/core/doctype/user/user.py", line 801, in reset_password user.reset_password(send_email=True) File "/home/frappe/frappe-bench/apps/frappe/frappe/core/doctype/user/user.py", line 234, in reset_password self.password_reset_mail(link) File "/home/frappe/frappe-bench/apps/frappe/frappe/core/doctype/user/user.py", line 253, in password_reset_mail "password_reset", {"link": link}, now=True) File "/home/frappe/frappe-bench/apps/frappe/frappe/core/doctype/user/user.py", line 298, in send_login_mail delayed=(not now) if now!=None else self.flags.delay_emails, retry=3) File "/home/frappe/frappe-bench/apps/frappe/frappe/__init__.py", line 476, in sendmail inline_images=inline_images, header=header, print_letterhead=print_letterhead) File "/home/frappe/frappe-bench/apps/frappe/frappe/email/queue.py", line 162, in send print_letterhead=print_letterhead) File "/home/frappe/frappe-bench/apps/frappe/frappe/email/queue.py", line 185, in add send_one(email_queue.name, now=True) File "/home/frappe/frappe-bench/apps/frappe/frappe/email/queue.py", line 475, in send_one raise e File "/home/frappe/frappe-bench/apps/frappe/frappe/email/queue.py", line 415, in send_one smtpserver.sess.sendmail(email.sender, recipient.recipient, message) File "/usr/lib64/python3.6/smtplib.py", line 855, in sendmail msg = _fix_eols(msg).encode('ascii') UnicodeEncodeError: 'ascii' codec can't encode characters in position 335-339: ordinal not in range(128) Signed-off-by: Chinmay D. Pai --- frappe/email/queue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/email/queue.py b/frappe/email/queue.py index 4a0a34c76e..62b0d9ea3f 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -6,7 +6,7 @@ import frappe import sys from six.moves import html_parser as HTMLParser import smtplib, quopri, json -from frappe import msgprint, _, safe_decode +from frappe import msgprint, _, safe_decode, safe_encode from frappe.email.smtp import SMTPServer, get_outgoing_email_account from frappe.email.email_body import get_email, get_formatted_html, add_attachment from frappe.utils.verified_command import get_signed_params, verify_request @@ -563,7 +563,7 @@ def prepare_message(email, recipient, recipients_list): print_format_file.update({"parent": message}) add_attachment(**print_format_file) - return message.as_string() + return safe_encode(message.as_string()) def clear_outbox(): """Remove low priority older than 31 days in Outbox and expire mails not sent for 7 days. From db8c19ee528e73c7063af2ae6b2e7821c9eebb3b Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Fri, 3 Jan 2020 15:08:59 +0530 Subject: [PATCH 19/70] fix: load latest custom report instead of latest reference report --- frappe/desk/query_report.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 7dc561193f..6d40db6a65 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -229,8 +229,9 @@ def get_prepared_report_result(report, filters, dn="", user=None): "status": "Completed", "filters": json.dumps(filters), "owner": user, - "report_name": report.report_name - } + "report_name": report.custom_report or report.report_name + }, + order_by = 'creation desc' ) if doc_list: From 250c44bed89478aedc194ede8ae403d9253790da Mon Sep 17 00:00:00 2001 From: "Chinmay D. Pai" Date: Fri, 3 Jan 2020 23:13:22 +0530 Subject: [PATCH 20/70] chore: pass email_body test Signed-off-by: Chinmay D. Pai --- frappe/email/test_email_body.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py index 26c4e5ba5d..f44c6e775a 100644 --- a/frappe/email/test_email_body.py +++ b/frappe/email/test_email_body.py @@ -3,9 +3,10 @@ from __future__ import unicode_literals import unittest, os, base64 +from frappe import safe_decode from frappe.email.receive import Email from frappe.email.email_body import (replace_filename_with_cid, - get_email, inline_style_in_html, get_header) + get_email, inline_style_in_html, get_header) from frappe.email.queue import prepare_message, get_email_queue from six import PY3 @@ -57,7 +58,7 @@ This is the text version of this email formatted='

' + uni_chr1 + 'abcd' + uni_chr2 + '

', text_content='whatever') result = prepare_message(email=email, recipient='test@test.com', recipients_list=[]) - self.assertTrue("

=EA=80=80abcd=DE=B4

" in result) + self.assertTrue(b"

=EA=80=80abcd=DE=B4

" in result) def test_prepare_message_returns_cr_lf(self): email = get_email_queue( @@ -67,7 +68,8 @@ This is the text version of this email content='

\n this is a test of newlines\n' + '

', formatted='

\n this is a test of newlines\n' + '

', text_content='whatever') - result = prepare_message(email=email, recipient='test@test.com', recipients_list=[]) + result = safe_decode(prepare_message(email=email, + recipient='test@test.com', recipients_list=[])) if PY3: self.assertTrue(result.count('\n') == result.count("\r")) else: @@ -81,9 +83,10 @@ This is the text version of this email subject='Test Subject', content='

Whatever

', text_content='whatever', - message_id= "a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" + - ".really.long.message.id.that.should.not.wrap.unti") - result = prepare_message(email=email, recipient='test@test.com', recipients_list=[]) + message_id="a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" + + ".really.long.message.id.that.should.not.wrap.unti") + result = safe_decode(prepare_message(email=email, recipient='test@test.com', + recipients_list=[])) self.assertTrue( "a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" + ".really.long.message.id.that.should.not.wrap.unti" in result) From 3418e84466ef733dac69794e28cf96f247b0e8c1 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Sat, 4 Jan 2020 01:07:02 +0530 Subject: [PATCH 21/70] refactor: updated variable name + style fixes --- frappe/build.py | 133 +++++++++++++++++++++++++++--------------------- 1 file changed, 75 insertions(+), 58 deletions(-) diff --git a/frappe/build.py b/frappe/build.py index f7437acf8f..761541f7a9 100644 --- a/frappe/build.py +++ b/frappe/build.py @@ -1,16 +1,24 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt -from __future__ import unicode_literals, print_function -import os, frappe, json, shutil, re, warnings, tempfile -from os.path import exists as path_exists, join as join_path, abspath, isdir +from __future__ import print_function, unicode_literals + +import os +import re +import json +import shutil +import warnings +import tempfile from distutils.spawn import find_executable + from six import iteritems, text_type + +import frappe from frappe.utils.minify import JavascriptMinify -""" -Build the `public` folders and setup languages -""" + +timestamps = {} +app_paths = None def symlink(target, link_name, overwrite=False): @@ -23,8 +31,7 @@ def symlink(target, link_name, overwrite=False): ''' if not overwrite: - os.symlink(target, linkname) - return + return os.symlink(target, link_name) # os.replace() may fail if files are on different filesystems link_dir = os.path.dirname(link_name) @@ -57,16 +64,17 @@ def symlink(target, link_name, overwrite=False): raise -app_paths = None def setup(): global app_paths pymodules = [] for app in frappe.get_all_apps(True): try: pymodules.append(frappe.get_module(app)) - except ImportError: pass + except ImportError: + pass app_paths = [os.path.dirname(pymodule.__file__) for pymodule in pymodules] + def get_node_pacman(): pacmans = ['yarn', 'npm'] for exec_ in pacmans: @@ -75,6 +83,7 @@ def get_node_pacman(): return exec_ raise ValueError('No Node.js Package Manager found.') + def bundle(no_compress, app=None, make_copy=False, restore=False, verbose=False): """concat / minify js files""" setup() @@ -87,77 +96,77 @@ def bundle(no_compress, app=None, make_copy=False, restore=False, verbose=False) if app: command += ' --app {app}'.format(app=app) - frappe_app_path = abspath(join_path(app_paths[0], '..')) + frappe_app_path = os.path.abspath(os.path.join(app_paths[0], '..')) check_yarn() frappe.commands.popen(command, cwd=frappe_app_path) + def watch(no_compress): """watch and rebuild if necessary""" setup() pacman = get_node_pacman() - frappe_app_path = abspath(join_path(app_paths[0], '..')) + frappe_app_path = os.path.abspath(os.path.join(app_paths[0], '..')) check_yarn() frappe_app_path = frappe.get_app_path('frappe', '..') - frappe.commands.popen('{pacman} run watch'.format(pacman=pacman), cwd = frappe_app_path) + frappe.commands.popen('{pacman} run watch'.format(pacman=pacman), cwd=frappe_app_path) + def check_yarn(): - from distutils.spawn import find_executable if not find_executable('yarn'): - print('Please install yarn using below command and try again.') - print('npm install -g yarn') - return + print('Please install yarn using below command and try again.\nnpm install -g yarn') + def make_asset_dirs(make_copy=False, restore=False): # don't even think of making assets_path absolute - rm -rf ahead. - assets_path = join_path(frappe.local.sites_path, "assets") - for dir_path in [ - join_path(assets_path, 'js'), - join_path(assets_path, 'css')]: + assets_path = os.path.join(frappe.local.sites_path, "assets") - if not path_exists(dir_path): + for dir_path in [os.path.join(assets_path, 'js'), os.path.join(assets_path, 'css')]: + if not os.path.exists(dir_path): os.makedirs(dir_path) for app_name in frappe.get_all_apps(True): pymodule = frappe.get_module(app_name) - app_base_path = abspath(os.path.dirname(pymodule.__file__)) + app_base_path = os.path.abspath(os.path.dirname(pymodule.__file__)) symlinks = [] - app_public_path = join_path(app_base_path, 'public') + app_public_path = os.path.join(app_base_path, 'public') # app/public > assets/app - symlinks.append([app_public_path, join_path(assets_path, app_name)]) + symlinks.append([app_public_path, os.path.join(assets_path, app_name)]) # app/node_modules > assets/app/node_modules - if path_exists(abspath(app_public_path)): - symlinks.append([join_path(app_base_path, '..', 'node_modules'), join_path(assets_path, app_name, 'node_modules')]) + if os.path.exists(os.path.abspath(app_public_path)): + symlinks.append([os.path.join(app_base_path, '..', 'node_modules'), os.path.join( + assets_path, app_name, 'node_modules')]) app_doc_path = None - if isdir(join_path(app_base_path, 'docs')): - app_doc_path = join_path(app_base_path, 'docs') + if os.path.isdir(os.path.join(app_base_path, 'docs')): + app_doc_path = os.path.join(app_base_path, 'docs') - elif isdir(join_path(app_base_path, 'www', 'docs')): - app_doc_path = join_path(app_base_path, 'www', 'docs') + elif os.path.isdir(os.path.join(app_base_path, 'www', 'docs')): + app_doc_path = os.path.join(app_base_path, 'www', 'docs') if app_doc_path: - symlinks.append([app_doc_path, join_path(assets_path, app_name + '_docs')]) + symlinks.append([app_doc_path, os.path.join( + assets_path, app_name + '_docs')]) for source, target in symlinks: - source = abspath(source) - if path_exists(source): + source = os.path.abspath(source) + if os.path.exists(source): if restore: - if path_exists(target): + if os.path.exists(target): if os.path.islink(target): os.unlink(target) else: shutil.rmtree(target) shutil.copytree(source, target) elif make_copy: - if path_exists(target): - warnings.warn('Target {target} already exists.'.format(target = target)) + if os.path.exists(target): + warnings.warn('Target {target} already exists.'.format(target=target)) else: shutil.copytree(source, target) else: - if path_exists(target): + if os.path.exists(target): if os.path.islink(target): os.unlink(target) else: @@ -170,11 +179,13 @@ def make_asset_dirs(make_copy=False, restore=False): # warnings.warn('Source {source} does not exist.'.format(source = source)) pass + def build(no_compress=False, verbose=False): - assets_path = join_path(frappe.local.sites_path, "assets") + assets_path = os.path.join(frappe.local.sites_path, "assets") for target, sources in iteritems(get_build_maps()): - pack(join_path(assets_path, target), sources, no_compress, verbose) + pack(os.path.join(assets_path, target), sources, no_compress, verbose) + def get_build_maps(): """get all build.jsons with absolute paths""" @@ -182,8 +193,8 @@ def get_build_maps(): build_maps = {} for app_path in app_paths: - path = join_path(app_path, 'public', 'build.json') - if path_exists(path): + path = os.path.join(app_path, 'public', 'build.json') + if os.path.exists(path): with open(path) as f: try: for target, sources in iteritems(json.loads(f.read())): @@ -191,9 +202,10 @@ def get_build_maps(): source_paths = [] for source in sources: if isinstance(source, list): - s = frappe.get_pymodule_path(source[0], *source[1].split("/")) + s = frappe.get_pymodule_path( + source[0], *source[1].split("/")) else: - s = join_path(app_path, source) + s = os.path.join(app_path, source) source_paths.append(s) build_maps[target] = source_paths @@ -202,7 +214,6 @@ def get_build_maps(): print('JSON syntax error {0}'.format(str(e))) return build_maps -timestamps = {} def pack(target, sources, no_compress, verbose): from six import StringIO @@ -212,8 +223,9 @@ def pack(target, sources, no_compress, verbose): for f in sources: suffix = None - if ':' in f: f, suffix = f.split(':') - if not path_exists(f) or isdir(f): + if ':' in f: + f, suffix = f.split(':') + if not os.path.exists(f) or os.path.isdir(f): print("did not find " + f) continue timestamps[f] = os.path.getmtime(f) @@ -223,7 +235,7 @@ def pack(target, sources, no_compress, verbose): extn = f.rsplit(".", 1)[1] - if outtype=="js" and extn=="js" and (not no_compress) and suffix!="concat" and (".min." not in f): + if outtype == "js" and extn == "js" and (not no_compress) and suffix != "concat" and (".min." not in f): tmpin, tmpout = StringIO(data.encode('utf-8')), StringIO() jsm.minify(tmpin, tmpout) minified = tmpout.getvalue() @@ -232,7 +244,7 @@ def pack(target, sources, no_compress, verbose): if verbose: print("{0}: {1}k".format(f, int(len(minified) / 1024))) - elif outtype=="js" and extn=="html": + elif outtype == "js" and extn == "html": # add to frappe.templates outtxt += html_to_js_template(f, data) else: @@ -248,43 +260,48 @@ def pack(target, sources, no_compress, verbose): print("Wrote %s - %sk" % (target, str(int(os.path.getsize(target)/1024)))) + def html_to_js_template(path, content): '''returns HTML template content as Javascript code, adding it to `frappe.templates`''' - return """frappe.templates["{key}"] = '{content}';\n""".format(\ + return """frappe.templates["{key}"] = '{content}';\n""".format( key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content)) + def scrub_html_template(content): '''Returns HTML content with removed whitespace and comments''' # remove whitespace to a single space content = re.sub("\s+", " ", content) # strip comments - content = re.sub("()", "", content) + content = re.sub("()", "", content) return content.replace("'", "\'") + def files_dirty(): for target, sources in iteritems(get_build_maps()): for f in sources: - if ':' in f: f, suffix = f.split(':') - if not path_exists(f) or isdir(f): continue + if ':' in f: + f, suffix = f.split(':') + if not os.path.exists(f) or os.path.isdir(f): + continue if os.path.getmtime(f) != timestamps.get(f): print(f + ' dirty') return True else: return False + def compile_less(): - from distutils.spawn import find_executable if not find_executable("lessc"): return for path in app_paths: - less_path = join_path(path, "public", "less") - if path_exists(less_path): + less_path = os.path.join(path, "public", "less") + if os.path.exists(less_path): for fname in os.listdir(less_path): if fname.endswith(".less") and fname != "variables.less": - fpath = join_path(less_path, fname) + fpath = os.path.join(less_path, fname) mtime = os.path.getmtime(fpath) if fpath in timestamps and mtime == timestamps[fpath]: continue @@ -293,5 +310,5 @@ def compile_less(): print("compiling {0}".format(fpath)) - css_path = join_path(path, "public", "css", fname.rsplit(".", 1)[0] + ".css") + css_path = os.path.join(path, "public", "css", fname.rsplit(".", 1)[0] + ".css") os.system("lessc {0} > {1}".format(fpath, css_path)) From 16c4c8f2027b141a0d4ac4df040b2e7ca1606272 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Sat, 4 Jan 2020 11:09:33 +0530 Subject: [PATCH 22/70] fix(install): bench jupyter --- frappe/commands/utils.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index d29f0a9c97..5013801960 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -389,19 +389,17 @@ def postgres(context): @click.command('jupyter') @pass_context def jupyter(context): - try: - from pip import main - except ImportError: - from pip._internal import main + installed_packages = (r.split('==')[0] for r in subprocess.check_output([sys.executable, '-m', 'pip', 'freeze'], encoding='utf8')) - reqs = subprocess.check_output([sys.executable, '-m', 'pip', 'freeze']) - installed_packages = [r.decode().split('==')[0] for r in reqs.split()] if 'jupyter' not in installed_packages: - main(['install', 'jupyter']) + subprocess.check_output([sys.executable, '-m', 'pip', 'install', 'jupyter']) + site = get_site(context) frappe.init(site=site) + jupyter_notebooks_path = os.path.abspath(frappe.get_site_path('jupyter_notebooks')) sites_path = os.path.abspath(frappe.get_site_path('..')) + try: os.stat(jupyter_notebooks_path) except OSError: @@ -434,7 +432,7 @@ def console(context): frappe.connect() frappe.local.lang = frappe.db.get_default("lang") import IPython - IPython.embed(display_banner = "") + IPython.embed(display_banner="") @click.command('run-tests') @click.option('--app', help="For App") @@ -472,8 +470,8 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), cov.start() ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests, - force=context.force, profile=profile, junit_xml_output=junit_xml_output, - ui_tests = ui_tests, doctype_list_path = doctype_list_path, failfast=failfast) + force=context.force, profile=profile, junit_xml_output=junit_xml_output, + ui_tests=ui_tests, doctype_list_path=doctype_list_path, failfast=failfast) if coverage: cov.stop() From 68659c55193f055d1d73a7d1cdc9fa2469865c74 Mon Sep 17 00:00:00 2001 From: "Chinmay D. Pai" Date: Sat, 4 Jan 2020 11:51:49 +0530 Subject: [PATCH 23/70] fix(email): define email_server before condition fixes issue where email_server is unhandled in test Traceback (most recent call last): File "/home/chnmy/workspace/frappe/benches/master-bench/apps/frappe/frappe/email/doctype/email_account/test_email_account.py", line 44, in test_unread_notification self.test_incoming() File "/home/chnmy/workspace/frappe/benches/master-bench/apps/frappe/frappe/email/doctype/email_account/test_email_account.py", line 35, in test_incoming email_account.receive(test_mails=test_mails) File "/home/chnmy/workspace/frappe/benches/master-bench/apps/frappe/frappe/email/doctype/email_account/email_account.py", line 291, in receive self.handle_bad_emails(email_server, uid, msg, frappe.get_traceback()) UnboundLocalError: local variable 'email_server' referenced before assignment Signed-off-by: Chinmay D. Pai --- frappe/email/doctype/email_account/email_account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 50daf1cf72..7b19ddccc6 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -247,13 +247,13 @@ class EmailAccount(Document): exceptions = [] seen_status = [] uid_reindexed = False + email_server = None if frappe.local.flags.in_test: incoming_mails = test_mails else: email_sync_rule = self.build_email_sync_rule() - email_server = None try: email_server = self.get_incoming_server(in_receive=True, email_sync_rule=email_sync_rule) except Exception: From ba9d6020e65e8dc248ca80e2821adfe832c9c2fd Mon Sep 17 00:00:00 2001 From: "Chinmay D. Pai" Date: Sat, 4 Jan 2020 12:19:57 +0530 Subject: [PATCH 24/70] chore: check if email_server exists before handling bad emails Signed-off-by: Chinmay D. Pai --- frappe/email/doctype/email_account/email_account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 7b19ddccc6..337bfac79b 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -305,7 +305,7 @@ class EmailAccount(Document): raise Exception(frappe.as_json(exceptions)) def handle_bad_emails(self, email_server, uid, raw, reason): - if cint(email_server.settings.use_imap): + if email_server and cint(email_server.settings.use_imap): import email try: mail = email.message_from_string(raw) From 9dc172692d62984d23c05149ec8b8e889065f6e6 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Sat, 4 Jan 2020 13:42:23 +0530 Subject: [PATCH 25/70] feat: added apps installed on site in console ns chore: dropped deprecated commands style: whitespacing and new line fixes --- frappe/commands/utils.py | 79 +++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 46 deletions(-) diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index 5013801960..ddf2d3a6c3 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -12,6 +12,7 @@ from coverage import Coverage import cProfile, pstats from six import StringIO + @click.command('build') @click.option('--app', help='Build assets for app') @click.option('--make-copy', is_flag=True, default=False, help='Copy the files instead of symlinking') @@ -26,15 +27,15 @@ def build(app=None, make_copy=False, restore = False, verbose=False): no_compress = frappe.local.conf.developer_mode or False frappe.build.bundle(no_compress, app=app, make_copy=make_copy, restore = restore, verbose=verbose) + @click.command('watch') def watch(): "Watch and concatenate JS and CSS files as and when they change" - # if os.environ.get('CI'): - # return import frappe.build frappe.init('') frappe.build.watch(True) + @click.command('clear-cache') @pass_context def clear_cache(context): @@ -51,6 +52,7 @@ def clear_cache(context): finally: frappe.destroy() + @click.command('clear-website-cache') @pass_context def clear_website_cache(context): @@ -64,6 +66,7 @@ def clear_website_cache(context): finally: frappe.destroy() + @click.command('destroy-all-sessions') @click.option('--reason') @pass_context @@ -79,6 +82,7 @@ def destroy_all_sessions(context, reason=None): finally: frappe.destroy() + @click.command('show-config') @pass_context def show_config(context): @@ -89,6 +93,7 @@ def show_config(context): configuration = frappe.get_site_config(sites_path=sites_path, site_path=site_path) print_config(configuration) + def print_config(config): for conf, value in config.items(): if isinstance(value, dict): @@ -96,6 +101,7 @@ def print_config(config): else: print("\t{:<50} {:<15}".format(conf, value)) + @click.command('reset-perms') @pass_context def reset_perms(context): @@ -112,6 +118,7 @@ def reset_perms(context): finally: frappe.destroy() + @click.command('execute') @click.argument('method') @click.option('--args') @@ -191,6 +198,7 @@ def export_doc(context, doctype, docname): finally: frappe.destroy() + @click.command('export-json') @click.argument('doctype') @click.argument('path') @@ -207,6 +215,7 @@ def export_json(context, doctype, path, name=None): finally: frappe.destroy() + @click.command('export-csv') @click.argument('doctype') @click.argument('path') @@ -222,6 +231,7 @@ def export_csv(context, doctype, path): finally: frappe.destroy() + @click.command('export-fixtures') @click.option('--app', default=None, help='Export fixtures of a specific app') @pass_context @@ -236,6 +246,7 @@ def export_fixtures(context, app=None): finally: frappe.destroy() + @click.command('import-doc') @click.argument('path') @pass_context @@ -257,6 +268,7 @@ def import_doc(context, path, force=False): finally: frappe.destroy() + @click.command('import-csv') @click.argument('path') @click.option('--only-insert', default=False, is_flag=True, help='Do not overwrite existing records') @@ -264,6 +276,7 @@ def import_doc(context, path, force=False): @click.option('--ignore-encoding-errors', default=False, is_flag=True, help='Ignore encoding errors while coverting to unicode') @click.option('--no-email', default=True, is_flag=True, help='Send email if applicable') + @pass_context def import_csv(context, path, only_insert=False, submit_after_import=False, ignore_encoding_errors=False, no_email=True): "Import CSV using data import" @@ -324,7 +337,7 @@ def data_import(context, file_path, doctype, import_type=None, submit_after_impo @click.argument('doctype') @click.argument('path') @pass_context -def _bulk_rename(context, doctype, path): +def bulk_rename(context, doctype, path): "Rename multiple records via CSV file" from frappe.model.rename_doc import bulk_rename from frappe.utils.csvutils import read_csv_content @@ -341,15 +354,6 @@ def _bulk_rename(context, doctype, path): frappe.destroy() -@click.command('mysql') -def mysql(): - """ - Deprecated - """ - click.echo(""" -mysql command is deprecated. -Did you mean "bench mariadb"? -""") @click.command('mariadb') @pass_context @@ -374,6 +378,7 @@ def mariadb(context): '--safe-updates', "-A"]) + @click.command('postgres') @pass_context def postgres(context): @@ -386,6 +391,7 @@ def postgres(context): psql = find_executable('psql') subprocess.run([ psql, '-d', frappe.conf.db_name]) + @click.command('jupyter') @pass_context def jupyter(context): @@ -423,6 +429,7 @@ frappe.db.connect() jupyter_notebooks_path, ]) + @click.command('console') @pass_context def console(context): @@ -432,7 +439,12 @@ def console(context): frappe.connect() frappe.local.lang = frappe.db.get_default("lang") import IPython - IPython.embed(display_banner="") + all_apps = frappe.get_installed_apps() + for app in all_apps: + locals()[app] = __import__(app) + print("Apps in this namespace:\n{}".format(", ".join(all_apps))) + IPython.embed(display_banner="", header="") + @click.command('run-tests') @click.option('--app', help="For App") @@ -483,6 +495,7 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), if os.environ.get('CI'): sys.exit(ret) + @click.command('run-ui-tests') @click.argument('app') @click.option('--headless', is_flag=True, help="Run UI Test in headless mode") @@ -505,6 +518,7 @@ def run_ui_tests(context, app, headless=False): 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) + @click.command('serve') @click.option('--port', default=8000) @click.option('--profile', is_flag=True, default=False) @@ -522,6 +536,7 @@ def serve(context, port=None, profile=False, no_reload=False, no_threading=False frappe.app.serve(port=port, profile=profile, no_reload=no_reload, no_threading=no_threading, site=site, sites_path='.') + @click.command('request') @click.option('--args', help='arguments like `?cmd=test&key=value` or `/api/request/method?..`') @click.option('--path', help='path to request JSON') @@ -554,6 +569,7 @@ def request(context, args=None, path=None): finally: frappe.destroy() + @click.command('make-app') @click.argument('destination') @click.argument('app_name') @@ -562,6 +578,7 @@ def make_app(destination, app_name): from frappe.utils.boilerplate import make_boilerplate make_boilerplate(destination, app_name) + @click.command('set-config') @click.argument('key') @click.argument('value') @@ -585,6 +602,7 @@ def set_config(context, key, value, global_ = False, as_dict=False): update_site_config(key, value, validate=False) frappe.destroy() + @click.command('version') def get_version(): "Show the versions of all the installed apps" @@ -603,32 +621,6 @@ def get_version(): print("{0} {1}".format(m, module.__version__)) -@click.command('setup-global-help') -@click.option('--db_type') -@click.option('--root_password') -def setup_global_help(db_type=None, root_password=None): - '''Deprecated: setup help table in a separate database that will be - shared by the whole bench and set `global_help_setup` as 1 in - common_site_config.json''' - print_in_app_help_deprecation() - -@click.command('get-docs-app') -@click.argument('app') -def get_docs_app(app): - '''Deprecated: Get the docs app for given app''' - print_in_app_help_deprecation() - -@click.command('get-all-docs-apps') -def get_all_docs_apps(): - '''Deprecated: Get docs apps for all apps''' - print_in_app_help_deprecation() - -@click.command('setup-help') -@pass_context -def setup_help(context): - '''Deprecated: Setup help table in the current site (called after migrate)''' - print_in_app_help_deprecation() - @click.command('rebuild-global-search') @click.option('--static-pages', is_flag=True, default=False, help='Rebuild global search for static pages') @pass_context @@ -658,6 +650,7 @@ def rebuild_global_search(context, static_pages=False): finally: frappe.destroy() + @click.command('auto-deploy') @click.argument('app') @click.option('--migrate', is_flag=True, default=False, help='Migrate after pulling') @@ -702,9 +695,6 @@ def auto_deploy(context, app, migrate=False, restart=False, remote='upstream'): else: print('No Updates') -def print_in_app_help_deprecation(): - print("In app help has been removed.\nYou can access the documentation on erpnext.com/docs or frappe.io/docs") - return commands = [ build, @@ -723,7 +713,6 @@ commands = [ data_import, import_doc, make_app, - mysql, mariadb, postgres, request, @@ -734,9 +723,7 @@ commands = [ set_config, show_config, watch, - _bulk_rename, + bulk_rename, add_to_email_queue, - setup_global_help, - setup_help, rebuild_global_search ] From 18d8e65cca606a1100b74d5864c9782036aa6574 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Sat, 4 Jan 2020 19:12:59 +0530 Subject: [PATCH 26/70] =?UTF-8?q?fix:=20Make=20background=20jobs=20logging?= =?UTF-8?q?=20quieter=20if=20quite=20flag=20is=20pass=E2=80=A6=20(#9200)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frappe/utils/background_jobs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py index c1ac7581dc..65b2326733 100755 --- a/frappe/utils/background_jobs.py +++ b/frappe/utils/background_jobs.py @@ -142,6 +142,8 @@ def start_worker(queue=None, quiet = False): with Connection(redis_connection): queues = get_queue_list(queue) logging_level = "INFO" + if quiet: + logging_level = "WARNING" Worker(queues, name=get_worker_name(queue)).work(logging_level = logging_level) def get_worker_name(queue): From 0e5e3497b1d474e1ff4f9655fb122529c2cdfce1 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Sat, 4 Jan 2020 21:05:02 +0530 Subject: [PATCH 27/70] fix: as_table msgprint (#9174) * chore: improve readability * fix: as_table msgprint - fix as_table msgprint style and functionality - Make table creation code more readable - Make a text translatable * style: Fix few deepsource issues * style: Fix few deepsource issues fix indentation * fix: Incorrect assignment * fix: Scrub URLs only for html format * fix(typo): msg -> message Co-authored-by: Raffael Meyer --- frappe/__init__.py | 140 ++++++++++++++++++++++++++------------------- 1 file changed, 81 insertions(+), 59 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index b383ae958e..5e797fd8cd 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -321,10 +321,19 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, return if as_table and type(msg) in (list, tuple): - out.msg = '' + ''.join([''+''.join(['' % c for c in r])+'' for r in msg]) + '
%s
' - if flags.print_messages and out.msg: - print("Message: " + repr(out.msg).encode("utf-8")) + table_rows = '' + for row in msg: + table_row_data = '' + for data in row: + table_row_data += '{}'.format(data) + table_rows += '{}'.format(table_row_data) + + out.message = '''{}
'''.format(table_rows) + + if flags.print_messages and out.message: + print("Message: " + repr(out.message).encode("utf-8")) if title: out.title = title @@ -363,7 +372,6 @@ def throw(msg, exc=ValidationError, title=None): msgprint(msg, raise_exception=exc, title=title, indicator='red') def emit_js(js, user=False, **kwargs): - from frappe.realtime import publish_realtime if user == False: user = session.user publish_realtime('eval_js', js, user=user, **kwargs) @@ -767,8 +775,8 @@ def get_meta_module(doctype): import frappe.modules return frappe.modules.load_doctype_module(doctype) -def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reload=False, - ignore_permissions=False, flags=None, ignore_on_trash=False, ignore_missing=True): +def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, + for_reload=False, ignore_permissions=False, flags=None, ignore_on_trash=False, ignore_missing=True): """Delete a document. Calls `frappe.model.delete_doc.delete_doc`. :param doctype: DocType of document to be delete. @@ -805,8 +813,8 @@ def reload_doc(module, dt=None, dn=None, force=False, reset_permissions=False): def rename_doc(*args, **kwargs): """Rename a document. Calls `frappe.model.rename_doc.rename_doc`""" - from frappe.model.rename_doc import rename_doc - return rename_doc(*args, **kwargs) + from frappe.model.rename_doc import rename_doc as _rename_doc + return _rename_doc(*args, **kwargs) def get_module(modulename): """Returns a module object for given Python module name using `importlib.import_module`.""" @@ -814,11 +822,11 @@ def get_module(modulename): def scrub(txt): """Returns sluggified string. e.g. `Sales Order` becomes `sales_order`.""" - return txt.replace(' ','_').replace('-', '_').lower() + return txt.replace(' ', '_').replace('-', '_').lower() def unscrub(txt): """Returns titlified string. e.g. `sales_order` becomes `Sales Order`.""" - return txt.replace('_',' ').replace('-', ' ').title() + return txt.replace('_', ' ').replace('-', ' ').title() def get_module_path(module, *joins): """Get the path of the given module name. @@ -980,7 +988,8 @@ def setup_module_map(): if not (local.app_modules and local.module_app): local.module_app, local.app_modules = {}, {} for app in get_all_apps(True): - if app=="webnotes": app="frappe" + if app == "webnotes": + app = "frappe" local.app_modules.setdefault(app, []) for module in get_module_list(app): module = scrub(module) @@ -999,7 +1008,10 @@ def get_file_items(path, raise_not_found=False, ignore_empty_lines=True): if content: content = frappe.utils.strip(content) - return [p.strip() for p in content.splitlines() if (not ignore_empty_lines) or (p.strip() and not p.startswith("#"))] + return [ + p.strip() for p in content.splitlines() + if (not ignore_empty_lines) or (p.strip() and not p.startswith("#")) + ] else: return [] @@ -1161,8 +1173,8 @@ def compare(val1, condition, val2): import frappe.utils return frappe.utils.compare(val1, condition, val2) -def respond_as_web_page(title, html, success=None, http_status_code=None, - context=None, indicator_color=None, primary_action='/', primary_label = None, fullpage=False, +def respond_as_web_page(title, html, success=None, http_status_code=None, context=None, + indicator_color=None, primary_action='/', primary_label = None, fullpage=False, width=None, template='message'): """Send response as a web page with a message rather than JSON. Used to show permission errors etc. @@ -1351,7 +1363,8 @@ def format(*args, **kwargs): import frappe.utils.formatters return frappe.utils.formatters.format_value(*args, **kwargs) -def get_print(doctype=None, name=None, print_format=None, style=None, html=None, as_pdf=False, doc=None, output = None, no_letterhead = 0, password=None): +def get_print(doctype=None, name=None, print_format=None, style=None, + html=None, as_pdf=False, doc=None, output=None, no_letterhead=0, password=None): """Get Print Format for given document. :param doctype: DocType of document. @@ -1382,7 +1395,8 @@ def get_print(doctype=None, name=None, print_format=None, style=None, html=None, else: return html -def attach_print(doctype, name, file_name=None, print_format=None, style=None, html=None, doc=None, lang=None, print_letterhead=True, password=None): +def attach_print(doctype, name, file_name=None, print_format=None, + style=None, html=None, doc=None, lang=None, print_letterhead=True, password=None): from frappe.utils import scrub_urls if not file_name: file_name = name @@ -1398,16 +1412,28 @@ def attach_print(doctype, name, file_name=None, print_format=None, style=None, h no_letterhead = not print_letterhead + kwargs = dict( + print_format=print_format, + style=style, + html=html, + doc=doc, + no_letterhead=no_letterhead, + password=password + ) + + content = '' if int(print_settings.send_print_as_pdf or 0): - out = { - "fname": file_name + ".pdf", - "fcontent": get_print(doctype, name, print_format=print_format, style=style, html=html, as_pdf=True, doc=doc, no_letterhead=no_letterhead, password=password) - } + ext = ".pdf" + kwargs["as_pdf"] = True + content = get_print(doctype, name, **kwargs) else: - out = { - "fname": file_name + ".html", - "fcontent": scrub_urls(get_print(doctype, name, print_format=print_format, style=style, html=html, doc=doc, no_letterhead=no_letterhead, password=password)).encode("utf-8") - } + ext = ".html" + content = scrub_urls(get_print(doctype, name, **kwargs)).encode('utf-8') + + out = { + "fname": file_name + ext, + "fcontent": content + } local.flags.ignore_print_permissions = False #reset lang to original local lang @@ -1526,7 +1552,12 @@ def log_error(message=None, title=None): method=title)).insert(ignore_permissions=True) def get_desk_link(doctype, name): - return '{2} {1}'.format(doctype, name, _(doctype)) + html = '{doctype_local} {name}' + return html.format( + doctype=doctype, + name=name, + doctype_local=_(doctype) + ) def bold(text): return '{0}'.format(text) @@ -1545,10 +1576,9 @@ def safe_eval(code, eval_globals=None, eval_locals=None): if not eval_globals: eval_globals = {} + eval_globals['__builtins__'] = {} - eval_globals.update(whitelisted_globals) - return eval(code, eval_globals, eval_locals) def get_system_settings(key): @@ -1560,9 +1590,11 @@ def get_active_domains(): from frappe.core.doctype.domain_settings.domain_settings import get_active_domains return get_active_domains() -def get_version(doctype, name, limit = None, head = False, raise_err = True): +def get_version(doctype, name, limit=None, head=False, raise_err=True): ''' - Returns a list of version information of a given DocType (Applicable only if DocType has changes tracked). + Returns a list of version information of a given DocType. + + Note: Applicable only if DocType has changes tracked. Example >>> frappe.get_version('User', 'foobar@gmail.com') @@ -1575,34 +1607,29 @@ def get_version(doctype, name, limit = None, head = False, raise_err = True): } ] ''' - meta = get_meta(doctype) + meta = get_meta(doctype) if meta.track_changes: - names = db.sql(""" - SELECT name from tabVersion - WHERE ref_doctype = '{doctype}' AND docname = '{name}' - {order_by} - {limit} - """.format( - doctype = doctype, - name = name, - order_by = 'ORDER BY creation' if head else '', - limit = 'LIMIT {limit}'.format(limit = limit) if limit else '' - )) + names = db.get_all('Version', filters={ + 'ref_doctype': doctype, + 'docname': name, + 'order_by': 'creation' if head else None, + 'limit': limit + }, as_list=1) from frappe.chat.util import squashify, dictify, safe_json_loads - versions = [ ] + versions = [] for name in names: name = squashify(name) - doc = get_doc('Version', name) + doc = get_doc('Version', name) data = doc.data data = safe_json_loads(data) data = dictify(dict( - version = data, - user = doc.owner, - creation = doc.creation + version=data, + user=doc.owner, + creation=doc.creation )) versions.append(data) @@ -1610,16 +1637,14 @@ def get_version(doctype, name, limit = None, head = False, raise_err = True): return versions else: if raise_err: - raise ValueError('{doctype} has no versions tracked.'.format( - doctype = doctype - )) + raise ValueError(_('{0} has no versions tracked.').format(doctype)) -@whitelist(allow_guest = True) +@whitelist(allow_guest=True) def ping(): return "pong" -def safe_encode(param, encoding = 'utf-8'): +def safe_encode(param, encoding='utf-8'): try: param = param.encode(encoding) except Exception: @@ -1627,7 +1652,7 @@ def safe_encode(param, encoding = 'utf-8'): return param -def safe_decode(param, encoding = 'utf-8'): +def safe_decode(param, encoding='utf-8'): try: param = param.decode(encoding) except Exception: @@ -1638,9 +1663,9 @@ def parse_json(val): from frappe.utils import parse_json return parse_json(val) -def mock(type, size = 1, locale = 'en'): - results = [ ] - faker = Faker(locale) +def mock(type, size=1, locale='en'): + results = [] + faker = Faker(locale) if not type in dir(faker): raise ValueError('Not a valid mock type.') else: @@ -1649,7 +1674,4 @@ def mock(type, size = 1, locale = 'en'): results.append(data) from frappe.chat.util import squashify - - results = squashify(results) - - return results + return squashify(results) From fb511b1dfd1d9fd5a2acf6bb93d2b67c5b69a878 Mon Sep 17 00:00:00 2001 From: "Chinmay D. Pai" Date: Sun, 5 Jan 2020 00:28:55 +0530 Subject: [PATCH 28/70] fix(email): check if communication hasattr for attachment fixes issue where previous communication has no attribute _attachments before it fails, causing the following error: Traceback (most recent call last): File "/home/frappe/frappe-bench/apps/frappe/frappe/utils/background_jobs.py", line 99, in execute_job method(**kwargs) File "/home/frappe/frappe-bench/apps/frappe/frappe/email/doctype/email_account/email_account.py", line 724, in pull_from_email_account email_account.receive() File "/home/frappe/frappe-bench/apps/frappe/frappe/email/doctype/email_account/email_account.py", line 295, in receive attachments = [d.file_name for d in communication._attachments] AttributeError: 'Communication' object has no attribute '_attachments' Signed-off-by: Chinmay D. Pai --- frappe/email/doctype/email_account/email_account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 50daf1cf72..cc02cfa0ff 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -293,7 +293,7 @@ class EmailAccount(Document): else: frappe.db.commit() - if communication: + if communication and hasattr(communication, "_attachments"): attachments = [d.file_name for d in communication._attachments] communication.notify(attachments=attachments, fetched_from_email_account=True) From 04e7b7a96b680e068ee4fa15794de27cc9272abc Mon Sep 17 00:00:00 2001 From: prssanna Date: Sun, 5 Jan 2020 15:59:38 +0530 Subject: [PATCH 29/70] fix: compare docs by state as key --- frappe/workflow/doctype/workflow/workflow.py | 24 ++++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/frappe/workflow/doctype/workflow/workflow.py b/frappe/workflow/doctype/workflow/workflow.py index 523a7437c4..663aae6ba4 100644 --- a/frappe/workflow/doctype/workflow/workflow.py +++ b/frappe/workflow/doctype/workflow/workflow.py @@ -59,14 +59,24 @@ class Workflow(Document): def update_doc_status(self): doc_before_save = self.get_doc_before_save() + before_save_states, new_states = {}, {} if doc_before_save: - for current_doc_state, doc_before_save_state in zip(self.states, doc_before_save.states): - if not doc_before_save_state.doc_status == current_doc_state.doc_status: - frappe.db.set_value(self.document_type, - {self.workflow_state_field: doc_before_save_state.state}, - 'docstatus', - current_doc_state.doc_status - ) + for d in doc_before_save.states: + before_save_states[d.state] = d + for d in self.states: + new_states[d.state] = d + + for key in new_states: + if key in before_save_states: + if not new_states[key].doc_status == before_save_states[key].doc_status: + frappe.db.set_value(self.document_type, + { + self.workflow_state_field: before_save_states[key].state + }, + 'docstatus', + new_states[key].doc_status, + update_modified = False + ) def validate_docstatus(self): def get_state(state): From 5754ef750b5ed76c0808347790da3fb4f82bea4e Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Sun, 5 Jan 2020 22:09:20 +0530 Subject: [PATCH 30/70] fix: use _user_tags instead of Tag Link --- frappe/desk/doctype/tag/tag.py | 1 - frappe/desk/reportview.py | 26 +------------------ frappe/public/js/frappe/list/list_sidebar.js | 7 +++-- frappe/public/js/frappe/list/list_view.js | 2 +- frappe/public/js/frappe/ui/filters/filter.js | 5 ---- .../js/frappe/ui/filters/filter_list.js | 5 ---- 6 files changed, 5 insertions(+), 41 deletions(-) diff --git a/frappe/desk/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py index 0e2afbb35c..63aa93e7ff 100644 --- a/frappe/desk/doctype/tag/tag.py +++ b/frappe/desk/doctype/tag/tag.py @@ -171,7 +171,6 @@ def get_documents_for_tag(tag): "content": res.title }) - print(results) return results @frappe.whitelist() diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index c0685b67f2..db37bc6df0 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -263,32 +263,8 @@ def delete_bulk(doctype, items): @frappe.whitelist() @frappe.read_only() def get_sidebar_stats(stats, doctype, filters=[]): - _user_tags, tag_list = [], [] - data = frappe._dict(frappe.local.form_dict) - filters = json.loads(data["filters"]) - # Show Tags irrespective of any tag filter set - for idx, filter in enumerate(filters): - if filter[0] == "Tag Link": - filters.pop(idx) - break - - for tag in frappe.get_all("Tag Link", filters={"document_type": doctype}, fields=["tag"]): - if tag.tag in tag_list: - continue - - tag_list.append(tag.tag) - tag_filters = [] - tag_filters.extend(filters) - tag_filters.extend([['Tag Link', 'tag', '=', tag.tag]]) - - fields = ["count(distinct `tab{0}`.`name`) AS total_count".format(doctype)] - count = frappe.get_all(doctype, filters=tag_filters, fields=fields) - - if count[0].get("total_count") > 0: - _user_tags.append([tag.tag, count[0].get("total_count")]) - - return {"stats": {"_user_tags": _user_tags}} + return {"stats": get_stats(stats, doctype, filters)} @frappe.whitelist() @frappe.read_only() diff --git a/frappe/public/js/frappe/list/list_sidebar.js b/frappe/public/js/frappe/list/list_sidebar.js index 88b5393c15..47807ac214 100644 --- a/frappe/public/js/frappe/list/list_sidebar.js +++ b/frappe/public/js/frappe/list/list_sidebar.js @@ -335,14 +335,13 @@ frappe.views.ListSidebar = class ListSidebar { field: field, stat: stats, sum: sum, - label: field === '_user_tags' ? (tags ? __(label) : __("Tag")) : __(label), + label: field === '_user_tags' ? (tags ? __(label) : __("Tags")) : __(label), }; $(frappe.render_template("list_sidebar_stat", context)) .on("click", ".stat-link", function() { - var doctype = "Tag Link"; var fieldname = $(this).attr('data-field'); var label = $(this).attr('data-label'); - var condition = "="; + var condition = "like"; var existing = me.list_view.filter_area.filter_list.get_filter(fieldname); if(existing) { existing.remove(); @@ -351,7 +350,7 @@ frappe.views.ListSidebar = class ListSidebar { label = "%,%"; condition = "not like"; } - me.list_view.filter_area.filter_list.add_filter(doctype, fieldname, condition, label) + me.list_view.filter_area.filter_list.add_filter(me.list_view.doctype, fieldname, condition, label) .then(function() { me.list_view.refresh(); }); diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 561117e342..377f668864 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -470,7 +470,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { tag_editor.wrapper.on('click', '.tagit-label', (e) => { const $this = $(e.currentTarget); - this.filter_area.add('Tag Link', 'tag', '=', $this.text()); + this.filter_area.add(this.doctype, '_user_tags', '=', $this.text()); }); }); } diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index 5cdc3424f8..08b6524afc 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -174,11 +174,6 @@ frappe.ui.Filter = class { let original_docfield = (this.fieldselect.fields_by_name[doctype] || {})[fieldname]; - if (doctype === "Tag Link" || fieldname === "_user_tags") { - original_docfield = {fieldname: "tag", fieldtype: "Data", label: "Tags", parent: "Tag Link"}; - doctype = "Tag Link"; - } - if(!original_docfield) { console.warn(`Field ${fieldname} is not selectable.`); this.remove(); diff --git a/frappe/public/js/frappe/ui/filters/filter_list.js b/frappe/public/js/frappe/ui/filters/filter_list.js index b9a131d4c7..77d3cbfcd3 100644 --- a/frappe/public/js/frappe/ui/filters/filter_list.js +++ b/frappe/public/js/frappe/ui/filters/filter_list.js @@ -63,11 +63,6 @@ frappe.ui.FilterGroup = class { } validate_args(doctype, fieldname) { - // Tags attached to the document are maintained seperately in Tag Link - // and is not the part of doctype meta therefore tag fieldname validation is ignored. - if (doctype === "Tag Link" && fieldname === "tag") { - return true; - } if(doctype && fieldname && !frappe.meta.has_field(doctype, fieldname) From e9f0e2b4c76c911ea742d124c93758b1ea797c25 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 6 Jan 2020 10:39:09 +0530 Subject: [PATCH 31/70] fix: Save error --- frappe/desk/query_report.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 7dc561193f..e3434885c6 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -424,9 +424,10 @@ def get_data_for_custom_report(columns): def save_report(reference_report, report_name, columns): report_doc = get_report_doc(reference_report) - docname = frappe.db.exists("Report", report_name) + docname = frappe.db.exists("Report", + {'report_name': report_name, 'is_standard': 'No', 'report_type': 'Custom Report'}) if docname: - report = frappe.get_doc("Report", {'report_name': docname, 'is_standard': 'No', 'report_type': 'Custom Report'}) + report = frappe.get_doc("Report", docname) report.update({"json": columns}) report.save() frappe.msgprint(_("Report updated successfully")) From 1e0f56bac705909315a84257bc285f184973e570 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 6 Jan 2020 13:34:31 +0530 Subject: [PATCH 32/70] fix: Persist scope search in search page --- frappe/templates/includes/search_template.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frappe/templates/includes/search_template.html b/frappe/templates/includes/search_template.html index 89f71994b6..8ad73f836a 100644 --- a/frappe/templates/includes/search_template.html +++ b/frappe/templates/includes/search_template.html @@ -27,6 +27,9 @@ + {% if frappe.form_dict.scope %} + + {% endif %} From 9acd30c306abeb7a4038e0830988fb0bb2320a57 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Mon, 6 Jan 2020 16:29:01 +0530 Subject: [PATCH 33/70] fix: allow multiple webhooks for identical doctype triggers --- frappe/integrations/doctype/webhook/webhook.json | 14 +++++++++++++- frappe/integrations/doctype/webhook/webhook.py | 3 --- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/frappe/integrations/doctype/webhook/webhook.json b/frappe/integrations/doctype/webhook/webhook.json index c62edd9274..8aa96e6859 100644 --- a/frappe/integrations/doctype/webhook/webhook.json +++ b/frappe/integrations/doctype/webhook/webhook.json @@ -1,10 +1,13 @@ { + "actions": [], + "autoname": "naming_series:", "creation": "2017-09-08 16:16:13.060641", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ "sb_doc_events", + "naming_series", "webhook_doctype", "cb_doc_events", "webhook_docevent", @@ -43,6 +46,7 @@ { "fieldname": "webhook_docevent", "fieldtype": "Select", + "in_list_view": 1, "label": "Doc Event", "options": "after_insert\non_update\non_submit\non_cancel\non_trash\non_update_after_submit\non_change", "set_only_once": 1 @@ -117,9 +121,16 @@ "fieldname": "webhook_json", "fieldtype": "Code", "label": "JSON Request Body" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "\nHOOK-.####" } ], - "modified": "2019-08-26 00:38:14.611267", + "links": [], + "modified": "2020-01-06 02:51:07.997566", "modified_by": "Administrator", "module": "Integrations", "name": "Webhook", @@ -140,5 +151,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "title_field": "webhook_doctype", "track_changes": 1 } \ No newline at end of file diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index b70a3e2d8c..0a97022f66 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -18,9 +18,6 @@ from frappe.utils.jinja import validate_template class Webhook(Document): - def autoname(self): - self.name = self.webhook_doctype + "-" + self.webhook_docevent - def validate(self): self.validate_docevent() self.validate_condition() From 3cce101c2dfb4de51ee7dbb64c8a906b0d354391 Mon Sep 17 00:00:00 2001 From: prssanna Date: Tue, 7 Jan 2020 00:58:43 +0530 Subject: [PATCH 34/70] test: add test to check if document status is updated --- frappe/workflow/doctype/workflow/test_workflow.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frappe/workflow/doctype/workflow/test_workflow.py b/frappe/workflow/doctype/workflow/test_workflow.py index 52a94d59c6..66c0577026 100644 --- a/frappe/workflow/doctype/workflow/test_workflow.py +++ b/frappe/workflow/doctype/workflow/test_workflow.py @@ -88,6 +88,20 @@ class TestWorkflow(unittest.TestCase): self.assertEqual(workflow_actions[0].status, 'Completed') frappe.set_user('Administrator') + def test_update_docstatus(self): + todo = create_new_todo() + apply_workflow(todo, 'Approve') + + self.workflow.states[1].doc_status = 0 + self.workflow.save() + todo.reload() + self.assertEqual(todo.docstatus, 0) + self.workflow.states[1].doc_status = 1 + self.workflow.save() + todo.reload() + self.assertEqual(todo.docstatus, 1) + + def create_todo_workflow(): if frappe.db.exists('Workflow', 'Test ToDo'): return frappe.get_doc('Workflow', 'Test ToDo').save(ignore_permissions=True) From ef54a4ec5d6daa2d186290d91d9fa3d788eceda6 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 7 Jan 2020 12:32:40 +0530 Subject: [PATCH 35/70] fix: frappe.confirm fixes --- frappe/public/js/frappe/form/toolbar.js | 8 ++++++-- frappe/public/js/frappe/model/model.js | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index a84432d792..f6bc6f7cc8 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -80,10 +80,12 @@ frappe.ui.form.Toolbar = Class.extend({ const title_field = this.frm.meta.title_field || ''; const doctype = this.frm.doctype; + let confirm_message=null; + if (new_name) { const warning = __("This cannot be undone"); const message = __("Are you sure you want to merge {0} with {1}?", [docname.bold(), new_name.bold()]); - const confirm_message = message + "
" + warning + ""; + confirm_message = `${message}
${warning}`; } let rename_document = () => { @@ -109,7 +111,9 @@ frappe.ui.form.Toolbar = Class.extend({ this.show_unchanged_document_alert(); resolve(); } else if (merge) { - frappe.confirm(confirm_message, function() { rename_document().then(resolve).catch(reject) }, reject); + frappe.confirm(confirm_message, () => { + rename_document().then(resolve).catch(reject); + }, reject); } else { rename_document().then(resolve).catch(reject); } diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 37e087905b..ec44be5830 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -557,8 +557,8 @@ $.extend(frappe.model, { var d = new frappe.ui.Dialog({ title: __("Rename {0}", [__(docname)]), fields: [ - {label:__("New Name"), fieldname: "new_name", fieldtype:"Data", reqd:1, "default": docname}, - {label:merge_label, fieldtype:"Check", fieldname:"merge"}, + {label: __("New Name"), fieldname: "new_name", fieldtype: "Data", reqd: 1, "default": docname}, + {label: merge_label, fieldtype: "Check", fieldname: "merge"}, ] }); From 994475877382d128398208fdc1cea749c562a462 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Tue, 7 Jan 2020 13:22:00 +0530 Subject: [PATCH 36/70] fix: webform view --- frappe/public/js/frappe/web_form/web_form_list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/web_form/web_form_list.js b/frappe/public/js/frappe/web_form/web_form_list.js index db5f1da3a0..79ee6ed1a5 100644 --- a/frappe/public/js/frappe/web_form/web_form_list.js +++ b/frappe/public/js/frappe/web_form/web_form_list.js @@ -201,7 +201,7 @@ export default class WebFormList { () => (window.location.href = window.location.pathname + "?new=1") ); - if (this.rows.length <= this.page_length) { + if (this.rows.length >= this.page_length) { addButton(footer, "more", "secondary", false, "More", () => this.more()); } From ade6794b3edcb73005db95f1795bf8cbca8c1ebf Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Tue, 7 Jan 2020 17:00:23 +0530 Subject: [PATCH 37/70] fix: missing column datafor Kanban board --- frappe/public/js/frappe/views/kanban/kanban_view.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frappe/public/js/frappe/views/kanban/kanban_view.js b/frappe/public/js/frappe/views/kanban/kanban_view.js index 72c07b3776..4b6792330f 100644 --- a/frappe/public/js/frappe/views/kanban/kanban_view.js +++ b/frappe/public/js/frappe/views/kanban/kanban_view.js @@ -108,6 +108,11 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView { }); } + get_fields() { + this.fields.push([this.board.field_name, this.board.reference_doctype]); + return super.get_fields(); + } + render() { const board_name = this.board_name; if (this.kanban && board_name === this.kanban.board_name) { From 160d49574361ef3585fb40d4a313b5bd9e8852e9 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Wed, 8 Jan 2020 09:37:06 +0530 Subject: [PATCH 38/70] style: Update docstring and fix formatting --- frappe/workflow/doctype/workflow/workflow.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frappe/workflow/doctype/workflow/workflow.py b/frappe/workflow/doctype/workflow/workflow.py index 663aae6ba4..62e0b39b08 100644 --- a/frappe/workflow/doctype/workflow/workflow.py +++ b/frappe/workflow/doctype/workflow/workflow.py @@ -58,6 +58,10 @@ class Workflow(Document): docstatus_map[d.doc_status] = d.state def update_doc_status(self): + ''' + Checks if the docstatus of a state was updated. + If yes then the docstatus of the document with same state will be updated + ''' doc_before_save = self.get_doc_before_save() before_save_states, new_states = {}, {} if doc_before_save: @@ -69,14 +73,12 @@ class Workflow(Document): for key in new_states: if key in before_save_states: if not new_states[key].doc_status == before_save_states[key].doc_status: - frappe.db.set_value(self.document_type, - { + frappe.db.set_value(self.document_type, { self.workflow_state_field: before_save_states[key].state }, 'docstatus', new_states[key].doc_status, - update_modified = False - ) + update_modified = False) def validate_docstatus(self): def get_state(state): From 57ea150cc7b0835c31924d68996decf9c1340d56 Mon Sep 17 00:00:00 2001 From: Ernesto Ruiz Date: Wed, 8 Jan 2020 09:49:20 -0600 Subject: [PATCH 39/70] fix: add translation function to string "Home" (develop) fix: add translation function to string "Home" (develop) --- frappe/templates/includes/navbar/navbar.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/templates/includes/navbar/navbar.html b/frappe/templates/includes/navbar/navbar.html index 5e3327e59f..3037c08c63 100644 --- a/frappe/templates/includes/navbar/navbar.html +++ b/frappe/templates/includes/navbar/navbar.html @@ -1,7 +1,7 @@ \ No newline at end of file + From ee459d23f0639237b9cae181f2589b5a2fcdf9c4 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 8 Jan 2020 23:12:56 +0530 Subject: [PATCH 40/70] fix: pop ignore_permissions flag via whitelisted frappe.rename_doc --- frappe/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index e3620a5f4e..90e87bb19e 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -801,14 +801,15 @@ def reload_doc(module, dt=None, dn=None, force=False, reset_permissions=False): return frappe.modules.reload_doc(module, dt, dn, force=force, reset_permissions=reset_permissions) @whitelist() -def rename_doc(doctype, old, new, force=False, merge=False, ignore_if_exists=False): +def rename_doc(*args, **kwargs): """ Renames a doc(dt, old) to doc(dt, new) and updates all linked fields of type "Link" Calls `frappe.model.rename_doc.rename_doc` """ + kwargs.pop('ignore_permissions', None) from frappe.model.rename_doc import rename_doc - return rename_doc(doctype, old, new, force, merge, ignore_if_exists) + return rename_doc(*args, **kwargs) def get_module(modulename): """Returns a module object for given Python module name using `importlib.import_module`.""" From fe86aed1bc68372ecb3655d2d99fffd64ebfdc28 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 9 Jan 2020 15:13:22 +0530 Subject: [PATCH 41/70] fix: Guess date format correctly from column --- .../core/doctype/data_import/importer_new.py | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/frappe/core/doctype/data_import/importer_new.py b/frappe/core/doctype/data_import/importer_new.py index b3392bf4ad..22c4778147 100644 --- a/frappe/core/doctype/data_import/importer_new.py +++ b/frappe/core/doctype/data_import/importer_new.py @@ -351,27 +351,25 @@ class Importer: return value def parse_date_format(self, value, df): - date_format = self.guess_date_format_for_column(df.fieldname) + date_format = self.guess_date_format_for_column(df) if date_format: return datetime.strptime(value, date_format) return value - def guess_date_format_for_column(self, fieldname): + def guess_date_format_for_column(self, df): """ Guesses date format for a column by parsing the first 10 values in the column, getting the date format and then returning the one which has the maximum frequency """ PARSE_ROW_COUNT = 10 - if not self._guessed_date_formats.get(fieldname): - column_index = -1 + if not self._guessed_date_formats.get(df.fieldname): + matches = [col for col in self.columns if col.df == df] + if not matches: + self._guessed_date_formats[df.fieldname] = None + return - for i, field in enumerate(self.header_row): - if self.meta.has_field(field) and field == fieldname: - column_index = i - break - - if column_index == -1: - self._guessed_date_formats[fieldname] = None + column = matches[0] + column_index = column.index - 1 date_values = [ row[column_index] for row in self.data[:PARSE_ROW_COUNT] if row[column_index] @@ -380,9 +378,9 @@ class Importer: if not date_formats: return max_occurred_date_format = max(set(date_formats), key=date_formats.count) - self._guessed_date_formats[fieldname] = max_occurred_date_format + self._guessed_date_formats[df.fieldname] = max_occurred_date_format - return self._guessed_date_formats[fieldname] + return self._guessed_date_formats[df.fieldname] def import_data(self): # set user lang for translations From ec53a57f4bdfccf40b3c4f17f6634446876f3802 Mon Sep 17 00:00:00 2001 From: Ben Knowles Date: Thu, 9 Jan 2020 13:04:03 -0600 Subject: [PATCH 42/70] fix: skip single doctype when rebuilding search index (#9187) --- frappe/utils/global_search.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frappe/utils/global_search.py b/frappe/utils/global_search.py index 7012b737c0..e0fe10ec2f 100644 --- a/frappe/utils/global_search.py +++ b/frappe/utils/global_search.py @@ -81,6 +81,10 @@ def rebuild_for_doctype(doctype): return filters meta = frappe.get_meta(doctype) + + if cint(meta.issingle) == 1: + return + if cint(meta.istable) == 1: parent_doctypes = frappe.get_all("DocField", fields="parent", filters={ "fieldtype": ["in", frappe.model.table_fields], From 518288fd2b41527586638198fdb7342fd062b4b5 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 10 Jan 2020 11:53:35 +0530 Subject: [PATCH 43/70] fix(grid): show delete all button only when all rows selected --- frappe/public/js/frappe/form/grid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 6da742638a..e38773e521 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -219,7 +219,7 @@ export default class Grid { this.remove_rows_button.toggleClass('hidden', this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true); this.remove_all_rows_button.toggleClass('hidden', - this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true); + this.wrapper.find('.grid-heading-row .grid-row-check:checked:first').length ? false : true); } get_selected() { From 3a8aed5e817408dcac920a6fc95168806498d869 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 10 Jan 2020 11:53:57 +0530 Subject: [PATCH 44/70] fix: fix sorting index --- frappe/public/js/frappe/form/grid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index e38773e521..6c66362426 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -426,7 +426,7 @@ export default class Grid { } }, onUpdate: (event) => { - let idx = $(event.item).closest('.grid-row').attr('data-idx'); + let idx = $(event.item).closest('.grid-row').attr('data-idx') - 1; let doc = this.data[idx%this.grid_pagination.page_length]; this.renumber_based_on_dom(); this.frm.script_manager.trigger(this.df.fieldname + "_move", this.df.options, doc.name); From 2f892f5ec0405c703949e0d2b337de1fed80ec12 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 10 Jan 2020 12:06:12 +0530 Subject: [PATCH 45/70] fix: change leaderboard link --- frappe/config/desktop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/config/desktop.py b/frappe/config/desktop.py index 4e8ffb00d5..538aa30390 100644 --- a/frappe/config/desktop.py +++ b/frappe/config/desktop.py @@ -105,7 +105,7 @@ def get_data(): "label": _('Leaderboard'), "icon": "fa fa-trophy", "type": 'link', - "link": '#social/users', + "link": '#leaderboard/User', "color": '#FF4136', 'standard': 1, }, From e2986d343a707ffb63d3b60aef52484d742300af Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 10 Jan 2020 12:42:46 +0530 Subject: [PATCH 46/70] fix: Return empty object if there are no settings for a report Query reports does not have settings. --- frappe/public/js/frappe/views/reports/query_report.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 1316e3c403..c411f40cbc 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -186,7 +186,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { let report_script_name = this.report_doc.report_type === 'Custom Report' ? this.report_doc.reference_report : this.report_name; - return frappe.query_reports[report_script_name]; + return frappe.query_reports[report_script_name] || {}; } setup_progress_bar() { From e7e155ee58ed2fa3b709c2a2a5248045dfd4e8a6 Mon Sep 17 00:00:00 2001 From: vishal Date: Fri, 10 Jan 2020 12:29:14 +0530 Subject: [PATCH 47/70] fix:ldap check prevent for login screen --- frappe/templates/includes/login/login.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index 63ec6588e5..0d5ea45593 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -66,7 +66,7 @@ login.bind_events = function() { } }); - {% if ldap_settings.enabled %} + {% if ldap_settings and ldap_settings.enabled %} $(".btn-ldap-login").on("click", function(){ var args = {}; args.cmd = "{{ ldap_settings.method }}"; From fbee4bc02a27fe06f9bcae230da922f97c09475d Mon Sep 17 00:00:00 2001 From: ci2014 Date: Fri, 10 Jan 2020 19:50:18 +0100 Subject: [PATCH 48/70] fix(email): send notification even if there are no attachments (#9228) Call communication.notify even if there is no _attachments attribute, otherwise the notification will not be sent with the current hotfix (24f1540) Duplicate for develop branch of https://github.com/frappe/frappe/pull/9227 --- frappe/email/doctype/email_account/email_account.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 2bbdf9db20..a108225a48 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -293,8 +293,12 @@ class EmailAccount(Document): else: frappe.db.commit() - if communication and hasattr(communication, "_attachments"): - attachments = [d.file_name for d in communication._attachments] + if communication: + attachments = [] + + if hasattr(communication, '_attachments'): + attachments = [d.file_name for d in communication._attachments] + communication.notify(attachments=attachments, fetched_from_email_account=True) #notify if user is linked to account From aceb2d9a8053c4ad377b789e4c11a738301936d7 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Sat, 11 Jan 2020 05:27:11 +0100 Subject: [PATCH 49/70] simplify statement (#9229) --- frappe/core/doctype/file/file.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 0e2619c012..6633884bb3 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -517,11 +517,7 @@ class File(Document): delete_file(self.thumbnail_url) def is_downloadable(self): - if self.is_private: - if has_permission(self, 'read'): - return True - - return False + return self.is_private and has_permission(self, 'read') def get_extension(self): '''returns split filename and extension''' From e7c82510e49c269b2a5deecb78c0fead9f72cf62 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Sat, 11 Jan 2020 15:43:34 +0530 Subject: [PATCH 50/70] fix: render more in web form list Co-Authored-By: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- frappe/public/js/frappe/web_form/web_form_list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/web_form/web_form_list.js b/frappe/public/js/frappe/web_form/web_form_list.js index 79ee6ed1a5..f0b2e03af1 100644 --- a/frappe/public/js/frappe/web_form/web_form_list.js +++ b/frappe/public/js/frappe/web_form/web_form_list.js @@ -201,7 +201,7 @@ export default class WebFormList { () => (window.location.href = window.location.pathname + "?new=1") ); - if (this.rows.length >= this.page_length) { + if (this.rows.length > this.page_length) { addButton(footer, "more", "secondary", false, "More", () => this.more()); } From 2b77c61f6b8eb0fae8f281ec17f30a5295ea9ca4 Mon Sep 17 00:00:00 2001 From: prssanna Date: Sun, 12 Jan 2020 13:43:53 +0530 Subject: [PATCH 51/70] style: fix leaderboard searchbox padding --- frappe/desk/page/leaderboard/leaderboard.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frappe/desk/page/leaderboard/leaderboard.css b/frappe/desk/page/leaderboard/leaderboard.css index a3cb4d09c4..1c6c8b7726 100644 --- a/frappe/desk/page/leaderboard/leaderboard.css +++ b/frappe/desk/page/leaderboard/leaderboard.css @@ -55,3 +55,7 @@ max-width: 75px; } +.leaderboard-search { + padding: 5px; +} + From a6f516c9735140bc1effef7673c9ed02216bf875 Mon Sep 17 00:00:00 2001 From: prssanna Date: Sun, 12 Jan 2020 13:44:25 +0530 Subject: [PATCH 52/70] fix: fix leaderboard search --- frappe/desk/page/leaderboard/leaderboard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/desk/page/leaderboard/leaderboard.js b/frappe/desk/page/leaderboard/leaderboard.js index c64d2dcb4f..c4c8f5790d 100644 --- a/frappe/desk/page/leaderboard/leaderboard.js +++ b/frappe/desk/page/leaderboard/leaderboard.js @@ -363,7 +363,7 @@ class Leaderboard { const link = `#Form/${this.options.selected_doctype}/${item.name}`; const name_html = item.formatted_name ? - `${item.formatted_name}` + `${item.formatted_name}` : ` ${item.name} `; const html = `
From dd2fba419b225c403064131f8eabc032f4079b51 Mon Sep 17 00:00:00 2001 From: prssanna Date: Sun, 12 Jan 2020 14:17:10 +0530 Subject: [PATCH 53/70] fix: use form-group class for leaderboard search --- frappe/desk/page/leaderboard/leaderboard.css | 4 ---- frappe/desk/page/leaderboard/leaderboard.js | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/frappe/desk/page/leaderboard/leaderboard.css b/frappe/desk/page/leaderboard/leaderboard.css index 1c6c8b7726..a3cb4d09c4 100644 --- a/frappe/desk/page/leaderboard/leaderboard.css +++ b/frappe/desk/page/leaderboard/leaderboard.css @@ -55,7 +55,3 @@ max-width: 75px; } -.leaderboard-search { - padding: 5px; -} - diff --git a/frappe/desk/page/leaderboard/leaderboard.js b/frappe/desk/page/leaderboard/leaderboard.js index c4c8f5790d..5359e9eab8 100644 --- a/frappe/desk/page/leaderboard/leaderboard.js +++ b/frappe/desk/page/leaderboard/leaderboard.js @@ -187,7 +187,7 @@ class Leaderboard { render_search_box() { this.$search_box = - $(`