From 67e1198d064ec6e0b838f2d242cb119e2b906c8e Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 12 Jun 2020 16:24:41 +0530 Subject: [PATCH 01/17] feat: prompt in case of site downgrades on restore --- frappe/commands/site.py | 12 +++++++++--- frappe/installer.py | 27 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 28e61282eb..c327d85af0 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -108,12 +108,14 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N @click.option('--install-app', multiple=True, help='Install app after installation') @click.option('--with-public-files', help='Restores the public files of the site, given path to its tar file') @click.option('--with-private-files', help='Restores the private files of the site, given path to its tar file') +@click.option('--force', is_flag=True, default=False, help='Use a bit of force to get the job done') @pass_context def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_password=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, with_private_files=None): "Restore site database from an sql file" - from frappe.installer import extract_sql_gzip, extract_tar_files - # Extract the gzip file if user has passed *.sql.gz file instead of *.sql file + from frappe.installer import extract_sql_gzip, extract_tar_files, is_downgrade + force = context.force or force + # Extract the gzip file if user has passed *.sql.gz file instead of *.sql file if not os.path.exists(sql_file_path): base_path = '..' sql_file_path = os.path.join(base_path, sql_file_path) @@ -125,7 +127,6 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas else: base_path = '.' - if sql_file_path.endswith('sql.gz'): decompressed_file_name = extract_sql_gzip(os.path.abspath(sql_file_path)) else: @@ -133,6 +134,11 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas site = get_site(context) frappe.init(site=site) + + # dont allow downgrading to older versions of frappe without force + if not force and is_downgrade(decompressed_file_name): + click.confirm("Downgrading sites may lead to a broken site. Do you wish to continue?", abort=True) + _new_site(frappe.conf.db_name, site, mariadb_root_username=mariadb_root_username, mariadb_root_password=mariadb_root_password, admin_password=admin_password, verbose=context.verbose, install_apps=install_app, source_sql=decompressed_file_name, diff --git a/frappe/installer.py b/frappe/installer.py index 4fc19b282a..38a8aadc3b 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -326,3 +326,30 @@ def extract_tar_files(site_name, file_path, folder_name): frappe.destroy() return tar_path + +def is_downgrade(sql_file_path): + """checks if input db backup will get downgraded on current bench""" + from semantic_version import Version + from frappe.utils.change_log import get_app_branch + head = "INSERT INTO `tabInstalled Application` VALUES" + + with open(sql_file_path) as f: + for line in f: + if head in line: + # ('2056588823','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',1,'frappe','v10.1.71-74 (3c50d5e) (v10.x.x)','v10.x.x'),('855c640b8e','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',2,'press','0.0.1','master') + line = line.strip().lstrip(head).rstrip(";").strip() + # [('frappe', '12.x.x-develop ()', 'develop'), ('press', '0.0.1', 'master')] + all_apps = [ x[-3:] for x in frappe.safe_eval(line) ] + + for app in all_apps: + app_name = app[0] + app_version = app[1].split(" ")[0] + + if app_name == "frappe": + try: + current_version = Version(frappe.__version__) + backup_version = Version(app_version[1:] if app_version[0] is "v" else app_version) + except ValueError: + return False + + return backup_version > current_version From 13ca526be8bc35a27a7a2d4ac1971ecf0d2391af Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 12 Jun 2020 16:36:50 +0530 Subject: [PATCH 02/17] chore: Sider --- frappe/installer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/installer.py b/frappe/installer.py index 38a8aadc3b..962a9e40e5 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -330,7 +330,6 @@ def extract_tar_files(site_name, file_path, folder_name): def is_downgrade(sql_file_path): """checks if input db backup will get downgraded on current bench""" from semantic_version import Version - from frappe.utils.change_log import get_app_branch head = "INSERT INTO `tabInstalled Application` VALUES" with open(sql_file_path) as f: @@ -348,7 +347,7 @@ def is_downgrade(sql_file_path): if app_name == "frappe": try: current_version = Version(frappe.__version__) - backup_version = Version(app_version[1:] if app_version[0] is "v" else app_version) + backup_version = Version(app_version[1:] if app_version[0] == "v" else app_version) except ValueError: return False From 04d0ce54af6873aed342c193be9080c9f486c11e Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 25 Jun 2020 23:12:25 +0530 Subject: [PATCH 03/17] fix: include start date in chart result --- frappe/desk/doctype/dashboard_chart/dashboard_chart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 703db16a48..c06020f175 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -259,7 +259,7 @@ def get_aggregate_function(chart_type): def get_result(data, timegrain, from_date, to_date): start_date = getdate(from_date) end_date = getdate(to_date) - result = [] + result = [[start_date, 0.0]] while start_date < end_date: next_date = get_next_expected_date(start_date, timegrain) From 1379a722f5bc0a5e8c1cdd1aa9a3a89b86575d47 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 26 Jun 2020 00:03:35 +0530 Subject: [PATCH 04/17] fix: append start date only if timegrain is daily --- frappe/desk/doctype/dashboard_chart/dashboard_chart.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index c06020f175..a5c5504db2 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -259,7 +259,10 @@ def get_aggregate_function(chart_type): def get_result(data, timegrain, from_date, to_date): start_date = getdate(from_date) end_date = getdate(to_date) - result = [[start_date, 0.0]] + + result = [] + if timegrain == 'Daily': + result.append([start_date, 0.0]) while start_date < end_date: next_date = get_next_expected_date(start_date, timegrain) From b482b2ce5c0821683d72d08f6bd1a83105cb85b8 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 26 Jun 2020 17:47:59 +0530 Subject: [PATCH 05/17] fix: test for daily charts --- .../dashboard_chart/test_dashboard_chart.py | 90 ++++++++++++------- 1 file changed, 60 insertions(+), 30 deletions(-) diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py index dfc6edbf58..1a300e471a 100644 --- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py @@ -35,9 +35,6 @@ class TestDashboardChart(unittest.TestCase): self.assertEqual(get_period_ending('2019-10-01', 'Quarterly'), getdate('2019-12-31')) - self.assertEqual(get_period_ending('2019-10-01', 'Yearly'), - getdate('2019-12-31')) - def test_dashboard_chart(self): if frappe.db.exists('Dashboard Chart', 'Test Dashboard Chart'): frappe.delete_doc('Dashboard Chart', 'Test Dashboard Chart') @@ -50,7 +47,7 @@ class TestDashboardChart(unittest.TestCase): based_on = 'creation', timespan = 'Last Year', time_interval = 'Monthly', - filters_json = '[]', + filters_json = '{}', timeseries = 1 )).insert() @@ -82,7 +79,7 @@ class TestDashboardChart(unittest.TestCase): based_on = 'creation', timespan = 'Last Year', time_interval = 'Monthly', - filters_json = '[]', + filters_json = '{}', timeseries = 1 )).insert() @@ -114,7 +111,7 @@ class TestDashboardChart(unittest.TestCase): based_on = 'creation', timespan = 'Last Year', time_interval = 'Monthly', - filters_json = '[]', + filters_json = '{}', timeseries = 1 )).insert() @@ -132,6 +129,60 @@ class TestDashboardChart(unittest.TestCase): frappe.db.rollback() + def test_group_by_chart_type(self): + if frappe.db.exists('Dashboard Chart', 'Test Group By Dashboard Chart'): + frappe.delete_doc('Dashboard Chart', 'Test Group By Dashboard Chart') + + frappe.get_doc({"doctype":"ToDo", "description": "test"}).insert() + + frappe.get_doc(dict( + doctype = 'Dashboard Chart', + chart_name = 'Test Group By Dashboard Chart', + chart_type = 'Group By', + document_type = 'ToDo', + group_by_based_on = 'status', + filters_json = '{}', + )).insert() + + result = get(chart_name ='Test Group By Dashboard Chart', refresh = 1) + todo_status_count = frappe.db.count('ToDo', {'status': result.get('labels')[0]}) + + self.assertEqual(result.get('datasets')[0].get('values')[0], todo_status_count) + + frappe.db.rollback() + + def test_daily_dashboard_chart(self): + insert_test_records() + + if frappe.db.exists('Dashboard Chart', 'Test Daily Dashboard Chart'): + frappe.delete_doc('Dashboard Chart', 'Test Daily Dashboard Chart') + + frappe.get_doc(dict( + doctype = 'Dashboard Chart', + chart_name = 'Test Daily Dashboard Chart', + chart_type = 'Sum', + document_type = 'Communication', + based_on = 'communication_date', + value_based_on = 'rating', + timespan = 'Select Date Range', + time_interval = 'Daily', + from_date = datetime(2019, 1, 6), + to_date = datetime(2019, 1, 11), + filters_json = '{}', + timeseries = 1 + )).insert() + + result = get(chart_name ='Test Daily Dashboard Chart', refresh = 1) + + self.assertEqual(result.get('datasets')[0].get('values'), [200.0, 400.0, 300.0, 0.0, 100.0, 0.0]) + self.assertEqual( + result.get('labels'), + [formatdate('2019-01-06'), formatdate('2019-01-07'), formatdate('2019-01-08'),\ + formatdate('2019-01-09'), formatdate('2019-01-10'), formatdate('2019-01-11')] + ) + + frappe.db.rollback() + def test_weekly_dashboard_chart(self): insert_test_records() @@ -149,42 +200,21 @@ class TestDashboardChart(unittest.TestCase): time_interval = 'Weekly', from_date = datetime(2018, 12, 30), to_date = datetime(2019, 1, 15), - filters_json = '[]', + filters_json = '{}', timeseries = 1 )).insert() result = get(chart_name ='Test Weekly Dashboard Chart', refresh = 1) - self.assertEqual(result.get('datasets')[0].get('values'), [200.0, 400.0, 0.0]) + self.assertEqual(result.get('datasets')[0].get('values'), [200.0, 800.0, 0.0]) self.assertEqual(result.get('labels'), [formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')]) frappe.db.rollback() - def test_group_by_chart_type(self): - if frappe.db.exists('Dashboard Chart', 'Test Group By Dashboard Chart'): - frappe.delete_doc('Dashboard Chart', 'Test Group By Dashboard Chart') - - frappe.get_doc({"doctype":"ToDo", "description": "test"}).insert() - - frappe.get_doc(dict( - doctype = 'Dashboard Chart', - chart_name = 'Test Group By Dashboard Chart', - chart_type = 'Group By', - document_type = 'ToDo', - group_by_based_on = 'status', - filters_json = '[]', - )).insert() - - result = get(chart_name ='Test Group By Dashboard Chart', refresh = 1) - todo_status_count = frappe.db.count('ToDo', {'status': result.get('labels')[0]}) - - self.assertEqual(result.get('datasets')[0].get('values')[0], todo_status_count) - - frappe.db.rollback() - def insert_test_records(): create_new_communication(datetime(2019, 1, 10), 100) create_new_communication(datetime(2019, 1, 6), 200) + create_new_communication(datetime(2019, 1, 7), 400) create_new_communication(datetime(2019, 1, 8), 300) def create_new_communication(date, rating): From 346937aed738fdb1f23e896153ea16ddb8096f1a Mon Sep 17 00:00:00 2001 From: Afshan Date: Fri, 26 Jun 2020 19:28:48 +0530 Subject: [PATCH 06/17] fix: handle condition that "Setters" could be "Array" or "Object" --- .../js/frappe/form/multi_select_dialog.js | 62 ++++++++++++------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/frappe/public/js/frappe/form/multi_select_dialog.js b/frappe/public/js/frappe/form/multi_select_dialog.js index 41b87e0207..bb157efc38 100644 --- a/frappe/public/js/frappe/form/multi_select_dialog.js +++ b/frappe/public/js/frappe/form/multi_select_dialog.js @@ -101,19 +101,25 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { columns[1] = []; columns[2] = []; - Object.keys(this.setters).forEach((setter, index) => { - let df_prop = frappe.meta.docfield_map[this.doctype][setter]; + if($.isArray(this.setters)) { + for (const df of this.setters) { + columns[1].push(df, {fieldtype: "Column Break"}); + } + } else { + Object.keys(this.setters).forEach((setter, index) => { + let df_prop = frappe.meta.docfield_map[this.doctype][setter]; - // Index + 1 to start filling from index 1 - // Since Search is a standrd field already pushed - columns[(index + 1) % 3].push({ - fieldtype: df_prop.fieldtype, - label: df_prop.label, - fieldname: setter, - options: df_prop.options, - default: this.setters[setter] + // Index + 1 to start filling from index 1 + // Since Search is a standrd field already pushed + columns[(index + 1) % 3].push({ + fieldtype: df_prop.fieldtype, + label: df_prop.label, + fieldname: setter, + options: df_prop.options, + default: this.setters[setter] + }); }); - }); + } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal if (Object.seal) { @@ -217,7 +223,13 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { let contents = ``; let columns = ["name"]; - columns = columns.concat(Object.keys(this.setters)); + if($.isArray(this.setters)) { + for (let df of this.setters) { + columns.push(df.fieldname); + } + } else { + columns = columns.concat(Object.keys(this.setters)); + } columns.forEach(function (column) { contents += `
@@ -290,16 +302,24 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { let filters = this.get_query ? this.get_query().filters : {} || {}; let filter_fields = []; - Object.keys(this.setters).forEach(function (setter) { - var value = me.dialog.fields_dict[setter].get_value(); - if (me.dialog.fields_dict[setter].df.fieldtype == "Data" && value) { - filters[setter] = ["like", "%" + value + "%"]; - } else { - filters[setter] = value || undefined; - me.args[setter] = filters[setter]; - filter_fields.push(setter); + if($.isArray(this.setters)) { + for (let df of this.setters) { + filters[df.fieldname] = me.dialog.fields_dict[df.fieldname].get_value() || undefined; + me.args[df.fieldname] = filters[df.fieldname]; + filter_fields.push(df.fieldname); } - }); + } else { + Object.keys(this.setters).forEach(function (setter) { + var value = me.dialog.fields_dict[setter].get_value(); + if (me.dialog.fields_dict[setter].df.fieldtype == "Data" && value) { + filters[setter] = ["like", "%" + value + "%"]; + } else { + filters[setter] = value || undefined; + me.args[setter] = filters[setter]; + filter_fields.push(setter); + } + }); + } let filter_group = this.get_custom_filters(); Object.assign(filters, filter_group); From 1bfa56cd0cac9af5e58d9f1dea8e59cd73e9b8f6 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Sat, 27 Jun 2020 16:01:30 +0530 Subject: [PATCH 07/17] feat: backup and restore postgres site --- frappe/commands/site.py | 2 +- frappe/database/postgres/setup_db.py | 8 +++-- frappe/utils/backups.py | 51 +++++++++++++++++++++++----- 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 55ac05bd71..a975d29a15 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -136,7 +136,7 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas _new_site(frappe.conf.db_name, site, mariadb_root_username=mariadb_root_username, mariadb_root_password=mariadb_root_password, admin_password=admin_password, verbose=context.verbose, install_apps=install_app, source_sql=decompressed_file_name, - force=True) + force=True, db_type=frappe.conf.db_type) # Extract public and/or private files to the restored site, if user has given the path if with_public_files: diff --git a/frappe/database/postgres/setup_db.py b/frappe/database/postgres/setup_db.py index 01a97178f9..1dc1ea4c97 100644 --- a/frappe/database/postgres/setup_db.py +++ b/frappe/database/postgres/setup_db.py @@ -1,7 +1,7 @@ import frappe, subprocess, os from six.moves import input -def setup_database(force, source_sql, verbose): +def setup_database(force, source_sql=None, verbose=False): root_conn = get_root_connection() root_conn.commit() root_conn.sql("DROP DATABASE IF EXISTS `{0}`".format(frappe.conf.db_name)) @@ -16,10 +16,12 @@ def setup_database(force, source_sql, verbose): subprocess_env = os.environ.copy() subprocess_env['PGPASSWORD'] = str(frappe.conf.db_password) # bootstrap db + if not source_sql: + source_sql = os.path.join(os.path.dirname(__file__), 'framework_postgres.sql') + subprocess.check_output([ 'psql', frappe.conf.db_name, '-h', frappe.conf.db_host or 'localhost', '-U', - frappe.conf.db_name, '-f', - os.path.join(os.path.dirname(__file__), 'framework_postgres.sql') + frappe.conf.db_name, '-f', source_sql ], env=subprocess_env) frappe.connect() diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index 7bb17d644b..dc73c2f84d 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -26,17 +26,24 @@ class BackupGenerator: If specifying db_file_name, also append ".sql.gz" """ def __init__(self, db_name, user, password, backup_path_db=None, backup_path_files=None, - backup_path_private_files=None, db_host="localhost", db_port=3306, verbose=False): + backup_path_private_files=None, db_host="localhost", db_port=None, verbose=False, + db_type='mariadb'): global _verbose self.db_host = db_host - self.db_port = db_port or 3306 + self.db_port = db_port self.db_name = db_name + self.db_type = db_type self.user = user self.password = password self.backup_path_files = backup_path_files self.backup_path_db = backup_path_db self.backup_path_private_files = backup_path_private_files + if not self.db_port and self.db_type == 'mariadb': + self.db_port = 3306 + elif not self.db_port and self.db_type == 'postgres': + self.db_port = 5432 + site = frappe.local.site or frappe.generate_hash(length=8) self.site_slug = site.replace('.', '_') @@ -141,6 +148,17 @@ class BackupGenerator: for item in self.__dict__.copy().items()) cmd_string = """mysqldump --single-transaction --quick --lock-tables=false -u %(user)s -p%(password)s %(db_name)s -h %(db_host)s -P %(db_port)s | gzip > %(backup_path_db)s """ % args + + if self.db_type == 'postgres': + cmd_string = "pg_dump postgres://{user}:{password}@{db_host}:{db_port}/{db_name} | gzip > {backup_path_db}".format( + user=args.get('user'), + password=args.get('password'), + db_host=args.get('db_host'), + db_port=args.get('db_port'), + db_name=args.get('db_name'), + backup_path_db=args.get('backup_path_db') + ) + err, out = frappe.utils.execute_in_shell(cmd_string) def send_email(self): @@ -181,7 +199,8 @@ def get_backup(): """ delete_temp_backups() odb = BackupGenerator(frappe.conf.db_name, frappe.conf.db_name,\ - frappe.conf.db_password, db_host = frappe.db.host) + frappe.conf.db_password, db_host = frappe.db.host,\ + db_type=frappe.conf.db_type, db_port=frappe.conf.db_port) odb.get_backup() recipient_list = odb.send_email() frappe.msgprint(_("Download link for your backup will be emailed on the following email address: {0}").format(', '.join(recipient_list))) @@ -201,6 +220,7 @@ def new_backup(older_than=6, ignore_files=False, backup_path_db=None, backup_pat backup_path_private_files=backup_path_private_files, db_host = frappe.db.host, db_port = frappe.db.port, + db_type = frappe.conf.db_type, verbose=verbose) odb.get_backup(older_than, ignore_files, force=force) return odb @@ -258,25 +278,38 @@ def backup(with_files=False, backup_path_db=None, backup_path_files=None, quiet= if __name__ == "__main__": """ - is_file_old db_name user password db_host - get_backup db_name user password db_host + is_file_old db_name user password db_host db_type db_port + get_backup db_name user password db_host db_type db_port """ import sys cmd = sys.argv[1] + + db_type = 'mariadb' + try: + db_type = sys.argv[6] + except IndexError as error: + pass + + db_port = 3306 + try: + db_port = int(sys.argv[7]) + except IndexError as error: + pass + if cmd == "is_file_old": - odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost") + odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost", db_type=db_type, db_port=db_port) is_file_old(odb.db_file_name) if cmd == "get_backup": - odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost") + odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost", db_type=db_type, db_port=db_port) odb.get_backup() if cmd == "take_dump": - odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost") + odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost", db_type=db_type, db_port=db_port) odb.take_dump() if cmd == "send_email": - odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost") + odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost", db_type=db_type, db_port=db_port) odb.send_email("abc.sql.gz") if cmd == "delete_temp_backups": From f146d1ffb251eb22d95daeb62f13711aef2471a1 Mon Sep 17 00:00:00 2001 From: Afshan Date: Sat, 27 Jun 2020 18:01:26 +0530 Subject: [PATCH 08/17] style: format according to sider --- frappe/public/js/frappe/form/multi_select_dialog.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/form/multi_select_dialog.js b/frappe/public/js/frappe/form/multi_select_dialog.js index bb157efc38..6920870859 100644 --- a/frappe/public/js/frappe/form/multi_select_dialog.js +++ b/frappe/public/js/frappe/form/multi_select_dialog.js @@ -101,7 +101,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { columns[1] = []; columns[2] = []; - if($.isArray(this.setters)) { + if ($.isArray(this.setters)) { for (const df of this.setters) { columns[1].push(df, {fieldtype: "Column Break"}); } @@ -223,7 +223,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { let contents = ``; let columns = ["name"]; - if($.isArray(this.setters)) { + if ($.isArray(this.setters)) { for (let df of this.setters) { columns.push(df.fieldname); } @@ -302,7 +302,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { let filters = this.get_query ? this.get_query().filters : {} || {}; let filter_fields = []; - if($.isArray(this.setters)) { + if ($.isArray(this.setters)) { for (let df of this.setters) { filters[df.fieldname] = me.dialog.fields_dict[df.fieldname].get_value() || undefined; me.args[df.fieldname] = filters[df.fieldname]; From e0ad88594459e22f19bafeeced29fb9248bd2f86 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Sat, 27 Jun 2020 19:20:38 +0530 Subject: [PATCH 09/17] fix: allow users to disable google drive sync --- frappe/integrations/doctype/google_drive/google_drive.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/google_drive/google_drive.py b/frappe/integrations/doctype/google_drive/google_drive.py index 60ee173bbf..869f0a4854 100644 --- a/frappe/integrations/doctype/google_drive/google_drive.py +++ b/frappe/integrations/doctype/google_drive/google_drive.py @@ -218,11 +218,13 @@ def upload_system_backup_to_google_drive(): return _("Google Drive Backup Successful.") def daily_backup(): - if frappe.db.get_single_value("Google Drive", "frequency") == "Daily": + drive_settings = frappe.db.get_singles_dict('Google Drive') + if drive_settings.enable and drive_settings.frequency == "Daily": upload_system_backup_to_google_drive() def weekly_backup(): - if frappe.db.get_single_value("Google Drive", "frequency") == "Weekly": + drive_settings = frappe.db.get_singles_dict('Google Drive') + if drive_settings.enable and drive_settings.frequency == "Weekly": upload_system_backup_to_google_drive() def get_absolute_path(filename): From 64c2fa374481f8a8c8a87678cf639712faa818b2 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Sat, 27 Jun 2020 19:54:41 +0530 Subject: [PATCH 10/17] fix: update google api client --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index db51826b1c..92a423e495 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,10 +15,10 @@ Faker==2.0.4 future==0.18.2 GitPython==2.1.15 gitdb2==2.0.6;python_version<'3.4' -google-api-python-client==1.7.11 +google-api-python-client==1.9.3 google-auth-httplib2==0.0.3 google-auth-oauthlib==0.4.1 -google-auth==1.17.1 +google-auth==1.18.0 googlemaps==3.1.1 gunicorn==19.10.0 html2text==2016.9.19 From ec881a49770a27e6358ef25e0650777177796428 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Sat, 27 Jun 2020 20:47:42 +0530 Subject: [PATCH 11/17] fix: remove unused variable --- frappe/utils/backups.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index dc73c2f84d..2343db8a96 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -287,13 +287,13 @@ if __name__ == "__main__": db_type = 'mariadb' try: db_type = sys.argv[6] - except IndexError as error: + except IndexError: pass db_port = 3306 try: db_port = int(sys.argv[7]) - except IndexError as error: + except IndexError: pass if cmd == "is_file_old": From 620fafeb964407412e34dc3efc69ffb88ba24f2e Mon Sep 17 00:00:00 2001 From: Afshan Date: Sat, 27 Jun 2020 22:19:27 +0530 Subject: [PATCH 12/17] fix: divide filter fields into 3 columns using index. --- frappe/public/js/frappe/form/multi_select_dialog.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/form/multi_select_dialog.js b/frappe/public/js/frappe/form/multi_select_dialog.js index 6920870859..a0bb927563 100644 --- a/frappe/public/js/frappe/form/multi_select_dialog.js +++ b/frappe/public/js/frappe/form/multi_select_dialog.js @@ -102,9 +102,9 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { columns[2] = []; if ($.isArray(this.setters)) { - for (const df of this.setters) { - columns[1].push(df, {fieldtype: "Column Break"}); - } + this.setters.forEach((setter, index) => { + columns[(index + 1) % 3].push(setter); + }); } else { Object.keys(this.setters).forEach((setter, index) => { let df_prop = frappe.meta.docfield_map[this.doctype][setter]; From fc69f77f738f1ae04b513c819dd4858865329d83 Mon Sep 17 00:00:00 2001 From: Anand Narayan Date: Fri, 26 Jun 2020 15:51:21 -0700 Subject: [PATCH 13/17] fix(linked_docs): infinite recursion due to loops --- frappe/desk/form/linked_with.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py index 72917d0341..a121e71dc8 100644 --- a/frappe/desk/form/linked_with.py +++ b/frappe/desk/form/linked_with.py @@ -13,7 +13,7 @@ from frappe.modules import load_doctype_module @frappe.whitelist() -def get_submitted_linked_docs(doctype, name, docs=None, linked=None): +def get_submitted_linked_docs(doctype, name, docs=None, linked=None, visited=None): """ Get all nested submitted linked doctype linkinfo @@ -34,10 +34,18 @@ def get_submitted_linked_docs(doctype, name, docs=None, linked=None): if not linked: linked = {} + if not visited: + visited = [] + + if name in visited: + return + linkinfo = get_linked_doctypes(doctype) linked_docs = get_linked_docs(doctype, name, linkinfo) link_count = 0 + visited.append(name) + for link_doctype, link_names in linked_docs.items(): if link_doctype not in linked: linked[link_doctype] = [] @@ -61,13 +69,14 @@ def get_submitted_linked_docs(doctype, name, docs=None, linked=None): if link.name in [doc.get("name") for doc in docs]: continue - links = get_submitted_linked_docs(link_doctype, link.name, docs, linked) - docs.append({ - "doctype": link_doctype, - "name": link.name, - "docstatus": link.docstatus, - "link_count": links.get("count") - }) + links = get_submitted_linked_docs(link_doctype, link.name, docs, linked, visited) + if links: + docs.append({ + "doctype": link_doctype, + "name": link.name, + "docstatus": link.docstatus, + "link_count": links.get("count") + }) # sort linked documents by ascending number of links docs.sort(key=lambda doc: doc.get("link_count")) From 500191f7fb1ab192be7d6c27601046d3eedf2b24 Mon Sep 17 00:00:00 2001 From: michellealva Date: Sun, 28 Jun 2020 11:09:13 +0530 Subject: [PATCH 14/17] fix: merge error in assigment rule --- frappe/automation/doctype/assignment_rule/assignment_rule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.py b/frappe/automation/doctype/assignment_rule/assignment_rule.py index bf45347c4f..78f05e7fe9 100644 --- a/frappe/automation/doctype/assignment_rule/assignment_rule.py +++ b/frappe/automation/doctype/assignment_rule/assignment_rule.py @@ -21,7 +21,7 @@ class AssignmentRule(Document): def on_update(self): # pylint: disable=no-self-use frappe.cache_manager.clear_doctype_map('Assignment Rule', self.name) - def after_rename(self): # pylint: disable=no-self-use + def after_rename(self, old, new, merge): # pylint: disable=no-self-use frappe.cache_manager.clear_doctype_map('Assignment Rule', self.name) def apply_unassign(self, doc, assignments): From 62a8774bb10f6d122ffad1775a3f2848b3903607 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 29 Jun 2020 08:13:23 +0530 Subject: [PATCH 15/17] feat(role permission): if desk access is removed from a role, attempt to remove it from users too (#10838) * feat(minor): if desk access is removed from a role, attempt to remove it from users too * fix(minor): exception for install; * fix(minor): exception for install --- frappe/core/doctype/role/role.py | 24 ++++++++++++++++++------ frappe/core/doctype/role/test_role.py | 25 +++++++++++++++++++++++++ frappe/model/document.py | 5 +++++ frappe/tests/test_document.py | 7 +++++++ 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py index 7ce2537da3..657340ec24 100644 --- a/frappe/core/doctype/role/role.py +++ b/frappe/core/doctype/role/role.py @@ -22,16 +22,28 @@ class Role(Document): frappe.db.sql("delete from `tabHas Role` where role = %s", self.name) frappe.clear_cache() + def on_update(self): + '''update system user desk access if this has changed in this update''' + if frappe.flags.in_install: return + if self.has_value_changed('desk_access'): + for user_name in get_users(self.name): + user = frappe.get_doc('User', user_name) + user_type = user.user_type + user.set_system_user() + if user_type != user.user_type: + user.save() + # Get email addresses of all users that have been assigned this role def get_emails_from_role(role): emails = [] - users = frappe.get_list("Has Role", filters={"role": role, "parenttype": "User"}, - fields=["parent"]) - - for user in users: - user_email, enabled = frappe.db.get_value("User", user.parent, ["email", "enabled"]) + for user in get_users(role): + user_email, enabled = frappe.db.get_value("User", user, ["email", "enabled"]) if enabled and user_email not in ["admin@example.com", "guest@example.com"]: emails.append(user_email) - return emails \ No newline at end of file + return emails + +def get_users(role): + return [d.parent for d in frappe.get_all("Has Role", filters={"role": role, "parenttype": "User"}, + fields=["parent"])] diff --git a/frappe/core/doctype/role/test_role.py b/frappe/core/doctype/role/test_role.py index 31efb5d4e8..6459a72c98 100644 --- a/frappe/core/doctype/role/test_role.py +++ b/frappe/core/doctype/role/test_role.py @@ -23,3 +23,28 @@ class TestUser(unittest.TestCase): frappe.get_doc("User", "test@example.com").add_roles("_Test Role 3") self.assertTrue("_Test Role 3" in frappe.get_roles("test@example.com")) + + def test_change_desk_access(self): + '''if we change desk acecss from role, remove from user''' + frappe.delete_doc_if_exists('User', 'test-user-for-desk-access@example.com') + frappe.delete_doc_if_exists('Role', 'desk-access-test') + user = frappe.get_doc(dict( + doctype='User', + email='test-user-for-desk-access@example.com', + first_name='test')).insert() + role = frappe.get_doc(dict( + doctype = 'Role', + role_name = 'desk-access-test', + desk_access = 0 + )).insert() + user.add_roles(role.name) + user.save() + self.assertTrue(user.user_type=='Website User') + role.desk_access = 1 + role.save() + user.reload() + self.assertTrue(user.user_type=='System User') + role.desk_access = 0 + role.save() + user.reload() + self.assertTrue(user.user_type=='Website User') diff --git a/frappe/model/document.py b/frappe/model/document.py index 24450f0cc6..30d3442954 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -396,6 +396,11 @@ class Document(BaseDocument): def get_doc_before_save(self): return getattr(self, '_doc_before_save', None) + def has_value_changed(self, fieldname): + '''Returns true if value is changed before and after saving''' + previous = self.get_doc_before_save() + return previous.get(fieldname)!=self.get(fieldname) if previous else True + def set_new_name(self, force=False, set_name=None, set_child_names=True): """Calls `frappe.naming.set_new_name` for parent and child docs.""" if self.flags.name_set and not force: diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index 470ab35fb6..c96076cfba 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -66,6 +66,13 @@ class TestDocument(unittest.TestCase): self.assertEqual(frappe.db.get_value(d.doctype, d.name, "subject"), "subject changed") + def test_value_changed(self): + d = self.test_insert() + d.subject = "subject changed again" + d.save() + self.assertTrue(d.has_value_changed('subject')) + self.assertFalse(d.has_value_changed('event_type')) + def test_mandatory(self): # TODO: recheck if it is OK to force delete frappe.delete_doc_if_exists("User", "test_mandatory@example.com", 1) From 1ad23c962cb2a5df11576137cb172cb15fef7ba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=BCrker=20Tunal=C4=B1?= Date: Mon, 29 Jun 2020 09:25:39 +0300 Subject: [PATCH 16/17] Turkish translations need this fix (#10555) num2words library has Turkish translation since version 0.5.6. And their last version 0.5.10 seems ok. We need this fix to better support Turkish users. Co-authored-by: Gavin D'souza --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b478e7abaa..2d38f12faf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,7 +29,7 @@ ldap3==2.7 markdown2==2.3.9 maxminddb-geolite2==2018.703 ndg-httpsclient==0.5.1 -num2words==0.5.5 +num2words==0.5.10 oauthlib==3.1.0 openpyxl==2.6.4 passlib==1.7.2 From 478b87e5be816dc02fa7cb89fa4e41d4265bde11 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Sun, 28 Jun 2020 06:27:18 +0530 Subject: [PATCH 17/17] fix: add verbosity in is_downgrade --- frappe/commands/site.py | 5 +++-- frappe/installer.py | 13 +++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index c327d85af0..812d63b2f6 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -136,8 +136,9 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas frappe.init(site=site) # dont allow downgrading to older versions of frappe without force - if not force and is_downgrade(decompressed_file_name): - click.confirm("Downgrading sites may lead to a broken site. Do you wish to continue?", abort=True) + if not force and is_downgrade(decompressed_file_name, verbose=True): + warn_message = "This is not recommended and may lead to unexpected behaviour. Do you want to continue anyway?" + click.confirm(warn_message, abort=True) _new_site(frappe.conf.db_name, site, mariadb_root_username=mariadb_root_username, mariadb_root_password=mariadb_root_password, admin_password=admin_password, diff --git a/frappe/installer.py b/frappe/installer.py index 962a9e40e5..fa6e25375e 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -327,7 +327,7 @@ def extract_tar_files(site_name, file_path, folder_name): return tar_path -def is_downgrade(sql_file_path): +def is_downgrade(sql_file_path, verbose=False): """checks if input db backup will get downgraded on current bench""" from semantic_version import Version head = "INSERT INTO `tabInstalled Application` VALUES" @@ -335,9 +335,9 @@ def is_downgrade(sql_file_path): with open(sql_file_path) as f: for line in f: if head in line: - # ('2056588823','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',1,'frappe','v10.1.71-74 (3c50d5e) (v10.x.x)','v10.x.x'),('855c640b8e','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',2,'press','0.0.1','master') + # 'line' (str) format: ('2056588823','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',1,'frappe','v10.1.71-74 (3c50d5e) (v10.x.x)','v10.x.x'),('855c640b8e','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',2,'your_custom_app','0.0.1','master') line = line.strip().lstrip(head).rstrip(";").strip() - # [('frappe', '12.x.x-develop ()', 'develop'), ('press', '0.0.1', 'master')] + # 'all_apps' (list) format: [('frappe', '12.x.x-develop ()', 'develop'), ('your_custom_app', '0.0.1', 'master')] all_apps = [ x[-3:] for x in frappe.safe_eval(line) ] for app in all_apps: @@ -351,4 +351,9 @@ def is_downgrade(sql_file_path): except ValueError: return False - return backup_version > current_version + downgrade = backup_version > current_version + + if verbose and downgrade: + print("Your site will be downgraded from Frappe {0} to {1}".format(current_version, backup_version)) + + return downgrade