From 9219db4c2a4dbb0709cc0f893622262cc4defc2e Mon Sep 17 00:00:00 2001 From: Saurabh Date: Fri, 16 Oct 2020 14:09:35 +0530 Subject: [PATCH 01/97] fix: validate email id before passing to formataddr --- frappe/utils/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index d3bf1dd10c..9640bcd394 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -66,9 +66,14 @@ def get_email_address(user=None): def get_formatted_email(user, mail=None): """get Email Address of user formatted as: `John Doe `""" fullname = get_fullname(user) + if not mail: - mail = get_email_address(user) - return cstr(make_header(decode_header(formataddr((fullname, mail))))) + mail = get_email_address(user) or validate_email_address(user) + + if not mail: + return '' + else: + return cstr(make_header(decode_header(formataddr((fullname, mail))))) def extract_email_id(email): """fetch only the email part of the Email Address""" From 59d35cb5aeadd161701c1b5651cf0aad21facc7e Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 20 Oct 2020 20:21:07 +0530 Subject: [PATCH 02/97] fix: Conditionally set parent field only on DocType rename --- frappe/model/rename_doc.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 7a2129e76e..e04d59ab6a 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -249,8 +249,11 @@ def update_link_field_values(link_fields, old, new, doctype): # or no longer exists pass else: + parent = field['parent'] + # because the table hasn't been renamed yet! - parent = field['parent'] if field['parent']!=new else old + if field['parent'] == new and doctype == "DocType": + parent = old frappe.db.sql(""" update `tab{table_name}` set `{fieldname}`=%s From 969aa86e68c0726d202a73bc1d488f9f89e72bc2 Mon Sep 17 00:00:00 2001 From: prssanna Date: Tue, 27 Oct 2020 17:46:21 +0530 Subject: [PATCH 03/97] fix: calculate chart data from beginning of period - show period as label --- .../dashboard_chart/dashboard_chart.py | 24 +++++++++++++++---- frappe/utils/data.py | 11 +++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 7e2d952928..c9b54561ea 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -8,8 +8,7 @@ from frappe import _ import datetime import json from frappe.utils.dashboard import cache_source, get_from_date_from_timespan -from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate,\ - get_datetime, cint, now_datetime +from frappe.utils import * from frappe.model.naming import append_number_if_name_exists from frappe.boot import get_allowed_reports from frappe.model.document import Document @@ -156,6 +155,7 @@ def add_chart_to_dashboard(args): def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): if not from_date: from_date = get_from_date_from_timespan(to_date, timespan) + from_date = get_period_beginning(from_date, timegrain) if not to_date: to_date = now_datetime() @@ -163,7 +163,6 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): datefield = chart.based_on aggregate_function = get_aggregate_function(chart.chart_type) value_field = chart.value_based_on or '1' - from_date = from_date.strftime('%Y-%m-%d') to_date = to_date filters.append([doctype, datefield, '>=', from_date, False]) @@ -185,7 +184,7 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): result = get_result(data, timegrain, from_date, to_date) chart_config = { - "labels": [formatdate(r[0].strftime('%Y-%m-%d')) for r in result], + "labels": [get_period(r[0], timegrain) for r in result], "datasets": [{ "name": chart.name, "values": [r[1] for r in result] @@ -282,7 +281,7 @@ 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 = [] while start_date < end_date: next_date = get_next_expected_date(start_date, timegrain) @@ -304,6 +303,21 @@ def get_next_expected_date(date, timegrain): next_date = get_period_ending(add_to_date(date, days=1), timegrain) return getdate(next_date) +def get_period_beginning(date, timegrain): + as_str = True + if timegrain == 'Daily': + pass + elif timegrain == 'Weekly': + date = get_first_day_of_week(date, as_str=as_str) + elif timegrain == 'Monthly': + date = get_first_day(date, as_str=as_str) + elif timegrain == 'Quarterly': + date = get_quarter_start(date, as_str=as_str) + elif timegrain == 'Yearly': + date = get_year_start(date, as_str=as_str) + + return date + def get_period_ending(date, timegrain): date = getdate(date) if timegrain == 'Daily': diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 41f247da45..f189b6aa53 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -441,6 +441,17 @@ def get_timespan_date_range(timespan): return date_range_map.get(timespan) +def get_period(date, interval='Monthly'): + date = getdate(date) + months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + return { + 'Daily': date.strftime('%d-%m-%y'), + 'Weekly': date.strftime('%d-%m-%y'), + 'Monthly': str(months[date.month - 1]) + ' ' + str(date.year), + 'Quarterly': 'Quarter ' + str(((date.month-1)//3)+1) + ' ' + str(date.year), + 'Yearly': str(date.year) + }[interval] + def global_date_format(date, format="long"): """returns localized date in the form of January 1, 2012""" date = getdate(date) From 7f43169c4a7f195c9b462cb076d3c7f6ef947884 Mon Sep 17 00:00:00 2001 From: prssanna Date: Wed, 28 Oct 2020 00:11:43 +0530 Subject: [PATCH 04/97] refactor: reorganise date functions indashboard_chart.py --- .../dashboard_chart/dashboard_chart.py | 75 ++----------------- frappe/utils/dashboard.py | 15 ---- frappe/utils/data.py | 32 +++++--- frappe/utils/dateutils.py | 62 ++++++++++++++- 4 files changed, 85 insertions(+), 99 deletions(-) diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index c9b54561ea..587f3c02b0 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -7,8 +7,11 @@ import frappe from frappe import _ import datetime import json -from frappe.utils.dashboard import cache_source, get_from_date_from_timespan -from frappe.utils import * +from frappe.utils.dashboard import cache_source +from frappe.utils import nowdate, add_to_date, getdate, formatdate,\ + get_datetime, cint, now_datetime +from frappe.utils.dateutils import\ + get_period, get_period_beginning, get_period_ending, get_from_date_from_timespan from frappe.model.naming import append_number_if_name_exists from frappe.boot import get_allowed_reports from frappe.model.document import Document @@ -303,74 +306,6 @@ def get_next_expected_date(date, timegrain): next_date = get_period_ending(add_to_date(date, days=1), timegrain) return getdate(next_date) -def get_period_beginning(date, timegrain): - as_str = True - if timegrain == 'Daily': - pass - elif timegrain == 'Weekly': - date = get_first_day_of_week(date, as_str=as_str) - elif timegrain == 'Monthly': - date = get_first_day(date, as_str=as_str) - elif timegrain == 'Quarterly': - date = get_quarter_start(date, as_str=as_str) - elif timegrain == 'Yearly': - date = get_year_start(date, as_str=as_str) - - return date - -def get_period_ending(date, timegrain): - date = getdate(date) - if timegrain == 'Daily': - pass - elif timegrain == 'Weekly': - date = get_week_ending(date) - elif timegrain == 'Monthly': - date = get_month_ending(date) - elif timegrain == 'Quarterly': - date = get_quarter_ending(date) - elif timegrain == 'Yearly': - date = get_year_ending(date) - - return getdate(date) - -def get_week_ending(date): - # week starts on monday - from datetime import timedelta - start = date - timedelta(days = date.weekday()) - end = start + timedelta(days=6) - - return end - -def get_month_ending(date): - month_of_the_year = int(date.strftime('%m')) - # first day of next month (note month starts from 1) - - date = add_to_date('{}-01-01'.format(date.year), months = month_of_the_year) - # last day of this month - return add_to_date(date, days=-1) - -def get_quarter_ending(date): - date = getdate(date) - - # find the earliest quarter ending date that is after - # the given date - for month in (3, 6, 9, 12): - quarter_end_month = getdate('{}-{}-01'.format(date.year, month)) - quarter_end_date = getdate(get_last_day(quarter_end_month)) - if date <= quarter_end_date: - date = quarter_end_date - break - - return date - -def get_year_ending(date): - ''' returns year ending of the given date ''' - - # first day of next year (note year starts from 1) - date = add_to_date('{}-01-01'.format(date.year), months = 12) - # last day of this month - return add_to_date(date, days=-1) - @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_charts_for_user(doctype, txt, searchfield, start, page_len, filters): diff --git a/frappe/utils/dashboard.py b/frappe/utils/dashboard.py index 7eaf470767..e386dcd881 100644 --- a/frappe/utils/dashboard.py +++ b/frappe/utils/dashboard.py @@ -61,21 +61,6 @@ def generate_and_cache_results(args, function, cache_key, chart): frappe.db.set_value("Dashboard Chart", args.chart_name, "last_synced_on", frappe.utils.now(), update_modified = False) return results -def get_from_date_from_timespan(to_date, timespan): - days = months = years = 0 - if timespan == "Last Week": - days = -7 - if timespan == "Last Month": - months = -1 - elif timespan == "Last Quarter": - months = -3 - elif timespan == "Last Year": - years = -1 - elif timespan == "All Time": - years = -50 - return add_to_date(to_date, years=years, months=months, days=days, - as_datetime=True) - def get_dashboards_with_link(docname, doctype): dashboards = [] links = [] diff --git a/frappe/utils/data.py b/frappe/utils/data.py index f189b6aa53..34659e1cac 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -221,6 +221,27 @@ def get_last_day(dt): """ return get_first_day(dt, 0, 1) + datetime.timedelta(-1) +def get_quarter_ending(date): + date = getdate(date) + + # find the earliest quarter ending date that is after + # the given date + for month in (3, 6, 9, 12): + quarter_end_month = getdate('{}-{}-01'.format(date.year, month)) + quarter_end_date = getdate(get_last_day(quarter_end_month)) + if date <= quarter_end_date: + date = quarter_end_date + break + + return date + +def get_year_ending(date): + ''' returns year ending of the given date ''' + + # first day of next year (note year starts from 1) + date = add_to_date('{}-01-01'.format(date.year), months = 12) + # last day of this month + return add_to_date(date, days=-1) def get_time(time_str): if isinstance(time_str, datetime.datetime): @@ -441,17 +462,6 @@ def get_timespan_date_range(timespan): return date_range_map.get(timespan) -def get_period(date, interval='Monthly'): - date = getdate(date) - months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] - return { - 'Daily': date.strftime('%d-%m-%y'), - 'Weekly': date.strftime('%d-%m-%y'), - 'Monthly': str(months[date.month - 1]) + ' ' + str(date.year), - 'Quarterly': 'Quarter ' + str(((date.month-1)//3)+1) + ' ' + str(date.year), - 'Yearly': str(date.year) - }[interval] - def global_date_format(date, format="long"): """returns localized date in the form of January 1, 2012""" date = getdate(date) diff --git a/frappe/utils/dateutils.py b/frappe/utils/dateutils.py index 90abdeb6cd..2895eb0568 100644 --- a/frappe/utils/dateutils.py +++ b/frappe/utils/dateutils.py @@ -7,8 +7,8 @@ import frappe.defaults import datetime from frappe.utils import get_datetime from frappe.utils import add_to_date, getdate -from frappe.utils.data import get_last_day_of_week -from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_period_ending +from frappe.utils.data import get_first_day, get_first_day_of_week, get_quarter_start, get_year_start,\ + get_last_day, get_last_day_of_week, get_quarter_ending, get_year_ending from six import string_types # global values -- used for caching @@ -102,4 +102,60 @@ def get_dates_from_timegrain(from_date, to_date, timegrain="Daily"): else: date = get_period_ending(add_to_date(dates[-1], years=years, months=months, days=days), timegrain) dates.append(date) - return dates \ No newline at end of file + return dates + +def get_from_date_from_timespan(to_date, timespan): + days = months = years = 0 + if timespan == "Last Week": + days = -7 + if timespan == "Last Month": + months = -1 + elif timespan == "Last Quarter": + months = -3 + elif timespan == "Last Year": + years = -1 + elif timespan == "All Time": + years = -50 + return add_to_date(to_date, years=years, months=months, days=days, + as_datetime=True) + +def get_period(date, interval='Monthly'): + date = getdate(date) + months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + return { + 'Daily': date.strftime('%d-%m-%y'), + 'Weekly': date.strftime('%d-%m-%y'), + 'Monthly': str(months[date.month - 1]) + ' ' + str(date.year), + 'Quarterly': 'Quarter ' + str(((date.month-1)//3)+1) + ' ' + str(date.year), + 'Yearly': str(date.year) + }[interval] + +def get_period_beginning(date, timegrain): + as_str = True + if timegrain == 'Daily': + pass + elif timegrain == 'Weekly': + date = get_first_day_of_week(date, as_str=as_str) + elif timegrain == 'Monthly': + date = get_first_day(date, as_str=as_str) + elif timegrain == 'Quarterly': + date = get_quarter_start(date, as_str=as_str) + elif timegrain == 'Yearly': + date = get_year_start(date, as_str=as_str) + + return date + +def get_period_ending(date, timegrain): + date = getdate(date) + if timegrain == 'Daily': + pass + elif timegrain == 'Weekly': + date = get_last_day_of_week(date) + elif timegrain == 'Monthly': + date = get_last_day(date) + elif timegrain == 'Quarterly': + date = get_quarter_ending(date) + elif timegrain == 'Yearly': + date = get_year_ending(date) + + return getdate(date) From 7302c85f55fe5cca5f222468235913057d03e03e Mon Sep 17 00:00:00 2001 From: prssanna Date: Wed, 28 Oct 2020 00:37:34 +0530 Subject: [PATCH 05/97] fix: dashboard chart tests --- .../dashboard_chart/test_dashboard_chart.py | 30 ++++--------- frappe/utils/dateutils.py | 45 ++++++++----------- 2 files changed, 27 insertions(+), 48 deletions(-) diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py index 5e39998e62..13fea8282d 100644 --- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py @@ -4,9 +4,9 @@ from __future__ import unicode_literals import unittest, frappe -from frappe.utils import getdate, formatdate, get_last_day -from frappe.desk.doctype.dashboard_chart.dashboard_chart import (get, - get_period_ending) +from frappe.utils import getdate, formatdate +from frappe.utils.dateutils import get_period_ending, get_period_beginning, get_period +from frappe.desk.doctype.dashboard_chart.dashboard_chart import get from datetime import datetime from dateutil.relativedelta import relativedelta @@ -53,15 +53,11 @@ class TestDashboardChart(unittest.TestCase): cur_date = datetime.now() - relativedelta(years=1) result = get(chart_name='Test Dashboard Chart', refresh=1) - self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d'))) - - if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')): - cur_date += relativedelta(months=1) + self.assertEqual(result.get('labels')[0], get_period(cur_date)) for idx in range(1, 13): - month = get_last_day(cur_date) month = formatdate(month.strftime('%Y-%m-%d')) - self.assertEqual(result.get('labels')[idx], month) + self.assertEqual(result.get('labels')[idx], get_period(month)) cur_date += relativedelta(months=1) frappe.db.rollback() @@ -87,15 +83,11 @@ class TestDashboardChart(unittest.TestCase): cur_date = datetime.now() - relativedelta(years=1) result = get(chart_name ='Test Empty Dashboard Chart', refresh=1) - self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d'))) - - if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')): - cur_date += relativedelta(months=1) + self.assertEqual(result.get('labels')[0], get_period(cur_date)) for idx in range(1, 13): - month = get_last_day(cur_date) month = formatdate(month.strftime('%Y-%m-%d')) - self.assertEqual(result.get('labels')[idx], month) + self.assertEqual(result.get('labels')[idx], get_period(month)) cur_date += relativedelta(months=1) frappe.db.rollback() @@ -124,15 +116,11 @@ class TestDashboardChart(unittest.TestCase): cur_date = datetime.now() - relativedelta(years=1) result = get(chart_name ='Test Empty Dashboard Chart 2', refresh = 1) - self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d'))) - - if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')): - cur_date += relativedelta(months=1) + self.assertEqual(result.get('labels')[0], get_period(cur_date)) for idx in range(1, 13): - month = get_last_day(cur_date) month = formatdate(month.strftime('%Y-%m-%d')) - self.assertEqual(result.get('labels')[idx], month) + self.assertEqual(result.get('labels')[idx], get_period(month)) cur_date += relativedelta(months=1) # only 1 data point with value diff --git a/frappe/utils/dateutils.py b/frappe/utils/dateutils.py index 2895eb0568..06b434a512 100644 --- a/frappe/utils/dateutils.py +++ b/frappe/utils/dateutils.py @@ -5,8 +5,7 @@ from __future__ import unicode_literals import frappe import frappe.defaults import datetime -from frappe.utils import get_datetime -from frappe.utils import add_to_date, getdate +from frappe.utils import get_datetime, add_to_date, getdate from frappe.utils.data import get_first_day, get_first_day_of_week, get_quarter_start, get_year_start,\ get_last_day, get_last_day_of_week, get_quarter_ending, get_year_ending from six import string_types @@ -130,32 +129,24 @@ def get_period(date, interval='Monthly'): 'Yearly': str(date.year) }[interval] -def get_period_beginning(date, timegrain): - as_str = True - if timegrain == 'Daily': - pass - elif timegrain == 'Weekly': - date = get_first_day_of_week(date, as_str=as_str) - elif timegrain == 'Monthly': - date = get_first_day(date, as_str=as_str) - elif timegrain == 'Quarterly': - date = get_quarter_start(date, as_str=as_str) - elif timegrain == 'Yearly': - date = get_year_start(date, as_str=as_str) - - return date +def get_period_beginning(date, timegrain, as_str=True): + return getdate({ + 'Daily': date, + 'Weekly': get_first_day_of_week(date), + 'Monthly': get_first_day(date), + 'Quarterly': get_quarter_start(date), + 'Yearly': get_year_start(date) + }[timegrain]) def get_period_ending(date, timegrain): date = getdate(date) if timegrain == 'Daily': - pass - elif timegrain == 'Weekly': - date = get_last_day_of_week(date) - elif timegrain == 'Monthly': - date = get_last_day(date) - elif timegrain == 'Quarterly': - date = get_quarter_ending(date) - elif timegrain == 'Yearly': - date = get_year_ending(date) - - return getdate(date) + return date + else: + return getdate({ + 'Daily': date, + 'Weekly': get_last_day_of_week(date), + 'Monthly': get_last_day(date), + 'Quarterly': get_quarter_ending(date), + 'Yearly': get_year_ending(date) + }[timegrain]) From 2dd9ae212755d24afd464d9167310adc90ac7f49 Mon Sep 17 00:00:00 2001 From: prssanna Date: Wed, 28 Oct 2020 11:10:58 +0530 Subject: [PATCH 06/97] fix: remove unused imports --- frappe/desk/doctype/dashboard_chart/dashboard_chart.py | 2 +- frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 587f3c02b0..c2e0f78624 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -8,7 +8,7 @@ from frappe import _ import datetime import json from frappe.utils.dashboard import cache_source -from frappe.utils import nowdate, add_to_date, getdate, formatdate,\ +from frappe.utils import nowdate, add_to_date, getdate,\ get_datetime, cint, now_datetime from frappe.utils.dateutils import\ get_period, get_period_beginning, get_period_ending, get_from_date_from_timespan diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py index 13fea8282d..d723171337 100644 --- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import unittest, frappe from frappe.utils import getdate, formatdate -from frappe.utils.dateutils import get_period_ending, get_period_beginning, get_period +from frappe.utils.dateutils import get_period_ending, get_period from frappe.desk.doctype.dashboard_chart.dashboard_chart import get from datetime import datetime From 99a260e32acda6e680b37c71c6d552f8e1e72278 Mon Sep 17 00:00:00 2001 From: prssanna Date: Wed, 28 Oct 2020 11:56:15 +0530 Subject: [PATCH 07/97] fix: chart tests --- .../dashboard_chart/dashboard_chart.py | 4 ++-- .../dashboard_chart/test_dashboard_chart.py | 19 ++++++++----------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index c2e0f78624..184fef7634 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -166,6 +166,7 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): datefield = chart.based_on aggregate_function = get_aggregate_function(chart.chart_type) value_field = chart.value_based_on or '1' + from_date = from_date.strftime('%Y-%m-%d') to_date = to_date filters.append([doctype, datefield, '>=', from_date, False]) @@ -283,8 +284,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]] if timegrain == 'Daily' else [] while start_date < end_date: next_date = get_next_expected_date(start_date, timegrain) diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py index d723171337..dcdebe6cd2 100644 --- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import unittest, frappe -from frappe.utils import getdate, formatdate +from frappe.utils import getdate, formatdate, get_last_day from frappe.utils.dateutils import get_period_ending, get_period from frappe.desk.doctype.dashboard_chart.dashboard_chart import get @@ -53,10 +53,9 @@ class TestDashboardChart(unittest.TestCase): cur_date = datetime.now() - relativedelta(years=1) result = get(chart_name='Test Dashboard Chart', refresh=1) - self.assertEqual(result.get('labels')[0], get_period(cur_date)) - for idx in range(1, 13): - month = formatdate(month.strftime('%Y-%m-%d')) + for idx in range(13): + month = get_last_day(cur_date) self.assertEqual(result.get('labels')[idx], get_period(month)) cur_date += relativedelta(months=1) @@ -83,10 +82,9 @@ class TestDashboardChart(unittest.TestCase): cur_date = datetime.now() - relativedelta(years=1) result = get(chart_name ='Test Empty Dashboard Chart', refresh=1) - self.assertEqual(result.get('labels')[0], get_period(cur_date)) - for idx in range(1, 13): - month = formatdate(month.strftime('%Y-%m-%d')) + for idx in range(13): + month = get_last_day(cur_date) self.assertEqual(result.get('labels')[idx], get_period(month)) cur_date += relativedelta(months=1) @@ -116,10 +114,9 @@ class TestDashboardChart(unittest.TestCase): cur_date = datetime.now() - relativedelta(years=1) result = get(chart_name ='Test Empty Dashboard Chart 2', refresh = 1) - self.assertEqual(result.get('labels')[0], get_period(cur_date)) - for idx in range(1, 13): - month = formatdate(month.strftime('%Y-%m-%d')) + for idx in range(13): + month = get_last_day(cur_date) self.assertEqual(result.get('labels')[idx], get_period(month)) cur_date += relativedelta(months=1) @@ -171,7 +168,7 @@ class TestDashboardChart(unittest.TestCase): timeseries = 1 )).insert() - result = get(chart_name ='Test Daily Dashboard Chart', refresh = 1) + 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( From 8fde766b8f3a0f4a8e87d7ea2a17d2d2a10a33f7 Mon Sep 17 00:00:00 2001 From: prssanna Date: Mon, 2 Nov 2020 18:47:14 +0530 Subject: [PATCH 08/97] fix: use get_dates_from_timegrain function --- .../dashboard_chart/dashboard_chart.py | 19 +++---------------- .../dashboard_chart/test_dashboard_chart.py | 3 +++ 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 184fef7634..c814f324f5 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -11,7 +11,7 @@ from frappe.utils.dashboard import cache_source from frappe.utils import nowdate, add_to_date, getdate,\ get_datetime, cint, now_datetime from frappe.utils.dateutils import\ - get_period, get_period_beginning, get_period_ending, get_from_date_from_timespan + get_period, get_period_beginning, get_period_ending, get_from_date_from_timespan, get_dates_from_timegrain from frappe.model.naming import append_number_if_name_exists from frappe.boot import get_allowed_reports from frappe.model.document import Document @@ -282,15 +282,8 @@ 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]] if timegrain == 'Daily' else [] - - while start_date < end_date: - next_date = get_next_expected_date(start_date, timegrain) - result.append([next_date, 0.0]) - start_date = next_date - + dates = get_dates_from_timegrain(from_date, to_date, timegrain) + result = [[date, 0] for date in dates] data_index = 0 if data: for i, d in enumerate(result): @@ -300,12 +293,6 @@ def get_result(data, timegrain, from_date, to_date): return result -def get_next_expected_date(date, timegrain): - next_date = None - # given date is always assumed to be the period ending date - next_date = get_period_ending(add_to_date(date, days=1), timegrain) - return getdate(next_date) - @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_charts_for_user(doctype, txt, searchfield, start, page_len, filters): diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py index dcdebe6cd2..b9503ee167 100644 --- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py @@ -56,6 +56,7 @@ class TestDashboardChart(unittest.TestCase): for idx in range(13): month = get_last_day(cur_date) + month = formatdate(month.strftime('%Y-%m-%d')) self.assertEqual(result.get('labels')[idx], get_period(month)) cur_date += relativedelta(months=1) @@ -85,6 +86,7 @@ class TestDashboardChart(unittest.TestCase): for idx in range(13): month = get_last_day(cur_date) + month = formatdate(month.strftime('%Y-%m-%d')) self.assertEqual(result.get('labels')[idx], get_period(month)) cur_date += relativedelta(months=1) @@ -117,6 +119,7 @@ class TestDashboardChart(unittest.TestCase): for idx in range(13): month = get_last_day(cur_date) + month = formatdate(month.strftime('%Y-%m-%d')) self.assertEqual(result.get('labels')[idx], get_period(month)) cur_date += relativedelta(months=1) From ba33aa1b78424cd45cc9b7bf26da599d5c895361 Mon Sep 17 00:00:00 2001 From: prssanna Date: Mon, 2 Nov 2020 19:06:42 +0530 Subject: [PATCH 09/97] fix: remove unused imports --- frappe/desk/doctype/dashboard_chart/dashboard_chart.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index c814f324f5..9cfc0a04c8 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -8,10 +8,10 @@ from frappe import _ import datetime import json from frappe.utils.dashboard import cache_source -from frappe.utils import nowdate, add_to_date, getdate,\ +from frappe.utils import nowdate, getdate, get_datetime,\ get_datetime, cint, now_datetime from frappe.utils.dateutils import\ - get_period, get_period_beginning, get_period_ending, get_from_date_from_timespan, get_dates_from_timegrain + get_period, get_period_beginning, get_from_date_from_timespan, get_dates_from_timegrain from frappe.model.naming import append_number_if_name_exists from frappe.boot import get_allowed_reports from frappe.model.document import Document From 875ba903b987bbdaffa4e838ad16f89174e3b1a4 Mon Sep 17 00:00:00 2001 From: prssanna Date: Mon, 2 Nov 2020 19:18:56 +0530 Subject: [PATCH 10/97] fix: chart test date format --- frappe/desk/doctype/dashboard_chart/dashboard_chart.py | 3 +-- .../desk/doctype/dashboard_chart/test_dashboard_chart.py | 8 +++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 9cfc0a04c8..3f8d7c3c79 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -8,8 +8,7 @@ from frappe import _ import datetime import json from frappe.utils.dashboard import cache_source -from frappe.utils import nowdate, getdate, get_datetime,\ - get_datetime, cint, now_datetime +from frappe.utils import nowdate, getdate, get_datetime, cint, now_datetime from frappe.utils.dateutils import\ get_period, get_period_beginning, get_from_date_from_timespan, get_dates_from_timegrain from frappe.model.naming import append_number_if_name_exists diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py index b9503ee167..3c37ad4a09 100644 --- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py @@ -176,8 +176,7 @@ class TestDashboardChart(unittest.TestCase): 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')] + ['06-01-19', '07-01-19', '08-01-19', '09-01-19', '10-01-19', '11-01-19'] ) frappe.db.rollback() @@ -206,7 +205,10 @@ class TestDashboardChart(unittest.TestCase): result = get(chart_name ='Test Weekly Dashboard Chart', refresh = 1) self.assertEqual(result.get('datasets')[0].get('values'), [50.0, 300.0, 800.0, 0.0]) - self.assertEqual(result.get('labels'), [formatdate('2018-12-30'), formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')]) + self.assertEqual( + result.get('labels'), + ['30-12-18', '06-01-19', '13-01-19', '20-01-19'] + ) frappe.db.rollback() From 841f2f4a36d984d9745146f656f2465abe183e1e Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 3 Nov 2020 21:51:37 +0530 Subject: [PATCH 11/97] chore: Rename Doctype Test and more explicit comment - Better decription of why the fix is done, what case it handles - Test for Renaming Doctype and Record having same name as DocType --- frappe/model/rename_doc.py | 10 ++++++++-- frappe/tests/test_document.py | 37 ++++++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index e04d59ab6a..789a7f51cf 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -251,8 +251,14 @@ def update_link_field_values(link_fields, old, new, doctype): else: parent = field['parent'] - # because the table hasn't been renamed yet! - if field['parent'] == new and doctype == "DocType": + # Handles the case where one of the link fields belongs to + # the DocType being renamed. + # Here this field could have the current DocType as its value too. + + # In this case while updating link field value, the field's parent + # or the current DocType table name hasn't been renamed yet, + # so consider it's old name. + if parent == new and doctype == "DocType": parent = old frappe.db.sql(""" diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index c96076cfba..4e9984e89a 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -288,4 +288,39 @@ class TestDocument(unittest.TestCase): 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 + frappe.delete_doc(doctype, docname) + + def test_rename_doctype(self): + from frappe.core.doctype.doctype.test_doctype import new_doctype + + fields =[{ + "label": "Linked To", + "fieldname": "linked_to_doctype", + "fieldtype": "Link", + "options": "DocType", + "unique": 0 + }] + if not frappe.db.exists("DocType", "Rename This"): + new_doctype("Rename This", unique=0, fields=fields).insert() + + to_rename_record = frappe.get_doc({ + "doctype": "Rename This", + "linked_to_doctype": "Rename This" + }) + to_rename_record.insert() + + # Rename doctype + self.assertEqual("Renamed Doc", frappe.rename_doc("DocType", "Rename This", "Renamed Doc", force=True)) + + # Test if Doctype value has changed in Link field + renamed_doctype_record = frappe.get_doc("Renamed Doc", to_rename_record.name) + self.assertEqual(renamed_doctype_record.linked_to_doctype, "Renamed Doc") + + # Test if there are conflicts between a record and a DocType + # having the same name + old_name = to_rename_record.name + new_name = "ToDo" + self.assertEqual(new_name, frappe.rename_doc("Renamed Doc", old_name, new_name, force=True)) + + frappe.delete_doc_if_exists("Renamed Doc", "ToDo") + frappe.delete_doc_if_exists("DocType", "Renamed Doc") \ No newline at end of file From 0e1807091087018338134b4aa1f10a3fd58e0589 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Wed, 11 Nov 2020 14:56:55 +0530 Subject: [PATCH 12/97] fix: error on trying to check semantic version --- frappe/utils/change_log.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/frappe/utils/change_log.py b/frappe/utils/change_log.py index 29fee2bac0..f7fac4cdf4 100644 --- a/frappe/utils/change_log.py +++ b/frappe/utils/change_log.py @@ -165,9 +165,10 @@ def check_for_update(): add_message_to_redis(updates) + def parse_latest_non_beta_release(response): """ - Pasrses the response JSON for all the releases and returns the latest non prerelease + Parses the response JSON for all the releases and returns the latest non prerelease Parameters response (list): response object returned by github @@ -182,32 +183,34 @@ def parse_latest_non_beta_release(response): return None + def check_release_on_github(app): - # Check if repo remote is on github from subprocess import CalledProcessError + try: + # Check if repo remote is on github remote_url = subprocess.check_output("cd ../apps/{} && git ls-remote --get-url".format(app), shell=True).decode() except CalledProcessError: # Passing this since some apps may not have git initializaed in them - return None + return if isinstance(remote_url, bytes): remote_url = remote_url.decode() if "github.com" not in remote_url: - return None + return # Get latest version from github if 'https' not in remote_url: - return None + return org_name = remote_url.split('/')[3] r = requests.get('https://api.github.com/repos/{}/{}/releases'.format(org_name, app)) if r.ok: - lastest_non_beta_release = parse_latest_non_beta_release(r.json()) - return Version(lastest_non_beta_release), org_name - # In case of an improper response or if there are no releases - return None + latest_non_beta_release = parse_latest_non_beta_release(r.json()) + if latest_non_beta_release: + return Version(latest_non_beta_release), org_name + def add_message_to_redis(update_json): # "update-message" will store the update message string From b52cfd1903a050f0d3493f120d75f00c2d715168 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Thu, 12 Nov 2020 15:24:12 +0530 Subject: [PATCH 13/97] fix: add git URL check --- frappe/utils/change_log.py | 16 ++++++++++------ frappe/utils/data.py | 6 ++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/frappe/utils/change_log.py b/frappe/utils/change_log.py index f7fac4cdf4..75421c43ea 100644 --- a/frappe/utils/change_log.py +++ b/frappe/utils/change_log.py @@ -1,15 +1,16 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt -from __future__ import unicode_literals -from six.moves import range -import json, os -from semantic_version import Version +import json +import os +import subprocess # nosec + import frappe import requests -import subprocess # nosec -from frappe.utils import cstr from frappe import _, safe_decode +from frappe.utils import cstr, is_git_url +from semantic_version import Version +from six.moves import range def get_change_log(user=None): @@ -204,6 +205,9 @@ def check_release_on_github(app): if 'https' not in remote_url: return + if is_git_url(remote_url): + return + org_name = remote_url.split('/')[3] r = requests.get('https://api.github.com/repos/{}/{}/releases'.format(org_name, app)) if r.ok: diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 41f247da45..1ddf59a82c 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -1376,3 +1376,9 @@ def validate_json_string(string): json.loads(string) except (TypeError, ValueError): raise frappe.ValidationError + + +def is_git_url(url): + # modified to allow without the tailing .git from https://github.com/jonschlinkert/is-git-url.git + pattern = r"(?:git|ssh|https?|git@[-\w.]+):(\/\/)?(.*?)(\.git)?(\/?|\#[-\d\w._]+?)$" + return bool(re.match(pattern, url)) From e8c6a7afc2ad7d70bdf5daf3bb3cae382f088410 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Sat, 14 Nov 2020 14:48:39 +0530 Subject: [PATCH 14/97] fix: Manage private images via get_local_image --- frappe/core/doctype/file/file.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index b8bed89a4d..2a4e1983c9 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -612,7 +612,12 @@ def get_extension(filename, extn, content): return extn def get_local_image(file_url): - file_path = frappe.get_site_path("public", file_url.lstrip("/")) + if file_url.startswith("/private"): + file_url_path = (file_url.lstrip("/"), ) + else: + file_url_path = ("public", file_url.lstrip("/")) + + file_path = frappe.get_site_path(*file_url_path) try: image = Image.open(file_path) From 2bc477331d2eab4a90a62982c53bff746cb50358 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Mon, 16 Nov 2020 13:59:56 +0530 Subject: [PATCH 15/97] refactor: cleanup --- .../public/js/frappe/form/controls/comment.js | 2 +- .../public/js/frappe/form/footer/timeline.js | 52 ++++++++++++------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/comment.js b/frappe/public/js/frappe/form/controls/comment.js index a64df56bca..d00c915065 100644 --- a/frappe/public/js/frappe/form/controls/comment.js +++ b/frappe/public/js/frappe/form/controls/comment.js @@ -60,7 +60,7 @@ frappe.ui.form.ControlComment = frappe.ui.form.ControlTextEditor.extend({ update_state() { const value = this.get_value(); - if (strip_html(value).trim() != "") { + if (strip_html(value).trim() != "" || value.includes('img')) { this.button.removeClass('btn-default').addClass('btn-primary'); } else { this.button.addClass('btn-default').removeClass('btn-primary'); diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index 84f34d4757..2e390dc3cb 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -30,7 +30,7 @@ frappe.ui.form.Timeline = class Timeline { render_input: true, only_input: true, on_submit: (val) => { - if(strip_html(val).trim() != "") { + if(strip_html(val).trim() != "" || val.includes('img')) { this.insert_comment(val, this.comment_area.button); } } @@ -547,10 +547,14 @@ frappe.ui.form.Timeline = class Timeline { log.color = 'dark'; log.sender = log.owner; log.comment_type = 'Milestone'; - log.content = __('{0} changed {1} to {2}', [ - frappe.user.full_name(log.owner).bold(), - frappe.meta.get_label(this.frm.doctype, log.track_field), - log.value.bold()]); + log.content = __( + '{0} changed {1} to {2}', + [ + frappe.user.full_name(log.owner).bold(), + frappe.meta.get_label(this.frm.doctype, log.track_field), + log.value.bold() + ] + ); return log; }); return milestones; @@ -613,11 +617,14 @@ frappe.ui.form.Timeline = class Timeline { const field_display_status = frappe.perm.get_field_display_status(df, null, me.frm.perm); if (field_display_status === 'Read' || field_display_status === 'Write') { - parts.push(__('{0} from {1} to {2}', [ - __(df.label), - me.format_content_for_timeline(p[1]), - me.format_content_for_timeline(p[2]) - ])); + parts.push(__( + '{0} from {1} to {2}', + [ + __(df.label), + me.format_content_for_timeline(p[1]), + me.format_content_for_timeline(p[2]) + ] + )); } } } @@ -648,13 +655,18 @@ frappe.ui.form.Timeline = class Timeline { null, me.frm.perm); if (field_display_status === 'Read' || field_display_status === 'Write') { - parts.push(__('{0} from {1} to {2} in row #{3}', [ - frappe.meta.get_label(me.frm.fields_dict[row[0]].grid.doctype, - p[0]), - me.format_content_for_timeline(p[1]), - me.format_content_for_timeline(p[2]), - row[1] - ])); + parts.push(__( + '{0} from {1} to {2} in row #{3}', + [ + frappe.meta.get_label( + me.frm.fields_dict[row[0]].grid.doctype, + p[0] + ), + me.format_content_for_timeline(p[1]), + me.format_content_for_timeline(p[2]), + row[1] + ] + )); } } return parts.length < 3; @@ -691,8 +703,10 @@ frappe.ui.form.Timeline = class Timeline { return p; }); if (parts.length) { - out.push(me.get_version_comment(version, __("{0} rows for {1}", - [__(key), parts.join(', ')]))); + out.push(me.get_version_comment(version, __( + "{0} rows for {1}", + [__(key), parts.join(', ')] + ))); } } }); From 4443dab3cbc4510574e306ba84d114e13ac8e6dc Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Mon, 16 Nov 2020 14:03:00 +0530 Subject: [PATCH 16/97] refactor: solve sider issues --- frappe/public/js/frappe/form/footer/timeline.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index 2e390dc3cb..c23b6d8127 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -30,7 +30,7 @@ frappe.ui.form.Timeline = class Timeline { render_input: true, only_input: true, on_submit: (val) => { - if(strip_html(val).trim() != "" || val.includes('img')) { + if (strip_html(val).trim() != "" || val.includes('img')) { this.insert_comment(val, this.comment_area.button); } } From 7f012b73b42616e8b622745831fa31e0212d678c Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Tue, 17 Nov 2020 19:18:44 +0530 Subject: [PATCH 17/97] feat: solve translation string issues --- .../public/js/frappe/form/footer/timeline.js | 36 +++---------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index c23b6d8127..159ab8a61b 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -547,14 +547,7 @@ frappe.ui.form.Timeline = class Timeline { log.color = 'dark'; log.sender = log.owner; log.comment_type = 'Milestone'; - log.content = __( - '{0} changed {1} to {2}', - [ - frappe.user.full_name(log.owner).bold(), - frappe.meta.get_label(this.frm.doctype, log.track_field), - log.value.bold() - ] - ); + log.content = __('{0} changed {1} to {2}', [ frappe.user.full_name(log.owner).bold(), frappe.meta.get_label(this.frm.doctype, log.track_field), log.value.bold()]); return log; }); return milestones; @@ -617,14 +610,7 @@ frappe.ui.form.Timeline = class Timeline { const field_display_status = frappe.perm.get_field_display_status(df, null, me.frm.perm); if (field_display_status === 'Read' || field_display_status === 'Write') { - parts.push(__( - '{0} from {1} to {2}', - [ - __(df.label), - me.format_content_for_timeline(p[1]), - me.format_content_for_timeline(p[2]) - ] - )); + parts.push(__('{0} from {1} to {2}', [ __(df.label), me.format_content_for_timeline(p[1]), me.format_content_for_timeline(p[2])])); } } } @@ -655,18 +641,7 @@ frappe.ui.form.Timeline = class Timeline { null, me.frm.perm); if (field_display_status === 'Read' || field_display_status === 'Write') { - parts.push(__( - '{0} from {1} to {2} in row #{3}', - [ - frappe.meta.get_label( - me.frm.fields_dict[row[0]].grid.doctype, - p[0] - ), - me.format_content_for_timeline(p[1]), - me.format_content_for_timeline(p[2]), - row[1] - ] - )); + parts.push(__('{0} from {1} to {2} in row #{3}', [ frappe.meta.get_label( me.frm.fields_dict[row[0]].grid.doctype, p[0]), me.format_content_for_timeline(p[1]), me.format_content_for_timeline(p[2]), row[1] ])); } } return parts.length < 3; @@ -703,10 +678,7 @@ frappe.ui.form.Timeline = class Timeline { return p; }); if (parts.length) { - out.push(me.get_version_comment(version, __( - "{0} rows for {1}", - [__(key), parts.join(', ')] - ))); + out.push(me.get_version_comment(version, __("{0} rows for {1}", [__(key), parts.join(', ')]))); } } }); From 51a1bf9a736e460f04efed4a0685571c7d19648b Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 18 Nov 2020 12:13:57 +0530 Subject: [PATCH 18/97] fix: Delete with force to bypass linked docs errors --- frappe/installer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/installer.py b/frappe/installer.py index be9b04d453..6d6ed4eb94 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -180,11 +180,11 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False) for record in frappe.get_all(doctype, filters={"module": module_name}, pluck="name"): print(f"* removing {doctype} '{record}'...") if not dry_run: - frappe.delete_doc(doctype, record, ignore_on_trash=True) + frappe.delete_doc(doctype, record, ignore_on_trash=True, force=True) print(f"* removing Module Def '{module_name}'...") if not dry_run: - frappe.delete_doc("Module Def", module_name, ignore_on_trash=True) + frappe.delete_doc("Module Def", module_name, ignore_on_trash=True, force=True) for doctype in set(drop_doctypes): print(f"* dropping Table for '{doctype}'...") From edbb26d73edb0873785502556c1634467cc9d4b1 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 20 Nov 2020 11:49:45 +0530 Subject: [PATCH 19/97] fix: display style removed from emails --- frappe/utils/html_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/utils/html_utils.py b/frappe/utils/html_utils.py index 302813645e..6fdd383eb9 100644 --- a/frappe/utils/html_utils.py +++ b/frappe/utils/html_utils.py @@ -34,7 +34,7 @@ def clean_email_html(html): 'margin', 'margin-top', 'margin-bottom', 'margin-left', 'margin-right', 'padding', 'padding-top', 'padding-bottom', 'padding-left', 'padding-right', 'font-size', 'font-weight', 'font-family', 'text-decoration', - 'line-height', 'text-align', 'vertical-align' + 'line-height', 'text-align', 'vertical-align', 'display' ], protocols=['cid', 'http', 'https', 'mailto', 'data'], strip=True, strip_comments=True) From 6bfe86d1272a7a9cc0585d17dc53e486654fec03 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 20 Nov 2020 17:40:20 +0100 Subject: [PATCH 20/97] feat: translate oauth confirmation dialog --- .../templates/includes/oauth_confirmation.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/frappe/templates/includes/oauth_confirmation.html b/frappe/templates/includes/oauth_confirmation.html index 73425af036..3fbbb75971 100644 --- a/frappe/templates/includes/oauth_confirmation.html +++ b/frappe/templates/includes/oauth_confirmation.html @@ -1,7 +1,7 @@ {% if not error %}
-

{{ client_id }} wants to access the following details from your account

+

{{ _("{} wants to access the following details from your account").format(client_id) }}

    @@ -11,10 +11,10 @@
  • - +
  • - +
@@ -22,24 +22,24 @@ {% else %}
-

Authorization error for {{ client_id }}

+

{{ _("Authorization error for {}.").format(client_id) }}

-

An unexpected error occurred while authorizing {{ client_id }}.

+

{{ _("An unexpected error occurred while authorizing {}.").format(client_id) }}

{{ error }}

  • - +
From 5a52bc73effc615f2720303d7b0c7fe30621af8b Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 20 Nov 2020 17:40:56 +0100 Subject: [PATCH 21/97] fix: cookie value --- frappe/oauth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/oauth.py b/frappe/oauth.py index bf225ac118..09af5ad809 100644 --- a/frappe/oauth.py +++ b/frappe/oauth.py @@ -148,7 +148,7 @@ class OAuthWebRequestValidator(RequestValidator): print("Failed body authentication: Application %s does not exist".format(cid=request.client_id)) cookie_dict = get_cookie_dict_from_headers(request) - user_id = unquote(cookie_dict['user_id']) if 'user_id' in cookie_dict else "Guest" + user_id = unquote(cookie_dict.get('user_id').value) if 'user_id' in cookie_dict else "Guest" return frappe.session.user == user_id def authenticate_client_id(self, client_id, request, *args, **kwargs): From 9fb635828fe900d8e0eef7fb6ae6e4735586bc03 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 20 Nov 2020 17:44:35 +0100 Subject: [PATCH 22/97] refactor: oauth2 --- frappe/integrations/oauth2.py | 120 +++++++++++++++++----------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/frappe/integrations/oauth2.py b/frappe/integrations/oauth2.py index c8dfc52c95..59d0278d79 100644 --- a/frappe/integrations/oauth2.py +++ b/frappe/integrations/oauth2.py @@ -1,41 +1,50 @@ from __future__ import unicode_literals -import frappe, json -from frappe.oauth import OAuthWebRequestValidator, WebApplicationServer + +import hashlib +import json +import jwt +from werkzeug.urls import url_fix from oauthlib.oauth2 import FatalClientError, OAuth2Error -from werkzeug import url_fix from six.moves.urllib.parse import quote, urlencode, urlparse -from frappe.integrations.doctype.oauth_provider_settings.oauth_provider_settings import get_oauth_settings + +import frappe from frappe import _ +from frappe.oauth import OAuthWebRequestValidator, WebApplicationServer +from frappe.integrations.doctype.oauth_provider_settings.oauth_provider_settings import get_oauth_settings def get_oauth_server(): if not getattr(frappe.local, 'oauth_server', None): oauth_validator = OAuthWebRequestValidator() - frappe.local.oauth_server = WebApplicationServer(oauth_validator) + frappe.local.oauth_server = WebApplicationServer(oauth_validator) return frappe.local.oauth_server -def get_urlparams_from_kwargs(param_kwargs): +def sanitize_kwargs(param_kwargs): arguments = param_kwargs - if arguments.get("data"): - arguments.pop("data") - if arguments.get("cmd"): - arguments.pop("cmd") + arguments.pop('data', None) + arguments.pop('cmd', None) - return urlencode(arguments) + return arguments @frappe.whitelist() def approve(*args, **kwargs): r = frappe.request - uri = url_fix(r.url.replace("+"," ")) - http_method = r.method - body = r.get_data() - headers = r.headers try: - scopes, frappe.flags.oauth_credentials = get_oauth_server().validate_authorization_request(uri, http_method, body, headers) + scopes, frappe.flags.oauth_credentials = get_oauth_server().validate_authorization_request( + r.url, + r.method, + r.get_data(), + r.headers + ) - headers, body, status = get_oauth_server().create_authorization_response(uri=frappe.flags.oauth_credentials['redirect_uri'], \ - body=body, headers=headers, scopes=scopes, credentials=frappe.flags.oauth_credentials) + headers, body, status = get_oauth_server().create_authorization_response( + uri=frappe.flags.oauth_credentials['redirect_uri'], + body=r.get_data(), + headers=r.headers, + scopes=scopes, + credentials=frappe.flags.oauth_credentials + ) uri = headers.get('Location', None) frappe.local.response["type"] = "redirect" @@ -47,34 +56,28 @@ def approve(*args, **kwargs): return e @frappe.whitelist(allow_guest=True) -def authorize(*args, **kwargs): - #Fetch provider URL from settings - oauth_settings = get_oauth_settings() - params = get_urlparams_from_kwargs(kwargs) - request_url = urlparse(frappe.request.url) - success_url = request_url.scheme + "://" + request_url.netloc + "/api/method/frappe.integrations.oauth2.approve?" + params +def authorize(**kwargs): + success_url = "/api/method/frappe.integrations.oauth2.approve?" + urlencode(sanitize_kwargs(kwargs)) failure_url = frappe.form_dict["redirect_uri"] + "?error=access_denied" - if frappe.session['user']=='Guest': + if frappe.session.user == 'Guest': #Force login, redirect to preauth again. frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = "/login?redirect-to=/api/method/frappe.integrations.oauth2.authorize?" + quote(params.replace("+"," ")) - - elif frappe.session['user']!='Guest': + frappe.local.response["location"] = "/login?" + urlencode({'redirect-to': frappe.request.url}) + else: try: r = frappe.request - uri = url_fix(r.url) - http_method = r.method - body = r.get_data() - headers = r.headers - - scopes, frappe.flags.oauth_credentials = get_oauth_server().validate_authorization_request(uri, http_method, body, headers) + scopes, frappe.flags.oauth_credentials = get_oauth_server().validate_authorization_request( + r.url, + r.method, + r.get_data(), + r.headers + ) skip_auth = frappe.db.get_value("OAuth Client", frappe.flags.oauth_credentials['client_id'], "skip_authorization") unrevoked_tokens = frappe.get_all("OAuth Bearer Token", filters={"status":"Active"}) - if skip_auth or (oauth_settings["skip_authorization"] == "Auto" and len(unrevoked_tokens)): - + if skip_auth or (get_oauth_settings().skip_authorization == "Auto" and unrevoked_tokens): frappe.local.response["type"] = "redirect" frappe.local.response["location"] = success_url else: @@ -87,7 +90,6 @@ def authorize(*args, **kwargs): }) resp_html = frappe.render_template("templates/includes/oauth_confirmation.html", response_html_params) frappe.respond_as_web_page("Confirm Access", resp_html) - except FatalClientError as e: return e except OAuth2Error as e: @@ -95,20 +97,20 @@ def authorize(*args, **kwargs): @frappe.whitelist(allow_guest=True) def get_token(*args, **kwargs): - r = frappe.request - - uri = url_fix(r.url) - http_method = r.method - body = r.form - headers = r.headers - #Check whether frappe server URL is set frappe_server_url = frappe.db.get_value("Social Login Key", "frappe", "base_url") or None if not frappe_server_url: frappe.throw(_("Please set Base URL in Social Login Key for Frappe")) try: - headers, body, status = get_oauth_server().create_token_response(uri, http_method, body, headers, frappe.flags.oauth_credentials) + r = frappe.request + headers, body, status = get_oauth_server().create_token_response( + r.url, + r.method, + r.form, + r.headers, + frappe.flags.oauth_credentials + ) out = frappe._dict(json.loads(body)) if not out.error and "openid" in out.scope: token_user = frappe.db.get_value("OAuth Bearer Token", out.access_token, "user") @@ -116,7 +118,7 @@ def get_token(*args, **kwargs): client_secret = frappe.db.get_value("OAuth Client", token_client, "client_secret") if token_user in ["Guest", "Administrator"]: frappe.throw(_("Logged in as Guest or Administrator")) - import hashlib + id_token_header = { "typ":"jwt", "alg":"HS256" @@ -128,9 +130,10 @@ def get_token(*args, **kwargs): "iss": frappe_server_url, "at_hash": frappe.oauth.calculate_at_hash(out.access_token, hashlib.sha256) } - import jwt + id_token_encoded = jwt.encode(id_token, client_secret, algorithm='HS256', headers=id_token_header) - out.update({"id_token":str(id_token_encoded)}) + out.update({"id_token": str(id_token_encoded)}) + frappe.local.response = out except FatalClientError as e: @@ -140,12 +143,12 @@ def get_token(*args, **kwargs): @frappe.whitelist(allow_guest=True) def revoke_token(*args, **kwargs): r = frappe.request - uri = url_fix(r.url) - http_method = r.method - body = r.form - headers = r.headers - - headers, body, status = get_oauth_server().create_revocation_response(uri, headers=headers, body=body, http_method=http_method) + headers, body, status = get_oauth_server().create_revocation_response( + r.url, + headers=r.headers, + body=r.form, + http_method=r.method + ) frappe.local.response['http_status_code'] = status if status == 200: @@ -174,15 +177,12 @@ def openid_profile(*args, **kwargs): "email": name, "picture": picture }) - + frappe.local.response = user_profile def validate_url(url_string): try: result = urlparse(url_string) - if result.scheme and result.scheme in ["http", "https", "ftp", "ftps"]: - return True - else: - return False + return result.scheme and result.scheme in ["http", "https", "ftp", "ftps"] except: - return False \ No newline at end of file + return False From bea4f6e11b5058c7aa457141f2fd0aa59b27615b Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 20 Nov 2020 18:03:17 +0100 Subject: [PATCH 23/97] fix: remove unused import --- frappe/integrations/oauth2.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/integrations/oauth2.py b/frappe/integrations/oauth2.py index 59d0278d79..7570a50127 100644 --- a/frappe/integrations/oauth2.py +++ b/frappe/integrations/oauth2.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import hashlib import json import jwt -from werkzeug.urls import url_fix from oauthlib.oauth2 import FatalClientError, OAuth2Error from six.moves.urllib.parse import quote, urlencode, urlparse From e93a38f912d8fb10f4610173d218508b7a4459b7 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 20 Nov 2020 19:02:33 +0100 Subject: [PATCH 24/97] refactor: move encode_params from test to oauth2.py --- frappe/integrations/oauth2.py | 17 ++++++++++++++--- frappe/tests/test_oauth20.py | 11 +---------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/frappe/integrations/oauth2.py b/frappe/integrations/oauth2.py index 7570a50127..a750c8328c 100644 --- a/frappe/integrations/oauth2.py +++ b/frappe/integrations/oauth2.py @@ -2,9 +2,10 @@ from __future__ import unicode_literals import hashlib import json +from urllib.parse import quote, urlencode, urlparse + import jwt from oauthlib.oauth2 import FatalClientError, OAuth2Error -from six.moves.urllib.parse import quote, urlencode, urlparse import frappe from frappe import _ @@ -56,13 +57,13 @@ def approve(*args, **kwargs): @frappe.whitelist(allow_guest=True) def authorize(**kwargs): - success_url = "/api/method/frappe.integrations.oauth2.approve?" + urlencode(sanitize_kwargs(kwargs)) + success_url = "/api/method/frappe.integrations.oauth2.approve?" + encode_params(sanitize_kwargs(kwargs)) failure_url = frappe.form_dict["redirect_uri"] + "?error=access_denied" if frappe.session.user == 'Guest': #Force login, redirect to preauth again. frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = "/login?" + urlencode({'redirect-to': frappe.request.url}) + frappe.local.response["location"] = "/login?" + encode_params({'redirect-to': frappe.request.url}) else: try: r = frappe.request @@ -185,3 +186,13 @@ def validate_url(url_string): return result.scheme and result.scheme in ["http", "https", "ftp", "ftps"] except: return False + +def encode_params(params): + """ + Encode a dict of params into a query string. + + Use `quote_via=urllib.parse.quote` so that whitespaces will be encoded as + `%20` instead of as `+`. This is needed because oauthlib cannot handle `+` + as a whitespace. + """ + return urlencode(params, quote_via=quote) diff --git a/frappe/tests/test_oauth20.py b/frappe/tests/test_oauth20.py index f4ecc8a68d..e2213145b7 100644 --- a/frappe/tests/test_oauth20.py +++ b/frappe/tests/test_oauth20.py @@ -6,6 +6,7 @@ import unittest, frappe, requests, time from frappe.test_runner import make_test_records from six.moves.urllib.parse import urlparse, parse_qs, urljoin from urllib.parse import urlencode, quote +from frappe.integrations.oauth2 import encode_params class TestOAuth20(unittest.TestCase): @@ -232,13 +233,3 @@ def login(session): def get_full_url(endpoint): """Turn '/endpoint' into 'http://127.0.0.1:8000/endpoint'.""" return urljoin(frappe.utils.get_url(), endpoint) - -def encode_params(params): - """ - Encode a dict of params into a query string. - - Use `quote_via=urllib.parse.quote` so that whitespaces will be encoded as - `%20` instead of as `+`. This is needed because oauthlib cannot handle `+` - as a whitespace. - """ - return urlencode(params, quote_via=quote) From 0312b9d67cd9ab45cf4f7e342e100dbb87ad1d26 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 17 Nov 2020 19:20:44 +0530 Subject: [PATCH 25/97] feat: check if auto_repeat field is already present --- .../doctype/customize_form/customize_form.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index cf674082ab..67b4f05856 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -76,17 +76,20 @@ class CustomizeForm(Document): def create_auto_repeat_custom_field_if_requried(self, meta): if self.allow_auto_repeat: - if not frappe.db.exists('Custom Field', {'fieldname': 'auto_repeat', 'dt': self.doc_type}) and \ - not frappe.db.exists('DocField', {'fieldname': 'auto_repeat', 'parent': self.name}): - insert_after = self.fields[len(self.fields) - 1].fieldname - df = dict( - fieldname='auto_repeat', - label='Auto Repeat', - fieldtype='Link', - options='Auto Repeat', - insert_after=insert_after, - read_only=1, no_copy=1, print_hide=1) - create_custom_field(self.doc_type, df) + all_fields = [df.fieldname for df in meta.fields] + + if "auto_repeat" in all_fields: + return + + insert_after = self.fields[len(self.fields) - 1].fieldname + create_custom_field(self.doc_type, dict( + fieldname='auto_repeat', + label='Auto Repeat', + fieldtype='Link', + options='Auto Repeat', + insert_after=insert_after, + read_only=1, no_copy=1, print_hide=1 + )) def get_name_translation(self): From 019fca9ef773e006a57069b8137a1043593da174 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 23 Nov 2020 11:33:16 +0530 Subject: [PATCH 26/97] fix: typo in function name --- frappe/custom/doctype/customize_form/customize_form.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 67b4f05856..60ae65091d 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -39,7 +39,7 @@ class CustomizeForm(Document): translation = self.get_name_translation() self.label = translation.translated_text if translation else '' - self.create_auto_repeat_custom_field_if_requried(meta) + self.create_auto_repeat_custom_field_if_required(meta) # NOTE doc (self) is sent to clientside by run_method @@ -74,7 +74,7 @@ class CustomizeForm(Document): for d in meta.get(fieldname): self.append(fieldname, d) - def create_auto_repeat_custom_field_if_requried(self, meta): + def create_auto_repeat_custom_field_if_required(self, meta): if self.allow_auto_repeat: all_fields = [df.fieldname for df in meta.fields] From 44413e9ba6010a88b25114c67fb12275f0de803f Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 23 Nov 2020 11:34:49 +0530 Subject: [PATCH 27/97] chore: add docstrings --- frappe/custom/doctype/customize_form/customize_form.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 60ae65091d..82513783c7 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -75,6 +75,9 @@ class CustomizeForm(Document): self.append(fieldname, d) def create_auto_repeat_custom_field_if_required(self, meta): + ''' + Create auto repeat custom field if it's not already present + ''' if self.allow_auto_repeat: all_fields = [df.fieldname for df in meta.fields] From 456030760dba7b18753e42f34df383c81c421f0e Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 23 Nov 2020 19:11:48 +0530 Subject: [PATCH 28/97] chore: Allow custom freeze message in Open Mapped Doc --- frappe/public/js/frappe/model/create_new.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/public/js/frappe/model/create_new.js b/frappe/public/js/frappe/model/create_new.js index 7be7fc5baa..f7a5982b96 100644 --- a/frappe/public/js/frappe/model/create_new.js +++ b/frappe/public/js/frappe/model/create_new.js @@ -306,6 +306,7 @@ $.extend(frappe.model, { selected_children: opts.frm ? opts.frm.get_selected() : null }, freeze: true, + freeze_message: opts.freeze_message || '', callback: function(r) { if(!r.exc) { frappe.model.sync(r.message); From 0866526d11bbe7c9afa102cbaa3665b4423af0ca Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Tue, 24 Nov 2020 09:35:18 +0530 Subject: [PATCH 29/97] fix(Document Follow): Skip Email Account and Email Domain (#11973) --- frappe/desk/form/document_follow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/desk/form/document_follow.py b/frappe/desk/form/document_follow.py index 3aa3a4fa88..66164948f2 100644 --- a/frappe/desk/form/document_follow.py +++ b/frappe/desk/form/document_follow.py @@ -21,7 +21,7 @@ def follow_document(doctype, doc_name, user, force=False): avoided for some doctype follow only if track changes are set to 1 ''' - if (doctype in ("Communication", "ToDo", "Email Unsubscribe", "File", "Comment") + if (doctype in ("Communication", "ToDo", "Email Unsubscribe", "File", "Comment", "Email Account", "Email Domain") or doctype in log_types): return From f71a8e0afcffae02dee34fd20642ee439c6b6017 Mon Sep 17 00:00:00 2001 From: everyx Date: Tue, 24 Nov 2020 14:36:53 +0800 Subject: [PATCH 30/97] fix: `PRIMARY KEY must be NOT NULL` error when install with MySQL 5.7+ --- frappe/database/mariadb/framework_mariadb.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql index 15b0bed699..a52efd01e3 100644 --- a/frappe/database/mariadb/framework_mariadb.sql +++ b/frappe/database/mariadb/framework_mariadb.sql @@ -233,7 +233,7 @@ CREATE TABLE `tabDocType` ( DROP TABLE IF EXISTS `tabSeries`; CREATE TABLE `tabSeries` ( - `name` varchar(100) DEFAULT NULL, + `name` varchar(100), `current` int(10) NOT NULL DEFAULT 0, PRIMARY KEY(`name`) ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; From 200211e11ef40db91b6bd5fe83203792d779d4ce Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 24 Nov 2020 14:27:10 +0530 Subject: [PATCH 31/97] feat: use modified by or owner to send notification from --- frappe/email/doctype/notification/notification.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index 75281d427e..2ea7a3785e 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -181,6 +181,7 @@ def get_context(context): 'document_type': doc.doctype, 'document_name': doc.name, 'subject': subject, + 'from_user': doc.modified_by or doc.owner, 'email_content': frappe.render_template(self.message, context), 'attached_file': attachments and json.dumps(attachments[0]) } From 856ff501e4399b4711de4b16f6e4e6afb712a7a7 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 24 Nov 2020 14:27:34 +0530 Subject: [PATCH 32/97] feat: show recipients section for System Notifications --- frappe/email/doctype/notification/notification.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/email/doctype/notification/notification.json b/frappe/email/doctype/notification/notification.json index 73a84e1d3e..c1c877efd4 100644 --- a/frappe/email/doctype/notification/notification.json +++ b/frappe/email/doctype/notification/notification.json @@ -207,7 +207,7 @@ "label": "Value To Be Set" }, { - "depends_on": "eval:in_list(['Email', 'SMS'], doc.channel)", + "depends_on": "eval:doc.channel !=\"Slack\"", "fieldname": "column_break_5", "fieldtype": "Section Break", "label": "Recipients" @@ -281,7 +281,7 @@ "icon": "fa fa-envelope", "index_web_pages_for_search": 1, "links": [], - "modified": "2020-10-28 11:04:54.955567", + "modified": "2020-11-24 14:25:43.245677", "modified_by": "Administrator", "module": "Email", "name": "Notification", From 0d6dafa3f589a224feafef1dedb5a6c09104a632 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Tue, 24 Nov 2020 11:36:23 +0100 Subject: [PATCH 33/97] fix: allow other github links in same PR --- .github/helper/documentation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/helper/documentation.py b/.github/helper/documentation.py index 3fc14ba61b..08d1d1aa9c 100644 --- a/.github/helper/documentation.py +++ b/.github/helper/documentation.py @@ -21,8 +21,8 @@ def docs_link_exists(body): if word.startswith('http') and uri_validator(word): parsed_url = urlparse(word) if parsed_url.netloc == "github.com": - _, org, repo, _type, ref = parsed_url.path.split('/') - if org == "frappe" and repo in docs_repos: + parts = parsed_url.path.split('/') + if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos: return True From ff059156d8757bc89c945256116479bd6308db11 Mon Sep 17 00:00:00 2001 From: bhavesh95863 <34086262+bhavesh95863@users.noreply.github.com> Date: Tue, 24 Nov 2020 22:28:11 +0530 Subject: [PATCH 34/97] feat: translate kanboard board title --- frappe/public/js/frappe/views/kanban/kanban_view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/kanban/kanban_view.js b/frappe/public/js/frappe/views/kanban/kanban_view.js index e4d0659965..48a45fddd6 100644 --- a/frappe/public/js/frappe/views/kanban/kanban_view.js +++ b/frappe/public/js/frappe/views/kanban/kanban_view.js @@ -30,7 +30,7 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView { return super.setup_defaults() .then(() => { this.board_name = frappe.get_route()[3]; - this.page_title = this.board_name; + this.page_title = __(this.board_name); this.card_meta = this.get_card_meta(); this.menu_items.push({ From c903286465c95e966a11b3a3e0b209f3f7997a20 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 24 Nov 2020 23:28:00 +0530 Subject: [PATCH 35/97] fix: Not able to save Domain Settings (#11984) --- frappe/core/doctype/domain_settings/domain_settings.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frappe/core/doctype/domain_settings/domain_settings.js b/frappe/core/doctype/domain_settings/domain_settings.js index 1428727993..7178cb4cd6 100644 --- a/frappe/core/doctype/domain_settings/domain_settings.js +++ b/frappe/core/doctype/domain_settings/domain_settings.js @@ -18,6 +18,9 @@ frappe.ui.form.on('Domain Settings', { checked: active_domains.includes(domain) }; }); + }, + on_change: () => { + frm.dirty(); } }, render_input: true From 1154cc931fafd454a98ca8c15ed21fbdd258757c Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Wed, 25 Nov 2020 07:29:34 +0200 Subject: [PATCH 36/97] fix(Snyk): Security upgrade snyk from 1.398.1 to 1.425.4 (#11990) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-Y18N-1021887 --- package.json | 2 +- yarn.lock | 349 +++++++++++++++++++++++++++++---------------------- 2 files changed, 202 insertions(+), 149 deletions(-) diff --git a/package.json b/package.json index c9eb9c0e56..91d204bec5 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "qz-tray": "^2.0.8", "redis": "^2.8.0", "showdown": "^1.9.1", - "snyk": "^1.398.1", + "snyk": "^1.425.4", "socket.io": "^2.3.0", "superagent": "^3.8.2", "touch": "^3.1.0", diff --git a/yarn.lock b/yarn.lock index 15a6321ae2..459f4139b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -93,10 +93,21 @@ source-map-support "^0.5.19" tslib "^1.13.0" -"@snyk/docker-registry-v2-client@^1.13.5": - version "1.13.5" - resolved "https://registry.yarnpkg.com/@snyk/docker-registry-v2-client/-/docker-registry-v2-client-1.13.5.tgz#8d862f0c53d4a9a25db09cd48b4cd44aa8e385c9" - integrity sha512-lgJiC071abCpFVLp47OnykU8MMrhdQe386Wt6QaDmjI0s2DQn/S58NfdLrPU7s6l4zoGT7UwRW9+7paozRgFTA== +"@snyk/dep-graph@^1.19.5": + version "1.20.0" + resolved "https://registry.yarnpkg.com/@snyk/dep-graph/-/dep-graph-1.20.0.tgz#258ae85f8a066dc63af4444cfca8b8d092b94bc0" + integrity sha512-/TOzXGh+JFgAu8pWdo1oLFKDNfFk99TnSQG2lbEu+vKLI2ZrGAk9oGO0geNogAN7Ib4EDQOEhgb7YwqwL7aA7w== + dependencies: + graphlib "^2.1.8" + lodash.isequal "^4.5.0" + object-hash "^2.0.3" + semver "^6.0.0" + tslib "^1.13.0" + +"@snyk/docker-registry-v2-client@1.13.9": + version "1.13.9" + resolved "https://registry.yarnpkg.com/@snyk/docker-registry-v2-client/-/docker-registry-v2-client-1.13.9.tgz#54c2e3071de58fc6fc12c5fef5eaeae174ecda12" + integrity sha512-DIFLEhr8m1GrAwsLGInJmpcQMacjuhf3jcbpQTR+LeMvZA9IuKq+B7kqw2O2FzMiHMZmUb5z+tV+BR7+IUHkFQ== dependencies: needle "^2.5.0" parse-link-header "^1.0.1" @@ -107,10 +118,10 @@ resolved "https://registry.yarnpkg.com/@snyk/gemfile/-/gemfile-1.2.0.tgz#919857944973cce74c650e5428aaf11bcd5c0457" integrity sha512-nI7ELxukf7pT4/VraL4iabtNNMz8mUo7EXlqCFld8O5z6mIMLX9llps24iPpaIZOwArkY3FWA+4t+ixyvtTSIA== -"@snyk/java-call-graph-builder@1.13.2": - version "1.13.2" - resolved "https://registry.yarnpkg.com/@snyk/java-call-graph-builder/-/java-call-graph-builder-1.13.2.tgz#6e4a9495d5c47bbab9bc69e066d4646473781b67" - integrity sha512-YN3a93ttscqFQRUeThrxa7i2SJkFPfYn0VpFqdPB6mIJz2fRVLxUkMtlCbG0aSEUvWiLnGVHN0IYxwWEzhq11w== +"@snyk/java-call-graph-builder@1.16.2": + version "1.16.2" + resolved "https://registry.yarnpkg.com/@snyk/java-call-graph-builder/-/java-call-graph-builder-1.16.2.tgz#a9f9a34107759cf2be847a114a759e347cef44e8" + integrity sha512-tJF+dY/wTfexwYuCgFB3RpWl4RGcf2H9RT9yurkTVi5wwKfvcNwZMUMwSlTDEFOqwmAsJ7e0uNVRlkPQHekCcQ== dependencies: ci-info "^2.0.0" debug "^4.1.1" @@ -119,11 +130,29 @@ jszip "^3.2.2" needle "^2.3.3" progress "^2.0.3" - snyk-config "^3.0.0" + snyk-config "^4.0.0-rc.2" source-map-support "^0.5.7" temp-dir "^2.0.0" tslib "^1.9.3" +"@snyk/java-call-graph-builder@1.16.5": + version "1.16.5" + resolved "https://registry.yarnpkg.com/@snyk/java-call-graph-builder/-/java-call-graph-builder-1.16.5.tgz#e57302cc6dc93f1adff7abe1e5eecff26d8a41f4" + integrity sha512-6H4hkq/qYljJoH1QnZsTRPMqp9Kt5AOEZYGJAeSHkhJdfUYSLtqwN4WsU6yVR3vWAaDQ8Lllp3m6EL7nstMPZA== + dependencies: + ci-info "^2.0.0" + debug "^4.1.1" + glob "^7.1.6" + graphlib "^2.1.8" + jszip "^3.2.2" + needle "^2.3.3" + progress "^2.0.3" + snyk-config "^4.0.0-rc.2" + source-map-support "^0.5.7" + temp-dir "^2.0.0" + tmp "^0.2.1" + tslib "^1.9.3" + "@snyk/rpm-parser@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@snyk/rpm-parser/-/rpm-parser-2.0.0.tgz#4ded7fa4b0a8efca7699359e4ca7a79bfbe38bc1" @@ -142,12 +171,12 @@ source-map-support "^0.5.7" tslib "^2.0.0" -"@snyk/snyk-docker-pull@^3.2.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@snyk/snyk-docker-pull/-/snyk-docker-pull-3.2.0.tgz#07c47b8be2d899d51d720099a73a0d89effe5d99" - integrity sha512-uWKtjh29I/d0mfmfBN7w6RwwNBQxQVKrauF5ND/gqb0PVsKV22GIpkI+viWjI7KNKso6/B0tMmsv7TX2tsNcLQ== +"@snyk/snyk-docker-pull@3.2.3": + version "3.2.3" + resolved "https://registry.yarnpkg.com/@snyk/snyk-docker-pull/-/snyk-docker-pull-3.2.3.tgz#9743ea624098c7abd0f95c438c76067530494f4b" + integrity sha512-hiFiSmWGLc2tOI7FfgIhVdFzO2f69im8O6p3OV4xEZ/Ss1l58vwtqudItoswsk7wj/azRlgfBW8wGu2MjoudQg== dependencies: - "@snyk/docker-registry-v2-client" "^1.13.5" + "@snyk/docker-registry-v2-client" "1.13.9" child-process "^1.0.2" tar-stream "^2.1.2" tmp "^0.1.0" @@ -545,10 +574,10 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg== -async@^1.4.0: - version "1.5.2" - resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" - integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= +async@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" + integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== asynckit@^0.4.0: version "0.4.0" @@ -757,6 +786,13 @@ braces@^2.3.1: split-string "^3.0.2" to-regex "^3.0.1" +braces@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + browserify-zlib@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d" @@ -896,7 +932,7 @@ camelcase-keys@^2.0.0: camelcase "^2.0.0" map-obj "^1.0.0" -camelcase@^2.0.0, camelcase@^2.0.1: +camelcase@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= @@ -1021,15 +1057,6 @@ cli-width@^3.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== -cliui@^3.0.3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" - integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrap-ansi "^2.0.0" - cliui@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" @@ -1507,7 +1534,14 @@ debug@^3.1.0, debug@^3.2.5, debug@^3.2.6: dependencies: ms "^2.1.1" -decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0: +debug@^4.2.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== + dependencies: + ms "2.1.2" + +decamelize@^1.1.2, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= @@ -1751,6 +1785,13 @@ electron-to-chromium@^1.3.523: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.551.tgz#a94d243a4ca90705189bd4a5eca4e0f56b745a4f" integrity sha512-11qcm2xvf2kqeFO5EIejaBx5cKXsW1quAyv3VctCMYwofnyVZLs97y6LCekss3/ghQpr7PYkSO3uId5FmxZsdw== +elfy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/elfy/-/elfy-1.0.0.tgz#7a1c86af7d41e0a568cbb4a3fa5b685648d9efcd" + integrity sha512-4Kp3AA94jC085IJox+qnvrZ3PudqTi4gQNvIoTZfJJ9IqkRuCoqP60vCVYlIg00c5aYusi5Wjh2bf0cHYt+6gQ== + dependencies: + endian-reader "^0.3.0" + email-validator@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/email-validator/-/email-validator-2.0.4.tgz#b8dfaa5d0dae28f1b03c95881d904d4e40bfe7ed" @@ -1783,6 +1824,11 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" +endian-reader@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/endian-reader/-/endian-reader-0.3.0.tgz#84eca436b80aed0d0639c47291338b932efe50a0" + integrity sha1-hOykNrgK7Q0GOcRykTOLky7+UKA= + engine.io-client@~3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.0.tgz#82a642b42862a9b3f7a188f41776b2deab643700" @@ -2177,6 +2223,13 @@ fill-range@^4.0.0: repeat-string "^1.6.1" to-regex-range "^2.1.0" +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + finalhandler@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" @@ -2678,6 +2731,13 @@ hosted-git-info@^3.0.4: dependencies: lru-cache "^6.0.0" +hosted-git-info@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-3.0.7.tgz#a30727385ea85acfcee94e0aad9e368c792e036c" + integrity sha512-fWqc0IcuXs+BmE9orLDyVykAG9GJtGLGuZAAqgcckPgv5xad4AcXGIv8galtQvlwutxSlaMcdw7BUtq2EIvqCQ== + dependencies: + lru-cache "^6.0.0" + hsl-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e" @@ -2857,7 +2917,7 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -ini@^1.3.0, ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: +ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== @@ -2881,11 +2941,6 @@ inquirer@^7.3.3: strip-ansi "^6.0.0" through "^2.3.6" -invert-kv@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" - integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= - iota-array@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/iota-array/-/iota-array-1.0.0.tgz#81ef57fe5d05814cd58c2483632a99c30a0e8087" @@ -3122,6 +3177,11 @@ is-number@^3.0.0: dependencies: kind-of "^3.0.2" +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + is-obj@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" @@ -3447,13 +3507,6 @@ latest-version@^5.0.0: dependencies: package-json "^6.3.0" -lcid@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" - integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= - dependencies: - invert-kv "^1.0.0" - less@^3.11.1: version "3.11.1" resolved "https://registry.yarnpkg.com/less/-/less-3.11.1.tgz#c6bf08e39e02404fe6b307a3dfffafdc55bd36e2" @@ -3750,6 +3803,14 @@ methods@^1.1.1, methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= +micromatch@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + micromatch@^3.1.10: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -3896,7 +3957,7 @@ ms@2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== -ms@^2.1.1: +ms@2.1.2, ms@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== @@ -3928,16 +3989,6 @@ nanomatch@^1.2.9: snapdragon "^0.8.1" to-regex "^3.0.1" -nconf@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/nconf/-/nconf-0.10.0.tgz#da1285ee95d0a922ca6cee75adcf861f48205ad2" - integrity sha512-fKiXMQrpP7CYWJQzKkPPx9hPgmq+YLDyxcG9N8RpiE9FoCkCbzD0NyW0YhE3xn3Aupe7nnDeIx4PFzYehpHT9Q== - dependencies: - async "^1.4.0" - ini "^1.3.0" - secure-keys "^1.0.0" - yargs "^3.19.0" - ndarray-linear-interpolate@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/ndarray-linear-interpolate/-/ndarray-linear-interpolate-1.0.0.tgz#78bc92b85b9abc15b6e67ee65828f9e2137ae72b" @@ -4268,13 +4319,6 @@ os-homedir@^1.0.0, os-homedir@^1.0.1: resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= -os-locale@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" - integrity sha1-IPnxeuKe00XoveWDsT0gCYA8FNk= - dependencies: - lcid "^1.0.0" - os-name@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/os-name/-/os-name-3.1.0.tgz#dec19d966296e1cd62d701a5a66ee1ddeae70801" @@ -4508,6 +4552,11 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= +picomatch@^2.0.5: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -5687,11 +5736,6 @@ scss-tokenizer@^0.2.3: js-base64 "^2.1.8" source-map "^0.4.2" -secure-keys@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/secure-keys/-/secure-keys-1.0.0.tgz#f0c82d98a3b139a8776a8808050b824431087fca" - integrity sha1-8MgtmKOxOah3aogIBQuCRDEIf8o= - semver-diff@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" @@ -5862,40 +5906,55 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" -snyk-config@3.1.1, snyk-config@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/snyk-config/-/snyk-config-3.1.1.tgz#a511ef8bf769545f0564e09d382b5ea3aacb9c6a" - integrity sha512-wwrMIEDozfLJ8LmakCsCC1FQ0siIX5icCQPCbUKKgRbeVsZ27NjPJs37BpTXX4rcHkaWpe8TbH3yOtp23qmszg== +snyk-config@4.0.0-rc.2: + version "4.0.0-rc.2" + resolved "https://registry.yarnpkg.com/snyk-config/-/snyk-config-4.0.0-rc.2.tgz#c6c94afe733e9063df546cd71a7adf6957135594" + integrity sha512-HIXpMCRp5IdQDFH/CY6WqOUt5X5Ec55KC9dFVjlMLe/2zeqsImJn1vbjpE5uBoLYIdYi1SteTqtsJhyJZWRK8g== dependencies: + async "^3.2.0" debug "^4.1.1" lodash.merge "^4.6.2" - nconf "^0.10.0" + minimist "^1.2.5" -snyk-cpp-plugin@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/snyk-cpp-plugin/-/snyk-cpp-plugin-1.5.0.tgz#2ec2068fdcf5e579eb7d9b9eed8bb984fd00a925" - integrity sha512-nBZ0cBmpT4RVJUFzYydQJOxwjcdXk7NtRJE1UIIOafQa2FcvIl3GBezfrCJ6pu61svOAf5r8Qi/likx6F15K1A== +snyk-config@^4.0.0-rc.2: + version "4.0.0" + resolved "https://registry.yarnpkg.com/snyk-config/-/snyk-config-4.0.0.tgz#21d459f19087991246cc07a7ffb4501dce6f4159" + integrity sha512-E6jNe0oUjjzVASWBOAc/mA23DhbzABDF9MI6UZvl0gylh2NSXSXw2/LjlqMNOKL2c1qkbSkzLOdIX5XACoLCAQ== + dependencies: + async "^3.2.0" + debug "^4.1.1" + lodash.merge "^4.6.2" + minimist "^1.2.5" + +snyk-cpp-plugin@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/snyk-cpp-plugin/-/snyk-cpp-plugin-2.2.1.tgz#55891511a43a6448e5a7c836a94f66f70fa705eb" + integrity sha512-NFwVLMCqKTocY66gcim0ukF6e31VRDJqDapg5sy3vCHqlD1OCNUXSK/aI4VQEEndDrsnFmQepsL5KpEU0dDRIQ== dependencies: "@snyk/dep-graph" "^1.19.3" chalk "^4.1.0" debug "^4.1.1" + hosted-git-info "^3.0.7" tslib "^2.0.0" -snyk-docker-plugin@3.21.0: - version "3.21.0" - resolved "https://registry.yarnpkg.com/snyk-docker-plugin/-/snyk-docker-plugin-3.21.0.tgz#a92074c0411578c1a7b86852a06f1421770e985d" - integrity sha512-A7oJS3QGR7bwm1qeeczCb8PDfi8go1KM6VWph/drJHBQ7JxVKKLb3j4AzrMmIM96mGZFbmyNOL4pznwumaOM8g== +snyk-docker-plugin@4.12.0: + version "4.12.0" + resolved "https://registry.yarnpkg.com/snyk-docker-plugin/-/snyk-docker-plugin-4.12.0.tgz#137a159baf627debef6178cfb8b40941a81a7168" + integrity sha512-iN5GUTpMR4dx/hmjxh1GnJ9vrMpbOUhD8gsdWgFPZ5Qg+ImPQ2WBJBal/hyfkauM0TaKQEAgIwT6xZ1ovaIvWQ== dependencies: + "@snyk/dep-graph" "^1.19.4" "@snyk/rpm-parser" "^2.0.0" - "@snyk/snyk-docker-pull" "^3.2.0" + "@snyk/snyk-docker-pull" "3.2.3" + chalk "^2.4.2" debug "^4.1.1" docker-modem "2.1.3" dockerfile-ast "0.0.30" + elfy "^1.0.0" event-loop-spinner "^2.0.0" gunzip-maybe "^1.4.2" mkdirp "^1.0.4" semver "^6.1.0" - snyk-nodejs-lockfile-parser "1.28.1" + snyk-nodejs-lockfile-parser "1.30.1" tar-stream "^2.1.0" tmp "^0.2.1" tslib "^1" @@ -5921,13 +5980,14 @@ snyk-go-plugin@1.16.2: tmp "0.2.1" tslib "^1.10.0" -snyk-gradle-plugin@3.6.3: - version "3.6.3" - resolved "https://registry.yarnpkg.com/snyk-gradle-plugin/-/snyk-gradle-plugin-3.6.3.tgz#484059bcb98469b6a674bbcbdc995eafb5581041" - integrity sha512-j/eQSLSsK3DHmvVX2fNig4+ugYrKlCOV8Xvo6OYFkNzhMpdyNFiGWTS1uyP1HH75Gyc78MaLANMgjlSYePukzQ== +snyk-gradle-plugin@3.10.2: + version "3.10.2" + resolved "https://registry.yarnpkg.com/snyk-gradle-plugin/-/snyk-gradle-plugin-3.10.2.tgz#f3e104d42989e49b5c05818f005cae8c544c9803" + integrity sha512-gTFKL0BLUN54asUQ4OIoa4lATGn27VZwWDJGQ0VuqSaaoy8I5W16Cbn/KN95oIKa7tgwrmasPLd5uviFWzo/Qw== dependencies: "@snyk/cli-interface" "2.9.1" "@snyk/dep-graph" "^1.19.4" + "@snyk/java-call-graph-builder" "1.16.2" "@types/debug" "^4.1.4" chalk "^3.0.0" debug "^4.1.1" @@ -5960,22 +6020,23 @@ snyk-module@^2.0.2: debug "^3.1.0" hosted-git-info "^2.7.1" -snyk-mvn-plugin@2.19.4: - version "2.19.4" - resolved "https://registry.yarnpkg.com/snyk-mvn-plugin/-/snyk-mvn-plugin-2.19.4.tgz#4e29fa82b9ca409789d441939c766797d6a2360f" - integrity sha512-kYPUKOugnNd31PFqx1YHJTo90pospELYHME4AzBx8dkMDgs5ZPjAmQXSxegQ3AMUqfqcETMSTzlKHe6uHujI8A== +snyk-mvn-plugin@2.23.4: + version "2.23.4" + resolved "https://registry.yarnpkg.com/snyk-mvn-plugin/-/snyk-mvn-plugin-2.23.4.tgz#3f43601058aa51e8a0f9e272a7c186cad4b26950" + integrity sha512-1dWqvFu6eo2KsXFDqRF28JFwrdzpc0k+GwpIqv7vF2kHarsMxnLnT/akhjbKzs+xlRTNFvqdKhEQxjdq2nSD1Q== dependencies: "@snyk/cli-interface" "2.9.1" - "@snyk/java-call-graph-builder" "1.13.2" + "@snyk/java-call-graph-builder" "1.16.5" debug "^4.1.1" + glob "^7.1.6" needle "^2.5.0" tmp "^0.1.0" tslib "1.11.1" -snyk-nodejs-lockfile-parser@1.28.1: - version "1.28.1" - resolved "https://registry.yarnpkg.com/snyk-nodejs-lockfile-parser/-/snyk-nodejs-lockfile-parser-1.28.1.tgz#9eda1354bbca1fc881a4e63a1e1042f80c37bff2" - integrity sha512-0zbmtidYLI2ia/DQD4rZm2YKrhfHLvHlVBdF2cMAGPwhOoKW5ovG9eBO4wNQdvjxNi7b4VeUyAj8SfuhjDraDQ== +snyk-nodejs-lockfile-parser@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/snyk-nodejs-lockfile-parser/-/snyk-nodejs-lockfile-parser-1.30.1.tgz#5d54180ae818ddbe8c2b55329528c4d68e390235" + integrity sha512-QyhE4pmy7GI7fQrVmZ+qrQB8GGSbxN7OoYueS4BEP9nDxIyH4dJAz8dME5zOUeUxh3frcgBWoWgZoSzE4VOYpg== dependencies: "@yarnpkg/lockfile" "^1.1.0" event-loop-spinner "^2.0.0" @@ -5987,16 +6048,15 @@ snyk-nodejs-lockfile-parser@1.28.1: lodash.set "^4.3.2" lodash.topairs "^4.3.0" p-map "2.1.0" - snyk-config "^3.0.0" - source-map-support "^0.5.7" + snyk-config "^4.0.0-rc.2" tslib "^1.9.3" - uuid "^3.3.2" + uuid "^8.3.0" yaml "^1.9.2" -snyk-nuget-plugin@1.19.3: - version "1.19.3" - resolved "https://registry.yarnpkg.com/snyk-nuget-plugin/-/snyk-nuget-plugin-1.19.3.tgz#5b4d9a5a61a543810c98bd4e67b9f6b1d95e3c3a" - integrity sha512-KwKoMumwcXVz/DQH80ifXfX7CTnm29bmHJ2fczjCGohxLGb4EKBGQtA3t7K98O7lTISQGgXDxnWIaM9ZXkxPdw== +snyk-nuget-plugin@1.19.4: + version "1.19.4" + resolved "https://registry.yarnpkg.com/snyk-nuget-plugin/-/snyk-nuget-plugin-1.19.4.tgz#cd1163a29f8002d54a965eab9e256345c97d4174" + integrity sha512-6BvLJc7gpNdfPJSnvpmTL4BrbaOVbXh/9q1FNMs5OVp8NbnZ3l97iM+bpQXWTJHOa3BJBZz7iEg+3suH4AWoWw== dependencies: debug "^4.1.1" dotnet-deps-parser "5.0.0" @@ -6022,6 +6082,17 @@ snyk-php-plugin@1.9.2: "@snyk/composer-lockfile-parser" "^1.4.1" tslib "1.11.1" +snyk-poetry-lockfile-parser@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/snyk-poetry-lockfile-parser/-/snyk-poetry-lockfile-parser-1.1.1.tgz#3f062953802916f6ae1767ec13dd1892fff0541e" + integrity sha512-G3LX27V2KUsKObwVN4vDDjrYr5BERad9pXHAf+SST5+vZsdPUUZjd1ZUIrHgCv7IQhwq+7mZrtqedY5x7+LIGA== + dependencies: + "@snyk/cli-interface" "^2.9.2" + "@snyk/dep-graph" "^1.19.5" + debug "^4.2.0" + toml "^3.0.0" + tslib "^2.0.0" + snyk-policy@1.14.1: version "1.14.1" resolved "https://registry.yarnpkg.com/snyk-policy/-/snyk-policy-1.14.1.tgz#4e48ea993573aca18e8d883b8c62171b9d35a3e0" @@ -6037,12 +6108,13 @@ snyk-policy@1.14.1: snyk-try-require "^1.3.1" then-fs "^2.0.0" -snyk-python-plugin@1.17.1: - version "1.17.1" - resolved "https://registry.yarnpkg.com/snyk-python-plugin/-/snyk-python-plugin-1.17.1.tgz#303ec2885ef748634d89f22f3099ef1febdc3325" - integrity sha512-KKklat9Hfbj4hw2y63LRhgmziYzmyRt+cSuzN5KDmBSAGYck0EAoPDtNpJXjrIs1kPNz28EXnE6NDnadXnOjiQ== +snyk-python-plugin@1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/snyk-python-plugin/-/snyk-python-plugin-1.19.1.tgz#91febcd260094a9d900bc54bf200aa0c2632613a" + integrity sha512-JoOUHnA76L3pekCblSuE9jQ9CuA5jt+GqXpsLQbEIZ0FQQTBa+0F7vfolg3Q7+s1it4ZdtgSbSWrlxCngIJt8g== dependencies: "@snyk/cli-interface" "^2.0.3" + snyk-poetry-lockfile-parser "^1.1.1" tmp "0.0.33" snyk-resolve-deps@4.4.0: @@ -6104,10 +6176,10 @@ snyk-try-require@1.3.1, snyk-try-require@^1.1.1, snyk-try-require@^1.3.1: lru-cache "^4.0.0" then-fs "^2.0.0" -snyk@^1.398.1: - version "1.398.1" - resolved "https://registry.yarnpkg.com/snyk/-/snyk-1.398.1.tgz#19aec8dfffa60e7412e6309117e96b2cfa960355" - integrity sha512-jH24ztdJY8DQlqkd1z8n/JutdOqHtTPccCynM2hfOedW20yAp9c108LFjXvqBEk/EH3YyNmWzyLkkHOySeDkwQ== +snyk@^1.425.4: + version "1.431.1" + resolved "https://registry.yarnpkg.com/snyk/-/snyk-1.431.1.tgz#1e360dae1b63d83f74fe90979f7b9a0fb1607aa7" + integrity sha512-OW48lG89ffLsSZPHwsjfdqQcu3XG6aRQOkwASPCgTAGcVcnXzS9XHB89h0gLsDzk0fZRskEVgYpvXdh4RFjNqA== dependencies: "@snyk/cli-interface" "2.9.2" "@snyk/dep-graph" "1.19.4" @@ -6120,28 +6192,28 @@ snyk@^1.398.1: configstore "^5.0.1" debug "^4.1.1" diff "^4.0.1" - glob "^7.1.3" graphlib "^2.1.8" inquirer "^7.3.3" lodash "^4.17.20" + micromatch "4.0.2" needle "2.5.0" open "^7.0.3" os-name "^3.0.0" proxy-agent "^3.1.1" proxy-from-env "^1.0.0" semver "^6.0.0" - snyk-config "3.1.1" - snyk-cpp-plugin "1.5.0" - snyk-docker-plugin "3.21.0" + snyk-config "4.0.0-rc.2" + snyk-cpp-plugin "2.2.1" + snyk-docker-plugin "4.12.0" snyk-go-plugin "1.16.2" - snyk-gradle-plugin "3.6.3" + snyk-gradle-plugin "3.10.2" snyk-module "3.1.0" - snyk-mvn-plugin "2.19.4" - snyk-nodejs-lockfile-parser "1.28.1" - snyk-nuget-plugin "1.19.3" + snyk-mvn-plugin "2.23.4" + snyk-nodejs-lockfile-parser "1.30.1" + snyk-nuget-plugin "1.19.4" snyk-php-plugin "1.9.2" snyk-policy "1.14.1" - snyk-python-plugin "1.17.1" + snyk-python-plugin "1.19.1" snyk-resolve "1.0.1" snyk-resolve-deps "4.4.0" snyk-sbt-plugin "2.11.0" @@ -6760,6 +6832,13 @@ to-regex-range@^2.1.0: is-number "^3.0.0" repeat-string "^1.6.1" +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + to-regex@^3.0.1, to-regex@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" @@ -7023,6 +7102,11 @@ uuid@^8.2.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== +uuid@^8.3.0: + version "8.3.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31" + integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg== + validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -7147,11 +7231,6 @@ widest-line@^3.1.0: dependencies: string-width "^4.0.0" -window-size@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876" - integrity sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY= - windows-release@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.2.0.tgz#8122dad5afc303d833422380680a79cdfa91785f" @@ -7164,14 +7243,6 @@ word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -wrap-ansi@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" - integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrap-ansi@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" @@ -7241,11 +7312,6 @@ xtend@~4.0.1: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== -y18n@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" - integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= - y18n@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" @@ -7320,19 +7386,6 @@ yargs@^14.2: y18n "^4.0.0" yargs-parser "^15.0.0" -yargs@^3.19.0: - version "3.32.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995" - integrity sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU= - dependencies: - camelcase "^2.0.1" - cliui "^3.0.3" - decamelize "^1.1.1" - os-locale "^1.4.0" - string-width "^1.0.1" - window-size "^0.1.4" - y18n "^3.2.0" - yeast@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" From 0c1dc810fba8a2d8b025663698a4fbb6cc82f432 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Wed, 25 Nov 2020 12:10:30 +0530 Subject: [PATCH 37/97] fix: add semicolons to unicode end --- frappe/public/js/frappe/utils/common.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frappe/public/js/frappe/utils/common.js b/frappe/public/js/frappe/utils/common.js index 9ff4ade761..0a145b098b 100644 --- a/frappe/public/js/frappe/utils/common.js +++ b/frappe/public/js/frappe/utils/common.js @@ -234,11 +234,11 @@ frappe.utils.xss_sanitise = function (string, options) { strategies: ['html', 'js'] // use all strategies. } const HTML_ESCAPE_MAP = { - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/' + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/' }; const REGEX_SCRIPT = /)<[^<]*)*<\/script>/gi; // used in jQuery 1.7.2 src/ajax.js Line 14 options = Object.assign({ }, DEFAULT_OPTIONS, options); // don't deep copy, immutable beauty. From f4c7c7aeedb3976847505b0987d7f8af2850cd0a Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 25 Nov 2020 15:07:19 +0530 Subject: [PATCH 38/97] fix: set self.port instead of self.smtp_port --- frappe/email/smtp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py index f53b835757..391ce06c74 100644 --- a/frappe/email/smtp.py +++ b/frappe/email/smtp.py @@ -210,7 +210,7 @@ class SMTPServer: try: if self.use_ssl: if not self.port: - self.smtp_port = 465 + self.port = 465 self._sess = smtplib.SMTP_SSL((self.server or "").encode('utf-8'), cint(self.port) or None) From b3a8ecad632bf3f12bd588447fdb4acc97833c55 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 25 Nov 2020 17:34:44 +0530 Subject: [PATCH 39/97] refactor: don't encode server string --- frappe/email/smtp.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py index 391ce06c74..9ba81fa146 100644 --- a/frappe/email/smtp.py +++ b/frappe/email/smtp.py @@ -212,8 +212,7 @@ class SMTPServer: if not self.port: self.port = 465 - self._sess = smtplib.SMTP_SSL((self.server or "").encode('utf-8'), - cint(self.port) or None) + self._sess = smtplib.SMTP_SSL((self.server or ""), cint(self.port)) else: if self.use_tls and not self.port: self.port = 587 From 08c8e517b6d484c5ad9aa1848ace855fec4c4c15 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Wed, 25 Nov 2020 18:08:18 +0530 Subject: [PATCH 40/97] fix: git check flow --- frappe/utils/change_log.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/frappe/utils/change_log.py b/frappe/utils/change_log.py index 75421c43ea..148d03ae39 100644 --- a/frappe/utils/change_log.py +++ b/frappe/utils/change_log.py @@ -185,29 +185,38 @@ def parse_latest_non_beta_release(response): return None -def check_release_on_github(app): - from subprocess import CalledProcessError +def check_release_on_github(app: str): + """ + Check the latest release for a given Frappe application hosted on Github. + + Args: + app (str): The name of the Frappe application. + + Returns: + tuple(Version, str): The Version object of the latest release and the + organization name, if the application exists, otherwise None. + """ try: # Check if repo remote is on github - remote_url = subprocess.check_output("cd ../apps/{} && git ls-remote --get-url".format(app), shell=True).decode() - except CalledProcessError: - # Passing this since some apps may not have git initializaed in them + remote_url = subprocess.check_output("cd ../apps/{} && git ls-remote --get-url".format(app), shell=True) + except subprocess.CalledProcessError: + # Passing this since some apps may not have git initialized in them return if isinstance(remote_url, bytes): remote_url = remote_url.decode() - if "github.com" not in remote_url: + if "github" not in remote_url: + return + + if is_git_url(remote_url): return # Get latest version from github if 'https' not in remote_url: return - if is_git_url(remote_url): - return - org_name = remote_url.split('/')[3] r = requests.get('https://api.github.com/repos/{}/{}/releases'.format(org_name, app)) if r.ok: From 6d9a56e43cb2e80e74e781c93151b023e8256fd5 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 25 Nov 2020 18:51:50 +0530 Subject: [PATCH 41/97] feat: add tests --- frappe/email/test_smtp.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 frappe/email/test_smtp.py diff --git a/frappe/email/test_smtp.py b/frappe/email/test_smtp.py new file mode 100644 index 0000000000..869d708430 --- /dev/null +++ b/frappe/email/test_smtp.py @@ -0,0 +1,25 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# License: The MIT License + +import unittest +from frappe.email.smtp import SMTPServer + +class TestSMTP(unittest.TestCase): + def test_smtp_ssl_session(self): + for port in [None, 0, 465, "465"]: + make_server(port, 1, 0) + + def test_smtp_tls_session(self): + for port in [None, 0, 587, "587"]: + make_server(port, 0, 1) + + +def make_server(port, ssl, tls): + server = SMTPServer( + server = "smtp.gmail.com", + port = port, + use_ssl = ssl, + use_tls = tls + ) + + server.sess \ No newline at end of file From a459ce40ed05efbf682ec8c5df00e819b315c3ec Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Thu, 26 Nov 2020 05:41:01 +0200 Subject: [PATCH 42/97] fix(snyk): Security upgrade highlight.js from 9.18.1 to 9.18.2 (#11999) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-HIGHLIGHTJS-1045326 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 91d204bec5..d1a94d0e35 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "frappe-datatable": "^1.15.3", "frappe-gantt": "^0.5.0", "fuse.js": "^3.4.6", - "highlight.js": "^9.18.1", + "highlight.js": "^9.18.2", "js-sha256": "^0.9.0", "jsbarcode": "^3.9.0", "moment": "^2.20.1", diff --git a/yarn.lock b/yarn.lock index 459f4139b9..b30ca7de0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2702,10 +2702,10 @@ hex-color-regex@^1.1.0: resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== -highlight.js@^9.18.1: - version "9.18.1" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.1.tgz#ed21aa001fe6252bb10a3d76d47573c6539fe13c" - integrity sha512-OrVKYz70LHsnCgmbXctv/bfuvntIKDz177h0Co37DQ5jamGZLVmoCVMtjMtNZY3X9DrCcKfklHPNeA0uPZhSJg== +highlight.js@^9.18.2: + version "9.18.5" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.5.tgz#d18a359867f378c138d6819edfc2a8acd5f29825" + integrity sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA== homedir-polyfill@^1.0.1: version "1.0.3" From ce05cc15ed4a4816d145aaed664bd905a5688c4e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 26 Nov 2020 09:26:58 +0530 Subject: [PATCH 43/97] fix: text editor field type changes in grid row form (bp #11974) (#11978) Co-authored-by: Shivam Mishra Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- frappe/public/js/frappe/form/grid_row.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 827fbfdee6..ec9cee9c39 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -373,6 +373,7 @@ export default class GridRow { // no text editor in grid if (df.fieldtype=='Text Editor') { + df = Object.assign({}, df); df.fieldtype = 'Text'; } From 926d7e78fd75dcfc5713de7c684900d54d84a843 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 26 Nov 2020 15:33:55 +0530 Subject: [PATCH 44/97] fix: clear localstorage if quota exceeds --- frappe/public/js/frappe/model/model.js | 33 +++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 308d9bd5f8..1d302215dd 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -103,6 +103,31 @@ $.extend(frappe.model, { return docfield[0]; }, + get_from_localstorage: function(doctype) { + if (localStorage["_doctype:" + doctype]) { + return JSON.parse(localStorage["_doctype:" + doctype]); + } + }, + + set_in_localstorage: function(doctype, docs) { + try { + localStorage["_doctype:" + doctype] = JSON.stringify(docs); + } catch(e) { + // if quota is exceeded, clear local storage and set item + console.warn("localStorage quota exceeded, clearing doctype cache") + frappe.model.clear_local_storage(); + localStorage["_doctype:" + doctype] = JSON.stringify(docs); + } + }, + + clear_local_storage: function() { + for(var key in localStorage) { + if (key.startsWith("_doctype:")) { + localStorage.removeItem(key); + } + } + }, + with_doctype: function(doctype, callback, async) { if(locals.DocType[doctype]) { callback && callback(); @@ -110,13 +135,15 @@ $.extend(frappe.model, { let cached_timestamp = null; let cached_doc = null; - if(localStorage["_doctype:" + doctype]) { - let cached_docs = JSON.parse(localStorage["_doctype:" + doctype]); + let cached_docs = frappe.model.get_from_localstorage(doctype) + + if (cached_docs) { cached_doc = cached_docs.filter(doc => doc.name === doctype)[0]; if(cached_doc) { cached_timestamp = cached_doc.modified; } } + return frappe.call({ method:'frappe.desk.form.load.getdoctype', type: "GET", @@ -134,7 +161,7 @@ $.extend(frappe.model, { if(r.message=="use_cache") { frappe.model.sync(cached_doc); } else { - localStorage["_doctype:" + doctype] = JSON.stringify(r.docs); + frappe.model.set_in_localstorage(doctype, r.docs) } frappe.model.init_doctype(doctype); From 8192db7382a02fd4334c227ad15339c10f7bede5 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 26 Nov 2020 16:22:40 +0530 Subject: [PATCH 45/97] chore: remove stray console --- frappe/public/js/frappe/desk.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index c8ed29fb76..5fa7a9dbcb 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -148,7 +148,6 @@ frappe.Application = Class.extend({ user: frappe.session.user }, callback: function(r) { - console.log(r); if(r.message.show_alert){ frappe.show_alert({ indicator: 'red', From 0d4f116b3ef07c83190db428b2868ab9c819f493 Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Thu, 26 Nov 2020 13:15:09 +0000 Subject: [PATCH 46/97] fix: Import ABC from collections.abc for Python 3.9 compatibility. --- frappe/chat/util/util.py | 4 ++-- frappe/utils/response.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/chat/util/util.py b/frappe/chat/util/util.py index 5aa80a85ae..1c3de3bbf5 100644 --- a/frappe/chat/util/util.py +++ b/frappe/chat/util/util.py @@ -7,7 +7,7 @@ import requests import six # imports - standard imports -from collections import Sequence, MutableSequence, Mapping, MutableMapping +from collections.abc import Sequence, MutableSequence, Mapping, MutableMapping if six.PY2: from urlparse import urlparse # PY2 else: @@ -113,4 +113,4 @@ def get_emojis(): emojis = resp.json() redis.hset('frappe_emojis', 'emojis', emojis) - return dictify(emojis) \ No newline at end of file + return dictify(emojis) diff --git a/frappe/utils/response.py b/frappe/utils/response.py index 20b5ea5678..c35ebc751e 100644 --- a/frappe/utils/response.py +++ b/frappe/utils/response.py @@ -123,7 +123,7 @@ def make_logs(response = None): def json_handler(obj): """serialize non-serializable data for json""" # serialize date - import collections + import collections.abc if isinstance(obj, (datetime.date, datetime.timedelta, datetime.datetime)): return text_type(obj) @@ -138,7 +138,7 @@ def json_handler(obj): doc = obj.as_dict(no_nulls=True) return doc - elif isinstance(obj, collections.Iterable): + elif isinstance(obj, collections.abc.Iterable): return list(obj) elif type(obj)==type or isinstance(obj, Exception): From 3a146580a8a1cafdb74c2af8e5e9522017a0069c Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Thu, 26 Nov 2020 13:30:07 +0000 Subject: [PATCH 47/97] fix: Use html.unescape for Python 3.9 compatibility. --- frappe/utils/global_search.py | 6 ++---- frappe/utils/html_utils.py | 5 ++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/frappe/utils/global_search.py b/frappe/utils/global_search.py index e945039d0d..f605c3bf66 100644 --- a/frappe/utils/global_search.py +++ b/frappe/utils/global_search.py @@ -10,6 +10,7 @@ import json import os from bs4 import BeautifulSoup from frappe.utils import cint, strip_html_tags +from frappe.utils.html_utils import unescape_html from frappe.model.base_document import get_controller from six import text_type @@ -345,11 +346,8 @@ def get_formatted_value(value, field): :return: """ - from six.moves.html_parser import HTMLParser - if getattr(field, 'fieldtype', None) in ["Text", "Text Editor"]: - h = HTMLParser() - value = h.unescape(frappe.safe_decode(value)) + value = unescape_html(frappe.safe_decode(value)) value = (re.subn(r'<[\s]*(script|style).*?(?s)', '', text_type(value))[0]) value = ' '.join(value.split()) return field.label + " : " + strip_html_tags(text_type(value)) diff --git a/frappe/utils/html_utils.py b/frappe/utils/html_utils.py index 6fdd383eb9..bccdbd9441 100644 --- a/frappe/utils/html_utils.py +++ b/frappe/utils/html_utils.py @@ -106,9 +106,8 @@ def get_icon_html(icon, small=False): return "".format(icon=icon) def unescape_html(value): - from six.moves.html_parser import HTMLParser - h = HTMLParser() - return h.unescape(value) + from html import unescape + return unescape(value) # adapted from https://raw.githubusercontent.com/html5lib/html5lib-python/4aa79f113e7486c7ec5d15a6e1777bfe546d3259/html5lib/sanitizer.py acceptable_elements = [ From 8d3894e8a537540899c0412e2a48724014b6c304 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 27 Nov 2020 11:01:49 +0530 Subject: [PATCH 48/97] fix: Validate Python syntax on saves --- frappe/core/doctype/server_script/server_script.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index 839b784651..ded397d5e3 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -4,6 +4,8 @@ from __future__ import unicode_literals +import ast + import frappe from frappe.model.document import Document from frappe.utils.safe_exec import safe_exec @@ -11,9 +13,9 @@ from frappe import _ class ServerScript(Document): - @staticmethod - def validate(): + def validate(self): frappe.only_for('Script Manager', True) + ast.parse(self.script) @staticmethod def on_update(): From 5babacac3eac0e4dd2c7734a779b59641b96c94b Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 27 Nov 2020 11:02:37 +0530 Subject: [PATCH 49/97] fix: Show function not available in namespace instead of nothing Prior to this, frappe._dict was being used to inject functions to the server script namespaces. This meant unimplemented methods returned None and we'd get a NoneType not callable error --- frappe/utils/safe_exec.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index fee6b404ac..50893330be 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -13,7 +13,17 @@ from frappe.www.printview import get_visible_columns import frappe.exceptions import frappe.integrations.utils -class ServerScriptNotEnabled(frappe.PermissionError): pass +class ServerScriptNotEnabled(frappe.PermissionError): + pass + +class NamespaceDict(frappe._dict): + """Raise AttributeError if function not found in namespace""" + def __getattr__(self, key): + ret = self.get(key) + if (not ret and key.startswith("__")) or (key not in self): + raise AttributeError(f"module has no attribute '{key}'") + return ret + def safe_exec(script, _globals=None, _locals=None): # script reports must be enabled via site_config.json @@ -46,13 +56,13 @@ def get_safe_globals(): user = getattr(frappe.local, "session", None) and frappe.local.session.user or "Guest" - out = frappe._dict( + out = NamespaceDict( # make available limited methods of frappe json=json, dict=dict, log=frappe.log, _dict=frappe._dict, - frappe=frappe._dict( + frappe=NamespaceDict( flags=frappe._dict(), format=frappe.format_value, format_value=frappe.format_value, @@ -112,7 +122,7 @@ def get_safe_globals(): out.get_visible_columns = get_visible_columns out.frappe.date_format = date_format out.frappe.time_format = time_format - out.frappe.db = frappe._dict( + out.frappe.db = NamespaceDict( get_list = frappe.get_list, get_all = frappe.get_all, get_value = frappe.db.get_value, From da0fa439bc4e96400cbf0b5dd0406d6951693712 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 27 Nov 2020 11:40:53 +0530 Subject: [PATCH 50/97] style: Optimize imports, fixed flake8 issues --- frappe/chat/util/util.py | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/frappe/chat/util/util.py b/frappe/chat/util/util.py index 1c3de3bbf5..82df6dd127 100644 --- a/frappe/chat/util/util.py +++ b/frappe/chat/util/util.py @@ -1,27 +1,21 @@ from __future__ import unicode_literals +# imports - standard imports +import json +from collections.abc import MutableMapping, MutableSequence, Sequence + # imports - third-party imports import requests - -# imports - compatibility imports -import six - -# imports - standard imports -from collections.abc import Sequence, MutableSequence, Mapping, MutableMapping -if six.PY2: - from urlparse import urlparse # PY2 -else: - from urllib.parse import urlparse # PY3 -import json +from urllib.parse import urlparse # imports - module imports -from frappe.model.document import Document -from frappe.exceptions import DuplicateEntryError -from frappe import _dict import frappe +from frappe.exceptions import DuplicateEntryError +from frappe.model.document import Document session = frappe.session + def get_user_doc(user = None): if isinstance(user, Document): return user @@ -38,12 +32,12 @@ def squashify(what): return what def safe_json_loads(*args): - results = [ ] + results = [] for arg in args: try: arg = json.loads(arg) - except Exception as e: + except Exception: pass results.append(arg) @@ -81,7 +75,7 @@ def dictify(arg): for i, a in enumerate(arg): arg[i] = dictify(a) elif isinstance(arg, MutableMapping): - arg = _dict(arg) + arg = frappe._dict(arg) return arg From 58ed57e8f53bf24c438ae7e91909ccdf9f0bad02 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 27 Nov 2020 15:01:46 +0530 Subject: [PATCH 51/97] fix: Add postgreSQL support for rename_doc queries --- frappe/core/doctype/doctype/doctype.py | 5 ++++- frappe/model/rename_doc.py | 24 ++++++++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 8a9c130fbe..f06f2017ae 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -392,7 +392,10 @@ class DocType(Document): frappe.db.sql("""update tabSingles set value=%s where doctype=%s and field='name' and value = %s""", (new, new, old)) else: - frappe.db.sql("rename table `tab%s` to `tab%s`" % (old, new)) + frappe.db.multisql({ + "mariadb": f"RENAME TABLE `tab{old}` TO `tab{new}`", + "postgres": f"ALTER TABLE `tab{old}` RENAME TO `tab{new}`" + }) def rename_files_and_folders(self, old, new): # move files diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 789a7f51cf..33f6fefb7d 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -315,8 +315,7 @@ def get_link_fields(doctype): def update_options_for_fieldtype(fieldtype, old, new): if frappe.conf.developer_mode: - for name in frappe.db.sql_list("""select parent from - tabDocField where options=%s""", old): + for name in frappe.get_all("DocField", filters={"options": old}, pluck="parent"): doctype = frappe.get_doc("DocType", name) save = False for f in doctype.fields: @@ -422,20 +421,21 @@ def update_parenttype_values(old, new): child_doctypes += custom_child_doctypes fields = [d['fieldname'] for d in child_doctypes] - property_setter_child_doctypes = frappe.db.sql("""\ - select value as options from `tabProperty Setter` - where doc_type=%s and property='options' and - field_name in ("%s")""" % ('%s', '", "'.join(fields)), - (new,)) + property_setter_child_doctypes = frappe.get_all( + "Property Setter", + filters={ + "doc_type": new, + "property": "options", + "field_name": ("in", fields) + }, + pluck="value" + ) + child_doctypes = list(d['options'] for d in child_doctypes) child_doctypes += property_setter_child_doctypes - child_doctypes = (d['options'] for d in child_doctypes) for doctype in child_doctypes: - frappe.db.sql("""\ - update `tab%s` set parenttype=%s - where parenttype=%s""" % (doctype, '%s', '%s'), - (new, old)) + frappe.db.sql(f"update `tab{doctype}` set parenttype=%s where parenttype=%s", (new, old)) def rename_dynamic_links(doctype, old, new): for df in get_dynamic_link_map().get(doctype, []): From 104bc1b16708af1417a5488b779ac931db4827e5 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 27 Nov 2020 15:09:23 +0530 Subject: [PATCH 52/97] chore: Remove dead code --- frappe/model/rename_doc.py | 60 -------------------------------------- 1 file changed, 60 deletions(-) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 33f6fefb7d..15d634dade 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -42,7 +42,6 @@ def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=F force = cint(force) merge = cint(merge) - meta = frappe.get_meta(doctype) # call before_rename @@ -489,62 +488,3 @@ def bulk_rename(doctype, rows=None, via_console = False): if not via_console: return rename_log - -def update_linked_doctypes(doctype, docname, linked_to, value, ignore_doctypes=None): - """ - linked_doctype_info_list = list formed by get_fetch_fields() function - docname = Master DocType's name in which modification are made - value = Value for the field thats set in other DocType's by fetching from Master DocType - """ - linked_doctype_info_list = get_fetch_fields(doctype, linked_to, ignore_doctypes) - - for d in linked_doctype_info_list: - frappe.db.sql(""" - update - `tab{doctype}` - set - {linked_to_fieldname} = "{value}" - where - {master_fieldname} = {docname} - and {linked_to_fieldname} != "{value}" - """.format( - doctype = d['doctype'], - linked_to_fieldname = d['linked_to_fieldname'], - value = value, - master_fieldname = d['master_fieldname'], - docname = frappe.db.escape(docname) - )) - -def get_fetch_fields(doctype, linked_to, ignore_doctypes=None): - """ - doctype = Master DocType in which the changes are being made - linked_to = DocType name of the field thats being updated in Master - - This function fetches list of all DocType where both doctype and linked_to is found - as link fields. - Forms a list of dict in the form - - [{doctype: , master_fieldname: , linked_to_fieldname: ] - where - doctype = DocType where changes need to be made - master_fieldname = Fieldname where options = doctype - linked_to_fieldname = Fieldname where options = linked_to - """ - - master_list = get_link_fields(doctype) - linked_to_list = get_link_fields(linked_to) - out = [] - - from itertools import product - product_list = product(master_list, linked_to_list) - - for d in product_list: - linked_doctype_info = frappe._dict() - if d[0]['parent'] == d[1]['parent'] \ - and (not ignore_doctypes or d[0]['parent'] not in ignore_doctypes) \ - and not d[1]['issingle']: - linked_doctype_info['doctype'] = d[0]['parent'] - linked_doctype_info['master_fieldname'] = d[0]['fieldname'] - linked_doctype_info['linked_to_fieldname'] = d[1]['fieldname'] - out.append(linked_doctype_info) - - return out From 7bba0b7da933950accbf654325bf668fd1bcf4ab Mon Sep 17 00:00:00 2001 From: gavin Date: Fri, 27 Nov 2020 15:56:28 +0530 Subject: [PATCH 53/97] chore(GitHub): Add issue template config --- .github/ISSUE_TEMPLATE/config.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..26bb7ab280 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Community Forum + url: https://discuss.erpnext.com/ + about: For general QnA, discussions and community help. From 9a84a7eb45c529d882f6b541db0916232468d783 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Fri, 27 Nov 2020 17:27:44 +0530 Subject: [PATCH 54/97] feat: use giturlparse to parse Git URLs --- frappe/utils/change_log.py | 32 ++++++++++++++++++++------------ frappe/utils/data.py | 6 ------ requirements.txt | 1 + 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/frappe/utils/change_log.py b/frappe/utils/change_log.py index 148d03ae39..9607c89784 100644 --- a/frappe/utils/change_log.py +++ b/frappe/utils/change_log.py @@ -5,13 +5,14 @@ import json import os import subprocess # nosec -import frappe import requests -from frappe import _, safe_decode -from frappe.utils import cstr, is_git_url from semantic_version import Version from six.moves import range +import frappe +from frappe import _, safe_decode +from frappe.utils import cstr + def get_change_log(user=None): if not user: user = frappe.session.user @@ -193,10 +194,13 @@ def check_release_on_github(app: str): app (str): The name of the Frappe application. Returns: - tuple(Version, str): The Version object of the latest release and the + tuple(Version, str): The semantic version object of the latest release and the organization name, if the application exists, otherwise None. """ + from giturlparse import parse + from giturlparse.parser import ParserError + try: # Check if repo remote is on github remote_url = subprocess.check_output("cd ../apps/{} && git ls-remote --get-url".format(app), shell=True) @@ -207,22 +211,26 @@ def check_release_on_github(app: str): if isinstance(remote_url, bytes): remote_url = remote_url.decode() - if "github" not in remote_url: + try: + parsed_url = parse(remote_url) + except ParserError: + # Invalid URL return - if is_git_url(remote_url): + # Get latest version from Github + if parsed_url.protocol == "http": + return + if parsed_url.resource != "github.com": return - # Get latest version from github - if 'https' not in remote_url: - return + owner = parsed_url.owner + repo = parsed_url.name - org_name = remote_url.split('/')[3] - r = requests.get('https://api.github.com/repos/{}/{}/releases'.format(org_name, app)) + r = requests.get('https://api.github.com/repos/{}/{}/releases'.format(owner, repo)) if r.ok: latest_non_beta_release = parse_latest_non_beta_release(r.json()) if latest_non_beta_release: - return Version(latest_non_beta_release), org_name + return Version(latest_non_beta_release), owner def add_message_to_redis(update_json): diff --git a/frappe/utils/data.py b/frappe/utils/data.py index cb61355a29..34659e1cac 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -1397,9 +1397,3 @@ def validate_json_string(string): json.loads(string) except (TypeError, ValueError): raise frappe.ValidationError - - -def is_git_url(url): - # modified to allow without the tailing .git from https://github.com/jonschlinkert/is-git-url.git - pattern = r"(?:git|ssh|https?|git@[-\w.]+):(\/\/)?(.*?)(\.git)?(\/?|\#[-\d\w._]+?)$" - return bool(re.match(pattern, url)) diff --git a/requirements.txt b/requirements.txt index de9e675a67..59c4a9dbf8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,7 @@ Faker==2.0.4 future==0.18.2 gitdb2==2.0.6;python_version<'3.4' GitPython==2.1.15 +git-url-parse==1.2.2 google-api-python-client==1.9.3 google-auth-httplib2==0.0.3 google-auth-oauthlib==0.4.1 From e3b09e2a2aa8d88760ea91cc51375ca485ef34ce Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 27 Nov 2020 20:15:08 +0530 Subject: [PATCH 55/97] fix: Add rename_doc utils for external API usages * The previously deleted code was being used in ERPNext's Customer module. * This will be moved into frappe.model.utils.rename_doc for the time and completely removed in time. --- frappe/model/rename_doc.py | 30 +++++++++++++++++ frappe/model/utils/rename_doc.py | 58 ++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 frappe/model/utils/rename_doc.py diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 15d634dade..a188705fcf 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -2,6 +2,7 @@ # MIT License. See license.txt from __future__ import unicode_literals, print_function +from click.termui import secho import frappe from frappe import _, bold from frappe.utils import cint @@ -488,3 +489,32 @@ def bulk_rename(doctype, rows=None, via_console = False): if not via_console: return rename_log + +def update_linked_doctypes(doctype, docname, linked_to, value, ignore_doctypes=None): + from frappe.model.utils.rename_doc import update_linked_doctypes + show_deprecation_warning("update_linked_doctypes") + + return update_linked_doctypes( + doctype=doctype, + docname=docname, + linked_to=linked_to, + value=value, + ignore_doctypes=ignore_doctypes, + ) + + +def get_fetch_fields(doctype, linked_to, ignore_doctypes=None): + from frappe.model.utils.rename_doc import get_fetch_fields + show_deprecation_warning("get_fetch_fields") + + return get_fetch_fields( + doctype=doctype, linked_to=linked_to, ignore_doctypes=ignore_doctypes + ) + +def show_deprecation_warning(funct): + from click import secho + message = ( + f"Function frappe.model.rename_doc.{funct} has been deprecated and " + "moved to the frappe.model.utils.rename_doc" + ) + secho(message, fg="yellow") diff --git a/frappe/model/utils/rename_doc.py b/frappe/model/utils/rename_doc.py new file mode 100644 index 0000000000..bf71d36a42 --- /dev/null +++ b/frappe/model/utils/rename_doc.py @@ -0,0 +1,58 @@ +from itertools import product + +import frappe +from frappe.model.rename_doc import get_link_fields + + +def update_linked_doctypes(doctype, docname, linked_to, value, ignore_doctypes=None): + """ + linked_doctype_info_list = list formed by get_fetch_fields() function + docname = Master DocType's name in which modification are made + value = Value for the field thats set in other DocType's by fetching from Master DocType + """ + linked_doctype_info_list = get_fetch_fields(doctype, linked_to, ignore_doctypes) + + for d in linked_doctype_info_list: + frappe.db.set_value( + d.doctype, + { + d.master_fieldname : docname, + d.linked_to_fieldname : ("!=", value), + }, + d.linked_to_fieldname, + value, + ) + + +def get_fetch_fields(doctype, linked_to, ignore_doctypes=None): + """ + doctype = Master DocType in which the changes are being made + linked_to = DocType name of the field thats being updated in Master + This function fetches list of all DocType where both doctype and linked_to is found + as link fields. + Forms a list of dict in the form - + [{doctype: , master_fieldname: , linked_to_fieldname: ] + where + doctype = DocType where changes need to be made + master_fieldname = Fieldname where options = doctype + linked_to_fieldname = Fieldname where options = linked_to + """ + + out = [] + master_list = get_link_fields(doctype) + linked_to_list = get_link_fields(linked_to) + product_list = product(master_list, linked_to_list) + + for d in product_list: + linked_doctype_info = frappe._dict() + if ( + d[0]["parent"] == d[1]["parent"] + and (not ignore_doctypes or d[0]["parent"] not in ignore_doctypes) + and not d[1]["issingle"] + ): + linked_doctype_info.doctype = d[0]["parent"] + linked_doctype_info.master_fieldname = d[0]["fieldname"] + linked_doctype_info.linked_to_fieldname = d[1]["fieldname"] + out.append(linked_doctype_info) + + return out From a0cb5930198ea1df0c8c6d1aaefd5d557e02b973 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 27 Nov 2020 20:33:10 +0530 Subject: [PATCH 56/97] style: Sort and remove unused imports --- frappe/model/rename_doc.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index a188705fcf..35fbf94dc6 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -1,15 +1,15 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt -from __future__ import unicode_literals, print_function -from click.termui import secho +from __future__ import print_function, unicode_literals + import frappe from frappe import _, bold -from frappe.utils import cint -from frappe.model.naming import validate_name from frappe.model.dynamic_links import get_dynamic_link_map -from frappe.utils.password import rename_password +from frappe.model.naming import validate_name from frappe.model.utils.user_settings import sync_user_settings, update_user_settings_data +from frappe.utils import cint +from frappe.utils.password import rename_password @frappe.whitelist() From dd8e2114ab5ef913418ea747a15e757350588ec7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 28 Nov 2020 18:53:59 +0530 Subject: [PATCH 57/97] fix: Trim long names in website navbar by adding ellipsis (bp #11730) (#12009) Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- frappe/public/scss/website.scss | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frappe/public/scss/website.scss b/frappe/public/scss/website.scss index e1b7d0a827..5291834aab 100644 --- a/frappe/public/scss/website.scss +++ b/frappe/public/scss/website.scss @@ -244,9 +244,18 @@ h5.modal-title { white-space: nowrap; text-overflow: ellipsis; } + .about-section { padding-top: 1rem; } + .about-footer { padding-top: 1rem; -} \ No newline at end of file +} + +.logged-in > .nav-link { + max-width: 200px; + @extend .ellipsis; + max-width: 100%; + vertical-align: middle; +} From bd2e3530cdf6491d1ab43fd1c4326e7d9275b1ab Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Mon, 30 Nov 2020 11:59:16 +0530 Subject: [PATCH 58/97] fix: strip exif data from image files before uploading --- frappe/core/doctype/file/file.py | 12 +++++++++--- frappe/utils/image.py | 25 ++++++++++++++++++++++++- requirements.txt | 3 ++- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 473d810a9f..1642e857c5 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -30,7 +30,7 @@ import frappe from frappe import _, conf from frappe.model.document import Document from frappe.utils import call_hook_method, cint, cstr, encode, get_files_path, get_hook_method, random_string, strip - +from frappe.utils.image import strip_exif_data class MaxFileSizeReachedError(frappe.ValidationError): pass @@ -456,6 +456,7 @@ class File(Document): def save_file(self, content=None, decode=False, ignore_existing_file_check=False): file_exists = False self.content = content + if decode: if isinstance(content, text_type): self.content = content.encode("utf-8") @@ -466,10 +467,15 @@ class File(Document): if not self.is_private: self.is_private = 0 + + self.content_type = mimetypes.guess_type(self.file_name)[0] + + if self.content_type and "image" in self.content_type: + self.content = strip_exif_data(self.content, self.content_type) + self.file_size = self.check_max_file_size() self.content_hash = get_content_hash(self.content) - self.content_type = mimetypes.guess_type(self.file_name)[0] - + duplicate_file = None # check if a file exists with the same content hash and is also in the same folder (public or private) diff --git a/frappe/utils/image.py b/frappe/utils/image.py index 1eada5acca..3d3d98a28c 100644 --- a/frappe/utils/image.py +++ b/frappe/utils/image.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals, print_function import os def resize_images(path, maxdim=700): - import Image + from PIL import Image size = (maxdim, maxdim) for basepath, folders, files in os.walk(path): for fname in files: @@ -17,3 +17,26 @@ def resize_images(path, maxdim=700): im.save(os.path.join(basepath, fname)) print("resized {0}".format(os.path.join(basepath, fname))) + +def strip_exif_data(content, content_type): + """ Strips exif from image files which support it. + + Works by creating a new Image object which ignores exif by + default and then extracts the binary data back into content. + + Returns: stripped image content + """ + + from PIL import Image + import io + + original_image = Image.open(io.BytesIO(content)) + output = io.BytesIO() + + new_image = Image.new(original_image.mode, original_image.size) + new_image.putdata(list(original_image.getdata())) + new_image.save(output, format=content_type.split('/')[-1].upper()) + + content = output.getvalue() + + return content \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index de9e675a67..92ea0e5572 100644 --- a/requirements.txt +++ b/requirements.txt @@ -73,4 +73,5 @@ pycryptodome==3.9.8 paytmchecksum==1.7.0 wrapt==1.10.11 razorpay==1.2.0 -rsa>=4.1 # not directly required, pinned by Snyk to avoid a vulnerability \ No newline at end of file +rsa>=4.1 # not directly required, pinned by Snyk to avoid a vulnerability +pillow==8.0.1 \ No newline at end of file From d276a2d8e0b826f18b13ae6183b56c787a2ab242 Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Mon, 30 Nov 2020 12:26:09 +0530 Subject: [PATCH 59/97] fix: remove extra pillow entry in requirements --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 92ea0e5572..de9e675a67 100644 --- a/requirements.txt +++ b/requirements.txt @@ -73,5 +73,4 @@ pycryptodome==3.9.8 paytmchecksum==1.7.0 wrapt==1.10.11 razorpay==1.2.0 -rsa>=4.1 # not directly required, pinned by Snyk to avoid a vulnerability -pillow==8.0.1 \ No newline at end of file +rsa>=4.1 # not directly required, pinned by Snyk to avoid a vulnerability \ No newline at end of file From 51928cccb0ff573ac314255cd29522c82a388c9f Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 30 Nov 2020 12:38:38 +0530 Subject: [PATCH 60/97] chore: bump frappe-charts to 1.5.4 --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index b30ca7de0c..26797675c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2300,9 +2300,9 @@ fragment-cache@^0.2.1: map-cache "^0.2.2" frappe-charts@^1.5.1: - version "1.5.3" - resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-1.5.3.tgz#0dcb86ea774fa7a3e1b79221e958d29701dfff04" - integrity sha512-VS5XVxek41ea8mVzetyFF3avNefiwGDcDSDJuHrZyJXgbqiTSXLoqlPFoMqTzuzRm1g+o6TXs+A7wLtVp3Vt0g== + version "1.5.4" + resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-1.5.4.tgz#5870f77ac6ffc8ea4dab32adda1d4e5e4fbda64b" + integrity sha512-hBr7cRLmsCC5VBj/HwKOCgdwyXnkeAO5CAvOd5H4IYFbk84VD9jOjx9fSaqAE0MygVVbY1nCN+5nb08WThW4Xw== frappe-datatable@^1.15.3: version "1.15.3" From 64e80d7aa3025290e61d15795bf9d53b0c8c7bd0 Mon Sep 17 00:00:00 2001 From: Leela vadlamudi Date: Mon, 30 Nov 2020 14:12:43 +0530 Subject: [PATCH 61/97] refactor: Remove telephony related code (#12017) --- frappe/public/js/frappe/form/controls/data.js | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js index 4db2553bd1..401de2ed5d 100644 --- a/frappe/public/js/frappe/form/controls/data.js +++ b/frappe/public/js/frappe/form/controls/data.js @@ -22,27 +22,9 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({ this.has_input = true; this.bind_change_event(); this.setup_autoname_check(); - if (this.df.options == 'Phone') { - this.setup_phone(); - } // somehow this event does not bubble up to document // after v7, if you can debug, remove this }, - setup_phone() { - if (frappe.phone_call.handler) { - this.$wrapper.find('.control-input') - .append(` - - - - - `) - .find('.phone-btn') - .click(() => { - frappe.phone_call.handler(this.get_value(), this.frm); - }); - } - }, setup_autoname_check: function() { if (!this.df.parent) return; this.meta = frappe.get_meta(this.df.parent); From 9808c868d70ce334dadda790ca55b3819d557ca7 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 30 Nov 2020 14:14:19 +0530 Subject: [PATCH 62/97] feat: allow html in email templates --- .../email_template/email_template.json | 22 +++++++++++++-- .../doctype/email_template/email_template.py | 27 ++++++++++++++++--- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/frappe/email/doctype/email_template/email_template.json b/frappe/email/doctype/email_template/email_template.json index 0d0922f16f..dc73acacc1 100644 --- a/frappe/email/doctype/email_template/email_template.json +++ b/frappe/email/doctype/email_template/email_template.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "Prompt", @@ -8,6 +9,8 @@ "engine": "InnoDB", "field_order": [ "subject", + "use_html", + "response_html", "response", "owner", "section_break_4", @@ -22,11 +25,12 @@ "reqd": 1 }, { + "depends_on": "eval:!doc.use_html", "fieldname": "response", "fieldtype": "Text Editor", "in_list_view": 1, "label": "Response", - "reqd": 1 + "mandatory_depends_on": "eval:!doc.use_html" }, { "default": "user", @@ -45,10 +49,24 @@ "fieldtype": "HTML", "label": "Email Reply Help", "options": "

Email Reply Example

\n\n
Order Overdue\n\nTransaction {{ name }} has exceeded Due Date. Please take necessary action.\n\nDetails\n\n- Customer: {{ customer }}\n- Amount: {{ grand_total }}\n
\n\n

How to get fieldnames

\n\n

The fieldnames you can use in your email template are the fields in the document from which you are sending the email. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Sales Invoice)

\n\n

Templating

\n\n

Templates are compiled using the Jinja Templating Language. To learn more about Jinja, read this documentation.

\n" + }, + { + "default": "0", + "fieldname": "use_html", + "fieldtype": "Check", + "label": "Use HTML" + }, + { + "depends_on": "eval:doc.use_html", + "fieldname": "response_html", + "fieldtype": "Code", + "label": "Response ", + "options": "HTML" } ], "icon": "fa fa-comment", - "modified": "2019-10-30 14:15:00.956347", + "links": [], + "modified": "2020-11-30 14:12:50.321633", "modified_by": "Administrator", "module": "Email", "name": "Email Template", diff --git a/frappe/email/doctype/email_template/email_template.py b/frappe/email/doctype/email_template/email_template.py index 2743032331..6708e9dd3f 100644 --- a/frappe/email/doctype/email_template/email_template.py +++ b/frappe/email/doctype/email_template/email_template.py @@ -9,7 +9,29 @@ from six import string_types class EmailTemplate(Document): def validate(self): - validate_template(self.response) + if self.use_html: + validate_template(self.response_html) + else: + validate_template(self.response) + + def get_formatted_subject(self, doc): + return frappe.render_template(self.subject, doc) + + def get_formatted_response(self, doc): + if self.use_html: + return frappe.render_template(self.response_html, doc) + + return frappe.render_template(self.response, doc) + + def get_formatted_email(self, doc): + if isinstance(doc, string_types): + doc = json.loads(doc) + + return { + "subject" : self.get_formatted_subject(doc), + "message" : self.get_formatted_response(doc) + } + @frappe.whitelist() def get_email_template(template_name, doc): @@ -18,5 +40,4 @@ def get_email_template(template_name, doc): doc = json.loads(doc) email_template = frappe.get_doc("Email Template", template_name) - return {"subject" : frappe.render_template(email_template.subject, doc), - "message" : frappe.render_template(email_template.response, doc)} \ No newline at end of file + return email_template.get_formatted_email(doc) \ No newline at end of file From 76b3fd811ec84ea86085e603d3dd6423f20c489c Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Mon, 30 Nov 2020 14:56:49 +0530 Subject: [PATCH 63/97] fix: added tests, cleaned up code --- frappe/tests/data/exif_sample_image.jpg | Bin 0 -> 161713 bytes frappe/tests/test_image.py | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 frappe/tests/data/exif_sample_image.jpg create mode 100644 frappe/tests/test_image.py diff --git a/frappe/tests/data/exif_sample_image.jpg b/frappe/tests/data/exif_sample_image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4a2c1552b991add9a356a460415626ad700f401b GIT binary patch literal 161713 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 literal 0 HcmV?d00001 diff --git a/frappe/tests/test_image.py b/frappe/tests/test_image.py new file mode 100644 index 0000000000..4da83ace8b --- /dev/null +++ b/frappe/tests/test_image.py @@ -0,0 +1,19 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt +from __future__ import unicode_literals + +import frappe, unittest +from PIL import Image +from frappe.utils.image import strip_exif_data +import io + +class TestImage(unittest.TestCase): + def test_strip_exif_data(self): + original_image = Image.open("../apps/frappe/frappe/tests/data/exif_sample_image.jpg") + original_image_content = io.open("../apps/frappe/frappe/tests/data/exif_sample_image.jpg", mode='rb').read() + + new_image_content = strip_exif_data(original_image_content) + new_image = Image.open(io.BytesIO(new_image_content)) + + self.assertEqual(new_image._getexif(), None) + self.assertNotEqual(original_image._getexif(), new_image._getexif()) \ No newline at end of file From 3a074756c1162579a2ae1cd92342248162deb208 Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Mon, 30 Nov 2020 14:58:01 +0530 Subject: [PATCH 64/97] fix: removed content_type param --- frappe/core/doctype/file/file.py | 4 ++-- frappe/utils/image.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 1642e857c5..8b1d8195dc 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -470,8 +470,8 @@ class File(Document): self.content_type = mimetypes.guess_type(self.file_name)[0] - if self.content_type and "image" in self.content_type: - self.content = strip_exif_data(self.content, self.content_type) + if self.content_type == "image": + self.content = strip_exif_data(self.content) self.file_size = self.check_max_file_size() self.content_hash = get_content_hash(self.content) diff --git a/frappe/utils/image.py b/frappe/utils/image.py index 3d3d98a28c..6cb5e9fc92 100644 --- a/frappe/utils/image.py +++ b/frappe/utils/image.py @@ -18,7 +18,7 @@ def resize_images(path, maxdim=700): print("resized {0}".format(os.path.join(basepath, fname))) -def strip_exif_data(content, content_type): +def strip_exif_data(content): """ Strips exif from image files which support it. Works by creating a new Image object which ignores exif by @@ -35,7 +35,7 @@ def strip_exif_data(content, content_type): new_image = Image.new(original_image.mode, original_image.size) new_image.putdata(list(original_image.getdata())) - new_image.save(output, format=content_type.split('/')[-1].upper()) + new_image.save(output, format='JPEG') # Since this is a temporary image, the extension does not matter content = output.getvalue() From f7444b7b440e770a34ff23d64c9c5c41ddc405d2 Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Mon, 30 Nov 2020 15:01:07 +0530 Subject: [PATCH 65/97] fix: removed unused import --- frappe/tests/test_image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/tests/test_image.py b/frappe/tests/test_image.py index 4da83ace8b..89cbc3225a 100644 --- a/frappe/tests/test_image.py +++ b/frappe/tests/test_image.py @@ -2,7 +2,7 @@ # MIT License. See license.txt from __future__ import unicode_literals -import frappe, unittest +import unittest from PIL import Image from frappe.utils.image import strip_exif_data import io From f8fd59b6eb26b03c324fe06f0ed10409ae5ac0a4 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Mon, 30 Nov 2020 15:15:19 +0530 Subject: [PATCH 66/97] fix: remove HTTP filter --- frappe/utils/change_log.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frappe/utils/change_log.py b/frappe/utils/change_log.py index 9607c89784..7cea1554b4 100644 --- a/frappe/utils/change_log.py +++ b/frappe/utils/change_log.py @@ -217,15 +217,13 @@ def check_release_on_github(app: str): # Invalid URL return - # Get latest version from Github - if parsed_url.protocol == "http": - return if parsed_url.resource != "github.com": return owner = parsed_url.owner repo = parsed_url.name + # Get latest version from Github r = requests.get('https://api.github.com/repos/{}/{}/releases'.format(owner, repo)) if r.ok: latest_non_beta_release = parse_latest_non_beta_release(r.json()) From 4810d07a8a4823a31ceef66b0896a991123ece6e Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 30 Nov 2020 15:23:12 +0530 Subject: [PATCH 67/97] test: Add tests for throwing AttributeError if method not found --- .../doctype/server_script/test_server_script.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py index 3356e584af..256cea57d7 100644 --- a/frappe/core/doctype/server_script/test_server_script.py +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -45,6 +45,15 @@ frappe.response['message'] = 'hello' allow_guest = 1, script = ''' frappe.flags = 'hello' +''' + ), + dict( + name='test_invalid_namespace_method', + script_type = 'DocType Event', + doctype_event = 'Before Insert', + reference_doctype = 'Note', + script = ''' +frappe.method_that_doesnt_exist("do some magic") ''' ) ] @@ -85,3 +94,8 @@ class TestServerScript(unittest.TestCase): def test_api_return(self): self.assertEqual(frappe.get_doc('Server Script', 'test_return_value').execute_method(), 'hello') + + def test_attribute_error(self): + """Raise AttributeError if method not found in Namespace""" + note = frappe.get_doc({"doctype": "Note", "title": "Test Note: Server Script"}) + self.assertRaises(AttributeError, note.insert) From d5d0bc8ea9983438366c059edea0be19a26bf359 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 30 Nov 2020 15:23:42 +0530 Subject: [PATCH 68/97] fix: Return dummy function to avoid NoneType not callable --- frappe/utils/safe_exec.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index 50893330be..2aacf5eda8 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -21,7 +21,9 @@ class NamespaceDict(frappe._dict): def __getattr__(self, key): ret = self.get(key) if (not ret and key.startswith("__")) or (key not in self): - raise AttributeError(f"module has no attribute '{key}'") + def default_function(*args, **kwargs): + raise AttributeError(f"module has no attribute '{key}'") + return default_function return ret From 3e1f6c8103fd705c67968d5b1c9f46867af0ecd5 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 30 Nov 2020 15:37:29 +0530 Subject: [PATCH 69/97] style: Use f-string instead of format --- frappe/utils/change_log.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/utils/change_log.py b/frappe/utils/change_log.py index 7cea1554b4..33801af722 100644 --- a/frappe/utils/change_log.py +++ b/frappe/utils/change_log.py @@ -223,8 +223,8 @@ def check_release_on_github(app: str): owner = parsed_url.owner repo = parsed_url.name - # Get latest version from Github - r = requests.get('https://api.github.com/repos/{}/{}/releases'.format(owner, repo)) + # Get latest version from GitHub + r = requests.get(f"https://api.github.com/repos/{owner}/{repo}/releases") if r.ok: latest_non_beta_release = parse_latest_non_beta_release(r.json()) if latest_non_beta_release: From 3296097df606df2998f01dde927541836504d6bc Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 30 Nov 2020 15:46:29 +0530 Subject: [PATCH 70/97] feat: show absolute value in print format --- frappe/model/base_document.py | 6 +++--- frappe/printing/doctype/print_format/print_format.json | 9 ++++++++- frappe/templates/print_formats/standard_macros.html | 5 ++--- frappe/www/printview.py | 1 + 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 0a219b4253..5d86b3bac8 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -802,12 +802,12 @@ class BaseDocument(object): if translated: val = _(val) - if absolute_value and isinstance(val, (int, float)): - val = abs(self.get(fieldname)) - if not doc: doc = getattr(self, "parent_doc", None) or self + if (absolute_value or doc.get('absolute_value')) and isinstance(val, (int, float)): + val = abs(self.get(fieldname)) + return format_value(val, df=df, doc=doc, currency=currency) def is_print_hide(self, fieldname, df=None, for_print=True): diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json index 63448ccc39..3867ce4502 100644 --- a/frappe/printing/doctype/print_format/print_format.json +++ b/frappe/printing/doctype/print_format/print_format.json @@ -22,6 +22,7 @@ "align_labels_right", "show_section_headings", "line_breaks", + "absolute_value", "column_break_11", "font", "css_section", @@ -196,13 +197,19 @@ "fieldtype": "Check", "hidden": 1, "label": "Print Format Builder" + }, + { + "default": "0", + "fieldname": "absolute_value", + "fieldtype": "Check", + "label": "Show absolute values" } ], "icon": "fa fa-print", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-10-27 18:27:58.307070", + "modified": "2020-11-30 15:26:35.605213", "modified_by": "Administrator", "module": "Printing", "name": "Print Format", diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html index 3681a87f53..168547798b 100644 --- a/frappe/templates/print_formats/standard_macros.html +++ b/frappe/templates/print_formats/standard_macros.html @@ -136,10 +136,9 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}" {%- if df.print_width %} style="width: {{ get_width(df) }};"{% endif %}> {% elif df.fieldtype=="HTML" %} {{ frappe.render_template(df.options, {"doc":doc}) }} - {% elif df.fieldtype=="Currency" %} - {{ doc.get_formatted(df.fieldname, doc, translated=df.translatable) }} {% else %} - {{ doc.get_formatted(df.fieldname, parent_doc or doc, translated=df.translatable) }} + {%- set parent = parent_doc or doc -%} + {{ doc.get_formatted(df.fieldname, parent, translated=df.translatable, absolute_value=parent.absolute_value) }} {% endif %} {%- endmacro %} diff --git a/frappe/www/printview.py b/frappe/www/printview.py index 545e5d581d..71316dc48c 100644 --- a/frappe/www/printview.py +++ b/frappe/www/printview.py @@ -100,6 +100,7 @@ def get_rendered_template(doc, name=None, print_format=None, meta=None, doc.print_section_headings = print_format.show_section_headings doc.print_line_breaks = print_format.line_breaks doc.align_labels_right = print_format.align_labels_right + doc.absolute_value = print_format.absolute_value def get_template_from_string(): return jenv.from_string(get_print_format(doc.doctype, From 7d4992549d69bc5485dad51cf75fa60f6143fcc5 Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Mon, 30 Nov 2020 17:19:35 +0530 Subject: [PATCH 71/97] feat: added option in system settings to remove exif tags, default 1 --- frappe/core/doctype/file/file.py | 5 +++-- frappe/core/doctype/system_settings/system_settings.json | 9 ++++++++- frappe/utils/image.py | 4 ++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 8b1d8195dc..34ff1f5599 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -470,8 +470,9 @@ class File(Document): self.content_type = mimetypes.guess_type(self.file_name)[0] - if self.content_type == "image": - self.content = strip_exif_data(self.content) + if self.content_type and "image" in self.content_type and \ + frappe.get_system_settings("remove_exif_tags"): + self.content = strip_exif_data(self.content, self.content_type) self.file_size = self.check_max_file_size() self.content_hash = get_content_hash(self.content) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 17f97b3e1a..987c079b77 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -37,6 +37,7 @@ "allow_login_using_mobile_number", "allow_login_using_user_name", "allow_error_traceback", + "remove_exif_tags", "password_settings", "logout_on_password_reset", "force_user_to_reset_password", @@ -460,12 +461,18 @@ "fieldname": "prepared_report_section", "fieldtype": "Section Break", "label": "Prepared Report" + }, + { + "default": "1", + "fieldname": "remove_exif_tags", + "fieldtype": "Check", + "label": "Remove EXIF tags from uploaded images" } ], "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2020-08-12 14:35:45.214327", + "modified": "2020-11-30 17:04:06.785282", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/utils/image.py b/frappe/utils/image.py index 6cb5e9fc92..4643429691 100644 --- a/frappe/utils/image.py +++ b/frappe/utils/image.py @@ -18,7 +18,7 @@ def resize_images(path, maxdim=700): print("resized {0}".format(os.path.join(basepath, fname))) -def strip_exif_data(content): +def strip_exif_data(content, content_type): """ Strips exif from image files which support it. Works by creating a new Image object which ignores exif by @@ -35,7 +35,7 @@ def strip_exif_data(content): new_image = Image.new(original_image.mode, original_image.size) new_image.putdata(list(original_image.getdata())) - new_image.save(output, format='JPEG') # Since this is a temporary image, the extension does not matter + new_image.save(output, format=content_type.split('/')[1]) content = output.getvalue() From 8f8489d9b39a8a5a649b93c0e3f5665d44275494 Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Mon, 30 Nov 2020 17:43:28 +0530 Subject: [PATCH 72/97] fix: test fix --- frappe/tests/test_image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/tests/test_image.py b/frappe/tests/test_image.py index 89cbc3225a..b8ed0792b6 100644 --- a/frappe/tests/test_image.py +++ b/frappe/tests/test_image.py @@ -12,7 +12,7 @@ class TestImage(unittest.TestCase): original_image = Image.open("../apps/frappe/frappe/tests/data/exif_sample_image.jpg") original_image_content = io.open("../apps/frappe/frappe/tests/data/exif_sample_image.jpg", mode='rb').read() - new_image_content = strip_exif_data(original_image_content) + new_image_content = strip_exif_data(original_image_content, "image/jpeg") new_image = Image.open(io.BytesIO(new_image_content)) self.assertEqual(new_image._getexif(), None) From e306f5601c0a841b5bda80c4ee9bd394d12c9eff Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Mon, 30 Nov 2020 18:58:53 +0530 Subject: [PATCH 73/97] fix: made requested changes --- frappe/core/doctype/file/file.py | 9 ++++++--- .../system_settings/system_settings.json | 8 ++++---- frappe/tests/test_image.py | 19 ------------------- frappe/tests/test_utils.py | 15 +++++++++++++++ frappe/utils/image.py | 5 +++-- 5 files changed, 28 insertions(+), 28 deletions(-) delete mode 100644 frappe/tests/test_image.py diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 34ff1f5599..8614740d26 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -470,11 +470,14 @@ class File(Document): self.content_type = mimetypes.guess_type(self.file_name)[0] - if self.content_type and "image" in self.content_type and \ - frappe.get_system_settings("remove_exif_tags"): + self.file_size = self.check_max_file_size() + + if ( + self.content_type and "image" in self.content_type + and frappe.get_system_settings("strip_exif_metadata_from_uploaded_images") + ): self.content = strip_exif_data(self.content, self.content_type) - self.file_size = self.check_max_file_size() self.content_hash = get_content_hash(self.content) duplicate_file = None diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 987c079b77..79fb84923a 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -37,7 +37,7 @@ "allow_login_using_mobile_number", "allow_login_using_user_name", "allow_error_traceback", - "remove_exif_tags", + "strip_exif_metadata_from_uploaded_images", "password_settings", "logout_on_password_reset", "force_user_to_reset_password", @@ -464,15 +464,15 @@ }, { "default": "1", - "fieldname": "remove_exif_tags", + "fieldname": "strip_exif_metadata_from_uploaded_images", "fieldtype": "Check", - "label": "Remove EXIF tags from uploaded images" + "label": "Strip EXIF tags from uploaded images" } ], "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2020-11-30 17:04:06.785282", + "modified": "2020-11-30 18:52:22.161391", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/tests/test_image.py b/frappe/tests/test_image.py deleted file mode 100644 index b8ed0792b6..0000000000 --- a/frappe/tests/test_image.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt -from __future__ import unicode_literals - -import unittest -from PIL import Image -from frappe.utils.image import strip_exif_data -import io - -class TestImage(unittest.TestCase): - def test_strip_exif_data(self): - original_image = Image.open("../apps/frappe/frappe/tests/data/exif_sample_image.jpg") - original_image_content = io.open("../apps/frappe/frappe/tests/data/exif_sample_image.jpg", mode='rb').read() - - new_image_content = strip_exif_data(original_image_content, "image/jpeg") - new_image = Image.open(io.BytesIO(new_image_content)) - - self.assertEqual(new_image._getexif(), None) - self.assertNotEqual(original_image._getexif(), new_image._getexif()) \ No newline at end of file diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index 8cdfe3e1a9..ebba60b8e8 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -7,6 +7,10 @@ import unittest from frappe.utils import evaluate_filters, money_in_words, scrub_urls, get_url from frappe.utils import ceil, floor +from PIL import Image +from frappe.utils.image import strip_exif_data +import io + class TestFilters(unittest.TestCase): def test_simple_dict(self): self.assertTrue(evaluate_filters({'doctype': 'User', 'status': 'Open'}, {'status': 'Open'})) @@ -122,3 +126,14 @@ class TestHTMLUtils(unittest.TestCase): clean = clean_email_html(sample) self.assertTrue('

Hello

' in clean) self.assertTrue('text' in clean) + +class TestImage(unittest.TestCase): + def test_strip_exif_data(self): + original_image = Image.open("../apps/frappe/frappe/tests/data/exif_sample_image.jpg") + original_image_content = io.open("../apps/frappe/frappe/tests/data/exif_sample_image.jpg", mode='rb').read() + + new_image_content = strip_exif_data(original_image_content, "image/jpeg") + new_image = Image.open(io.BytesIO(new_image_content)) + + self.assertEqual(new_image._getexif(), None) + self.assertNotEqual(original_image._getexif(), new_image._getexif()) \ No newline at end of file diff --git a/frappe/utils/image.py b/frappe/utils/image.py index 4643429691..60595464a1 100644 --- a/frappe/utils/image.py +++ b/frappe/utils/image.py @@ -19,12 +19,13 @@ def resize_images(path, maxdim=700): print("resized {0}".format(os.path.join(basepath, fname))) def strip_exif_data(content, content_type): - """ Strips exif from image files which support it. + """ Strips EXIF from image files which support it. Works by creating a new Image object which ignores exif by default and then extracts the binary data back into content. - Returns: stripped image content + Returns: + Bytes: Stripped image content """ from PIL import Image From e760c39c68d9d9d9c2561fc5989f2a218b849821 Mon Sep 17 00:00:00 2001 From: Leela vadlamudi Date: Tue, 1 Dec 2020 16:34:19 +0530 Subject: [PATCH 74/97] chore: Add .editorconfig to enforce consistent styling (#12021) Github respects editorconfig settings. Adding indent_size as 4 in settings makes github indent code to 4 columns. --- .editorconfig | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..24f122a8d4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# Root editor config file +root = true + +# Common settings +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +# python, js indentation settings +[{*.py,*.js}] +indent_style = tab +indent_size = 4 From fa80d26f4c2a29ccbdef70058d600114afb42aa9 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 2 Dec 2020 09:46:48 +0530 Subject: [PATCH 75/97] fix(minor): update requirements.txt --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 59c4a9dbf8..3cc92264a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,7 +35,7 @@ oauthlib==3.1.0 openpyxl==2.6.4 passlib==1.7.3 pdfkit==0.6.1 -Pillow==7.1.0 +Pillow>=8.0.0 premailer==3.6.1 psycopg2-binary==2.8.4 pyasn1==0.4.8 @@ -74,4 +74,4 @@ pycryptodome==3.9.8 paytmchecksum==1.7.0 wrapt==1.10.11 razorpay==1.2.0 -rsa>=4.1 # not directly required, pinned by Snyk to avoid a vulnerability \ No newline at end of file +rsa>=4.1 # not directly required, pinned by Snyk to avoid a vulnerability From aadbdfbd84482a37ab8503a5cc5a868019138346 Mon Sep 17 00:00:00 2001 From: Anuja Pawar <60467153+Anuja-pawar@users.noreply.github.com> Date: Wed, 2 Dec 2020 17:51:38 +0530 Subject: [PATCH 76/97] refactor(System Settings): Remove redundant code (#12027) Co-authored-by: Anuja --- .../system_settings/system_settings.js | 63 +++++++++---------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/frappe/core/doctype/system_settings/system_settings.js b/frappe/core/doctype/system_settings/system_settings.js index b6514dea9f..c0c9074cbc 100644 --- a/frappe/core/doctype/system_settings/system_settings.js +++ b/frappe/core/doctype/system_settings/system_settings.js @@ -1,37 +1,36 @@ -frappe.ui.form.on("System Settings", "refresh", function(frm) { - frappe.call({ - method: "frappe.core.doctype.system_settings.system_settings.load", - callback: function(data) { - frappe.all_timezones = data.message.timezones; - frm.set_df_property("time_zone", "options", frappe.all_timezones); +frappe.ui.form.on("System Settings", { + refresh: function(frm) { + frappe.call({ + method: "frappe.core.doctype.system_settings.system_settings.load", + callback: function(data) { + frappe.all_timezones = data.message.timezones; + frm.set_df_property("time_zone", "options", frappe.all_timezones); - $.each(data.message.defaults, function(key, val) { - frm.set_value(key, val); - frappe.sys_defaults[key] = val; - }) + $.each(data.message.defaults, function(key, val) { + frm.set_value(key, val); + frappe.sys_defaults[key] = val; + }); + } + }); + }, + enable_password_policy: function(frm) { + if (frm.doc.enable_password_policy == 0) { + frm.set_value("minimum_password_score", ""); + } else { + frm.set_value("minimum_password_score", "2"); } - }); -}); - -frappe.ui.form.on("System Settings", "enable_password_policy", function(frm) { - if(frm.doc.enable_password_policy == 0){ - frm.set_value("minimum_password_score", ""); - } else { - frm.set_value("minimum_password_score", "2"); - } -}); - -frappe.ui.form.on("System Settings", "enable_two_factor_auth", function(frm) { - if(frm.doc.enable_two_factor_auth == 0){ - frm.set_value("bypass_2fa_for_retricted_ip_users", 0); - frm.set_value("bypass_restrict_ip_check_if_2fa_enabled", 0); - } -}); - -frappe.ui.form.on("System Settings", "enable_prepared_report_auto_deletion", function(frm) { - if (frm.doc.enable_prepared_report_auto_deletion) { - if (!frm.doc.prepared_report_expiry_period) { - frm.set_value('prepared_report_expiry_period', 7); + }, + enable_two_factor_auth: function(frm) { + if (frm.doc.enable_two_factor_auth == 0) { + frm.set_value("bypass_2fa_for_retricted_ip_users", 0); + frm.set_value("bypass_restrict_ip_check_if_2fa_enabled", 0); + } + }, + enable_prepared_report_auto_deletion: function(frm) { + if (frm.doc.enable_prepared_report_auto_deletion) { + if (!frm.doc.prepared_report_expiry_period) { + frm.set_value('prepared_report_expiry_period', 7); + } } } }); From a19932835a8e8ab66042e19c8ae52f87a19af946 Mon Sep 17 00:00:00 2001 From: Anupam Date: Thu, 3 Dec 2020 12:30:10 +0530 Subject: [PATCH 77/97] feat: provision to open child table in customize form --- frappe/custom/doctype/customize_form/customize_form.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 2d220b864c..17343573ed 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -81,6 +81,11 @@ frappe.ui.form.on("Customize Form", { } else { f._sortable = false; } + if (f.fieldtype == "Table") { + frm.add_custom_button(f.options, function() { + frm.set_value('doc_type', f.options); + }, __('Customize Child Table')); + } }); frm.fields_dict.fields.grid.refresh(); }, From 6c48c2d14aa81d9ed2dc0545f45cda6672e41cef Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Thu, 3 Dec 2020 14:10:47 +0530 Subject: [PATCH 78/97] fix(charts): escape nan in each y axis --- frappe/public/js/frappe/views/reports/report_utils.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/reports/report_utils.js b/frappe/public/js/frappe/views/reports/report_utils.js index 158dbd653b..ebb08bd24b 100644 --- a/frappe/public/js/frappe/views/reports/report_utils.js +++ b/frappe/public/js/frappe/views/reports/report_utils.js @@ -15,7 +15,9 @@ frappe.report_utils = { if (raw_data.add_total_row) { labels = labels.slice(0, -1); - datasets[0].values = datasets[0].values.slice(0, -1); + datasets.forEach(dataset => { + dataset.values = dataset.values.slice(0, -1); + }); } return { From ee3fa3e4e0ebf414a9a7e777a509f6a7535be0a0 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 3 Dec 2020 22:39:38 +0530 Subject: [PATCH 79/97] fix: Remove unreferenced variable base_path --- frappe/commands/site.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index bc65aa178c..35f5b13582 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -100,12 +100,10 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas # Extract public and/or private files to the restored site, if user has given the path if with_public_files: - with_public_files = os.path.join(base_path, with_public_files) public = extract_files(site, with_public_files, 'public') os.remove(public) if with_private_files: - with_private_files = os.path.join(base_path, with_private_files) private = extract_files(site, with_private_files, 'private') os.remove(private) From 9ac14fb5abf6930234e4da41217dd74718c858da Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 3 Dec 2020 23:26:34 +0530 Subject: [PATCH 80/97] feat: Permission Query script type Add dynamic conditions in where clause of get_list query --- .../doctype/server_script/server_script.js | 41 ++++++++++++++----- .../doctype/server_script/server_script.json | 6 +-- .../doctype/server_script/server_script.py | 6 +++ .../server_script/server_script_utils.py | 11 ++++- .../server_script/test_server_script.py | 12 ++++++ frappe/model/db_query.py | 13 +++++- 6 files changed, 71 insertions(+), 18 deletions(-) diff --git a/frappe/core/doctype/server_script/server_script.js b/frappe/core/doctype/server_script/server_script.js index 78ef2d0509..a317d69166 100644 --- a/frappe/core/doctype/server_script/server_script.js +++ b/frappe/core/doctype/server_script/server_script.js @@ -48,29 +48,33 @@ frappe.ui.form.on('Server Script', { setup_help(frm) { frm.get_field('help_html').html(` -

Examples

DocType Event

-

+

Add logic for standard doctype events like Before Insert, After Submit, etc.

+
+	
 # set property
 if "test" in doc.description:
-    doc.status = 'Closed'
+	doc.status = 'Closed'
 
 
 # validate
 if "validate" in doc.description:
-    raise frappe.ValidationError
+	raise frappe.ValidationError
 
 # auto create another document
-if doc.allocted_to:
-    frappe.get_doc(dict(
-        doctype = 'ToDo'
-        owner = doc.allocated_to,
-        description = doc.subject
-    )).insert()
-
+if doc.allocated_to: + frappe.get_doc(dict( + doctype = 'ToDo' + owner = doc.allocated_to, + description = doc.subject + )).insert() +
+
+

API Call

+

Respond to /api/method/<method-name> calls, just like whitelisted methods


 # respond to API
 
@@ -79,6 +83,21 @@ if frappe.form_dict.message == "ping":
 else:
 	frappe.response['message'] = "ok"
 
+ +
+ +

Permission Query

+

Add conditions to the where clause of list queries.

+

+# generate dynamic conditions and set it in the conditions variable
+tenant_id = frappe.db.get_value(...)
+conditions = 'tenant_id = {}'.format(tenant_id)
+
+# resulting select query
+select name from \`tabPerson\`
+where tenant_id = 2
+order by creation desc
+
`); } diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index 420f96ec2f..94a48f196c 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -24,7 +24,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Script Type", - "options": "DocType Event\nScheduler Event\nAPI", + "options": "DocType Event\nScheduler Event\nPermission Query\nAPI", "reqd": 1 }, { @@ -35,7 +35,7 @@ "reqd": 1 }, { - "depends_on": "eval:doc.script_type==='DocType Event'", + "depends_on": "eval:['DocType Event', 'Permission Query'].includes(doc.script_type)", "fieldname": "reference_doctype", "fieldtype": "Link", "in_list_view": 1, @@ -88,7 +88,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-11 12:39:41.391052", + "modified": "2020-12-03 22:42:02.708148", "modified_by": "Administrator", "module": "Core", "name": "Server Script", diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index 839b784651..b1bf79dc52 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -41,6 +41,12 @@ class ServerScript(Document): # wrong report type! raise frappe.DoesNotExistError + def get_permission_query_conditions(self, user): + locals = {"user": user, "conditions": ""} + safe_exec(self.script, None, locals) + if locals["conditions"]: + return locals["conditions"] + @frappe.whitelist() def setup_scheduler_events(script_name, frequency): method = frappe.scrub('{0}-{1}'.format(script_name, frequency)) diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index e03504f30b..4dc4f12b34 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -50,6 +50,9 @@ def get_server_script_map(): # }, # '_api': { # '[path]': '[server script]' + # }, + # 'permission_query': { + # 'DocType': '[server script]' # } # } if frappe.flags.in_patch and not frappe.db.table_exists('Server Script'): @@ -57,16 +60,20 @@ def get_server_script_map(): script_map = frappe.cache().get_value('server_script_map') if script_map is None: - script_map = {} + script_map = { + 'permission_query': {} + } enabled_server_scripts = frappe.get_all('Server Script', fields=('name', 'reference_doctype', 'doctype_event','api_method', 'script_type'), filters={'disabled': 0}) for script in enabled_server_scripts: if script.script_type == 'DocType Event': script_map.setdefault(script.reference_doctype, {}).setdefault(script.doctype_event, []).append(script.name) + elif script.script_type == 'Permission Query': + script_map['permission_query'][script.reference_doctype] = script.name else: script_map.setdefault('_api', {})[script.api_method] = script.name frappe.cache().set_value('server_script_map', script_map) - return script_map \ No newline at end of file + return script_map diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py index 3356e584af..565436d9f2 100644 --- a/frappe/core/doctype/server_script/test_server_script.py +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -45,6 +45,14 @@ frappe.response['message'] = 'hello' allow_guest = 1, script = ''' frappe.flags = 'hello' +''' + ), + dict( + name='test_permission_query', + script_type = 'Permission Query', + reference_doctype = 'ToDo', + script = ''' +conditions = '1 = 1' ''' ) ] @@ -85,3 +93,7 @@ class TestServerScript(unittest.TestCase): def test_api_return(self): self.assertEqual(frappe.get_doc('Server Script', 'test_return_value').execute_method(), 'hello') + + def test_permission_query(self): + self.assertTrue('where (1 = 1)' in frappe.db.get_list('ToDo', return_query=1)) + self.assertTrue(isinstance(frappe.db.get_list('ToDo'), list)) diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index ace9b04cec..b936251b50 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -18,6 +18,7 @@ from frappe.client import check_parent_permission from frappe.model.utils.user_settings import get_user_settings, update_user_settings from frappe.utils import flt, cint, get_time, make_filter_tuple, get_filter, add_to_date, cstr, get_timespan_date_range from frappe.model.meta import get_table_columns +from frappe.core.doctype.server_script.server_script_utils import get_server_script_map class DatabaseQuery(object): def __init__(self, doctype, user=None): @@ -683,15 +684,23 @@ class DatabaseQuery(object): self.match_filters.append(match_filters) def get_permission_query_conditions(self): + conditions = [] condition_methods = frappe.get_hooks("permission_query_conditions", {}).get(self.doctype, []) if condition_methods: - conditions = [] for method in condition_methods: c = frappe.call(frappe.get_attr(method), self.user) if c: conditions.append(c) - return " and ".join(conditions) if conditions else None + permision_script_name = get_server_script_map().get("permission_query").get(self.doctype) + if permision_script_name: + script = frappe.get_doc("Server Script", permision_script_name) + condition = script.get_permission_query_conditions(self.user) + if condition: + conditions.append(condition) + + return " and ".join(conditions) if conditions else "" + def run_custom_query(self, query): if '%(key)s' in query: From e338f2f7b6cb5e446dacd9a7527d12e1c4806405 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 3 Dec 2020 23:56:26 +0530 Subject: [PATCH 81/97] fix(newsletter): Render template for HTML content type --- frappe/email/doctype/newsletter/newsletter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index a4d60706eb..2791ebb75b 100755 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -85,11 +85,11 @@ class Newsletter(WebsiteGenerator): self.db_set("scheduled_to_send", len(self.recipients)) def get_message(self): - + if self.content_type == "HTML": + return frappe.render_template(self.message_html, {"doc": self.as_dict()}) return { 'Rich Text': self.message, - 'Markdown': markdown(self.message_md), - 'HTML': self.message_html + 'Markdown': markdown(self.message_md) }[self.content_type or 'Rich Text'] def get_recipients(self): From 69befda08efa8d42f6c9ea2bb0fd84936d124f99 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 4 Dec 2020 16:12:22 +0530 Subject: [PATCH 82/97] fix: label for _assign field --- frappe/model/meta.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 8c17a5b19b..c740d495c1 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -209,7 +209,8 @@ class Meta(Document): 'owner': _('Created By'), 'modified_by': _('Modified By'), 'creation': _('Created On'), - 'modified': _('Last Modified On') + 'modified': _('Last Modified On'), + '_assign': _('Assigned To') }.get(fieldname) or _('No Label') return label From 9d060f96533679b7f9fa187e63ae548a73f4258f Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Fri, 4 Dec 2020 17:01:55 +0530 Subject: [PATCH 83/97] fix: cstr import added --- frappe/utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 9640bcd394..d76e6c951e 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -10,7 +10,7 @@ import functools from .html_utils import sanitize_html import frappe from frappe.utils.identicon import Identicon -from email.utils import parseaddr, formataddr +from email.utils import parseaddr, formataddr, cstr from email.header import decode_header, make_header # utility functions like cint, int, flt, etc. from frappe.utils.data import * From d229e363b39d26a0ff11ee4efb2ed2693da6c158 Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Fri, 4 Dec 2020 17:10:42 +0530 Subject: [PATCH 84/97] fix: revert cstr import added --- frappe/utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index d76e6c951e..9640bcd394 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -10,7 +10,7 @@ import functools from .html_utils import sanitize_html import frappe from frappe.utils.identicon import Identicon -from email.utils import parseaddr, formataddr, cstr +from email.utils import parseaddr, formataddr from email.header import decode_header, make_header # utility functions like cint, int, flt, etc. from frappe.utils.data import * From 1b609af8e5de0b0cc75fdd9d60e8fe9748371180 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 4 Dec 2020 18:15:17 +0530 Subject: [PATCH 85/97] fix: Handle paths relative to bench root and sites folder --- frappe/commands/site.py | 4 ++-- frappe/installer.py | 20 +++++++------------- frappe/utils/__init__.py | 24 ++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 35f5b13582..4a631be3ac 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -100,11 +100,11 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas # Extract public and/or private files to the restored site, if user has given the path if with_public_files: - public = extract_files(site, with_public_files, 'public') + public = extract_files(site, with_public_files) os.remove(public) if with_private_files: - private = extract_files(site, with_private_files, 'private') + private = extract_files(site, with_private_files) os.remove(private) # Removing temporarily created file diff --git a/frappe/installer.py b/frappe/installer.py index 1245a08cb7..a11c8dfbfa 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -440,20 +440,11 @@ def extract_sql_from_archive(sql_file_path): Returns: str: Path of the decompressed SQL file """ + from frappe.utils import get_bench_relative_path + sql_file_path = get_bench_relative_path(sql_file_path) # 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) - if not os.path.exists(sql_file_path): - print('Invalid path {0}'.format(sql_file_path[3:])) - sys.exit(1) - elif sql_file_path.startswith(os.sep): - base_path = os.sep - else: - base_path = '.' - if sql_file_path.endswith('sql.gz'): - decompressed_file_name = extract_sql_gzip(os.path.abspath(sql_file_path)) + decompressed_file_name = extract_sql_gzip(sql_file_path) else: decompressed_file_name = sql_file_path @@ -475,9 +466,12 @@ def extract_sql_gzip(sql_gz_path): return decompressed_file -def extract_files(site_name, file_path, folder_name): +def extract_files(site_name, file_path): import shutil import subprocess + from frappe.utils import get_bench_relative_path + + file_path = get_bench_relative_path(file_path) # Need to do frappe.init to maintain the site locals frappe.init(site=site_name) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index c209ee13c9..cc5b42acb7 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -729,3 +729,27 @@ def get_build_version(): # .build can sometimes not exist # this is not a major problem so send fallback return frappe.utils.random_string(8) + +def get_bench_relative_path(file_path): + """Fixes paths relative to the bench root directory if exists and returns the absolute path + + Args: + file_path (str, Path): Path of a file that exists on the file system + + Returns: + str: Absolute path of the file_path + """ + if not os.path.exists(file_path): + base_path = '..' + elif file_path.startswith(os.sep): + base_path = os.sep + else: + base_path = '.' + + file_path = os.path.join(base_path, file_path) + + if not os.path.exists(file_path): + print('Invalid path {0}'.format(file_path[3:])) + sys.exit(1) + + return os.path.abspath(file_path) From beec0b48fbac14766d005d73c874b4b874420d91 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 4 Dec 2020 18:31:16 +0530 Subject: [PATCH 86/97] fix: use frappe.utils.shorten_number --- frappe/public/js/frappe/widgets/number_card_widget.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index 20503ed4f1..8855de435b 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -269,7 +269,7 @@ export default class NumberCardWidget extends Widget { result: this.number }).then(res => { if (res !== undefined) { - this.percentage_stat = shorten_number(res); + this.percentage_stat = frappe.utils.shorten_number(res); } }); } From c5677b52d0af3dd31812625db8b22a2fab501983 Mon Sep 17 00:00:00 2001 From: prssanna Date: Mon, 7 Dec 2020 14:42:54 +0530 Subject: [PATCH 87/97] refactor: shorten_number function - pass min_length and max_no_of_decimals --- frappe/public/js/frappe/utils/utils.js | 41 +++++++++++++++---- .../js/frappe/widgets/number_card_widget.js | 2 +- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 4bf9c5bbd8..f8f25293b3 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -979,17 +979,42 @@ Object.assign(frappe.utils, { return route; }, - shorten_number: function (number, country) { - country = (country == 'India') ? country : ''; + shorten_number: function (number, country, min_length=4, max_no_of_decimals=2) { + /* returns the number as an abbreviated string + * PARAMS + * number - number to be shortened + * country - country that determines the numnber system to be used + * min_length - length below which the number will not be shortened + * max_no_of_decimals - max number of decimals of the shortened number + */ + + // return number if total digits is lesser than min_length + const len = String(number).match(/\d/g).length; + if (len < min_length) return number.toString(); + const number_system = this.get_number_system(country); let x = Math.abs(Math.round(number)); for (const map of number_system) { - const condition = map.condition ? map.condition(x) : x >= map.divisor; - if (condition) { - return (number/map.divisor).toFixed(2) + ' ' + map.symbol; + if (x >= map.divisor) { + let result = number/map.divisor; + const no_of_decimals = this.get_number_of_decimals(result); + /* + If no_of_decimals is greater than max_no_of_decimals, + round the number to max_no_of_decimals + */ + result = no_of_decimals > max_no_of_decimals + ? result.toFixed(max_no_of_decimals) + : result; + return result + ' ' + map.symbol; } } - return number.toFixed(); + + return number.toFixed(max_no_of_decimals); + }, + + get_number_of_decimals: function (number) { + if (Math.floor(number) === number) return 0; + return number.toString().split(".")[1].length || 0; }, get_number_system: function (country) { @@ -1019,9 +1044,11 @@ Object.assign(frappe.utils, { { divisor: 1.0e+3, symbol: 'K', - condition: (num) => num.toFixed().length > 5 }] }; + + if (!Object.keys(number_system_map).includes(country)) country = ''; + return number_system_map[country]; }, }); diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index 8855de435b..c41f9bc6e7 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -204,7 +204,7 @@ export default class NumberCardWidget extends Widget { get_formatted_number(df) { const default_country = frappe.sys_defaults.country; - const shortened_number = frappe.utils.shorten_number(this.number, default_country); + const shortened_number = frappe.utils.shorten_number(this.number, default_country, 5); let number_parts = shortened_number.split(' '); const symbol = number_parts[1] || ''; From 3a11ef3365ed4838f1bd82929af5d4f5589110e0 Mon Sep 17 00:00:00 2001 From: prssanna Date: Tue, 8 Dec 2020 12:46:16 +0530 Subject: [PATCH 88/97] fix: filter dashboards, dashboard charts, number cards by modules --- frappe/desk/doctype/dashboard/dashboard.py | 19 ++++++++++++ .../dashboard_chart/dashboard_chart.py | 30 ++++++++++++------- .../desk/doctype/number_card/number_card.py | 13 ++++++-- frappe/hooks.py | 1 + 4 files changed, 50 insertions(+), 13 deletions(-) diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py index b12bcfe27d..1c04b6a2fe 100644 --- a/frappe/desk/doctype/dashboard/dashboard.py +++ b/frappe/desk/doctype/dashboard/dashboard.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals from frappe.model.document import Document from frappe.modules.export_file import export_to_files +from frappe.config import get_modules_from_all_apps_for_user import frappe from frappe import _ import json @@ -42,6 +43,24 @@ class Dashboard(Document): except ValueError as error: frappe.throw(_("Invalid json added in the custom options: {0}").format(error)) + +def get_permission_query_conditions(user): + if not user: + user = frappe.session.user + + if user == 'Administrator': + return + + roles = frappe.get_roles(user) + if "System Manager" in roles: + return None + + allowed_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()] + module_condition = '`tabDashboard`.`module` in ({allowed_modules}) or `tabDashboard`.`module` is NULL'.format( + allowed_modules=','.join(allowed_modules)) + + return '{module_condition}'.format(module_condition=module_condition) + @frappe.whitelist() def get_permitted_charts(dashboard_name): permitted_charts = [] diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 3f8d7c3c79..2fa36b5514 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -13,12 +13,12 @@ from frappe.utils.dateutils import\ get_period, get_period_beginning, get_from_date_from_timespan, get_dates_from_timegrain from frappe.model.naming import append_number_if_name_exists from frappe.boot import get_allowed_reports +from frappe.config import get_modules_from_all_apps_for_user from frappe.model.document import Document from frappe.modules.export_file import export_to_files def get_permission_query_conditions(user): - if not user: user = frappe.session.user @@ -31,9 +31,11 @@ def get_permission_query_conditions(user): doctype_condition = False report_condition = False + module_condition = False allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()] allowed_reports = [frappe.db.escape(key) if type(key) == str else key.encode('UTF8') for key in get_allowed_reports()] + allowed_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()] if allowed_doctypes: doctype_condition = '`tabDashboard Chart`.`document_type` in ({allowed_doctypes})'.format( @@ -41,18 +43,24 @@ def get_permission_query_conditions(user): if allowed_reports: report_condition = '`tabDashboard Chart`.`report_name` in ({allowed_reports})'.format( allowed_reports=','.join(allowed_reports)) + if allowed_modules: + module_condition = '''`tabDashboard Chart`.`module` in ({allowed_modules}) + or `tabDashboard Chart`.`module` is NULL'''.format( + allowed_modules=','.join(allowed_modules)) return ''' - (`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average') - and {doctype_condition}) - or - (`tabDashboard Chart`.`chart_type` = 'Report' - and {report_condition}) - '''.format( - doctype_condition=doctype_condition, - report_condition=report_condition - ) - + ((`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average') + and {doctype_condition}) + or + (`tabDashboard Chart`.`chart_type` = 'Report' + and {report_condition})) + and + ({module_condition}) + '''.format( + doctype_condition=doctype_condition, + report_condition=report_condition, + module_condition=module_condition + ) def has_permission(doc, ptype, user): roles = frappe.get_roles(user) diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py index d4a2b00c57..6bddd09fc7 100644 --- a/frappe/desk/doctype/number_card/number_card.py +++ b/frappe/desk/doctype/number_card/number_card.py @@ -8,6 +8,7 @@ from frappe.model.document import Document from frappe.utils import cint from frappe.model.naming import append_number_if_name_exists from frappe.modules.export_file import export_to_files +from frappe.config import get_modules_from_all_apps_for_user class NumberCard(Document): def autoname(self): @@ -33,16 +34,24 @@ def get_permission_query_conditions(user=None): return None doctype_condition = False + module_condition = False allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()] + allowed_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()] if allowed_doctypes: doctype_condition = '`tabNumber Card`.`document_type` in ({allowed_doctypes})'.format( allowed_doctypes=','.join(allowed_doctypes)) + if allowed_modules: + module_condition = '''`tabNumber Card`.`module` in ({allowed_modules}) + or `tabNumber Card`.`module` is NULL'''.format( + allowed_modules=','.join(allowed_modules)) return ''' - {doctype_condition} - '''.format(doctype_condition=doctype_condition) + {doctype_condition} + and + {module_condition} + '''.format(doctype_condition=doctype_condition, module_condition=module_condition) def has_permission(doc, ptype, user): roles = frappe.get_roles(user) diff --git a/frappe/hooks.py b/frappe/hooks.py index d8c8cd841c..3d7ae0abb4 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -94,6 +94,7 @@ permission_query_conditions = { "User": "frappe.core.doctype.user.user.get_permission_query_conditions", "Dashboard Settings": "frappe.desk.doctype.dashboard_settings.dashboard_settings.get_permission_query_conditions", "Notification Log": "frappe.desk.doctype.notification_log.notification_log.get_permission_query_conditions", + "Dashboard": "frappe.desk.doctype.dashboard.dashboard.get_permission_query_conditions", "Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.get_permission_query_conditions", "Number Card": "frappe.desk.doctype.number_card.number_card.get_permission_query_conditions", "Notification Settings": "frappe.desk.doctype.notification_settings.notification_settings.get_permission_query_conditions", From e197a6fd7fbecfca9eadd8c2d92577eb20ba12b7 Mon Sep 17 00:00:00 2001 From: Anupam Date: Tue, 8 Dec 2020 11:35:41 +0530 Subject: [PATCH 89/97] feat: added data format support DD-Mon-YY --- frappe/public/js/frappe/data_import/import_preview.js | 1 + frappe/utils/data.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/frappe/public/js/frappe/data_import/import_preview.js b/frappe/public/js/frappe/data_import/import_preview.js index 6c17cb4351..477cfb0786 100644 --- a/frappe/public/js/frappe/data_import/import_preview.js +++ b/frappe/public/js/frappe/data_import/import_preview.js @@ -101,6 +101,7 @@ frappe.data_import.ImportPreview = class ImportPreview { .replace('%H', 'HH') .replace('%M', 'mm') .replace('%S', 'ss') + .replace('%b', 'Mon') : null; let column_title = ` diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 41f247da45..cef4243913 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -1300,12 +1300,14 @@ def generate_hash(*args, **kwargs): def guess_date_format(date_string): DATE_FORMATS = [ + r"%d/%b/%y", r"%d-%m-%Y", r"%m-%d-%Y", r"%Y-%m-%d", r"%d-%m-%y", r"%m-%d-%y", r"%y-%m-%d", + r"%y-%b-%d", r"%d/%m/%Y", r"%m/%d/%Y", r"%Y/%m/%d", From 309f483bb3c158fe0b3186e3d6c4762f66b2049c Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 9 Dec 2020 16:54:40 +0530 Subject: [PATCH 90/97] refactor: update return statement --- frappe/desk/doctype/dashboard/dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py index 1c04b6a2fe..fa03bf8f80 100644 --- a/frappe/desk/doctype/dashboard/dashboard.py +++ b/frappe/desk/doctype/dashboard/dashboard.py @@ -59,7 +59,7 @@ def get_permission_query_conditions(user): module_condition = '`tabDashboard`.`module` in ({allowed_modules}) or `tabDashboard`.`module` is NULL'.format( allowed_modules=','.join(allowed_modules)) - return '{module_condition}'.format(module_condition=module_condition) + return module_condition @frappe.whitelist() def get_permitted_charts(dashboard_name): From c5d08305536c77162ba7821bd4ac9ec9a0cee13b Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 10 Dec 2020 10:37:29 +0530 Subject: [PATCH 91/97] fix: cint seconds before operations --- frappe/utils/data.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 34659e1cac..75e8dedbe9 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -369,6 +369,8 @@ def format_duration(seconds, hide_days=False): example: converts 12885 to '3h 34m 45s' where 12885 = seconds in float """ + + seconds = cint(seconds) total_duration = { 'days': math.floor(seconds / (3600 * 24)), From c805851ebe20ea60719a0d9929fbfb900265e28b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 10 Dec 2020 10:52:50 +0530 Subject: [PATCH 92/97] feat(Auto Repeat): Submit on Creation configuration --- .../automation/doctype/auto_repeat/auto_repeat.js | 14 ++++++++++++++ .../doctype/auto_repeat/auto_repeat.json | 11 ++++++++++- .../automation/doctype/auto_repeat/auto_repeat.py | 5 ++++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.js b/frappe/automation/doctype/auto_repeat/auto_repeat.js index a11de1d881..121b4bd2f0 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.js +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.js @@ -44,6 +44,20 @@ frappe.ui.form.on('Auto Repeat', { // auto repeat schedule frappe.auto_repeat.render_schedule(frm); + + frm.trigger('toggle_submit_on_creation'); + }, + + reference_doctype: function(frm) { + frm.trigger('toggle_submit_on_creation'); + }, + + toggle_submit_on_creation: function(frm) { + // submit on creation checkbox + frappe.model.with_doctype(frm.doc.reference_doctype, () => { + let meta = frappe.get_meta(frm.doc.reference_doctype); + frm.toggle_display('submit_on_creation', meta.is_submittable); + }); }, template: function(frm) { diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.json b/frappe/automation/doctype/auto_repeat/auto_repeat.json index 8ee6ca1d45..80975dd4f5 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.json +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "format:AUT-AR-{#####}", @@ -12,6 +13,7 @@ "section_break_3", "reference_doctype", "reference_document", + "submit_on_creation", "column_break_5", "start_date", "end_date", @@ -186,9 +188,16 @@ "fieldname": "repeat_on_last_day", "fieldtype": "Check", "label": "Repeat on Last Day of the Month" + }, + { + "default": "0", + "fieldname": "submit_on_creation", + "fieldtype": "Check", + "label": "Submit on Creation" } ], - "modified": "2019-07-17 11:30:51.412317", + "links": [], + "modified": "2020-12-10 10:43:13.449172", "modified_by": "Administrator", "module": "Automation", "name": "Auto Repeat", diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index fcf24bf1a9..019692f136 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -150,6 +150,9 @@ class AutoRepeat(Document): self.update_doc(new_doc, reference_doc) new_doc.insert(ignore_permissions = True) + if self.submit_on_creation: + new_doc.submit(gnore_permissions = True) + return new_doc def update_doc(self, new_doc, reference_doc): @@ -160,7 +163,7 @@ class AutoRepeat(Document): if new_doc.meta.get_field('auto_repeat'): new_doc.set('auto_repeat', self.name) - for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'remarks', 'owner']: + for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'user_remark', 'remarks', 'owner']: if new_doc.meta.get_field(fieldname): new_doc.set(fieldname, reference_doc.get(fieldname)) From 30fbf37f25530cec068d39a98066684cfccd8848 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 10 Dec 2020 11:48:12 +0530 Subject: [PATCH 93/97] test: submit on creation --- .../doctype/auto_repeat/auto_repeat.py | 2 +- .../doctype/auto_repeat/test_auto_repeat.py | 51 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 019692f136..7eb533ce44 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -151,7 +151,7 @@ class AutoRepeat(Document): new_doc.insert(ignore_permissions = True) if self.submit_on_creation: - new_doc.submit(gnore_permissions = True) + new_doc.submit() return new_doc diff --git a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py index 60fa9cb59e..e40b12e3b9 100644 --- a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py @@ -111,6 +111,25 @@ class TestAutoRepeat(unittest.TestCase): doc = make_auto_repeat(frequency='Daily', reference_document=todo.name, start_date=add_days(today(), -2)) self.assertEqual(getdate(doc.next_schedule_date), current_date) + def test_submit_on_creation(self): + doctype = 'Test Submittable DocType' + create_submittable_doctype(doctype) + + current_date = getdate() + submittable_doc = frappe.get_doc(dict(doctype=doctype, test='test submit on creation')).insert() + submittable_doc.submit() + doc = make_auto_repeat(frequency='Daily', reference_doctype=doctype, reference_document=submittable_doc.name, + start_date=add_days(current_date, -1), submit_on_creation=1) + + data = get_auto_repeat_entries(current_date) + create_repeated_entries(data) + docnames = frappe.db.get_all(doc.reference_doctype, + filters={'auto_repeat': doc.name}, + fields=['docstatus'], + limit=1 + ) + self.assertEquals(docnames[0].docstatus, 1) + def make_auto_repeat(**args): args = frappe._dict(args) @@ -118,6 +137,7 @@ def make_auto_repeat(**args): 'doctype': 'Auto Repeat', 'reference_doctype': args.reference_doctype or 'ToDo', 'reference_document': args.reference_document or frappe.db.get_value('ToDo', 'name'), + 'submit_on_creation': args.submit_on_creation or 0, 'frequency': args.frequency or 'Daily', 'start_date': args.start_date or add_days(today(), -1), 'end_date': args.end_date or "", @@ -128,3 +148,34 @@ def make_auto_repeat(**args): }).insert(ignore_permissions=True) return doc + + +def create_submittable_doctype(doctype): + if frappe.db.exists('DocType', doctype): + return + else: + doc = frappe.get_doc({ + 'doctype': 'DocType', + '__newname': doctype, + 'module': 'Custom', + 'custom': 1, + 'is_submittable': 1, + 'fields': [{ + 'fieldname': 'test', + 'label': 'Test', + 'fieldtype': 'Data' + }], + 'permissions': [{ + 'role': 'System Manager', + 'read': 1, + 'write': 1, + 'create': 1, + 'delete': 1, + 'submit': 1, + 'cancel': 1, + 'amend': 1 + }] + }).insert() + + doc.allow_auto_repeat = 1 + doc.save() \ No newline at end of file From 5a60048a0a9e57a5227796a3585b402c02435348 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 10 Dec 2020 12:31:01 +0530 Subject: [PATCH 94/97] test: for get_bench_relative_path --- frappe/tests/test_commands.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/frappe/tests/test_commands.py b/frappe/tests/test_commands.py index 8c76ce2f48..2da08718a4 100644 --- a/frappe/tests/test_commands.py +++ b/frappe/tests/test_commands.py @@ -14,7 +14,7 @@ import glob import frappe import frappe.recorder from frappe.installer import add_to_installed_apps -from frappe.utils import add_to_date, now +from frappe.utils import add_to_date, get_bench_relative_path, now from frappe.utils.backups import fetch_latest_backups @@ -364,3 +364,21 @@ class TestCommands(BaseTestCommands): else: installed_apps = set(frappe.get_installed_apps()) self.assertSetEqual(list_apps, installed_apps) + + def test_get_bench_relative_path(self): + bench_path = frappe.utils.get_bench_path() + test1_path = os.path.join(bench_path, 'test1.txt') + test2_path = os.path.join(bench_path, 'sites/test2.txt') + + with open(test1_path, 'w+') as test1: + test1.write('asdf') + with open(test2_path, 'w+') as test2: + test2.write('asdf') + + self.assertTrue('test1.txt' in get_bench_relative_path('test1.txt')) + self.assertTrue('sites/test2.txt' in get_bench_relative_path('test2.txt')) + with self.assertRaises(SystemExit): + get_bench_relative_path('test3.txt') + + os.remove(test1_path) + os.remove(test2_path) From 9e954a737257b3fb11821ae56255d3bf07082cb6 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 10 Dec 2020 13:52:21 +0530 Subject: [PATCH 95/97] style: quotes --- frappe/tests/test_commands.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/frappe/tests/test_commands.py b/frappe/tests/test_commands.py index 2da08718a4..0786a0e14f 100644 --- a/frappe/tests/test_commands.py +++ b/frappe/tests/test_commands.py @@ -367,18 +367,18 @@ class TestCommands(BaseTestCommands): def test_get_bench_relative_path(self): bench_path = frappe.utils.get_bench_path() - test1_path = os.path.join(bench_path, 'test1.txt') - test2_path = os.path.join(bench_path, 'sites/test2.txt') + test1_path = os.path.join(bench_path, "test1.txt") + test2_path = os.path.join(bench_path, "sites", "test2.txt") - with open(test1_path, 'w+') as test1: - test1.write('asdf') - with open(test2_path, 'w+') as test2: - test2.write('asdf') + with open(test1_path, "w+") as test1: + test1.write("asdf") + with open(test2_path, "w+") as test2: + test2.write("asdf") - self.assertTrue('test1.txt' in get_bench_relative_path('test1.txt')) - self.assertTrue('sites/test2.txt' in get_bench_relative_path('test2.txt')) + self.assertTrue("test1.txt" in get_bench_relative_path("test1.txt")) + self.assertTrue("sites/test2.txt" in get_bench_relative_path("test2.txt")) with self.assertRaises(SystemExit): - get_bench_relative_path('test3.txt') + get_bench_relative_path("test3.txt") os.remove(test1_path) os.remove(test2_path) From ec0d1dd63f727ca7d4713fbf2f739e1c49151a30 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 10 Dec 2020 17:18:13 +0530 Subject: [PATCH 96/97] fix: added server side validation for submit on creation --- frappe/automation/doctype/auto_repeat/auto_repeat.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 7eb533ce44..31d6539e61 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -21,6 +21,7 @@ class AutoRepeat(Document): def validate(self): self.update_status() self.validate_reference_doctype() + self.validate_submit_on_creation() self.validate_dates() self.validate_email_id() self.set_dates() @@ -60,6 +61,11 @@ class AutoRepeat(Document): if not frappe.get_meta(self.reference_doctype).allow_auto_repeat: frappe.throw(_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(self.reference_doctype)) + def validate_submit_on_creation(self): + if self.submit_on_creation and not frappe.get_meta(self.reference_doctype).is_submittable: + frappe.throw(_('Cannot enable {0} for a non-submittable doctype').format( + frappe.bold('Submit on Creation'))) + def validate_dates(self): if frappe.flags.in_patch: return From 0651341bf8535af743d0fda128008ee180f3d533 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 10 Dec 2020 18:53:21 +0530 Subject: [PATCH 97/97] feat: show absolute value checkbox if doctype has appropriate fields --- .../doctype/print_format/print_format.js | 18 +++++++++++++++++- .../doctype/print_format/print_format.json | 4 +++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/frappe/printing/doctype/print_format/print_format.js b/frappe/printing/doctype/print_format/print_format.js index e6599b2496..9ef5652dda 100644 --- a/frappe/printing/doctype/print_format/print_format.js +++ b/frappe/printing/doctype/print_format/print_format.js @@ -19,6 +19,7 @@ frappe.ui.form.on("Print Format", { } frm.trigger('render_buttons'); frm.toggle_display('standard', frappe.boot.developer_mode); + frm.trigger('hide_absolute_value_field'); }, render_buttons: function (frm) { frm.page.clear_inner_toolbar(); @@ -58,5 +59,20 @@ frappe.ui.form.on("Print Format", { frm.set_value('show_section_headings', value); frm.set_value('line_breaks', value); frm.trigger('render_buttons'); + }, + doc_type: function (frm) { + frm.trigger('hide_absolute_value_field'); + }, + hide_absolute_value_field: function (frm) { + // TODO: make it work with frm.doc.doc_type + // Problem: frm isn't updated in some random cases + const doctype = locals[frm.doc.doctype][frm.doc.name]; + if (doctype) { + frappe.model.with_doctype(doctype, () => { + const meta = frappe.get_meta(doctype); + const has_int_float_currency_field = meta.fields.filter(df => in_list(['Int', 'Float', 'Currency'], df.fieldtype)); + frm.toggle_display('absolute_value', has_int_float_currency_field.length); + }); + } } -}) +}); diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json index 3867ce4502..6e64e802c9 100644 --- a/frappe/printing/doctype/print_format/print_format.json +++ b/frappe/printing/doctype/print_format/print_format.json @@ -200,6 +200,8 @@ }, { "default": "0", + "depends_on": "doc_type", + "description": "If checked, negative numberic values of Currency, Quantity or Count would be shown as positive", "fieldname": "absolute_value", "fieldtype": "Check", "label": "Show absolute values" @@ -209,7 +211,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-30 15:26:35.605213", + "modified": "2020-12-10 18:58:55.598269", "modified_by": "Administrator", "module": "Printing", "name": "Print Format",