From d08a332a85c0013125936f2c98fed9be352eccab Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Thu, 30 Dec 2021 14:50:33 +0530 Subject: [PATCH 001/139] feat: Phone Field Control Type --- frappe/boot.py | 7 + frappe/core/doctype/docfield/docfield.json | 1085 +++++++++-------- .../doctype/custom_field/custom_field.json | 4 +- .../customize_form_field.json | 4 +- frappe/database/mariadb/database.py | 3 +- frappe/database/postgres/database.py | 3 +- frappe/model/__init__.py | 3 +- frappe/model/base_document.py | 12 + frappe/model/meta.py | 3 + .../public/js/frappe/form/controls/control.js | 1 + .../public/js/frappe/form/controls/phone.js | 159 +++ .../js/frappe/phone_picker/phone_picker.js | 92 ++ frappe/public/js/frappe/utils/utils.js | 13 + frappe/public/scss/common/controls.scss | 1 + frappe/public/scss/common/phone_picker.scss | 119 ++ 15 files changed, 960 insertions(+), 549 deletions(-) create mode 100644 frappe/public/js/frappe/form/controls/phone.js create mode 100644 frappe/public/js/frappe/phone_picker/phone_picker.js create mode 100644 frappe/public/scss/common/phone_picker.scss diff --git a/frappe/boot.py b/frappe/boot.py index cf2b914436..026c0d6f16 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -58,6 +58,7 @@ def get_bootinfo(): bootinfo.home_folder = frappe.db.get_value("File", {"is_home_folder": 1}) bootinfo.navbar_settings = get_navbar_settings() bootinfo.notification_settings = get_notification_settings() + get_country_codes(bootinfo) # ipinfo if frappe.session.data.get('ipinfo'): @@ -324,3 +325,9 @@ def get_desk_settings(): def get_notification_settings(): return frappe.get_cached_doc('Notification Settings', frappe.session.user) + +def get_country_codes(bootinfo): + country_codes = { + "United States": {"isd":"+1","code":"us" }, + "India": {"isd":"+91","code":"in" }} + bootinfo.country_codes = frappe._dict(country_codes) diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index 6910d615d3..56c3ff6037 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -1,543 +1,544 @@ { - "actions": [], - "autoname": "hash", - "creation": "2013-02-22 01:27:33", - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "label_and_type", - "label", - "fieldtype", - "fieldname", - "precision", - "length", - "non_negative", - "hide_days", - "hide_seconds", - "reqd", - "search_index", - "column_break_18", - "options", - "defaults_section", - "default", - "column_break_6", - "fetch_from", - "fetch_if_empty", - "visibility_section", - "hidden", - "bold", - "allow_in_quick_entry", - "translatable", - "print_hide", - "print_hide_if_no_value", - "report_hide", - "column_break_28", - "depends_on", - "collapsible", - "collapsible_depends_on", - "hide_border", - "list__search_settings_section", - "in_list_view", - "in_standard_filter", - "in_preview", - "column_break_35", - "in_filter", - "in_global_search", - "permissions", - "read_only", - "allow_on_submit", - "ignore_user_permissions", - "allow_bulk_edit", - "column_break_13", - "permlevel", - "ignore_xss_filter", - "constraints_section", - "unique", - "no_copy", - "set_only_once", - "remember_last_selected_value", - "column_break_38", - "mandatory_depends_on", - "read_only_depends_on", - "display", - "print_width", - "width", - "max_height", - "columns", - "column_break_22", - "description", - "oldfieldname", - "oldfieldtype" - ], - "fields": [{ - "fieldname": "label_and_type", - "fieldtype": "Section Break" - }, - { - "bold": 1, - "fieldname": "label", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Label", - "oldfieldname": "label", - "oldfieldtype": "Data", - "print_width": "163", - "search_index": 1, - "width": "163" - }, - { - "bold": 1, - "default": "Data", - "fieldname": "fieldtype", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Type", - "oldfieldname": "fieldtype", - "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break", - "reqd": 1, - "search_index": 1 - }, - { - "bold": 1, - "fieldname": "fieldname", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Name", - "oldfieldname": "fieldname", - "oldfieldtype": "Data", - "search_index": 1 - }, - { - "default": "0", - "depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)", - "fieldname": "reqd", - "fieldtype": "Check", - "in_list_view": 1, - "label": "Mandatory", - "oldfieldname": "reqd", - "oldfieldtype": "Check", - "print_width": "50px", - "width": "50px" - }, - { - "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)", - "description": "Set non-standard precision for a Float or Currency field", - "fieldname": "precision", - "fieldtype": "Select", - "label": "Precision", - "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9", - "print_hide": 1 - }, - { - "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)", - "fieldname": "length", - "fieldtype": "Int", - "label": "Length" - }, - { - "default": "0", - "fieldname": "search_index", - "fieldtype": "Check", - "label": "Index", - "oldfieldname": "search_index", - "oldfieldtype": "Check", - "print_width": "50px", - "width": "50px" - }, - { - "default": "0", - "fieldname": "in_list_view", - "fieldtype": "Check", - "label": "In List View", - "print_width": "70px", - "width": "70px" - }, - { - "default": "0", - "fieldname": "in_standard_filter", - "fieldtype": "Check", - "label": "In List Filter" - }, - { - "default": "0", - "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)", - "fieldname": "in_global_search", - "fieldtype": "Check", - "label": "In Global Search" - }, - { - "default": "0", - "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);", - "fieldname": "in_preview", - "fieldtype": "Check", - "label": "In Preview" - }, - { - "default": "0", - "fieldname": "allow_in_quick_entry", - "fieldtype": "Check", - "label": "Allow in Quick Entry" - }, - { - "default": "0", - "fieldname": "bold", - "fieldtype": "Check", - "label": "Bold" - }, - { - "default": "0", - "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)", - "fieldname": "translatable", - "fieldtype": "Check", - "label": "Translatable" - }, - { - "default": "0", - "depends_on": "eval:doc.fieldtype===\"Section Break\"", - "fieldname": "collapsible", - "fieldtype": "Check", - "label": "Collapsible", - "length": 255 - }, - { - "depends_on": "eval:doc.fieldtype==\"Section Break\" && doc.collapsible", - "fieldname": "collapsible_depends_on", - "fieldtype": "Code", - "label": "Collapsible Depends On (JS)", - "max_height": "3rem", - "options": "JS" - }, - { - "fieldname": "column_break_6", - "fieldtype": "Column Break" - }, - { - "description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.", - "fieldname": "options", - "fieldtype": "Small Text", - "in_list_view": 1, - "label": "Options", - "oldfieldname": "options", - "oldfieldtype": "Text" - }, - { - "fieldname": "default", - "fieldtype": "Small Text", - "label": "Default", - "max_height": "3rem", - "oldfieldname": "default", - "oldfieldtype": "Text" - }, - { - "fieldname": "fetch_from", - "fieldtype": "Small Text", - "label": "Fetch From" - }, - { - "default": "0", - "fieldname": "fetch_if_empty", - "fieldtype": "Check", - "label": "Fetch only if value is not set" - }, - { - "fieldname": "permissions", - "fieldtype": "Section Break", - "label": "Permissions" - }, - { - "fieldname": "depends_on", - "fieldtype": "Code", - "label": "Display Depends On (JS)", - "length": 255, - "max_height": "3rem", - "oldfieldname": "depends_on", - "oldfieldtype": "Data", - "options": "JS" - }, - { - "default": "0", - "fieldname": "hidden", - "fieldtype": "Check", - "label": "Hidden", - "oldfieldname": "hidden", - "oldfieldtype": "Check", - "print_width": "50px", - "width": "50px" - }, - { - "default": "0", - "fieldname": "read_only", - "fieldtype": "Check", - "label": "Read Only", - "print_width": "50px", - "width": "50px" - }, - { - "default": "0", - "fieldname": "unique", - "fieldtype": "Check", - "label": "Unique" - }, - { - "default": "0", - "fieldname": "set_only_once", - "fieldtype": "Check", - "label": "Set only once" - }, - { - "default": "0", - "depends_on": "eval: doc.fieldtype == \"Table\"", - "fieldname": "allow_bulk_edit", - "fieldtype": "Check", - "label": "Allow Bulk Edit" - }, - { - "fieldname": "column_break_13", - "fieldtype": "Column Break" - }, - { - "default": "0", - "fieldname": "permlevel", - "fieldtype": "Int", - "label": "Perm Level", - "oldfieldname": "permlevel", - "oldfieldtype": "Int", - "print_width": "50px", - "width": "50px" - }, - { - "default": "0", - "fieldname": "ignore_user_permissions", - "fieldtype": "Check", - "label": "Ignore User Permissions" - }, - { - "default": "0", - "depends_on": "eval: parent.is_submittable", - "fieldname": "allow_on_submit", - "fieldtype": "Check", - "label": "Allow on Submit", - "oldfieldname": "allow_on_submit", - "oldfieldtype": "Check", - "print_width": "50px", - "width": "50px" - }, - { - "default": "0", - "fieldname": "report_hide", - "fieldtype": "Check", - "label": "Report Hide", - "oldfieldname": "report_hide", - "oldfieldtype": "Check", - "print_width": "50px", - "width": "50px" - }, - { - "default": "0", - "depends_on": "eval:(doc.fieldtype == 'Link')", - "fieldname": "remember_last_selected_value", - "fieldtype": "Check", - "label": "Remember Last Selected Value" - }, - { - "default": "0", - "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field", - "fieldname": "ignore_xss_filter", - "fieldtype": "Check", - "label": "Ignore XSS Filter" - }, - { - "fieldname": "display", - "fieldtype": "Section Break", - "label": "Display" - }, - { - "default": "0", - "fieldname": "in_filter", - "fieldtype": "Check", - "label": "In Filter", - "oldfieldname": "in_filter", - "oldfieldtype": "Check", - "print_width": "50px", - "width": "50px" - }, - { - "default": "0", - "fieldname": "no_copy", - "fieldtype": "Check", - "label": "No Copy", - "oldfieldname": "no_copy", - "oldfieldtype": "Check", - "print_width": "50px", - "width": "50px" - }, - { - "default": "0", - "fieldname": "print_hide", - "fieldtype": "Check", - "label": "Print Hide", - "oldfieldname": "print_hide", - "oldfieldtype": "Check", - "print_width": "50px", - "width": "50px" - }, - { - "default": "0", - "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1", - "fieldname": "print_hide_if_no_value", - "fieldtype": "Check", - "label": "Print Hide If No Value" - }, - { - "fieldname": "print_width", - "fieldtype": "Data", - "label": "Print Width", - "length": 10 - }, - { - "fieldname": "width", - "fieldtype": "Data", - "label": "Width", - "length": 10, - "oldfieldname": "width", - "oldfieldtype": "Data", - "print_width": "50px", - "width": "50px" - }, - { - "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)", - "fieldname": "columns", - "fieldtype": "Int", - "label": "Columns" - }, - { - "fieldname": "column_break_22", - "fieldtype": "Column Break" - }, - { - "fieldname": "description", - "fieldtype": "Small Text", - "in_list_view": 1, - "label": "Description", - "oldfieldname": "description", - "oldfieldtype": "Text", - "print_width": "300px", - "width": "300px" - }, - { - "fieldname": "oldfieldname", - "fieldtype": "Data", - "hidden": 1, - "oldfieldname": "oldfieldname", - "oldfieldtype": "Data" - }, - { - "fieldname": "oldfieldtype", - "fieldtype": "Data", - "hidden": 1, - "oldfieldname": "oldfieldtype", - "oldfieldtype": "Data" - }, - { - "fieldname": "mandatory_depends_on", - "fieldtype": "Code", - "label": "Mandatory Depends On (JS)", - "max_height": "3rem", - "options": "JS" - }, - { - "fieldname": "read_only_depends_on", - "fieldtype": "Code", - "label": "Read Only Depends On (JS)", - "max_height": "3rem", - "options": "JS" - }, - { - "fieldname": "column_break_38", - "fieldtype": "Column Break" - }, - { - "default": "0", - "depends_on": "eval:doc.fieldtype=='Duration'", - "fieldname": "hide_days", - "fieldtype": "Check", - "label": "Hide Days" - }, - { - "default": "0", - "depends_on": "eval:doc.fieldtype=='Duration'", - "fieldname": "hide_seconds", - "fieldtype": "Check", - "label": "Hide Seconds" - }, - { - "default": "0", - "depends_on": "eval:doc.fieldtype=='Section Break'", - "fieldname": "hide_border", - "fieldtype": "Check", - "label": "Hide Border" - }, - { - "default": "0", - "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)", - "fieldname": "non_negative", - "fieldtype": "Check", - "label": "Non Negative" - }, - { - "fieldname": "column_break_18", - "fieldtype": "Column Break" - }, - { - "fieldname": "defaults_section", - "fieldtype": "Section Break", - "label": "Defaults", - "max_height": "2rem" - }, - { - "fieldname": "visibility_section", - "fieldtype": "Section Break", - "label": "Visibility" - }, - { - "fieldname": "column_break_28", - "fieldtype": "Column Break" - }, - { - "fieldname": "constraints_section", - "fieldtype": "Section Break", - "label": "Constraints" - }, - { - "fieldname": "max_height", - "fieldtype": "Data", - "label": "Max Height", - "length": 10 - }, - { - "fieldname": "list__search_settings_section", - "fieldtype": "Section Break", - "label": "List / Search Settings" - }, - { - "fieldname": "column_break_35", - "fieldtype": "Column Break" - } - ], - "idx": 1, - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2021-09-04 19:41:23.684094", - "modified_by": "Administrator", - "module": "Core", - "name": "DocField", - "naming_rule": "Random", - "owner": "Administrator", - "permissions": [], - "sort_field": "modified", - "sort_order": "ASC" -} \ No newline at end of file + "actions": [], + "autoname": "hash", + "creation": "2013-02-22 01:27:33", + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label_and_type", + "label", + "fieldtype", + "fieldname", + "precision", + "length", + "non_negative", + "hide_days", + "hide_seconds", + "reqd", + "search_index", + "column_break_18", + "options", + "defaults_section", + "default", + "column_break_6", + "fetch_from", + "fetch_if_empty", + "visibility_section", + "hidden", + "bold", + "allow_in_quick_entry", + "translatable", + "print_hide", + "print_hide_if_no_value", + "report_hide", + "column_break_28", + "depends_on", + "collapsible", + "collapsible_depends_on", + "hide_border", + "list__search_settings_section", + "in_list_view", + "in_standard_filter", + "in_preview", + "column_break_35", + "in_filter", + "in_global_search", + "permissions", + "read_only", + "allow_on_submit", + "ignore_user_permissions", + "allow_bulk_edit", + "column_break_13", + "permlevel", + "ignore_xss_filter", + "constraints_section", + "unique", + "no_copy", + "set_only_once", + "remember_last_selected_value", + "column_break_38", + "mandatory_depends_on", + "read_only_depends_on", + "display", + "print_width", + "width", + "max_height", + "columns", + "column_break_22", + "description", + "oldfieldname", + "oldfieldtype" + ], + "fields": [ + { + "fieldname": "label_and_type", + "fieldtype": "Section Break" + }, + { + "bold": 1, + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "oldfieldname": "label", + "oldfieldtype": "Data", + "print_width": "163", + "search_index": 1, + "width": "163" + }, + { + "bold": 1, + "default": "Data", + "fieldname": "fieldtype", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Type", + "oldfieldname": "fieldtype", + "oldfieldtype": "Select", + "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nPhone\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break", + "reqd": 1, + "search_index": 1 + }, + { + "bold": 1, + "fieldname": "fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Name", + "oldfieldname": "fieldname", + "oldfieldtype": "Data", + "search_index": 1 + }, + { + "default": "0", + "depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)", + "fieldname": "reqd", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Mandatory", + "oldfieldname": "reqd", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)", + "description": "Set non-standard precision for a Float or Currency field", + "fieldname": "precision", + "fieldtype": "Select", + "label": "Precision", + "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9", + "print_hide": 1 + }, + { + "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)", + "fieldname": "length", + "fieldtype": "Int", + "label": "Length" + }, + { + "default": "0", + "fieldname": "search_index", + "fieldtype": "Check", + "label": "Index", + "oldfieldname": "search_index", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "in_list_view", + "fieldtype": "Check", + "label": "In List View", + "print_width": "70px", + "width": "70px" + }, + { + "default": "0", + "fieldname": "in_standard_filter", + "fieldtype": "Check", + "label": "In List Filter" + }, + { + "default": "0", + "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)", + "fieldname": "in_global_search", + "fieldtype": "Check", + "label": "In Global Search" + }, + { + "default": "0", + "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);", + "fieldname": "in_preview", + "fieldtype": "Check", + "label": "In Preview" + }, + { + "default": "0", + "fieldname": "allow_in_quick_entry", + "fieldtype": "Check", + "label": "Allow in Quick Entry" + }, + { + "default": "0", + "fieldname": "bold", + "fieldtype": "Check", + "label": "Bold" + }, + { + "default": "0", + "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)", + "fieldname": "translatable", + "fieldtype": "Check", + "label": "Translatable" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype===\"Section Break\"", + "fieldname": "collapsible", + "fieldtype": "Check", + "label": "Collapsible", + "length": 255 + }, + { + "depends_on": "eval:doc.fieldtype==\"Section Break\" && doc.collapsible", + "fieldname": "collapsible_depends_on", + "fieldtype": "Code", + "label": "Collapsible Depends On (JS)", + "max_height": "3rem", + "options": "JS" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.", + "fieldname": "options", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Options", + "oldfieldname": "options", + "oldfieldtype": "Text" + }, + { + "fieldname": "default", + "fieldtype": "Small Text", + "label": "Default", + "max_height": "3rem", + "oldfieldname": "default", + "oldfieldtype": "Text" + }, + { + "fieldname": "fetch_from", + "fieldtype": "Small Text", + "label": "Fetch From" + }, + { + "default": "0", + "fieldname": "fetch_if_empty", + "fieldtype": "Check", + "label": "Fetch only if value is not set" + }, + { + "fieldname": "permissions", + "fieldtype": "Section Break", + "label": "Permissions" + }, + { + "fieldname": "depends_on", + "fieldtype": "Code", + "label": "Display Depends On (JS)", + "length": 255, + "max_height": "3rem", + "oldfieldname": "depends_on", + "oldfieldtype": "Data", + "options": "JS" + }, + { + "default": "0", + "fieldname": "hidden", + "fieldtype": "Check", + "label": "Hidden", + "oldfieldname": "hidden", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "read_only", + "fieldtype": "Check", + "label": "Read Only", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "unique", + "fieldtype": "Check", + "label": "Unique" + }, + { + "default": "0", + "fieldname": "set_only_once", + "fieldtype": "Check", + "label": "Set only once" + }, + { + "default": "0", + "depends_on": "eval: doc.fieldtype == \"Table\"", + "fieldname": "allow_bulk_edit", + "fieldtype": "Check", + "label": "Allow Bulk Edit" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "permlevel", + "fieldtype": "Int", + "label": "Perm Level", + "oldfieldname": "permlevel", + "oldfieldtype": "Int", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "ignore_user_permissions", + "fieldtype": "Check", + "label": "Ignore User Permissions" + }, + { + "default": "0", + "depends_on": "eval: parent.is_submittable", + "fieldname": "allow_on_submit", + "fieldtype": "Check", + "label": "Allow on Submit", + "oldfieldname": "allow_on_submit", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "report_hide", + "fieldtype": "Check", + "label": "Report Hide", + "oldfieldname": "report_hide", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "depends_on": "eval:(doc.fieldtype == 'Link')", + "fieldname": "remember_last_selected_value", + "fieldtype": "Check", + "label": "Remember Last Selected Value" + }, + { + "default": "0", + "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field", + "fieldname": "ignore_xss_filter", + "fieldtype": "Check", + "label": "Ignore XSS Filter" + }, + { + "fieldname": "display", + "fieldtype": "Section Break", + "label": "Display" + }, + { + "default": "0", + "fieldname": "in_filter", + "fieldtype": "Check", + "label": "In Filter", + "oldfieldname": "in_filter", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "no_copy", + "fieldtype": "Check", + "label": "No Copy", + "oldfieldname": "no_copy", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "print_hide", + "fieldtype": "Check", + "label": "Print Hide", + "oldfieldname": "print_hide", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1", + "fieldname": "print_hide_if_no_value", + "fieldtype": "Check", + "label": "Print Hide If No Value" + }, + { + "fieldname": "print_width", + "fieldtype": "Data", + "label": "Print Width", + "length": 10 + }, + { + "fieldname": "width", + "fieldtype": "Data", + "label": "Width", + "length": 10, + "oldfieldname": "width", + "oldfieldtype": "Data", + "print_width": "50px", + "width": "50px" + }, + { + "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)", + "fieldname": "columns", + "fieldtype": "Int", + "label": "Columns" + }, + { + "fieldname": "column_break_22", + "fieldtype": "Column Break" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Text", + "print_width": "300px", + "width": "300px" + }, + { + "fieldname": "oldfieldname", + "fieldtype": "Data", + "hidden": 1, + "oldfieldname": "oldfieldname", + "oldfieldtype": "Data" + }, + { + "fieldname": "oldfieldtype", + "fieldtype": "Data", + "hidden": 1, + "oldfieldname": "oldfieldtype", + "oldfieldtype": "Data" + }, + { + "fieldname": "mandatory_depends_on", + "fieldtype": "Code", + "label": "Mandatory Depends On (JS)", + "max_height": "3rem", + "options": "JS" + }, + { + "fieldname": "read_only_depends_on", + "fieldtype": "Code", + "label": "Read Only Depends On (JS)", + "max_height": "3rem", + "options": "JS" + }, + { + "fieldname": "column_break_38", + "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Duration'", + "fieldname": "hide_days", + "fieldtype": "Check", + "label": "Hide Days" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Duration'", + "fieldname": "hide_seconds", + "fieldtype": "Check", + "label": "Hide Seconds" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Section Break'", + "fieldname": "hide_border", + "fieldtype": "Check", + "label": "Hide Border" + }, + { + "default": "0", + "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)", + "fieldname": "non_negative", + "fieldtype": "Check", + "label": "Non Negative" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "fieldname": "defaults_section", + "fieldtype": "Section Break", + "label": "Defaults", + "max_height": "2rem" + }, + { + "fieldname": "visibility_section", + "fieldtype": "Section Break", + "label": "Visibility" + }, + { + "fieldname": "column_break_28", + "fieldtype": "Column Break" + }, + { + "fieldname": "constraints_section", + "fieldtype": "Section Break", + "label": "Constraints" + }, + { + "fieldname": "max_height", + "fieldtype": "Data", + "label": "Max Height", + "length": 10 + }, + { + "fieldname": "list__search_settings_section", + "fieldtype": "Section Break", + "label": "List / Search Settings" + }, + { + "fieldname": "column_break_35", + "fieldtype": "Column Break" + } + ], + "idx": 1, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-12-26 23:39:38.341443", + "modified_by": "Administrator", + "module": "Core", + "name": "DocField", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "ASC" +} diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json index 235f11aad8..516daf3521 100644 --- a/frappe/custom/doctype/custom_field/custom_field.json +++ b/frappe/custom/doctype/custom_field/custom_field.json @@ -120,7 +120,7 @@ "label": "Field Type", "oldfieldname": "fieldtype", "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break", + "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nPhone\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break", "reqd": 1 }, { @@ -455,4 +455,4 @@ "sort_field": "modified", "sort_order": "ASC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index 986b99a7af..971bc51f96 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -82,7 +82,7 @@ "label": "Type", "oldfieldname": "fieldtype", "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nTab Break", + "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nPhone\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nTab Break", "reqd": 1, "search_index": 1 }, @@ -436,4 +436,4 @@ "permissions": [], "sort_field": "modified", "sort_order": "ASC" -} \ No newline at end of file +} diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index 2f6d640743..5e838db842 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -52,7 +52,8 @@ class MariaDBDatabase(Database): 'Barcode': ('longtext', ''), 'Geolocation': ('longtext', ''), 'Duration': ('decimal', '21,9'), - 'Icon': ('varchar', self.VARCHAR_LEN) + 'Icon': ('varchar', self.VARCHAR_LEN), + 'Phone': ('varchar', self.VARCHAR_LEN) } def get_connection(self): diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index bfa5515111..71b5918d4a 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -62,7 +62,8 @@ class PostgresDatabase(Database): 'Barcode': ('text', ''), 'Geolocation': ('text', ''), 'Duration': ('decimal', '21,9'), - 'Icon': ('varchar', self.VARCHAR_LEN) + 'Icon': ('varchar', self.VARCHAR_LEN), + 'Phone': ('varchar', self.VARCHAR_LEN) } def get_connection(self): diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index b460db29a7..00486d3d87 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -35,7 +35,8 @@ data_fieldtypes = ( 'Barcode', 'Geolocation', 'Duration', - 'Icon' + 'Icon', + 'Phone' ) no_value_fields = ( diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 1826cca9a3..ac87e564ac 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -11,6 +11,7 @@ from frappe.model import display_fieldtypes from frappe.utils import (cint, flt, now, cstr, strip_html, sanitize_html, sanitize_email, cast_fieldtype) from frappe.utils.html_utils import unescape_html +import phonenumbers as ph max_positive_value = { 'smallint': 2 ** 15, @@ -652,6 +653,17 @@ class BaseDocument(object): from frappe.core.doctype.user.user import STANDARD_USERS # data_field options defined in frappe.model.data_field_options + for phone_field in self.meta.get_phone_fields(): + phone = self.get(phone_field.fieldname) + try: + phone = ph.parse(phone) + except Exception as e: + if e.error_type == 1: + frappe.throw(_("The entered value is not a phone number."), title="Invalid Number") + frappe.throw(_("Please select a country code."), title = _("Country Code Required")) + if not ph.is_valid_number(phone): + frappe.throw('This is not a valid phone number', title = "Invalid Number") + for data_field in self.meta.get_data_fields(): data = self.get(data_field.fieldname) data_field_options = data_field.get("options") diff --git a/frappe/model/meta.py b/frappe/model/meta.py index cd0d8e0f3a..7430c35946 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -130,6 +130,9 @@ class Meta(Document): def get_data_fields(self): return self.get("fields", {"fieldtype": "Data"}) + def get_phone_fields(self): + return self.get("fields", {"fieldtype": "Phone"}) + def get_dynamic_link_fields(self): if not hasattr(self, '_dynamic_link_fields'): self._dynamic_link_fields = self.get("fields", {"fieldtype": "Dynamic Link"}) diff --git a/frappe/public/js/frappe/form/controls/control.js b/frappe/public/js/frappe/form/controls/control.js index bd04938e35..578ddd3276 100644 --- a/frappe/public/js/frappe/form/controls/control.js +++ b/frappe/public/js/frappe/form/controls/control.js @@ -40,6 +40,7 @@ import './multiselect_list'; import './rating'; import './duration'; import './icon'; +import './phone' frappe.ui.form.make_control = function (opts) { var control_class_name = "Control" + opts.df.fieldtype.replace(/ /g, ""); diff --git a/frappe/public/js/frappe/form/controls/phone.js b/frappe/public/js/frappe/form/controls/phone.js new file mode 100644 index 0000000000..c00c966539 --- /dev/null +++ b/frappe/public/js/frappe/form/controls/phone.js @@ -0,0 +1,159 @@ + +import Picker from '../../phone_picker/phone_picker'; + +frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlData { + + make_input() { + super.make_input(); + this.make_icon_input(); + this.input_events(); + } + + input_events() { + // Replaces code when selected and removes previously selected. + this.picker.on_change = (country) => { + const country_code = frappe.boot.country_codes[country].code; + const country_isd = frappe.boot.country_codes[country].isd; + this.selected_icon.find('use').attr('href', '#'+country_code) + this.$icon = this.selected_icon.find('svg'); + if (this.$icon.hasClass('icon-sm')) { + this.$icon.removeClass('icon-sm'); + this.selected_icon.find('svg').addClass('flag-md') + } + if (!this.$isd.length) { + this.selected_icon.append($(` ${country_isd}`)) + } else { + this.$isd.text(country_isd) + } + // this.selected_icon.text('+' + this.get_country(country)) + if(this.$input.val()) { + this.set_formatted_input(this.get_country(country) +'-'+ this.$input.val()) + } + }; + + this.$wrapper.find('.selected-phone').on('click', (e) => { + this.$wrapper.popover('toggle'); + e.stopPropagation(); + + $('body').on('click.phone-popover', (ev) => { + if (!$(ev.target).parents().is('.popover')) { + this.$wrapper.popover('hide'); + } + }); + $(window).on('hashchange.phone-popover', () => { + this.$wrapper.popover('hide'); + }); + }); + } + + make_icon_input() { + let picker_wrapper = $('
'); + this.picker = new Picker({ + parent: picker_wrapper, + countries: frappe.boot.country_codes + }); + + this.$wrapper.popover({ + trigger: 'manual', + offset: `${-this.$wrapper.width() / 4.5}, 5`, + boundary: 'viewport', + placement: 'bottom', + template: ` +
+
+
+
+ `, + content: () => picker_wrapper, + html: true + }).on('show.bs.popover', () => { + setTimeout(() => { + this.picker.refresh(); + }, 10); + }).on('hidden.bs.popover', () => { + $('body').off('click.phone-popover'); + $(window).off('hashchange.phone-popover'); + }); + + // Default icon when nothing is selected. + this.selected_icon = this.$wrapper.find('.selected-phone'); + let input_value = this.get_input_value() + if (!this.selected_icon.length) { + this.selected_icon = $(`
${frappe.utils.icon("down", "sm")}
`); + this.selected_icon.insertAfter(this.$input); + this.selected_icon.append($(``)) + this.$isd = this.selected_icon.find('.country'); + if(input_value && input_value.split("-").length == 2) { + this.$isd.text(this.value.split("-")[0]) + } + } + } + + refresh() { + super.refresh(); + + // Previously opened doc values get fetched. + if(!this.value && this.frm.is_new()) { + this.$input.val(""); + this.$wrapper.find('.country').text("") + this.selected_icon.find('use').attr('href', '#icon-down') + this.flag = this.selected_icon.find('svg'); + let has_flag = this.flag.hasClass('flag-md'); + if (has_flag) { + this.flag.toggleClass('flag-md'); + this.flag.toggleClass('icon-sm'); + } + } + + if(this.value && this.value.split("-").length == 2) { + let isd = this.value.split("-")[0]; + let country_data = frappe.boot.country_codes; + + for (const country in country_data) { + if (Object.values(country_data[country]).includes(isd)) { + let code = country_data[country].code; + this.change_flag(code); + } + } + this.picker.set_country(isd); + this.picker.refresh(); + if (this.picker.country && this.picker.country !== this.$isd.text()) { + this.$isd.length && this.$isd.text(isd) + } + } + } + + + set_formatted_input(value) { + if(value && value.includes('-')) { + this.set_model_value(value) + } else if(this.$isd.text().trim() && this.value) { + let code_number = this.$isd.text() + '-' + value; + this.set_model_value(code_number) + } + this.$input && value && this.$input.val(value.split("-").pop()) + } + + reset_icon() { + + } + + change_flag(country_code) { + this.selected_icon.find('use').attr('href', '#'+country_code) + this.$icon = this.selected_icon.find('svg'); + if (this.$icon.hasClass('icon-sm')) { + this.$icon.removeClass('icon-sm'); + this.selected_icon.find('svg').addClass('flag-md') + } + } + + get_country(country=null) { + const country_codes = frappe.boot.country_codes; + return country_codes[country].isd; + } + get_country_flag(country) { + const country_codes = frappe.boot.country_codes; + let code = country_codes[country].code; + return frappe.utils.flag(code, "md") + } +}; diff --git a/frappe/public/js/frappe/phone_picker/phone_picker.js b/frappe/public/js/frappe/phone_picker/phone_picker.js new file mode 100644 index 0000000000..12285cca7e --- /dev/null +++ b/frappe/public/js/frappe/phone_picker/phone_picker.js @@ -0,0 +1,92 @@ +class Picker { + constructor(opts) { + this.parent = opts.parent; + this.width = opts.width; + this.height = opts.height; + this.country = opts.country; + opts.country && this.set_country(opts.country); + this.countries = opts.countries; + this.setup_picker(); + } + + refresh() { + this.update_icon_selected(true); + } + + setup_picker() { + this.icon_picker_wrapper = $(` +
+
+ + ${frappe.utils.icon('search', "sm")} +
+
+
+
+
+ `); + this.parent.append(this.icon_picker_wrapper); + this.icon_wrapper = this.icon_picker_wrapper.find('.phones'); + this.search_input = this.icon_picker_wrapper.find('.search-phones > input'); + this.refresh(); + this.setup_icons(); + } + + setup_icons() { + + Object.entries(this.countries).forEach(([country, info]) => { + let $country = $(`
${frappe.utils.flag(info.code, "md")}${country}
`); + this.icon_wrapper.append($country); + const set_values = () => { + this.set_country(country); + this.update_icon_selected(); + }; + $country.on('click', () => { + set_values(); + }); + $country.hover(() => { + $country.toggleClass("bg-gray-100"); + }); + this.search_input.keydown((e) => { + const key_code = e.keyCode; + if ([13, 32].includes(key_code)) { + e.preventDefault(); + set_values(); + } + }); + this.search_input.keyup((e) => { + e.preventDefault(); + this.filter_icons(); + }); + + this.search_input.on('search', () => { + this.filter_icons(); + }); + }); + } + + filter_icons() { + let value = this.search_input.val(); + if (!value) { + this.icon_wrapper.find(".phone-wrapper").removeClass('hidden'); + } else { + this.icon_wrapper.find(".phone-wrapper").addClass('hidden'); + this.icon_wrapper.find(`.phone-wrapper[id*='${value}']`).removeClass('hidden'); + } + } + + update_icon_selected(silent) { + !silent && this.on_change && this.on_change(this.get_country()); + } + + set_country(country) { + this.country = country || ''; + } + + get_country() { + if (!this.country) return frappe.utils.icon("down", "sm") + return this.country; + } +} + +export default Picker; diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 2baff996c6..8b3dc80ce6 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1156,6 +1156,19 @@ Object.assign(frappe.utils, { `; }, + flag(icon_name, size="sm", icon_class="", icon_style="", svg_class="") { + let size_class = ""; + + if (typeof size == "object") { + icon_style += ` width: ${size.width}; height: ${size.height}`; + } else { + size_class = `flag-${size}`; + } + return ` + + `; + }, + make_chart(wrapper, custom_options={}) { let chart_args = { type: 'bar', diff --git a/frappe/public/scss/common/controls.scss b/frappe/public/scss/common/controls.scss index 954916c911..15fd493096 100644 --- a/frappe/public/scss/common/controls.scss +++ b/frappe/public/scss/common/controls.scss @@ -2,6 +2,7 @@ @import "color_picker"; @import "icon_picker"; @import "datepicker"; +@import "phone_picker"; // password .form-control[data-fieldtype="Password"] { diff --git a/frappe/public/scss/common/phone_picker.scss b/frappe/public/scss/common/phone_picker.scss new file mode 100644 index 0000000000..74a61cbbe8 --- /dev/null +++ b/frappe/public/scss/common/phone_picker.scss @@ -0,0 +1,119 @@ +.phone-picker { + font-size: var(--text-xs); + color: var(--text-muted); + --phone-picker-width: 210px; + width: var(--phone-picker-width); + .phones { + margin-top: 10px; + display: flex; + flex-wrap: wrap; + overflow-y: scroll; + max-height: 210px; + cursor: pointer; + + /* Hide scrollbar for IE, Edge and Firefox */ + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + + /* Hide scrollbar for Chrome, Safari and Opera */ + &::-webkit-scrollbar { + display: none; + } + + .phone-wrapper { + display: flex; + width: 210px; + height: 30px; + text-align: center; + align-items: center; + border-radius: 0.375rem; + padding: 0.5rem; + + .country { + display: flex; + margin-left: 0.6rem; + flex-grow: 1; + } + } + } + + .search-phones { + position: relative; + + input[type='search'] { + height: inherit; + padding-left: 30px; + } + + .search-phone { + position: absolute; + top: 7px; + left: 7px; + } + } +} +.phone-picker-popover { + left: -20px !important; + .picker-arrow { + left: 15px !important; + } +} +.frappe-control[data-fieldtype='Phone'] + { + input { + padding-left: 70px; + } + .selected-phone { + display: flex; + cursor: pointer; + width: 52px; + height: 18px; + border-radius: 5px; + position: absolute; + top: calc(50% + 2.6px); + left: 8px; + content: ' '; + + .country { + display: flex; + margin-left: 0.6rem; + flex-grow: 1; + } + + } + .like-disabled-input { + .phone-value { + padding-left: 25px; + } + .selected-phone { + top: 20%; + cursor: default; + } + } +} + +.data-row.row { + .selected-phone { + top: calc(50% - 11px); + z-index: 2; + } +} + +.bg-gray-100 { + --tw-bg-opacity: 1; + background-color: rgba(244,245,246,var(--tw-bg-opacity)); +} + +.dt-cell__content { + .selected-phone { + display: contents; + } +} + +.dt-cell__edit, .filter-field { + .selected-phone { + top: 5px !important; + } +} + + From 5cc1da476dcac0952c24aeafbc7d68e57d768ebc Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Thu, 30 Dec 2021 14:41:54 +0530 Subject: [PATCH 002/139] chore: added svg file for country flags --- frappe/public/icons/timeless/flags.svg | 18341 +++++++++++++++++++++++ frappe/www/app.html | 1 + 2 files changed, 18342 insertions(+) create mode 100644 frappe/public/icons/timeless/flags.svg diff --git a/frappe/public/icons/timeless/flags.svg b/frappe/public/icons/timeless/flags.svg new file mode 100644 index 0000000000..d57fda3d0e --- /dev/null +++ b/frappe/public/icons/timeless/flags.svg @@ -0,0 +1,18341 @@ + + diff --git a/frappe/www/app.html b/frappe/www/app.html index 68a6dc8e86..8fcaf581e2 100644 --- a/frappe/www/app.html +++ b/frappe/www/app.html @@ -26,6 +26,7 @@ {% include "public/icons/timeless/symbol-defs.svg" %} + {% include "public/icons/timeless/flags.svg" %}
From aa84930dac68776f013a2c442f80c242d72a880d Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Thu, 30 Dec 2021 15:12:54 +0530 Subject: [PATCH 003/139] fix: made the search for country case insensitive --- frappe/public/js/frappe/phone_picker/phone_picker.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/phone_picker/phone_picker.js b/frappe/public/js/frappe/phone_picker/phone_picker.js index 12285cca7e..0d9910e9a2 100644 --- a/frappe/public/js/frappe/phone_picker/phone_picker.js +++ b/frappe/public/js/frappe/phone_picker/phone_picker.js @@ -35,7 +35,7 @@ class Picker { setup_icons() { Object.entries(this.countries).forEach(([country, info]) => { - let $country = $(`
${frappe.utils.flag(info.code, "md")}${country}
`); + let $country = $(`
${frappe.utils.flag(info.code, "md")}${country}
`); this.icon_wrapper.append($country); const set_values = () => { this.set_country(country); @@ -49,7 +49,7 @@ class Picker { }); this.search_input.keydown((e) => { const key_code = e.keyCode; - if ([13, 32].includes(key_code)) { + if ([13].includes(key_code)) { e.preventDefault(); set_values(); } @@ -71,7 +71,7 @@ class Picker { this.icon_wrapper.find(".phone-wrapper").removeClass('hidden'); } else { this.icon_wrapper.find(".phone-wrapper").addClass('hidden'); - this.icon_wrapper.find(`.phone-wrapper[id*='${value}']`).removeClass('hidden'); + this.icon_wrapper.find(`.phone-wrapper[id*='${value.toLowerCase()}']`).removeClass('hidden'); } } From 3521cc97f137a981df07c3ac3683a2fcd83c04fd Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Mon, 3 Jan 2022 10:09:28 +0530 Subject: [PATCH 004/139] fix: exclude flags in icon picker --- frappe/public/js/frappe/form/controls/icon.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/controls/icon.js b/frappe/public/js/frappe/form/controls/icon.js index 7ab2e11f24..000731fa52 100644 --- a/frappe/public/js/frappe/form/controls/icon.js +++ b/frappe/public/js/frappe/form/controls/icon.js @@ -11,7 +11,7 @@ frappe.ui.form.ControlIcon = class ControlIcon extends frappe.ui.form.ControlDat get_all_icons() { frappe.symbols = []; $("#frappe-symbols > symbol[id]").each(function() { - frappe.symbols.push(this.id.replace('icon-', '')); + this.id.includes('icon-') && frappe.symbols.push(this.id.replace('icon-', '')); }); } From 6f8a53cdfdedddf7d31298c559458396bd8e9d43 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Mon, 3 Jan 2022 10:11:44 +0530 Subject: [PATCH 005/139] fix(ui): added isd code in the picker --- .../js/frappe/phone_picker/phone_picker.js | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/frappe/public/js/frappe/phone_picker/phone_picker.js b/frappe/public/js/frappe/phone_picker/phone_picker.js index 0d9910e9a2..9e60acc257 100644 --- a/frappe/public/js/frappe/phone_picker/phone_picker.js +++ b/frappe/public/js/frappe/phone_picker/phone_picker.js @@ -14,7 +14,7 @@ class Picker { } setup_picker() { - this.icon_picker_wrapper = $(` + this.phone_picker_wrapper = $(`
@@ -25,18 +25,19 @@ class Picker {
`); - this.parent.append(this.icon_picker_wrapper); - this.icon_wrapper = this.icon_picker_wrapper.find('.phones'); - this.search_input = this.icon_picker_wrapper.find('.search-phones > input'); + this.parent.append(this.phone_picker_wrapper); + this.phone_wrapper = this.phone_picker_wrapper.find('.phones'); + this.search_input = this.phone_picker_wrapper.find('.search-phones > input'); this.refresh(); - this.setup_icons(); + this.setup_countries(); } - setup_icons() { + setup_countries() { Object.entries(this.countries).forEach(([country, info]) => { - let $country = $(`
${frappe.utils.flag(info.code, "md")}${country}
`); - this.icon_wrapper.append($country); + let $country = $(`
${frappe.utils.flag(info.code, "md")} + ${country} (${info.isd})
`); + this.phone_wrapper.append($country); const set_values = () => { this.set_country(country); this.update_icon_selected(); @@ -68,10 +69,10 @@ class Picker { filter_icons() { let value = this.search_input.val(); if (!value) { - this.icon_wrapper.find(".phone-wrapper").removeClass('hidden'); + this.phone_wrapper.find(".phone-wrapper").removeClass('hidden'); } else { - this.icon_wrapper.find(".phone-wrapper").addClass('hidden'); - this.icon_wrapper.find(`.phone-wrapper[id*='${value.toLowerCase()}']`).removeClass('hidden'); + this.phone_wrapper.find(".phone-wrapper").addClass('hidden'); + this.phone_wrapper.find(`.phone-wrapper[id*='${value.toLowerCase()}']`).removeClass('hidden'); } } From eb314f2e959baf5f0fee3eafba8063c17eda6bf2 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Mon, 3 Jan 2022 13:18:09 +0530 Subject: [PATCH 006/139] chore: updated country_info.json with iso codes --- frappe/boot.py | 5 +- frappe/geo/country_info.json | 738 +++++++++++++++++++++++------------ 2 files changed, 493 insertions(+), 250 deletions(-) diff --git a/frappe/boot.py b/frappe/boot.py index 026c0d6f16..4b764dabfc 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -17,6 +17,7 @@ from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_p from frappe.model.base_document import get_controller from frappe.social.doctype.post.post import frequently_visited_links from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings, get_app_logo +from frappe.geo.country_info import get_all def get_bootinfo(): """build and return boot info""" @@ -327,7 +328,5 @@ def get_notification_settings(): return frappe.get_cached_doc('Notification Settings', frappe.session.user) def get_country_codes(bootinfo): - country_codes = { - "United States": {"isd":"+1","code":"us" }, - "India": {"isd":"+91","code":"in" }} + country_codes = get_all() bootinfo.country_codes = frappe._dict(country_codes) diff --git a/frappe/geo/country_info.json b/frappe/geo/country_info.json index 7ffdf0a8bf..72e3e0ba24 100644 --- a/frappe/geo/country_info.json +++ b/frappe/geo/country_info.json @@ -8,7 +8,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Kabul" - ] + ], + "iso": "+93" }, "Albania": { "code": "al", @@ -20,7 +21,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Tirane" - ] + ], + "iso": "+355" }, "Algeria": { "code": "dz", @@ -32,11 +34,13 @@ "number_format": "#,###.##", "timezones": [ "Africa/Algiers" - ] + ], + "iso": "+213" }, "American Samoa": { "code": "as", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+1684" }, "Andorra": { "code": "ad", @@ -48,7 +52,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Andorra" - ] + ], + "iso": "+376" }, "Angola": { "code": "ao", @@ -60,7 +65,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Luanda" - ] + ], + "iso": "+244" }, "Anguilla": { "code": "ai", @@ -72,7 +78,8 @@ "number_format": "#,###.##", "timezones": [ "America/Anguilla" - ] + ], + "iso": "+1264" }, "Antarctica": { "code": "aq", @@ -88,7 +95,8 @@ "Antarctica/Rothera", "Antarctica/Syowa", "Antarctica/Vostok" - ] + ], + "iso": "+672" }, "Antigua and Barbuda": { "code": "ag", @@ -100,7 +108,8 @@ "number_format": "#,###.##", "timezones": [ "America/Antigua" - ] + ], + "iso": "+1268" }, "Argentina": { "code": "ar", @@ -123,7 +132,8 @@ "America/Argentina/San_Luis", "America/Argentina/Tucuman", "America/Argentina/Ushuaia" - ] + ], + "iso": "+54" }, "Armenia": { "code": "am", @@ -135,7 +145,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Yerevan" - ] + ], + "iso": "+374" }, "Aruba": { "code": "aw", @@ -147,7 +158,8 @@ "number_format": "#,###.##", "timezones": [ "America/Aruba" - ] + ], + "iso": "+297" }, "Australia": { "code": "au", @@ -170,7 +182,8 @@ "Australia/Melbourne", "Australia/Perth", "Australia/Sydney" - ] + ], + "iso": "+61" }, "Austria": { "code": "at", @@ -182,7 +195,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Vienna" - ] + ], + "iso": "+43" }, "Azerbaijan": { "code": "az", @@ -192,7 +206,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Baku" - ] + ], + "iso": "+994" }, "Bahamas": { "code": "bs", @@ -201,7 +216,8 @@ "number_format": "#,###.##", "timezones": [ "America/Nassau" - ] + ], + "iso": "+1242" }, "Bahrain": { "code": "bh", @@ -213,7 +229,8 @@ "number_format": "#,###.###", "timezones": [ "Asia/Bahrain" - ] + ], + "iso": "+973" }, "Bangladesh": { "code": "bd", @@ -225,7 +242,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Dhaka" - ] + ], + "iso": "+880" }, "Barbados": { "code": "bb", @@ -237,7 +255,8 @@ "number_format": "#,###.##", "timezones": [ "America/Barbados" - ] + ], + "iso": "+1246" }, "Belarus": { "code": "by", @@ -247,7 +266,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Minsk" - ] + ], + "iso": "+375" }, "Belgium": { "code": "be", @@ -259,7 +279,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Brussels" - ] + ], + "iso": "+32" }, "Belize": { "code": "bz", @@ -272,7 +293,8 @@ "number_format": "#,###.##", "timezones": [ "America/Belize" - ] + ], + "iso": "+501" }, "Benin": { "code": "bj", @@ -284,7 +306,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Porto-Novo" - ] + ], + "iso": "+229" }, "Bermuda": { "code": "bm", @@ -296,7 +319,8 @@ "number_format": "#,###.##", "timezones": [ "Atlantic/Bermuda" - ] + ], + "iso": "+1441" }, "Bhutan": { "code": "bt", @@ -308,13 +332,15 @@ "number_format": "#,###.##", "timezones": [ "Asia/Thimphu" - ] + ], + "iso": "+975" }, "Bolivia, Plurinational State of": { "code": "bo", "currency": "BOB", "currency_name": "Boliviano", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+591" }, "Bonaire, Sint Eustatius and Saba": { "code": "bq", @@ -329,7 +355,8 @@ "number_format": "#.###,##", "timezones": [ "Europe/Sarajevo" - ] + ], + "iso": "+387" }, "Botswana": { "code": "bw", @@ -341,11 +368,13 @@ "number_format": "#,###.##", "timezones": [ "Africa/Gaborone" - ] + ], + "iso": "+267" }, "Bouvet Island": { "code": "bv", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+47" }, "Brazil": { "code": "br", @@ -372,7 +401,8 @@ "America/Rio_Branco", "America/Santarem", "America/Sao_Paulo" - ] + ], + "iso": "+55" }, "British Indian Ocean Territory": { "code": "io", @@ -382,7 +412,8 @@ "number_format": "#,###.##", "timezones": [ "Indian/Chagos" - ] + ], + "iso": "+246" }, "Brunei Darussalam": { "code": "bn", @@ -391,7 +422,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Brunei" - ] + ], + "iso": "+673" }, "Bulgaria": { "code": "bg", @@ -403,7 +435,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Sofia" - ] + ], + "iso": "+359" }, "Burkina Faso": { "code": "bf", @@ -415,7 +448,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Ouagadougou" - ] + ], + "iso": "+226" }, "Burundi": { "code": "bi", @@ -427,7 +461,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Bujumbura" - ] + ], + "iso": "+257" }, "Cambodia": { "code": "kh", @@ -439,7 +474,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Phnom_Penh" - ] + ], + "iso": "+855" }, "Cameroon": { "code": "cm", @@ -451,7 +487,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Douala" - ] + ], + "iso": "+237" }, "Canada": { "code": "ca", @@ -491,7 +528,8 @@ "America/Whitehorse", "America/Winnipeg", "America/Yellowknife" - ] + ], + "iso": "+1" }, "Cape Verde": { "code": "cv", @@ -503,7 +541,8 @@ "number_format": "#,###.##", "timezones": [ "Atlantic/Cape_Verde" - ] + ], + "iso": "+238" }, "Cayman Islands": { "code": "ky", @@ -515,7 +554,8 @@ "number_format": "#,###.##", "timezones": [ "America/Cayman" - ] + ], + "iso": "+ 345" }, "Central African Republic": { "code": "cf", @@ -527,7 +567,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Bangui" - ] + ], + "iso": "+236" }, "Chad": { "code": "td", @@ -539,7 +580,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Ndjamena" - ] + ], + "iso": "+235" }, "Chile": { "code": "cl", @@ -552,7 +594,8 @@ "timezones": [ "America/Santiago", "Pacific/Easter" - ] + ], + "iso": "+56" }, "China": { "code": "cn", @@ -568,14 +611,16 @@ "Asia/Kashgar", "Asia/Shanghai", "Asia/Urumqi" - ] + ], + "iso": "+86" }, "Christmas Island": { "code": "cx", "number_format": "#,###.##", "timezones": [ "Indian/Christmas" - ] + ], + "iso": "+61" }, "Cocos (Keeling) Islands": { "code": "cc", @@ -585,7 +630,8 @@ "number_format": "#,###.##", "timezones": [ "Indian/Cocos" - ] + ], + "iso": "+61" }, "Colombia": { "code": "co", @@ -597,7 +643,8 @@ "number_format": "#.###,##", "timezones": [ "America/Bogota" - ] + ], + "iso": "+57" }, "Comoros": { "code": "km", @@ -609,7 +656,8 @@ "number_format": "#,###.##", "timezones": [ "Indian/Comoro" - ] + ], + "iso": "+269" }, "Congo": { "code": "cg", @@ -618,7 +666,8 @@ "currency_name": "Central African CFA Franc", "currency_symbol": "FCFA", "currency_fraction": "Centime", - "currency_fraction_units": 100 + "currency_fraction_units": 100, + "iso": "+242" }, "Congo, The Democratic Republic of the": { "code": "cd", @@ -627,7 +676,8 @@ "currency_name": "Congolese franc", "currency_symbol": "FC", "currency_fraction": "Centime", - "currency_fraction_units": 100 + "currency_fraction_units": 100, + "iso": "+243" }, "Cook Islands": { "code": "ck", @@ -637,7 +687,8 @@ "number_format": "#,###.##", "timezones": [ "Pacific/Rarotonga" - ] + ], + "iso": "+682" }, "Costa Rica": { "code": "cr", @@ -649,7 +700,8 @@ "number_format": "#.###,##", "timezones": [ "America/Costa_Rica" - ] + ], + "iso": "+506" }, "Croatia": { "code": "hr", @@ -661,7 +713,8 @@ "number_format": "#.###,##", "timezones": [ "Europe/Zagreb" - ] + ], + "iso": "+385" }, "Cuba": { "code": "cu", @@ -673,7 +726,8 @@ "number_format": "#,###.##", "timezones": [ "America/Havana" - ] + ], + "iso": "+53" }, "Cura\u00e7ao": { "code": "cw", @@ -692,7 +746,8 @@ "number_format": "#.###,##", "timezones": [ "Asia/Nicosia" - ] + ], + "iso": "+357" }, "Czech Republic": { "code": "cz", @@ -704,7 +759,8 @@ "number_format": "#.###,##", "timezones": [ "Europe/Prague" - ] + ], + "iso": "+420" }, "Denmark": { "code": "dk", @@ -716,7 +772,8 @@ "number_format": "#.###,##", "timezones": [ "Europe/Copenhagen" - ] + ], + "iso": "+45" }, "Djibouti": { "code": "dj", @@ -728,7 +785,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Djibouti" - ] + ], + "iso": "+253" }, "Dominica": { "code": "dm", @@ -740,7 +798,8 @@ "number_format": "#,###.##", "timezones": [ "America/Dominica" - ] + ], + "iso": "+1767" }, "Dominican Republic": { "code": "do", @@ -752,7 +811,8 @@ "number_format": "#,###.##", "timezones": [ "America/Santo_Domingo" - ] + ], + "iso": "+1849" }, "Ecuador": { "code": "ec", @@ -763,7 +823,8 @@ "timezones": [ "America/Guayaquil", "Pacific/Galapagos" - ] + ], + "iso": "+593" }, "Egypt": { "code": "eg", @@ -775,7 +836,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Cairo" - ] + ], + "iso": "+20" }, "El Salvador": { "code": "sv", @@ -789,7 +851,8 @@ "number_format": "#,###.##", "timezones": [ "America/El_Salvador" - ] + ], + "iso": "+503" }, "Equatorial Guinea": { "code": "gq", @@ -801,7 +864,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Malabo" - ] + ], + "iso": "+240" }, "Eritrea": { "code": "er", @@ -813,7 +877,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Asmara" - ] + ], + "iso": "+291" }, "Estonia": { "code": "ee", @@ -825,7 +890,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Tallinn" - ] + ], + "iso": "+372" }, "Ethiopia": { "code": "et", @@ -837,13 +903,15 @@ "number_format": "#,###.##", "timezones": [ "Africa/Addis_Ababa" - ] + ], + "iso": "+251" }, "Falkland Islands (Malvinas)": { "code": "fk", "currency": "FKP", "currency_name": "Falkland Islands Pound", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+500" }, "Faroe Islands": { "code": "fo", @@ -853,7 +921,8 @@ "number_format": "#,###.##", "timezones": [ "Atlantic/Faroe" - ] + ], + "iso": "+298" }, "Fiji": { "code": "fj", @@ -865,7 +934,8 @@ "number_format": "#,###.##", "timezones": [ "Pacific/Fiji" - ] + ], + "iso": "+679" }, "Finland": { "code": "fi", @@ -877,7 +947,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Helsinki" - ] + ], + "iso": "+358" }, "France": { "code": "fr", @@ -890,14 +961,16 @@ "date_format": "dd/mm/yyyy", "timezones": [ "Europe/Paris" - ] + ], + "iso": "+33" }, "French Guiana": { "code": "gf", "number_format": "#,###.##", "timezones": [ "America/Cayenne" - ] + ], + "iso": "+594" }, "French Polynesia": { "code": "pf", @@ -909,11 +982,13 @@ "Pacific/Gambier", "Pacific/Marquesas", "Pacific/Tahiti" - ] + ], + "iso": "+689" }, "French Southern Territories": { "code": "tf", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+262" }, "Gabon": { "code": "ga", @@ -925,7 +1000,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Libreville" - ] + ], + "iso": "+241" }, "Gambia": { "code": "gm", @@ -934,7 +1010,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Banjul" - ] + ], + "iso": "+220" }, "Georgia": { "code": "ge", @@ -944,7 +1021,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Tbilisi" - ] + ], + "iso": "+995" }, "Germany": { "code": "de", @@ -956,7 +1034,8 @@ "number_format": "#.###,##", "timezones": [ "Europe/Berlin" - ] + ], + "iso": "+49" }, "Ghana": { "code": "gh", @@ -967,7 +1046,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Accra" - ] + ], + "iso": "+233" }, "Gibraltar": { "code": "gi", @@ -979,7 +1059,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Gibraltar" - ] + ], + "iso": "+350" }, "Greece": { "code": "gr", @@ -991,7 +1072,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Athens" - ] + ], + "iso": "+30" }, "Greenland": { "code": "gl", @@ -1001,7 +1083,8 @@ "America/Godthab", "America/Scoresbysund", "America/Thule" - ] + ], + "iso": "+299" }, "Grenada": { "code": "gd", @@ -1013,21 +1096,24 @@ "number_format": "#,###.##", "timezones": [ "America/Grenada" - ] + ], + "iso": "+1473" }, "Guadeloupe": { "code": "gp", "number_format": "#,###.##", "timezones": [ "America/Guadeloupe" - ] + ], + "iso": "+590" }, "Guam": { "code": "gu", "number_format": "#,###.##", "timezones": [ "Pacific/Guam" - ] + ], + "iso": "+1671" }, "Guatemala": { "code": "gt", @@ -1039,7 +1125,8 @@ "number_format": "#,###.##", "timezones": [ "America/Guatemala" - ] + ], + "iso": "+502" }, "Guernsey": { "code": "gg", @@ -1049,7 +1136,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/London" - ] + ], + "iso": "+44" }, "Guinea": { "code": "gn", @@ -1061,7 +1149,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Conakry" - ] + ], + "iso": "+224" }, "Guinea-Bissau": { "code": "gw", @@ -1073,7 +1162,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Bissau" - ] + ], + "iso": "+245" }, "Guyana": { "code": "gy", @@ -1085,7 +1175,8 @@ "number_format": "#,###.##", "timezones": [ "America/Guyana" - ] + ], + "iso": "+592" }, "Haiti": { "code": "ht", @@ -1098,15 +1189,18 @@ "timezones": [ "America/Guatemala", "America/Port-au-Prince" - ] + ], + "iso": "+509" }, "Heard Island and McDonald Islands": { "code": "hm", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+0" }, "Holy See (Vatican City State)": { "code": "va", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+379" }, "Honduras": { "code": "hn", @@ -1118,7 +1212,8 @@ "number_format": "#,###.##", "timezones": [ "America/Tegucigalpa" - ] + ], + "iso": "+504" }, "Hong Kong": { "code": "hk", @@ -1130,7 +1225,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Hong_Kong" - ] + ], + "iso": "+852" }, "Hungary": { "code": "hu", @@ -1143,7 +1239,8 @@ "number_format": "#.###", "timezones": [ "Europe/Budapest" - ] + ], + "iso": "+36" }, "Iceland": { "code": "is", @@ -1155,7 +1252,8 @@ "number_format": "#.###", "timezones": [ "Atlantic/Reykjavik" - ] + ], + "iso": "+354" }, "India": { "code": "in", @@ -1167,7 +1265,8 @@ "number_format": "#,##,###.##", "timezones": [ "Asia/Kolkata" - ] + ], + "iso": "+91" }, "Indonesia": { "code": "id", @@ -1182,7 +1281,8 @@ "Asia/Jayapura", "Asia/Makassar", "Asia/Pontianak" - ] + ], + "iso": "+62" }, "Iran": { "code": "ir", @@ -1192,7 +1292,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Tehran" - ] + ], + "iso": "+98" }, "Iraq": { "code": "iq", @@ -1204,7 +1305,8 @@ "number_format": "#,###.###", "timezones": [ "Asia/Baghdad" - ] + ], + "iso": "+964" }, "Ireland": { "code": "ie", @@ -1216,7 +1318,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Dublin" - ] + ], + "iso": "+353" }, "Isle of Man": { "code": "im", @@ -1226,7 +1329,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/London" - ] + ], + "iso": "+44" }, "Israel": { "code": "il", @@ -1238,7 +1342,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Jerusalem" - ] + ], + "iso": "+972" }, "Italy": { "code": "it", @@ -1251,7 +1356,8 @@ "date_format": "dd/mm/yyyy", "timezones": [ "Europe/Rome" - ] + ], + "iso": "+39" }, "Ivory Coast": { "code": "ci", @@ -1263,7 +1369,8 @@ "number_format": "#,###.##", "timeszones": [ "Africa/Abidjan" - ] + ], + "iso": "+225" }, "Jamaica": { "code": "jm", @@ -1275,7 +1382,8 @@ "number_format": "#,###.##", "timezones": [ "America/Jamaica" - ] + ], + "iso": "+1876" }, "Japan": { "code": "jp", @@ -1287,7 +1395,8 @@ "number_format": "#,###", "timezones": [ "Asia/Tokyo" - ] + ], + "iso": "+81" }, "Jersey": { "code": "je", @@ -1297,7 +1406,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/London" - ] + ], + "iso": "+44" }, "Jordan": { "code": "jo", @@ -1309,7 +1419,8 @@ "number_format": "#,###.###", "timezones": [ "Asia/Amman" - ] + ], + "iso": "+962" }, "Kazakhstan": { "code": "kz", @@ -1325,7 +1436,8 @@ "Asia/Aqtobe", "Asia/Oral", "Asia/Qyzylorda" - ] + ], + "iso": "+7" }, "Kenya": { "code": "ke", @@ -1337,7 +1449,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Nairobi" - ] + ], + "iso": "+254" }, "Kiribati": { "code": "ki", @@ -1349,19 +1462,22 @@ "Pacific/Enderbury", "Pacific/Kiritimati", "Pacific/Tarawa" - ] + ], + "iso": "+686" }, "Korea, Democratic Peoples Republic of": { "code": "kp", "currency": "KPW", "currency_name": "North Korean Won", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+850" }, "Korea, Republic of": { "code": "kr", "currency": "KRW", "currency_name": "Won", - "number_format": "#,###" + "number_format": "#,###", + "iso": "+82" }, "Kuwait": { "code": "kw", @@ -1373,7 +1489,8 @@ "number_format": "#,###.###", "timezones": [ "Asia/Kuwait" - ] + ], + "iso": "+965" }, "Kyrgyzstan": { "code": "kg", @@ -1385,16 +1502,18 @@ "number_format": "#,###.##", "timezones": [ "Asia/Bishkek" - ] + ], + "iso": "+996" }, "Lao Peoples Democratic Republic": { "code": "la", "currency": "LAK", "currency_name": "Kip", "number_format": "#,###.##", - "timezones":[ - "Asia/Vientiane" - ] + "timezones": [ + "Asia/Vientiane" + ], + "iso": "+856" }, "Latvia": { "code": "lv", @@ -1406,7 +1525,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Riga" - ] + ], + "iso": "+371" }, "Lebanon": { "code": "lb", @@ -1418,7 +1538,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Beirut" - ] + ], + "iso": "+961" }, "Lesotho": { "code": "ls", @@ -1430,7 +1551,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Maseru" - ] + ], + "iso": "+266" }, "Liberia": { "code": "lr", @@ -1442,7 +1564,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Monrovia" - ] + ], + "iso": "+231" }, "Libya": { "code": "ly", @@ -1454,7 +1577,8 @@ "number_format": "#,###.###", "timezones": [ "Africa/Tripoli" - ] + ], + "iso": "+218" }, "Liechtenstein": { "code": "li", @@ -1464,7 +1588,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Vaduz" - ] + ], + "iso": "+423" }, "Lithuania": { "code": "lt", @@ -1477,7 +1602,8 @@ "number_format": "# ###,##", "timezones": [ "Europe/Vilnius" - ] + ], + "iso": "+370" }, "Luxembourg": { "code": "lu", @@ -1489,13 +1615,15 @@ "number_format": "#,###.##", "timezones": [ "Europe/Luxembourg" - ] + ], + "iso": "+352" }, "Macao": { "code": "mo", "currency": "MOP", "currency_name": "Pataca", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+853" }, "Macedonia": { "code": "mk", @@ -1504,7 +1632,8 @@ "currency_fraction_units": 100, "currency_name": "Denar", "currency_symbol": "\u0434\u0435\u043d", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+389" }, "Madagascar": { "code": "mg", @@ -1514,7 +1643,8 @@ "number_format": "#,###.##", "timezones": [ "Indian/Antananarivo" - ] + ], + "iso": "+261" }, "Malawi": { "code": "mw", @@ -1526,7 +1656,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Blantyre" - ] + ], + "iso": "+265" }, "Malaysia": { "code": "my", @@ -1539,7 +1670,8 @@ "timezones": [ "Asia/Kuala_Lumpur", "Asia/Kuching" - ] + ], + "iso": "+60" }, "Maldives": { "code": "mv", @@ -1551,7 +1683,8 @@ "number_format": "#,###.##", "timezones": [ "Indian/Maldives" - ] + ], + "iso": "+960" }, "Mali": { "code": "ml", @@ -1563,7 +1696,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Bamako" - ] + ], + "iso": "+223" }, "Malta": { "code": "mt", @@ -1575,7 +1709,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Malta" - ] + ], + "iso": "+356" }, "Marshall Islands": { "code": "mh", @@ -1586,14 +1721,16 @@ "timezones": [ "Pacific/Kwajalein", "Pacific/Majuro" - ] + ], + "iso": "+692" }, "Martinique": { "code": "mq", "number_format": "#,###.##", "timezones": [ "America/Martinique" - ] + ], + "iso": "+596" }, "Mauritania": { "code": "mr", @@ -1605,7 +1742,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Nouakchott" - ] + ], + "iso": "+222" }, "Mauritius": { "code": "mu", @@ -1617,14 +1755,16 @@ "number_format": "#,###", "timezones": [ "Indian/Mauritius" - ] + ], + "iso": "+230" }, "Mayotte": { "code": "yt", "number_format": "#,###.##", "timezones": [ "Indian/Mayotte" - ] + ], + "iso": "+262" }, "Mexico": { "code": "mx", @@ -1647,17 +1787,20 @@ "America/Ojinaga", "America/Santa_Isabel", "America/Tijuana" - ] + ], + "iso": "+52" }, "Micronesia, Federated States of": { "code": "fm", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+691" }, "Moldova, Republic of": { "code": "md", "currency": "MDL", "currency_name": "Moldovan Leu", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+373" }, "Monaco": { "code": "mc", @@ -1669,7 +1812,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Monaco" - ] + ], + "iso": "+377" }, "Mongolia": { "code": "mn", @@ -1684,7 +1828,8 @@ "Asia/Choibalsan", "Asia/Hovd", "Asia/Ulaanbaatar" - ] + ], + "iso": "+976" }, "Montenegro": { "code": "me", @@ -1696,7 +1841,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Belgrade" - ] + ], + "iso": "+382" }, "Montserrat": { "code": "ms", @@ -1708,7 +1854,8 @@ "number_format": "#,###.##", "timezones": [ "America/Montserrat" - ] + ], + "iso": "+1664" }, "Morocco": { "code": "ma", @@ -1720,7 +1867,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Casablanca" - ] + ], + "iso": "+212" }, "Mozambique": { "code": "mz", @@ -1731,7 +1879,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Maputo" - ] + ], + "iso": "+258" }, "Myanmar": { "code": "mm", @@ -1740,7 +1889,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Rangoon" - ] + ], + "iso": "+95" }, "Namibia": { "code": "na", @@ -1752,7 +1902,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Windhoek" - ] + ], + "iso": "+264" }, "Nauru": { "code": "nr", @@ -1762,7 +1913,8 @@ "number_format": "#,###.##", "timezones": [ "Pacific/Nauru" - ] + ], + "iso": "+674" }, "Nepal": { "code": "np", @@ -1774,7 +1926,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Kathmandu" - ] + ], + "iso": "+977" }, "Netherlands": { "code": "nl", @@ -1786,7 +1939,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Amsterdam" - ] + ], + "iso": "+31" }, "New Caledonia": { "code": "nc", @@ -1796,7 +1950,8 @@ "number_format": "#,###.##", "timezones": [ "Pacific/Noumea" - ] + ], + "iso": "+687" }, "New Zealand": { "code": "nz", @@ -1809,7 +1964,8 @@ "timezones": [ "Pacific/Auckland", "Pacific/Chatham" - ] + ], + "iso": "+64" }, "Nicaragua": { "code": "ni", @@ -1821,7 +1977,8 @@ "number_format": "#,###.##", "timezones": [ "America/Managua" - ] + ], + "iso": "+505" }, "Niger": { "code": "ne", @@ -1833,7 +1990,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Niamey" - ] + ], + "iso": "+227" }, "Nigeria": { "code": "ng", @@ -1845,7 +2003,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Lagos" - ] + ], + "iso": "+234" }, "Niue": { "code": "nu", @@ -1855,21 +2014,24 @@ "number_format": "#,###.##", "timezones": [ "Pacific/Niue" - ] + ], + "iso": "+683" }, "Norfolk Island": { "code": "nf", "number_format": "#,###.##", "timezones": [ "Pacific/Norfolk" - ] + ], + "iso": "+672" }, "Northern Mariana Islands": { "code": "mp", "number_format": "#,###.##", "timezones": [ "Pacific/Saipan" - ] + ], + "iso": "+1670" }, "Norway": { "code": "no", @@ -1881,7 +2043,8 @@ "number_format": "#.###,##", "timezones": [ "Europe/Oslo" - ] + ], + "iso": "+47" }, "Oman": { "code": "om", @@ -1893,7 +2056,8 @@ "number_format": "#,###.###", "timezones": [ "Asia/Muscat" - ] + ], + "iso": "+968" }, "Pakistan": { "code": "pk", @@ -1905,7 +2069,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Karachi" - ] + ], + "iso": "+92" }, "Palau": { "code": "pw", @@ -1916,11 +2081,13 @@ "number_format": "#,###.##", "timezones": [ "Pacific/Palau" - ] + ], + "iso": "+680" }, "Palestinian Territory, Occupied": { "code": "ps", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+970" }, "Panama": { "code": "pa", @@ -1930,7 +2097,8 @@ "number_format": "#,###.##", "timezones": [ "America/Panama" - ] + ], + "iso": "+507" }, "Papua New Guinea": { "code": "pg", @@ -1942,7 +2110,8 @@ "number_format": "#,###.##", "timezones": [ "Pacific/Port_Moresby" - ] + ], + "iso": "+675" }, "Paraguay": { "code": "py", @@ -1954,7 +2123,8 @@ "number_format": "#,###.##", "timezones": [ "America/Asuncion" - ] + ], + "iso": "+595" }, "Peru": { "code": "pe", @@ -1966,7 +2136,8 @@ "number_format": "#,###.##", "timezones": [ "America/Lima" - ] + ], + "iso": "+51" }, "Philippines": { "code": "ph", @@ -1979,14 +2150,16 @@ "number_format": "#,###.##", "timezones": [ "Asia/Manila" - ] + ], + "iso": "+63" }, "Pitcairn": { "code": "pn", "number_format": "#,###.##", "timezones": [ "Pacific/Pitcairn" - ] + ], + "iso": "+64" }, "Poland": { "code": "pl", @@ -1997,7 +2170,8 @@ "number_format": "#.###,##", "timezones": [ "Europe/Warsaw" - ] + ], + "iso": "+48" }, "Portugal": { "code": "pt", @@ -2011,14 +2185,16 @@ "Atlantic/Azores", "Atlantic/Madeira", "Europe/Lisbon" - ] + ], + "iso": "+351" }, "Puerto Rico": { "code": "pr", "number_format": "#,###.##", "timezones": [ "America/Puerto_Rico" - ] + ], + "iso": "+1939" }, "Qatar": { "code": "qa", @@ -2030,7 +2206,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Qatar" - ] + ], + "iso": "+974" }, "Romania": { "code": "ro", @@ -2042,13 +2219,15 @@ "number_format": "#,###.##", "timezones": [ "Europe/Bucharest" - ] + ], + "iso": "+40" }, "Russian Federation": { "code": "ru", "currency": "RUB", "currency_name": "Russian Ruble", - "number_format": "#.###,##" + "number_format": "#.###,##", + "iso": "+7" }, "Rwanda": { "code": "rw", @@ -2060,21 +2239,25 @@ "number_format": "#,###.##", "timezones": [ "Africa/Kigali" - ] + ], + "iso": "+250" }, "R\u00e9union": { "code": "re", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+262" }, "Saint Barth\u00e9lemy": { "code": "bl", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+590" }, "Saint Helena, Ascension and Tristan da Cunha": { "code": "sh", "currency": "SHP", "currency_name": "Saint Helena Pound", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+290" }, "Saint Kitts and Nevis": { "code": "kn", @@ -2086,7 +2269,8 @@ "number_format": "#,###.##", "timezones": [ "America/St_Kitts" - ] + ], + "iso": "+1869" }, "Saint Lucia": { "code": "lc", @@ -2098,15 +2282,18 @@ "number_format": "#,###.##", "timezones": [ "America/St_Lucia" - ] + ], + "iso": "+1758" }, "Saint Martin (French part)": { "code": "mf", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+590" }, "Saint Pierre and Miquelon": { "code": "pm", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+508" }, "Saint Vincent and the Grenadines": { "code": "vc", @@ -2118,7 +2305,8 @@ "number_format": "#,###.##", "timezones": [ "America/St_Vincent" - ] + ], + "iso": "+1784" }, "Samoa": { "code": "ws", @@ -2130,7 +2318,8 @@ "number_format": "#,###.##", "timezones": [ "Pacific/Apia" - ] + ], + "iso": "+685" }, "San Marino": { "code": "sm", @@ -2142,13 +2331,15 @@ "number_format": "#,###.##", "timezones": [ "Europe/Rome" - ] + ], + "iso": "+378" }, "Sao Tome and Principe": { "code": "st", "currency": "STD", "currency_name": "Dobra", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+239" }, "Saudi Arabia": { "code": "sa", @@ -2160,7 +2351,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Riyadh" - ] + ], + "iso": "+966" }, "Senegal": { "code": "sn", @@ -2172,7 +2364,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Dakar" - ] + ], + "iso": "+221" }, "Serbia": { "code": "rs", @@ -2184,7 +2377,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Belgrade" - ] + ], + "iso": "+381" }, "Seychelles": { "code": "sc", @@ -2196,7 +2390,8 @@ "number_format": "#,###.##", "timezones": [ "Indian/Mahe" - ] + ], + "iso": "+248" }, "Sierra Leone": { "code": "sl", @@ -2208,7 +2403,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Freetown" - ] + ], + "iso": "+232" }, "Singapore": { "code": "sg", @@ -2220,7 +2416,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Singapore" - ] + ], + "iso": "+65" }, "Sint Maarten (Dutch part)": { "code": "sx", @@ -2236,7 +2433,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Bratislava" - ] + ], + "iso": "+421" }, "Slovenia": { "code": "si", @@ -2248,7 +2446,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Belgrade" - ] + ], + "iso": "+386" }, "Solomon Islands": { "code": "sb", @@ -2260,7 +2459,8 @@ "number_format": "#,###.##", "timezones": [ "Pacific/Guadalcanal" - ] + ], + "iso": "+677" }, "Somalia": { "code": "so", @@ -2272,7 +2472,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Mogadishu" - ] + ], + "iso": "+252" }, "South Africa": { "code": "za", @@ -2285,14 +2486,16 @@ "number_format": "# ###.##", "timezones": [ "Africa/Johannesburg" - ] + ], + "iso": "+27" }, "South Georgia and the South Sandwich Islands": { "code": "gs", "currency_fraction": "Penny", "currency_fraction_units": 100, "currency_symbol": "\u00a3", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+500" }, "South Sudan": { "code": "ss", @@ -2302,7 +2505,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Juba" - ] + ], + "iso": "+211" }, "Spain": { "code": "es", @@ -2316,7 +2520,8 @@ "Africa/Ceuta", "Atlantic/Canary", "Europe/Madrid" - ] + ], + "iso": "+34" }, "Sri Lanka": { "code": "lk", @@ -2328,7 +2533,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Colombo" - ] + ], + "iso": "+94" }, "Sudan": { "code": "sd", @@ -2338,7 +2544,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Khartoum" - ] + ], + "iso": "+249" }, "Suriname": { "code": "sr", @@ -2349,11 +2556,13 @@ "number_format": "#,###.##", "timezones": [ "America/Paramaribo" - ] + ], + "iso": "+597" }, "Svalbard and Jan Mayen": { "code": "sj", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+47" }, "Swaziland": { "code": "sz", @@ -2365,7 +2574,8 @@ "number_format": "#, ###.##", "timezones": [ "Africa/Mbabane" - ] + ], + "iso": "+268" }, "Sweden": { "code": "se", @@ -2377,7 +2587,8 @@ "number_format": "#.###,##", "timezones": [ "Europe/Stockholm" - ] + ], + "iso": "+46" }, "Switzerland": { "code": "ch", @@ -2390,19 +2601,22 @@ "number_format": "#'###.##", "timezones": [ "Europe/Zurich" - ] + ], + "iso": "+41" }, "Syria": { "code": "sy", "currency": "SYP", "currency_name": "Syrian Pound", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+963" }, "Taiwan": { "code": "tw", "currency": "TWD", "date_format": "yyyy-mm-dd", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+886" }, "Tajikistan": { "code": "tj", @@ -2412,13 +2626,15 @@ "number_format": "#,###.##", "timezones": [ "Asia/Dushanbe" - ] + ], + "iso": "+992" }, "Tanzania": { "code": "tz", "currency": "TZS", "currency_name": "Tanzanian Shilling", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+255" }, "Thailand": { "code": "th", @@ -2430,11 +2646,13 @@ "number_format": "#,###.##", "timezones": [ "Asia/Bangkok" - ] + ], + "iso": "+66" }, "Timor-Leste": { "code": "tl", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+670" }, "Togo": { "code": "tg", @@ -2446,14 +2664,16 @@ "number_format": "#,###.##", "timezones": [ "Africa/Lome" - ] + ], + "iso": "+228" }, "Tokelau": { "code": "tk", "number_format": "#,###.##", "timezones": [ "Pacific/Fakaofo" - ] + ], + "iso": "+690" }, "Tonga": { "code": "to", @@ -2465,7 +2685,8 @@ "number_format": "#,###.##", "timezones": [ "Pacific/Tongatapu" - ] + ], + "iso": "+676" }, "Trinidad and Tobago": { "code": "tt", @@ -2477,7 +2698,8 @@ "number_format": "#,###.##", "timezones": [ "America/Port_of_Spain" - ] + ], + "iso": "+1868" }, "Tunisia": { "code": "tn", @@ -2489,7 +2711,8 @@ "number_format": "#,###.###", "timezones": [ "Africa/Tunis" - ] + ], + "iso": "+216" }, "Turkey": { "code": "tr", @@ -2500,7 +2723,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Istanbul" - ] + ], + "iso": "+90" }, "Turkmenistan": { "code": "tm", @@ -2512,14 +2736,16 @@ "number_format": "#,###.##", "timezones": [ "Asia/Ashgabat" - ] + ], + "iso": "+993" }, "Turks and Caicos Islands": { "code": "tc", "currency_fraction": "Cent", "currency_fraction_units": 100, "currency_symbol": "$", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+1649" }, "Tuvalu": { "code": "tv", @@ -2529,7 +2755,8 @@ "number_format": "#,###.##", "timezones": [ "Pacific/Funafuti" - ] + ], + "iso": "+688" }, "Uganda": { "code": "ug", @@ -2541,7 +2768,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Kampala" - ] + ], + "iso": "+256" }, "Ukraine": { "code": "ua", @@ -2556,7 +2784,8 @@ "Europe/Simferopol", "Europe/Uzhgorod", "Europe/Zaporozhye" - ] + ], + "iso": "+380" }, "United Arab Emirates": { "code": "ae", @@ -2568,7 +2797,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Dubai" - ] + ], + "iso": "+971" }, "United Kingdom": { "code": "gb", @@ -2580,7 +2810,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/London" - ] + ], + "iso": "+44" }, "United States": { "code": "us", @@ -2623,7 +2854,8 @@ "America/Sitka", "America/Yakutat", "Pacific/Honolulu" - ] + ], + "iso": "+1" }, "United States Minor Outlying Islands": { "code": "um", @@ -2639,7 +2871,8 @@ "number_format": "#.###,##", "timezones": [ "America/Montevideo" - ] + ], + "iso": "+598" }, "Uzbekistan": { "code": "uz", @@ -2652,7 +2885,8 @@ "timezones": [ "Asia/Samarkand", "Asia/Tashkent" - ] + ], + "iso": "+998" }, "Vanuatu": { "code": "vu", @@ -2664,7 +2898,8 @@ "number_format": "#,###", "timezones": [ "Pacific/Efate" - ] + ], + "iso": "+678" }, "Venezuela, Bolivarian Republic of": { "code": "ve", @@ -2672,28 +2907,33 @@ "currency": "VEF", "currency_symbol": "Bs.", "currency_fraction": "Centimos", - "currency_fraction_units": 100 + "currency_fraction_units": 100, + "iso": "+58" }, "Vietnam": { "code": "vn", "currency": "VND", "currency_name": "Dong", - "number_format": "#.###" + "number_format": "#.###", + "iso": "+84" }, "Virgin Islands, British": { "code": "vg", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+1284" }, "Virgin Islands, U.S.": { "code": "vi", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+1340" }, "Wallis and Futuna": { "code": "wf", "currency_fraction": "Centime", "currency_fraction_units": 100, "currency_symbol": "Fr", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+681" }, "Western Sahara": { "code": "eh", @@ -2713,7 +2953,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Aden" - ] + ], + "iso": "+967" }, "Zambia": { "code": "zm", @@ -2725,7 +2966,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Lusaka" - ] + ], + "iso": "+260" }, "Zimbabwe": { "code": "zw", @@ -2737,10 +2979,12 @@ "number_format": "# ###.##", "timezones": [ "Africa/Harare" - ] + ], + "iso": "+263" }, "\u00c5land Islands": { "code": "ax", - "number_format": "#,###.##" + "number_format": "#,###.##", + "iso": "+358" } -} +} \ No newline at end of file From 596a7c28278515ae7263f11b7c5109b84a186025 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Mon, 3 Jan 2022 13:19:33 +0530 Subject: [PATCH 007/139] fix(ui): inconsistent flag size in the field --- frappe/public/js/frappe/form/controls/phone.js | 9 ++------- frappe/public/js/frappe/phone_picker/phone_picker.js | 6 ++++-- frappe/public/scss/common/phone_picker.scss | 11 ++++++----- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/phone.js b/frappe/public/js/frappe/form/controls/phone.js index c00c966539..c5191ee224 100644 --- a/frappe/public/js/frappe/form/controls/phone.js +++ b/frappe/public/js/frappe/form/controls/phone.js @@ -13,7 +13,7 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD // Replaces code when selected and removes previously selected. this.picker.on_change = (country) => { const country_code = frappe.boot.country_codes[country].code; - const country_isd = frappe.boot.country_codes[country].isd; + const country_isd = frappe.boot.country_codes[country].iso; this.selected_icon.find('use').attr('href', '#'+country_code) this.$icon = this.selected_icon.find('svg'); if (this.$icon.hasClass('icon-sm')) { @@ -25,7 +25,6 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD } else { this.$isd.text(country_isd) } - // this.selected_icon.text('+' + this.get_country(country)) if(this.$input.val()) { this.set_formatted_input(this.get_country(country) +'-'+ this.$input.val()) } @@ -134,10 +133,6 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD this.$input && value && this.$input.val(value.split("-").pop()) } - reset_icon() { - - } - change_flag(country_code) { this.selected_icon.find('use').attr('href', '#'+country_code) this.$icon = this.selected_icon.find('svg'); @@ -149,7 +144,7 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD get_country(country=null) { const country_codes = frappe.boot.country_codes; - return country_codes[country].isd; + return country_codes[country].iso; } get_country_flag(country) { const country_codes = frappe.boot.country_codes; diff --git a/frappe/public/js/frappe/phone_picker/phone_picker.js b/frappe/public/js/frappe/phone_picker/phone_picker.js index 9e60acc257..f12ddf46ec 100644 --- a/frappe/public/js/frappe/phone_picker/phone_picker.js +++ b/frappe/public/js/frappe/phone_picker/phone_picker.js @@ -33,10 +33,12 @@ class Picker { } setup_countries() { - Object.entries(this.countries).forEach(([country, info]) => { + if (!info.iso) { + return + } let $country = $(`
${frappe.utils.flag(info.code, "md")} - ${country} (${info.isd})
`); + ${country} (${info.iso})
`); this.phone_wrapper.append($country); const set_values = () => { this.set_country(country); diff --git a/frappe/public/scss/common/phone_picker.scss b/frappe/public/scss/common/phone_picker.scss index 74a61cbbe8..a80d9aa83e 100644 --- a/frappe/public/scss/common/phone_picker.scss +++ b/frappe/public/scss/common/phone_picker.scss @@ -1,7 +1,7 @@ .phone-picker { font-size: var(--text-xs); color: var(--text-muted); - --phone-picker-width: 210px; + --phone-picker-width: 250px; width: var(--phone-picker-width); .phones { margin-top: 10px; @@ -22,7 +22,7 @@ .phone-wrapper { display: flex; - width: 210px; + width: 250px; height: 30px; text-align: center; align-items: center; @@ -61,13 +61,13 @@ .frappe-control[data-fieldtype='Phone'] { input { - padding-left: 70px; + padding-left: 80px; } .selected-phone { display: flex; cursor: pointer; - width: 52px; - height: 18px; + width: 66px; + height: 20px; border-radius: 5px; position: absolute; top: calc(50% + 2.6px); @@ -77,6 +77,7 @@ .country { display: flex; margin-left: 0.6rem; + align-items: flex-end; flex-grow: 1; } From 934d34084ef11bc3737d6ef55c1ab06219c8d529 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Mon, 3 Jan 2022 15:07:03 +0530 Subject: [PATCH 008/139] fix(ui): increased width of popover --- frappe/public/scss/common/phone_picker.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frappe/public/scss/common/phone_picker.scss b/frappe/public/scss/common/phone_picker.scss index a80d9aa83e..631090f145 100644 --- a/frappe/public/scss/common/phone_picker.scss +++ b/frappe/public/scss/common/phone_picker.scss @@ -1,7 +1,7 @@ .phone-picker { font-size: var(--text-xs); color: var(--text-muted); - --phone-picker-width: 250px; + --phone-picker-width: 290px; width: var(--phone-picker-width); .phones { margin-top: 10px; @@ -22,7 +22,7 @@ .phone-wrapper { display: flex; - width: 250px; + width: 290px; height: 30px; text-align: center; align-items: center; @@ -33,6 +33,7 @@ display: flex; margin-left: 0.6rem; flex-grow: 1; + width: 290px; } } } @@ -53,6 +54,7 @@ } } .phone-picker-popover { + max-width: 325px; left: -20px !important; .picker-arrow { left: 15px !important; From 3010d537bbe0a8c2b41a08581f59867904074511 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Mon, 3 Jan 2022 16:19:14 +0530 Subject: [PATCH 009/139] test: UI test for Phone Control --- cypress/integration/control_phone.js | 57 +++++++++++++++++++ .../public/js/frappe/form/controls/phone.js | 2 +- 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 cypress/integration/control_phone.js diff --git a/cypress/integration/control_phone.js b/cypress/integration/control_phone.js new file mode 100644 index 0000000000..684e0d7d99 --- /dev/null +++ b/cypress/integration/control_phone.js @@ -0,0 +1,57 @@ +context('Control Phone', () => { + before(() => { + cy.login(); + cy.visit('/app/website'); + }); + + function get_dialog_with_phone(s) { + return cy.dialog({ + title: 'Phone', + fields: [{ + 'fieldname': 'phone', + 'fieldtype': 'Phone', + }] + }); + } + + it('should set flag and data', () => { + get_dialog_with_phone().as('dialog'); + cy.get('.selected-phone').click(); + cy.get('.phone-picker .phone-wrapper[id="afghanistan"]').click(); + cy.get('.phone-picker .phone-wrapper[id="india"]').click(); + cy.get('.selected-phone .country').should('have.text', '+91') + cy.get('.selected-phone > .icon > use').should('have.attr', 'href').and('include', '#in') + + let phone_number = '9312672712' + cy.get('.selected-phone').click().first(); + cy.get('.frappe-control[data-fieldname=phone] input') + .first() + .click(); + cy.get('.frappe-control[data-fieldname=phone]') + .findByRole('textbox') + .first() + .type(phone_number); + + cy.get('.frappe-control[data-fieldname=phone] input').first().should('have.value', phone_number); + cy.get('.frappe-control[data-fieldname=phone] input').first().blur(); + cy.get('@dialog').then(dialog => { + let value = dialog.fields_dict.phone.value; + expect(value).to.equal('+91-' + phone_number); + }); + }); + + it('case insensitive search for country and clear search', () => { + let search_text = 'india'; + cy.get('.selected-phone').click().first(); + cy.get('.phone-picker').findByRole('searchbox').click().type(search_text); + cy.get('.phone-section .phone-wrapper:not(.hidden)').then(i => { + cy.get(`.phone-section .phone-wrapper[id*='${search_text.toLowerCase()}']`).then(countries => { + expect(i.length).to.equal(countries.length); + }); + }); + + cy.get('.phone-picker').findByRole('searchbox').clear().blur(); + cy.get('.phone-section .phone-wrapper').should('not.have.class', 'hidden'); + }); + +}); diff --git a/frappe/public/js/frappe/form/controls/phone.js b/frappe/public/js/frappe/form/controls/phone.js index c5191ee224..98764bc76c 100644 --- a/frappe/public/js/frappe/form/controls/phone.js +++ b/frappe/public/js/frappe/form/controls/phone.js @@ -92,7 +92,7 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD super.refresh(); // Previously opened doc values get fetched. - if(!this.value && this.frm.is_new()) { + if(!this.value) { this.$input.val(""); this.$wrapper.find('.country').text("") this.selected_icon.find('use').attr('href', '#icon-down') From 849dadf6e12e415c8a292b6efb3a585d3ccd9ecd Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Wed, 5 Jan 2022 16:55:10 +0530 Subject: [PATCH 010/139] chore: updated requirements.txt --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 671f6ced11..5a985985e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -75,3 +75,5 @@ zxcvbn-python~=4.4.24 tenacity~=8.0.1 cairocffi==1.2.0 WeasyPrint==52.5 +phonenumbers==8.12.40 + From 224c91f92ce3b9b541e0e0f6f5929cdc0c4b23ff Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 7 Jan 2022 12:14:07 +0530 Subject: [PATCH 011/139] refactor: using flagcdn instead of flags.svg --- frappe/public/icons/timeless/flags.svg | 18341 ---------------- .../public/js/frappe/form/controls/phone.js | 81 +- .../js/frappe/phone_picker/phone_picker.js | 5 +- frappe/public/js/frappe/utils/utils.js | 6 +- frappe/public/scss/common/phone_picker.scss | 8 + frappe/www/app.html | 1 - 6 files changed, 57 insertions(+), 18385 deletions(-) delete mode 100644 frappe/public/icons/timeless/flags.svg diff --git a/frappe/public/icons/timeless/flags.svg b/frappe/public/icons/timeless/flags.svg deleted file mode 100644 index d57fda3d0e..0000000000 --- a/frappe/public/icons/timeless/flags.svg +++ /dev/null @@ -1,18341 +0,0 @@ - - diff --git a/frappe/public/js/frappe/form/controls/phone.js b/frappe/public/js/frappe/form/controls/phone.js index 98764bc76c..bf5a865d02 100644 --- a/frappe/public/js/frappe/form/controls/phone.js +++ b/frappe/public/js/frappe/form/controls/phone.js @@ -14,19 +14,22 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD this.picker.on_change = (country) => { const country_code = frappe.boot.country_codes[country].code; const country_isd = frappe.boot.country_codes[country].iso; - this.selected_icon.find('use').attr('href', '#'+country_code) + this.change_flag(country_code); this.$icon = this.selected_icon.find('svg'); - if (this.$icon.hasClass('icon-sm')) { - this.$icon.removeClass('icon-sm'); - this.selected_icon.find('svg').addClass('flag-md') + this.$flag = this.selected_icon.find('img'); + if (!this.$icon.hasClass('hide')){ + this.$icon.toggleClass('hide'); + } + if (!this.$flag.length) { + this.selected_icon.prepend(this.get_country_flag(country)); } if (!this.$isd.length) { - this.selected_icon.append($(` ${country_isd}`)) + this.selected_icon.append($(` ${country_isd}`)); } else { - this.$isd.text(country_isd) + this.$isd.text(country_isd); } if(this.$input.val()) { - this.set_formatted_input(this.get_country(country) +'-'+ this.$input.val()) + this.set_formatted_input(this.get_country(country) +'-'+ this.$input.val()); } }; @@ -76,14 +79,14 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD // Default icon when nothing is selected. this.selected_icon = this.$wrapper.find('.selected-phone'); - let input_value = this.get_input_value() + let input_value = this.get_input_value(); if (!this.selected_icon.length) { this.selected_icon = $(`
${frappe.utils.icon("down", "sm")}
`); this.selected_icon.insertAfter(this.$input); - this.selected_icon.append($(``)) + this.selected_icon.append($(``)); this.$isd = this.selected_icon.find('.country'); if(input_value && input_value.split("-").length == 2) { - this.$isd.text(this.value.split("-")[0]) + this.$isd.text(this.value.split("-")[0]); } } } @@ -94,26 +97,15 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD // Previously opened doc values get fetched. if(!this.value) { this.$input.val(""); - this.$wrapper.find('.country').text("") - this.selected_icon.find('use').attr('href', '#icon-down') - this.flag = this.selected_icon.find('svg'); - let has_flag = this.flag.hasClass('flag-md'); - if (has_flag) { - this.flag.toggleClass('flag-md'); - this.flag.toggleClass('icon-sm'); + this.$wrapper.find('.country').text(""); + if (this.selected_icon.find('svg').hasClass('hide')) { + this.selected_icon.find('svg').toggleClass('hide'); + this.selected_icon.find('img').addClass('hide'); } } - if(this.value && this.value.split("-").length == 2) { let isd = this.value.split("-")[0]; - let country_data = frappe.boot.country_codes; - - for (const country in country_data) { - if (Object.values(country_data[country]).includes(isd)) { - let code = country_data[country].code; - this.change_flag(code); - } - } + this.get_country_code_and_change_flag(isd); this.picker.set_country(isd); this.picker.refresh(); if (this.picker.country && this.picker.country !== this.$isd.text()) { @@ -125,30 +117,45 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD set_formatted_input(value) { if(value && value.includes('-')) { - this.set_model_value(value) + this.set_model_value(value); + this.$input.val(value.split("-").pop()); } else if(this.$isd.text().trim() && this.value) { let code_number = this.$isd.text() + '-' + value; - this.set_model_value(code_number) + this.set_model_value(code_number); } - this.$input && value && this.$input.val(value.split("-").pop()) } change_flag(country_code) { - this.selected_icon.find('use').attr('href', '#'+country_code) - this.$icon = this.selected_icon.find('svg'); - if (this.$icon.hasClass('icon-sm')) { - this.$icon.removeClass('icon-sm'); - this.selected_icon.find('svg').addClass('flag-md') - } + this.selected_icon.find('img').attr('src', 'https://flagcdn.com/h20/'+country_code+'.png') + this.$icon = this.selected_icon.find('img'); + // this.$icon.hasClass('hide') && this.$icon.toggleClass('hide'); } - get_country(country=null) { + // country_code for India is 'in' + get_country_code_and_change_flag(isd) { + let country_data = frappe.boot.country_codes; + let flag = this.selected_icon.find('img'); + for (const country in country_data) { + if (Object.values(country_data[country]).includes(isd)) { + let code = country_data[country].code; + flag = this.selected_icon.find('img'); + if (!flag.length) { + this.selected_icon.prepend(this.get_country_flag(country)); + this.selected_icon.find('svg').addClass('hide'); + } + else { + this.change_flag(code); + } + } + } + } + get_country(country) { const country_codes = frappe.boot.country_codes; return country_codes[country].iso; } get_country_flag(country) { const country_codes = frappe.boot.country_codes; let code = country_codes[country].code; - return frappe.utils.flag(code, "md") + return frappe.utils.flag(code); } }; diff --git a/frappe/public/js/frappe/phone_picker/phone_picker.js b/frappe/public/js/frappe/phone_picker/phone_picker.js index f12ddf46ec..7270fe63a1 100644 --- a/frappe/public/js/frappe/phone_picker/phone_picker.js +++ b/frappe/public/js/frappe/phone_picker/phone_picker.js @@ -37,7 +37,7 @@ class Picker { if (!info.iso) { return } - let $country = $(`
${frappe.utils.flag(info.code, "md")} + let $country = $(`
${frappe.utils.flag(info.code)} ${country} (${info.iso})
`); this.phone_wrapper.append($country); const set_values = () => { @@ -49,7 +49,7 @@ class Picker { }); $country.hover(() => { $country.toggleClass("bg-gray-100"); - }); + }); this.search_input.keydown((e) => { const key_code = e.keyCode; if ([13].includes(key_code)) { @@ -87,7 +87,6 @@ class Picker { } get_country() { - if (!this.country) return frappe.utils.icon("down", "sm") return this.country; } } diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 8b3dc80ce6..db21adf886 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1164,9 +1164,9 @@ Object.assign(frappe.utils, { } else { size_class = `flag-${size}`; } - return ` - - `; + return `` }, make_chart(wrapper, custom_options={}) { diff --git a/frappe/public/scss/common/phone_picker.scss b/frappe/public/scss/common/phone_picker.scss index 631090f145..cc3387ecb0 100644 --- a/frappe/public/scss/common/phone_picker.scss +++ b/frappe/public/scss/common/phone_picker.scss @@ -29,6 +29,9 @@ border-radius: 0.375rem; padding: 0.5rem; + img { + height: 15px; + } .country { display: flex; margin-left: 0.6rem; @@ -75,6 +78,7 @@ top: calc(50% + 2.6px); left: 8px; content: ' '; + align-items: center; .country { display: flex; @@ -83,6 +87,10 @@ flex-grow: 1; } + img { + height: 15px; + } + } .like-disabled-input { .phone-value { diff --git a/frappe/www/app.html b/frappe/www/app.html index 8fcaf581e2..68a6dc8e86 100644 --- a/frappe/www/app.html +++ b/frappe/www/app.html @@ -26,7 +26,6 @@ {% include "public/icons/timeless/symbol-defs.svg" %} - {% include "public/icons/timeless/flags.svg" %}
From b3076c67b8b477112a0a57a198e95f67049da193 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 7 Jan 2022 12:18:33 +0530 Subject: [PATCH 012/139] chore: renamed iso to isd --- frappe/geo/country_info.json | 490 +++++++++--------- .../public/js/frappe/form/controls/phone.js | 4 +- .../js/frappe/phone_picker/phone_picker.js | 4 +- 3 files changed, 249 insertions(+), 249 deletions(-) diff --git a/frappe/geo/country_info.json b/frappe/geo/country_info.json index 72e3e0ba24..e8b8bed1c7 100644 --- a/frappe/geo/country_info.json +++ b/frappe/geo/country_info.json @@ -9,7 +9,7 @@ "timezones": [ "Asia/Kabul" ], - "iso": "+93" + "isd": "+93" }, "Albania": { "code": "al", @@ -22,7 +22,7 @@ "timezones": [ "Europe/Tirane" ], - "iso": "+355" + "isd": "+355" }, "Algeria": { "code": "dz", @@ -35,12 +35,12 @@ "timezones": [ "Africa/Algiers" ], - "iso": "+213" + "isd": "+213" }, "American Samoa": { "code": "as", "number_format": "#,###.##", - "iso": "+1684" + "isd": "+1684" }, "Andorra": { "code": "ad", @@ -53,7 +53,7 @@ "timezones": [ "Europe/Andorra" ], - "iso": "+376" + "isd": "+376" }, "Angola": { "code": "ao", @@ -66,7 +66,7 @@ "timezones": [ "Africa/Luanda" ], - "iso": "+244" + "isd": "+244" }, "Anguilla": { "code": "ai", @@ -79,7 +79,7 @@ "timezones": [ "America/Anguilla" ], - "iso": "+1264" + "isd": "+1264" }, "Antarctica": { "code": "aq", @@ -96,7 +96,7 @@ "Antarctica/Syowa", "Antarctica/Vostok" ], - "iso": "+672" + "isd": "+672" }, "Antigua and Barbuda": { "code": "ag", @@ -109,7 +109,7 @@ "timezones": [ "America/Antigua" ], - "iso": "+1268" + "isd": "+1268" }, "Argentina": { "code": "ar", @@ -133,7 +133,7 @@ "America/Argentina/Tucuman", "America/Argentina/Ushuaia" ], - "iso": "+54" + "isd": "+54" }, "Armenia": { "code": "am", @@ -146,7 +146,7 @@ "timezones": [ "Asia/Yerevan" ], - "iso": "+374" + "isd": "+374" }, "Aruba": { "code": "aw", @@ -159,7 +159,7 @@ "timezones": [ "America/Aruba" ], - "iso": "+297" + "isd": "+297" }, "Australia": { "code": "au", @@ -183,7 +183,7 @@ "Australia/Perth", "Australia/Sydney" ], - "iso": "+61" + "isd": "+61" }, "Austria": { "code": "at", @@ -196,7 +196,7 @@ "timezones": [ "Europe/Vienna" ], - "iso": "+43" + "isd": "+43" }, "Azerbaijan": { "code": "az", @@ -207,7 +207,7 @@ "timezones": [ "Asia/Baku" ], - "iso": "+994" + "isd": "+994" }, "Bahamas": { "code": "bs", @@ -217,7 +217,7 @@ "timezones": [ "America/Nassau" ], - "iso": "+1242" + "isd": "+1242" }, "Bahrain": { "code": "bh", @@ -230,7 +230,7 @@ "timezones": [ "Asia/Bahrain" ], - "iso": "+973" + "isd": "+973" }, "Bangladesh": { "code": "bd", @@ -243,7 +243,7 @@ "timezones": [ "Asia/Dhaka" ], - "iso": "+880" + "isd": "+880" }, "Barbados": { "code": "bb", @@ -256,7 +256,7 @@ "timezones": [ "America/Barbados" ], - "iso": "+1246" + "isd": "+1246" }, "Belarus": { "code": "by", @@ -267,7 +267,7 @@ "timezones": [ "Europe/Minsk" ], - "iso": "+375" + "isd": "+375" }, "Belgium": { "code": "be", @@ -280,7 +280,7 @@ "timezones": [ "Europe/Brussels" ], - "iso": "+32" + "isd": "+32" }, "Belize": { "code": "bz", @@ -294,7 +294,7 @@ "timezones": [ "America/Belize" ], - "iso": "+501" + "isd": "+501" }, "Benin": { "code": "bj", @@ -307,7 +307,7 @@ "timezones": [ "Africa/Porto-Novo" ], - "iso": "+229" + "isd": "+229" }, "Bermuda": { "code": "bm", @@ -320,7 +320,7 @@ "timezones": [ "Atlantic/Bermuda" ], - "iso": "+1441" + "isd": "+1441" }, "Bhutan": { "code": "bt", @@ -333,14 +333,14 @@ "timezones": [ "Asia/Thimphu" ], - "iso": "+975" + "isd": "+975" }, "Bolivia, Plurinational State of": { "code": "bo", "currency": "BOB", "currency_name": "Boliviano", "number_format": "#,###.##", - "iso": "+591" + "isd": "+591" }, "Bonaire, Sint Eustatius and Saba": { "code": "bq", @@ -356,7 +356,7 @@ "timezones": [ "Europe/Sarajevo" ], - "iso": "+387" + "isd": "+387" }, "Botswana": { "code": "bw", @@ -369,12 +369,12 @@ "timezones": [ "Africa/Gaborone" ], - "iso": "+267" + "isd": "+267" }, "Bouvet Island": { "code": "bv", "number_format": "#,###.##", - "iso": "+47" + "isd": "+47" }, "Brazil": { "code": "br", @@ -402,7 +402,7 @@ "America/Santarem", "America/Sao_Paulo" ], - "iso": "+55" + "isd": "+55" }, "British Indian Ocean Territory": { "code": "io", @@ -413,7 +413,7 @@ "timezones": [ "Indian/Chagos" ], - "iso": "+246" + "isd": "+246" }, "Brunei Darussalam": { "code": "bn", @@ -423,7 +423,7 @@ "timezones": [ "Asia/Brunei" ], - "iso": "+673" + "isd": "+673" }, "Bulgaria": { "code": "bg", @@ -436,7 +436,7 @@ "timezones": [ "Europe/Sofia" ], - "iso": "+359" + "isd": "+359" }, "Burkina Faso": { "code": "bf", @@ -449,7 +449,7 @@ "timezones": [ "Africa/Ouagadougou" ], - "iso": "+226" + "isd": "+226" }, "Burundi": { "code": "bi", @@ -462,7 +462,7 @@ "timezones": [ "Africa/Bujumbura" ], - "iso": "+257" + "isd": "+257" }, "Cambodia": { "code": "kh", @@ -475,7 +475,7 @@ "timezones": [ "Asia/Phnom_Penh" ], - "iso": "+855" + "isd": "+855" }, "Cameroon": { "code": "cm", @@ -488,7 +488,7 @@ "timezones": [ "Africa/Douala" ], - "iso": "+237" + "isd": "+237" }, "Canada": { "code": "ca", @@ -529,7 +529,7 @@ "America/Winnipeg", "America/Yellowknife" ], - "iso": "+1" + "isd": "+1" }, "Cape Verde": { "code": "cv", @@ -542,7 +542,7 @@ "timezones": [ "Atlantic/Cape_Verde" ], - "iso": "+238" + "isd": "+238" }, "Cayman Islands": { "code": "ky", @@ -555,7 +555,7 @@ "timezones": [ "America/Cayman" ], - "iso": "+ 345" + "isd": "+ 345" }, "Central African Republic": { "code": "cf", @@ -568,7 +568,7 @@ "timezones": [ "Africa/Bangui" ], - "iso": "+236" + "isd": "+236" }, "Chad": { "code": "td", @@ -581,7 +581,7 @@ "timezones": [ "Africa/Ndjamena" ], - "iso": "+235" + "isd": "+235" }, "Chile": { "code": "cl", @@ -595,7 +595,7 @@ "America/Santiago", "Pacific/Easter" ], - "iso": "+56" + "isd": "+56" }, "China": { "code": "cn", @@ -612,7 +612,7 @@ "Asia/Shanghai", "Asia/Urumqi" ], - "iso": "+86" + "isd": "+86" }, "Christmas Island": { "code": "cx", @@ -620,7 +620,7 @@ "timezones": [ "Indian/Christmas" ], - "iso": "+61" + "isd": "+61" }, "Cocos (Keeling) Islands": { "code": "cc", @@ -631,7 +631,7 @@ "timezones": [ "Indian/Cocos" ], - "iso": "+61" + "isd": "+61" }, "Colombia": { "code": "co", @@ -644,7 +644,7 @@ "timezones": [ "America/Bogota" ], - "iso": "+57" + "isd": "+57" }, "Comoros": { "code": "km", @@ -657,7 +657,7 @@ "timezones": [ "Indian/Comoro" ], - "iso": "+269" + "isd": "+269" }, "Congo": { "code": "cg", @@ -667,7 +667,7 @@ "currency_symbol": "FCFA", "currency_fraction": "Centime", "currency_fraction_units": 100, - "iso": "+242" + "isd": "+242" }, "Congo, The Democratic Republic of the": { "code": "cd", @@ -677,7 +677,7 @@ "currency_symbol": "FC", "currency_fraction": "Centime", "currency_fraction_units": 100, - "iso": "+243" + "isd": "+243" }, "Cook Islands": { "code": "ck", @@ -688,7 +688,7 @@ "timezones": [ "Pacific/Rarotonga" ], - "iso": "+682" + "isd": "+682" }, "Costa Rica": { "code": "cr", @@ -701,7 +701,7 @@ "timezones": [ "America/Costa_Rica" ], - "iso": "+506" + "isd": "+506" }, "Croatia": { "code": "hr", @@ -714,7 +714,7 @@ "timezones": [ "Europe/Zagreb" ], - "iso": "+385" + "isd": "+385" }, "Cuba": { "code": "cu", @@ -727,7 +727,7 @@ "timezones": [ "America/Havana" ], - "iso": "+53" + "isd": "+53" }, "Cura\u00e7ao": { "code": "cw", @@ -747,7 +747,7 @@ "timezones": [ "Asia/Nicosia" ], - "iso": "+357" + "isd": "+357" }, "Czech Republic": { "code": "cz", @@ -760,7 +760,7 @@ "timezones": [ "Europe/Prague" ], - "iso": "+420" + "isd": "+420" }, "Denmark": { "code": "dk", @@ -773,7 +773,7 @@ "timezones": [ "Europe/Copenhagen" ], - "iso": "+45" + "isd": "+45" }, "Djibouti": { "code": "dj", @@ -786,7 +786,7 @@ "timezones": [ "Africa/Djibouti" ], - "iso": "+253" + "isd": "+253" }, "Dominica": { "code": "dm", @@ -799,7 +799,7 @@ "timezones": [ "America/Dominica" ], - "iso": "+1767" + "isd": "+1767" }, "Dominican Republic": { "code": "do", @@ -812,7 +812,7 @@ "timezones": [ "America/Santo_Domingo" ], - "iso": "+1849" + "isd": "+1849" }, "Ecuador": { "code": "ec", @@ -824,7 +824,7 @@ "America/Guayaquil", "Pacific/Galapagos" ], - "iso": "+593" + "isd": "+593" }, "Egypt": { "code": "eg", @@ -837,7 +837,7 @@ "timezones": [ "Africa/Cairo" ], - "iso": "+20" + "isd": "+20" }, "El Salvador": { "code": "sv", @@ -852,7 +852,7 @@ "timezones": [ "America/El_Salvador" ], - "iso": "+503" + "isd": "+503" }, "Equatorial Guinea": { "code": "gq", @@ -865,7 +865,7 @@ "timezones": [ "Africa/Malabo" ], - "iso": "+240" + "isd": "+240" }, "Eritrea": { "code": "er", @@ -878,7 +878,7 @@ "timezones": [ "Africa/Asmara" ], - "iso": "+291" + "isd": "+291" }, "Estonia": { "code": "ee", @@ -891,7 +891,7 @@ "timezones": [ "Europe/Tallinn" ], - "iso": "+372" + "isd": "+372" }, "Ethiopia": { "code": "et", @@ -904,14 +904,14 @@ "timezones": [ "Africa/Addis_Ababa" ], - "iso": "+251" + "isd": "+251" }, "Falkland Islands (Malvinas)": { "code": "fk", "currency": "FKP", "currency_name": "Falkland Islands Pound", "number_format": "#,###.##", - "iso": "+500" + "isd": "+500" }, "Faroe Islands": { "code": "fo", @@ -922,7 +922,7 @@ "timezones": [ "Atlantic/Faroe" ], - "iso": "+298" + "isd": "+298" }, "Fiji": { "code": "fj", @@ -935,7 +935,7 @@ "timezones": [ "Pacific/Fiji" ], - "iso": "+679" + "isd": "+679" }, "Finland": { "code": "fi", @@ -948,7 +948,7 @@ "timezones": [ "Europe/Helsinki" ], - "iso": "+358" + "isd": "+358" }, "France": { "code": "fr", @@ -962,7 +962,7 @@ "timezones": [ "Europe/Paris" ], - "iso": "+33" + "isd": "+33" }, "French Guiana": { "code": "gf", @@ -970,7 +970,7 @@ "timezones": [ "America/Cayenne" ], - "iso": "+594" + "isd": "+594" }, "French Polynesia": { "code": "pf", @@ -983,12 +983,12 @@ "Pacific/Marquesas", "Pacific/Tahiti" ], - "iso": "+689" + "isd": "+689" }, "French Southern Territories": { "code": "tf", "number_format": "#,###.##", - "iso": "+262" + "isd": "+262" }, "Gabon": { "code": "ga", @@ -1001,7 +1001,7 @@ "timezones": [ "Africa/Libreville" ], - "iso": "+241" + "isd": "+241" }, "Gambia": { "code": "gm", @@ -1011,7 +1011,7 @@ "timezones": [ "Africa/Banjul" ], - "iso": "+220" + "isd": "+220" }, "Georgia": { "code": "ge", @@ -1022,7 +1022,7 @@ "timezones": [ "Asia/Tbilisi" ], - "iso": "+995" + "isd": "+995" }, "Germany": { "code": "de", @@ -1035,7 +1035,7 @@ "timezones": [ "Europe/Berlin" ], - "iso": "+49" + "isd": "+49" }, "Ghana": { "code": "gh", @@ -1047,7 +1047,7 @@ "timezones": [ "Africa/Accra" ], - "iso": "+233" + "isd": "+233" }, "Gibraltar": { "code": "gi", @@ -1060,7 +1060,7 @@ "timezones": [ "Europe/Gibraltar" ], - "iso": "+350" + "isd": "+350" }, "Greece": { "code": "gr", @@ -1073,7 +1073,7 @@ "timezones": [ "Europe/Athens" ], - "iso": "+30" + "isd": "+30" }, "Greenland": { "code": "gl", @@ -1084,7 +1084,7 @@ "America/Scoresbysund", "America/Thule" ], - "iso": "+299" + "isd": "+299" }, "Grenada": { "code": "gd", @@ -1097,7 +1097,7 @@ "timezones": [ "America/Grenada" ], - "iso": "+1473" + "isd": "+1473" }, "Guadeloupe": { "code": "gp", @@ -1105,7 +1105,7 @@ "timezones": [ "America/Guadeloupe" ], - "iso": "+590" + "isd": "+590" }, "Guam": { "code": "gu", @@ -1113,7 +1113,7 @@ "timezones": [ "Pacific/Guam" ], - "iso": "+1671" + "isd": "+1671" }, "Guatemala": { "code": "gt", @@ -1126,7 +1126,7 @@ "timezones": [ "America/Guatemala" ], - "iso": "+502" + "isd": "+502" }, "Guernsey": { "code": "gg", @@ -1137,7 +1137,7 @@ "timezones": [ "Europe/London" ], - "iso": "+44" + "isd": "+44" }, "Guinea": { "code": "gn", @@ -1150,7 +1150,7 @@ "timezones": [ "Africa/Conakry" ], - "iso": "+224" + "isd": "+224" }, "Guinea-Bissau": { "code": "gw", @@ -1163,7 +1163,7 @@ "timezones": [ "Africa/Bissau" ], - "iso": "+245" + "isd": "+245" }, "Guyana": { "code": "gy", @@ -1176,7 +1176,7 @@ "timezones": [ "America/Guyana" ], - "iso": "+592" + "isd": "+592" }, "Haiti": { "code": "ht", @@ -1190,17 +1190,17 @@ "America/Guatemala", "America/Port-au-Prince" ], - "iso": "+509" + "isd": "+509" }, "Heard Island and McDonald Islands": { "code": "hm", "number_format": "#,###.##", - "iso": "+0" + "isd": "+0" }, "Holy See (Vatican City State)": { "code": "va", "number_format": "#,###.##", - "iso": "+379" + "isd": "+379" }, "Honduras": { "code": "hn", @@ -1213,7 +1213,7 @@ "timezones": [ "America/Tegucigalpa" ], - "iso": "+504" + "isd": "+504" }, "Hong Kong": { "code": "hk", @@ -1226,7 +1226,7 @@ "timezones": [ "Asia/Hong_Kong" ], - "iso": "+852" + "isd": "+852" }, "Hungary": { "code": "hu", @@ -1240,7 +1240,7 @@ "timezones": [ "Europe/Budapest" ], - "iso": "+36" + "isd": "+36" }, "Iceland": { "code": "is", @@ -1253,7 +1253,7 @@ "timezones": [ "Atlantic/Reykjavik" ], - "iso": "+354" + "isd": "+354" }, "India": { "code": "in", @@ -1266,7 +1266,7 @@ "timezones": [ "Asia/Kolkata" ], - "iso": "+91" + "isd": "+91" }, "Indonesia": { "code": "id", @@ -1282,7 +1282,7 @@ "Asia/Makassar", "Asia/Pontianak" ], - "iso": "+62" + "isd": "+62" }, "Iran": { "code": "ir", @@ -1293,7 +1293,7 @@ "timezones": [ "Asia/Tehran" ], - "iso": "+98" + "isd": "+98" }, "Iraq": { "code": "iq", @@ -1306,7 +1306,7 @@ "timezones": [ "Asia/Baghdad" ], - "iso": "+964" + "isd": "+964" }, "Ireland": { "code": "ie", @@ -1319,7 +1319,7 @@ "timezones": [ "Europe/Dublin" ], - "iso": "+353" + "isd": "+353" }, "Isle of Man": { "code": "im", @@ -1330,7 +1330,7 @@ "timezones": [ "Europe/London" ], - "iso": "+44" + "isd": "+44" }, "Israel": { "code": "il", @@ -1343,7 +1343,7 @@ "timezones": [ "Asia/Jerusalem" ], - "iso": "+972" + "isd": "+972" }, "Italy": { "code": "it", @@ -1357,7 +1357,7 @@ "timezones": [ "Europe/Rome" ], - "iso": "+39" + "isd": "+39" }, "Ivory Coast": { "code": "ci", @@ -1370,7 +1370,7 @@ "timeszones": [ "Africa/Abidjan" ], - "iso": "+225" + "isd": "+225" }, "Jamaica": { "code": "jm", @@ -1383,7 +1383,7 @@ "timezones": [ "America/Jamaica" ], - "iso": "+1876" + "isd": "+1876" }, "Japan": { "code": "jp", @@ -1396,7 +1396,7 @@ "timezones": [ "Asia/Tokyo" ], - "iso": "+81" + "isd": "+81" }, "Jersey": { "code": "je", @@ -1407,7 +1407,7 @@ "timezones": [ "Europe/London" ], - "iso": "+44" + "isd": "+44" }, "Jordan": { "code": "jo", @@ -1420,7 +1420,7 @@ "timezones": [ "Asia/Amman" ], - "iso": "+962" + "isd": "+962" }, "Kazakhstan": { "code": "kz", @@ -1437,7 +1437,7 @@ "Asia/Oral", "Asia/Qyzylorda" ], - "iso": "+7" + "isd": "+7" }, "Kenya": { "code": "ke", @@ -1450,7 +1450,7 @@ "timezones": [ "Africa/Nairobi" ], - "iso": "+254" + "isd": "+254" }, "Kiribati": { "code": "ki", @@ -1463,21 +1463,21 @@ "Pacific/Kiritimati", "Pacific/Tarawa" ], - "iso": "+686" + "isd": "+686" }, "Korea, Democratic Peoples Republic of": { "code": "kp", "currency": "KPW", "currency_name": "North Korean Won", "number_format": "#,###.##", - "iso": "+850" + "isd": "+850" }, "Korea, Republic of": { "code": "kr", "currency": "KRW", "currency_name": "Won", "number_format": "#,###", - "iso": "+82" + "isd": "+82" }, "Kuwait": { "code": "kw", @@ -1490,7 +1490,7 @@ "timezones": [ "Asia/Kuwait" ], - "iso": "+965" + "isd": "+965" }, "Kyrgyzstan": { "code": "kg", @@ -1503,7 +1503,7 @@ "timezones": [ "Asia/Bishkek" ], - "iso": "+996" + "isd": "+996" }, "Lao Peoples Democratic Republic": { "code": "la", @@ -1513,7 +1513,7 @@ "timezones": [ "Asia/Vientiane" ], - "iso": "+856" + "isd": "+856" }, "Latvia": { "code": "lv", @@ -1526,7 +1526,7 @@ "timezones": [ "Europe/Riga" ], - "iso": "+371" + "isd": "+371" }, "Lebanon": { "code": "lb", @@ -1539,7 +1539,7 @@ "timezones": [ "Asia/Beirut" ], - "iso": "+961" + "isd": "+961" }, "Lesotho": { "code": "ls", @@ -1552,7 +1552,7 @@ "timezones": [ "Africa/Maseru" ], - "iso": "+266" + "isd": "+266" }, "Liberia": { "code": "lr", @@ -1565,7 +1565,7 @@ "timezones": [ "Africa/Monrovia" ], - "iso": "+231" + "isd": "+231" }, "Libya": { "code": "ly", @@ -1578,7 +1578,7 @@ "timezones": [ "Africa/Tripoli" ], - "iso": "+218" + "isd": "+218" }, "Liechtenstein": { "code": "li", @@ -1589,7 +1589,7 @@ "timezones": [ "Europe/Vaduz" ], - "iso": "+423" + "isd": "+423" }, "Lithuania": { "code": "lt", @@ -1603,7 +1603,7 @@ "timezones": [ "Europe/Vilnius" ], - "iso": "+370" + "isd": "+370" }, "Luxembourg": { "code": "lu", @@ -1616,14 +1616,14 @@ "timezones": [ "Europe/Luxembourg" ], - "iso": "+352" + "isd": "+352" }, "Macao": { "code": "mo", "currency": "MOP", "currency_name": "Pataca", "number_format": "#,###.##", - "iso": "+853" + "isd": "+853" }, "Macedonia": { "code": "mk", @@ -1633,7 +1633,7 @@ "currency_name": "Denar", "currency_symbol": "\u0434\u0435\u043d", "number_format": "#,###.##", - "iso": "+389" + "isd": "+389" }, "Madagascar": { "code": "mg", @@ -1644,7 +1644,7 @@ "timezones": [ "Indian/Antananarivo" ], - "iso": "+261" + "isd": "+261" }, "Malawi": { "code": "mw", @@ -1657,7 +1657,7 @@ "timezones": [ "Africa/Blantyre" ], - "iso": "+265" + "isd": "+265" }, "Malaysia": { "code": "my", @@ -1671,7 +1671,7 @@ "Asia/Kuala_Lumpur", "Asia/Kuching" ], - "iso": "+60" + "isd": "+60" }, "Maldives": { "code": "mv", @@ -1684,7 +1684,7 @@ "timezones": [ "Indian/Maldives" ], - "iso": "+960" + "isd": "+960" }, "Mali": { "code": "ml", @@ -1697,7 +1697,7 @@ "timezones": [ "Africa/Bamako" ], - "iso": "+223" + "isd": "+223" }, "Malta": { "code": "mt", @@ -1710,7 +1710,7 @@ "timezones": [ "Europe/Malta" ], - "iso": "+356" + "isd": "+356" }, "Marshall Islands": { "code": "mh", @@ -1722,7 +1722,7 @@ "Pacific/Kwajalein", "Pacific/Majuro" ], - "iso": "+692" + "isd": "+692" }, "Martinique": { "code": "mq", @@ -1730,7 +1730,7 @@ "timezones": [ "America/Martinique" ], - "iso": "+596" + "isd": "+596" }, "Mauritania": { "code": "mr", @@ -1743,7 +1743,7 @@ "timezones": [ "Africa/Nouakchott" ], - "iso": "+222" + "isd": "+222" }, "Mauritius": { "code": "mu", @@ -1756,7 +1756,7 @@ "timezones": [ "Indian/Mauritius" ], - "iso": "+230" + "isd": "+230" }, "Mayotte": { "code": "yt", @@ -1764,7 +1764,7 @@ "timezones": [ "Indian/Mayotte" ], - "iso": "+262" + "isd": "+262" }, "Mexico": { "code": "mx", @@ -1788,19 +1788,19 @@ "America/Santa_Isabel", "America/Tijuana" ], - "iso": "+52" + "isd": "+52" }, "Micronesia, Federated States of": { "code": "fm", "number_format": "#,###.##", - "iso": "+691" + "isd": "+691" }, "Moldova, Republic of": { "code": "md", "currency": "MDL", "currency_name": "Moldovan Leu", "number_format": "#,###.##", - "iso": "+373" + "isd": "+373" }, "Monaco": { "code": "mc", @@ -1813,7 +1813,7 @@ "timezones": [ "Europe/Monaco" ], - "iso": "+377" + "isd": "+377" }, "Mongolia": { "code": "mn", @@ -1829,7 +1829,7 @@ "Asia/Hovd", "Asia/Ulaanbaatar" ], - "iso": "+976" + "isd": "+976" }, "Montenegro": { "code": "me", @@ -1842,7 +1842,7 @@ "timezones": [ "Europe/Belgrade" ], - "iso": "+382" + "isd": "+382" }, "Montserrat": { "code": "ms", @@ -1855,7 +1855,7 @@ "timezones": [ "America/Montserrat" ], - "iso": "+1664" + "isd": "+1664" }, "Morocco": { "code": "ma", @@ -1868,7 +1868,7 @@ "timezones": [ "Africa/Casablanca" ], - "iso": "+212" + "isd": "+212" }, "Mozambique": { "code": "mz", @@ -1880,7 +1880,7 @@ "timezones": [ "Africa/Maputo" ], - "iso": "+258" + "isd": "+258" }, "Myanmar": { "code": "mm", @@ -1890,7 +1890,7 @@ "timezones": [ "Asia/Rangoon" ], - "iso": "+95" + "isd": "+95" }, "Namibia": { "code": "na", @@ -1903,7 +1903,7 @@ "timezones": [ "Africa/Windhoek" ], - "iso": "+264" + "isd": "+264" }, "Nauru": { "code": "nr", @@ -1914,7 +1914,7 @@ "timezones": [ "Pacific/Nauru" ], - "iso": "+674" + "isd": "+674" }, "Nepal": { "code": "np", @@ -1927,7 +1927,7 @@ "timezones": [ "Asia/Kathmandu" ], - "iso": "+977" + "isd": "+977" }, "Netherlands": { "code": "nl", @@ -1940,7 +1940,7 @@ "timezones": [ "Europe/Amsterdam" ], - "iso": "+31" + "isd": "+31" }, "New Caledonia": { "code": "nc", @@ -1951,7 +1951,7 @@ "timezones": [ "Pacific/Noumea" ], - "iso": "+687" + "isd": "+687" }, "New Zealand": { "code": "nz", @@ -1965,7 +1965,7 @@ "Pacific/Auckland", "Pacific/Chatham" ], - "iso": "+64" + "isd": "+64" }, "Nicaragua": { "code": "ni", @@ -1978,7 +1978,7 @@ "timezones": [ "America/Managua" ], - "iso": "+505" + "isd": "+505" }, "Niger": { "code": "ne", @@ -1991,7 +1991,7 @@ "timezones": [ "Africa/Niamey" ], - "iso": "+227" + "isd": "+227" }, "Nigeria": { "code": "ng", @@ -2004,7 +2004,7 @@ "timezones": [ "Africa/Lagos" ], - "iso": "+234" + "isd": "+234" }, "Niue": { "code": "nu", @@ -2015,7 +2015,7 @@ "timezones": [ "Pacific/Niue" ], - "iso": "+683" + "isd": "+683" }, "Norfolk Island": { "code": "nf", @@ -2023,7 +2023,7 @@ "timezones": [ "Pacific/Norfolk" ], - "iso": "+672" + "isd": "+672" }, "Northern Mariana Islands": { "code": "mp", @@ -2031,7 +2031,7 @@ "timezones": [ "Pacific/Saipan" ], - "iso": "+1670" + "isd": "+1670" }, "Norway": { "code": "no", @@ -2044,7 +2044,7 @@ "timezones": [ "Europe/Oslo" ], - "iso": "+47" + "isd": "+47" }, "Oman": { "code": "om", @@ -2057,7 +2057,7 @@ "timezones": [ "Asia/Muscat" ], - "iso": "+968" + "isd": "+968" }, "Pakistan": { "code": "pk", @@ -2070,7 +2070,7 @@ "timezones": [ "Asia/Karachi" ], - "iso": "+92" + "isd": "+92" }, "Palau": { "code": "pw", @@ -2082,12 +2082,12 @@ "timezones": [ "Pacific/Palau" ], - "iso": "+680" + "isd": "+680" }, "Palestinian Territory, Occupied": { "code": "ps", "number_format": "#,###.##", - "iso": "+970" + "isd": "+970" }, "Panama": { "code": "pa", @@ -2098,7 +2098,7 @@ "timezones": [ "America/Panama" ], - "iso": "+507" + "isd": "+507" }, "Papua New Guinea": { "code": "pg", @@ -2111,7 +2111,7 @@ "timezones": [ "Pacific/Port_Moresby" ], - "iso": "+675" + "isd": "+675" }, "Paraguay": { "code": "py", @@ -2124,7 +2124,7 @@ "timezones": [ "America/Asuncion" ], - "iso": "+595" + "isd": "+595" }, "Peru": { "code": "pe", @@ -2137,7 +2137,7 @@ "timezones": [ "America/Lima" ], - "iso": "+51" + "isd": "+51" }, "Philippines": { "code": "ph", @@ -2151,7 +2151,7 @@ "timezones": [ "Asia/Manila" ], - "iso": "+63" + "isd": "+63" }, "Pitcairn": { "code": "pn", @@ -2159,7 +2159,7 @@ "timezones": [ "Pacific/Pitcairn" ], - "iso": "+64" + "isd": "+64" }, "Poland": { "code": "pl", @@ -2171,7 +2171,7 @@ "timezones": [ "Europe/Warsaw" ], - "iso": "+48" + "isd": "+48" }, "Portugal": { "code": "pt", @@ -2186,7 +2186,7 @@ "Atlantic/Madeira", "Europe/Lisbon" ], - "iso": "+351" + "isd": "+351" }, "Puerto Rico": { "code": "pr", @@ -2194,7 +2194,7 @@ "timezones": [ "America/Puerto_Rico" ], - "iso": "+1939" + "isd": "+1939" }, "Qatar": { "code": "qa", @@ -2207,7 +2207,7 @@ "timezones": [ "Asia/Qatar" ], - "iso": "+974" + "isd": "+974" }, "Romania": { "code": "ro", @@ -2220,14 +2220,14 @@ "timezones": [ "Europe/Bucharest" ], - "iso": "+40" + "isd": "+40" }, "Russian Federation": { "code": "ru", "currency": "RUB", "currency_name": "Russian Ruble", "number_format": "#.###,##", - "iso": "+7" + "isd": "+7" }, "Rwanda": { "code": "rw", @@ -2240,24 +2240,24 @@ "timezones": [ "Africa/Kigali" ], - "iso": "+250" + "isd": "+250" }, "R\u00e9union": { "code": "re", "number_format": "#,###.##", - "iso": "+262" + "isd": "+262" }, "Saint Barth\u00e9lemy": { "code": "bl", "number_format": "#,###.##", - "iso": "+590" + "isd": "+590" }, "Saint Helena, Ascension and Tristan da Cunha": { "code": "sh", "currency": "SHP", "currency_name": "Saint Helena Pound", "number_format": "#,###.##", - "iso": "+290" + "isd": "+290" }, "Saint Kitts and Nevis": { "code": "kn", @@ -2270,7 +2270,7 @@ "timezones": [ "America/St_Kitts" ], - "iso": "+1869" + "isd": "+1869" }, "Saint Lucia": { "code": "lc", @@ -2283,17 +2283,17 @@ "timezones": [ "America/St_Lucia" ], - "iso": "+1758" + "isd": "+1758" }, "Saint Martin (French part)": { "code": "mf", "number_format": "#,###.##", - "iso": "+590" + "isd": "+590" }, "Saint Pierre and Miquelon": { "code": "pm", "number_format": "#,###.##", - "iso": "+508" + "isd": "+508" }, "Saint Vincent and the Grenadines": { "code": "vc", @@ -2306,7 +2306,7 @@ "timezones": [ "America/St_Vincent" ], - "iso": "+1784" + "isd": "+1784" }, "Samoa": { "code": "ws", @@ -2319,7 +2319,7 @@ "timezones": [ "Pacific/Apia" ], - "iso": "+685" + "isd": "+685" }, "San Marino": { "code": "sm", @@ -2332,14 +2332,14 @@ "timezones": [ "Europe/Rome" ], - "iso": "+378" + "isd": "+378" }, "Sao Tome and Principe": { "code": "st", "currency": "STD", "currency_name": "Dobra", "number_format": "#,###.##", - "iso": "+239" + "isd": "+239" }, "Saudi Arabia": { "code": "sa", @@ -2352,7 +2352,7 @@ "timezones": [ "Asia/Riyadh" ], - "iso": "+966" + "isd": "+966" }, "Senegal": { "code": "sn", @@ -2365,7 +2365,7 @@ "timezones": [ "Africa/Dakar" ], - "iso": "+221" + "isd": "+221" }, "Serbia": { "code": "rs", @@ -2378,7 +2378,7 @@ "timezones": [ "Europe/Belgrade" ], - "iso": "+381" + "isd": "+381" }, "Seychelles": { "code": "sc", @@ -2391,7 +2391,7 @@ "timezones": [ "Indian/Mahe" ], - "iso": "+248" + "isd": "+248" }, "Sierra Leone": { "code": "sl", @@ -2404,7 +2404,7 @@ "timezones": [ "Africa/Freetown" ], - "iso": "+232" + "isd": "+232" }, "Singapore": { "code": "sg", @@ -2417,7 +2417,7 @@ "timezones": [ "Asia/Singapore" ], - "iso": "+65" + "isd": "+65" }, "Sint Maarten (Dutch part)": { "code": "sx", @@ -2434,7 +2434,7 @@ "timezones": [ "Europe/Bratislava" ], - "iso": "+421" + "isd": "+421" }, "Slovenia": { "code": "si", @@ -2447,7 +2447,7 @@ "timezones": [ "Europe/Belgrade" ], - "iso": "+386" + "isd": "+386" }, "Solomon Islands": { "code": "sb", @@ -2460,7 +2460,7 @@ "timezones": [ "Pacific/Guadalcanal" ], - "iso": "+677" + "isd": "+677" }, "Somalia": { "code": "so", @@ -2473,7 +2473,7 @@ "timezones": [ "Africa/Mogadishu" ], - "iso": "+252" + "isd": "+252" }, "South Africa": { "code": "za", @@ -2487,7 +2487,7 @@ "timezones": [ "Africa/Johannesburg" ], - "iso": "+27" + "isd": "+27" }, "South Georgia and the South Sandwich Islands": { "code": "gs", @@ -2495,7 +2495,7 @@ "currency_fraction_units": 100, "currency_symbol": "\u00a3", "number_format": "#,###.##", - "iso": "+500" + "isd": "+500" }, "South Sudan": { "code": "ss", @@ -2506,7 +2506,7 @@ "timezones": [ "Africa/Juba" ], - "iso": "+211" + "isd": "+211" }, "Spain": { "code": "es", @@ -2521,7 +2521,7 @@ "Atlantic/Canary", "Europe/Madrid" ], - "iso": "+34" + "isd": "+34" }, "Sri Lanka": { "code": "lk", @@ -2534,7 +2534,7 @@ "timezones": [ "Asia/Colombo" ], - "iso": "+94" + "isd": "+94" }, "Sudan": { "code": "sd", @@ -2545,7 +2545,7 @@ "timezones": [ "Africa/Khartoum" ], - "iso": "+249" + "isd": "+249" }, "Suriname": { "code": "sr", @@ -2557,12 +2557,12 @@ "timezones": [ "America/Paramaribo" ], - "iso": "+597" + "isd": "+597" }, "Svalbard and Jan Mayen": { "code": "sj", "number_format": "#,###.##", - "iso": "+47" + "isd": "+47" }, "Swaziland": { "code": "sz", @@ -2575,7 +2575,7 @@ "timezones": [ "Africa/Mbabane" ], - "iso": "+268" + "isd": "+268" }, "Sweden": { "code": "se", @@ -2588,7 +2588,7 @@ "timezones": [ "Europe/Stockholm" ], - "iso": "+46" + "isd": "+46" }, "Switzerland": { "code": "ch", @@ -2602,21 +2602,21 @@ "timezones": [ "Europe/Zurich" ], - "iso": "+41" + "isd": "+41" }, "Syria": { "code": "sy", "currency": "SYP", "currency_name": "Syrian Pound", "number_format": "#,###.##", - "iso": "+963" + "isd": "+963" }, "Taiwan": { "code": "tw", "currency": "TWD", "date_format": "yyyy-mm-dd", "number_format": "#,###.##", - "iso": "+886" + "isd": "+886" }, "Tajikistan": { "code": "tj", @@ -2627,14 +2627,14 @@ "timezones": [ "Asia/Dushanbe" ], - "iso": "+992" + "isd": "+992" }, "Tanzania": { "code": "tz", "currency": "TZS", "currency_name": "Tanzanian Shilling", "number_format": "#,###.##", - "iso": "+255" + "isd": "+255" }, "Thailand": { "code": "th", @@ -2647,12 +2647,12 @@ "timezones": [ "Asia/Bangkok" ], - "iso": "+66" + "isd": "+66" }, "Timor-Leste": { "code": "tl", "number_format": "#,###.##", - "iso": "+670" + "isd": "+670" }, "Togo": { "code": "tg", @@ -2665,7 +2665,7 @@ "timezones": [ "Africa/Lome" ], - "iso": "+228" + "isd": "+228" }, "Tokelau": { "code": "tk", @@ -2673,7 +2673,7 @@ "timezones": [ "Pacific/Fakaofo" ], - "iso": "+690" + "isd": "+690" }, "Tonga": { "code": "to", @@ -2686,7 +2686,7 @@ "timezones": [ "Pacific/Tongatapu" ], - "iso": "+676" + "isd": "+676" }, "Trinidad and Tobago": { "code": "tt", @@ -2699,7 +2699,7 @@ "timezones": [ "America/Port_of_Spain" ], - "iso": "+1868" + "isd": "+1868" }, "Tunisia": { "code": "tn", @@ -2712,7 +2712,7 @@ "timezones": [ "Africa/Tunis" ], - "iso": "+216" + "isd": "+216" }, "Turkey": { "code": "tr", @@ -2724,7 +2724,7 @@ "timezones": [ "Europe/Istanbul" ], - "iso": "+90" + "isd": "+90" }, "Turkmenistan": { "code": "tm", @@ -2737,7 +2737,7 @@ "timezones": [ "Asia/Ashgabat" ], - "iso": "+993" + "isd": "+993" }, "Turks and Caicos Islands": { "code": "tc", @@ -2745,7 +2745,7 @@ "currency_fraction_units": 100, "currency_symbol": "$", "number_format": "#,###.##", - "iso": "+1649" + "isd": "+1649" }, "Tuvalu": { "code": "tv", @@ -2756,7 +2756,7 @@ "timezones": [ "Pacific/Funafuti" ], - "iso": "+688" + "isd": "+688" }, "Uganda": { "code": "ug", @@ -2769,7 +2769,7 @@ "timezones": [ "Africa/Kampala" ], - "iso": "+256" + "isd": "+256" }, "Ukraine": { "code": "ua", @@ -2785,7 +2785,7 @@ "Europe/Uzhgorod", "Europe/Zaporozhye" ], - "iso": "+380" + "isd": "+380" }, "United Arab Emirates": { "code": "ae", @@ -2798,7 +2798,7 @@ "timezones": [ "Asia/Dubai" ], - "iso": "+971" + "isd": "+971" }, "United Kingdom": { "code": "gb", @@ -2811,7 +2811,7 @@ "timezones": [ "Europe/London" ], - "iso": "+44" + "isd": "+44" }, "United States": { "code": "us", @@ -2855,7 +2855,7 @@ "America/Yakutat", "Pacific/Honolulu" ], - "iso": "+1" + "isd": "+1" }, "United States Minor Outlying Islands": { "code": "um", @@ -2872,7 +2872,7 @@ "timezones": [ "America/Montevideo" ], - "iso": "+598" + "isd": "+598" }, "Uzbekistan": { "code": "uz", @@ -2886,7 +2886,7 @@ "Asia/Samarkand", "Asia/Tashkent" ], - "iso": "+998" + "isd": "+998" }, "Vanuatu": { "code": "vu", @@ -2899,7 +2899,7 @@ "timezones": [ "Pacific/Efate" ], - "iso": "+678" + "isd": "+678" }, "Venezuela, Bolivarian Republic of": { "code": "ve", @@ -2908,24 +2908,24 @@ "currency_symbol": "Bs.", "currency_fraction": "Centimos", "currency_fraction_units": 100, - "iso": "+58" + "isd": "+58" }, "Vietnam": { "code": "vn", "currency": "VND", "currency_name": "Dong", "number_format": "#.###", - "iso": "+84" + "isd": "+84" }, "Virgin Islands, British": { "code": "vg", "number_format": "#,###.##", - "iso": "+1284" + "isd": "+1284" }, "Virgin Islands, U.S.": { "code": "vi", "number_format": "#,###.##", - "iso": "+1340" + "isd": "+1340" }, "Wallis and Futuna": { "code": "wf", @@ -2933,7 +2933,7 @@ "currency_fraction_units": 100, "currency_symbol": "Fr", "number_format": "#,###.##", - "iso": "+681" + "isd": "+681" }, "Western Sahara": { "code": "eh", @@ -2954,7 +2954,7 @@ "timezones": [ "Asia/Aden" ], - "iso": "+967" + "isd": "+967" }, "Zambia": { "code": "zm", @@ -2967,7 +2967,7 @@ "timezones": [ "Africa/Lusaka" ], - "iso": "+260" + "isd": "+260" }, "Zimbabwe": { "code": "zw", @@ -2980,11 +2980,11 @@ "timezones": [ "Africa/Harare" ], - "iso": "+263" + "isd": "+263" }, "\u00c5land Islands": { "code": "ax", "number_format": "#,###.##", - "iso": "+358" + "isd": "+358" } -} \ No newline at end of file +} diff --git a/frappe/public/js/frappe/form/controls/phone.js b/frappe/public/js/frappe/form/controls/phone.js index bf5a865d02..308957ccb5 100644 --- a/frappe/public/js/frappe/form/controls/phone.js +++ b/frappe/public/js/frappe/form/controls/phone.js @@ -13,7 +13,7 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD // Replaces code when selected and removes previously selected. this.picker.on_change = (country) => { const country_code = frappe.boot.country_codes[country].code; - const country_isd = frappe.boot.country_codes[country].iso; + const country_isd = frappe.boot.country_codes[country].isd; this.change_flag(country_code); this.$icon = this.selected_icon.find('svg'); this.$flag = this.selected_icon.find('img'); @@ -151,7 +151,7 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD } get_country(country) { const country_codes = frappe.boot.country_codes; - return country_codes[country].iso; + return country_codes[country].isd; } get_country_flag(country) { const country_codes = frappe.boot.country_codes; diff --git a/frappe/public/js/frappe/phone_picker/phone_picker.js b/frappe/public/js/frappe/phone_picker/phone_picker.js index 7270fe63a1..34241be915 100644 --- a/frappe/public/js/frappe/phone_picker/phone_picker.js +++ b/frappe/public/js/frappe/phone_picker/phone_picker.js @@ -34,11 +34,11 @@ class Picker { setup_countries() { Object.entries(this.countries).forEach(([country, info]) => { - if (!info.iso) { + if (!info.isd) { return } let $country = $(`
${frappe.utils.flag(info.code)} - ${country} (${info.iso})
`); + ${country} (${info.isd})
`); this.phone_wrapper.append($country); const set_values = () => { this.set_country(country); From ad26e277182a245a5a30b32f3a88b59efb9d199e Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 7 Jan 2022 12:51:42 +0530 Subject: [PATCH 013/139] fix: updated test --- cypress/integration/control_phone.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cypress/integration/control_phone.js b/cypress/integration/control_phone.js index 684e0d7d99..f59ebd2667 100644 --- a/cypress/integration/control_phone.js +++ b/cypress/integration/control_phone.js @@ -4,7 +4,7 @@ context('Control Phone', () => { cy.visit('/app/website'); }); - function get_dialog_with_phone(s) { + function get_dialog_with_phone() { return cy.dialog({ title: 'Phone', fields: [{ @@ -19,10 +19,10 @@ context('Control Phone', () => { cy.get('.selected-phone').click(); cy.get('.phone-picker .phone-wrapper[id="afghanistan"]').click(); cy.get('.phone-picker .phone-wrapper[id="india"]').click(); - cy.get('.selected-phone .country').should('have.text', '+91') - cy.get('.selected-phone > .icon > use').should('have.attr', 'href').and('include', '#in') + cy.get('.selected-phone .country').should('have.text', '+91'); + cy.get('.selected-phone > img').should('have.attr', 'src').and('include', '/in.png'); - let phone_number = '9312672712' + let phone_number = '9312672712'; cy.get('.selected-phone').click().first(); cy.get('.frappe-control[data-fieldname=phone] input') .first() From 938d12b14258fd5fbdb51ed0d2351397d3159df1 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 7 Jan 2022 12:52:09 +0530 Subject: [PATCH 014/139] fix: sider --- frappe/database/mariadb/database.py | 4 ++-- frappe/database/postgres/database.py | 4 ++-- frappe/public/js/frappe/form/controls/control.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index 5e838db842..9e0e8e1b51 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -52,8 +52,8 @@ class MariaDBDatabase(Database): 'Barcode': ('longtext', ''), 'Geolocation': ('longtext', ''), 'Duration': ('decimal', '21,9'), - 'Icon': ('varchar', self.VARCHAR_LEN), - 'Phone': ('varchar', self.VARCHAR_LEN) + 'Icon': ('varchar', self.VARCHAR_LEN), + 'Phone': ('varchar', self.VARCHAR_LEN) } def get_connection(self): diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index 71b5918d4a..3ad3c10869 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -62,8 +62,8 @@ class PostgresDatabase(Database): 'Barcode': ('text', ''), 'Geolocation': ('text', ''), 'Duration': ('decimal', '21,9'), - 'Icon': ('varchar', self.VARCHAR_LEN), - 'Phone': ('varchar', self.VARCHAR_LEN) + 'Icon': ('varchar', self.VARCHAR_LEN), + 'Phone': ('varchar', self.VARCHAR_LEN) } def get_connection(self): diff --git a/frappe/public/js/frappe/form/controls/control.js b/frappe/public/js/frappe/form/controls/control.js index 578ddd3276..dad3ed1bec 100644 --- a/frappe/public/js/frappe/form/controls/control.js +++ b/frappe/public/js/frappe/form/controls/control.js @@ -40,7 +40,7 @@ import './multiselect_list'; import './rating'; import './duration'; import './icon'; -import './phone' +import './phone'; frappe.ui.form.make_control = function (opts) { var control_class_name = "Control" + opts.df.fieldtype.replace(/ /g, ""); From b45e5dda63df0bc95341033a5d847cf6f2c27155 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 7 Jan 2022 13:40:03 +0530 Subject: [PATCH 015/139] refactor: moved phone_validation to frappe.utils --- frappe/model/base_document.py | 10 +--------- frappe/public/js/frappe/form/controls/phone.js | 2 +- frappe/utils/__init__.py | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index ac87e564ac..b4f1910bbc 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -11,7 +11,6 @@ from frappe.model import display_fieldtypes from frappe.utils import (cint, flt, now, cstr, strip_html, sanitize_html, sanitize_email, cast_fieldtype) from frappe.utils.html_utils import unescape_html -import phonenumbers as ph max_positive_value = { 'smallint': 2 ** 15, @@ -655,14 +654,7 @@ class BaseDocument(object): # data_field options defined in frappe.model.data_field_options for phone_field in self.meta.get_phone_fields(): phone = self.get(phone_field.fieldname) - try: - phone = ph.parse(phone) - except Exception as e: - if e.error_type == 1: - frappe.throw(_("The entered value is not a phone number."), title="Invalid Number") - frappe.throw(_("Please select a country code."), title = _("Country Code Required")) - if not ph.is_valid_number(phone): - frappe.throw('This is not a valid phone number', title = "Invalid Number") + frappe.utils.validate_phone_number_with_isd(phone, throw=True) for data_field in self.meta.get_data_fields(): data = self.get(data_field.fieldname) diff --git a/frappe/public/js/frappe/form/controls/phone.js b/frappe/public/js/frappe/form/controls/phone.js index 308957ccb5..492b75f8c3 100644 --- a/frappe/public/js/frappe/form/controls/phone.js +++ b/frappe/public/js/frappe/form/controls/phone.js @@ -128,7 +128,7 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD change_flag(country_code) { this.selected_icon.find('img').attr('src', 'https://flagcdn.com/h20/'+country_code+'.png') this.$icon = this.selected_icon.find('img'); - // this.$icon.hasClass('hide') && this.$icon.toggleClass('hide'); + this.$icon.hasClass('hide') && this.$icon.toggleClass('hide'); } // country_code for India is 'in' diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 1b4cdc7922..add55fc238 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -17,6 +17,7 @@ from typing import Generator, Iterable from urllib.parse import quote, urlparse from werkzeug.test import Client from redis.exceptions import ConnectionError +import phonenumbers as ph import frappe # utility functions like cint, int, flt, etc. @@ -71,6 +72,21 @@ def extract_email_id(email): email_id = email_id.decode("utf-8", "ignore") return email_id +def validate_phone_number_with_isd(phone, throw=False): + if not phone: + return + try: + phone = ph.parse(phone) + except Exception as e: + if e.error_type == 1: + frappe.throw(frappe._("The entered value is not a phone number."), frappe.InvalidPhoneNumberError, + title=frappe._("Invalid Number")) + frappe.throw(frappe._("Please select a country code."), frappe.InvalidPhoneNumberError, + title = frappe._("Country Code Required")) + if not ph.is_valid_number(phone): + frappe.throw(frappe._("This is not a valid phone number"), frappe.InvalidPhoneNumberError, + title = frappe._("Invalid Number")) + def validate_phone_number(phone_number, throw=False): """Returns True if valid phone number""" if not phone_number: From 8d00c4ff7611363ba8b94bd6f70cc68c58f60cf7 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 7 Jan 2022 14:17:56 +0530 Subject: [PATCH 016/139] fix: sider and styling --- cypress/integration/control_phone.js | 2 +- frappe/database/mariadb/database.py | 2 +- .../public/js/frappe/form/controls/phone.js | 21 ++++--- .../js/frappe/phone_picker/phone_picker.js | 58 +++++++++---------- frappe/public/js/frappe/utils/utils.js | 13 +---- frappe/public/scss/common/phone_picker.scss | 2 +- 6 files changed, 45 insertions(+), 53 deletions(-) diff --git a/cypress/integration/control_phone.js b/cypress/integration/control_phone.js index f59ebd2667..952e31a9eb 100644 --- a/cypress/integration/control_phone.js +++ b/cypress/integration/control_phone.js @@ -20,7 +20,7 @@ context('Control Phone', () => { cy.get('.phone-picker .phone-wrapper[id="afghanistan"]').click(); cy.get('.phone-picker .phone-wrapper[id="india"]').click(); cy.get('.selected-phone .country').should('have.text', '+91'); - cy.get('.selected-phone > img').should('have.attr', 'src').and('include', '/in.png'); + cy.get('.selected-phone > img').should('have.attr', 'src').and('include', '/in.svg'); let phone_number = '9312672712'; cy.get('.selected-phone').click().first(); diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index 21d5a9962c..fde86a7876 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -52,7 +52,7 @@ class MariaDBDatabase(Database): 'Barcode': ('longtext', ''), 'Geolocation': ('longtext', ''), 'Duration': ('decimal', '21,9'), - 'Icon': ('varchar', self.VARCHAR_LEN), + 'Icon': ('varchar', self.VARCHAR_LEN), 'Phone': ('varchar', self.VARCHAR_LEN) } diff --git a/frappe/public/js/frappe/form/controls/phone.js b/frappe/public/js/frappe/form/controls/phone.js index 492b75f8c3..7144b31f1e 100644 --- a/frappe/public/js/frappe/form/controls/phone.js +++ b/frappe/public/js/frappe/form/controls/phone.js @@ -17,7 +17,7 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD this.change_flag(country_code); this.$icon = this.selected_icon.find('svg'); this.$flag = this.selected_icon.find('img'); - if (!this.$icon.hasClass('hide')){ + if (!this.$icon.hasClass('hide')) { this.$icon.toggleClass('hide'); } if (!this.$flag.length) { @@ -28,7 +28,7 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD } else { this.$isd.text(country_isd); } - if(this.$input.val()) { + if (this.$input.val()) { this.set_formatted_input(this.get_country(country) +'-'+ this.$input.val()); } }; @@ -85,7 +85,7 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD this.selected_icon.insertAfter(this.$input); this.selected_icon.append($(``)); this.$isd = this.selected_icon.find('.country'); - if(input_value && input_value.split("-").length == 2) { + if (input_value && input_value.split("-").length == 2) { this.$isd.text(this.value.split("-")[0]); } } @@ -95,7 +95,7 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD super.refresh(); // Previously opened doc values get fetched. - if(!this.value) { + if (!this.value) { this.$input.val(""); this.$wrapper.find('.country').text(""); if (this.selected_icon.find('svg').hasClass('hide')) { @@ -103,30 +103,30 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD this.selected_icon.find('img').addClass('hide'); } } - if(this.value && this.value.split("-").length == 2) { + if (this.value && this.value.split("-").length == 2) { let isd = this.value.split("-")[0]; this.get_country_code_and_change_flag(isd); this.picker.set_country(isd); this.picker.refresh(); if (this.picker.country && this.picker.country !== this.$isd.text()) { - this.$isd.length && this.$isd.text(isd) + this.$isd.length && this.$isd.text(isd); } } } set_formatted_input(value) { - if(value && value.includes('-')) { + if (value && value.includes('-')) { this.set_model_value(value); this.$input.val(value.split("-").pop()); - } else if(this.$isd.text().trim() && this.value) { + } else if (this.$isd.text().trim() && this.value) { let code_number = this.$isd.text() + '-' + value; this.set_model_value(code_number); } } change_flag(country_code) { - this.selected_icon.find('img').attr('src', 'https://flagcdn.com/h20/'+country_code+'.png') + this.selected_icon.find('img').attr('src', 'https://flagcdn.com/'+country_code+'.svg'); this.$icon = this.selected_icon.find('img'); this.$icon.hasClass('hide') && this.$icon.toggleClass('hide'); } @@ -142,8 +142,7 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD if (!flag.length) { this.selected_icon.prepend(this.get_country_flag(country)); this.selected_icon.find('svg').addClass('hide'); - } - else { + } else { this.change_flag(code); } } diff --git a/frappe/public/js/frappe/phone_picker/phone_picker.js b/frappe/public/js/frappe/phone_picker/phone_picker.js index 34241be915..cbc5df4b12 100644 --- a/frappe/public/js/frappe/phone_picker/phone_picker.js +++ b/frappe/public/js/frappe/phone_picker/phone_picker.js @@ -34,38 +34,38 @@ class Picker { setup_countries() { Object.entries(this.countries).forEach(([country, info]) => { - if (!info.isd) { - return - } - let $country = $(`
${frappe.utils.flag(info.code)} - ${country} (${info.isd})
`); - this.phone_wrapper.append($country); - const set_values = () => { - this.set_country(country); - this.update_icon_selected(); - }; - $country.on('click', () => { - set_values(); - }); - $country.hover(() => { - $country.toggleClass("bg-gray-100"); - }); - this.search_input.keydown((e) => { - const key_code = e.keyCode; - if ([13].includes(key_code)) { - e.preventDefault(); - set_values(); + if (!info.isd) { + return; } - }); - this.search_input.keyup((e) => { - e.preventDefault(); - this.filter_icons(); - }); + let $country = $(`
${frappe.utils.flag(info.code)} + ${country} (${info.isd})
`); + this.phone_wrapper.append($country); + const set_values = () => { + this.set_country(country); + this.update_icon_selected(); + }; + $country.on('click', () => { + set_values(); + }); + $country.hover(() => { + $country.toggleClass("bg-gray-100"); + }); + this.search_input.keydown((e) => { + const key_code = e.keyCode; + if ([13].includes(key_code)) { + e.preventDefault(); + set_values(); + } + }); + this.search_input.keyup((e) => { + e.preventDefault(); + this.filter_icons(); + }); - this.search_input.on('search', () => { - this.filter_icons(); + this.search_input.on('search', () => { + this.filter_icons(); + }); }); - }); } filter_icons() { diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index c7739c82a1..3cde43f529 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1164,17 +1164,10 @@ Object.assign(frappe.utils, { `; }, - flag(icon_name, size="sm", icon_class="", icon_style="", svg_class="") { - let size_class = ""; - - if (typeof size == "object") { - icon_style += ` width: ${size.width}; height: ${size.height}`; - } else { - size_class = `flag-${size}`; - } + flag(country_code) { return `` + src="https://flagcdn.com/${country_code}.svg" + width="20" height="15">`; }, make_chart(wrapper, custom_options={}) { diff --git a/frappe/public/scss/common/phone_picker.scss b/frappe/public/scss/common/phone_picker.scss index cc3387ecb0..dea364d157 100644 --- a/frappe/public/scss/common/phone_picker.scss +++ b/frappe/public/scss/common/phone_picker.scss @@ -75,7 +75,7 @@ height: 20px; border-radius: 5px; position: absolute; - top: calc(50% + 2.6px); + top: calc(50% + 2px); left: 8px; content: ' '; align-items: center; From 0b418d27281fecded77a5c545bbd606689119fa5 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 25 Feb 2022 01:08:31 +0530 Subject: [PATCH 017/139] perf: Pass document through rename_doc API to bypass extra get_doc --- frappe/model/rename_doc.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 787f276b17..8891bd20ea 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Dict, List, Optional import frappe from frappe import _, bold +from frappe.model.document import Document from frappe.model.dynamic_links import get_dynamic_link_map from frappe.model.naming import validate_name from frappe.model.utils.user_settings import sync_user_settings, update_user_settings_data @@ -47,7 +48,7 @@ def update_document_title( name_updated = updated_name != doc.name if name_updated: - docname = rename_doc(doctype=doctype, old=docname, new=updated_name, merge=merge) + docname = rename_doc(doc=doc, new=updated_name, merge=merge) if title_updated: try: @@ -65,17 +66,27 @@ def update_document_title( return docname def rename_doc( - doctype: str, - old: str, - new: str, + doctype: Optional[str] = None, + old: Optional[str] = None, + new: str = None, force: bool = False, merge: bool = False, ignore_permissions: bool = False, ignore_if_exists: bool = False, show_alert: bool = True, rebuild_search: bool = True, + doc: Optional[Document] = None, ) -> str: """Rename a doc(dt, old) to doc(dt, new) and update all linked fields of type "Link".""" + old_usage_style = doctype and old and new + new_usage_style = doc and new + + if not (new_usage_style or old_usage_style): + raise TypeError("{doctype, old, new} or {doc, new} are required arguments for frappe.model.rename_doc") + + old = old or doc.name + doctype = doctype or doc.doctype + if not frappe.db.exists(doctype, old): return @@ -91,7 +102,7 @@ def rename_doc( meta = frappe.get_meta(doctype) # call before_rename - old_doc = frappe.get_doc(doctype, old) + old_doc = doc or frappe.get_doc(doctype, old) out = old_doc.run_method("before_rename", old, new, merge) or {} new = (out.get("new") or new) if isinstance(out, dict) else (out or new) new = validate_rename(doctype, new, meta, merge, force, ignore_permissions) From 5a3d8f925ee93a393ff5143d372b4e97bdfa4bb2 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 25 Feb 2022 01:27:21 +0530 Subject: [PATCH 018/139] feat: Document.rename API Transform current document object using the rename_doc API. The design of the API should allow for easy action queueing. Defined as `rename(self, name: str, merge: bool = False, force: bool = False)` Usage: In [1]: doc = frappe.get_doc("Person", "5a188f66c1") In [2]: doc.rename("5a188f66c2") In [3]: doc.name Out[3]: '5a188f66c2' --- frappe/model/document.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frappe/model/document.py b/frappe/model/document.py index cb36c18b47..5017d342c1 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -938,6 +938,14 @@ class Document(BaseDocument): self.docstatus = DocStatus.cancelled() return self.save() + @whitelist.__func__ + def _rename(self, name: str, merge: bool = False, force: bool = False): + """Cancel the document. Sets `docstatus` = 2, then saves. + """ + from frappe.model.rename_doc import rename_doc + self.name = rename_doc(doc=self, new=name, merge=merge, force=force) + self.reload() + @whitelist.__func__ def submit(self): """Submit the document. Sets `docstatus` = 1, then saves.""" @@ -948,6 +956,12 @@ class Document(BaseDocument): """Cancel the document. Sets `docstatus` = 2, then saves.""" return self._cancel() + @whitelist.__func__ + def rename(self, name: str, merge: bool = False, force: bool = False): + """Rename the document to `name`. This transforms the current object. + """ + return self._rename(name=name, merge=merge, force=force) + def delete(self, ignore_permissions=False): """Delete document.""" frappe.delete_doc(self.doctype, self.name, ignore_permissions = ignore_permissions, flags=self.flags) From 040e01eb59b90b7c2b0ebf103324a83065306c08 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 25 Feb 2022 01:43:11 +0530 Subject: [PATCH 019/139] refactor: doc.enqueue_action & execute_action * Make args "internal" to avoid name collision. Added __ since these args would be utilized by the internal API alone * perf(EAFP): Try taking a lock and then figure out if there's an error instead of the double computation * Notify document update on completion of Document Action --- frappe/model/document.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/frappe/model/document.py b/frappe/model/document.py index 5017d342c1..0bfba3d78b 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -1318,16 +1318,17 @@ class Document(BaseDocument): # See: Stock Reconciliation from frappe.utils.background_jobs import enqueue - if hasattr(self, '_' + action): - action = '_' + action + if hasattr(self, f"_{action}"): + action = f"_{action}" - if file_lock.lock_exists(self.get_signature()): + try: + self.lock() + except frappe.DocumentLockedError: frappe.throw(_('This document is currently queued for execution. Please try again'), title=_('Document Queued')) - self.lock() - enqueue('frappe.model.document.execute_action', doctype=self.doctype, name=self.name, - action=action, **kwargs) + return enqueue('frappe.model.document.execute_action', __doctype=self.doctype, __name=self.name, + __action=action, **kwargs) def lock(self, timeout=None): """Creates a lock file for the given document. If timeout is set, @@ -1402,12 +1403,12 @@ class Document(BaseDocument): return f"{doctype}({name})" -def execute_action(doctype, name, action, **kwargs): +def execute_action(__doctype, __name, __action, **kwargs): """Execute an action on a document (called by background worker)""" - doc = frappe.get_doc(doctype, name) + doc = frappe.get_doc(__doctype, __name) doc.unlock() try: - getattr(doc, action)(**kwargs) + getattr(doc, __action)(**kwargs) except Exception: frappe.db.rollback() @@ -1418,7 +1419,4 @@ def execute_action(doctype, name, action, **kwargs): msg = '
' + frappe.get_traceback() + '
' doc.add_comment('Comment', _('Action Failed') + '

' + msg) - doc.notify_update() - - - + doc.notify_update() From 6c5a975cec8b2cd625651cf768e07a08fa0da379 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 28 Feb 2022 18:02:38 +0530 Subject: [PATCH 020/139] test: Added test_doc_rename --- frappe/tests/test_rename_doc.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frappe/tests/test_rename_doc.py b/frappe/tests/test_rename_doc.py index 0c1cba31fb..7630356c6d 100644 --- a/frappe/tests/test_rename_doc.py +++ b/frappe/tests/test_rename_doc.py @@ -237,3 +237,10 @@ class TestRenameDoc(unittest.TestCase): update_linked_doctypes("User", "ToDo", "str", "str") self.assertTrue("Function frappe.model.rename_doc.update_linked_doctypes" in stdout.getvalue()) + + def test_doc_rename(self): + name = choice(self.available_documents) + new_name = f"{name}-{frappe.generate_hash(length=4)}" + doc = frappe.get_doc(self.test_doctype, name) + doc.rename(new_name, merge=frappe.db.exists(self.test_doctype, new_name)) + self.assertEqual(doc.name, new_name) From 429162ba313f117ade5cd4857e65407d1e93ef74 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 28 Feb 2022 18:05:36 +0530 Subject: [PATCH 021/139] fix: Use rename doc method wrapper --- frappe/model/rename_doc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 8891bd20ea..196cbee27d 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -48,7 +48,7 @@ def update_document_title( name_updated = updated_name != doc.name if name_updated: - docname = rename_doc(doc=doc, new=updated_name, merge=merge) + doc.rename(updated_name, merge=merge) if title_updated: try: @@ -63,7 +63,7 @@ def update_document_title( ) raise - return docname + return doc.name def rename_doc( doctype: Optional[str] = None, From cd824f1c0815dcca2b784bcf185a90de59d27cd0 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 28 Feb 2022 18:58:52 +0530 Subject: [PATCH 022/139] test(fix): Add property setter for ToDo to be made rename-able --- frappe/tests/test_rename_doc.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frappe/tests/test_rename_doc.py b/frappe/tests/test_rename_doc.py index 7630356c6d..f02a43a83d 100644 --- a/frappe/tests/test_rename_doc.py +++ b/frappe/tests/test_rename_doc.py @@ -48,6 +48,14 @@ class TestRenameDoc(unittest.TestCase): # data generation: for base and merge tests self.available_documents = [] self.test_doctype = "ToDo" + self.property_setter = frappe.get_doc({ + "doctype":"Property Setter", + "doctype_or_field": "DocType", + "doc_type": self.test_doctype, + "property": "allow_rename", + "property_type": "Check", + "value": "1", + }).insert() for num in range(1, 5): doc = frappe.get_doc({ @@ -85,6 +93,7 @@ class TestRenameDoc(unittest.TestCase): # delete the documents created for docname in self.available_documents: frappe.delete_doc(self.test_doctype, docname) + self.property_setter.delete() for dt in self.doctype.values(): if frappe.db.exists("DocType", dt): From 311a42ae5279b910f3e195f6b797e2e6eb14c993 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 28 Feb 2022 19:00:59 +0530 Subject: [PATCH 023/139] fix: Print to STDOUT to help debug via is_scheduler_inactive --- frappe/utils/scheduler.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/frappe/utils/scheduler.py b/frappe/utils/scheduler.py index 8ebb4b2937..346049141a 100755 --- a/frappe/utils/scheduler.py +++ b/frappe/utils/scheduler.py @@ -24,6 +24,15 @@ from frappe.utils.background_jobs import get_jobs DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S' + +def cprint(*args, **kwargs): + """Prints only if called from STDOUT""" + try: + os.get_terminal_size() + print(*args, **kwargs) + except Exception: + pass + def start_scheduler(): '''Run enqueue_events_for_all_sites every 2 minutes (default). Specify scheduler_interval in seconds in common_site_config.json''' @@ -86,9 +95,11 @@ def enqueue_events(site): def is_scheduler_inactive(): if frappe.local.conf.maintenance_mode: + cprint("Maintenance mode is ON") return True if frappe.local.conf.pause_scheduler: + cprint("frappe.conf.pause_scheduler is SET") return True if is_scheduler_disabled(): @@ -98,9 +109,13 @@ def is_scheduler_inactive(): def is_scheduler_disabled(): if frappe.conf.disable_scheduler: + cprint("frappe.conf.disable_scheduler is SET") return True - return not frappe.utils.cint(frappe.db.get_single_value("System Settings", "enable_scheduler")) + scheduler_disabled = not frappe.utils.cint(frappe.db.get_single_value("System Settings", "enable_scheduler")) + if scheduler_disabled: + cprint("SystemSettings.enable_scheduler is UNSET") + return scheduler_disabled def toggle_scheduler(enable): frappe.db.set_value("System Settings", None, "enable_scheduler", 1 if enable else 0) From 2b3d9cbcd8566622a19457643f56f19af611d161 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 28 Feb 2022 19:46:58 +0530 Subject: [PATCH 024/139] feat: Rename Document via Background Job --- frappe/model/rename_doc.py | 6 +++++- frappe/public/js/frappe/form/toolbar.js | 11 +++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 196cbee27d..3231c82230 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -11,6 +11,7 @@ from frappe.model.utils.user_settings import sync_user_settings, update_user_set from frappe.query_builder import Field from frappe.utils import cint from frappe.utils.password import rename_password +from frappe.utils.scheduler import is_scheduler_inactive if TYPE_CHECKING: from frappe.model.meta import Meta @@ -48,7 +49,10 @@ def update_document_title( name_updated = updated_name != doc.name if name_updated: - doc.rename(updated_name, merge=merge) + if is_scheduler_inactive(): + doc.rename(updated_name, merge=merge) + else: + doc.queue_action("rename", name=updated_name, merge=merge) if title_updated: try: diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 500ec88068..a5938391c2 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -98,6 +98,17 @@ frappe.ui.form.Toolbar = class Toolbar { } let rename_document = () => { + frappe.socketio.doc_subscribe(doctype, new_name); + frappe.realtime.on("doc_update", data => { + if (data.doctype == doctype && data.name == new_name) { + $(document).trigger("rename", [doctype, docname, new_name]); + if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname]; + this.frm.reload_doc(); + frappe.show_alert( + __('Document renamed from {0} to {1}', [docname.bold(), new_name.bold()]) + ) + } + }); return frappe.xcall("frappe.model.rename_doc.update_document_title", { doctype, docname, From 1ff0bbf6c64e0b940e9d44faebddc1b25178a89b Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 1 Mar 2022 11:49:44 +0530 Subject: [PATCH 025/139] test(fix): Add cleanup for test_doc_rename_method --- frappe/tests/test_rename_doc.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/frappe/tests/test_rename_doc.py b/frappe/tests/test_rename_doc.py index f02a43a83d..2a4a3805f7 100644 --- a/frappe/tests/test_rename_doc.py +++ b/frappe/tests/test_rename_doc.py @@ -48,14 +48,6 @@ class TestRenameDoc(unittest.TestCase): # data generation: for base and merge tests self.available_documents = [] self.test_doctype = "ToDo" - self.property_setter = frappe.get_doc({ - "doctype":"Property Setter", - "doctype_or_field": "DocType", - "doc_type": self.test_doctype, - "property": "allow_rename", - "property_type": "Check", - "value": "1", - }).insert() for num in range(1, 5): doc = frappe.get_doc({ @@ -93,7 +85,6 @@ class TestRenameDoc(unittest.TestCase): # delete the documents created for docname in self.available_documents: frappe.delete_doc(self.test_doctype, docname) - self.property_setter.delete() for dt in self.doctype.values(): if frappe.db.exists("DocType", dt): @@ -107,8 +98,23 @@ class TestRenameDoc(unittest.TestCase): def setUp(self): frappe.flags.link_fields = {} + if self._testMethodName == "test_doc_rename_method": + self.property_setter = frappe.get_doc({ + "doctype":"Property Setter", + "doctype_or_field": "DocType", + "doc_type": self.test_doctype, + "property": "allow_rename", + "property_type": "Check", + "value": "1", + }).insert() + super().setUp() + def tearDown(self) -> None: + if self._testMethodName == "test_doc_rename_method": + self.property_setter.delete() + return super().tearDown() + def test_rename_doc(self): """Rename an existing document via frappe.rename_doc""" old_name = choice(self.available_documents) @@ -247,9 +253,11 @@ class TestRenameDoc(unittest.TestCase): update_linked_doctypes("User", "ToDo", "str", "str") self.assertTrue("Function frappe.model.rename_doc.update_linked_doctypes" in stdout.getvalue()) - def test_doc_rename(self): + def test_doc_rename_method(self): name = choice(self.available_documents) new_name = f"{name}-{frappe.generate_hash(length=4)}" doc = frappe.get_doc(self.test_doctype, name) doc.rename(new_name, merge=frappe.db.exists(self.test_doctype, new_name)) self.assertEqual(doc.name, new_name) + self.available_documents.append(new_name) + self.available_documents.remove(name) From 81a3058808c9cac2c0543f9606408fe086341fdf Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 1 Mar 2022 13:22:46 +0530 Subject: [PATCH 026/139] fix: enqueue option for update_document_title API Pass enqueue=True to enqueue rename document operation. Desk will be using this from now on ;) --- frappe/model/rename_doc.py | 17 +++++++++++++---- frappe/public/js/frappe/form/toolbar.js | 3 ++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 3231c82230..8c66f408b3 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -25,10 +25,19 @@ def update_document_title( title: Optional[str] = None, name: Optional[str] = None, merge: bool = False, + enqueue: bool = False, **kwargs ) -> str: """ - Update title from header in form view + Update the name or title of a document. Returns `name` if document was renamed, + `docname` if renaming operation was queued. + + :param doctype: DocType of the document + :param docname: Name of the document + :param title: New Title of the document + :param name: New Name of the document + :param merge: Merge the current Document with the existing one if exists + :param enqueue: Enqueue the rename operation, title is updated in current process """ # to maintain backwards API compatibility @@ -49,10 +58,10 @@ def update_document_title( name_updated = updated_name != doc.name if name_updated: - if is_scheduler_inactive(): - doc.rename(updated_name, merge=merge) - else: + if enqueue and not is_scheduler_inactive(): doc.queue_action("rename", name=updated_name, merge=merge) + else: + doc.rename(updated_name, merge=merge) if title_updated: try: diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index a5938391c2..c0c24d86bf 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -106,7 +106,7 @@ frappe.ui.form.Toolbar = class Toolbar { this.frm.reload_doc(); frappe.show_alert( __('Document renamed from {0} to {1}', [docname.bold(), new_name.bold()]) - ) + ); } }); return frappe.xcall("frappe.model.rename_doc.update_document_title", { @@ -114,6 +114,7 @@ frappe.ui.form.Toolbar = class Toolbar { docname, name: new_name, title: new_title, + enqueue: true, merge }).then(new_docname => { if (new_name != docname) { From 9e36005c70a815089f3dacdd75c0262d5169eb6e Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 3 Mar 2022 19:59:56 +0530 Subject: [PATCH 027/139] fix: Remove taken file locks at end of background job --- frappe/__init__.py | 1 + frappe/model/document.py | 2 ++ frappe/utils/background_jobs.py | 5 +++++ 3 files changed, 8 insertions(+) diff --git a/frappe/__init__.py b/frappe/__init__.py index 8a8b70afe3..c985270b70 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -181,6 +181,7 @@ def init(site, sites_path=None, new_site=False): "new_site": new_site }) local.rollback_observers = [] + local.locked_documents = [] local.before_commit = [] local.test_objects = {} diff --git a/frappe/model/document.py b/frappe/model/document.py index 0bfba3d78b..68a02c883d 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -1347,10 +1347,12 @@ class Document(BaseDocument): if lock_exists: raise frappe.DocumentLockedError file_lock.create_lock(signature) + frappe.local.locked_documents.append(self) def unlock(self): """Delete the lock file for this document""" file_lock.delete_lock(self.get_signature()) + frappe.local.locked_documents.remove(self) # validation helpers def validate_from_to_dates(self, from_date_field, to_date_field): diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py index 58029dbc5f..b93fcc5605 100755 --- a/frappe/utils/background_jobs.py +++ b/frappe/utils/background_jobs.py @@ -147,6 +147,11 @@ def execute_job(site, method, event, job_name, kwargs, user=None, is_async=True, frappe.db.commit() finally: + # background job hygiene: release file locks if unreleased + # if this breaks something, move it to failed jobs alone - gavin@frappe.io + for doc in frappe.local.locked_documents: + doc.unlock() + frappe.monitor.stop() if is_async: frappe.destroy() From c6a0469f6a4d946d6a348de7bbf180e3fbc1669e Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 8 Mar 2022 17:57:18 +0530 Subject: [PATCH 028/139] fix: Remove lock record if exists --- frappe/model/document.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/model/document.py b/frappe/model/document.py index 5443dcff2f..883b90b464 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -1350,7 +1350,8 @@ class Document(BaseDocument): def unlock(self): """Delete the lock file for this document""" file_lock.delete_lock(self.get_signature()) - frappe.local.locked_documents.remove(self) + if self in frappe.local.locked_documents: + frappe.local.locked_documents.remove(self) # validation helpers def validate_from_to_dates(self, from_date_field, to_date_field): From ea0e0a5a7f66e4b37e48e51fd5941b993b32439f Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 8 Mar 2022 18:01:17 +0530 Subject: [PATCH 029/139] fix: Setup queued renaming listener based on API response --- frappe/public/js/frappe/form/toolbar.js | 32 +++++++++++++++---------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 44625c66ea..5cd429a4c3 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -85,12 +85,11 @@ frappe.ui.form.Toolbar = class Toolbar { }); } rename_document_title(new_name, new_title, merge=false) { + let confirm_message = null; const docname = this.frm.doc.name; const title_field = this.frm.meta.title_field || ''; const doctype = this.frm.doctype; - let confirm_message=null; - if (new_name) { const warning = __("This cannot be undone"); const message = __("Are you sure you want to merge {0} with {1}?", [docname.bold(), new_name.bold()]); @@ -98,17 +97,6 @@ frappe.ui.form.Toolbar = class Toolbar { } let rename_document = () => { - frappe.socketio.doc_subscribe(doctype, new_name); - frappe.realtime.on("doc_update", data => { - if (data.doctype == doctype && data.name == new_name) { - $(document).trigger("rename", [doctype, docname, new_name]); - if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname]; - this.frm.reload_doc(); - frappe.show_alert( - __('Document renamed from {0} to {1}', [docname.bold(), new_name.bold()]) - ); - } - }); return frappe.xcall("frappe.model.rename_doc.update_document_title", { doctype, docname, @@ -119,6 +107,24 @@ frappe.ui.form.Toolbar = class Toolbar { freeze: true, freeze_message: __("Updating related fields...") }).then(new_docname => { + // handle document renaming queued action + if (new_docname == docname) { + frappe.socketio.doc_subscribe(doctype, new_name); + frappe.realtime.on("doc_update", data => { + if (data.doctype == doctype && data.name == new_name) { + $(document).trigger("rename", [doctype, docname, new_name]); + if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname]; + this.frm.reload_doc(); + frappe.show_alert({ + message: __('Document renamed from {0} to {1}', [docname.bold(), new_name.bold()]), + indicator: 'success', + }); + } + }); + frappe.show_alert( + __('Document renaming from {0} to {1} has been queued', [docname.bold(), new_name.bold()]) + ); + } if (new_name != docname) { $(document).trigger("rename", [doctype, docname, new_docname || new_name]); if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname]; From d92a64e76795209fb422f5bd9c363c8cf905cde4 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 22 Mar 2022 14:53:22 +0530 Subject: [PATCH 030/139] fix: validate before enqueuing rename_doc * refactor validate_rename * don't run before_rename hooks twice * validate_rename kwarg in doc.rename --- frappe/model/document.py | 8 ++--- frappe/model/rename_doc.py | 67 ++++++++++++++++++++++++++------------ 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/frappe/model/document.py b/frappe/model/document.py index 883b90b464..52bd1f3196 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -939,11 +939,11 @@ class Document(BaseDocument): return self.save() @whitelist.__func__ - def _rename(self, name: str, merge: bool = False, force: bool = False): + def _rename(self, name: str, merge: bool = False, force: bool = False, validate_rename: bool = True): """Cancel the document. Sets `docstatus` = 2, then saves. """ from frappe.model.rename_doc import rename_doc - self.name = rename_doc(doc=self, new=name, merge=merge, force=force) + self.name = rename_doc(doc=self, new=name, merge=merge, force=force, validate=validate_rename) self.reload() @whitelist.__func__ @@ -957,10 +957,10 @@ class Document(BaseDocument): return self._cancel() @whitelist.__func__ - def rename(self, name: str, merge: bool = False, force: bool = False): + def rename(self, name: str, merge: bool = False, force: bool = False, validate_rename: bool = True): """Rename the document to `name`. This transforms the current object. """ - return self._rename(name=name, merge=merge, force=force) + return self._rename(name=name, merge=merge, force=force, validate_rename=validate_rename) def delete(self, ignore_permissions=False): """Delete document.""" diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index aad16664c8..e1a823ddf5 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -49,6 +49,10 @@ def update_document_title( if not isinstance(obj, (str, type(None))): frappe.throw(f"{obj=} must be of type str or None") + # handle bad API usages + merge = cint(merge) + enqueue = cint(enqueue) + doc = frappe.get_doc(doctype, docname) doc.check_permission(permtype="write") @@ -59,7 +63,25 @@ def update_document_title( if name_updated: if enqueue and not is_scheduler_inactive(): - doc.queue_action("rename", name=updated_name, merge=merge) + current_name = doc.name + + # before_name hook may have DocType specific validations or transformations + transformed_name = doc.run_method("before_rename", current_name, updated_name, merge) + if isinstance(transformed_name, dict): + transformed_name = transformed_name.get("new") + transformed_name = transformed_name or updated_name + + # run rename validations before queueing + new_name = validate_rename( + doctype=doctype, + old=current_name, + new=transformed_name, + meta=doc.meta, + merge=merge, + ) + + # we don't want to re-validate since the before_rename hooks would be run again ;) + doc.queue_action("rename", name=new_name, merge=merge, validate_rename=False) else: doc.rename(updated_name, merge=merge) @@ -89,6 +111,7 @@ def rename_doc( show_alert: bool = True, rebuild_search: bool = True, doc: Optional[Document] = None, + validate: bool = True, ) -> str: """Rename a doc(dt, old) to doc(dt, new) and update all linked fields of type "Link".""" old_usage_style = doctype and old and new @@ -99,28 +122,24 @@ def rename_doc( old = old or doc.name doctype = doctype or doc.doctype - - if not frappe.db.exists(doctype, old): - frappe.errprint(_("Failed: {0} to {1} because {0} doesn't exist.").format(old, new)) - return - - if ignore_if_exists and frappe.db.exists(doctype, new): - frappe.errprint(_("Failed: {0} to {1} because {1} already exists.").format(old, new)) - return - - if old==new: - frappe.errprint(_("Ignored: {0} to {1} no changes made because old and new name are the same.").format(old, new)) - return - force = cint(force) merge = cint(merge) meta = frappe.get_meta(doctype) - # call before_rename - old_doc = doc or frappe.get_doc(doctype, old) - out = old_doc.run_method("before_rename", old, new, merge) or {} - new = (out.get("new") or new) if isinstance(out, dict) else (out or new) - new = validate_rename(doctype, new, meta, merge, force, ignore_permissions) + if validate: + old_doc = doc or frappe.get_doc(doctype, old) + out = old_doc.run_method("before_rename", old, new, merge) or {} + new = (out.get("new") or new) if isinstance(out, dict) else (out or new) + new = validate_rename( + doctype=doctype, + old=old, + new=new, + meta=meta, + merge=merge, + force=force, + ignore_permissions=ignore_permissions, + ignore_if_exists=ignore_if_exists, + ) if not merge: rename_parent_and_child(doctype, old, new, meta) @@ -272,7 +291,7 @@ def update_autoname_field(doctype: str, new: str, meta: "Meta") -> None: if field and field[0] == "field": frappe.db.sql("UPDATE `tab{0}` SET `{1}`={2} WHERE `name`={2}".format(doctype, field[1], '%s'), (new, new)) -def validate_rename(doctype: str, new: str, meta: "Meta", merge: bool, force: bool, ignore_permissions: bool) -> str: +def validate_rename(doctype: str, old: str, new: str, meta: "Meta", merge: bool, force: bool = False, ignore_permissions: bool = False, ignore_if_exists: bool = False) -> str: # using for update so that it gets locked and someone else cannot edit it while this rename is going on! exists = ( frappe.qb.from_(doctype) @@ -283,6 +302,12 @@ def validate_rename(doctype: str, new: str, meta: "Meta", merge: bool, force: bo ) exists = exists[0] if exists else None + if not frappe.db.exists(doctype, old): + frappe.throw(_("Can't rename {0} to {1} because {0} doesn't exist.").format(old, new)) + + if old == new: + frappe.throw(_("No changes made because old and new name are the same.").format(old, new)) + if merge and not exists: frappe.throw(_("{0} {1} does not exist, select a new target to merge").format(doctype, new)) @@ -290,7 +315,7 @@ def validate_rename(doctype: str, new: str, meta: "Meta", merge: bool, force: bo # for fixing case, accents exists = None - if (not merge) and exists: + if not merge and exists and not ignore_if_exists: frappe.throw(_("Another {0} with name {1} exists, select another name").format(doctype, new)) if not (ignore_permissions or frappe.permissions.has_permission(doctype, "write", raise_exception=False)): From e2489f8377d9a141a8da50955014e984b6a5a54a Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 22 Mar 2022 14:55:10 +0530 Subject: [PATCH 031/139] fix: Validate title updates via update_document_title API --- frappe/model/rename_doc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index e1a823ddf5..eea47c3b38 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -87,7 +87,8 @@ def update_document_title( if title_updated: try: - frappe.db.set_value(doctype, docname, title_field, updated_title) + setattr(doc, title_field, updated_title) + doc.save() frappe.msgprint(_("Saved"), alert=True, indicator="green") except Exception as e: if frappe.db.is_duplicate_entry(e): From d4bf210a033659a957892f978823dd04fc2c5e0f Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Tue, 22 Mar 2022 15:06:42 +0530 Subject: [PATCH 032/139] style(ui): dynamic spacing between isd code and phone number --- frappe/public/js/frappe/form/controls/phone.js | 8 ++++++++ frappe/public/scss/common/phone_picker.scss | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/phone.js b/frappe/public/js/frappe/form/controls/phone.js index 7144b31f1e..060d41f328 100644 --- a/frappe/public/js/frappe/form/controls/phone.js +++ b/frappe/public/js/frappe/form/controls/phone.js @@ -17,6 +17,7 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD this.change_flag(country_code); this.$icon = this.selected_icon.find('svg'); this.$flag = this.selected_icon.find('img'); + if (!this.$icon.hasClass('hide')) { this.$icon.toggleClass('hide'); } @@ -111,6 +112,13 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD if (this.picker.country && this.picker.country !== this.$isd.text()) { this.$isd.length && this.$isd.text(isd); } + let len = this.$isd.text().length; + let diff = len - 3; + if (len > 3) { + this.$input.css("padding-left", 67 + (diff * 9)); + } else { + this.$input.css("padding-left", 67); + } } } diff --git a/frappe/public/scss/common/phone_picker.scss b/frappe/public/scss/common/phone_picker.scss index dea364d157..bbac895018 100644 --- a/frappe/public/scss/common/phone_picker.scss +++ b/frappe/public/scss/common/phone_picker.scss @@ -58,7 +58,7 @@ } .phone-picker-popover { max-width: 325px; - left: -20px !important; + left: 11px !important; .picker-arrow { left: 15px !important; } @@ -66,7 +66,7 @@ .frappe-control[data-fieldtype='Phone'] { input { - padding-left: 80px; + padding-left: 67px; } .selected-phone { display: flex; From 782a0b0c8482ab7c7a9adca4b442d174a4d9941c Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 22 Mar 2022 15:06:58 +0530 Subject: [PATCH 033/139] fix: Check if name was changed in client --- frappe/public/js/frappe/form/toolbar.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 9606c7ccf7..01bfae2cdd 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -108,7 +108,7 @@ frappe.ui.form.Toolbar = class Toolbar { freeze_message: __("Updating related fields...") }).then(new_docname => { // handle document renaming queued action - if (new_docname == docname) { + if (new_name && (new_docname == docname)) { frappe.socketio.doc_subscribe(doctype, new_name); frappe.realtime.on("doc_update", data => { if (data.doctype == doctype && data.name == new_name) { @@ -125,7 +125,8 @@ frappe.ui.form.Toolbar = class Toolbar { __('Document renaming from {0} to {1} has been queued', [docname.bold(), new_name.bold()]) ); } - if (new_name != docname) { + // handle document sync rename action + if (new_name && (new_name != docname)) { $(document).trigger("rename", [doctype, docname, new_docname || new_name]); if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname]; } From 70d0aaf52d0580d56a04afbe2f43c5e51ea81e07 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Tue, 22 Mar 2022 15:52:59 +0530 Subject: [PATCH 034/139] test(ui): updated click events to be specific --- cypress/integration/control_phone.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cypress/integration/control_phone.js b/cypress/integration/control_phone.js index 952e31a9eb..9bfce3059c 100644 --- a/cypress/integration/control_phone.js +++ b/cypress/integration/control_phone.js @@ -16,14 +16,14 @@ context('Control Phone', () => { it('should set flag and data', () => { get_dialog_with_phone().as('dialog'); - cy.get('.selected-phone').click(); + cy.get('.selected-phone > svg').click(); cy.get('.phone-picker .phone-wrapper[id="afghanistan"]').click(); cy.get('.phone-picker .phone-wrapper[id="india"]').click(); cy.get('.selected-phone .country').should('have.text', '+91'); cy.get('.selected-phone > img').should('have.attr', 'src').and('include', '/in.svg'); let phone_number = '9312672712'; - cy.get('.selected-phone').click().first(); + cy.get('.selected-phone > img').click().first(); cy.get('.frappe-control[data-fieldname=phone] input') .first() .click(); @@ -42,7 +42,7 @@ context('Control Phone', () => { it('case insensitive search for country and clear search', () => { let search_text = 'india'; - cy.get('.selected-phone').click().first(); + cy.get('.selected-phone > img').click().first(); cy.get('.phone-picker').findByRole('searchbox').click().type(search_text); cy.get('.phone-section .phone-wrapper:not(.hidden)').then(i => { cy.get(`.phone-section .phone-wrapper[id*='${search_text.toLowerCase()}']`).then(countries => { From 0e87013421a0e5d2022026be0f3ec0cb0173146c Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 22 Mar 2022 16:12:36 +0530 Subject: [PATCH 035/139] chore: Add docstring for rename_doc --- frappe/model/rename_doc.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index eea47c3b38..a321963527 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -114,7 +114,20 @@ def rename_doc( doc: Optional[Document] = None, validate: bool = True, ) -> str: - """Rename a doc(dt, old) to doc(dt, new) and update all linked fields of type "Link".""" + """Rename a doc(dt, old) to doc(dt, new) and update all linked fields of type "Link". + + doc: Document object to be renamed. + new: New name for the record. If None, and doctype is specified, new name may be automatically generated via before_rename hooks. + doctype: DocType of the document. Not required if doc is passed. + old: Current name of the document. Not required if doc is passed. + force: Allow even if document is not allowed to be renamed. + merge: Merge with existing document of new name. + ignore_permissions: Ignore user permissions while renaming. + ignore_if_exists: Don't raise exception if document with new name already exists. This will quietely overwrite the existing document. + show_alert: Display alert if document is renamed successfully. + rebuild_search: Rebuild linked doctype search after renaming. + validate: Validate before renaming. If False, it is assumed that the caller has already validated. + """ old_usage_style = doctype and old and new new_usage_style = doc and new From 1cb956d83540a58079c0d1dee98687132388e3de Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 22 Mar 2022 17:33:51 +0530 Subject: [PATCH 036/139] fix(rename_doc): Use sbool instead of cint cint("false") returns True which is what is sent by frappe dialog. This may be required to be fixed in the client alone but making this change to make the API more "robust" as this has been working in this particular way for far too long now :') --- 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 a321963527..5b609d35fb 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -9,7 +9,7 @@ from frappe.model.dynamic_links import get_dynamic_link_map from frappe.model.naming import validate_name from frappe.model.utils.user_settings import sync_user_settings, update_user_settings_data from frappe.query_builder import Field -from frappe.utils import cint +from frappe.utils.data import sbool from frappe.utils.password import rename_password from frappe.utils.scheduler import is_scheduler_inactive @@ -50,8 +50,8 @@ def update_document_title( frappe.throw(f"{obj=} must be of type str or None") # handle bad API usages - merge = cint(merge) - enqueue = cint(enqueue) + merge = sbool(merge) + enqueue = sbool(enqueue) doc = frappe.get_doc(doctype, docname) doc.check_permission(permtype="write") @@ -136,8 +136,8 @@ def rename_doc( old = old or doc.name doctype = doctype or doc.doctype - force = cint(force) - merge = cint(merge) + force = sbool(force) + merge = sbool(merge) meta = frappe.get_meta(doctype) if validate: From 3351cc9c805e5591c095be273d89a07f7f8e738a Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 22 Mar 2022 19:26:58 +0530 Subject: [PATCH 037/139] refactor(rename_doc): Use QB notation inplace of raw SQLs * Converted ~22 queries from raw SQL to use frappe.qb notation * Made queries DRY-er --- frappe/model/rename_doc.py | 288 +++++++++++++++++++------------------ 1 file changed, 147 insertions(+), 141 deletions(-) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 5b609d35fb..b68af0f1ce 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -191,8 +191,12 @@ def rename_doc( rename_password(doctype, old, new) # update user_permissions - frappe.db.sql("""UPDATE `tabDefaultValue` SET `defvalue`=%s WHERE `parenttype`='User Permission' - AND `defkey`=%s AND `defvalue`=%s""", (new, doctype, old)) + DefaultValue = DocType("DefaultValue") + frappe.qb.update(DefaultValue).set(DefaultValue.defvalue, new).where( + (DefaultValue.parenttype == "User Permission") + & (DefaultValue.defkey == doctype) + & (DefaultValue.defvalue == old) + ).run() if merge: new_doc.add_comment('Edit', _("merged {0} into {1}").format(frappe.bold(old), frappe.bold(new))) @@ -246,10 +250,11 @@ def update_user_settings(old: str, new: str, link_fields: List[Dict]) -> None: # find the user settings for the linked doctypes linked_doctypes = {d.parent for d in link_fields if not d.issingle} - user_settings_details = frappe.db.sql('''SELECT `user`, `doctype`, `data` - FROM `__UserSettings` - WHERE `data` like %s - AND `doctype` IN ('{doctypes}')'''.format(doctypes="', '".join(linked_doctypes)), (old), as_dict=1) + UserSettings = Table("__UserSettings") + + user_settings_details = frappe.qb.from_(UserSettings).select("user", "doctype", "data").where( + UserSettings.data.like(old) & UserSettings.doctype.isin(linked_doctypes) + ).run(as_dict=True) # create the dict using the doctype name as key and values as list of the user settings from collections import defaultdict @@ -270,31 +275,32 @@ def update_customizations(old: str, new: str) -> None: frappe.db.set_value("Custom DocPerm", {"parent": old}, "parent", new, update_modified=False) def update_attachments(doctype: str, old: str, new: str) -> None: - try: - if old != "File Data" and doctype != "DocType": - frappe.db.sql("""update `tabFile` set attached_to_name=%s - where attached_to_name=%s and attached_to_doctype=%s""", (new, old, doctype)) - except frappe.db.ProgrammingError as e: - if not frappe.db.is_column_missing(e): - raise + if doctype != "DocType": + File = DocType("File") + + frappe.qb.update(File).set(File.attached_to_name, new).where( + (File.attached_to_name == old) & (File.attached_to_doctype == doctype) + ).run() + def rename_versions(doctype: str, old: str, new: str) -> None: - frappe.db.sql("""UPDATE `tabVersion` SET `docname`=%s WHERE `ref_doctype`=%s AND `docname`=%s""", - (new, doctype, old)) + Version = DocType("Version") + + frappe.qb.update(Version).set(Version.docname, new).where( + (Version.docname == old) & (Version.ref_doctype == doctype) + ).run() + def rename_eps_records(doctype: str, old: str, new: str) -> None: - epl = frappe.qb.DocType("Energy Point Log") - (frappe.qb.update(epl) - .set(epl.reference_name, new) - .where( - (epl.reference_doctype == doctype) - & (epl.reference_name == old) - ) + EPL = DocType("Energy Point Log") + + frappe.qb.update(EPL).set(EPL.reference_name, new).where( + (EPL.reference_doctype == doctype) & (EPL.reference_name == old) ).run() def rename_parent_and_child(doctype: str, old: str, new: str, meta: "Meta") -> None: - # rename the doc - frappe.db.sql("UPDATE `tab{0}` SET `name`={1} WHERE `name`={1}".format(doctype, '%s'), (new, old)) + frappe.qb.update(doctype).set("name", new).where(Field("name") == old).run() + update_autoname_field(doctype, new, meta) update_child_docs(old, new, meta) @@ -303,7 +309,9 @@ def update_autoname_field(doctype: str, new: str, meta: "Meta") -> None: if meta.get('autoname'): field = meta.get('autoname').split(':') if field and field[0] == "field": - frappe.db.sql("UPDATE `tab{0}` SET `{1}`={2} WHERE `name`={2}".format(doctype, field[1], '%s'), (new, new)) + frappe.qb.update(doctype).set(field[1], new).where( + Field("name") == new + ).run() def validate_rename(doctype: str, old: str, new: str, meta: "Meta", merge: bool, force: bool = False, ignore_permissions: bool = False, ignore_if_exists: bool = False) -> str: # using for update so that it gets locked and someone else cannot edit it while this rename is going on! @@ -361,8 +369,7 @@ def rename_doctype(doctype: str, old: str, new: str) -> None: def update_child_docs(old: str, new: str, meta: "Meta") -> None: # update "parent" for df in meta.get_table_fields(): - frappe.db.sql("update `tab%s` set parent=%s where parent=%s" \ - % (df.options, '%s', '%s'), (new, old)) + frappe.qb.update(df.options).set("parent", new).where(Field("parent") == old).run() def update_link_field_values(link_fields: List[Dict], old: str, new: str, doctype: str) -> None: for field in link_fields: @@ -405,44 +412,35 @@ def get_link_fields(doctype: str) -> List[Dict]: frappe.flags.link_fields = {} if doctype not in frappe.flags.link_fields: - link_fields = frappe.db.sql("""\ - select parent, fieldname, - (select issingle from tabDocType dt - where dt.name = df.parent) as issingle - from tabDocField df - where - df.options=%s and df.fieldtype='Link'""", (doctype,), as_dict=1) + dt = DocType("DocType") + df = DocType("DocField") + cf = DocType("Custom Field") + ps = DocType("Property Setter") - # get link fields from tabCustom Field - custom_link_fields = frappe.db.sql("""\ - select dt as parent, fieldname, - (select issingle from tabDocType dt - where dt.name = df.dt) as issingle - from `tabCustom Field` df - where - df.options=%s and df.fieldtype='Link'""", (doctype,), as_dict=1) + st_issingle = frappe.qb.from_(dt).select(dt.issingle).where(dt.name == df.parent).as_("issingle") + standard_fields = frappe.qb.from_(df).select(df.parent, df.fieldname, st_issingle).where( + (df.options == doctype) & (df.fieldtype == "Link") + ).run(as_dict=True) - # add custom link fields list to link fields list - link_fields += custom_link_fields - # remove fields whose options have been changed using property setter - property_setter_link_fields = frappe.db.sql("""\ - select ps.doc_type as parent, ps.field_name as fieldname, - (select issingle from tabDocType dt - where dt.name = ps.doc_type) as issingle - from `tabProperty Setter` ps - where - ps.property_type='options' and - ps.field_name is not null and - ps.value=%s""", (doctype,), as_dict=1) + cf_issingle = frappe.qb.from_(dt).select(dt.issingle).where(dt.name == cf.dt).as_("issingle") + custom_fields = frappe.qb.from_(cf).select(cf.dt.as_("parent"), cf.fieldname, cf_issingle).where( + (cf.options == doctype) & (cf.fieldtype == "Link") + ).run(as_dict=True) - link_fields += property_setter_link_fields + ps_issingle = frappe.qb.from_(dt).select(dt.issingle).where(dt.name == ps.doc_type).as_("issingle") + property_setter_fields = frappe.qb.from_(ps).select(ps.doc_type.as_("parent"), ps.field_name.as_("fieldname"), ps_issingle).where( + (ps.property == "options") & (ps.value == doctype) & (ps.field_name.notnull()) + ).run(as_dict=True) - frappe.flags.link_fields[doctype] = link_fields + frappe.flags.link_fields[doctype] = standard_fields + custom_fields + property_setter_fields return frappe.flags.link_fields[doctype] def update_options_for_fieldtype(fieldtype: str, old: str, new: str) -> None: + CustomField = DocType("Custom Field") + PropertySetter = DocType("Property Setter") + if frappe.conf.developer_mode: for name in frappe.get_all("DocField", filters={"options": old}, pluck="parent"): doctype = frappe.get_doc("DocType", name) @@ -454,133 +452,141 @@ def update_options_for_fieldtype(fieldtype: str, old: str, new: str) -> None: if save: doctype.save() else: - frappe.db.sql("""update `tabDocField` set options=%s - where fieldtype=%s and options=%s""", (new, fieldtype, old)) + DocField = DocType("DocField") + frappe.qb.update(DocField).set(DocField.options, new).where( + (DocField.fieldtype == fieldtype) & (DocField.options == old) + ).run() - frappe.db.sql("""update `tabCustom Field` set options=%s - where fieldtype=%s and options=%s""", (new, fieldtype, old)) + frappe.qb.update(CustomField).set(CustomField.options, new).where( + (CustomField.fieldtype == fieldtype) & (CustomField.options == old) + ).run() + + frappe.qb.update(PropertySetter).set(PropertySetter.value, new).where( + (PropertySetter.property == "options") & (PropertySetter.value == old) + ).run() - frappe.db.sql("""update `tabProperty Setter` set value=%s - where property='options' and value=%s""", (new, old)) def get_select_fields(old: str, new: str) -> List[Dict]: """ get select type fields where doctype's name is hardcoded as new line separated list """ + df = DocType("DocField") + dt = DocType("DocType") + cf = DocType("Custom Field") + ps = DocType("Property Setter") + # get link fields from tabDocField - select_fields = frappe.db.sql(""" - select parent, fieldname, - (select issingle from tabDocType dt - where dt.name = df.parent) as issingle - from tabDocField df - where - df.parent != %s and df.fieldtype = 'Select' and - df.options like {0} """.format(frappe.db.escape('%' + old + '%')), (new,), as_dict=1) + st_issingle = frappe.qb.from_(dt).select(dt.issingle).where(dt.name == df.parent).as_("issingle") + standard_fields = frappe.qb.from_(df).select( + df.parent, df.fieldname, st_issingle + ).where( + (df.parent != new) + & (df.fieldtype == "Select") + & (df.options.like(f"%{old}%")) + ).run(as_dict=True) # get link fields from tabCustom Field - custom_select_fields = frappe.db.sql(""" - select dt as parent, fieldname, - (select issingle from tabDocType dt - where dt.name = df.dt) as issingle - from `tabCustom Field` df - where - df.dt != %s and df.fieldtype = 'Select' and - df.options like {0} """ .format(frappe.db.escape('%' + old + '%')), (new,), as_dict=1) - - # add custom link fields list to link fields list - select_fields += custom_select_fields + cf_issingle = frappe.qb.from_(dt).select(dt.issingle).where(dt.name == cf.dt).as_("issingle") + custom_select_fields = frappe.qb.from_(cf).select( + cf.dt.as_("parent"), cf.fieldname, cf_issingle + ).where( + (cf.dt != new) + & (cf.fieldtype == "Select") + & (cf.options.like(f"%{old}%")) + ).run(as_dict=True) # remove fields whose options have been changed using property setter - property_setter_select_fields = frappe.db.sql(""" - select ps.doc_type as parent, ps.field_name as fieldname, - (select issingle from tabDocType dt - where dt.name = ps.doc_type) as issingle - from `tabProperty Setter` ps - where - ps.doc_type != %s and - ps.property_type='options' and - ps.field_name is not null and - ps.value like {0} """.format(frappe.db.escape('%' + old + '%')), (new,), as_dict=1) + ps_issingle = frappe.qb.from_(dt).select(dt.issingle).where(dt.name == ps.doc_type).as_("issingle") + property_setter_select_fields = frappe.qb.from_(ps).select( + ps.doc_type.as_("parent"), ps.field_name.as_("fieldname"), ps_issingle + ).where( + (ps.doc_type != new) + & (ps.property == "options") + & (ps.field_name.notnull()) + & (ps.value.like(f"%{old}%")) + ).run(as_dict=True) - select_fields += property_setter_select_fields + return standard_fields + custom_select_fields + property_setter_select_fields - return select_fields def update_select_field_values(old: str, new: str): - frappe.db.sql(""" - update `tabDocField` set options=replace(options, %s, %s) - where - parent != %s and fieldtype = 'Select' and - (options like {0} or options like {1})""" - .format(frappe.db.escape('%' + '\n' + old + '%'), frappe.db.escape('%' + old + '\n' + '%')), (old, new, new)) + from frappe.query_builder.functions import Replace - frappe.db.sql(""" - update `tabCustom Field` set options=replace(options, %s, %s) - where - dt != %s and fieldtype = 'Select' and - (options like {0} or options like {1})""" - .format(frappe.db.escape('%' + '\n' + old + '%'), frappe.db.escape('%' + old + '\n' + '%')), (old, new, new)) + DocField = DocType("DocField") + CustomField = DocType("Custom Field") + PropertySetter = DocType("Property Setter") + + frappe.qb.update(DocField).set(DocField.options, Replace(DocField.options, old, new)).where( + (DocField.fieldtype == "Select") + & (DocField.parent != new) + & (DocField.options.like(f"%\n{old}%") | DocField.options.like(f"%{old}\n%")) + ).run() + + frappe.qb.update(CustomField).set(CustomField.options, Replace(CustomField.options, old, new)).where( + (CustomField.fieldtype == "Select") + & (CustomField.dt != new) + & (CustomField.options.like(f"%\n{old}%") | CustomField.options.like(f"%{old}\n%")) + ).run() + + frappe.qb.update(PropertySetter).set(PropertySetter.value, Replace(PropertySetter.value, old, new)).where( + (PropertySetter.property == "options") + & (PropertySetter.field_name.notnull()) + & (PropertySetter.doc_type != new) + & (PropertySetter.value.like(f"%\n{old}%") | PropertySetter.value.like(f"%{old}\n%")) + ).run() - frappe.db.sql(""" - update `tabProperty Setter` set value=replace(value, %s, %s) - where - doc_type != %s and field_name is not null and - property='options' and - (value like {0} or value like {1})""" - .format(frappe.db.escape('%' + '\n' + old + '%'), frappe.db.escape('%' + old + '\n' + '%')), (old, new, new)) def update_parenttype_values(old: str, new: str): - child_doctypes = frappe.db.get_all('DocField', - fields=['options', 'fieldname'], - filters={ - 'parent': new, - 'fieldtype': ['in', frappe.model.table_fields] - } + child_doctypes = frappe.get_all( + "DocField", + fields=["options", "fieldname"], + filters={"parent": new, "fieldtype": ["in", frappe.model.table_fields]}, ) - custom_child_doctypes = frappe.db.get_all('Custom Field', - fields=['options', 'fieldname'], - filters={ - 'dt': new, - 'fieldtype': ['in', frappe.model.table_fields] - } + custom_child_doctypes = frappe.get_all( + "Custom Field", + fields=["options", "fieldname"], + filters={"dt": new, "fieldtype": ["in", frappe.model.table_fields]}, ) child_doctypes += custom_child_doctypes - fields = [d['fieldname'] for d in child_doctypes] + fields = [d["fieldname"] for d in child_doctypes] property_setter_child_doctypes = frappe.get_all( "Property Setter", - filters={ - "doc_type": new, - "property": "options", - "field_name": ("in", fields) - }, - pluck="value" + 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 = set(list(d["options"] for d in child_doctypes) + property_setter_child_doctypes) for doctype in child_doctypes: - frappe.db.sql(f"update `tab{doctype}` set parenttype=%s where parenttype=%s", (new, old)) + Table = DocType(doctype) + frappe.qb.update(Table).set(Table.parenttype, new).where(Table.parenttype == old).run() + def rename_dynamic_links(doctype: str, old: str, new: str): + Singles = DocType("Singles") for df in get_dynamic_link_map().get(doctype, []): # dynamic link in single, just one value to check if frappe.get_meta(df.parent).issingle: refdoc = frappe.db.get_singles_dict(df.parent) - if refdoc.get(df.options)==doctype and refdoc.get(df.fieldname)==old: - - frappe.db.sql("""update tabSingles set value=%s where - field=%s and value=%s and doctype=%s""", (new, df.fieldname, old, df.parent)) + if refdoc.get(df.options) == doctype and refdoc.get(df.fieldname) == old: + frappe.qb.update(Singles).set(Singles.value, new).where( + (Singles.field == df.fieldname) + & (Singles.doctype == df.parent) + & (Singles.value == old) + ).run() else: # because the table hasn't been renamed yet! parent = df.parent if df.parent != new else old - frappe.db.sql("""update `tab{parent}` set {fieldname}=%s - where {options}=%s and {fieldname}=%s""".format(parent = parent, - fieldname=df.fieldname, options=df.options), (new, doctype, old)) + + frappe.qb.update(parent).set(df.fieldname, new).where( + (Field(df.options) == doctype) + & (Field(df.fieldname) == old) + ).run() + def bulk_rename(doctype: str, rows: Optional[List[List]] = None, via_console: bool = False) -> Optional[List[str]]: """Bulk rename documents From f92f77dab798c5ac0ba02bacd5f46b576c7f6ce1 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 22 Mar 2022 19:38:53 +0530 Subject: [PATCH 038/139] fix(qb): Make Table importable --- frappe/model/rename_doc.py | 1 + frappe/query_builder/utils.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index b68af0f1ce..f25613f71e 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -9,6 +9,7 @@ from frappe.model.dynamic_links import get_dynamic_link_map from frappe.model.naming import validate_name from frappe.model.utils.user_settings import sync_user_settings, update_user_settings_data from frappe.query_builder import Field +from frappe.query_builder.utils import DocType, Table from frappe.utils.data import sbool from frappe.utils.password import rename_password from frappe.utils.scheduler import is_scheduler_inactive diff --git a/frappe/query_builder/utils.py b/frappe/query_builder/utils.py index 1ddf4fc034..8c95eb8a70 100644 --- a/frappe/query_builder/utils.py +++ b/frappe/query_builder/utils.py @@ -49,6 +49,9 @@ def get_attr(method_string): def DocType(*args, **kwargs): return frappe.qb.DocType(*args, **kwargs) +def Table(*args, **kwargs): + return frappe.qb.Table(*args, **kwargs) + def patch_query_execute(): """Patch the Query Builder with helper execute method This excludes the use of `frappe.db.sql` method while From 0cf945f5f1edeaea221ada08c0b07d86932c7471 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Mon, 28 Mar 2022 10:52:20 +0530 Subject: [PATCH 039/139] fix: alignment for flag in dialogs, padding for text area at initial state --- .../public/js/frappe/form/controls/phone.js | 22 ++++++++------ .../js/frappe/phone_picker/phone_picker.js | 4 +-- frappe/public/scss/common/phone_picker.scss | 29 +++++++++++++++---- 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/phone.js b/frappe/public/js/frappe/form/controls/phone.js index 060d41f328..e4252b762f 100644 --- a/frappe/public/js/frappe/form/controls/phone.js +++ b/frappe/public/js/frappe/form/controls/phone.js @@ -1,5 +1,5 @@ -import Picker from '../../phone_picker/phone_picker'; +import PhonePicker from '../../phone_picker/phone_picker'; frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlData { @@ -32,6 +32,7 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD if (this.$input.val()) { this.set_formatted_input(this.get_country(country) +'-'+ this.$input.val()); } + this.change_padding(); }; this.$wrapper.find('.selected-phone').on('click', (e) => { @@ -51,7 +52,7 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD make_icon_input() { let picker_wrapper = $('
'); - this.picker = new Picker({ + this.picker = new PhonePicker({ parent: picker_wrapper, countries: frappe.boot.country_codes }); @@ -103,6 +104,7 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD this.selected_icon.find('svg').toggleClass('hide'); this.selected_icon.find('img').addClass('hide'); } + this.$input.css("padding-left", 30); } if (this.value && this.value.split("-").length == 2) { let isd = this.value.split("-")[0]; @@ -112,13 +114,6 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD if (this.picker.country && this.picker.country !== this.$isd.text()) { this.$isd.length && this.$isd.text(isd); } - let len = this.$isd.text().length; - let diff = len - 3; - if (len > 3) { - this.$input.css("padding-left", 67 + (diff * 9)); - } else { - this.$input.css("padding-left", 67); - } } } @@ -165,4 +160,13 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD let code = country_codes[country].code; return frappe.utils.flag(code); } + change_padding() { + let len = this.$isd.text().length; + let diff = len - 3; + if (len > 3) { + this.$input.css("padding-left", 67 + (diff * 7)); + } else { + this.$input.css("padding-left", 67); + } + } }; diff --git a/frappe/public/js/frappe/phone_picker/phone_picker.js b/frappe/public/js/frappe/phone_picker/phone_picker.js index cbc5df4b12..c5c1437c12 100644 --- a/frappe/public/js/frappe/phone_picker/phone_picker.js +++ b/frappe/public/js/frappe/phone_picker/phone_picker.js @@ -1,4 +1,4 @@ -class Picker { +class PhonePicker { constructor(opts) { this.parent = opts.parent; this.width = opts.width; @@ -91,4 +91,4 @@ class Picker { } } -export default Picker; +export default PhonePicker; diff --git a/frappe/public/scss/common/phone_picker.scss b/frappe/public/scss/common/phone_picker.scss index bbac895018..7c9bbcf7e5 100644 --- a/frappe/public/scss/common/phone_picker.scss +++ b/frappe/public/scss/common/phone_picker.scss @@ -56,15 +56,22 @@ } } } + .phone-picker-popover { max-width: 325px; - left: 11px !important; + left: 29px !important; .picker-arrow { left: 15px !important; } + @media (max-width: 992px) { + max-width: 325px; + left: 48px !important; + } } + + .frappe-control[data-fieldtype='Phone'] - { +{ input { padding-left: 67px; } @@ -103,6 +110,18 @@ } } +.modal-body { + .frappe-control[data-fieldtype='Phone'] + { + input { + padding-left: 67px; + } + .selected-phone { + top: calc(50%); + } + } +} + .data-row.row { .selected-phone { top: calc(50% - 11px); @@ -111,8 +130,8 @@ } .bg-gray-100 { - --tw-bg-opacity: 1; - background-color: rgba(244,245,246,var(--tw-bg-opacity)); + --tw-bg-opacity: 1; + background-color: rgba(244,245,246,var(--tw-bg-opacity)); } .dt-cell__content { @@ -126,5 +145,3 @@ top: 5px !important; } } - - From 9dc123182737fa69a5982b47abc3222bae4549de Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Mon, 28 Mar 2022 12:27:49 +0530 Subject: [PATCH 040/139] refactor: code cleanup --- cypress/integration/control_phone.js | 11 ++--- .../public/js/frappe/form/controls/phone.js | 41 ++++++++++--------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/cypress/integration/control_phone.js b/cypress/integration/control_phone.js index 9bfce3059c..8d1e7c1d05 100644 --- a/cypress/integration/control_phone.js +++ b/cypress/integration/control_phone.js @@ -24,18 +24,19 @@ context('Control Phone', () => { let phone_number = '9312672712'; cy.get('.selected-phone > img').click().first(); - cy.get('.frappe-control[data-fieldname=phone] input') + cy.get_field("phone") .first() - .click(); + .click({multiple: true}); cy.get('.frappe-control[data-fieldname=phone]') .findByRole('textbox') .first() .type(phone_number); - cy.get('.frappe-control[data-fieldname=phone] input').first().should('have.value', phone_number); - cy.get('.frappe-control[data-fieldname=phone] input').first().blur(); + cy.get_field("phone").first().should('have.value', phone_number); + cy.get_field("phone").first().blur({force: true}); + cy.get('@dialog').then(dialog => { - let value = dialog.fields_dict.phone.value; + let value = dialog.get_value("phone"); expect(value).to.equal('+91-' + phone_number); }); }); diff --git a/frappe/public/js/frappe/form/controls/phone.js b/frappe/public/js/frappe/form/controls/phone.js index e4252b762f..98de47ff41 100644 --- a/frappe/public/js/frappe/form/controls/phone.js +++ b/frappe/public/js/frappe/form/controls/phone.js @@ -30,7 +30,7 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD this.$isd.text(country_isd); } if (this.$input.val()) { - this.set_formatted_input(this.get_country(country) +'-'+ this.$input.val()); + this.set_value(this.get_country(country) +'-'+ this.$input.val()); } this.change_padding(); }; @@ -95,9 +95,8 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD refresh() { super.refresh(); - - // Previously opened doc values get fetched. - if (!this.value) { + // Previously opened doc values showing up on a new doc + if (this.frm.doc.__islocal && !this.get_value()) { this.$input.val(""); this.$wrapper.find('.country').text(""); if (this.selected_icon.find('svg').hasClass('hide')) { @@ -106,7 +105,10 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD } this.$input.css("padding-left", 30); } - if (this.value && this.value.split("-").length == 2) { + } + + set_formatted_input(value) { + if (value && value.includes('-') && value.split('-').length == 2) { let isd = this.value.split("-")[0]; this.get_country_code_and_change_flag(isd); this.picker.set_country(isd); @@ -114,18 +116,17 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD if (this.picker.country && this.picker.country !== this.$isd.text()) { this.$isd.length && this.$isd.text(isd); } + this.change_padding(); + this.$input.val(value.split('-').pop()); + + } else if (this.$isd.text().trim() && this.value) { + let code_number = this.$isd.text() + '-' + value; + this.set_value(code_number); } } - - set_formatted_input(value) { - if (value && value.includes('-')) { - this.set_model_value(value); - this.$input.val(value.split("-").pop()); - } else if (this.$isd.text().trim() && this.value) { - let code_number = this.$isd.text() + '-' + value; - this.set_model_value(code_number); - } + get_value() { + return this.value; } change_flag(country_code) { @@ -162,11 +163,11 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD } change_padding() { let len = this.$isd.text().length; - let diff = len - 3; - if (len > 3) { - this.$input.css("padding-left", 67 + (diff * 7)); - } else { - this.$input.css("padding-left", 67); - } + let diff = len - 2; + if (len > 2) { + this.$input.css("padding-left", 60 + (diff * 7)); + } else { + this.$input.css("padding-left", 60); } + } }; From 2864c4322e348c68d264a4389862b2324144b455 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Mon, 28 Mar 2022 13:13:04 +0530 Subject: [PATCH 041/139] style: alignment of isd code for grid and dialog --- frappe/public/js/frappe/form/controls/phone.js | 3 ++- frappe/public/scss/common/phone_picker.scss | 13 +++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/phone.js b/frappe/public/js/frappe/form/controls/phone.js index 98de47ff41..92abd6e20c 100644 --- a/frappe/public/js/frappe/form/controls/phone.js +++ b/frappe/public/js/frappe/form/controls/phone.js @@ -96,7 +96,8 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD refresh() { super.refresh(); // Previously opened doc values showing up on a new doc - if (this.frm.doc.__islocal && !this.get_value()) { + + if (this.frm && this.frm.doc.__islocal && !this.get_value()) { this.$input.val(""); this.$wrapper.find('.country').text(""); if (this.selected_icon.find('svg').hasClass('hide')) { diff --git a/frappe/public/scss/common/phone_picker.scss b/frappe/public/scss/common/phone_picker.scss index 7c9bbcf7e5..f2131e8b84 100644 --- a/frappe/public/scss/common/phone_picker.scss +++ b/frappe/public/scss/common/phone_picker.scss @@ -73,12 +73,11 @@ .frappe-control[data-fieldtype='Phone'] { input { - padding-left: 67px; + padding-left: 30px; } .selected-phone { display: flex; cursor: pointer; - width: 66px; height: 20px; border-radius: 5px; position: absolute; @@ -86,6 +85,7 @@ left: 8px; content: ' '; align-items: center; + z-index: 1; .country { display: flex; @@ -113,18 +113,15 @@ .modal-body { .frappe-control[data-fieldtype='Phone'] { - input { - padding-left: 67px; - } .selected-phone { - top: calc(50%); + top: calc(50% - 0.5px); } } } .data-row.row { .selected-phone { - top: calc(50% - 11px); + top: calc(50% - 10.1px); z-index: 2; } } @@ -142,6 +139,6 @@ .dt-cell__edit, .filter-field { .selected-phone { - top: 5px !important; + top: 5.5px !important; } } From 28f9802fd9c4ad4f2025b2e75e9e95fa158dd4e2 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 28 Mar 2022 18:46:41 +0530 Subject: [PATCH 042/139] fix: rollback to savepoint to avoid partial commits It's better to keep the validations in and out AND separate...this is a humble attempt for the same :) --- frappe/model/document.py | 2 +- frappe/model/rename_doc.py | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/frappe/model/document.py b/frappe/model/document.py index 52bd1f3196..ab16b68e6a 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -940,7 +940,7 @@ class Document(BaseDocument): @whitelist.__func__ def _rename(self, name: str, merge: bool = False, force: bool = False, validate_rename: bool = True): - """Cancel the document. Sets `docstatus` = 2, then saves. + """Rename the document. Triggers frappe.rename_doc, then reloads. """ from frappe.model.rename_doc import rename_doc self.name = rename_doc(doc=self, new=name, merge=merge, force=force, validate=validate_rename) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index f25613f71e..f27484697f 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -73,16 +73,17 @@ def update_document_title( transformed_name = transformed_name or updated_name # run rename validations before queueing - new_name = validate_rename( + # use savepoints to avoid partial renames / commits + validate_rename( doctype=doctype, old=current_name, new=transformed_name, meta=doc.meta, merge=merge, + save_point=True, ) - # we don't want to re-validate since the before_rename hooks would be run again ;) - doc.queue_action("rename", name=new_name, merge=merge, validate_rename=False) + doc.queue_action("rename", name=transformed_name, merge=merge) else: doc.rename(updated_name, merge=merge) @@ -314,8 +315,12 @@ def update_autoname_field(doctype: str, new: str, meta: "Meta") -> None: Field("name") == new ).run() -def validate_rename(doctype: str, old: str, new: str, meta: "Meta", merge: bool, force: bool = False, ignore_permissions: bool = False, ignore_if_exists: bool = False) -> str: +def validate_rename(doctype: str, old: str, new: str, meta: "Meta", merge: bool, force: bool = False, ignore_permissions: bool = False, ignore_if_exists: bool = False, save_point=False) -> str: # using for update so that it gets locked and someone else cannot edit it while this rename is going on! + if save_point: + _SAVE_POINT = f"validate_rename_{frappe.generate_hash(8)}" + frappe.db.savepoint(_SAVE_POINT) + exists = ( frappe.qb.from_(doctype) .where(Field("name") == new) @@ -350,6 +355,9 @@ def validate_rename(doctype: str, old: str, new: str, meta: "Meta", merge: bool, # validate naming like it's done in doc.py new = validate_name(doctype, new) + if save_point: + frappe.db.rollback(save_point=_SAVE_POINT) + return new def rename_doctype(doctype: str, old: str, new: str) -> None: From 949646222f8a1314ff10b2180353646620eea3bf Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Wed, 30 Mar 2022 14:19:39 +0530 Subject: [PATCH 043/139] test(ui): potential fix for failing test --- cypress/integration/control_phone.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cypress/integration/control_phone.js b/cypress/integration/control_phone.js index 8d1e7c1d05..0d0812b983 100644 --- a/cypress/integration/control_phone.js +++ b/cypress/integration/control_phone.js @@ -30,9 +30,10 @@ context('Control Phone', () => { cy.get('.frappe-control[data-fieldname=phone]') .findByRole('textbox') .first() - .type(phone_number); + .type(phone_number, {force: true}); cy.get_field("phone").first().should('have.value', phone_number); + cy.wait(1000) cy.get_field("phone").first().blur({force: true}); cy.get('@dialog').then(dialog => { From 544d248323576f56b8d70073e132f9831af6e835 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Wed, 30 Mar 2022 18:34:25 +0530 Subject: [PATCH 044/139] test(ui): increasing coverage in tests --- cypress/fixtures/doctype_with_phone.js | 47 +++++++++++ cypress/integration/control_phone.js | 110 ++++++++++++++++++------- cypress/support/commands.js | 4 + 3 files changed, 133 insertions(+), 28 deletions(-) create mode 100644 cypress/fixtures/doctype_with_phone.js diff --git a/cypress/fixtures/doctype_with_phone.js b/cypress/fixtures/doctype_with_phone.js new file mode 100644 index 0000000000..c62922ade2 --- /dev/null +++ b/cypress/fixtures/doctype_with_phone.js @@ -0,0 +1,47 @@ +export default { + name: "Doctype With Phone", + actions: [], + custom: 1, + is_submittable: 1, + autoname: "field:title", + creation: '2022-03-30 06:29:07.215072', + doctype: 'DocType', + engine: 'InnoDB', + fields: [ + + { + fieldname: 'title', + fieldtype: 'Data', + label: 'title', + unique: 1, + }, + { + fieldname: 'phone', + fieldtype: 'Phone', + label: 'Phone' + } + ], + links: [], + modified: '2019-03-30 14:40:53.127615', + modified_by: 'Administrator', + naming_rule: "By fieldname", + module: 'Custom', + owner: 'Administrator', + permissions: [ + { + create: 1, + delete: 1, + email: 1, + print: 1, + read: 1, + role: 'System Manager', + share: 1, + write: 1, + submit: 1, + cancel: 1 + } + ], + sort_field: 'modified', + sort_order: 'ASC', + track_changes: 1 +}; diff --git a/cypress/integration/control_phone.js b/cypress/integration/control_phone.js index 0d0812b983..2e6d30daf8 100644 --- a/cypress/integration/control_phone.js +++ b/cypress/integration/control_phone.js @@ -1,59 +1,113 @@ -context('Control Phone', () => { +import doctype_with_phone from '../fixtures/doctype_with_phone'; + +context("Control Phone", () => { before(() => { cy.login(); - cy.visit('/app/website'); + cy.visit("/app/website"); }); function get_dialog_with_phone() { return cy.dialog({ - title: 'Phone', + title: "Phone", fields: [{ - 'fieldname': 'phone', - 'fieldtype': 'Phone', + "fieldname": "phone", + "fieldtype": "Phone", }] }); } - it('should set flag and data', () => { - get_dialog_with_phone().as('dialog'); - cy.get('.selected-phone > svg').click(); - cy.get('.phone-picker .phone-wrapper[id="afghanistan"]').click(); - cy.get('.phone-picker .phone-wrapper[id="india"]').click(); - cy.get('.selected-phone .country').should('have.text', '+91'); - cy.get('.selected-phone > img').should('have.attr', 'src').and('include', '/in.svg'); + it("should set flag and data", () => { + get_dialog_with_phone().as("dialog"); + cy.get(".selected-phone > svg").click(); + cy.get(".phone-picker .phone-wrapper[id='afghanistan']").click(); + cy.get(".phone-picker .phone-wrapper[id='india']").click(); + cy.get(".selected-phone .country").should("have.text", "+91"); + cy.get(".selected-phone > img").should("have.attr", "src").and("include", "/in.svg"); - let phone_number = '9312672712'; - cy.get('.selected-phone > img').click().first(); + let phone_number = "9312672712"; + cy.get(".selected-phone > img").click().first(); cy.get_field("phone") .first() .click({multiple: true}); - cy.get('.frappe-control[data-fieldname=phone]') - .findByRole('textbox') + cy.get(".frappe-control[data-fieldname=phone]") + .findByRole("textbox") .first() .type(phone_number, {force: true}); - cy.get_field("phone").first().should('have.value', phone_number); - cy.wait(1000) + cy.get_field("phone").first().should("have.value", phone_number); + cy.wait(1000); cy.get_field("phone").first().blur({force: true}); - cy.get('@dialog').then(dialog => { + cy.get("@dialog").then(dialog => { let value = dialog.get_value("phone"); - expect(value).to.equal('+91-' + phone_number); + expect(value).to.equal("+91-" + phone_number); }); }); - it('case insensitive search for country and clear search', () => { - let search_text = 'india'; - cy.get('.selected-phone > img').click().first(); - cy.get('.phone-picker').findByRole('searchbox').click().type(search_text); - cy.get('.phone-section .phone-wrapper:not(.hidden)').then(i => { - cy.get(`.phone-section .phone-wrapper[id*='${search_text.toLowerCase()}']`).then(countries => { + it("case insensitive search for country and clear search", () => { + let search_text = "india"; + cy.get(".selected-phone > img").click().first(); + cy.get(".phone-picker").findByRole("searchbox").click().type(search_text); + cy.get(".phone-section .phone-wrapper:not(.hidden)").then(i => { + cy.get(`.phone-section .phone-wrapper[id*="${search_text.toLowerCase()}"]`).then(countries => { expect(i.length).to.equal(countries.length); }); }); - cy.get('.phone-picker').findByRole('searchbox').clear().blur(); - cy.get('.phone-section .phone-wrapper').should('not.have.class', 'hidden'); + cy.get(".phone-picker").findByRole("searchbox").clear().blur(); + cy.get(".phone-section .phone-wrapper").should("not.have.class", "hidden"); }); + it("Already existing docs with phone field", () => { + cy.visit("/app/doctype"); + cy.insert_doc("DocType", doctype_with_phone, true); + cy.clear_cache(); + + // Creating custom doctype + cy.insert_doc("DocType", doctype_with_phone, true); + cy.visit("/app/doctype-with-phone"); + cy.click_listview_primary_button("Add Doctype With Phone"); + + //Adding a new entry for the created custom doctype + cy.fill_field("title", "Test Phone 1"); + cy.fill_field("phone", "+91-9823341234"); + cy.wait(500); + cy.get_field("phone").should("have.value", "9823341234"); + cy.click_doc_primary_button("Save"); + cy.wait(500); + cy.get_doc("Doctype With Phone", "Test Phone 1").then((doc) => { + let value = doc.data.phone; + expect(value).to.equal("+91-9823341234"); + }); + cy.go_to_list("Doctype With Phone"); + cy.click_listview_primary_button("Add Doctype With Phone"); + // Field should be empty on new doc + cy.get_field("phone").should("have.value", ""); + cy.get(".selected-phone .country").should("have.text", ""); + cy.fill_field("title", "Test Phone 2"); + cy.fill_field("phone", "+91-9823341291"); + cy.wait(500); + cy.get_field("phone").should("have.value", "9823341291"); + cy.click_doc_primary_button("Save"); + cy.wait(500); + cy.go_to_list("Doctype With Phone"); + cy.clear_cache(); + cy.click_listview_row_item(0); + cy.title().should("eq", "Test Phone 2"); + cy.get(".selected-phone .country").should("have.text", "+91"); + cy.get(".selected-phone > img").should("have.attr", "src").and("include", "/in.svg"); + cy.get_field("phone").should("have.value", "9823341291"); + cy.go_to_list("Doctype With Phone"); + cy.click_listview_row_item(1); + cy.title().should("eq", "Test Phone 1"); + cy.get(".selected-phone .country").should("have.text", "+91"); + cy.get(".selected-phone > img").should("have.attr", "src").and("include", "/in.svg"); + cy.get_field("phone").should("have.value", "9823341234"); + cy.get_doc("Doctype With Phone", "Test Phone 2").then((doc) => { + let value = doc.data.phone; + expect(value).to.equal("+91-9823341291"); + cy.remove_doc("Doctype With Phone", "Test Phone 1", true); + cy.remove_doc("Doctype With Phone", "Test Phone 2", true); + }); + }); }); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 4f273af21f..7d9932198d 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -325,6 +325,10 @@ Cypress.Commands.add('click_listview_primary_button', (btn_name) => { cy.get('.primary-action').contains(btn_name).click({force: true}); }); +Cypress.Commands.add('click_doc_primary_button', (btn_name) => { + cy.get('.primary-action').contains(btn_name).click({force: true}); +}); + Cypress.Commands.add('click_timeline_action_btn', (btn_name) => { cy.get('.timeline-message-box .actions .action-btn').contains(btn_name).click(); }); From 7d49693a339efb4c03b6e35199efcab5e8d619d4 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Thu, 7 Apr 2022 15:04:26 +0530 Subject: [PATCH 045/139] refactor: updated error messages for invalid phone numbers --- frappe/utils/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 03877518d3..44debde273 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -80,15 +80,15 @@ def validate_phone_number_with_isd(phone, throw=False): if not phone: return try: - phone = ph.parse(phone) + phone_number = ph.parse(phone) except Exception as e: if e.error_type == 1: - frappe.throw(frappe._("The entered value is not a phone number."), frappe.InvalidPhoneNumberError, + frappe.throw(frappe._("{0} is not a valid Phone Number.").format(frappe.bold(phone)), frappe.InvalidPhoneNumberError, title=frappe._("Invalid Number")) - frappe.throw(frappe._("Please select a country code."), frappe.InvalidPhoneNumberError, + frappe.throw(frappe._("Please select a country code for the Phone Number {0}.").format(frappe.bold(phone)), frappe.InvalidPhoneNumberError, title = frappe._("Country Code Required")) - if not ph.is_valid_number(phone): - frappe.throw(frappe._("This is not a valid phone number"), frappe.InvalidPhoneNumberError, + if not ph.is_valid_number(phone_number): + frappe.throw(frappe._("{0} is not a valid Phone Number").format(frappe.bold(phone)), frappe.InvalidPhoneNumberError, title = frappe._("Invalid Number")) def validate_phone_number(phone_number, throw=False): From 2d3c1053a0fce13d05aace48a596fa9124478837 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Mon, 11 Apr 2022 15:25:27 +0530 Subject: [PATCH 046/139] refactor(ui): updated validation messages with fieldname --- frappe/model/base_document.py | 2 +- frappe/utils/__init__.py | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index ada5119995..b90dbed9ac 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -726,7 +726,7 @@ class BaseDocument(object): # data_field options defined in frappe.model.data_field_options for phone_field in self.meta.get_phone_fields(): phone = self.get(phone_field.fieldname) - frappe.utils.validate_phone_number_with_isd(phone, throw=True) + frappe.utils.validate_phone_number_with_isd(phone, phone_field.fieldname, throw=True) for data_field in self.meta.get_data_fields(): data = self.get(data_field.fieldname) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index e5a8ac45fb..c1c19a0bd4 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -76,20 +76,22 @@ def extract_email_id(email): email_id = email_id.decode("utf-8", "ignore") return email_id -def validate_phone_number_with_isd(phone, throw=False): +def validate_phone_number_with_isd(phone, fieldname, throw=False): + from frappe import _ if not phone: return try: phone_number = ph.parse(phone) except Exception as e: if e.error_type == 1: - frappe.throw(frappe._("{0} is not a valid Phone Number.").format(frappe.bold(phone)), frappe.InvalidPhoneNumberError, - title=frappe._("Invalid Number")) - frappe.throw(frappe._("Please select a country code for the Phone Number {0}.").format(frappe.bold(phone)), frappe.InvalidPhoneNumberError, - title = frappe._("Country Code Required")) + + frappe.throw(_("Phone Number {0} set in field {1} is not valid.").format(frappe.bold(phone), frappe.bold(fieldname)), frappe.InvalidPhoneNumberError, + title=_("Invalid Phone Number")) + frappe.throw(_("Please select a country code for field {1}.").format(frappe.bold(phone), frappe.bold(fieldname)), frappe.InvalidPhoneNumberError, + title = _("Country Code Required")) if not ph.is_valid_number(phone_number): - frappe.throw(frappe._("{0} is not a valid Phone Number").format(frappe.bold(phone)), frappe.InvalidPhoneNumberError, - title = frappe._("Invalid Number")) + frappe.throw(_("Phone Number {0} set in field {1} is not valid.").format(frappe.bold(phone), frappe.bold(fieldname)), frappe.InvalidPhoneNumberError, + title = _("Invalid Phone Number")) def validate_phone_number(phone_number, throw=False): """Returns True if valid phone number""" From 0d7024fa3cef712ff35304ba60ead2e6fbeffd20 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 11 Apr 2022 23:32:16 +0200 Subject: [PATCH 047/139] fix: handle no data in set_open_count --- frappe/public/js/frappe/form/dashboard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index 6e3dd3eb0b..0731bdf8fb 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -375,7 +375,7 @@ frappe.ui.form.Dashboard = class FormDashboard { } set_open_count() { - if (!this.data.transactions || !this.data.fieldname) { + if (!this.data || (!this.data.transactions || !this.data.fieldname)) { return; } From 36ca84c9ff1a010abc6816688826e322c2b372f2 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Tue, 12 Apr 2022 21:06:19 +0530 Subject: [PATCH 048/139] test: suggested changes --- cypress/integration/control_phone.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cypress/integration/control_phone.js b/cypress/integration/control_phone.js index 2e6d30daf8..1a12ddf2b4 100644 --- a/cypress/integration/control_phone.js +++ b/cypress/integration/control_phone.js @@ -88,8 +88,9 @@ context("Control Phone", () => { cy.fill_field("phone", "+91-9823341291"); cy.wait(500); cy.get_field("phone").should("have.value", "9823341291"); + cy.intercept("POST", "/api/method/frappe.desk.form.save.savedocs").as("save_form"); cy.click_doc_primary_button("Save"); - cy.wait(500); + cy.wait("@save_form"); cy.go_to_list("Doctype With Phone"); cy.clear_cache(); cy.click_listview_row_item(0); From 1637e3af087acf7292e7058952c735313194aebd Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Tue, 12 Apr 2022 21:06:59 +0530 Subject: [PATCH 049/139] fix: linting --- frappe/boot.py | 3 ++- frappe/utils/__init__.py | 32 +++++++++++++++++++++++++------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/frappe/boot.py b/frappe/boot.py index 59981b19cf..62122ed4e5 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -11,8 +11,8 @@ from frappe.core.doctype.navbar_settings.navbar_settings import get_app_logo, ge from frappe.desk.doctype.route_history.route_history import frequently_visited_links from frappe.desk.form.load import get_meta_bundle from frappe.email.inbox import get_email_accounts -from frappe.model.base_document import get_controller from frappe.geo.country_info import get_all +from frappe.model.base_document import get_controller from frappe.query_builder import DocType from frappe.query_builder.functions import Count from frappe.query_builder.terms import subqry @@ -390,6 +390,7 @@ def get_country_codes(bootinfo): country_codes = get_all() bootinfo.country_codes = frappe._dict(country_codes) + @frappe.whitelist() def get_link_title_doctypes(): dts = frappe.get_all("DocType", {"show_title_field_in_link": 1}) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 010171f7a1..e651af3ff5 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -17,8 +17,8 @@ from gzip import GzipFile from typing import Generator, Iterable from urllib.parse import quote, urlparse -from redis.exceptions import ConnectionError import phonenumbers as ph +from redis.exceptions import ConnectionError from werkzeug.test import Client import frappe @@ -82,8 +82,10 @@ def extract_email_id(email): email_id = email_id.decode("utf-8", "ignore") return email_id + def validate_phone_number_with_isd(phone, fieldname, throw=False): from frappe import _ + if not phone: return try: @@ -91,13 +93,29 @@ def validate_phone_number_with_isd(phone, fieldname, throw=False): except Exception as e: if e.error_type == 1: - frappe.throw(_("Phone Number {0} set in field {1} is not valid.").format(frappe.bold(phone), frappe.bold(fieldname)), frappe.InvalidPhoneNumberError, - title=_("Invalid Phone Number")) - frappe.throw(_("Please select a country code for field {1}.").format(frappe.bold(phone), frappe.bold(fieldname)), frappe.InvalidPhoneNumberError, - title = _("Country Code Required")) + frappe.throw( + _("Phone Number {0} set in field {1} is not valid.").format( + frappe.bold(phone), frappe.bold(fieldname) + ), + frappe.InvalidPhoneNumberError, + title=_("Invalid Phone Number"), + ) + frappe.throw( + _("Please select a country code for field {1}.").format( + frappe.bold(phone), frappe.bold(fieldname) + ), + frappe.InvalidPhoneNumberError, + title=_("Country Code Required"), + ) if not ph.is_valid_number(phone_number): - frappe.throw(_("Phone Number {0} set in field {1} is not valid.").format(frappe.bold(phone), frappe.bold(fieldname)), frappe.InvalidPhoneNumberError, - title = _("Invalid Phone Number")) + frappe.throw( + _("Phone Number {0} set in field {1} is not valid.").format( + frappe.bold(phone), frappe.bold(fieldname) + ), + frappe.InvalidPhoneNumberError, + title=_("Invalid Phone Number"), + ) + def validate_phone_number(phone_number, throw=False): """Returns True if valid phone number""" From 651aa36cd2e08796b7810401ca1e9c6075cce05c Mon Sep 17 00:00:00 2001 From: Komal-Saraf0609 Date: Thu, 14 Apr 2022 13:20:54 +0530 Subject: [PATCH 050/139] test: Added script for control type "Data" --- cypress/integration/control_data.js | 110 ++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 cypress/integration/control_data.js diff --git a/cypress/integration/control_data.js b/cypress/integration/control_data.js new file mode 100644 index 0000000000..b448c8b998 --- /dev/null +++ b/cypress/integration/control_data.js @@ -0,0 +1,110 @@ +context('Data Control', () => { + before(() => { + cy.login(); + cy.visit('/app/doctype'); + return cy.window().its('frappe').then(frappe => { + return frappe.xcall('frappe.tests.ui_test_helpers.create_doctype', { + name: 'Test Data Control', + fields: [ + { + "label": "Name", + "fieldname": "name1", + "fieldtype": "Data", + "options":"Name", + "in_list_view": 1, + }, + { + "label": "Email-ID", + "fieldname": "email", + "fieldtype": "Data", + "options":"Email", + "in_list_view": 1, + }, + { + "label": "Phone No.", + "fieldname": "phone", + "fieldtype": "Data", + "options":"Phone", + "in_list_view": 1, + }, + ] + }); + }); + }); + it('Verifying data control by inputting different patterns', () => { + cy.new_form('Test Data Control'); + + //Checking the URL for the new form of the doctype + cy.location("pathname").should('eq', '/app/test-data-control/new-test-data-control-1'); + cy.get('.title-text').should('have.text', 'New Test Data Control'); + + //Checking if the status is "Not Saved" initially + cy.get('.indicator-pill').should('have.text', 'Not Saved'); + + //Inputting data in the field + cy.fill_field('name1', '@@###', 'Data'); + + //Checking if the border color of the field changes to red + cy.get_field('name1', 'Data').should('have.css', 'border', '1px solid rgb(236, 100, 94)'); + cy.findByRole('button', {name: 'Save'}).click(); + + //Checking for the error message + cy.get('.modal-title').should('have.text', 'Message'); + cy.get('.msgprint').should('have.text', '@@### is not a valid Name'); + cy.reload(); + + cy.fill_field('name1', 'Komal{}/!', 'Data'); + cy.get_field('name1', 'Data').should('have.css', 'border', '1px solid rgb(236, 100, 94)'); + cy.findByRole('button', {name: 'Save'}).click(); + cy.get('.modal-title').should('have.text', 'Message'); + cy.get('.msgprint').should('have.text', 'Komal{}/! is not a valid Name'); + cy.reload(); + + cy.fill_field('name1', 'Komal', 'Data'); + cy.get_field('name1', 'Data').should('have.css', 'border', '0px none rgb(152, 161, 169)'); + cy.fill_field('email', 'komal', 'Data'); + cy.get_field('email', 'Data').should('have.css', 'border', '1px solid rgb(236, 100, 94)'); + cy.findByRole('button', {name: 'Save'}).click(); + cy.get('.modal-title').should('have.text', 'Message'); + cy.get('.msgprint').should('have.text', 'komal is not a valid Email Address'); + cy.reload(); + + cy.fill_field('email', 'komal@test', 'Data'); + cy.get_field('email', 'Data').should('have.css', 'border', '1px solid rgb(236, 100, 94)'); + cy.findByRole('button', {name: 'Save'}).click(); + cy.get('.modal-title').should('have.text', 'Message'); + cy.get('.msgprint').should('have.text', 'komal@test is not a valid Email Address'); + cy.reload(); + + cy.fill_field('email', 'komal@test.com', 'Data'); + cy.get_field('email', 'Data').should('have.css', 'border', '0px none rgb(152, 161, 169)'); + + cy.fill_field('phone', 'komal', 'Data'); + cy.get_field('phone', 'Data').should('have.css', 'border', '1px solid rgb(236, 100, 94)'); + cy.findByRole('button', {name: 'Save'}).click(); + cy.get('.modal-title').should('have.text', 'Message'); + cy.get('.msgprint').should('have.text', 'komal is not a valid Phone Number'); + cy.reload(); + + //Inputting the data as expected and saving the document + cy.fill_field('name1', 'Komal', 'Data'); + cy.fill_field('email', 'komal@test.com', 'Data'); + cy.fill_field('phone', '9432380001', 'Data'); + cy.get_field('phone', 'Data').should('have.css', 'border', '0px none rgb(152, 161, 169)'); + cy.findByRole('button', {name: 'Save'}).click(); + + //Checking if the fields contains the data which has been filled in + cy.location("pathname").should('not.be', '/app/test-data-control/new-test-data-control-1'); + cy.get_field('name1').should('have.value', 'Komal'); + cy.get_field('email').should('have.value', 'komal@test.com'); + cy.get_field('phone').should('have.value', '9432380001'); + + //Deleting the inserted document + cy.visit('/app/test-data-control'); + cy.get('.list-row-checkbox').eq(0).click(); + cy.get('.actions-btn-group > .btn').contains('Actions').click(); + cy.get('.actions-btn-group > .dropdown-menu [data-label="Delete"]').click(); + cy.click_modal_primary_button('Yes'); + cy.get('.btn-modal-close').click(); + }); +}); \ No newline at end of file From dda56b375925ff90d1afc754f9cdec3138d7bb6c Mon Sep 17 00:00:00 2001 From: Komal-Saraf0609 Date: Thu, 14 Apr 2022 13:27:20 +0530 Subject: [PATCH 051/139] test: Fixed sider issues --- cypress/integration/control_data.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cypress/integration/control_data.js b/cypress/integration/control_data.js index b448c8b998..dad2e0ef70 100644 --- a/cypress/integration/control_data.js +++ b/cypress/integration/control_data.js @@ -10,21 +10,21 @@ context('Data Control', () => { "label": "Name", "fieldname": "name1", "fieldtype": "Data", - "options":"Name", + "options": "Name", "in_list_view": 1, }, { "label": "Email-ID", "fieldname": "email", "fieldtype": "Data", - "options":"Email", + "options": "Email", "in_list_view": 1, }, { "label": "Phone No.", "fieldname": "phone", "fieldtype": "Data", - "options":"Phone", + "options": "Phone", "in_list_view": 1, }, ] From 48cd1c2ad4e8aa1cbef320b0dca70f60673b0ed3 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 14 Apr 2022 14:14:00 +0530 Subject: [PATCH 052/139] refactor: Fix naming and simpify code - Ability to clear code, by hiting backspace --- frappe/model/base_document.py | 2 +- .../public/js/frappe/form/controls/phone.js | 67 +++++++++++++------ .../js/frappe/phone_picker/phone_picker.js | 15 ++++- frappe/utils/__init__.py | 46 ++++++------- 4 files changed, 77 insertions(+), 53 deletions(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 71e829f61a..784c398030 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -769,7 +769,7 @@ class BaseDocument(object): # data_field options defined in frappe.model.data_field_options for phone_field in self.meta.get_phone_fields(): phone = self.get(phone_field.fieldname) - frappe.utils.validate_phone_number_with_isd(phone, phone_field.fieldname, throw=True) + frappe.utils.validate_phone_number_with_country_code(phone, phone_field.fieldname) for data_field in self.meta.get_data_fields(): data = self.get(data_field.fieldname) diff --git a/frappe/public/js/frappe/form/controls/phone.js b/frappe/public/js/frappe/form/controls/phone.js index 92abd6e20c..d67b449ac8 100644 --- a/frappe/public/js/frappe/form/controls/phone.js +++ b/frappe/public/js/frappe/form/controls/phone.js @@ -5,16 +5,28 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD make_input() { super.make_input(); - this.make_icon_input(); + this.setup_country_code_picker(); this.input_events(); } input_events() { + this.$input.keydown((e) => { + const key_code = e.keyCode; + if ([frappe.ui.keyCode.BACKSPACE].includes(key_code)) { + if (this.$input.val().length == 0) { + this.country_code_picker.reset(); + } + } + }); + // Replaces code when selected and removes previously selected. - this.picker.on_change = (country) => { + this.country_code_picker.on_change = (country) => { + if (!country) { + return this.reset_inputx(); + } const country_code = frappe.boot.country_codes[country].code; const country_isd = frappe.boot.country_codes[country].isd; - this.change_flag(country_code); + this.set_flag(country_code); this.$icon = this.selected_icon.find('svg'); this.$flag = this.selected_icon.find('img'); @@ -32,7 +44,10 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD if (this.$input.val()) { this.set_value(this.get_country(country) +'-'+ this.$input.val()); } - this.change_padding(); + this.update_padding(); + // hide popover and focus input + this.$wrapper.popover('hide'); + this.$input.focus(); }; this.$wrapper.find('.selected-phone').on('click', (e) => { @@ -50,9 +65,9 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD }); } - make_icon_input() { + setup_country_code_picker() { let picker_wrapper = $('
'); - this.picker = new PhonePicker({ + this.country_code_picker = new PhonePicker({ parent: picker_wrapper, countries: frappe.boot.country_codes }); @@ -72,7 +87,8 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD html: true }).on('show.bs.popover', () => { setTimeout(() => { - this.picker.refresh(); + this.country_code_picker.refresh(); + this.country_code_picker.search_input.focus(); }, 10); }).on('hidden.bs.popover', () => { $('body').off('click.phone-popover'); @@ -98,26 +114,30 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD // Previously opened doc values showing up on a new doc if (this.frm && this.frm.doc.__islocal && !this.get_value()) { - this.$input.val(""); - this.$wrapper.find('.country').text(""); - if (this.selected_icon.find('svg').hasClass('hide')) { - this.selected_icon.find('svg').toggleClass('hide'); - this.selected_icon.find('img').addClass('hide'); - } - this.$input.css("padding-left", 30); + this.reset_input(); } } + reset_input() { + this.$input.val(""); + this.$wrapper.find('.country').text(""); + if (this.selected_icon.find('svg').hasClass('hide')) { + this.selected_icon.find('svg').toggleClass('hide'); + this.selected_icon.find('img').addClass('hide'); + } + this.$input.css("padding-left", 30); + } + set_formatted_input(value) { if (value && value.includes('-') && value.split('-').length == 2) { let isd = this.value.split("-")[0]; this.get_country_code_and_change_flag(isd); - this.picker.set_country(isd); - this.picker.refresh(); - if (this.picker.country && this.picker.country !== this.$isd.text()) { + this.country_code_picker.set_country(isd); + this.country_code_picker.refresh(); + if (this.country_code_picker.country && this.country_code_picker.country !== this.$isd.text()) { this.$isd.length && this.$isd.text(isd); } - this.change_padding(); + this.update_padding(); this.$input.val(value.split('-').pop()); } else if (this.$isd.text().trim() && this.value) { @@ -130,8 +150,8 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD return this.value; } - change_flag(country_code) { - this.selected_icon.find('img').attr('src', 'https://flagcdn.com/'+country_code+'.svg'); + set_flag(country_code) { + this.selected_icon.find('img').attr('src', `https://flagcdn.com/${country_code}.svg`); this.$icon = this.selected_icon.find('img'); this.$icon.hasClass('hide') && this.$icon.toggleClass('hide'); } @@ -148,21 +168,24 @@ frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlD this.selected_icon.prepend(this.get_country_flag(country)); this.selected_icon.find('svg').addClass('hide'); } else { - this.change_flag(code); + this.set_flag(code); } } } } + get_country(country) { const country_codes = frappe.boot.country_codes; return country_codes[country].isd; } + get_country_flag(country) { const country_codes = frappe.boot.country_codes; let code = country_codes[country].code; return frappe.utils.flag(code); } - change_padding() { + + update_padding() { let len = this.$isd.text().length; let diff = len - 2; if (len > 2) { diff --git a/frappe/public/js/frappe/phone_picker/phone_picker.js b/frappe/public/js/frappe/phone_picker/phone_picker.js index c5c1437c12..e96d34b991 100644 --- a/frappe/public/js/frappe/phone_picker/phone_picker.js +++ b/frappe/public/js/frappe/phone_picker/phone_picker.js @@ -17,7 +17,7 @@ class PhonePicker { this.phone_picker_wrapper = $(`
- + ${frappe.utils.icon('search', "sm")}
@@ -37,8 +37,12 @@ class PhonePicker { if (!info.isd) { return; } - let $country = $(`
${frappe.utils.flag(info.code)} - ${country} (${info.isd})
`); + let $country = $(` +
+ ${frappe.utils.flag(info.code)} + ${country} (${info.isd}) +
+ `); this.phone_wrapper.append($country); const set_values = () => { this.set_country(country); @@ -89,6 +93,11 @@ class PhonePicker { get_country() { return this.country; } + + reset() { + this.set_country(); + this.update_icon_selected(); + } } export default PhonePicker; diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index e651af3ff5..1ed46ecb64 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -17,7 +17,6 @@ from gzip import GzipFile from typing import Generator, Iterable from urllib.parse import quote, urlparse -import phonenumbers as ph from redis.exceptions import ConnectionError from werkzeug.test import Client @@ -83,37 +82,30 @@ def extract_email_id(email): return email_id -def validate_phone_number_with_isd(phone, fieldname, throw=False): +def validate_phone_number_with_country_code(phone_number, fieldname): + from phonenumbers import NumberParseException, is_valid_number, parse + from frappe import _ - if not phone: + if not phone_number: return try: - phone_number = ph.parse(phone) - except Exception as e: - if e.error_type == 1: - - frappe.throw( - _("Phone Number {0} set in field {1} is not valid.").format( - frappe.bold(phone), frappe.bold(fieldname) - ), - frappe.InvalidPhoneNumberError, - title=_("Invalid Phone Number"), - ) + if is_valid_number(parse(phone_number)): + return True + error_message = _("Phone Number {0} set in field {1} is not valid.") + error_title = _("Invalid Phone Number") + except NumberParseException as e: + if e.error_type == NumberParseException.INVALID_COUNTRY_CODE: + error_message = _("Please select a country code for field {1}.") + error_title = _("Country Code Required") + if e.error_type == NumberParseException.NOT_A_NUMBER: + error_message = _("Phone Number {0} set in field {1} is not valid.") + error_title = _("Invalid Phone Number") + finally: frappe.throw( - _("Please select a country code for field {1}.").format( - frappe.bold(phone), frappe.bold(fieldname) - ), - frappe.InvalidPhoneNumberError, - title=_("Country Code Required"), - ) - if not ph.is_valid_number(phone_number): - frappe.throw( - _("Phone Number {0} set in field {1} is not valid.").format( - frappe.bold(phone), frappe.bold(fieldname) - ), - frappe.InvalidPhoneNumberError, - title=_("Invalid Phone Number"), + error_message.format(frappe.bold(phone_number), frappe.bold(fieldname)), + title=error_title, + exc=frappe.InvalidPhoneNumberError, ) From a396afc7d471b117aca6c01d9e8c1439155c00c0 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 14 Apr 2022 17:05:07 +0530 Subject: [PATCH 053/139] fix: Validation error --- cypress/integration/control_phone.js | 3 --- frappe/utils/__init__.py | 22 +++++++++++----------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/cypress/integration/control_phone.js b/cypress/integration/control_phone.js index 1a12ddf2b4..03f01f5630 100644 --- a/cypress/integration/control_phone.js +++ b/cypress/integration/control_phone.js @@ -71,10 +71,8 @@ context("Control Phone", () => { //Adding a new entry for the created custom doctype cy.fill_field("title", "Test Phone 1"); cy.fill_field("phone", "+91-9823341234"); - cy.wait(500); cy.get_field("phone").should("have.value", "9823341234"); cy.click_doc_primary_button("Save"); - cy.wait(500); cy.get_doc("Doctype With Phone", "Test Phone 1").then((doc) => { let value = doc.data.phone; expect(value).to.equal("+91-9823341234"); @@ -86,7 +84,6 @@ context("Control Phone", () => { cy.get(".selected-phone .country").should("have.text", ""); cy.fill_field("title", "Test Phone 2"); cy.fill_field("phone", "+91-9823341291"); - cy.wait(500); cy.get_field("phone").should("have.value", "9823341291"); cy.intercept("POST", "/api/method/frappe.desk.form.save.savedocs").as("save_form"); cy.click_doc_primary_button("Save"); diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 1ed46ecb64..7907dc8cd4 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -89,24 +89,24 @@ def validate_phone_number_with_country_code(phone_number, fieldname): if not phone_number: return + + valid_number = False + error_message = _("Phone Number {0} set in field {1} is not valid.") + error_title = _("Invalid Phone Number") try: - if is_valid_number(parse(phone_number)): + if valid_number := is_valid_number(parse(phone_number)): return True - error_message = _("Phone Number {0} set in field {1} is not valid.") - error_title = _("Invalid Phone Number") except NumberParseException as e: if e.error_type == NumberParseException.INVALID_COUNTRY_CODE: error_message = _("Please select a country code for field {1}.") error_title = _("Country Code Required") - if e.error_type == NumberParseException.NOT_A_NUMBER: - error_message = _("Phone Number {0} set in field {1} is not valid.") - error_title = _("Invalid Phone Number") finally: - frappe.throw( - error_message.format(frappe.bold(phone_number), frappe.bold(fieldname)), - title=error_title, - exc=frappe.InvalidPhoneNumberError, - ) + if not valid_number: + frappe.throw( + error_message.format(frappe.bold(phone_number), frappe.bold(fieldname)), + title=error_title, + exc=frappe.InvalidPhoneNumberError, + ) def validate_phone_number(phone_number, throw=False): From a0e9b61e04770c5d1be782af18ac8157bd5ca225 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 14 Apr 2022 18:14:55 +0530 Subject: [PATCH 054/139] test: Simplify phone control test --- cypress/integration/control_phone.js | 37 ++++++---------------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/cypress/integration/control_phone.js b/cypress/integration/control_phone.js index 03f01f5630..5a26decdee 100644 --- a/cypress/integration/control_phone.js +++ b/cypress/integration/control_phone.js @@ -18,8 +18,9 @@ context("Control Phone", () => { it("should set flag and data", () => { get_dialog_with_phone().as("dialog"); - cy.get(".selected-phone > svg").click(); + cy.get(".selected-phone").click(); cy.get(".phone-picker .phone-wrapper[id='afghanistan']").click(); + cy.get(".selected-phone").click(); cy.get(".phone-picker .phone-wrapper[id='india']").click(); cy.get(".selected-phone .country").should("have.text", "+91"); cy.get(".selected-phone > img").should("have.attr", "src").and("include", "/in.svg"); @@ -35,9 +36,8 @@ context("Control Phone", () => { .type(phone_number, {force: true}); cy.get_field("phone").first().should("have.value", phone_number); - cy.wait(1000); cy.get_field("phone").first().blur({force: true}); - + cy.wait(100); cy.get("@dialog").then(dialog => { let value = dialog.get_value("phone"); expect(value).to.equal("+91-" + phone_number); @@ -46,7 +46,7 @@ context("Control Phone", () => { it("case insensitive search for country and clear search", () => { let search_text = "india"; - cy.get(".selected-phone > img").click().first(); + cy.get(".selected-phone").click().first(); cy.get(".phone-picker").findByRole("searchbox").click().type(search_text); cy.get(".phone-section .phone-wrapper:not(.hidden)").then(i => { cy.get(`.phone-section .phone-wrapper[id*="${search_text.toLowerCase()}"]`).then(countries => { @@ -58,7 +58,7 @@ context("Control Phone", () => { cy.get(".phone-section .phone-wrapper").should("not.have.class", "hidden"); }); - it("Already existing docs with phone field", () => { + it("existing document should render phone field with data", () => { cy.visit("/app/doctype"); cy.insert_doc("DocType", doctype_with_phone, true); cy.clear_cache(); @@ -68,7 +68,7 @@ context("Control Phone", () => { cy.visit("/app/doctype-with-phone"); cy.click_listview_primary_button("Add Doctype With Phone"); - //Adding a new entry for the created custom doctype + // create a record cy.fill_field("title", "Test Phone 1"); cy.fill_field("phone", "+91-9823341234"); cy.get_field("phone").should("have.value", "9823341234"); @@ -77,35 +77,14 @@ context("Control Phone", () => { let value = doc.data.phone; expect(value).to.equal("+91-9823341234"); }); - cy.go_to_list("Doctype With Phone"); - cy.click_listview_primary_button("Add Doctype With Phone"); - // Field should be empty on new doc - cy.get_field("phone").should("have.value", ""); - cy.get(".selected-phone .country").should("have.text", ""); - cy.fill_field("title", "Test Phone 2"); - cy.fill_field("phone", "+91-9823341291"); - cy.get_field("phone").should("have.value", "9823341291"); - cy.intercept("POST", "/api/method/frappe.desk.form.save.savedocs").as("save_form"); - cy.click_doc_primary_button("Save"); - cy.wait("@save_form"); + + // open the doc from list view cy.go_to_list("Doctype With Phone"); cy.clear_cache(); cy.click_listview_row_item(0); - cy.title().should("eq", "Test Phone 2"); - cy.get(".selected-phone .country").should("have.text", "+91"); - cy.get(".selected-phone > img").should("have.attr", "src").and("include", "/in.svg"); - cy.get_field("phone").should("have.value", "9823341291"); - cy.go_to_list("Doctype With Phone"); - cy.click_listview_row_item(1); cy.title().should("eq", "Test Phone 1"); cy.get(".selected-phone .country").should("have.text", "+91"); cy.get(".selected-phone > img").should("have.attr", "src").and("include", "/in.svg"); cy.get_field("phone").should("have.value", "9823341234"); - cy.get_doc("Doctype With Phone", "Test Phone 2").then((doc) => { - let value = doc.data.phone; - expect(value).to.equal("+91-9823341291"); - cy.remove_doc("Doctype With Phone", "Test Phone 1", true); - cy.remove_doc("Doctype With Phone", "Test Phone 2", true); - }); }); }); From 8cd372d90b97343c8ac09c396932dc12efcd852e Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 18 Apr 2022 11:08:07 +0530 Subject: [PATCH 055/139] refactor: Commonify autoname validation - Validate autoname for Customize Form --- frappe/core/doctype/doctype/doctype.js | 78 +--------------- frappe/core/doctype/doctype/doctype.json | 2 +- frappe/core/doctype/doctype/doctype.py | 15 ++-- .../customize_form/customize_form.json | 10 ++- frappe/public/js/frappe/doctype/index.js | 90 ++++++++++++++++++- 5 files changed, 110 insertions(+), 85 deletions(-) diff --git a/frappe/core/doctype/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js index 88cc5577a6..514e3a9455 100644 --- a/frappe/core/doctype/doctype/doctype.js +++ b/frappe/core/doctype/doctype/doctype.js @@ -57,8 +57,8 @@ frappe.ui.form.on('DocType', { frm.get_docfield('fields', 'in_list_view').label = frm.doc.istable ? __('In Grid View') : __('In List View'); - frm.events.autoname(frm); - frm.events.set_naming_rule_description(frm); + frm.cscript.autoname(frm); + frm.cscript.set_naming_rule_description(frm); }, istable: (frm) => { @@ -67,80 +67,6 @@ frappe.ui.form.on('DocType', { frm.set_value('allow_rename', 0); } }, - - naming_rule: function(frm) { - // set the "autoname" property based on naming_rule - if (frm.doc.naming_rule && !frm.__from_autoname) { - - // flag to avoid recursion - frm.__from_naming_rule = true; - - if (frm.doc.naming_rule=='Set by user') { - frm.set_value('autoname', 'Prompt'); - } else if (frm.doc.naming_rule === 'Autoincrement') { - frm.set_value('autoname', 'autoincrement'); - // set allow rename to be false when using autoincrement - frm.set_value('allow_rename', 0); - } else if (frm.doc.naming_rule=='By fieldname') { - frm.set_value('autoname', 'field:'); - } else if (frm.doc.naming_rule=='By "Naming Series" field') { - frm.set_value('autoname', 'naming_series:'); - } else if (frm.doc.naming_rule=='Expression') { - frm.set_value('autoname', 'format:'); - } else if (frm.doc.naming_rule=='Expression (old style)') { - // pass - } else if (frm.doc.naming_rule=='Random') { - frm.set_value('autoname', 'hash'); - } - setTimeout(() =>frm.__from_naming_rule = false, 500); - - frm.events.set_naming_rule_description(frm); - } - - }, - - set_naming_rule_description(frm) { - let naming_rule_description = { - 'Set by user': '', - 'Autoincrement': 'Uses Auto Increment feature of database.
WARNING: After using this option, any other naming option will not be accessible.', - 'By fieldname': 'Format: field:[fieldname]. Valid fieldname must exist', - 'By "Naming Series" field': 'Format: naming_series:[fieldname]. Fieldname called naming_series must exist', - 'Expression': 'Format: format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.', - 'Expression (old style)': 'Format: EXAMPLE-.##### Series by prefix (separated by a dot)', - 'Random': '', - 'By script': '' - }; - - if (frm.doc.naming_rule) { - frm.get_field('autoname').set_description(naming_rule_description[frm.doc.naming_rule]); - } - }, - - autoname: function(frm) { - // set naming_rule based on autoname (for old doctypes where its not been set) - if (frm.doc.autoname && !frm.doc.naming_rule && !frm.__from_naming_rule) { - // flag to avoid recursion - frm.__from_autoname = true; - if (frm.doc.autoname.toLowerCase() === 'prompt') { - frm.set_value('naming_rule', 'Set by user'); - } else if (frm.doc.autoname.toLowerCase() === 'autoincrement') { - frm.set_value('naming_rule', 'Autoincrement'); - } else if (frm.doc.autoname.startsWith('field:')) { - frm.set_value('naming_rule', 'By fieldname'); - } else if (frm.doc.autoname.startsWith('naming_series:')) { - frm.set_value('naming_rule', 'By "Naming Series" field'); - } else if (frm.doc.autoname.startsWith('format:')) { - frm.set_value('naming_rule', 'Expression'); - } else if (frm.doc.autoname.toLowerCase() === 'hash') { - frm.set_value('naming_rule', 'Random'); - } else { - frm.set_value('naming_rule', 'Expression (old style)'); - } - setTimeout(() => frm.__from_autoname = false, 500); - } - - frm.set_df_property('fields', 'reqd', frm.doc.autoname !== 'Prompt'); - }, }); frappe.ui.form.on("DocField", { diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index 8169a59566..b1579f35cd 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -208,7 +208,7 @@ "label": "Naming" }, { - "description": "Naming Options:\n
  1. field:[fieldname] - By Field
  2. autoincrement - Uses Databases' Auto Increment feature
  3. naming_series: - By Naming Series (field called naming_series must be present
  4. Prompt - Prompt user for a name
  5. [series] - Series by prefix (separated by a dot); for example PRE.#####
  6. \n
  7. format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
", + "description": "Naming Options:\n
  1. field:[fieldname] - By Field
  2. autoincrement - Uses Databases' Auto Increment feature
  3. naming_series: - By Naming Series (field called naming_series must be present)
  4. Prompt - Prompt user for a name
  5. [series] - Series by prefix (separated by a dot); for example PRE.#####
  6. \n
  7. format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
", "fieldname": "autoname", "fieldtype": "Data", "label": "Auto Name", diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 1d02f09820..3a73eb9d42 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -817,10 +817,8 @@ class DocType(Document): self.autoname != "autoincrement" and doc_before_save.autoname == "autoincrement" ): frappe.throw(_("Cannot change to/from Autoincrement naming rule")) - - else: - if self.autoname == "autoincrement": - self.allow_rename = 0 + if self.autoname == "autoincrement": + self.allow_rename = 0 def validate_name(self, name=None): if not name: @@ -865,8 +863,13 @@ def validate_series(dt, autoname=None, name=None): if not autoname and dt.get("fields", {"fieldname": "naming_series"}): dt.autoname = "naming_series:" - elif dt.autoname == "naming_series:" and not dt.get("fields", {"fieldname": "naming_series"}): - frappe.throw(_("Invalid fieldname '{0}' in autoname").format(dt.autoname)) + elif dt.autoname.startswith("naming_series:"): + fieldname = dt.autoname.split("naming_series:")[0] or "naming_series" + if not dt.get("fields", {"fieldname": fieldname}): + frappe.throw( + _("Fieldname called {0} must exist to enable autonaming").format(frappe.bold(fieldname)), + title=_("Field Missing"), + ) # validate field name if autoname field:fieldname is used # Create unique index on autoname field automatically. diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json index 1ee9d4a02a..d51762effa 100644 --- a/frappe/custom/doctype/customize_form/customize_form.json +++ b/frappe/custom/doctype/customize_form/customize_form.json @@ -24,6 +24,7 @@ "fields_section_break", "fields", "naming_section", + "naming_rule", "autoname", "view_settings_section", "title_field", @@ -50,6 +51,13 @@ "sort_order" ], "fields": [ + { + "fieldname": "naming_rule", + "fieldtype": "Select", + "label": "Naming Rule", + "length": 40, + "options": "\nSet by user\nAutoincrement\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script" + }, { "fieldname": "doc_type", "fieldtype": "Link", @@ -279,7 +287,7 @@ "label": "Naming" }, { - "description": "Naming Options:\n
  1. field:[fieldname] - By Field
  2. naming_series: - By Naming Series (field called naming_series must be present
  3. Prompt - Prompt user for a name
  4. [series] - Series by prefix (separated by a dot); for example PRE.#####
  5. \n
  6. format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
", + "description": "Naming Options:\n
  1. field:[fieldname] - By Field
  2. autoincrement - Uses Databases' Auto Increment feature
  3. naming_series: - By Naming Series (field called naming_series must be present)
  4. Prompt - Prompt user for a name
  5. [series] - Series by prefix (separated by a dot); for example PRE.#####
  6. \n
  7. format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
", "fieldname": "autoname", "fieldtype": "Data", "label": "Auto Name" diff --git a/frappe/public/js/frappe/doctype/index.js b/frappe/public/js/frappe/doctype/index.js index 9fe8957c60..e71f0b6f09 100644 --- a/frappe/public/js/frappe/doctype/index.js +++ b/frappe/public/js/frappe/doctype/index.js @@ -20,4 +20,92 @@ frappe.model.DocTypeController = class DocTypeController extends frappe.ui.form. __("Number of attachment fields are more than {}, limit updated to {}.", [label, no_of_attach_fields])); } } -} + + naming_rule() { + // set the "autoname" property based on naming_rule + if (this.frm.doc.naming_rule && !this.frm.__from_autoname) { + + // flag to avoid recursion + this.frm.__from_naming_rule = true; + + switch (this.frm.doc.naming_rule) { + case "Set by user": + this.frm.set_value("autoname", "Prompt"); + break; + case "Autoincrement": + this.frm.set_value("autoname", "autoincrement"); + break; + case "By fieldname": + this.frm.set_value("autoname", "field:"); + break; + case 'By "Naming Series" field': + this.frm.set_value("autoname", "naming_series:"); + break; + case "Expression": + this.frm.set_value("autoname", "format:"); + break; + case "Expression (old style)": + break; + case "Random": + this.frm.set_value("autoname", "hash"); + break; + } + setTimeout(() =>this.frm.__from_naming_rule = false, 500); + + this.set_naming_rule_description(); + } + + } + + set_naming_rule_description() { + let naming_rule_description = { + 'Set by user': '', + 'Autoincrement': 'Uses Auto Increment feature of database.
WARNING: After using this option, any other naming option will not be accessible.', + 'By fieldname': 'Format: field:[fieldname]. Valid fieldname must exist', + 'By "Naming Series" field': 'Format: naming_series:[fieldname]. Default fieldname is naming_series', + 'Expression': 'Format: format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.', + 'Expression (old style)': 'Format: EXAMPLE-.##### Series by prefix (separated by a dot)', + 'Random': '', + 'By script': '' + }; + + if (this.frm.doc.naming_rule) { + this.frm.get_field('autoname').set_description(naming_rule_description[this.frm.doc.naming_rule]); + } + } + + autoname() { + // set naming_rule based on autoname (for old doctypes where its not been set) + if (this.frm.doc.autoname && !this.frm.doc.naming_rule && !this.frm.__from_naming_rule) { + // flag to avoid recursion + this.frm.__from_autoname = true; + const autoname = this.frm.doc.autoname.toLowerCase(); + + switch (autoname) { + case 'prompt': + this.frm.set_value('naming_rule', 'Set by user'); + break; + case 'autoincrement': + this.frm.set_value('naming_rule', 'Autoincrement'); + break; + case (autoname.startsWith('field:')): + this.frm.set_value('naming_rule', 'By fieldname'); + break; + case (autoname.startsWith('naming_series:')): + this.frm.set_value('naming_rule', 'By "Naming Series" field'); + break; + case (autoname.startsWith('format:')): + this.frm.set_value('naming_rule', 'Expression'); + break; + case 'hash': + this.frm.set_value('naming_rule', 'Random'); + break; + default: + this.frm.set_value('naming_rule', 'Expression (old style)'); + } + setTimeout(() => this.frm.__from_autoname = false, 500); + } + + this.frm.set_df_property('fields', 'reqd', this.frm.doc.autoname !== 'Prompt'); + } +}; From 666a33a5750d951f5b48529f288fb348f86f1016 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 30 Mar 2022 11:32:16 +0530 Subject: [PATCH 056/139] fix: dont animate "Jump to field" modal It's supposed to be keyboard driven and currrently wasting 500 ms of my precious life. sd --- frappe/public/js/frappe/form/toolbar.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index e55eb9fdeb..e4b1d09c68 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -569,7 +569,8 @@ frappe.ui.form.Toolbar = class Toolbar { primary_action: ({ fieldname }) => { dialog.hide(); this.frm.scroll_to_field(fieldname); - } + }, + animate: false, }); dialog.show(); From 2d3c75a4a759d1bdb3315e88b72d3b0fea73ef76 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 14 Apr 2022 11:20:46 +0530 Subject: [PATCH 057/139] fix(ux): lose focus from all fields on ESC --- frappe/public/js/frappe/ui/keyboard.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/frappe/public/js/frappe/ui/keyboard.js b/frappe/public/js/frappe/ui/keyboard.js index e28a8f680d..40d1a93b8c 100644 --- a/frappe/public/js/frappe/ui/keyboard.js +++ b/frappe/public/js/frappe/ui/keyboard.js @@ -221,11 +221,11 @@ frappe.ui.keys.add_shortcut({ }); frappe.ui.keys.on('escape', function(e) { - close_grid_and_dialog(); + handle_escape_key(); }); frappe.ui.keys.on('esc', function(e) { - close_grid_and_dialog(); + handle_escape_key(); }); frappe.ui.keys.on('enter', function(e) { @@ -293,6 +293,11 @@ frappe.ui.keyCode = { BACKSPACE: 8 } +function handle_escape_key() { + close_grid_and_dialog(); + document.activeElement?.blur(); +} + function close_grid_and_dialog() { // close open grid row var open_row = $(".grid-row-open"); @@ -308,10 +313,3 @@ function close_grid_and_dialog() { return false; } } - -// blur when escape is pressed on dropdowns -$(document).on('keydown', '.dropdown-toggle', (e) => { - if (e.which === frappe.ui.keyCode.ESCAPE) { - $(e.currentTarget).blur(); - } -}); From af0b98334f7c96b78d51c49f43e0ab51f4eeae8e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 14 Apr 2022 11:36:02 +0530 Subject: [PATCH 058/139] chore: eslint - use es2020 for parsing --- .eslintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index 937f11586c..adc4aebb28 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,7 +5,7 @@ "es6": true }, "parserOptions": { - "ecmaVersion": 9, + "ecmaVersion": 11, "sourceType": "module" }, "extends": "eslint:recommended", From c68da5e6c725053794ab9f5995c29f9b83bdaadd Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 16 Apr 2022 14:55:02 +0530 Subject: [PATCH 059/139] fix(UX): focus field immediately after jump Not sure why we need to wait 1sec :) --- frappe/public/js/frappe/form/form.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 0c8939cf5d..13fa712c05 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -1765,11 +1765,13 @@ frappe.ui.form.Form = class FrappeForm { // scroll to input frappe.utils.scroll_to($el, true, 15); + // focus if text field + $el.find('input, select, textarea').focus(); + // highlight input $el.addClass('has-error'); setTimeout(() => { $el.removeClass('has-error'); - $el.find('input, select, textarea').focus(); }, 1000); } From 8df645c9040317a3bdf382a9797d592ef8af7f4a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 16 Apr 2022 15:28:05 +0530 Subject: [PATCH 060/139] fix(UX): better highlighting for jumped field --- frappe/public/js/frappe/form/form.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 13fa712c05..3c07907c50 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -1768,11 +1768,12 @@ frappe.ui.form.Form = class FrappeForm { // focus if text field $el.find('input, select, textarea').focus(); - // highlight input - $el.addClass('has-error'); + // highlight control inside field + let control_element = $el.find('.form-control') + control_element.addClass('highlight'); setTimeout(() => { - $el.removeClass('has-error'); - }, 1000); + control_element.removeClass('highlight'); + }, 2000); } setup_docinfo_change_listener() { From 6cdd33f26bffb850db65a60837dbc2b8ed12abc0 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 20 Apr 2022 16:41:25 +0530 Subject: [PATCH 061/139] fix: Generate hash of length 8 for save point --- frappe/model/rename_doc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index fede42a333..a0cd10f967 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -350,7 +350,7 @@ def validate_rename( ) -> str: # using for update so that it gets locked and someone else cannot edit it while this rename is going on! if save_point: - _SAVE_POINT = f"validate_rename_{frappe.generate_hash(8)}" + _SAVE_POINT = f"validate_rename_{frappe.generate_hash(length=8)}" frappe.db.savepoint(_SAVE_POINT) exists = ( From da191390a5eb4da803c8ac732fb46aad60920519 Mon Sep 17 00:00:00 2001 From: phot0n Date: Wed, 20 Apr 2022 14:40:24 +0530 Subject: [PATCH 062/139] fix: support for multiple order by in add_conditions --- frappe/database/query.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/frappe/database/query.py b/frappe/database/query.py index 8d8a767370..ba19ab6356 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -108,11 +108,14 @@ def change_orderby(order: str): tuple: field, order """ order = order.split() - if order[1].lower() == "asc": - orderby, order = order[0], Order.asc - return orderby, order - orderby, order = order[0], Order.desc - return orderby, order + + try: + if order[1].lower() == "asc": + return order[0], Order.asc + except IndexError: + pass + + return order[0], Order.desc OPERATOR_MAP = { @@ -175,10 +178,13 @@ class Query: """ if kwargs.get("orderby"): orderby = kwargs.get("orderby") - order = kwargs.get("order") if kwargs.get("order") else Order.desc if isinstance(orderby, str) and len(orderby.split()) > 1: - orderby, order = change_orderby(orderby) - conditions = conditions.orderby(orderby, order=order) + for ordby in orderby.split(","): + if ordby := ordby.strip(): + orderby, order = change_orderby(ordby) + conditions = conditions.orderby(orderby, order=order) + else: + conditions = conditions.orderby(orderby, order=kwargs.get("order") or Order.desc) if kwargs.get("limit"): conditions = conditions.limit(kwargs.get("limit")) From c51635702716c64e9c3947176520828adaab27d9 Mon Sep 17 00:00:00 2001 From: phot0n Date: Wed, 20 Apr 2022 17:18:13 +0530 Subject: [PATCH 063/139] test(get_value): test for multiple order bys --- frappe/tests/test_db.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index 5b469cd5db..b83c290268 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -87,6 +87,13 @@ class TestDB(unittest.TestCase): frappe.db.get_values("User", filters=[["name", "=", "Administrator"]], fieldname="email"), ) + # test multiple orderby's + delimiter = '"' if frappe.db.db_type == "postgres" else "`" + self.assertIn( + "ORDER BY {deli}creation{deli} DESC,{deli}modified{deli} ASC,{deli}name{deli} DESC".format(deli=delimiter), + frappe.db.get_value("DocType", "DocField", order_by="creation desc, modified asc, name", run=0) + ) + def test_get_value_limits(self): # check both dict and list style filters From 6405b0510a39488d7d51b3e23a575eddf790e365 Mon Sep 17 00:00:00 2001 From: phot0n Date: Wed, 20 Apr 2022 17:32:09 +0530 Subject: [PATCH 064/139] chore: fix linter --- frappe/database/query.py | 2 +- frappe/tests/test_db.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frappe/database/query.py b/frappe/database/query.py index ba19ab6356..136f5c86b6 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -294,7 +294,7 @@ class Query: table: str, fields: Union[List, Tuple], filters: Union[Dict[str, Union[str, int]], str, int] = None, - **kwargs + **kwargs, ): criterion = self.build_conditions(table, filters, **kwargs) if isinstance(fields, (list, tuple)): diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index b83c290268..6cba55c425 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -90,8 +90,10 @@ class TestDB(unittest.TestCase): # test multiple orderby's delimiter = '"' if frappe.db.db_type == "postgres" else "`" self.assertIn( - "ORDER BY {deli}creation{deli} DESC,{deli}modified{deli} ASC,{deli}name{deli} DESC".format(deli=delimiter), - frappe.db.get_value("DocType", "DocField", order_by="creation desc, modified asc, name", run=0) + "ORDER BY {deli}creation{deli} DESC,{deli}modified{deli} ASC,{deli}name{deli} DESC".format( + deli=delimiter + ), + frappe.db.get_value("DocType", "DocField", order_by="creation desc, modified asc, name", run=0), ) def test_get_value_limits(self): From 56bb11dc17ed16de577263908e51ea6d076ea44f Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 21 Apr 2022 08:49:48 +0530 Subject: [PATCH 065/139] fix: Do not change direction of text if already in RTL mode --- frappe/public/scss/common/quill.scss | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frappe/public/scss/common/quill.scss b/frappe/public/scss/common/quill.scss index 3d5ce61c15..9aab2e9fcd 100644 --- a/frappe/public/scss/common/quill.scss +++ b/frappe/public/scss/common/quill.scss @@ -7,6 +7,7 @@ font-family: inherit; } +/*rtl:begin:ignore*/ .ql-editor { font-family: var(--font-stack); color: var(--text-color); @@ -22,7 +23,15 @@ a[href] { text-decoration: underline; } + .ql-direction-rtl { + direction: rtl; + + .table { + direction: ltr; + } + } } +/*rtl:end:ignore*/ + .ql-toolbar.ql-snow { border-top-left-radius: var(--border-radius); @@ -70,6 +79,7 @@ min-height: 0; max-height: none; overflow: hidden; + resize: none; } } From b5c3ff16ee9dd19bbedb9a8f8d561e82130f8f4e Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 21 Apr 2022 12:43:46 +0530 Subject: [PATCH 066/139] fix: Do not use `set_value` for default to avoid change event --- frappe/public/js/frappe/ui/field_group.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/frappe/public/js/frappe/ui/field_group.js b/frappe/public/js/frappe/ui/field_group.js index eb3dcc4f89..178d1a65cb 100644 --- a/frappe/public/js/frappe/ui/field_group.js +++ b/frappe/public/js/frappe/ui/field_group.js @@ -22,15 +22,17 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { super.make(); this.refresh(); // set default - $.each(this.fields_list, (_, field) => { - if (!is_null(field.df.default)) { - let def_value = field.df.default; + $.each(this.fields_list, function(i, field) { + if (field.df["default"]) { + let def_value = field.df["default"]; - if (def_value === "Today" && field.df.fieldtype === "Date") { + if (def_value == 'Today' && field.df["fieldtype"] == 'Date') { def_value = frappe.datetime.get_today(); } - this.set_value(field.df.fieldname, def_value); + field.set_input(def_value); + // if default and has depends_on, render its fields. + me.refresh_dependency(); } }) @@ -127,7 +129,6 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { if (f) { f.set_value(val).then(() => { f.set_input(val); - f.refresh(); this.refresh_dependency(); resolve(); }); From 4e533682ba2ee3d822016ded2222bcbe4453a70a Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 21 Apr 2022 13:26:12 +0530 Subject: [PATCH 067/139] feat: get_traceback with context --- frappe/__init__.py | 4 ++-- frappe/utils/__init__.py | 14 ++++++++++---- requirements.txt | 1 + 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 97e605394b..257d02a09a 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -354,11 +354,11 @@ def cache() -> "RedisWrapper": return redis_server -def get_traceback(): +def get_traceback(with_context=False): """Returns error traceback.""" from frappe.utils import get_traceback - return get_traceback() + return get_traceback(with_context=with_context) def errprint(msg): diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 3e62589664..d0dfe44760 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -18,6 +18,7 @@ from typing import Generator, Iterable from urllib.parse import quote, urlparse from redis.exceptions import ConnectionError +from traceback_with_variables import iter_exc_lines from werkzeug.test import Client import frappe @@ -255,7 +256,7 @@ def get_gravatar(email): return gravatar_url -def get_traceback() -> str: +def get_traceback(with_context=False) -> str: """ Returns the traceback of the Exception """ @@ -264,10 +265,15 @@ def get_traceback() -> str: if not any([exc_type, exc_value, exc_tb]): return "" - trace_list = traceback.format_exception(exc_type, exc_value, exc_tb) - bench_path = get_bench_path() + "/" + if with_context: + trace_list = iter_exc_lines() + tb = "\n".join(trace_list) + else: + trace_list = traceback.format_exception(exc_type, exc_value, exc_tb) + tb = "".join(cstr(t) for t in trace_list) - return "".join(cstr(t) for t in trace_list).replace(bench_path, "") + bench_path = get_bench_path() + "/" + return tb.replace(bench_path, "") def log(event, details): diff --git a/requirements.txt b/requirements.txt index c77ab1d424..495574e05b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -63,6 +63,7 @@ semantic-version~=2.8.5 sqlparse~=0.4.1 stripe~=2.56.0 terminaltables~=3.1.0 +traceback-with-variables~=2.0.4 urllib3~=1.26.4 Werkzeug~=2.0.3 Whoosh~=2.7.4 From 5ecc9fe4ffec55a70fde9f4196ce74c15630f758 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 21 Apr 2022 13:27:01 +0530 Subject: [PATCH 068/139] refactor(log_error): de-clutter & log context with traceback --- frappe/__init__.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 257d02a09a..4653845c3e 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -2069,7 +2069,6 @@ def logger( def log_error(title=None, message=None, reference_doctype=None, reference_name=None): """Log error to Error Log""" - # Parameter ALERT: # the title and message may be swapped # the better API for this is log_error(title, message), and used in many cases this way @@ -2082,20 +2081,15 @@ def log_error(title=None, message=None, reference_doctype=None, reference_name=N else: traceback = message - if not traceback: - traceback = get_traceback() - - if not title: - title = "Error" + title = title or "Error" + traceback = as_unicode(traceback or get_traceback(with_context=True)) return get_doc( - dict( - doctype="Error Log", - error=as_unicode(traceback), - method=title, - reference_doctype=reference_doctype, - reference_name=reference_name, - ) + doctype="Error Log", + error=traceback, + method=title, + reference_doctype=reference_doctype, + reference_name=reference_name, ).insert(ignore_permissions=True) From c691537e61b5d2d7f0151e4b13a7842d083bdbbb Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 21 Apr 2022 13:28:49 +0530 Subject: [PATCH 069/139] chore: Add typing for ease of development --- frappe/database/database.py | 2 +- frappe/model/base_document.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/frappe/database/database.py b/frappe/database/database.py index d4677a1295..edc57ae543 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -1066,7 +1066,7 @@ class Database(object): now_datetime() - relativedelta(minutes=minutes), )[0][0] - def get_db_table_columns(self, table): + def get_db_table_columns(self, table) -> List[str]: """Returns list of column names from given table.""" columns = frappe.cache().hget("table_columns", table) if columns is None: diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 02eb2ab38c..f8d60d0763 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -2,6 +2,7 @@ # License: MIT. See LICENSE import datetime import json +from typing import Dict, List import frappe from frappe import _ @@ -252,7 +253,7 @@ class BaseDocument(object): def get_valid_dict( self, sanitize=True, convert_dates_to_str=False, ignore_nulls=False, ignore_virtual=False - ): + ) -> Dict: d = frappe._dict() for fieldname in self.meta.get_valid_columns(): d[fieldname] = self.get(fieldname) @@ -329,7 +330,7 @@ class BaseDocument(object): if key not in self.__dict__: self.__dict__[key] = None - def get_valid_columns(self): + def get_valid_columns(self) -> List[str]: if self.doctype not in frappe.local.valid_columns: if self.doctype in DOCTYPES_FOR_DOCTYPE: from frappe.model.meta import get_table_columns @@ -342,7 +343,7 @@ class BaseDocument(object): return frappe.local.valid_columns[self.doctype] - def is_new(self): + def is_new(self) -> bool: return self.get("__islocal") @property @@ -359,7 +360,7 @@ class BaseDocument(object): no_default_fields=False, convert_dates_to_str=False, no_child_table_fields=False, - ): + ) -> Dict: doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str) doc["doctype"] = self.doctype From 418dcdd2f445d39da4fa01bce94f19f973d27d50 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 21 Apr 2022 13:32:28 +0530 Subject: [PATCH 070/139] fix!: Use event as a differentiator for frappe.utils.log --- 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 d0dfe44760..13e6911634 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -277,7 +277,7 @@ def get_traceback(with_context=False) -> str: def log(event, details): - frappe.logger().info(details) + frappe.logger(event).info(details) def dict_to_str(args, sep="&"): From dfef7192da8e99a0285c6a445806f452f69b2365 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 21 Apr 2022 15:27:59 +0530 Subject: [PATCH 071/139] refactor: remove duplicate code from db.get_descendants (#16699) --- frappe/database/database.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/frappe/database/database.py b/frappe/database/database.py index d4677a1295..978667a7e9 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -1146,18 +1146,13 @@ class Database(object): return frappe.db.is_missing_column(e) def get_descendants(self, doctype, name): - """Return descendants of the current record""" - node_location_indexes = self.get_value(doctype, name, ("lft", "rgt")) - if node_location_indexes: - lft, rgt = node_location_indexes - return self.sql_list( - """select name from `tab{doctype}` - where lft > {lft} and rgt < {rgt}""".format( - doctype=doctype, lft=lft, rgt=rgt - ) - ) - else: - # when document does not exist + """Return descendants of the group node in tree""" + from frappe.utils.nestedset import get_descendants_of + + try: + return get_descendants_of(doctype, name, ignore_permissions=True) + except Exception: + # Can only happen if document doesn't exists - kept for backward compatibility return [] def is_missing_table_or_column(self, e): From 9823e51512e3d4870f6bece76c36d69123724d3c Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 21 Apr 2022 16:00:24 +0530 Subject: [PATCH 072/139] feat(safe_exec): Allow new_doc, get_last_doc, rename_doc, delte_doc * rename_doc points to the unwhitelisted method which supports ignore_permissions check * Allowed other safe utils for better DX --- frappe/utils/safe_exec.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index aa6fa8b67f..2e7ef5da21 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -15,6 +15,8 @@ import frappe.utils.data from frappe import _ from frappe.frappeclient import FrappeClient from frappe.handler import execute_cmd +from frappe.model.delete_doc import delete_doc +from frappe.model.rename_doc import rename_doc from frappe.modules import scrub from frappe.utils.background_jobs import enqueue, get_jobs from frappe.website.utils import get_next_link, get_shade, get_toc @@ -110,12 +112,15 @@ def get_safe_globals(): errprint=frappe.errprint, qb=frappe.qb, get_meta=frappe.get_meta, + new_doc=frappe.new_doc, get_doc=frappe.get_doc, + get_last_doc=frappe.get_last_doc, get_cached_doc=frappe.get_cached_doc, get_list=frappe.get_list, get_all=frappe.get_all, get_system_settings=frappe.get_system_settings, - rename_doc=frappe.rename_doc, + rename_doc=rename_doc, + delete_doc=delete_doc, utils=datautils, get_url=frappe.utils.get_url, render_template=frappe.render_template, From e565deb88e07d952ec5f410e82c6fa8ccc826e6e Mon Sep 17 00:00:00 2001 From: KrutikaBhatt <65107474+KrutikaBhatt@users.noreply.github.com> Date: Thu, 21 Apr 2022 16:00:56 +0530 Subject: [PATCH 073/139] fix(UI): Duration Filter overlapping issue (#16599) --- frappe/public/scss/common/controls.scss | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frappe/public/scss/common/controls.scss b/frappe/public/scss/common/controls.scss index fcc924650e..9df89f23e1 100644 --- a/frappe/public/scss/common/controls.scss +++ b/frappe/public/scss/common/controls.scss @@ -343,11 +343,10 @@ textarea.form-control { .duration-picker { position: absolute; z-index: 999; - border-radius: var(--border-radius); box-shadow: var(--shadow-sm); background: var(--popover-bg); - + width: max-content; &:after, &:before { border: solid transparent; @@ -466,4 +465,4 @@ button.data-pill { top: 0; right: 0; cursor: pointer; -} \ No newline at end of file +} From b6683db57e477df7949ad2658e060c495e8cd447 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 21 Apr 2022 16:09:08 +0530 Subject: [PATCH 074/139] refactor: frappe.rename_doc definition Use explicit naming of args, kwargs and don't accept cmd and ignore_permissions explicitly --- frappe/__init__.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 97e605394b..4e85eb3afc 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1210,18 +1210,35 @@ def reload_doc(module, dt=None, dn=None, force=False, reset_permissions=False): @whitelist() -def rename_doc(*args, **kwargs): +def rename_doc( + doctype: str, + old: str, + new: str, + force: bool = False, + merge: bool = False, + *, + ignore_if_exists: bool = False, + show_alert: bool = True, + rebuild_search: bool = True, +) -> str: """ Renames a doc(dt, old) to doc(dt, new) and updates all linked fields of type "Link" Calls `frappe.model.rename_doc.rename_doc` """ - kwargs.pop("ignore_permissions", None) - kwargs.pop("cmd", None) from frappe.model.rename_doc import rename_doc - return rename_doc(*args, **kwargs) + return rename_doc( + doctype=doctype, + old=old, + new=new, + force=force, + merge=merge, + ignore_if_exists=ignore_if_exists, + show_alert=show_alert, + rebuild_search=rebuild_search, + ) def get_module(modulename): From ab1f893e41c91843512c1b92dac548eb4dfe08d2 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 21 Apr 2022 16:46:49 +0530 Subject: [PATCH 075/139] feat: Add get_mapped_doc in safe_exec under frappe --- frappe/utils/safe_exec.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index 2e7ef5da21..fc53243021 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -16,6 +16,7 @@ from frappe import _ from frappe.frappeclient import FrappeClient from frappe.handler import execute_cmd from frappe.model.delete_doc import delete_doc +from frappe.model.mapper import get_mapped_doc from frappe.model.rename_doc import rename_doc from frappe.modules import scrub from frappe.utils.background_jobs import enqueue, get_jobs @@ -114,6 +115,7 @@ def get_safe_globals(): get_meta=frappe.get_meta, new_doc=frappe.new_doc, get_doc=frappe.get_doc, + get_mapped_doc=get_mapped_doc, get_last_doc=frappe.get_last_doc, get_cached_doc=frappe.get_cached_doc, get_list=frappe.get_list, From 7d9695a0d2aafcc0e15c20d06a64f43d3d96ee54 Mon Sep 17 00:00:00 2001 From: chillaranand Date: Fri, 22 Apr 2022 11:10:54 +0530 Subject: [PATCH 076/139] chore: Bump wrapt to 1.14.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c77ab1d424..099e49a0b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -66,7 +66,7 @@ terminaltables~=3.1.0 urllib3~=1.26.4 Werkzeug~=2.0.3 Whoosh~=2.7.4 -wrapt~=1.12.1 +wrapt~=1.14.0 xlrd~=2.0.1 zxcvbn-python~=4.4.24 tenacity~=8.0.1 From e6e01627f89ab421963591ac0099376a24ff3aa3 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 22 Apr 2022 12:09:26 +0530 Subject: [PATCH 077/139] refactor: rename_document_title * Remove extra reload_doc that caused extra onload * Refactor variable naming, DRY-er * Simplify flow --- frappe/public/js/frappe/form/toolbar.js | 39 +++++++++++++------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 01bfae2cdd..00b4efbf30 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -84,15 +84,15 @@ frappe.ui.form.Toolbar = class Toolbar { message: __("Unchanged") }); } - rename_document_title(new_name, new_title, merge=false) { + rename_document_title(input_name, input_title, merge=false) { let confirm_message = null; const docname = this.frm.doc.name; const title_field = this.frm.meta.title_field || ''; const doctype = this.frm.doctype; - if (new_name) { + if (input_name) { const warning = __("This cannot be undone"); - const message = __("Are you sure you want to merge {0} with {1}?", [docname.bold(), new_name.bold()]); + const message = __("Are you sure you want to merge {0} with {1}?", [docname.bold(), input_name.bold()]); confirm_message = `${message}
${warning}`; } @@ -100,42 +100,45 @@ frappe.ui.form.Toolbar = class Toolbar { return frappe.xcall("frappe.model.rename_doc.update_document_title", { doctype, docname, - name: new_name, - title: new_title, + name: input_name, + title: input_title, enqueue: true, merge, freeze: true, freeze_message: __("Updating related fields...") }).then(new_docname => { + const reload_form = (input_name) => { + $(document).trigger("rename", [doctype, docname, input_name]); + if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname]; + this.frm.reload_doc(); + } + // handle document renaming queued action - if (new_name && (new_docname == docname)) { - frappe.socketio.doc_subscribe(doctype, new_name); + if (input_name && (new_docname == docname)) { + frappe.socketio.doc_subscribe(doctype, input_name); frappe.realtime.on("doc_update", data => { - if (data.doctype == doctype && data.name == new_name) { - $(document).trigger("rename", [doctype, docname, new_name]); - if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname]; - this.frm.reload_doc(); + if (data.doctype == doctype && data.name == input_name) { + reload_form(input_name); frappe.show_alert({ - message: __('Document renamed from {0} to {1}', [docname.bold(), new_name.bold()]), + message: __('Document renamed from {0} to {1}', [docname.bold(), input_name.bold()]), indicator: 'success', }); } }); frappe.show_alert( - __('Document renaming from {0} to {1} has been queued', [docname.bold(), new_name.bold()]) + __('Document renaming from {0} to {1} has been queued', [docname.bold(), input_name.bold()]) ); } + // handle document sync rename action - if (new_name && (new_name != docname)) { - $(document).trigger("rename", [doctype, docname, new_docname || new_name]); - if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname]; + if (input_name && ((new_docname || input_name) != docname)) { + reload_form(new_docname || input_name); } - this.frm.reload_doc(); }); }; return new Promise((resolve, reject) => { - if (new_title === this.frm.doc[title_field] && new_name === docname) { + if (input_title === this.frm.doc[title_field] && input_name === docname) { this.show_unchanged_document_alert(); resolve(); } else if (merge) { From a245cb51a24f044ce9e332c7193c16abfa21d325 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 22 Apr 2022 13:32:31 +0530 Subject: [PATCH 078/139] feat: configurable auto email reports limit (#16684) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The previous limit was 3 per user which is way too less, no known reason to restrict this other than hogging of system with too many reports. Bumped default limit to 20. - site config is not easily discoverable or editable, added config in system settings. - Moved auto email report background job form daily queue to `daily_long` queue. closes https://github.com/frappe/frappe/issues/16681 Screenshot 2022-04-20 at 12 33 06 PM `no-docs` (error message is sufficient to explain to user what to do without referring docs) ref: ISS-21-22-10245 ISS-21-22-07742 ISS-20-21-10850 ISS-20-21-10112 and many more times. This is such a stupid validation 🤦 --- .../system_settings/system_settings.json | 16 ++++++++++++++-- .../auto_email_report/auto_email_report.py | 17 +++++++++++------ frappe/hooks.py | 2 +- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 61410fb1a8..0c9b87e618 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -68,6 +68,8 @@ "prepared_report_section", "enable_prepared_report_auto_deletion", "prepared_report_expiry_period", + "column_break_64", + "max_auto_email_report_per_user", "system_updates_section", "disable_system_update_notification" ], @@ -445,7 +447,7 @@ "collapsible": 1, "fieldname": "prepared_report_section", "fieldtype": "Section Break", - "label": "Prepared Report" + "label": "Reports" }, { "default": "Frappe", @@ -485,12 +487,22 @@ "fieldtype": "Select", "label": "First Day of the Week", "options": "Sunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday" + }, + { + "fieldname": "column_break_64", + "fieldtype": "Column Break" + }, + { + "default": "20", + "fieldname": "max_auto_email_report_per_user", + "fieldtype": "Int", + "label": "Max auto email report per user" } ], "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2022-01-04 11:28:34.881192", + "modified": "2022-04-21 09:11:35.218721", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py index 7a9af6149a..9f897a1308 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.py +++ b/frappe/email/doctype/auto_email_report/auto_email_report.py @@ -12,6 +12,7 @@ from frappe.model.document import Document from frappe.model.naming import append_number_if_name_exists from frappe.utils import ( add_to_date, + cint, format_time, get_link_to_form, get_url_to_report, @@ -51,14 +52,18 @@ class AutoEmailReport(Document): self.email_to = "\n".join(valid) def validate_report_count(self): - """check that there are only 3 enabled reports per user""" - count = frappe.db.sql( - "select count(*) from `tabAuto Email Report` where user=%s and enabled=1", self.user - )[0][0] - max_reports_per_user = frappe.local.conf.max_reports_per_user or 3 + count = frappe.db.count("Auto Email Report", {"user": self.user, "enabled": 1}) + + max_reports_per_user = ( + cint(frappe.local.conf.max_reports_per_user) # kept for backward compatibilty + or cint(frappe.db.get_single_value("System Settings", "max_auto_email_report_per_user")) + or 20 + ) if count > max_reports_per_user + (-1 if self.flags.in_insert else 0): - frappe.throw(_("Only {0} emailed reports are allowed per user").format(max_reports_per_user)) + msg = _("Only {0} emailed reports are allowed per user.").format(max_reports_per_user) + msg += " " + _("To allow more reports update limit in System Settings.") + frappe.throw(msg, title=_("Report limit reached")) def validate_report_format(self): """check if user has select correct report format""" diff --git a/frappe/hooks.py b/frappe/hooks.py index d3de3877ba..f7a67dc7ec 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -226,7 +226,6 @@ scheduler_events = { "frappe.sessions.clear_expired_sessions", "frappe.email.doctype.notification.notification.trigger_daily_alerts", "frappe.utils.scheduler.restrict_scheduler_events_if_dormant", - "frappe.email.doctype.auto_email_report.auto_email_report.send_daily", "frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.remove_unverified_record", "frappe.desk.form.document_follow.send_daily_updates", "frappe.social.doctype.energy_point_settings.energy_point_settings.allocate_review_points", @@ -241,6 +240,7 @@ scheduler_events = { "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily", "frappe.utils.change_log.check_for_update", "frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_daily", + "frappe.email.doctype.auto_email_report.auto_email_report.send_daily", "frappe.integrations.doctype.google_drive.google_drive.daily_backup", ], "weekly_long": [ From 161fa186607d82e3df042bb2acfc81f9988556b6 Mon Sep 17 00:00:00 2001 From: Komal-Saraf0609 Date: Fri, 22 Apr 2022 14:33:48 +0530 Subject: [PATCH 079/139] test: Corrected selectors --- cypress/integration/control_data.js | 54 ++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/cypress/integration/control_data.js b/cypress/integration/control_data.js index dad2e0ef70..e45af17888 100644 --- a/cypress/integration/control_data.js +++ b/cypress/integration/control_data.js @@ -12,6 +12,7 @@ context('Data Control', () => { "fieldtype": "Data", "options": "Name", "in_list_view": 1, + "reqd": 1, }, { "label": "Email-ID", @@ -19,6 +20,7 @@ context('Data Control', () => { "fieldtype": "Data", "options": "Email", "in_list_view": 1, + "reqd": 1, }, { "label": "Phone No.", @@ -26,79 +28,97 @@ context('Data Control', () => { "fieldtype": "Data", "options": "Phone", "in_list_view": 1, + "reqd": 1, }, ] }); }); }); - it('Verifying data control by inputting different patterns', () => { + it('Verifying data control by inputting different patterns for "Name" field', () => { cy.new_form('Test Data Control'); //Checking the URL for the new form of the doctype cy.location("pathname").should('eq', '/app/test-data-control/new-test-data-control-1'); cy.get('.title-text').should('have.text', 'New Test Data Control'); + cy.get('.frappe-control[data-fieldname="name1"]').find('label').should('have.class','reqd'); + cy.get('.frappe-control[data-fieldname="email"]').find('label').should('have.class','reqd'); + cy.get('.frappe-control[data-fieldname="phone"]').find('label').should('have.class','reqd'); //Checking if the status is "Not Saved" initially cy.get('.indicator-pill').should('have.text', 'Not Saved'); //Inputting data in the field cy.fill_field('name1', '@@###', 'Data'); + cy.fill_field('email', 'test@example.com', 'Data'); + cy.fill_field('phone', '9834280031', 'Data'); //Checking if the border color of the field changes to red - cy.get_field('name1', 'Data').should('have.css', 'border', '1px solid rgb(236, 100, 94)'); + cy.get('.frappe-control[data-fieldname="name1"]').should('have.class', 'has-error'); cy.findByRole('button', {name: 'Save'}).click(); //Checking for the error message cy.get('.modal-title').should('have.text', 'Message'); cy.get('.msgprint').should('have.text', '@@### is not a valid Name'); - cy.reload(); + cy.get('.modal').type('{esc}'); + cy.get_field('name1','Data').clear({force: true}); cy.fill_field('name1', 'Komal{}/!', 'Data'); - cy.get_field('name1', 'Data').should('have.css', 'border', '1px solid rgb(236, 100, 94)'); + cy.get('.frappe-control[data-fieldname="name1"]').should('have.class', 'has-error'); cy.findByRole('button', {name: 'Save'}).click(); cy.get('.modal-title').should('have.text', 'Message'); cy.get('.msgprint').should('have.text', 'Komal{}/! is not a valid Name'); - cy.reload(); + }); + it('Verifying data control by inputting different patterns for "Email" field', () => { + cy.get('.modal-actions > .btn-modal-close').trigger("click"); + cy.get_field('name1','Data').clear({force: true}); cy.fill_field('name1', 'Komal', 'Data'); - cy.get_field('name1', 'Data').should('have.css', 'border', '0px none rgb(152, 161, 169)'); + cy.get_field('email', 'Data').clear({force: true}); cy.fill_field('email', 'komal', 'Data'); - cy.get_field('email', 'Data').should('have.css', 'border', '1px solid rgb(236, 100, 94)'); + cy.get('.frappe-control[data-fieldname="email"]').should('have.class', 'has-error'); cy.findByRole('button', {name: 'Save'}).click(); cy.get('.modal-title').should('have.text', 'Message'); cy.get('.msgprint').should('have.text', 'komal is not a valid Email Address'); - cy.reload(); - + cy.get('.modal-actions > .btn-modal-close').trigger("click"); + cy.get_field('email', 'Data').clear({force: true}); cy.fill_field('email', 'komal@test', 'Data'); - cy.get_field('email', 'Data').should('have.css', 'border', '1px solid rgb(236, 100, 94)'); + cy.get('.frappe-control[data-fieldname="email"]').should('have.class', 'has-error'); cy.findByRole('button', {name: 'Save'}).click(); cy.get('.modal-title').should('have.text', 'Message'); cy.get('.msgprint').should('have.text', 'komal@test is not a valid Email Address'); - cy.reload(); + }); + it('Verifying data control by inputting different patterns for "Phone" field', () => { + cy.get('.modal').type('{esc}'); + cy.get_field('email', 'Data').clear({force: true}); cy.fill_field('email', 'komal@test.com', 'Data'); - cy.get_field('email', 'Data').should('have.css', 'border', '0px none rgb(152, 161, 169)'); - + cy.get_field('phone', 'Data').clear({force: true}); cy.fill_field('phone', 'komal', 'Data'); - cy.get_field('phone', 'Data').should('have.css', 'border', '1px solid rgb(236, 100, 94)'); + cy.get('.frappe-control[data-fieldname="phone"]').should('have.class', 'has-error'); cy.findByRole('button', {name: 'Save'}).click(); cy.get('.modal-title').should('have.text', 'Message'); cy.get('.msgprint').should('have.text', 'komal is not a valid Phone Number'); - cy.reload(); + cy.get('.modal-actions > .btn-modal-close').trigger("click"); + //cy.get('.modal').type('{esc}'); + }); + it('Inputting correct data and saving the doc', () => { //Inputting the data as expected and saving the document + cy.get_field('name1', 'Data').clear({force: true}); + cy.get_field('email', 'Data').clear({force: true}); + cy.get_field('phone', 'Data').clear({force: true}); cy.fill_field('name1', 'Komal', 'Data'); cy.fill_field('email', 'komal@test.com', 'Data'); cy.fill_field('phone', '9432380001', 'Data'); - cy.get_field('phone', 'Data').should('have.css', 'border', '0px none rgb(152, 161, 169)'); cy.findByRole('button', {name: 'Save'}).click(); - //Checking if the fields contains the data which has been filled in cy.location("pathname").should('not.be', '/app/test-data-control/new-test-data-control-1'); cy.get_field('name1').should('have.value', 'Komal'); cy.get_field('email').should('have.value', 'komal@test.com'); cy.get_field('phone').should('have.value', '9432380001'); + }); + it('Deleting the doc', () => { //Deleting the inserted document cy.visit('/app/test-data-control'); cy.get('.list-row-checkbox').eq(0).click(); From a9de2382a90a348e9ff90d45021153825dd26603 Mon Sep 17 00:00:00 2001 From: Komal-Saraf0609 Date: Fri, 22 Apr 2022 14:39:47 +0530 Subject: [PATCH 080/139] test: Fixed sider issues --- cypress/integration/control_data.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cypress/integration/control_data.js b/cypress/integration/control_data.js index e45af17888..d0a563ed56 100644 --- a/cypress/integration/control_data.js +++ b/cypress/integration/control_data.js @@ -40,9 +40,9 @@ context('Data Control', () => { //Checking the URL for the new form of the doctype cy.location("pathname").should('eq', '/app/test-data-control/new-test-data-control-1'); cy.get('.title-text').should('have.text', 'New Test Data Control'); - cy.get('.frappe-control[data-fieldname="name1"]').find('label').should('have.class','reqd'); - cy.get('.frappe-control[data-fieldname="email"]').find('label').should('have.class','reqd'); - cy.get('.frappe-control[data-fieldname="phone"]').find('label').should('have.class','reqd'); + cy.get('.frappe-control[data-fieldname="name1"]').find('label').should('have.class', 'reqd'); + cy.get('.frappe-control[data-fieldname="email"]').find('label').should('have.class', 'reqd'); + cy.get('.frappe-control[data-fieldname="phone"]').find('label').should('have.class', 'reqd'); //Checking if the status is "Not Saved" initially cy.get('.indicator-pill').should('have.text', 'Not Saved'); @@ -61,7 +61,7 @@ context('Data Control', () => { cy.get('.msgprint').should('have.text', '@@### is not a valid Name'); cy.get('.modal').type('{esc}'); - cy.get_field('name1','Data').clear({force: true}); + cy.get_field('name1', 'Data').clear({force: true}); cy.fill_field('name1', 'Komal{}/!', 'Data'); cy.get('.frappe-control[data-fieldname="name1"]').should('have.class', 'has-error'); cy.findByRole('button', {name: 'Save'}).click(); @@ -71,7 +71,7 @@ context('Data Control', () => { it('Verifying data control by inputting different patterns for "Email" field', () => { cy.get('.modal-actions > .btn-modal-close').trigger("click"); - cy.get_field('name1','Data').clear({force: true}); + cy.get_field('name1', 'Data').clear({force: true}); cy.fill_field('name1', 'Komal', 'Data'); cy.get_field('email', 'Data').clear({force: true}); cy.fill_field('email', 'komal', 'Data'); From 8592f0736337afd05de15361a0946b8fb23a6248 Mon Sep 17 00:00:00 2001 From: Komal-Saraf0609 Date: Fri, 22 Apr 2022 15:00:52 +0530 Subject: [PATCH 081/139] test: Corrected selector --- cypress/integration/control_data.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cypress/integration/control_data.js b/cypress/integration/control_data.js index d0a563ed56..804c016cae 100644 --- a/cypress/integration/control_data.js +++ b/cypress/integration/control_data.js @@ -89,17 +89,16 @@ context('Data Control', () => { }); it('Verifying data control by inputting different patterns for "Phone" field', () => { - cy.get('.modal').type('{esc}'); + cy.get('.modal-actions > .btn-modal-close').trigger("click"); cy.get_field('email', 'Data').clear({force: true}); cy.fill_field('email', 'komal@test.com', 'Data'); cy.get_field('phone', 'Data').clear({force: true}); cy.fill_field('phone', 'komal', 'Data'); cy.get('.frappe-control[data-fieldname="phone"]').should('have.class', 'has-error'); - cy.findByRole('button', {name: 'Save'}).click(); + cy.findByRole('button', {name: 'Save'}).click({force: true}); cy.get('.modal-title').should('have.text', 'Message'); cy.get('.msgprint').should('have.text', 'komal is not a valid Phone Number'); cy.get('.modal-actions > .btn-modal-close').trigger("click"); - //cy.get('.modal').type('{esc}'); }); it('Inputting correct data and saving the doc', () => { From e1db9bf653eb63b56ba1814fb0f584b1cf2de858 Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Fri, 22 Apr 2022 15:37:00 +0530 Subject: [PATCH 082/139] fix: Grid layout broken for some grids (#16702) --- frappe/public/js/frappe/form/grid_row.js | 36 +++++++++++++++--------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 4bba8ae7ad..a1c3dce91f 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -289,19 +289,23 @@ export default class GridRow { var me = this; if(this.doc && !this.grid.df.in_place_edit) { // remove row - if(!this.open_form_button) { - this.open_form_button = $(` -
- ${frappe.utils.icon('edit', 'xs')} - -
- `) - .appendTo($('
').appendTo(this.row)) - .on('click', function() { - me.toggle_view(); return false; - }); + if (!this.open_form_button) { + this.open_form_button = $('
').appendTo(this.row); - if(this.is_too_small()) { + if (!this.configure_columns) { + this.open_form_button = $(` +
+ ${frappe.utils.icon('edit', 'xs')} + +
+ `) + .appendTo(this.open_form_button) + .on('click', function() { + me.toggle_view(); return false; + }); + } + + if (this.is_too_small()) { // narrow this.open_form_button.css({'margin-right': '-2px'}); } @@ -310,7 +314,9 @@ export default class GridRow { } add_column_configure_button() { - if (this.configure_columns) { + if (this.grid.df.in_place_edit && !this.frm) return; + + if (this.configure_columns && this.frm) { this.configure_columns_button = $(`
${frappe.utils.icon('setting-gear', 'sm', '', 'filter: opacity(0.5)')} @@ -320,6 +326,10 @@ export default class GridRow { .on('click', () => { this.configure_dialog_for_columns_selector(); }); + } else if (this.configure_columns && !this.frm) { + this.configure_columns_button = $(` +
+ `).appendTo(this.row); } } From 5c3b448c09d0b19bb5f2256d6827ccf7656ca6c4 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Fri, 22 Apr 2022 16:59:03 +0530 Subject: [PATCH 083/139] refactor: used list comprehension --- frappe/tests/ui_test_helpers.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frappe/tests/ui_test_helpers.py b/frappe/tests/ui_test_helpers.py index 4cb3e95a6d..494ba8872d 100644 --- a/frappe/tests/ui_test_helpers.py +++ b/frappe/tests/ui_test_helpers.py @@ -137,12 +137,10 @@ def create_contact_records(): @frappe.whitelist() def create_multiple_todo_records(): - values = [] if frappe.db.get_all("ToDo", {"description": "Multiple ToDo 1"}): return - for index in range(1, 1002): - values.append(("100{}".format(index), "Multiple ToDo {}".format(index))) + values = [("100{}".format(i), "Multiple ToDo {}".format(i)) for i in range(1, 1002)] frappe.db.bulk_insert("ToDo", fields=["name", "description"], values=set(values)) From 7f83178556e6e81690b5d7d94345794035daa399 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Fri, 22 Apr 2022 16:59:22 +0530 Subject: [PATCH 084/139] test: flaky markdown editor fix --- cypress/integration/control_markdown_editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/integration/control_markdown_editor.js b/cypress/integration/control_markdown_editor.js index b527d854d4..128add5397 100644 --- a/cypress/integration/control_markdown_editor.js +++ b/cypress/integration/control_markdown_editor.js @@ -16,7 +16,7 @@ context("Control Markdown Editor", () => { cy.click_modal_primary_button("Upload"); cy.get_field("main_section_md", "Markdown Editor").should( "contain", - "![](/files/sample_image.jpg)" + "![](/files/sample_image" ); }); }); From 8c953b4f74070ec534a42e957b8b075acd852a82 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Fri, 22 Apr 2022 16:59:42 +0530 Subject: [PATCH 085/139] test: removing drag drop test(flaky) --- cypress/integration/kanban.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/cypress/integration/kanban.js b/cypress/integration/kanban.js index c7f3f08336..070af10b0d 100644 --- a/cypress/integration/kanban.js +++ b/cypress/integration/kanban.js @@ -72,14 +72,16 @@ context('Kanban Board', () => { }); - it('Drag todo', () => { - cy.intercept({ - method: 'POST', - url: 'api/method/frappe.desk.doctype.kanban_board.kanban_board.update_order_for_single_card' - }).as('drag-completed'); + // it('Drag todo', () => { + // cy.intercept({ + // method: 'POST', + // url: 'api/method/frappe.desk.doctype.kanban_board.kanban_board.update_order_for_single_card' + // }).as('drag-completed'); - cy.get('.kanban-card-body:first').drag('[data-column-value="Closed"] .kanban-cards', {force: true}); + // cy.get('.kanban-card-body') + // .contains('Test Kanban ToDo').first() + // .drag('[data-column-value="Closed"] .kanban-cards', { force: true }); - cy.wait('@drag-completed'); - }); + // cy.wait('@drag-completed'); + // }); }); \ No newline at end of file From 09a00770795e1296beedb253e7941db3a1fda167 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Fri, 22 Apr 2022 17:13:46 +0530 Subject: [PATCH 086/139] style: fixed indentation --- cypress/integration/kanban.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/integration/kanban.js b/cypress/integration/kanban.js index 070af10b0d..50fc41afe3 100644 --- a/cypress/integration/kanban.js +++ b/cypress/integration/kanban.js @@ -82,6 +82,6 @@ context('Kanban Board', () => { // .contains('Test Kanban ToDo').first() // .drag('[data-column-value="Closed"] .kanban-cards', { force: true }); - // cy.wait('@drag-completed'); + // cy.wait('@drag-completed'); // }); }); \ No newline at end of file From d558f16e714f598502e2897bd873202a2d7ea0cd Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Fri, 22 Apr 2022 17:43:20 +0530 Subject: [PATCH 087/139] test: fixed flaky test for form, timeline_email --- cypress/integration/form.js | 2 +- cypress/integration/timeline_email.js | 4 ++-- cypress/support/commands.js | 7 +++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/cypress/integration/form.js b/cypress/integration/form.js index acaff9a191..99a4336bcb 100644 --- a/cypress/integration/form.js +++ b/cypress/integration/form.js @@ -27,7 +27,7 @@ context('Form', () => { cy.clear_filters(); cy.get('.standard-filter-section [data-fieldname="name"] input').type('Test Form Contact 3').blur(); - cy.click_listview_row_item(0); + cy.click_listview_row_item_with_text('Test Form Contact 3'); cy.get('#page-Contact .page-head').findByTitle('Test Form Contact 3').should('exist'); cy.get('.prev-doc').should('be.visible').click(); diff --git a/cypress/integration/timeline_email.js b/cypress/integration/timeline_email.js index 5808bd52ef..f2a239401d 100644 --- a/cypress/integration/timeline_email.js +++ b/cypress/integration/timeline_email.js @@ -16,7 +16,7 @@ context('Timeline Email', () => { it('Adding email and verifying timeline content for email attachment', () => { cy.visit('/app/todo'); - cy.get('.list-row > .level-left > .list-subject').eq(0).click(); + cy.click_listview_row_item_with_text('Test ToDo'); //Creating a new email cy.get('.timeline-actions > .timeline-item > .action-buttons > .action-btn').click(); @@ -47,7 +47,7 @@ context('Timeline Email', () => { it('Deleting attachment and ToDo', () => { cy.visit('/app/todo'); - cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click(); + cy.click_listview_row_item_with_text('Test ToDo'); //Removing the added attachment cy.get('.attachment-row > .data-pill > .remove-btn > .icon').click(); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 026c622e78..7e2f3116ae 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -352,6 +352,13 @@ Cypress.Commands.add('click_listview_row_item', (row_no) => { cy.get('.list-row > .level-left > .list-subject > .level-item > .ellipsis').eq(row_no).click({force: true}); }); +Cypress.Commands.add('click_listview_row_item_with_text', (text) => { + cy.get('.list-row > .level-left > .list-subject > .level-item > .ellipsis') + .contains(text) + .first() + .click({force: true}); +}); + Cypress.Commands.add('click_filter_button', () => { cy.get('.filter-selector > .btn').click(); }); From 042f98bcc5cc5886e0d0e5ce19483fb760b58cf8 Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Fri, 22 Apr 2022 18:00:44 +0530 Subject: [PATCH 088/139] fix: Workspace Miscellaneous fixes (#16578) --- cypress/integration/workspace.js | 113 ++++++++++++++++-- frappe/desk/doctype/workspace/workspace.py | 29 ++++- frappe/public/js/frappe/form/toolbar.js | 2 +- .../js/frappe/views/workspace/blocks/block.js | 2 +- .../js/frappe/views/workspace/blocks/card.js | 2 +- .../js/frappe/views/workspace/blocks/chart.js | 2 +- .../views/workspace/blocks/onboarding.js | 2 +- .../frappe/views/workspace/blocks/shortcut.js | 2 +- .../public/js/frappe/widgets/widget_dialog.js | 45 +++---- 9 files changed, 159 insertions(+), 40 deletions(-) diff --git a/cypress/integration/workspace.js b/cypress/integration/workspace.js index fbff451305..a12d86b3d6 100644 --- a/cypress/integration/workspace.js +++ b/cypress/integration/workspace.js @@ -2,7 +2,6 @@ context('Workspace 2.0', () => { before(() => { cy.visit('/login'); cy.login(); - cy.visit('/app/website'); }); it('Navigate to page from sidebar', () => { @@ -13,6 +12,11 @@ context('Workspace 2.0', () => { }); it('Create Private Page', () => { + cy.intercept({ + method: 'POST', + url: 'api/method/frappe.desk.doctype.workspace.workspace.new_page' + }).as('new_page'); + cy.get('.codex-editor__redactor .ce-block'); cy.get('.custom-actions button[data-label="Create%20Workspace"]').click(); cy.fill_field('title', 'Test Private Page', 'Data'); @@ -27,12 +31,100 @@ context('Workspace 2.0', () => { cy.wait(300); cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('have.attr', 'item-public', '0'); - cy.wait(500); + cy.wait('@new_page'); + }); + + it('Create Child Page', () => { + cy.intercept({ + method: 'POST', + url: 'api/method/frappe.desk.doctype.workspace.workspace.new_page' + }).as('new_page'); + + cy.get('.codex-editor__redactor .ce-block'); + cy.get('.custom-actions button[data-label="Create%20Workspace"]').click(); + cy.fill_field('title', 'Test Child Page', 'Data'); + cy.fill_field('parent', 'Test Private Page', 'Select'); + cy.fill_field('icon', 'edit', 'Icon'); + cy.get_open_dialog().find('.modal-header').click(); + cy.get_open_dialog().find('.btn-primary').click(); + + // check if sidebar item is added in pubic section + cy.get('.sidebar-item-container[item-name="Test Child Page"]').should('have.attr', 'item-public', '0'); + + cy.get('.standard-actions .btn-primary[data-label="Save"]').click(); + cy.wait(300); + cy.get('.sidebar-item-container[item-name="Test Child Page"]').should('have.attr', 'item-public', '0'); + + cy.wait('@new_page'); + }); + + it('Duplicate Page', () => { + cy.intercept({ + method: 'POST', + url: 'api/method/frappe.desk.doctype.workspace.workspace.duplicate_page' + }).as('page_duplicated'); + cy.get('.codex-editor__redactor .ce-block'); cy.get('.standard-actions .btn-secondary[data-label=Edit]').click(); + + cy.get('.sidebar-item-container[item-name="Test Private Page"]').as('sidebar-item'); + + cy.get('@sidebar-item').find('.standard-sidebar-item').first().click(); + cy.get('@sidebar-item').find('.dropdown-btn').first().click(); + cy.get('@sidebar-item').find('.dropdown-list .dropdown-item').contains('Duplicate').first().click({force: true}); + + cy.get_open_dialog().fill_field('title', 'Duplicate Page', 'Data'); + cy.click_modal_primary_button('Duplicate'); + + cy.wait('@page_duplicated'); + }); + + it('Drag Sidebar Item', () => { + cy.intercept({ + method: 'POST', + url: 'api/method/frappe.desk.doctype.workspace.workspace.sort_pages' + }).as('page_sorted'); + + cy.get('.sidebar-item-container[item-name="Duplicate Page"]').as('sidebar-item'); + + cy.get('@sidebar-item').find('.standard-sidebar-item').first().click(); + cy.get('@sidebar-item').find('.drag-handle').first().move({ deltaX: 0, deltaY: 100 }); + + cy.get('.sidebar-item-container[item-name="Build"]').as('sidebar-item'); + + cy.get('@sidebar-item').find('.standard-sidebar-item').first().click(); + cy.get('@sidebar-item').find('.drag-handle').first().move({ deltaX: 0, deltaY: 100 }); + + cy.wait('@page_sorted'); + }); + + it('Edit Page Detail', () => { + cy.intercept({ + method: 'POST', + url: 'api/method/frappe.desk.doctype.workspace.workspace.update_page' + }).as('page_updated'); + + cy.get('.sidebar-item-container[item-name="Test Private Page"]').as('sidebar-item'); + + cy.get('@sidebar-item').find('.standard-sidebar-item').first().click(); + cy.get('@sidebar-item').find('.dropdown-btn').first().click(); + cy.get('@sidebar-item').find('.dropdown-list .dropdown-item').contains('Edit').first().click({force: true}); + + cy.get_open_dialog().fill_field('title', ' 1', 'Data'); + cy.get_open_dialog().find('input[data-fieldname="is_public"]').check(); + cy.click_modal_primary_button('Update'); + + cy.get('.standard-sidebar-section:first .sidebar-item-container[item-name="Test Private Page"]').should('not.exist'); + cy.get('.standard-sidebar-section:last .sidebar-item-container[item-name="Test Private Page 1"]').should('exist'); + + cy.wait('@page_updated'); }); it('Add New Block', () => { + cy.get('.sidebar-item-container[item-name="Duplicate Page"]').as('sidebar-item'); + + cy.get('@sidebar-item').find('.standard-sidebar-item').first().click(); + cy.get('.ce-block').click().type('{enter}'); cy.get('.block-list-container .block-list-item').contains('Heading').click(); cy.get(":focus").type('Header'); @@ -70,19 +162,24 @@ context('Workspace 2.0', () => { cy.get('.standard-actions .btn-primary[data-label="Save"]').click(); }); - it('Delete Private Page', () => { + it('Delete Duplicate Page', () => { + cy.intercept({ + method: 'POST', + url: 'api/method/frappe.desk.doctype.workspace.workspace.delete_page' + }).as('page_deleted'); + cy.get('.codex-editor__redactor .ce-block'); cy.get('.standard-actions .btn-secondary[data-label=Edit]').click(); - cy.get('.sidebar-item-container[item-name="Test Private Page"]') + cy.get('.sidebar-item-container[item-name="Duplicate Page"]') .find('.sidebar-item-control .setting-btn').click(); - cy.get('.sidebar-item-container[item-name="Test Private Page"]') + cy.get('.sidebar-item-container[item-name="Duplicate Page"]') .find('.dropdown-item[title="Delete Workspace"]').click({force: true}); cy.wait(300); cy.get('.modal-footer > .standard-actions > .btn-modal-primary:visible').first().click(); - cy.get('.standard-actions .btn-primary[data-label="Save"]').click(); - cy.get('.codex-editor__redactor .ce-block'); - cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('not.exist'); + cy.get('.sidebar-item-container[item-name="Duplicate Page"]').should('not.exist'); + + cy.wait('@page_deleted'); }); }); \ No newline at end of file diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index a2dbcbfbe2..284fecbe31 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -80,7 +80,14 @@ class Workspace(Document): # remove duplicate before adding for idx, link in enumerate(self.links): - if link.label == card.get("label") and link.type == "Card Break": + if link.get("label") == card.get("label") and link.get("type") == "Card Break": + # count and set number of links for the card if link_count is 0 + if link.link_count == 0: + for count, card_link in enumerate(self.links[idx + 1 :]): + if card_link.get("type") == "Card Break": + break + link.link_count = count + 1 + del self.links[idx : idx + link.link_count + 1] self.append( @@ -199,21 +206,31 @@ def update_page(name, title, icon, parent, public): doc.sequence_id = frappe.db.count("Workspace", {"public": public}, cache=True) doc.public = public doc.for_user = "" if public else doc.for_user or frappe.session.user - doc.label = "{0}-{1}".format(title, doc.for_user) if doc.for_user else title + doc.label = new_name = "{0}-{1}".format(title, doc.for_user) if doc.for_user else title doc.save(ignore_permissions=True) - if name != doc.label: - rename_doc("Workspace", name, doc.label, force=True, ignore_permissions=True) + if name != new_name: + rename_doc("Workspace", name, new_name, force=True, ignore_permissions=True) # update new name and public in child pages if child_docs: for child in child_docs: child_doc = frappe.get_doc("Workspace", child.name) child_doc.parent_page = doc.title - child_doc.public = doc.public + if child_doc.public != public: + child_doc.public = public + child_doc.for_user = "" if public else child_doc.for_user or frappe.session.user + child_doc.label = new_child_name = ( + "{0}-{1}".format(child_doc.title, child_doc.for_user) + if child_doc.for_user + else child_doc.title + ) child_doc.save(ignore_permissions=True) - return {"name": doc.title, "public": doc.public, "label": doc.label} + if child.name != new_child_name: + rename_doc("Workspace", child.name, new_child_name, force=True, ignore_permissions=True) + + return {"name": title, "public": public, "label": new_name} @frappe.whitelist() diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index e55eb9fdeb..6841640341 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -323,7 +323,7 @@ frappe.ui.form.Toolbar = class Toolbar { } // New - if(p[CREATE] && !this.frm.meta.issingle) { + if (p[CREATE] && !this.frm.meta.issingle && !this.frm.meta.in_create) { this.page.add_menu_item(__("New {0}", [__(me.frm.doctype)]), function() { frappe.new_doc(me.frm.doctype, true); }, true, { diff --git a/frappe/public/js/frappe/views/workspace/blocks/block.js b/frappe/public/js/frappe/views/workspace/blocks/block.js index 9605d30c81..1df6b707fe 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/block.js +++ b/frappe/public/js/frappe/views/workspace/blocks/block.js @@ -7,7 +7,7 @@ export default class Block { make(block, block_name, widget_type = block) { let block_data = this.config.page_data[block+'s'].items.find(obj => { - return frappe.utils.unescape_html(obj.label) == frappe.utils.unescape_html(block_name); + return frappe.utils.unescape_html(obj.label) == frappe.utils.unescape_html(__(block_name)); }); if (!block_data) return false; this.wrapper.innerHTML = ''; diff --git a/frappe/public/js/frappe/views/workspace/blocks/card.js b/frappe/public/js/frappe/views/workspace/blocks/card.js index 9ce6ce8b4d..4b46b12890 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/card.js +++ b/frappe/public/js/frappe/views/workspace/blocks/card.js @@ -31,7 +31,7 @@ export default class Card extends Block { this.new('card', 'links'); if (this.data && this.data.card_name) { - let has_data = this.make('card', __(this.data.card_name), 'links'); + let has_data = this.make('card', this.data.card_name, 'links'); if (!has_data) return; } diff --git a/frappe/public/js/frappe/views/workspace/blocks/chart.js b/frappe/public/js/frappe/views/workspace/blocks/chart.js index ccef1fa15f..cb688f48ed 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/chart.js +++ b/frappe/public/js/frappe/views/workspace/blocks/chart.js @@ -32,7 +32,7 @@ export default class Chart extends Block { this.new('chart'); if (this.data && this.data.chart_name) { - let has_data = this.make('chart', __(this.data.chart_name)); + let has_data = this.make('chart', this.data.chart_name); if (!has_data) return; } diff --git a/frappe/public/js/frappe/views/workspace/blocks/onboarding.js b/frappe/public/js/frappe/views/workspace/blocks/onboarding.js index c0ba529853..c76141996f 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/onboarding.js +++ b/frappe/public/js/frappe/views/workspace/blocks/onboarding.js @@ -73,7 +73,7 @@ export default class Onboarding extends Block { make(block, block_name) { let block_data = this.config.page_data['onboardings'].items.find(obj => { - return obj.label == block_name; + return obj.label == __(block_name); }); if (!block_data) return false; this.wrapper.innerHTML = ''; diff --git a/frappe/public/js/frappe/views/workspace/blocks/shortcut.js b/frappe/public/js/frappe/views/workspace/blocks/shortcut.js index 2be5da0d4b..ef9bfa8cf9 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/shortcut.js +++ b/frappe/public/js/frappe/views/workspace/blocks/shortcut.js @@ -51,7 +51,7 @@ export default class Shortcut extends Block { this.new('shortcut'); if (this.data && this.data.shortcut_name) { - let has_data = this.make('shortcut', __(this.data.shortcut_name)); + let has_data = this.make('shortcut', this.data.shortcut_name); if (!has_data) return; } diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js index d1ba75227b..bba29ffaf9 100644 --- a/frappe/public/js/frappe/widgets/widget_dialog.js +++ b/frappe/public/js/frappe/widgets/widget_dialog.js @@ -228,30 +228,35 @@ class CardDialog extends WidgetDialog { } process_data(data) { - data.links.map((item, idx) => { - let message = ''; - let row = idx+1; + let message = ''; - if (!item.link_type) { - message = "Following fields have missing values:

    "; - message += `
  • Link Type in Row ${row}
  • `; - } + if (!data.links) { + message = "You must add atleast one link."; + } else { + data.links.map((item, idx) => { + let row = idx+1; - if (!item.link_to) { - message += `
  • Link To in Row ${row}
  • `; - } + if (!item.link_type) { + message = "Following fields have missing values:

      "; + message += `
    • Link Type in Row ${row}
    • `; + } - if (message) { - message += "
    "; - frappe.throw({ - message: __(message), - title: __("Missing Values Required"), - indicator: 'orange' - }); - } + if (!item.link_to) { + message += `
  • Link To in Row ${row}
  • `; + } - item.label = item.label ? item.label : item.link_to; - }); + item.label = item.label ? item.label : item.link_to; + }); + } + + if (message) { + message += "
"; + frappe.throw({ + message: __(message), + title: __("Missing Values Required"), + indicator: 'orange' + }); + } data.label = data.label ? data.label : data.chart_name; return data; From 6ac1d955843adbde8169824d06fc91c10ca82110 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 15 Apr 2022 11:37:50 +0530 Subject: [PATCH 089/139] test: fix badly written tests --- frappe/tests/test_permissions.py | 33 ++++++++++--------- .../doctype/blog_post/test_blog_post.py | 6 ++-- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/frappe/tests/test_permissions.py b/frappe/tests/test_permissions.py index 70297a4f54..4164b0be36 100644 --- a/frappe/tests/test_permissions.py +++ b/frappe/tests/test_permissions.py @@ -24,25 +24,26 @@ test_dependencies = ["Blogger", "Blog Post", "User", "Contact", "Salutation"] class TestPermissions(FrappeTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + frappe.clear_cache(doctype="Blog Post") + user = frappe.get_doc("User", "test1@example.com") + user.add_roles("Website Manager") + user.add_roles("System Manager") + + user = frappe.get_doc("User", "test2@example.com") + user.add_roles("Blogger") + + user = frappe.get_doc("User", "test3@example.com") + user.add_roles("Sales User") + + user = frappe.get_doc("User", "testperm@example.com") + user.add_roles("Website Manager") + def setUp(self): frappe.clear_cache(doctype="Blog Post") - if not frappe.flags.permission_user_setup_done: - user = frappe.get_doc("User", "test1@example.com") - user.add_roles("Website Manager") - user.add_roles("System Manager") - - user = frappe.get_doc("User", "test2@example.com") - user.add_roles("Blogger") - - user = frappe.get_doc("User", "test3@example.com") - user.add_roles("Sales User") - - user = frappe.get_doc("User", "testperm@example.com") - user.add_roles("Website Manager") - - frappe.flags.permission_user_setup_done = True - reset("Blogger") reset("Blog Post") diff --git a/frappe/website/doctype/blog_post/test_blog_post.py b/frappe/website/doctype/blog_post/test_blog_post.py index 0eddad4bfe..558795458b 100644 --- a/frappe/website/doctype/blog_post/test_blog_post.py +++ b/frappe/website/doctype/blog_post/test_blog_post.py @@ -1,12 +1,12 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE import re -import unittest from bs4 import BeautifulSoup import frappe from frappe.custom.doctype.customize_form.customize_form import reset_customization +from frappe.tests.utils import FrappeTestCase from frappe.utils import random_string, set_request from frappe.website.doctype.blog_post.blog_post import get_blog_list from frappe.website.serve import get_response @@ -16,7 +16,7 @@ from frappe.website.website_generator import WebsiteGenerator test_dependencies = ["Blog Post"] -class TestBlogPost(unittest.TestCase): +class TestBlogPost(FrappeTestCase): def setUp(self): reset_customization("Blog Post") @@ -61,7 +61,7 @@ class TestBlogPost(unittest.TestCase): category_page_link = list(soup.find_all("a", href=re.compile(blog.blog_category)))[0] category_page_url = category_page_link["href"] - cached_value = frappe.db.value_cache[("DocType", "Blog Post", "name")] + cached_value = frappe.db.value_cache.get(("DocType", "Blog Post", "name")) frappe.db.value_cache[("DocType", "Blog Post", "name")] = (("Blog Post",),) # Visit the category page (by following the link found in above stage) From f748ae85fc8f51b4c4dcf9029f452703aa238b7a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 15 Apr 2022 11:48:13 +0530 Subject: [PATCH 090/139] fix: set docstatus to 0 if None present --- frappe/model/base_document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index f8d60d0763..fd8bb8e71d 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -348,7 +348,7 @@ class BaseDocument(object): @property def docstatus(self): - return DocStatus(self.get("docstatus")) + return DocStatus(cint(self.get("docstatus"))) @docstatus.setter def docstatus(self, value): From 8a0a5c54da9caa0fc3323a90415d9e8e045360cc Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 15 Apr 2022 11:56:34 +0530 Subject: [PATCH 091/139] test!: dont autocommit on test object recreation --- frappe/parallel_test_runner.py | 6 +++--- frappe/test_runner.py | 29 ++++++++++++++++------------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/frappe/parallel_test_runner.py b/frappe/parallel_test_runner.py index 65c8eb470b..4fd03773ef 100644 --- a/frappe/parallel_test_runner.py +++ b/frappe/parallel_test_runner.py @@ -46,7 +46,7 @@ class ParallelTestRunner: if hasattr(test_module, "global_test_dependencies"): for doctype in test_module.global_test_dependencies: - make_test_records(doctype) + make_test_records(doctype, commit=True) elapsed = time.time() - start_time elapsed = click.style(f" ({elapsed:.03}s)", fg="red") @@ -76,7 +76,7 @@ class ParallelTestRunner: def create_test_dependency_records(self, module, path, filename): if hasattr(module, "test_dependencies"): for doctype in module.test_dependencies: - make_test_records(doctype) + make_test_records(doctype, commit=True) if os.path.basename(os.path.dirname(path)) == "doctype": # test_data_migration_connector.py > data_migration_connector.json @@ -86,7 +86,7 @@ class ParallelTestRunner: with open(test_record_file_path, "r") as f: doc = json.loads(f.read()) doctype = doc["name"] - make_test_records(doctype) + make_test_records(doctype, commit=True) def get_module(self, path, filename): app_path = frappe.get_pymodule_path(self.app) diff --git a/frappe/test_runner.py b/frappe/test_runner.py index 509be36f86..96feac532f 100644 --- a/frappe/test_runner.py +++ b/frappe/test_runner.py @@ -226,7 +226,7 @@ def run_tests_for_doctype( if force: for name in frappe.db.sql_list("select name from `tab%s`" % doctype): frappe.delete_doc(doctype, name, force=True) - make_test_records(doctype, verbose=verbose, force=force) + make_test_records(doctype, verbose=verbose, force=force, commit=True) modules.append(importlib.import_module(test_module)) return _run_unittest( @@ -245,7 +245,7 @@ def run_tests_for_module( module = importlib.import_module(module) if hasattr(module, "test_dependencies"): for doctype in module.test_dependencies: - make_test_records(doctype, verbose=verbose) + make_test_records(doctype, verbose=verbose, commit=True) frappe.db.commit() return _run_unittest( @@ -330,7 +330,7 @@ def _add_test(app, path, filename, verbose, test_suite=None, ui_tests=False): if hasattr(module, "test_dependencies"): for doctype in module.test_dependencies: - make_test_records(doctype, verbose=verbose) + make_test_records(doctype, verbose=verbose, commit=True) is_ui_test = True if hasattr(module, "TestDriver") else False @@ -346,12 +346,12 @@ def _add_test(app, path, filename, verbose, test_suite=None, ui_tests=False): with open(txt_file, "r") as f: doc = json.loads(f.read()) doctype = doc["name"] - make_test_records(doctype, verbose) + make_test_records(doctype, verbose, commit=True) test_suite.addTest(unittest.TestLoader().loadTestsFromModule(module)) -def make_test_records(doctype, verbose=0, force=False): +def make_test_records(doctype, verbose=0, force=False, commit=False): if not frappe.db: frappe.connect() @@ -364,8 +364,8 @@ def make_test_records(doctype, verbose=0, force=False): if options not in frappe.local.test_objects: frappe.local.test_objects[options] = [] - make_test_records(options, verbose, force) - make_test_records_for_doctype(options, verbose, force) + make_test_records(options, verbose, force, commit=commit) + make_test_records_for_doctype(options, verbose, force, commit=commit) def get_modules(doctype): @@ -405,7 +405,7 @@ def get_dependencies(doctype): return options_list -def make_test_records_for_doctype(doctype, verbose=0, force=False): +def make_test_records_for_doctype(doctype, verbose=0, force=False, commit=False): if not force and doctype in get_test_record_log(): return @@ -420,17 +420,19 @@ def make_test_records_for_doctype(doctype, verbose=0, force=False): elif hasattr(test_module, "test_records"): if doctype in frappe.local.test_objects: frappe.local.test_objects[doctype] += make_test_objects( - doctype, test_module.test_records, verbose, force + doctype, test_module.test_records, verbose, force, commit=commit ) else: frappe.local.test_objects[doctype] = make_test_objects( - doctype, test_module.test_records, verbose, force + doctype, test_module.test_records, verbose, force, commit=commit ) else: test_records = frappe.get_test_records(doctype) if test_records: - frappe.local.test_objects[doctype] += make_test_objects(doctype, test_records, verbose, force) + frappe.local.test_objects[doctype] += make_test_objects( + doctype, test_records, verbose, force, commit=commit + ) elif verbose: print_mandatory_fields(doctype) @@ -438,7 +440,7 @@ def make_test_records_for_doctype(doctype, verbose=0, force=False): add_to_test_record_log(doctype) -def make_test_objects(doctype, test_records=None, verbose=None, reset=False): +def make_test_objects(doctype, test_records=None, verbose=None, reset=False, commit=False): """Make test objects from given list of `test_records` or from `test_records.json`""" records = [] @@ -495,7 +497,8 @@ def make_test_objects(doctype, test_records=None, verbose=None, reset=False): records.append(d.name) - frappe.db.commit() + if commit: + frappe.db.commit() return records From 7274014b3b453a537d7bafc6c6fc37725fd143d0 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 22 Mar 2022 17:22:27 +0530 Subject: [PATCH 092/139] test: improve FrappeTestCase - discard state after test finishes - add assertDocumentEqual for quick document check - add commit "watcher" to find commits during tests - add tests for tests. who watches the watchmen? --- frappe/tests/test_test_utils.py | 34 +++++++++++++++ frappe/tests/utils.py | 77 ++++++++++++++++++++++++++++++--- 2 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 frappe/tests/test_test_utils.py diff --git a/frappe/tests/test_test_utils.py b/frappe/tests/test_test_utils.py new file mode 100644 index 0000000000..4e5c424ca6 --- /dev/null +++ b/frappe/tests/test_test_utils.py @@ -0,0 +1,34 @@ +import frappe +from frappe.tests.utils import FrappeTestCase, change_settings + + +class TestTestUtils(FrappeTestCase): + SHOW_TRANSACTION_COMMIT_WARNINGS = True + + def test_document_assertions(self): + + currency = frappe.new_doc("Currency") + currency.currency_name = "STONKS" + currency.smallest_currency_fraction_value = 0.420_001 + currency.save() + + self.assertDocumentEqual(currency.as_dict(), currency) + + def test_thread_locals(self): + frappe.flags.temp_flag_to_be_discarded = True + + def test_temp_setting_changes(self): + current_setting = frappe.get_system_settings("logout_on_password_reset") + + with change_settings("System Settings", {"logout_on_password_reset": int(not current_setting)}): + updated_settings = frappe.get_system_settings("logout_on_password_reset") + self.assertNotEqual(current_setting, updated_settings) + + restored_settings = frappe.get_system_settings("logout_on_password_reset") + self.assertEqual(current_setting, restored_settings) + + +def tearDownModule(): + """assertions for ensuring tests didn't leave state behind""" + assert "temp_flag_to_be_discarded" not in frappe.flags + assert not frappe.db.exists("Currency", "STONKS") diff --git a/frappe/tests/utils.py b/frappe/tests/utils.py index bad368afd0..d5c25db26a 100644 --- a/frappe/tests/utils.py +++ b/frappe/tests/utils.py @@ -1,23 +1,86 @@ import copy +import datetime import signal import unittest from contextlib import contextmanager import frappe +from frappe.model.base_document import BaseDocument +from frappe.utils import cint + +datetime_like_types = (datetime.datetime, datetime.date, datetime.time, datetime.timedelta) class FrappeTestCase(unittest.TestCase): """Base test class for Frappe tests.""" - @classmethod - def setUpClass(cls) -> None: - frappe.db.commit() - return super().setUpClass() + SHOW_TRANSACTION_COMMIT_WARNINGS = False @classmethod - def tearDownClass(cls) -> None: - frappe.db.rollback() - return super().tearDownClass() + def setUpClass(cls) -> None: + # flush changes done so far to avoid flake + frappe.db.commit() + if cls.SHOW_TRANSACTION_COMMIT_WARNINGS: + frappe.db.add_before_commit(_commit_watcher) + + # enqueue teardown actions (executed in LIFO order) + cls.addClassCleanup(_restore_thread_locals, copy.deepcopy(frappe.local.flags)) + cls.addClassCleanup(_rollback_db) + + return super().setUpClass() + + # --- Frappe Framework specific assertions + def assertDocumentEqual(self, expected, actual): + """Compare a (partial) expected document with actual Document.""" + + if isinstance(expected, BaseDocument): + expected = expected.as_dict() + + for field, value in expected.items(): + if isinstance(value, list): + actual_child_docs = actual.get(field) + self.assertEqual(len(value), len(actual_child_docs), msg=f"{field} length should be same") + for exp_child, actual_child in zip(value, actual_child_docs): + self.assertDocumentEqual(exp_child, actual_child) + else: + self._compare_field(value, actual.get(field), actual, field) + + def _compare_field(self, expected, actual, doc, field): + msg = f"{field} should be same." + + if isinstance(expected, float): + precision = doc.precision(field) + self.assertAlmostEqual(expected, actual, f"{field} should be same to {precision} digits") + elif isinstance(expected, (bool, int)): + self.assertEqual(expected, cint(actual), msg=msg) + elif isinstance(expected, datetime_like_types): + self.assertEqual(str(expected), str(actual), msg=msg) + else: + self.assertEqual(expected, actual, msg=msg) + + +def _commit_watcher(): + import traceback + + print("Warning:, transaction committed during tests.") + traceback.print_stack(limit=5) + + +def _rollback_db(): + frappe.local.before_commit = [] + frappe.local.rollback_observers = [] + frappe.db.value_cache = {} + frappe.db.rollback() + + +def _restore_thread_locals(flags): + frappe.local.flags = flags + frappe.local.error_log = [] + frappe.local.message_log = [] + frappe.local.debug_log = [] + frappe.local.realtime_log = [] + frappe.local.conf = frappe._dict(frappe.get_site_config()) + frappe.local.cache = {} @contextmanager From fa7ea4bce8604f3835f0831a9e952ae66832bf73 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Sat, 23 Apr 2022 09:01:20 +0530 Subject: [PATCH 093/139] perf(BaseDocument): ~50% faster `as_dict` (#16549) Co-authored-by: Pruthvi Patel --- frappe/model/__init__.py | 3 ++ frappe/model/base_document.py | 75 +++++++++++++++++------------------ 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index bd607e7119..570df4dab8 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -40,6 +40,9 @@ data_fieldtypes = ( "JSON", ) +float_like_fields = {"Float", "Currency", "Percent"} +datetime_fields = {"Datetime", "Date", "Time"} + attachment_fieldtypes = ( "Attach", "Attach Image", diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index f8d60d0763..93446fb99e 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -6,7 +6,14 @@ from typing import Dict, List import frappe from frappe import _ -from frappe.model import child_table_fields, default_fields, display_fieldtypes, table_fields +from frappe.model import ( + child_table_fields, + datetime_fields, + default_fields, + display_fieldtypes, + float_like_fields, + table_fields, +) from frappe.model.docstatus import DocStatus from frappe.model.naming import set_new_name from frappe.model.utils.link_count import notify_link_count @@ -256,7 +263,8 @@ class BaseDocument(object): ) -> Dict: d = frappe._dict() for fieldname in self.meta.get_valid_columns(): - d[fieldname] = self.get(fieldname) + # column is valid, we can use getattr + d[fieldname] = getattr(self, fieldname, None) # if no need for sanitization and value is None, continue if not sanitize and d[fieldname] is None: @@ -264,25 +272,24 @@ class BaseDocument(object): df = self.meta.get_field(fieldname) - if df and df.get("is_virtual"): - if ignore_virtual: - del d[fieldname] - continue + if df: + if getattr(df, "is_virtual", False): + if ignore_virtual: + del d[fieldname] + continue - from frappe.utils.safe_exec import get_safe_globals + if d[fieldname] is None and (options := getattr(df, "options", None)): + from frappe.utils.safe_exec import get_safe_globals - if d[fieldname] is None: - if df.get("options"): d[fieldname] = frappe.safe_eval( - code=df.get("options"), + code=options, eval_globals=get_safe_globals(), eval_locals={"doc": self}, ) - else: - _val = getattr(self, fieldname, None) - if _val and not callable(_val): - d[fieldname] = _val - elif df: + + if isinstance(d[fieldname], list) and df.fieldtype not in table_fields: + frappe.throw(_("Value for {0} cannot be a list").format(_(df.label))) + if df.fieldtype == "Check": d[fieldname] = 1 if cint(d[fieldname]) else 0 @@ -292,25 +299,20 @@ class BaseDocument(object): elif df.fieldtype == "JSON" and isinstance(d[fieldname], dict): d[fieldname] = json.dumps(d[fieldname], sort_keys=True, indent=4, separators=(",", ": ")) - elif df.fieldtype in ("Currency", "Float", "Percent") and not isinstance(d[fieldname], float): + elif df.fieldtype in float_like_fields and not isinstance(d[fieldname], float): d[fieldname] = flt(d[fieldname]) - elif df.fieldtype in ("Datetime", "Date", "Time") and d[fieldname] == "": + elif (df.fieldtype in datetime_fields and d[fieldname] == "") or ( + getattr(df, "unique", False) and cstr(d[fieldname]).strip() == "" + ): d[fieldname] = None - elif df.get("unique") and cstr(d[fieldname]).strip() == "": - # unique empty field should be set to None - d[fieldname] = None - - if isinstance(d[fieldname], list) and df.fieldtype not in table_fields: - frappe.throw(_("Value for {0} cannot be a list").format(_(df.label))) - if convert_dates_to_str and isinstance( d[fieldname], (datetime.datetime, datetime.date, datetime.time, datetime.timedelta) ): d[fieldname] = str(d[fieldname]) - if d[fieldname] is None and ignore_nulls: + if ignore_nulls and d[fieldname] is None: del d[fieldname] return d @@ -361,7 +363,7 @@ class BaseDocument(object): convert_dates_to_str=False, no_child_table_fields=False, ) -> Dict: - doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str) + doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str, ignore_nulls=no_nulls) doc["doctype"] = self.doctype for df in self.meta.get_table_fields(): @@ -376,20 +378,15 @@ class BaseDocument(object): for d in children ] - if no_nulls: - for k in list(doc): - if doc[k] is None: - del doc[k] - if no_default_fields: - for k in list(doc): - if k in default_fields: - del doc[k] + for key in default_fields: + if key in doc: + del doc[key] if no_child_table_fields: - for k in list(doc): - if k in child_table_fields: - del doc[k] + for key in child_table_fields: + if key in doc: + del doc[key] for key in ( "_user_tags", @@ -399,8 +396,8 @@ class BaseDocument(object): "__run_link_triggers", "__unsaved", ): - if self.get(key): - doc[key] = self.get(key) + if value := getattr(self, key, None): + doc[key] = value return doc From 1fa60ba3e729b5e006b592c53a94c3466eca8b05 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 23 Apr 2022 13:08:02 +0530 Subject: [PATCH 094/139] test: explicitly start transaction Postgres for some reason is going in autocommit mode if transaction isn't started with BEGIN... will fix it separately. --- frappe/tests/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/tests/utils.py b/frappe/tests/utils.py index d5c25db26a..7d00a0c1f9 100644 --- a/frappe/tests/utils.py +++ b/frappe/tests/utils.py @@ -20,6 +20,7 @@ class FrappeTestCase(unittest.TestCase): def setUpClass(cls) -> None: # flush changes done so far to avoid flake frappe.db.commit() + frappe.db.begin() if cls.SHOW_TRANSACTION_COMMIT_WARNINGS: frappe.db.add_before_commit(_commit_watcher) From 95816f03408fe85059a7d1b665853cb33229e179 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Sat, 23 Apr 2022 17:59:54 +0530 Subject: [PATCH 095/139] fix: better validation for child insert --- frappe/client.py | 5 ++++- frappe/tests/test_client.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/frappe/client.py b/frappe/client.py index a8223cdeee..1bad2bed2f 100644 --- a/frappe/client.py +++ b/frappe/client.py @@ -189,7 +189,10 @@ def insert(doc=None): if isinstance(doc, str): doc = json.loads(doc) - if doc.get("parenttype"): + doc = frappe._dict(doc) + if frappe.is_table(doc.doctype): + if not (doc.parenttype and doc.parent and doc.parentfield): + frappe.throw(_("parenttype, parent and parentfield are required to insert a child record")) # inserting a child record parent = frappe.get_doc(doc.parenttype, doc.parent) parent.append(doc.parentfield, doc) diff --git a/frappe/tests/test_client.py b/frappe/tests/test_client.py index c86c482651..677f59a366 100644 --- a/frappe/tests/test_client.py +++ b/frappe/tests/test_client.py @@ -141,3 +141,40 @@ class TestClient(unittest.TestCase): self.assertEqual(get("ToDo", filters=filters_json).description, "test") todo.delete() + + def test_client_insert(self): + from frappe.client import insert + + def get_random_title(): + return "test-{0}".format(frappe.generate_hash()) + + # test insert dict + doc = {"doctype": "Note", "title": get_random_title(), "content": "test"} + note1 = insert(doc) + self.assertTrue(note1) + + # test insert json + doc["title"] = get_random_title() + json_doc = frappe.as_json(doc) + note2 = insert(json_doc) + self.assertTrue(note2) + + # test insert child doc without parent fields + child_doc = {"doctype": "Note Seen By", "user": "Administrator"} + with self.assertRaises(frappe.ValidationError): + insert(child_doc) + + # test insert child doc with parent fields + child_doc = { + "doctype": "Note Seen By", + "user": "Administrator", + "parenttype": "Note", + "parent": note1.name, + "parentfield": "seen_by", + } + note3 = insert(child_doc) + self.assertTrue(note3) + + # cleanup + frappe.delete_doc("Note", note1.name) + frappe.delete_doc("Note", note2.name) From 2767ea9d255b9f8c8a2e3840cf7b2df50ce36b56 Mon Sep 17 00:00:00 2001 From: Nikhil Kothari Date: Mon, 25 Apr 2022 15:07:04 +0530 Subject: [PATCH 096/139] feat: more color options for form alerts (#16729) * Added green and orange color options for message --- frappe/public/js/frappe/form/layout.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 578956f0ca..403abf0981 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -98,7 +98,7 @@ frappe.ui.form.Layout = class Layout { // remove previous color this.message.removeClass(this.message_color); } - this.message_color = (color && ['yellow', 'blue', 'red'].includes(color)) ? color : 'blue'; + this.message_color = (color && ['yellow', 'blue', 'red', 'green', 'orange'].includes(color)) ? color : 'blue'; if (html) { if (html.substr(0, 1)!=='<') { // wrap in a block @@ -439,7 +439,7 @@ frappe.ui.form.Layout = class Layout { } handle_tab(doctype, fieldname, shift) { - let grid_row = null, + let grid_row = null, prev = null, fields = this.fields_list, focused = false; From 4418c032371116ed9488f6e770aeda55bdc509d6 Mon Sep 17 00:00:00 2001 From: phot0n Date: Mon, 25 Apr 2022 20:13:10 +0530 Subject: [PATCH 097/139] fix: remove default null definition for name column in tabSeries for postgres --- frappe/database/postgres/framework_postgres.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql index 1e79bf67d8..2cae3ab82f 100644 --- a/frappe/database/postgres/framework_postgres.sql +++ b/frappe/database/postgres/framework_postgres.sql @@ -240,7 +240,7 @@ CREATE TABLE "tabDocType" ( DROP TABLE IF EXISTS "tabSeries"; CREATE TABLE "tabSeries" ( - "name" varchar(100) DEFAULT NULL, + "name" varchar(100), "current" bigint NOT NULL DEFAULT 0, PRIMARY KEY ("name") ) ; From 59ee952d82548b1616ce79349487bd4210dd5863 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 26 Apr 2022 12:53:44 +0530 Subject: [PATCH 098/139] chore: failing semantic release --- .github/workflows/release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a6c1243f64..9c7ecf989e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,6 @@ jobs: npm install @semantic-release/git @semantic-release/exec --no-save - name: Create Release env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} GIT_AUTHOR_NAME: "Frappe PR Bot" From e0c89cdc7375092e150bfbbbd61288cc7099ae7c Mon Sep 17 00:00:00 2001 From: Shadrak Gurupnor <30501401+shadrak98@users.noreply.github.com> Date: Tue, 26 Apr 2022 15:17:19 +0530 Subject: [PATCH 099/139] fix: properly validate google sheets url (#16683) * fix: properly validate google sheets url * fix: check for spreadsheets in path * fix(minor): error should throw if any of the cond fails --- frappe/utils/csvutils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/utils/csvutils.py b/frappe/utils/csvutils.py index 547372778b..b61e209b72 100644 --- a/frappe/utils/csvutils.py +++ b/frappe/utils/csvutils.py @@ -215,7 +215,10 @@ def get_csv_content_from_google_sheets(url): def validate_google_sheets_url(url): - if "docs.google.com/spreadsheets" not in url: + from urllib.parse import urlparse + + u = urlparse(url) + if u.scheme != "https" or u.netloc != "docs.google.com" or "/spreadsheets/" not in u.path: frappe.throw( _('"{0}" is not a valid Google Sheets URL').format(url), title=_("Invalid URL"), From e2d3d1d0be53fd09409febf038159805c27ec5b3 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Tue, 26 Apr 2022 15:32:30 +0530 Subject: [PATCH 100/139] feat: provision to handle payment authorization event in server script for custom documents (#16712) Currently, there is no provision to handle payment authorization events via server script. So it's not possible if a user wants to link payments against custom documents. Thus adding a provision in server script - Setup checkout for custom doc Screenshot 2022-03-11 at 2 44 19 PM - Handle payment callback Screenshot 2022-04-22 at 11 28 19 AM ## Documentation https://frappeframework.com/docs/v13/user/en/desk/scripting/server-script/edit?wiki_page_patch=bbed0fcd9a --- frappe/core/doctype/server_script/server_script.json | 4 ++-- frappe/core/doctype/server_script/server_script_utils.py | 1 + frappe/utils/safe_exec.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index 548d21bb60..700a97cfeb 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -49,7 +49,7 @@ "fieldname": "doctype_event", "fieldtype": "Select", "label": "DocType Event", - "options": "Before Insert\nBefore Validate\nBefore Save\nAfter Insert\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)" + "options": "Before Insert\nBefore Validate\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)\nOn Payment Authorization" }, { "depends_on": "eval:doc.script_type==='API'", @@ -109,7 +109,7 @@ "link_fieldname": "server_script" } ], - "modified": "2022-04-07 19:41:23.178772", + "modified": "2022-04-22 11:24:01.151662", "modified_by": "Administrator", "module": "Core", "name": "Server Script", diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index 5300baa199..b807b43d10 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -17,6 +17,7 @@ EVENT_MAP = { "after_delete": "After Delete", "before_update_after_submit": "Before Save (Submitted Document)", "on_update_after_submit": "After Save (Submitted Document)", + "on_payment_authorized": "On Payment Authorization", } diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index fc53243021..30cf38bcf9 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -146,6 +146,7 @@ def get_safe_globals(): ), make_get_request=frappe.integrations.utils.make_get_request, make_post_request=frappe.integrations.utils.make_post_request, + get_payment_gateway_controller=frappe.integrations.utils.get_payment_gateway_controller, socketio_port=frappe.conf.socketio_port, get_hooks=get_hooks, enqueue=safe_enqueue, From 57e379ac6815af872449585ba96e08333fcc8bfa Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Tue, 26 Apr 2022 15:48:06 +0530 Subject: [PATCH 101/139] fix: Newsletter unsubscribe button not working (#16756) --- frappe/www/unsubscribe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/www/unsubscribe.py b/frappe/www/unsubscribe.py index bae54f740d..d679bb3319 100644 --- a/frappe/www/unsubscribe.py +++ b/frappe/www/unsubscribe.py @@ -8,7 +8,7 @@ no_cache = True def get_context(context): frappe.flags.ignore_permissions = True # Called for confirmation. - if "email" in frappe.form_dict: + if "email" in frappe.form_dict and frappe.request.method == "GET": if verify_request(): user_email = frappe.form_dict["email"] context.email = user_email @@ -18,7 +18,7 @@ def get_context(context): context.status = "waiting_for_confirmation" # Called when form is submitted. - elif "user_email" in frappe.form_dict: + elif "user_email" in frappe.form_dict and frappe.request.method == "POST": context.status = "unsubscribed" email = frappe.form_dict["user_email"] email_group = get_email_groups(email) From 224dd319e890c25a7531ef8e66616254bc1b762b Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Tue, 26 Apr 2022 15:52:03 +0530 Subject: [PATCH 102/139] perf(BaseDocument): remove duplicate code (#16733) --- frappe/model/base_document.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index b236bce1f3..186ef52c12 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -241,7 +241,6 @@ class BaseDocument(object): raise AttributeError(key) value = get_controller(value["doctype"])(value) - value.init_valid_columns() value.parent = self.name value.parenttype = self.doctype From 5d61482fb35cba4dba8032aa6c14092e28bcff88 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Tue, 26 Apr 2022 12:30:46 +0200 Subject: [PATCH 103/139] fix: Replace password with asterisks before logging (#16743) --- frappe/utils/backups.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index e2579444bd..927ae9c2db 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -461,7 +461,7 @@ class BackupGenerator: ) if self.verbose: - print(command + "\n") + print(command.replace(args.password, "*" * 10) + "\n") frappe.utils.execute_in_shell(command, low_priority=True) From 0551a2f1e7b80c2f5e3ae16ba579aa7d2d063674 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 26 Apr 2022 16:34:34 +0530 Subject: [PATCH 104/139] fix(mobile): Show checkbox in file list view --- frappe/public/js/frappe/views/file/file_view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/file/file_view.js b/frappe/public/js/frappe/views/file/file_view.js index b351ce6109..de06e6013e 100644 --- a/frappe/public/js/frappe/views/file/file_view.js +++ b/frappe/public/js/frappe/views/file/file_view.js @@ -390,7 +390,7 @@ frappe.views.FileView = class FileView extends frappe.views.ListView { return `
- From 0334936449e5e2fdb80782316c0e543ca666542f Mon Sep 17 00:00:00 2001 From: Tom-Finke Date: Tue, 26 Apr 2022 14:38:18 +0200 Subject: [PATCH 105/139] fix: filters not working on "select items" dialog boxes (#16765) * Fix: is_child_selection_enabled for multiselect dialog filter * fix: check if field exists before fetching value Co-authored-by: Ankush Menat --- frappe/public/js/frappe/form/multi_select_dialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/multi_select_dialog.js b/frappe/public/js/frappe/form/multi_select_dialog.js index 61922a2422..92d2759f7f 100644 --- a/frappe/public/js/frappe/form/multi_select_dialog.js +++ b/frappe/public/js/frappe/form/multi_select_dialog.js @@ -151,7 +151,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { } is_child_selection_enabled() { - return this.dialog.fields_dict['allow_child_item_selection'].get_value(); + return this.dialog.fields_dict['allow_child_item_selection']?.get_value(); } toggle_child_selection() { From 8f53a039a6665d3e4cda28f1075ab41cb7761bda Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 26 Apr 2022 23:43:07 +0200 Subject: [PATCH 106/139] refactor: Sync fixtures --- frappe/utils/fixtures.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/frappe/utils/fixtures.py b/frappe/utils/fixtures.py index f00d310c9d..42e86e9f11 100644 --- a/frappe/utils/fixtures.py +++ b/frappe/utils/fixtures.py @@ -17,11 +17,9 @@ def sync_fixtures(app=None): frappe.flags.in_fixtures = True for app in apps: - if os.path.exists(frappe.get_app_path(app, "fixtures")): - fixture_files = sorted(os.listdir(frappe.get_app_path(app, "fixtures"))) - for fname in fixture_files: - if fname.endswith(".json"): - import_doc(frappe.get_app_path(app, "fixtures", fname)) + fixtures_path = frappe.get_app_path(app, "fixtures") + if os.path.exists(fixtures_path): + import_doc(fixtures_path) import_custom_scripts(app) From 2c4773ff4482ed3a2902846c886a7a16905f11a8 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 27 Apr 2022 09:48:17 +0530 Subject: [PATCH 107/139] fix(newsletter): Pass parent instead of parentfield while getting successful recipients of a newsletter --- frappe/email/doctype/newsletter/newsletter.py | 4 ++-- .../doctype/newsletter/test_newsletter.py | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index 6aa881ed5c..b04ad4db40 100644 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -124,7 +124,7 @@ class Newsletter(WebsiteGenerator): ) def get_success_recipients(self) -> List[str]: - """Recipients who have already recieved the newsletter. + """Recipients who have already received the newsletter. Couldn't think of a better name ;) """ @@ -132,7 +132,7 @@ class Newsletter(WebsiteGenerator): "Email Queue Recipient", filters={ "status": ("in", ["Not Sent", "Sending", "Sent"]), - "parentfield": ("in", self.get_linked_email_queue()), + "parent": ("in", self.get_linked_email_queue()), }, pluck="recipient", ) diff --git a/frappe/email/doctype/newsletter/test_newsletter.py b/frappe/email/doctype/newsletter/test_newsletter.py index c62b7e84aa..81702f3a09 100644 --- a/frappe/email/doctype/newsletter/test_newsletter.py +++ b/frappe/email/doctype/newsletter/test_newsletter.py @@ -221,3 +221,24 @@ class TestNewsletter(TestNewsletterMixin, unittest.TestCase): newsletter.reload() self.assertEqual(newsletter.email_sent, 0) + + def test_retry_partially_sent_newsletter(self): + frappe.db.delete("Email Queue") + frappe.db.delete("Email Queue Recipient") + frappe.db.delete("Newsletter") + + newsletter = self.get_newsletter() + newsletter.send_emails() + email_queue_list = [frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue")] + self.assertEqual(len(email_queue_list), 4) + + # emulate partial send + email_queue_list[0].status = "Error" + email_queue_list[0].recipients[0].status = "Error" + email_queue_list[0].save() + newsletter.email_sent = False + + # retry + newsletter.send_emails() + email_queue_list = [frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue")] + self.assertEqual(len(email_queue_list), 5) From 9422a4fc886f5460ad93394c1a7ada46778a2be5 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 27 Apr 2022 10:25:47 +0530 Subject: [PATCH 108/139] fix: Add `naming_rule` to retain changes from Customize Form --- frappe/custom/doctype/customize_form/customize_form.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index b4ccb21167..5b0827b919 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -571,6 +571,7 @@ doctype_properties = { "email_append_to": "Check", "subject_field": "Data", "sender_field": "Data", + "naming_rule": "Data", "autoname": "Data", "show_title_field_in_link": "Check", } From ebf6a8ce322b608aa1c7844a69153e3fc25127ea Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 27 Apr 2022 10:30:23 +0530 Subject: [PATCH 109/139] style: Fix formatting --- frappe/public/js/frappe/doctype/index.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/doctype/index.js b/frappe/public/js/frappe/doctype/index.js index e71f0b6f09..36a1daf64e 100644 --- a/frappe/public/js/frappe/doctype/index.js +++ b/frappe/public/js/frappe/doctype/index.js @@ -5,7 +5,6 @@ frappe.provide("frappe.model"); apply to both DocType form and customize form. */ frappe.model.DocTypeController = class DocTypeController extends frappe.ui.form.Controller { - max_attachments() { if (!this.frm.doc.max_attachments) { return; @@ -50,7 +49,7 @@ frappe.model.DocTypeController = class DocTypeController extends frappe.ui.form. this.frm.set_value("autoname", "hash"); break; } - setTimeout(() =>this.frm.__from_naming_rule = false, 500); + setTimeout(() => (this.frm.__from_naming_rule = false), 500); this.set_naming_rule_description(); } @@ -103,7 +102,7 @@ frappe.model.DocTypeController = class DocTypeController extends frappe.ui.form. default: this.frm.set_value('naming_rule', 'Expression (old style)'); } - setTimeout(() => this.frm.__from_autoname = false, 500); + setTimeout(() => (this.frm.__from_autoname = false), 500); } this.frm.set_df_property('fields', 'reqd', this.frm.doc.autoname !== 'Prompt'); From b714dbb190311b43f22c898d5925d045b4150a62 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 27 Apr 2022 10:49:56 +0530 Subject: [PATCH 110/139] fix: dont throw error for a bad translation (#16769) --- frappe/translate.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frappe/translate.py b/frappe/translate.py index d95c8eb3e8..0ebf4eaf1b 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -22,7 +22,7 @@ from pypika.terms import PseudoColumn import frappe from frappe.model.utils import InvalidIncludePath, render_include from frappe.query_builder import DocType, Field -from frappe.utils import get_bench_path, is_html, strip, strip_html_tags +from frappe.utils import cstr, get_bench_path, is_html, strip, strip_html_tags TRANSLATE_PATTERN = re.compile( r"_\([\s\n]*" # starts with literal `_(`, ignore following whitespace/newlines @@ -319,11 +319,11 @@ def get_translation_dict_from_file(path, lang, app): elif len(item) in [2, 3]: translation_map[item[0]] = strip(item[1]) elif item: - raise Exception( - "Bad translation in '{app}' for language '{lang}': {values}".format( - app=app, lang=lang, values=repr(item).encode("utf-8") - ) + msg = "Bad translation in '{app}' for language '{lang}': {values}".format( + app=app, lang=lang, values=cstr(item) ) + frappe.log_error(message=msg, title="Error in translation file") + frappe.msgprint(msg) return translation_map From 274d5e69ee4c0f2259315ee6286cf6ceb77f849b Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 27 Apr 2022 07:26:58 +0200 Subject: [PATCH 111/139] feat: add default date and time format for Germany (#16772) --- frappe/geo/country_info.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/geo/country_info.json b/frappe/geo/country_info.json index 23cadc2156..c1031fe211 100644 --- a/frappe/geo/country_info.json +++ b/frappe/geo/country_info.json @@ -954,6 +954,8 @@ "smallest_currency_fraction_value": 0.01, "currency_symbol": "\u20ac", "number_format": "#.###,##", + "date_format": "dd.mm.yyyy", + "time_format": "HH:mm", "timezones": [ "Europe/Berlin" ] From a9492ba5fa9fa7c0d6186714c290c9930fde97cd Mon Sep 17 00:00:00 2001 From: chillaranand Date: Fri, 22 Apr 2022 11:28:00 +0530 Subject: [PATCH 112/139] refactor: Remove wrapt package Also replaced inspect.getfullargspec with inspect.signature to preserve signature of a decorated functions. --- frappe/__init__.py | 14 ++++++-------- frappe/desk/search.py | 25 +++++++++++++------------ requirements.txt | 1 - 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 97e605394b..bcd6ef4f45 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1490,10 +1490,11 @@ def get_newargs(fn, kwargs): if hasattr(fn, "fnargs"): fnargs = fn.fnargs else: - fullargspec = inspect.getfullargspec(fn) - fnargs = fullargspec.args - fnargs.extend(fullargspec.kwonlyargs) - varkw = fullargspec.varkw + signature = inspect.signature(fn) + fnargs = list(signature.parameters) + varkw = "kwargs" in fnargs + if varkw: + fnargs.pop(-1) newargs = {} for a in kwargs: @@ -2252,7 +2253,4 @@ def mock(type, size=1, locale="en"): return squashify(results) -def validate_and_sanitize_search_inputs(fn): - from frappe.desk.search import validate_and_sanitize_search_inputs as func - - return func(fn) +from frappe.desk.search import validate_and_sanitize_search_inputs # noqa diff --git a/frappe/desk/search.py b/frappe/desk/search.py index ba4c5fb4fb..eb1a2e82ba 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -1,12 +1,10 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE +import functools import json import re -import wrapt - -# Search import frappe from frappe import _, is_whitelisted from frappe.permissions import has_permission @@ -314,17 +312,20 @@ def relevance_sorter(key, query, as_dict): return (cstr(value).lower().startswith(query.lower()) is not True, value) -@wrapt.decorator -def validate_and_sanitize_search_inputs(fn, instance, args, kwargs): - kwargs.update(dict(zip(fn.__code__.co_varnames, args))) - sanitize_searchfield(kwargs["searchfield"]) - kwargs["start"] = cint(kwargs["start"]) - kwargs["page_len"] = cint(kwargs["page_len"]) +def validate_and_sanitize_search_inputs(fn): + @functools.wraps(fn) + def wrapper(*args, **kwargs): + kwargs.update(dict(zip(fn.__code__.co_varnames, args))) + sanitize_searchfield(kwargs["searchfield"]) + kwargs["start"] = cint(kwargs["start"]) + kwargs["page_len"] = cint(kwargs["page_len"]) - if kwargs["doctype"] and not frappe.db.exists("DocType", kwargs["doctype"]): - return [] + if kwargs["doctype"] and not frappe.db.exists("DocType", kwargs["doctype"]): + return [] - return fn(**kwargs) + return fn(**kwargs) + + return wrapper @frappe.whitelist() diff --git a/requirements.txt b/requirements.txt index 099e49a0b0..ec8d76367e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -66,7 +66,6 @@ terminaltables~=3.1.0 urllib3~=1.26.4 Werkzeug~=2.0.3 Whoosh~=2.7.4 -wrapt~=1.14.0 xlrd~=2.0.1 zxcvbn-python~=4.4.24 tenacity~=8.0.1 From 834706e33be590cc9170dd7a7c64909b72c6c77a Mon Sep 17 00:00:00 2001 From: Saurabh Date: Wed, 27 Apr 2022 11:44:08 +0530 Subject: [PATCH 113/139] chore: add missing doc event type --- frappe/core/doctype/server_script/server_script.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index 700a97cfeb..9312ae178b 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -49,7 +49,7 @@ "fieldname": "doctype_event", "fieldtype": "Select", "label": "DocType Event", - "options": "Before Insert\nBefore Validate\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)\nOn Payment Authorization" + "options": "Before Insert\nBefore Validate\nBefore Save\nAfter Insert\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)\nOn Payment Authorization" }, { "depends_on": "eval:doc.script_type==='API'", @@ -109,7 +109,7 @@ "link_fieldname": "server_script" } ], - "modified": "2022-04-22 11:24:01.151662", + "modified": "2022-04-27 11:42:52.032963", "modified_by": "Administrator", "module": "Core", "name": "Server Script", From 9bec3480ed90376f12052b2933123aeab5a456ce Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 27 Apr 2022 14:25:45 +0530 Subject: [PATCH 114/139] ci: failfast in case of conflicts (#16777) --- .github/helper/install_dependencies.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/helper/install_dependencies.sh b/.github/helper/install_dependencies.sh index d16f5b62ad..f0e8016860 100644 --- a/.github/helper/install_dependencies.sh +++ b/.github/helper/install_dependencies.sh @@ -2,6 +2,13 @@ set -e +# Check for merge conflicts before proceeding +python -m compileall -f "${GITHUB_WORKSPACE}" +if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}" + then echo "Found merge conflicts" + exit 1 +fi + # install wkhtmltopdf wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz tar -xf /tmp/wkhtmltox.tar.xz -C /tmp From a667b00681fb290168b1a22fb5a0119bd7d1a9cf Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 27 Apr 2022 15:55:29 +0530 Subject: [PATCH 115/139] refactor: Remove unnecessary switch case --- frappe/public/js/frappe/doctype/index.js | 70 +++++++++--------------- 1 file changed, 26 insertions(+), 44 deletions(-) diff --git a/frappe/public/js/frappe/doctype/index.js b/frappe/public/js/frappe/doctype/index.js index 36a1daf64e..09f020f370 100644 --- a/frappe/public/js/frappe/doctype/index.js +++ b/frappe/public/js/frappe/doctype/index.js @@ -27,28 +27,17 @@ frappe.model.DocTypeController = class DocTypeController extends frappe.ui.form. // flag to avoid recursion this.frm.__from_naming_rule = true; - switch (this.frm.doc.naming_rule) { - case "Set by user": - this.frm.set_value("autoname", "Prompt"); - break; - case "Autoincrement": - this.frm.set_value("autoname", "autoincrement"); - break; - case "By fieldname": - this.frm.set_value("autoname", "field:"); - break; - case 'By "Naming Series" field': - this.frm.set_value("autoname", "naming_series:"); - break; - case "Expression": - this.frm.set_value("autoname", "format:"); - break; - case "Expression (old style)": - break; - case "Random": - this.frm.set_value("autoname", "hash"); - break; - } + const naming_rule_default_autoname_map = { + "Autoincrement": "autoincrement", + "Set by user": "prompt", + "By fieldname": "field:", + 'By "Naming Series" field': "naming_series:", + "Expression": "format:", + "Expression (sld style)": "", + "Random": "hash", + "By script": "" + }; + this.frm.set_value("autoname", naming_rule_default_autoname_map[this.frm.doc.naming_rule] || ""); setTimeout(() => (this.frm.__from_naming_rule = false), 500); this.set_naming_rule_description(); @@ -80,28 +69,21 @@ frappe.model.DocTypeController = class DocTypeController extends frappe.ui.form. this.frm.__from_autoname = true; const autoname = this.frm.doc.autoname.toLowerCase(); - switch (autoname) { - case 'prompt': - this.frm.set_value('naming_rule', 'Set by user'); - break; - case 'autoincrement': - this.frm.set_value('naming_rule', 'Autoincrement'); - break; - case (autoname.startsWith('field:')): - this.frm.set_value('naming_rule', 'By fieldname'); - break; - case (autoname.startsWith('naming_series:')): - this.frm.set_value('naming_rule', 'By "Naming Series" field'); - break; - case (autoname.startsWith('format:')): - this.frm.set_value('naming_rule', 'Expression'); - break; - case 'hash': - this.frm.set_value('naming_rule', 'Random'); - break; - default: - this.frm.set_value('naming_rule', 'Expression (old style)'); - } + if (autoname === "prompt") + this.frm.set_value("naming_rule", "Set by user"); + else if (autoname === "autoincrement") + this.frm.set_value("naming_rule", "Autoincrement"); + else if (autoname.startsWith("field:")) + this.frm.set_value("naming_rule", "By fieldname"); + else if (autoname.startsWith("naming_series:")) + this.frm.set_value("naming_rule", 'By "Naming Series" field'); + else if (autoname.startsWith("format:")) + this.frm.set_value("naming_rule", "Expression"); + else if (autoname === "hash") + this.frm.set_value("naming_rule", "Random"); + else + this.frm.set_value("naming_rule", "Expression (old style)"); + setTimeout(() => (this.frm.__from_autoname = false), 500); } From 2f2fc0440de523909226bc358886ef8f2b758698 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 27 Apr 2022 15:56:20 +0530 Subject: [PATCH 116/139] test: Init UI test for customize form --- cypress/integration/customize_form.js | 23 +++++++++++++++++++++++ cypress/support/commands.js | 4 ++++ 2 files changed, 27 insertions(+) create mode 100644 cypress/integration/customize_form.js diff --git a/cypress/integration/customize_form.js b/cypress/integration/customize_form.js new file mode 100644 index 0000000000..01b4ebd731 --- /dev/null +++ b/cypress/integration/customize_form.js @@ -0,0 +1,23 @@ +context('Customize Form', () => { + before(() => { + cy.visit('/app/customize-form'); + }); + it('Changing to naming rule should update autoname', () => { + cy.fill_field("doc_type", "ToDo", "Link").blur(); + cy.click_form_section("Naming"); + const naming_rule_default_autoname_map = { + "Autoincrement": "autoincrement", + "Set by user": "prompt", + "By fieldname": "field:", + 'By "Naming Series" field': "naming_series:", + "Expression": "format:", + "Expression (old style)": "", + "Random": "hash", + "By script": "" + }; + Cypress._.forOwn(naming_rule_default_autoname_map, (value, naming_rule) => { + cy.fill_field("naming_rule", naming_rule, "Select"); + cy.get_field("autoname", "Data").should("have.value", value); + }); + }); +}); \ No newline at end of file diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 636312376d..99bec50b37 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -340,3 +340,7 @@ Cypress.Commands.add('click_timeline_action_btn', (btn_name) => { Cypress.Commands.add('select_listview_row_checkbox', (row_no) => { cy.get('.frappe-list .select-like > .list-row-checkbox').eq(row_no).click(); }); + +Cypress.Commands.add('click_form_section', (section_name) => { + cy.get('.section-head').contains(section_name).click(); +}); From 88992c66862b3f958db5c29b3bdb2e8d8f69da38 Mon Sep 17 00:00:00 2001 From: Max Solanki <84378369+lapardnemihk1099@users.noreply.github.com> Date: Wed, 27 Apr 2022 17:08:26 +0530 Subject: [PATCH 117/139] fix: console error while using TAB shortcut in grid (#16701) --- frappe/public/js/frappe/form/form.js | 2 +- frappe/public/js/frappe/ui/keyboard.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 3c07907c50..c56ffc592d 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -179,7 +179,7 @@ frappe.ui.form.Form = class FrappeForm { grid_shortcut_keys.forEach(row => { frappe.ui.keys.add_shortcut({ shortcut: row.shortcut, - page: this, + page: this.page, description: __(row.description), ignore_inputs: true, condition: () => !this.is_new() diff --git a/frappe/public/js/frappe/ui/keyboard.js b/frappe/public/js/frappe/ui/keyboard.js index 40d1a93b8c..85ce248175 100644 --- a/frappe/public/js/frappe/ui/keyboard.js +++ b/frappe/public/js/frappe/ui/keyboard.js @@ -37,7 +37,7 @@ frappe.ui.keys.add_shortcut = ({shortcut, action, description, page, target, con if (is_input_focused && !ignore_inputs) return; if (!condition()) return; - if (!page || page.wrapper.is(':visible')) { + if (action && (!page || page.wrapper.is(':visible'))) { let prevent_default = action(e); // prevent default if true is explicitly returned // or nothing returned (undefined) From 2bc06a1058c5e465641975553c92e8c23176aff9 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 27 Apr 2022 17:08:42 +0530 Subject: [PATCH 118/139] fix: Check if autoname exists --- frappe/core/doctype/doctype/doctype.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 3a73eb9d42..2bb971bac8 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -863,7 +863,7 @@ def validate_series(dt, autoname=None, name=None): if not autoname and dt.get("fields", {"fieldname": "naming_series"}): dt.autoname = "naming_series:" - elif dt.autoname.startswith("naming_series:"): + elif dt.autoname and dt.autoname.startswith("naming_series:"): fieldname = dt.autoname.split("naming_series:")[0] or "naming_series" if not dt.get("fields", {"fieldname": fieldname}): frappe.throw( From 672bd8a52318296cdbdf8f37ed2c470f96bc696c Mon Sep 17 00:00:00 2001 From: Aditya Hase Date: Thu, 28 Apr 2022 11:05:50 +0530 Subject: [PATCH 119/139] fix: Update links to Frappe Cloud (#16788) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8c8317c8bd..bd4b50db96 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Full-stack web application framework that uses Python and MariaDB on the server side and a tightly integrated client side library. Built for [ERPNext](https://erpnext.com) @@ -52,7 +52,7 @@ Full-stack web application framework that uses Python and MariaDB on the server * [Install via Docker](https://github.com/frappe/frappe_docker) * [Install via Frappe Bench](https://github.com/frappe/bench) * [Offical Documentation](https://frappeframework.com/docs/user/en/installation) -* [Managed Hosting on Frappe Cloud](https://frappecloud.com/deploy?apps=frappe&source=frappe_readme) +* [Managed Hosting on Frappe Cloud](https://frappecloud.com/frappe/signup) ## Contributing From 30440026db205bc58dffe0946bc01815b5eb72f9 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 28 Apr 2022 11:44:33 +0530 Subject: [PATCH 120/139] fix: Do not allow autoname changes to/from autoincrement in customize form --- cypress/integration/customize_form.js | 1 - frappe/custom/doctype/customize_form/customize_form.js | 4 ++++ frappe/custom/doctype/customize_form/customize_form.json | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cypress/integration/customize_form.js b/cypress/integration/customize_form.js index 01b4ebd731..70615085c3 100644 --- a/cypress/integration/customize_form.js +++ b/cypress/integration/customize_form.js @@ -6,7 +6,6 @@ context('Customize Form', () => { cy.fill_field("doc_type", "ToDo", "Link").blur(); cy.click_form_section("Naming"); const naming_rule_default_autoname_map = { - "Autoincrement": "autoincrement", "Set by user": "prompt", "By fieldname": "field:", 'By "Naming Series" field': "naming_series:", diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 4c2d207df9..4ce2c73fa3 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -152,6 +152,10 @@ frappe.ui.form.on("Customize Form", { }, __("Actions") ); + + const is_autoname_autoincrement = frm.doc.autoname === 'autoincrement'; + frm.set_df_property("naming_rule", "hidden", is_autoname_autoincrement); + frm.set_df_property("autoname", "read_only", is_autoname_autoincrement); } frm.events.setup_export(frm); diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json index d51762effa..6563cae94c 100644 --- a/frappe/custom/doctype/customize_form/customize_form.json +++ b/frappe/custom/doctype/customize_form/customize_form.json @@ -56,7 +56,7 @@ "fieldtype": "Select", "label": "Naming Rule", "length": 40, - "options": "\nSet by user\nAutoincrement\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script" + "options": "\nSet by user\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script" }, { "fieldname": "doc_type", @@ -287,7 +287,7 @@ "label": "Naming" }, { - "description": "Naming Options:\n
  1. field:[fieldname] - By Field
  2. autoincrement - Uses Databases' Auto Increment feature
  3. naming_series: - By Naming Series (field called naming_series must be present)
  4. Prompt - Prompt user for a name
  5. [series] - Series by prefix (separated by a dot); for example PRE.#####
  6. \n
  7. format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
", + "description": "Naming Options:\n
  1. field:[fieldname] - By Field
  2. naming_series: - By Naming Series (field called naming_series must be present)
  3. Prompt - Prompt user for a name
  4. [series] - Series by prefix (separated by a dot); for example PRE.#####
  5. \n
  6. format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
", "fieldname": "autoname", "fieldtype": "Data", "label": "Auto Name" From bc5ab839da9e731bcc237d4f215b90949d3abf39 Mon Sep 17 00:00:00 2001 From: Nidhi Purohit Date: Thu, 28 Apr 2022 11:54:36 +0530 Subject: [PATCH 121/139] Added condition for cypress >> Link dropdown selection --- frappe/public/js/frappe/form/controls/link.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 2295cad41a..2081a301c3 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -237,7 +237,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat no_spinner: true, args: args, callback: function(r) { - if(!me.$input.is(":focus")) { + if (!window.Cypress && !me.$input.is(":focus")) { return; } r.results = me.merge_duplicates(r.results); From 03efa25d0376ba8a7260fba0c29ac60ece8db7e9 Mon Sep 17 00:00:00 2001 From: Komal-Saraf0609 Date: Thu, 28 Apr 2022 11:57:43 +0530 Subject: [PATCH 122/139] test: Corrected failed tests --- cypress/integration/control_data.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cypress/integration/control_data.js b/cypress/integration/control_data.js index 804c016cae..021b9032c1 100644 --- a/cypress/integration/control_data.js +++ b/cypress/integration/control_data.js @@ -109,7 +109,7 @@ context('Data Control', () => { cy.fill_field('name1', 'Komal', 'Data'); cy.fill_field('email', 'komal@test.com', 'Data'); cy.fill_field('phone', '9432380001', 'Data'); - cy.findByRole('button', {name: 'Save'}).click(); + cy.findByRole('button', {name: 'Save'}).click({force: true}); //Checking if the fields contains the data which has been filled in cy.location("pathname").should('not.be', '/app/test-data-control/new-test-data-control-1'); cy.get_field('name1').should('have.value', 'Komal'); @@ -119,8 +119,8 @@ context('Data Control', () => { it('Deleting the doc', () => { //Deleting the inserted document - cy.visit('/app/test-data-control'); - cy.get('.list-row-checkbox').eq(0).click(); + cy.go_to_list('Test Data Control'); + cy.get('.list-row-checkbox').eq(0).click({force: true}); cy.get('.actions-btn-group > .btn').contains('Actions').click(); cy.get('.actions-btn-group > .dropdown-menu [data-label="Delete"]').click(); cy.click_modal_primary_button('Yes'); From d1423f15171f72745d77bebd10e44b709f731f53 Mon Sep 17 00:00:00 2001 From: Ritwik Puri Date: Thu, 28 Apr 2022 13:46:09 +0530 Subject: [PATCH 123/139] test: use data fieldtype instead of int in `test_fieldname_starting_with_int` (#16771) * fix(test): use data fieldtype instead of int postgres doesn't allow using like operator on int/any non-text/varchar column ref: https://github.com/frappe/frappe/issues/16722 * minor: updated test_fieldname_starting_with_int with child table filters --- frappe/tests/test_db_query.py | 36 ++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/frappe/tests/test_db_query.py b/frappe/tests/test_db_query.py index 8bdd66a045..c1ff1a1a15 100644 --- a/frappe/tests/test_db_query.py +++ b/frappe/tests/test_db_query.py @@ -639,23 +639,53 @@ class TestReportview(unittest.TestCase): def test_fieldname_starting_with_int(self): from frappe.core.doctype.doctype.test_doctype import new_doctype + frappe.delete_doc_if_exists("DocType", "dt_with_int_named_fieldname") + frappe.delete_doc_if_exists("DocType", "table_dt") + + table_dt = new_doctype( + "table_dt", istable=1, fields=[{"label": "1field", "fieldname": "2field", "fieldtype": "Data"}] + ).insert() + dt = new_doctype( "dt_with_int_named_fieldname", - fields=[{"label": "1field", "fieldname": "1field", "fieldtype": "Int"}], + fields=[ + {"label": "1field", "fieldname": "1field", "fieldtype": "Data"}, + { + "label": "2table_field", + "fieldname": "2table_field", + "fieldtype": "Table", + "options": table_dt.name, + }, + ], ).insert(ignore_permissions=True) - frappe.get_doc({"doctype": "dt_with_int_named_fieldname", "1field": 10}).insert( + dt_data = frappe.get_doc({"doctype": "dt_with_int_named_fieldname", "1field": "10"}).insert( ignore_permissions=True ) query = DatabaseQuery("dt_with_int_named_fieldname") - self.assertTrue(query.execute(filters={"1field": 10})) + self.assertTrue(query.execute(filters={"1field": "10"})) self.assertTrue(query.execute(filters={"1field": ["like", "1%"]})) self.assertTrue(query.execute(filters={"1field": ["in", "1,2,10"]})) self.assertTrue(query.execute(filters={"1field": ["is", "set"]})) self.assertFalse(query.execute(filters={"1field": ["not like", "1%"]})) + self.assertTrue(query.execute(filters=[["table_dt", "2field", "is", "not set"]])) + frappe.get_doc( + { + "doctype": table_dt.name, + "2field": "10", + "parent": dt_data.name, + "parenttype": dt_data.doctype, + "parentfield": "2table_field", + } + ).insert(ignore_permissions=True) + + self.assertTrue(query.execute(filters=[["table_dt", "2field", "is", "set"]])) + + # cleanup dt.delete() + table_dt.delete() def add_child_table_to_blog_post(): From b64d10a5ee518f7db775732dd86832217ce3493f Mon Sep 17 00:00:00 2001 From: gavin Date: Thu, 28 Apr 2022 14:14:09 +0530 Subject: [PATCH 124/139] docs(README): Add PWD button for Frappe (#16790) Clicking on the button will take you to Play with Docker site which will give you a new vanilla Frappe site to test and play around with for 3 hours PWD File from https://github.com/gavindsouza/install-scripts/blob/main/frappe/pwd.yml Related ERPNext PR: https://github.com/frappe/erpnext/pull/30817 --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bd4b50db96..5a024175b4 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,17 @@ Full-stack web application framework that uses Python and MariaDB on the server side and a tightly integrated client side library. Built for [ERPNext](https://erpnext.com) -
+ +> Login for the PWD site: (username: Administrator, password: admin) + ## Table of Contents * [Installation](#installation) * [Contributing](#contributing) From ec2c65f0a5986aa6f2583566f3b3188dc2c809d4 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 28 Apr 2022 14:27:46 +0530 Subject: [PATCH 125/139] fix(test): control_dynamic_link.js --- cypress/integration/control_dynamic_link.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cypress/integration/control_dynamic_link.js b/cypress/integration/control_dynamic_link.js index cc1eb0b695..68490547da 100644 --- a/cypress/integration/control_dynamic_link.js +++ b/cypress/integration/control_dynamic_link.js @@ -62,8 +62,8 @@ context('Dynamic Link', () => { "label": "Document ID", "fieldname": "doc_id", "fieldtype": "Dynamic Link", - "get_options": () => { - return "User"; + "get_options": () => { + return "User"; }, "in_list_view": 1, }] @@ -118,11 +118,11 @@ context('Dynamic Link', () => { cy.get_field('doc_type').clear(); //Entering System Settings in the Doctype field - cy.fill_field('doc_type', 'System Settings', 'Link', {delay: 500}); + cy.fill_field('doc_type', 'System Settings{enter}', 'Link', {delay: 500}); cy.get_field('doc_id').click(); //Checking if the system throws error cy.get('.modal-title').should('have.text', 'Error'); cy.get('.msgprint').should('have.text', 'System Settings is not a valid DocType for Dynamic Link'); }); -}); \ No newline at end of file +}); From 6d266b19af6fed6cbe4b112b2f1e3dca6a181711 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 28 Apr 2022 14:42:17 +0530 Subject: [PATCH 126/139] fix(minor): fix email error logging and strip Guest --- frappe/email/doctype/email_queue/email_queue.py | 4 +++- frappe/email/email_body.py | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py index 61d730829b..db2ca9e32b 100644 --- a/frappe/email/doctype/email_queue/email_queue.py +++ b/frappe/email/doctype/email_queue/email_queue.py @@ -622,11 +622,13 @@ class QueueBuilder: mail_to_string = cstr(mail.as_string()) except frappe.InvalidEmailAddressError: # bad Email Address - don't add to queue - self.log_error( + frappe.log_error( title="Invalid email address", message="Invalid email address Sender: {0}, Recipients: {1}, \nTraceback: {2} ".format( self.sender, ", ".join(self.final_recipients()), traceback.format_exc() ), + reference_doctype=self.reference_doctype, + reference_name=self.reference_name, ) return diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index 07f698f740..adaad8db48 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -268,9 +268,9 @@ class EMail: self.replace_sender() self.replace_sender_name() - self.recipients = [strip(r) for r in self.recipients] - self.cc = [strip(r) for r in self.cc] - self.bcc = [strip(r) for r in self.bcc] + self.recipients = [strip(r) for r in self.recipients if r != "Guest"] + self.cc = [strip(r) for r in self.cc if r != "Guest"] + self.bcc = [strip(r) for r in self.bcc if r != "Guest"] for e in self.recipients + (self.cc or []) + (self.bcc or []): validate_email_address(e, True) From efd993cdc3527aed117e69e9c30366e16aa13dc9 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 28 Apr 2022 14:45:27 +0530 Subject: [PATCH 127/139] fix(test): control_dynamic_link.js --- cypress/integration/control_dynamic_link.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cypress/integration/control_dynamic_link.js b/cypress/integration/control_dynamic_link.js index 68490547da..f1a5e43402 100644 --- a/cypress/integration/control_dynamic_link.js +++ b/cypress/integration/control_dynamic_link.js @@ -118,7 +118,10 @@ context('Dynamic Link', () => { cy.get_field('doc_type').clear(); //Entering System Settings in the Doctype field - cy.fill_field('doc_type', 'System Settings{enter}', 'Link', {delay: 500}); + cy.fill_field('doc_type', 'System Settings', 'Link', {delay: 500}); + cy.get(`[data-fieldname="${field.fieldname}"] ul:visible li:first-child`) + .click({scrollBehavior: false}); + cy.get_field('doc_id').click(); //Checking if the system throws error From 9847019457be4532e2257c47d9b325113405fac8 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 28 Apr 2022 14:47:07 +0530 Subject: [PATCH 128/139] fix(test): control_dynamic_link.js --- cypress/integration/control_dynamic_link.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/integration/control_dynamic_link.js b/cypress/integration/control_dynamic_link.js index f1a5e43402..d47f7a8855 100644 --- a/cypress/integration/control_dynamic_link.js +++ b/cypress/integration/control_dynamic_link.js @@ -119,7 +119,7 @@ context('Dynamic Link', () => { //Entering System Settings in the Doctype field cy.fill_field('doc_type', 'System Settings', 'Link', {delay: 500}); - cy.get(`[data-fieldname="${field.fieldname}"] ul:visible li:first-child`) + cy.get(`[data-fieldname="doc_type"] ul:visible li:first-child`) .click({scrollBehavior: false}); cy.get_field('doc_id').click(); From b1effcab4b001c89b6b6b825976aea42e2172b02 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 28 Apr 2022 14:44:37 +0530 Subject: [PATCH 129/139] docs: Removed whitespace from Try on FC button --- .github/try-on-f-cloud-button.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/try-on-f-cloud-button.svg b/.github/try-on-f-cloud-button.svg index fe0bb2c52d..6a7119bdee 100644 --- a/.github/try-on-f-cloud-button.svg +++ b/.github/try-on-f-cloud-button.svg @@ -1,4 +1,4 @@ - + @@ -29,4 +29,4 @@ - + \ No newline at end of file From 760974a05012d6b12c2ae2162e6cb741289d151f Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 28 Apr 2022 14:50:54 +0530 Subject: [PATCH 130/139] style(README): Fixed inconsistent whitespaces --- README.md | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 5a024175b4..4942d87e18 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@
-

-
- - - -

-

- a web framework with "batteries included" -

-
- it's pronounced - fra-pay -
+

+
+ + + +

+

+ a web framework with "batteries included" +

+
+ it's pronounced - fra-pay +
@@ -27,12 +27,11 @@ - +
- Full-stack web application framework that uses Python and MariaDB on the server side and a tightly integrated client side library. Built for [ERPNext](https://erpnext.com)
From d3ca83c4d82fa05649f8a9f4d23ada42d8327aeb Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 28 Apr 2022 14:55:39 +0530 Subject: [PATCH 131/139] fix(minor): fix email error logging and strip Guest --- frappe/email/email_body.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index adaad8db48..5e2f14d9bf 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -268,9 +268,9 @@ class EMail: self.replace_sender() self.replace_sender_name() - self.recipients = [strip(r) for r in self.recipients if r != "Guest"] - self.cc = [strip(r) for r in self.cc if r != "Guest"] - self.bcc = [strip(r) for r in self.bcc if r != "Guest"] + self.recipients = [strip(r) for r in self.recipients if r not in frappe.STANDARD_USERS] + self.cc = [strip(r) for r in self.cc if r not in frappe.STANDARD_USERS] + self.bcc = [strip(r) for r in self.bcc if r not in frappe.STANDARD_USERS] for e in self.recipients + (self.cc or []) + (self.bcc or []): validate_email_address(e, True) From c23cc52948390b5065de735715272f6f24db8006 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 28 Apr 2022 15:17:56 +0530 Subject: [PATCH 132/139] fix(test): control_dynamic_link.js --- cypress/integration/control_dynamic_link.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cypress/integration/control_dynamic_link.js b/cypress/integration/control_dynamic_link.js index d47f7a8855..944dae5072 100644 --- a/cypress/integration/control_dynamic_link.js +++ b/cypress/integration/control_dynamic_link.js @@ -118,7 +118,9 @@ context('Dynamic Link', () => { cy.get_field('doc_type').clear(); //Entering System Settings in the Doctype field + cy.intercept('/api').as('api/*/search_link.py'); cy.fill_field('doc_type', 'System Settings', 'Link', {delay: 500}); + cy.wait('@api'); cy.get(`[data-fieldname="doc_type"] ul:visible li:first-child`) .click({scrollBehavior: false}); From dfeb16f58da9f0ed826633dcd4734684667052a7 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 28 Apr 2022 15:20:29 +0530 Subject: [PATCH 133/139] fix(test): control_dynamic_link.js --- cypress/integration/control_dynamic_link.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cypress/integration/control_dynamic_link.js b/cypress/integration/control_dynamic_link.js index 944dae5072..32b2c274a8 100644 --- a/cypress/integration/control_dynamic_link.js +++ b/cypress/integration/control_dynamic_link.js @@ -118,9 +118,9 @@ context('Dynamic Link', () => { cy.get_field('doc_type').clear(); //Entering System Settings in the Doctype field - cy.intercept('/api').as('api/*/search_link.py'); + cy.intercept('/api/method/frappe.desk.search.search_link').as('search_query'); cy.fill_field('doc_type', 'System Settings', 'Link', {delay: 500}); - cy.wait('@api'); + cy.wait('@search_query'); cy.get(`[data-fieldname="doc_type"] ul:visible li:first-child`) .click({scrollBehavior: false}); From 6d96f11dd961d7f71cd54cae421793209c86f921 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 29 Apr 2022 14:38:35 +0530 Subject: [PATCH 134/139] fix: Use BlogPosting schema for Article Fixes https://github.com/frappe/frappe/issues/1730 --- frappe/website/doctype/help_article/templates/help_article.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/website/doctype/help_article/templates/help_article.html b/frappe/website/doctype/help_article/templates/help_article.html index 9ef9a398f5..105b63e651 100644 --- a/frappe/website/doctype/help_article/templates/help_article.html +++ b/frappe/website/doctype/help_article/templates/help_article.html @@ -5,7 +5,7 @@ {% endblock %} {% block page_content %} -
+
By {{ author }} on {{ frappe.format_date(creation) }}
{{ level }} From b41379c78ba527350b0118863448fed4ee11ba15 Mon Sep 17 00:00:00 2001 From: Ritwik Puri Date: Fri, 29 Apr 2022 15:06:03 +0530 Subject: [PATCH 135/139] fix: misc fixes (integer primary keys) (#16307) * fix: misc fixes local.x gets resetted on every request so switched to a simple dict simplified is_val_used in set_next_val function for sequences * chore: use multisql for sequence methods * fix: fields not updating on form * minor(base_input): removed unnecessary branching in update_input * chore: remove prints and rename autoincremented_status_map * chore: added proper type hint + comment + formatting * fix: added searching in cast_name rather than handling it manually * fix: share condition query + test_build_match_conditions * fix: add cast_name to more places * test: test for sequence * fix: sequence functions * fix: inherit frappetestcase * minor: attach sequence methods to db context local * chore: update sequence function names in Database use frappe.db for sequences in naming.py * fix: convert filename to str (for autoincremented doctypes) * chore: better regex for modifying values for postgres * minor: allow changing name column type (if no data is present in the doctype) * refactor: validate_autoname converted it to a simple function enabled changing autoincrement autoname from customize form * fix: use sql_ddl for change_column_type in postgres * fix: use not null constraint in postgres when changing name type * fix(test): updated test_autoincremented_doctype_transition with transitioning when no data is present * fix(test): updated test_cast_name probably messed up during rebase * fix(test): used rollback upon error in transaction for postgres * chore: use frappe.db.x methods for sequences * minor: use temporary sequences in test * minor: use generate_hash for sequence naming in sequence tests * chore: replace sequence imports with frappe.db.x * chore: move out casting name fields to a separate method * refactor: cast_name more explicit cases for casts and added docstring * fix: added space in test_cast_name * chore: fix linter * chore: better naming for can_change_name_column_type * chore: add comment for autoincremented_site_status_map * chore: update/add docstrings --- frappe/__init__.py | 3 +- frappe/core/doctype/doctype/doctype.py | 70 +++++++++--- frappe/core/doctype/doctype/test_doctype.py | 21 +++- .../customize_form/customize_form.json | 4 +- .../doctype/customize_form/customize_form.py | 10 ++ frappe/database/database.py | 15 +++ frappe/database/mariadb/database.py | 2 +- frappe/database/mariadb/schema.py | 3 +- frappe/database/postgres/database.py | 12 ++- frappe/database/postgres/schema.py | 3 +- frappe/database/sequence.py | 53 ++++----- frappe/model/db_query.py | 101 +++++++++--------- frappe/model/naming.py | 37 +++---- .../js/frappe/form/controls/base_input.js | 6 +- frappe/public/js/frappe/form/form.js | 6 +- frappe/public/js/frappe/model/model.js | 2 +- frappe/tests/test_db_query.py | 17 +-- frappe/tests/test_sequence.py | 54 ++++++++++ 18 files changed, 279 insertions(+), 140 deletions(-) create mode 100644 frappe/tests/test_sequence.py diff --git a/frappe/__init__.py b/frappe/__init__.py index d54686304f..8bd7783283 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -232,7 +232,6 @@ def init(site, sites_path=None, new_site=False): local.cache = {} local.document_cache = {} local.meta_cache = {} - local.autoincremented_status_map = {site: -1} local.form_dict = _dict() local.session = _dict() local.dev_server = _dev_server @@ -1926,7 +1925,7 @@ def attach_print( if not file_name: file_name = name - file_name = file_name.replace(" ", "").replace("/", "-") + file_name = cstr(file_name).replace(" ", "").replace("/", "-") print_settings = db.get_singles_dict("Print Settings") diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 2bb971bac8..06ebcc7d42 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -92,10 +92,10 @@ class DocType(Document): self.check_developer_mode() - self.validate_autoname() self.validate_name() self.set_defaults_for_single_and_table() + self.set_defaults_for_autoincremented() self.scrub_field_names() self.set_default_in_list_view() self.set_default_translatable() @@ -124,6 +124,12 @@ class DocType(Document): if self.default_print_format and not self.custom: frappe.throw(_("Standard DocType cannot have default print format, use Customize Form")) + if check_if_can_change_name_type(self): + change_name_column_type( + self.name, + "bigint" if self.autoname == "autoincrement" else f"varchar({frappe.db.VARCHAR_LEN})", + ) + def validate_field_name_conflicts(self): """Check if field names dont conflict with controller properties and methods""" core_doctypes = [ @@ -184,6 +190,10 @@ class DocType(Document): self.allow_import = 0 self.permissions = [] + def set_defaults_for_autoincremented(self): + if self.autoname and self.autoname == "autoincrement": + self.allow_rename = 0 + def set_default_in_list_view(self): """Set default in-list-view for first 4 mandatory fields""" if not [d.fieldname for d in self.fields if d.in_list_view]: @@ -809,17 +819,6 @@ class DocType(Document): max_idx = frappe.db.sql("""select max(idx) from `tabDocField` where parent = %s""", self.name) return max_idx and max_idx[0][0] or 0 - def validate_autoname(self): - if not self.is_new(): - doc_before_save = self.get_doc_before_save() - if doc_before_save: - if (self.autoname == "autoincrement" and doc_before_save.autoname != "autoincrement") or ( - self.autoname != "autoincrement" and doc_before_save.autoname == "autoincrement" - ): - frappe.throw(_("Cannot change to/from Autoincrement naming rule")) - if self.autoname == "autoincrement": - self.allow_rename = 0 - def validate_name(self, name=None): if not name: name = self.name @@ -887,7 +886,7 @@ def validate_series(dt, autoname=None, name=None): autoname and (not autoname.startswith("field:")) and (not autoname.startswith("eval:")) - and (not autoname.lower() in ("prompt", "hash")) + and (autoname.lower() not in ("prompt", "hash")) and (not autoname.startswith("naming_series:")) and (not autoname.startswith("format:")) ): @@ -904,6 +903,51 @@ def validate_series(dt, autoname=None, name=None): frappe.throw(_("Series {0} already used in {1}").format(prefix, used_in[0][0])) +def check_if_can_change_name_type(dt: DocType, raise_err: bool = True) -> bool: + def get_autoname_before_save(doctype: str, to_be_customized_dt: str) -> str: + if doctype == "Customize Form": + property_value = frappe.db.get_value( + "Property Setter", {"doc_type": to_be_customized_dt, "property": "autoname"}, "value" + ) + + # initially no property setter is set, + # hence getting autoname value from the doctype itself + if not property_value: + return frappe.db.get_value("DocType", to_be_customized_dt, "autoname") or "" + + return property_value + + return getattr(dt.get_doc_before_save(), "autoname", "") + + doctype_name = dt.doc_type if dt.doctype == "Customize Form" else dt.name + + if not dt.is_new(): + autoname_before_save = get_autoname_before_save(dt.doctype, doctype_name) + is_autoname_autoincrement = dt.autoname == "autoincrement" + + if ( + is_autoname_autoincrement + and autoname_before_save != "autoincrement" + or (not is_autoname_autoincrement and autoname_before_save == "autoincrement") + ): + if not frappe.get_all(doctype_name, limit=1): + # allow changing the column type if there is no data + return True + + if raise_err: + frappe.throw( + _("Can only change to/from Autoincrement naming rule when there is no data in the doctype") + ) + + return False + + +def change_name_column_type(doctype_name: str, type: str) -> None: + return frappe.db.change_column_type( + doctype_name, "name", type, True if frappe.db.db_type == "mariadb" else False + ) + + def validate_links_table_fieldnames(meta): """Validate fieldnames in Links table""" if not meta.links or frappe.flags.in_patch or frappe.flags.in_fixtures: diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py index 7b4806da59..59475a95a7 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -524,18 +524,33 @@ class TestDocType(unittest.TestCase): dt.delete() def test_autoincremented_doctype_transition(self): - frappe.delete_doc("testy_autoinc_dt") + frappe.delete_doc_if_exists("DocType", "testy_autoinc_dt") dt = new_doctype("testy_autoinc_dt", autoname="autoincrement").insert(ignore_permissions=True) dt.autoname = "hash" + dt.save(ignore_permissions=True) + + dt_data = frappe.get_doc({"doctype": dt.name, "some_fieldname": "test data"}).insert( + ignore_permissions=True + ) + + dt.autoname = "autoincrement" + try: dt.save(ignore_permissions=True) except frappe.ValidationError as e: - self.assertEqual(e.args[0], "Cannot change to/from Autoincrement naming rule") + self.assertEqual( + e.args[0], + "Can only change to/from Autoincrement naming rule when there is no data in the doctype", + ) else: - self.fail("Shouldnt be possible to transition autoincremented doctype to any other naming rule") + self.fail( + """Shouldn't be possible to transition to/from autoincremented doctype + when data is present in doctype""" + ) finally: # cleanup + dt_data.delete(ignore_permissions=True) dt.delete(ignore_permissions=True) def test_json_field(self): diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json index 6563cae94c..a0bc994c45 100644 --- a/frappe/custom/doctype/customize_form/customize_form.json +++ b/frappe/custom/doctype/customize_form/customize_form.json @@ -287,7 +287,7 @@ "label": "Naming" }, { - "description": "Naming Options:\n
  1. field:[fieldname] - By Field
  2. naming_series: - By Naming Series (field called naming_series must be present)
  3. Prompt - Prompt user for a name
  4. [series] - Series by prefix (separated by a dot); for example PRE.#####
  5. \n
  6. format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
", + "description": "Naming Options:\n
  1. field:[fieldname] - By Field
  2. \n
  3. autoincrement - Uses Databases' Auto Increment feature
  4. naming_series: - By Naming Series (field called naming_series must be present
  5. Prompt - Prompt user for a name
  6. [series] - Series by prefix (separated by a dot); for example PRE.#####
  7. \n
  8. format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
", "fieldname": "autoname", "fieldtype": "Data", "label": "Auto Name" @@ -319,7 +319,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-01-07 16:07:06.196534", + "modified": "2022-04-21 15:36:16.772277", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form", diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index e7187db0d6..262542fd4b 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -11,7 +11,9 @@ import frappe import frappe.translate from frappe import _ from frappe.core.doctype.doctype.doctype import ( + change_name_column_type, check_email_append_to, + check_if_can_change_name_type, validate_fields_for_doctype, validate_series, ) @@ -159,7 +161,9 @@ class CustomizeForm(Document): def save_customization(self): if not self.doc_type: return + validate_series(self, self.autoname, self.doc_type) + can_change_name_type = check_if_can_change_name_type(self) self.flags.update_db = False self.flags.rebuild_doctype_for_global_search = False self.set_property_setters() @@ -168,6 +172,12 @@ class CustomizeForm(Document): validate_fields_for_doctype(self.doc_type) check_email_append_to(self) + if can_change_name_type: + change_name_column_type( + self.doc_type, + "bigint" if self.autoname == "autoincrement" else f"varchar({frappe.db.VARCHAR_LEN})", + ) + if self.flags.update_db: frappe.db.updatedb(self.doc_type) diff --git a/frappe/database/database.py b/frappe/database/database.py index fb631951fa..ca4b5a5310 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -1246,6 +1246,21 @@ class Database(object): values_to_insert = values[start_index : start_index + chunk_size] query.columns(fields).insert(*values_to_insert).run() + def create_sequence(self, *args, **kwargs): + from frappe.database.sequence import create_sequence + + return create_sequence(*args, **kwargs) + + def set_next_sequence_val(self, *args, **kwargs): + from frappe.database.sequence import set_next_val + + set_next_val(*args, **kwargs) + + def get_next_sequence_val(self, *args, **kwargs): + from frappe.database.sequence import get_next_val + + return get_next_val(*args, **kwargs) + def enqueue_jobs_after_commit(): from frappe.utils.background_jobs import execute_job, get_queue diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index 0f5410a403..28d78471d2 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -149,7 +149,7 @@ class MariaDBDatabase(Database): ) -> Union[List, Tuple]: table_name = get_table_name(doctype) null_constraint = "NOT NULL" if not nullable else "" - return self.sql(f"ALTER TABLE `{table_name}` MODIFY `{column}` {type} {null_constraint}") + return self.sql_ddl(f"ALTER TABLE `{table_name}` MODIFY `{column}` {type} {null_constraint}") # exception types @staticmethod diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py index 7c95e9ffcb..784fa23c13 100644 --- a/frappe/database/mariadb/schema.py +++ b/frappe/database/mariadb/schema.py @@ -1,7 +1,6 @@ import frappe from frappe import _ from frappe.database.schema import DBTable -from frappe.database.sequence import create_sequence from frappe.model import log_types @@ -48,7 +47,7 @@ class MariaDBTable(DBTable): # By default the cache is 1000 which will mess up the sequence when # using the system after a restore. # issue link: https://jira.mariadb.org/browse/MDEV-21786 - create_sequence(self.doctype, check_not_exists=True, cache=50) + frappe.db.create_sequence(self.doctype, check_not_exists=True, cache=50) # NOTE: not used nextval func as default as the ability to restore # database with sequences has bugs in mariadb and gives a scary error. diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index 4cd6ab9873..d69e0bea94 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -213,7 +213,11 @@ class PostgresDatabase(Database): ) -> Union[List, Tuple]: table_name = get_table_name(doctype) null_constraint = "SET NOT NULL" if not nullable else "DROP NOT NULL" - return self.sql( + + # postgres allows ddl in transactions but since we've currently made + # things same as mariadb (raising exception on ddl commands if the transaction has any writes), + # hence using sql_ddl here for committing and then moving forward. + return self.sql_ddl( f"""ALTER TABLE "{table_name}" ALTER COLUMN "{column}" TYPE {type}, ALTER COLUMN "{column}" {null_constraint}""" @@ -382,12 +386,10 @@ def modify_query(query): # drop .0 from decimals and add quotes around them # # >>> query = "c='abcd' , a >= 45, b = -45.0, c = 40, d=4500.0, e=3500.53, f=40psdfsd, g=9092094312, h=12.00023" - # >>> re.sub(r"([=><]+)\s*(?!\d+[a-zA-Z])(?![+-]?\d+\.\d\d+)([+-]?\d+)(\.0)?", r"\1 '\2'", query) + # >>> re.sub(r"([=><]+)\s*([+-]?\d+)(\.0)?(?![a-zA-Z\.\d])", r"\1 '\2'", query) # "c='abcd' , a >= '45', b = '-45', c = '40', d= '4500', e=3500.53, f=40psdfsd, g= '9092094312', h=12.00023 - query = re.sub( - r"([=><]+)\s*(?!\d+[a-zA-Z])(?![+-]?\d+\.\d\d+)([+-]?\d+)(\.0)?", r"\1 '\2'", query - ) + query = re.sub(r"([=><]+)\s*([+-]?\d+)(\.0)?(?![a-zA-Z\.\d])", r"\1 '\2'", query) return query diff --git a/frappe/database/postgres/schema.py b/frappe/database/postgres/schema.py index 3432c8b548..2abd5f37c7 100644 --- a/frappe/database/postgres/schema.py +++ b/frappe/database/postgres/schema.py @@ -1,7 +1,6 @@ import frappe from frappe import _ from frappe.database.schema import DBTable, get_definition -from frappe.database.sequence import create_sequence from frappe.model import log_types from frappe.utils import cint, flt @@ -39,7 +38,7 @@ class PostgresTable(DBTable): # Since we're opening and closing connections for every transaction this results in skipping the cache # to the next non-cached value hence not using cache in postgres. # ref: https://stackoverflow.com/questions/21356375/postgres-9-0-4-sequence-skipping-numbers - create_sequence(self.doctype, check_not_exists=True) + frappe.db.create_sequence(self.doctype, check_not_exists=True) name_column = "name bigint primary key" # TODO: set docstatus length diff --git a/frappe/database/sequence.py b/frappe/database/sequence.py index c4789dbdaf..ede4689485 100644 --- a/frappe/database/sequence.py +++ b/frappe/database/sequence.py @@ -5,6 +5,7 @@ def create_sequence( doctype_name: str, *, slug: str = "_id_seq", + temporary=False, check_not_exists: bool = False, cycle: bool = False, cache: int = 0, @@ -14,7 +15,7 @@ def create_sequence( max_value: int = 0, ) -> str: - query = "create sequence" + query = "create sequence" if not temporary else "create temporary sequence" sequence_name = scrub(doctype_name + slug) if check_not_exists: @@ -22,29 +23,29 @@ def create_sequence( query += f" {sequence_name}" - if cache: - query += f" cache {cache}" - else: - # in postgres, the default is cache 1 - if db.db_type == "mariadb": - query += " nocache" - - if start_value: - # default is 1 - query += f" start with {start_value}" - if increment_by: # default is 1 query += f" increment by {increment_by}" if min_value: # default is 1 - query += f" min value {min_value}" + query += f" minvalue {min_value}" if max_value: - query += f" max value {max_value}" + query += f" maxvalue {max_value}" + + if start_value: + # default is 1 + query += f" start {start_value}" + + # in postgres, the default is cache 1 / no cache + if cache: + query += f" cache {cache}" + elif db.db_type == "mariadb": + query += " nocache" if not cycle: + # in postgres, default is no cycle if db.db_type == "mariadb": query += " nocycle" else: @@ -56,21 +57,23 @@ def create_sequence( def get_next_val(doctype_name: str, slug: str = "_id_seq") -> int: - if db.db_type == "postgres": - return db.sql(f"select nextval('\"{scrub(doctype_name + slug)}\"')")[0][0] - return db.sql(f"select nextval(`{scrub(doctype_name + slug)}`)")[0][0] + return db.multisql( + { + "postgres": f"select nextval('\"{scrub(doctype_name + slug)}\"')", + "mariadb": f"select nextval(`{scrub(doctype_name + slug)}`)", + } + )[0][0] def set_next_val( doctype_name: str, next_val: int, *, slug: str = "_id_seq", is_val_used: bool = False ) -> None: - if not is_val_used: - is_val_used = 0 if db.db_type == "mariadb" else "f" - else: - is_val_used = 1 if db.db_type == "mariadb" else "t" + is_val_used = "false" if not is_val_used else "true" - if db.db_type == "postgres": - db.sql(f"SELECT SETVAL('\"{scrub(doctype_name + slug)}\"', {next_val}, '{is_val_used}')") - else: - db.sql(f"SELECT SETVAL(`{scrub(doctype_name + slug)}`, {next_val}, {is_val_used})") + db.multisql( + { + "postgres": f"SELECT SETVAL('\"{scrub(doctype_name + slug)}\"', {next_val}, {is_val_used})", + "mariadb": f"SELECT SETVAL(`{scrub(doctype_name + slug)}`, {next_val}, {is_val_used})", + } + ) diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index a7d9536ebc..acb63b5bfa 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -213,7 +213,7 @@ class DatabaseQuery(object): # left join parent, child tables for child in self.tables[1:]: - parent_name = self.cast_name(f"{self.tables[0]}.name") + parent_name = cast_name(f"{self.tables[0]}.name") args.tables += f" {self.join} {child} on ({child}.parent = {parent_name})" if self.grouped_or_conditions: @@ -225,6 +225,7 @@ class DatabaseQuery(object): args.conditions += (" or " if args.conditions else "") + " or ".join(self.or_conditions) self.set_field_tables() + self.cast_name_fields() fields = [] @@ -385,16 +386,8 @@ class DatabaseQuery(object): ] # add tables from fields if self.fields: - for i, field in enumerate(self.fields): - # add cast in locate/strpos - func_found = False - for func in sql_functions: - if func in field.lower(): - self.fields[i] = self.cast_name(field, func) - func_found = True - break - - if func_found or not ("tab" in field and "." in field): + for field in self.fields: + if not ("tab" in field and "." in field) or any(x for x in sql_functions if x in field): continue table_name = field.split(".")[0] @@ -406,38 +399,6 @@ class DatabaseQuery(object): if table_name not in self.tables: self.append_table(table_name) - def cast_name( - self, - column: str, - sql_function: str = "", - ) -> str: - if frappe.db.db_type == "postgres": - if "name" in column.lower(): - if "cast(" not in column.lower() or "::" not in column: - if not sql_function: - return f"cast({column} as varchar)" - - elif sql_function == "locate(": - return re.sub( - r"locate\(([^,]+),([^)]+)\)", - r"locate(\1, cast(\2 as varchar))", - column, - flags=re.IGNORECASE, - ) - - elif sql_function == "strpos(": - return re.sub( - r"strpos\(([^,]+),([^)]+)\)", - r"strpos(cast(\1 as varchar), \2)", - column, - flags=re.IGNORECASE, - ) - - elif sql_function == "ifnull(": - return re.sub(r"ifnull\(([^,]+)", r"ifnull(cast(\1 as varchar)", column, flags=re.IGNORECASE) - - return column - def append_table(self, table_name): self.tables.append(table_name) doctype = table_name[4:-1] @@ -462,6 +423,10 @@ class DatabaseQuery(object): if "." not in field and not _in_standard_sql_methods(field): self.fields[idx] = f"{self.tables[0]}.{field}" + def cast_name_fields(self): + for i, field in enumerate(self.fields): + self.fields[i] = cast_name(field) + def get_table_columns(self): try: return get_table_columns(self.doctype) @@ -541,10 +506,7 @@ class DatabaseQuery(object): if tname not in self.tables: self.append_table(tname) - if "ifnull(" in f.fieldname: - column_name = self.cast_name(f.fieldname, "ifnull(") - else: - column_name = self.cast_name(f"{tname}.`{f.fieldname}`") + column_name = cast_name(f.fieldname if "ifnull(" in f.fieldname else f"{tname}.`{f.fieldname}`") if f.operator.lower() in additional_filters_config: f.update(get_additional_filter_field(additional_filters_config, f, f.value)) @@ -766,7 +728,10 @@ class DatabaseQuery(object): return self.match_filters def get_share_condition(self): - return f"`tab{self.doctype}`.name in ({', '.join(frappe.db.escape(s, percent=False) for s in self.shared)})" + return ( + cast_name(f"`tab{self.doctype}`.name") + + f" in ({', '.join(frappe.db.escape(s, percent=False) for s in self.shared)})" + ) def add_user_permissions(self, user_permissions): meta = frappe.get_meta(self.doctype) @@ -794,7 +759,9 @@ class DatabaseQuery(object): if frappe.get_system_settings("apply_strict_user_permissions"): condition = "" else: - empty_value_condition = f"ifnull(`tab{self.doctype}`.`{df.get('fieldname')}`, '')=''" + empty_value_condition = cast_name( + f"ifnull(`tab{self.doctype}`.`{df.get('fieldname')}`, '')=''" + ) condition = empty_value_condition + " or " for permission in user_permission_values: @@ -815,7 +782,7 @@ class DatabaseQuery(object): if docs: values = ", ".join(frappe.db.escape(doc, percent=False) for doc in docs) - condition += f"`tab{self.doctype}`.`{df.get('fieldname')}` in ({values})" + condition += cast_name(f"`tab{self.doctype}`.`{df.get('fieldname')}`") + f" in ({values})" match_conditions.append(f"({condition})") match_filters[df.get("options")] = docs @@ -933,6 +900,40 @@ class DatabaseQuery(object): update_user_settings(self.doctype, user_settings) +def cast_name(column: str) -> str: + """Casts name field to varchar for postgres + + Handles majorly 4 cases: + 1. locate + 2. strpos + 3. ifnull + 4. coalesce + + Uses regex substitution. + + Example: + input - "ifnull(`tabBlog Post`.`name`, '')=''" + output - "ifnull(cast(`tabBlog Post`.`name` as varchar), '')=''" """ + + if frappe.db.db_type == "mariadb": + return column + + kwargs = {"string": column, "flags": re.IGNORECASE} + if "cast(" not in column.lower() and "::" not in column: + if re.search(r"locate\(([^,]+),\s*([`\"]?name[`\"]?)\s*\)", **kwargs): + return re.sub( + r"locate\(([^,]+),\s*([`\"]?name[`\"]?)\)", r"locate(\1, cast(\2 as varchar))", **kwargs + ) + + elif match := re.search(r"(strpos|ifnull|coalesce)\(\s*([`\"]?name[`\"]?)\s*,", **kwargs): + func = match.groups()[0] + return re.sub(rf"{func}\(\s*([`\"]?name[`\"]?)\s*,", rf"{func}(cast(\1 as varchar),", **kwargs) + + return re.sub(r"([`\"]?tab[\w`\" -]+\.[`\"]?name[`\"]?)(?!\w)", r"cast(\1 as varchar)", **kwargs) + + return column + + def check_parent_permission(parent, child_doctype): if parent: # User may pass fake parent and get the information from the child table diff --git a/frappe/model/naming.py b/frappe/model/naming.py index aa502f5a4c..bb93244a66 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -14,6 +14,11 @@ if TYPE_CHECKING: from frappe.model.meta import Meta +# NOTE: This is used to keep track of status of sites +# whether `log_types` have autoincremented naming set for the site or not. +autoincremented_site_status_map = {} + + def set_new_name(doc): """ Sets the `name` property for the document based on various rules. @@ -35,9 +40,7 @@ def set_new_name(doc): doc.name = None if is_autoincremented(doc.doctype, meta): - from frappe.database.sequence import get_next_val - - doc.name = get_next_val(doc.doctype) + doc.name = frappe.db.get_next_sequence_val(doc.doctype) return if getattr(doc, "amended_from", None): @@ -72,12 +75,11 @@ def set_new_name(doc): doc.name = validate_name(doc.doctype, doc.name, meta.get_field("name_case")) -def is_autoincremented(doctype: str, meta: "Meta" = None): +def is_autoincremented(doctype: str, meta: Optional["Meta"] = None) -> bool: + """Checks if the doctype has autoincrement autoname set""" + if doctype in log_types: - if ( - frappe.local.autoincremented_status_map.get(frappe.local.site) is None - or frappe.local.autoincremented_status_map[frappe.local.site] == -1 - ): + if autoincremented_site_status_map.get(frappe.local.site) is None: if ( frappe.db.sql( f"""select data_type FROM information_schema.columns @@ -85,22 +87,19 @@ def is_autoincremented(doctype: str, meta: "Meta" = None): )[0][0] == "bigint" ): - frappe.local.autoincremented_status_map[frappe.local.site] = 1 + autoincremented_site_status_map[frappe.local.site] = 1 return True else: - frappe.local.autoincremented_status_map[frappe.local.site] = 0 + autoincremented_site_status_map[frappe.local.site] = 0 - elif frappe.local.autoincremented_status_map[frappe.local.site]: + elif autoincremented_site_status_map[frappe.local.site]: return True else: if not meta: meta = frappe.get_meta(doctype) - if getattr(meta, "issingle", False): - return False - - if meta.autoname == "autoincrement": + if not getattr(meta, "issingle", False) and meta.autoname == "autoincrement": return True return False @@ -329,11 +328,9 @@ def validate_name(doctype: str, name: Union[int, str], case: Optional[str] = Non if isinstance(name, int): if is_autoincremented(doctype): - from frappe.database.sequence import set_next_val - - # this will set the sequence val to be the provided name and set it to be used - # so that the sequence will start from the next val of the setted val(name) - set_next_val(doctype, name, is_val_used=True) + # this will set the sequence value to be the provided name/value and set it to be used + # so that the sequence will start from the next value + frappe.db.set_next_sequence_val(doctype, name, is_val_used=True) return name frappe.throw(_("Invalid name type (integer) for varchar name column"), frappe.NameError) diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js index b7fe61b385..5a5af389ee 100644 --- a/frappe/public/js/frappe/form/controls/base_input.js +++ b/frappe/public/js/frappe/form/controls/base_input.js @@ -65,11 +65,7 @@ frappe.ui.form.ControlInput = class ControlInput extends frappe.ui.form.Control }; var update_input = function() { - if (me.doctype && me.docname) { - me.set_input(me.value); - } else { - me.set_input(me.value || null); - } + me.set_input(me.value); }; if (me.disp_status != "None") { diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index c56ffc592d..2572f7b2e3 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -248,7 +248,7 @@ frappe.ui.form.Form = class FrappeForm { // on main doc frappe.model.on(me.doctype, "*", function(fieldname, value, doc, skip_dirty_trigger=false) { // set input - if (cstr(doc.name) === me.docname) { + if (doc.name == me.docname) { if (!skip_dirty_trigger) { me.dirty(); } @@ -273,7 +273,7 @@ frappe.ui.form.Form = class FrappeForm { // using $.each to preserve df via closure $.each(table_fields, function(i, df) { frappe.model.on(df.options, "*", function(fieldname, value, doc) { - if(doc.parent===me.docname && doc.parentfield===df.fieldname) { + if (doc.parent == me.docname && doc.parentfield === df.fieldname) { me.dirty(); me.fields_dict[df.fieldname].grid.set_value(fieldname, value, doc); return me.script_manager.trigger(fieldname, doc.doctype, doc.name); @@ -356,7 +356,7 @@ frappe.ui.form.Form = class FrappeForm { // check permissions if (!this.has_read_permission()) { - frappe.show_not_permitted(__(this.doctype) + " " + __(this.docname)); + frappe.show_not_permitted(__(this.doctype) + " " + __(cstr(this.docname))); return; } diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 3b95a4b3f1..ad7a1181f6 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -403,7 +403,7 @@ $.extend(frappe.model, { } }); } else { - if(typeof filters==="string" && locals[doctype] && locals[doctype][filters]) { + if (["number", "string"].includes(typeof filters) && locals[doctype] && locals[doctype][filters]) { return locals[doctype][filters][fieldname]; } else { var l = frappe.get_list(doctype, filters); diff --git a/frappe/tests/test_db_query.py b/frappe/tests/test_db_query.py index c1ff1a1a15..19a8c445f8 100644 --- a/frappe/tests/test_db_query.py +++ b/frappe/tests/test_db_query.py @@ -61,10 +61,12 @@ class TestReportview(unittest.TestCase): in build_match_conditions(as_condition=False) ) # get as conditions - self.assertEqual( - build_match_conditions(as_condition=True), - """(((ifnull(`tabBlog Post`.`name`, '')='' or `tabBlog Post`.`name` in ('-test-blog-post-1', '-test-blog-post'))))""", - ) + if frappe.db.db_type == "mariadb": + assertion_string = """(((ifnull(`tabBlog Post`.`name`, '')='' or `tabBlog Post`.`name` in ('-test-blog-post-1', '-test-blog-post'))))""" + else: + assertion_string = """(((ifnull(cast(`tabBlog Post`.`name` as varchar), '')='' or cast(`tabBlog Post`.`name` as varchar) in ('-test-blog-post-1', '-test-blog-post'))))""" + + self.assertEqual(build_match_conditions(as_condition=True), assertion_string) frappe.set_user("Administrator") @@ -619,19 +621,22 @@ class TestReportview(unittest.TestCase): def test_cast_name(self): from frappe.core.doctype.doctype.test_doctype import new_doctype + frappe.delete_doc_if_exists("DocType", "autoinc_dt_test") dt = new_doctype("autoinc_dt_test", autoname="autoincrement").insert(ignore_permissions=True) query = DatabaseQuery("autoinc_dt_test").execute( - fields=["locate('1', `tabautoinc_dt_test`.`name`)", "`tabautoinc_dt_test`.`name`"], + fields=["locate('1', `tabautoinc_dt_test`.`name`)", "name", "locate('1', name)"], filters={"name": 1}, run=False, ) if frappe.db.db_type == "postgres": - self.assertTrue('strpos( cast( "tabautoinc_dt_test"."name" as varchar), \'1\')' in query) + self.assertTrue('strpos( cast("tabautoinc_dt_test"."name" as varchar), \'1\')' in query) + self.assertTrue("strpos( cast(name as varchar), '1')" in query) self.assertTrue('where cast("tabautoinc_dt_test"."name" as varchar) = \'1\'' in query) else: self.assertTrue("locate('1', `tabautoinc_dt_test`.`name`)" in query) + self.assertTrue("locate('1', name)" in query) self.assertTrue("where `tabautoinc_dt_test`.`name` = 1" in query) dt.delete(ignore_permissions=True) diff --git a/frappe/tests/test_sequence.py b/frappe/tests/test_sequence.py new file mode 100644 index 0000000000..a60e4b1ac9 --- /dev/null +++ b/frappe/tests/test_sequence.py @@ -0,0 +1,54 @@ +import psycopg2 +import pymysql + +import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestSequence(FrappeTestCase): + def generate_sequence_name(self) -> str: + return self._testMethodName + "_" + frappe.generate_hash(length=5) + + def test_set_next_val(self): + seq_name = self.generate_sequence_name() + frappe.db.create_sequence(seq_name, check_not_exists=True, temporary=True) + + next_val = frappe.db.get_next_sequence_val(seq_name) + frappe.db.set_next_sequence_val(seq_name, next_val + 1) + self.assertEqual(next_val + 1, frappe.db.get_next_sequence_val(seq_name)) + + next_val = frappe.db.get_next_sequence_val(seq_name) + frappe.db.set_next_sequence_val(seq_name, next_val + 1, is_val_used=True) + self.assertEqual(next_val + 2, frappe.db.get_next_sequence_val(seq_name)) + + def test_create_sequence(self): + seq_name = self.generate_sequence_name() + frappe.db.create_sequence(seq_name, max_value=2, cycle=True, temporary=True) + frappe.db.get_next_sequence_val(seq_name) + frappe.db.get_next_sequence_val(seq_name) + self.assertEqual(1, frappe.db.get_next_sequence_val(seq_name)) + + seq_name = self.generate_sequence_name() + frappe.db.create_sequence(seq_name, max_value=2, temporary=True) + frappe.db.get_next_sequence_val(seq_name) + frappe.db.get_next_sequence_val(seq_name) + + try: + frappe.db.get_next_sequence_val(seq_name) + except pymysql.err.OperationalError as e: + self.assertEqual(e.args[0], 4084) + except psycopg2.errors.SequenceGeneratorLimitExceeded: + pass + else: + self.fail("NEXTVAL didn't raise any error upon sequence's end") + + # without this, we're not able to move further + # as postgres doesn't allow moving further in a transaction + # when an error occurs + frappe.db.rollback() + + seq_name = self.generate_sequence_name() + frappe.db.create_sequence(seq_name, min_value=10, max_value=20, increment_by=5, temporary=True) + self.assertEqual(10, frappe.db.get_next_sequence_val(seq_name)) + self.assertEqual(15, frappe.db.get_next_sequence_val(seq_name)) + self.assertEqual(20, frappe.db.get_next_sequence_val(seq_name)) From 76a6c282b81dfdc81ebf64a1e5cb9bdc0a5b15d6 Mon Sep 17 00:00:00 2001 From: Ritwik Puri Date: Fri, 29 Apr 2022 16:07:40 +0530 Subject: [PATCH 136/139] fix: use proper validations for number card validation method (#16581) --- .../desk/doctype/number_card/number_card.js | 5 +++- .../desk/doctype/number_card/number_card.py | 25 +++++++++++++------ frappe/desk/query_report.py | 4 ++- .../public/js/frappe/utils/dashboard_utils.js | 2 +- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js index f548388a99..79ddb71187 100644 --- a/frappe/desk/doctype/number_card/number_card.js +++ b/frappe/desk/doctype/number_card/number_card.js @@ -27,8 +27,11 @@ frappe.ui.form.on('Number Card', { frm.trigger('set_method_description'); frm.trigger('render_filters_table'); } - frm.trigger('create_add_to_dashboard_button'); frm.trigger('set_parent_document_type'); + + if (!frm.is_new()) { + frm.trigger('create_add_to_dashboard_button'); + } }, create_add_to_dashboard_button: function(frm) { diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py index 370b187ffe..e1b2b19026 100644 --- a/frappe/desk/doctype/number_card/number_card.py +++ b/frappe/desk/doctype/number_card/number_card.py @@ -20,15 +20,24 @@ class NumberCard(Document): self.name = append_number_if_name_exists("Number Card", self.name) def validate(self): - if not self.document_type: - frappe.throw(_("Document type is required to create a number card")) + if self.type == "Document Type": + if not (self.document_type and self.function): + frappe.throw(_("Document Type and Function are required to create a number card")) - if ( - self.document_type - and frappe.get_meta(self.document_type).istable - and not self.parent_document_type - ): - frappe.throw(_("Parent document type is required to create a number card")) + if ( + self.document_type + and frappe.get_meta(self.document_type).istable + and not self.parent_document_type + ): + frappe.throw(_("Parent Document Type is required to create a number card")) + + elif self.type == "Report": + if not (self.report_name and self.report_field and self.function): + frappe.throw(_("Report Name, Report Field and Fucntion are required to create a number card")) + + elif self.type == "Custom": + if not self.method: + frappe.throw(_("Method is required to create a number card")) def on_update(self): if frappe.conf.developer_mode and self.is_standard: diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 894e82d117..a51fd8b1e3 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -58,6 +58,8 @@ def get_report_doc(report_name): def get_report_result(report, filters): + res = None + if report.report_type == "Query Report": res = report.execute_query_report(filters) @@ -84,7 +86,7 @@ def generate_report_result( res = get_report_result(report, filters) or [] columns, result, message, chart, report_summary, skip_total_row = ljust_list(res, 6) - columns = [get_column_as_dict(col) for col in columns] + columns = [get_column_as_dict(col) for col in (columns or [])] report_column_names = [col["fieldname"] for col in columns] # convert to list of dicts diff --git a/frappe/public/js/frappe/utils/dashboard_utils.js b/frappe/public/js/frappe/utils/dashboard_utils.js index 85a4048a47..c4b094b216 100644 --- a/frappe/public/js/frappe/utils/dashboard_utils.js +++ b/frappe/public/js/frappe/utils/dashboard_utils.js @@ -249,7 +249,7 @@ frappe.dashboard_utils = { {args: values} ).then(()=> { let dashboard_route_html = - `${values.dashboard}`; + `${values.dashboard}`; let message = __("{0} {1} added to Dashboard {2}", [doctype, values.name, dashboard_route_html]); From 2e4953b8ecf4d4606d54b7cc6a8bfdea323f0dce Mon Sep 17 00:00:00 2001 From: KrutikaBhatt <65107474+KrutikaBhatt@users.noreply.github.com> Date: Sat, 30 Apr 2022 19:58:52 +0530 Subject: [PATCH 137/139] fix(bug): required meta field is used inplace of mandatory --- frappe/public/js/frappe/ui/sort_selector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/ui/sort_selector.js b/frappe/public/js/frappe/ui/sort_selector.js index 879466e8f7..837454ed09 100644 --- a/frappe/public/js/frappe/ui/sort_selector.js +++ b/frappe/public/js/frappe/ui/sort_selector.js @@ -132,7 +132,7 @@ frappe.ui.SortSelector = class SortSelector { // bold, mandatory and fields that are available in list view meta.fields.forEach(function(df) { if ( - (df.mandatory || df.bold || df.in_list_view) + (df.mandatory || df.bold || df.in_list_view || df.reqd) && frappe.model.is_value_type(df.fieldtype) && frappe.perm.has_perm(me.doctype, df.permlevel, "read") ) { From 693a6a7789be4c578174f2a1d014f3158986c26f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 1 May 2022 11:34:08 +0530 Subject: [PATCH 138/139] fix: `frappe.log_error` arguments while capturing razorpay payment failures --- .../doctype/razorpay_settings/razorpay_settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py index c4ffb74325..cc620aa32c 100644 --- a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py +++ b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py @@ -140,7 +140,7 @@ class RazorpaySettings(Document): headers={"content-type": "application/json"}, ) if not resp.get("id"): - frappe.log_error(str(resp), "Razorpay Failed while creating subscription") + frappe.log_error(message=str(resp), title="Razorpay Failed while creating subscription") except: frappe.log_error(frappe.get_traceback()) # failed @@ -179,7 +179,7 @@ class RazorpaySettings(Document): frappe.flags.status = "created" return kwargs else: - frappe.log_error(str(resp), "Razorpay Failed while creating subscription") + frappe.log_error(message=str(resp), title="Razorpay Failed while creating subscription") except: frappe.log_error(frappe.get_traceback()) @@ -281,7 +281,7 @@ class RazorpaySettings(Document): self.flags.status_changed_to = "Verified" else: - frappe.log_error(str(resp), "Razorpay Payment not authorized") + frappe.log_error(message=str(resp), title="Razorpay Payment not authorized") except: frappe.log_error(frappe.get_traceback()) From 4310a2ecca213193a42065ccde0c7d2fbbd4a807 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 2 May 2022 17:30:21 +0530 Subject: [PATCH 139/139] fix(UX): suggest app-specific password for gmail logins (#16812) --- frappe/email/doctype/email_account/email_account.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frappe/email/doctype/email_account/email_account.js b/frappe/email/doctype/email_account/email_account.js index 54f0d2372d..a357126a48 100644 --- a/frappe/email/doctype/email_account/email_account.js +++ b/frappe/email/doctype/email_account/email_account.js @@ -134,10 +134,11 @@ frappe.ui.form.on("Email Account", { show_gmail_message_for_less_secure_apps: function(frm) { frm.dashboard.clear_headline(); + let msg = __("GMail will only work if you enable 2-step authentication and use app-specific password."); + let cta = __("Read the step by step guide here."); + msg += ` ${cta}`; if (frm.doc.service==="GMail") { - frm.dashboard.set_headline_alert('Gmail will only work if you allow access for less secure \ - apps in Gmail settings. Read this for details'); + frm.dashboard.set_headline_alert(msg); } },