diff --git a/.mergify.yml b/.mergify.yml index b0e11e0e6d..d8f1c09bee 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -7,7 +7,21 @@ pull_request_rules: - status-success=security/snyk - package.json (frappe) - status-success=security/snyk - requirements.txt (frappe) - label!=don't-merge + - label!=squash - "#approved-reviews-by>=1" actions: merge: method: merge + - name: Automatic squash on CI success and review + conditions: + - status-success=Codacy/PR Quality Review + - status-success=Semantic Pull Request + - status-success=continuous-integration/travis-ci/pr + - status-success=security/snyk - package.json (frappe) + - status-success=security/snyk - requirements.txt (frappe) + - label!=don't-merge + - label=squash + - "#approved-reviews-by>=1" + actions: + merge: + method: squash diff --git a/frappe/__init__.py b/frappe/__init__.py index 78e0cdb355..8ea5148873 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -23,7 +23,7 @@ if sys.version[0] == '2': reload(sys) sys.setdefaultencoding("utf-8") -__version__ = '12.0.13' +__version__ = '12.0.14' __title__ = "Frappe Framework" local = Local() diff --git a/frappe/core/doctype/comment/test_comment.py b/frappe/core/doctype/comment/test_comment.py index 2adc5eb899..3cf8fbaa3f 100644 --- a/frappe/core/doctype/comment/test_comment.py +++ b/frappe/core/doctype/comment/test_comment.py @@ -53,5 +53,7 @@ class TestComment(unittest.TestCase): reference_name = test_blog.name ))), 0) + test_blog.delete() + diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index eb4424db30..bfc5ba845a 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -586,9 +586,11 @@ class DocType(Document): if not self.get('is_tree'): return self.add_nestedset_fields() - # set field as mandatory - field = self.meta.get_field('nsm_parent_field') - field.reqd = 1 + + if not self.nsm_parent_field: + field_label = frappe.bold(_("Parent Field (Tree)")) + frappe.throw(_("{0} is a mandatory field").format(field_label), frappe.MandatoryError) + # check if field is valid fieldnames = [df.fieldname for df in self.fields] if self.nsm_parent_field and self.nsm_parent_field not in fieldnames: diff --git a/frappe/core/doctype/file/file.json b/frappe/core/doctype/file/file.json index 9bf7d03512..d9ab504db7 100644 --- a/frappe/core/doctype/file/file.json +++ b/frappe/core/doctype/file/file.json @@ -22,8 +22,6 @@ "column_break_10", "attached_to_name", "attached_to_field", - "lft", - "rgt", "old_parent", "content_hash", "uploaded_to_dropbox", @@ -145,18 +143,6 @@ "label": "Attached To Field", "read_only": 1 }, - { - "fieldname": "lft", - "fieldtype": "Int", - "hidden": 1, - "label": "lft" - }, - { - "fieldname": "rgt", - "fieldtype": "Int", - "hidden": 1, - "label": "rgt" - }, { "fieldname": "old_parent", "fieldtype": "Data", @@ -186,7 +172,7 @@ ], "icon": "fa fa-file", "idx": 1, - "modified": "2019-08-16 16:41:03.086023", + "modified": "2019-08-30 19:46:20.796453", "modified_by": "Administrator", "module": "Core", "name": "File", diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 3c89519faf..fa80d0ed6e 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -26,6 +26,7 @@ from frappe.utils import get_hook_method, get_files_path, random_string, encode, from frappe import _ from frappe import conf from frappe.utils.nestedset import NestedSet +from frappe.model.document import Document from frappe.utils import strip from PIL import Image, ImageOps from six import StringIO, string_types @@ -42,8 +43,7 @@ class FolderNotEmpty(frappe.ValidationError): pass exclude_from_linked_with = True -class File(NestedSet): - nsm_parent_field = 'folder' +class File(Document): no_feed_on_delete = True def before_insert(self): @@ -72,8 +72,6 @@ class File(NestedSet): self.name = frappe.generate_hash("", 10) def after_insert(self): - self.update_parent_folder_size() - if not self.is_folder: self.add_comment_in_reference_doc('Attachment', _('Added {0}').format("{file_name}{icon}".format(**{ @@ -100,7 +98,6 @@ class File(NestedSet): self.validate_file() self.generate_content_hash() - self.set_folder_size() self.validate_url() if frappe.db.exists('File', {'name': self.name, 'is_folder': 0}): @@ -136,31 +133,6 @@ class File(NestedSet): frappe.db.set_value(self.attached_to_doctype, self.attached_to_name, self.attached_to_field, self.file_url) - - def set_folder_size(self): - """Set folder size if folder""" - if self.is_folder and not self.is_new(): - self.file_size = cint(self.get_folder_size()) - self.db_set('file_size', self.file_size) - - for folder in self.get_ancestors(): - frappe.db.set_value("File", folder, "file_size", self.get_folder_size(folder)) - - def get_folder_size(self, folder=None): - """Returns folder size for current folder""" - if not folder: - folder = self.name - - file_size = frappe.db.sql("""select ifnull(sum(file_size), 0) - from tabFile where folder=%s """, (folder))[0][0] - - return file_size - - def update_parent_folder_size(self): - """Update size of parent folder""" - if self.folder and not self.is_folder: # it not home - frappe.get_doc("File", self.folder).set_folder_size() - def set_folder_name(self): """Make parent folders if not exists based on reference doctype and name""" if self.attached_to_doctype and not self.folder: @@ -232,7 +204,6 @@ class File(NestedSet): if self.is_home_folder or self.is_attachments_folder: frappe.throw(_("Cannot delete Home and Attachments folders")) self.check_folder_is_empty() - super(File, self).on_trash() self.call_delete_file() if not self.is_folder: self.add_comment_in_reference_doc('Attachment Removed', _("Removed {0}").format(self.file_name)) @@ -274,9 +245,6 @@ class File(NestedSet): return thumbnail_url - def after_delete(self): - self.update_parent_folder_size() - def check_folder_is_empty(self): """Throw exception if folder is not empty""" files = frappe.get_all("File", filters={"folder": self.name}, fields=("name", "file_name")) @@ -598,7 +566,6 @@ class File(NestedSet): def on_doctype_update(): frappe.db.add_index("File", ["attached_to_doctype", "attached_to_name"]) - frappe.db.add_index("File", ["lft", "rgt"]) def make_home_folder(): home = frappe.get_doc({ @@ -619,12 +586,16 @@ def make_home_folder(): @frappe.whitelist() def get_breadcrumbs(folder): """returns name, file_name of parent folder""" - values = frappe.db.get_value("File", folder, ["lft", "rgt"], as_dict=True) - if not values: - frappe.throw(_("Folder {0} does not exist").format(folder)) + path = folder.split('/') - return frappe.db.sql("""select name, file_name from tabFile - where lft < %s and rgt > %s order by lft asc""", (values.lft, values.rgt), as_dict=1) + folders = [] + for i, _ in enumerate(path): + indexes = range(0, i) + folder = '/'.join([path[i] for i in indexes]) + if folder: + folders.append(folder) + + return [frappe._dict(file_name=f) for f in folders] @frappe.whitelist() def create_new_folder(file_name, folder): diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py index e3c48e7469..be68f1fee2 100644 --- a/frappe/core/doctype/file/test_file.py +++ b/frappe/core/doctype/file/test_file.py @@ -183,7 +183,7 @@ class TestFile(unittest.TestCase): def delete_test_data(self): for f in frappe.db.sql('''select name, file_name from tabFile where - is_home_folder = 0 and is_attachments_folder = 0 order by rgt-lft asc'''): + is_home_folder = 0 and is_attachments_folder = 0 order by creation desc'''): frappe.delete_doc("File", f[0]) @@ -212,11 +212,8 @@ class TestFile(unittest.TestCase): def tests_after_upload(self): self.assertEqual(self.saved_folder, _("Home/Test Folder 1")) - - folder_size = frappe.db.get_value("File", _("Home/Test Folder 1"), "file_size") - saved_file_size = frappe.db.get_value("File", self.saved_name, "file_size") - - self.assertEqual(folder_size, saved_file_size) + file_folder = frappe.db.get_value("File", self.saved_name, "folder") + self.assertEqual(file_folder, _("Home/Test Folder 1")) def test_file_copy(self): @@ -227,8 +224,6 @@ class TestFile(unittest.TestCase): file = frappe.get_doc("File", {"file_name": "file_copy.txt"}) self.assertEqual(_("Home/Test Folder 2"), file.folder) - self.assertEqual(frappe.db.get_value("File", _("Home/Test Folder 2"), "file_size"), file.file_size) - self.assertEqual(frappe.db.get_value("File", _("Home/Test Folder 1"), "file_size"), 0) def test_folder_copy(self): @@ -251,8 +246,6 @@ class TestFile(unittest.TestCase): frappe.get_doc("File", file_copy_txt).delete() self.assertEqual(_("Home/Test Folder 1/Test Folder 3"), file.folder) - self.assertEqual(frappe.db.get_value("File", _("Home/Test Folder 1"), "file_size"), file.file_size) - self.assertEqual(frappe.db.get_value("File", _("Home/Test Folder 2"), "file_size"), 0) def test_default_folder(self): diff --git a/frappe/core/doctype/prepared_report/prepared_report.json b/frappe/core/doctype/prepared_report/prepared_report.json index ec89c6327a..ab6650d9e3 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.json +++ b/frappe/core/doctype/prepared_report/prepared_report.json @@ -1,463 +1,128 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "REP.#####", - "beta": 0, - "creation": "2018-06-25 18:39:11.152960", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "autoname": "REP.#####", + "creation": "2018-06-25 18:39:11.152960", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "report_name", + "ref_report_doctype", + "status", + "column_break_4", + "report_start_time", + "report_end_time", + "section_break_7", + "error_message", + "filters_sb", + "filters", + "filter_values", + "columns" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "report_name", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Report Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "report_name", + "fieldtype": "Data", + "hidden": 1, + "label": "Report Name", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "ref_report_doctype", - "fieldtype": "Link", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Ref Report DocType", - "length": 0, - "no_copy": 0, - "options": "Report", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "ref_report_doctype", + "fieldtype": "Link", + "hidden": 1, + "label": "Ref Report DocType", + "options": "Report", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Queued", - "fetch_if_empty": 0, - "fieldname": "status", - "fieldtype": "Select", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Status", - "length": 0, - "no_copy": 0, - "options": "Error\nQueued\nCompleted", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "Queued", + "fieldname": "status", + "fieldtype": "Select", + "hidden": 1, + "in_list_view": 1, + "label": "Status", + "options": "Error\nQueued\nCompleted", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_4", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "report_start_time", - "fieldtype": "Datetime", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Report Start Time", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "report_start_time", + "fieldtype": "Datetime", + "label": "Report Start Time", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "report_end_time", - "fieldtype": "Datetime", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Report End Time", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "report_end_time", + "fieldtype": "Datetime", + "label": "Report End Time", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.status == 'Error'", - "fetch_if_empty": 0, - "fieldname": "section_break_7", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:doc.status == 'Error'", + "fieldname": "section_break_7", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "error_message", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Error Message", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "error_message", + "fieldtype": "Text", + "label": "Error Message", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "filters_sb", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Filters", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "filters_sb", + "fieldtype": "Section Break", + "label": "Filters" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "filters", - "fieldtype": "Small Text", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Filters", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "filters", + "fieldtype": "Small Text", + "hidden": 1, + "label": "Filters", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "filter_values", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Filter Values", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "filter_values", + "fieldtype": "HTML", + "label": "Filter Values" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "columns", - "fieldtype": "Code", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Columns", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "columns", + "fieldtype": "Code", + "hidden": 1, + "label": "Columns", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-04-19 12:39:47.211516", - "modified_by": "Administrator", - "module": "Core", - "name": "Prepared Report", - "name_case": "", - "owner": "Administrator", + ], + "in_create": 1, + "modified": "2019-09-18 04:00:55.644257", + "modified_by": "Administrator", + "module": "Core", + "name": "Prepared Report", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "report_name", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "report_name", + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/core/doctype/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py index 29c069515f..989a99511a 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.py +++ b/frappe/core/doctype/prepared_report/prepared_report.py @@ -4,32 +4,25 @@ from __future__ import unicode_literals -import base64 + import json -import io import frappe -from frappe.model.document import Document -from frappe.utils.background_jobs import enqueue -from frappe.desk.query_report import generate_report_result, get_columns_dict -from frappe.core.doctype.file.file import remove_all -from frappe.utils.csvutils import to_csv, read_csv_content_from_attached_file from frappe.desk.form.load import get_attachments +from frappe.desk.query_report import generate_report_result +from frappe.model.document import Document from frappe.utils import gzip_compress, gzip_decompress -from six import PY2 -from frappe.utils import encode +from frappe.utils.background_jobs import enqueue +from frappe.core.doctype.file.file import remove_all + class PreparedReport(Document): - def before_insert(self): self.status = "Queued" self.report_start_time = frappe.utils.now() def enqueue_report(self): - enqueue( - run_background, - prepared_report=self.name, timeout=6000 - ) + enqueue(run_background, prepared_report=self.name, timeout=6000) def on_trash(self): remove_all("PreparedReport", self.name, from_delete=True) @@ -42,14 +35,18 @@ def run_background(prepared_report): try: report.custom_columns = [] - if report.report_type == 'Custom Report': + if report.report_type == "Custom Report": custom_report_doc = report reference_report = custom_report_doc.reference_report report = frappe.get_doc("Report", reference_report) report.custom_columns = custom_report_doc.json - result = generate_report_result(report, filters=instance.filters, user=instance.owner) - create_json_gz_file(result['result'], 'Prepared Report', instance.name) + result = generate_report_result( + report=report, + filters=instance.filters, + user=instance.owner + ) + create_json_gz_file(result["result"], "Prepared Report", instance.name) instance.status = "Completed" instance.columns = json.dumps(result["columns"]) @@ -64,8 +61,11 @@ def run_background(prepared_report): instance.save(ignore_permissions=True) frappe.publish_realtime( - 'report_generated', - {"report_name": instance.report_name, "name": instance.name}, + "report_generated", + { + "report_name": instance.report_name, + "name": instance.name + }, user=frappe.session.user ) @@ -73,7 +73,9 @@ def run_background(prepared_report): def create_json_gz_file(data, dt, dn): # Storing data in CSV file causes information loss # Reports like P&L Statement were completely unsuable because of this - json_filename = '{0}.json.gz'.format(frappe.utils.data.format_datetime(frappe.utils.now(), "Y-m-d-H:M")) + json_filename = "{0}.json.gz".format( + frappe.utils.data.format_datetime(frappe.utils.now(), "Y-m-d-H:M") + ) encoded_content = frappe.safe_encode(frappe.as_json(data)) compressed_content = gzip_compress(encoded_content) @@ -87,10 +89,11 @@ def create_json_gz_file(data, dt, dn): }) _file.save() + @frappe.whitelist() def download_attachment(dn): attachment = get_attachments("Prepared Report", dn)[0] frappe.local.response.filename = attachment.file_name[:-2] - attached_file = frappe.get_doc('File', attachment.name) + attached_file = frappe.get_doc("File", attachment.name) frappe.local.response.filecontent = gzip_decompress(attached_file.get_content()) frappe.local.response.type = "binary" diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py index 1467eafc5d..7ce2537da3 100644 --- a/frappe/core/doctype/role/role.py +++ b/frappe/core/doctype/role/role.py @@ -31,7 +31,7 @@ def get_emails_from_role(role): for user in users: user_email, enabled = frappe.db.get_value("User", user.parent, ["email", "enabled"]) - if enabled: + if enabled and user_email not in ["admin@example.com", "guest@example.com"]: emails.append(user_email) return emails \ No newline at end of file diff --git a/frappe/database/database.py b/frappe/database/database.py index 6702ce2a5e..412051c76f 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -758,7 +758,10 @@ class Database(object): def field_exists(self, dt, fn): """Return true of field exists.""" - return self.sql("select name from tabDocField where fieldname=%s and parent=%s", (dt, fn)) + return self.exists('DocField', { + 'fieldname': fn, + 'parent': dt + }) def table_exists(self, doctype): """Returns True if table for given doctype exists.""" diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index e3e408eeb1..dd984625fd 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -213,6 +213,8 @@ def get_labels(fields, doctype): for key in fields: key = key.split(" as ")[0] + if key.startswith(('count(', 'sum(', 'avg(')): continue + if "." in key: parenttype, fieldname = key.split(".")[0][4:-1], key.split(".")[1].strip("`") else: diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index 8032e020d7..f8d3002a2c 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -309,9 +309,7 @@ def evaluate_alert(doc, alert, event): else: raise db_value = parse_val(db_value) - if (doc.get(alert.value_changed) == db_value) or \ - (not db_value and not doc.get(alert.value_changed)): - + if (doc.get(alert.value_changed) == db_value) or (not db_value and not doc.get(alert.value_changed)): return # value not changed if event != "Value Change" and not doc.is_new(): diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 7480970711..6eb2efce6b 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -38,8 +38,8 @@ def get_controller(doctype): or ["Core", False] if custom: - if frappe.db.field_exists(doctype, "is_tree"): - is_tree = frappe.db.get_value("DocType", doctype, ("is_tree"), cache=True) + if frappe.db.field_exists("DocType", "is_tree"): + is_tree = frappe.db.get_value("DocType", doctype, "is_tree", cache=True) else: is_tree = False _class = NestedSet if is_tree else Document diff --git a/frappe/permissions.py b/frappe/permissions.py index a27ba5ee75..a0d1677fac 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -423,7 +423,7 @@ def clear_user_permissions_for_doctype(doctype, user=None): filters = {'allow': doctype} if user: filters['user'] = user - user_permissions_for_doctype = frappe.db.get_list('User Permission', filters=filters) + user_permissions_for_doctype = frappe.db.get_all('User Permission', filters=filters) for d in user_permissions_for_doctype: frappe.delete_doc('User Permission', d.name) diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js index 38250f2ad8..e680c8ada1 100644 --- a/frappe/public/js/frappe/form/quick_entry.js +++ b/frappe/public/js/frappe/form/quick_entry.js @@ -186,7 +186,9 @@ frappe.ui.form.QuickEntryForm = Class.extend({ } }, error: function() { - me.open_doc(); + if (!me.skip_redirect_on_error) { + me.open_doc(); + } }, always: function() { me.dialog.working = false; diff --git a/frappe/public/js/frappe/views/file/file_view.js b/frappe/public/js/frappe/views/file/file_view.js index 6ee37aec3d..5f9ca642f4 100644 --- a/frappe/public/js/frappe/views/file/file_view.js +++ b/frappe/public/js/frappe/views/file/file_view.js @@ -259,7 +259,7 @@ frappe.views.FileView = class FileView extends frappe.views.ListView { get_left_html(file) { file = this.prepare_datum(file); - const file_size = frappe.form.formatters.FileSize(file.file_size); + const file_size = file.file_size ? frappe.form.formatters.FileSize(file.file_size) : ''; const route_url = this.get_route_url(file); let created_on; diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index ad4380bb4a..6ac26c00da 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -1231,23 +1231,27 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { action: () => { const args = this.get_args(); const selected_items = this.get_checked_items(true); + let fields = [ + { + fieldtype: 'Select', + label: __('Select File Type'), + fieldname:'file_format_type', + options: ['Excel', 'CSV'], + default: 'Excel' + } + ]; + + if (this.total_count > args.page_length) { + fields.push({ + fieldtype: 'Check', + fieldname: 'export_all_rows', + label: __('Export All {0} rows?', [(this.total_count + "").bold()]) + }); + } const d = new frappe.ui.Dialog({ title: __("Export Report: {0}",[__(this.doctype)]), - fields: [ - { - fieldtype: 'Select', - label: __('Select File Type'), - fieldname:'file_format_type', - options: ['Excel', 'CSV'], - default: 'Excel' - }, - { - fieldtype: 'Check', - fieldname: 'export_all_rows', - label: __('Export All {0} rows?', [(this.total_count + "").bold()]) - } - ], + fields: fields, primary_action_label: __('Download'), primary_action: (data) => { args.cmd = 'frappe.desk.reportview.export_query'; diff --git a/frappe/public/js/frappe/web_form/web_form.js b/frappe/public/js/frappe/web_form/web_form.js index 216bf2d831..ca44dd65d0 100644 --- a/frappe/public/js/frappe/web_form/web_form.js +++ b/frappe/public/js/frappe/web_form/web_form.js @@ -107,7 +107,6 @@ export default class WebForm extends frappe.ui.FieldGroup { let for_payment = Boolean(this.accept_payment && !this.doc.paid); this.doc.doctype = this.doc_type; - this.doc.name = this.doc_name; this.doc.web_form_name = this.name; // Save diff --git a/frappe/public/js/frappe/web_form/web_form_list.js b/frappe/public/js/frappe/web_form/web_form_list.js index 7b14844c6f..db5f1da3a0 100644 --- a/frappe/public/js/frappe/web_form/web_form_list.js +++ b/frappe/public/js/frappe/web_form/web_form_list.js @@ -201,14 +201,13 @@ export default class WebFormList { () => (window.location.href = window.location.pathname + "?new=1") ); - if (!(this.rows.length < this.page_length)) { + if (this.rows.length <= this.page_length) { addButton(footer, "more", "secondary", false, "More", () => this.more()); } function addButton(wrapper, id, type, hidden, name, action) { const button = document.createElement("button"); - button.classList.add("btn", "btn-primary", "btn-sm", "ml-2"); - if (type == "secondary") + if (type == "secondary") { button.classList.add( "btn", "btn-secondary", @@ -216,7 +215,8 @@ export default class WebFormList { "ml-2", "text-white" ); - if (type == "danger") + } + else if (type == "danger") { button.classList.add( "btn", "btn-danger", @@ -224,6 +224,10 @@ export default class WebFormList { "btn-sm", "ml-2" ); + } + else { + button.classList.add("btn", "btn-primary", "btn-sm", "ml-2"); + } button.id = id; button.innerText = name; diff --git a/frappe/tests/test_db_query.py b/frappe/tests/test_db_query.py index b1b12f642e..18c19cbf3e 100644 --- a/frappe/tests/test_db_query.py +++ b/frappe/tests/test_db_query.py @@ -185,37 +185,29 @@ class TestReportview(unittest.TestCase): self.assertTrue('date_diff' in data[0]) def test_nested_permission(self): - clear_user_permissions_for_doctype("File") - delete_test_file_hierarchy() # delete already existing folders - from frappe.core.doctype.file.file import create_new_folder frappe.set_user('Administrator') - - create_new_folder('level1-A', 'Home') - create_new_folder('level2-A', 'Home/level1-A') - create_new_folder('level2-B', 'Home/level1-A') - create_new_folder('level3-A', 'Home/level1-A/level2-A') - - create_new_folder('level1-B', 'Home') - create_new_folder('level2-A', 'Home/level1-B') + create_nested_doctype() + create_nested_doctype_records() + clear_user_permissions_for_doctype('Nested DocType') # user permission for only one root folder - add_user_permission('File', 'Home/level1-A', 'test2@example.com') + add_user_permission('Nested DocType', 'Level 1 A', 'test2@example.com') from frappe.core.page.permission_manager.permission_manager import update - update('File', 'All', 0, 'if_owner', 0) # to avoid if_owner filter + # to avoid if_owner filter + update('Nested DocType', 'All', 0, 'if_owner', 0) frappe.set_user('test2@example.com') - data = DatabaseQuery("File").execute() + data = DatabaseQuery('Nested DocType').execute() # children of root folder (for which we added user permission) should be accessible - self.assertTrue({"name": "Home/level1-A/level2-A"} in data) - self.assertTrue({"name": "Home/level1-A/level2-B"} in data) - self.assertTrue({"name": "Home/level1-A/level2-A/level3-A"} in data) + self.assertTrue({'name': 'Level 2 A'} in data) + self.assertTrue({'name': 'Level 2 A'} in data) # other folders should not be accessible - self.assertFalse({"name": "Home/level1-B"} in data) - self.assertFalse({"name": "Home/level1-B/level2-B"} in data) - update('File', 'All', 0, 'if_owner', 1) + self.assertFalse({'name': 'Level 1 B'} in data) + self.assertFalse({'name': 'Level 2 B'} in data) + update('Nested DocType', 'All', 0, 'if_owner', 1) frappe.set_user('Administrator') def test_filter_sanitizer(self): @@ -259,83 +251,74 @@ class TestReportview(unittest.TestCase): self.assertTrue('DefaultValue' in [d['name'] for d in out]) def test_of_not_of_descendant_ancestors(self): - clear_user_permissions_for_doctype("File") - delete_test_file_hierarchy() # delete already existing folders - from frappe.core.doctype.file.file import create_new_folder - - create_new_folder('level1-A', 'Home') - create_new_folder('level2-A', 'Home/level1-A') - create_new_folder('level2-B', 'Home/level1-A') - create_new_folder('level3-A', 'Home/level1-A/level2-A') - - create_new_folder('level1-B', 'Home') - create_new_folder('level2-A', 'Home/level1-B') + frappe.set_user('Administrator') + clear_user_permissions_for_doctype('Nested DocType') # in descendants filter - data = frappe.get_all('File', {'name': ('descendants of', 'Home/level1-A/level2-A')}) - self.assertTrue({"name": "Home/level1-A/level2-A/level3-A"} in data) + data = frappe.get_all('Nested DocType', {'name': ('descendants of', 'Level 2 A')}) + self.assertTrue({"name": "Level 3 A"} in data) - data = frappe.get_all('File', {'name': ('descendants of', 'Home/level1-A')}) - self.assertTrue({"name": "Home/level1-A/level2-A/level3-A"} in data) - self.assertTrue({"name": "Home/level1-A/level2-A"} in data) - self.assertTrue({"name": "Home/level1-A/level2-B"} in data) - self.assertFalse({"name": "Home/level1-B"} in data) - self.assertFalse({"name": "Home/level1-A"} in data) - self.assertFalse({"name": "Home"} in data) + data = frappe.get_all('Nested DocType', {'name': ('descendants of', 'Level 1 A')}) + self.assertTrue({"name": "Level 3 A"} in data) + self.assertTrue({"name": "Level 2 A"} in data) + self.assertFalse({"name": "Level 2 B"} in data) + self.assertFalse({"name": "Level 1 B"} in data) + self.assertFalse({"name": "Level 1 A"} in data) + self.assertFalse({"name": "Root"} in data) # in ancestors of filter - data = frappe.get_all('File', {'name': ('ancestors of', 'Home/level1-A/level2-A')}) - self.assertFalse({"name": "Home/level1-A/level2-A/level3-A"} in data) - self.assertFalse({"name": "Home/level1-A/level2-A"} in data) - self.assertFalse({"name": "Home/level1-A/level2-B"} in data) - self.assertFalse({"name": "Home/level1-B"} in data) - self.assertTrue({"name": "Home/level1-A"} in data) - self.assertTrue({"name": "Home"} in data) + data = frappe.get_all('Nested DocType', {'name': ('ancestors of', 'Level 2 A')}) + self.assertFalse({"name": "Level 3 A"} in data) + self.assertFalse({"name": "Level 2 A"} in data) + self.assertFalse({"name": "Level 2 B"} in data) + self.assertFalse({"name": "Level 1 B"} in data) + self.assertTrue({"name": "Level 1 A"} in data) + self.assertTrue({"name": "Root"} in data) - data = frappe.get_all('File', {'name': ('ancestors of', 'Home/level1-A')}) - self.assertFalse({"name": "Home/level1-A/level2-A/level3-A"} in data) - self.assertFalse({"name": "Home/level1-A/level2-A"} in data) - self.assertFalse({"name": "Home/level1-A/level2-B"} in data) - self.assertFalse({"name": "Home/level1-B"} in data) - self.assertFalse({"name": "Home/level1-A"} in data) - self.assertTrue({"name": "Home"} in data) + data = frappe.get_all('Nested DocType', {'name': ('ancestors of', 'Level 1 A')}) + self.assertFalse({"name": "Level 3 A"} in data) + self.assertFalse({"name": "Level 2 A"} in data) + self.assertFalse({"name": "Level 2 B"} in data) + self.assertFalse({"name": "Level 1 B"} in data) + self.assertFalse({"name": "Level 1 A"} in data) + self.assertTrue({"name": "Root"} in data) # not descendants filter - data = frappe.get_all('File', {'name': ('not descendants of', 'Home/level1-A/level2-A')}) - self.assertFalse({"name": "Home/level1-A/level2-A/level3-A"} in data) - self.assertTrue({"name": "Home/level1-A/level2-A"} in data) - self.assertTrue({"name": "Home/level1-A/level2-B"} in data) - self.assertTrue({"name": "Home/level1-A"} in data) - self.assertTrue({"name": "Home"} in data) + data = frappe.get_all('Nested DocType', {'name': ('not descendants of', 'Level 2 A')}) + self.assertFalse({"name": "Level 3 A"} in data) + self.assertTrue({"name": "Level 2 A"} in data) + self.assertTrue({"name": "Level 2 B"} in data) + self.assertTrue({"name": "Level 1 A"} in data) + self.assertTrue({"name": "Root"} in data) - data = frappe.get_all('File', {'name': ('not descendants of', 'Home/level1-A')}) - self.assertFalse({"name": "Home/level1-A/level2-A/level3-A"} in data) - self.assertFalse({"name": "Home/level1-A/level2-A"} in data) - self.assertFalse({"name": "Home/level1-A/level2-B"} in data) - self.assertTrue({"name": "Home/level1-B"} in data) - self.assertTrue({"name": "Home/level1-A"} in data) - self.assertTrue({"name": "Home"} in data) + data = frappe.get_all('Nested DocType', {'name': ('not descendants of', 'Level 1 A')}) + self.assertFalse({"name": "Level 3 A"} in data) + self.assertFalse({"name": "Level 2 A"} in data) + self.assertTrue({"name": "Level 2 B"} in data) + self.assertTrue({"name": "Level 1 B"} in data) + self.assertTrue({"name": "Level 1 A"} in data) + self.assertTrue({"name": "Root"} in data) # not ancestors of filter - data = frappe.get_all('File', {'name': ('not ancestors of', 'Home/level1-A/level2-A')}) - self.assertTrue({"name": "Home/level1-A/level2-A/level3-A"} in data) - self.assertTrue({"name": "Home/level1-A/level2-A"} in data) - self.assertTrue({"name": "Home/level1-A/level2-B"} in data) - self.assertTrue({"name": "Home/level1-B"} in data) - self.assertTrue({"name": "Home/level1-A"} not in data) - self.assertTrue({"name": "Home"} not in data) + data = frappe.get_all('Nested DocType', {'name': ('not ancestors of', 'Level 2 A')}) + self.assertTrue({"name": "Level 3 A"} in data) + self.assertTrue({"name": "Level 2 A"} in data) + self.assertTrue({"name": "Level 2 B"} in data) + self.assertTrue({"name": "Level 1 B"} in data) + self.assertTrue({"name": "Level 1 A"} not in data) + self.assertTrue({"name": "Root"} not in data) - data = frappe.get_all('File', {'name': ('not ancestors of', 'Home/level1-A')}) - self.assertTrue({"name": "Home/level1-A/level2-A/level3-A"} in data) - self.assertTrue({"name": "Home/level1-A/level2-A"} in data) - self.assertTrue({"name": "Home/level1-A/level2-B"} in data) - self.assertTrue({"name": "Home/level1-B"} in data) - self.assertTrue({"name": "Home/level1-A"} in data) - self.assertFalse({"name": "Home"} in data) + data = frappe.get_all('Nested DocType', {'name': ('not ancestors of', 'Level 1 A')}) + self.assertTrue({"name": "Level 3 A"} in data) + self.assertTrue({"name": "Level 2 A"} in data) + self.assertTrue({"name": "Level 2 B"} in data) + self.assertTrue({"name": "Level 1 B"} in data) + self.assertTrue({"name": "Level 1 A"} in data) + self.assertFalse({"name": "Root"} in data) - data = frappe.get_all('File', {'name': ('ancestors of', 'Home')}) + data = frappe.get_all('Nested DocType', {'name': ('ancestors of', 'Root')}) self.assertTrue(len(data) == 0) - self.assertTrue(len(frappe.get_all('File', {'name': ('not ancestors of', 'Home')})) == len(frappe.get_all('File'))) + self.assertTrue(len(frappe.get_all('Nested DocType', {'name': ('not ancestors of', 'Root')})) == len(frappe.get_all('Nested DocType'))) def test_is_set_is_not_set(self): @@ -364,14 +347,45 @@ def create_event(subject="_Test Event", starts_on=None): return event -def delete_test_file_hierarchy(): - files_to_delete = [ - 'Home/level1-A/level2-A/level3-A', - 'Home/level1-A/level2-A', - 'Home/level1-A/level2-B', - 'Home/level1-A', - 'Home/level1-B/level2-A', - 'Home/level1-B' +def create_nested_doctype(): + if frappe.db.exists('DocType', 'Nested DocType'): + return + + frappe.get_doc({ + 'doctype': 'DocType', + 'name': 'Nested DocType', + 'module': 'Custom', + 'is_tree': 1, + 'custom': 1, + 'autoname': 'Prompt', + 'fields': [ + {'label': 'Description', 'fieldname': 'description'} + ], + 'permissions': [ + {'role': 'Blogger'} + ] + }).insert() + +def create_nested_doctype_records(): + ''' + Create a structure like: + - Root + - Level 1 A + - Level 2 A + - Level 3 A + - Level 1 B + - Level 2 B + ''' + records = [ + {'name': 'Root', 'is_group': 1}, + {'name': 'Level 1 A', 'parent_nested_doctype': 'Root', 'is_group': 1}, + {'name': 'Level 2 A', 'parent_nested_doctype': 'Level 1 A', 'is_group': 1}, + {'name': 'Level 3 A', 'parent_nested_doctype': 'Level 2 A'}, + {'name': 'Level 1 B', 'parent_nested_doctype': 'Root', 'is_group': 1}, + {'name': 'Level 2 B', 'parent_nested_doctype': 'Level 1 B'}, ] - for file_name in files_to_delete: - frappe.delete_doc('File', file_name) + + for r in records: + d = frappe.new_doc('Nested DocType') + d.update(r) + d.insert(ignore_permissions=True, ignore_if_duplicate=True) diff --git a/frappe/tests/test_twofactor.py b/frappe/tests/test_twofactor.py index 4064853dac..586748fcd3 100644 --- a/frappe/tests/test_twofactor.py +++ b/frappe/tests/test_twofactor.py @@ -7,8 +7,7 @@ from frappe.auth import HTTPRequest from frappe.utils import cint from frappe.tests import set_request from frappe.twofactor import (should_run_2fa, authenticate_for_2factor, get_cached_user_pass, - two_factor_is_enabled_for_, confirm_otp_token, get_otpsecret_for_, get_verification_obj, - render_string_template) + two_factor_is_enabled_for_, confirm_otp_token, get_otpsecret_for_, get_verification_obj) import time @@ -123,7 +122,7 @@ class TestTwoFactor(unittest.TestCase): '''String template renders as expected with variables.''' args = {'issuer_name':'Frappe Technologies'} _str = 'Verification Code from {{issuer_name}}' - _str = render_string_template(_str,args) + _str = frappe.render_template(_str,args) self.assertEqual(_str,'Verification Code from Frappe Technologies') def test_bypass_restict_ip(self): diff --git a/frappe/twofactor.py b/frappe/twofactor.py index d8f68fb050..a539532f25 100644 --- a/frappe/twofactor.py +++ b/frappe/twofactor.py @@ -7,7 +7,6 @@ import frappe from frappe import _ import pyotp, os from frappe.utils.background_jobs import enqueue -from jinja2 import Template from pyqrcode import create as qrcreate from six import BytesIO from base64 import b64encode, b32encode @@ -223,33 +222,27 @@ def process_2fa_for_email(user, token, otp_secret, otp_issuer, method='Email'): def get_email_subject_for_2fa(kwargs_dict): '''Get email subject for 2fa.''' subject_template = _('Login Verification Code from {}').format(frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')) - subject = render_string_template(subject_template, kwargs_dict) + subject = frappe.render_template(subject_template, kwargs_dict) return subject def get_email_body_for_2fa(kwargs_dict): '''Get email body for 2fa.''' body_template = 'Enter this code to complete your login:

{{otp}}' - body = render_string_template(body_template, kwargs_dict) + body = frappe.render_template(body_template, kwargs_dict) return body def get_email_subject_for_qr_code(kwargs_dict): '''Get QRCode email subject.''' subject_template = _('One Time Password (OTP) Registration Code from {}').format(frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')) - subject = render_string_template(subject_template, kwargs_dict) + subject = frappe.render_template(subject_template, kwargs_dict) return subject def get_email_body_for_qr_code(kwargs_dict): '''Get QRCode email body.''' body_template = 'Please click on the following link and follow the instructions on the page.

{{qrcode_link}}' - body = render_string_template(body_template, kwargs_dict) + body = frappe.render_template(body_template, kwargs_dict) return body -def render_string_template(_str, kwargs_dict): - '''Render string with jinja.''' - s = Template(_str) - s = s.render(**kwargs_dict) - return s - def get_link_for_qrcode(user, totp_uri): '''Get link to temporary page showing QRCode.''' key = frappe.generate_hash(length=20)