');
+ $wrapper.append($doctype_select, $field_select);
+ field.$input_wrapper.append($wrapper);
+ $doctype_select.wrap('
');
+ $field_select.wrap('
');
+
+ let row = frappe.get_doc(doctype, docname);
+ let curr_value = { doctype: null, fieldname: null };
+ if (row.fetch_from) {
+ let [doctype, fieldname] = row.fetch_from.split(".");
+ curr_value.doctype = doctype;
+ curr_value.fieldname = fieldname;
+ }
+ let curr_df_link_doctype = row.fieldtype == "Link" ? row.options : null;
+
+ let doctypes = frm.doc.fields
+ .filter(df => df.fieldtype == "Link")
+ .filter(df => df.options && df.options != curr_df_link_doctype)
+ .map(df => ({
+ label: `${df.options} (${df.fieldname})`,
+ value: df.fieldname
+ }));
+ $doctype_select.add_options([
+ { label: __("Select DocType"), value: "", selected: true },
+ ...doctypes
+ ]);
+
+ $doctype_select.on("change", () => {
+ row.fetch_from = "";
+ frm.dirty();
+ update_fieldname_options();
+ });
+
+ function update_fieldname_options() {
+ $field_select.find("option").remove();
+
+ let link_fieldname = $doctype_select.val();
+ if (!link_fieldname) return;
+ let link_field = frm.doc.fields.find(
+ df => df.fieldname === link_fieldname
+ );
+ let link_doctype = link_field.options;
+ frappe.model.with_doctype(link_doctype, () => {
+ let fields = frappe.meta
+ .get_docfields(link_doctype, null, {
+ fieldtype: ["not in", frappe.model.no_value_type]
+ })
+ .map(df => ({
+ label: `${df.label} (${df.fieldtype})`,
+ value: df.fieldname
+ }));
+ $field_select.add_options([
+ {
+ label: __("Select Field"),
+ value: "",
+ selected: true,
+ disabled: true
+ },
+ ...fields
+ ]);
+
+ if (curr_value.fieldname) {
+ $field_select.val(curr_value.fieldname);
+ }
+ });
+ }
+
+ $field_select.on("change", () => {
+ let fetch_from = `${$doctype_select.val()}.${$field_select.val()}`;
+ row.fetch_from = fetch_from;
+ frm.dirty();
+ });
+
+ if (curr_value.doctype) {
+ $doctype_select.val(curr_value.doctype);
+ update_fieldname_options();
+ }
+ }
+});
diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json
index 7f93d3130a..6a427f71e1 100644
--- a/frappe/core/doctype/doctype/doctype.json
+++ b/frappe/core/doctype/doctype/doctype.json
@@ -76,6 +76,7 @@
"index_web_pages_for_search",
"route",
"is_published_field",
+ "website_search_field",
"advanced",
"engine"
],
@@ -547,6 +548,12 @@
{
"fieldname": "column_break_51",
"fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "has_web_view",
+ "fieldname": "website_search_field",
+ "fieldtype": "Data",
+ "label": "Website Search Field"
}
],
"icon": "fa fa-bolt",
@@ -628,7 +635,7 @@
"link_fieldname": "reference_doctype"
}
],
- "modified": "2021-04-16 12:26:41.031135",
+ "modified": "2021-06-17 23:31:44.974199",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
@@ -662,4 +669,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index f9dbeb0907..a7f4dd9def 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -2,18 +2,12 @@
# MIT License. See license.txt
# imports - standard imports
-from __future__ import unicode_literals
import re, copy, os, shutil
import json
from frappe.cache_manager import clear_user_cache, clear_controller_cache
-# imports - third party imports
-import six
-from six import iteritems
-
# imports - module imports
import frappe
-import frappe.website.render
from frappe import _
from frappe.utils import now, cint
from frappe.model import no_value_fields, default_fields, data_fieldtypes, table_fields, data_field_options
@@ -28,6 +22,7 @@ from frappe.model.docfield import supports_translation
from frappe.modules.import_file import get_file_path
from frappe.model.meta import Meta
from frappe.desk.utils import validate_route_conflict
+from frappe.website.utils import clear_cache
class InvalidFieldNameError(frappe.ValidationError): pass
class UniqueFieldnameError(frappe.ValidationError): pass
@@ -198,7 +193,7 @@ class DocType(Document):
self.flags.update_fields_to_fetch_queries = []
- if set(old_fields_to_fetch) != set([df.fieldname for df in new_meta.get_fields_to_fetch()]):
+ if set(old_fields_to_fetch) != set(df.fieldname for df in new_meta.get_fields_to_fetch()):
for df in new_meta.get_fields_to_fetch():
if df.fieldname not in old_fields_to_fetch:
link_fieldname, source_fieldname = df.fetch_from.split('.', 1)
@@ -253,7 +248,7 @@ class DocType(Document):
frappe.throw(_('Field "route" is mandatory for Web Views'), title='Missing Field')
# clear website cache
- frappe.website.render.clear_cache()
+ clear_cache()
def change_modified_of_parent(self):
"""Change the timestamp of parent DocType if the current one is a child to clear caches."""
@@ -401,10 +396,7 @@ class DocType(Document):
frappe.db.sql("""update tabSingles set value=%s
where doctype=%s and field='name' and value = %s""", (new, new, old))
else:
- frappe.db.multisql({
- "mariadb": f"RENAME TABLE `tab{old}` TO `tab{new}`",
- "postgres": f"ALTER TABLE `tab{old}` RENAME TO `tab{new}`"
- })
+ frappe.db.rename_table(old, new)
frappe.db.commit()
# Do not rename and move files and folders for custom doctype
@@ -486,7 +478,7 @@ class DocType(Document):
# remove null and empty fields
def remove_null_fields(o):
to_remove = []
- for attr, value in iteritems(o):
+ for attr, value in o.items():
if isinstance(value, list):
for v in value:
remove_null_fields(v)
@@ -555,11 +547,6 @@ class DocType(Document):
from frappe.modules.export_file import export_to_files
export_to_files(record_list=[['DocType', self.name]], create_init=True)
- def import_doc(self):
- """Import from standard folder `[module]/doctype/[name]/[name].json`."""
- from frappe.modules.import_module import import_from_files
- import_from_files(record_list=[[self.module, 'doctype', self.name]])
-
def make_controller_template(self):
"""Make boilerplate controller template."""
make_boilerplate("controller._py", self)
@@ -670,7 +657,7 @@ class DocType(Document):
if not name:
name = self.name
- flags = {"flags": re.ASCII} if six.PY3 else {}
+ flags = {"flags": re.ASCII}
# a DocType name should not start or end with an empty space
if re.search(r"^[ \t\n\r]+|[ \t\n\r]+$", name, **flags):
@@ -735,6 +722,19 @@ def validate_links_table_fieldnames(meta):
message = _("Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype))
frappe.throw(message, InvalidFieldNameError, _("Invalid Fieldname"))
+ if link.is_child_table and not meta.get_field(link.table_fieldname):
+ message = _("Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.table_fieldname), frappe.bold(meta.name))
+ frappe.throw(message, frappe.ValidationError, _("Invalid Table Fieldname"))
+
+ if link.is_child_table:
+ if not link.parent_doctype:
+ message = _("Row #{0}: Parent DocType is mandatory for internal links").format(index+1)
+ frappe.throw(message, frappe.ValidationError, _("Parent Missing"))
+
+ if not link.table_fieldname:
+ message = _("Row #{0}: Table Fieldname is mandatory for internal links").format(index+1)
+ frappe.throw(message, frappe.ValidationError, _("Table Fieldname Missing"))
+
def validate_fields_for_doctype(doctype):
meta = frappe.get_meta(doctype, cached=False)
validate_links_table_fieldnames(meta)
@@ -767,7 +767,7 @@ def validate_fields(meta):
invalid_fields = ('doctype',)
if fieldname in invalid_fields:
frappe.throw(_("{0}: Fieldname cannot be one of {1}")
- .format(docname, ", ".join([frappe.bold(d) for d in invalid_fields])))
+ .format(docname, ", ".join(frappe.bold(d) for d in invalid_fields)))
def check_unique_fieldname(docname, fieldname):
duplicates = list(filter(None, map(lambda df: df.fieldname==fieldname and str(df.idx) or None, fields)))
@@ -937,6 +937,16 @@ def validate_fields(meta):
if meta.is_published_field not in fieldname_list:
frappe.throw(_("Is Published Field must be a valid fieldname"), InvalidFieldNameError)
+ def check_website_search_field(meta):
+ if not meta.website_search_field:
+ return
+
+ if meta.website_search_field not in fieldname_list:
+ frappe.throw(_("Website Search Field must be a valid fieldname"), InvalidFieldNameError)
+
+ if "title" not in fieldname_list:
+ frappe.throw(_('Field "title" is mandatory if "Website Search Field" is set.'), title=_("Missing Field"))
+
def check_timeline_field(meta):
if not meta.timeline_field:
return
@@ -1001,7 +1011,7 @@ def validate_fields(meta):
if docfield.options and (docfield.options not in data_field_options):
df_str = frappe.bold(_(docfield.label))
text_str = _("{0} is an invalid Data field.").format(df_str) + "
" * 2 + _("Only Options allowed for Data field are:") + "
"
- df_options_str = "
- " + "
- ".join([_(x) for x in data_field_options]) + "
"
+ df_options_str = "
- " + "
- ".join(_(x) for x in data_field_options) + "
"
frappe.msgprint(text_str + df_options_str, title="Invalid Data Field", raise_exception=True)
@@ -1056,6 +1066,7 @@ def validate_fields(meta):
check_title_field(meta)
check_timeline_field(meta)
check_is_published_field(meta)
+ check_website_search_field(meta)
check_sort_field(meta)
check_image_field(meta)
diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py
index 9c492d2c36..9aaaf5a1ac 100644
--- a/frappe/core/doctype/doctype/test_doctype.py
+++ b/frappe/core/doctype/doctype/test_doctype.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
from frappe.core.doctype.doctype.doctype import (UniqueFieldnameError,
@@ -350,6 +348,7 @@ class TestDocType(unittest.TestCase):
dump_docs = json.dumps(docs.get('docs'))
cancel_all_linked_docs(dump_docs)
data_link_doc.cancel()
+ data_doc.name = '{}-CANC-0'.format(data_doc.name)
data_doc.load_from_db()
self.assertEqual(data_link_doc.docstatus, 2)
self.assertEqual(data_doc.docstatus, 2)
@@ -373,7 +372,7 @@ class TestDocType(unittest.TestCase):
for data in link_doc.get('permissions'):
data.submit = 1
data.cancel = 1
- link_doc.insert()
+ link_doc.insert(ignore_if_duplicate=True)
#create first parent doctype
test_doc_1 = new_doctype('Test Doctype 1')
@@ -388,7 +387,7 @@ class TestDocType(unittest.TestCase):
for data in test_doc_1.get('permissions'):
data.submit = 1
data.cancel = 1
- test_doc_1.insert()
+ test_doc_1.insert(ignore_if_duplicate=True)
#crete second parent doctype
doc = new_doctype('Test Doctype 2')
@@ -403,7 +402,7 @@ class TestDocType(unittest.TestCase):
for data in link_doc.get('permissions'):
data.submit = 1
data.cancel = 1
- doc.insert()
+ doc.insert(ignore_if_duplicate=True)
# create doctype data
data_link_doc_1 = frappe.new_doc('Test Linked Doctype 1')
@@ -434,6 +433,7 @@ class TestDocType(unittest.TestCase):
# checking that doc for Test Doctype 2 is not canceled
self.assertRaises(frappe.LinkExistsError, data_link_doc_1.cancel)
+ data_doc_2.name = '{}-CANC-0'.format(data_doc_2.name)
data_doc.load_from_db()
data_doc_2.load_from_db()
self.assertEqual(data_link_doc_1.docstatus, 2)
diff --git a/frappe/core/doctype/doctype_action/doctype_action.py b/frappe/core/doctype/doctype_action/doctype_action.py
index a745c7da40..203b06ec1b 100644
--- a/frappe/core/doctype/doctype_action/doctype_action.py
+++ b/frappe/core/doctype/doctype_action/doctype_action.py
@@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/doctype_link/doctype_link.json b/frappe/core/doctype/doctype_link/doctype_link.json
index 0453894467..4baec6746d 100644
--- a/frappe/core/doctype/doctype_link/doctype_link.json
+++ b/frappe/core/doctype/doctype_link/doctype_link.json
@@ -7,8 +7,11 @@
"field_order": [
"link_doctype",
"link_fieldname",
+ "parent_doctype",
+ "table_fieldname",
"group",
"hidden",
+ "is_child_table",
"custom"
],
"fields": [
@@ -45,12 +48,33 @@
"fieldtype": "Check",
"hidden": 1,
"label": "Custom"
+ },
+ {
+ "depends_on": "is_child_table",
+ "fieldname": "parent_doctype",
+ "fieldtype": "Link",
+ "label": "Parent DocType",
+ "mandatory_depends_on": "is_child_table",
+ "options": "DocType"
+ },
+ {
+ "default": "0",
+ "fetch_from": "link_doctype.istable",
+ "fieldname": "is_child_table",
+ "fieldtype": "Check",
+ "label": "Is Child Table",
+ "read_only": 1
+ },
+ {
+ "fieldname": "table_fieldname",
+ "fieldtype": "Data",
+ "label": "Table Fieldname"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-09-24 14:19:25.189511",
+ "modified": "2021-07-31 15:23:12.237491",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType Link",
diff --git a/frappe/core/doctype/doctype_link/doctype_link.py b/frappe/core/doctype/doctype_link/doctype_link.py
index efe8b09809..07e0efdace 100644
--- a/frappe/core/doctype/doctype_link/doctype_link.py
+++ b/frappe/core/doctype/doctype_link/doctype_link.py
@@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.js b/frappe/core/doctype/document_naming_rule/document_naming_rule.js
index 56b5c2fdf4..097a4e9a6e 100644
--- a/frappe/core/doctype/document_naming_rule/document_naming_rule.js
+++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.js
@@ -4,6 +4,7 @@
frappe.ui.form.on('Document Naming Rule', {
refresh: function(frm) {
frm.trigger('document_type');
+ if (!frm.doc.__islocal) frm.trigger("add_update_counter_button");
},
document_type: (frm) => {
// update the select field options with fieldnames
@@ -20,5 +21,44 @@ frappe.ui.form.on('Document Naming Rule', {
);
});
}
+ },
+ add_update_counter_button: (frm) => {
+ frm.add_custom_button(__('Update Counter'), function() {
+
+ const fields = [{
+ fieldtype: 'Data',
+ fieldname: 'new_counter',
+ label: __('New Counter'),
+ default: frm.doc.counter,
+ reqd: 1,
+ description: __('Warning: Updating counter may lead to document name conflicts if not done properly')
+ }];
+
+ let primary_action_label = __('Save');
+
+ let primary_action = (fields) => {
+ frappe.call({
+ method: 'frappe.core.doctype.document_naming_rule.document_naming_rule.update_current',
+ args: {
+ name: frm.doc.name,
+ new_counter: fields.new_counter
+ },
+ callback: function() {
+ frm.set_value("counter", fields.new_counter);
+ dialog.hide();
+ }
+ });
+ };
+
+ const dialog = new frappe.ui.Dialog({
+ title: __('Update Counter Value for Prefix: {0}', [frm.doc.prefix]),
+ fields,
+ primary_action_label,
+ primary_action
+ });
+
+ dialog.show();
+
+ });
}
});
diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.py b/frappe/core/doctype/document_naming_rule/document_naming_rule.py
index 4b34293af6..10099bd19a 100644
--- a/frappe/core/doctype/document_naming_rule/document_naming_rule.py
+++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.py
@@ -2,7 +2,6 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils.data import evaluate_filters
@@ -30,3 +29,8 @@ class DocumentNamingRule(Document):
counter = frappe.db.get_value(self.doctype, self.name, 'counter', for_update=True) or 0
doc.name = self.prefix + ('%0'+str(self.prefix_digits)+'d') % (counter + 1)
frappe.db.set_value(self.doctype, self.name, 'counter', counter + 1)
+
+@frappe.whitelist()
+def update_current(name, new_counter):
+ frappe.only_for('System Manager')
+ frappe.db.set_value('Document Naming Rule', name, 'counter', new_counter)
diff --git a/frappe/core/doctype/document_naming_rule/test_document_naming_rule.py b/frappe/core/doctype/document_naming_rule/test_document_naming_rule.py
index 1b91f6a0cf..2206d173d7 100644
--- a/frappe/core/doctype/document_naming_rule/test_document_naming_rule.py
+++ b/frappe/core/doctype/document_naming_rule/test_document_naming_rule.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
diff --git a/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py b/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py
index 0895c9f93f..dfca052d95 100644
--- a/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py
+++ b/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py
@@ -2,7 +2,6 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/document_naming_rule_condition/test_document_naming_rule_condition.py b/frappe/core/doctype/document_naming_rule_condition/test_document_naming_rule_condition.py
index 6f1376dc62..643e963bd7 100644
--- a/frappe/core/doctype/document_naming_rule_condition/test_document_naming_rule_condition.py
+++ b/frappe/core/doctype/document_naming_rule_condition/test_document_naming_rule_condition.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
# import frappe
import unittest
diff --git a/frappe/core/doctype/domain/domain.py b/frappe/core/doctype/domain/domain.py
index a4e9f503ab..bbd20f3b70 100644
--- a/frappe/core/doctype/domain/domain.py
+++ b/frappe/core/doctype/domain/domain.py
@@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
@@ -111,7 +110,7 @@ class Domain(Document):
# enable
frappe.db.sql('''update `tabPortal Menu Item` set enabled=1
- where route in ({0})'''.format(', '.join(['"{0}"'.format(d) for d in self.data.allow_sidebar_items])))
+ where route in ({0})'''.format(', '.join('"{0}"'.format(d) for d in self.data.allow_sidebar_items)))
if self.data.remove_sidebar_items:
# disable all
@@ -119,4 +118,4 @@ class Domain(Document):
# enable
frappe.db.sql('''update `tabPortal Menu Item` set enabled=0
- where route in ({0})'''.format(', '.join(['"{0}"'.format(d) for d in self.data.remove_sidebar_items])))
+ where route in ({0})'''.format(', '.join('"{0}"'.format(d) for d in self.data.remove_sidebar_items)))
diff --git a/frappe/core/doctype/domain/test_domain.py b/frappe/core/doctype/domain/test_domain.py
index 8e0bc65c54..c2686a7566 100644
--- a/frappe/core/doctype/domain/test_domain.py
+++ b/frappe/core/doctype/domain/test_domain.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
diff --git a/frappe/core/doctype/domain_settings/domain_settings.py b/frappe/core/doctype/domain_settings/domain_settings.py
index d4d394a5cb..a8c7c6a747 100644
--- a/frappe/core/doctype/domain_settings/domain_settings.py
+++ b/frappe/core/doctype/domain_settings/domain_settings.py
@@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
@@ -35,7 +34,7 @@ class DomainSettings(Document):
all_domains = list((frappe.get_hooks('domains') or {}))
def remove_role(role):
- frappe.db.sql('delete from `tabHas Role` where role=%s', role)
+ frappe.db.delete("Has Role", {"role": role})
frappe.set_value('Role', role, 'disabled', 1)
for domain in all_domains:
diff --git a/frappe/core/doctype/dynamic_link/dynamic_link.py b/frappe/core/doctype/dynamic_link/dynamic_link.py
index 30e0ef1f1f..a7adb9ae72 100644
--- a/frappe/core/doctype/dynamic_link/dynamic_link.py
+++ b/frappe/core/doctype/dynamic_link/dynamic_link.py
@@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/error_log/error_log.py b/frappe/core/doctype/error_log/error_log.py
index ec02aaf446..3d66253b08 100644
--- a/frappe/core/doctype/error_log/error_log.py
+++ b/frappe/core/doctype/error_log/error_log.py
@@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
@@ -21,4 +20,4 @@ def set_old_logs_as_seen():
def clear_error_logs():
'''Flush all Error Logs'''
frappe.only_for('System Manager')
- frappe.db.sql('''DELETE FROM `tabError Log`''')
\ No newline at end of file
+ frappe.db.truncate("Error Log")
diff --git a/frappe/core/doctype/error_log/test_error_log.py b/frappe/core/doctype/error_log/test_error_log.py
index d93fe07c61..d7444ab2a7 100644
--- a/frappe/core/doctype/error_log/test_error_log.py
+++ b/frappe/core/doctype/error_log/test_error_log.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
diff --git a/frappe/core/doctype/error_snapshot/error_snapshot.py b/frappe/core/doctype/error_snapshot/error_snapshot.py
index 5badaad63f..247a796a6b 100644
--- a/frappe/core/doctype/error_snapshot/error_snapshot.py
+++ b/frappe/core/doctype/error_snapshot/error_snapshot.py
@@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/error_snapshot/test_error_snapshot.py b/frappe/core/doctype/error_snapshot/test_error_snapshot.py
index b6438eae1d..135136294a 100644
--- a/frappe/core/doctype/error_snapshot/test_error_snapshot.py
+++ b/frappe/core/doctype/error_snapshot/test_error_snapshot.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
diff --git a/frappe/core/doctype/data_import_legacy/__init__.py b/frappe/core/doctype/feedback/__init__.py
similarity index 100%
rename from frappe/core/doctype/data_import_legacy/__init__.py
rename to frappe/core/doctype/feedback/__init__.py
diff --git a/frappe/core/doctype/feedback/feedback.js b/frappe/core/doctype/feedback/feedback.js
new file mode 100644
index 0000000000..131f0e19d8
--- /dev/null
+++ b/frappe/core/doctype/feedback/feedback.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Feedback', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/core/doctype/feedback/feedback.json b/frappe/core/doctype/feedback/feedback.json
new file mode 100644
index 0000000000..b77e7a6677
--- /dev/null
+++ b/frappe/core/doctype/feedback/feedback.json
@@ -0,0 +1,87 @@
+{
+ "actions": [],
+ "creation": "2021-06-03 19:02:55.328423",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "reference_doctype",
+ "reference_name",
+ "column_break_3",
+ "rating",
+ "ip_address",
+ "section_break_6",
+ "feedback"
+ ],
+ "fields": [
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "rating",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Rating",
+ "precision": "1",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "feedback",
+ "fieldtype": "Small Text",
+ "label": "Feedback",
+ "reqd": 1
+ },
+ {
+ "fieldname": "reference_doctype",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Reference Document Type",
+ "options": "\nBlog Post"
+ },
+ {
+ "fieldname": "reference_name",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "label": "Reference Name",
+ "options": "reference_doctype",
+ "reqd": 1
+ },
+ {
+ "fieldname": "ip_address",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "IP Address",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-06-23 12:45:42.045696",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Feedback",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "reference_name",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/feedback/feedback.py b/frappe/core/doctype/feedback/feedback.py
new file mode 100644
index 0000000000..655bed6eb1
--- /dev/null
+++ b/frappe/core/doctype/feedback/feedback.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+class Feedback(Document):
+ pass
diff --git a/frappe/core/doctype/feedback/test_feedback.py b/frappe/core/doctype/feedback/test_feedback.py
new file mode 100644
index 0000000000..c7551420c3
--- /dev/null
+++ b/frappe/core/doctype/feedback/test_feedback.py
@@ -0,0 +1,27 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+
+import frappe
+import unittest
+
+class TestFeedback(unittest.TestCase):
+ def test_feedback_creation_updation(self):
+ from frappe.website.doctype.blog_post.test_blog_post import make_test_blog
+ test_blog = make_test_blog()
+
+ frappe.db.delete("Feedback", {"reference_doctype": "Blog Post"})
+
+ from frappe.templates.includes.feedback.feedback import add_feedback, update_feedback
+ feedback = add_feedback('Blog Post', test_blog.name, 5, 'New feedback')
+
+ self.assertEqual(feedback.feedback, 'New feedback')
+ self.assertEqual(feedback.rating, 5)
+
+ updated_feedback = update_feedback('Blog Post', test_blog.name, 6, 'Updated feedback')
+
+ self.assertEqual(updated_feedback.feedback, 'Updated feedback')
+ self.assertEqual(updated_feedback.rating, 6)
+
+ frappe.db.delete("Feedback", {"reference_doctype": "Blog Post"})
+
+ test_blog.delete()
\ No newline at end of file
diff --git a/frappe/core/doctype/file/file.js b/frappe/core/doctype/file/file.js
index 6d77cb91ad..bc0cc17553 100644
--- a/frappe/core/doctype/file/file.js
+++ b/frappe/core/doctype/file/file.js
@@ -23,6 +23,25 @@ frappe.ui.form.on("File", "refresh", function(frm) {
wrapper.empty();
}
+ var is_raster_image = (/\.(gif|jpg|jpeg|tiff|png)$/i).test(frm.doc.file_url);
+ var is_optimizable = !frm.doc.is_folder && is_raster_image && frm.doc.file_size > 0;
+
+ if (is_optimizable) {
+ frm.add_custom_button(__("Optimize"), function() {
+ frappe.show_alert(__("Optimizing image..."));
+ frappe.call({
+ method: "frappe.core.doctype.file.file.optimize_saved_image",
+ args: {
+ doc_name: frm.doc.name,
+ },
+ callback: function() {
+ frappe.show_alert(__("Image optimized"));
+ frappe.set_route("List", "File");
+ }
+ });
+ });
+ }
+
if(frm.doc.file_name && frm.doc.file_name.split('.').splice(-1)[0]==='zip') {
frm.add_custom_button(__('Unzip'), function() {
frappe.call({
diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py
index c4c37e6d13..b8ea134db5 100755
--- a/frappe/core/doctype/file/file.py
+++ b/frappe/core/doctype/file/file.py
@@ -7,8 +7,6 @@ record of files
naming for same name files: file.gif, file-1.gif, file-2.gif etc
"""
-from __future__ import unicode_literals
-
import base64
import hashlib
import imghdr
@@ -23,14 +21,14 @@ import zipfile
import requests
import requests.exceptions
from PIL import Image, ImageFile, ImageOps
-from six import PY2, StringIO, string_types, text_type
-from six.moves.urllib.parse import quote, unquote
+from io import BytesIO
+from urllib.parse import quote, unquote
import frappe
-from frappe import _, conf
+from frappe import _, conf, safe_decode
from frappe.model.document import Document
from frappe.utils import call_hook_method, cint, cstr, encode, get_files_path, get_hook_method, random_string, strip
-from frappe.utils.image import strip_exif_data
+from frappe.utils.image import strip_exif_data, optimize_image
class MaxFileSizeReachedError(frappe.ValidationError):
pass
@@ -259,8 +257,7 @@ class File(Document):
with open(get_files_path(file_name, is_private=self.is_private), "rb") as f:
self.content_hash = get_content_hash(f.read())
except IOError:
- frappe.msgprint(_("File {0} does not exist").format(self.file_url))
- raise
+ frappe.throw(_("File {0} does not exist").format(self.file_url))
def on_trash(self):
if self.is_home_folder or self.is_attachments_folder:
@@ -272,16 +269,12 @@ class File(Document):
def make_thumbnail(self, set_as_thumbnail=True, width=300, height=300, suffix="small", crop=False):
if self.file_url:
- if self.file_url.startswith("/files"):
- try:
+ try:
+ if self.file_url.startswith(("/files", "/private/files")):
image, filename, extn = get_local_image(self.file_url)
- except IOError:
- return
-
- else:
- try:
+ else:
image, filename, extn = get_web_image(self.file_url)
- except (requests.exceptions.HTTPError, requests.exceptions.SSLError, IOError, TypeError):
+ except (requests.exceptions.HTTPError, requests.exceptions.SSLError, IOError, TypeError):
return
size = width, height
@@ -291,16 +284,13 @@ class File(Document):
image.thumbnail(size, Image.ANTIALIAS)
thumbnail_url = filename + "_" + suffix + "." + extn
-
path = os.path.abspath(frappe.get_site_path("public", thumbnail_url.lstrip("/")))
try:
image.save(path)
-
if set_as_thumbnail:
self.db_set("thumbnail_url", thumbnail_url)
- self.db_set("thumbnail_url", thumbnail_url)
except IOError:
frappe.msgprint(_("Unable to write file format for {0}").format(path))
return
@@ -328,12 +318,10 @@ class File(Document):
def unzip(self):
'''Unzip current file and replace it by its children'''
- if not ".zip" in self.file_name:
- frappe.msgprint(_("Not a zip file"))
- return
+ if not self.file_url.endswith(".zip"):
+ frappe.throw(_("{0} is not a zip file").format(self.file_name))
- zip_path = frappe.get_site_path(self.file_url.strip('/'))
- base_url = os.path.dirname(self.file_url)
+ zip_path = self.get_full_path()
files = []
with zipfile.ZipFile(zip_path) as z:
@@ -361,10 +349,6 @@ class File(Document):
return files
- def get_file_url(self):
- data = frappe.db.get_value("File", self.file_data_name, ["file_name", "file_url"], as_dict=True)
- return data.file_url or data.file_name
-
def exists_on_disk(self):
exists = os.path.exists(self.get_full_path())
return exists
@@ -382,18 +366,14 @@ class File(Document):
file_path = self.get_full_path()
# read the file
- if PY2:
- with open(encode(file_path)) as f:
- content = f.read()
- else:
- with io.open(encode(file_path), mode='rb') as f:
- content = f.read()
- try:
- # for plain text files
- content = content.decode()
- except UnicodeDecodeError:
- # for .png, .jpg, etc
- pass
+ with io.open(encode(file_path), mode='rb') as f:
+ content = f.read()
+ try:
+ # for plain text files
+ content = content.decode()
+ except UnicodeDecodeError:
+ # for .png, .jpg, etc
+ pass
return content
@@ -430,60 +410,19 @@ class File(Document):
frappe.create_folder(file_path)
# write the file
self.content = self.get_content()
- if isinstance(self.content, text_type):
+ if isinstance(self.content, str):
self.content = self.content.encode()
with open(os.path.join(file_path.encode('utf-8'), self.file_name.encode('utf-8')), 'wb+') as f:
f.write(self.content)
return get_files_path(self.file_name, is_private=self.is_private)
- def get_file_doc(self):
- '''returns File object (Document) from given parameters or form_dict'''
- r = frappe.form_dict
-
- if self.file_url is None: self.file_url = r.file_url
- if self.file_name is None: self.file_name = r.file_name
- if self.attached_to_doctype is None: self.attached_to_doctype = r.doctype
- if self.attached_to_name is None: self.attached_to_name = r.docname
- if self.attached_to_field is None: self.attached_to_field = r.docfield
- if self.folder is None: self.folder = r.folder
- if self.is_private is None: self.is_private = r.is_private
-
- if r.filedata:
- file_doc = self.save_uploaded()
-
- elif r.file_url:
- file_doc = self.save()
-
- return file_doc
-
-
- def save_uploaded(self):
- self.content = self.get_uploaded_content()
- if self.content:
- return self.save()
- else:
- raise Exception
-
- def get_uploaded_content(self):
- # should not be unicode when reading a file, hence using frappe.form
- if 'filedata' in frappe.form_dict:
- if "," in frappe.form_dict.filedata:
- frappe.form_dict.filedata = frappe.form_dict.filedata.rsplit(",", 1)[1]
- frappe.uploaded_content = base64.b64decode(frappe.form_dict.filedata)
- return frappe.uploaded_content
- elif self.content:
- return self.content
- frappe.msgprint(_('No file attached'))
- return None
-
-
def save_file(self, content=None, decode=False, ignore_existing_file_check=False):
file_exists = False
self.content = content
if decode:
- if isinstance(content, text_type):
+ if isinstance(content, str):
self.content = content.encode("utf-8")
if b"," in self.content:
@@ -545,14 +484,6 @@ class File(Document):
'file_url': self.file_url
}
- def get_file_data_from_hash(self):
- for name in frappe.db.sql_list("select name from `tabFile` where content_hash=%s and is_private=%s",
- (self.content_hash, self.is_private)):
- b = frappe.get_doc('File', name)
- return {k: b.get(k) for k in frappe.get_hooks()['write_file_keys']}
- return False
-
-
def check_max_file_size(self):
max_file_size = get_max_file_size()
file_size = len(self.content)
@@ -627,12 +558,13 @@ def create_new_folder(file_name, folder):
file.file_name = file_name
file.is_folder = 1
file.folder = folder
- file.insert()
+ file.insert(ignore_if_duplicate=True)
+ return file
@frappe.whitelist()
def move_file(file_list, new_parent, old_parent):
- if isinstance(file_list, string_types):
+ if isinstance(file_list, str):
file_list = json.loads(file_list)
for file_obj in file_list:
@@ -678,7 +610,7 @@ def get_local_image(file_url):
try:
image = Image.open(file_path)
except IOError:
- frappe.msgprint(_("Unable to read file format for {0}").format(file_url), raise_exception=True)
+ frappe.throw(_("Unable to read file format for {0}").format(file_url))
content = None
@@ -709,7 +641,10 @@ def get_web_image(file_url):
frappe.msgprint(_("Unable to read file format for {0}").format(file_url))
raise
- image = Image.open(StringIO(frappe.safe_decode(r.content)))
+ try:
+ image = Image.open(BytesIO(r.content))
+ except Exception as e:
+ frappe.msgprint(_("Image link '{0}' is not valid").format(file_url), raise_exception=e)
try:
filename, extn = file_url.rsplit("/", 1)[1].rsplit(".", 1)
@@ -743,48 +678,12 @@ def delete_file(path):
os.remove(path)
-def remove_file(fid=None, attached_to_doctype=None, attached_to_name=None, from_delete=False, delete_permanently=False):
- """Remove file and File entry"""
- file_name = None
- if not (attached_to_doctype and attached_to_name):
- attached = frappe.db.get_value("File", fid,
- ["attached_to_doctype", "attached_to_name", "file_name"])
- if attached:
- attached_to_doctype, attached_to_name, file_name = attached
-
- ignore_permissions, comment = False, None
- if attached_to_doctype and attached_to_name and not from_delete:
- doc = frappe.get_doc(attached_to_doctype, attached_to_name)
- ignore_permissions = doc.has_permission("write") or False
- if frappe.flags.in_web_form:
- ignore_permissions = True
- if not file_name:
- file_name = frappe.db.get_value("File", fid, "file_name")
- comment = doc.add_comment("Attachment Removed", _("Removed {0}").format(file_name))
- frappe.delete_doc("File", fid, ignore_permissions=ignore_permissions, delete_permanently=delete_permanently)
-
- return comment
def get_max_file_size():
return cint(conf.get('max_file_size')) or 10485760
-def remove_all(dt, dn, from_delete=False, delete_permanently=False):
- """remove all files in a transaction"""
- try:
- for fid in frappe.db.sql_list("""select name from `tabFile` where
- attached_to_doctype=%s and attached_to_name=%s""", (dt, dn)):
- if from_delete:
- # If deleting a doc, directly delete files
- frappe.delete_doc("File", fid, ignore_permissions=True, delete_permanently=delete_permanently)
- else:
- # Removes file and adds a comment in the document it is attached to
- remove_file(fid=fid, attached_to_doctype=dt, attached_to_name=dn,
- from_delete=from_delete, delete_permanently=delete_permanently)
- except Exception as e:
- if e.args[0]!=1054: raise # (temp till for patched)
-
def has_permission(doc, ptype=None, user=None):
has_access = False
@@ -830,11 +729,12 @@ def remove_file_by_url(file_url, doctype=None, name=None):
fid = frappe.db.get_value("File", {"file_url": file_url})
if fid:
+ from frappe.utils.file_manager import remove_file
return remove_file(fid=fid)
def get_content_hash(content):
- if isinstance(content, text_type):
+ if isinstance(content, str):
content = content.encode()
return hashlib.md5(content).hexdigest() #nosec
@@ -882,15 +782,21 @@ def extract_images_from_html(doc, content):
data = match.group(1)
data = data.split("data:")[1]
headers, content = data.split(",")
+ mtype = headers.split(";")[0]
+
+ if isinstance(content, str):
+ content = content.encode("utf-8")
+ if b"," in content:
+ content = content.split(b",")[1]
+ content = base64.b64decode(content)
+
+ content = optimize_image(content, mtype)
if "filename=" in headers:
filename = headers.split("filename=")[-1]
+ filename = safe_decode(filename).split(";")[0]
- # decode filename
- if not isinstance(filename, text_type):
- filename = text_type(filename, 'utf-8')
else:
- mtype = headers.split(";")[0]
filename = get_random_filename(content_type=mtype)
doctype = doc.parenttype if doc.parent else doc.doctype
@@ -902,7 +808,7 @@ def extract_images_from_html(doc, content):
"attached_to_doctype": doctype,
"attached_to_name": name,
"content": content,
- "decode": True
+ "decode": False
})
_file.save(ignore_permissions=True)
file_url = _file.file_url
@@ -911,18 +817,15 @@ def extract_images_from_html(doc, content):
return '

]*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, content)
return content
-def get_random_filename(extn=None, content_type=None):
- if extn:
- if not extn.startswith("."):
- extn = "." + extn
-
- elif content_type:
+def get_random_filename(content_type=None):
+ extn = None
+ if content_type:
extn = mimetypes.guess_extension(content_type)
return random_string(7) + (extn or "")
@@ -933,15 +836,31 @@ def unzip_file(name):
'''Unzip the given file and make file records for each of the extracted files'''
file_obj = frappe.get_doc('File', name)
files = file_obj.unzip()
- return len(files)
+ return files
+@frappe.whitelist()
+def optimize_saved_image(doc_name):
+ file_doc = frappe.get_doc('File', doc_name)
+ content = file_doc.get_content()
+ content_type = mimetypes.guess_type(file_doc.file_name)[0]
+
+ optimized_content = optimize_image(content, content_type)
+
+ file_path = get_files_path(is_private=file_doc.is_private)
+ file_path = os.path.join(file_path.encode('utf-8'), file_doc.file_name.encode('utf-8'))
+ with open(file_path, 'wb+') as f:
+ f.write(optimized_content)
+
+ file_doc.file_size = len(optimized_content)
+ file_doc.content_hash = get_content_hash(optimized_content)
+ file_doc.save()
@frappe.whitelist()
def get_attached_images(doctype, names):
'''get list of image urls attached in form
returns {name: ['image.jpg', 'image.png']}'''
- if isinstance(names, string_types):
+ if isinstance(names, str):
names = json.loads(names)
img_urls = frappe.db.get_list('File', filters={
@@ -958,13 +877,6 @@ def get_attached_images(doctype, names):
return out
-@frappe.whitelist()
-def validate_filename(filename):
- from frappe.utils import now_datetime
- timestamp = now_datetime().strftime(" %Y-%m-%d %H:%M:%S")
- fname = get_file_name(filename, timestamp)
- return fname
-
@frappe.whitelist()
def get_files_in_folder(folder, start=0, page_length=20):
start = cint(start)
diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py
index 2596fe94d0..5478d7ab85 100644
--- a/frappe/core/doctype/file/test_file.py
+++ b/frappe/core/doctype/file/test_file.py
@@ -1,14 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import base64
+import json
import frappe
import os
import unittest
from frappe import _
-from frappe.core.doctype.file.file import move_file, get_files_in_folder
+from frappe.core.doctype.file.file import get_attached_images, move_file, get_files_in_folder, unzip_file
from frappe.utils import get_files_path
# test_records = frappe.get_test_records('File')
@@ -367,6 +366,80 @@ class TestFile(unittest.TestCase):
file1.file_url = '/private/files/parent_dir2.txt'
file1.save()
+ def test_file_url_validation(self):
+ test_file = frappe.get_doc({
+ "doctype": "File",
+ "file_name": 'logo',
+ "file_url": 'https://frappe.io/files/frappe.png'
+ })
+
+ self.assertIsNone(test_file.validate())
+
+ # bad path
+ test_file.file_url = "/usr/bin/man"
+ self.assertRaisesRegex(frappe.exceptions.ValidationError, "URL must start with http:// or https://", test_file.validate)
+
+ test_file.file_url = None
+ test_file.file_name = "/usr/bin/man"
+ self.assertRaisesRegex(frappe.exceptions.ValidationError, "There is some problem with the file url", test_file.validate)
+
+ test_file.file_url = None
+ test_file.file_name = "_file"
+ self.assertRaisesRegex(IOError, "does not exist", test_file.validate)
+
+ test_file.file_url = None
+ test_file.file_name = "/private/files/_file"
+ self.assertRaisesRegex(IOError, "does not exist", test_file.validate)
+
+ def test_make_thumbnail(self):
+ # test web image
+ test_file = frappe.get_doc({
+ "doctype": "File",
+ "file_name": 'logo',
+ "file_url": frappe.utils.get_url('/_test/assets/image.jpg'),
+ }).insert(ignore_permissions=True)
+
+ test_file.make_thumbnail()
+ self.assertEquals(test_file.thumbnail_url, '/files/image_small.jpg')
+
+ # test local image
+ test_file.db_set('thumbnail_url', None)
+ test_file.reload()
+ test_file.file_url = "/files/image_small.jpg"
+ test_file.make_thumbnail(suffix="xs", crop=True)
+ self.assertEquals(test_file.thumbnail_url, '/files/image_small_xs.jpg')
+
+ frappe.clear_messages()
+ test_file.db_set('thumbnail_url', None)
+ test_file.reload()
+ test_file.file_url = frappe.utils.get_url('unknown.jpg')
+ test_file.make_thumbnail(suffix="xs")
+ self.assertEqual(json.loads(frappe.message_log[0]), {"message": f"File '{frappe.utils.get_url('unknown.jpg')}' not found"})
+ self.assertEquals(test_file.thumbnail_url, None)
+
+ def test_file_unzip(self):
+ file_path = frappe.get_app_path('frappe', 'www/_test/assets/file.zip')
+ public_file_path = frappe.get_site_path('public', 'files')
+ try:
+ import shutil
+ shutil.copy(file_path, public_file_path)
+ except Exception:
+ pass
+
+ test_file = frappe.get_doc({
+ "doctype": "File",
+ "file_url": '/files/file.zip',
+ }).insert(ignore_permissions=True)
+
+ self.assertListEqual([file.file_name for file in unzip_file(test_file.name)],
+ ['css_asset.css', 'image.jpg', 'js_asset.min.js'])
+
+ test_file = frappe.get_doc({
+ "doctype": "File",
+ "file_url": frappe.utils.get_url('/_test/assets/image.jpg'),
+ }).insert(ignore_permissions=True)
+ self.assertRaisesRegex(frappe.exceptions.ValidationError, 'not a zip file', test_file.unzip)
+
class TestAttachment(unittest.TestCase):
test_doctype = 'Test For Attachment'
@@ -471,3 +544,28 @@ class TestAttachmentsAccess(unittest.TestCase):
frappe.set_user('Administrator')
frappe.db.rollback()
+
+
+class TestFileUtils(unittest.TestCase):
+ def test_extract_images_from_doc(self):
+ # with filename in data URI
+ todo = frappe.get_doc({
+ "doctype": "ToDo",
+ "description": 'Test

'
+ }).insert()
+ self.assertTrue(frappe.db.exists("File", {"attached_to_name": todo.name}))
+ self.assertIn('

', todo.description)
+ self.assertListEqual(get_attached_images('ToDo', [todo.name])[todo.name], ['/files/pix.png'])
+
+ # without filename in data URI
+ todo = frappe.get_doc({
+ "doctype": "ToDo",
+ "description": 'Test

'
+ }).insert()
+ filename = frappe.db.exists("File", {"attached_to_name": todo.name})
+ self.assertIn(f'

@@ -229,6 +209,7 @@ class TestUser(unittest.TestCase):
self.assertEqual(extract_mentions(comment)[0], "test_user@example.com")
self.assertEqual(extract_mentions(comment)[1], "test.again@example1.com")
+ frappe.delete_doc("User Group", "Team")
doc = frappe.get_doc({
'doctype': 'User Group',
'name': 'Team',
@@ -238,14 +219,18 @@ class TestUser(unittest.TestCase):
'user': 'test1@example.com'
}]
})
- doc.insert(ignore_if_duplicate=True)
+
+ doc.insert()
comment = '''
Testing comment for
@Team
-
+ and
+
+ @Unknown Team
+
please check
'''
@@ -269,32 +254,125 @@ class TestUser(unittest.TestCase):
self.assertEqual(res1.status_code, 200)
self.assertEqual(res2.status_code, 417)
- # def test_user_rollback(self):
- # """
- # FIXME: This is failing with PR #12693 as Rollback can't happen if notifications sent on user creation.
- # Make sure that notifications disabled.
- # """
- # frappe.db.commit()
- # frappe.db.begin()
- # user_id = str(uuid.uuid4())
- # email = f'{user_id}@example.com'
- # try:
- # frappe.flags.in_import = True # disable throttling
- # frappe.get_doc(dict(
- # doctype='User',
- # email=email,
- # first_name=user_id,
- # )).insert()
- # finally:
- # frappe.flags.in_import = False
+ def test_user_rename(self):
+ old_name = "test_user_rename@example.com"
+ new_name = "test_user_rename_new@example.com"
+ user = frappe.get_doc({
+ "doctype": "User",
+ "email": old_name,
+ "enabled": 1,
+ "first_name": "_Test",
+ "new_password": "Eastern_43A1W",
+ "roles": [
+ {
+ "doctype": "Has Role",
+ "parentfield": "roles",
+ "role": "System Manager"
+ }]
+ }).insert(ignore_permissions=True, ignore_if_duplicate=True)
- # # Check user has been added
- # self.assertIsNotNone(frappe.db.get("User", {"email": email}))
+ frappe.rename_doc('User', user.name, new_name)
+ self.assertTrue(frappe.db.exists("Notification Settings", new_name))
+
+ frappe.delete_doc("User", new_name)
+
+ def test_signup(self):
+ import frappe.website.utils
+ random_user = frappe.mock('email')
+ random_user_name = frappe.mock('name')
+ # disabled signup
+ with patch.object(user_module, "is_signup_disabled", return_value=True):
+ self.assertRaisesRegex(frappe.exceptions.ValidationError, "Sign Up is disabled",
+ sign_up, random_user, random_user_name, "/signup")
+
+ self.assertTupleEqual(sign_up(random_user, random_user_name, "/welcome"), (1, "Please check your email for verification"))
+ self.assertEqual(frappe.cache().hget('redirect_after_login', random_user), "/welcome")
+
+ # re-register
+ self.assertTupleEqual(sign_up(random_user, random_user_name, "/welcome"), (0, "Already Registered"))
+
+ # disabled user
+ user = frappe.get_doc("User", random_user)
+ user.enabled = 0
+ user.save()
+
+ self.assertTupleEqual(sign_up(random_user, random_user_name, "/welcome"), (0, "Registered but disabled"))
+
+ # throttle user creation
+ with patch.object(user_module.frappe.db, "get_creation_count", return_value=301):
+ self.assertRaisesRegex(frappe.exceptions.ValidationError, "Throttled",
+ sign_up, frappe.mock('email'), random_user_name, "/signup")
+
+
+ def test_reset_password(self):
+ from frappe.auth import CookieManager, LoginManager
+ from frappe.utils import set_request
+ old_password = "Eastern_43A1W"
+ new_password = "easy_password"
+
+ set_request(path="/random")
+ frappe.local.cookie_manager = CookieManager()
+ frappe.local.login_manager = LoginManager()
+
+ frappe.set_user("testpassword@example.com")
+ test_user = frappe.get_doc("User", "testpassword@example.com")
+ test_user.reset_password()
+ self.assertEqual(update_password(new_password, key=test_user.reset_password_key), "/app")
+ self.assertEqual(update_password(new_password, key="wrong_key"), "The Link specified has either been used before or Invalid")
+
+ # password verification should fail with old password
+ self.assertRaises(frappe.exceptions.AuthenticationError, verify_password, old_password)
+ verify_password(new_password)
+
+ # reset password
+ update_password(old_password, old_password=new_password)
+
+ self.assertRaisesRegex(frappe.exceptions.ValidationError, "Invalid key type", update_password, "test", 1, ['like', '%'])
+
+ password_strength_response = {
+ "feedback": {
+ "password_policy_validation_passed": False,
+ "suggestions": ["Fix password"]
+ }
+ }
+
+ # password strength failure test
+ with patch.object(user_module, "test_password_strength", return_value=password_strength_response):
+ self.assertRaisesRegex(frappe.exceptions.ValidationError, "Fix password", update_password, new_password, 0, test_user.reset_password_key)
+
+
+ # test redirect URL for website users
+ frappe.set_user("test2@example.com")
+ self.assertEqual(update_password(new_password, old_password=old_password), "/")
+ # reset password
+ update_password(old_password, old_password=new_password)
+
+ # test API endpoint
+ with patch.object(user_module.frappe, 'sendmail') as sendmail:
+ frappe.clear_messages()
+ test_user = frappe.get_doc("User", "test2@example.com")
+ self.assertEqual(reset_password(user="test2@example.com"), None)
+ test_user.reload()
+ self.assertEqual(update_password(new_password, key=test_user.reset_password_key), "/")
+ update_password(old_password, old_password=new_password)
+ self.assertEqual(json.loads(frappe.message_log[0]), {"message": "Password reset instructions have been sent to your email"})
+ sendmail.assert_called_once()
+ self.assertEqual(sendmail.call_args[1]["recipients"], "test2@example.com")
+
+ self.assertEqual(reset_password(user="test2@example.com"), None)
+ self.assertEqual(reset_password(user="Administrator"), "not allowed")
+ self.assertEqual(reset_password(user="random"), "not found")
+
+ def test_user_onload_modules(self):
+ from frappe.config import get_modules_from_all_apps
+ from frappe.desk.form.load import getdoc
+ frappe.response.docs = []
+ getdoc("User", "Administrator")
+ doc = frappe.response.docs[0]
+ self.assertListEqual(doc.get("__onload").get('all_modules', []),
+ [m.get("module_name") for m in get_modules_from_all_apps()])
- # # Check that rollback works
- # frappe.db.rollback()
- # self.assertIsNone(frappe.db.get("User", {"email": email}))
def delete_contact(user):
- frappe.db.sql("DELETE FROM `tabContact` WHERE `email_id`= %s", user)
- frappe.db.sql("DELETE FROM `tabContact Email` WHERE `email_id`= %s", user)
+ frappe.db.delete("Contact", {"email_id": user})
+ frappe.db.delete("Contact Email", {"email_id": user})
diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js
index 8c5b89c5fc..96726d875c 100644
--- a/frappe/core/doctype/user/user.js
+++ b/frappe/core/doctype/user/user.js
@@ -166,7 +166,7 @@ frappe.ui.form.on('User', {
frm.add_custom_button(__("Reset OTP Secret"), function() {
frappe.call({
- method: "frappe.core.doctype.user.user.reset_otp_secret",
+ method: "frappe.twofactor.reset_otp_secret",
args: {
"user": frm.doc.name
}
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index a4d13a57e0..1336f6eab7 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -1,10 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-
-from __future__ import unicode_literals, print_function
-
from bs4 import BeautifulSoup
-
import frappe
import frappe.share
import frappe.defaults
@@ -17,19 +13,13 @@ from frappe.utils.password import update_password as _update_password, check_pas
from frappe.desk.notifications import clear_notifications
from frappe.desk.doctype.notification_settings.notification_settings import create_notification_settings, toggle_notifications
from frappe.utils.user import get_system_managers
-from frappe.website.utils import is_signup_enabled
+from frappe.website.utils import is_signup_disabled
from frappe.rate_limiter import rate_limit
-from frappe.utils.background_jobs import enqueue
from frappe.core.doctype.user_type.user_type import user_linked_with_permission_on_doctype
STANDARD_USERS = ("Guest", "Administrator")
-
-class MaxUsersReachedError(frappe.ValidationError):
- pass
-
-
class User(Document):
__new_password = None
@@ -57,10 +47,9 @@ class User(Document):
def after_insert(self):
create_notification_settings(self.name)
frappe.cache().delete_key('users_for_mentions')
+ frappe.cache().delete_key('enabled_users')
def validate(self):
- self.check_demo()
-
# clear new password
self.__new_password = self.new_password
self.new_password = ""
@@ -133,14 +122,13 @@ class User(Document):
if self.has_value_changed('allow_in_mentions') or self.has_value_changed('user_type'):
frappe.cache().delete_key('users_for_mentions')
+ if self.has_value_changed('enabled'):
+ frappe.cache().delete_key('enabled_users')
+
def has_website_permission(self, ptype, user, verbose=False):
"""Returns true if current user is the session user"""
return self.name == frappe.session.user
- def check_demo(self):
- if frappe.session.user == 'demo@erpnext.com':
- frappe.throw(_('Cannot change user details in demo. Please signup for a new account at https://erpnext.com'), title=_('Not Allowed'))
-
def set_full_name(self):
self.full_name = " ".join(filter(None, [self.first_name, self.last_name]))
@@ -368,17 +356,15 @@ class User(Document):
frappe.local.login_manager.logout(user=self.name)
# delete todos
- frappe.db.sql("""DELETE FROM `tabToDo` WHERE `owner`=%s""", (self.name,))
+ frappe.db.delete("ToDo", {"owner": self.name})
frappe.db.sql("""UPDATE `tabToDo` SET `assigned_by`=NULL WHERE `assigned_by`=%s""",
(self.name,))
# delete events
- frappe.db.sql("""delete from `tabEvent` where owner=%s
- and event_type='Private'""", (self.name,))
+ frappe.db.delete("Event", {"owner": self.name, "event_type": "Private"})
# delete shares
- frappe.db.sql("""delete from `tabDocShare` where user=%s""", self.name)
-
+ frappe.db.delete("DocShare", {"user": self.name})
# delete messages
frappe.db.sql("""delete from `tabCommunication`
where communication_type in ('Chat', 'Notification')
@@ -396,9 +382,10 @@ class User(Document):
if self.get('allow_in_mentions'):
frappe.cache().delete_key('users_for_mentions')
+ frappe.cache().delete_key('enabled_users')
+
def before_rename(self, old_name, new_name, merge=False):
- self.check_demo()
frappe.clear_cache(user=old_name)
self.validate_rename(old_name, new_name)
@@ -718,85 +705,6 @@ def get_email_awaiting(user):
where parent = %(user)s""",{"user":user})
return False
-@frappe.whitelist(allow_guest=False)
-def set_email_password(email_account, user, password):
- account = frappe.get_doc("Email Account", email_account)
- if account.awaiting_password:
- account.awaiting_password = 0
- account.password = password
- try:
- account.save(ignore_permissions=True)
- except Exception:
- frappe.db.rollback()
- return False
-
- return True
-
-def setup_user_email_inbox(email_account, awaiting_password, email_id, enable_outgoing):
- """ setup email inbox for user """
- def add_user_email(user):
- user = frappe.get_doc("User", user)
- row = user.append("user_emails", {})
-
- row.email_id = email_id
- row.email_account = email_account
- row.awaiting_password = awaiting_password or 0
- row.enable_outgoing = enable_outgoing or 0
-
- user.save(ignore_permissions=True)
-
- udpate_user_email_settings = False
- if not all([email_account, email_id]):
- return
-
- user_names = frappe.db.get_values("User", { "email": email_id }, as_dict=True)
- if not user_names:
- return
-
- for user in user_names:
- user_name = user.get("name")
-
- # check if inbox is alreay configured
- user_inbox = frappe.db.get_value("User Email", {
- "email_account": email_account,
- "parent": user_name
- }, ["name"]) or None
-
- if not user_inbox:
- add_user_email(user_name)
- else:
- # update awaiting password for email account
- udpate_user_email_settings = True
-
- if udpate_user_email_settings:
- frappe.db.sql("""UPDATE `tabUser Email` SET awaiting_password = %(awaiting_password)s,
- enable_outgoing = %(enable_outgoing)s WHERE email_account = %(email_account)s""", {
- "email_account": email_account,
- "enable_outgoing": enable_outgoing,
- "awaiting_password": awaiting_password or 0
- })
- else:
- users = " and ".join([frappe.bold(user.get("name")) for user in user_names])
- frappe.msgprint(_("Enabled email inbox for user {0}").format(users))
-
- ask_pass_update()
-
-def remove_user_email_inbox(email_account):
- """ remove user email inbox settings if email account is deleted """
- if not email_account:
- return
-
- users = frappe.get_all("User Email", filters={
- "email_account": email_account
- }, fields=["parent as name"])
-
- for user in users:
- doc = frappe.get_doc("User", user.get("name"))
- to_remove = [ row for row in doc.user_emails if row.email_account == email_account ]
- [ doc.remove(row) for row in to_remove ]
-
- doc.save(ignore_permissions=True)
-
def ask_pass_update():
# update the sys defaults as to awaiting users
from frappe.utils import set_default
@@ -809,24 +717,19 @@ def ask_pass_update():
def _get_user_for_update_password(key, old_password):
# verify old password
+ result = frappe._dict()
if key:
- user = frappe.db.get_value("User", {"reset_password_key": key})
- if not user:
- return {
- 'message': _("The Link specified has either been used before or Invalid")
- }
+ result.user = frappe.db.get_value("User", {"reset_password_key": key})
+ if not result.user:
+ result.message = _("The Link specified has either been used before or Invalid")
elif old_password:
# verify old password
frappe.local.login_manager.check_password(frappe.session.user, old_password)
user = frappe.session.user
+ result.user = user
- else:
- return
-
- return {
- 'user': user
- }
+ return result
def reset_user_data(user):
user_doc = frappe.get_doc("User", user)
@@ -843,19 +746,17 @@ def verify_password(password):
@frappe.whitelist(allow_guest=True)
def sign_up(email, full_name, redirect_to):
- if not is_signup_enabled():
+ if is_signup_disabled():
frappe.throw(_('Sign Up is disabled'), title='Not Allowed')
user = frappe.db.get("User", {"email": email})
if user:
- if user.disabled:
- return 0, _("Registered but disabled")
- else:
+ if user.enabled:
return 0, _("Already Registered")
+ else:
+ return 0, _("Registered but disabled")
else:
- if frappe.db.sql("""select count(*) from tabUser where
- HOUR(TIMEDIFF(CURRENT_TIMESTAMP, TIMESTAMP(modified)))=1""")[0][0] > 300:
-
+ if frappe.db.get_creation_count('User', 60) > 300:
frappe.respond_as_web_page(_('Temporarily Disabled'),
_('Too many users signed up recently, so the registration is disabled. Please try back in an hour'),
http_status_code=429)
@@ -935,7 +836,7 @@ def user_query(doctype, txt, searchfield, start, page_len, filters):
LIMIT %(page_len)s OFFSET %(start)s
""".format(
user_type_condition = user_type_condition,
- standard_users=", ".join([frappe.db.escape(u) for u in STANDARD_USERS]),
+ standard_users=", ".join(frappe.db.escape(u) for u in STANDARD_USERS),
key=searchfield,
fcond=get_filters_cond(doctype, filters, conditions),
mcond=get_match_cond(doctype)
@@ -1048,91 +949,6 @@ def update_gravatar(name):
if gravatar:
frappe.db.set_value('User', name, 'user_image', gravatar)
-@frappe.whitelist(allow_guest=True)
-def send_token_via_sms(tmp_id,phone_no=None,user=None):
- try:
- from frappe.core.doctype.sms_settings.sms_settings import send_request
- except:
- return False
-
- if not frappe.cache().ttl(tmp_id + '_token'):
- return False
- ss = frappe.get_doc('SMS Settings', 'SMS Settings')
- if not ss.sms_gateway_url:
- return False
-
- token = frappe.cache().get(tmp_id + '_token')
- args = {ss.message_parameter: 'verification code is {}'.format(token)}
-
- for d in ss.get("parameters"):
- args[d.parameter] = d.value
-
- if user:
- user_phone = frappe.db.get_value('User', user, ['phone','mobile_no'], as_dict=1)
- usr_phone = user_phone.mobile_no or user_phone.phone
- if not usr_phone:
- return False
- else:
- if phone_no:
- usr_phone = phone_no
- else:
- return False
-
- args[ss.receiver_parameter] = usr_phone
- status = send_request(ss.sms_gateway_url, args, use_post=ss.use_post)
-
- if 200 <= status < 300:
- frappe.cache().delete(tmp_id + '_token')
- return True
- else:
- return False
-
-@frappe.whitelist(allow_guest=True)
-def send_token_via_email(tmp_id,token=None):
- import pyotp
-
- user = frappe.cache().get(tmp_id + '_user')
- count = token or frappe.cache().get(tmp_id + '_token')
-
- if ((not user) or (user == 'None') or (not count)):
- return False
- user_email = frappe.db.get_value('User',user, 'email')
- if not user_email:
- return False
-
- otpsecret = frappe.cache().get(tmp_id + '_otp_secret')
- hotp = pyotp.HOTP(otpsecret)
-
- frappe.sendmail(
- recipients=user_email,
- sender=None,
- subject="Verification Code",
- template="verification_code",
- args=dict(code=hotp.at(int(count))),
- delayed=False,
- retry=3
- )
-
- return True
-
-@frappe.whitelist(allow_guest=True)
-def reset_otp_secret(user):
- otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')
- user_email = frappe.db.get_value('User',user, 'email')
- if frappe.session.user in ["Administrator", user] :
- frappe.defaults.clear_default(user + '_otplogin')
- frappe.defaults.clear_default(user + '_otpsecret')
- email_args = {
- 'recipients':user_email, 'sender':None, 'subject':'OTP Secret Reset - {}'.format(otp_issuer or "Frappe Framework"),
- 'message':'
Your OTP secret on {} has been reset. If you did not perform this reset and did not request it, please contact your System Administrator immediately.
'.format(otp_issuer or "Frappe Framework"),
- 'delayed':False,
- 'retry':3
- }
- enqueue(method=frappe.sendmail, queue='short', timeout=300, event=None, is_async=True, job_name=None, now=False, **email_args)
- return frappe.msgprint(_("OTP Secret has been reset. Re-registration will be required on next login."))
- else:
- return frappe.throw(_("OTP secret can only be reset by the Administrator."))
-
def throttle_user_creation():
if frappe.flags.in_import:
return
@@ -1150,15 +966,6 @@ def get_module_profile(module_profile):
module_profile = frappe.get_doc('Module Profile', {'module_profile_name': module_profile})
return module_profile.get('block_modules')
-def update_roles(role_profile):
- users = frappe.get_all('User', filters={'role_profile_name': role_profile})
- role_profile = frappe.get_doc('Role Profile', role_profile)
- roles = [role.role for role in role_profile.roles]
- for d in users:
- user = frappe.get_doc('User', d)
- user.set('roles', [])
- user.add_roles(*roles)
-
def create_contact(user, ignore_links=False, ignore_mandatory=False):
from frappe.contacts.doctype.contact.contact import get_contact_name
if user.name in ["Administrator", "Guest"]: return
@@ -1217,20 +1024,27 @@ def generate_keys(user):
:param user: str
"""
- if "System Manager" in frappe.get_roles():
- user_details = frappe.get_doc("User", user)
- api_secret = frappe.generate_hash(length=15)
- # if api key is not set generate api key
- if not user_details.api_key:
- api_key = frappe.generate_hash(length=15)
- user_details.api_key = api_key
- user_details.api_secret = api_secret
- user_details.save()
+ frappe.only_for("System Manager")
+ user_details = frappe.get_doc("User", user)
+ api_secret = frappe.generate_hash(length=15)
+ # if api key is not set generate api key
+ if not user_details.api_key:
+ api_key = frappe.generate_hash(length=15)
+ user_details.api_key = api_key
+ user_details.api_secret = api_secret
+ user_details.save()
+
+ return {"api_secret": api_secret}
- return {"api_secret": api_secret}
- frappe.throw(frappe._("Not Permitted"), frappe.PermissionError)
@frappe.whitelist()
def switch_theme(theme):
if theme in ["Dark", "Light"]:
frappe.db.set_value("User", frappe.session.user, "desk_theme", theme)
+
+def get_enabled_users():
+ def _get_enabled_users():
+ enabled_users = frappe.get_all("User", filters={"enabled": "1"}, pluck="name")
+ return enabled_users
+
+ return frappe.cache().get_value("enabled_users", _get_enabled_users)
\ No newline at end of file
diff --git a/frappe/core/doctype/user_document_type/user_document_type.py b/frappe/core/doctype/user_document_type/user_document_type.py
index 979bfcb250..48dbf87b3d 100644
--- a/frappe/core/doctype/user_document_type/user_document_type.py
+++ b/frappe/core/doctype/user_document_type/user_document_type.py
@@ -2,7 +2,6 @@
# Copyright (c) 2021, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/user_email/user_email.py b/frappe/core/doctype/user_email/user_email.py
index a0ce2e169d..729aa03444 100644
--- a/frappe/core/doctype/user_email/user_email.py
+++ b/frappe/core/doctype/user_email/user_email.py
@@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/user_group/test_user_group.py b/frappe/core/doctype/user_group/test_user_group.py
index c7e28f3d31..2f89d032e1 100644
--- a/frappe/core/doctype/user_group/test_user_group.py
+++ b/frappe/core/doctype/user_group/test_user_group.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
# import frappe
import unittest
diff --git a/frappe/core/doctype/user_group/user_group.py b/frappe/core/doctype/user_group/user_group.py
index b1d0fede4c..178775d407 100644
--- a/frappe/core/doctype/user_group/user_group.py
+++ b/frappe/core/doctype/user_group/user_group.py
@@ -2,7 +2,6 @@
# Copyright (c) 2021, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
import frappe
diff --git a/frappe/core/doctype/user_group_member/test_user_group_member.py b/frappe/core/doctype/user_group_member/test_user_group_member.py
index 38aade4608..8dbaed9e65 100644
--- a/frappe/core/doctype/user_group_member/test_user_group_member.py
+++ b/frappe/core/doctype/user_group_member/test_user_group_member.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
# import frappe
import unittest
diff --git a/frappe/core/doctype/user_group_member/user_group_member.py b/frappe/core/doctype/user_group_member/user_group_member.py
index 4d0656913d..f85ddc3209 100644
--- a/frappe/core/doctype/user_group_member/user_group_member.py
+++ b/frappe/core/doctype/user_group_member/user_group_member.py
@@ -2,7 +2,6 @@
# Copyright (c) 2021, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/user_permission/test_user_permission.py b/frappe/core/doctype/user_permission/test_user_permission.py
index 47651fee72..85db846982 100644
--- a/frappe/core/doctype/user_permission/test_user_permission.py
+++ b/frappe/core/doctype/user_permission/test_user_permission.py
@@ -1,7 +1,5 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
-from __future__ import unicode_literals
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See LICENSE
from frappe.core.doctype.user_permission.user_permission import add_user_permissions, remove_applicable
from frappe.permissions import has_user_permission
from frappe.core.doctype.doctype.test_doctype import new_doctype
@@ -11,11 +9,14 @@ import unittest
class TestUserPermission(unittest.TestCase):
def setUp(self):
- frappe.db.sql("""DELETE FROM `tabUser Permission`
- WHERE `user` in (
- 'test_bulk_creation_update@example.com',
- 'test_user_perm1@example.com',
- 'nested_doc_user@example.com')""")
+ test_users = (
+ "test_bulk_creation_update@example.com",
+ "test_user_perm1@example.com",
+ "nested_doc_user@example.com",
+ )
+ frappe.db.delete("User Permission", {
+ "user": ("in", test_users)
+ })
frappe.delete_doc_if_exists("DocType", "Person")
frappe.db.sql_ddl("DROP TABLE IF EXISTS `tabPerson`")
frappe.delete_doc_if_exists("DocType", "Doc A")
diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py
index fec5019ca9..5201ffef8d 100644
--- a/frappe/core/doctype/user_permission/user_permission.py
+++ b/frappe/core/doctype/user_permission/user_permission.py
@@ -1,8 +1,6 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, Frappe Technologies and contributors
+# Copyright (c) 2021, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe, json
from frappe.model.document import Document
from frappe.permissions import (get_valid_perms, update_permission_property)
@@ -17,11 +15,11 @@ class UserPermission(Document):
self.validate_default_permission()
def on_update(self):
- frappe.cache().delete_value('user_permissions')
+ frappe.cache().hdel('user_permissions', self.user)
frappe.publish_realtime('update_user_permissions')
def on_trash(self): # pylint: disable=no-self-use
- frappe.cache().delete_value('user_permissions')
+ frappe.cache().hdel('user_permissions', self.user)
frappe.publish_realtime('update_user_permissions')
def validate_user_permission(self):
@@ -180,11 +178,16 @@ def check_applicable_doc_perm(user, doctype, docname):
@frappe.whitelist()
def clear_user_permissions(user, for_doctype):
- frappe.only_for('System Manager')
- total = frappe.db.count('User Permission', filters = dict(user=user, allow=for_doctype))
+ frappe.only_for("System Manager")
+ total = frappe.db.count("User Permission", {"user": user, "allow": for_doctype})
+
if total:
- frappe.db.sql('DELETE FROM `tabUser Permission` WHERE `user`=%s AND `allow`=%s', (user, for_doctype))
+ frappe.db.delete("User Permission", {
+ "allow": for_doctype,
+ "user": user,
+ })
frappe.clear_cache()
+
return total
@frappe.whitelist()
@@ -226,7 +229,7 @@ def insert_user_perm(user, doctype, docname, is_default=0, hide_descendants=0, a
user_perm.is_default = is_default
user_perm.hide_descendants = hide_descendants
if applicable:
- user_perm.applicable_for = applicable
+ user_perm.applicable_for = applicable
user_perm.apply_to_all_doctypes = 0
else:
user_perm.apply_to_all_doctypes = 1
@@ -234,27 +237,27 @@ def insert_user_perm(user, doctype, docname, is_default=0, hide_descendants=0, a
def remove_applicable(perm_applied_docs, user, doctype, docname):
for applicable_for in perm_applied_docs:
- frappe.db.sql("""DELETE FROM `tabUser Permission`
- WHERE `user`=%s
- AND `applicable_for`=%s
- AND `allow`=%s
- AND `for_value`=%s
- """, (user, applicable_for, doctype, docname))
+ frappe.db.delete("User Permission", {
+ "applicable_for": applicable_for,
+ "for_value": docname,
+ "allow": doctype,
+ "user": user,
+ })
def remove_apply_to_all(user, doctype, docname):
- frappe.db.sql("""DELETE from `tabUser Permission`
- WHERE `user`=%s
- AND `apply_to_all_doctypes`=1
- AND `allow`=%s
- AND `for_value`=%s
- """,(user, doctype, docname))
+ frappe.db.delete("User Permission", {
+ "apply_to_all_doctypes": 1,
+ "for_value": docname,
+ "allow": doctype,
+ "user": user,
+ })
def update_applicable(already_applied, to_apply, user, doctype, docname):
for applied in already_applied:
if applied not in to_apply:
- frappe.db.sql("""DELETE FROM `tabUser Permission`
- WHERE `user`=%s
- AND `applicable_for`=%s
- AND `allow`=%s
- AND `for_value`=%s
- """,(user, applied, doctype, docname))
+ frappe.db.delete("User Permission", {
+ "applicable_for": applied,
+ "for_value": docname,
+ "allow": doctype,
+ "user": user,
+ })
diff --git a/frappe/core/doctype/user_select_document_type/user_select_document_type.py b/frappe/core/doctype/user_select_document_type/user_select_document_type.py
index 373eaf7aa3..13e3f0d351 100644
--- a/frappe/core/doctype/user_select_document_type/user_select_document_type.py
+++ b/frappe/core/doctype/user_select_document_type/user_select_document_type.py
@@ -2,7 +2,6 @@
# Copyright (c) 2021, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/user_social_login/user_social_login.py b/frappe/core/doctype/user_social_login/user_social_login.py
index cc6c3d0e05..4a34006d2b 100644
--- a/frappe/core/doctype/user_social_login/user_social_login.py
+++ b/frappe/core/doctype/user_social_login/user_social_login.py
@@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
from frappe.model.document import Document
class UserSocialLogin(Document):
diff --git a/frappe/core/doctype/user_type/test_user_type.py b/frappe/core/doctype/user_type/test_user_type.py
index de61e0f476..1c47f02bbb 100644
--- a/frappe/core/doctype/user_type/test_user_type.py
+++ b/frappe/core/doctype/user_type/test_user_type.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
# import frappe
import unittest
diff --git a/frappe/core/doctype/user_type/user_type.py b/frappe/core/doctype/user_type/user_type.py
index 0e8b692416..82ffb090f1 100644
--- a/frappe/core/doctype/user_type/user_type.py
+++ b/frappe/core/doctype/user_type/user_type.py
@@ -2,10 +2,8 @@
# Copyright (c) 2021, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe import _
-from six import iteritems
from frappe.utils import get_link_to_form
from frappe.config import get_modules_from_app
from frappe.permissions import add_permission, add_user_permission
@@ -114,7 +112,7 @@ class UserType(Document):
self.select_doctypes = []
select_doctypes = []
- user_doctypes = tuple([row.document_type for row in self.user_doctypes])
+ user_doctypes = [row.document_type for row in self.user_doctypes]
for doctype in user_doctypes:
doc = frappe.get_meta(doctype)
@@ -247,7 +245,7 @@ def apply_permissions_for_non_standard_user_type(doc, method=None):
if not user_types:
return
- for user_type, data in iteritems(user_types):
+ for user_type, data in user_types.items():
if (not doc.get(data[1]) or doc.doctype != data[0]):
continue
@@ -267,4 +265,4 @@ def apply_permissions_for_non_standard_user_type(doc, method=None):
user_doc.update_children()
add_user_permission(doc.doctype, doc.name, doc.get(data[1]))
else:
- frappe.db.set_value('User Permission', perm_data[0], 'user', doc.get(data[1]))
\ No newline at end of file
+ frappe.db.set_value('User Permission', perm_data[0], 'user', doc.get(data[1]))
diff --git a/frappe/core/doctype/user_type/user_type_dashboard.py b/frappe/core/doctype/user_type/user_type_dashboard.py
index 7e14198bca..6cdd2f82a5 100644
--- a/frappe/core/doctype/user_type/user_type_dashboard.py
+++ b/frappe/core/doctype/user_type/user_type_dashboard.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
from frappe import _
def get_data():
diff --git a/frappe/core/doctype/user_type_module/user_type_module.py b/frappe/core/doctype/user_type_module/user_type_module.py
index 6cd2cbacdb..9afbcd294d 100644
--- a/frappe/core/doctype/user_type_module/user_type_module.py
+++ b/frappe/core/doctype/user_type_module/user_type_module.py
@@ -2,7 +2,6 @@
# Copyright (c) 2021, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/version/test_version.py b/frappe/core/doctype/version/test_version.py
index 51b3c21f58..f6c099c4ea 100644
--- a/frappe/core/doctype/version/test_version.py
+++ b/frappe/core/doctype/version/test_version.py
@@ -1,7 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest, copy
from frappe.test_runner import make_test_objects
diff --git a/frappe/core/doctype/version/version.py b/frappe/core/doctype/version/version.py
index 7654db4ae5..a1bd851346 100644
--- a/frappe/core/doctype/version/version.py
+++ b/frappe/core/doctype/version/version.py
@@ -3,7 +3,6 @@
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe, json
from frappe.model.document import Document
diff --git a/frappe/core/doctype/view_log/test_view_log.py b/frappe/core/doctype/view_log/test_view_log.py
index 83967a39a4..025f3d8ad9 100644
--- a/frappe/core/doctype/view_log/test_view_log.py
+++ b/frappe/core/doctype/view_log/test_view_log.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
@@ -25,11 +23,11 @@ class TestViewLog(unittest.TestCase):
# load the form
getdoc('Event', ev.name)
a = frappe.get_value(
- doctype="View Log",
+ doctype="View Log",
filters={
"reference_doctype": "Event",
"reference_name": ev.name
- },
+ },
fieldname=['viewed_by']
)
diff --git a/frappe/core/doctype/view_log/view_log.py b/frappe/core/doctype/view_log/view_log.py
index 45e98e37c7..242250be8b 100644
--- a/frappe/core/doctype/view_log/view_log.py
+++ b/frappe/core/doctype/view_log/view_log.py
@@ -2,7 +2,6 @@
# Copyright (c) 2018, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
diff --git a/frappe/core/notifications.py b/frappe/core/notifications.py
index 771a15a2e7..707de43f28 100644
--- a/frappe/core/notifications.py
+++ b/frappe/core/notifications.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
def get_notification_config():
diff --git a/frappe/core/page/__init__.py b/frappe/core/page/__init__.py
index 4dbcd0d163..0e57cb68c3 100644
--- a/frappe/core/page/__init__.py
+++ b/frappe/core/page/__init__.py
@@ -1,4 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py
index 847b23bd3e..1f3555e351 100644
--- a/frappe/core/page/background_jobs/background_jobs.py
+++ b/frappe/core/page/background_jobs/background_jobs.py
@@ -4,12 +4,12 @@
import json
from typing import TYPE_CHECKING, Dict, List
-from rq import Queue, Worker
+from rq import Worker
import frappe
from frappe import _
from frappe.utils import convert_utc_to_user_timezone, format_datetime
-from frappe.utils.background_jobs import get_redis_conn
+from frappe.utils.background_jobs import get_redis_conn, get_queues
from frappe.utils.scheduler import is_scheduler_inactive
if TYPE_CHECKING:
@@ -29,7 +29,7 @@ def get_info(show_failed=False) -> List[Dict]:
show_failed = json.loads(show_failed)
conn = get_redis_conn()
- queues = Queue.all(conn)
+ queues = get_queues()
workers = Worker.all(conn)
jobs = []
@@ -75,7 +75,7 @@ def get_info(show_failed=False) -> List[Dict]:
@frappe.whitelist()
def remove_failed_jobs():
conn = get_redis_conn()
- queues = Queue.all(conn)
+ queues = get_queues()
for queue in queues:
fail_registry = queue.failed_job_registry
for job_id in fail_registry.get_job_ids():
diff --git a/frappe/core/page/permission_manager/permission_manager.py b/frappe/core/page/permission_manager/permission_manager.py
index 1c215eb6e1..2a99283dda 100644
--- a/frappe/core/page/permission_manager/permission_manager.py
+++ b/frappe/core/page/permission_manager/permission_manager.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
from frappe import _
import frappe.defaults
@@ -93,14 +92,14 @@ def update(doctype, role, permlevel, ptype, value=None):
"""Update role permission params
Args:
- doctype (str): Name of the DocType to update params for
- role (str): Role to be updated for, eg "Website Manager".
- permlevel (int): perm level the provided rule applies to
- ptype (str): permission type, example "read", "delete", etc.
- value (None, optional): value for ptype, None indicates False
+ doctype (str): Name of the DocType to update params for
+ role (str): Role to be updated for, eg "Website Manager".
+ permlevel (int): perm level the provided rule applies to
+ ptype (str): permission type, example "read", "delete", etc.
+ value (None, optional): value for ptype, None indicates False
Returns:
- str: Refresh flag is permission is updated successfully
+ str: Refresh flag is permission is updated successfully
"""
frappe.only_for("System Manager")
out = update_permission_property(doctype, role, permlevel, ptype, value)
@@ -111,10 +110,9 @@ def remove(doctype, role, permlevel):
frappe.only_for("System Manager")
setup_custom_perms(doctype)
- name = frappe.get_value('Custom DocPerm', dict(parent=doctype, role=role, permlevel=permlevel))
+ frappe.db.delete("Custom DocPerm", {"parent": doctype, "role": role, "permlevel": permlevel})
- frappe.db.sql('delete from `tabCustom DocPerm` where name=%s', name)
- if not frappe.get_all('Custom DocPerm', dict(parent=doctype)):
+ if not frappe.get_all('Custom DocPerm', {"parent": doctype}):
frappe.throw(_('There must be atleast one permission rule.'), title=_('Cannot Remove'))
validate_permissions_for_doctype(doctype, for_remove=True, alert=True)
diff --git a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py
index c928939119..13602ca777 100644
--- a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py
+++ b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
from frappe import _, throw
import frappe.utils.user
diff --git a/frappe/core/report/transaction_log_report/transaction_log_report.py b/frappe/core/report/transaction_log_report/transaction_log_report.py
index 9d84901f22..ff8d8345d6 100644
--- a/frappe/core/report/transaction_log_report/transaction_log_report.py
+++ b/frappe/core/report/transaction_log_report/transaction_log_report.py
@@ -1,7 +1,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
import hashlib
from frappe import _
diff --git a/frappe/core/utils.py b/frappe/core/utils.py
index 55cfbc34d7..9b8ee3a326 100644
--- a/frappe/core/utils.py
+++ b/frappe/core/utils.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
diff --git a/frappe/core/web_form/edit_profile/edit_profile.py b/frappe/core/web_form/edit_profile/edit_profile.py
index 2334f8b26d..e1ada61927 100644
--- a/frappe/core/web_form/edit_profile/edit_profile.py
+++ b/frappe/core/web_form/edit_profile/edit_profile.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
import frappe
def get_context(context):
diff --git a/frappe/core/workspace/build/build.json b/frappe/core/workspace/build/build.json
index aefda698b1..464052ba39 100644
--- a/frappe/core/workspace/build/build.json
+++ b/frappe/core/workspace/build/build.json
@@ -1,24 +1,28 @@
{
"cards_label": "Elements",
- "category": "Modules",
+ "category": "",
"charts": [],
+ "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"DocType\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Workspace\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Report\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Elements\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Modules\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Models\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Views\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Scripting\", \"col\": 4}}]",
"creation": "2021-01-02 10:51:16.579957",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
+ "extends": "",
"extends_another_page": 0,
+ "for_user": "",
"hide_custom": 0,
"icon": "tool",
"idx": 0,
"is_default": 0,
- "is_standard": 1,
+ "is_standard": 0,
"label": "Build",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Modules",
+ "link_count": 0,
"link_type": "DocType",
"onboard": 0,
"only_for": "",
@@ -28,6 +32,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Module Def",
+ "link_count": 0,
"link_to": "Module Def",
"link_type": "DocType",
"onboard": 0,
@@ -38,6 +43,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Workspace",
+ "link_count": 0,
"link_to": "Workspace",
"link_type": "DocType",
"onboard": 0,
@@ -48,6 +54,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Module Onboarding",
+ "link_count": 0,
"link_to": "Module Onboarding",
"link_type": "DocType",
"onboard": 0,
@@ -58,6 +65,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Block Module",
+ "link_count": 0,
"link_to": "Block Module",
"link_type": "DocType",
"onboard": 0,
@@ -68,6 +76,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Models",
+ "link_count": 0,
"link_type": "DocType",
"onboard": 0,
"only_for": "",
@@ -77,6 +86,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "DocType",
+ "link_count": 0,
"link_to": "DocType",
"link_type": "DocType",
"onboard": 0,
@@ -87,6 +97,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Workflow",
+ "link_count": 0,
"link_to": "Workflow",
"link_type": "DocType",
"onboard": 0,
@@ -97,6 +108,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Views",
+ "link_count": 0,
"link_type": "DocType",
"onboard": 0,
"only_for": "",
@@ -106,6 +118,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Report",
+ "link_count": 0,
"link_to": "Report",
"link_type": "DocType",
"onboard": 0,
@@ -116,6 +129,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Print Format",
+ "link_count": 0,
"link_to": "Print Format",
"link_type": "DocType",
"onboard": 0,
@@ -126,6 +140,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Workspace",
+ "link_count": 0,
"link_to": "Workspace",
"link_type": "DocType",
"onboard": 0,
@@ -136,6 +151,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Dashboard",
+ "link_count": 0,
"link_to": "Dashboard",
"link_type": "DocType",
"onboard": 0,
@@ -146,6 +162,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Scripting",
+ "link_count": 0,
"link_type": "DocType",
"onboard": 0,
"only_for": "",
@@ -155,6 +172,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Server Script",
+ "link_count": 0,
"link_to": "Server Script",
"link_type": "DocType",
"onboard": 0,
@@ -165,6 +183,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Client Script",
+ "link_count": 0,
"link_to": "Client Script",
"link_type": "DocType",
"onboard": 0,
@@ -175,6 +194,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Scheduled Job Type",
+ "link_count": 0,
"link_to": "Scheduled Job Type",
"link_type": "DocType",
"onboard": 0,
@@ -182,13 +202,19 @@
"type": "Link"
}
],
- "modified": "2021-02-04 13:48:48.493146",
+ "modified": "2021-08-05 12:15:55.793022",
"modified_by": "Administrator",
"module": "Core",
"name": "Build",
+ "onboarding": "",
"owner": "Administrator",
+ "parent_page": "",
"pin_to_bottom": 0,
"pin_to_top": 0,
+ "public": 1,
+ "restrict_to_domain": "",
+ "roles": [],
+ "sequence_id": 5,
"shortcuts": [
{
"doc_view": "",
@@ -208,5 +234,6 @@
"link_to": "Report",
"type": "DocType"
}
- ]
+ ],
+ "title": "Build"
}
\ No newline at end of file
diff --git a/frappe/core/workspace/settings/settings.json b/frappe/core/workspace/settings/settings.json
index fb26b73cfc..93a6c81c90 100644
--- a/frappe/core/workspace/settings/settings.json
+++ b/frappe/core/workspace/settings/settings.json
@@ -1,22 +1,27 @@
{
- "category": "Modules",
+ "category": "",
"charts": [],
+ "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Settings\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"System Settings\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Print Settings\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Website Settings\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Data\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Email / Notifications\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Website\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Core\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Printing\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Workflow\", \"col\": 4}}]",
"creation": "2020-03-02 15:09:40.527211",
"developer_mode_only": 0,
- "disable_user_customization": 1,
+ "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
+ "extends": "",
"extends_another_page": 0,
+ "for_user": "",
"hide_custom": 0,
"icon": "setting",
"idx": 0,
- "is_standard": 1,
+ "is_default": 0,
+ "is_standard": 0,
"label": "Settings",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Data",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -25,6 +30,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Import Data",
+ "link_count": 0,
"link_to": "Data Import",
"link_type": "DocType",
"onboard": 0,
@@ -35,6 +41,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Export Data",
+ "link_count": 0,
"link_to": "Data Export",
"link_type": "DocType",
"onboard": 0,
@@ -45,6 +52,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Bulk Update",
+ "link_count": 0,
"link_to": "Bulk Update",
"link_type": "DocType",
"onboard": 0,
@@ -55,6 +63,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Download Backups",
+ "link_count": 0,
"link_to": "backups",
"link_type": "Page",
"onboard": 0,
@@ -65,6 +74,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Deleted Documents",
+ "link_count": 0,
"link_to": "Deleted Document",
"link_type": "DocType",
"onboard": 0,
@@ -74,6 +84,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Email / Notifications",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -82,6 +93,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Email Account",
+ "link_count": 0,
"link_to": "Email Account",
"link_type": "DocType",
"onboard": 0,
@@ -92,6 +104,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Email Domain",
+ "link_count": 0,
"link_to": "Email Domain",
"link_type": "DocType",
"onboard": 0,
@@ -102,6 +115,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Notification",
+ "link_count": 0,
"link_to": "Notification",
"link_type": "DocType",
"onboard": 0,
@@ -112,6 +126,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Email Template",
+ "link_count": 0,
"link_to": "Email Template",
"link_type": "DocType",
"onboard": 0,
@@ -122,6 +137,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Auto Email Report",
+ "link_count": 0,
"link_to": "Auto Email Report",
"link_type": "DocType",
"onboard": 0,
@@ -132,6 +148,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Newsletter",
+ "link_count": 0,
"link_to": "Newsletter",
"link_type": "DocType",
"onboard": 0,
@@ -142,6 +159,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Notification Settings",
+ "link_count": 0,
"link_to": "Notification Settings",
"link_type": "DocType",
"onboard": 0,
@@ -151,6 +169,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Website",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -159,6 +178,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Website Settings",
+ "link_count": 0,
"link_to": "Website Settings",
"link_type": "DocType",
"onboard": 1,
@@ -169,6 +189,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Website Theme",
+ "link_count": 0,
"link_to": "Website Theme",
"link_type": "DocType",
"onboard": 1,
@@ -179,6 +200,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Website Script",
+ "link_count": 0,
"link_to": "Website Script",
"link_type": "DocType",
"onboard": 0,
@@ -189,6 +211,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "About Us Settings",
+ "link_count": 0,
"link_to": "About Us Settings",
"link_type": "DocType",
"onboard": 0,
@@ -199,6 +222,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Contact Us Settings",
+ "link_count": 0,
"link_to": "Contact Us Settings",
"link_type": "DocType",
"onboard": 0,
@@ -208,6 +232,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Core",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -216,6 +241,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "System Settings",
+ "link_count": 0,
"link_to": "System Settings",
"link_type": "DocType",
"onboard": 0,
@@ -226,6 +252,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Error Log",
+ "link_count": 0,
"link_to": "Error Log",
"link_type": "DocType",
"onboard": 0,
@@ -236,6 +263,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Error Snapshot",
+ "link_count": 0,
"link_to": "Error Snapshot",
"link_type": "DocType",
"onboard": 0,
@@ -246,6 +274,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Domain Settings",
+ "link_count": 0,
"link_to": "Domain Settings",
"link_type": "DocType",
"onboard": 0,
@@ -255,6 +284,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Printing",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -263,6 +293,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Print Format Builder",
+ "link_count": 0,
"link_to": "print-format-builder",
"link_type": "Page",
"onboard": 0,
@@ -273,6 +304,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Print Settings",
+ "link_count": 0,
"link_to": "Print Settings",
"link_type": "DocType",
"onboard": 0,
@@ -283,6 +315,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Print Format",
+ "link_count": 0,
"link_to": "Print Format",
"link_type": "DocType",
"onboard": 0,
@@ -293,6 +326,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Print Style",
+ "link_count": 0,
"link_to": "Print Style",
"link_type": "DocType",
"onboard": 0,
@@ -302,6 +336,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Workflow",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -310,6 +345,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Workflow",
+ "link_count": 0,
"link_to": "Workflow",
"link_type": "DocType",
"onboard": 0,
@@ -320,6 +356,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Workflow State",
+ "link_count": 0,
"link_to": "Workflow State",
"link_type": "DocType",
"onboard": 0,
@@ -330,19 +367,26 @@
"hidden": 0,
"is_query_report": 0,
"label": "Workflow Action",
+ "link_count": 0,
"link_to": "Workflow Action",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
- "modified": "2020-12-01 13:38:40.235323",
+ "modified": "2021-08-05 12:16:03.456173",
"modified_by": "Administrator",
"module": "Core",
"name": "Settings",
+ "onboarding": "",
"owner": "Administrator",
- "pin_to_bottom": 1,
+ "parent_page": "",
+ "pin_to_bottom": 0,
"pin_to_top": 0,
+ "public": 1,
+ "restrict_to_domain": "",
+ "roles": [],
+ "sequence_id": 29,
"shortcuts": [
{
"icon": "setting",
@@ -363,5 +407,6 @@
"type": "DocType"
}
],
- "shortcuts_label": "Settings"
+ "shortcuts_label": "Settings",
+ "title": "Settings"
}
\ No newline at end of file
diff --git a/frappe/core/workspace/users/users.json b/frappe/core/workspace/users/users.json
index ba82461b57..09a835ea2c 100644
--- a/frappe/core/workspace/users/users.json
+++ b/frappe/core/workspace/users/users.json
@@ -1,23 +1,27 @@
{
- "category": "Administration",
+ "category": "",
"charts": [],
+ "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"User\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Role\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Permission Manager\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"User Profile\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"User Type\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Users\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Logs\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Permissions\", \"col\": 4}}]",
"creation": "2020-03-02 15:12:16.754449",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
+ "extends": "",
"extends_another_page": 0,
+ "for_user": "",
"hide_custom": 0,
"icon": "users",
"idx": 0,
"is_default": 0,
- "is_standard": 1,
+ "is_standard": 0,
"label": "Users",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Users",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -26,6 +30,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "User",
+ "link_count": 0,
"link_to": "User",
"link_type": "DocType",
"onboard": 0,
@@ -36,6 +41,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Role",
+ "link_count": 0,
"link_to": "Role",
"link_type": "DocType",
"onboard": 0,
@@ -46,6 +52,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Role Profile",
+ "link_count": 0,
"link_to": "Role Profile",
"link_type": "DocType",
"onboard": 0,
@@ -55,6 +62,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Logs",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -63,6 +71,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Activity Log",
+ "link_count": 0,
"link_to": "Activity Log",
"link_type": "DocType",
"onboard": 0,
@@ -73,6 +82,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Access Log",
+ "link_count": 0,
"link_to": "Access Log",
"link_type": "DocType",
"onboard": 0,
@@ -82,6 +92,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Permissions",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -90,6 +101,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Role Permissions Manager",
+ "link_count": 0,
"link_to": "permission-manager",
"link_type": "Page",
"onboard": 0,
@@ -100,6 +112,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "User Permissions",
+ "link_count": 0,
"link_to": "User Permission",
"link_type": "DocType",
"onboard": 0,
@@ -110,6 +123,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Role Permission for Page and Report",
+ "link_count": 0,
"link_to": "Role Permission for Page and Report",
"link_type": "DocType",
"onboard": 0,
@@ -120,6 +134,7 @@
"hidden": 0,
"is_query_report": 1,
"label": "Permitted Documents For User",
+ "link_count": 0,
"link_to": "Permitted Documents For User",
"link_type": "Report",
"onboard": 0,
@@ -130,19 +145,26 @@
"hidden": 0,
"is_query_report": 0,
"label": "Document Share Report",
+ "link_count": 0,
"link_to": "Document Share Report",
"link_type": "Report",
"onboard": 0,
"type": "Link"
}
],
- "modified": "2021-03-25 23:02:34.582569",
+ "modified": "2021-08-05 12:16:03.010204",
"modified_by": "Administrator",
"module": "Core",
"name": "Users",
+ "onboarding": "",
"owner": "Administrator",
+ "parent_page": "",
"pin_to_bottom": 0,
"pin_to_top": 0,
+ "public": 1,
+ "restrict_to_domain": "",
+ "roles": [],
+ "sequence_id": 27,
"shortcuts": [
{
"label": "User",
@@ -170,5 +192,6 @@
"link_to": "User Type",
"type": "DocType"
}
- ]
+ ],
+ "title": "Users"
}
\ No newline at end of file
diff --git a/frappe/coverage.py b/frappe/coverage.py
new file mode 100644
index 0000000000..33f945be40
--- /dev/null
+++ b/frappe/coverage.py
@@ -0,0 +1,61 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See LICENSE
+"""
+ frappe.coverage
+ ~~~~~~~~~~~~~~~~
+
+ Coverage settings for frappe
+"""
+
+STANDARD_INCLUSIONS = ["*.py"]
+
+STANDARD_EXCLUSIONS = [
+ '*.js',
+ '*.xml',
+ '*.pyc',
+ '*.css',
+ '*.less',
+ '*.scss',
+ '*.vue',
+ '*.html',
+ '*/test_*',
+ '*/node_modules/*',
+ '*/doctype/*/*_dashboard.py',
+ '*/patches/*',
+]
+
+FRAPPE_EXCLUSIONS = [
+ "*/tests/*",
+ "*/commands/*",
+ "*/frappe/change_log/*",
+ "*/frappe/exceptions*",
+ "*frappe/setup.py",
+ "*/doctype/*/*_dashboard.py",
+ "*/patches/*",
+]
+
+class CodeCoverage():
+ def __init__(self, with_coverage, app):
+ self.with_coverage = with_coverage
+ self.app = app or 'frappe'
+
+ def __enter__(self):
+ if self.with_coverage:
+ import os
+ from coverage import Coverage
+ from frappe.utils import get_bench_path
+
+ # Generate coverage report only for app that is being tested
+ source_path = os.path.join(get_bench_path(), 'apps', self.app)
+ omit = STANDARD_EXCLUSIONS[:]
+
+ if self.app == 'frappe':
+ omit.extend(FRAPPE_EXCLUSIONS)
+
+ self.coverage = Coverage(source=[source_path], omit=omit, include=STANDARD_INCLUSIONS)
+ self.coverage.start()
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ if self.with_coverage:
+ self.coverage.stop()
+ self.coverage.save()
\ No newline at end of file
diff --git a/frappe/custom/doctype/client_script/__init__.py b/frappe/custom/doctype/client_script/__init__.py
index 4dbcd0d163..0e57cb68c3 100644
--- a/frappe/custom/doctype/client_script/__init__.py
+++ b/frappe/custom/doctype/client_script/__init__.py
@@ -1,4 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
diff --git a/frappe/custom/doctype/client_script/client_script.py b/frappe/custom/doctype/client_script/client_script.py
index 049f979263..9c098fe8c9 100644
--- a/frappe/custom/doctype/client_script/client_script.py
+++ b/frappe/custom/doctype/client_script/client_script.py
@@ -1,6 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
from frappe import _
diff --git a/frappe/custom/doctype/client_script/test_client_script.py b/frappe/custom/doctype/client_script/test_client_script.py
index de113c1ce7..b8358468b9 100644
--- a/frappe/custom/doctype/client_script/test_client_script.py
+++ b/frappe/custom/doctype/client_script/test_client_script.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
diff --git a/frappe/custom/doctype/custom_field/__init__.py b/frappe/custom/doctype/custom_field/__init__.py
index 4dbcd0d163..0e57cb68c3 100644
--- a/frappe/custom/doctype/custom_field/__init__.py
+++ b/frappe/custom/doctype/custom_field/__init__.py
@@ -1,4 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json
index 2f0819ab68..55a7ec5963 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\nGeolocation\nHTML\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
+ "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",
"reqd": 1
},
{
@@ -417,7 +417,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-10-29 06:14:43.073329",
+ "modified": "2021-07-12 05:54:13.042319",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",
diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py
index 39aff8b4a7..e266455f7a 100644
--- a/frappe/custom/doctype/custom_field/custom_field.py
+++ b/frappe/custom/doctype/custom_field/custom_field.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
import json
from frappe.utils import cstr
@@ -86,12 +85,10 @@ class CustomField(Document):
frappe.bold(self.label)))
# delete property setter entries
- frappe.db.sql("""\
- DELETE FROM `tabProperty Setter`
- WHERE doc_type = %s
- AND field_name = %s""",
- (self.dt, self.fieldname))
-
+ frappe.db.delete("Property Setter", {
+ "doc_type": self.dt,
+ "field_name": self.fieldname
+ })
frappe.clear_cache(doctype=self.dt)
def validate_insert_after(self, meta):
diff --git a/frappe/custom/doctype/custom_field/test_custom_field.py b/frappe/custom/doctype/custom_field/test_custom_field.py
index 819917050a..3196b66ee8 100644
--- a/frappe/custom/doctype/custom_field/test_custom_field.py
+++ b/frappe/custom/doctype/custom_field/test_custom_field.py
@@ -3,8 +3,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
diff --git a/frappe/custom/doctype/customize_form/__init__.py b/frappe/custom/doctype/customize_form/__init__.py
index 4dbcd0d163..0e57cb68c3 100644
--- a/frappe/custom/doctype/customize_form/__init__.py
+++ b/frappe/custom/doctype/customize_form/__init__.py
@@ -1,4 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js
index d9d8ae196e..4e00456f0d 100644
--- a/frappe/custom/doctype/customize_form/customize_form.js
+++ b/frappe/custom/doctype/customize_form/customize_form.js
@@ -117,7 +117,7 @@ frappe.ui.form.on("Customize Form", {
frappe.customize_form.set_primary_action(frm);
frm.add_custom_button(
- __("Go to {0} List", [frm.doc.doc_type]),
+ __("Go to {0} List", [__(frm.doc.doc_type)]),
function() {
frappe.set_route("List", frm.doc.doc_type);
},
diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json
index 1807678673..c2940a92e3 100644
--- a/frappe/custom/doctype/customize_form/customize_form.json
+++ b/frappe/custom/doctype/customize_form/customize_form.json
@@ -31,7 +31,6 @@
"default_print_format",
"column_break_29",
"show_preview_popup",
- "image_view",
"email_settings_section",
"default_email_template",
"column_break_26",
@@ -109,13 +108,6 @@
"fieldtype": "Check",
"label": "Track Changes"
},
- {
- "default": "0",
- "depends_on": "eval: doc.image_field",
- "fieldname": "image_view",
- "fieldtype": "Check",
- "label": "Image View"
- },
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
@@ -288,16 +280,6 @@
"fieldname": "autoname",
"fieldtype": "Data",
"label": "Auto Name"
- },
- {
- "fieldname": "default_email_template",
- "fieldtype": "Link",
- "label": "Default Email Template",
- "options": "Email Template"
- },
- {
- "fieldname": "column_break_26",
- "fieldtype": "Column Break"
}
],
"hide_toolbar": 1,
@@ -306,7 +288,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-04-29 21:21:06.476372",
+ "modified": "2021-06-21 19:01:06.920663",
"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 be0dded99c..8de194fb00 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -1,7 +1,6 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See LICENSE
-from __future__ import unicode_literals
"""
Customize Form is a Single DocType used to mask the Property Setter
Thus providing a better UI from user perspective
@@ -19,10 +18,11 @@ from frappe.custom.doctype.property_setter.property_setter import delete_propert
from frappe.model.docfield import supports_translation
from frappe.core.doctype.doctype.doctype import validate_series
+
class CustomizeForm(Document):
def on_update(self):
- frappe.db.sql("delete from tabSingles where doctype='Customize Form'")
- frappe.db.sql("delete from `tabCustomize Form Field`")
+ frappe.db.delete("Singles", {"doctype": "Customize Form"})
+ frappe.db.delete("Customize Form Field")
@frappe.whitelist()
def fetch_to_customize(self):
@@ -356,9 +356,9 @@ class CustomizeForm(Document):
def delete_custom_fields(self):
meta = frappe.get_meta(self.doc_type)
- fields_to_remove = (set([df.fieldname for df in meta.get("fields")])
- - set(df.fieldname for df in self.get("fields")))
-
+ fields_to_remove = (
+ {df.fieldname for df in meta.get("fields")} - {df.fieldname for df in self.get("fields")}
+ )
for fieldname in fields_to_remove:
df = meta.get("fields", {"fieldname": fieldname})[0]
if df.get("is_custom_field"):
diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py
index 75555a8205..aef95cd676 100644
--- a/frappe/custom/doctype/customize_form/test_customize_form.py
+++ b/frappe/custom/doctype/customize_form/test_customize_form.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe, unittest, json
from frappe.test_runner import make_test_records_for_doctype
from frappe.core.doctype.doctype.doctype import InvalidFieldNameError
@@ -233,6 +232,32 @@ class TestCustomizeForm(unittest.TestCase):
testdt.delete()
testdt1.delete()
+ def test_custom_internal_links(self):
+ # add a custom internal link
+ frappe.clear_cache()
+ d = self.get_customize_form("User Group")
+
+ d.append('links', dict(link_doctype='User Group Member', parent_doctype='User',
+ link_fieldname='user', table_fieldname='user_group_members', group='Tests', custom=1))
+
+ d.run_method("save_customization")
+
+ frappe.clear_cache()
+ user_group = frappe.get_meta('User Group')
+
+ # check links exist
+ self.assertTrue([d.name for d in user_group.links if d.link_doctype == 'User Group Member'])
+ self.assertTrue([d.name for d in user_group.links if d.parent_doctype == 'User'])
+
+ # remove the link
+ d = self.get_customize_form("User Group")
+ d.links = []
+ d.run_method("save_customization")
+
+ frappe.clear_cache()
+ user_group = frappe.get_meta('Event')
+ self.assertFalse([d.name for d in (user_group.links or []) if d.link_doctype == 'User Group Member'])
+
def test_custom_action(self):
test_route = '/app/List/DocType'
diff --git a/frappe/custom/doctype/customize_form_field/__init__.py b/frappe/custom/doctype/customize_form_field/__init__.py
index 4dbcd0d163..0e57cb68c3 100644
--- a/frappe/custom/doctype/customize_form_field/__init__.py
+++ b/frappe/custom/doctype/customize_form_field/__init__.py
@@ -1,4 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
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 227114137c..0a456b1026 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\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",
+ "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",
"reqd": 1,
"search_index": 1
},
@@ -428,7 +428,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-10-29 06:11:57.661039",
+ "modified": "2021-07-10 21:57:24.479749",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",
diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.py b/frappe/custom/doctype/customize_form_field/customize_form_field.py
index 20c206328c..f288e70754 100644
--- a/frappe/custom/doctype/customize_form_field/customize_form_field.py
+++ b/frappe/custom/doctype/customize_form_field/customize_form_field.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.js b/frappe/custom/doctype/doctype_layout/doctype_layout.js
index 679330e065..533efea9b8 100644
--- a/frappe/custom/doctype/doctype_layout/doctype_layout.js
+++ b/frappe/custom/doctype/doctype_layout/doctype_layout.js
@@ -23,7 +23,7 @@ frappe.ui.form.on('DocType Layout', {
set_button(frm) {
if (!frm.is_new()) {
frm.add_custom_button(__('Go to {0} List', [frm.doc.name]), () => {
- window.open(`/app/list/${frappe.router.slug(frm.doc.name)}/list`);
+ window.open(`/app/${frappe.router.slug(frm.doc.name)}`);
});
}
}
diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.py b/frappe/custom/doctype/doctype_layout/doctype_layout.py
index a4fe9a9bce..0dc320353d 100644
--- a/frappe/custom/doctype/doctype_layout/doctype_layout.py
+++ b/frappe/custom/doctype/doctype_layout/doctype_layout.py
@@ -2,8 +2,6 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
-
from frappe.model.document import Document
from frappe.desk.utils import slug
diff --git a/frappe/custom/doctype/doctype_layout/test_doctype_layout.py b/frappe/custom/doctype/doctype_layout/test_doctype_layout.py
index 5765c86262..dcde3c00a4 100644
--- a/frappe/custom/doctype/doctype_layout/test_doctype_layout.py
+++ b/frappe/custom/doctype/doctype_layout/test_doctype_layout.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
# import frappe
import unittest
diff --git a/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.json b/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.json
index a1a36216c3..006c01ae4e 100644
--- a/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.json
+++ b/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.json
@@ -20,14 +20,13 @@
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
- "label": "Label",
- "reqd": 1
+ "label": "Label"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-11-16 17:13:01.892345",
+ "modified": "2021-05-19 16:27:40.585865",
"modified_by": "Administrator",
"module": "Custom",
"name": "DocType Layout Field",
@@ -36,4 +35,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py b/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py
index 7f8c8edfce..c1e963602f 100644
--- a/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py
+++ b/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py
@@ -2,7 +2,6 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/custom/doctype/property_setter/__init__.py b/frappe/custom/doctype/property_setter/__init__.py
index 4dbcd0d163..0e57cb68c3 100644
--- a/frappe/custom/doctype/property_setter/__init__.py
+++ b/frappe/custom/doctype/property_setter/__init__.py
@@ -1,4 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
diff --git a/frappe/custom/doctype/property_setter/property_setter.py b/frappe/custom/doctype/property_setter/property_setter.py
index 56e5829271..2a6c06b70a 100644
--- a/frappe/custom/doctype/property_setter/property_setter.py
+++ b/frappe/custom/doctype/property_setter/property_setter.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
from frappe import _
diff --git a/frappe/custom/doctype/property_setter/test_property_setter.py b/frappe/custom/doctype/property_setter/test_property_setter.py
index 33e7d288a4..4d4de66d51 100644
--- a/frappe/custom/doctype/property_setter/test_property_setter.py
+++ b/frappe/custom/doctype/property_setter/test_property_setter.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
diff --git a/frappe/custom/doctype/test_rename_new/test_rename_new.py b/frappe/custom/doctype/test_rename_new/test_rename_new.py
index aa5984e466..32d2396b2b 100644
--- a/frappe/custom/doctype/test_rename_new/test_rename_new.py
+++ b/frappe/custom/doctype/test_rename_new/test_rename_new.py
@@ -2,7 +2,6 @@
# Copyright (c) 2021, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/custom/doctype/test_rename_new/test_test_rename_new.py b/frappe/custom/doctype/test_rename_new/test_test_rename_new.py
index 554efbae45..b3ea4818de 100644
--- a/frappe/custom/doctype/test_rename_new/test_test_rename_new.py
+++ b/frappe/custom/doctype/test_rename_new/test_test_rename_new.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
# import frappe
import unittest
diff --git a/frappe/custom/workspace/customization/customization.json b/frappe/custom/workspace/customization/customization.json
index cdc3b73366..136b1a57eb 100644
--- a/frappe/custom/workspace/customization/customization.json
+++ b/frappe/custom/workspace/customization/customization.json
@@ -1,23 +1,27 @@
{
- "category": "Administration",
+ "category": "",
"charts": [],
+ "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Customize Form\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Custom Role\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Client Script\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Server Script\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Dashboards\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Form Customization\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other\", \"col\": 4}}]",
"creation": "2020-03-02 15:15:03.839594",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
+ "extends": "",
"extends_another_page": 0,
+ "for_user": "",
"hide_custom": 0,
"icon": "customization",
"idx": 0,
"is_default": 0,
- "is_standard": 1,
+ "is_standard": 0,
"label": "Customization",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Dashboards",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -26,6 +30,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Dashboard",
+ "link_count": 0,
"link_to": "Dashboard",
"link_type": "DocType",
"onboard": 0,
@@ -36,6 +41,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Dashboard Chart",
+ "link_count": 0,
"link_to": "Dashboard Chart",
"link_type": "DocType",
"onboard": 0,
@@ -46,6 +52,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Dashboard Chart Source",
+ "link_count": 0,
"link_to": "Dashboard Chart Source",
"link_type": "DocType",
"onboard": 0,
@@ -55,6 +62,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Form Customization",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -63,6 +71,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Customize Form",
+ "link_count": 0,
"link_to": "Customize Form",
"link_type": "DocType",
"onboard": 0,
@@ -73,6 +82,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Custom Field",
+ "link_count": 0,
"link_to": "Custom Field",
"link_type": "DocType",
"onboard": 0,
@@ -83,6 +93,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Client Script",
+ "link_count": 0,
"link_to": "Client Script",
"link_type": "DocType",
"onboard": 0,
@@ -93,6 +104,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "DocType",
+ "link_count": 0,
"link_to": "DocType",
"link_type": "DocType",
"onboard": 0,
@@ -102,6 +114,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Other",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -110,19 +123,26 @@
"hidden": 0,
"is_query_report": 0,
"label": "Custom Translations",
+ "link_count": 0,
"link_to": "Translation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
- "modified": "2021-02-04 13:50:35.750463",
+ "modified": "2021-08-05 12:15:57.486112",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customization",
+ "onboarding": "",
"owner": "Administrator",
+ "parent_page": "",
"pin_to_bottom": 0,
"pin_to_top": 0,
+ "public": 1,
+ "restrict_to_domain": "",
+ "roles": [],
+ "sequence_id": 8,
"shortcuts": [
{
"label": "Customize Form",
@@ -145,5 +165,6 @@
"link_to": "Server Script",
"type": "DocType"
}
- ]
+ ],
+ "title": "Customization"
}
\ No newline at end of file
diff --git a/frappe/data_migration/doctype/data_migration_connector/connectors/base.py b/frappe/data_migration/doctype/data_migration_connector/connectors/base.py
index 97f9f5f4a3..5eca7cfac5 100644
--- a/frappe/data_migration/doctype/data_migration_connector/connectors/base.py
+++ b/frappe/data_migration/doctype/data_migration_connector/connectors/base.py
@@ -1,10 +1,7 @@
-from __future__ import unicode_literals
-from six import with_metaclass
from abc import ABCMeta, abstractmethod
from frappe.utils.password import get_decrypted_password
-class BaseConnection(with_metaclass(ABCMeta)):
-
+class BaseConnection(metaclass=ABCMeta):
@abstractmethod
def get(self, remote_objectname, fields=None, filters=None, start=0, page_length=10):
pass
diff --git a/frappe/data_migration/doctype/data_migration_connector/connectors/frappe_connection.py b/frappe/data_migration/doctype/data_migration_connector/connectors/frappe_connection.py
index 6ee41afdf2..473a15c2dc 100644
--- a/frappe/data_migration/doctype/data_migration_connector/connectors/frappe_connection.py
+++ b/frappe/data_migration/doctype/data_migration_connector/connectors/frappe_connection.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
from frappe.frappeclient import FrappeClient
from .base import BaseConnection
diff --git a/frappe/data_migration/doctype/data_migration_connector/data_migration_connector.py b/frappe/data_migration/doctype/data_migration_connector/data_migration_connector.py
index 793dfe6694..d1137f2e67 100644
--- a/frappe/data_migration/doctype/data_migration_connector/data_migration_connector.py
+++ b/frappe/data_migration/doctype/data_migration_connector/data_migration_connector.py
@@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe, os
from frappe.model.document import Document
from frappe import _
@@ -76,8 +75,7 @@ def get_connection_class(python_module):
return _class
-connection_boilerplate = """from __future__ import unicode_literals
-from frappe.data_migration.doctype.data_migration_connector.connectors.base import BaseConnection
+connection_boilerplate = """from frappe.data_migration.doctype.data_migration_connector.connectors.base import BaseConnection
class {connection_class}(BaseConnection):
def __init__(self, connector):
diff --git a/frappe/data_migration/doctype/data_migration_connector/test_data_migration_connector.py b/frappe/data_migration/doctype/data_migration_connector/test_data_migration_connector.py
index a6e30fbe44..fd45f86ec1 100644
--- a/frappe/data_migration/doctype/data_migration_connector/test_data_migration_connector.py
+++ b/frappe/data_migration/doctype/data_migration_connector/test_data_migration_connector.py
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
import unittest
class TestDataMigrationConnector(unittest.TestCase):
diff --git a/frappe/data_migration/doctype/data_migration_mapping/data_migration_mapping.py b/frappe/data_migration/doctype/data_migration_mapping/data_migration_mapping.py
index 1cc54a0d1a..5cb20ba56c 100644
--- a/frappe/data_migration/doctype/data_migration_mapping/data_migration_mapping.py
+++ b/frappe/data_migration/doctype/data_migration_mapping/data_migration_mapping.py
@@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils.safe_exec import get_safe_globals
diff --git a/frappe/data_migration/doctype/data_migration_mapping/test_data_migration_mapping.py b/frappe/data_migration/doctype/data_migration_mapping/test_data_migration_mapping.py
index e6f0ce2796..df11fc0522 100644
--- a/frappe/data_migration/doctype/data_migration_mapping/test_data_migration_mapping.py
+++ b/frappe/data_migration/doctype/data_migration_mapping/test_data_migration_mapping.py
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
import unittest
class TestDataMigrationMapping(unittest.TestCase):
diff --git a/frappe/data_migration/doctype/data_migration_mapping_detail/data_migration_mapping_detail.py b/frappe/data_migration/doctype/data_migration_mapping_detail/data_migration_mapping_detail.py
index 1ccdf76eed..6d3ef50937 100644
--- a/frappe/data_migration/doctype/data_migration_mapping_detail/data_migration_mapping_detail.py
+++ b/frappe/data_migration/doctype/data_migration_mapping_detail/data_migration_mapping_detail.py
@@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
from frappe.model.document import Document
class DataMigrationMappingDetail(Document):
diff --git a/frappe/data_migration/doctype/data_migration_plan/data_migration_plan.py b/frappe/data_migration/doctype/data_migration_plan/data_migration_plan.py
index 5cd195f4fe..a8d0e40a4c 100644
--- a/frappe/data_migration/doctype/data_migration_plan/data_migration_plan.py
+++ b/frappe/data_migration/doctype/data_migration_plan/data_migration_plan.py
@@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.modules import get_module_path, scrub_dt_dn
from frappe.modules.export_file import export_to_files, create_init_py
diff --git a/frappe/data_migration/doctype/data_migration_plan/test_data_migration_plan.py b/frappe/data_migration/doctype/data_migration_plan/test_data_migration_plan.py
index 3a33039c3d..14c585a82d 100644
--- a/frappe/data_migration/doctype/data_migration_plan/test_data_migration_plan.py
+++ b/frappe/data_migration/doctype/data_migration_plan/test_data_migration_plan.py
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
import unittest
class TestDataMigrationPlan(unittest.TestCase):
diff --git a/frappe/data_migration/doctype/data_migration_plan_mapping/data_migration_plan_mapping.py b/frappe/data_migration/doctype/data_migration_plan_mapping/data_migration_plan_mapping.py
index 85f879069c..ba4cf28eb8 100644
--- a/frappe/data_migration/doctype/data_migration_plan_mapping/data_migration_plan_mapping.py
+++ b/frappe/data_migration/doctype/data_migration_plan_mapping/data_migration_plan_mapping.py
@@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
from frappe.model.document import Document
class DataMigrationPlanMapping(Document):
diff --git a/frappe/data_migration/doctype/data_migration_run/data_migration_run.py b/frappe/data_migration/doctype/data_migration_run/data_migration_run.py
index aed9c6cb1d..c35af5827b 100644
--- a/frappe/data_migration/doctype/data_migration_run/data_migration_run.py
+++ b/frappe/data_migration/doctype/data_migration_run/data_migration_run.py
@@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe, json, math
from frappe.model.document import Document
from frappe import _
diff --git a/frappe/data_migration/doctype/data_migration_run/test_data_migration_run.py b/frappe/data_migration/doctype/data_migration_run/test_data_migration_run.py
index c6c3ea138c..ef7b70dca2 100644
--- a/frappe/data_migration/doctype/data_migration_run/test_data_migration_run.py
+++ b/frappe/data_migration/doctype/data_migration_run/test_data_migration_run.py
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
import frappe, unittest
class TestDataMigrationRun(unittest.TestCase):
diff --git a/frappe/database/__init__.py b/frappe/database/__init__.py
index 1f0d3f9bf5..a899bec3d1 100644
--- a/frappe/database/__init__.py
+++ b/frappe/database/__init__.py
@@ -4,8 +4,6 @@
# Database Module
# --------------------
-from __future__ import unicode_literals
-
def setup_database(force, source_sql=None, verbose=None, no_mariadb_socket=False):
import frappe
if frappe.conf.db_type == 'postgres':
diff --git a/frappe/database/database.py b/frappe/database/database.py
index c9c1ec3909..d6ecf0795d 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -4,10 +4,9 @@
# Database Module
# --------------------
-from __future__ import unicode_literals
-
import re
import time
+from typing import Dict, List, Union
import frappe
import datetime
import frappe.defaults
@@ -15,17 +14,9 @@ import frappe.model.meta
from frappe import _
from time import time
-from frappe.utils import now, getdate, cast_fieldtype, get_datetime
+from frappe.utils import now, getdate, cast_fieldtype, get_datetime, get_table_name
from frappe.model.utils.link_count import flush_local_link_count
-from frappe.utils import cint
-# imports - compatibility imports
-from six import (
- integer_types,
- string_types,
- text_type,
- iteritems
-)
class Database(object):
"""
@@ -113,6 +104,7 @@ class Database(object):
{"name": "a%", "owner":"test@example.com"})
"""
+ query = str(query)
if re.search(r'ifnull\(', query, flags=re.IGNORECASE):
# replaces ifnull in query with coalesce
query = re.sub(r'ifnull\(', 'coalesce(', query, flags=re.IGNORECASE)
@@ -277,7 +269,7 @@ class Database(object):
for r in result:
values = []
for value in r:
- if as_utf8 and isinstance(value, text_type):
+ if as_utf8 and isinstance(value, str):
value = value.encode('utf-8')
values.append(value)
@@ -294,7 +286,7 @@ class Database(object):
"""Returns true if the first row in the result has a Date, Datetime, Long Int."""
if result and result[0]:
for v in result[0]:
- if isinstance(v, (datetime.date, datetime.timedelta, datetime.datetime, integer_types)):
+ if isinstance(v, (datetime.date, datetime.timedelta, datetime.datetime, int)):
return True
if formatted and isinstance(v, (int, float)):
return True
@@ -312,7 +304,7 @@ class Database(object):
for r in res:
nr = []
for val in r:
- if as_utf8 and isinstance(val, text_type):
+ if as_utf8 and isinstance(val, str):
val = val.encode('utf-8')
nr.append(val)
nres.append(nr)
@@ -344,7 +336,7 @@ class Database(object):
values[key] = value[1]
if isinstance(value[1], (tuple, list)):
# value is a list in tuple ("in", ("A", "B"))
- _rhs = " ({0})".format(", ".join([self.escape(v) for v in value[1]]))
+ _rhs = " ({0})".format(", ".join(self.escape(v) for v in value[1]))
del values[key]
if _operator not in ["=", "!=", ">", ">=", "<", "<=", "like", "in", "not in", "not like"]:
@@ -363,7 +355,7 @@ class Database(object):
# docname is a number, convert to string
filters = str(filters)
- if isinstance(filters, string_types):
+ if isinstance(filters, str):
filters = { "name": filters }
for f in filters:
@@ -428,7 +420,7 @@ class Database(object):
user = frappe.db.get_values("User", "test@example.com", "*")[0]
"""
out = None
- if cache and isinstance(filters, string_types) and \
+ if cache and isinstance(filters, str) and \
(doctype, filters, fieldname) in self.value_cache:
return self.value_cache[(doctype, filters, fieldname)]
@@ -440,7 +432,7 @@ class Database(object):
else:
fields = fieldname
if fieldname!="*":
- if isinstance(fieldname, string_types):
+ if isinstance(fieldname, str):
fields = [fieldname]
else:
fields = fieldname
@@ -461,7 +453,7 @@ class Database(object):
else:
out = self.get_values_from_single(fields, filters, doctype, as_dict, debug, update)
- if cache and isinstance(filters, string_types):
+ if cache and isinstance(filters, str):
self.value_cache[(doctype, filters, fieldname)] = out
return out
@@ -551,7 +543,7 @@ class Database(object):
"""
if not doctype in self.value_cache:
- self.value_cache = self.value_cache[doctype] = {}
+ self.value_cache[doctype] = {}
if fieldname in self.value_cache[doctype]:
return self.value_cache[doctype][fieldname]
@@ -565,8 +557,7 @@ class Database(object):
if not df:
frappe.throw(_('Invalid field name: {0}').format(frappe.bold(fieldname)), self.InvalidColumnName)
- if df.fieldtype in frappe.model.numeric_fieldtypes:
- val = cint(val)
+ val = cast_fieldtype(df.fieldtype, val)
self.value_cache[doctype][fieldname] = val
@@ -673,7 +664,7 @@ class Database(object):
where field in ({0}) and
doctype=%s'''.format(', '.join(['%s']*len(keys))),
list(keys) + [dt], debug=debug)
- for key, value in iteritems(to_update):
+ for key, value in to_update.items():
self.sql('''insert into `tabSingles` (doctype, field, value) values (%s, %s, %s)''',
(dt, key, value), debug=debug)
@@ -811,7 +802,7 @@ class Database(object):
:param dt: DocType name.
:param dn: Document name or filter dict."""
- if isinstance(dt, string_types):
+ if isinstance(dt, str):
if dt!="DocType" and dt==dn:
return True # single always exists (!)
try:
@@ -962,15 +953,37 @@ class Database(object):
query = sql_dict.get(current_dialect)
return self.sql(query, values, **kwargs)
- def delete(self, doctype, conditions, debug=False):
- if conditions:
- conditions, values = self.build_conditions(conditions)
- return self.sql("DELETE FROM `tab{doctype}` where {conditions}".format(
- doctype=doctype,
- conditions=conditions
- ), values, debug=debug)
- else:
- frappe.throw(_('No conditions provided'))
+ def delete(self, doctype: str, filters: Union[Dict, List] = None, debug=False, **kwargs):
+ """Delete rows from a table in site which match the passed filters. This
+ does trigger DocType hooks. Simply runs a DELETE query in the database.
+
+ Doctype name can be passed directly, it will be pre-pended with `tab`.
+ """
+ values = ()
+ filters = filters or kwargs.get("conditions")
+ table = get_table_name(doctype)
+ query = f"DELETE FROM `{table}`"
+
+ if "debug" not in kwargs:
+ kwargs["debug"] = debug
+
+ if filters:
+ conditions, values = self.build_conditions(filters)
+ query = f"{query} WHERE {conditions}"
+
+ return self.sql(query, values, **kwargs)
+
+ def truncate(self, doctype: str):
+ """Truncate a table in the database. This runs a DDL command `TRUNCATE TABLE`.
+ This cannot be rolled back.
+
+ Doctype name can be passed directly, it will be pre-pended with `tab`.
+ """
+ table = doctype if doctype.startswith("__") else f"tab{doctype}"
+ return self.sql_ddl(f"truncate `{table}`")
+
+ def clear_table(self, doctype):
+ return self.truncate(doctype)
def get_last_created(self, doctype):
last_record = self.get_all(doctype, ('creation'), limit=1, order_by='creation desc')
@@ -979,9 +992,6 @@ class Database(object):
else:
return None
- def clear_table(self, doctype):
- self.sql('truncate `tab{}`'.format(doctype))
-
def log_touched_tables(self, query, values=None):
if values:
query = frappe.safe_decode(self._cursor.mogrify(query, values))
@@ -1019,7 +1029,7 @@ class Database(object):
:params values: list of list of values
"""
insert_list = []
- fields = ", ".join(["`"+field+"`" for field in fields])
+ fields = ", ".join("`"+field+"`" for field in fields)
for idx, value in enumerate(values):
insert_list.append(tuple(value))
@@ -1032,6 +1042,7 @@ class Database(object):
), tuple(insert_list))
insert_list = []
+
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 879c8394d7..d4a119804b 100644
--- a/frappe/database/mariadb/database.py
+++ b/frappe/database/mariadb/database.py
@@ -1,3 +1,5 @@
+from typing import List, Tuple, Union
+
import pymysql
from pymysql.constants import ER, FIELD_TYPE
from pymysql.converters import conversions, escape_string
@@ -5,7 +7,7 @@ from pymysql.converters import conversions, escape_string
import frappe
from frappe.database.database import Database
from frappe.database.mariadb.schema import MariaDBTable
-from frappe.utils import UnicodeWithAttrs, cstr, get_datetime
+from frappe.utils import UnicodeWithAttrs, cstr, get_datetime, get_table_name
class MariaDBDatabase(Database):
@@ -49,7 +51,8 @@ class MariaDBDatabase(Database):
'Color': ('varchar', self.VARCHAR_LEN),
'Barcode': ('longtext', ''),
'Geolocation': ('longtext', ''),
- 'Duration': ('decimal', '18,6')
+ 'Duration': ('decimal', '18,6'),
+ 'Icon': ('varchar', self.VARCHAR_LEN)
}
def get_connection(self):
@@ -123,6 +126,19 @@ class MariaDBDatabase(Database):
def is_type_datetime(code):
return code in (pymysql.DATE, pymysql.DATETIME)
+ def rename_table(self, old_name: str, new_name: str) -> Union[List, Tuple]:
+ old_name = get_table_name(old_name)
+ new_name = get_table_name(new_name)
+ return self.sql(f"RENAME TABLE `{old_name}` TO `{new_name}`")
+
+ def describe(self, doctype: str) -> Union[List, Tuple]:
+ table_name = get_table_name(doctype)
+ return self.sql(f"DESC `{table_name}`")
+
+ def change_column_type(self, table: str, column: str, type: str) -> Union[List, Tuple]:
+ table_name = get_table_name(table)
+ return self.sql(f"ALTER TABLE `{table_name}` MODIFY `{column}` {type} NOT NULL")
+
# exception types
@staticmethod
def is_deadlocked(e):
diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql
index a52efd01e3..f8841e9417 100644
--- a/frappe/database/mariadb/framework_mariadb.sql
+++ b/frappe/database/mariadb/framework_mariadb.sql
@@ -220,6 +220,7 @@ CREATE TABLE `tabDocType` (
`allow_guest_to_view` int(1) NOT NULL DEFAULT 0,
`route` varchar(255) DEFAULT NULL,
`is_published_field` varchar(255) DEFAULT NULL,
+ `website_search_field` varchar(255) DEFAULT NULL,
`email_append_to` int(1) NOT NULL DEFAULT 0,
`subject_field` varchar(255) DEFAULT NULL,
`sender_field` varchar(255) DEFAULT NULL,
diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py
index 4bbecd2a2e..b40af59286 100644
--- a/frappe/database/mariadb/schema.py
+++ b/frappe/database/mariadb/schema.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
import frappe
from frappe import _
from frappe.database.schema import DBTable
diff --git a/frappe/database/mariadb/setup_db.py b/frappe/database/mariadb/setup_db.py
index 9b73d77171..6be08c66bb 100644
--- a/frappe/database/mariadb/setup_db.py
+++ b/frappe/database/mariadb/setup_db.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
import frappe
import os
from frappe.database.db_manager import DbManager
diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py
index 6ac2767a71..00e60fb8d2 100644
--- a/frappe/database/postgres/database.py
+++ b/frappe/database/postgres/database.py
@@ -1,13 +1,14 @@
import re
-import frappe
+from typing import List, Tuple, Union
+
import psycopg2
import psycopg2.extensions
-from six import string_types
-from frappe.utils import cstr
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
+import frappe
from frappe.database.database import Database
from frappe.database.postgres.schema import PostgresTable
+from frappe.utils import cstr, get_table_name
# cast decimals as floats
DEC2FLOAT = psycopg2.extensions.new_type(
@@ -59,7 +60,8 @@ class PostgresDatabase(Database):
'Color': ('varchar', self.VARCHAR_LEN),
'Barcode': ('text', ''),
'Geolocation': ('text', ''),
- 'Duration': ('decimal', '18,6')
+ 'Duration': ('decimal', '18,6'),
+ 'Icon': ('varchar', self.VARCHAR_LEN)
}
def get_connection(self):
@@ -171,6 +173,19 @@ class PostgresDatabase(Database):
def is_data_too_long(e):
return e.pgcode == '22001'
+ def rename_table(self, old_name: str, new_name: str) -> Union[List, Tuple]:
+ old_name = get_table_name(old_name)
+ new_name = get_table_name(new_name)
+ return self.sql(f"ALTER TABLE `{old_name}` RENAME TO `{new_name}`")
+
+ def describe(self, doctype: str)-> Union[List, Tuple]:
+ table_name = get_table_name(doctype)
+ return self.sql(f"SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME = '{table_name}'")
+
+ def change_column_type(self, table: str, column: str, type: str) -> Union[List, Tuple]:
+ table_name = get_table_name(table)
+ return self.sql(f'ALTER TABLE "{table_name}" ALTER COLUMN "{column}" TYPE {type}')
+
def create_auth_table(self):
self.sql_ddl("""create table if not exists "__Auth" (
"doctype" VARCHAR(140) NOT NULL,
@@ -253,7 +268,7 @@ class PostgresDatabase(Database):
self.sql("""CREATE INDEX IF NOT EXISTS "{}" ON `{}`("{}")""".format(index_name, table_name, '", "'.join(fields)))
def add_unique(self, doctype, fields, constraint_name=None):
- if isinstance(fields, string_types):
+ if isinstance(fields, str):
fields = [fields]
if not constraint_name:
constraint_name = "unique_" + "_".join(fields)
@@ -298,6 +313,7 @@ class PostgresDatabase(Database):
def modify_query(query):
""""Modifies query according to the requirements of postgres"""
# replace ` with " for definitions
+ query = str(query)
query = query.replace('`', '"')
query = replace_locate_with_strpos(query)
# select from requires ""
diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql
index eeb0eecd3f..a4e94aa326 100644
--- a/frappe/database/postgres/framework_postgres.sql
+++ b/frappe/database/postgres/framework_postgres.sql
@@ -225,6 +225,7 @@ CREATE TABLE "tabDocType" (
"allow_guest_to_view" smallint NOT NULL DEFAULT 0,
"route" varchar(255) DEFAULT NULL,
"is_published_field" varchar(255) DEFAULT NULL,
+ "website_search_field" varchar(255) DEFAULT NULL,
"email_append_to" smallint NOT NULL DEFAULT 0,
"subject_field" varchar(255) DEFAULT NULL,
"sender_field" varchar(255) DEFAULT NULL,
diff --git a/frappe/database/postgres/setup_db.py b/frappe/database/postgres/setup_db.py
index 3ee6b6a286..19ba681237 100644
--- a/frappe/database/postgres/setup_db.py
+++ b/frappe/database/postgres/setup_db.py
@@ -83,7 +83,6 @@ def get_root_connection(root_login=None, root_password=None):
root_login = frappe.conf.get("root_login") or None
if not root_login:
- from six.moves import input
root_login = input("Enter postgres super user: ")
if not root_password:
diff --git a/frappe/database/schema.py b/frappe/database/schema.py
index 5f5ba06d8b..31f11dbd5e 100644
--- a/frappe/database/schema.py
+++ b/frappe/database/schema.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
import re
import frappe
diff --git a/frappe/defaults.py b/frappe/defaults.py
index 4bec6677c7..d4c338388d 100644
--- a/frappe/defaults.py
+++ b/frappe/defaults.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
from frappe.desk.notifications import clear_notifications
from frappe.cache_manager import clear_defaults_cache, common_default_keys
@@ -125,11 +124,10 @@ def set_default(key, value, parent, parenttype="__default"):
where
defkey=%s and parent=%s
for update''', (key, parent)):
- frappe.db.sql("""
- delete from
- `tabDefaultValue`
- where
- defkey=%s and parent=%s""", (key, parent))
+ frappe.db.delete("DefaultValue", {
+ "defkey": key,
+ "parent": parent
+ })
if value != None:
add_default(key, value, parent)
else:
@@ -156,29 +154,23 @@ def clear_default(key=None, value=None, parent=None, name=None, parenttype=None)
:param name: Default ID.
:param parenttype: Clear defaults table for a particular type e.g. **User**.
"""
- conditions = []
- values = []
+ filters = {}
if name:
- conditions.append("name=%s")
- values.append(name)
+ filters.update({"name": name})
else:
if key:
- conditions.append("defkey=%s")
- values.append(key)
+ filters.update({"defkey": key})
if value:
- conditions.append("defvalue=%s")
- values.append(value)
+ filters.update({"defvalue": value})
if parent:
- conditions.append("parent=%s")
- values.append(parent)
+ filters.update({"parent": parent})
if parenttype:
- conditions.append("parenttype=%s")
- values.append(parenttype)
+ filters.update({"parenttype": parenttype})
if parent:
clear_defaults_cache(parent)
@@ -186,11 +178,10 @@ def clear_default(key=None, value=None, parent=None, name=None, parenttype=None)
clear_defaults_cache("__default")
clear_defaults_cache("__global")
- if not conditions:
+ if not filters:
raise Exception("[clear_default] No key specified.")
- frappe.db.sql("""delete from tabDefaultValue where {0}""".format(" and ".join(conditions)),
- tuple(values))
+ frappe.db.delete("DefaultValue", filters)
_clear_cache(parent)
diff --git a/frappe/desk/__init__.py b/frappe/desk/__init__.py
index 4dbcd0d163..0e57cb68c3 100644
--- a/frappe/desk/__init__.py
+++ b/frappe/desk/__init__.py
@@ -1,4 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
diff --git a/frappe/desk/calendar.py b/frappe/desk/calendar.py
index 064d870092..273b2654bf 100644
--- a/frappe/desk/calendar.py
+++ b/frappe/desk/calendar.py
@@ -1,8 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
-
import frappe
from frappe import _
import json
@@ -27,7 +25,6 @@ def get_event_conditions(doctype, filters=None):
@frappe.whitelist()
def get_events(doctype, start, end, field_map, filters=None, fields=None):
-
field_map = frappe._dict(json.loads(field_map))
fields = frappe.parse_json(fields)
@@ -38,8 +35,7 @@ def get_events(doctype, start, end, field_map, filters=None, fields=None):
"color": d.fieldname
})
- if filters:
- filters = json.loads(filters or '')
+ filters = json.loads(filters) if filters else []
if not fields:
fields = [field_map.start, field_map.end, field_map.title, 'name']
@@ -54,5 +50,5 @@ def get_events(doctype, start, end, field_map, filters=None, fields=None):
[doctype, start_date, '<=', end],
[doctype, end_date, '>=', start],
]
-
+ fields = list({field for field in fields if field})
return frappe.get_list(doctype, fields=fields, filters=filters)
diff --git a/frappe/desk/desk_page.py b/frappe/desk/desk_page.py
index 6c5fdc6821..d373dbda0e 100644
--- a/frappe/desk/desk_page.py
+++ b/frappe/desk/desk_page.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
from frappe.translate import send_translations
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index 1a3b1ca99b..e9036b98b0 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -2,12 +2,11 @@
# MIT License. See license.txt
# Author - Shivam Mishra
-from __future__ import unicode_literals
import frappe
from json import loads, dumps
from frappe import _, DoesNotExistError, ValidationError, _dict
from frappe.boot import get_allowed_pages, get_allowed_reports
-from six import string_types
+from frappe.core.doctype.custom_role.custom_role import get_custom_allowed_roles
from functools import wraps
from frappe.cache_manager import (
build_domain_restriced_doctype_cache,
@@ -29,18 +28,21 @@ def handle_not_exist(fn):
class Workspace:
- def __init__(self, page_name, minimal=False):
- self.page_name = page_name
+ def __init__(self, page, minimal=False):
+ self.page_name = page.get('name')
+ self.page_title = page.get('title')
+ self.public_page = page.get('public')
self.extended_links = []
self.extended_charts = []
self.extended_shortcuts = []
+ self.workspace_manager = "Workspace Manager" in frappe.get_roles()
self.user = frappe.get_user()
self.allowed_modules = self.get_cached('user_allowed_modules', self.get_allowed_modules)
- self.doc = self.get_page_for_user()
+ self.doc = frappe.get_cached_doc("Workspace", self.page_name)
- if self.doc.module and self.doc.module not in self.allowed_modules:
+ if self.doc and self.doc.module and self.doc.module not in self.allowed_modules and not self.workspace_manager:
raise frappe.PermissionError
self.can_read = self.get_cached('user_perm_can_read', self.get_can_read_items)
@@ -49,19 +51,20 @@ class Workspace:
self.allowed_reports = get_allowed_reports(cache=True)
if not minimal:
- self.onboarding_doc = self.get_onboarding_doc()
- self.onboarding = None
+ if self.doc.content:
+ self.onboarding_list = [x['data']['onboarding_name'] for x in loads(self.doc.content) if x['type'] == 'onboarding']
+ self.onboardings = []
self.table_counts = get_table_with_counts()
self.restricted_doctypes = frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache()
self.restricted_pages = frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache()
def is_page_allowed(self):
- cards = self.doc.get_link_groups() + get_custom_reports_and_doctypes(self.doc.module) + self.extended_links
- shortcuts = self.doc.shortcuts + self.extended_shortcuts
+ cards = self.doc.get_link_groups() + get_custom_reports_and_doctypes(self.doc.module)
+ shortcuts = self.doc.shortcuts
for section in cards:
- links = loads(section.get('links')) if isinstance(section.get('links'), string_types) else section.get('links')
+ links = loads(section.get('links')) if isinstance(section.get('links'), str) else section.get('links')
for item in links:
if self.is_item_allowed(item.get('link_to'), item.get('link_type')):
return True
@@ -76,8 +79,28 @@ class Workspace:
if self.is_item_allowed(item.link_to, item.type) and _in_active_domains(item):
return True
+ if not shortcuts and not self.doc.links:
+ return True
+
return False
+ def is_permitted(self):
+ """Returns true if Has Role is not set or the user is allowed."""
+ from frappe.utils import has_common
+
+ allowed = [d.role for d in frappe.get_all("Has Role", fields=["role"], filters={"parent": self.doc.name})]
+
+ custom_roles = get_custom_allowed_roles('page', self.doc.name)
+ allowed.extend(custom_roles)
+
+ if not allowed:
+ return True
+
+ roles = frappe.get_roles()
+
+ if has_common(roles, allowed):
+ return True
+
def get_cached(self, cache_key, fallback_fn):
_cache = frappe.cache()
@@ -103,39 +126,18 @@ class Workspace:
return self.user.allow_modules
- def get_page_for_user(self):
- filters = {
- 'extends': self.page_name,
- 'for_user': frappe.session.user
- }
- user_pages = frappe.get_all("Workspace", filters=filters, limit=1)
- if user_pages:
- return frappe.get_cached_doc("Workspace", user_pages[0])
-
- filters = {
- 'extends_another_page': 1,
- 'extends': self.page_name,
- 'is_default': 1
- }
- default_page = frappe.get_all("Workspace", filters=filters, limit=1)
- if default_page:
- return frappe.get_cached_doc("Workspace", default_page[0])
-
- self.get_pages_to_extend()
- return frappe.get_cached_doc("Workspace", self.page_name)
-
- def get_onboarding_doc(self):
+ def get_onboarding_doc(self, onboarding):
# Check if onboarding is enabled
if not frappe.get_system_settings("enable_onboarding"):
return None
- if not self.doc.onboarding:
+ if not self.onboarding_list:
return None
- if frappe.db.get_value("Module Onboarding", self.doc.onboarding, "is_complete"):
+ if frappe.db.get_value("Module Onboarding", onboarding, "is_complete"):
return None
- doc = frappe.get_doc("Module Onboarding", self.doc.onboarding)
+ doc = frappe.get_doc("Module Onboarding", onboarding)
# Check if user is allowed
allowed_roles = set(doc.get_allowed_roles())
@@ -199,14 +201,9 @@ class Workspace:
'items': self.get_shortcuts()
}
- if self.onboarding_doc:
- self.onboarding = {
- 'label': _(self.onboarding_doc.title),
- 'subtitle': _(self.onboarding_doc.subtitle),
- 'success': _(self.onboarding_doc.success_message),
- 'docs_url': self.onboarding_doc.documentation_url,
- 'items': self.get_onboarding_steps()
- }
+ self.onboardings = {
+ 'items': self.get_onboardings()
+ }
def _doctype_contains_a_record(self, name):
exists = self.table_counts.get(name, False)
@@ -335,9 +332,26 @@ class Workspace:
return items
@handle_not_exist
- def get_onboarding_steps(self):
+ def get_onboardings(self):
+ if self.onboarding_list:
+ for onboarding in self.onboarding_list:
+ onboarding_doc = self.get_onboarding_doc(onboarding)
+ if onboarding_doc:
+ item = {
+ 'label': _(onboarding),
+ 'title': _(onboarding_doc.title),
+ 'subtitle': _(onboarding_doc.subtitle),
+ 'success': _(onboarding_doc.success_message),
+ 'docs_url': onboarding_doc.documentation_url,
+ 'items': self.get_onboarding_steps(onboarding_doc)
+ }
+ self.onboardings.append(item)
+ return self.onboardings
+
+ @handle_not_exist
+ def get_onboarding_steps(self, onboarding_doc):
steps = []
- for doc in self.onboarding_doc.get_steps():
+ for doc in onboarding_doc.get_steps():
step = doc.as_dict().copy()
step.label = _(doc.title)
if step.action == "Create Entry":
@@ -354,58 +368,65 @@ def get_desktop_page(page):
on desk.
Args:
- page (string): page name
+ page (json): page data
Returns:
dict: dictionary of cards, charts and shortcuts to be displayed on website
"""
try:
- wspace = Workspace(page)
+ wspace = Workspace(loads(page))
wspace.build_workspace()
return {
'charts': wspace.charts,
'shortcuts': wspace.shortcuts,
'cards': wspace.cards,
- 'onboarding': wspace.onboarding,
+ 'onboardings': wspace.onboardings,
'allow_customization': not wspace.doc.disable_user_customization
}
except DoesNotExistError:
+ frappe.log_error(frappe.get_traceback())
return {}
@frappe.whitelist()
-def get_desk_sidebar_items():
+def get_wspace_sidebar_items():
"""Get list of sidebar items for desk"""
+ has_access = "Workspace Manager" in frappe.get_roles()
# don't get domain restricted pages
blocked_modules = frappe.get_doc('User', frappe.session.user).get_blocked_modules()
+ blocked_modules.append('Dummy Module')
filters = {
'restrict_to_domain': ['in', frappe.get_active_domains()],
- 'extends_another_page': 0,
- 'for_user': '',
'module': ['not in', blocked_modules]
}
- if not frappe.local.conf.developer_mode:
- filters['developer_mode_only'] = '0'
+ if has_access:
+ filters = []
- # pages sorted based on pinned to top and then by name
- order_by = "pin_to_top desc, pin_to_bottom asc, name asc"
- all_pages = frappe.get_all("Workspace", fields=["name", "category", "icon", "module"],
- filters=filters, order_by=order_by, ignore_permissions=True)
+ # pages sorted based on sequence id
+ order_by = "sequence_id asc"
+ fields = ["name", "title", "for_user", "parent_page", "content", "public", "module", "icon"]
+ all_pages = frappe.get_all("Workspace", fields=fields, filters=filters, order_by=order_by, ignore_permissions=True)
pages = []
+ private_pages = []
# Filter Page based on Permission
for page in all_pages:
try:
- wspace = Workspace(page.get('name'), True)
- if wspace.is_page_allowed():
- pages.append(page)
+ wspace = Workspace(page)
+ if wspace.is_permitted() and wspace.is_page_allowed() or has_access:
+ if page.public:
+ pages.append(page)
+ elif page.for_user == frappe.session.user:
+ private_pages.append(page)
page['label'] = _(page.get('name'))
except frappe.PermissionError:
pass
+ if private_pages:
+ pages.extend(private_pages)
- return pages
+ return {'pages': pages, 'has_access': has_access}
def get_table_with_counts():
counts = frappe.cache().get_value("information_schema:counts")
@@ -453,6 +474,7 @@ def get_custom_report_list(module):
"type": "Link",
"link_type": "report",
"doctype": r.ref_doctype,
+ "dependencies": r.ref_doctype,
"is_query_report": 1 if r.report_type in ("Query Report", "Script Report", "Custom Report") else 0,
"label": _(r.name),
"link_to": r.name,
@@ -471,7 +493,7 @@ def get_custom_workspace_for_user(page):
"""
filters = {
'extends': page,
- 'for_user': frappe.session.user
+ 'for_user': frappe.session.user,
}
pages = frappe.get_list("Workspace", filters=filters)
if pages:
@@ -481,7 +503,6 @@ def get_custom_workspace_for_user(page):
doc.for_user = frappe.session.user
return doc
-
@frappe.whitelist()
def save_customization(page, config):
"""Save customizations as a separate doctype in Workspace per user
@@ -540,6 +561,80 @@ def save_customization(page, config):
return True
+def save_new_widget(doc, page, blocks, new_widgets):
+
+ widgets = _dict(loads(new_widgets))
+
+ if widgets.chart:
+ doc.charts.extend(new_widget(widgets.chart, "Workspace Chart", "charts"))
+ if widgets.shortcut:
+ doc.shortcuts.extend(new_widget(widgets.shortcut, "Workspace Shortcut", "shortcuts"))
+ if widgets.card:
+ doc.build_links_table_from_card(widgets.card)
+
+ # remove duplicate and unwanted widgets
+ if widgets:
+ clean_up(doc, blocks)
+
+ try:
+ doc.save(ignore_permissions=True)
+ except (ValidationError, TypeError) as e:
+ # Create a json string to log
+ json_config = dumps(widgets, sort_keys=True, indent=4)
+
+ # Error log body
+ log = \
+ """
+ page: {0}
+ config: {1}
+ exception: {2}
+ """.format(page, json_config, e)
+ frappe.log_error(log, _("Could not save customization"))
+ return False
+
+ return True
+def clean_up(original_page, blocks):
+ page_widgets = {}
+
+ for wid in ['shortcut', 'card', 'chart']:
+ # get list of widget's name from blocks
+ page_widgets[wid] = [x['data'][wid + '_name'] for x in loads(blocks) if x['type'] == wid]
+
+ # shortcut & chart cleanup
+ for wid in ['shortcut', 'chart']:
+ updated_widgets = []
+ original_page.get(wid+'s').reverse()
+
+ for w in original_page.get(wid+'s'):
+ if w.label in page_widgets[wid] and w.label not in [x.label for x in updated_widgets]:
+ updated_widgets.append(w)
+ original_page.set(wid+'s', updated_widgets)
+
+ # card cleanup
+ for i, v in enumerate(original_page.links):
+ if v.type == 'Card Break' and v.label not in page_widgets['card']:
+ del original_page.links[i : i+v.link_count+1]
+
+def new_widget(config, doctype, parentfield):
+ if not config:
+ return []
+ prepare_widget_list = []
+ for idx, widget in enumerate(config):
+ # Some cleanup
+ widget.pop("name", None)
+
+ # New Doc
+ doc = frappe.new_doc(doctype)
+ doc.update(widget)
+
+ # Manually Set IDX
+ doc.idx = idx + 1
+
+ # Set Parent Field
+ doc.parentfield = parentfield
+
+ prepare_widget_list.append(doc)
+ return prepare_widget_list
def prepare_widget(config, doctype, parentfield):
"""Create widget child table entries with parent details
diff --git a/frappe/desk/doctype/bulk_update/bulk_update.py b/frappe/desk/doctype/bulk_update/bulk_update.py
index 9b9f7d7a73..469ee839f1 100644
--- a/frappe/desk/doctype/bulk_update/bulk_update.py
+++ b/frappe/desk/doctype/bulk_update/bulk_update.py
@@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe import _
diff --git a/frappe/desk/doctype/calendar_view/calendar_view.py b/frappe/desk/doctype/calendar_view/calendar_view.py
index ae8ab1eb46..3a986f3273 100644
--- a/frappe/desk/doctype/calendar_view/calendar_view.py
+++ b/frappe/desk/doctype/calendar_view/calendar_view.py
@@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
from frappe.model.document import Document
class CalendarView(Document):
diff --git a/frappe/desk/doctype/console_log/console_log.py b/frappe/desk/doctype/console_log/console_log.py
index 635c4c1ba7..5d0f1cfa93 100644
--- a/frappe/desk/doctype/console_log/console_log.py
+++ b/frappe/desk/doctype/console_log/console_log.py
@@ -2,7 +2,6 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/console_log/test_console_log.py b/frappe/desk/doctype/console_log/test_console_log.py
index 04dc4f241f..3bb1605204 100644
--- a/frappe/desk/doctype/console_log/test_console_log.py
+++ b/frappe/desk/doctype/console_log/test_console_log.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
# import frappe
import unittest
diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py
index 4e66318769..1d333609db 100644
--- a/frappe/desk/doctype/dashboard/dashboard.py
+++ b/frappe/desk/doctype/dashboard/dashboard.py
@@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
from frappe.config import get_modules_from_all_apps_for_user
@@ -22,7 +21,7 @@ class Dashboard(Document):
def validate(self):
if not frappe.conf.developer_mode and self.is_standard:
- frappe.throw('Cannot edit Standard Dashboards')
+ frappe.throw(_("Cannot edit Standard Dashboards"))
if self.is_standard:
non_standard_docs_map = {
diff --git a/frappe/desk/doctype/dashboard/test_dashboard.py b/frappe/desk/doctype/dashboard/test_dashboard.py
index d5485d8f70..dd1bc31d86 100644
--- a/frappe/desk/doctype/dashboard/test_dashboard.py
+++ b/frappe/desk/doctype/dashboard/test_dashboard.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import unittest
class TestDashboard(unittest.TestCase):
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
index 3b4d5e7be5..635d32d969 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
@@ -223,7 +223,7 @@ frappe.ui.form.on('Dashboard Chart', {
if (['Date', 'Datetime'].includes(df.fieldtype)) {
date_fields.push({label: df.label, value: df.fieldname});
}
- if (['Int', 'Float', 'Currency', 'Percent'].includes(df.fieldtype)) {
+ if (['Int', 'Float', 'Currency', 'Percent', 'Duration'].includes(df.fieldtype)) {
value_fields.push({label: df.label, value: df.fieldname});
aggregate_function_fields.push({label: df.label, value: df.fieldname});
}
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
index 48b34e6cd9..db5964e7b2 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
@@ -2,15 +2,13 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe import _
import datetime
import json
from frappe.utils.dashboard import cache_source
from frappe.utils import nowdate, getdate, get_datetime, cint, now_datetime
-from frappe.utils.dateutils import\
- get_period, get_period_beginning, get_from_date_from_timespan, get_dates_from_timegrain
+from frappe.utils.dateutils import get_period, get_period_beginning, get_from_date_from_timespan, get_dates_from_timegrain
from frappe.model.naming import append_number_if_name_exists
from frappe.boot import get_allowed_reports
from frappe.config import get_modules_from_all_apps_for_user
@@ -326,7 +324,7 @@ class DashboardChart(Document):
def validate(self):
if not frappe.conf.developer_mode and self.is_standard:
- frappe.throw('Cannot edit Standard charts')
+ frappe.throw(_("Cannot edit Standard charts"))
if self.chart_type != 'Custom' and self.chart_type != 'Report':
self.check_required_field()
self.check_document_type()
diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
index 72ab18385d..9f10522b12 100644
--- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import unittest, frappe
from frappe.utils import getdate, formatdate, get_last_day
from frappe.utils.dateutils import get_period_ending, get_period
@@ -66,7 +64,7 @@ class TestDashboardChart(unittest.TestCase):
if frappe.db.exists('Dashboard Chart', 'Test Empty Dashboard Chart'):
frappe.delete_doc('Dashboard Chart', 'Test Empty Dashboard Chart')
- frappe.db.sql('delete from `tabError Log`')
+ frappe.db.delete("Error Log")
frappe.get_doc(dict(
doctype = 'Dashboard Chart',
@@ -96,7 +94,7 @@ class TestDashboardChart(unittest.TestCase):
if frappe.db.exists('Dashboard Chart', 'Test Empty Dashboard Chart 2'):
frappe.delete_doc('Dashboard Chart', 'Test Empty Dashboard Chart 2')
- frappe.db.sql('delete from `tabError Log`')
+ frappe.db.delete("Error Log")
# create one data point
frappe.get_doc(dict(doctype = 'Error Log', creation = '2018-06-01 00:00:00')).insert()
diff --git a/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.py b/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.py
index 734f27cc28..7d6f66daa2 100644
--- a/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.py
+++ b/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.py
@@ -2,7 +2,6 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/dashboard_chart_link/dashboard_chart_link.py b/frappe/desk/doctype/dashboard_chart_link/dashboard_chart_link.py
index 7cd4f9daa3..359801a303 100644
--- a/frappe/desk/doctype/dashboard_chart_link/dashboard_chart_link.py
+++ b/frappe/desk/doctype/dashboard_chart_link/dashboard_chart_link.py
@@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.py b/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.py
index 6685009078..791dbc563b 100644
--- a/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.py
+++ b/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.py
@@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe, os
from frappe import _
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/dashboard_chart_source/test_dashboard_chart_source.py b/frappe/desk/doctype/dashboard_chart_source/test_dashboard_chart_source.py
index 822526b591..53fe127dfb 100644
--- a/frappe/desk/doctype/dashboard_chart_source/test_dashboard_chart_source.py
+++ b/frappe/desk/doctype/dashboard_chart_source/test_dashboard_chart_source.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import unittest
class TestDashboardChartSource(unittest.TestCase):
diff --git a/frappe/desk/doctype/dashboard_settings/dashboard_settings.py b/frappe/desk/doctype/dashboard_settings/dashboard_settings.py
index 4697d897fc..df61c52114 100644
--- a/frappe/desk/doctype/dashboard_settings/dashboard_settings.py
+++ b/frappe/desk/doctype/dashboard_settings/dashboard_settings.py
@@ -2,7 +2,6 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
import frappe
diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.py b/frappe/desk/doctype/desktop_icon/desktop_icon.py
index fcf10ef61d..28c5a670cb 100644
--- a/frappe/desk/doctype/desktop_icon/desktop_icon.py
+++ b/frappe/desk/doctype/desktop_icon/desktop_icon.py
@@ -2,14 +2,11 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
-
import frappe
from frappe import _
import json
import random
from frappe.model.document import Document
-from six import iteritems, string_types
from frappe.utils.user import UserPermissions
class DesktopIcon(Document):
@@ -173,7 +170,7 @@ def add_user_icon(_doctype, _report=None, label=None, link=None, type='link', st
@frappe.whitelist()
def set_order(new_order, user=None):
'''set new order by duplicating user icons (if user is set) or set global order'''
- if isinstance(new_order, string_types):
+ if isinstance(new_order, str):
new_order = json.loads(new_order)
for i, module_name in enumerate(new_order):
if module_name not in ('Explore',):
@@ -200,7 +197,7 @@ def set_desktop_icons(visible_list, ignore_duplicate=True):
# clear all custom only if setup is not complete
if not int(frappe.defaults.get_defaults().setup_complete or 0):
- frappe.db.sql('delete from `tabDesktop Icon` where standard=0')
+ frappe.db.delete("Desktop Icon", {"standard": 0})
# set standard as blocked and hidden if setting first active domain
if not frappe.flags.keep_desktop_icons:
@@ -232,7 +229,7 @@ def set_hidden_list(hidden_list, user=None):
'''Sets property `hidden`=1 in **Desktop Icon** for given user.
If user is None then it will set global values.
It will also set the rest of the icons as shown (`hidden` = 0)'''
- if isinstance(hidden_list, string_types):
+ if isinstance(hidden_list, str):
hidden_list = json.loads(hidden_list)
# set as hidden
@@ -329,7 +326,7 @@ def sync_from_app(app):
if isinstance(modules, dict):
modules_list = []
- for m, desktop_icon in iteritems(modules):
+ for m, desktop_icon in modules.items():
desktop_icon['module_name'] = m
modules_list.append(desktop_icon)
else:
diff --git a/frappe/desk/doctype/event/__init__.py b/frappe/desk/doctype/event/__init__.py
index 4dbcd0d163..0e57cb68c3 100644
--- a/frappe/desk/doctype/event/__init__.py
+++ b/frappe/desk/doctype/event/__init__.py
@@ -1,4 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
diff --git a/frappe/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py
index 54905bed6a..e7e7be530b 100644
--- a/frappe/desk/doctype/event/event.py
+++ b/frappe/desk/doctype/event/event.py
@@ -1,9 +1,7 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
-from six.moves import range
-from six import string_types
+
import frappe
import json
@@ -106,7 +104,7 @@ class Event(Document):
@frappe.whitelist()
def delete_communication(event, reference_doctype, reference_docname):
deleted_participant = frappe.get_doc(reference_doctype, reference_docname)
- if isinstance(event, string_types):
+ if isinstance(event, str):
event = json.loads(event)
filters = [
@@ -168,7 +166,7 @@ def get_events(start, end, user=None, for_reminder=False, filters=None):
if not user:
user = frappe.session.user
- if isinstance(filters, string_types):
+ if isinstance(filters, str):
filters = json.loads(filters)
filter_condition = get_filters_cond('Event', filters, [])
@@ -340,9 +338,8 @@ def delete_events(ref_type, ref_name, delete_event=False):
total_participants = frappe.get_all("Event Participants", filters={"parenttype": "Event", "parent": participation.parent})
if len(total_participants) <= 1:
- frappe.db.sql("DELETE FROM `tabEvent` WHERE `name` = %(name)s", {'name': participation.parent})
-
- frappe.db.sql("DELETE FROM `tabEvent Participants ` WHERE `name` = %(name)s", {'name': participation.name})
+ frappe.db.delete("Event", {"name": participation.parent})
+ frappe.db.delete("Event Participants", {"name": participation.name})
# Close events if ends_on or repeat_till is less than now_datetime
def set_status_of_events():
diff --git a/frappe/desk/doctype/event/test_event.py b/frappe/desk/doctype/event/test_event.py
index 2926a74a55..8f56d11da3 100644
--- a/frappe/desk/doctype/event/test_event.py
+++ b/frappe/desk/doctype/event/test_event.py
@@ -1,7 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
-
"""Use blog post test to test user permissions logic"""
import frappe
@@ -16,7 +14,7 @@ test_records = frappe.get_test_records('Event')
class TestEvent(unittest.TestCase):
def setUp(self):
- frappe.db.sql('delete from tabEvent')
+ frappe.db.delete("Event")
make_test_objects('Event', reset=True)
self.test_records = frappe.get_test_records('Event')
diff --git a/frappe/desk/doctype/event_participants/event_participants.py b/frappe/desk/doctype/event_participants/event_participants.py
index 18e4672140..ca4fae9930 100644
--- a/frappe/desk/doctype/event_participants/event_participants.py
+++ b/frappe/desk/doctype/event_participants/event_participants.py
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
from frappe.model.document import Document
class EventParticipants(Document):
diff --git a/frappe/patches/v4_0/__init__.py b/frappe/desk/doctype/form_tour/__init__.py
similarity index 100%
rename from frappe/patches/v4_0/__init__.py
rename to frappe/desk/doctype/form_tour/__init__.py
diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js
new file mode 100644
index 0000000000..8d70dcd3dc
--- /dev/null
+++ b/frappe/desk/doctype/form_tour/form_tour.js
@@ -0,0 +1,123 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Form Tour', {
+ setup: function(frm) {
+ if (!frm.doc.is_standard || frappe.boot.developer_mode) {
+ frm.trigger('setup_queries');
+ }
+ },
+
+ refresh(frm) {
+ if (frm.doc.is_standard && !frappe.boot.developer_mode) {
+ frm.trigger("disable_form");
+ }
+
+ frm.add_custom_button(__('Show Tour'), async () => {
+ const issingle = await check_if_single(frm.doc.reference_doctype);
+ let route_changed = null;
+
+ if (issingle) {
+ route_changed = frappe.set_route('Form', frm.doc.reference_doctype);
+ } else {
+ route_changed = frappe.set_route('Form', frm.doc.reference_doctype, 'new');
+ }
+ route_changed.then(() => {
+ const tour_name = frm.doc.name;
+ cur_frm.tour
+ .init({ tour_name })
+ .then(() => cur_frm.tour.start());
+ });
+ });
+ },
+
+ disable_form: function(frm) {
+ frm.set_read_only();
+ frm.fields
+ .filter((field) => field.has_input)
+ .forEach((field) => {
+ frm.set_df_property(field.df.fieldname, "read_only", "1");
+ });
+ frm.disable_save();
+ },
+
+ setup_queries(frm) {
+ frm.set_query("reference_doctype", function() {
+ return {
+ filters: {
+ istable: 0
+ }
+ };
+ });
+
+ frm.set_query("field", "steps", function() {
+ return {
+ query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list",
+ filters: {
+ doctype: frm.doc.reference_doctype,
+ hidden: 0
+ }
+ };
+ });
+
+ frm.set_query("parent_field", "steps", function() {
+ return {
+ query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list",
+ filters: {
+ doctype: frm.doc.reference_doctype,
+ fieldtype: "Table",
+ hidden: 0,
+ }
+ };
+ });
+
+ frm.trigger('reference_doctype');
+ },
+
+ reference_doctype(frm) {
+ if (!frm.doc.reference_doctype) return;
+
+ frappe.db.get_list('DocField', {
+ filters: {
+ parent: frm.doc.reference_doctype,
+ parenttype: 'DocType',
+ fieldtype: 'Table'
+ },
+ fields: ['options']
+ }).then(res => {
+ if (Array.isArray(res)) {
+ frm.child_doctypes = res.map(r => r.options);
+ }
+ });
+
+ }
+});
+
+frappe.ui.form.on('Form Tour Step', {
+ parent_field(frm, cdt, cdn) {
+ const child_row = locals[cdt][cdn];
+ frappe.model.set_value(cdt, cdn, 'field', '');
+ const field_control = get_child_field("steps", cdn, "field");
+ field_control.get_query = function() {
+ return {
+ query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list",
+ filters: {
+ doctype: child_row.child_doctype,
+ hidden: 0
+ }
+ };
+ };
+ }
+});
+
+function get_child_field(child_table, child_name, fieldname) {
+ // gets the field from grid row form
+ const grid = cur_frm.fields_dict[child_table].grid;
+ const grid_row = grid.grid_rows_by_docname[child_name];
+ return grid_row.grid_form.fields_dict[fieldname];
+}
+
+async function check_if_single(doctype) {
+ const { message } = await frappe.db.get_value('DocType', doctype, 'issingle');
+ return message.issingle || 0;
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json
new file mode 100644
index 0000000000..e4ea528fcc
--- /dev/null
+++ b/frappe/desk/doctype/form_tour/form_tour.json
@@ -0,0 +1,91 @@
+{
+ "actions": [],
+ "autoname": "field:title",
+ "creation": "2021-05-21 23:02:52.242721",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "title",
+ "reference_doctype",
+ "module",
+ "is_standard",
+ "save_on_complete",
+ "section_break_3",
+ "steps"
+ ],
+ "fields": [
+ {
+ "fieldname": "reference_doctype",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Reference Document",
+ "options": "DocType",
+ "reqd": 1
+ },
+ {
+ "depends_on": "reference_doctype",
+ "fieldname": "steps",
+ "fieldtype": "Table",
+ "label": "Steps",
+ "options": "Form Tour Step",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_3",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "label": "Title",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "save_on_complete",
+ "fieldtype": "Check",
+ "label": "Save on Completion"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_standard",
+ "fieldtype": "Check",
+ "label": "Is Standard"
+ },
+ {
+ "fetch_from": "reference_doctype.module",
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Module",
+ "options": "Module Def",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-06-06 20:32:54.068774",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Form Tour",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py
new file mode 100644
index 0000000000..dbc667ce28
--- /dev/null
+++ b/frappe/desk/doctype/form_tour/form_tour.py
@@ -0,0 +1,62 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.model.document import Document
+from frappe.modules.export_file import export_to_files
+
+class FormTour(Document):
+ def before_insert(self):
+ if not self.is_standard:
+ return
+
+ # while syncing, set proper docfield reference
+ for d in self.steps:
+ if not frappe.db.exists('DocField', d.field):
+ d.field = frappe.db.get_value('DocField', {
+ 'fieldname': d.fieldname, 'parent': self.reference_doctype, 'fieldtype': d.fieldtype
+ }, "name")
+
+ if d.is_table_field and not frappe.db.exists('DocField', d.parent_field):
+ d.parent_field = frappe.db.get_value('DocField', {
+ 'fieldname': d.parent_fieldname, 'parent': self.reference_doctype, 'fieldtype': 'Table'
+ }, "name")
+
+ def on_update(self):
+ if frappe.conf.developer_mode and self.is_standard:
+ export_to_files([['Form Tour', self.name]], self.module)
+
+ def before_export(self, doc):
+ for d in doc.steps:
+ d.field = ""
+ d.parent_field = ""
+
+@frappe.whitelist()
+@frappe.validate_and_sanitize_search_inputs
+def get_docfield_list(doctype, txt, searchfield, start, page_len, filters):
+ or_filters = [
+ ['fieldname', 'like', '%' + txt + '%'],
+ ['label', 'like', '%' + txt + '%'],
+ ['fieldtype', 'like', '%' + txt + '%']
+ ]
+
+ parent_doctype = filters.get('doctype')
+ fieldtype = filters.get('fieldtype')
+ if not fieldtype:
+ excluded_fieldtypes = ['Column Break']
+ excluded_fieldtypes += filters.get('excluded_fieldtypes', [])
+ fieldtype_filter = ['not in', excluded_fieldtypes]
+ else:
+ fieldtype_filter = fieldtype
+
+ docfields = frappe.get_all(
+ doctype,
+ fields=["name as value", "label", "fieldtype"],
+ filters={'parent': parent_doctype, 'fieldtype': fieldtype_filter},
+ or_filters=or_filters,
+ limit_start=start,
+ limit_page_length=page_len,
+ order_by="idx",
+ as_list=1,
+ )
+ return docfields
diff --git a/frappe/desk/doctype/form_tour/test_form_tour.py b/frappe/desk/doctype/form_tour/test_form_tour.py
new file mode 100644
index 0000000000..a4a796ce41
--- /dev/null
+++ b/frappe/desk/doctype/form_tour/test_form_tour.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+class TestFormTour(unittest.TestCase):
+ pass
diff --git a/frappe/patches/v4_1/__init__.py b/frappe/desk/doctype/form_tour_step/__init__.py
similarity index 100%
rename from frappe/patches/v4_1/__init__.py
rename to frappe/desk/doctype/form_tour_step/__init__.py
diff --git a/frappe/desk/doctype/form_tour_step/form_tour_step.json b/frappe/desk/doctype/form_tour_step/form_tour_step.json
new file mode 100644
index 0000000000..3b6c91a208
--- /dev/null
+++ b/frappe/desk/doctype/form_tour_step/form_tour_step.json
@@ -0,0 +1,151 @@
+{
+ "actions": [],
+ "creation": "2021-05-21 23:05:45.342114",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "is_table_field",
+ "section_break_2",
+ "parent_field",
+ "field",
+ "title",
+ "description",
+ "column_break_2",
+ "position",
+ "label",
+ "has_next_condition",
+ "next_step_condition",
+ "section_break_13",
+ "fieldname",
+ "parent_fieldname",
+ "fieldtype",
+ "child_doctype"
+ ],
+ "fields": [
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Title",
+ "reqd": 1
+ },
+ {
+ "columns": 4,
+ "fieldname": "description",
+ "fieldtype": "HTML Editor",
+ "in_list_view": 1,
+ "label": "Description",
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval: (!doc.is_table_field || (doc.is_table_field && doc.parent_field))",
+ "fieldname": "field",
+ "fieldtype": "Link",
+ "label": "Field",
+ "options": "DocField",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "field.fieldname",
+ "fieldname": "fieldname",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Fieldname",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "field.label",
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Label",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "Bottom",
+ "fieldname": "position",
+ "fieldtype": "Select",
+ "label": "Position",
+ "options": "Left\nLeft Center\nLeft Bottom\nTop\nTop Center\nTop Right\nRight\nRight Center\nRight Bottom\nBottom\nBottom Center\nBottom Right\nMid Center"
+ },
+ {
+ "depends_on": "has_next_condition",
+ "fieldname": "next_step_condition",
+ "fieldtype": "Code",
+ "label": "Next Step Condition",
+ "oldfieldname": "condition",
+ "options": "JS"
+ },
+ {
+ "default": "0",
+ "fieldname": "has_next_condition",
+ "fieldtype": "Check",
+ "label": "Has Next Condition"
+ },
+ {
+ "default": "0",
+ "fetch_from": "field.fieldtype",
+ "fieldname": "fieldtype",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Fieldtype",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "is_table_field",
+ "fieldtype": "Check",
+ "label": "Is Table Field"
+ },
+ {
+ "fieldname": "section_break_2",
+ "fieldtype": "Section Break"
+ },
+ {
+ "depends_on": "is_table_field",
+ "fieldname": "parent_field",
+ "fieldtype": "Link",
+ "label": "Parent Field",
+ "mandatory_depends_on": "is_table_field",
+ "options": "DocField"
+ },
+ {
+ "fieldname": "section_break_13",
+ "fieldtype": "Section Break",
+ "hidden": 1,
+ "label": "Hidden Fields"
+ },
+ {
+ "fetch_from": "parent_field.options",
+ "fieldname": "child_doctype",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Child Doctype",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "parent_field.fieldname",
+ "fieldname": "parent_fieldname",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Parent Fieldname",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-06-06 20:52:21.076972",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Form Tour Step",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/form_tour_step/form_tour_step.py b/frappe/desk/doctype/form_tour_step/form_tour_step.py
new file mode 100644
index 0000000000..0df5665c63
--- /dev/null
+++ b/frappe/desk/doctype/form_tour_step/form_tour_step.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+class FormTourStep(Document):
+ pass
diff --git a/frappe/desk/doctype/global_search_doctype/global_search_doctype.py b/frappe/desk/doctype/global_search_doctype/global_search_doctype.py
index 4c9a948278..de8a48af01 100644
--- a/frappe/desk/doctype/global_search_doctype/global_search_doctype.py
+++ b/frappe/desk/doctype/global_search_doctype/global_search_doctype.py
@@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/global_search_settings/global_search_settings.py b/frappe/desk/doctype/global_search_settings/global_search_settings.py
index 85c9687ab3..9112349c1b 100644
--- a/frappe/desk/doctype/global_search_settings/global_search_settings.py
+++ b/frappe/desk/doctype/global_search_settings/global_search_settings.py
@@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe import _
@@ -22,7 +21,7 @@ class GlobalSearchSettings(Document):
dts.append(dt.document_type)
if core_dts:
- core_dts = (", ".join([frappe.bold(dt) for dt in core_dts]))
+ core_dts = ", ".join(frappe.bold(dt) for dt in core_dts)
frappe.throw(_("Core Modules {0} cannot be searched in Global Search.").format(core_dts))
if repeated_dts:
@@ -61,7 +60,7 @@ def update_global_search_doctypes():
if search_doctypes.get(domain):
global_search_doctypes.extend(search_doctypes.get(domain))
- doctype_list = set([dt.name for dt in frappe.get_all("DocType")])
+ doctype_list = {dt.name for dt in frappe.get_all("DocType")}
allowed_in_global_search = []
for dt in global_search_doctypes:
diff --git a/frappe/desk/doctype/kanban_board/kanban_board.py b/frappe/desk/doctype/kanban_board/kanban_board.py
index a655e9e1da..5100727f43 100644
--- a/frappe/desk/doctype/kanban_board/kanban_board.py
+++ b/frappe/desk/doctype/kanban_board/kanban_board.py
@@ -2,12 +2,10 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
import json
from frappe import _
from frappe.model.document import Document
-from six import iteritems
class KanbanBoard(Document):
@@ -107,7 +105,7 @@ def update_order(board_name, order):
order_dict = json.loads(order)
updated_cards = []
- for col_name, cards in iteritems(order_dict):
+ for col_name, cards in order_dict.items():
order_list = []
for card in cards:
column = frappe.get_value(
diff --git a/frappe/desk/doctype/kanban_board/test_kanban_board.py b/frappe/desk/doctype/kanban_board/test_kanban_board.py
index 33947f4a54..f9503d736a 100644
--- a/frappe/desk/doctype/kanban_board/test_kanban_board.py
+++ b/frappe/desk/doctype/kanban_board/test_kanban_board.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
diff --git a/frappe/desk/doctype/kanban_board_column/kanban_board_column.py b/frappe/desk/doctype/kanban_board_column/kanban_board_column.py
index 4ea30d21b2..aebba3351c 100644
--- a/frappe/desk/doctype/kanban_board_column/kanban_board_column.py
+++ b/frappe/desk/doctype/kanban_board_column/kanban_board_column.py
@@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/list_filter/list_filter.py b/frappe/desk/doctype/list_filter/list_filter.py
index 035f7e90b9..2467ae40a4 100644
--- a/frappe/desk/doctype/list_filter/list_filter.py
+++ b/frappe/desk/doctype/list_filter/list_filter.py
@@ -2,7 +2,6 @@
# Copyright (c) 2018, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe, json
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/list_view_settings/list_view_settings.py b/frappe/desk/doctype/list_view_settings/list_view_settings.py
index 74e029f499..f4a288b7ba 100644
--- a/frappe/desk/doctype/list_view_settings/list_view_settings.py
+++ b/frappe/desk/doctype/list_view_settings/list_view_settings.py
@@ -2,7 +2,6 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/list_view_settings/test_list_view_settings.py b/frappe/desk/doctype/list_view_settings/test_list_view_settings.py
index c1b2f4a0da..00010d7604 100644
--- a/frappe/desk/doctype/list_view_settings/test_list_view_settings.py
+++ b/frappe/desk/doctype/list_view_settings/test_list_view_settings.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
# import frappe
import unittest
diff --git a/frappe/desk/doctype/module_onboarding/module_onboarding.py b/frappe/desk/doctype/module_onboarding/module_onboarding.py
index 8315c0b304..6f01e0fd8d 100644
--- a/frappe/desk/doctype/module_onboarding/module_onboarding.py
+++ b/frappe/desk/doctype/module_onboarding/module_onboarding.py
@@ -2,7 +2,6 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
diff --git a/frappe/desk/doctype/module_onboarding/test_module_onboarding.py b/frappe/desk/doctype/module_onboarding/test_module_onboarding.py
index ef305667b1..39184401a1 100644
--- a/frappe/desk/doctype/module_onboarding/test_module_onboarding.py
+++ b/frappe/desk/doctype/module_onboarding/test_module_onboarding.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
# import frappe
import unittest
diff --git a/frappe/desk/doctype/note/note.py b/frappe/desk/doctype/note/note.py
index c54689418e..790f9a514c 100644
--- a/frappe/desk/doctype/note/note.py
+++ b/frappe/desk/doctype/note/note.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: See license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/note/test_note.py b/frappe/desk/doctype/note/test_note.py
index 38894a9c3d..3207fa9b8d 100644
--- a/frappe/desk/doctype/note/test_note.py
+++ b/frappe/desk/doctype/note/test_note.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# See license.txt
-from __future__ import unicode_literals
import frappe
import unittest
@@ -9,9 +8,9 @@ test_records = frappe.get_test_records('Note')
class TestNote(unittest.TestCase):
def insert_note(self):
- frappe.db.sql('delete from tabVersion')
- frappe.db.sql('delete from tabNote')
- frappe.db.sql('delete from `tabNote Seen By`')
+ frappe.db.delete("Version")
+ frappe.db.delete("Note")
+ frappe.db.delete("Note Seen By")
return frappe.get_doc(dict(doctype='Note', title='test note',
content='test note content')).insert()
diff --git a/frappe/desk/doctype/note_seen_by/note_seen_by.py b/frappe/desk/doctype/note_seen_by/note_seen_by.py
index 6123f20929..cec4628b20 100644
--- a/frappe/desk/doctype/note_seen_by/note_seen_by.py
+++ b/frappe/desk/doctype/note_seen_by/note_seen_by.py
@@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py
index 25af92f532..d7d7f68b74 100644
--- a/frappe/desk/doctype/notification_log/notification_log.py
+++ b/frappe/desk/doctype/notification_log/notification_log.py
@@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
@@ -13,7 +12,10 @@ class NotificationLog(Document):
frappe.publish_realtime('notification', after_commit=True, user=self.for_user)
set_notifications_as_unseen(self.for_user)
if is_email_notifications_enabled_for_type(self.for_user, self.type):
- send_notification_email(self)
+ try:
+ send_notification_email(self)
+ except frappe.OutgoingEmailError:
+ frappe.log_error(message=frappe.get_traceback(), title=_("Failed to send notification email"))
def get_permission_query_conditions(for_user):
diff --git a/frappe/desk/doctype/notification_log/test_notification_log.py b/frappe/desk/doctype/notification_log/test_notification_log.py
index e59aee30c9..bedb10b495 100644
--- a/frappe/desk/doctype/notification_log/test_notification_log.py
+++ b/frappe/desk/doctype/notification_log/test_notification_log.py
@@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
+from frappe.core.doctype.user.user import get_system_users
from frappe.desk.form.assign_to import add as assign_task
import unittest
@@ -56,7 +55,4 @@ def get_todo():
return frappe.get_cached_doc('ToDo', res[0].name)
def get_user():
- users = frappe.db.get_all('User',
- filters={'name': ('not in', ['Administrator', 'Guest'])},
- fields='name', limit=1)
- return users[0].name
+ return get_system_users(limit=1)[0]
diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py
index 4ab40bffe9..eb3a16435f 100644
--- a/frappe/desk/doctype/notification_settings/notification_settings.py
+++ b/frappe/desk/doctype/notification_settings/notification_settings.py
@@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/notification_subscribed_document/notification_subscribed_document.py b/frappe/desk/doctype/notification_subscribed_document/notification_subscribed_document.py
index f005efae76..6931e77754 100644
--- a/frappe/desk/doctype/notification_subscribed_document/notification_subscribed_document.py
+++ b/frappe/desk/doctype/notification_subscribed_document/notification_subscribed_document.py
@@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py
index 7d1a697f6b..d8d5fe0953 100644
--- a/frappe/desk/doctype/number_card/number_card.py
+++ b/frappe/desk/doctype/number_card/number_card.py
@@ -2,7 +2,6 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils import cint
diff --git a/frappe/desk/doctype/number_card/test_number_card.py b/frappe/desk/doctype/number_card/test_number_card.py
index 4aa1ecf282..c395f5f915 100644
--- a/frappe/desk/doctype/number_card/test_number_card.py
+++ b/frappe/desk/doctype/number_card/test_number_card.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
# import frappe
import unittest
diff --git a/frappe/desk/doctype/number_card_link/number_card_link.py b/frappe/desk/doctype/number_card_link/number_card_link.py
index 67ad7e70cd..6c16f45f4b 100644
--- a/frappe/desk/doctype/number_card_link/number_card_link.py
+++ b/frappe/desk/doctype/number_card_link/number_card_link.py
@@ -2,7 +2,6 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/onboarding_permission/onboarding_permission.py b/frappe/desk/doctype/onboarding_permission/onboarding_permission.py
index f8772480df..40d3dc33b1 100644
--- a/frappe/desk/doctype/onboarding_permission/onboarding_permission.py
+++ b/frappe/desk/doctype/onboarding_permission/onboarding_permission.py
@@ -2,7 +2,6 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/onboarding_permission/test_onboarding_permission.py b/frappe/desk/doctype/onboarding_permission/test_onboarding_permission.py
index 9a7e8ae6fd..80b166de0a 100644
--- a/frappe/desk/doctype/onboarding_permission/test_onboarding_permission.py
+++ b/frappe/desk/doctype/onboarding_permission/test_onboarding_permission.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
# import frappe
import unittest
diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.py b/frappe/desk/doctype/onboarding_step/onboarding_step.py
index e1cc5dfba4..2336ff52f8 100644
--- a/frappe/desk/doctype/onboarding_step/onboarding_step.py
+++ b/frappe/desk/doctype/onboarding_step/onboarding_step.py
@@ -2,11 +2,26 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
-# import frappe
+import frappe
+from frappe import _
+import json
from frappe.model.document import Document
class OnboardingStep(Document):
def before_export(self, doc):
doc.is_complete = 0
doc.is_skipped = 0
+
+
+@frappe.whitelist()
+def get_onboarding_steps(ob_steps):
+ steps = []
+ for s in json.loads(ob_steps):
+ doc = frappe.get_doc('Onboarding Step', s.get('step'))
+ step = doc.as_dict().copy()
+ step.label = _(doc.title)
+ if step.action == "Create Entry":
+ step.is_submittable = frappe.db.get_value("DocType", step.reference_document, 'is_submittable', cache=True)
+ steps.append(step)
+
+ return steps
diff --git a/frappe/desk/doctype/onboarding_step/test_onboarding_step.py b/frappe/desk/doctype/onboarding_step/test_onboarding_step.py
index 66bd0c6660..2425577478 100644
--- a/frappe/desk/doctype/onboarding_step/test_onboarding_step.py
+++ b/frappe/desk/doctype/onboarding_step/test_onboarding_step.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
# import frappe
import unittest
diff --git a/frappe/desk/doctype/onboarding_step_map/onboarding_step_map.py b/frappe/desk/doctype/onboarding_step_map/onboarding_step_map.py
index ea34de6088..c79244c4ad 100644
--- a/frappe/desk/doctype/onboarding_step_map/onboarding_step_map.py
+++ b/frappe/desk/doctype/onboarding_step_map/onboarding_step_map.py
@@ -2,7 +2,6 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/route_history/route_history.py b/frappe/desk/doctype/route_history/route_history.py
index 12d898afa5..95872440c7 100644
--- a/frappe/desk/doctype/route_history/route_history.py
+++ b/frappe/desk/doctype/route_history/route_history.py
@@ -1,14 +1,13 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2018, Frappe Technologies and contributors
+# Copyright (c) 2021, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class RouteHistory(Document):
pass
+
def flush_old_route_records():
"""Deletes all route records except last 500 records per user"""
@@ -25,19 +24,14 @@ def flush_old_route_records():
for user in users:
user = user[0]
last_record_to_keep = frappe.db.get_all('Route History',
- filters={
- 'user': user,
- },
+ filters={'user': user},
limit=1,
limit_start=500,
fields=['modified'],
- order_by='modified desc')
+ order_by='modified desc'
+ )
- frappe.db.sql('''
- DELETE
- FROM `tabRoute History`
- WHERE `modified` <= %(modified)s and `user`=%(modified)s
- ''', {
- "modified": last_record_to_keep[0].modified,
+ frappe.db.delete("Route History", {
+ "modified": ("<=", last_record_to_keep[0].modified),
"user": user
- })
\ No newline at end of file
+ })
diff --git a/frappe/desk/doctype/system_console/system_console.js b/frappe/desk/doctype/system_console/system_console.js
index c7eac39490..48dd2ba108 100644
--- a/frappe/desk/doctype/system_console/system_console.js
+++ b/frappe/desk/doctype/system_console/system_console.js
@@ -5,7 +5,7 @@ frappe.ui.form.on('System Console', {
onload: function(frm) {
frappe.ui.keys.add_shortcut({
shortcut: 'shift+enter',
- action: () => frm.execute_action('Execute'),
+ action: () => frm.page.btn_primary.trigger('click'),
page: frm.page,
description: __('Execute Console script'),
ignore_inputs: true,
@@ -14,8 +14,11 @@ frappe.ui.form.on('System Console', {
refresh: function(frm) {
frm.disable_save();
- frm.page.set_primary_action(__("Execute"), () => {
- frm.execute_action('Execute');
+ frm.page.set_primary_action(__("Execute"), $btn => {
+ $btn.text(__('Executing...'));
+ return frm.execute_action("Execute").then(() => {
+ $btn.text(__('Execute'));
+ });
});
}
});
diff --git a/frappe/desk/doctype/system_console/system_console.py b/frappe/desk/doctype/system_console/system_console.py
index 6c87ca8c36..e2b5656bc0 100644
--- a/frappe/desk/doctype/system_console/system_console.py
+++ b/frappe/desk/doctype/system_console/system_console.py
@@ -2,8 +2,6 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
-
import json
import frappe
diff --git a/frappe/desk/doctype/system_console/test_system_console.py b/frappe/desk/doctype/system_console/test_system_console.py
index 55ef199122..743c2d6dde 100644
--- a/frappe/desk/doctype/system_console/test_system_console.py
+++ b/frappe/desk/doctype/system_console/test_system_console.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
diff --git a/frappe/desk/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py
index 7e016ee91b..2341d721e2 100644
--- a/frappe/desk/doctype/tag/tag.py
+++ b/frappe/desk/doctype/tag/tag.py
@@ -1,8 +1,6 @@
-# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils import unique
@@ -124,7 +122,10 @@ def delete_tags_for_document(doc):
if not frappe.db.table_exists("Tag Link"):
return
- frappe.db.sql("""DELETE FROM `tabTag Link` WHERE `document_type`=%s AND `document_name`=%s""", (doc.doctype, doc.name))
+ frappe.db.delete("Tag Link", {
+ "document_type": doc.doctype,
+ "document_name": doc.name
+ })
def update_tags(doc, tags):
"""
@@ -132,7 +133,7 @@ def update_tags(doc, tags):
:param doc: Document to be added to global tags
"""
- new_tags = list(set([tag.strip() for tag in tags.split(",") if tag]))
+ new_tags = {tag.strip() for tag in tags.split(",") if tag}
for tag in new_tags:
if not frappe.db.exists("Tag Link", {"parenttype": doc.doctype, "parent": doc.name, "tag": tag}):
@@ -162,7 +163,11 @@ def get_deleted_tags(new_tags, existing_tags):
return list(set(existing_tags) - set(new_tags))
def delete_tag_for_document(dt, dn, tag):
- frappe.db.sql("""DELETE FROM `tabTag Link` WHERE `document_type`=%s AND `document_name`=%s AND tag=%s""", (dt, dn, tag))
+ frappe.db.delete("Tag Link", {
+ "document_type": dt,
+ "document_name": dn,
+ "tag": tag
+ })
@frappe.whitelist()
def get_documents_for_tag(tag):
@@ -187,4 +192,4 @@ def get_documents_for_tag(tag):
@frappe.whitelist()
def get_tags_list_for_awesomebar():
- return [t.name for t in frappe.get_list("Tag")]
\ No newline at end of file
+ return [t.name for t in frappe.get_list("Tag")]
diff --git a/frappe/desk/doctype/tag/test_tag.py b/frappe/desk/doctype/tag/test_tag.py
index 8efd692f43..b9c6e0b744 100644
--- a/frappe/desk/doctype/tag/test_tag.py
+++ b/frappe/desk/doctype/tag/test_tag.py
@@ -1,10 +1,26 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
-from __future__ import unicode_literals
-
-# import frappe
import unittest
+import frappe
+
+from frappe.desk.reportview import get_stats
+from frappe.desk.doctype.tag.tag import add_tag
class TestTag(unittest.TestCase):
- pass
+ def setUp(self) -> None:
+ frappe.db.delete("Tag")
+ frappe.db.sql("UPDATE `tabDocType` set _user_tags=''")
+
+ def test_tag_count_query(self):
+ self.assertDictEqual(get_stats('["_user_tags"]', 'DocType'),
+ {'_user_tags': [['No Tags', frappe.db.count('DocType')]]})
+ add_tag('Standard', 'DocType', 'User')
+ add_tag('Standard', 'DocType', 'ToDo')
+
+ # count with no filter
+ self.assertDictEqual(get_stats('["_user_tags"]', 'DocType'),
+ {'_user_tags': [['Standard', 2], ['No Tags', frappe.db.count('DocType') - 2]]})
+
+ # count with child table field filter
+ self.assertDictEqual(get_stats('["_user_tags"]',
+ 'DocType',
+ filters='[["DocField", "fieldname", "like", "%last_name%"], ["DocType", "name", "like", "%use%"]]'),
+ {'_user_tags': [['Standard', 1], ['No Tags', 0]]})
\ No newline at end of file
diff --git a/frappe/desk/doctype/tag_link/tag_link.py b/frappe/desk/doctype/tag_link/tag_link.py
index 87c8af7212..4c5149f42c 100644
--- a/frappe/desk/doctype/tag_link/tag_link.py
+++ b/frappe/desk/doctype/tag_link/tag_link.py
@@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/tag_link/test_tag_link.py b/frappe/desk/doctype/tag_link/test_tag_link.py
index 1c22ac18bc..297ee3cc96 100644
--- a/frappe/desk/doctype/tag_link/test_tag_link.py
+++ b/frappe/desk/doctype/tag_link/test_tag_link.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
# import frappe
import unittest
diff --git a/frappe/desk/doctype/todo/__init__.py b/frappe/desk/doctype/todo/__init__.py
index 4dbcd0d163..0e57cb68c3 100644
--- a/frappe/desk/doctype/todo/__init__.py
+++ b/frappe/desk/doctype/todo/__init__.py
@@ -1,4 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
diff --git a/frappe/desk/doctype/todo/test_todo.py b/frappe/desk/doctype/todo/test_todo.py
index de5b6724a6..f6371c5921 100644
--- a/frappe/desk/doctype/todo/test_todo.py
+++ b/frappe/desk/doctype/todo/test_todo.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
from frappe.model.db_query import DatabaseQuery
@@ -16,7 +14,7 @@ class TestToDo(unittest.TestCase):
todo = frappe.get_doc(dict(doctype='ToDo', description='test todo',
assigned_by='Administrator')).insert()
- frappe.db.sql('delete from `tabDeleted Document`')
+ frappe.db.delete("Deleted Document")
todo.delete()
deleted = frappe.get_doc('Deleted Document', dict(deleted_doctype=todo.doctype, deleted_name=todo.name))
@@ -29,7 +27,7 @@ class TestToDo(unittest.TestCase):
frappe.db.get_value('User', todo.assigned_by, 'full_name'))
def test_fetch_setup(self):
- frappe.db.sql('delete from tabToDo')
+ frappe.db.delete("ToDo")
todo_meta = frappe.get_doc('DocType', 'ToDo')
todo_meta.get('fields', dict(fieldname='assigned_by_full_name'))[0].fetch_from = ''
@@ -106,8 +104,8 @@ class TestToDo(unittest.TestCase):
clear_permissions_cache('ToDo')
frappe.db.rollback()
-def test_fetch_if_empty(self):
- frappe.db.sql('delete from tabToDo')
+ def test_fetch_if_empty(self):
+ frappe.db.delete("ToDo")
# Allow user changes
todo_meta = frappe.get_doc('DocType', 'ToDo')
@@ -124,9 +122,8 @@ def test_fetch_if_empty(self):
self.assertEqual(todo.assigned_by_full_name, 'Admin')
# Overwrite user changes
- todo_meta = frappe.get_doc('DocType', 'ToDo')
- todo_meta.get('fields', dict(fieldname='assigned_by_full_name'))[0].fetch_if_empty = 0
- todo_meta.save()
+ todo.meta.get('fields', dict(fieldname='assigned_by_full_name'))[0].fetch_if_empty = 0
+ todo.meta.save()
todo.reload()
todo.save()
diff --git a/frappe/desk/doctype/todo/todo.py b/frappe/desk/doctype/todo/todo.py
index a766375fde..754b94cdcb 100644
--- a/frappe/desk/doctype/todo/todo.py
+++ b/frappe/desk/doctype/todo/todo.py
@@ -1,16 +1,17 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
import json
from frappe.model.document import Document
-from frappe.utils import get_fullname
+from frappe.utils import get_fullname, parse_addr
exclude_from_linked_with = True
class ToDo(Document):
+ DocType = 'ToDo'
+
def validate(self):
self._assignment = None
if self.is_new():
@@ -28,8 +29,15 @@ class ToDo(Document):
else:
# NOTE the previous value is only available in validate method
if self.get_db_value("status") != self.status:
+ if self.owner == frappe.session.user:
+ removal_message = frappe._("{0} removed their assignment.").format(
+ get_fullname(frappe.session.user))
+ else:
+ removal_message = frappe._("Assignment of {0} removed by {1}").format(
+ get_fullname(self.owner), get_fullname(frappe.session.user))
+
self._assignment = {
- "text": frappe._("Assignment closed by {0}").format(get_fullname(frappe.session.user)),
+ "text": removal_message,
"comment_type": "Assignment Completed"
}
@@ -40,13 +48,7 @@ class ToDo(Document):
self.update_in_reference()
def on_trash(self):
- # unlink todo from linked comments
- frappe.db.sql("""
- delete from `tabCommunication Link`
- where link_doctype=%(doctype)s and link_name=%(name)s""", {
- "doctype": self.doctype, "name": self.name
- })
-
+ self.delete_communication_links()
self.update_in_reference()
def add_assign_comment(self, text, comment_type):
@@ -55,6 +57,13 @@ class ToDo(Document):
frappe.get_doc(self.reference_type, self.reference_name).add_comment(comment_type, text)
+ def delete_communication_links(self):
+ # unlink todo from linked comments
+ return frappe.db.delete("Communication Link", {
+ "link_doctype": self.doctype,
+ "link_name": self.name
+ })
+
def update_in_reference(self):
if not (self.reference_type and self.reference_name):
return
@@ -85,6 +94,13 @@ class ToDo(Document):
else:
raise
+ @classmethod
+ def get_owners(cls, filters=None):
+ """Returns list of owners after applying filters on todo's.
+ """
+ rows = frappe.get_all(cls.DocType, filters=filters or {}, fields=['owner'])
+ return [parse_addr(row.owner)[1] for row in rows if row.owner]
+
# NOTE: todo is viewable if a user is an owner, or set as assigned_to value, or has any role that is allowed to access ToDo doctype.
def on_doctype_update():
frappe.db.add_index("ToDo", ["reference_type", "reference_name"])
@@ -93,7 +109,7 @@ def get_permission_query_conditions(user):
if not user: user = frappe.session.user
todo_roles = frappe.permissions.get_doctype_roles('ToDo')
- if 'All' in todo_roles:
+ if 'All' in todo_roles:
todo_roles.remove('All')
if any(check in todo_roles for check in frappe.get_roles(user)):
@@ -105,7 +121,7 @@ def get_permission_query_conditions(user):
def has_permission(doc, ptype="read", user=None):
user = user or frappe.session.user
todo_roles = frappe.permissions.get_doctype_roles('ToDo', ptype)
- if 'All' in todo_roles:
+ if 'All' in todo_roles:
todo_roles.remove('All')
if any(check in todo_roles for check in frappe.get_roles(user)):
diff --git a/frappe/desk/doctype/workspace/test_workspace.py b/frappe/desk/doctype/workspace/test_workspace.py
index 7a3f122ee2..f13a136c20 100644
--- a/frappe/desk/doctype/workspace/test_workspace.py
+++ b/frappe/desk/doctype/workspace/test_workspace.py
@@ -1,10 +1,95 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
-# import frappe
+import frappe
import unittest
-
class TestWorkspace(unittest.TestCase):
- pass
+ def setUp(self):
+ create_module("Test Module")
+
+ def tearDown(self):
+ frappe.db.delete("Workspace", {"module": "Test Module"})
+ frappe.db.delete("DocType", {"module": "Test Module"})
+ frappe.delete_doc("Module Def", "Test Module")
+
+ # TODO: FIX ME - flaky test!!!
+ # def test_workspace_with_cards_specific_to_a_country(self):
+ # workspace = create_workspace()
+ # insert_card(workspace, "Card Label 1", "DocType 1", "DocType 2", "France")
+ # insert_card(workspace, "Card Label 2", "DocType A", "DocType B")
+
+ # workspace.insert(ignore_if_duplicate = True)
+
+ # cards = workspace.get_link_groups()
+
+ # if frappe.get_system_settings('country') == "France":
+ # self.assertEqual(len(cards), 2)
+ # else:
+ # self.assertEqual(len(cards), 1)
+
+def create_module(module_name):
+ module = frappe.get_doc({
+ "doctype": "Module Def",
+ "module_name": module_name,
+ "app_name": "frappe"
+ })
+ module.insert(ignore_if_duplicate = True)
+
+ return module
+
+def create_workspace(**args):
+ workspace = frappe.new_doc("Workspace")
+ args = frappe._dict(args)
+
+ workspace.name = args.name or "Test Workspace"
+ workspace.label = args.label or "Test Workspace"
+ workspace.category = args.category or "Modules"
+ workspace.is_standard = args.is_standard or 1
+ workspace.module = "Test Module"
+
+ return workspace
+
+def insert_card(workspace, card_label, doctype1, doctype2, country=None):
+ workspace.append("links", {
+ "type": "Card Break",
+ "label": card_label,
+ "only_for": country
+ })
+
+ create_doctype(doctype1, "Test Module")
+ workspace.append("links", {
+ "type": "Link",
+ "label": doctype1,
+ "only_for": country,
+ "link_type": "DocType",
+ "link_to": doctype1
+ })
+
+ create_doctype(doctype2, "Test Module")
+ workspace.append("links", {
+ "type": "Link",
+ "label": doctype2,
+ "only_for": country,
+ "link_type": "DocType",
+ "link_to": doctype2
+ })
+
+def create_doctype(doctype_name, module):
+ frappe.get_doc({
+ 'doctype': 'DocType',
+ 'name': doctype_name,
+ 'module': module,
+ 'custom': 1,
+ 'autoname': 'field:title',
+ 'fields': [
+ {'label': 'Title', 'fieldname': 'title', 'reqd': 1, 'fieldtype': 'Data'},
+ {'label': 'Description', 'fieldname': 'description', 'fieldtype': 'Small Text'},
+ {'label': 'Date', 'fieldname': 'date', 'fieldtype': 'Date'},
+ {'label': 'Duration', 'fieldname': 'duration', 'fieldtype': 'Duration'},
+ {'label': 'Number', 'fieldname': 'number', 'fieldtype': 'Int'},
+ {'label': 'Number', 'fieldname': 'another_number', 'fieldtype': 'Int'}
+ ],
+ 'permissions': [
+ {'role': 'System Manager'}
+ ]
+ }).insert(ignore_if_duplicate = True)
diff --git a/frappe/desk/doctype/workspace/workspace.json b/frappe/desk/doctype/workspace/workspace.json
index 386267b699..020f3153df 100644
--- a/frappe/desk/doctype/workspace/workspace.json
+++ b/frappe/desk/doctype/workspace/workspace.json
@@ -8,8 +8,11 @@
"engine": "InnoDB",
"field_order": [
"label",
+ "title",
+ "sequence_id",
"for_user",
"extends",
+ "parent_page",
"module",
"category",
"icon",
@@ -24,6 +27,8 @@
"pin_to_top",
"pin_to_bottom",
"hide_custom",
+ "public",
+ "content",
"section_break_2",
"charts_label",
"charts",
@@ -32,13 +37,16 @@
"shortcuts",
"section_break_18",
"cards_label",
- "links"
+ "links",
+ "roles_section",
+ "roles"
],
"fields": [
{
"fieldname": "label",
"fieldtype": "Data",
"label": "Name",
+ "reqd": 1,
"unique": 1
},
{
@@ -199,7 +207,7 @@
},
{
"fieldname": "icon",
- "fieldtype": "Data",
+ "fieldtype": "Icon",
"label": "Icon"
},
{
@@ -209,16 +217,55 @@
"options": "Workspace Link"
},
{
- "default": "0",
- "depends_on": "extends_another_page",
- "description": "Sets the current page as default for all users",
- "fieldname": "is_default",
- "fieldtype": "Check",
- "label": "Is Default"
- }
+ "default": "0",
+ "depends_on": "extends_another_page",
+ "description": "Sets the current page as default for all users",
+ "fieldname": "is_default",
+ "fieldtype": "Check",
+ "label": "Is Default"
+ },
+ {
+ "default": "0",
+ "fieldname": "public",
+ "fieldtype": "Check",
+ "label": "Public"
+ },
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "label": "Title",
+ "reqd": 1
+ },
+ {
+ "fieldname": "parent_page",
+ "fieldtype": "Data",
+ "label": "Parent Page"
+ },
+ {
+ "fieldname": "content",
+ "fieldtype": "Long Text",
+ "hidden": 1,
+ "label": "Content"
+ },
+ {
+ "fieldname": "sequence_id",
+ "fieldtype": "Int",
+ "label": "Sequence Id"
+ },
+ {
+ "fieldname": "roles",
+ "fieldtype": "Table",
+ "label": "Roles",
+ "options": "Has Role"
+ },
+ {
+ "fieldname": "roles_section",
+ "fieldtype": "Section Break",
+ "label": "Roles"
+ }
],
"links": [],
- "modified": "2021-01-21 12:09:36.156614",
+ "modified": "2021-08-19 12:51:00.233017",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace",
@@ -232,7 +279,7 @@
"print": 1,
"read": 1,
"report": 1,
- "role": "System Manager",
+ "role": "Workspace Manager",
"share": 1,
"write": 1
},
@@ -248,4 +295,4 @@
],
"sort_field": "modified",
"sort_order": "DESC"
-}
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py
index 0934138821..31bb551330 100644
--- a/frappe/desk/doctype/workspace/workspace.py
+++ b/frappe/desk/doctype/workspace/workspace.py
@@ -2,11 +2,11 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.modules.export_file import export_to_files
from frappe.model.document import Document
+from frappe.desk.desktop import save_new_widget
from frappe.desk.utils import validate_route_conflict
from json import loads
@@ -17,6 +17,12 @@ class Workspace(Document):
frappe.throw(_("You need to be in developer mode to edit this document"))
validate_route_conflict(self.doctype, self.name)
+ try:
+ if not isinstance(loads(self.content), list):
+ raise
+ except Exception:
+ frappe.throw(_("Content data shoud be a list"))
+
duplicate_exists = frappe.db.exists("Workspace", {
"name": ["!=", self.name], 'is_default': 1, 'extends': self.extends
})
@@ -28,7 +34,7 @@ class Workspace(Document):
if disable_saving_as_standard():
return
- if frappe.conf.developer_mode and self.is_standard:
+ if frappe.conf.developer_mode and self.module and self.public:
export_to_files(record_list=[['Workspace', self.name]], record_module=self.module)
@staticmethod
@@ -44,20 +50,19 @@ class Workspace(Document):
def get_link_groups(self):
cards = []
- current_card = {
+ current_card = frappe._dict({
"label": "Link",
"type": "Card Break",
"icon": None,
"hidden": False,
- }
+ })
card_links = []
for link in self.links:
link = link.as_dict()
if link.type == "Card Break":
-
- if card_links:
+ if card_links and (not current_card['only_for'] or current_card['only_for'] == frappe.get_system_settings('country')):
current_card['links'] = card_links
cards.append(current_card)
@@ -100,6 +105,37 @@ class Workspace(Document):
"is_query_report": link.get('is_query_report')
})
+ def build_links_table_from_card(self, config):
+
+ for idx, card in enumerate(config):
+ links = loads(card.get('links'))
+
+ # remove duplicate before adding
+ for idx, link in enumerate(self.links):
+ if link.label == card.get('label') and link.type == 'Card Break':
+ del self.links[idx : idx + link.link_count + 1]
+
+ self.append('links', {
+ "label": card.get('label'),
+ "type": "Card Break",
+ "icon": card.get('icon'),
+ "hidden": card.get('hidden') or False,
+ "link_count": card.get('link_count'),
+ "idx": 1 if not self.links else self.links[-1].idx + 1
+ })
+
+ for link in links:
+ self.append('links', {
+ "label": link.get('label'),
+ "type": "Link",
+ "link_type": link.get('link_type'),
+ "link_to": link.get('link_to'),
+ "onboard": link.get('onboard'),
+ "only_for": link.get('only_for'),
+ "dependencies": link.get('dependencies'),
+ "is_query_report": link.get('is_query_report'),
+ "idx": self.links[-1].idx + 1
+ })
def disable_saving_as_standard():
return frappe.flags.in_install or \
@@ -125,3 +161,84 @@ def get_link_type(key):
def get_report_type(report):
report_type = frappe.get_value("Report", report, "report_type")
return report_type in ["Query Report", "Script Report", "Custom Report"]
+
+
+@frappe.whitelist()
+def save_page(title, icon, parent, public, sb_public_items, sb_private_items, deleted_pages, new_widgets, blocks, save):
+ save = frappe.parse_json(save)
+ public = frappe.parse_json(public)
+ if save:
+ doc = frappe.new_doc('Workspace')
+ doc.title = title
+ doc.icon = icon
+ doc.content = blocks
+ doc.parent_page = parent
+
+ if public:
+ doc.label = title
+ doc.public = 1
+ else:
+ doc.label = title + "-" + frappe.session.user
+ doc.for_user = frappe.session.user
+ doc.save(ignore_permissions=True)
+ else:
+ if public:
+ filters = {
+ 'public': public,
+ 'label': title
+ }
+ else:
+ filters = {
+ 'for_user': frappe.session.user,
+ 'label': title + "-" + frappe.session.user
+ }
+ pages = frappe.get_list("Workspace", filters=filters)
+ if pages:
+ doc = frappe.get_doc("Workspace", pages[0])
+
+ doc.content = blocks
+ doc.save(ignore_permissions=True)
+
+ if loads(new_widgets):
+ save_new_widget(doc, title, blocks, new_widgets)
+
+ if loads(sb_public_items) or loads(sb_private_items):
+ sort_pages(loads(sb_public_items), loads(sb_private_items))
+
+ if loads(deleted_pages):
+ return delete_pages(loads(deleted_pages))
+
+ return {"name": title, "public": public}
+
+def delete_pages(deleted_pages):
+ for page in deleted_pages:
+ if page.get("public") and "Workspace Manager" not in frappe.get_roles():
+ return {"name": page.get("title"), "public": 1}
+
+ if frappe.db.exists("Workspace", page.get("name")):
+ frappe.get_doc("Workspace", page.get("name")).delete(ignore_permissions=True)
+
+ return {"name": "Home", "public": 1}
+
+def sort_pages(sb_public_items, sb_private_items):
+ wspace_public_pages = get_page_list(['name', 'title'], {'public': 1})
+ wspace_private_pages = get_page_list(['name', 'title'], {'for_user': frappe.session.user})
+
+ if sb_private_items:
+ sort_page(wspace_private_pages, sb_private_items)
+
+ if sb_public_items and "Workspace Manager" in frappe.get_roles():
+ sort_page(wspace_public_pages, sb_public_items)
+
+def sort_page(wspace_pages, pages):
+ for seq, d in enumerate(pages):
+ for page in wspace_pages:
+ if page.title == d.get('title'):
+ doc = frappe.get_doc('Workspace', page.name)
+ doc.sequence_id = seq + 1
+ doc.parent_page = d.get('parent_page') or ""
+ doc.save(ignore_permissions=True)
+ break
+
+def get_page_list(fields, filters):
+ return frappe.get_list("Workspace", fields=fields, filters=filters, order_by='sequence_id asc')
diff --git a/frappe/desk/doctype/workspace_chart/workspace_chart.py b/frappe/desk/doctype/workspace_chart/workspace_chart.py
index 0bb6194d2e..6ec7abfd3c 100644
--- a/frappe/desk/doctype/workspace_chart/workspace_chart.py
+++ b/frappe/desk/doctype/workspace_chart/workspace_chart.py
@@ -2,7 +2,6 @@
# Copyright (c) 2021, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/workspace_link/workspace_link.json b/frappe/desk/doctype/workspace_link/workspace_link.json
index 53dadad83d..a7b217be9e 100644
--- a/frappe/desk/doctype/workspace_link/workspace_link.json
+++ b/frappe/desk/doctype/workspace_link/workspace_link.json
@@ -8,15 +8,16 @@
"type",
"label",
"icon",
- "only_for",
"hidden",
"link_details_section",
"link_type",
"link_to",
"column_break_7",
"dependencies",
+ "only_for",
"onboard",
- "is_query_report"
+ "is_query_report",
+ "link_count"
],
"fields": [
{
@@ -99,12 +100,19 @@
"fieldname": "is_query_report",
"fieldtype": "Check",
"label": "Is Query Report"
+ },
+ {
+ "depends_on": "eval:doc.type == \"Card Break\"",
+ "fieldname": "link_count",
+ "fieldtype": "Int",
+ "hidden": 1,
+ "label": "Link Count"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-05-13 13:10:18.128512",
+ "modified": "2021-06-01 11:23:28.990593",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace Link",
diff --git a/frappe/desk/doctype/workspace_link/workspace_link.py b/frappe/desk/doctype/workspace_link/workspace_link.py
index 8a139077a6..d6ccc5306a 100644
--- a/frappe/desk/doctype/workspace_link/workspace_link.py
+++ b/frappe/desk/doctype/workspace_link/workspace_link.py
@@ -2,7 +2,6 @@
# Copyright (c) 2021, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.py b/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.py
index d676f08b73..83b446e454 100644
--- a/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.py
+++ b/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.py
@@ -2,7 +2,6 @@
# Copyright (c) 2021, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/form/__init__.py b/frappe/desk/form/__init__.py
index 4dbcd0d163..0e57cb68c3 100644
--- a/frappe/desk/form/__init__.py
+++ b/frappe/desk/form/__init__.py
@@ -1,4 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
diff --git a/frappe/desk/form/assign_to.py b/frappe/desk/form/assign_to.py
index aee7a8e52a..3eda291d1e 100644
--- a/frappe/desk/form/assign_to.py
+++ b/frappe/desk/form/assign_to.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
"""assign/unassign to ToDo"""
import frappe
diff --git a/frappe/desk/form/document_follow.py b/frappe/desk/form/document_follow.py
index f5e5c0ca9b..7f65f76a58 100644
--- a/frappe/desk/form/document_follow.py
+++ b/frappe/desk/form/document_follow.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
import frappe.utils
from frappe.utils import get_url_to_form
diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py
index a62e2837d5..ae48b7fc6b 100644
--- a/frappe/desk/form/linked_with.py
+++ b/frappe/desk/form/linked_with.py
@@ -1,9 +1,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import json
from collections import defaultdict
-from six import string_types
+
import frappe
import frappe.desk.form.load
import frappe.desk.form.meta
@@ -11,6 +10,7 @@ from frappe import _
from frappe.model.meta import is_single
from frappe.modules import load_doctype_module
+
@frappe.whitelist()
def get_submitted_linked_docs(doctype, name, docs=None, visited=None):
"""
@@ -87,7 +87,7 @@ def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=[]):
"""
docs = json.loads(docs)
- if isinstance(ignore_doctypes_on_cancel_all, string_types):
+ if isinstance(ignore_doctypes_on_cancel_all, str):
ignore_doctypes_on_cancel_all = json.loads(ignore_doctypes_on_cancel_all)
for i, doc in enumerate(docs, 1):
if validate_linked_doc(doc, ignore_doctypes_on_cancel_all):
@@ -139,7 +139,7 @@ def get_exempted_doctypes():
@frappe.whitelist()
def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None):
- if isinstance(linkinfo, string_types):
+ if isinstance(linkinfo, str):
# additional fields are added in linkinfo
linkinfo = json.loads(linkinfo)
@@ -202,7 +202,8 @@ def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None):
else:
link_fieldnames = link.get("fieldname")
if link_fieldnames:
- if isinstance(link_fieldnames, string_types): link_fieldnames = [link_fieldnames]
+ if isinstance(link_fieldnames, str):
+ link_fieldnames = [link_fieldnames]
or_filters = [[dt, fieldname, '=', name] for fieldname in link_fieldnames]
# dynamic link
if link.get("doctype_fieldname"):
diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py
index d81bb8c26c..a62bfd01d0 100644
--- a/frappe/desk/form/load.py
+++ b/frappe/desk/form/load.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe, json
import frappe.utils
import frappe.share
@@ -11,7 +10,7 @@ from frappe.model.utils.user_settings import get_user_settings
from frappe.permissions import get_doc_permissions
from frappe.desk.form.document_follow import is_document_followed
from frappe import _
-from six.moves.urllib.parse import quote
+from urllib.parse import quote
@frappe.whitelist(allow_guest=True)
def getdoc(doctype, name, user=None):
diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py
index 087cc54d9d..cf3606e785 100644
--- a/frappe/desk/form/meta.py
+++ b/frappe/desk/form/meta.py
@@ -1,20 +1,16 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-
-# metadata
-
-from __future__ import unicode_literals
-import frappe, os
-from frappe.model.meta import Meta
-from frappe.modules import scrub, get_module_path, load_doctype_module
-from frappe.utils import get_html_format
-from frappe.translate import make_dict_from_messages, extract_messages_from_code
-from frappe.model.utils import render_include
-from frappe.build import scrub_html_template
-
import io
+import os
+
+import frappe
+from frappe.build import scrub_html_template
+from frappe.model.meta import Meta
+from frappe.model.utils import render_include
+from frappe.modules import get_module_path, load_doctype_module, scrub
+from frappe.translate import extract_messages_from_code, make_dict_from_messages
+from frappe.utils import get_html_format
-from six import iteritems
def get_meta(doctype, cached=True):
# don't cache for developer mode as js files, templates may be edited
@@ -199,7 +195,7 @@ class FormMeta(Meta):
app = module.__name__.split(".")[0]
templates = {}
if hasattr(module, "form_grid_templates"):
- for key, path in iteritems(module.form_grid_templates):
+ for key, path in module.form_grid_templates.items():
templates[key] = get_html_format(frappe.get_app_path(app, path))
self.set("__form_grid_templates", templates)
diff --git a/frappe/desk/form/save.py b/frappe/desk/form/save.py
index da43b14fce..a7a4b829d8 100644
--- a/frappe/desk/form/save.py
+++ b/frappe/desk/form/save.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe, json
from frappe.desk.form.load import run_onload
diff --git a/frappe/desk/form/test_form.py b/frappe/desk/form/test_form.py
index ff0343b6e0..f3c4132777 100644
--- a/frappe/desk/form/test_form.py
+++ b/frappe/desk/form/test_form.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe, unittest
from frappe.desk.form.linked_with import get_linked_docs, get_linked_doctypes
diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py
index 395d2b9571..d7ac940d21 100644
--- a/frappe/desk/form/utils.py
+++ b/frappe/desk/form/utils.py
@@ -1,15 +1,13 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe, json
import frappe.desk.form.meta
import frappe.desk.form.load
from frappe.desk.form.document_follow import follow_document
-from frappe.utils.file_manager import extract_images_from_html
+from frappe.core.doctype.file.file import extract_images_from_html
from frappe import _
-from six import string_types
@frappe.whitelist()
def remove_attach():
@@ -90,7 +88,7 @@ def get_next(doctype, value, prev, filters=None, sort_order='desc', sort_field='
prev = int(prev)
if not filters: filters = []
- if isinstance(filters, string_types):
+ if isinstance(filters, str):
filters = json.loads(filters)
# # condition based on sort order
diff --git a/frappe/desk/gantt.py b/frappe/desk/gantt.py
index 521884beaa..7f0889c751 100644
--- a/frappe/desk/gantt.py
+++ b/frappe/desk/gantt.py
@@ -1,8 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
-
import frappe, json
@frappe.whitelist()
diff --git a/frappe/desk/leaderboard.py b/frappe/desk/leaderboard.py
index d651687256..a98ae1a1c6 100644
--- a/frappe/desk/leaderboard.py
+++ b/frappe/desk/leaderboard.py
@@ -1,5 +1,3 @@
-
-from __future__ import unicode_literals, print_function
import frappe
from frappe.utils import get_fullname
diff --git a/frappe/desk/like.py b/frappe/desk/like.py
index 6d2e9704af..d44d58a761 100644
--- a/frappe/desk/like.py
+++ b/frappe/desk/like.py
@@ -1,8 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
-
"""Allow adding of likes to documents"""
import frappe, json
diff --git a/frappe/desk/listview.py b/frappe/desk/listview.py
index 91dc0f3ba9..d2c84d36bf 100644
--- a/frappe/desk/listview.py
+++ b/frappe/desk/listview.py
@@ -1,7 +1,5 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
-
import frappe
@frappe.whitelist(allow_guest=True)
diff --git a/frappe/desk/moduleview.py b/frappe/desk/moduleview.py
index df25b77e2d..021698ac92 100644
--- a/frappe/desk/moduleview.py
+++ b/frappe/desk/moduleview.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
import json
from frappe import _
diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py
index 4b584a2429..c84027928e 100644
--- a/frappe/desk/notifications.py
+++ b/frappe/desk/notifications.py
@@ -1,11 +1,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
-
import frappe
from frappe.desk.doctype.notification_settings.notification_settings import get_subscribed_documents
-from six import string_types
import json
@frappe.whitelist()
@@ -149,7 +146,7 @@ def clear_doctype_notifications(doc, method=None, *args, **kwargs):
config = get_notification_config()
if not config:
return
- if isinstance(doc, string_types):
+ if isinstance(doc, str):
doctype = doc # assuming doctype name was passed directly
else:
doctype = doc.doctype
@@ -213,7 +210,7 @@ def get_filters_for(doctype):
'''get open filters for doctype'''
config = get_notification_config()
doctype_config = config.get("for_doctype").get(doctype, {})
- filters = doctype_config if not isinstance(doctype_config, string_types) else None
+ filters = doctype_config if not isinstance(doctype_config, str) else None
return filters
diff --git a/frappe/desk/page/activity/__init__.py b/frappe/desk/page/activity/__init__.py
index baffc48825..8b13789179 100644
--- a/frappe/desk/page/activity/__init__.py
+++ b/frappe/desk/page/activity/__init__.py
@@ -1 +1 @@
-from __future__ import unicode_literals
+
diff --git a/frappe/desk/page/activity/activity.py b/frappe/desk/page/activity/activity.py
index 7de294d2f0..3abc8e0ea5 100644
--- a/frappe/desk/page/activity/activity.py
+++ b/frappe/desk/page/activity/activity.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: See license.txt
-from __future__ import unicode_literals
import frappe
from frappe.utils import cint
from frappe.core.doctype.activity_log.feed import get_feed_match_conditions
diff --git a/frappe/desk/page/backups/backups.py b/frappe/desk/page/backups/backups.py
index eaa0c65143..2229a6d89e 100644
--- a/frappe/desk/page/backups/backups.py
+++ b/frappe/desk/page/backups/backups.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import os
import frappe
from frappe import _
diff --git a/frappe/desk/page/leaderboard/leaderboard.py b/frappe/desk/page/leaderboard/leaderboard.py
index 819e7fe9d1..9469096f50 100644
--- a/frappe/desk/page/leaderboard/leaderboard.py
+++ b/frappe/desk/page/leaderboard/leaderboard.py
@@ -1,7 +1,5 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-
-from __future__ import unicode_literals, print_function
import frappe
@frappe.whitelist()
diff --git a/frappe/desk/page/setup_wizard/install_fixtures.py b/frappe/desk/page/setup_wizard/install_fixtures.py
index 6d3aaee22b..06301cdeaf 100644
--- a/frappe/desk/page/setup_wizard/install_fixtures.py
+++ b/frappe/desk/page/setup_wizard/install_fixtures.py
@@ -1,8 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
-
import frappe
from frappe import _
from frappe.desk.doctype.global_search_settings.global_search_settings import update_global_search_doctypes
diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py
index 1ac5279508..5edb44e182 100755
--- a/frappe/desk/page/setup_wizard/setup_wizard.py
+++ b/frappe/desk/page/setup_wizard/setup_wizard.py
@@ -1,8 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: See license.txt
-from __future__ import unicode_literals
-
import frappe, json, os
from frappe.utils import strip, cint
from frappe.translate import (set_default_language, get_dict, send_translations)
@@ -10,7 +8,6 @@ from frappe.geo.country_info import get_country_info
from frappe.utils.password import update_password
from werkzeug.useragents import UserAgent
from . import install_fixtures
-from six import string_types
def get_setup_stages(args):
@@ -208,14 +205,14 @@ def update_user_name(args):
def parse_args(args):
if not args:
args = frappe.local.form_dict
- if isinstance(args, string_types):
+ if isinstance(args, str):
args = json.loads(args)
args = frappe._dict(args)
# strip the whitespace
for key, value in args.items():
- if isinstance(value, string_types):
+ if isinstance(value, str):
args[key] = strip(value)
return args
@@ -294,7 +291,7 @@ def reset_is_first_startup():
def prettify_args(args):
# remove attachments
for key, val in args.items():
- if isinstance(val, string_types) and "data:image" in val:
+ if isinstance(val, str) and "data:image" in val:
filename = val.split("data:image", 1)[0].strip(", ")
size = round((len(val) * 3 / 4) / 1048576.0, 2)
args[key] = "Image Attached: '{0}' of size {1} MB".format(filename, size)
diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py
index befaf7b01f..b42c9c89a0 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -1,8 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
-
import frappe
import os
import json
@@ -22,7 +20,6 @@ from frappe.model.utils import render_include
from frappe.translate import send_translations
import frappe.desk.reportview
from frappe.permissions import get_role_permissions
-from six import string_types, iteritems
from datetime import timedelta
from frappe.core.utils import ljust_list
@@ -66,7 +63,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
user = user or frappe.session.user
filters = filters or []
- if filters and isinstance(filters, string_types):
+ if filters and isinstance(filters, str):
filters = json.loads(filters)
res = []
@@ -180,11 +177,13 @@ def get_script(report_name):
if os.path.exists(script_path):
with open(script_path, "r") as f:
script = f.read()
+ script += f"\n\n//# sourceURL={scrub(report.name)}.js"
html_format = get_html_format(print_path)
if not script and report.javascript:
script = report.javascript
+ script += f"\n\n//# sourceURL={scrub(report.name)}__custom"
if not script:
script = "frappe.query_reports['%s']={}" % report_name
@@ -222,7 +221,7 @@ def run(report_name, filters=None, user=None, ignore_prepared_report=False, cust
and not custom_columns
):
if filters:
- if isinstance(filters, string_types):
+ if isinstance(filters, str):
filters = json.loads(filters)
dn = filters.get("prepared_report_name")
@@ -317,7 +316,7 @@ def export_query():
data.pop("cmd", None)
data.pop("csrf_token", None)
- if isinstance(data.get("filters"), string_types):
+ if isinstance(data.get("filters"), str):
filters = json.loads(data["filters"])
if data.get("report_name"):
@@ -332,7 +331,7 @@ def export_query():
include_indentation = data.get("include_indentation")
visible_idx = data.get("visible_idx")
- if isinstance(visible_idx, string_types):
+ if isinstance(visible_idx, str):
visible_idx = json.loads(visible_idx)
if file_format_type == "Excel":
@@ -363,7 +362,7 @@ def export_query():
def handle_duration_fieldtype_values(result, columns):
for i, col in enumerate(columns):
fieldtype = None
- if isinstance(col, string_types):
+ if isinstance(col, str):
col = col.split(":")
if len(col) > 1:
if col[1]:
@@ -433,7 +432,7 @@ def add_total_row(result, columns, meta=None):
has_percent = []
for i, col in enumerate(columns):
fieldtype, options, fieldname = None, None, None
- if isinstance(col, string_types):
+ if isinstance(col, str):
if meta:
# get fieldtype from the meta
field = meta.get_field(col)
@@ -483,7 +482,7 @@ def add_total_row(result, columns, meta=None):
total_row[i] = flt(total_row[i]) / len(result)
first_col_fieldtype = None
- if isinstance(columns[0], string_types):
+ if isinstance(columns[0], str):
first_col = columns[0].split(":")
if len(first_col) > 1:
first_col_fieldtype = first_col[1].split("/")[0]
@@ -701,7 +700,7 @@ def get_linked_doctypes(columns, data):
if val and col not in columns_with_value:
columns_with_value.append(col)
- items = list(iteritems(linked_doctypes))
+ items = list(linked_doctypes.items())
for doctype, key in items:
if key not in columns_with_value:
@@ -728,7 +727,7 @@ def get_column_as_dict(col):
col_dict = frappe._dict()
# string
- if isinstance(col, string_types):
+ if isinstance(col, str):
col = col.split(":")
if len(col) > 1:
if "/" in col[1]:
diff --git a/frappe/desk/report/todo/todo.py b/frappe/desk/report/todo/todo.py
index f4fe2dc805..6bd22b843e 100644
--- a/frappe/desk/report/todo/todo.py
+++ b/frappe/desk/report/todo/todo.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import getdate
diff --git a/frappe/desk/report_dump.py b/frappe/desk/report_dump.py
index 86b1765814..b2d3ca3443 100644
--- a/frappe/desk/report_dump.py
+++ b/frappe/desk/report_dump.py
@@ -1,8 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
-from six.moves import range
+
import frappe
import json
import copy
diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py
index 86f8ec0aa7..1dbc52eb5b 100644
--- a/frappe/desk/reportview.py
+++ b/frappe/desk/reportview.py
@@ -1,16 +1,14 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
"""build query for doclistview and return results"""
import frappe, json
-from six.moves import range
import frappe.permissions
from frappe.model.db_query import DatabaseQuery
from frappe.model import default_fields, optional_fields
from frappe import _
-from six import string_types, StringIO
+from io import StringIO
from frappe.core.doctype.access_log.access_log import make_access_log
from frappe.utils import cstr, format_duration
from frappe.model.base_document import get_controller
@@ -171,7 +169,7 @@ def get_meta_and_docfield(fieldname, data):
return meta, df
def update_wildcard_field_param(data):
- if ((isinstance(data.fields, string_types) and data.fields == "*")
+ if ((isinstance(data.fields, str) and data.fields == "*")
or (isinstance(data.fields, (list, tuple)) and len(data.fields) == 1 and data.fields[0] == "*")):
data.fields = frappe.db.get_table_columns(data.doctype)
return True
@@ -191,15 +189,15 @@ def clean_params(data):
def parse_json(data):
- if isinstance(data.get("filters"), string_types):
+ if isinstance(data.get("filters"), str):
data["filters"] = json.loads(data["filters"])
- if isinstance(data.get("or_filters"), string_types):
+ if isinstance(data.get("or_filters"), str):
data["or_filters"] = json.loads(data["or_filters"])
- if isinstance(data.get("fields"), string_types):
+ if isinstance(data.get("fields"), str):
data["fields"] = json.loads(data["fields"])
- if isinstance(data.get("docstatus"), string_types):
+ if isinstance(data.get("docstatus"), str):
data["docstatus"] = json.loads(data["docstatus"])
- if isinstance(data.get("save_user_settings"), string_types):
+ if isinstance(data.get("save_user_settings"), str):
data["save_user_settings"] = json.loads(data["save_user_settings"])
else:
data["save_user_settings"] = True
@@ -311,7 +309,7 @@ def export_query():
for r in data:
# encode only unicode type strings and not int, floats etc.
writer.writerow([handle_html(frappe.as_unicode(v)) \
- if isinstance(v, string_types) else v for v in r])
+ if isinstance(v, str) else v for v in r])
f.seek(0)
frappe.response['result'] = cstr(f.read())
@@ -447,24 +445,36 @@ def get_stats(stats, doctype, filters=[]):
for tag in tags:
if not tag in columns: continue
try:
- tagcount = frappe.get_list(doctype, fields=[tag, "count(*)"],
- #filters=["ifnull(`%s`,'')!=''" % tag], group_by=tag, as_list=True)
- filters = filters + ["ifnull(`%s`,'')!=''" % tag], group_by = tag, as_list = True)
+ tag_count = frappe.get_list(doctype,
+ fields=[tag, "count(*)"],
+ filters=filters + [[tag, '!=', '']],
+ group_by=tag,
+ as_list=True,
+ distinct=1,
+ )
- if tag=='_user_tags':
- stats[tag] = scrub_user_tags(tagcount)
- stats[tag].append([_("No Tags"), frappe.get_list(doctype,
+ if tag == '_user_tags':
+ stats[tag] = scrub_user_tags(tag_count)
+ no_tag_count = frappe.get_list(doctype,
fields=[tag, "count(*)"],
- filters=filters +["({0} = ',' or {0} = '' or {0} is null)".format(tag)], as_list=True)[0][1]])
+ filters=filters + [[tag, "in", ('', ',')]],
+ as_list=True,
+ group_by=tag,
+ order_by=tag,
+ )
+
+ no_tag_count = no_tag_count[0][1] if no_tag_count else 0
+
+ stats[tag].append([_("No Tags"), no_tag_count])
else:
- stats[tag] = tagcount
+ stats[tag] = tag_count
except frappe.db.SQLError:
- # does not work for child tables
pass
- except frappe.db.InternalError:
+ except frappe.db.InternalError as e:
# raised when _user_tags column is added on the fly
pass
+
return stats
@frappe.whitelist()
@@ -540,7 +550,7 @@ def build_match_conditions(doctype, user=None, as_condition=True):
return match_conditions
def get_filters_cond(doctype, filters, conditions, ignore_permissions=None, with_match_conditions=False):
- if isinstance(filters, string_types):
+ if isinstance(filters, str):
filters = json.loads(filters)
if filters:
@@ -549,7 +559,7 @@ def get_filters_cond(doctype, filters, conditions, ignore_permissions=None, with
filters = filters.items()
flt = []
for f in filters:
- if isinstance(f[1], string_types) and f[1][0] == '!':
+ if isinstance(f[1], str) and f[1][0] == '!':
flt.append([doctype, f[0], '!=', f[1][1:]])
elif isinstance(f[1], (list, tuple)) and \
f[1][0] in (">", "<", ">=", "<=", "!=", "like", "not like", "in", "not in", "between"):
diff --git a/frappe/desk/search.py b/frappe/desk/search.py
index 3c9109eca9..f9b65fc98e 100644
--- a/frappe/desk/search.py
+++ b/frappe/desk/search.py
@@ -2,12 +2,10 @@
# MIT License. See license.txt
# Search
-from __future__ import unicode_literals
import frappe, json
from frappe.utils import cstr, unique, cint
from frappe.permissions import has_permission
from frappe import _, is_whitelisted
-from six import string_types
import re
import wrapt
@@ -62,7 +60,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
start = cint(start)
- if isinstance(filters, string_types):
+ if isinstance(filters, str):
filters = json.loads(filters)
if searchfield:
@@ -170,7 +168,18 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
strict=False)
if doctype in UNTRANSLATED_DOCTYPES:
- values = tuple([v for v in list(values) if re.search(re.escape(txt)+".*", (_(v.name) if as_dict else _(v[0])), re.IGNORECASE)])
+ # Filtering the values array so that query is included in very element
+ values = (
+ v for v in values
+ if re.search(
+ f"{re.escape(txt)}.*", _(v.name if as_dict else v[0]), re.IGNORECASE
+ )
+ )
+
+ # Sorting the values array so that relevant results always come first
+ # This will first bring elements on top in which query is a prefix of element
+ # Then it will bring the rest of the elements and sort them in lexicographical order
+ values = sorted(values, key=lambda x: relevance_sorter(x, txt, as_dict))
# remove _relevance from results
if as_dict:
@@ -210,6 +219,13 @@ def scrub_custom_query(query, key, txt):
query = query.replace('%s', ((txt or '') + '%'))
return query
+def relevance_sorter(key, query, as_dict):
+ value = _(key.name if as_dict else key[0])
+ return (
+ 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)))
diff --git a/frappe/desk/treeview.py b/frappe/desk/treeview.py
index 6f0d7d3d5f..66acde4cb2 100644
--- a/frappe/desk/treeview.py
+++ b/frappe/desk/treeview.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
from frappe import _
diff --git a/frappe/email/__init__.py b/frappe/email/__init__.py
index b05aef7639..3fb539398a 100644
--- a/frappe/email/__init__.py
+++ b/frappe/email/__init__.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
from frappe.desk.reportview import build_match_conditions
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 6f1cd8eebd..f30279e308 100644
--- a/frappe/email/doctype/auto_email_report/auto_email_report.py
+++ b/frappe/email/doctype/auto_email_report/auto_email_report.py
@@ -2,8 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
-
import calendar
from datetime import timedelta
@@ -245,6 +243,7 @@ def send_monthly():
def make_links(columns, data):
for row in data:
+ doc_name = row.get('name')
for col in columns:
if col.fieldtype == "Link" and col.options != "Currency":
if col.options and row.get(col.fieldname):
@@ -253,8 +252,9 @@ def make_links(columns, data):
if col.options and row.get(col.fieldname) and row.get(col.options):
row[col.fieldname] = get_link_to_form(row[col.options], row[col.fieldname])
elif col.fieldtype == "Currency" and row.get(col.fieldname):
- row[col.fieldname] = frappe.format_value(row[col.fieldname], col)
-
+ doc = frappe.get_doc(col.parent, doc_name) if doc_name else None
+ # Pass the Document to get the currency based on docfield option
+ row[col.fieldname] = frappe.format_value(row[col.fieldname], col, doc=doc)
return columns, data
def update_field_types(columns):
@@ -262,4 +262,4 @@ def update_field_types(columns):
if col.fieldtype in ("Link", "Dynamic Link", "Currency") and col.options != "Currency":
col.fieldtype = "Data"
col.options = ""
- return columns
\ No newline at end of file
+ return columns
diff --git a/frappe/email/doctype/auto_email_report/test_auto_email_report.py b/frappe/email/doctype/auto_email_report/test_auto_email_report.py
index e656ff18f7..211a141ec0 100644
--- a/frappe/email/doctype/auto_email_report/test_auto_email_report.py
+++ b/frappe/email/doctype/auto_email_report/test_auto_email_report.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import json
import unittest
diff --git a/frappe/email/doctype/document_follow/document_follow.py b/frappe/email/doctype/document_follow/document_follow.py
index aaabffab6b..a04f8ef4c2 100644
--- a/frappe/email/doctype/document_follow/document_follow.py
+++ b/frappe/email/doctype/document_follow/document_follow.py
@@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
from frappe.model.document import Document
class DocumentFollow(Document):
diff --git a/frappe/email/doctype/document_follow/test_document_follow.py b/frappe/email/doctype/document_follow/test_document_follow.py
index 38aa870232..456c0931f8 100644
--- a/frappe/email/doctype/document_follow/test_document_follow.py
+++ b/frappe/email/doctype/document_follow/test_document_follow.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
import frappe.desk.form.document_follow as document_follow
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index 36b662bb39..fb7349adba 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -1,34 +1,25 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-
-from __future__ import unicode_literals, print_function
-import frappe
+import email.utils
+import functools
import imaplib
-import re
-import json
import socket
import time
-import functools
-
-import email.utils
-
-from frappe import _, are_emails_muted
-from frappe.model.document import Document
-from frappe.utils import (validate_email_address, cint, cstr, get_datetime,
- DATE_FORMAT, strip, comma_or, sanitize_html, add_days, parse_addr)
-from frappe.utils.user import is_system_user
-from frappe.utils.jinja import render_template
-from frappe.email.smtp import SMTPServer
-from frappe.email.receive import EmailServer, Email
-from poplib import error_proto
-from dateutil.relativedelta import relativedelta
from datetime import datetime, timedelta
+from poplib import error_proto
+
+import frappe
+from frappe import _, are_emails_muted, safe_encode
from frappe.desk.form import assign_to
-from frappe.utils.user import get_system_managers
-from frappe.utils.background_jobs import enqueue, get_jobs
-from frappe.utils.html_utils import clean_email_html
-from frappe.utils.error import raise_error_on_no_output
+from frappe.email.receive import EmailServer, InboundMail, SentEmailInInboxError
+from frappe.email.smtp import SMTPServer
from frappe.email.utils import get_port
+from frappe.model.document import Document
+from frappe.utils import cint, comma_or, cstr, parse_addr, validate_email_address
+from frappe.utils.background_jobs import enqueue, get_jobs
+from frappe.utils.error import raise_error_on_no_output
+from frappe.utils.jinja import render_template
+from frappe.utils.user import get_system_managers
OUTGOING_EMAIL_ACCOUNT_MISSING = _("Please setup default Email Account from Setup > Email > Email Account")
@@ -146,8 +137,6 @@ class EmailAccount(Document):
def on_update(self):
"""Check there is only one default of each type."""
- from frappe.core.doctype.user.user import setup_user_email_inbox
-
self.check_automatic_linking_email_account()
self.there_must_be_only_one_default()
setup_user_email_inbox(email_account=self.name, awaiting_password=self.awaiting_password,
@@ -430,89 +419,74 @@ class EmailAccount(Document):
def receive(self, test_mails=None):
"""Called by scheduler to receive emails from this EMail account using POP3/IMAP."""
- def get_seen(status):
- if not status:
- return None
- seen = 1 if status == "SEEN" else 0
- return seen
+ exceptions = []
+ inbound_mails = self.get_inbound_mails(test_mails=test_mails)
+ for mail in inbound_mails:
+ try:
+ communication = mail.process()
+ frappe.db.commit()
+ # If email already exists in the system
+ # then do not send notifications for the same email.
+ if communication and mail.flags.is_new_communication:
+ # notify all participants of this thread
+ if self.enable_auto_reply:
+ self.send_auto_reply(communication, mail)
- if self.enable_incoming:
- uid_list = []
- exceptions = []
- seen_status = []
- uid_reindexed = False
- email_server = None
-
- if frappe.local.flags.in_test:
- incoming_mails = test_mails or []
+ communication.send_email(is_inbound_mail_communcation=True)
+ except SentEmailInInboxError:
+ frappe.db.rollback()
+ except Exception:
+ frappe.db.rollback()
+ frappe.log_error('email_account.receive')
+ if self.use_imap:
+ self.handle_bad_emails(mail.uid, mail.raw_message, frappe.get_traceback())
+ exceptions.append(frappe.get_traceback())
else:
- email_sync_rule = self.build_email_sync_rule()
+ frappe.db.commit()
- try:
- email_server = self.get_incoming_server(in_receive=True, email_sync_rule=email_sync_rule)
- except Exception:
- frappe.log_error(title=_("Error while connecting to email account {0}").format(self.name))
+ #notify if user is linked to account
+ if len(inbound_mails)>0 and not frappe.local.flags.in_test:
+ frappe.publish_realtime('new_email',
+ {"account":self.email_account_name, "number":len(inbound_mails)}
+ )
- if not email_server:
- return
+ if exceptions:
+ raise Exception(frappe.as_json(exceptions))
- emails = email_server.get_messages()
- if not emails:
- return
+ def get_inbound_mails(self, test_mails=None):
+ """retrive and return inbound mails.
- incoming_mails = emails.get("latest_messages", [])
- uid_list = emails.get("uid_list", [])
- seen_status = emails.get("seen_status", [])
- uid_reindexed = emails.get("uid_reindexed", False)
+ """
+ if frappe.local.flags.in_test:
+ return [InboundMail(msg, self) for msg in test_mails or []]
- for idx, msg in enumerate(incoming_mails):
- uid = None if not uid_list else uid_list[idx]
- self.flags.notify = True
+ if not self.enable_incoming:
+ return []
- try:
- args = {
- "uid": uid,
- "seen": None if not seen_status else get_seen(seen_status.get(uid, None)),
- "uid_reindexed": uid_reindexed
- }
- communication = self.insert_communication(msg, args=args)
+ email_sync_rule = self.build_email_sync_rule()
+ try:
+ email_server = self.get_incoming_server(in_receive=True, email_sync_rule=email_sync_rule)
+ messages = email_server.get_messages() or {}
+ except Exception:
+ frappe.log_error(title=_("Error while connecting to email account {0}").format(self.name))
+ return []
- except SentEmailInInbox:
- frappe.db.rollback()
+ mails = []
+ for index, message in enumerate(messages.get("latest_messages", [])):
+ uid = messages['uid_list'][index] if messages.get('uid_list') else None
+ seen_status = 1 if messages.get('seen_status', {}).get(uid)=='SEEN' else 0
+ mails.append(InboundMail(message, self, uid, seen_status))
- except Exception:
- frappe.db.rollback()
- frappe.log_error('email_account.receive')
- if self.use_imap:
- self.handle_bad_emails(email_server, uid, msg, frappe.get_traceback())
- exceptions.append(frappe.get_traceback())
+ return mails
- else:
- frappe.db.commit()
- if communication and self.flags.notify:
-
- # If email already exists in the system
- # then do not send notifications for the same email.
-
- attachments = []
-
- if hasattr(communication, '_attachments'):
- attachments = [d.file_name for d in communication._attachments]
-
- communication.notify(attachments=attachments, fetched_from_email_account=True)
-
- #notify if user is linked to account
- if len(incoming_mails)>0 and not frappe.local.flags.in_test:
- frappe.publish_realtime('new_email', {"account":self.email_account_name, "number":len(incoming_mails)})
-
- if exceptions:
- raise Exception(frappe.as_json(exceptions))
-
- def handle_bad_emails(self, email_server, uid, raw, reason):
- if email_server and cint(email_server.settings.use_imap):
+ def handle_bad_emails(self, uid, raw, reason):
+ if cint(self.use_imap):
import email
try:
- mail = email.message_from_string(raw)
+ if isinstance(raw, bytes):
+ mail = email.message_from_bytes(raw)
+ else:
+ mail = email.message_from_string(raw)
message_id = mail.get('Message-ID')
except Exception:
@@ -524,275 +498,18 @@ class EmailAccount(Document):
"reason":reason,
"message_id": message_id,
"doctype": "Unhandled Email",
- "email_account": email_server.settings.email_account
+ "email_account": self.name
})
unhandled_email.insert(ignore_permissions=True)
frappe.db.commit()
- def insert_communication(self, msg, args=None):
- if isinstance(msg, list):
- raw, uid, seen = msg
- else:
- raw = msg
- uid = -1
- seen = 0
- if isinstance(args, dict):
- if args.get("uid", -1): uid = args.get("uid", -1)
- if args.get("seen", 0): seen = args.get("seen", 0)
-
- email = Email(raw)
-
- if email.from_email == self.email_id and not email.mail.get("Reply-To"):
- # gmail shows sent emails in inbox
- # and we don't want emails sent by us to be pulled back into the system again
- # dont count emails sent by the system get those
- if frappe.flags.in_test:
- print('WARN: Cannot pull email. Sender sames as recipient inbox')
- raise SentEmailInInbox
-
- if email.message_id:
- # https://stackoverflow.com/a/18367248
- names = frappe.db.sql("""SELECT DISTINCT `name`, `creation` FROM `tabCommunication`
- WHERE `message_id`='{message_id}'
- ORDER BY `creation` DESC LIMIT 1""".format(
- message_id=email.message_id
- ), as_dict=True)
-
- if names:
- name = names[0].get("name")
- # email is already available update communication uid instead
- frappe.db.set_value("Communication", name, "uid", uid, update_modified=False)
-
- self.flags.notify = False
-
- return frappe.get_doc("Communication", name)
-
- if email.content_type == 'text/html':
- email.content = clean_email_html(email.content)
-
- communication = frappe.get_doc({
- "doctype": "Communication",
- "subject": email.subject,
- "content": email.content,
- 'text_content': email.text_content,
- "sent_or_received": "Received",
- "sender_full_name": email.from_real_name,
- "sender": email.from_email,
- "recipients": email.mail.get("To"),
- "cc": email.mail.get("CC"),
- "email_account": self.name,
- "communication_medium": "Email",
- "uid": int(uid or -1),
- "message_id": email.message_id,
- "communication_date": email.date,
- "has_attachment": 1 if email.attachments else 0,
- "seen": seen or 0
- })
-
- self.set_thread(communication, email)
- if communication.seen:
- # get email account user and set communication as seen
- users = frappe.get_all("User Email", filters={ "email_account": self.name },
- fields=["parent"])
- users = list(set([ user.get("parent") for user in users ]))
- communication._seen = json.dumps(users)
-
- communication.flags.in_receive = True
- communication.insert(ignore_permissions=True)
-
- # save attachments
- communication._attachments = email.save_attachments_in_doc(communication)
-
- # replace inline images
- dirty = False
- for file in communication._attachments:
- if file.name in email.cid_map and email.cid_map[file.name]:
- dirty = True
-
- email.content = email.content.replace("cid:{0}".format(email.cid_map[file.name]),
- file.file_url)
-
- if dirty:
- # not sure if using save() will trigger anything
- communication.db_set("content", sanitize_html(email.content))
-
- # notify all participants of this thread
- if self.enable_auto_reply and getattr(communication, "is_first", False):
- self.send_auto_reply(communication, email)
-
- return communication
-
- def set_thread(self, communication, email):
- """Appends communication to parent based on thread ID. Will extract
- parent communication and will link the communication to the reference of that
- communication. Also set the status of parent transaction to Open or Replied.
-
- If no thread id is found and `append_to` is set for the email account,
- it will create a new parent transaction (e.g. Issue)"""
- parent = None
-
- parent = self.find_parent_from_in_reply_to(communication, email)
-
- if not parent and self.append_to:
- self.set_sender_field_and_subject_field()
-
- if not parent and self.append_to:
- parent = self.find_parent_based_on_subject_and_sender(communication, email)
-
- if not parent and self.append_to and self.append_to!="Communication":
- parent = self.create_new_parent(communication, email)
-
- if parent:
- communication.reference_doctype = parent.doctype
- communication.reference_name = parent.name
-
- # check if message is notification and disable notifications for this message
- isnotification = email.mail.get("isnotification")
- if isnotification:
- if "notification" in isnotification:
- communication.unread_notification_sent = 1
-
- def set_sender_field_and_subject_field(self):
- '''Identify the sender and subject fields from the `append_to` DocType'''
- # set subject_field and sender_field
- meta = frappe.get_meta(self.append_to)
- self.subject_field = None
- self.sender_field = None
-
- if hasattr(meta, "subject_field"):
- self.subject_field = meta.subject_field
-
- if hasattr(meta, "sender_field"):
- self.sender_field = meta.sender_field
-
- def find_parent_based_on_subject_and_sender(self, communication, email):
- '''Find parent document based on subject and sender match'''
- parent = None
-
- if self.append_to and self.sender_field:
- if self.subject_field:
- if '#' in email.subject:
- # try and match if ID is found
- # document ID is appended to subject
- # example "Re: Your email (#OPP-2020-2334343)"
- parent_id = email.subject.rsplit('#', 1)[-1].strip(' ()')
- if parent_id:
- parent = frappe.db.get_all(self.append_to, filters = dict(name = parent_id),
- fields = 'name')
-
- if not parent:
- # try and match by subject and sender
- # if sent by same sender with same subject,
- # append it to old coversation
- subject = frappe.as_unicode(strip(re.sub(r"(^\s*(fw|fwd|wg)[^:]*:|\s*(re|aw)[^:]*:\s*)*",
- "", email.subject, 0, flags=re.IGNORECASE)))
-
- parent = frappe.db.get_all(self.append_to, filters={
- self.sender_field: email.from_email,
- self.subject_field: ("like", "%{0}%".format(subject)),
- "creation": (">", (get_datetime() - relativedelta(days=60)).strftime(DATE_FORMAT))
- }, fields = "name", limit = 1)
-
- if not parent and len(subject) > 10 and is_system_user(email.from_email):
- # match only subject field
- # when the from_email is of a user in the system
- # and subject is atleast 10 chars long
- parent = frappe.db.get_all(self.append_to, filters={
- self.subject_field: ("like", "%{0}%".format(subject)),
- "creation": (">", (get_datetime() - relativedelta(days=60)).strftime(DATE_FORMAT))
- }, fields = "name", limit = 1)
-
-
-
- if parent:
- parent = frappe._dict(doctype=self.append_to, name=parent[0].name)
- return parent
-
- def create_new_parent(self, communication, email):
- '''If no parent found, create a new reference document'''
-
- # no parent found, but must be tagged
- # insert parent type doc
- parent = frappe.new_doc(self.append_to)
-
- if self.subject_field:
- parent.set(self.subject_field, frappe.as_unicode(email.subject)[:140])
-
- if self.sender_field:
- parent.set(self.sender_field, frappe.as_unicode(email.from_email))
-
- if parent.meta.has_field("email_account"):
- parent.email_account = self.name
-
- parent.flags.ignore_mandatory = True
-
- try:
- parent.insert(ignore_permissions=True)
- except frappe.DuplicateEntryError:
- # try and find matching parent
- parent_name = frappe.db.get_value(self.append_to, {self.sender_field: email.from_email})
- if parent_name:
- parent.name = parent_name
- else:
- parent = None
-
- # NOTE if parent isn't found and there's no subject match, it is likely that it is a new conversation thread and hence is_first = True
- communication.is_first = True
-
- return parent
-
- def find_parent_from_in_reply_to(self, communication, email):
- '''Returns parent reference if embedded in In-Reply-To header
-
- Message-ID is formatted as `{message_id}@{site}`'''
- parent = None
- in_reply_to = (email.mail.get("In-Reply-To") or "").strip(" <>")
-
- if in_reply_to:
- if "@{0}".format(frappe.local.site) in in_reply_to:
- # reply to a communication sent from the system
- email_queue = frappe.db.get_value('Email Queue', dict(message_id=in_reply_to), ['communication','reference_doctype', 'reference_name'])
- if email_queue:
- parent_communication, parent_doctype, parent_name = email_queue
- if parent_communication:
- communication.in_reply_to = parent_communication
- else:
- reference, domain = in_reply_to.split("@", 1)
- parent_doctype, parent_name = 'Communication', reference
-
- if frappe.db.exists(parent_doctype, parent_name):
- parent = frappe._dict(doctype=parent_doctype, name=parent_name)
-
- # set in_reply_to of current communication
- if parent_doctype=='Communication':
- # communication.in_reply_to = email_queue.communication
-
- if parent.reference_name:
- # the true parent is the communication parent
- parent = frappe.get_doc(parent.reference_doctype,
- parent.reference_name)
- else:
- comm = frappe.db.get_value('Communication',
- dict(
- message_id=in_reply_to,
- creation=['>=', add_days(get_datetime(), -30)]),
- ['reference_doctype', 'reference_name'], as_dict=1)
- if comm:
- parent = frappe._dict(doctype=comm.reference_doctype, name=comm.reference_name)
-
- return parent
-
def send_auto_reply(self, communication, email):
"""Send auto reply if set."""
from frappe.core.doctype.communication.email import set_incoming_outgoing_accounts
-
if self.enable_auto_reply:
set_incoming_outgoing_accounts(communication)
- if self.send_unsubscribe_message:
- unsubscribe_message = _("Leave this conversation")
- else:
- unsubscribe_message = ""
+ unsubscribe_message = (self.send_unsubscribe_message and _("Leave this conversation")) or ""
frappe.sendmail(recipients = [email.from_email],
sender = self.email_id,
@@ -813,8 +530,6 @@ class EmailAccount(Document):
def on_trash(self):
"""Clear communications where email account is linked"""
- from frappe.core.doctype.user.user import remove_user_email_inbox
-
frappe.db.sql("update `tabCommunication` set email_account='' where email_account=%s", self.name)
remove_user_email_inbox(email_account=self.name)
@@ -851,8 +566,8 @@ class EmailAccount(Document):
email_server.update_flag(uid_list=uid_list)
# mark communication as read
- docnames = ",".join([ "'%s'"%flag.get("communication") for flag in flags \
- if flag.get("action") == "Read" ])
+ docnames = ",".join("'%s'"%flag.get("communication") for flag in flags \
+ if flag.get("action") == "Read")
self.set_communication_seen_status(docnames, seen=1)
# mark communication as unread
@@ -882,7 +597,6 @@ class EmailAccount(Document):
def append_email_to_sent_folder(self, message):
-
email_server = None
try:
email_server = self.get_incoming_server(in_receive=True)
@@ -896,7 +610,8 @@ class EmailAccount(Document):
if email_server.imap:
try:
- email_server.imap.append("Sent", "\\Seen", imaplib.Time2Internaldate(time.time()), message.encode())
+ message = safe_encode(message)
+ email_server.imap.append("Sent", "\\Seen", imaplib.Time2Internaldate(time.time()), message)
except Exception:
frappe.log_error()
@@ -1005,3 +720,84 @@ def get_max_email_uid(email_account):
else:
max_uid = cint(result[0].get("uid", 0)) + 1
return max_uid
+
+
+def setup_user_email_inbox(email_account, awaiting_password, email_id, enable_outgoing):
+ """ setup email inbox for user """
+ from frappe.core.doctype.user.user import ask_pass_update
+
+ def add_user_email(user):
+ user = frappe.get_doc("User", user)
+ row = user.append("user_emails", {})
+
+ row.email_id = email_id
+ row.email_account = email_account
+ row.awaiting_password = awaiting_password or 0
+ row.enable_outgoing = enable_outgoing or 0
+
+ user.save(ignore_permissions=True)
+
+ update_user_email_settings = False
+ if not all([email_account, email_id]):
+ return
+
+ user_names = frappe.db.get_values("User", {"email": email_id}, as_dict=True)
+ if not user_names:
+ return
+
+ for user in user_names:
+ user_name = user.get("name")
+
+ # check if inbox is alreay configured
+ user_inbox = frappe.db.get_value("User Email", {
+ "email_account": email_account,
+ "parent": user_name
+ }, ["name"]) or None
+
+ if not user_inbox:
+ add_user_email(user_name)
+ else:
+ # update awaiting password for email account
+ update_user_email_settings = True
+
+ if update_user_email_settings:
+ frappe.db.sql("""UPDATE `tabUser Email` SET awaiting_password = %(awaiting_password)s,
+ enable_outgoing = %(enable_outgoing)s WHERE email_account = %(email_account)s""", {
+ "email_account": email_account,
+ "enable_outgoing": enable_outgoing,
+ "awaiting_password": awaiting_password or 0
+ })
+ else:
+ users = " and ".join([frappe.bold(user.get("name")) for user in user_names])
+ frappe.msgprint(_("Enabled email inbox for user {0}").format(users))
+ ask_pass_update()
+
+def remove_user_email_inbox(email_account):
+ """ remove user email inbox settings if email account is deleted """
+ if not email_account:
+ return
+
+ users = frappe.get_all("User Email", filters={
+ "email_account": email_account
+ }, fields=["parent as name"])
+
+ for user in users:
+ doc = frappe.get_doc("User", user.get("name"))
+ to_remove = [row for row in doc.user_emails if row.email_account == email_account]
+ [doc.remove(row) for row in to_remove]
+
+ doc.save(ignore_permissions=True)
+
+@frappe.whitelist(allow_guest=False)
+def set_email_password(email_account, user, password):
+ account = frappe.get_doc("Email Account", email_account)
+ if account.awaiting_password:
+ account.awaiting_password = 0
+ account.password = password
+ try:
+ account.save(ignore_permissions=True)
+ except Exception:
+ frappe.db.rollback()
+ return False
+
+ return True
\ No newline at end of file
diff --git a/frappe/email/doctype/email_account/test_email_account.py b/frappe/email/doctype/email_account/test_email_account.py
index f87ee32bb1..da03a5959e 100644
--- a/frappe/email/doctype/email_account/test_email_account.py
+++ b/frappe/email/doctype/email_account/test_email_account.py
@@ -1,45 +1,56 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-from __future__ import unicode_literals
-import frappe, os
-import unittest, email
+import os
+import email
+import unittest
+from datetime import datetime, timedelta
+from frappe.email.receive import InboundMail, SentEmailInInboxError, Email
+from frappe.email.email_body import get_message_id
+import frappe
from frappe.test_runner import make_test_records
+from frappe.core.doctype.communication.email import make
+from frappe.desk.form.load import get_attachments
+from frappe.email.doctype.email_account.email_account import notify_unreplied
make_test_records("User")
make_test_records("Email Account")
-from frappe.core.doctype.communication.email import make
-from frappe.desk.form.load import get_attachments
-from frappe.email.doctype.email_account.email_account import notify_unreplied
-from datetime import datetime, timedelta
+
class TestEmailAccount(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
+ email_account.db_set("enable_incoming", 1)
+ email_account.db_set("enable_auto_reply", 1)
+
+ @classmethod
+ def tearDownClass(cls):
+ email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
+ email_account.db_set("enable_incoming", 0)
+
def setUp(self):
frappe.flags.mute_emails = False
frappe.flags.sent_mail = None
+ frappe.db.delete("Email Queue")
+ frappe.db.delete("Unhandled Email")
- email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
- email_account.db_set("enable_incoming", 1)
- frappe.db.sql('delete from `tabEmail Queue`')
-
- def tearDown(self):
- email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
- email_account.db_set("enable_incoming", 0)
+ def get_test_mail(self, fname):
+ with open(os.path.join(os.path.dirname(__file__), "test_mails", fname), "r") as f:
+ return f.read()
def test_incoming(self):
cleanup("test_sender@example.com")
- with open(os.path.join(os.path.dirname(__file__), "test_mails", "incoming-1.raw"), "r") as f:
- test_mails = [f.read()]
+ test_mails = [self.get_test_mail('incoming-1.raw')]
email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
email_account.receive(test_mails=test_mails)
comm = frappe.get_doc("Communication", {"sender": "test_sender@example.com"})
self.assertTrue("test_receiver@example.com" in comm.recipients)
-
# check if todo is created
self.assertTrue(frappe.db.get_value(comm.reference_doctype, comm.reference_name, "name"))
@@ -49,7 +60,7 @@ class TestEmailAccount(unittest.TestCase):
comm = frappe.get_doc("Communication", {"sender": "test_sender@example.com"})
comm.db_set("creation", datetime.now() - timedelta(seconds = 30 * 60))
- frappe.db.sql("DELETE FROM `tabEmail Queue`")
+ frappe.db.delete("Email Queue")
notify_unreplied()
self.assertTrue(frappe.db.get_value("Email Queue", {"reference_doctype": comm.reference_doctype,
"reference_name": comm.reference_name, "status":"Not Sent"}))
@@ -88,7 +99,7 @@ class TestEmailAccount(unittest.TestCase):
email_account.receive(test_mails=test_mails)
comm = frappe.get_doc("Communication", {"sender": "test_sender@example.com"})
- self.assertTrue("From: \"Microsoft Outlook\" <test_sender@example.com>" in comm.content)
+ self.assertTrue("From: "Microsoft Outlook" <test_sender@example.com>" in comm.content)
self.assertTrue("This is an e-mail message sent automatically by Microsoft Outlook while" in comm.content)
def test_incoming_attached_email_from_outlook_layers(self):
@@ -101,7 +112,7 @@ class TestEmailAccount(unittest.TestCase):
email_account.receive(test_mails=test_mails)
comm = frappe.get_doc("Communication", {"sender": "test_sender@example.com"})
- self.assertTrue("From: \"Microsoft Outlook\" <test_sender@example.com>" in comm.content)
+ self.assertTrue("From: "Microsoft Outlook" <test_sender@example.com>" in comm.content)
self.assertTrue("This is an e-mail message sent automatically by Microsoft Outlook while" in comm.content)
def test_outgoing(self):
@@ -166,14 +177,13 @@ class TestEmailAccount(unittest.TestCase):
comm_list = frappe.get_all("Communication", filters={"sender":"test_sender@example.com"},
fields=["name", "reference_doctype", "reference_name"])
-
# both communications attached to the same reference
self.assertEqual(comm_list[0].reference_doctype, comm_list[1].reference_doctype)
self.assertEqual(comm_list[0].reference_name, comm_list[1].reference_name)
def test_threading_by_message_id(self):
cleanup()
- frappe.db.sql("""delete from `tabEmail Queue`""")
+ frappe.db.delete("Email Queue")
# reference document for testing
event = frappe.get_doc(dict(doctype='Event', subject='test-message')).insert()
@@ -199,6 +209,215 @@ class TestEmailAccount(unittest.TestCase):
self.assertEqual(comm_list[0].reference_doctype, event.doctype)
self.assertEqual(comm_list[0].reference_name, event.name)
+ def test_auto_reply(self):
+ cleanup("test_sender@example.com")
+
+ test_mails = [self.get_test_mail('incoming-1.raw')]
+
+ email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
+ email_account.receive(test_mails=test_mails)
+
+ comm = frappe.get_doc("Communication", {"sender": "test_sender@example.com"})
+ self.assertTrue(frappe.db.get_value("Email Queue", {"reference_doctype": comm.reference_doctype,
+ "reference_name": comm.reference_name}))
+
+ def test_handle_bad_emails(self):
+ mail_content = self.get_test_mail(fname="incoming-1.raw")
+ message_id = Email(mail_content).mail.get('Message-ID')
+
+ email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
+ email_account.handle_bad_emails(uid=-1, raw=mail_content, reason="Testing")
+ self.assertTrue(frappe.db.get_value("Unhandled Email", {'message_id': message_id}))
+
+class TestInboundMail(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
+ email_account.db_set("enable_incoming", 1)
+
+ @classmethod
+ def tearDownClass(cls):
+ email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
+ email_account.db_set("enable_incoming", 0)
+
+ def setUp(self):
+ cleanup()
+ frappe.db.delete("Email Queue")
+ frappe.db.delete("ToDo")
+
+ def get_test_mail(self, fname):
+ with open(os.path.join(os.path.dirname(__file__), "test_mails", fname), "r") as f:
+ return f.read()
+
+ def new_doc(self, doctype, **data):
+ doc = frappe.new_doc(doctype)
+ for field, value in data.items():
+ setattr(doc, field, value)
+ doc.insert()
+ return doc
+
+ def new_communication(self, **kwargs):
+ defaults = {
+ 'subject': "Test Subject"
+ }
+ d = {**defaults, **kwargs}
+ return self.new_doc('Communication', **d)
+
+ def new_email_queue(self, **kwargs):
+ defaults = {
+ 'message_id': get_message_id().strip(" <>")
+ }
+ d = {**defaults, **kwargs}
+ return self.new_doc('Email Queue', **d)
+
+ def new_todo(self, **kwargs):
+ defaults = {
+ 'description': "Description"
+ }
+ d = {**defaults, **kwargs}
+ return self.new_doc('ToDo', **d)
+
+ def test_self_sent_mail(self):
+ """Check that we raise SentEmailInInboxError if the inbound mail is self sent mail.
+ """
+ mail_content = self.get_test_mail(fname="incoming-self-sent.raw")
+ email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
+ inbound_mail = InboundMail(mail_content, email_account, 1, 1)
+ with self.assertRaises(SentEmailInInboxError):
+ inbound_mail.process()
+
+ def test_mail_exist_validation(self):
+ """Do not create communication record if the mail is already downloaded into the system.
+ """
+ mail_content = self.get_test_mail(fname="incoming-1.raw")
+ message_id = Email(mail_content).message_id
+ # Create new communication record in DB
+ communication = self.new_communication(message_id=message_id)
+
+ email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
+ inbound_mail = InboundMail(mail_content, email_account, 12345, 1)
+ new_communiction = inbound_mail.process()
+
+ # Make sure that uid is changed to new uid
+ self.assertEqual(new_communiction.uid, 12345)
+ self.assertEqual(communication.name, new_communiction.name)
+
+ def test_find_parent_email_queue(self):
+ """If the mail is reply to the already sent mail, there will be a email queue record.
+ """
+ # Create email queue record
+ queue_record = self.new_email_queue()
+
+ mail_content = self.get_test_mail(fname="reply-4.raw").replace(
+ "{{ message_id }}", queue_record.message_id
+ )
+
+ email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
+ inbound_mail = InboundMail(mail_content, email_account, 12345, 1)
+ parent_queue = inbound_mail.parent_email_queue()
+ self.assertEqual(queue_record.name, parent_queue.name)
+
+ def test_find_parent_communication_through_queue(self):
+ """Find parent communication of an inbound mail.
+ Cases where parent communication does exist:
+ 1. No parent communication is the mail is not a reply.
+
+ Cases where parent communication does not exist:
+ 2. If mail is not a reply to system sent mail, then there can exist co
+ """
+ # Create email queue record
+ communication = self.new_communication()
+ queue_record = self.new_email_queue(communication=communication.name)
+ mail_content = self.get_test_mail(fname="reply-4.raw").replace(
+ "{{ message_id }}", queue_record.message_id
+ )
+
+ email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
+ inbound_mail = InboundMail(mail_content, email_account, 12345, 1)
+ parent_communication = inbound_mail.parent_communication()
+ self.assertEqual(parent_communication.name, communication.name)
+
+ def test_find_parent_communication_for_self_reply(self):
+ """If the inbound email is a reply but not reply to system sent mail.
+
+ Ex: User replied to his/her mail.
+ """
+ message_id = "new-message-id"
+ mail_content = self.get_test_mail(fname="reply-4.raw").replace(
+ "{{ message_id }}", message_id
+ )
+
+ email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
+ inbound_mail = InboundMail(mail_content, email_account, 12345, 1)
+ parent_communication = inbound_mail.parent_communication()
+ self.assertFalse(parent_communication)
+
+ communication = self.new_communication(message_id=message_id)
+ inbound_mail = InboundMail(mail_content, email_account, 12345, 1)
+ parent_communication = inbound_mail.parent_communication()
+ self.assertEqual(parent_communication.name, communication.name)
+
+ def test_find_parent_communication_from_header(self):
+ """Incase of header contains parent communication name
+ """
+ communication = self.new_communication()
+ mail_content = self.get_test_mail(fname="reply-4.raw").replace(
+ "{{ message_id }}", f"<{communication.name}@{frappe.local.site}>"
+ )
+
+ email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
+ inbound_mail = InboundMail(mail_content, email_account, 12345, 1)
+ parent_communication = inbound_mail.parent_communication()
+ self.assertEqual(parent_communication.name, communication.name)
+
+ def test_reference_document(self):
+ # Create email queue record
+ todo = self.new_todo()
+ # communication = self.new_communication(reference_doctype='ToDo', reference_name=todo.name)
+ queue_record = self.new_email_queue(reference_doctype='ToDo', reference_name=todo.name)
+ mail_content = self.get_test_mail(fname="reply-4.raw").replace(
+ "{{ message_id }}", queue_record.message_id
+ )
+
+ email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
+ inbound_mail = InboundMail(mail_content, email_account, 12345, 1)
+ reference_doc = inbound_mail.reference_document()
+ self.assertEqual(todo.name, reference_doc.name)
+
+ def test_reference_document_by_record_name_in_subject(self):
+ # Create email queue record
+ todo = self.new_todo()
+
+ mail_content = self.get_test_mail(fname="incoming-subject-placeholder.raw").replace(
+ "{{ subject }}", f"RE: (#{todo.name})"
+ )
+
+ email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
+ inbound_mail = InboundMail(mail_content, email_account, 12345, 1)
+ reference_doc = inbound_mail.reference_document()
+ self.assertEqual(todo.name, reference_doc.name)
+
+ def test_reference_document_by_subject_match(self):
+ subject = "New todo"
+ todo = self.new_todo(sender='test_sender@example.com', description=subject)
+
+ mail_content = self.get_test_mail(fname="incoming-subject-placeholder.raw").replace(
+ "{{ subject }}", f"RE: {subject}"
+ )
+ email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
+ inbound_mail = InboundMail(mail_content, email_account, 12345, 1)
+ reference_doc = inbound_mail.reference_document()
+ self.assertEqual(todo.name, reference_doc.name)
+
+ def test_create_communication_from_mail(self):
+ # Create email queue record
+ mail_content = self.get_test_mail(fname="incoming-2.raw")
+ email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
+ inbound_mail = InboundMail(mail_content, email_account, 12345, 1)
+ communication = inbound_mail.process()
+ self.assertTrue(communication.is_first)
+ self.assertTrue(communication._attachments)
+
def cleanup(sender=None):
filters = {}
if sender:
@@ -207,4 +426,4 @@ def cleanup(sender=None):
names = frappe.get_list("Communication", filters=filters, fields=["name"])
for name in names:
frappe.delete_doc_if_exists("Communication", name.name)
- frappe.delete_doc_if_exists("Communication Link", {"parent": name.name})
\ No newline at end of file
+ frappe.delete_doc_if_exists("Communication Link", {"parent": name.name})
diff --git a/frappe/email/doctype/email_account/test_mails/incoming-self-sent.raw b/frappe/email/doctype/email_account/test_mails/incoming-self-sent.raw
new file mode 100644
index 0000000000..a16eecccd5
--- /dev/null
+++ b/frappe/email/doctype/email_account/test_mails/incoming-self-sent.raw
@@ -0,0 +1,91 @@
+Delivered-To: test_receiver@example.com
+Received: by 10.96.153.227 with SMTP id vj3csp416144qdb;
+ Mon, 15 Sep 2014 03:35:07 -0700 (PDT)
+X-Received: by 10.66.119.103 with SMTP id kt7mr36981968pab.95.1410777306321;
+ Mon, 15 Sep 2014 03:35:06 -0700 (PDT)
+Return-Path:
+Received: from mail-pa0-x230.google.com (mail-pa0-x230.google.com [2607:f8b0:400e:c03::230])
+ by mx.google.com with ESMTPS id dg10si22178346pdb.115.2014.09.15.03.35.06
+ for
+ (version=TLSv1 cipher=ECDHE-RSA-RC4-SHA bits=128/128);
+ Mon, 15 Sep 2014 03:35:06 -0700 (PDT)
+Received-SPF: pass (google.com: domain of test@example.com designates 2607:f8b0:400e:c03::230 as permitted sender) client-ip=2607:f8b0:400e:c03::230;
+Authentication-Results: mx.google.com;
+ spf=pass (google.com: domain of test@example.com designates 2607:f8b0:400e:c03::230 as permitted sender) smtp.mail=test@example.com;
+ dkim=pass header.i=@gmail.com;
+ dmarc=pass (p=NONE dis=NONE) header.from=gmail.com
+Received: by mail-pa0-f48.google.com with SMTP id hz1so6118714pad.21
+ for ; Mon, 15 Sep 2014 03:35:06 -0700 (PDT)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+ d=gmail.com; s=20120113;
+ h=from:content-type:subject:message-id:date:to:mime-version;
+ bh=rwiLijtF3lfy9M6cP/7dv2Hm7NJuBwFZn1OFsN8Tlvs=;
+ b=x7U4Ny3Kz2ULRJ7a04NDBrBTVhP2ImIB9n3LVNGQDnDonPUM5Ro/wZcxPTVnBWZ2L1
+ o1bGfP+lhBrvYUlHsd5r4FYC0Uvpad6hbzLr0DGUQgPTxW4cGKbtDEAq+BR2JWd9f803
+ vdjSWdGk8w2dt2qbngTqIZkm5U2XWjICDOAYuPIseLUgCFwi9lLyOSARFB7mjAa2YL7Q
+ Nswk7mbWU1hbnHP6jaBb0m8QanTc7Up944HpNDRxIrB1ZHgKzYhXtx8nhnOx588ZGIAe
+ E6tyG8IwogR11vLkkrBhtMaOme9PohYx4F1CSTiwspmDCadEzJFGRe//lEXKmZHAYH6g
+ 90Zg==
+X-Received: by 10.70.38.135 with SMTP id g7mr22078275pdk.100.1410777305744;
+ Mon, 15 Sep 2014 03:35:05 -0700 (PDT)
+Return-Path:
+Received: from [192.168.0.100] ([27.106.4.70])
+ by mx.google.com with ESMTPSA id zr6sm11025126pbc.50.2014.09.15.03.35.02
+ for
+ (version=TLSv1 cipher=ECDHE-RSA-RC4-SHA bits=128/128);
+ Mon, 15 Sep 2014 03:35:04 -0700 (PDT)
+From: Rushabh Mehta
+Content-Type: multipart/alternative; boundary="Apple-Mail=_57F71261-5C3A-43F6-918B-4438B96F61AA"
+Subject: test mail 🦄🌈😎
+Message-Id: <9143999C-8456-4399-9CF1-4A2DA9DD7711@gmail.com>
+Date: Mon, 15 Sep 2014 16:04:57 +0530
+To: Rushabh Mehta
+Mime-Version: 1.0 (Mac OS X Mail 7.3 \(1878.6\))
+X-Mailer: Apple Mail (2.1878.6)
+
+
+--Apple-Mail=_57F71261-5C3A-43F6-918B-4438B96F61AA
+Content-Transfer-Encoding: 7bit
+Content-Type: text/plain;
+ charset=us-ascii
+
+test mail
+
+
+
+@rushabh_mehta
+https://erpnext.org
+
+
+--Apple-Mail=_57F71261-5C3A-43F6-918B-4438B96F61AA
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/html;
+ charset=us-ascii
+
+test =
+mail
+
=
+
+--Apple-Mail=_57F71261-5C3A-43F6-918B-4438B96F61AA--
diff --git a/frappe/email/doctype/email_account/test_mails/incoming-subject-placeholder.raw b/frappe/email/doctype/email_account/test_mails/incoming-subject-placeholder.raw
new file mode 100644
index 0000000000..35ddf06b01
--- /dev/null
+++ b/frappe/email/doctype/email_account/test_mails/incoming-subject-placeholder.raw
@@ -0,0 +1,183 @@
+Return-path:
+Envelope-to: test_receiver@example.com
+Delivery-date: Wed, 27 Jan 2016 16:24:20 +0800
+Received: from 23-59-23-10.perm.iinet.net.au ([23.59.23.10]:62191 helo=DESKTOP7C66I2M)
+ by webcloud85.au.syrahost.com with esmtp (Exim 4.86)
+ (envelope-from )
+ id 1aOLOj-002xFL-CP
+ for test_receiver@example.com; Wed, 27 Jan 2016 16:24:20 +0800
+From:
+To:
+References:
+In-Reply-To:
+Subject: RE: {{ subject }}
+Date: Wed, 27 Jan 2016 16:24:09 +0800
+Message-ID: <000001d158dc$1b8363a0$528a2ae0$@example.com>
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="----=_NextPart_000_0001_01D1591F.29A7DC20"
+X-Mailer: Microsoft Outlook 14.0
+Thread-Index: AQJZfZxrgcB9KnMqoZ+S4Qq9hcoSeZ3+vGiQ
+Content-Language: en-au
+
+This is a multipart message in MIME format.
+
+------=_NextPart_000_0001_01D1591F.29A7DC20
+Content-Type: multipart/alternative;
+ boundary="----=_NextPart_001_0002_01D1591F.29A7DC20"
+
+
+------=_NextPart_001_0002_01D1591F.29A7DC20
+Content-Type: text/plain;
+ charset="utf-8"
+Content-Transfer-Encoding: quoted-printable
+
+Test purely for testing with the debugger has email attached
+
+=20
+
+From: Notification [mailto:test_receiver@example.com]=20
+Sent: Wednesday, 27 January 2016 9:30 AM
+To: test_receiver@example.com
+Subject: Sales Invoice: SINV-12276
+
+=20
+
+test no 6 sent from bench to outlook to be replied to with messaging
+
+
+
+
+------=_NextPart_001_0002_01D1591F.29A7DC20
+Content-Type: text/html;
+ charset="utf-8"
+Content-Transfer-Encoding: quoted-printable
+
+hi thereTest purely for testing with the debugger has email =
+attached
From:=
+ =
+Notification [mailto:test_receiver@example.com]
Sent: Wednesday, 27 =
+January 2016 9:30 AM
To: =
+test_receiver@example.com
Subject: Sales Invoice: =
+SINV-12276
test no 3 sent from bench to outlook to be replied to with =
+messaging
fizz buzz
+------=_NextPart_001_0002_01D1591F.29A7DC20--
+
+------=_NextPart_000_0001_01D1591F.29A7DC20
+Content-Type: message/rfc822
+Content-Transfer-Encoding: 7bit
+Content-Disposition: attachment
+
+Received: from 203-59-223-10.perm.iinet.net.au ([23.59.23.10]:49772 helo=DESKTOP7C66I2M)
+ by webcloud85.au.syrahost.com with esmtpsa (TLSv1.2:DHE-RSA-AES256-GCM-SHA384:256)
+ (Exim 4.86)
+ (envelope-from )
+ id 1aOEtO-003tI4-Kv
+ for test_receiver@example.com; Wed, 27 Jan 2016 09:27:30 +0800
+Return-Path:
+From: "Microsoft Outlook"
+To:
+Subject: Microsoft Outlook Test Message
+MIME-Version: 1.0
+Content-Type: text/plain;
+ charset="utf-8"
+Content-Transfer-Encoding: quoted-printable
+X-Mailer: Microsoft Outlook 14.0
+Thread-Index: AdFYoeN8x8wUI/+QSoCJkp33NKPVmw==
+
+This is an e-mail message sent automatically by Microsoft Outlook while =
+testing the settings for your account.
diff --git a/frappe/email/doctype/email_domain/email_domain.py b/frappe/email/doctype/email_domain/email_domain.py
index ce39523564..0856549eb7 100644
--- a/frappe/email/doctype/email_domain/email_domain.py
+++ b/frappe/email/doctype/email_domain/email_domain.py
@@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
diff --git a/frappe/email/doctype/email_domain/test_email_domain.py b/frappe/email/doctype/email_domain/test_email_domain.py
index 1c5306e9c2..8607151ca8 100644
--- a/frappe/email/doctype/email_domain/test_email_domain.py
+++ b/frappe/email/doctype/email_domain/test_email_domain.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
from frappe.test_runner import make_test_objects
diff --git a/frappe/email/doctype/email_flag_queue/email_flag_queue.py b/frappe/email/doctype/email_flag_queue/email_flag_queue.py
index 487ef7db50..9bb30f08b2 100644
--- a/frappe/email/doctype/email_flag_queue/email_flag_queue.py
+++ b/frappe/email/doctype/email_flag_queue/email_flag_queue.py
@@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
diff --git a/frappe/email/doctype/email_flag_queue/test_email_flag_queue.py b/frappe/email/doctype/email_flag_queue/test_email_flag_queue.py
index 644a2a8ff7..d09b823ce6 100644
--- a/frappe/email/doctype/email_flag_queue/test_email_flag_queue.py
+++ b/frappe/email/doctype/email_flag_queue/test_email_flag_queue.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
diff --git a/frappe/email/doctype/email_group/email_group.json b/frappe/email/doctype/email_group/email_group.json
index c49de841e6..cb74249143 100644
--- a/frappe/email/doctype/email_group/email_group.json
+++ b/frappe/email/doctype/email_group/email_group.json
@@ -1,6 +1,7 @@
{
"actions": [],
"allow_import": 1,
+ "allow_rename": 1,
"autoname": "field:title",
"creation": "2015-03-18 06:08:32.729800",
"doctype": "DocType",
@@ -50,7 +51,7 @@
"link_fieldname": "email_group"
}
],
- "modified": "2020-09-24 16:41:55.286377",
+ "modified": "2021-06-15 11:25:13.556201",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Group",
diff --git a/frappe/email/doctype/email_group/email_group.py b/frappe/email/doctype/email_group/email_group.py
index b19a134713..2679353edf 100755
--- a/frappe/email/doctype/email_group/email_group.py
+++ b/frappe/email/doctype/email_group/email_group.py
@@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import validate_email_address
@@ -105,6 +104,6 @@ def send_welcome_email(welcome_email, email, email_group):
email=email,
email_group=email_group
)
-
- message = frappe.render_template(welcome_email.response, args)
+ email_message = welcome_email.response or welcome_email.response_html
+ message = frappe.render_template(email_message, args)
frappe.sendmail(email, subject=welcome_email.subject, message=message)
diff --git a/frappe/email/doctype/email_group/test_email_group.py b/frappe/email/doctype/email_group/test_email_group.py
index 09f4f4c32c..3e894118df 100644
--- a/frappe/email/doctype/email_group/test_email_group.py
+++ b/frappe/email/doctype/email_group/test_email_group.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
diff --git a/frappe/email/doctype/email_group_member/email_group_member.py b/frappe/email/doctype/email_group_member/email_group_member.py
index 23b279e755..1f9303b83e 100644
--- a/frappe/email/doctype/email_group_member/email_group_member.py
+++ b/frappe/email/doctype/email_group_member/email_group_member.py
@@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
diff --git a/frappe/email/doctype/email_group_member/test_email_group_member.py b/frappe/email/doctype/email_group_member/test_email_group_member.py
index 35259617c1..829d686400 100644
--- a/frappe/email/doctype/email_group_member/test_email_group_member.py
+++ b/frappe/email/doctype/email_group_member/test_email_group_member.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py
index 9e2fe32250..e1e332f978 100644
--- a/frappe/email/doctype/email_queue/email_queue.py
+++ b/frappe/email/doctype/email_queue/email_queue.py
@@ -9,14 +9,18 @@ from rq.timeouts import JobTimeoutException
import smtplib
import quopri
from email.parser import Parser
+from email.policy import SMTPUTF8
+from html2text import html2text
+from six.moves import html_parser as HTMLParser
import frappe
from frappe import _, safe_encode, task
from frappe.model.document import Document
-from frappe.email.queue import get_unsubcribed_url
-from frappe.email.email_body import add_attachment
-from frappe.utils import cint
-from email.policy import SMTPUTF8
+from frappe.email.queue import get_unsubcribed_url, get_unsubscribe_message
+from frappe.email.email_body import add_attachment, get_formatted_html, get_email
+from frappe.utils import cint, split_emails, add_days, nowdate, cstr
+from frappe.email.doctype.email_account.email_account import EmailAccount
+
MAX_RETRY_COUNT = 3
class EmailQueue(Document):
@@ -41,10 +45,28 @@ class EmailQueue(Document):
duplicate.set_recipients(recipients)
return duplicate
+ @classmethod
+ def new(cls, doc_data, ignore_permissions=False):
+ data = doc_data.copy()
+ if not data.get('recipients'):
+ return
+
+ recipients = data.pop('recipients')
+ doc = frappe.new_doc(cls.DOCTYPE)
+ doc.update(data)
+ doc.set_recipients(recipients)
+ doc.insert(ignore_permissions=ignore_permissions)
+ return doc
+
@classmethod
def find(cls, name):
return frappe.get_doc(cls.DOCTYPE, name)
+ @classmethod
+ def find_one_by_filters(cls, **kwargs):
+ name = frappe.db.get_value(cls.DOCTYPE, kwargs)
+ return cls.find(name) if name else None
+
def update_db(self, commit=False, **kwargs):
frappe.db.set_value(self.DOCTYPE, self.name, kwargs)
if commit:
@@ -69,8 +91,6 @@ class EmailQueue(Document):
return json.loads(self.attachments) if self.attachments else []
def get_email_account(self):
- from frappe.email.doctype.email_account.email_account import EmailAccount
-
if self.email_account:
return frappe.get_doc('Email Account', self.email_account)
@@ -159,7 +179,14 @@ class SendMailContext:
else:
email_status = self.is_mail_sent_to_all() and 'Sent'
email_status = email_status or (self.sent_to and 'Partially Sent') or 'Not Sent'
- self.queue_doc.update_status(status = email_status, commit = True)
+
+ update_fields = {'status': email_status}
+ if self.email_account_doc.is_exists_in_db():
+ update_fields['email_account'] = self.email_account_doc.name
+ else:
+ update_fields['email_account'] = None
+
+ self.queue_doc.update_status(**update_fields, commit = True)
def log_exception(self, exc_type, exc_val, exc_tb):
if exc_type:
@@ -295,3 +322,283 @@ def send_now(name):
def on_doctype_update():
"""Add index in `tabCommunication` for `(reference_doctype, reference_name)`"""
frappe.db.add_index('Email Queue', ('status', 'send_after', 'priority', 'creation'), 'index_bulk_flush')
+
+class QueueBuilder:
+ """Builds Email Queue from the given data
+ """
+ def __init__(self, recipients=None, sender=None, subject=None, message=None,
+ text_content=None, reference_doctype=None, reference_name=None,
+ unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None,
+ attachments=None, reply_to=None, cc=None, bcc=None, message_id=None, in_reply_to=None,
+ send_after=None, expose_recipients=None, send_priority=1, communication=None,
+ read_receipt=None, queue_separately=False, is_notification=False,
+ add_unsubscribe_link=1, inline_images=None, header=None,
+ print_letterhead=False, with_container=False):
+ """Add email to sending queue (Email Queue)
+
+ :param recipients: List of recipients.
+ :param sender: Email sender.
+ :param subject: Email subject.
+ :param message: Email message.
+ :param text_content: Text version of email message.
+ :param reference_doctype: Reference DocType of caller document.
+ :param reference_name: Reference name of caller document.
+ :param send_priority: Priority for Email Queue, default 1.
+ :param unsubscribe_method: URL method for unsubscribe. Default is `/api/method/frappe.email.queue.unsubscribe`.
+ :param unsubscribe_params: additional params for unsubscribed links. default are name, doctype, email
+ :param attachments: Attachments to be sent.
+ :param reply_to: Reply to be captured here (default inbox)
+ :param in_reply_to: Used to send the Message-Id of a received email back as In-Reply-To.
+ :param send_after: Send this email after the given datetime. If value is in integer, then `send_after` will be the automatically set to no of days from current date.
+ :param communication: Communication link to be set in Email Queue record
+ :param queue_separately: Queue each email separately
+ :param is_notification: Marks email as notification so will not trigger notifications from system
+ :param add_unsubscribe_link: Send unsubscribe link in the footer of the Email, default 1.
+ :param inline_images: List of inline images as {"filename", "filecontent"}. All src properties will be replaced with random Content-Id
+ :param header: Append header in email (boolean)
+ :param with_container: Wraps email inside styled container
+ """
+
+ self._unsubscribe_method = unsubscribe_method
+ self._recipients = recipients
+ self._cc = cc
+ self._bcc = bcc
+ self._send_after = send_after
+ self._sender = sender
+ self._text_content = text_content
+ self._message = message
+ self._add_unsubscribe_link = add_unsubscribe_link
+ self._unsubscribe_message = unsubscribe_message
+ self._attachments = attachments
+
+ self._unsubscribed_user_emails = None
+ self._email_account = None
+
+ self.unsubscribe_params = unsubscribe_params
+ self.subject = subject
+ self.reference_doctype = reference_doctype
+ self.reference_name = reference_name
+ self.expose_recipients = expose_recipients
+ self.with_container = with_container
+ self.header = header
+ self.reply_to = reply_to
+ self.message_id = message_id
+ self.in_reply_to = in_reply_to
+ self.send_priority = send_priority
+ self.communication = communication
+ self.read_receipt = read_receipt
+ self.queue_separately = queue_separately
+ self.is_notification = is_notification
+ self.inline_images = inline_images
+ self.print_letterhead = print_letterhead
+
+ @property
+ def unsubscribe_method(self):
+ return self._unsubscribe_method or '/api/method/frappe.email.queue.unsubscribe'
+
+ def _get_emails_list(self, emails=None):
+ emails = split_emails(emails) if isinstance(emails, str) else (emails or [])
+ return [each for each in set(emails) if each]
+
+ @property
+ def recipients(self):
+ return self._get_emails_list(self._recipients)
+
+ @property
+ def cc(self):
+ return self._get_emails_list(self._cc)
+
+ @property
+ def bcc(self):
+ return self._get_emails_list(self._bcc)
+
+ @property
+ def send_after(self):
+ if isinstance(self._send_after, int):
+ return add_days(nowdate(), self._send_after)
+ return self._send_after
+
+ @property
+ def sender(self):
+ if not self._sender or self._sender == "Administrator":
+ email_account = self.get_outgoing_email_account()
+ return email_account.default_sender
+ return self._sender
+
+ def email_text_content(self):
+ unsubscribe_msg = self.unsubscribe_message()
+ unsubscribe_text_message = (unsubscribe_msg and unsubscribe_msg.text) or ''
+
+ if self._text_content:
+ return self._text_content + unsubscribe_text_message
+
+ try:
+ text_content = html2text(self._message)
+ except HTMLParser.HTMLParseError:
+ text_content = "See html attachment"
+ return text_content + unsubscribe_text_message
+
+ def email_html_content(self):
+ email_account = self.get_outgoing_email_account()
+ return get_formatted_html(self.subject, self._message, header=self.header,
+ email_account=email_account, unsubscribe_link=self.unsubscribe_message(),
+ with_container=self.with_container)
+
+ def should_include_unsubscribe_link(self):
+ return (self._add_unsubscribe_link == 1
+ and self.reference_doctype
+ and (self._unsubscribe_message or self.reference_doctype=="Newsletter"))
+
+ def unsubscribe_message(self):
+ if self.should_include_unsubscribe_link():
+ return get_unsubscribe_message(self._unsubscribe_message, self.expose_recipients)
+
+ def get_outgoing_email_account(self):
+ if self._email_account:
+ return self._email_account
+
+ self._email_account = EmailAccount.find_outgoing(
+ match_by_doctype=self.reference_doctype, match_by_email=self._sender, _raise_error=True)
+ return self._email_account
+
+ def get_unsubscribed_user_emails(self):
+ if self._unsubscribed_user_emails is not None:
+ return self._unsubscribed_user_emails
+
+ all_ids = tuple(set(self.recipients + self.cc))
+
+ unsubscribed = frappe.db.sql_list('''
+ SELECT
+ distinct email
+ from
+ `tabEmail Unsubscribe`
+ where
+ email in %(all_ids)s
+ and (
+ (
+ reference_doctype = %(reference_doctype)s
+ and reference_name = %(reference_name)s
+ )
+ or global_unsubscribe = 1
+ )
+ ''', {
+ 'all_ids': all_ids,
+ 'reference_doctype': self.reference_doctype,
+ 'reference_name': self.reference_name,
+ })
+
+ self._unsubscribed_user_emails = unsubscribed or []
+ return self._unsubscribed_user_emails
+
+ def final_recipients(self):
+ unsubscribed_emails = self.get_unsubscribed_user_emails()
+ return [mail_id for mail_id in self.recipients if mail_id not in unsubscribed_emails]
+
+ def final_cc(self):
+ unsubscribed_emails = self.get_unsubscribed_user_emails()
+ return [mail_id for mail_id in self.cc if mail_id not in unsubscribed_emails]
+
+ def get_attachments(self):
+ attachments = []
+ if self._attachments:
+ # store attachments with fid or print format details, to be attached on-demand later
+ for att in self._attachments:
+ if att.get('fid'):
+ attachments.append(att)
+ elif att.get("print_format_attachment") == 1:
+ if not att.get('lang', None):
+ att['lang'] = frappe.local.lang
+ att['print_letterhead'] = self.print_letterhead
+ attachments.append(att)
+ return attachments
+
+ def prepare_email_content(self):
+ mail = get_email(recipients=self.final_recipients(),
+ sender=self.sender,
+ subject=self.subject,
+ formatted=self.email_html_content(),
+ text_content=self.email_text_content(),
+ attachments=self._attachments,
+ reply_to=self.reply_to,
+ cc=self.final_cc(),
+ bcc=self.bcc,
+ email_account=self.get_outgoing_email_account(),
+ expose_recipients=self.expose_recipients,
+ inline_images=self.inline_images,
+ header=self.header)
+
+ mail.set_message_id(self.message_id, self.is_notification)
+ if self.read_receipt:
+ mail.msg_root["Disposition-Notification-To"] = self.sender
+ if self.in_reply_to:
+ mail.set_in_reply_to(self.in_reply_to)
+ return mail
+
+ def process(self, send_now=False):
+ """Build and return the email queues those are created.
+
+ Sends email incase if it is requested to send now.
+ """
+ final_recipients = self.final_recipients()
+ queue_separately = (final_recipients and self.queue_separately) or len(final_recipients) > 20
+ if not (final_recipients + self.final_cc()):
+ return []
+
+ email_queues = []
+ queue_data = self.as_dict(include_recipients=False)
+ if not queue_data:
+ return []
+
+ if not queue_separately:
+ recipients = list(set(final_recipients + self.final_cc() + self.bcc))
+ q = EmailQueue.new({**queue_data, **{'recipients': recipients}}, ignore_permissions=True)
+ email_queues.append(q)
+ else:
+ for r in final_recipients:
+ recipients = [r] if email_queues else list(set([r] + self.final_cc() + self.bcc))
+ q = EmailQueue.new({**queue_data, **{'recipients': recipients}}, ignore_permissions=True)
+ email_queues.append(q)
+
+ if send_now:
+ for doc in email_queues:
+ doc.send()
+ return email_queues
+
+ def as_dict(self, include_recipients=True):
+ email_account = self.get_outgoing_email_account()
+ email_account_name = email_account and email_account.is_exists_in_db() and email_account.name
+
+ mail = self.prepare_email_content()
+ try:
+ mail_to_string = cstr(mail.as_string())
+ except frappe.InvalidEmailAddressError:
+ # bad Email Address - don't add to queue
+ frappe.log_error('Invalid Email ID Sender: {0}, Recipients: {1}, \nTraceback: {2} '
+ .format(self.sender, ', '.join(self.final_recipients()), traceback.format_exc()),
+ 'Email Not Sent'
+ )
+ return
+
+ d = {
+ 'priority': self.send_priority,
+ 'attachments': json.dumps(self.get_attachments()),
+ 'message_id': mail.msg_root["Message-Id"].strip(" <>"),
+ 'message': mail_to_string,
+ 'sender': self.sender,
+ 'reference_doctype': self.reference_doctype,
+ 'reference_name': self.reference_name,
+ 'add_unsubscribe_link': self._add_unsubscribe_link,
+ 'unsubscribe_method': self.unsubscribe_method,
+ 'unsubscribe_params': self.unsubscribe_params,
+ 'expose_recipients': self.expose_recipients,
+ 'communication': self.communication,
+ 'send_after': self.send_after,
+ 'show_as_cc': ",".join(self.final_cc()),
+ 'show_as_bcc': ','.join(self.bcc),
+ 'email_account': email_account_name or None
+ }
+
+ if include_recipients:
+ d['recipients'] = self.final_recipients()
+
+ return d
diff --git a/frappe/email/doctype/email_queue/test_email_queue.py b/frappe/email/doctype/email_queue/test_email_queue.py
index 7cd79f9259..b76d6347b9 100644
--- a/frappe/email/doctype/email_queue/test_email_queue.py
+++ b/frappe/email/doctype/email_queue/test_email_queue.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
diff --git a/frappe/email/doctype/email_queue_recipient/email_queue_recipient.py b/frappe/email/doctype/email_queue_recipient/email_queue_recipient.py
index 3f07ec58f3..055bdb3fc1 100644
--- a/frappe/email/doctype/email_queue_recipient/email_queue_recipient.py
+++ b/frappe/email/doctype/email_queue_recipient/email_queue_recipient.py
@@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
diff --git a/frappe/email/doctype/email_rule/email_rule.py b/frappe/email/doctype/email_rule/email_rule.py
index 220798bbdc..9807724ef1 100644
--- a/frappe/email/doctype/email_rule/email_rule.py
+++ b/frappe/email/doctype/email_rule/email_rule.py
@@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
diff --git a/frappe/email/doctype/email_rule/test_email_rule.py b/frappe/email/doctype/email_rule/test_email_rule.py
index 3c7f9c83e6..b2213f7405 100644
--- a/frappe/email/doctype/email_rule/test_email_rule.py
+++ b/frappe/email/doctype/email_rule/test_email_rule.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
diff --git a/frappe/email/doctype/email_template/email_template.py b/frappe/email/doctype/email_template/email_template.py
index 6708e9dd3f..4711451fd2 100644
--- a/frappe/email/doctype/email_template/email_template.py
+++ b/frappe/email/doctype/email_template/email_template.py
@@ -1,11 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe, json
from frappe.model.document import Document
from frappe.utils.jinja import validate_template
-from six import string_types
class EmailTemplate(Document):
def validate(self):
@@ -24,7 +22,7 @@ class EmailTemplate(Document):
return frappe.render_template(self.response, doc)
def get_formatted_email(self, doc):
- if isinstance(doc, string_types):
+ if isinstance(doc, str):
doc = json.loads(doc)
return {
@@ -36,7 +34,7 @@ class EmailTemplate(Document):
@frappe.whitelist()
def get_email_template(template_name, doc):
'''Returns the processed HTML of a email template with the given doc'''
- if isinstance(doc, string_types):
+ if isinstance(doc, str):
doc = json.loads(doc)
email_template = frappe.get_doc("Email Template", template_name)
diff --git a/frappe/email/doctype/email_template/test_email_template.py b/frappe/email/doctype/email_template/test_email_template.py
index a48ce94ac5..5a9ee969c6 100644
--- a/frappe/email/doctype/email_template/test_email_template.py
+++ b/frappe/email/doctype/email_template/test_email_template.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import unittest
class TestEmailTemplate(unittest.TestCase):
diff --git a/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py b/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py
index e532e2b7eb..6c47d8c538 100644
--- a/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py
+++ b/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py
@@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe import _
diff --git a/frappe/email/doctype/email_unsubscribe/test_email_unsubscribe.py b/frappe/email/doctype/email_unsubscribe/test_email_unsubscribe.py
index ea84253ab6..602840fe3b 100644
--- a/frappe/email/doctype/email_unsubscribe/test_email_unsubscribe.py
+++ b/frappe/email/doctype/email_unsubscribe/test_email_unsubscribe.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
diff --git a/frappe/email/doctype/newsletter/exceptions.py b/frappe/email/doctype/newsletter/exceptions.py
new file mode 100644
index 0000000000..a6c688dbe8
--- /dev/null
+++ b/frappe/email/doctype/newsletter/exceptions.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See LICENSE
+
+from frappe.exceptions import ValidationError
+
+class NewsletterAlreadySentError(ValidationError):
+ pass
+
+class NoRecipientFoundError(ValidationError):
+ pass
+
+class NewsletterNotSavedError(ValidationError):
+ pass
diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py
index 6412338e96..a118240488 100755
--- a/frappe/email/doctype/newsletter/newsletter.py
+++ b/frappe/email/doctype/newsletter/newsletter.py
@@ -1,244 +1,323 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See LICENSE
-from __future__ import unicode_literals
+from typing import Dict, List
import frappe
import frappe.utils
-from frappe import throw, _
+
+from frappe import _
from frappe.website.website_generator import WebsiteGenerator
from frappe.utils.verified_command import get_signed_params, verify_request
-from frappe.email.queue import send
from frappe.email.doctype.email_group.email_group import add_subscribers
-from frappe.utils import parse_addr, now_datetime, markdown, validate_email_address
+
+from .exceptions import NewsletterAlreadySentError, NoRecipientFoundError, NewsletterNotSavedError
+
class Newsletter(WebsiteGenerator):
def onload(self):
- if self.email_sent:
- self.get("__onload").status_count = dict(frappe.db.sql("""select status, count(name)
- from `tabEmail Queue` where reference_doctype=%s and reference_name=%s
- group by status""", (self.doctype, self.name))) or None
+ self.setup_newsletter_status()
def validate(self):
- self.route = "newsletters/" + self.name
- if self.send_from:
- validate_email_address(self.send_from, True)
+ self.route = f"newsletters/{self.name}"
+ self.validate_sender_address()
+ self.validate_recipient_address()
+
+ @property
+ def newsletter_recipients(self) -> List[str]:
+ if getattr(self, "_recipients", None) is None:
+ self._recipients = self.get_recipients()
+ return self._recipients
@frappe.whitelist()
- def test_send(self, doctype="Lead"):
- self.recipients = frappe.utils.split_emails(self.test_email_id)
- self.queue_all(test_email=True)
+ def test_send(self):
+ test_emails = frappe.utils.split_emails(self.test_email_id)
+ self.queue_all(test_emails=test_emails)
frappe.msgprint(_("Test email sent to {0}").format(self.test_email_id))
@frappe.whitelist()
def send_emails(self):
"""send emails to leads and customers"""
+ self.queue_all()
+ frappe.msgprint(_("Email queued to {0} recipients").format(len(self.newsletter_recipients)))
+
+ def setup_newsletter_status(self):
+ """Setup analytical status for current Newsletter. Can be accessible from desk.
+ """
if self.email_sent:
- throw(_("Newsletter has already been sent"))
-
- self.recipients = self.get_recipients()
-
- if self.recipients:
- self.queue_all()
- frappe.msgprint(_("Email queued to {0} recipients").format(len(self.recipients)))
-
- else:
- frappe.msgprint(_("Newsletter should have atleast one recipient"))
-
- def queue_all(self, test_email=False):
- if not self.get("recipients"):
- # in case it is called via worker
- self.recipients = self.get_recipients()
-
- self.validate_send()
-
- sender = self.send_from or frappe.utils.get_formatted_email(self.owner)
-
- if not frappe.flags.in_test:
- frappe.db.auto_commit_on_many_writes = True
-
- attachments = []
- if self.send_attachments:
- files = frappe.get_all("File", fields=["name"], filters={"attached_to_doctype": "Newsletter",
- "attached_to_name": self.name}, order_by="creation desc")
-
- for file in files:
- try:
- # these attachments will be attached on-demand
- # and won't be stored in the message
- attachments.append({"fid": file.name})
- except IOError:
- frappe.throw(_("Unable to find attachment {0}").format(file.name))
-
- args = {
- "message": self.get_message(),
- "name": self.name
- }
- frappe.sendmail(recipients=self.recipients, sender=sender,
- subject=self.subject, message=self.get_message(), template="newsletter",
- reference_doctype=self.doctype, reference_name=self.name,
- add_unsubscribe_link=self.send_unsubscribe_link, attachments=attachments,
- unsubscribe_method="/unsubscribe",
- unsubscribe_params={"name": self.name},
- send_priority=0, queue_separately=True, args=args)
-
- if not frappe.flags.in_test:
- frappe.db.auto_commit_on_many_writes = False
-
- if not test_email:
- self.db_set("email_sent", 1)
- self.db_set("schedule_send", now_datetime())
- self.db_set("scheduled_to_send", len(self.recipients))
-
- def get_message(self):
- if self.content_type == "HTML":
- return frappe.render_template(self.message_html, {"doc": self.as_dict()})
- return {
- 'Rich Text': self.message,
- 'Markdown': markdown(self.message_md)
- }[self.content_type or 'Rich Text']
-
- def get_recipients(self):
- """Get recipients from Email Group"""
- recipients_list = []
- for email_group in get_email_groups(self.name):
- for d in frappe.db.get_all("Email Group Member", ["email"],
- {"unsubscribed": 0, "email_group": email_group.email_group}):
- recipients_list.append(d.email)
- return list(set(recipients_list))
+ status_count = frappe.get_all("Email Queue",
+ filters={"reference_doctype": self.doctype, "reference_name": self.name},
+ fields=["status", "count(name)"],
+ group_by="status",
+ order_by="status",
+ as_list=True,
+ )
+ self.get("__onload").status_count = dict(status_count)
def validate_send(self):
- if self.get("__islocal"):
- throw(_("Please save the Newsletter before sending"))
+ """Validate if Newsletter can be sent.
+ """
+ self.validate_newsletter_status()
+ self.validate_newsletter_recipients()
- if not self.recipients:
- frappe.throw(_("Newsletter should have at least one recipient"))
+ def validate_newsletter_status(self):
+ if self.email_sent:
+ frappe.throw(_("Newsletter has already been sent"), exc=NewsletterAlreadySentError)
+
+ if self.get("__islocal"):
+ frappe.throw(_("Please save the Newsletter before sending"), exc=NewsletterNotSavedError)
+
+ def validate_newsletter_recipients(self):
+ if not self.newsletter_recipients:
+ frappe.throw(_("Newsletter should have atleast one recipient"), exc=NoRecipientFoundError)
+ self.validate_recipient_address()
+
+ def validate_sender_address(self):
+ """Validate self.send_from is a valid email address or not.
+ """
+ if self.send_from:
+ frappe.utils.validate_email_address(self.send_from, throw=True)
+
+ def validate_recipient_address(self):
+ """Validate if self.newsletter_recipients are all valid email addresses or not.
+ """
+ for recipient in self.newsletter_recipients:
+ frappe.utils.validate_email_address(recipient, throw=True)
+
+ def get_linked_email_queue(self) -> List[str]:
+ """Get list of email queue linked to this newsletter.
+ """
+ return frappe.get_all("Email Queue",
+ filters={
+ "reference_doctype": self.doctype,
+ "reference_name": self.name,
+ },
+ pluck="name",
+ )
+
+ def get_success_recipients(self) -> List[str]:
+ """Recipients who have already recieved the newsletter.
+
+ Couldn't think of a better name ;)
+ """
+ return frappe.get_all("Email Queue Recipient",
+ filters={
+ "status": ("in", ["Not Sent", "Sending", "Sent"]),
+ "parentfield": ("in", self.get_linked_email_queue()),
+ },
+ pluck="recipient",
+ )
+
+ def get_pending_recipients(self) -> List[str]:
+ """Get list of pending recipients of the newsletter. These
+ recipients may not have receive the newsletter in the previous iteration.
+ """
+ return [
+ x for x in self.newsletter_recipients if x not in self.get_success_recipients()
+ ]
+
+ def queue_all(self, test_emails: List[str] = None):
+ """Queue Newsletter to all the recipients generated from the `Email Group`
+ table
+
+ Args:
+ test_email (List[str], optional): Send test Newsletter to the passed set of emails.
+ Defaults to None.
+ """
+ if test_emails:
+ for test_email in test_emails:
+ frappe.utils.validate_email_address(test_email, throw=True)
+ else:
+ self.validate()
+ self.validate_send()
+
+ newsletter_recipients = test_emails or self.get_pending_recipients()
+ self.send_newsletter(emails=newsletter_recipients)
+
+ if not test_emails:
+ self.email_sent = True
+ self.schedule_send = frappe.utils.now_datetime()
+ self.scheduled_to_send = len(newsletter_recipients)
+ self.save()
+
+ def get_newsletter_attachments(self) -> List[Dict[str, str]]:
+ """Get list of attachments on current Newsletter
+ """
+ attachments = []
+
+ if self.send_attachments:
+ files = frappe.get_all(
+ "File",
+ filters={"attached_to_doctype": "Newsletter", "attached_to_name": self.name},
+ order_by="creation desc",
+ pluck="name",
+ )
+ attachments.extend({"fid": file} for file in files)
+
+ return attachments
+
+ def send_newsletter(self, emails: List[str]):
+ """Trigger email generation for `emails` and add it in Email Queue.
+ """
+ # TODO: get rid of this maybe?
+ message = self.get_message()
+ attachments = self.get_newsletter_attachments()
+ sender = self.send_from or frappe.utils.get_formatted_email(self.owner)
+ args = {"message": message, "name": self.name}
+
+ is_auto_commit_set = bool(frappe.db.auto_commit_on_many_writes)
+ frappe.db.auto_commit_on_many_writes = not frappe.flags.in_test
+
+ frappe.sendmail(
+ subject=self.subject,
+ sender=sender,
+ recipients=emails,
+ message=message,
+ attachments=attachments,
+ template="newsletter",
+ add_unsubscribe_link=self.send_unsubscribe_link,
+ unsubscribe_method="/unsubscribe",
+ unsubscribe_params={"name": self.name},
+ reference_doctype=self.doctype,
+ reference_name=self.name,
+ queue_separately=True,
+ send_priority=0,
+ args=args,
+ )
+
+ frappe.db.auto_commit_on_many_writes = is_auto_commit_set
+
+ def get_message(self) -> str:
+ if self.content_type == "HTML":
+ return frappe.render_template(self.message_html, {"doc": self.as_dict()})
+ if self.content_type == "Markdown":
+ return frappe.utils.markdown(self.message_md)
+ # fallback to Rich Text
+ return self.message
+
+ def get_recipients(self) -> List[str]:
+ """Get recipients from Email Group"""
+ emails = frappe.get_all(
+ "Email Group Member",
+ filters={"unsubscribed": 0, "email_group": ("in", self.get_email_groups())},
+ pluck="email",
+ )
+ return list(set(emails))
+
+ def get_email_groups(self) -> List[str]:
+ # wondering why the 'or'? i can't figure out why both aren't equivalent - @gavin
+ return [
+ x.email_group for x in self.email_group
+ ] or frappe.get_all(
+ "Newsletter Email Group",
+ filters={"parent": self.name, "parenttype": "Newsletter"},
+ pluck="email_group",
+ )
+
+ def get_attachments(self) -> List[Dict[str, str]]:
+ return frappe.get_all(
+ "File",
+ fields=["name", "file_name", "file_url", "is_private"],
+ filters={
+ "attached_to_name": self.name,
+ "attached_to_doctype": "Newsletter",
+ "is_private": 0,
+ },
+ )
def get_context(self, context):
newsletters = get_newsletter_list("Newsletter", None, None, 0)
if newsletters:
newsletter_list = [d.name for d in newsletters]
if self.name not in newsletter_list:
- frappe.redirect_to_message(_('Permission Error'),
- _("You are not permitted to view the newsletter."))
+ frappe.redirect_to_message(
+ _("Permission Error"), _("You are not permitted to view the newsletter.")
+ )
frappe.local.flags.redirect_location = frappe.local.response.location
raise frappe.Redirect
else:
- context.attachments = get_attachments(self.name)
+ context.attachments = self.get_attachments()
context.no_cache = 1
context.show_sidebar = True
-def get_attachments(name):
- return frappe.get_all("File",
- fields=["name", "file_name", "file_url", "is_private"],
- filters = {"attached_to_name": name, "attached_to_doctype": "Newsletter", "is_private":0})
-
-
-def get_email_groups(name):
- return frappe.db.get_all("Newsletter Email Group", ["email_group"],{"parent":name, "parenttype":"Newsletter"})
-
-
@frappe.whitelist(allow_guest=True)
def confirmed_unsubscribe(email, group):
""" unsubscribe the email(user) from the mailing list(email_group) """
- frappe.flags.ignore_permissions=True
+ frappe.flags.ignore_permissions = True
doc = frappe.get_doc("Email Group Member", {"email": email, "email_group": group})
if not doc.unsubscribed:
doc.unsubscribed = 1
- doc.save(ignore_permissions = True)
-
-def create_lead(email_id):
- """create a lead if it does not exist"""
- from frappe.model.naming import get_default_naming_series
- full_name, email_id = parse_addr(email_id)
- if frappe.db.get_value("Lead", {"email_id": email_id}):
- return
-
- lead = frappe.get_doc({
- "doctype": "Lead",
- "email_id": email_id,
- "lead_name": full_name or email_id,
- "status": "Lead",
- "naming_series": get_default_naming_series("Lead"),
- "company": frappe.db.get_default("Company"),
- "source": "Email"
- })
- lead.insert()
+ doc.save(ignore_permissions=True)
@frappe.whitelist(allow_guest=True)
-def subscribe(email, email_group=_('Website')):
- url = frappe.utils.get_url("/api/method/frappe.email.doctype.newsletter.newsletter.confirm_subscription") +\
- "?" + get_signed_params({"email": email, "email_group": email_group})
+def subscribe(email, email_group=_("Website")):
+ """API endpoint to subscribe an email to a particular email group. Triggers a confirmation email.
+ """
- email_template = frappe.db.get_value('Email Group', email_group, ['confirmation_email_template'])
+ # build subscription confirmation URL
+ api_endpoint = frappe.utils.get_url(
+ "/api/method/frappe.email.doctype.newsletter.newsletter.confirm_subscription"
+ )
+ signed_params = get_signed_params({"email": email, "email_group": email_group})
+ confirm_subscription_url = f"{api_endpoint}?{signed_params}"
- content=''
- if email_template:
- args = dict(
- email=email,
- confirmation_url=url,
- email_group=email_group
- )
+ # fetch custom template if available
+ email_confirmation_template = frappe.db.get_value(
+ "Email Group", email_group, "confirmation_email_template"
+ )
- email_template = frappe.get_doc("Email Template", email_template)
+ # build email and send
+ if email_confirmation_template:
+ args = {"email": email, "confirmation_url": confirm_subscription_url, "email_group": email_group}
+ email_template = frappe.get_doc("Email Template", email_confirmation_template)
+ email_subject = email_template.subject
content = frappe.render_template(email_template.response, args)
-
- if not content:
- messages = (
+ else:
+ email_subject = _("Confirm Your Email")
+ translatable_content = (
_("Thank you for your interest in subscribing to our updates"),
_("Please verify your Email Address"),
- url,
- _("Click here to verify")
+ confirm_subscription_url,
+ _("Click here to verify"),
)
-
content = """
- {0}. {1}.
- {3}
- """.format(*messages)
+ {0}. {1}.
+ {3}
+ """.format(*translatable_content)
+
+ frappe.sendmail(
+ email,
+ subject=email_subject,
+ content=content,
+ now=True,
+ )
- frappe.sendmail(email, subject=getattr('email_template', 'subject', '') or _("Confirm Your Email"), content=content, now=True)
@frappe.whitelist(allow_guest=True)
-def confirm_subscription(email, email_group=_('Website')):
+def confirm_subscription(email, email_group=_("Website")):
+ """API endpoint to confirm email subscription.
+ This endpoint is called when user clicks on the link sent to their mail.
+ """
if not verify_request():
return
if not frappe.db.exists("Email Group", email_group):
- frappe.get_doc({
- "doctype": "Email Group",
- "title": email_group
- }).insert(ignore_permissions=True)
+ frappe.get_doc({"doctype": "Email Group", "title": email_group}).insert(
+ ignore_permissions=True
+ )
frappe.flags.ignore_permissions = True
add_subscribers(email_group, email)
frappe.db.commit()
- frappe.respond_as_web_page(_("Confirmed"),
+ frappe.respond_as_web_page(
+ _("Confirmed"),
_("{0} has been successfully added to the Email Group.").format(email),
- indicator_color='green')
-
-
-def send_newsletter(newsletter):
- try:
- doc = frappe.get_doc("Newsletter", newsletter)
- doc.queue_all()
-
- except:
- frappe.db.rollback()
-
- # wasn't able to send emails :(
- doc.db_set("email_sent", 0)
- frappe.db.commit()
-
- frappe.log_error(title='Send Newsletter')
-
- raise
-
- else:
- frappe.db.commit()
+ indicator_color="green",
+ )
def get_list_context(context=None):
@@ -271,12 +350,35 @@ def get_newsletter_list(doctype, txt, filters, limit_start, limit_page_length=20
'''.format(','.join(['%s'] * len(email_group_list)),
limit_page_length, limit_start), email_group_list, as_dict=1)
+
def send_scheduled_email():
"""Send scheduled newsletter to the recipients."""
- scheduled_newsletter = frappe.get_all('Newsletter', filters = {
- 'schedule_send': ('<=', now_datetime()),
- 'email_sent': 0,
- 'schedule_sending': 1
- }, fields = ['name'], ignore_ifnull=True)
+ scheduled_newsletter = frappe.get_all(
+ "Newsletter",
+ filters={
+ "schedule_send": ("<=", frappe.utils.now_datetime()),
+ "email_sent": False,
+ "schedule_sending": True,
+ },
+ ignore_ifnull=True,
+ pluck="name",
+ )
+
for newsletter in scheduled_newsletter:
- send_newsletter(newsletter.name)
+ try:
+ frappe.get_doc("Newsletter", newsletter).queue_all()
+
+ except Exception:
+ frappe.db.rollback()
+
+ # wasn't able to send emails :(
+ frappe.db.set_value("Newsletter", newsletter, "email_sent", 0)
+ message = (
+ f"Newsletter {newsletter} failed to send"
+ "\n\n"
+ f"Traceback: {frappe.get_traceback()}"
+ )
+ frappe.log_error(title="Send Newsletter", message=message)
+
+ if not frappe.flags.in_test:
+ frappe.db.commit()
diff --git a/frappe/email/doctype/newsletter/test_newsletter.py b/frappe/email/doctype/newsletter/test_newsletter.py
index bd8fadc29c..abbcc6440c 100644
--- a/frappe/email/doctype/newsletter/test_newsletter.py
+++ b/frappe/email/doctype/newsletter/test_newsletter.py
@@ -1,19 +1,26 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See LICENSE
import unittest
from random import choice
+from typing import Union
+from unittest.mock import MagicMock, PropertyMock, patch
import frappe
-from frappe.email.doctype.newsletter.newsletter import (
- confirmed_unsubscribe,
- send_scheduled_email,
+from frappe.desk.form.load import run_onload
+from frappe.email.doctype.newsletter.exceptions import (
+ NewsletterAlreadySentError, NoRecipientFoundError
+)
+from frappe.email.doctype.newsletter.newsletter import (
+ Newsletter,
+ confirmed_unsubscribe,
+ get_newsletter_list,
+ send_scheduled_email
)
-from frappe.email.doctype.newsletter.newsletter import get_newsletter_list
from frappe.email.queue import flush
from frappe.utils import add_days, getdate
+
test_dependencies = ["Email Group"]
emails = [
"test_subscriber1@example.com",
@@ -21,30 +28,114 @@ emails = [
"test_subscriber3@example.com",
"test1@example.com",
]
+newsletters = []
-class TestNewsletter(unittest.TestCase):
+def get_dotted_path(obj: type) -> str:
+ klass = obj.__class__
+ module = klass.__module__
+ if module == 'builtins':
+ return klass.__qualname__ # avoid outputs like 'builtins.str'
+ return f"{module}.{klass.__qualname__}"
+
+
+class TestNewsletterMixin:
def setUp(self):
frappe.set_user("Administrator")
- frappe.db.sql("delete from `tabEmail Group Member`")
+ self.setup_email_group()
+ def tearDown(self):
+ frappe.set_user("Administrator")
+ for newsletter in newsletters:
+ frappe.db.delete("Email Queue", {
+ "reference_doctype": "Newsletter",
+ "reference_name": newsletter,
+ })
+ frappe.delete_doc("Newsletter", newsletter)
+ frappe.db.delete("Newsletter Email Group", newsletter)
+ newsletters.remove(newsletter)
+
+ def setup_email_group(self):
if not frappe.db.exists("Email Group", "_Test Email Group"):
- frappe.get_doc({"doctype": "Email Group", "title": "_Test Email Group"}).insert()
-
- for email in emails:
frappe.get_doc({
- "doctype": "Email Group Member",
- "email": email,
- "email_group": "_Test Email Group"
+ "doctype": "Email Group",
+ "title": "_Test Email Group"
}).insert()
+ for email in emails:
+ doctype = "Email Group Member"
+ email_filters = {
+ "email": email,
+ "email_group": "_Test Email Group"
+ }
+ try:
+ frappe.get_doc({
+ "doctype": doctype,
+ **email_filters,
+ }).insert()
+ except Exception:
+ frappe.db.update(doctype, email_filters, "unsubscribed", 0)
+
+ def send_newsletter(self, published=0, schedule_send=None) -> Union[str, None]:
+ frappe.db.delete("Email Queue")
+ frappe.db.delete("Email Queue Recipient")
+ frappe.db.delete("Newsletter")
+
+ newsletter_options = {
+ "published": published,
+ "schedule_sending": bool(schedule_send),
+ "schedule_send": schedule_send
+ }
+ newsletter = self.get_newsletter(**newsletter_options)
+
+ if schedule_send:
+ send_scheduled_email()
+ else:
+ newsletter.send_emails()
+ return newsletter.name
+
+ @staticmethod
+ def get_newsletter(**kwargs) -> "Newsletter":
+ """Generate and return Newsletter object
+ """
+ doctype = "Newsletter"
+ newsletter_content = {
+ "subject": "_Test Newsletter",
+ "send_from": "Test Sender ",
+ "content_type": "Rich Text",
+ "message": "Testing my news.",
+ }
+ similar_newsletters = frappe.db.get_all(doctype, newsletter_content, pluck="name")
+
+ for similar_newsletter in similar_newsletters:
+ frappe.delete_doc(doctype, similar_newsletter)
+
+ newsletter = frappe.get_doc({"doctype": doctype, **newsletter_content, **kwargs})
+ newsletter.append("email_group", {"email_group": "_Test Email Group"})
+ newsletter.save(ignore_permissions=True)
+ newsletter.reload()
+ newsletters.append(newsletter.name)
+
+ attached_files = frappe.get_all("File", {
+ "attached_to_doctype": newsletter.doctype,
+ "attached_to_name": newsletter.name,
+ },
+ pluck="name",
+ )
+ for file in attached_files:
+ frappe.delete_doc("File", file)
+
+ return newsletter
+
+
+class TestNewsletter(TestNewsletterMixin, unittest.TestCase):
def test_send(self):
self.send_newsletter()
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)
- recipients = set([e.recipients[0].recipient for e in email_queue_list])
+ recipients = {e.recipients[0].recipient for e in email_queue_list}
self.assertTrue(set(emails).issubset(recipients))
def test_unsubscribe(self):
@@ -66,40 +157,15 @@ class TestNewsletter(unittest.TestCase):
if email != to_unsubscribe:
self.assertTrue(email in recipients)
- @staticmethod
- def send_newsletter(published=0, schedule_send=None):
- frappe.db.sql("delete from `tabEmail Queue`")
- frappe.db.sql("delete from `tabEmail Queue Recipient`")
- frappe.db.sql("delete from `tabNewsletter`")
- newsletter = frappe.get_doc({
- "doctype": "Newsletter",
- "subject": "_Test Newsletter",
- "send_from": "Test Sender ",
- "content_type": "Rich Text",
- "message": "Testing my news.",
- "published": published,
- "schedule_sending": bool(schedule_send),
- "schedule_send": schedule_send
- }).insert(ignore_permissions=True)
-
- newsletter.append("email_group", {"email_group": "_Test Email Group"})
- newsletter.save()
- if schedule_send:
- send_scheduled_email()
- return
-
- newsletter.send_emails()
- return newsletter.name
-
def test_portal(self):
- self.send_newsletter(1)
+ self.send_newsletter(published=1)
frappe.set_user("test1@example.com")
- newsletters = get_newsletter_list("Newsletter", None, None, 0)
- self.assertEqual(len(newsletters), 1)
+ newsletter_list = get_newsletter_list("Newsletter", None, None, 0)
+ self.assertEqual(len(newsletter_list), 1)
def test_newsletter_context(self):
context = frappe._dict()
- newsletter_name = self.send_newsletter(1)
+ newsletter_name = self.send_newsletter(published=1)
frappe.set_user("test2@example.com")
doc = frappe.get_doc("Newsletter", newsletter_name)
doc.get_context(context)
@@ -114,3 +180,68 @@ class TestNewsletter(unittest.TestCase):
recipients = [e.recipients[0].recipient for e in email_queue_list]
for email in emails:
self.assertTrue(email in recipients)
+
+ def test_newsletter_test_send(self):
+ """Test "Test Send" functionality of Newsletter
+ """
+ newsletter = self.get_newsletter()
+ newsletter.test_email_id = choice(emails)
+ newsletter.test_send()
+
+ self.assertFalse(newsletter.email_sent)
+ newsletter.save = MagicMock()
+ self.assertFalse(newsletter.save.called)
+
+ def test_newsletter_status(self):
+ """Test for Newsletter's stats on onload event
+ """
+ newsletter = self.get_newsletter()
+ newsletter.email_sent = True
+ # had to use run_onload as calling .onload directly bought weird errors
+ # like TestNewsletter has no attribute "_TestNewsletter__onload"
+ run_onload(newsletter)
+ self.assertIsInstance(newsletter.get("__onload").status_count, dict)
+
+ def test_already_sent_newsletter(self):
+ newsletter = self.get_newsletter()
+ newsletter.send_emails()
+
+ with self.assertRaises(NewsletterAlreadySentError):
+ newsletter.send_emails()
+
+ def test_newsletter_with_no_recipient(self):
+ newsletter = self.get_newsletter()
+ property_path = f"{get_dotted_path(newsletter)}.newsletter_recipients"
+
+ with patch(property_path, new_callable=PropertyMock) as mock_newsletter_recipients:
+ mock_newsletter_recipients.return_value = []
+ with self.assertRaises(NoRecipientFoundError):
+ newsletter.send_emails()
+
+ def test_send_newsletter_with_attachments(self):
+ newsletter = self.get_newsletter()
+ newsletter.reload()
+ file_attachment = frappe.get_doc({
+ "doctype": "File",
+ "file_name": "test1.txt",
+ "attached_to_doctype": newsletter.doctype,
+ "attached_to_name": newsletter.name,
+ "content": frappe.mock("paragraph")
+ })
+ file_attachment.save()
+ newsletter.send_attachments = True
+ newsletter_attachments = newsletter.get_newsletter_attachments()
+ self.assertEqual(len(newsletter_attachments), 1)
+ self.assertEqual(newsletter_attachments[0]["fid"], file_attachment.name)
+
+ def test_send_scheduled_email_error_handling(self):
+ newsletter = self.get_newsletter(schedule_send=add_days(getdate(), -1))
+ job_path = "frappe.email.doctype.newsletter.newsletter.Newsletter.queue_all"
+ m = MagicMock(side_effect=frappe.OutgoingEmailError)
+
+ with self.assertRaises(frappe.OutgoingEmailError):
+ with patch(job_path, new_callable=m):
+ send_scheduled_email()
+
+ newsletter.reload()
+ self.assertEqual(newsletter.email_sent, 0)
diff --git a/frappe/email/doctype/newsletter_email_group/newsletter_email_group.py b/frappe/email/doctype/newsletter_email_group/newsletter_email_group.py
index a59ac372fd..a453dda9e4 100644
--- a/frappe/email/doctype/newsletter_email_group/newsletter_email_group.py
+++ b/frappe/email/doctype/newsletter_email_group/newsletter_email_group.py
@@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py
index 2940a34f63..57418515f5 100644
--- a/frappe/email/doctype/notification/notification.py
+++ b/frappe/email/doctype/notification/notification.py
@@ -2,7 +2,6 @@
# Copyright (c) 2018, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
import json, os
from frappe import _
@@ -12,7 +11,6 @@ from frappe.utils import validate_email_address, nowdate, parse_val, is_html, ad
from frappe.utils.jinja import validate_template
from frappe.utils.safe_exec import get_safe_globals
from frappe.modules.utils import export_module_json, get_doc_module
-from six import string_types
from frappe.integrations.doctype.slack_webhook_url.slack_webhook_url import send_slack_message
from frappe.core.doctype.sms_settings.sms_settings import send_sms
from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification
@@ -55,9 +53,7 @@ class Notification(Document):
# py
if not os.path.exists(path + '.py'):
with open(path + '.py', 'w') as f:
- f.write("""from __future__ import unicode_literals
-
-import frappe
+ f.write("""import frappe
def get_context(context):
# do your magic here
@@ -397,7 +393,7 @@ def trigger_notifications(doc, method=None):
def evaluate_alert(doc, alert, event):
from jinja2 import TemplateError
try:
- if isinstance(alert, string_types):
+ if isinstance(alert, str):
alert = frappe.get_doc("Notification", alert)
context = get_context(doc)
diff --git a/frappe/email/doctype/notification/test_notification.py b/frappe/email/doctype/notification/test_notification.py
index 31d5d9d1cc..2629050c1b 100644
--- a/frappe/email/doctype/notification/test_notification.py
+++ b/frappe/email/doctype/notification/test_notification.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe, frappe.utils, frappe.utils.scheduler
from frappe.desk.form import assign_to
import unittest
@@ -11,7 +9,7 @@ test_dependencies = ["User", "Notification"]
class TestNotification(unittest.TestCase):
def setUp(self):
- frappe.db.sql("""delete from `tabEmail Queue`""")
+ frappe.db.delete("Email Queue")
frappe.set_user("test@example.com")
if not frappe.db.exists('Notification', {'name': 'ToDo Status Update'}, 'name'):
@@ -52,7 +50,7 @@ class TestNotification(unittest.TestCase):
self.assertTrue(frappe.db.get_value("Email Queue", {"reference_doctype": "Communication",
"reference_name": communication.name, "status":"Not Sent"}))
- frappe.db.sql("""delete from `tabEmail Queue`""")
+ frappe.db.delete("Email Queue")
communication.reload()
communication.content = "test 2"
@@ -191,9 +189,9 @@ class TestNotification(unittest.TestCase):
def test_cc_jinja(self):
- frappe.db.sql("""delete from `tabUser` where email='test_jinja@example.com'""")
- frappe.db.sql("""delete from `tabEmail Queue`""")
- frappe.db.sql("""delete from `tabEmail Queue Recipient`""")
+ frappe.db.delete("User", {"email": "test_jinja@example.com"})
+ frappe.db.delete("Email Queue")
+ frappe.db.delete("Email Queue Recipient")
test_user = frappe.new_doc("User")
test_user.name = 'test_jinja'
@@ -207,9 +205,9 @@ class TestNotification(unittest.TestCase):
self.assertTrue(frappe.db.get_value("Email Queue Recipient", {"recipient": "test_jinja@example.com"}))
- frappe.db.sql("""delete from `tabUser` where email='test_jinja@example.com'""")
- frappe.db.sql("""delete from `tabEmail Queue`""")
- frappe.db.sql("""delete from `tabEmail Queue Recipient`""")
+ frappe.db.delete("User", {"email": "test_jinja@example.com"})
+ frappe.db.delete("Email Queue")
+ frappe.db.delete("Email Queue Recipient")
def test_notification_to_assignee(self):
todo = frappe.new_doc('ToDo')
diff --git a/frappe/email/doctype/notification_recipient/notification_recipient.py b/frappe/email/doctype/notification_recipient/notification_recipient.py
index a85ed62c04..d8480c5455 100644
--- a/frappe/email/doctype/notification_recipient/notification_recipient.py
+++ b/frappe/email/doctype/notification_recipient/notification_recipient.py
@@ -2,7 +2,6 @@
# Copyright (c) 2018, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
diff --git a/frappe/email/doctype/unhandled_email/test_unhandled_email.py b/frappe/email/doctype/unhandled_email/test_unhandled_email.py
index 6cabcf6ec2..5606b8ff30 100644
--- a/frappe/email/doctype/unhandled_email/test_unhandled_email.py
+++ b/frappe/email/doctype/unhandled_email/test_unhandled_email.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
diff --git a/frappe/email/doctype/unhandled_email/unhandled_email.py b/frappe/email/doctype/unhandled_email/unhandled_email.py
index 1276da71a1..b445c98aa6 100644
--- a/frappe/email/doctype/unhandled_email/unhandled_email.py
+++ b/frappe/email/doctype/unhandled_email/unhandled_email.py
@@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
@@ -11,5 +10,6 @@ class UnhandledEmail(Document):
def remove_old_unhandled_emails():
- frappe.db.sql("""DELETE FROM `tabUnhandled Email`
- WHERE creation < %s""", frappe.utils.add_days(frappe.utils.nowdate(), -30))
+ frappe.db.delete("Unhandled Email", {
+ "creation": ("<", frappe.utils.add_days(frappe.utils.nowdate(), -30))
+ })
\ No newline at end of file
diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py
index 3b03c42b95..ffb44d3412 100755
--- a/frappe/email/email_body.py
+++ b/frappe/email/email_body.py
@@ -1,14 +1,12 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe, re, os
from frappe.utils.pdf import get_pdf
from frappe.email.doctype.email_account.email_account import EmailAccount
from frappe.utils import (get_url, scrub_urls, strip, expand_relative_urls, cint,
split_emails, to_markdown, markdown, random_string, parse_addr)
import email.utils
-from six import iteritems, text_type, string_types
from email.mime.multipart import MIMEMultipart
from email.header import Header
from email import policy
@@ -55,7 +53,7 @@ class EMail:
from email import charset as Charset
Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8')
- if isinstance(recipients, string_types):
+ if isinstance(recipients, str):
recipients = recipients.replace(';', ',').replace('\n', '')
recipients = split_emails(recipients)
@@ -225,7 +223,7 @@ class EMail:
}
# reset headers as values may be changed.
- for key, val in iteritems(headers):
+ for key, val in headers.items():
if val:
self.set_header(key, val)
@@ -328,7 +326,7 @@ def add_attachment(fname, fcontent, content_type=None,
maintype, subtype = content_type.split('/', 1)
if maintype == 'text':
# Note: we should handle calculating the charset
- if isinstance(fcontent, text_type):
+ if isinstance(fcontent, str):
fcontent = fcontent.encode("utf-8")
part = MIMEText(fcontent, _subtype=subtype, _charset="utf-8")
elif maintype == 'image':
@@ -345,7 +343,7 @@ def add_attachment(fname, fcontent, content_type=None,
# Set the filename parameter
if fname:
attachment_type = 'inline' if inline else 'attachment'
- part.add_header('Content-Disposition', attachment_type, filename=text_type(fname))
+ part.add_header('Content-Disposition', attachment_type, filename=str(fname))
if content_id:
part.add_header('Content-ID', '<{0}>'.format(content_id))
@@ -353,9 +351,7 @@ def add_attachment(fname, fcontent, content_type=None,
def get_message_id():
'''Returns Message ID created from doctype and name'''
- return "<{unique}@{site}>".format(
- site=frappe.local.site,
- unique=email.utils.make_msgid(random_string(10)).split('@')[0].split('<')[1])
+ return email.utils.make_msgid(domain=frappe.local.site)
def get_signature(email_account):
if email_account and email_account.add_signature and email_account.signature:
@@ -452,7 +448,7 @@ def get_header(header=None):
if not header: return None
- if isinstance(header, string_types):
+ if isinstance(header, str):
# header = 'My Title'
header = [header, None]
if len(header) == 1:
diff --git a/frappe/email/inbox.py b/frappe/email/inbox.py
index 395a2d3e2d..c6020e14e4 100644
--- a/frappe/email/inbox.py
+++ b/frappe/email/inbox.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
import json
@@ -18,7 +18,7 @@ def get_email_accounts(user=None):
"all_accounts": ""
}
- all_accounts = ",".join([ account.get("email_account") for account in accounts ])
+ all_accounts = ",".join(account.get("email_account") for account in accounts)
if len(accounts) > 1:
email_accounts.append({
"email_account": all_accounts,
diff --git a/frappe/email/queue.py b/frappe/email/queue.py
index 52c91baf1c..ef59302bab 100755
--- a/frappe/email/queue.py
+++ b/frappe/email/queue.py
@@ -1,269 +1,66 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
-import sys
-from six.moves import html_parser as HTMLParser
-import smtplib, quopri, json
-from frappe import msgprint, _, safe_decode, safe_encode, enqueue
-from frappe.email.smtp import SMTPServer
-from frappe.email.doctype.email_account.email_account import EmailAccount
-from frappe.email.email_body import get_email, get_formatted_html, add_attachment
+from frappe import msgprint, _
from frappe.utils.verified_command import get_signed_params, verify_request
-from html2text import html2text
-from frappe.utils import get_url, nowdate, now_datetime, add_days, split_emails, cstr, cint
-from rq.timeouts import JobTimeoutException
-from six import text_type, string_types, PY3
-from email.parser import Parser
+from frappe.utils import get_url, now_datetime, cint
+def get_emails_sent_this_month(email_account=None):
+ """Get count of emails sent from a specific email account.
-class EmailLimitCrossedError(frappe.ValidationError): pass
+ :param email_account: name of the email account used to send mail
-def send(recipients=None, sender=None, subject=None, message=None, text_content=None, reference_doctype=None,
- reference_name=None, unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None,
- attachments=None, reply_to=None, cc=None, bcc=None, message_id=None, in_reply_to=None, send_after=None,
- expose_recipients=None, send_priority=1, communication=None, now=False, read_receipt=None,
- queue_separately=False, is_notification=False, add_unsubscribe_link=1, inline_images=None,
- header=None, print_letterhead=False, with_container=False):
- """Add email to sending queue (Email Queue)
-
- :param recipients: List of recipients.
- :param sender: Email sender.
- :param subject: Email subject.
- :param message: Email message.
- :param text_content: Text version of email message.
- :param reference_doctype: Reference DocType of caller document.
- :param reference_name: Reference name of caller document.
- :param send_priority: Priority for Email Queue, default 1.
- :param unsubscribe_method: URL method for unsubscribe. Default is `/api/method/frappe.email.queue.unsubscribe`.
- :param unsubscribe_params: additional params for unsubscribed links. default are name, doctype, email
- :param attachments: Attachments to be sent.
- :param reply_to: Reply to be captured here (default inbox)
- :param in_reply_to: Used to send the Message-Id of a received email back as In-Reply-To.
- :param send_after: Send this email after the given datetime. If value is in integer, then `send_after` will be the automatically set to no of days from current date.
- :param communication: Communication link to be set in Email Queue record
- :param now: Send immediately (don't send in the background)
- :param queue_separately: Queue each email separately
- :param is_notification: Marks email as notification so will not trigger notifications from system
- :param add_unsubscribe_link: Send unsubscribe link in the footer of the Email, default 1.
- :param inline_images: List of inline images as {"filename", "filecontent"}. All src properties will be replaced with random Content-Id
- :param header: Append header in email (boolean)
- :param with_container: Wraps email inside styled container
+ if email_account=None, email account filter is not applied while counting
"""
- if not unsubscribe_method:
- unsubscribe_method = "/api/method/frappe.email.queue.unsubscribe"
-
- if not recipients and not cc:
- return
-
- if not cc:
- cc = []
- if not bcc:
- bcc = []
-
- if isinstance(recipients, string_types):
- recipients = split_emails(recipients)
-
- if isinstance(cc, string_types):
- cc = split_emails(cc)
-
- if isinstance(bcc, string_types):
- bcc = split_emails(bcc)
-
- if isinstance(send_after, int):
- send_after = add_days(nowdate(), send_after)
-
- email_account = EmailAccount.find_outgoing(
- match_by_doctype=reference_doctype, match_by_email=sender, _raise_error=True)
-
- if not sender or sender == "Administrator":
- sender = email_account.default_sender
-
- if not text_content:
- try:
- text_content = html2text(message)
- except HTMLParser.HTMLParseError:
- text_content = "See html attachment"
-
- recipients = list(set(recipients))
- cc = list(set(cc))
-
- all_ids = tuple(recipients + cc)
-
- unsubscribed = frappe.db.sql_list('''
+ q = """
SELECT
- distinct email
- from
- `tabEmail Unsubscribe`
- where
- email in %(all_ids)s
- and (
- (
- reference_doctype = %(reference_doctype)s
- and reference_name = %(reference_name)s
- )
- or global_unsubscribe = 1
- )
- ''', {
- 'all_ids': all_ids,
- 'reference_doctype': reference_doctype,
- 'reference_name': reference_name,
- })
+ COUNT(*)
+ FROM
+ `tabEmail Queue`
+ WHERE
+ `status`='Sent'
+ AND
+ EXTRACT(YEAR_MONTH FROM `creation`) = EXTRACT(YEAR_MONTH FROM NOW())
+ """
- recipients = [r for r in recipients if r and r not in unsubscribed]
+ q_args = {}
+ if email_account is not None:
+ if email_account:
+ q += " AND email_account = %(email_account)s"
+ q_args['email_account'] = email_account
+ else:
+ q += " AND (email_account is null OR email_account='')"
- if cc:
- cc = [r for r in cc if r and r not in unsubscribed]
+ return frappe.db.sql(q, q_args)[0][0]
- if not recipients and not cc:
- # Recipients may have been unsubscribed, exit quietly
- return
+def get_emails_sent_today(email_account=None):
+ """Get count of emails sent from a specific email account.
- email_text_context = text_content
+ :param email_account: name of the email account used to send mail
- should_append_unsubscribe = (add_unsubscribe_link
- and reference_doctype
- and (unsubscribe_message or reference_doctype=="Newsletter")
- and add_unsubscribe_link==1)
+ if email_account=None, email account filter is not applied while counting
+ """
+ q = """
+ SELECT
+ COUNT(`name`)
+ FROM
+ `tabEmail Queue`
+ WHERE
+ `status` in ('Sent', 'Not Sent', 'Sending')
+ AND
+ `creation` > (NOW() - INTERVAL '24' HOUR)
+ """
- unsubscribe_link = None
- if should_append_unsubscribe:
- unsubscribe_link = get_unsubscribe_message(unsubscribe_message, expose_recipients)
- email_text_context += unsubscribe_link.text
+ q_args = {}
+ if email_account is not None:
+ if email_account:
+ q += " AND email_account = %(email_account)s"
+ q_args['email_account'] = email_account
+ else:
+ q += " AND (email_account is null OR email_account='')"
- email_content = get_formatted_html(subject, message,
- email_account=email_account, header=header,
- unsubscribe_link=unsubscribe_link, with_container=with_container)
-
- # add to queue
- add(recipients, sender, subject,
- formatted=email_content,
- text_content=email_text_context,
- reference_doctype=reference_doctype,
- reference_name=reference_name,
- attachments=attachments,
- reply_to=reply_to,
- cc=cc,
- bcc=bcc,
- message_id=message_id,
- in_reply_to=in_reply_to,
- send_after=send_after,
- send_priority=send_priority,
- email_account=email_account,
- communication=communication,
- add_unsubscribe_link=add_unsubscribe_link,
- unsubscribe_method=unsubscribe_method,
- unsubscribe_params=unsubscribe_params,
- expose_recipients=expose_recipients,
- read_receipt=read_receipt,
- queue_separately=queue_separately,
- is_notification = is_notification,
- inline_images = inline_images,
- header=header,
- now=now,
- print_letterhead=print_letterhead)
-
-
-def add(recipients, sender, subject, **kwargs):
- """Add to Email Queue"""
- if kwargs.get('queue_separately') or len(recipients) > 20:
- email_queue = None
- for r in recipients:
- if not email_queue:
- email_queue = get_email_queue([r], sender, subject, **kwargs)
- if kwargs.get('now'):
- email_queue.send()
- else:
- duplicate = email_queue.get_duplicate([r])
- duplicate.insert(ignore_permissions=True)
-
- if kwargs.get('now'):
- duplicate.send()
-
- frappe.db.commit()
- else:
- email_queue = get_email_queue(recipients, sender, subject, **kwargs)
- if kwargs.get('now'):
- email_queue.send()
-
-def get_email_queue(recipients, sender, subject, **kwargs):
- '''Make Email Queue object'''
- e = frappe.new_doc('Email Queue')
- e.priority = kwargs.get('send_priority')
- attachments = kwargs.get('attachments')
- if attachments:
- # store attachments with fid or print format details, to be attached on-demand later
- _attachments = []
- for att in attachments:
- if att.get('fid'):
- _attachments.append(att)
- elif att.get("print_format_attachment") == 1:
- if not att.get('lang', None):
- att['lang'] = frappe.local.lang
- att['print_letterhead'] = kwargs.get('print_letterhead')
- _attachments.append(att)
- e.attachments = json.dumps(_attachments)
-
- try:
- mail = get_email(recipients,
- sender=sender,
- subject=subject,
- formatted=kwargs.get('formatted'),
- text_content=kwargs.get('text_content'),
- attachments=kwargs.get('attachments'),
- reply_to=kwargs.get('reply_to'),
- cc=kwargs.get('cc'),
- bcc=kwargs.get('bcc'),
- email_account=kwargs.get('email_account'),
- expose_recipients=kwargs.get('expose_recipients'),
- inline_images=kwargs.get('inline_images'),
- header=kwargs.get('header'))
-
- mail.set_message_id(kwargs.get('message_id'),kwargs.get('is_notification'))
- if kwargs.get('read_receipt'):
- mail.msg_root["Disposition-Notification-To"] = sender
- if kwargs.get('in_reply_to'):
- mail.set_in_reply_to(kwargs.get('in_reply_to'))
-
- e.message_id = mail.msg_root["Message-Id"].strip(" <>")
- e.message = cstr(mail.as_string())
- e.sender = mail.sender
-
- except frappe.InvalidEmailAddressError:
- # bad Email Address - don't add to queue
- import traceback
- frappe.log_error('Invalid Email ID Sender: {0}, Recipients: {1}, \nTraceback: {2} '.format(mail.sender,
- ', '.join(mail.recipients), traceback.format_exc()), 'Email Not Sent')
-
- recipients = list(set(recipients + kwargs.get('cc', []) + kwargs.get('bcc', [])))
- email_account = kwargs.get('email_account')
- email_account_name = email_account and email_account.is_exists_in_db() and email_account.name
-
- e.set_recipients(recipients)
- e.reference_doctype = kwargs.get('reference_doctype')
- e.reference_name = kwargs.get('reference_name')
- e.add_unsubscribe_link = kwargs.get("add_unsubscribe_link")
- e.unsubscribe_method = kwargs.get('unsubscribe_method')
- e.unsubscribe_params = kwargs.get('unsubscribe_params')
- e.expose_recipients = kwargs.get('expose_recipients')
- e.communication = kwargs.get('communication')
- e.send_after = kwargs.get('send_after')
- e.show_as_cc = ",".join(kwargs.get('cc', []))
- e.show_as_bcc = ",".join(kwargs.get('bcc', []))
- e.email_account = email_account_name or None
- e.insert(ignore_permissions=True)
- return e
-
-def get_emails_sent_this_month():
- return frappe.db.sql("""
- SELECT COUNT(*) FROM `tabEmail Queue`
- WHERE `status`='Sent' AND EXTRACT(YEAR_MONTH FROM `creation`) = EXTRACT(YEAR_MONTH FROM NOW())
- """)[0][0]
-
-def get_emails_sent_today():
- return frappe.db.sql("""SELECT COUNT(`name`) FROM `tabEmail Queue` WHERE
- `status` in ('Sent', 'Not Sent', 'Sending') AND `creation` > (NOW() - INTERVAL '24' HOUR)""")[0][0]
+ return frappe.db.sql(q, q_args)[0][0]
def get_unsubscribe_message(unsubscribe_message, expose_recipients):
if unsubscribe_message:
@@ -376,13 +173,8 @@ def clear_outbox(days=None):
WHERE `priority`=0 AND `modified` < (NOW() - INTERVAL '{0}' DAY)""".format(days))
if email_queues:
- frappe.db.sql("""DELETE FROM `tabEmail Queue` WHERE `name` IN ({0})""".format(
- ','.join(['%s']*len(email_queues)
- )), tuple(email_queues))
-
- frappe.db.sql("""DELETE FROM `tabEmail Queue Recipient` WHERE `parent` IN ({0})""".format(
- ','.join(['%s']*len(email_queues)
- )), tuple(email_queues))
+ frappe.db.delete("Email Queue", {"name": ("in", email_queues)})
+ frappe.db.delete("Email Queue Recipient", {"parent": ("in", email_queues)})
def set_expiry_for_email_queue():
''' Mark emails as expire that has not sent for 7 days.
diff --git a/frappe/email/receive.py b/frappe/email/receive.py
index 6d60007cdb..2e42008951 100644
--- a/frappe/email/receive.py
+++ b/frappe/email/receive.py
@@ -8,11 +8,11 @@ import imaplib
import poplib
import re
import time
+import json
from email.header import decode_header
import _socket
import chardet
-import six
from email_reply_parser import EmailReplyParser
import frappe
@@ -20,13 +20,26 @@ from frappe import _, safe_decode, safe_encode
from frappe.core.doctype.file.file import (MaxFileSizeReachedError,
get_random_filename)
from frappe.utils import (cint, convert_utc_to_user_timezone, cstr,
- extract_email_id, markdown, now, parse_addr, strip)
+ extract_email_id, markdown, now, parse_addr, strip, get_datetime,
+ add_days, sanitize_html)
+from frappe.utils.user import is_system_user
+from frappe.utils.html_utils import clean_email_html
+
+# fix due to a python bug in poplib that limits it to 2048
+poplib._MAXLINE = 20480
+imaplib._MAXLINE = 20480
+
+# fix due to a python bug in poplib that limits it to 2048
+poplib._MAXLINE = 20480
+imaplib._MAXLINE = 20480
class EmailSizeExceededError(frappe.ValidationError): pass
class EmailTimeoutError(frappe.ValidationError): pass
class TotalSizeExceededError(frappe.ValidationError): pass
class LoginLimitExceeded(frappe.ValidationError): pass
+class SentEmailInInboxError(Exception):
+ pass
class EmailServer:
"""Wrapper for POP server to pull emails."""
@@ -100,14 +113,11 @@ class EmailServer:
def get_messages(self):
"""Returns new email messages in a list."""
- if not self.check_mails():
- return # nothing to do
+ if not (self.check_mails() or self.connect()):
+ return []
frappe.db.commit()
- if not self.connect():
- return
-
uid_list = []
try:
@@ -116,7 +126,6 @@ class EmailServer:
self.latest_messages = []
self.seen_status = {}
self.uid_reindexed = False
-
uid_list = email_list = self.get_new_mails()
if not email_list:
@@ -132,11 +141,7 @@ class EmailServer:
self.max_email_size = cint(frappe.local.conf.get("max_email_size"))
self.max_total_size = 5 * self.max_email_size
- for i, message_meta in enumerate(email_list):
- # do not pull more than NUM emails
- if (i+1) > num:
- break
-
+ for i, message_meta in enumerate(email_list[:num]):
try:
self.retrieve_message(message_meta, i+1)
except (TotalSizeExceededError, EmailTimeoutError, LoginLimitExceeded):
@@ -152,7 +157,6 @@ class EmailServer:
except Exception as e:
if self.has_login_limit_exceeded(e):
pass
-
else:
raise
@@ -361,14 +365,12 @@ class Email:
"""Parses headers, content, attachments from given raw message.
:param content: Raw message."""
- if six.PY2:
- self.mail = email.message_from_string(safe_encode(content))
+ if isinstance(content, bytes):
+ self.mail = email.message_from_bytes(content)
else:
- if isinstance(content, bytes):
- self.mail = email.message_from_bytes(content)
- else:
- self.mail = email.message_from_string(content)
+ self.mail = email.message_from_string(content)
+ self.raw_message = content
self.text_content = ''
self.html_content = ''
self.attachments = []
@@ -391,6 +393,10 @@ class Email:
if self.date > now():
self.date = now()
+ @property
+ def in_reply_to(self):
+ return (self.mail.get("In-Reply-To") or "").strip(" <>")
+
def parse(self):
"""Walk and process multi-part email."""
for part in self.mail.walk():
@@ -558,10 +564,327 @@ class Email:
l = re.findall(r'(?<=\[)[\w/-]+', self.subject)
return l and l[0] or None
+ def is_reply(self):
+ return bool(self.in_reply_to)
-# fix due to a python bug in poplib that limits it to 2048
-poplib._MAXLINE = 20480
-imaplib._MAXLINE = 20480
+class InboundMail(Email):
+ """Class representation of incoming mail along with mail handlers.
+ """
+ def __init__(self, content, email_account, uid=None, seen_status=None):
+ super().__init__(content)
+ self.email_account = email_account
+ self.uid = uid or -1
+ self.seen_status = seen_status or 0
+
+ # System documents related to this mail
+ self._parent_email_queue = None
+ self._parent_communication = None
+ self._reference_document = None
+
+ self.flags = frappe._dict()
+
+ def get_content(self):
+ if self.content_type == 'text/html':
+ return clean_email_html(self.content)
+
+ def process(self):
+ """Create communication record from email.
+ """
+ if self.is_sender_same_as_receiver() and not self.is_reply():
+ if frappe.flags.in_test:
+ print('WARN: Cannot pull email. Sender same as recipient inbox')
+ raise SentEmailInInboxError
+
+ communication = self.is_exist_in_system()
+ if communication:
+ communication.update_db(uid=self.uid)
+ communication.reload()
+ return communication
+
+ self.flags.is_new_communication = True
+ return self._build_communication_doc()
+
+ def _build_communication_doc(self):
+ data = self.as_dict()
+ data['doctype'] = "Communication"
+
+ if self.parent_communication():
+ data['in_reply_to'] = self.parent_communication().name
+
+ if self.reference_document():
+ data['reference_doctype'] = self.reference_document().doctype
+ data['reference_name'] = self.reference_document().name
+ elif self.email_account.append_to and self.email_account.append_to != 'Communication':
+ reference_doc = self._create_reference_document(self.email_account.append_to)
+ if reference_doc:
+ data['reference_doctype'] = reference_doc.doctype
+ data['reference_name'] = reference_doc.name
+ data['is_first'] = True
+
+ if self.is_notification():
+ # Disable notifications for notification.
+ data['unread_notification_sent'] = 1
+
+ if self.seen_status:
+ data['_seen'] = json.dumps(self.get_users_linked_to_account(self.email_account))
+
+ communication = frappe.get_doc(data)
+ communication.flags.in_receive = True
+ communication.insert(ignore_permissions=True)
+
+ # save attachments
+ communication._attachments = self.save_attachments_in_doc(communication)
+ communication.content = sanitize_html(self.replace_inline_images(communication._attachments))
+ communication.save()
+ return communication
+
+ def replace_inline_images(self, attachments):
+ # replace inline images
+ content = self.content
+ for file in attachments:
+ if file.name in self.cid_map and self.cid_map[file.name]:
+ content = content.replace("cid:{0}".format(self.cid_map[file.name]),
+ file.file_url)
+ return content
+
+ def is_notification(self):
+ isnotification = self.mail.get("isnotification")
+ return isnotification and ("notification" in isnotification)
+
+ def is_exist_in_system(self):
+ """Check if this email already exists in the system(as communication document).
+ """
+ from frappe.core.doctype.communication.communication import Communication
+ if not self.message_id:
+ return
+
+ return Communication.find_one_by_filters(message_id = self.message_id,
+ order_by = 'creation DESC')
+
+ def is_sender_same_as_receiver(self):
+ return self.from_email == self.email_account.email_id
+
+ def is_reply_to_system_sent_mail(self):
+ """Is it a reply to already sent mail.
+ """
+ return self.is_reply() and frappe.local.site in self.in_reply_to
+
+ def parent_email_queue(self):
+ """Get parent record from `Email Queue`.
+
+ If it is a reply to already sent mail, then there will be a parent record in EMail Queue.
+ """
+ from frappe.email.doctype.email_queue.email_queue import EmailQueue
+
+ if self._parent_email_queue is not None:
+ return self._parent_email_queue
+
+ parent_email_queue = ''
+ if self.is_reply_to_system_sent_mail():
+ parent_email_queue = EmailQueue.find_one_by_filters(message_id=self.in_reply_to)
+
+ self._parent_email_queue = parent_email_queue or ''
+ return self._parent_email_queue
+
+ def parent_communication(self):
+ """Find a related communication so that we can prepare a mail thread.
+
+ The way it happens is by using in-reply-to header, and we can't make thread if it does not exist.
+
+ Here are the cases to handle:
+ 1. If mail is a reply to already sent mail, then we can get parent communicaion from
+ Email Queue record.
+ 2. Sometimes we send communication name in message-ID directly, use that to get parent communication.
+ 3. Sender sent a reply but reply is on top of what (s)he sent before,
+ then parent record exists directly in communication.
+ """
+ from frappe.core.doctype.communication.communication import Communication
+ if self._parent_communication is not None:
+ return self._parent_communication
+
+ if not self.is_reply():
+ return ''
+
+ if not self.is_reply_to_system_sent_mail():
+ communication = Communication.find_one_by_filters(message_id=self.in_reply_to,
+ creation = ['>=', self.get_relative_dt(-30)])
+ elif self.parent_email_queue() and self.parent_email_queue().communication:
+ communication = Communication.find(self.parent_email_queue().communication, ignore_error=True)
+ else:
+ reference = self.in_reply_to
+ if '@' in self.in_reply_to:
+ reference, _ = self.in_reply_to.split("@", 1)
+ communication = Communication.find(reference, ignore_error=True)
+
+ self._parent_communication = communication or ''
+ return self._parent_communication
+
+ def reference_document(self):
+ """Reference document is a document to which mail relate to.
+
+ We can get reference document from Parent record(EmailQueue | Communication) if exists.
+ Otherwise we do subject match to find reference document if we know the reference(append_to) doctype.
+ """
+ if self._reference_document is not None:
+ return self._reference_document
+
+ reference_document = ""
+ parent = self.parent_email_queue() or self.parent_communication()
+
+ if parent and parent.reference_doctype:
+ reference_doctype, reference_name = parent.reference_doctype, parent.reference_name
+ reference_document = self.get_doc(reference_doctype, reference_name, ignore_error=True)
+
+ if not reference_document and self.email_account.append_to:
+ reference_document = self.match_record_by_subject_and_sender(self.email_account.append_to)
+
+ self._reference_document = reference_document or ''
+ return self._reference_document
+
+ def get_reference_name_from_subject(self):
+ """
+ Ex: "Re: Your email (#OPP-2020-2334343)"
+ """
+ return self.subject.rsplit('#', 1)[-1].strip(' ()')
+
+ def match_record_by_subject_and_sender(self, doctype):
+ """Find a record in the given doctype that matches with email subject and sender.
+
+ Cases:
+ 1. Sometimes record name is part of subject. We can get document by parsing name from subject
+ 2. Find by matching sender and subject
+ 3. Find by matching subject alone (Special case)
+ Ex: when a System User is using Outlook and replies to an email from their own client,
+ it reaches the Email Account with the threading info lost and the (sender + subject match)
+ doesn't work because the sender in the first communication was someone different to whom
+ the system user is replying to via the common email account in Frappe. This fix bypasses
+ the sender match when the sender is a system user and subject is atleast 10 chars long
+ (for additional safety)
+
+ NOTE: We consider not to match by subject if match record is very old.
+ """
+ name = self.get_reference_name_from_subject()
+ email_fields = self.get_email_fields(doctype)
+
+ record = self.get_doc(doctype, name, ignore_error=True) if name else None
+
+ if not record:
+ subject = self.clean_subject(self.subject)
+ filters = {
+ email_fields.subject_field: ("like", f"%{subject}%"),
+ "creation": (">", self.get_relative_dt(days=-60))
+ }
+
+ # Sender check is not needed incase mail is from system user.
+ if not (len(subject) > 10 and is_system_user(self.from_email)):
+ filters[email_fields.sender_field] = self.from_email
+
+ name = frappe.db.get_value(self.email_account.append_to, filters = filters)
+ record = self.get_doc(doctype, name, ignore_error=True) if name else None
+ return record
+
+ def _create_reference_document(self, doctype):
+ """ Create reference document if it does not exist in the system.
+ """
+ parent = frappe.new_doc(doctype)
+ email_fileds = self.get_email_fields(doctype)
+
+ if email_fileds.subject_field:
+ parent.set(email_fileds.subject_field, frappe.as_unicode(self.subject)[:140])
+
+ if email_fileds.sender_field:
+ parent.set(email_fileds.sender_field, frappe.as_unicode(self.from_email))
+
+ parent.flags.ignore_mandatory = True
+
+ try:
+ parent.insert(ignore_permissions=True)
+ except frappe.DuplicateEntryError:
+ # try and find matching parent
+ parent_name = frappe.db.get_value(self.email_account.append_to,
+ {email_fileds.sender_field: self.from_email}
+ )
+ if parent_name:
+ parent.name = parent_name
+ else:
+ parent = None
+ return parent
+
+
+ @staticmethod
+ def get_doc(doctype, docname, ignore_error=False):
+ try:
+ return frappe.get_doc(doctype, docname)
+ except frappe.DoesNotExistError:
+ if ignore_error:
+ return
+ raise
+
+ @staticmethod
+ def get_relative_dt(days):
+ """Get relative to current datetime. Only relative days are supported.
+ """
+ return add_days(get_datetime(), days)
+
+ @staticmethod
+ def get_users_linked_to_account(email_account):
+ """Get list of users who linked to Email account.
+ """
+ users = frappe.get_all("User Email", filters={"email_account": email_account.name},
+ fields=["parent"])
+ return list(set([user.get("parent") for user in users]))
+
+ @staticmethod
+ def clean_subject(subject):
+ """Remove Prefixes like 'fw', FWD', 're' etc from subject.
+ """
+ # Match strings like "fw:", "re :" etc.
+ regex = r"(^\s*(fw|fwd|wg)[^:]*:|\s*(re|aw)[^:]*:\s*)*"
+ return frappe.as_unicode(strip(re.sub(regex, "", subject, 0, flags=re.IGNORECASE)))
+
+ @staticmethod
+ def get_email_fields(doctype):
+ """Returns Email related fields of a doctype.
+ """
+ fields = frappe._dict()
+
+ email_fields = ['subject_field', 'sender_field']
+ meta = frappe.get_meta(doctype)
+
+ for field in email_fields:
+ if hasattr(meta, field):
+ fields[field] = getattr(meta, field)
+ return fields
+
+ @staticmethod
+ def get_document(self, doctype, name):
+ """Is same as frappe.get_doc but suppresses the DoesNotExist error.
+ """
+ try:
+ return frappe.get_doc(doctype, name)
+ except frappe.DoesNotExistError:
+ return None
+
+ def as_dict(self):
+ """
+ """
+ return {
+ "subject": self.subject,
+ "content": self.get_content(),
+ 'text_content': self.text_content,
+ "sent_or_received": "Received",
+ "sender_full_name": self.from_real_name,
+ "sender": self.from_email,
+ "recipients": self.mail.get("To"),
+ "cc": self.mail.get("CC"),
+ "email_account": self.email_account.name,
+ "communication_medium": "Email",
+ "uid": self.uid,
+ "message_id": self.message_id,
+ "communication_date": self.date,
+ "has_attachment": 1 if self.attachments else 0,
+ "seen": self.seen_status or 0
+ }
class TimerMixin(object):
def __init__(self, *args, **kwargs):
diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py
index 3acb76af23..74492c09c3 100644
--- a/frappe/email/smtp.py
+++ b/frappe/email/smtp.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
import smtplib
import email.utils
@@ -85,18 +84,19 @@ class SMTPServer:
SMTP = smtplib.SMTP_SSL if self.use_ssl else smtplib.SMTP
try:
- self._session = SMTP(self.server, self.port)
- if not self._session:
+ _session = SMTP(self.server, self.port)
+ if not _session:
frappe.msgprint(CONNECTION_FAILED, raise_exception=frappe.OutgoingEmailError)
- self.secure_session(self._session)
+ self.secure_session(_session)
if self.login and self.password:
- res = self._session.login(str(self.login or ""), str(self.password or ""))
+ res = _session.login(str(self.login or ""), str(self.password or ""))
# check if logged correctly
if res[0]!=235:
frappe.msgprint(res[1], raise_exception=frappe.OutgoingEmailError)
+ self._session = _session
return self._session
except smtplib.SMTPAuthenticationError as e:
diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py
index 33668cddba..2c7d119fce 100644
--- a/frappe/email/test_email_body.py
+++ b/frappe/email/test_email_body.py
@@ -1,15 +1,12 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
-
import unittest, os, base64
from frappe import safe_decode
from frappe.email.receive import Email
from frappe.email.email_body import (replace_filename_with_cid,
get_email, inline_style_in_html, get_header)
-from frappe.email.queue import get_email_queue
-from frappe.email.doctype.email_queue.email_queue import SendMailContext
-from six import PY3
+from frappe.email.doctype.email_queue.email_queue import SendMailContext, QueueBuilder
+
class TestEmailBody(unittest.TestCase):
def setUp(self):
@@ -42,41 +39,31 @@ This is the text version of this email
).as_string().replace("\r\n", "\n")
def test_prepare_message_returns_already_encoded_string(self):
+ uni_chr1 = chr(40960)
+ uni_chr2 = chr(1972)
- if PY3:
- uni_chr1 = chr(40960)
- uni_chr2 = chr(1972)
- else:
- uni_chr1 = unichr(40960)
- uni_chr2 = unichr(1972)
-
- email = get_email_queue(
+ queue_doc = QueueBuilder(
recipients=['test@example.com'],
sender='me@example.com',
subject='Test Subject',
- content='' + uni_chr1 + 'abcd' + uni_chr2 + '
',
- formatted='' + uni_chr1 + 'abcd' + uni_chr2 + '
',
- text_content='whatever')
- mail_ctx = SendMailContext(queue_doc = email)
+ message='' + uni_chr1 + 'abcd' + uni_chr2 + '
',
+ text_content='whatever').process()[0]
+ mail_ctx = SendMailContext(queue_doc = queue_doc)
result = mail_ctx.build_message(recipient_email = 'test@test.com')
self.assertTrue(b"=EA=80=80abcd=DE=B4
" in result)
def test_prepare_message_returns_cr_lf(self):
- email = get_email_queue(
+ queue_doc = QueueBuilder(
recipients=['test@example.com'],
sender='me@example.com',
subject='Test Subject',
- content='\n this is a test of newlines\n' + '
',
- formatted='\n this is a test of newlines\n' + '
',
- text_content='whatever')
+ message='\n this is a test of newlines\n' + '
',
+ text_content='whatever').process()[0]
- mail_ctx = SendMailContext(queue_doc = email)
+ mail_ctx = SendMailContext(queue_doc = queue_doc)
result = safe_decode(mail_ctx.build_message(recipient_email='test@test.com'))
- if PY3:
- self.assertTrue(result.count('\n') == result.count("\r"))
- else:
- self.assertTrue(True)
+ self.assertTrue(result.count('\n') == result.count("\r"))
def test_image(self):
img_signature = '''
@@ -140,7 +127,7 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
'''
transformed_html = '''
Hi John
-This is a test email
+This is a test email
'''
self.assertTrue(transformed_html in inline_style_in_html(html))
diff --git a/frappe/email/utils.py b/frappe/email/utils.py
index 8b4bd95ba0..24ce77b922 100644
--- a/frappe/email/utils.py
+++ b/frappe/email/utils.py
@@ -1,7 +1,5 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-
-from __future__ import unicode_literals, print_function
import imaplib, poplib
from frappe.utils import cint
diff --git a/frappe/event_streaming/doctype/document_type_field_mapping/document_type_field_mapping.py b/frappe/event_streaming/doctype/document_type_field_mapping/document_type_field_mapping.py
index 1ab9534bdc..fc8164d8a4 100644
--- a/frappe/event_streaming/doctype/document_type_field_mapping/document_type_field_mapping.py
+++ b/frappe/event_streaming/doctype/document_type_field_mapping/document_type_field_mapping.py
@@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/event_streaming/doctype/document_type_mapping/document_type_mapping.py b/frappe/event_streaming/doctype/document_type_mapping/document_type_mapping.py
index bf96e4e27b..2cf7282a5a 100644
--- a/frappe/event_streaming/doctype/document_type_mapping/document_type_mapping.py
+++ b/frappe/event_streaming/doctype/document_type_mapping/document_type_mapping.py
@@ -1,12 +1,9 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-
-from __future__ import unicode_literals
import frappe
import json
from frappe import _
-from six import iteritems
from frappe.model.document import Document
from frappe.model import default_fields
@@ -100,7 +97,7 @@ class DocumentTypeMapping(Document):
def get_mapped_dependency(self, mapping, producer_site, doc):
inner_mapping = frappe.get_doc('Document Type Mapping', mapping.mapping)
filters = json.loads(mapping.remote_value_filters)
- for key, value in iteritems(filters):
+ for key, value in filters.items():
if value.startswith('eval:'):
val = frappe.safe_eval(value[5:], None, dict(doc=doc))
filters[key] = val
@@ -117,7 +114,7 @@ class DocumentTypeMapping(Document):
def map_rows_removed(self, update_diff, mapping):
removed = []
mapping['removed'] = update_diff.removed
- for key, value in iteritems(update_diff.removed.copy()):
+ for key, value in update_diff.removed.copy().items():
local_table_name = frappe.db.get_value('Document Type Field Mapping', {
'remote_fieldname': key,
'parent': self.name
@@ -133,7 +130,7 @@ class DocumentTypeMapping(Document):
def map_rows(self, update_diff, mapping, producer_site, operation):
remote_fields = []
- for tablename, entries in iteritems(update_diff.get(operation).copy()):
+ for tablename, entries in update_diff.get(operation).copy().items():
local_table_name = frappe.db.get_value('Document Type Field Mapping', {'remote_fieldname': tablename}, 'local_fieldname')
table_map = frappe.db.get_value('Document Type Field Mapping', {'local_fieldname': local_table_name, 'parent': self.name}, 'mapping')
table_map = frappe.get_doc('Document Type Mapping', table_map)
diff --git a/frappe/event_streaming/doctype/document_type_mapping/test_document_type_mapping.py b/frappe/event_streaming/doctype/document_type_mapping/test_document_type_mapping.py
index 178d7b6b6a..b1bb322855 100644
--- a/frappe/event_streaming/doctype/document_type_mapping/test_document_type_mapping.py
+++ b/frappe/event_streaming/doctype/document_type_mapping/test_document_type_mapping.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
# import frappe
import unittest
diff --git a/frappe/event_streaming/doctype/event_consumer/event_consumer.py b/frappe/event_streaming/doctype/event_consumer/event_consumer.py
index 5789e09e74..00d304f7f4 100644
--- a/frappe/event_streaming/doctype/event_consumer/event_consumer.py
+++ b/frappe/event_streaming/doctype/event_consumer/event_consumer.py
@@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
import json
import requests
@@ -31,7 +30,7 @@ class EventConsumer(Document):
self.update_consumer_status()
else:
frappe.db.set_value(self.doctype, self.name, 'incoming_change', 0)
-
+
frappe.cache().delete_value('event_consumer_document_type_map')
def on_trash(self):
diff --git a/frappe/event_streaming/doctype/event_consumer/test_event_consumer.py b/frappe/event_streaming/doctype/event_consumer/test_event_consumer.py
index 9e344842bd..b8072ecabd 100644
--- a/frappe/event_streaming/doctype/event_consumer/test_event_consumer.py
+++ b/frappe/event_streaming/doctype/event_consumer/test_event_consumer.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
# import frappe
import unittest
diff --git a/frappe/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.py b/frappe/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.py
index 197338027f..cf5d18edfd 100644
--- a/frappe/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.py
+++ b/frappe/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.py
@@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/event_streaming/doctype/event_producer/test_event_producer.py b/frappe/event_streaming/doctype/event_producer/test_event_producer.py
index 4c259c3729..883f4f2df2 100644
--- a/frappe/event_streaming/doctype/event_producer/test_event_producer.py
+++ b/frappe/event_streaming/doctype/event_producer/test_event_producer.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
import json
@@ -154,7 +152,7 @@ class TestEventProducer(unittest.TestCase):
def test_conditional_events(self):
producer = get_remote_site()
-
+
# Add Condition
event_producer = frappe.get_doc('Event Producer', producer_url)
note_producer_entry = [
@@ -192,7 +190,7 @@ class TestEventProducer(unittest.TestCase):
def test_conditional_events_with_cmd(self):
producer = get_remote_site()
-
+
# Add Condition
event_producer = frappe.get_doc('Event Producer', producer_url)
note_producer_entry = [
diff --git a/frappe/event_streaming/doctype/event_producer_document_type/event_producer_document_type.py b/frappe/event_streaming/doctype/event_producer_document_type/event_producer_document_type.py
index 2870d5330f..9ae70e0f97 100644
--- a/frappe/event_streaming/doctype/event_producer_document_type/event_producer_document_type.py
+++ b/frappe/event_streaming/doctype/event_producer_document_type/event_producer_document_type.py
@@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py
index 02e297bdd5..391cf79c27 100644
--- a/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py
+++ b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py
@@ -2,7 +2,6 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py b/frappe/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py
index 0311cb2df9..62ea71edab 100644
--- a/frappe/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py
+++ b/frappe/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
# import frappe
import unittest
diff --git a/frappe/event_streaming/doctype/event_sync_log/event_sync_log.py b/frappe/event_streaming/doctype/event_sync_log/event_sync_log.py
index 31b1f863aa..1d255a5c30 100644
--- a/frappe/event_streaming/doctype/event_sync_log/event_sync_log.py
+++ b/frappe/event_streaming/doctype/event_sync_log/event_sync_log.py
@@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/event_streaming/doctype/event_sync_log/test_event_sync_log.py b/frappe/event_streaming/doctype/event_sync_log/test_event_sync_log.py
index 6c621b8b0e..ef55dc0f16 100644
--- a/frappe/event_streaming/doctype/event_sync_log/test_event_sync_log.py
+++ b/frappe/event_streaming/doctype/event_sync_log/test_event_sync_log.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
# import frappe
import unittest
diff --git a/frappe/event_streaming/doctype/event_update_log/event_update_log.py b/frappe/event_streaming/doctype/event_update_log/event_update_log.py
index 1c31718c2b..ae851c70d1 100644
--- a/frappe/event_streaming/doctype/event_update_log/event_update_log.py
+++ b/frappe/event_streaming/doctype/event_update_log/event_update_log.py
@@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils.background_jobs import get_jobs
@@ -235,7 +234,7 @@ def get_update_logs_for_consumer(event_consumer, doctypes, last_update):
if isinstance(doctypes, str):
doctypes = frappe.parse_json(doctypes)
-
+
from frappe.event_streaming.doctype.event_consumer.event_consumer import has_consumer_access
consumer = frappe.get_doc('Event Consumer', event_consumer)
diff --git a/frappe/event_streaming/doctype/event_update_log/test_event_update_log.py b/frappe/event_streaming/doctype/event_update_log/test_event_update_log.py
index e00fc767d9..99ced3c209 100644
--- a/frappe/event_streaming/doctype/event_update_log/test_event_update_log.py
+++ b/frappe/event_streaming/doctype/event_update_log/test_event_update_log.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-from __future__ import unicode_literals
-
# import frappe
import unittest
diff --git a/frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.py b/frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.py
index ee6d5d8ca9..80a59e4c31 100644
--- a/frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.py
+++ b/frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.py
@@ -2,7 +2,6 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/exceptions.py b/frappe/exceptions.py
index ab65e6e006..13abd8f4f8 100644
--- a/frappe/exceptions.py
+++ b/frappe/exceptions.py
@@ -1,18 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
-import sys
-
# BEWARE don't put anything in this file except exceptions
from werkzeug.exceptions import NotFound
-
-if sys.version_info.major == 2:
- class FileNotFoundError(Exception): pass
-else:
- from builtins import FileNotFoundError
-
class SiteNotSpecifiedError(Exception):
def __init__(self, *args, **kwargs):
self.message = "Please specify --site sitename"
diff --git a/frappe/frappeclient.py b/frappe/frappeclient.py
index 054a8c9369..e57f82b60a 100644
--- a/frappe/frappeclient.py
+++ b/frappe/frappeclient.py
@@ -1,8 +1,6 @@
-from __future__ import print_function, unicode_literals
import requests
import json
import frappe
-from six import iteritems, string_types
import base64
'''
@@ -88,7 +86,7 @@ class FrappeClient(object):
def get_list(self, doctype, fields='["name"]', filters=None, limit_start=0, limit_page_length=0):
"""Returns list of records of a particular type"""
- if not isinstance(fields, string_types):
+ if not isinstance(fields, str):
fields = json.dumps(fields)
params = {
"fields": fields,
@@ -310,7 +308,7 @@ class FrappeClient(object):
def preprocess(self, params):
"""convert dicts, lists to json"""
- for key, value in iteritems(params):
+ for key, value in params.items():
if isinstance(value, (dict, list)):
params[key] = json.dumps(value)
diff --git a/frappe/geo/country_info.py b/frappe/geo/country_info.py
index 4f878325ad..ddebd1fb0e 100644
--- a/frappe/geo/country_info.py
+++ b/frappe/geo/country_info.py
@@ -2,8 +2,6 @@
# MIT License. See license.txt
# all country info
-from __future__ import unicode_literals
-
import os, json, frappe
from frappe.utils.momentjs import get_all_timezones
diff --git a/frappe/geo/doctype/country/__init__.py b/frappe/geo/doctype/country/__init__.py
index baffc48825..8b13789179 100644
--- a/frappe/geo/doctype/country/__init__.py
+++ b/frappe/geo/doctype/country/__init__.py
@@ -1 +1 @@
-from __future__ import unicode_literals
+
diff --git a/frappe/geo/doctype/country/country.py b/frappe/geo/doctype/country/country.py
index 5f8b6f7bd5..54935e6eaf 100644
--- a/frappe/geo/doctype/country/country.py
+++ b/frappe/geo/doctype/country/country.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: See license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
diff --git a/frappe/geo/doctype/country/test_country.py b/frappe/geo/doctype/country/test_country.py
index 81849d6886..e00d6ecf37 100644
--- a/frappe/geo/doctype/country/test_country.py
+++ b/frappe/geo/doctype/country/test_country.py
@@ -1,6 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: See license.txt
-from __future__ import unicode_literals
import frappe
test_records = frappe.get_test_records('Country')
\ No newline at end of file
diff --git a/frappe/geo/doctype/currency/__init__.py b/frappe/geo/doctype/currency/__init__.py
index baffc48825..8b13789179 100644
--- a/frappe/geo/doctype/currency/__init__.py
+++ b/frappe/geo/doctype/currency/__init__.py
@@ -1 +1 @@
-from __future__ import unicode_literals
+
diff --git a/frappe/geo/doctype/currency/currency.py b/frappe/geo/doctype/currency/currency.py
index 688303fd50..b3ce67cc67 100644
--- a/frappe/geo/doctype/currency/currency.py
+++ b/frappe/geo/doctype/currency/currency.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: See license.txt
-from __future__ import unicode_literals
import frappe
from frappe import throw, _
diff --git a/frappe/geo/doctype/currency/test_currency.py b/frappe/geo/doctype/currency/test_currency.py
index 7945e193da..5552e675ec 100644
--- a/frappe/geo/doctype/currency/test_currency.py
+++ b/frappe/geo/doctype/currency/test_currency.py
@@ -3,6 +3,5 @@
# pre loaded
-from __future__ import unicode_literals
import frappe
test_records = frappe.get_test_records('Currency')
\ No newline at end of file
diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py
index d94a13ea41..89de176f0b 100644
--- a/frappe/geo/utils.py
+++ b/frappe/geo/utils.py
@@ -2,8 +2,6 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
-
import frappe
from pymysql import InternalError
diff --git a/frappe/handler.py b/frappe/handler.py
index b622667e18..2e9fb7b454 100755
--- a/frappe/handler.py
+++ b/frappe/handler.py
@@ -1,8 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
-
from werkzeug.wrappers import Response
import frappe
@@ -12,6 +10,8 @@ from frappe.utils import cint
from frappe import _, is_whitelisted
from frappe.utils.response import build_response
from frappe.utils.csvutils import build_csv_response
+from frappe.utils.image import optimize_image
+from mimetypes import guess_type
from frappe.core.doctype.server_script.server_script_utils import run_server_script_api
@@ -55,7 +55,7 @@ def execute_cmd(cmd, from_async=False):
try:
method = get_attr(cmd)
except Exception as e:
- frappe.throw(_('Invalid Method'))
+ frappe.throw(_('Failed to get method for command {0} with {1}').format(cmd, e))
if from_async:
method = method.queue
@@ -146,20 +146,32 @@ def upload_file():
file_url = frappe.form_dict.file_url
folder = frappe.form_dict.folder or 'Home'
method = frappe.form_dict.method
+ filename = frappe.form_dict.file_name
+ optimize = frappe.form_dict.optimize
content = None
- filename = None
if 'file' in files:
file = files['file']
content = file.stream.read()
filename = file.filename
+ content_type = guess_type(filename)[0]
+ if optimize and content_type.startswith("image/"):
+ args = {
+ "content": content,
+ "content_type": content_type
+ }
+ if frappe.form_dict.max_width:
+ args["max_width"] = int(frappe.form_dict.max_width)
+ if frappe.form_dict.max_height:
+ args["max_height"] = int(frappe.form_dict.max_height)
+ content = optimize_image(**args)
+
frappe.local.uploaded_file = content
frappe.local.uploaded_filename = filename
- if frappe.session.user == 'Guest' or (user and not user.has_desk_access()):
- import mimetypes
- filetype = mimetypes.guess_type(filename)[0]
+ if not file_url and (frappe.session.user == "Guest" or (user and not user.has_desk_access())):
+ filetype = guess_type(filename)[0]
if filetype not in ALLOWED_MIMETYPES:
frappe.throw(_("You can only upload JPG, PNG, PDF, or Microsoft documents."))
diff --git a/frappe/hooks.py b/frappe/hooks.py
index d0968ce051..f3d25d6bf4 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
from . import __version__ as app_version
@@ -171,6 +171,9 @@ doc_events = {
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions",
"frappe.event_streaming.doctype.event_update_log.event_update_log.notify_consumers"
],
+ "on_update_after_submit": [
+ "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions"
+ ],
"on_change": [
"frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points",
"frappe.automation.doctype.milestone_tracker.milestone_tracker.evaluate_milestone"
diff --git a/frappe/installer.py b/frappe/installer.py
index d7d885d60e..d4d8117fcb 100755
--- a/frappe/installer.py
+++ b/frappe/installer.py
@@ -282,10 +282,10 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
def post_install(rebuild_website=False):
- from frappe.website import render
+ from frappe.website.utils import clear_website_cache
if rebuild_website:
- render.clear_cache()
+ clear_website_cache()
init_singles()
frappe.db.commit()
@@ -537,7 +537,7 @@ def is_downgrade(sql_file_path, verbose=False):
def is_partial(sql_file_path):
with open(sql_file_path) as f:
- header = " ".join([f.readline() for _ in range(5)])
+ header = " ".join(f.readline() for _ in range(5))
if "Partial Backup" in header:
return True
return False
diff --git a/frappe/integrations/doctype/braintree_settings/braintree_settings.py b/frappe/integrations/doctype/braintree_settings/braintree_settings.py
index 768f58c0a0..9dc9778bee 100644
--- a/frappe/integrations/doctype/braintree_settings/braintree_settings.py
+++ b/frappe/integrations/doctype/braintree_settings/braintree_settings.py
@@ -2,12 +2,11 @@
# Copyright (c) 2018, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
import braintree
from frappe import _
-from six.moves.urllib.parse import urlencode
+from urllib.parse import urlencode
from frappe.utils import get_url, call_hook_method
from frappe.integrations.utils import create_request_log, create_payment_gateway
diff --git a/frappe/integrations/doctype/braintree_settings/test_braintree_settings.py b/frappe/integrations/doctype/braintree_settings/test_braintree_settings.py
index 80fa3c54b8..72a678a92c 100644
--- a/frappe/integrations/doctype/braintree_settings/test_braintree_settings.py
+++ b/frappe/integrations/doctype/braintree_settings/test_braintree_settings.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import unittest
class TestBraintreeSettings(unittest.TestCase):
diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py
index b4304f6ee8..d1ff19ecb2 100644
--- a/frappe/integrations/doctype/connected_app/test_connected_app.py
+++ b/frappe/integrations/doctype/connected_app/test_connected_app.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# See license.txt
-from __future__ import unicode_literals
-
import unittest
import requests
from urllib.parse import urljoin
diff --git a/frappe/integrations/doctype/dropbox_settings/test_dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/test_dropbox_settings.py
index 539fc417f2..d34e65de50 100644
--- a/frappe/integrations/doctype/dropbox_settings/test_dropbox_settings.py
+++ b/frappe/integrations/doctype/dropbox_settings/test_dropbox_settings.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
# import frappe
import unittest
diff --git a/frappe/integrations/doctype/google_drive/test_google_drive.py b/frappe/integrations/doctype/google_drive/test_google_drive.py
index f06e13572c..96e8577c7c 100644
--- a/frappe/integrations/doctype/google_drive/test_google_drive.py
+++ b/frappe/integrations/doctype/google_drive/test_google_drive.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
# import frappe
import unittest
diff --git a/frappe/integrations/doctype/google_settings/google_settings.json b/frappe/integrations/doctype/google_settings/google_settings.json
index 086c56c020..6f25fa4bf6 100644
--- a/frappe/integrations/doctype/google_settings/google_settings.json
+++ b/frappe/integrations/doctype/google_settings/google_settings.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2019-06-14 00:08:37.255003",
"doctype": "DocType",
"engine": "InnoDB",
@@ -8,7 +9,10 @@
"client_id",
"client_secret",
"sb_01",
- "api_key"
+ "api_key",
+ "section_break_7",
+ "google_drive_picker_enabled",
+ "app_id"
],
"fields": [
{
@@ -18,10 +22,12 @@
"label": "Enable"
},
{
+ "description": "The Client ID obtained from the Google Cloud Console under \n\"APIs & Services\" > \"Credentials\"\n",
"fieldname": "client_id",
"fieldtype": "Data",
"in_list_view": 1,
- "label": "Client ID"
+ "label": "Client ID",
+ "mandatory_depends_on": "google_drive_picker_enabled"
},
{
"fieldname": "client_secret",
@@ -30,10 +36,11 @@
"label": "Client Secret"
},
{
- "description": "Used For Google Maps Integration.",
+ "description": "The browser API key obtained from the Google Cloud Console under \n\"APIs & Services\" > \"Credentials\"\n",
"fieldname": "api_key",
"fieldtype": "Data",
- "label": "API Key"
+ "label": "API Key",
+ "mandatory_depends_on": "google_drive_picker_enabled"
},
{
"depends_on": "enable",
@@ -46,10 +53,30 @@
"fieldname": "sb_01",
"fieldtype": "Section Break",
"label": "API Key"
+ },
+ {
+ "depends_on": "google_drive_picker_enabled",
+ "description": "The project number obtained from Google Cloud Console under \n\"IAM & Admin\" > \"Settings\"\n",
+ "fieldname": "app_id",
+ "fieldtype": "Data",
+ "label": "App ID",
+ "mandatory_depends_on": "google_drive_picker_enabled"
+ },
+ {
+ "fieldname": "section_break_7",
+ "fieldtype": "Section Break",
+ "label": "Google Drive Picker"
+ },
+ {
+ "default": "0",
+ "fieldname": "google_drive_picker_enabled",
+ "fieldtype": "Check",
+ "label": "Google Drive Picker Enabled"
}
],
"issingle": 1,
- "modified": "2019-08-06 22:37:41.699703",
+ "links": [],
+ "modified": "2021-06-29 18:26:07.094851",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Google Settings",
@@ -64,16 +91,6 @@
"role": "System Manager",
"share": 1,
"write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "All",
- "share": 1,
- "write": 1
}
],
"quick_entry": 1,
diff --git a/frappe/integrations/doctype/google_settings/google_settings.py b/frappe/integrations/doctype/google_settings/google_settings.py
index ecc975235a..db65abdb65 100644
--- a/frappe/integrations/doctype/google_settings/google_settings.py
+++ b/frappe/integrations/doctype/google_settings/google_settings.py
@@ -2,12 +2,26 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
-# import frappe
+import frappe
from frappe.model.document import Document
class GoogleSettings(Document):
pass
def get_auth_url():
- return "https://www.googleapis.com/oauth2/v4/token"
\ No newline at end of file
+ return "https://www.googleapis.com/oauth2/v4/token"
+
+
+@frappe.whitelist()
+def get_file_picker_settings():
+ """Return all the data FileUploader needs to start the Google Drive Picker."""
+ google_settings = frappe.get_single("Google Settings")
+ if not (google_settings.enable and google_settings.google_drive_picker_enabled):
+ return {}
+
+ return {
+ "enabled": True,
+ "appId": google_settings.app_id,
+ "developerKey": google_settings.api_key,
+ "clientId": google_settings.client_id
+ }
diff --git a/frappe/integrations/doctype/google_settings/test_google_settings.py b/frappe/integrations/doctype/google_settings/test_google_settings.py
new file mode 100644
index 0000000000..32d43a323b
--- /dev/null
+++ b/frappe/integrations/doctype/google_settings/test_google_settings.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+
+from .google_settings import get_file_picker_settings
+
+class TestGoogleSettings(unittest.TestCase):
+
+ def setUp(self):
+ settings = frappe.get_single("Google Settings")
+ settings.client_id = "test_client_id"
+ settings.app_id = "test_app_id"
+ settings.api_key = "test_api_key"
+ settings.save()
+
+ def test_picker_disabled(self):
+ """Google Drive Picker should be disabled if it is not enabled in Google Settings."""
+ frappe.db.set_value("Google Settings", None, "enable", 1)
+ frappe.db.set_value("Google Settings", None, "google_drive_picker_enabled", 0)
+ settings = get_file_picker_settings()
+
+ self.assertEqual(settings, {})
+
+ def test_google_disabled(self):
+ """Google Drive Picker should be disabled if Google integration is not enabled."""
+ frappe.db.set_value("Google Settings", None, "enable", 0)
+ frappe.db.set_value("Google Settings", None, "google_drive_picker_enabled", 1)
+ settings = get_file_picker_settings()
+
+ self.assertEqual(settings, {})
+
+ def test_picker_enabled(self):
+ """If picker is enabled, get_file_picker_settings should return the credentials."""
+ frappe.db.set_value("Google Settings", None, "enable", 1)
+ frappe.db.set_value("Google Settings", None, "google_drive_picker_enabled", 1)
+ settings = get_file_picker_settings()
+
+ self.assertEqual(True, settings.get("enabled", False))
+ self.assertEqual("test_client_id", settings.get("clientId", ""))
+ self.assertEqual("test_app_id", settings.get("appId", ""))
+ self.assertEqual("test_api_key", settings.get("developerKey", ""))
diff --git a/frappe/integrations/doctype/integration_request/integration_request.py b/frappe/integrations/doctype/integration_request/integration_request.py
index f1d59beb5a..4c4961d96d 100644
--- a/frappe/integrations/doctype/integration_request/integration_request.py
+++ b/frappe/integrations/doctype/integration_request/integration_request.py
@@ -2,11 +2,9 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
import json
-from six import string_types
from frappe.integrations.utils import json_handler
class IntegrationRequest(Document):
@@ -25,14 +23,14 @@ class IntegrationRequest(Document):
def handle_success(self, response):
"""update the output field with the response along with the relevant status"""
- if isinstance(response, string_types):
+ if isinstance(response, str):
response = json.loads(response)
self.db_set("status", "Completed")
self.db_set("output", json.dumps(response, default=json_handler))
def handle_failure(self, response):
"""update the error field with the response along with the relevant status"""
- if isinstance(response, string_types):
+ if isinstance(response, str):
response = json.loads(response)
self.db_set("status", "Failed")
self.db_set("error", json.dumps(response, default=json_handler))
\ No newline at end of file
diff --git a/frappe/integrations/doctype/integration_request/test_integration_request.py b/frappe/integrations/doctype/integration_request/test_integration_request.py
index 6b77b57de4..a26eb4ba93 100644
--- a/frappe/integrations/doctype/integration_request/test_integration_request.py
+++ b/frappe/integrations/doctype/integration_request/test_integration_request.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
diff --git a/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py b/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py
index f9f2adeed0..b6bb77d964 100644
--- a/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py
+++ b/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py
@@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.json b/frappe/integrations/doctype/ldap_settings/ldap_settings.json
index 5d30a873fb..d915ae2ad6 100644
--- a/frappe/integrations/doctype/ldap_settings/ldap_settings.json
+++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2016-09-22 04:16:48.829658",
"doctype": "DocType",
"document_type": "System",
@@ -6,18 +7,24 @@
"engine": "InnoDB",
"field_order": [
"enabled",
- "ldap_server_url",
+ "ldap_server_settings_section",
+ "ldap_directory_server",
"column_break_4",
+ "ldap_server_url",
+ "ldap_auth_section",
"base_dn",
+ "column_break_8",
"password",
- "section_break_5",
- "organizational_unit",
- "default_role",
+ "ldap_search_and_paths_section",
+ "ldap_search_path_user",
"ldap_search_string",
+ "column_break_12",
+ "ldap_search_path_group",
+ "ldap_user_creation_and_mapping_section",
"ldap_email_field",
"ldap_username_field",
- "column_break_11",
"ldap_first_name_field",
+ "column_break_19",
"ldap_middle_name_field",
"ldap_last_name_field",
"ldap_phone_field",
@@ -25,13 +32,18 @@
"ldap_security",
"ssl_tls_mode",
"require_trusted_certificate",
- "column_break_17",
+ "column_break_27",
"local_private_key_file",
"local_server_certificate_file",
"local_ca_certs_file",
+ "ldap_custom_settings_section",
+ "ldap_group_objectclass",
+ "column_break_33",
+ "ldap_group_member_attribute",
"ldap_group_mappings_section",
- "ldap_group_field",
- "ldap_groups"
+ "default_role",
+ "ldap_groups",
+ "ldap_group_field"
],
"fields": [
{
@@ -65,18 +77,6 @@
"label": "Password for Base DN",
"reqd": 1
},
- {
- "fieldname": "section_break_5",
- "fieldtype": "Section Break",
- "label": "LDAP User Creation and Mapping"
- },
- {
- "fieldname": "organizational_unit",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Organizational Unit for Users",
- "reqd": 1
- },
{
"fieldname": "default_role",
"fieldtype": "Link",
@@ -85,6 +85,7 @@
"reqd": 1
},
{
+ "description": "Must be enclosed in '()' and include '{0}', which is a placeholder for the user/login name. i.e. (&(objectclass=user)(uid={0}))",
"fieldname": "ldap_search_string",
"fieldtype": "Data",
"label": "LDAP Search String",
@@ -102,10 +103,6 @@
"label": "LDAP Username Field",
"reqd": 1
},
- {
- "fieldname": "column_break_11",
- "fieldtype": "Column Break"
- },
{
"fieldname": "ldap_first_name_field",
"fieldtype": "Data",
@@ -152,10 +149,6 @@
"options": "No\nYes",
"reqd": 1
},
- {
- "fieldname": "column_break_17",
- "fieldtype": "Column Break"
- },
{
"fieldname": "local_private_key_file",
"fieldtype": "Data",
@@ -177,6 +170,7 @@
"label": "LDAP Group Mappings"
},
{
+ "description": "NOTE: This box is due for depreciation. Please re-setup LDAP to work with the newer settings",
"fieldname": "ldap_group_field",
"fieldtype": "Data",
"label": "LDAP Group Field"
@@ -186,11 +180,93 @@
"fieldtype": "Table",
"label": "LDAP Group Mappings",
"options": "LDAP Group Mapping"
+ },
+ {
+ "fieldname": "ldap_server_settings_section",
+ "fieldtype": "Section Break",
+ "label": "LDAP Server Settings"
+ },
+ {
+ "fieldname": "ldap_auth_section",
+ "fieldtype": "Section Break",
+ "label": "LDAP Auth"
+ },
+ {
+ "fieldname": "column_break_8",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "ldap_search_and_paths_section",
+ "fieldtype": "Section Break",
+ "label": "LDAP Search and Paths"
+ },
+ {
+ "fieldname": "column_break_12",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "ldap_user_creation_and_mapping_section",
+ "fieldtype": "Section Break",
+ "label": "LDAP User Creation and Mapping"
+ },
+ {
+ "fieldname": "column_break_19",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_27",
+ "fieldtype": "Column Break"
+ },
+ {
+ "description": "These settings are required if 'Custom' LDAP Directory is used",
+ "fieldname": "ldap_custom_settings_section",
+ "fieldtype": "Section Break",
+ "label": "LDAP Custom Settings"
+ },
+ {
+ "fieldname": "column_break_33",
+ "fieldtype": "Column Break"
+ },
+ {
+ "description": "string value, i.e. member",
+ "fieldname": "ldap_group_member_attribute",
+ "fieldtype": "Data",
+ "label": "LDAP Group Member attribute"
+ },
+ {
+ "description": "Please select the LDAP Directory being used",
+ "fieldname": "ldap_directory_server",
+ "fieldtype": "Select",
+ "label": "Directory Server",
+ "options": "\nActive Directory\nOpenLDAP\nCustom",
+ "reqd": 1
+ },
+ {
+ "description": "string value, i.e. group",
+ "fieldname": "ldap_group_objectclass",
+ "fieldtype": "Data",
+ "label": "Group Object Class"
+ },
+ {
+ "description": "Requires any valid fdn path. i.e. ou=users,dc=example,dc=com",
+ "fieldname": "ldap_search_path_user",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "LDAP search path for Users",
+ "reqd": 1
+ },
+ {
+ "description": "Requires any valid fdn path. i.e. ou=groups,dc=example,dc=com",
+ "fieldname": "ldap_search_path_group",
+ "fieldtype": "Data",
+ "label": "LDAP search path for Groups",
+ "reqd": 1
}
],
"in_create": 1,
"issingle": 1,
- "modified": "2019-07-15 06:48:16.562109",
+ "links": [],
+ "modified": "2021-07-27 11:51:43.328271",
"modified_by": "Administrator",
"module": "Integrations",
"name": "LDAP Settings",
diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py
index 80dfef2693..7c89c31844 100644
--- a/frappe/integrations/doctype/ldap_settings/ldap_settings.py
+++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py
@@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe import _, safe_encode
from frappe.model.document import Document
@@ -14,10 +13,44 @@ class LDAPSettings(Document):
return
if not self.flags.ignore_mandatory:
- if self.ldap_search_string and self.ldap_search_string.endswith("={0}"):
- self.connect_to_ldap(base_dn=self.base_dn, password=self.get_password(raise_exception=False))
+
+ if self.ldap_search_string.count('(') == self.ldap_search_string.count(')') and \
+ self.ldap_search_string.startswith('(') and \
+ self.ldap_search_string.endswith(')') and \
+ self.ldap_search_string and \
+ "{0}" in self.ldap_search_string:
+
+ conn = self.connect_to_ldap(base_dn=self.base_dn, password=self.get_password(raise_exception=False))
+
+ try:
+ if conn.result['type'] == 'bindResponse' and self.base_dn:
+ import ldap3
+
+ conn.search(
+ search_base=self.ldap_search_path_user,
+ search_filter="(objectClass=*)",
+ attributes=self.get_ldap_attributes())
+
+ conn.search(
+ search_base=self.ldap_search_path_group,
+ search_filter="(objectClass=*)",
+ attributes=['cn'])
+
+ except ldap3.core.exceptions.LDAPAttributeError as ex:
+ frappe.throw(_("LDAP settings incorrect. validation response was: {0}").format(ex),
+ title=_("Misconfigured"))
+
+ except ldap3.core.exceptions.LDAPNoSuchObjectResult:
+ frappe.throw(_("Ensure the user and group search paths are correct."),
+ title=_("Misconfigured"))
+
+ if self.ldap_directory_server.lower() == 'custom':
+ if not self.ldap_group_member_attribute or not self.ldap_group_mappings_section:
+ frappe.throw(_("Custom LDAP Directoy Selected, please ensure 'LDAP Group Member attribute' and 'LDAP Group Mappings' are entered"),
+ title=_("Misconfigured"))
+
else:
- frappe.throw(_("LDAP Search String needs to end with a placeholder, eg sAMAccountName={0}"))
+ frappe.throw(_("LDAP Search String must be enclosed in '()' and needs to contian the user placeholder {0}, eg sAMAccountName={0}"))
def connect_to_ldap(self, base_dn, password, read_only=True):
try:
@@ -80,7 +113,7 @@ class LDAPSettings(Document):
def sync_roles(self, user, additional_groups=None):
- current_roles = set([d.role for d in user.get("roles")])
+ current_roles = set(d.role for d in user.get("roles"))
needed_roles = set()
needed_roles.add(self.default_role)
@@ -119,8 +152,8 @@ class LDAPSettings(Document):
user.insert(ignore_permissions=True)
# always add default role.
user.add_roles(self.default_role)
- if self.ldap_group_field:
- self.sync_roles(user, groups)
+ self.sync_roles(user, groups)
+
return user
def get_ldap_attributes(self):
@@ -143,6 +176,66 @@ class LDAPSettings(Document):
return ldap_attributes
+
+ def fetch_ldap_groups(self, user, conn):
+ import ldap3
+
+ if type(user) is not ldap3.abstract.entry.Entry:
+ raise TypeError("Invalid type, attribute {0} must be of type '{1}'".format('user', 'ldap3.abstract.entry.Entry'))
+
+ if type(conn) is not ldap3.core.connection.Connection:
+ raise TypeError("Invalid type, attribute {0} must be of type '{1}'".format('conn', 'ldap3.Connection'))
+
+ fetch_ldap_groups = None
+
+ ldap_object_class = None
+ ldap_group_members_attribute = None
+
+
+ if self.ldap_directory_server.lower() == 'active directory':
+
+ ldap_object_class = 'Group'
+ ldap_group_members_attribute = 'member'
+ user_search_str = user.entry_dn
+
+
+ elif self.ldap_directory_server.lower() == 'openldap':
+
+ ldap_object_class = 'posixgroup'
+ ldap_group_members_attribute = 'memberuid'
+ user_search_str = getattr(user, self.ldap_username_field).value
+
+ elif self.ldap_directory_server.lower() == 'custom':
+
+ ldap_object_class = self.ldap_group_objectclass
+ ldap_group_members_attribute = self.ldap_group_member_attribute
+ user_search_str = getattr(user, self.ldap_username_field).value
+
+ else:
+ # NOTE: depreciate this else path
+ # this path will be hit for everyone with preconfigured ldap settings. this must be taken into account so as not to break ldap for those users.
+
+ if self.ldap_group_field:
+
+ fetch_ldap_groups = getattr(user, self.ldap_group_field).values
+
+ if ldap_object_class is not None:
+ conn.search(
+ search_base=self.ldap_search_path_group,
+ search_filter="(&(objectClass={0})({1}={2}))".format(ldap_object_class,ldap_group_members_attribute, user_search_str),
+ attributes=['cn']) # Build search query
+
+ if len(conn.entries) >= 1:
+
+ fetch_ldap_groups = []
+ for group in conn.entries:
+ fetch_ldap_groups.append(group['cn'].value)
+
+ return fetch_ldap_groups
+
+
+
+
def authenticate(self, username, password):
if not self.enabled:
@@ -153,23 +246,33 @@ class LDAPSettings(Document):
conn = self.connect_to_ldap(self.base_dn, self.get_password(raise_exception=False))
- conn.search(
- search_base=self.organizational_unit,
- search_filter="({0})".format(user_filter),
- attributes=ldap_attributes)
+ try:
+ import ldap3
- if len(conn.entries) == 1 and conn.entries[0]:
- user = conn.entries[0]
- # only try and connect as the user, once we have their fqdn entry.
- self.connect_to_ldap(base_dn=user.entry_dn, password=password)
+ conn.search(
+ search_base=self.ldap_search_path_user,
+ search_filter="{0}".format(user_filter),
+ attributes=ldap_attributes)
- groups = None
- if self.ldap_group_field:
- groups = getattr(user, self.ldap_group_field).values
- return self.create_or_update_user(self.convert_ldap_entry_to_dict(user), groups=groups)
- else:
+ if len(conn.entries) == 1 and conn.entries[0]:
+ user = conn.entries[0]
+
+ groups = self.fetch_ldap_groups(user, conn)
+
+ # only try and connect as the user, once we have their fqdn entry.
+ if user.entry_dn and password and conn.rebind(user=user.entry_dn, password=password):
+
+ return self.create_or_update_user(self.convert_ldap_entry_to_dict(user), groups=groups)
+
+ raise ldap3.core.exceptions.LDAPInvalidCredentialsResult # even though nothing foundor failed authentication raise invalid credentials
+
+ except ldap3.core.exceptions.LDAPInvalidFilterError:
+ frappe.throw(_("Please use a valid LDAP search filter"), title=_("Misconfigured"))
+
+ except ldap3.core.exceptions.LDAPInvalidCredentialsResult:
frappe.throw(_("Invalid username or password"))
+
def reset_password(self, user, password, logout_sessions=False):
from ldap3 import HASHED_SALTED_SHA, MODIFY_REPLACE
from ldap3.utils.hashed import hashed
@@ -180,7 +283,7 @@ class LDAPSettings(Document):
read_only=False)
if conn.search(
- search_base=self.organizational_unit,
+ search_base=self.ldap_search_path_user,
search_filter=search_filter,
attributes=self.get_ldap_attributes()
):
diff --git a/frappe/integrations/doctype/ldap_settings/test_data_ldif_activedirectory.json b/frappe/integrations/doctype/ldap_settings/test_data_ldif_activedirectory.json
new file mode 100644
index 0000000000..9777452af8
--- /dev/null
+++ b/frappe/integrations/doctype/ldap_settings/test_data_ldif_activedirectory.json
@@ -0,0 +1,338 @@
+{
+ "entries": [
+ {
+ "attributes": {
+ "cn": "base_dn_user",
+ "memberOf": [
+ "cn=Domain Users,ou=Groups,dc=unit,dc=testing",
+ "cn=Enterprise Administrators,ou=Groups,dc=unit,dc=testing"
+ ],
+ "objectClass": [
+ "user",
+ "top",
+ "person",
+ "organizationalPerson"
+ ],
+ "samaccountname": "cn=base_dn_user,dc=unit,dc=testing",
+ "sn": "user_sn",
+ "userPassword": [
+ "my_password"
+ ]
+ },
+ "dn": "cn=base_dn_user,dc=unit,dc=testing",
+ "raw": {
+ "cn": [
+ "base_dn_user"
+ ],
+ "memberOf": [
+ "cn=Domain Users,ou=Groups,dc=unit,dc=testing",
+ "cn=Enterprise Administrators,ou=Groups,dc=unit,dc=testing"
+ ],
+ "objectClass": [
+ "user",
+ "top",
+ "person",
+ "organizationalPerson"
+ ],
+ "samaccountname": [
+ "cn=base_dn_user,dc=unit,dc=testing"
+ ],
+ "sn": [
+ "user_sn"
+ ],
+ "userPassword": [
+ "my_password"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "cn": "Posix User1",
+ "description": [
+ "ACCESS:test1,ACCESS:test2"
+ ],
+ "givenname": "Posix",
+ "mail": "posix.user1@unit.testing",
+ "memberOf": [
+ "cn=Domain Users,ou=Groups,dc=unit,dc=testing",
+ "cn=Domain Administrators,ou=Groups,dc=unit,dc=testing"
+ ],
+ "mobile": "0421 123 456",
+ "objectClass": [
+ "user",
+ "top",
+ "person",
+ "organizationalPerson"
+ ],
+ "samaccountname": "posix.user",
+ "sn": "User1",
+ "telephonenumber": "08 8912 3456",
+ "userpassword": [
+ "posix_user_password"
+ ]
+ },
+ "dn": "cn=Posix User1,ou=Users,dc=unit,dc=testing",
+ "raw": {
+ "cn": [
+ "Posix User1"
+ ],
+ "description": [
+ "ACCESS:test1,ACCESS:test2"
+ ],
+ "givenname": [
+ "Posix"
+ ],
+ "mail": [
+ "posix.user1@unit.testing"
+ ],
+ "memberOf": [
+ "cn=Domain Users,ou=Groups,dc=unit,dc=testing",
+ "cn=Domain Administrators,ou=Groups,dc=unit,dc=testing"
+ ],
+ "mobile": [
+ "0421 123 456"
+ ],
+ "objectClass": [
+ "user",
+ "top",
+ "person",
+ "organizationalPerson"
+ ],
+ "samaccountname": [
+ "posix.user"
+ ],
+ "sn": [
+ "User1"
+ ],
+ "telephonenumber": [
+ "08 8912 3456"
+ ],
+ "userpassword": [
+ "posix_user_password"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "cn": "Posix User2",
+ "description": [
+ "ACCESS:test1,ACCESS:test3"
+ ],
+ "givenname": "Posix",
+ "homedirectory": "/home/users/posix.user2",
+ "mail": "posix.user2@unit.testing",
+ "memberOf": [
+ "cn=Domain Users,ou=Groups,dc=unit,dc=testing",
+ "cn=Enterprise Administrators,ou=Groups,dc=unit,dc=testing"
+ ],
+ "mobile": "0421 456 789",
+ "objectClass": [
+ "user",
+ "top",
+ "person",
+ "organizationalPerson"
+ ],
+ "samaccountname": "posix.user2",
+ "sn": "User2",
+ "telephonenumber": "08 8978 1234",
+ "userpassword": [
+ "posix_user2_password"
+ ]
+ },
+ "dn": "cn=Posix User2,ou=Users,dc=unit,dc=testing",
+ "raw": {
+ "cn": [
+ "Posix User2"
+ ],
+ "description": [
+ "ACCESS:test1,ACCESS:test3"
+ ],
+ "givenname": [
+ "Posix"
+ ],
+ "homedirectory": [
+ "/home/users/posix.user2"
+ ],
+ "mail": [
+ "posix.user2@unit.testing"
+ ],
+ "memberOf": [
+ "cn=Domain Users,ou=Groups,dc=unit,dc=testing",
+ "cn=Enterprise Administrators,ou=Groups,dc=unit,dc=testing"
+ ],
+ "mobile": [
+ "0421 456 789"
+ ],
+ "objectClass": [
+ "user",
+ "top",
+ "person",
+ "organizationalPerson"
+ ],
+ "samaccountname": [
+ "posix.user2"
+ ],
+ "sn": [
+ "User2"
+ ],
+ "telephonenumber": [
+ "08 8978 1234"
+ ],
+ "userpassword": [
+ "posix_user2_password"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "objectClass": [
+ "top",
+ "organizationalUnit"
+ ],
+ "ou": [
+ "Users"
+ ]
+ },
+ "dn": "ou=Users,dc=unit,dc=testing",
+ "raw": {
+ "objectClass": [
+ "top",
+ "organizationalUnit"
+ ],
+ "ou": [
+ "Users"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "Member": [
+ "cn=Posix User2,ou=Users,dc=unit,dc=testing"
+ ],
+ "cn": "Enterprise Administrators",
+ "description": [
+ "group contains only posix.user2"
+ ],
+ "groupType": 2147483652,
+ "objectClass": [
+ "top",
+ "group"
+ ]
+ },
+ "dn": "cn=Enterprise Administrators,ou=Groups,dc=unit,dc=testing",
+ "raw": {
+ "Member": [
+ "cn=Posix User2,ou=Users,dc=unit,dc=testing"
+ ],
+ "cn": [
+ "Enterprise Administrators"
+ ],
+ "description": [
+ "group contains only posix.user2"
+ ],
+ "groupType": [
+ "2147483652"
+ ],
+ "objectClass": [
+ "top",
+ "group"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "Member": [
+ "cn=Posix User1,ou=Users,dc=unit,dc=testing",
+ "cn=Posix User2,ou=Users,dc=unit,dc=testing"
+ ],
+ "cn": "Domain Users",
+ "description": [
+ "group2 Users contains only posix.user and posix.user2"
+ ],
+ "groupType": 2147483652,
+ "objectClass": [
+ "top",
+ "group"
+ ]
+ },
+ "dn": "cn=Domain Users,ou=Groups,dc=unit,dc=testing",
+ "raw": {
+ "Member": [
+ "cn=Posix User1,ou=Users,dc=unit,dc=testing",
+ "cn=Posix User2,ou=Users,dc=unit,dc=testing"
+ ],
+ "cn": [
+ "Domain Users"
+ ],
+ "description": [
+ "group2 Users contains only posix.user and posix.user2"
+ ],
+ "groupType": [
+ "2147483652"
+ ],
+ "objectClass": [
+ "top",
+ "group"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "Member": [
+ "cn=Posix User1,ou=Users,dc=unit,dc=testing",
+ "cn=base_dn_user,dc=unit,dc=testing"
+ ],
+ "cn": "Domain Administrators",
+ "description": [
+ "group1 Administrators contains only posix.user only"
+ ],
+ "groupType": 2147483652,
+ "objectClass": [
+ "top",
+ "group"
+ ]
+ },
+ "dn": "cn=Domain Administrators,ou=Groups,dc=unit,dc=testing",
+ "raw": {
+ "Member": [
+ "cn=Posix User1,ou=Users,dc=unit,dc=testing",
+ "cn=base_dn_user,dc=unit,dc=testing"
+ ],
+ "cn": [
+ "Domain Administrators"
+ ],
+ "description": [
+ "group1 Administrators contains only posix.user only"
+ ],
+ "groupType": [
+ "2147483652"
+ ],
+ "objectClass": [
+ "top",
+ "group"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "objectClass": [
+ "top",
+ "organizationalUnit"
+ ],
+ "ou": [
+ "Groups"
+ ]
+ },
+ "dn": "ou=Groups,dc=unit,dc=testing",
+ "raw": {
+ "objectClass": [
+ "top",
+ "organizationalUnit"
+ ],
+ "ou": [
+ "Groups"
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/frappe/integrations/doctype/ldap_settings/test_data_ldif_openldap.json b/frappe/integrations/doctype/ldap_settings/test_data_ldif_openldap.json
new file mode 100644
index 0000000000..86a76c1abc
--- /dev/null
+++ b/frappe/integrations/doctype/ldap_settings/test_data_ldif_openldap.json
@@ -0,0 +1,400 @@
+{
+ "entries": [
+ {
+ "attributes": {
+ "cn": [
+ "base_dn_user"
+ ],
+ "objectClass": [
+ "simpleSecurityObject",
+ "organizationalRole",
+ "top"
+ ],
+ "sn": [
+ "user_sn"
+ ],
+ "userPassword": [
+ "my_password"
+ ]
+ },
+ "dn": "cn=base_dn_user,dc=unit,dc=testing",
+ "raw": {
+ "cn": [
+ "base_dn_user"
+ ],
+ "objectClass": [
+ "simpleSecurityObject",
+ "organizationalRole",
+ "top"
+ ],
+ "sn": [
+ "user_sn"
+ ],
+ "userPassword": [
+ "my_password"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "cn": [
+ "Posix User2"
+ ],
+ "description": [
+ "ACCESS:test1,ACCESS:test3"
+ ],
+ "gidnumber": 501,
+ "givenname": [
+ "Posix2"
+ ],
+ "homedirectory": "/home/users/posix.user2",
+ "mail": [
+ "posix.user2@unit.testing"
+ ],
+ "mobile": [
+ "0421 456 789"
+ ],
+ "objectClass": [
+ "posixAccount",
+ "top",
+ "inetOrgPerson",
+ "person",
+ "organizationalPerson"
+ ],
+ "sn": [
+ "User2"
+ ],
+ "telephonenumber": [
+ "08 8978 1234"
+ ],
+ "uid": [
+ "posix.user2"
+ ],
+ "uidnumber": 1000,
+ "userpassword": [
+ "posix_user2_password"
+ ]
+ },
+ "dn": "cn=Posix User2,ou=users,dc=unit,dc=testing",
+ "raw": {
+ "cn": [
+ "Posix User2"
+ ],
+ "description": [
+ "ACCESS:test1,ACCESS:test3"
+ ],
+ "gidnumber": [
+ "501"
+ ],
+ "givenname": [
+ "Posix2"
+ ],
+ "homedirectory": [
+ "/home/users/posix.user2"
+ ],
+ "mail": [
+ "posix.user2@unit.testing"
+ ],
+ "mobile": [
+ "0421 456 789"
+ ],
+ "objectClass": [
+ "posixAccount",
+ "top",
+ "inetOrgPerson",
+ "person",
+ "organizationalPerson"
+ ],
+ "sn": [
+ "User2"
+ ],
+ "telephonenumber": [
+ "08 8978 1234"
+ ],
+ "uid": [
+ "posix.user2"
+ ],
+ "uidnumber": [
+ "1000"
+ ],
+ "userpassword": [
+ "posix_user2_password"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "cn": [
+ "Posix User1"
+ ],
+ "description": [
+ "ACCESS:test1,ACCESS:test2"
+ ],
+ "gidnumber": 501,
+ "givenname": [
+ "Posix"
+ ],
+ "homedirectory": "/home/users/posix.user",
+ "mail": [
+ "posix.user1@unit.testing"
+ ],
+ "mobile": [
+ "0421 123 456"
+ ],
+ "objectClass": [
+ "posixAccount",
+ "top",
+ "inetOrgPerson",
+ "person",
+ "organizationalPerson"
+ ],
+ "sn": [
+ "User1"
+ ],
+ "telephonenumber": [
+ "08 8912 3456"
+ ],
+ "uid": [
+ "posix.user"
+ ],
+ "uidnumber": 1000,
+ "userpassword": [
+ "posix_user_password"
+ ]
+ },
+ "dn": "cn=Posix User1,ou=users,dc=unit,dc=testing",
+ "raw": {
+ "cn": [
+ "Posix User1"
+ ],
+ "description": [
+ "ACCESS:test1,ACCESS:test2"
+ ],
+ "gidnumber": [
+ "501"
+ ],
+ "givenname": [
+ "Posix"
+ ],
+ "homedirectory": [
+ "/home/users/posix.user"
+ ],
+ "mail": [
+ "posix.user1@unit.testing"
+ ],
+ "mobile": [
+ "0421 123 456"
+ ],
+ "objectClass": [
+ "posixAccount",
+ "top",
+ "inetOrgPerson",
+ "person",
+ "organizationalPerson"
+ ],
+ "sn": [
+ "User1"
+ ],
+ "telephonenumber": [
+ "08 8912 3456"
+ ],
+ "uid": [
+ "posix.user"
+ ],
+ "uidnumber": [
+ "1000"
+ ],
+ "userpassword": [
+ "posix_user_password"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "objectClass": [
+ "top",
+ "organizationalUnit"
+ ],
+ "ou": [
+ "Users",
+ "users"
+ ]
+ },
+ "dn": "ou=users,dc=unit,dc=testing",
+ "raw": {
+ "objectClass": [
+ "top",
+ "organizationalUnit"
+ ],
+ "ou": [
+ "Users",
+ "users"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "dc": "testing",
+ "o": [
+ "Testing"
+ ],
+ "objectClass": [
+ "top",
+ "organization",
+ "dcObject"
+ ]
+ },
+ "dn": "dc=unit,dc=testing",
+ "raw": {
+ "dc": [
+ "testing",
+ "unit"
+ ],
+ "o": [
+ "Testing"
+ ],
+ "objectClass": [
+ "top",
+ "organization",
+ "dcObject"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "cn": [
+ "Users"
+ ],
+ "description": [
+ "group2 Users contains only posix.user and posix.user2"
+ ],
+ "gidnumber": 501,
+ "memberuid": [
+ "posix.user2",
+ "posix.user"
+ ],
+ "objectClass": [
+ "top",
+ "posixGroup"
+ ]
+ },
+ "dn": "cn=Users,ou=groups,dc=unit,dc=testing",
+ "raw": {
+ "cn": [
+ "Users"
+ ],
+ "description": [
+ "group2 Users contains only posix.user and posix.user2"
+ ],
+ "gidnumber": [
+ "501"
+ ],
+ "memberuid": [
+ "posix.user2",
+ "posix.user"
+ ],
+ "objectClass": [
+ "top",
+ "posixGroup"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "cn": [
+ "Administrators"
+ ],
+ "description": [
+ "group1 Administrators contains only posix.user only"
+ ],
+ "gidnumber": 500,
+ "memberuid": [
+ "posix.user"
+ ],
+ "objectClass": [
+ "top",
+ "posixGroup"
+ ]
+ },
+ "dn": "cn=Administrators,ou=groups,dc=unit,dc=testing",
+ "raw": {
+ "cn": [
+ "Administrators"
+ ],
+ "description": [
+ "group1 Administrators contains only posix.user only"
+ ],
+ "gidnumber": [
+ "500"
+ ],
+ "memberuid": [
+ "posix.user"
+ ],
+ "objectClass": [
+ "top",
+ "posixGroup"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "cn": [
+ "Group3"
+ ],
+ "description": [
+ "group3 Group3 contains only posix.user2 only"
+ ],
+ "gidnumber": 502,
+ "memberuid": [
+ "posix.user2"
+ ],
+ "objectClass": [
+ "top",
+ "posixGroup"
+ ]
+ },
+ "dn": "cn=Group3,ou=groups,dc=unit,dc=testing",
+ "raw": {
+ "cn": [
+ "Group3"
+ ],
+ "description": [
+ "group3 Group3 contains only posix.user2 only"
+ ],
+ "gidnumber": [
+ "502"
+ ],
+ "memberuid": [
+ "posix.user2"
+ ],
+ "objectClass": [
+ "top",
+ "posixGroup"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "objectClass": [
+ "top",
+ "organizationalUnit"
+ ],
+ "ou": [
+ "Users",
+ "groups"
+ ]
+ },
+ "dn": "ou=groups,dc=unit,dc=testing",
+ "raw": {
+ "objectClass": [
+ "top",
+ "organizationalUnit"
+ ],
+ "ou": [
+ "Users",
+ "groups"
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py b/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py
index e6cf4eef3a..1b2a9b155f 100644
--- a/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py
+++ b/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py
@@ -1,10 +1,684 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
-# import frappe
+import frappe
import unittest
+import functools
+import ldap3
+import ssl
+import os
+
+from unittest import mock
+from frappe.integrations.doctype.ldap_settings.ldap_settings import LDAPSettings
+from ldap3 import Server, Connection, MOCK_SYNC, OFFLINE_SLAPD_2_4, OFFLINE_AD_2012_R2
+
+
+class LDAP_TestCase():
+ TEST_LDAP_SERVER = None # must match the 'LDAP Settings' field option
+ TEST_LDAP_SEARCH_STRING = None
+ LDAP_USERNAME_FIELD = None
+ DOCUMENT_GROUP_MAPPINGS = []
+ LDAP_SCHEMA = None
+ LDAP_LDIF_JSON = None
+ TEST_VALUES_LDAP_COMPLEX_SEARCH_STRING = None
+
+ def mock_ldap_connection(f):
+
+ @functools.wraps(f)
+ def wrapped(self, *args, **kwargs):
+
+ with mock.patch('frappe.integrations.doctype.ldap_settings.ldap_settings.LDAPSettings.connect_to_ldap') as mock_connection:
+ mock_connection.return_value = self.connection
+
+ self.test_class = LDAPSettings(self.doc)
+
+ # Create a clean doc
+ localdoc = self.doc.copy()
+ frappe.get_doc(localdoc).save()
+
+ rv = f(self, *args, **kwargs)
+
+
+ # Clean-up
+ self.test_class = None
+
+ return rv
+
+ return wrapped
+
+ def clean_test_users():
+ try: # clean up test user 1
+ frappe.get_doc("User", 'posix.user1@unit.testing').delete()
+ except Exception:
+ pass
+
+ try: # clean up test user 2
+ frappe.get_doc("User", 'posix.user2@unit.testing').delete()
+ except Exception:
+ pass
+
+
+ @classmethod
+ def setUpClass(self, ldapServer='OpenLDAP'):
+
+ self.clean_test_users()
+ # Save user data for restoration in tearDownClass()
+ self.user_ldap_settings = frappe.get_doc('LDAP Settings')
+
+ # Create test user1
+ self.user1doc = {
+ 'username': 'posix.user',
+ 'email': 'posix.user1@unit.testing',
+ 'first_name': 'posix'
+ }
+ self.user1doc.update({
+ "doctype": "User",
+ "send_welcome_email": 0,
+ "language": "",
+ "user_type": "System User",
+ })
+
+ user = frappe.get_doc(self.user1doc)
+ user.insert(ignore_permissions=True)
+
+ # Create test user1
+ self.user2doc = {
+ 'username': 'posix.user2',
+ 'email': 'posix.user2@unit.testing',
+ 'first_name': 'posix'
+ }
+ self.user2doc.update({
+ "doctype": "User",
+ "send_welcome_email": 0,
+ "language": "",
+ "user_type": "System User",
+ })
+
+ user = frappe.get_doc(self.user2doc)
+ user.insert(ignore_permissions=True)
+
+
+ # Setup Mock OpenLDAP Directory
+ self.ldap_dc_path = 'dc=unit,dc=testing'
+ self.ldap_user_path = 'ou=users,' + self.ldap_dc_path
+ self.ldap_group_path = 'ou=groups,' + self.ldap_dc_path
+ self.base_dn = 'cn=base_dn_user,' + self.ldap_dc_path
+ self.base_password = 'my_password'
+ self.ldap_server = 'ldap://my_fake_server:389'
+
+
+ self.doc = {
+ "doctype": "LDAP Settings",
+ "enabled": True,
+ "ldap_directory_server": self.TEST_LDAP_SERVER,
+ "ldap_server_url": self.ldap_server,
+ "base_dn": self.base_dn,
+ "password": self.base_password,
+ "ldap_search_path_user": self.ldap_user_path,
+ "ldap_search_string": self.TEST_LDAP_SEARCH_STRING,
+ "ldap_search_path_group": self.ldap_group_path,
+ "ldap_user_creation_and_mapping_section": '',
+ "ldap_email_field": 'mail',
+ "ldap_username_field": self.LDAP_USERNAME_FIELD,
+ "ldap_first_name_field": 'givenname',
+ "ldap_middle_name_field": '',
+ "ldap_last_name_field": 'sn',
+ "ldap_phone_field": 'telephonenumber',
+ "ldap_mobile_field": 'mobile',
+ "ldap_security": '',
+ "ssl_tls_mode": '',
+ "require_trusted_certificate": 'No',
+ "local_private_key_file": '',
+ "local_server_certificate_file": '',
+ "local_ca_certs_file": '',
+ "ldap_group_objectclass": '',
+ "ldap_group_member_attribute": '',
+ "default_role": 'Newsletter Manager',
+ "ldap_groups": self.DOCUMENT_GROUP_MAPPINGS,
+ "ldap_group_field": ''}
+
+ self.server = Server(host=self.ldap_server, port=389, get_info=self.LDAP_SCHEMA)
+
+ self.connection = Connection(
+ self.server,
+ user=self.base_dn,
+ password=self.base_password,
+ read_only=True,
+ client_strategy=MOCK_SYNC)
+
+ self.connection.strategy.entries_from_json(os.path.abspath(os.path.dirname(__file__)) + '/' + self.LDAP_LDIF_JSON)
+
+ self.connection.bind()
+
+
+ @classmethod
+ def tearDownClass(self):
+ try:
+ frappe.get_doc('LDAP Settings').delete()
+
+ except Exception:
+ pass
+
+ try:
+ # return doc back to user data
+ self.user_ldap_settings.save()
+
+ except Exception:
+ pass
+
+ # Clean-up test users
+ self.clean_test_users()
+
+ # Clear OpenLDAP connection
+ self.connection = None
+
+
+ @mock_ldap_connection
+ def test_mandatory_fields(self):
+
+ mandatory_fields = [
+ 'ldap_server_url',
+ 'ldap_directory_server',
+ 'base_dn',
+ 'password',
+ 'ldap_search_path_user',
+ 'ldap_search_path_group',
+ 'ldap_search_string',
+ 'ldap_email_field',
+ 'ldap_username_field',
+ 'ldap_first_name_field',
+ 'require_trusted_certificate',
+ 'default_role'
+ ] # fields that are required to have ldap functioning need to be mandatory
+
+ for mandatory_field in mandatory_fields:
+
+ localdoc = self.doc.copy()
+ localdoc[mandatory_field] = ''
+
+ try:
+
+ frappe.get_doc(localdoc).save()
+
+ self.fail('Document LDAP Settings field [{0}] is not mandatory'.format(mandatory_field))
+
+ except frappe.exceptions.MandatoryError:
+ pass
+
+ except frappe.exceptions.ValidationError:
+ if mandatory_field == 'ldap_search_string':
+ # additional validation is done on this field, pass in this instance
+ pass
+
+
+ for non_mandatory_field in self.doc: # Ensure remaining fields have not been made mandatory
+
+ if non_mandatory_field == 'doctype' or non_mandatory_field in mandatory_fields:
+ continue
+
+ localdoc = self.doc.copy()
+ localdoc[non_mandatory_field] = ''
+
+ try:
+
+ frappe.get_doc(localdoc).save()
+
+ except frappe.exceptions.MandatoryError:
+ self.fail('Document LDAP Settings field [{0}] should not be mandatory'.format(non_mandatory_field))
+
+
+ @mock_ldap_connection
+ def test_validation_ldap_search_string(self):
+
+ invalid_ldap_search_strings = [
+ '',
+ 'uid={0}',
+ '(uid={0}',
+ 'uid={0})',
+ '(&(objectclass=posixgroup)(uid={0})',
+ '&(objectclass=posixgroup)(uid={0}))',
+ '(uid=no_placeholder)'
+ ] # ldap search string must be enclosed in '()' for ldap search to work for finding user and have the same number of opening and closing brackets.
+
+ for invalid_search_string in invalid_ldap_search_strings:
+
+ localdoc = self.doc.copy()
+ localdoc['ldap_search_string'] = invalid_search_string
+
+ try:
+ frappe.get_doc(localdoc).save()
+
+ self.fail("LDAP search string [{0}] should not validate".format(invalid_search_string))
+
+ except frappe.exceptions.ValidationError:
+ pass
+
+
+ def test_connect_to_ldap(self):
+
+ # setup a clean doc with ldap disabled so no validation occurs (this is tested seperatly)
+ local_doc = self.doc.copy()
+ local_doc['enabled'] = False
+ self.test_class = LDAPSettings(self.doc)
+
+ with mock.patch('ldap3.Server') as ldap3_server_method:
+
+ with mock.patch('ldap3.Connection') as ldap3_connection_method:
+ ldap3_connection_method.return_value = self.connection
+
+ with mock.patch('ldap3.Tls') as ldap3_Tls_method:
+
+ function_return = self.test_class.connect_to_ldap(base_dn=self.base_dn, password=self.base_password)
+
+ args, kwargs = ldap3_connection_method.call_args
+
+ prevent_connection_parameters = {
+ # prevent these parameters for security or lack of the und user from being able to configure
+ 'mode': {
+ 'IP_V4_ONLY': 'Locks the user to IPv4 without frappe providing a way to configure',
+ 'IP_V6_ONLY': 'Locks the user to IPv6 without frappe providing a way to configure'
+ },
+ 'auto_bind': {
+ 'NONE': 'ldap3.Connection must autobind with base_dn',
+ 'NO_TLS': 'ldap3.Connection must have TLS',
+ 'TLS_AFTER_BIND': '[Security] ldap3.Connection TLS bind must occur before bind'
+ }
+ }
+
+ for connection_arg in kwargs:
+
+ if connection_arg in prevent_connection_parameters and \
+ kwargs[connection_arg] in prevent_connection_parameters[connection_arg]:
+
+ self.fail('ldap3.Connection was called with {0}, failed reason: [{1}]'.format(
+ kwargs[connection_arg],
+ prevent_connection_parameters[connection_arg][kwargs[connection_arg]]))
+
+ if local_doc['require_trusted_certificate'] == 'Yes':
+ tls_validate = ssl.CERT_REQUIRED
+ tls_version = ssl.PROTOCOL_TLSv1
+ tls_configuration = ldap3.Tls(validate=tls_validate, version=tls_version)
+
+ self.assertTrue(kwargs['auto_bind'] == ldap3.AUTO_BIND_TLS_BEFORE_BIND,
+ 'Security: [ldap3.Connection] autobind TLS before bind with value ldap3.AUTO_BIND_TLS_BEFORE_BIND')
+
+ else:
+ tls_validate = ssl.CERT_NONE
+ tls_version = ssl.PROTOCOL_TLSv1
+ tls_configuration = ldap3.Tls(validate=tls_validate, version=tls_version)
+
+ self.assertTrue(kwargs['auto_bind'],
+ 'ldap3.Connection must autobind')
+
+
+ ldap3_Tls_method.assert_called_with(validate=tls_validate, version=tls_version)
+
+ ldap3_server_method.assert_called_with(host=self.doc['ldap_server_url'], tls=tls_configuration)
+
+ self.assertTrue(kwargs['password'] == self.base_password,
+ 'ldap3.Connection password does not match provided password')
+
+ self.assertTrue(kwargs['raise_exceptions'],
+ 'ldap3.Connection must raise exceptions for error handling')
+
+ self.assertTrue(kwargs['user'] == self.base_dn,
+ 'ldap3.Connection user does not match provided user')
+
+ ldap3_connection_method.assert_called_with(server=ldap3_server_method.return_value,
+ auto_bind=True,
+ password=self.base_password,
+ raise_exceptions=True,
+ read_only=True,
+ user=self.base_dn)
+
+ self.assertTrue(type(function_return) is ldap3.core.connection.Connection,
+ 'The return type must be of ldap3.Connection')
+
+ function_return = self.test_class.connect_to_ldap(base_dn=self.base_dn, password=self.base_password, read_only=False)
+
+ args, kwargs = ldap3_connection_method.call_args
+
+ self.assertFalse(kwargs['read_only'], 'connect_to_ldap() read_only parameter supplied as False but does not match the ldap3.Connection() read_only named parameter')
+
+
+
+
+ @mock_ldap_connection
+ def test_get_ldap_client_settings(self):
+
+ result = self.test_class.get_ldap_client_settings()
+
+ self.assertIsInstance(result, dict)
+
+ self.assertTrue(result['enabled'] == self.doc['enabled']) # settings should match doc
+
+ localdoc = self.doc.copy()
+ localdoc['enabled'] = False
+ frappe.get_doc(localdoc).save()
+
+ result = self.test_class.get_ldap_client_settings()
+
+ self.assertFalse(result['enabled']) # must match the edited doc
+
+
+ @mock_ldap_connection
+ def test_update_user_fields(self):
+
+ test_user_data = {
+ 'username': 'posix.user',
+ 'email': 'posix.user1@unit.testing',
+ 'first_name': 'posix',
+ 'middle_name': 'another',
+ 'last_name': 'user',
+ 'phone': '08 1234 5678',
+ 'mobile_no': '0421 123 456'
+ }
+
+ test_user = frappe.get_doc("User", test_user_data['email'])
+
+ self.test_class.update_user_fields(test_user, test_user_data)
+
+ updated_user = frappe.get_doc("User", test_user_data['email'])
+
+ self.assertTrue(updated_user.middle_name == test_user_data['middle_name'])
+ self.assertTrue(updated_user.last_name == test_user_data['last_name'])
+ self.assertTrue(updated_user.phone == test_user_data['phone'])
+ self.assertTrue(updated_user.mobile_no == test_user_data['mobile_no'])
+
+
+ @mock_ldap_connection
+ def test_sync_roles(self):
+
+ if self.TEST_LDAP_SERVER.lower() == 'openldap':
+ test_user_data = {
+ 'posix.user1': ['Users', 'Administrators', 'default_role', 'frappe_default_all','frappe_default_guest'],
+ 'posix.user2': ['Users', 'Group3', 'default_role', 'frappe_default_all', 'frappe_default_guest']
+ }
+
+ elif self.TEST_LDAP_SERVER.lower() == 'active directory':
+ test_user_data = {
+ 'posix.user1': ['Domain Users', 'Domain Administrators', 'default_role', 'frappe_default_all','frappe_default_guest'],
+ 'posix.user2': ['Domain Users', 'Enterprise Administrators', 'default_role', 'frappe_default_all', 'frappe_default_guest']
+ }
+
+
+ role_to_group_map = {
+ self.doc['ldap_groups'][0]['erpnext_role']: self.doc['ldap_groups'][0]['ldap_group'],
+ self.doc['ldap_groups'][1]['erpnext_role']: self.doc['ldap_groups'][1]['ldap_group'],
+ self.doc['ldap_groups'][2]['erpnext_role']: self.doc['ldap_groups'][2]['ldap_group'],
+ 'Newsletter Manager': 'default_role',
+ 'All': 'frappe_default_all',
+ 'Guest': 'frappe_default_guest',
+
+ }
+
+ # re-create user1 to ensure clean
+ frappe.get_doc("User", 'posix.user1@unit.testing').delete()
+ user = frappe.get_doc(self.user1doc)
+ user.insert(ignore_permissions=True)
+
+ for test_user in test_user_data:
+
+ test_user_doc = frappe.get_doc("User", test_user + '@unit.testing')
+ test_user_roles = frappe.get_roles(test_user + '@unit.testing')
+
+ self.assertTrue(len(test_user_roles) == 2,
+ 'User should only be a part of the All and Guest roles') # check default frappe roles
+
+ self.test_class.sync_roles(test_user_doc, test_user_data[test_user]) # update user roles
+
+ frappe.get_doc("User", test_user + '@unit.testing')
+ updated_user_roles = frappe.get_roles(test_user + '@unit.testing')
+
+ self.assertTrue(len(updated_user_roles) == len(test_user_data[test_user]),
+ 'syncing of the user roles failed. {0} != {1} for user {2}'.format(len(updated_user_roles), len(test_user_data[test_user]), test_user))
+
+ for user_role in updated_user_roles: # match each users role mapped to ldap groups
+
+ self.assertTrue(role_to_group_map[user_role] in test_user_data[test_user],
+ 'during sync_roles(), the user was given role {0} which should not have occured'.format(user_role))
+
+ @mock_ldap_connection
+ def test_create_or_update_user(self):
+
+ test_user_data = {
+ 'posix.user1': ['Users', 'Administrators', 'default_role', 'frappe_default_all','frappe_default_guest'],
+ }
+
+ test_user = 'posix.user1'
+
+ frappe.get_doc("User", test_user + '@unit.testing').delete() # remove user 1
+
+ with self.assertRaises(frappe.exceptions.DoesNotExistError): # ensure user deleted so function can be tested
+ frappe.get_doc("User", test_user + '@unit.testing')
+
+
+ with mock.patch('frappe.integrations.doctype.ldap_settings.ldap_settings.LDAPSettings.update_user_fields') \
+ as update_user_fields_method:
+
+ update_user_fields_method.return_value = None
+
+
+ with mock.patch('frappe.integrations.doctype.ldap_settings.ldap_settings.LDAPSettings.sync_roles') as sync_roles_method:
+
+ sync_roles_method.return_value = None
+
+ # New user
+ self.test_class.create_or_update_user(self.user1doc, test_user_data[test_user])
+
+ self.assertTrue(sync_roles_method.called, 'User roles need to be updated for a new user')
+ self.assertFalse(update_user_fields_method.called,
+ 'User roles are not required to be updated for a new user, this will occur during logon')
+
+
+ # Existing user
+ self.test_class.create_or_update_user(self.user1doc, test_user_data[test_user])
+
+ self.assertTrue(sync_roles_method.called, 'User roles need to be updated for an existing user')
+ self.assertTrue(update_user_fields_method.called, 'User fields need to be updated for an existing user')
+
+
+ @mock_ldap_connection
+ def test_get_ldap_attributes(self):
+
+ method_return = self.test_class.get_ldap_attributes()
+
+ self.assertTrue(type(method_return) is list)
+
+
+
+ @mock_ldap_connection
+ def test_fetch_ldap_groups(self):
+
+ if self.TEST_LDAP_SERVER.lower() == 'openldap':
+ test_users = {
+ 'posix.user': ['Users', 'Administrators'],
+ 'posix.user2': ['Users', 'Group3']
+
+ }
+ elif self.TEST_LDAP_SERVER.lower() == 'active directory':
+ test_users = {
+ 'posix.user': ['Domain Users', 'Domain Administrators'],
+ 'posix.user2': ['Domain Users', 'Enterprise Administrators']
+
+ }
+
+ for test_user in test_users:
+
+ self.connection.search(
+ search_base=self.ldap_user_path,
+ search_filter=self.TEST_LDAP_SEARCH_STRING.format(test_user),
+ attributes=self.test_class.get_ldap_attributes())
+
+ method_return = self.test_class.fetch_ldap_groups(self.connection.entries[0], self.connection)
+
+ self.assertIsInstance(method_return, list)
+ self.assertTrue(len(method_return) == len(test_users[test_user]))
+
+ for returned_group in method_return:
+
+ self.assertTrue(returned_group in test_users[test_user])
+
+
+
+ @mock_ldap_connection
+ def test_authenticate(self):
+
+ with mock.patch('frappe.integrations.doctype.ldap_settings.ldap_settings.LDAPSettings.fetch_ldap_groups') as \
+ fetch_ldap_groups_function:
+
+ fetch_ldap_groups_function.return_value = None
+
+ self.assertTrue(self.test_class.authenticate('posix.user', 'posix_user_password'))
+
+ self.assertTrue(fetch_ldap_groups_function.called,
+ 'As part of authentication function fetch_ldap_groups_function needs to be called')
+
+ invalid_users = [
+ {'prefix_posix.user': 'posix_user_password'},
+ {'posix.user_postfix': 'posix_user_password'},
+ {'posix.user': 'posix_user_password_postfix'},
+ {'posix.user': 'prefix_posix_user_password'},
+ {'posix.user': ''},
+ {'': 'posix_user_password'},
+ {'': ''}
+ ] # All invalid users should return 'invalid username or password'
+
+ for username, password in enumerate(invalid_users):
+
+ with self.assertRaises(frappe.exceptions.ValidationError) as display_massage:
+
+ self.test_class.authenticate(username, password)
+
+ self.assertTrue(str(display_massage.exception).lower() == 'invalid username or password',
+ 'invalid credentials passed authentication [user: {0}, password: {1}]'.format(username, password))
+
+
+ @mock_ldap_connection
+ def test_complex_ldap_search_filter(self):
+
+ ldap_search_filters = self.TEST_VALUES_LDAP_COMPLEX_SEARCH_STRING
+
+ for search_filter in ldap_search_filters:
+
+ self.test_class.ldap_search_string = search_filter
+
+ if 'ACCESS:test3' in search_filter: # posix.user does not have str in ldap.description auth should fail
+
+ with self.assertRaises(frappe.exceptions.ValidationError) as display_massage:
+
+ self.test_class.authenticate('posix.user', 'posix_user_password')
+
+ self.assertTrue(str(display_massage.exception).lower() == 'invalid username or password')
+
+ else:
+ self.assertTrue(self.test_class.authenticate('posix.user', 'posix_user_password'))
+
+
+ def test_reset_password(self):
+
+ self.test_class = LDAPSettings(self.doc)
+
+ # Create a clean doc
+ localdoc = self.doc.copy()
+
+ localdoc['enabled'] = False
+ frappe.get_doc(localdoc).save()
+
+ with mock.patch('frappe.integrations.doctype.ldap_settings.ldap_settings.LDAPSettings.connect_to_ldap') as connect_to_ldap:
+ connect_to_ldap.return_value = self.connection
+
+ with self.assertRaises(frappe.exceptions.ValidationError) as validation: # Fail if username string used
+ self.test_class.reset_password('posix.user', 'posix_user_password')
+
+ self.assertTrue(str(validation.exception) == 'No LDAP User found for email: posix.user')
+
+ try:
+ self.test_class.reset_password('posix.user1@unit.testing', 'posix_user_password') # Change Password
+
+ except Exception: # An exception from the tested class is ok, as long as the connection to LDAP was made writeable
+ pass
+
+ connect_to_ldap.assert_called_with(self.base_dn, self.base_password, read_only=False)
+
+
+ @mock_ldap_connection
+ def test_convert_ldap_entry_to_dict(self):
+
+ self.connection.search(
+ search_base=self.ldap_user_path,
+ search_filter=self.TEST_LDAP_SEARCH_STRING.format("posix.user"),
+ attributes=self.test_class.get_ldap_attributes())
+
+ test_ldap_entry = self.connection.entries[0]
+
+ method_return = self.test_class.convert_ldap_entry_to_dict(test_ldap_entry)
+
+ self.assertTrue(type(method_return) is dict) # must be dict
+ self.assertTrue(len(method_return) == 6) # there are 6 fields in mock_ldap for use
+
+
+
+class Test_OpenLDAP(LDAP_TestCase, unittest.TestCase):
+ TEST_LDAP_SERVER = 'OpenLDAP'
+ TEST_LDAP_SEARCH_STRING = '(uid={0})'
+ DOCUMENT_GROUP_MAPPINGS = [
+ {
+ "doctype": "LDAP Group Mapping",
+ "ldap_group": "Administrators",
+ "erpnext_role": "System Manager"
+ },
+ {
+ "doctype": "LDAP Group Mapping",
+ "ldap_group": "Users",
+ "erpnext_role": "Blogger"
+ },
+ {
+ "doctype": "LDAP Group Mapping",
+ "ldap_group": "Group3",
+ "erpnext_role": "Accounts User"
+ }
+ ]
+ LDAP_USERNAME_FIELD = 'uid'
+ LDAP_SCHEMA = OFFLINE_SLAPD_2_4
+ LDAP_LDIF_JSON = 'test_data_ldif_openldap.json'
+
+ TEST_VALUES_LDAP_COMPLEX_SEARCH_STRING = [
+ '(uid={0})',
+ '(&(objectclass=posixaccount)(uid={0}))',
+ '(&(description=*ACCESS:test1*)(uid={0}))', # OpenLDAP has no member of group, use description to filter posix.user has equivilent of AD 'memberOf'
+ '(&(objectclass=posixaccount)(description=*ACCESS:test3*)(uid={0}))' # OpenLDAP has no member of group, use description to filter posix.user doesn't have. equivilent of AD 'memberOf'
+ ]
+
+
+class Test_ActiveDirectory(LDAP_TestCase, unittest.TestCase):
+ TEST_LDAP_SERVER = 'Active Directory'
+ TEST_LDAP_SEARCH_STRING = '(samaccountname={0})'
+ DOCUMENT_GROUP_MAPPINGS = [
+ {
+ "doctype": "LDAP Group Mapping",
+ "ldap_group": "Domain Administrators",
+ "erpnext_role": "System Manager"
+ },
+ {
+ "doctype": "LDAP Group Mapping",
+ "ldap_group": "Domain Users",
+ "erpnext_role": "Blogger"
+ },
+ {
+ "doctype": "LDAP Group Mapping",
+ "ldap_group": "Enterprise Administrators",
+ "erpnext_role": "Accounts User"
+ }
+ ]
+ LDAP_USERNAME_FIELD = 'samaccountname'
+ LDAP_SCHEMA = OFFLINE_AD_2012_R2
+ LDAP_LDIF_JSON = 'test_data_ldif_activedirectory.json'
+
+ TEST_VALUES_LDAP_COMPLEX_SEARCH_STRING = [
+ '(samaccountname={0})',
+ '(&(objectclass=user)(samaccountname={0}))',
+ '(&(description=*ACCESS:test1*)(samaccountname={0}))', # OpenLDAP has no member of group, use description to filter posix.user has equivilent of AD 'memberOf'
+ '(&(objectclass=user)(description=*ACCESS:test3*)(samaccountname={0}))' # OpenLDAP has no member of group, use description to filter posix.user doesn't have. equivilent of AD 'memberOf'
+ ]
-class TestLDAPSettings(unittest.TestCase):
- pass
diff --git a/frappe/integrations/doctype/oauth_authorization_code/oauth_authorization_code.py b/frappe/integrations/doctype/oauth_authorization_code/oauth_authorization_code.py
index f08e7eb5bb..0c7f02844c 100644
--- a/frappe/integrations/doctype/oauth_authorization_code/oauth_authorization_code.py
+++ b/frappe/integrations/doctype/oauth_authorization_code/oauth_authorization_code.py
@@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
diff --git a/frappe/integrations/doctype/oauth_authorization_code/test_oauth_authorization_code.py b/frappe/integrations/doctype/oauth_authorization_code/test_oauth_authorization_code.py
index cecf187e61..6084dd64b4 100644
--- a/frappe/integrations/doctype/oauth_authorization_code/test_oauth_authorization_code.py
+++ b/frappe/integrations/doctype/oauth_authorization_code/test_oauth_authorization_code.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
diff --git a/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py b/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py
index 09fd29075b..916d0205d2 100644
--- a/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py
+++ b/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py
@@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
diff --git a/frappe/integrations/doctype/oauth_bearer_token/test_oauth_bearer_token.py b/frappe/integrations/doctype/oauth_bearer_token/test_oauth_bearer_token.py
index af7de360ab..6028cebcf9 100644
--- a/frappe/integrations/doctype/oauth_bearer_token/test_oauth_bearer_token.py
+++ b/frappe/integrations/doctype/oauth_bearer_token/test_oauth_bearer_token.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
diff --git a/frappe/integrations/doctype/oauth_client/oauth_client.py b/frappe/integrations/doctype/oauth_client/oauth_client.py
index 02f5041dfb..0b449ff968 100644
--- a/frappe/integrations/doctype/oauth_client/oauth_client.py
+++ b/frappe/integrations/doctype/oauth_client/oauth_client.py
@@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
diff --git a/frappe/integrations/doctype/oauth_client/test_oauth_client.py b/frappe/integrations/doctype/oauth_client/test_oauth_client.py
index ee119455e5..a4e50e15d8 100644
--- a/frappe/integrations/doctype/oauth_client/test_oauth_client.py
+++ b/frappe/integrations/doctype/oauth_client/test_oauth_client.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
diff --git a/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py b/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py
index 2bf086e0fe..3ab5df92ac 100644
--- a/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py
+++ b/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py
@@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe import _
diff --git a/frappe/integrations/doctype/oauth_scope/oauth_scope.py b/frappe/integrations/doctype/oauth_scope/oauth_scope.py
index a5dfe7e1ce..ae579e6b51 100644
--- a/frappe/integrations/doctype/oauth_scope/oauth_scope.py
+++ b/frappe/integrations/doctype/oauth_scope/oauth_scope.py
@@ -2,7 +2,6 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/integrations/doctype/paypal_settings/paypal_settings.py b/frappe/integrations/doctype/paypal_settings/paypal_settings.py
index efd1b03355..da045d2c6a 100644
--- a/frappe/integrations/doctype/paypal_settings/paypal_settings.py
+++ b/frappe/integrations/doctype/paypal_settings/paypal_settings.py
@@ -63,12 +63,11 @@ More Details:
"""
-from __future__ import unicode_literals
import frappe
import json
import pytz
from frappe import _
-from six.moves.urllib.parse import urlencode
+from urllib.parse import urlencode
from frappe.model.document import Document
from frappe.integrations.utils import create_request_log, make_post_request, create_payment_gateway
from frappe.utils import get_url, call_hook_method, cint, get_datetime
diff --git a/frappe/integrations/doctype/paytm_settings/paytm_settings.py b/frappe/integrations/doctype/paytm_settings/paytm_settings.py
index 616c3837d4..9f15d73f09 100644
--- a/frappe/integrations/doctype/paytm_settings/paytm_settings.py
+++ b/frappe/integrations/doctype/paytm_settings/paytm_settings.py
@@ -2,10 +2,9 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import json
import requests
-from six.moves.urllib.parse import urlencode
+from urllib.parse import urlencode
import frappe
from frappe.model.document import Document
@@ -59,7 +58,7 @@ def get_paytm_params(payment_details, order_id, paytm_config):
# initialize a dictionary
paytm_params = dict()
-
+
redirect_uri = get_request_site_address(True) + "/api/method/frappe.integrations.doctype.paytm_settings.paytm_settings.verify_transaction"
diff --git a/frappe/integrations/doctype/paytm_settings/test_paytm_settings.py b/frappe/integrations/doctype/paytm_settings/test_paytm_settings.py
index 77a16c82ae..a00ce86327 100644
--- a/frappe/integrations/doctype/paytm_settings/test_paytm_settings.py
+++ b/frappe/integrations/doctype/paytm_settings/test_paytm_settings.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
# import frappe
import unittest
diff --git a/frappe/integrations/doctype/query_parameters/query_parameters.py b/frappe/integrations/doctype/query_parameters/query_parameters.py
index bfb8eae0b6..13fb94dbe3 100644
--- a/frappe/integrations/doctype/query_parameters/query_parameters.py
+++ b/frappe/integrations/doctype/query_parameters/query_parameters.py
@@ -2,7 +2,6 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py
index af7686c9b0..d24e15f480 100644
--- a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py
+++ b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py
@@ -60,14 +60,13 @@ For razorpay payment status is Authorized
"""
-from __future__ import unicode_literals
import frappe
from frappe import _
import json
import hmac
import razorpay
import hashlib
-from six.moves.urllib.parse import urlencode
+from urllib.parse import urlencode
from frappe.model.document import Document
from frappe.utils import get_url, call_hook_method, cint, get_timestamp
from frappe.integrations.utils import (make_get_request, make_post_request, create_request_log,
diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py
index 308d34c5c2..1346811652 100755
--- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py
+++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
-
-from __future__ import print_function, unicode_literals
import os
import os.path
import frappe
diff --git a/frappe/integrations/doctype/s3_backup_settings/test_s3_backup_settings.py b/frappe/integrations/doctype/s3_backup_settings/test_s3_backup_settings.py
index 04d90f9b44..3aecdf3489 100755
--- a/frappe/integrations/doctype/s3_backup_settings/test_s3_backup_settings.py
+++ b/frappe/integrations/doctype/s3_backup_settings/test_s3_backup_settings.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import unittest
class TestS3BackupSettings(unittest.TestCase):
diff --git a/frappe/integrations/doctype/slack_webhook_url/slack_webhook_url.py b/frappe/integrations/doctype/slack_webhook_url/slack_webhook_url.py
index 8756d19c88..a970fc1f11 100644
--- a/frappe/integrations/doctype/slack_webhook_url/slack_webhook_url.py
+++ b/frappe/integrations/doctype/slack_webhook_url/slack_webhook_url.py
@@ -2,7 +2,6 @@
# Copyright (c) 2018, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils import get_url_to_form
diff --git a/frappe/integrations/doctype/slack_webhook_url/test_slack_webhook_url.py b/frappe/integrations/doctype/slack_webhook_url/test_slack_webhook_url.py
index a7f9316ddd..4285c2c4bc 100644
--- a/frappe/integrations/doctype/slack_webhook_url/test_slack_webhook_url.py
+++ b/frappe/integrations/doctype/slack_webhook_url/test_slack_webhook_url.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import unittest
class TestSlackWebhookURL(unittest.TestCase):
diff --git a/frappe/integrations/doctype/social_login_key/social_login_key.py b/frappe/integrations/doctype/social_login_key/social_login_key.py
index dffb730513..4a4fcd44f4 100644
--- a/frappe/integrations/doctype/social_login_key/social_login_key.py
+++ b/frappe/integrations/doctype/social_login_key/social_login_key.py
@@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe, json
from frappe import _
from frappe.model.document import Document
diff --git a/frappe/integrations/doctype/social_login_key/test_social_login_key.py b/frappe/integrations/doctype/social_login_key/test_social_login_key.py
index e0b99ad391..23effd6a44 100644
--- a/frappe/integrations/doctype/social_login_key/test_social_login_key.py
+++ b/frappe/integrations/doctype/social_login_key/test_social_login_key.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
from frappe.integrations.doctype.social_login_key.social_login_key import BaseUrlNotSetError
import unittest
diff --git a/frappe/integrations/doctype/social_login_keys/social_login_keys.py b/frappe/integrations/doctype/social_login_keys/social_login_keys.py
index bd4cea01af..da9e21cd8e 100644
--- a/frappe/integrations/doctype/social_login_keys/social_login_keys.py
+++ b/frappe/integrations/doctype/social_login_keys/social_login_keys.py
@@ -1,5 +1,4 @@
# see license
-from __future__ import unicode_literals
from frappe.model.document import Document
class SocialLoginKeys(Document):
diff --git a/frappe/integrations/doctype/stripe_settings/stripe_settings.py b/frappe/integrations/doctype/stripe_settings/stripe_settings.py
index 70ca6002e4..9bb9c60775 100644
--- a/frappe/integrations/doctype/stripe_settings/stripe_settings.py
+++ b/frappe/integrations/doctype/stripe_settings/stripe_settings.py
@@ -2,11 +2,10 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe import _
-from six.moves.urllib.parse import urlencode
+from urllib.parse import urlencode
from frappe.utils import get_url, call_hook_method, cint, flt
from frappe.integrations.utils import make_get_request, make_post_request, create_request_log, create_payment_gateway
diff --git a/frappe/integrations/doctype/stripe_settings/test_stripe_settings.py b/frappe/integrations/doctype/stripe_settings/test_stripe_settings.py
index 39e128192f..ba11c3c38b 100644
--- a/frappe/integrations/doctype/stripe_settings/test_stripe_settings.py
+++ b/frappe/integrations/doctype/stripe_settings/test_stripe_settings.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import unittest
class TestStripeSettings(unittest.TestCase):
diff --git a/frappe/integrations/doctype/token_cache/test_token_cache.py b/frappe/integrations/doctype/token_cache/test_token_cache.py
index 7aa069647d..2ffd57403b 100644
--- a/frappe/integrations/doctype/token_cache/test_token_cache.py
+++ b/frappe/integrations/doctype/token_cache/test_token_cache.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# See license.txt
-from __future__ import unicode_literals
-
import unittest
import frappe
diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py
index 7cac58fae0..3001d12b2b 100644
--- a/frappe/integrations/doctype/token_cache/token_cache.py
+++ b/frappe/integrations/doctype/token_cache/token_cache.py
@@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
from datetime import datetime, timedelta
import frappe
diff --git a/frappe/integrations/doctype/webhook/__init__.py b/frappe/integrations/doctype/webhook/__init__.py
index 19233bd175..b92497f16c 100644
--- a/frappe/integrations/doctype/webhook/__init__.py
+++ b/frappe/integrations/doctype/webhook/__init__.py
@@ -2,8 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
-
import frappe
@@ -21,7 +19,7 @@ def run_webhooks(doc, method):
if webhooks is None:
# query webhooks
webhooks_list = frappe.get_all('Webhook',
- fields=["name", "`condition`", "webhook_docevent", "webhook_doctype"],
+ fields=["name", "`condition`", "webhook_docevent", "webhook_doctype"],
filters={"enabled": True}
)
diff --git a/frappe/integrations/doctype/webhook/test_webhook.py b/frappe/integrations/doctype/webhook/test_webhook.py
index acf2f609e7..1470f666a1 100644
--- a/frappe/integrations/doctype/webhook/test_webhook.py
+++ b/frappe/integrations/doctype/webhook/test_webhook.py
@@ -1,19 +1,19 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import unittest
import frappe
-from frappe.integrations.doctype.webhook.webhook import get_webhook_headers, get_webhook_data
+from frappe.integrations.doctype.webhook.webhook import get_webhook_headers, get_webhook_data, enqueue_webhook
class TestWebhook(unittest.TestCase):
@classmethod
def setUpClass(cls):
# delete any existing webhooks
- frappe.db.sql("DELETE FROM tabWebhook")
+ frappe.db.delete("Webhook")
+ # Delete existing logs if any
+ frappe.db.delete("Webhook Request Log")
# create test webhooks
cls.create_sample_webhooks()
@@ -46,7 +46,7 @@ class TestWebhook(unittest.TestCase):
@classmethod
def tearDownClass(cls):
# delete any existing webhooks
- frappe.db.sql("DELETE FROM tabWebhook")
+ frappe.db.delete("Webhook")
def setUp(self):
# retrieve or create a User webhook for `after_insert`
@@ -86,7 +86,7 @@ class TestWebhook(unittest.TestCase):
# Insert the user to db
self.test_user.insert()
-
+
self.assertTrue("User" in frappe.flags.webhooks)
# only 1 hook (enabled) must be queued
self.assertEqual(
@@ -95,7 +95,7 @@ class TestWebhook(unittest.TestCase):
)
self.assertTrue(self.test_user.email in frappe.flags.webhooks_executed)
self.assertEqual(
- frappe.flags.webhooks_executed.get(self.test_user.email)[0],
+ frappe.flags.webhooks_executed.get(self.test_user.email)[0],
self.sample_webhooks[0].name
)
@@ -164,3 +164,18 @@ class TestWebhook(unittest.TestCase):
data = get_webhook_data(doc=self.user, webhook=self.webhook)
self.assertEqual(data, {"name": self.user.name})
+
+ def test_webhook_req_log_creation(self):
+ if not frappe.db.get_value('User', 'user2@integration.webhooks.test.com'):
+ user = frappe.get_doc({
+ 'doctype': 'User',
+ 'email': 'user2@integration.webhooks.test.com',
+ 'first_name': 'user2'
+ }).insert()
+ else:
+ user = frappe.get_doc('User', 'user2@integration.webhooks.test.com')
+
+ webhook = frappe.get_doc('Webhook', {'webhook_doctype': 'User'})
+ enqueue_webhook(user, webhook)
+
+ self.assertTrue(frappe.db.get_all('Webhook Request Log', pluck='name'))
\ No newline at end of file
diff --git a/frappe/integrations/doctype/webhook/webhook.json b/frappe/integrations/doctype/webhook/webhook.json
index 85895c052c..880874cb25 100644
--- a/frappe/integrations/doctype/webhook/webhook.json
+++ b/frappe/integrations/doctype/webhook/webhook.json
@@ -18,6 +18,7 @@
"html_condition",
"sb_webhook",
"request_url",
+ "request_method",
"cb_webhook",
"request_structure",
"sb_security",
@@ -154,10 +155,18 @@
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled"
+ },
+ {
+ "default": "POST",
+ "fieldname": "request_method",
+ "fieldtype": "Select",
+ "label": "Request Method",
+ "options": "POST\nPUT\nDELETE",
+ "reqd": 1
}
],
"links": [],
- "modified": "2021-04-14 05:35:28.532049",
+ "modified": "2021-05-25 11:11:28.555291",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Webhook",
diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py
index ad64d9f714..e3a8bda2aa 100644
--- a/frappe/integrations/doctype/webhook/webhook.py
+++ b/frappe/integrations/doctype/webhook/webhook.py
@@ -2,8 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
-
import base64
import datetime
import hashlib
@@ -12,7 +10,7 @@ import json
from time import sleep
import requests
-from six.moves.urllib.parse import urlparse
+from urllib.parse import urlparse
import frappe
from frappe import _
@@ -61,7 +59,6 @@ class Webhook(Document):
if self.request_structure == "Form URL-Encoded":
self.webhook_json = None
elif self.request_structure == "JSON":
- validate_json(self.webhook_json)
validate_template(self.webhook_json)
self.webhook_data = []
@@ -85,18 +82,32 @@ def enqueue_webhook(doc, webhook):
for i in range(3):
try:
- r = requests.post(webhook.request_url, data=json.dumps(data, default=str), headers=headers, timeout=5)
+ r = requests.request(method=webhook.request_method, url=webhook.request_url,
+ data=json.dumps(data, default=str), headers=headers, timeout=5)
r.raise_for_status()
frappe.logger().debug({"webhook_success": r.text})
+ log_request(webhook.request_url, headers, data, r)
break
except Exception as e:
frappe.logger().debug({"webhook_error": e, "try": i + 1})
+ log_request(webhook.request_url, headers, data, r)
sleep(3 * i + 1)
if i != 2:
continue
else:
raise e
+def log_request(url, headers, data, res):
+ request_log = frappe.get_doc({
+ "doctype": "Webhook Request Log",
+ "user": frappe.session.user if frappe.session.user else None,
+ "url": url,
+ "headers": json.dumps(headers, indent=4) if headers else None,
+ "data": json.dumps(data, indent=4) if isinstance(data, dict) else data,
+ "response": json.dumps(res.json(), indent=4) if res else None
+ })
+
+ request_log.save(ignore_permissions=True)
def get_webhook_headers(doc, webhook):
headers = {}
@@ -131,10 +142,3 @@ def get_webhook_data(doc, webhook):
data = json.loads(data)
return data
-
-
-def validate_json(string):
- try:
- json.loads(string)
- except (TypeError, ValueError):
- frappe.throw(_("Request Body consists of an invalid JSON structure"), title=_("Invalid JSON"))
diff --git a/frappe/integrations/doctype/webhook_data/webhook_data.py b/frappe/integrations/doctype/webhook_data/webhook_data.py
index b7d989410f..dbd9328482 100644
--- a/frappe/integrations/doctype/webhook_data/webhook_data.py
+++ b/frappe/integrations/doctype/webhook_data/webhook_data.py
@@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/integrations/doctype/webhook_header/webhook_header.py b/frappe/integrations/doctype/webhook_header/webhook_header.py
index 11d3ee4085..428b287db2 100644
--- a/frappe/integrations/doctype/webhook_header/webhook_header.py
+++ b/frappe/integrations/doctype/webhook_header/webhook_header.py
@@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
diff --git a/frappe/patches/v4_2/__init__.py b/frappe/integrations/doctype/webhook_request_log/__init__.py
similarity index 100%
rename from frappe/patches/v4_2/__init__.py
rename to frappe/integrations/doctype/webhook_request_log/__init__.py
diff --git a/frappe/integrations/doctype/webhook_request_log/test_webhook_request_log.py b/frappe/integrations/doctype/webhook_request_log/test_webhook_request_log.py
new file mode 100644
index 0000000000..dd11bf8a01
--- /dev/null
+++ b/frappe/integrations/doctype/webhook_request_log/test_webhook_request_log.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+class TestWebhookRequestLog(unittest.TestCase):
+ pass
diff --git a/frappe/integrations/doctype/webhook_request_log/webhook_request_log.js b/frappe/integrations/doctype/webhook_request_log/webhook_request_log.js
new file mode 100644
index 0000000000..9ec4f11536
--- /dev/null
+++ b/frappe/integrations/doctype/webhook_request_log/webhook_request_log.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Webhook Request Log', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/integrations/doctype/webhook_request_log/webhook_request_log.json b/frappe/integrations/doctype/webhook_request_log/webhook_request_log.json
new file mode 100644
index 0000000000..96690f6e8c
--- /dev/null
+++ b/frappe/integrations/doctype/webhook_request_log/webhook_request_log.json
@@ -0,0 +1,81 @@
+{
+ "actions": [],
+ "autoname": "WEBHOOK-REQ-.#####",
+ "creation": "2021-05-24 21:35:59.104776",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "user",
+ "headers",
+ "data",
+ "column_break_4",
+ "url",
+ "response"
+ ],
+ "fields": [
+ {
+ "fieldname": "url",
+ "fieldtype": "Data",
+ "label": "URL",
+ "read_only": 1
+ },
+ {
+ "fieldname": "headers",
+ "fieldtype": "Code",
+ "label": "Headers",
+ "options": "JSON",
+ "read_only": 1
+ },
+ {
+ "fieldname": "response",
+ "fieldtype": "Code",
+ "label": "Response",
+ "options": "JSON",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "data",
+ "fieldtype": "Code",
+ "label": "Data",
+ "options": "JSON",
+ "read_only": 1
+ },
+ {
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "label": "User",
+ "options": "User",
+ "read_only": 1
+ }
+ ],
+ "in_create": 1,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-05-26 23:57:58.495261",
+ "modified_by": "Administrator",
+ "module": "Integrations",
+ "name": "Webhook Request Log",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/integrations/doctype/webhook_request_log/webhook_request_log.py b/frappe/integrations/doctype/webhook_request_log/webhook_request_log.py
new file mode 100644
index 0000000000..493ebfd8f7
--- /dev/null
+++ b/frappe/integrations/doctype/webhook_request_log/webhook_request_log.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+class WebhookRequestLog(Document):
+ pass
diff --git a/frappe/integrations/oauth2_logins.py b/frappe/integrations/oauth2_logins.py
index 14a6bcc417..c38b43beb7 100644
--- a/frappe/integrations/oauth2_logins.py
+++ b/frappe/integrations/oauth2_logins.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
import frappe.utils
from frappe.utils.oauth import login_via_oauth2, login_via_oauth2_id_token
@@ -33,7 +32,7 @@ def login_via_salesforce(code, state):
@frappe.whitelist(allow_guest=True)
def login_via_fairlogin(code, state):
- login_via_oauth2("fairlogin", code, state, decoder=decoder_compat)
+ login_via_oauth2("fairlogin", code, state, decoder=decoder_compat)
@frappe.whitelist(allow_guest=True)
def custom(code, state):
diff --git a/frappe/integrations/offsite_backup_utils.py b/frappe/integrations/offsite_backup_utils.py
index 48a2c89107..7a263e9d04 100644
--- a/frappe/integrations/offsite_backup_utils.py
+++ b/frappe/integrations/offsite_backup_utils.py
@@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
import glob
import os
diff --git a/frappe/integrations/utils.py b/frappe/integrations/utils.py
index 1af9682073..b897a35062 100644
--- a/frappe/integrations/utils.py
+++ b/frappe/integrations/utils.py
@@ -2,43 +2,20 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
import json,datetime
-from six.moves.urllib.parse import parse_qs
-from six import string_types, text_type
+from urllib.parse import parse_qs
from frappe.utils import get_request_session
from frappe import _
-def make_get_request(url, auth=None, headers=None, data=None):
- if not auth:
- auth = ''
- if not data:
- data = {}
- if not headers:
- headers = {}
+def make_request(method, url, auth=None, headers=None, data=None):
+ auth = auth or ''
+ data = data or {}
+ headers = headers or {}
try:
s = get_request_session()
- frappe.flags.integration_request = s.get(url, data={}, auth=auth, headers=headers)
- frappe.flags.integration_request.raise_for_status()
- return frappe.flags.integration_request.json()
-
- except Exception as exc:
- frappe.log_error(frappe.get_traceback())
- raise exc
-
-def make_post_request(url, auth=None, headers=None, data=None):
- if not auth:
- auth = ''
- if not data:
- data = {}
- if not headers:
- headers = {}
-
- try:
- s = get_request_session()
- frappe.flags.integration_request = s.post(url, data=data, auth=auth, headers=headers)
+ frappe.flags.integration_request = s.request(method, url, data=data, auth=auth, headers=headers)
frappe.flags.integration_request.raise_for_status()
if frappe.flags.integration_request.headers.get("content-type") == "text/plain; charset=utf-8":
@@ -49,11 +26,20 @@ def make_post_request(url, auth=None, headers=None, data=None):
frappe.log_error()
raise exc
+def make_get_request(url, **kwargs):
+ return make_request('GET', url, **kwargs)
+
+def make_post_request(url, **kwargs):
+ return make_request('POST', url, **kwargs)
+
+def make_put_request(url, **kwargs):
+ return make_request('PUT', url, **kwargs)
+
def create_request_log(data, integration_type, service_name, name=None, error=None):
- if isinstance(data, string_types):
+ if isinstance(data, str):
data = json.loads(data)
- if isinstance(error, string_types):
+ if isinstance(error, str):
error = json.loads(error)
integration_request = frappe.get_doc({
@@ -116,4 +102,4 @@ def create_payment_gateway(gateway, settings=None, controller=None):
def json_handler(obj):
if isinstance(obj, (datetime.date, datetime.timedelta, datetime.datetime)):
- return text_type(obj)
+ return str(obj)
diff --git a/frappe/integrations/workspace/integrations/integrations.json b/frappe/integrations/workspace/integrations/integrations.json
index db96304207..4167858db2 100644
--- a/frappe/integrations/workspace/integrations/integrations.json
+++ b/frappe/integrations/workspace/integrations/integrations.json
@@ -1,22 +1,27 @@
{
- "category": "Administration",
+ "category": "",
"charts": [],
+ "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Backup\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Google Services\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Authentication\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Payments\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}]",
"creation": "2020-03-02 15:16:18.714190",
"developer_mode_only": 0,
- "disable_user_customization": 1,
+ "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
+ "extends": "",
"extends_another_page": 0,
+ "for_user": "",
"hide_custom": 0,
"icon": "integration",
"idx": 0,
- "is_standard": 1,
+ "is_default": 0,
+ "is_standard": 0,
"label": "Integrations",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Backup",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -25,6 +30,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Dropbox Settings",
+ "link_count": 0,
"link_to": "Dropbox Settings",
"link_type": "DocType",
"onboard": 0,
@@ -35,6 +41,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "S3 Backup Settings",
+ "link_count": 0,
"link_to": "S3 Backup Settings",
"link_type": "DocType",
"onboard": 0,
@@ -45,6 +52,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Google Drive",
+ "link_count": 0,
"link_to": "Google Drive",
"link_type": "DocType",
"onboard": 0,
@@ -54,6 +62,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Google Services",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -62,6 +71,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Google Settings",
+ "link_count": 0,
"link_to": "Google Settings",
"link_type": "DocType",
"onboard": 0,
@@ -72,6 +82,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Google Contacts",
+ "link_count": 0,
"link_to": "Google Contacts",
"link_type": "DocType",
"onboard": 0,
@@ -82,6 +93,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Google Calendar",
+ "link_count": 0,
"link_to": "Google Calendar",
"link_type": "DocType",
"onboard": 0,
@@ -92,6 +104,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Google Drive",
+ "link_count": 0,
"link_to": "Google Drive",
"link_type": "DocType",
"onboard": 0,
@@ -101,6 +114,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Authentication",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -109,6 +123,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Social Login Key",
+ "link_count": 0,
"link_to": "Social Login Key",
"link_type": "DocType",
"onboard": 0,
@@ -119,6 +134,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "LDAP Settings",
+ "link_count": 0,
"link_to": "LDAP Settings",
"link_type": "DocType",
"onboard": 0,
@@ -129,6 +145,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "OAuth Client",
+ "link_count": 0,
"link_to": "OAuth Client",
"link_type": "DocType",
"onboard": 0,
@@ -139,6 +156,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "OAuth Provider Settings",
+ "link_count": 0,
"link_to": "OAuth Provider Settings",
"link_type": "DocType",
"onboard": 0,
@@ -148,6 +166,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Payments",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -156,6 +175,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Braintree Settings",
+ "link_count": 0,
"link_to": "Braintree Settings",
"link_type": "DocType",
"onboard": 0,
@@ -166,6 +186,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "PayPal Settings",
+ "link_count": 0,
"link_to": "PayPal Settings",
"link_type": "DocType",
"onboard": 0,
@@ -176,6 +197,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Razorpay Settings",
+ "link_count": 0,
"link_to": "Razorpay Settings",
"link_type": "DocType",
"onboard": 0,
@@ -186,6 +208,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Stripe Settings",
+ "link_count": 0,
"link_to": "Stripe Settings",
"link_type": "DocType",
"onboard": 0,
@@ -196,6 +219,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Paytm Settings",
+ "link_count": 0,
"link_to": "Paytm Settings",
"link_type": "DocType",
"onboard": 0,
@@ -205,6 +229,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Settings",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -213,6 +238,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Webhook",
+ "link_count": 0,
"link_to": "Webhook",
"link_type": "DocType",
"onboard": 0,
@@ -223,38 +249,37 @@
"hidden": 0,
"is_query_report": 0,
"label": "Slack Webhook URL",
+ "link_count": 0,
"link_to": "Slack Webhook URL",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Twilio Settings",
- "link_to": "Twilio Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "SMS Settings",
+ "link_count": 0,
"link_to": "SMS Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
- "modified": "2020-12-01 13:38:39.706680",
+ "modified": "2021-08-05 12:16:00.355267",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Integrations",
+ "onboarding": "",
"owner": "Administrator",
+ "parent_page": "",
"pin_to_bottom": 0,
"pin_to_top": 0,
- "shortcuts": []
+ "public": 1,
+ "restrict_to_domain": "",
+ "roles": [],
+ "sequence_id": 15,
+ "shortcuts": [],
+ "title": "Integrations"
}
\ No newline at end of file
diff --git a/frappe/middlewares.py b/frappe/middlewares.py
index 252be56c47..05944ec37a 100644
--- a/frappe/middlewares.py
+++ b/frappe/middlewares.py
@@ -1,8 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
-
import frappe
import os
from werkzeug.exceptions import NotFound
diff --git a/frappe/migrate.py b/frappe/migrate.py
index 619510fe5e..061e4c98d7 100644
--- a/frappe/migrate.py
+++ b/frappe/migrate.py
@@ -1,8 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
-
import json
import os
import sys
@@ -15,7 +13,7 @@ from frappe.utils.connections import check_connection
from frappe.utils.dashboard import sync_dashboards
from frappe.cache_manager import clear_global_cache
from frappe.desk.notifications import clear_notifications
-from frappe.website import render
+from frappe.website.utils import clear_website_cache
from frappe.core.doctype.language.language import sync_languages
from frappe.modules.utils import sync_customizations
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
@@ -78,7 +76,7 @@ Otherwise, check the server logs and ensure that all the required services are r
frappe.get_doc('Portal Settings', 'Portal Settings').sync_menu()
# syncs statics
- render.clear_cache()
+ clear_website_cache()
# updating installed applications data
frappe.get_single('Installed Applications').update_versions()
diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py
index 205b451336..1acd7ee670 100644
--- a/frappe/model/__init__.py
+++ b/frappe/model/__init__.py
@@ -2,7 +2,6 @@
# MIT License. See license.txt
# model __init__.py
-from __future__ import unicode_literals
import frappe
data_fieldtypes = (
@@ -35,7 +34,8 @@ data_fieldtypes = (
'Color',
'Barcode',
'Geolocation',
- 'Duration'
+ 'Duration',
+ 'Icon'
)
no_value_fields = (
@@ -72,7 +72,8 @@ data_field_options = (
'Email',
'Name',
'Phone',
- 'URL'
+ 'URL',
+ 'Barcode'
)
default_fields = (
@@ -153,32 +154,22 @@ def delete_fields(args_dict, delete=0):
if not fields:
continue
- frappe.db.sql("""
- DELETE FROM `tabDocField`
- WHERE parent='%s' AND fieldname IN (%s)
- """ % (dt, ", ".join(["'{}'".format(f) for f in fields])))
+ frappe.db.delete("DocField", {
+ "parent": dt,
+ "fieldname": ("in", fields),
+ })
# Delete the data/column only if delete is specified
if not delete:
continue
if frappe.db.get_value("DocType", dt, "issingle"):
- frappe.db.sql("""
- DELETE FROM `tabSingles`
- WHERE doctype='%s' AND field IN (%s)
- """ % (dt, ", ".join(["'{}'".format(f) for f in fields])))
+ frappe.db.delete("Singles", {
+ "doctype": dt,
+ "field": ("in", fields),
+ })
else:
- existing_fields = frappe.db.multisql({
- "mariadb": "DESC `tab%s`" % dt,
- "postgres": """
- SELECT
- COLUMN_NAME
- FROM
- information_schema.COLUMNS
- WHERE
- TABLE_NAME = 'tab%s';
- """ % dt,
- })
+ existing_fields = frappe.db.describe(dt)
existing_fields = existing_fields and [e[0] for e in existing_fields] or []
fields_need_to_delete = set(fields) & set(existing_fields)
if not fields_need_to_delete:
@@ -189,7 +180,7 @@ def delete_fields(args_dict, delete=0):
frappe.db.commit()
query = "ALTER TABLE `tab%s` " % dt + \
- ", ".join(["DROP COLUMN `%s`" % f for f in fields_need_to_delete])
+ ", ".join("DROP COLUMN `%s`" % f for f in fields_need_to_delete)
frappe.db.sql(query)
if frappe.db.db_type == 'postgres':
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index 54d77ba988..752543f46a 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -1,9 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-
-from __future__ import unicode_literals
-from six import iteritems, string_types
-
import frappe
import datetime
from frappe import _
@@ -87,11 +83,15 @@ class BaseDocument(object):
@property
def meta(self):
- if not hasattr(self, "_meta"):
+ if not getattr(self, "_meta", None):
self._meta = frappe.get_meta(self.doctype)
return self._meta
+ def __getstate__(self):
+ self._meta = None
+ return self.__dict__
+
def update(self, d):
""" Update multiple fields of a doctype using a dictionary of key-value pairs.
@@ -109,7 +109,7 @@ class BaseDocument(object):
if key in d:
self.set(key, d.get(key))
- for key, value in iteritems(d):
+ for key, value in d.items():
self.set(key, value)
return self
@@ -120,7 +120,7 @@ class BaseDocument(object):
if "doctype" in d:
self.set("doctype", d.get("doctype"))
- for key, value in iteritems(d):
+ for key, value in d.items():
# dont_update_if_missing is a list of fieldnames, for which, you don't want to set default value
if (self.get(key) is None) and (value is not None) and (key not in self.dont_update_if_missing):
self.set(key, value)
@@ -358,7 +358,7 @@ class BaseDocument(object):
frappe.db.sql("""INSERT INTO `tab{doctype}` ({columns})
VALUES ({values})""".format(
doctype = self.doctype,
- columns = ", ".join(["`"+c+"`" for c in columns]),
+ columns = ", ".join("`"+c+"`" for c in columns),
values = ", ".join(["%s"] * len(columns))
), list(d.values()))
except Exception as e:
@@ -401,7 +401,7 @@ class BaseDocument(object):
frappe.db.sql("""UPDATE `tab{doctype}`
SET {values} WHERE `name`=%s""".format(
doctype = self.doctype,
- values = ", ".join(["`"+c+"`=%s" for c in columns])
+ values = ", ".join("`"+c+"`=%s" for c in columns)
), list(d.values()) + [name])
except Exception as e:
if frappe.db.is_unique_key_violation(e):
@@ -670,7 +670,7 @@ class BaseDocument(object):
if data_field_options == "URL":
if not data:
continue
-
+
frappe.utils.validate_url(data, throw=True)
def _validate_constants(self):
@@ -705,7 +705,7 @@ class BaseDocument(object):
type_map = frappe.db.type_map
- for fieldname, value in iteritems(self.get_valid_dict()):
+ for fieldname, value in self.get_valid_dict().items():
df = self.meta.get_field(fieldname)
if not df or df.fieldtype == 'Check':
@@ -727,6 +727,18 @@ class BaseDocument(object):
if abs(cint(value)) > max_length:
self.throw_length_exceeded_error(df, max_length, value)
+ def _validate_code_fields(self):
+ for field in self.meta.get_code_fields():
+ code_string = self.get(field.fieldname)
+ language = field.get("options")
+
+ if language == "Python":
+ frappe.utils.validate_python_code(code_string, fieldname=field.label, is_expression=False)
+
+ elif language == "PythonExpression":
+ frappe.utils.validate_python_code(code_string, fieldname=field.label)
+
+
def throw_length_exceeded_error(self, df, max_length, value):
if self.parentfield and self.idx:
reference = _("{0}, Row {1}").format(_(self.doctype), self.idx)
@@ -770,7 +782,7 @@ class BaseDocument(object):
return
for fieldname, value in self.get_valid_dict().items():
- if not value or not isinstance(value, string_types):
+ if not value or not isinstance(value, str):
continue
value = frappe.as_unicode(value)
@@ -839,7 +851,7 @@ class BaseDocument(object):
:param parentfield: If fieldname is in child table."""
from frappe.model.meta import get_field_precision
- if parentfield and not isinstance(parentfield, string_types):
+ if parentfield and not isinstance(parentfield, str):
parentfield = parentfield.parentfield
cache_key = parentfield or "main"
@@ -986,7 +998,7 @@ def _filter(data, filters, limit=None):
fval = ("not None", fval)
elif fval is False:
fval = ("None", fval)
- elif isinstance(fval, string_types) and fval.startswith("^"):
+ elif isinstance(fval, str) and fval.startswith("^"):
fval = ("^", fval[1:])
else:
fval = ("=", fval)
@@ -995,7 +1007,7 @@ def _filter(data, filters, limit=None):
for d in data:
add = True
- for f, fval in iteritems(_filters):
+ for f, fval in _filters.items():
if not frappe.compare(getattr(d, f, None), fval[0], fval[1]):
add = False
break
diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py
index dc4fd97e4c..fba6765479 100644
--- a/frappe/model/create_new.py
+++ b/frappe/model/create_new.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
"""
Create a new document with defaults set
"""
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index e0c3406c46..7ed681644f 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -1,10 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-
-from __future__ import unicode_literals
-
-from six import iteritems, string_types
-
"""build query for doclistview and return results"""
import frappe.defaults
@@ -48,23 +43,26 @@ class DatabaseQuery(object):
# filters and fields swappable
# its hard to remember what comes first
- if (isinstance(fields, dict)
- or (isinstance(fields, list) and fields and isinstance(fields[0], list))):
+ if (
+ isinstance(fields, dict)
+ or (
+ fields
+ and isinstance(fields, list)
+ and isinstance(fields[0], list)
+ )
+ ):
# if fields is given as dict/list of list, its probably filters
filters, fields = fields, filters
elif fields and isinstance(filters, list) \
- and len(filters) > 1 and isinstance(filters[0], string_types):
+ and len(filters) > 1 and isinstance(filters[0], str):
# if `filters` is a list of strings, its probably fields
filters, fields = fields, filters
if fields:
self.fields = fields
else:
- if pluck:
- self.fields = ["`tab{0}`.`{1}`".format(self.doctype, pluck)]
- else:
- self.fields = ["`tab{0}`.`name`".format(self.doctype)]
+ self.fields = [f"`tab{self.doctype}`.`{pluck or 'name'}`"]
if start: limit_start = start
if page_length: limit_page_length = page_length
@@ -75,7 +73,7 @@ class DatabaseQuery(object):
self.docstatus = docstatus or []
self.group_by = group_by
self.order_by = order_by
- self.limit_start = 0 if (limit_start is False) else cint(limit_start)
+ self.limit_start = cint(limit_start)
self.limit_page_length = cint(limit_page_length) if limit_page_length else None
self.with_childnames = with_childnames
self.debug = debug
@@ -162,11 +160,10 @@ class DatabaseQuery(object):
# left join parent, child tables
for child in self.tables[1:]:
- args.tables += " {join} {child} on ({child}.parent = {main}.name)".format(join=self.join,
- child=child, main=self.tables[0])
+ args.tables += f" {self.join} {child} on ({child}.parent = {self.tables[0]}.name)"
if self.grouped_or_conditions:
- self.conditions.append("({0})".format(" or ".join(self.grouped_or_conditions)))
+ self.conditions.append(f"({' or '.join(self.grouped_or_conditions)})")
args.conditions = ' and '.join(self.conditions)
@@ -191,9 +188,9 @@ class DatabaseQuery(object):
fields.append(field)
elif "as" in field.lower().split(" "):
col, _, new = field.split()
- fields.append("`{0}` as {1}".format(col, new))
+ fields.append(f"`{col}` as {new}")
else:
- fields.append("`{0}`".format(field))
+ fields.append(f"`{field}`")
args.fields = ", ".join(fields)
@@ -209,7 +206,7 @@ class DatabaseQuery(object):
def parse_args(self):
"""Convert fields and filters from strings to list, dicts"""
- if isinstance(self.fields, string_types):
+ if isinstance(self.fields, str):
if self.fields == "*":
self.fields = ["*"]
else:
@@ -223,13 +220,13 @@ class DatabaseQuery(object):
for filter_name in ["filters", "or_filters"]:
filters = getattr(self, filter_name)
- if isinstance(filters, string_types):
+ if isinstance(filters, str):
filters = json.loads(filters)
if isinstance(filters, dict):
fdict = filters
filters = []
- for key, value in iteritems(fdict):
+ for key, value in fdict.items():
filters.append(make_filter_tuple(self.doctype, key, value))
setattr(self, filter_name, filters)
@@ -265,10 +262,10 @@ class DatabaseQuery(object):
if any(keyword in field.lower().split() for keyword in blacklisted_keywords):
_raise_exception()
- if any("({0}".format(keyword) in field.lower() for keyword in blacklisted_keywords):
+ if any(f"({keyword}" in field.lower() for keyword in blacklisted_keywords):
_raise_exception()
- if any("{0}(".format(keyword) in field.lower() for keyword in blacklisted_functions):
+ if any(f"{keyword}(" in field.lower() for keyword in blacklisted_functions):
_raise_exception()
if '@' in field.lower():
@@ -292,22 +289,30 @@ class DatabaseQuery(object):
def extract_tables(self):
"""extract tables from fields"""
- self.tables = ['`tab' + self.doctype + '`']
-
+ self.tables = [f"`tab{self.doctype}`"]
+ sql_functions = [
+ "dayofyear(",
+ "extract(",
+ "locate(",
+ "strpos(",
+ "count(",
+ "sum(",
+ "avg(",
+ ]
# add tables from fields
if self.fields:
- for f in self.fields:
- if ( not ("tab" in f and "." in f) ) or ("locate(" in f) or ("strpos(" in f) or \
- ("count(" in f) or ("avg(" in f) or ("sum(" in f) or ("extract(" in f) or ("dayofyear(" in f):
+ 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 = f.split('.')[0]
+ table_name = field.split('.')[0]
+
if table_name.lower().startswith('group_concat('):
table_name = table_name[13:]
if table_name.lower().startswith('ifnull('):
table_name = table_name[7:]
if not table_name[0]=='`':
- table_name = '`' + table_name + '`'
+ table_name = f"`{table_name}`"
if not table_name in self.tables:
self.append_table(table_name)
@@ -316,8 +321,7 @@ class DatabaseQuery(object):
doctype = table_name[4:-1]
ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read'
- if (not self.flags.ignore_permissions) and\
- (not frappe.has_permission(doctype, ptype=ptype)):
+ if not self.flags.ignore_permissions and not frappe.has_permission(doctype, ptype=ptype):
frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(doctype))
raise frappe.PermissionError(doctype)
@@ -331,7 +335,7 @@ class DatabaseQuery(object):
if len(self.tables) > 1:
for idx, field in enumerate(self.fields):
if '.' not in field and not _in_standard_sql_methods(field):
- self.fields[idx] = '{0}.{1}'.format(self.tables[0], field)
+ self.fields[idx] = f"{self.tables[0]}.{field}"
def get_table_columns(self):
try:
@@ -357,7 +361,7 @@ class DatabaseQuery(object):
# remove from filters
to_remove = []
for each in self.filters:
- if isinstance(each, string_types):
+ if isinstance(each, str):
each = [each]
for element in each:
@@ -380,7 +384,7 @@ class DatabaseQuery(object):
if not self.flags.ignore_permissions:
match_conditions = self.build_match_conditions()
if match_conditions:
- self.conditions.append("(" + match_conditions + ")")
+ self.conditions.append(f"({match_conditions})")
def build_filter_conditions(self, filters, conditions, ignore_permissions=None):
"""build conditions from user filters"""
@@ -391,7 +395,7 @@ class DatabaseQuery(object):
filters = [filters]
for f in filters:
- if isinstance(f, string_types):
+ if isinstance(f, str):
conditions.append(f)
else:
conditions.append(self.prepare_filter_condition(f))
@@ -412,8 +416,7 @@ class DatabaseQuery(object):
if 'ifnull(' in f.fieldname:
column_name = f.fieldname
else:
- column_name = '{tname}.{fname}'.format(tname=tname,
- fname=f.fieldname)
+ column_name = f"{tname}.{f.fieldname}"
can_be_null = True
@@ -455,7 +458,7 @@ class DatabaseQuery(object):
fallback = "''"
value = [frappe.db.escape((v.name or '').strip(), percent=False) for v in result]
if len(value):
- value = "({0})".format(", ".join(value))
+ value = f"({', '.join(value)})"
else:
value = "('')"
# changing operator to IN as the above code fetches all the parent / child values and convert into tuple
@@ -471,7 +474,7 @@ class DatabaseQuery(object):
fallback = "''"
value = [frappe.db.escape((v or '').strip(), percent=False) for v in values]
if len(value):
- value = "({0})".format(", ".join(value))
+ value = f"({', '.join(value)})"
else:
value = "('')"
else:
@@ -508,7 +511,7 @@ class DatabaseQuery(object):
can_be_null = True
if 'ifnull' not in column_name:
- column_name = 'ifnull({}, {})'.format(column_name, fallback)
+ column_name = f'ifnull({column_name}, {fallback})'
elif df and df.fieldtype=="Date":
value = frappe.db.format_date(f.value)
@@ -522,12 +525,12 @@ class DatabaseQuery(object):
value = get_time(f.value).strftime("%H:%M:%S.%f")
fallback = "'00:00:00'"
- elif f.operator.lower() in ("like", "not like") or (isinstance(f.value, string_types) and
+ elif f.operator.lower() in ("like", "not like") or (isinstance(f.value, str) and
(not df or df.fieldtype not in ["Float", "Int", "Currency", "Percent", "Check"])):
value = "" if f.value==None else f.value
fallback = "''"
- if f.operator.lower() in ("like", "not like") and isinstance(value, string_types):
+ if f.operator.lower() in ("like", "not like") and isinstance(value, str):
# because "like" uses backslash (\) for escaping
value = value.replace("\\", "\\\\").replace("%", "%%")
@@ -544,22 +547,20 @@ class DatabaseQuery(object):
fallback = 0
# escape value
- if isinstance(value, string_types) and not f.operator.lower() == 'between':
- value = "{0}".format(frappe.db.escape(value, percent=False))
+ if isinstance(value, str) and not f.operator.lower() == 'between':
+ value = f"{frappe.db.escape(value, percent=False)}"
- if (self.ignore_ifnull
+ if (
+ self.ignore_ifnull
or not can_be_null
or (f.value and f.operator.lower() in ('=', 'like'))
- or 'ifnull(' in column_name.lower()):
+ or 'ifnull(' in column_name.lower()
+ ):
if f.operator.lower() == 'like' and frappe.conf.get('db_type') == 'postgres':
f.operator = 'ilike'
- condition = '{column_name} {operator} {value}'.format(
- column_name=column_name, operator=f.operator,
- value=value)
+ condition = f'{column_name} {f.operator} {value}'
else:
- condition = 'ifnull({column_name}, {fallback}) {operator} {value}'.format(
- column_name=column_name, fallback=fallback, operator=f.operator,
- value=value)
+ condition = f'ifnull({column_name}, {fallback}) {f.operator} {value}'
return condition
@@ -577,10 +578,12 @@ class DatabaseQuery(object):
role_permissions = frappe.permissions.get_role_permissions(meta, user=self.user)
self.shared = frappe.share.get_shared(self.doctype, self.user)
- if (not meta.istable and
+ if (
+ not meta.istable and
not (role_permissions.get("select") or role_permissions.get("read")) and
not self.flags.ignore_permissions and
- not has_any_user_permission_for_doctype(self.doctype, self.user, self.reference_doctype)):
+ not has_any_user_permission_for_doctype(self.doctype, self.user, self.reference_doctype)
+ ):
only_if_shared = True
if not self.shared:
frappe.throw(_("No permission to read {0}").format(self.doctype), frappe.PermissionError)
@@ -590,8 +593,10 @@ class DatabaseQuery(object):
else:
#if has if_owner permission skip user perm check
if role_permissions.get("has_if_owner_enabled") and role_permissions.get("if_owner", {}):
- self.match_conditions.append("`tab{0}`.`owner` = {1}".format(self.doctype,
- frappe.db.escape(self.user, percent=False)))
+ self.match_conditions.append(
+ f"`tab{self.doctype}`.`owner` = {frappe.db.escape(self.user, percent=False)}"
+ )
+
# add user permission only if role has read perm
elif role_permissions.get("read") or role_permissions.get("select"):
# get user permissions
@@ -610,8 +615,7 @@ class DatabaseQuery(object):
# share is an OR condition, if there is a role permission
if not only_if_shared and self.shared and conditions:
- conditions = "({conditions}) or ({shared_condition})".format(
- conditions=conditions, shared_condition=self.get_share_condition())
+ conditions = f"({conditions}) or ({self.get_share_condition()})"
return conditions
@@ -619,8 +623,7 @@ class DatabaseQuery(object):
return self.match_filters
def get_share_condition(self):
- return """`tab{0}`.name in ({1})""".format(self.doctype, ", ".join(["%s"] * len(self.shared))) % \
- tuple([frappe.db.escape(s, percent=False) for s in self.shared])
+ return f"`tab{self.doctype}`.name 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)
@@ -645,9 +648,7 @@ class DatabaseQuery(object):
if frappe.get_system_settings("apply_strict_user_permissions"):
condition = ""
else:
- empty_value_condition = "ifnull(`tab{doctype}`.`{fieldname}`, '')=''".format(
- doctype=self.doctype, fieldname=df.get('fieldname')
- )
+ empty_value_condition = f"ifnull(`tab{self.doctype}`.`{df.get('fieldname')}`, '')=''"
condition = empty_value_condition + " or "
for permission in user_permission_values:
@@ -655,9 +656,7 @@ class DatabaseQuery(object):
docs.append(permission.get('doc'))
# append docs based on user permission applicable on reference doctype
-
# this is useful when getting list of docs from a link field
-
# in this case parent doctype of the link
# will be the reference doctype
@@ -669,14 +668,9 @@ class DatabaseQuery(object):
docs.append(permission.get('doc'))
if docs:
- condition += "`tab{doctype}`.`{fieldname}` in ({values})".format(
- doctype=self.doctype,
- fieldname=df.get('fieldname'),
- values=", ".join(
- [(frappe.db.escape(doc, percent=False)) for doc in docs])
- )
-
- match_conditions.append("({condition})".format(condition=condition))
+ values = ", ".join(frappe.db.escape(doc, percent=False) for doc in docs)
+ condition += f"`tab{self.doctype}`.`{df.get('fieldname')}` in ({values})"
+ match_conditions.append(f"({condition})")
match_filters[df.get('options')] = docs
if match_conditions:
@@ -726,17 +720,17 @@ class DatabaseQuery(object):
# `idx desc, modified desc`
# will covert to
# `tabItem`.`idx` desc, `tabItem`.`modified` desc
- args.order_by = ', '.join(['`tab{0}`.`{1}` {2}'.format(self.doctype,
- f.split()[0].strip(), f.split()[1].strip()) for f in meta.sort_field.split(',')])
+ args.order_by = ', '.join(
+ f"`tab{self.doctype}`.`{f.split()[0].strip()}` {f.split()[1].strip()}" for f in meta.sort_field.split(',')
+ )
else:
sort_field = meta.sort_field or 'modified'
sort_order = (meta.sort_field and meta.sort_order) or 'desc'
-
- args.order_by = "`tab{0}`.`{1}` {2}".format(self.doctype, sort_field or "modified", sort_order or "desc")
+ args.order_by = f"`tab{self.doctype}`.`{sort_field or 'modified'}` {sort_order or 'desc'}"
# draft docs always on top
- if meta.is_submittable:
- args.order_by = "`tab{0}`.docstatus asc, {1}".format(self.doctype, args.order_by)
+ if hasattr(meta, 'is_submittable') and meta.is_submittable:
+ args.order_by = f"`tab{self.doctype}`.docstatus asc, {args.order_by}"
def validate_order_by_and_group_by(self, parameters):
"""Check order by, group by so that atleast one column is selected and does not have subquery"""
@@ -807,17 +801,16 @@ def get_order_by(doctype, meta):
# `idx desc, modified desc`
# will covert to
# `tabItem`.`idx` desc, `tabItem`.`modified` desc
- order_by = ', '.join(['`tab{0}`.`{1}` {2}'.format(doctype,
- f.split()[0].strip(), f.split()[1].strip()) for f in meta.sort_field.split(',')])
+ order_by = ', '.join(f"`tab{doctype}`.`{f.split()[0].strip()}` {f.split()[1].strip()}" for f in meta.sort_field.split(','))
+
else:
sort_field = meta.sort_field or 'modified'
sort_order = (meta.sort_field and meta.sort_order) or 'desc'
-
- order_by = "`tab{0}`.`{1}` {2}".format(doctype, sort_field or "modified", sort_order or "desc")
+ order_by = f"`tab{doctype}`.`{sort_field or 'modified'}` {sort_order or 'desc'}"
# draft docs always on top
if meta.is_submittable:
- order_by = "`tab{0}`.docstatus asc, {1}".format(doctype, order_by)
+ order_by = f"`tab{doctype}`.docstatus asc, {order_by}"
return order_by
diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py
index 5fcc74a734..9ce74054e7 100644
--- a/frappe/model/delete_doc.py
+++ b/frappe/model/delete_doc.py
@@ -1,9 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import os
-from six import string_types, integer_types
import shutil
import frappe
@@ -12,12 +10,12 @@ import frappe.model.meta
from frappe import _
from frappe import get_module_path
from frappe.model.dynamic_links import get_dynamic_link_map
-from frappe.core.doctype.file.file import remove_all
+from frappe.utils.file_manager import remove_all
from frappe.utils.password import delete_all_passwords_for
from frappe.model.naming import revert_series_if_last
from frappe.utils.global_search import delete_for_document
from frappe.desk.doctype.tag.tag import delete_tags_for_document
-from frappe.exceptions import FileNotFoundError
+
doctypes_to_skip = ("Communication", "ToDo", "DocShare", "Email Unsubscribe", "Activity Log", "File",
"Version", "Document Follow", "Comment" , "View Log", "Tag Link", "Notification Log", "Email Queue")
@@ -35,7 +33,7 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa
name = frappe.form_dict.get('dn')
names = name
- if isinstance(name, string_types) or isinstance(name, integer_types):
+ if isinstance(name, str) or isinstance(name, int):
names = [name]
for name in names or []:
@@ -67,12 +65,12 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa
update_flags(doc, flags, ignore_permissions)
check_permission_and_not_submitted(doc)
- frappe.db.sql("delete from `tabCustom Field` where dt = %s", name)
- frappe.db.sql("delete from `tabClient Script` where dt = %s", name)
- frappe.db.sql("delete from `tabProperty Setter` where doc_type = %s", name)
- frappe.db.sql("delete from `tabReport` where ref_doctype=%s", name)
- frappe.db.sql("delete from `tabCustom DocPerm` where parent=%s", name)
- frappe.db.sql("delete from `__global_search` where doctype=%s", name)
+ frappe.db.delete("Custom Field", {"dt": name})
+ frappe.db.delete("Client Script", {"dt": name})
+ frappe.db.delete("Property Setter", {"doc_type": name})
+ frappe.db.delete("Report", {"ref_doctype": name})
+ frappe.db.delete("Custom DocPerm", {"parent": name})
+ frappe.db.delete("__global_search", {"doctype": name})
delete_from_table(doctype, name, ignore_doctypes, None)
@@ -164,10 +162,9 @@ def update_naming_series(doc):
def delete_from_table(doctype, name, ignore_doctypes, doc):
if doctype!="DocType" and doctype==name:
- frappe.db.sql("delete from `tabSingles` where `doctype`=%s", name)
+ frappe.db.delete("Singles", {"doctype": name})
else:
- frappe.db.sql("delete from `tab{0}` where `name`=%s".format(doctype), name)
-
+ frappe.db.delete(doctype, {"name": name})
# get child tables
if doc:
tables = [d.options for d in doc.meta.get_table_fields()]
@@ -193,7 +190,7 @@ def delete_from_table(doctype, name, ignore_doctypes, doc):
# delete from child tables
for t in list(set(tables)):
if t not in ignore_doctypes:
- frappe.db.sql("delete from `tab%s` where parenttype=%s and parent = %s" % (t, '%s', '%s'), (doctype, name))
+ frappe.db.delete(t, {"parenttype": doctype, "parent": name})
def update_flags(doc, flags=None, ignore_permissions=False):
if ignore_permissions:
@@ -326,9 +323,10 @@ def delete_dynamic_links(doctype, name):
def delete_references(doctype, reference_doctype, reference_name,
reference_doctype_field = 'reference_doctype', reference_name_field = 'reference_name'):
- frappe.db.sql('''delete from `tab{0}`
- where {1}=%s and {2}=%s'''.format(doctype, reference_doctype_field, reference_name_field), # nosec
- (reference_doctype, reference_name))
+ frappe.db.delete(doctype, {
+ reference_doctype_field: reference_doctype,
+ reference_name_field: reference_name
+ })
def clear_references(doctype, reference_doctype, reference_name,
reference_doctype_field = 'reference_doctype', reference_name_field = 'reference_name'):
@@ -341,8 +339,10 @@ def clear_references(doctype, reference_doctype, reference_name,
(reference_doctype, reference_name))
def clear_timeline_references(link_doctype, link_name):
- frappe.db.sql("""DELETE FROM `tabCommunication Link`
- WHERE `tabCommunication Link`.link_doctype=%s AND `tabCommunication Link`.link_name=%s""", (link_doctype, link_name))
+ frappe.db.delete("Communication Link", {
+ "link_doctype": link_doctype,
+ "link_name": link_name
+ })
def insert_feed(doc):
if (
diff --git a/frappe/model/docfield.py b/frappe/model/docfield.py
index 19b78e329d..6360c3866d 100644
--- a/frappe/model/docfield.py
+++ b/frappe/model/docfield.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
"""docfield utililtes"""
import frappe
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 623916597e..37549e2001 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -1,14 +1,11 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-
-from __future__ import unicode_literals, print_function
import frappe
import time
from frappe import _, msgprint, is_whitelisted
from frappe.utils import flt, cstr, now, get_datetime_str, file_lock, date_diff
from frappe.model.base_document import BaseDocument, get_controller
-from frappe.model.naming import set_new_name
-from six import iteritems, string_types
+from frappe.model.naming import set_new_name, gen_new_name_for_cancelled_doc
from werkzeug.exceptions import NotFound, Forbidden
import hashlib, json
from frappe.model import optional_fields, table_fields
@@ -18,6 +15,7 @@ from frappe.utils.global_search import update_global_search
from frappe.integrations.doctype.webhook import run_webhooks
from frappe.desk.form.document_follow import follow_document
from frappe.core.doctype.server_script.server_script_utils import run_server_script_for_doc_event
+from frappe.utils.data import get_absolute_url
# once_only validation
# methods
@@ -53,7 +51,7 @@ def get_doc(*args, **kwargs):
if isinstance(args[0], BaseDocument):
# already a document
return args[0]
- elif isinstance(args[0], string_types):
+ elif isinstance(args[0], str):
doctype = args[0]
elif isinstance(args[0], dict):
@@ -90,7 +88,7 @@ class Document(BaseDocument):
self._default_new_docs = {}
self.flags = frappe._dict()
- if args and args[0] and isinstance(args[0], string_types):
+ if args and args[0] and isinstance(args[0], str):
# first arugment is doctype
if len(args)==1:
# single
@@ -387,15 +385,15 @@ class Document(BaseDocument):
[self.name, self.doctype, fieldname] + rows)
if len(deleted_rows) > 0:
# delete rows that do not match the ones in the document
- frappe.db.sql("""delete from `tab{0}` where name in ({1})""".format(df.options,
- ','.join(['%s'] * len(deleted_rows))), tuple(row[0] for row in deleted_rows))
+ frappe.db.delete(df.options, {"name": ("in", tuple(row[0] for row in deleted_rows))})
else:
# no rows found, delete all rows
- frappe.db.sql("""delete from `tab{0}` where parent=%s
- and parenttype=%s and parentfield=%s""".format(df.options),
- (self.name, self.doctype, fieldname))
-
+ frappe.db.delete(df.options, {
+ "parent": self.name,
+ "parenttype": self.doctype,
+ "parentfield": fieldname
+ })
def get_doc_before_save(self):
return getattr(self, '_doc_before_save', None)
@@ -437,7 +435,7 @@ class Document(BaseDocument):
def get_values():
values = self.as_dict()
# format values
- for key, value in iteritems(values):
+ for key, value in values.items():
if value==None:
values[key] = ""
return values
@@ -453,8 +451,10 @@ class Document(BaseDocument):
def update_single(self, d):
"""Updates values for Single type Document in `tabSingles`."""
- frappe.db.sql("""delete from `tabSingles` where doctype=%s""", self.doctype)
- for field, value in iteritems(d):
+ frappe.db.delete("Singles", {
+ "doctype": self.doctype
+ })
+ for field, value in d.items():
if field != "doctype":
frappe.db.sql("""insert into `tabSingles` (doctype, field, value)
values (%s, %s, %s)""", (self.doctype, field, value))
@@ -494,6 +494,7 @@ class Document(BaseDocument):
self._validate_selects()
self._validate_non_negative()
self._validate_length()
+ self._validate_code_fields()
self._extract_images_from_text_editor()
self._sanitize_content()
self._save_passwords()
@@ -505,6 +506,7 @@ class Document(BaseDocument):
d._validate_selects()
d._validate_non_negative()
d._validate_length()
+ d._validate_code_fields()
d._extract_images_from_text_editor()
d._sanitize_content()
d._save_passwords()
@@ -707,7 +709,6 @@ class Document(BaseDocument):
else:
tmp = frappe.db.sql("""select modified, docstatus from `tab{0}`
where name = %s for update""".format(self.doctype), self.name, as_dict=True)
-
if not tmp:
frappe.throw(_("Record does not exist"))
else:
@@ -918,8 +919,12 @@ class Document(BaseDocument):
@whitelist.__func__
def _cancel(self):
- """Cancel the document. Sets `docstatus` = 2, then saves."""
+ """Cancel the document. Sets `docstatus` = 2, then saves.
+ """
self.docstatus = 2
+ new_name = gen_new_name_for_cancelled_doc(self)
+ frappe.rename_doc(self.doctype, self.name, new_name, force=True, show_alert=False)
+ self.name = new_name
self.save()
@whitelist.__func__
@@ -1062,7 +1067,10 @@ class Document(BaseDocument):
self.set("modified", now())
self.set("modified_by", frappe.session.user)
- self.load_doc_before_save()
+ # load but do not reload doc_before_save because before_change or on_change might expect it
+ if not self.get_doc_before_save():
+ self.load_doc_before_save()
+
# to trigger notification on value change
self.run_method('before_change')
@@ -1202,8 +1210,8 @@ class Document(BaseDocument):
doc.set(fieldname, flt(doc.get(fieldname), self.precision(fieldname, doc.parentfield)))
def get_url(self):
- """Returns Desk URL for this document. `/app/Form/{doctype}/{name}`"""
- return "/app/Form/{doctype}/{name}".format(doctype=self.doctype, name=self.name)
+ """Returns Desk URL for this document."""
+ return get_absolute_url(self.doctype, self.name)
def add_comment(self, comment_type='Comment', text=None, comment_email=None, link_doctype=None, link_name=None, comment_by=None):
"""Add a comment to this document.
diff --git a/frappe/model/dynamic_links.py b/frappe/model/dynamic_links.py
index 7404ba407e..676c86d7da 100644
--- a/frappe/model/dynamic_links.py
+++ b/frappe/model/dynamic_links.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
# select doctypes that are accessed by the user (not read_only) first, so that the
diff --git a/frappe/model/mapper.py b/frappe/model/mapper.py
index d3014435e0..fa8858d950 100644
--- a/frappe/model/mapper.py
+++ b/frappe/model/mapper.py
@@ -1,12 +1,12 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
+import json
-from __future__ import unicode_literals
-import frappe, json
+import frappe
from frappe import _
-from frappe.utils import cstr
from frappe.model import default_fields, table_fields
-from six import string_types
+from frappe.utils import cstr
+
@frappe.whitelist()
def make_mapped_doc(method, source_name, selected_children=None, args=None):
@@ -60,7 +60,7 @@ def get_mapped_doc(from_doctype, from_docname, table_maps, target_doc=None,
# main
if not target_doc:
target_doc = frappe.new_doc(table_maps[from_doctype]["doctype"])
- elif isinstance(target_doc, string_types):
+ elif isinstance(target_doc, str):
target_doc = frappe.get_doc(json.loads(target_doc))
if (not apply_strict_user_permissions
@@ -137,10 +137,8 @@ def get_mapped_doc(from_doctype, from_docname, table_maps, target_doc=None,
def map_doc(source_doc, target_doc, table_map, source_parent=None):
if table_map.get("validation"):
for key, condition in table_map["validation"].items():
- if condition[0]=="=":
- if source_doc.get(key) != condition[1]:
- frappe.throw(_("Cannot map because following condition fails: ")
- + key + "=" + cstr(condition[1]))
+ if condition[0] == "=" and source_doc.get(key) != condition[1]:
+ frappe.throw(_("Cannot map because following condition fails:") + f" {key}={cstr(condition[1])}")
map_fields(source_doc, target_doc, table_map, source_parent)
diff --git a/frappe/model/meta.py b/frappe/model/meta.py
index 66e8b08d92..de794ba77f 100644
--- a/frappe/model/meta.py
+++ b/frappe/model/meta.py
@@ -14,10 +14,7 @@ Example:
'''
-
-from __future__ import unicode_literals, print_function
from datetime import datetime
-from six.moves import range
import frappe, json, os
from frappe.utils import cstr, cint, cast_fieldtype
from frappe.model import default_fields, no_value_fields, optional_fields, data_fieldtypes, table_fields
@@ -144,6 +141,9 @@ class Meta(Document):
def get_image_fields(self):
return self.get("fields", {"fieldtype": "Attach Image"})
+ def get_code_fields(self):
+ return self.get("fields", {"fieldtype": "Code"})
+
def get_set_only_once_fields(self):
'''Return fields with `set_only_once` set'''
if not hasattr(self, "_set_only_once_fields"):
@@ -507,6 +507,9 @@ class Meta(Document):
if not data.non_standard_fieldnames:
data.non_standard_fieldnames = {}
+ if not data.internal_links:
+ data.internal_links = {}
+
for link in dashboard_links:
link.added = False
if link.hidden:
@@ -514,24 +517,32 @@ class Meta(Document):
for group in data.transactions:
group = frappe._dict(group)
+
+ # For internal links parent doctype will be the key
+ doctype = link.parent_doctype or link.link_doctype
# group found
if link.group and group.label == link.group:
- if link.link_doctype not in group.get('items'):
- group.get('items').append(link.link_doctype)
+ if doctype not in group.get('items'):
+ group.get('items').append(doctype)
link.added = True
if not link.added:
# group not found, make a new group
data.transactions.append(dict(
label = link.group,
- items = [link.link_doctype]
+ items = [link.parent_doctype or link.link_doctype]
))
-
- if link.link_fieldname != data.fieldname:
- if data.fieldname:
- data.non_standard_fieldnames[link.link_doctype] = link.link_fieldname
- else:
+
+ if not link.is_child_table:
+ if link.link_fieldname != data.fieldname:
+ if data.fieldname:
+ data.non_standard_fieldnames[link.link_doctype] = link.link_fieldname
+ else:
+ data.fieldname = link.link_fieldname
+ elif link.is_child_table:
+ if not data.fieldname:
data.fieldname = link.link_fieldname
+ data.internal_links[link.parent_doctype] = [link.table_fieldname, link.link_fieldname]
def get_row_template(self):
@@ -667,7 +678,7 @@ def trim_tables(doctype=None):
and not f.startswith("_")]
if columns_to_remove:
print(doctype, "columns removed:", columns_to_remove)
- columns_to_remove = ", ".join(["drop `{0}`".format(c) for c in columns_to_remove])
+ columns_to_remove = ", ".join("drop `{0}`".format(c) for c in columns_to_remove)
query = """alter table `tab{doctype}` {columns}""".format(
doctype=doctype, columns=columns_to_remove)
frappe.db.sql_ddl(query)
diff --git a/frappe/model/naming.py b/frappe/model/naming.py
index b8d6a6f8d7..7705002706 100644
--- a/frappe/model/naming.py
+++ b/frappe/model/naming.py
@@ -1,12 +1,21 @@
+"""utilities to generate a document name based on various rules defined.
+
+NOTE:
+Till version 13, whenever a submittable document is amended it's name is set to orig_name-X,
+where X is a counter and it increments when amended again and so on.
+
+From Version 14, The naming pattern is changed in a way that amended documents will
+have the original name `orig_name` instead of `orig_name-X`. To make this happen
+the cancelled document naming pattern is changed to 'orig_name-CANC-X'.
+"""
+
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import now_datetime, cint, cstr
import re
-from six import string_types
from frappe.model import log_types
@@ -30,7 +39,7 @@ def set_new_name(doc):
doc.name = None
if getattr(doc, "amended_from", None):
- _set_amended_name(doc)
+ doc.name = _get_amended_name(doc)
return
elif getattr(doc.meta, "issingle", False):
@@ -146,7 +155,7 @@ def make_autoname(key="", doctype="", doc=""):
def parse_naming_series(parts, doctype='', doc=''):
n = ''
- if isinstance(parts, string_types):
+ if isinstance(parts, str):
parts = parts.split('.')
series_set = False
today = now_datetime()
@@ -177,7 +186,7 @@ def parse_naming_series(parts, doctype='', doc=''):
else:
part = e
- if isinstance(part, string_types):
+ if isinstance(part, str):
n += part
return n
@@ -203,7 +212,7 @@ def revert_series_if_last(key, name, doc=None):
Reverts the series for particular naming series:
* key is naming series - SINV-.YYYY-.####
* name is actual name - SINV-2021-0001
-
+
1. This function split the key into two parts prefix (SINV-YYYY) & hashes (####).
2. Use prefix to get the current index of that naming series from Series table
3. Then revert the current index.
@@ -213,7 +222,7 @@ def revert_series_if_last(key, name, doc=None):
2. If hash doesn't exit in hashes, we get the hash from prefix, then update name and prefix accordingly.
*Example:*
- 1. key = SINV-.YYYY.-
+ 1. key = SINV-.YYYY.-
* If key doesn't have hash it will add hash at the end
* prefix will be SINV-YYYY based on this will get current index from Series table.
2. key = SINV-.####.-2021
@@ -221,9 +230,21 @@ def revert_series_if_last(key, name, doc=None):
* will search hash in key then accordingly get prefix = SINV-
3. key = ####.-2021
* prefix = #### and hashes = 2021 (hash doesn't exist)
- * will search hash in key then accordingly get prefix = ""
+ * will search hash in key then accordingly get prefix = ""
"""
- if ".#" in key:
+ if hasattr(doc, 'amended_from'):
+ # Do not revert the series if the document is amended.
+ if doc.amended_from:
+ return
+
+ # Get document name by parsing incase of fist cancelled document
+ if doc.docstatus == 2 and not doc.amended_from:
+ if doc.name.endswith('-CANC'):
+ name, _ = NameParser.parse_docname(doc.name, sep='-CANC')
+ else:
+ name, _ = NameParser.parse_docname(doc.name, sep='-CANC-')
+
+ if ".#" in key:
prefix, hashes = key.rsplit(".", 1)
if "#" not in hashes:
# get the hash part from the key
@@ -305,16 +326,9 @@ def append_number_if_name_exists(doctype, value, fieldname="name", separator="-"
return value
-def _set_amended_name(doc):
- am_id = 1
- am_prefix = doc.amended_from
- if frappe.db.get_value(doc.doctype, doc.amended_from, "amended_from"):
- am_id = cint(doc.amended_from.split("-")[-1]) + 1
- am_prefix = "-".join(doc.amended_from.split("-")[:-1]) # except the last hyphen
-
- doc.name = am_prefix + "-" + str(am_id)
- return doc.name
-
+def _get_amended_name(doc):
+ name, _ = NameParser(doc).parse_amended_from()
+ return name
def _field_autoname(autoname, doc, skip_slicing=None):
"""
@@ -325,7 +339,6 @@ def _field_autoname(autoname, doc, skip_slicing=None):
name = (cstr(doc.get(fieldname)) or "").strip()
return name
-
def _prompt_autoname(autoname, doc):
"""
Generate a name using Prompt option. This simply means the user will have to set the name manually.
@@ -356,3 +369,83 @@ def _format_autoname(autoname, doc):
name = re.sub(r"(\{[\w | #]+\})", get_param_value_for_match, autoname_value)
return name
+
+class NameParser:
+ """Parse document name and return parts of it.
+
+ NOTE: It handles cancellend and amended doc parsing for now. It can be expanded.
+ """
+ def __init__(self, doc):
+ self.doc = doc
+
+ def parse_amended_from(self):
+ """
+ Cancelled document naming will be in one of these formats
+
+ * original_name-X-CANC - This is introduced to migrate old style naming to new style
+ * original_name-CANC - This is introduced to migrate old style naming to new style
+ * original_name-CANC-X - This is the new style naming
+
+ New style naming: In new style naming amended documents will have original name. That says,
+ when a document gets cancelled we need rename the document by adding `-CANC-X` to the end
+ so that amended documents can use the original name.
+
+ Old style naming: cancelled documents stay with original name and when amended, amended one
+ gets a new name as `original_name-X`. To bring new style naming we had to change the existing
+ cancelled document names and that is done by adding `-CANC` to cancelled documents through patch.
+ """
+ if not getattr(self.doc, 'amended_from', None):
+ return (None, None)
+
+ # Handle old style cancelled documents (original_name-X-CANC, original_name-CANC)
+ if self.doc.amended_from.endswith('-CANC'):
+ name, _ = self.parse_docname(self.doc.amended_from, '-CANC')
+ amended_from_doc = frappe.get_all(
+ self.doc.doctype,
+ filters = {'name': self.doc.amended_from},
+ fields = ['amended_from'],
+ limit=1)
+
+ # Handle format original_name-X-CANC.
+ if amended_from_doc and amended_from_doc[0].amended_from:
+ return self.parse_docname(name, '-')
+ return name, None
+
+ # Handle new style cancelled documents
+ return self.parse_docname(self.doc.amended_from, '-CANC-')
+
+ @classmethod
+ def parse_docname(cls, name, sep='-'):
+ split_list = name.rsplit(sep, 1)
+
+ if len(split_list) == 1:
+ return (name, None)
+ return (split_list[0], split_list[1])
+
+def get_cancelled_doc_latest_counter(tname, docname):
+ """Get the latest counter used for cancelled docs of given docname.
+ """
+ name_prefix = f'{docname}-CANC-'
+
+ rows = frappe.db.sql("""
+ select
+ name
+ from `tab{tname}`
+ where
+ name like %(name_prefix)s and docstatus=2
+ """.format(tname=tname), {'name_prefix': name_prefix+'%'}, as_dict=1)
+
+ if not rows:
+ return -1
+ return max([int(row.name.replace(name_prefix, '') or -1) for row in rows])
+
+def gen_new_name_for_cancelled_doc(doc):
+ """Generate a new name for cancelled document.
+ """
+ if getattr(doc, "amended_from", None):
+ name, _ = NameParser(doc).parse_amended_from()
+ else:
+ name = doc.name
+
+ counter = get_cancelled_doc_latest_counter(doc.doctype, name)
+ return f'{name}-CANC-{counter+1}'
diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py
index 2c9dc5d823..9b8ac2574d 100644
--- a/frappe/model/rename_doc.py
+++ b/frappe/model/rename_doc.py
@@ -1,8 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-
-from __future__ import print_function, unicode_literals
-
import frappe
from frappe import _, bold
from frappe.model.dynamic_links import get_dynamic_link_map
@@ -144,7 +141,7 @@ def update_user_settings(old, new, link_fields):
if not link_fields: return
# find the user settings for the linked doctypes
- linked_doctypes = set([d.parent for d in link_fields if not d.issingle])
+ 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
diff --git a/frappe/model/sync.py b/frappe/model/sync.py
index 61983d322c..836f70dd55 100644
--- a/frappe/model/sync.py
+++ b/frappe/model/sync.py
@@ -1,7 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-
-from __future__ import unicode_literals, print_function
"""
Sync's doctype and docfields from txt files to database
perms will get synced only if none exist
@@ -84,7 +82,7 @@ def get_doc_files(files, start_path):
document_types = ['doctype', 'page', 'report', 'dashboard_chart_source', 'print_format',
'website_theme', 'web_form', 'web_template', 'notification', 'print_style',
'data_migration_mapping', 'data_migration_plan', 'workspace',
- 'onboarding_step', 'module_onboarding']
+ 'onboarding_step', 'module_onboarding', 'form_tour']
for doctype in document_types:
doctype_path = os.path.join(start_path, doctype)
diff --git a/frappe/model/utils/__init__.py b/frappe/model/utils/__init__.py
index efbe46a4ab..47615182e4 100644
--- a/frappe/model/utils/__init__.py
+++ b/frappe/model/utils/__init__.py
@@ -1,15 +1,10 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-
-from __future__ import unicode_literals, print_function
-from six.moves import range
import frappe
from frappe import _
from frappe.utils import cstr
from frappe.build import html_to_js_template
import re
-from six import text_type
-
import io
STANDARD_FIELD_CONVERSION_MAP = {
diff --git a/frappe/model/utils/link_count.py b/frappe/model/utils/link_count.py
index 5faa5ba44b..7562aaae45 100644
--- a/frappe/model/utils/link_count.py
+++ b/frappe/model/utils/link_count.py
@@ -1,10 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
-
import frappe
-from six import iteritems
ignore_doctypes = ("DocType", "Print Format", "Role", "Module Def", "Communication",
"ToDo")
@@ -39,7 +36,7 @@ def update_link_count():
link_count = frappe.cache().get_value('_link_count')
if link_count:
- for key, count in iteritems(link_count):
+ for key, count in link_count.items():
if key[0] not in ignore_doctypes:
try:
frappe.db.sql('update `tab{0}` set idx = idx + {1} where name=%s'.format(key[0], count),
diff --git a/frappe/model/utils/rename_field.py b/frappe/model/utils/rename_field.py
index 778f623092..9fe9d64041 100644
--- a/frappe/model/utils/rename_field.py
+++ b/frappe/model/utils/rename_field.py
@@ -1,8 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-
-from __future__ import unicode_literals, print_function
-
import frappe
import json
from frappe.model import no_value_fields, table_fields
diff --git a/frappe/model/utils/user_settings.py b/frappe/model/utils/user_settings.py
index d59bda71a5..ad378ab93f 100644
--- a/frappe/model/utils/user_settings.py
+++ b/frappe/model/utils/user_settings.py
@@ -1,9 +1,8 @@
-from __future__ import unicode_literals
+
# Settings saved per user basis
# such as page_limit, filters, last_view
import frappe, json
-from six import iteritems, string_types
from frappe import safe_decode
# dict for mapping the index and index type for the filters of different views
@@ -36,7 +35,7 @@ def update_user_settings(doctype, user_settings, for_update=False):
else:
current = json.loads(get_user_settings(doctype, for_update = True))
- if isinstance(current, string_types):
+ if isinstance(current, str):
# corrupt due to old code, remove this in a future release
current = {}
@@ -47,7 +46,7 @@ def update_user_settings(doctype, user_settings, for_update=False):
def sync_user_settings():
'''Sync from cache to database (called asynchronously via the browser)'''
- for key, data in iteritems(frappe.cache().hgetall('_user_settings')):
+ for key, data in frappe.cache().hgetall('_user_settings').items():
key = safe_decode(key)
doctype, user = key.split('::') # WTF?
frappe.db.multisql({
diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py
index 3e8125f9b1..fa2f557370 100644
--- a/frappe/model/workflow.py
+++ b/frappe/model/workflow.py
@@ -1,11 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
from frappe.utils import cint
from frappe import _
-from six import string_types
import json
class WorkflowStateError(frappe.ValidationError): pass
@@ -268,7 +266,7 @@ def print_workflow_log(messages, title, doctype, indicator):
@frappe.whitelist()
def get_common_transition_actions(docs, doctype):
common_actions = []
- if isinstance(docs, string_types):
+ if isinstance(docs, str):
docs = json.loads(docs)
try:
for (i, doc) in enumerate(docs, 1):
diff --git a/frappe/modules/__init__.py b/frappe/modules/__init__.py
index fef4829bb6..33411f8d74 100644
--- a/frappe/modules/__init__.py
+++ b/frappe/modules/__init__.py
@@ -1,2 +1,2 @@
-from __future__ import unicode_literals
+
from .utils import *
\ No newline at end of file
diff --git a/frappe/modules/export_file.py b/frappe/modules/export_file.py
index 4b22c82105..ae9f11d53b 100644
--- a/frappe/modules/export_file.py
+++ b/frappe/modules/export_file.py
@@ -1,8 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
-
import frappe, os
import frappe.model
from frappe.modules import scrub, get_module_path, scrub_dt_dn
diff --git a/frappe/modules/import_file.py b/frappe/modules/import_file.py
index fdfd00404c..e743f0c3da 100644
--- a/frappe/modules/import_file.py
+++ b/frappe/modules/import_file.py
@@ -1,8 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-
-from __future__ import unicode_literals, print_function
-
import frappe, os, json
from frappe.modules import get_module_path, scrub_dt_dn
from frappe.utils import get_datetime_str
diff --git a/frappe/modules/patch_handler.py b/frappe/modules/patch_handler.py
index 0ed10d1e0d..029234d5d9 100644
--- a/frappe/modules/patch_handler.py
+++ b/frappe/modules/patch_handler.py
@@ -1,7 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-
-from __future__ import unicode_literals, print_function
"""
Execute Patch Files
@@ -14,9 +12,6 @@ from __future__ import unicode_literals, print_function
"""
import frappe, frappe.permissions, time
-# for patches
-import os
-
class PatchError(Exception): pass
def run_all(skip_failing=False):
diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py
index 132aa1e2a5..ed2a839dc1 100644
--- a/frappe/modules/utils.py
+++ b/frappe/modules/utils.py
@@ -1,7 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-
-from __future__ import unicode_literals, print_function
"""
Utilities for using modules
"""
@@ -116,8 +114,7 @@ def sync_customizations_for_doctype(data, folder):
doc.db_insert()
if custom_doctype != 'Custom Field':
- frappe.db.sql('delete from `tab{0}` where `{1}` =%s'.format(
- custom_doctype, doctype_fieldname), doc_type)
+ frappe.db.delete(custom_doctype, {doctype_fieldname: doc_type})
for d in data[key]:
_insert(d)
diff --git a/frappe/monitor.py b/frappe/monitor.py
index 6802a59584..34ca7d67f7 100644
--- a/frappe/monitor.py
+++ b/frappe/monitor.py
@@ -2,8 +2,6 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
-
from datetime import datetime
import json
import traceback
diff --git a/frappe/oauth.py b/frappe/oauth.py
index a4c66bf3f2..67d346ad8a 100644
--- a/frappe/oauth.py
+++ b/frappe/oauth.py
@@ -486,6 +486,7 @@ class OAuthWebRequestValidator(RequestValidator):
user = None
payload = jwt.decode(
id_token_hint,
+ algorithms=["HS256"],
options={
"verify_signature": False,
"verify_aud": False,
@@ -508,7 +509,7 @@ class OAuthWebRequestValidator(RequestValidator):
id_token_hint,
key=client_secret,
audience=client_id,
- algorithm="HS256",
+ algorithms=["HS256"],
options={
"verify_exp": False,
},
diff --git a/frappe/parallel_test_runner.py b/frappe/parallel_test_runner.py
index 1dbb24f191..c7f723bbdc 100644
--- a/frappe/parallel_test_runner.py
+++ b/frappe/parallel_test_runner.py
@@ -15,10 +15,9 @@ if click_ctx:
click_ctx.color = True
class ParallelTestRunner():
- def __init__(self, app, site, build_number=1, total_builds=1, with_coverage=False):
+ def __init__(self, app, site, build_number=1, total_builds=1):
self.app = app
self.site = site
- self.with_coverage = with_coverage
self.build_number = frappe.utils.cint(build_number) or 1
self.total_builds = frappe.utils.cint(total_builds)
self.setup_test_site()
@@ -53,12 +52,9 @@ class ParallelTestRunner():
def run_tests(self):
self.test_result = ParallelTestResult(stream=sys.stderr, descriptions=True, verbosity=2)
- self.start_coverage()
-
for test_file_info in self.get_test_file_list():
self.run_tests_for_file(test_file_info)
- self.save_coverage()
self.print_result()
def run_tests_for_file(self, file_info):
@@ -107,28 +103,6 @@ class ParallelTestRunner():
if os.environ.get('CI'):
sys.exit(1)
- def start_coverage(self):
- if self.with_coverage:
- from coverage import Coverage
- from frappe.utils import get_bench_path
-
- # Generate coverage report only for app that is being tested
- source_path = os.path.join(get_bench_path(), 'apps', self.app)
- omit=['*.html', '*.js', '*.xml', '*.css', '*.less', '*.scss',
- '*.vue', '*/doctype/*/*_dashboard.py', '*/patches/*']
-
- if self.app == 'frappe':
- omit.append('*/commands/*')
-
- self.coverage = Coverage(source=[source_path], omit=omit)
- self.coverage.start()
-
- def save_coverage(self):
- if not self.with_coverage:
- return
- self.coverage.stop()
- self.coverage.save()
-
def get_test_file_list(self):
test_list = get_all_tests(self.app)
split_size = frappe.utils.ceil(len(test_list) / self.total_builds)
@@ -224,7 +198,7 @@ class ParallelTestWithOrchestrator(ParallelTestRunner):
- get-next-test-spec (, )
- test-completed (, )
'''
- def __init__(self, app, site, with_coverage=False):
+ def __init__(self, app, site):
self.orchestrator_url = os.environ.get('ORCHESTRATOR_URL')
if not self.orchestrator_url:
click.echo('ORCHESTRATOR_URL environment variable not found!')
@@ -237,7 +211,7 @@ class ParallelTestWithOrchestrator(ParallelTestRunner):
click.echo('CI_BUILD_ID environment variable not found!')
sys.exit(1)
- ParallelTestRunner.__init__(self, app, site, with_coverage=with_coverage)
+ ParallelTestRunner.__init__(self, app, site)
def run_tests(self):
self.test_status = 'ongoing'
diff --git a/frappe/patches.txt b/frappe/patches.txt
index e70be0a37b..41ca1a1724 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -1,11 +1,5 @@
frappe.patches.v12_0.remove_deprecated_fields_from_doctype #3
-execute:frappe.db.sql("""update `tabPatch Log` set patch=replace(patch, '.4_0.', '.v4_0.')""") #2014-05-12
-frappe.patches.v5_0.convert_to_barracuda_and_utf8mb4
execute:frappe.utils.global_search.setup_global_search_table()
-frappe.patches.v8_0.update_global_search_table
-frappe.patches.v7_0.update_auth
-frappe.patches.v8_0.drop_in_dialog #2017-09-22
-frappe.patches.v7_2.remove_in_filter
execute:frappe.reload_doc('core', 'doctype', 'doctype_action', force=True) #2019-09-23
execute:frappe.reload_doc('core', 'doctype', 'doctype_link', force=True) #2020-10-17
execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) #2017-09-22
@@ -14,7 +8,6 @@ frappe.patches.v11_0.drop_column_apply_user_permissions
execute:frappe.reload_doc('core', 'doctype', 'custom_docperm')
execute:frappe.reload_doc('core', 'doctype', 'docperm') #2018-05-29
execute:frappe.reload_doc('core', 'doctype', 'comment')
-frappe.patches.v8_0.drop_is_custom_from_docperm
execute:frappe.reload_doc('core', 'doctype', 'document_naming_rule', force=True)
execute:frappe.reload_doc('core', 'doctype', 'module_def') #2020-08-28
execute:frappe.reload_doc('core', 'doctype', 'version') #2017-04-01
@@ -25,190 +18,40 @@ execute:frappe.reload_doc('core', 'doctype', 'communication') #2019-10-02
execute:frappe.reload_doc('core', 'doctype', 'server_script')
frappe.patches.v11_0.replicate_old_user_permissions
frappe.patches.v11_0.reload_and_rename_view_log #2019-01-03
-frappe.patches.v7_1.rename_scheduler_log_to_error_log
-frappe.patches.v6_1.rename_file_data
-frappe.patches.v7_0.re_route #2016-06-27
-frappe.patches.v8_0.update_records_in_global_search #11-05-2017
-frappe.patches.v8_0.update_published_in_global_search
frappe.patches.v11_0.copy_fetch_data_from_options
frappe.patches.v11_0.change_email_signature_fieldtype
execute:frappe.reload_doc('core', 'doctype', 'activity_log')
execute:frappe.reload_doc('core', 'doctype', 'deleted_document')
execute:frappe.reload_doc('core', 'doctype', 'domain_settings')
frappe.patches.v13_0.rename_custom_client_script
-frappe.patches.v8_0.rename_page_role_to_has_role #2017-03-16
-frappe.patches.v7_2.setup_custom_perms #2017-01-19
-frappe.patches.v8_0.set_user_permission_for_page_and_report #2017-03-20
execute:frappe.reload_doc('core', 'doctype', 'role') #2017-05-23
execute:frappe.reload_doc('core', 'doctype', 'user') #2017-10-27
-execute:frappe.reload_doc('custom', 'doctype', 'custom_field') #2015-10-19
-execute:frappe.reload_doc('core', 'doctype', 'page') #2013-13-26
execute:frappe.reload_doc('core', 'doctype', 'report_column')
execute:frappe.reload_doc('core', 'doctype', 'report_filter')
execute:frappe.reload_doc('core', 'doctype', 'report') #2020-08-25
-execute:frappe.reload_doc('core', 'doctype', 'translation') #2016-03-03
-execute:frappe.reload_doc('email', 'doctype', 'email_alert') #2014-07-15
-execute:frappe.reload_doc('desk', 'doctype', 'todo') #2014-12-31-1
-execute:frappe.reload_doc('custom', 'doctype', 'property_setter') #2014-12-31-1
-execute:frappe.reload_doc('core', 'doctype', 'patch_log') #2016-10-31
-execute:frappe.reload_doctype("File") # 2015-10-19
execute:frappe.reload_doc('core', 'doctype', 'error_snapshot')
-execute:frappe.clear_cache()
-frappe.patches.v7_1.rename_scheduler_log_to_error_log
-frappe.patches.v7_1.sync_language_doctype
-frappe.patches.v7_0.rename_bulk_email_to_email_queue
-frappe.patches.v7_1.rename_chinese_language_codes
-
-execute:frappe.db.sql("alter table `tabSessions` modify `user` varchar(255), engine=InnoDB")
-execute:frappe.db.sql("delete from `tabDocField` where parent='0'")
-frappe.patches.v4_0.change_varchar_length
-frappe.patches.v6_4.reduce_varchar_length
-frappe.patches.v5_2.change_checks_to_not_null
-frappe.patches.v6_9.int_float_not_null #2015-11-25
-frappe.patches.v5_0.v4_to_v5
-
-frappe.patches.v5_0.remove_shopping_cart_app
-frappe.patches.v4_0.webnotes_to_frappe
-execute:frappe.permissions.reset_perms("Module Def")
-execute:import frappe.installer;frappe.installer.make_site_dirs() #2014-02-19
-frappe.patches.v4_0.rename_profile_to_user
-frappe.patches.v4_0.deprecate_control_panel
-frappe.patches.v4_0.remove_old_parent
-frappe.patches.v4_0.rename_sitemap_to_route
-frappe.patches.v4_0.website_sitemap_hierarchy
-frappe.patches.v4_0.remove_index_sitemap
-frappe.patches.v4_0.set_website_route_idx
-frappe.patches.v4_0.add_delete_permission
-frappe.patches.v4_0.set_todo_checked_as_closed
-frappe.patches.v4_0.private_backups
-frappe.patches.v4_0.set_module_in_report
-frappe.patches.v4_0.update_datetime
-frappe.patches.v4_0.file_manager_hooks
execute:frappe.get_doc("User", "Guest").save()
-frappe.patches.v4_0.update_custom_field_insert_after
-frappe.patches.v4_0.deprecate_link_selects
-frappe.patches.v4_0.set_user_gravatar
-frappe.patches.v4_0.set_user_permissions
-frappe.patches.v4_0.create_custom_field_for_owner_match
-frappe.patches.v4_0.enable_scheduler_in_system_settings
-execute:frappe.db.sql("update tabReport set apply_user_permissions=1") #2014-06-03
-frappe.patches.v4_0.replace_deprecated_timezones
-execute:import frappe.website.render; frappe.website.render.clear_cache("login"); #2014-06-10
-frappe.patches.v4_0.fix_attach_field_file_url
-execute:frappe.permissions.reset_perms("User") #2015-03-24
-execute:frappe.db.sql("""delete from `tabUserRole` where ifnull(parentfield, '')='' or ifnull(`role`, '')=''""") #2014-08-18
-frappe.patches.v4_0.remove_user_owner_custom_field
-execute:frappe.delete_doc("DocType", "Website Template")
-execute:frappe.db.sql("""update `tabProperty Setter` set property_type='Text' where property in ('options', 'default')""") #2014-06-20
-frappe.patches.v4_1.enable_outgoing_email_settings
-execute:frappe.db.sql("""update `tabSingles` set `value`=`doctype` where `field`='name'""") #2014-07-04
-frappe.patches.v4_1.enable_print_as_pdf #2014-06-17
-execute:frappe.db.sql("""update `tabDocPerm` set email=1 where parent='User' and permlevel=0 and `role`='All' and `read`=1 and apply_user_permissions=1""") #2014-07-15
-execute:frappe.db.sql("""update `tabPrint Format` set print_format_type='Client' where ifnull(print_format_type, '')=''""") #2014-07-28
-frappe.patches.v4_1.file_manager_fix
-frappe.patches.v4_2.print_with_letterhead
execute:frappe.delete_doc("DocType", "Control Panel", force=1)
-execute:frappe.reload_doc('website', 'doctype', 'web_form') #2014-09-04
-execute:frappe.reload_doc('website', 'doctype', 'web_form_field') #2014-09-04
-frappe.patches.v4_2.refactor_website_routing
-frappe.patches.v4_2.set_assign_in_doc
-frappe.patches.v4_3.remove_allow_on_submit_customization
-frappe.patches.v5_0.rename_table_fieldnames
-frappe.patches.v5_0.communication_parent
-frappe.patches.v5_0.clear_website_group_and_notifications
-frappe.patches.v5_0.update_shared
-execute:frappe.reload_doc("core", "doctype", "docshare") #2015-07-21
-frappe.patches.v6_19.comment_feed_communication
-frappe.patches.v6_16.star_to_like
-frappe.patches.v5_0.bookmarks_to_stars
-frappe.patches.v5_0.style_settings_to_website_theme
-frappe.patches.v5_0.rename_ref_type_fieldnames
-frappe.patches.v5_0.fix_email_alert
-frappe.patches.v5_0.fix_null_date_datetime
-frappe.patches.v5_0.force_sync_website
execute:frappe.delete_doc("DocType", "Tag")
execute:frappe.db.sql("delete from `tabProperty Setter` where `property` in ('idx', '_idx')")
-frappe.patches.v5_0.move_scheduler_last_event_to_system_settings
execute:frappe.db.sql("update tabUser set new_password='' where ifnull(new_password, '')!=''")
-frappe.patches.v5_0.fix_text_editor_file_urls
-frappe.patches.v5_0.modify_session
-frappe.patches.v5_0.expire_old_scheduler_logs
execute:frappe.permissions.reset_perms("DocType")
execute:frappe.db.sql("delete from `tabProperty Setter` where `property` = 'idx'")
-frappe.patches.v6_0.communication_status_and_permission
-frappe.patches.v6_0.make_task_log_folder
-frappe.patches.v6_0.document_type_rename
-frappe.patches.v6_0.fix_ghana_currency
-frappe.patches.v6_2.ignore_user_permissions_if_missing
execute:frappe.db.sql("delete from tabSessions where user is null")
-frappe.patches.v6_2.rename_backup_manager
execute:frappe.delete_doc("DocType", "Backup Manager")
-execute:frappe.db.sql("""update `tabCommunication` set parenttype=null, parent=null, parentfield=null""") #2015-10-22
execute:frappe.permissions.reset_perms("Web Page")
-frappe.patches.v6_6.user_last_active
-frappe.patches.v6_6.fix_file_url
-frappe.patches.v6_11.rename_field_in_email_account
-frappe.patches.v7_0.create_private_file_folder
-frappe.patches.v6_15.remove_property_setter_for_previous_field #2015-12-29
-frappe.patches.v6_15.set_username
execute:frappe.permissions.reset_perms("Error Snapshot")
-frappe.patches.v6_16.feed_doc_owner
-frappe.patches.v6_21.print_settings_repeat_header_footer
-frappe.patches.v6_24.set_language_as_code
-frappe.patches.v6_20x.update_insert_after
-frappe.patches.v6_20x.set_allow_draft_for_print
-frappe.patches.v6_20x.remove_roles_from_website_user
-frappe.patches.v7_0.set_user_fullname
-frappe.patches.v7_0.add_communication_in_doc
-frappe.patches.v7_0.update_send_after_in_bulk_email
-execute:frappe.db.sql('''delete from `tabSingles` where doctype="Email Settings"''') # 2016-06-13
execute:frappe.db.sql("delete from `tabWeb Page` where ifnull(template_path, '')!=''")
-frappe.patches.v7_0.rename_newsletter_list_to_email_group
-frappe.patches.v7_0.set_email_group
-frappe.patches.v7_1.setup_integration_services #2016-10-27
-frappe.patches.v7_1.rename_chinese_language_codes
execute:frappe.core.doctype.language.language.update_language_names() # 2017-04-12
execute:frappe.db.set_value("Print Settings", "Print Settings", "add_draft_heading", 1)
-frappe.patches.v7_0.cleanup_list_settings
execute:frappe.db.set_default('language', '')
-frappe.patches.v7_1.refactor_integration_broker
-frappe.patches.v7_1.set_backup_limit
-frappe.patches.v7_2.set_doctype_engine
-frappe.patches.v7_2.merge_knowledge_base
-frappe.patches.v7_0.update_report_builder_json
-frappe.patches.v7_2.set_in_standard_filter_property #1
-frappe.patches.v8_0.drop_unwanted_indexes
execute:frappe.db.sql("update tabCommunication set communication_date = creation where time(communication_date) = 0")
-frappe.patches.v7_2.fix_email_queue_recipient
-frappe.patches.v7_2.update_feedback_request # 2017-02-27
execute:frappe.rename_doc('Country', 'Macedonia, Republic of', 'Macedonia', ignore_if_exists=True)
execute:frappe.rename_doc('Country', 'Iran, Islamic Republic of', 'Iran', ignore_if_exists=True)
execute:frappe.rename_doc('Country', 'Tanzania, United Republic of', 'Tanzania', ignore_if_exists=True)
execute:frappe.rename_doc('Country', 'Syrian Arab Republic', 'Syria', ignore_if_exists=True)
-frappe.patches.v8_0.rename_listsettings_to_usersettings
-frappe.patches.v7_2.update_communications
-frappe.patches.v8_0.deprecate_integration_broker
-frappe.patches.v8_0.update_gender_and_salutation
-frappe.patches.v8_0.setup_email_inbox #2017-03-29
-frappe.patches.v8_0.newsletter_childtable_migrate
-frappe.patches.v8_0.set_doctype_values_in_custom_role
-frappe.patches.v8_0.install_new_build_system_requirements
-frappe.patches.v8_0.set_currency_field_precision # 2017-05-09
execute:frappe.reload_doc('desk', 'doctype', 'notification_log')
-frappe.patches.v8_0.rename_print_to_printing
-frappe.patches.v7_1.disabled_print_settings_for_custom_print_format
execute:frappe.db.sql('update tabReport set module="Desk" where name="ToDo"')
-frappe.patches.v8_1.enable_allow_error_traceback_in_system_settings
-frappe.patches.v8_1.update_format_options_in_auto_email_report
-frappe.patches.v8_1.delete_custom_docperm_if_doctype_not_exists
-frappe.patches.v8_5.delete_email_group_member_with_invalid_emails
-frappe.patches.v8_x.update_user_permission
-frappe.patches.v8_5.patch_event_colors
-frappe.patches.v8_10.delete_static_web_page_from_global_search
-frappe.patches.v9_1.add_sms_sender_name_as_parameters
-frappe.patches.v9_1.resave_domain_settings
-frappe.patches.v9_1.revert_domain_settings
-frappe.patches.v9_1.move_feed_to_activity_log
execute:frappe.delete_doc('Page', 'data-import-tool', ignore_missing=True)
frappe.patches.v10_0.reload_countries_and_currencies # 2021-02-03
frappe.patches.v10_0.refactor_social_login_keys
@@ -337,3 +180,6 @@ frappe.patches.v12_0.rename_uploaded_files_with_proper_name
frappe.patches.v13_0.queryreport_columns
frappe.patches.v13_0.jinja_hook
frappe.patches.v13_0.update_notification_channel_if_empty
+frappe.patches.v14_0.drop_data_import_legacy
+frappe.patches.v14_0.rename_cancelled_documents
+frappe.patches.v14_0.update_workspace2 # 25.08.2021
diff --git a/frappe/patches/v10_0/enable_chat_by_default_within_system_settings.py b/frappe/patches/v10_0/enable_chat_by_default_within_system_settings.py
index eddca78051..24f915c512 100644
--- a/frappe/patches/v10_0/enable_chat_by_default_within_system_settings.py
+++ b/frappe/patches/v10_0/enable_chat_by_default_within_system_settings.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
def execute():
diff --git a/frappe/patches/v10_0/enhance_security.py b/frappe/patches/v10_0/enhance_security.py
index 865d18dcff..4f6ca4faa1 100644
--- a/frappe/patches/v10_0/enhance_security.py
+++ b/frappe/patches/v10_0/enhance_security.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
import frappe
from frappe.utils import cint
diff --git a/frappe/patches/v10_0/increase_single_table_column_length.py b/frappe/patches/v10_0/increase_single_table_column_length.py
index 18de0cff9e..e578d192fc 100644
--- a/frappe/patches/v10_0/increase_single_table_column_length.py
+++ b/frappe/patches/v10_0/increase_single_table_column_length.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
"""
Run this after updating country_info.json and or
"""
diff --git a/frappe/patches/v10_0/migrate_passwords_passlib.py b/frappe/patches/v10_0/migrate_passwords_passlib.py
index 22b7a86f85..d0b36efbaa 100644
--- a/frappe/patches/v10_0/migrate_passwords_passlib.py
+++ b/frappe/patches/v10_0/migrate_passwords_passlib.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
from frappe.utils.password import LegacyPassword
diff --git a/frappe/patches/v10_0/modify_naming_series_table.py b/frappe/patches/v10_0/modify_naming_series_table.py
index 659e247a38..ca6114eb55 100644
--- a/frappe/patches/v10_0/modify_naming_series_table.py
+++ b/frappe/patches/v10_0/modify_naming_series_table.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
'''
Modify the Integer 10 Digits Value to BigInt 20 Digit value
to generate long Naming Series
diff --git a/frappe/patches/v10_0/modify_smallest_currency_fraction.py b/frappe/patches/v10_0/modify_smallest_currency_fraction.py
index f875d6b87d..c9ae477359 100644
--- a/frappe/patches/v10_0/modify_smallest_currency_fraction.py
+++ b/frappe/patches/v10_0/modify_smallest_currency_fraction.py
@@ -1,7 +1,6 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
def execute():
diff --git a/frappe/patches/v10_0/refactor_social_login_keys.py b/frappe/patches/v10_0/refactor_social_login_keys.py
index 07737912df..a3f08939ec 100644
--- a/frappe/patches/v10_0/refactor_social_login_keys.py
+++ b/frappe/patches/v10_0/refactor_social_login_keys.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
import frappe
from frappe.utils import cstr
diff --git a/frappe/patches/v10_0/reload_countries_and_currencies.py b/frappe/patches/v10_0/reload_countries_and_currencies.py
index f83ed9c3aa..8d019a4855 100644
--- a/frappe/patches/v10_0/reload_countries_and_currencies.py
+++ b/frappe/patches/v10_0/reload_countries_and_currencies.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
"""
Run this after updating country_info.json and or
"""
diff --git a/frappe/patches/v10_0/remove_custom_field_for_disabled_domain.py b/frappe/patches/v10_0/remove_custom_field_for_disabled_domain.py
index f27639388e..54839cfe02 100644
--- a/frappe/patches/v10_0/remove_custom_field_for_disabled_domain.py
+++ b/frappe/patches/v10_0/remove_custom_field_for_disabled_domain.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
def execute():
diff --git a/frappe/patches/v10_0/set_default_locking_time.py b/frappe/patches/v10_0/set_default_locking_time.py
index 1c9797a6cc..045fa0e3fa 100644
--- a/frappe/patches/v10_0/set_default_locking_time.py
+++ b/frappe/patches/v10_0/set_default_locking_time.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
def execute():
diff --git a/frappe/patches/v10_0/set_no_copy_to_workflow_state.py b/frappe/patches/v10_0/set_no_copy_to_workflow_state.py
index 800d4a4d1b..eb469b8452 100644
--- a/frappe/patches/v10_0/set_no_copy_to_workflow_state.py
+++ b/frappe/patches/v10_0/set_no_copy_to_workflow_state.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
def execute():
diff --git a/frappe/patches/v11_0/apply_customization_to_custom_doctype.py b/frappe/patches/v11_0/apply_customization_to_custom_doctype.py
index 49b68ed240..7e84c5ae24 100644
--- a/frappe/patches/v11_0/apply_customization_to_custom_doctype.py
+++ b/frappe/patches/v11_0/apply_customization_to_custom_doctype.py
@@ -28,7 +28,7 @@ def execute():
for prop in property_setters:
property_setter_map[prop.field_name] = prop
- frappe.db.sql('DELETE FROM `tabProperty Setter` WHERE `name`=%s', prop.name)
+ frappe.db.delete("Property Setter", {"name": prop.name})
meta = frappe.get_meta(doctype.name)
@@ -50,6 +50,6 @@ def execute():
df = frappe.new_doc('DocField', meta, 'fields')
df.update(cf)
meta.fields.append(df)
- frappe.db.sql('DELETE FROM `tabCustom Field` WHERE name=%s', cf.name)
+ frappe.db.delete("Custom Field", {"name": cf.name})
meta.save()
diff --git a/frappe/patches/v11_0/change_email_signature_fieldtype.py b/frappe/patches/v11_0/change_email_signature_fieldtype.py
index f6d4bd5dcb..ccfa8541c3 100644
--- a/frappe/patches/v11_0/change_email_signature_fieldtype.py
+++ b/frappe/patches/v11_0/change_email_signature_fieldtype.py
@@ -1,7 +1,6 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
def execute():
diff --git a/frappe/patches/v11_0/copy_fetch_data_from_options.py b/frappe/patches/v11_0/copy_fetch_data_from_options.py
index ae7788450a..e256c7085f 100644
--- a/frappe/patches/v11_0/copy_fetch_data_from_options.py
+++ b/frappe/patches/v11_0/copy_fetch_data_from_options.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
def execute():
diff --git a/frappe/patches/v11_0/create_contact_for_user.py b/frappe/patches/v11_0/create_contact_for_user.py
index b4722ab3ae..5a483b630e 100644
--- a/frappe/patches/v11_0/create_contact_for_user.py
+++ b/frappe/patches/v11_0/create_contact_for_user.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
from frappe.core.doctype.user.user import create_contact
import re
@@ -8,7 +8,6 @@ def execute():
frappe.reload_doc('integrations', 'doctype', 'google_contacts')
frappe.reload_doc('contacts', 'doctype', 'contact')
frappe.reload_doc('core', 'doctype', 'dynamic_link')
- frappe.reload_doc('communication', 'doctype', 'call_log')
contact_meta = frappe.get_meta("Contact")
if contact_meta.has_field("phone_nos") and contact_meta.has_field("email_ids"):
diff --git a/frappe/patches/v11_0/delete_all_prepared_reports.py b/frappe/patches/v11_0/delete_all_prepared_reports.py
index 1d722da7e6..77f041e3ee 100644
--- a/frappe/patches/v11_0/delete_all_prepared_reports.py
+++ b/frappe/patches/v11_0/delete_all_prepared_reports.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
def execute():
diff --git a/frappe/patches/v11_0/delete_duplicate_user_permissions.py b/frappe/patches/v11_0/delete_duplicate_user_permissions.py
index 9d9d516ac5..518c1f7714 100644
--- a/frappe/patches/v11_0/delete_duplicate_user_permissions.py
+++ b/frappe/patches/v11_0/delete_duplicate_user_permissions.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
def execute():
diff --git a/frappe/patches/v11_0/drop_column_apply_user_permissions.py b/frappe/patches/v11_0/drop_column_apply_user_permissions.py
index 4f46bc0907..629d5a5da4 100644
--- a/frappe/patches/v11_0/drop_column_apply_user_permissions.py
+++ b/frappe/patches/v11_0/drop_column_apply_user_permissions.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
def execute():
diff --git a/frappe/patches/v11_0/fix_order_by_in_reports_json.py b/frappe/patches/v11_0/fix_order_by_in_reports_json.py
index 2cd82d442d..096e0e7654 100644
--- a/frappe/patches/v11_0/fix_order_by_in_reports_json.py
+++ b/frappe/patches/v11_0/fix_order_by_in_reports_json.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe, json
def execute():
diff --git a/frappe/patches/v11_0/make_all_prepared_report_attachments_private.py b/frappe/patches/v11_0/make_all_prepared_report_attachments_private.py
index f7b9e476a9..a099b89b40 100644
--- a/frappe/patches/v11_0/make_all_prepared_report_attachments_private.py
+++ b/frappe/patches/v11_0/make_all_prepared_report_attachments_private.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
diff --git a/frappe/patches/v11_0/migrate_report_settings_for_new_listview.py b/frappe/patches/v11_0/migrate_report_settings_for_new_listview.py
index 5bef52c295..e5b18368db 100644
--- a/frappe/patches/v11_0/migrate_report_settings_for_new_listview.py
+++ b/frappe/patches/v11_0/migrate_report_settings_for_new_listview.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe, json
def execute():
diff --git a/frappe/patches/v11_0/multiple_references_in_events.py b/frappe/patches/v11_0/multiple_references_in_events.py
index 57d4787eca..9fa5968d8e 100644
--- a/frappe/patches/v11_0/multiple_references_in_events.py
+++ b/frappe/patches/v11_0/multiple_references_in_events.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
def execute():
diff --git a/frappe/patches/v11_0/reload_and_rename_view_log.py b/frappe/patches/v11_0/reload_and_rename_view_log.py
index 12c71b746f..fa0432c4e2 100644
--- a/frappe/patches/v11_0/reload_and_rename_view_log.py
+++ b/frappe/patches/v11_0/reload_and_rename_view_log.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
def execute():
diff --git a/frappe/patches/v11_0/remove_doctype_user_permissions_for_page_and_report.py b/frappe/patches/v11_0/remove_doctype_user_permissions_for_page_and_report.py
index e2c2ef5f0e..5c54b1e5c1 100644
--- a/frappe/patches/v11_0/remove_doctype_user_permissions_for_page_and_report.py
+++ b/frappe/patches/v11_0/remove_doctype_user_permissions_for_page_and_report.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
def execute():
diff --git a/frappe/patches/v11_0/remove_skip_for_doctype.py b/frappe/patches/v11_0/remove_skip_for_doctype.py
index edd385e317..638a5a0fd7 100644
--- a/frappe/patches/v11_0/remove_skip_for_doctype.py
+++ b/frappe/patches/v11_0/remove_skip_for_doctype.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
from frappe.desk.form.linked_with import get_linked_doctypes
from frappe.patches.v11_0.replicate_old_user_permissions import get_doctypes_to_skip
diff --git a/frappe/patches/v11_0/rename_email_alert_to_notification.py b/frappe/patches/v11_0/rename_email_alert_to_notification.py
index 727055fcc4..365b76ea48 100644
--- a/frappe/patches/v11_0/rename_email_alert_to_notification.py
+++ b/frappe/patches/v11_0/rename_email_alert_to_notification.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
from frappe.model.rename_doc import rename_doc
diff --git a/frappe/patches/v11_0/rename_google_maps_doctype.py b/frappe/patches/v11_0/rename_google_maps_doctype.py
index 5420dcfc20..8091154b9c 100644
--- a/frappe/patches/v11_0/rename_google_maps_doctype.py
+++ b/frappe/patches/v11_0/rename_google_maps_doctype.py
@@ -1,8 +1,7 @@
-from __future__ import unicode_literals
+
import frappe
from frappe.model.rename_doc import rename_doc
def execute():
if frappe.db.exists("DocType","Google Maps") and not frappe.db.exists("DocType","Google Maps Settings"):
rename_doc('DocType', 'Google Maps', 'Google Maps Settings')
- frappe.reload_doc('integrations', 'doctype', 'google_maps_settings')
\ No newline at end of file
diff --git a/frappe/patches/v11_0/rename_standard_reply_to_email_template.py b/frappe/patches/v11_0/rename_standard_reply_to_email_template.py
index 06869530e2..2906085738 100644
--- a/frappe/patches/v11_0/rename_standard_reply_to_email_template.py
+++ b/frappe/patches/v11_0/rename_standard_reply_to_email_template.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
from frappe.model.rename_doc import rename_doc
diff --git a/frappe/patches/v11_0/rename_workflow_action_to_workflow_action_master.py b/frappe/patches/v11_0/rename_workflow_action_to_workflow_action_master.py
index 32f17ac2d8..9a48104611 100644
--- a/frappe/patches/v11_0/rename_workflow_action_to_workflow_action_master.py
+++ b/frappe/patches/v11_0/rename_workflow_action_to_workflow_action_master.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
from frappe.model.rename_doc import rename_doc
diff --git a/frappe/patches/v11_0/replicate_old_user_permissions.py b/frappe/patches/v11_0/replicate_old_user_permissions.py
index d1ceae8a7f..50a81b5ce7 100644
--- a/frappe/patches/v11_0/replicate_old_user_permissions.py
+++ b/frappe/patches/v11_0/replicate_old_user_permissions.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
import json
from frappe.utils import cint
diff --git a/frappe/patches/v11_0/set_allow_self_approval_in_workflow.py b/frappe/patches/v11_0/set_allow_self_approval_in_workflow.py
index 24c01e1a58..63ae5f949f 100644
--- a/frappe/patches/v11_0/set_allow_self_approval_in_workflow.py
+++ b/frappe/patches/v11_0/set_allow_self_approval_in_workflow.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
def execute():
diff --git a/frappe/patches/v11_0/set_default_letter_head_source.py b/frappe/patches/v11_0/set_default_letter_head_source.py
index a43ea397e4..3639524e7d 100644
--- a/frappe/patches/v11_0/set_default_letter_head_source.py
+++ b/frappe/patches/v11_0/set_default_letter_head_source.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
import frappe
def execute():
diff --git a/frappe/patches/v11_0/set_dropbox_file_backup.py b/frappe/patches/v11_0/set_dropbox_file_backup.py
index 884fef320e..27492b3ab2 100644
--- a/frappe/patches/v11_0/set_dropbox_file_backup.py
+++ b/frappe/patches/v11_0/set_dropbox_file_backup.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
from frappe.utils import cint
import frappe
diff --git a/frappe/patches/v11_0/sync_stripe_settings_before_migrate.py b/frappe/patches/v11_0/sync_stripe_settings_before_migrate.py
index 331b0eba32..901ab66bfd 100644
--- a/frappe/patches/v11_0/sync_stripe_settings_before_migrate.py
+++ b/frappe/patches/v11_0/sync_stripe_settings_before_migrate.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
from frappe.utils.password import get_decrypted_password
@@ -17,4 +17,4 @@ def execute():
settings.secret_key = secret_key
settings.save(ignore_permissions=True)
- frappe.db.sql("""DELETE FROM tabSingles WHERE doctype='Stripe Settings'""")
\ No newline at end of file
+ frappe.db.delete("Singles", {"doctype": "Stripe Settings"})
diff --git a/frappe/patches/v11_0/sync_user_permission_doctype_before_migrate.py b/frappe/patches/v11_0/sync_user_permission_doctype_before_migrate.py
index 738fea1a48..55a7b74f7e 100644
--- a/frappe/patches/v11_0/sync_user_permission_doctype_before_migrate.py
+++ b/frappe/patches/v11_0/sync_user_permission_doctype_before_migrate.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
def execute():
diff --git a/frappe/patches/v11_0/update_list_user_settings.py b/frappe/patches/v11_0/update_list_user_settings.py
index d492ff1704..1b179d8cdf 100644
--- a/frappe/patches/v11_0/update_list_user_settings.py
+++ b/frappe/patches/v11_0/update_list_user_settings.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe, json
from frappe.model.utils.user_settings import update_user_settings, sync_user_settings
diff --git a/frappe/patches/v12_0/create_notification_settings_for_user.py b/frappe/patches/v12_0/create_notification_settings_for_user.py
index 63eeccc07a..6edfd88872 100644
--- a/frappe/patches/v12_0/create_notification_settings_for_user.py
+++ b/frappe/patches/v12_0/create_notification_settings_for_user.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
from frappe.desk.doctype.notification_settings.notification_settings import create_notification_settings
diff --git a/frappe/patches/v12_0/delete_feedback_request_if_exists.py b/frappe/patches/v12_0/delete_feedback_request_if_exists.py
index fdbcecfc5a..c1bf46b14a 100644
--- a/frappe/patches/v12_0/delete_feedback_request_if_exists.py
+++ b/frappe/patches/v12_0/delete_feedback_request_if_exists.py
@@ -2,7 +2,4 @@
import frappe
def execute():
- frappe.db.sql('''
- DELETE from `tabDocType`
- WHERE name = 'Feedback Request'
- ''')
\ No newline at end of file
+ frappe.db.delete("DocType", {"name": "Feedback Request"})
diff --git a/frappe/patches/v12_0/init_desk_settings.py b/frappe/patches/v12_0/init_desk_settings.py
index ecd9c94d5b..fceb44b924 100644
--- a/frappe/patches/v12_0/init_desk_settings.py
+++ b/frappe/patches/v12_0/init_desk_settings.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
import json
import frappe
from frappe.config import get_modules_from_all_apps_for_user
diff --git a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py
index 040fde1bee..85be3f7feb 100644
--- a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py
+++ b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
import frappe
def execute():
diff --git a/frappe/patches/v12_0/remove_deprecated_fields_from_doctype.py b/frappe/patches/v12_0/remove_deprecated_fields_from_doctype.py
index 60599066e6..9c9a79ccbf 100644
--- a/frappe/patches/v12_0/remove_deprecated_fields_from_doctype.py
+++ b/frappe/patches/v12_0/remove_deprecated_fields_from_doctype.py
@@ -8,7 +8,6 @@ def execute():
'DocType': ['hide_heading', 'image_view', 'read_only_onload']
}, delete=1)
- frappe.db.sql('''
- DELETE from `tabProperty Setter`
- WHERE property = 'read_only_onload'
- ''')
+ frappe.db.delete("Property Setter", {
+ "property": "read_only_onload"
+ })
\ No newline at end of file
diff --git a/frappe/patches/v12_0/set_correct_assign_value_in_docs.py b/frappe/patches/v12_0/set_correct_assign_value_in_docs.py
index 65a635c170..5aaadd00e8 100644
--- a/frappe/patches/v12_0/set_correct_assign_value_in_docs.py
+++ b/frappe/patches/v12_0/set_correct_assign_value_in_docs.py
@@ -1,32 +1,27 @@
import frappe
+from frappe.query_builder.functions import GroupConcat, Coalesce
def execute():
- frappe.reload_doc('desk', 'doctype', 'todo')
+ frappe.reload_doc("desk", "doctype", "todo")
- query = '''
- SELECT
- name, reference_type, reference_name, {} as assignees
- FROM
- `tabToDo`
- WHERE
- COALESCE(reference_type, '') != '' AND
- COALESCE(reference_name, '') != '' AND
- status != 'Cancelled'
- GROUP BY
- reference_type, reference_name
- '''
+ ToDo = frappe.qb.DocType("ToDo")
+ assignees = GroupConcat("owner").distinct().as_("assignees")
- assignments = frappe.db.multisql({
- 'mariadb': query.format('GROUP_CONCAT(DISTINCT `owner`)'),
- 'postgres': query.format('STRING_AGG(DISTINCT "owner", ",")')
- }, as_dict=True)
+ assignments = (
+ frappe.qb.from_(ToDo)
+ .select(ToDo.name, ToDo.reference_type, assignees)
+ .where(Coalesce(ToDo.reference_type, "") != "")
+ .where(Coalesce(ToDo.reference_name, "") != "")
+ .where(ToDo.status != "Cancelled")
+ .groupby(ToDo.reference_type, ToDo.reference_name)
+ ).run(as_dict=True)
for doc in assignments:
- assignments = doc.assignees.split(',')
+ assignments = doc.assignees.split(",")
frappe.db.set_value(
doc.reference_type,
doc.reference_name,
- '_assign',
+ "_assign",
frappe.as_json(assignments),
update_modified=False
- )
+ )
\ No newline at end of file
diff --git a/frappe/patches/v12_0/set_primary_key_in_series.py b/frappe/patches/v12_0/set_primary_key_in_series.py
index e5ed2204ba..83a903fc2d 100644
--- a/frappe/patches/v12_0/set_primary_key_in_series.py
+++ b/frappe/patches/v12_0/set_primary_key_in_series.py
@@ -1,21 +1,24 @@
import frappe
def execute():
- #if current = 0, simply delete the key as it'll be recreated on first entry
- frappe.db.sql('delete from `tabSeries` where current = 0')
- duplicate_keys = frappe.db.sql('''
- SELECT name, max(current) as current
- from
- `tabSeries`
- group by
- name
- having count(name) > 1
- ''', as_dict=True)
- for row in duplicate_keys:
- frappe.db.sql('delete from `tabSeries` where name = %(key)s', {
- 'key': row.name
- })
- if row.current:
- frappe.db.sql('insert into `tabSeries`(`name`, `current`) values (%(name)s, %(current)s)', row)
- frappe.db.commit()
- frappe.db.sql('ALTER table `tabSeries` ADD PRIMARY KEY IF NOT EXISTS (name)')
+ #if current = 0, simply delete the key as it'll be recreated on first entry
+ frappe.db.delete("Series", {"current": 0})
+
+ duplicate_keys = frappe.db.sql('''
+ SELECT name, max(current) as current
+ from
+ `tabSeries`
+ group by
+ name
+ having count(name) > 1
+ ''', as_dict=True)
+
+ for row in duplicate_keys:
+ frappe.db.delete("Series", {
+ "name": row.name
+ })
+ if row.current:
+ frappe.db.sql('insert into `tabSeries`(`name`, `current`) values (%(name)s, %(current)s)', row)
+ frappe.db.commit()
+
+ frappe.db.sql('ALTER table `tabSeries` ADD PRIMARY KEY IF NOT EXISTS (name)')
diff --git a/frappe/patches/v12_0/setup_comments_from_communications.py b/frappe/patches/v12_0/setup_comments_from_communications.py
index 28c7aa93c0..11e02965f1 100644
--- a/frappe/patches/v12_0/setup_comments_from_communications.py
+++ b/frappe/patches/v12_0/setup_comments_from_communications.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
import frappe
def execute():
@@ -31,4 +29,6 @@ def execute():
frappe.db.auto_commit_on_many_writes = False
# clean up
- frappe.db.sql("delete from `tabCommunication` where communication_type = 'Comment'")
+ frappe.db.delete("Communication", {
+ "communication_type": "Comment"
+ })
diff --git a/frappe/patches/v12_0/setup_email_linking.py b/frappe/patches/v12_0/setup_email_linking.py
index 08f57ca5e4..9e939e1245 100644
--- a/frappe/patches/v12_0/setup_email_linking.py
+++ b/frappe/patches/v12_0/setup_email_linking.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
from frappe.desk.page.setup_wizard.install_fixtures import setup_email_linking
def execute():
diff --git a/frappe/patches/v12_0/update_auto_repeat_status_and_not_submittable.py b/frappe/patches/v12_0/update_auto_repeat_status_and_not_submittable.py
index d696b6c53a..3a3dcec315 100644
--- a/frappe/patches/v12_0/update_auto_repeat_status_and_not_submittable.py
+++ b/frappe/patches/v12_0/update_auto_repeat_status_and_not_submittable.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
diff --git a/frappe/patches/v13_0/add_standard_navbar_items.py b/frappe/patches/v13_0/add_standard_navbar_items.py
index 9982e6e3f5..4473cb8c07 100644
--- a/frappe/patches/v13_0/add_standard_navbar_items.py
+++ b/frappe/patches/v13_0/add_standard_navbar_items.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
from frappe.utils.install import add_standard_navbar_items
diff --git a/frappe/patches/v13_0/add_switch_theme_to_navbar_settings.py b/frappe/patches/v13_0/add_switch_theme_to_navbar_settings.py
index 29b99464b5..b5542c9c8a 100644
--- a/frappe/patches/v13_0/add_switch_theme_to_navbar_settings.py
+++ b/frappe/patches/v13_0/add_switch_theme_to_navbar_settings.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
def execute():
diff --git a/frappe/patches/v13_0/add_toggle_width_in_navbar_settings.py b/frappe/patches/v13_0/add_toggle_width_in_navbar_settings.py
index 59acb77480..bd3367377c 100644
--- a/frappe/patches/v13_0/add_toggle_width_in_navbar_settings.py
+++ b/frappe/patches/v13_0/add_toggle_width_in_navbar_settings.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
def execute():
diff --git a/frappe/patches/v13_0/cleanup_desk_cards.py b/frappe/patches/v13_0/cleanup_desk_cards.py
index 6ac8604041..b6fab66475 100644
--- a/frappe/patches/v13_0/cleanup_desk_cards.py
+++ b/frappe/patches/v13_0/cleanup_desk_cards.py
@@ -1,11 +1,10 @@
import frappe
-from six import string_types
from json import loads
from frappe.desk.doctype.workspace.workspace import get_link_type, get_report_type
def execute():
frappe.reload_doc('desk', 'doctype', 'workspace')
-
+
pages = frappe.db.sql("Select `name` from `tabDesk Page`")
# pages = frappe.get_all("Workspace", filters={"is_standard": 0}, pluck="name")
@@ -21,14 +20,14 @@ def rebuild_links(page):
doc = frappe.get_doc("Workspace", page)
except frappe.DoesNotExistError:
db_doc = get_doc_from_db(page)
-
+
doc = frappe.get_doc(db_doc)
doc.insert(ignore_permissions=True)
-
+
doc.links = []
for card in get_all_cards(page):
- if isinstance(card.links, string_types):
+ if isinstance(card.links, str):
links = loads(card.links)
else:
links = card.links
@@ -43,7 +42,7 @@ def rebuild_links(page):
for link in links:
if not frappe.db.exists(get_link_type(link.get('type')), link.get('name')):
continue
-
+
doc.append('links', {
"label": link.get('label') or link.get('name'),
"type": "Link",
@@ -53,7 +52,7 @@ def rebuild_links(page):
"dependencies": ', '.join(link.get('dependencies', [])),
"is_query_report": get_report_type(link.get('name')) if link.get('type').lower() == "report" else 0
})
-
+
try:
doc.save(ignore_permissions=True)
except frappe.LinkValidationError:
diff --git a/frappe/patches/v13_0/delete_event_producer_and_consumer_keys.py b/frappe/patches/v13_0/delete_event_producer_and_consumer_keys.py
index 1eba5871c2..776e9c796e 100644
--- a/frappe/patches/v13_0/delete_event_producer_and_consumer_keys.py
+++ b/frappe/patches/v13_0/delete_event_producer_and_consumer_keys.py
@@ -1,7 +1,6 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
def execute():
diff --git a/frappe/patches/v13_0/delete_package_publish_tool.py b/frappe/patches/v13_0/delete_package_publish_tool.py
index 25024f58dd..bf9aaf5a76 100644
--- a/frappe/patches/v13_0/delete_package_publish_tool.py
+++ b/frappe/patches/v13_0/delete_package_publish_tool.py
@@ -1,7 +1,6 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
diff --git a/frappe/patches/v13_0/enable_custom_script.py b/frappe/patches/v13_0/enable_custom_script.py
index edc242e700..0684074fe7 100644
--- a/frappe/patches/v13_0/enable_custom_script.py
+++ b/frappe/patches/v13_0/enable_custom_script.py
@@ -1,7 +1,6 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
def execute():
diff --git a/frappe/patches/v13_0/generate_theme_files_in_public_folder.py b/frappe/patches/v13_0/generate_theme_files_in_public_folder.py
index bcb47bec24..dd9fb1961a 100644
--- a/frappe/patches/v13_0/generate_theme_files_in_public_folder.py
+++ b/frappe/patches/v13_0/generate_theme_files_in_public_folder.py
@@ -1,7 +1,6 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
diff --git a/frappe/patches/v13_0/increase_password_length.py b/frappe/patches/v13_0/increase_password_length.py
index 1bb1979051..62ca2ed779 100644
--- a/frappe/patches/v13_0/increase_password_length.py
+++ b/frappe/patches/v13_0/increase_password_length.py
@@ -1,7 +1,4 @@
import frappe
def execute():
- frappe.db.multisql({
- "mariadb": "ALTER TABLE `__Auth` MODIFY `password` TEXT NOT NULL",
- "postgres": 'ALTER TABLE "__Auth" ALTER COLUMN "password" TYPE TEXT'
- })
+ frappe.db.change_column_type(table="__Auth", column="password", type="TEXT")
diff --git a/frappe/patches/v13_0/jinja_hook.py b/frappe/patches/v13_0/jinja_hook.py
index 84ed6e6cff..990ae50f35 100644
--- a/frappe/patches/v13_0/jinja_hook.py
+++ b/frappe/patches/v13_0/jinja_hook.py
@@ -1,7 +1,6 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
from click import secho
diff --git a/frappe/patches/v13_0/queryreport_columns.py b/frappe/patches/v13_0/queryreport_columns.py
index 6c2a1b1219..5c381f4f3e 100644
--- a/frappe/patches/v13_0/queryreport_columns.py
+++ b/frappe/patches/v13_0/queryreport_columns.py
@@ -1,7 +1,6 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
import json
diff --git a/frappe/patches/v13_0/remove_duplicate_navbar_items.py b/frappe/patches/v13_0/remove_duplicate_navbar_items.py
index cb4de4ca07..b6c6033f64 100644
--- a/frappe/patches/v13_0/remove_duplicate_navbar_items.py
+++ b/frappe/patches/v13_0/remove_duplicate_navbar_items.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
def execute():
diff --git a/frappe/patches/v13_0/remove_tailwind_from_page_builder.py b/frappe/patches/v13_0/remove_tailwind_from_page_builder.py
index 6e7bf67bac..2bf2c7bf87 100644
--- a/frappe/patches/v13_0/remove_tailwind_from_page_builder.py
+++ b/frappe/patches/v13_0/remove_tailwind_from_page_builder.py
@@ -1,7 +1,6 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
diff --git a/frappe/patches/v13_0/remove_twilio_settings.py b/frappe/patches/v13_0/remove_twilio_settings.py
index 363cbdd4b6..7efaf876e2 100644
--- a/frappe/patches/v13_0/remove_twilio_settings.py
+++ b/frappe/patches/v13_0/remove_twilio_settings.py
@@ -12,7 +12,9 @@ def execute():
frappe.delete_doc_if_exists('DocType', 'Twilio Number Group')
if twilio_settings_doctype_in_integrations():
frappe.delete_doc_if_exists('DocType', 'Twilio Settings')
- frappe.db.sql("delete from `tabSingles` where `doctype`=%s", 'Twilio Settings')
+ frappe.db.delete("Singles", {
+ "doctype": "Twilio Settings"
+ })
def twilio_settings_doctype_in_integrations() -> bool:
"""Check Twilio Settings doctype exists in integrations module or not.
diff --git a/frappe/patches/v13_0/rename_list_view_setting_to_list_view_settings.py b/frappe/patches/v13_0/rename_list_view_setting_to_list_view_settings.py
index 7c3aec9510..3122de8bea 100644
--- a/frappe/patches/v13_0/rename_list_view_setting_to_list_view_settings.py
+++ b/frappe/patches/v13_0/rename_list_view_setting_to_list_view_settings.py
@@ -1,7 +1,6 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
diff --git a/frappe/patches/v13_0/rename_notification_fields.py b/frappe/patches/v13_0/rename_notification_fields.py
index 2984e6503c..1413d80358 100644
--- a/frappe/patches/v13_0/rename_notification_fields.py
+++ b/frappe/patches/v13_0/rename_notification_fields.py
@@ -1,7 +1,6 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.utils.rename_field import rename_field
diff --git a/frappe/patches/v13_0/rename_onboarding.py b/frappe/patches/v13_0/rename_onboarding.py
index c506c6076e..852065dfd2 100644
--- a/frappe/patches/v13_0/rename_onboarding.py
+++ b/frappe/patches/v13_0/rename_onboarding.py
@@ -1,7 +1,6 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
def execute():
diff --git a/frappe/patches/v13_0/replace_old_data_import.py b/frappe/patches/v13_0/replace_old_data_import.py
index 920ee7b553..838881b48e 100644
--- a/frappe/patches/v13_0/replace_old_data_import.py
+++ b/frappe/patches/v13_0/replace_old_data_import.py
@@ -1,7 +1,6 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
diff --git a/frappe/patches/v13_0/update_date_filters_in_user_settings.py b/frappe/patches/v13_0/update_date_filters_in_user_settings.py
index d4c6aa1d03..3b1b07fe0a 100644
--- a/frappe/patches/v13_0/update_date_filters_in_user_settings.py
+++ b/frappe/patches/v13_0/update_date_filters_in_user_settings.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe, json
from frappe.model.utils.user_settings import update_user_settings, sync_user_settings
diff --git a/frappe/patches/v13_0/update_duration_options.py b/frappe/patches/v13_0/update_duration_options.py
index 60eef8fc93..e0d8dea4ea 100644
--- a/frappe/patches/v13_0/update_duration_options.py
+++ b/frappe/patches/v13_0/update_duration_options.py
@@ -1,7 +1,6 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
def execute():
diff --git a/frappe/patches/v13_0/update_icons_in_customized_desk_pages.py b/frappe/patches/v13_0/update_icons_in_customized_desk_pages.py
index 93bf5c766e..ff58f99c2f 100644
--- a/frappe/patches/v13_0/update_icons_in_customized_desk_pages.py
+++ b/frappe/patches/v13_0/update_icons_in_customized_desk_pages.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+
import frappe
def execute():
diff --git a/frappe/patches/v13_0/update_newsletter_content_type.py b/frappe/patches/v13_0/update_newsletter_content_type.py
index 6f8dcc1935..5f047680ee 100644
--- a/frappe/patches/v13_0/update_newsletter_content_type.py
+++ b/frappe/patches/v13_0/update_newsletter_content_type.py
@@ -1,7 +1,6 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
def execute():
diff --git a/frappe/patches/v13_0/update_notification_channel_if_empty.py b/frappe/patches/v13_0/update_notification_channel_if_empty.py
index 2c2a40e81b..bcf9a7b28c 100644
--- a/frappe/patches/v13_0/update_notification_channel_if_empty.py
+++ b/frappe/patches/v13_0/update_notification_channel_if_empty.py
@@ -1,7 +1,6 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
def execute():
diff --git a/frappe/patches/v13_0/web_template_set_module.py b/frappe/patches/v13_0/web_template_set_module.py
index df008557d8..2ee9e3ba2d 100644
--- a/frappe/patches/v13_0/web_template_set_module.py
+++ b/frappe/patches/v13_0/web_template_set_module.py
@@ -1,7 +1,6 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
def execute():
diff --git a/frappe/patches/v4_3/__init__.py b/frappe/patches/v14_0/__init__.py
similarity index 100%
rename from frappe/patches/v4_3/__init__.py
rename to frappe/patches/v14_0/__init__.py
diff --git a/frappe/patches/v14_0/drop_data_import_legacy.py b/frappe/patches/v14_0/drop_data_import_legacy.py
new file mode 100644
index 0000000000..2037930c9f
--- /dev/null
+++ b/frappe/patches/v14_0/drop_data_import_legacy.py
@@ -0,0 +1,22 @@
+import frappe
+import click
+
+
+def execute():
+ doctype = "Data Import Legacy"
+ table = frappe.utils.get_table_name(doctype)
+
+ # delete the doctype record to avoid broken links
+ frappe.db.delete("DocType", {"name": doctype})
+
+ # leaving table in database for manual cleanup
+ click.secho(
+ f"`{doctype}` has been deprecated. The DocType is deleted, but the data still"
+ " exists on the database. If this data is worth recovering, you may export it"
+ f" using\n\n\tbench --site {frappe.local.site} backup -i '{doctype}'\n\nAfter"
+ " this, the table will continue to persist in the database, until you choose"
+ " to remove it yourself. If you want to drop the table, you may run\n\n\tbench"
+ f" --site {frappe.local.site} execute frappe.db.sql --args \"('DROP TABLE IF"
+ f" EXISTS `{table}`', )\"\n",
+ fg="yellow",
+ )
diff --git a/frappe/patches/v14_0/rename_cancelled_documents.py b/frappe/patches/v14_0/rename_cancelled_documents.py
new file mode 100644
index 0000000000..4b565d4f76
--- /dev/null
+++ b/frappe/patches/v14_0/rename_cancelled_documents.py
@@ -0,0 +1,213 @@
+import functools
+import traceback
+
+import frappe
+
+def execute():
+ """Rename cancelled documents by adding a postfix.
+ """
+ rename_cancelled_docs()
+
+def get_submittable_doctypes():
+ """Returns list of submittable doctypes in the system.
+ """
+ return frappe.db.get_all('DocType', filters={'is_submittable': 1}, pluck='name')
+
+def get_cancelled_doc_names(doctype):
+ """Return names of cancelled document names those are in old format.
+ """
+ docs = frappe.db.get_all(doctype, filters={'docstatus': 2}, pluck='name')
+ return [each for each in docs if not (each.endswith('-CANC') or ('-CANC-' in each))]
+
+@functools.lru_cache()
+def get_linked_doctypes():
+ """Returns list of doctypes those are linked with given doctype using 'Link' fieldtype.
+ """
+ filters=[['fieldtype','=', 'Link']]
+ links = frappe.get_all("DocField",
+ fields=["parent", "fieldname", "options as linked_to"],
+ filters=filters,
+ as_list=1)
+
+ links+= frappe.get_all("Custom Field",
+ fields=["dt as parent", "fieldname", "options as linked_to"],
+ filters=filters,
+ as_list=1)
+
+ links_by_doctype = {}
+ for doctype, fieldname, linked_to in links:
+ links_by_doctype.setdefault(linked_to, []).append((doctype, fieldname))
+ return links_by_doctype
+
+@functools.lru_cache()
+def get_single_doctypes():
+ return frappe.get_all("DocType", filters={'issingle': 1}, pluck='name')
+
+@functools.lru_cache()
+def get_dynamic_linked_doctypes():
+ filters=[['fieldtype','=', 'Dynamic Link']]
+
+ # find dynamic links of parents
+ links = frappe.get_all("DocField",
+ fields=["parent as doctype", "fieldname", "options as doctype_fieldname"],
+ filters=filters,
+ as_list=1)
+ links+= frappe.get_all("Custom Field",
+ fields=["dt as doctype", "fieldname", "options as doctype_fieldname"],
+ filters=filters,
+ as_list=1)
+ return links
+
+@functools.lru_cache()
+def get_child_tables():
+ """
+ """
+ filters =[['fieldtype', 'in', ('Table', 'Table MultiSelect')]]
+ links = frappe.get_all("DocField",
+ fields=["parent as doctype", "options as child_table"],
+ filters=filters,
+ as_list=1)
+
+ links+= frappe.get_all("Custom Field",
+ fields=["dt as doctype", "options as child_table"],
+ filters=filters,
+ as_list=1)
+
+ map = {}
+ for doctype, child_table in links:
+ map.setdefault(doctype, []).append(child_table)
+ return map
+
+def update_cancelled_document_names(doctype, cancelled_doc_names):
+ return frappe.db.sql("""
+ update
+ `tab{doctype}`
+ set
+ name=CONCAT(name, '-CANC')
+ where
+ docstatus=2
+ and
+ name in %(cancelled_doc_names)s;
+ """.format(doctype=doctype), {'cancelled_doc_names': cancelled_doc_names})
+
+def update_amended_field(doctype, cancelled_doc_names):
+ return frappe.db.sql("""
+ update
+ `tab{doctype}`
+ set
+ amended_from=CONCAT(amended_from, '-CANC')
+ where
+ amended_from in %(cancelled_doc_names)s;
+ """.format(doctype=doctype), {'cancelled_doc_names': cancelled_doc_names})
+
+def update_attachments(doctype, cancelled_doc_names):
+ frappe.db.sql("""
+ update
+ `tabFile`
+ set
+ attached_to_name=CONCAT(attached_to_name, '-CANC')
+ where
+ attached_to_doctype=%(dt)s and attached_to_name in %(cancelled_doc_names)s
+ """, {'cancelled_doc_names': cancelled_doc_names, 'dt': doctype})
+
+def update_versions(doctype, cancelled_doc_names):
+ frappe.db.sql("""
+ UPDATE
+ `tabVersion`
+ SET
+ docname=CONCAT(docname, '-CANC')
+ WHERE
+ ref_doctype=%(dt)s AND docname in %(cancelled_doc_names)s
+ """, {'cancelled_doc_names': cancelled_doc_names, 'dt': doctype})
+
+def update_linked_doctypes(doctype, cancelled_doc_names):
+ single_doctypes = get_single_doctypes()
+
+ for linked_dt, field in get_linked_doctypes().get(doctype, []):
+ if linked_dt not in single_doctypes:
+ frappe.db.sql("""
+ update
+ `tab{linked_dt}`
+ set
+ `{column}`=CONCAT(`{column}`, '-CANC')
+ where
+ `{column}` in %(cancelled_doc_names)s;
+ """.format(linked_dt=linked_dt, column=field),
+ {'cancelled_doc_names': cancelled_doc_names})
+ else:
+ doc = frappe.get_single(linked_dt)
+ if getattr(doc, field) in cancelled_doc_names:
+ setattr(doc, field, getattr(doc, field)+'-CANC')
+ doc.flags.ignore_mandatory=True
+ doc.flags.ignore_validate=True
+ doc.save(ignore_permissions=True)
+
+def update_dynamic_linked_doctypes(doctype, cancelled_doc_names):
+ single_doctypes = get_single_doctypes()
+
+ for linked_dt, fieldname, doctype_fieldname in get_dynamic_linked_doctypes():
+ if linked_dt not in single_doctypes:
+ frappe.db.sql("""
+ update
+ `tab{linked_dt}`
+ set
+ `{column}`=CONCAT(`{column}`, '-CANC')
+ where
+ `{column}` in %(cancelled_doc_names)s and {doctype_fieldname}=%(dt)s;
+ """.format(linked_dt=linked_dt, column=fieldname, doctype_fieldname=doctype_fieldname),
+ {'cancelled_doc_names': cancelled_doc_names, 'dt': doctype})
+ else:
+ doc = frappe.get_single(linked_dt)
+ if getattr(doc, doctype_fieldname) == doctype and getattr(doc, fieldname) in cancelled_doc_names:
+ setattr(doc, fieldname, getattr(doc, fieldname)+'-CANC')
+ doc.flags.ignore_mandatory=True
+ doc.flags.ignore_validate=True
+ doc.save(ignore_permissions=True)
+
+def update_child_tables(doctype, cancelled_doc_names):
+ child_tables = get_child_tables().get(doctype, [])
+ single_doctypes = get_single_doctypes()
+
+ for table in child_tables:
+ if table not in single_doctypes:
+ frappe.db.sql("""
+ update
+ `tab{table}`
+ set
+ parent=CONCAT(parent, '-CANC')
+ where
+ parenttype=%(dt)s and parent in %(cancelled_doc_names)s;
+ """.format(table=table), {'cancelled_doc_names': cancelled_doc_names, 'dt': doctype})
+ else:
+ doc = frappe.get_single(table)
+ if getattr(doc, 'parenttype')==doctype and getattr(doc, 'parent') in cancelled_doc_names:
+ setattr(doc, 'parent', getattr(doc, 'parent')+'-CANC')
+ doc.flags.ignore_mandatory=True
+ doc.flags.ignore_validate=True
+ doc.save(ignore_permissions=True)
+
+def rename_cancelled_docs():
+ submittable_doctypes = get_submittable_doctypes()
+
+ for dt in submittable_doctypes:
+ for retry in range(2):
+ try:
+ cancelled_doc_names = tuple(get_cancelled_doc_names(dt))
+ if not cancelled_doc_names:
+ break
+ update_cancelled_document_names(dt, cancelled_doc_names)
+ update_amended_field(dt, cancelled_doc_names)
+ update_child_tables(dt, cancelled_doc_names)
+ update_linked_doctypes(dt, cancelled_doc_names)
+ update_dynamic_linked_doctypes(dt, cancelled_doc_names)
+ update_attachments(dt, cancelled_doc_names)
+ update_versions(dt, cancelled_doc_names)
+ print(f"Renaming cancelled records of {dt} doctype")
+ frappe.db.commit()
+ break
+ except Exception:
+ if retry == 1:
+ print(f"Failed to rename the cancelled records of {dt} doctype, moving on!")
+ traceback.print_exc()
+ frappe.db.rollback()
+
diff --git a/frappe/patches/v14_0/update_workspace2.py b/frappe/patches/v14_0/update_workspace2.py
new file mode 100644
index 0000000000..c212faee76
--- /dev/null
+++ b/frappe/patches/v14_0/update_workspace2.py
@@ -0,0 +1,69 @@
+import frappe
+import json
+from frappe import _
+
+def execute():
+ frappe.reload_doc('desk', 'doctype', 'workspace', force=True)
+ order_by = "pin_to_top desc, pin_to_bottom asc, name asc"
+ for seq, wspace in enumerate(frappe.get_all('Workspace', order_by=order_by)):
+ doc = frappe.get_doc('Workspace', wspace.name)
+ content = create_content(doc)
+ update_wspace(doc, seq, content)
+ frappe.db.commit()
+
+def create_content(doc):
+ content = []
+ if doc.onboarding:
+ content.append({"type":"onboarding","data":{"onboarding_name":doc.onboarding,"col":12}})
+ if doc.charts:
+ invalid_links = []
+ for c in doc.charts:
+ if c.get_invalid_links()[0]:
+ invalid_links.append(c)
+ else:
+ content.append({"type":"chart","data":{"chart_name":c.label,"col":12}})
+ for l in invalid_links:
+ del doc.charts[doc.charts.index(l)]
+ if doc.shortcuts:
+ invalid_links = []
+ if doc.charts:
+ content.append({"type":"spacer","data":{"col":12}})
+ content.append({"type":"header","data":{"text":doc.shortcuts_label or _("Your Shortcuts"),"level":4,"col":12}})
+ for s in doc.shortcuts:
+ if s.get_invalid_links()[0]:
+ invalid_links.append(s)
+ else:
+ content.append({"type":"shortcut","data":{"shortcut_name":s.label,"col":4}})
+ for l in invalid_links:
+ del doc.shortcuts[doc.shortcuts.index(l)]
+ if doc.links:
+ invalid_links = []
+ content.append({"type":"spacer","data":{"col":12}})
+ content.append({"type":"header","data":{"text":doc.cards_label or _("Reports & Masters"),"level":4,"col":12}})
+ for l in doc.links:
+ if l.type == 'Card Break':
+ content.append({"type":"card","data":{"card_name":l.label,"col":4}})
+ if l.get_invalid_links()[0]:
+ invalid_links.append(l)
+ for l in invalid_links:
+ del doc.links[doc.links.index(l)]
+ return content
+
+def update_wspace(doc, seq, content):
+ if not doc.title and not doc.content and not doc.is_standard and not doc.public:
+ doc.sequence_id = seq + 1
+ doc.content = json.dumps(content)
+ doc.public = 0
+ doc.title = doc.extends or doc.label
+ doc.extends = ''
+ doc.category = ''
+ doc.onboarding = ''
+ doc.extends_another_page = 0
+ doc.is_default = 0
+ doc.is_standard = 0
+ doc.developer_mode_only = 0
+ doc.disable_user_customization = 0
+ doc.pin_to_top = 0
+ doc.pin_to_bottom = 0
+ doc.hide_custom = 0
+ doc.save(ignore_permissions=True)
\ No newline at end of file
diff --git a/frappe/patches/v4_0/add_delete_permission.py b/frappe/patches/v4_0/add_delete_permission.py
deleted file mode 100644
index 091bdab3ff..0000000000
--- a/frappe/patches/v4_0/add_delete_permission.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doc("core", "doctype", "docperm")
-
- # delete same as cancel (map old permissions)
- frappe.db.sql("""update tabDocPerm set `delete`=ifnull(`cancel`,0)""")
-
- # can't cancel if can't submit
- frappe.db.sql("""update tabDocPerm set `cancel`=0 where ifnull(`submit`,0)=0""")
-
- frappe.clear_cache()
\ No newline at end of file
diff --git a/frappe/patches/v4_0/change_varchar_length.py b/frappe/patches/v4_0/change_varchar_length.py
deleted file mode 100644
index 29fe8f310d..0000000000
--- a/frappe/patches/v4_0/change_varchar_length.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.db.sql('update tabDocField set search_index=0 where fieldtype="Small Text"')
- frappe.db.sql('update tabDocField set in_list_view=0 where fieldtype="Image"')
-
- for dt in frappe.db.sql_list("""select name from `tabDocType` where issingle=0"""):
- desc = dict((d["Field"], d) for d in frappe.db.sql("desc `tab{}`".format(dt), as_dict=True))
- alter_table = []
-
- if desc["name"]["Type"] != "varchar(255)":
- alter_table.append("change `name` `name` varchar(255) not null")
-
- for fieldname in ("modified_by", "owner", "parent", "parentfield", "parenttype"):
- if desc[fieldname]["Type"] != "varchar(255)":
- alter_table.append("change `{fieldname}` `{fieldname}` varchar(255)".format(fieldname=fieldname))
-
- if alter_table:
- alter_table_query = "alter table `tab{doctype}` {alter_table}".format(doctype=dt, alter_table=",\n".join(alter_table))
- # print alter_table_query
- frappe.db.sql_ddl(alter_table_query)
-
diff --git a/frappe/patches/v4_0/create_custom_field_for_owner_match.py b/frappe/patches/v4_0/create_custom_field_for_owner_match.py
deleted file mode 100644
index 60dafc27da..0000000000
--- a/frappe/patches/v4_0/create_custom_field_for_owner_match.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-from __future__ import unicode_literals, print_function
-import frappe
-from frappe.custom.doctype.custom_field.custom_field import create_custom_field
-
-def execute():
- if "match" in frappe.db.get_table_columns("DocPerm"):
- create_custom_field_for_owner_match()
-
-def create_custom_field_for_owner_match():
- docperm_meta = frappe.get_meta('DocPerm')
- if docperm_meta.get_field('apply_user_permissions'):
- frappe.db.sql("""update `tabDocPerm` set apply_user_permissions=1 where `match`='owner'""")
-
- for dt in frappe.db.sql_list("""select distinct parent from `tabDocPerm`
- where `match`='owner' and permlevel=0 and parent != 'User'"""):
-
- # a link field pointing to User already exists
- if (frappe.db.get_value("DocField", {"parent": dt, "fieldtype": "Link", "options": "User", "default": "__user"})
- or frappe.db.get_value("Custom Field", {"dt": dt, "fieldtype": "Link", "options": "User", "default": "__user"})):
- print("User link field already exists for", dt)
- continue
-
- fieldname = "{}_owner".format(frappe.scrub(dt))
-
- create_custom_field(dt, frappe._dict({
- "permlevel": 0,
- "label": "{} Owner".format(dt),
- "fieldname": fieldname,
- "fieldtype": "Link",
- "options": "User",
- "default": "__user"
- }))
-
- frappe.db.sql("""update `tab{doctype}` set `{fieldname}`=owner""".format(doctype=dt,
- fieldname=fieldname))
-
- # commit is required so that we don't lose these changes because of an error in next loop's ddl
- frappe.db.commit()
diff --git a/frappe/patches/v4_0/deprecate_control_panel.py b/frappe/patches/v4_0/deprecate_control_panel.py
deleted file mode 100644
index 892d3043c4..0000000000
--- a/frappe/patches/v4_0/deprecate_control_panel.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.db.sql("update `tabDefaultValue` set parenttype='__default' where parenttype='Control Panel'")
- frappe.db.sql("update `tabDefaultValue` set parent='__default' where parent='Control Panel'")
- frappe.clear_cache()
diff --git a/frappe/patches/v4_0/deprecate_link_selects.py b/frappe/patches/v4_0/deprecate_link_selects.py
deleted file mode 100644
index a3243cffb8..0000000000
--- a/frappe/patches/v4_0/deprecate_link_selects.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- for name in frappe.db.sql_list("""select name from `tabCustom Field`
- where fieldtype="Select" and options like "link:%" """):
- custom_field = frappe.get_doc("Custom Field", name)
- custom_field.fieldtype = "Link"
- custom_field.options = custom_field.options[5:]
- custom_field.save()
diff --git a/frappe/patches/v4_0/enable_scheduler_in_system_settings.py b/frappe/patches/v4_0/enable_scheduler_in_system_settings.py
deleted file mode 100644
index 5d1b836270..0000000000
--- a/frappe/patches/v4_0/enable_scheduler_in_system_settings.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe.utils.scheduler import disable_scheduler, enable_scheduler
-from frappe.utils import cint
-
-def execute():
- frappe.reload_doc("core", "doctype", "system_settings")
- if cint(frappe.db.get_global("disable_scheduler")):
- disable_scheduler()
- else:
- enable_scheduler()
diff --git a/frappe/patches/v4_0/file_manager_hooks.py b/frappe/patches/v4_0/file_manager_hooks.py
deleted file mode 100644
index 6be3b25124..0000000000
--- a/frappe/patches/v4_0/file_manager_hooks.py
+++ /dev/null
@@ -1,37 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-from __future__ import unicode_literals, print_function
-
-import frappe
-import os
-from frappe.utils import get_files_path
-from frappe.core.doctype.file.file import get_content_hash
-
-
-def execute():
- frappe.reload_doc('core', 'doctype', 'file_data')
- for name, file_name, file_url in frappe.db.sql(
- """select name, file_name, file_url from `tabFile`
- where file_name is not null"""):
- b = frappe.get_doc('File', name)
- old_file_name = b.file_name
- b.file_name = os.path.basename(old_file_name)
- if old_file_name.startswith('files/') or old_file_name.startswith('/files/'):
- b.file_url = os.path.normpath('/' + old_file_name)
- else:
- b.file_url = os.path.normpath('/files/' + old_file_name)
- try:
- _file = frappe.get_doc("File", {"file_name": name})
- content = _file.get_content()
- b.content_hash = get_content_hash(content)
- except IOError:
- print('Warning: Error processing ', name)
- _file_name = old_file_name
- b.content_hash = None
-
- try:
- b.save()
- except frappe.DuplicateEntryError:
- frappe.delete_doc(b.doctype, b.name)
-
diff --git a/frappe/patches/v4_0/fix_attach_field_file_url.py b/frappe/patches/v4_0/fix_attach_field_file_url.py
deleted file mode 100644
index c29e5763f1..0000000000
--- a/frappe/patches/v4_0/fix_attach_field_file_url.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- attach_fields = (frappe.db.sql("""select parent, fieldname from `tabDocField` where fieldtype in ('Attach', 'Attach Image')""") +
- frappe.db.sql("""select dt, fieldname from `tabCustom Field` where fieldtype in ('Attach', 'Attach Image')"""))
-
- for doctype, fieldname in attach_fields:
- frappe.db.sql("""update `tab{doctype}` set `{fieldname}`=concat("/", `{fieldname}`)
- where `{fieldname}` like 'files/%'""".format(doctype=doctype, fieldname=fieldname))
diff --git a/frappe/patches/v4_0/private_backups.py b/frappe/patches/v4_0/private_backups.py
deleted file mode 100644
index 016af0615d..0000000000
--- a/frappe/patches/v4_0/private_backups.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe.installer import make_site_dirs
-
-def execute():
- make_site_dirs()
- if frappe.local.conf.backup_path and frappe.local.conf.backup_path.startswith("public"):
- raise Exception("Backups path in conf set to public directory")
diff --git a/frappe/patches/v4_0/remove_index_sitemap.py b/frappe/patches/v4_0/remove_index_sitemap.py
deleted file mode 100644
index 5dcd0d79c7..0000000000
--- a/frappe/patches/v4_0/remove_index_sitemap.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- pass
diff --git a/frappe/patches/v4_0/remove_old_parent.py b/frappe/patches/v4_0/remove_old_parent.py
deleted file mode 100644
index 7717f7b7e3..0000000000
--- a/frappe/patches/v4_0/remove_old_parent.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- for doctype in frappe.db.sql_list("""select name from `tabDocType` where istable=1"""):
- frappe.db.sql("""delete from `tab{0}` where parent like "old_par%:%" """.format(doctype))
- frappe.db.sql("""delete from `tabDocField` where parent="0" """)
diff --git a/frappe/patches/v4_0/remove_user_owner_custom_field.py b/frappe/patches/v4_0/remove_user_owner_custom_field.py
deleted file mode 100644
index be6a45e090..0000000000
--- a/frappe/patches/v4_0/remove_user_owner_custom_field.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- user_owner = frappe.db.get_value("Custom Field", {"fieldname": "user_owner"})
- if user_owner:
- frappe.delete_doc("Custom Field", user_owner)
diff --git a/frappe/patches/v4_0/rename_profile_to_user.py b/frappe/patches/v4_0/rename_profile_to_user.py
deleted file mode 100644
index 48555ead9e..0000000000
--- a/frappe/patches/v4_0/rename_profile_to_user.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-from frappe.model.utils.rename_field import rename_field
-from frappe.model.meta import get_table_columns
-
-def execute():
- tables = frappe.db.sql_list("show tables")
- if "tabUser" not in tables:
- frappe.rename_doc("DocType", "Profile", "User", force=True)
-
- frappe.reload_doc("website", "doctype", "blogger")
-
- if "profile" in get_table_columns("Blogger"):
- rename_field("Blogger", "profile", "user")
diff --git a/frappe/patches/v4_0/rename_sitemap_to_route.py b/frappe/patches/v4_0/rename_sitemap_to_route.py
deleted file mode 100644
index 8ae5170b44..0000000000
--- a/frappe/patches/v4_0/rename_sitemap_to_route.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-from frappe.model.utils.rename_field import rename_field
-
-def execute():
- tables = frappe.db.sql_list("show tables")
- for doctype in ("Website Sitemap", "Website Sitemap Config"):
- if "tab{}".format(doctype) in tables:
- frappe.delete_doc("DocType", doctype, force=1)
- frappe.db.sql("drop table `tab{}`".format(doctype))
-
- for d in ("Blog Category", "Blog Post", "Web Page"):
- frappe.reload_doc("website", "doctype", frappe.scrub(d))
- rename_field_if_exists(d, "parent_website_sitemap", "parent_website_route")
-
- for d in ("blog_category", "blog_post", "web_page", "post", "user_vote"):
- frappe.reload_doc("website", "doctype", d)
-
-def rename_field_if_exists(doctype, old_fieldname, new_fieldname):
- try:
- rename_field(doctype, old_fieldname, new_fieldname)
- except frappe.db.ProgrammingError as e:
- if not frappe.db.is_column_missing(e):
- raise
diff --git a/frappe/patches/v4_0/replace_deprecated_timezones.py b/frappe/patches/v4_0/replace_deprecated_timezones.py
deleted file mode 100644
index a491325ebc..0000000000
--- a/frappe/patches/v4_0/replace_deprecated_timezones.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe.utils.momentjs import data as momentjs_data
-
-def execute():
- frappe.reload_doc("core", "doctype", "user")
-
- ss = frappe.get_doc("System Settings", "System Settings")
- if ss.time_zone in momentjs_data.get("links"):
- ss.time_zone = momentjs_data["links"][ss.time_zone]
- ss.flags.ignore_mandatory = True
- ss.save()
-
- for user, time_zone in frappe.db.sql("select name, time_zone from `tabUser` where ifnull(time_zone, '')!=''"):
- if time_zone in momentjs_data.get("links"):
- user = frappe.get_doc("User", user)
- user.time_zone = momentjs_data["links"][user.time_zone]
- user.save()
diff --git a/frappe/patches/v4_0/set_module_in_report.py b/frappe/patches/v4_0/set_module_in_report.py
deleted file mode 100644
index 9760f7efb3..0000000000
--- a/frappe/patches/v4_0/set_module_in_report.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doc("core", "doctype", "report")
- frappe.db.sql("""update `tabReport` r set r.module=(select d.module from `tabDocType` d
- where d.name=r.ref_doctype) where ifnull(r.module, '')=''""")
\ No newline at end of file
diff --git a/frappe/patches/v4_0/set_todo_checked_as_closed.py b/frappe/patches/v4_0/set_todo_checked_as_closed.py
deleted file mode 100644
index 59e8df3793..0000000000
--- a/frappe/patches/v4_0/set_todo_checked_as_closed.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doc("core", "doctype", "todo")
- try:
- frappe.db.sql("""update tabToDo set status = if(ifnull(checked,0)=0, 'Open', 'Closed')""")
- except:
- pass
diff --git a/frappe/patches/v4_0/set_user_gravatar.py b/frappe/patches/v4_0/set_user_gravatar.py
deleted file mode 100644
index 733b9bfe11..0000000000
--- a/frappe/patches/v4_0/set_user_gravatar.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- for name in frappe.db.sql_list("select name from `tabUser` where ifnull(user_image, '')=''"):
- user = frappe.get_doc("User", name)
- user.update_gravatar()
- user.db_set("user_image", user.user_image)
diff --git a/frappe/patches/v4_0/set_user_permissions.py b/frappe/patches/v4_0/set_user_permissions.py
deleted file mode 100644
index 726b9ee715..0000000000
--- a/frappe/patches/v4_0/set_user_permissions.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-import frappe.permissions
-
-def execute():
- frappe.reload_doc("core", "doctype", "docperm")
- table_columns = frappe.db.get_table_columns("DocPerm")
-
- if "restricted" in table_columns:
- frappe.db.sql("""update `tabDocPerm` set apply_user_permissions=1 where apply_user_permissions=0
- and restricted=1""")
-
- if "match" in table_columns:
- frappe.db.sql("""update `tabDocPerm` set apply_user_permissions=1
- where apply_user_permissions=0 and ifnull(`match`, '')!=''""")
-
- # change Restriction to User Permission in tabDefaultValue
- frappe.db.sql("""update `tabDefaultValue` set parenttype='User Permission' where parenttype='Restriction'""")
-
- frappe.clear_cache()
-
diff --git a/frappe/patches/v4_0/set_website_route_idx.py b/frappe/patches/v4_0/set_website_route_idx.py
deleted file mode 100644
index 663a324008..0000000000
--- a/frappe/patches/v4_0/set_website_route_idx.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- pass
- # from frappe.website.doctype.website_template.website_template import \
- # get_pages_and_generators, get_template_controller
- #
- # frappe.reload_doc("website", "doctype", "website_template")
- # frappe.reload_doc("website", "doctype", "website_route")
- #
- # for app in frappe.get_installed_apps():
- # pages, generators = get_pages_and_generators(app)
- # for g in generators:
- # doctype = frappe.get_attr(get_template_controller(app, g["path"], g["fname"]) + ".doctype")
- # module = frappe.db.get_value("DocType", doctype, "module")
- # frappe.reload_doc(frappe.scrub(module), "doctype", frappe.scrub(doctype))
- #
- # frappe.db.sql("""update `tabBlog Category` set `title`=`name` where ifnull(`title`, '')=''""")
- # frappe.db.sql("""update `tabWebsite Route` set idx=null""")
- # for doctype in ["Blog Category", "Blog Post", "Web Page", "Website Group"]:
- # frappe.db.sql("""update `tab{}` set idx=null""".format(doctype))
- #
- # from frappe.website.doctype.website_template.website_template import rebuild_website_template
- # rebuild_website_template()
diff --git a/frappe/patches/v4_0/update_custom_field_insert_after.py b/frappe/patches/v4_0/update_custom_field_insert_after.py
deleted file mode 100644
index ddb888c493..0000000000
--- a/frappe/patches/v4_0/update_custom_field_insert_after.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- for d in frappe.db.sql("""select name, dt, insert_after from `tabCustom Field`
- where docstatus < 2""", as_dict=1):
- dt_meta = frappe.get_meta(d.dt)
- if not dt_meta.get_field(d.insert_after):
- cf = frappe.get_doc("Custom Field", d.name)
- df = dt_meta.get("fields", {"label": d.insert_after})
- if df:
- cf.insert_after = df[0].fieldname
- else:
- cf.insert_after = None
- cf.save()
diff --git a/frappe/patches/v4_0/update_datetime.py b/frappe/patches/v4_0/update_datetime.py
deleted file mode 100644
index 0e91174780..0000000000
--- a/frappe/patches/v4_0/update_datetime.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- for table in frappe.db.sql_list("show tables"):
- for field in frappe.db.sql("desc `%s`" % table):
- if field[1]=="datetime":
- frappe.db.sql("alter table `%s` change `%s` `%s` datetime(6)" % \
- (table, field[0], field[0]))
- elif field[1]=="time":
- frappe.db.sql("alter table `%s` change `%s` `%s` time(6)" % \
- (table, field[0], field[0]))
diff --git a/frappe/patches/v4_0/webnotes_to_frappe.py b/frappe/patches/v4_0/webnotes_to_frappe.py
deleted file mode 100644
index 22b3848d5a..0000000000
--- a/frappe/patches/v4_0/webnotes_to_frappe.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from __future__ import unicode_literals
-import frappe, json
-
-def execute():
- frappe.clear_cache()
- installed = frappe.get_installed_apps()
- if "webnotes" in installed:
- installed.remove("webnotes")
- if "frappe" not in installed:
- installed = ["frappe"] + installed
- frappe.db.set_global("installed_apps", json.dumps(installed))
- frappe.clear_cache()
diff --git a/frappe/patches/v4_0/website_sitemap_hierarchy.py b/frappe/patches/v4_0/website_sitemap_hierarchy.py
deleted file mode 100644
index bb22144cd7..0000000000
--- a/frappe/patches/v4_0/website_sitemap_hierarchy.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-from __future__ import unicode_literals
-
-import frappe
-
-def execute():
- # frappe.db.sql("""update `tabWebsite Route` ws set ref_doctype=(select wsc.ref_doctype
- # from `tabWebsite Template` wsc where wsc.name=ws.website_template)
- # where ifnull(page_or_generator, '')!='Page'""")
-
- frappe.reload_doc("website", "doctype", "website_settings")
-
- # original_home_page = frappe.db.get_value("Website Settings", "Website Settings", "home_page")
- #
- # home_page = frappe.db.sql("""select name from `tabWebsite Route`
- # where (name=%s or docname=%s) and name!='index'""", (original_home_page, original_home_page))
- # home_page = home_page[0][0] if home_page else original_home_page
- #
- # frappe.db.set_value("Website Settings", "Website Settings", "home_page", home_page)
diff --git a/frappe/patches/v4_1/enable_outgoing_email_settings.py b/frappe/patches/v4_1/enable_outgoing_email_settings.py
deleted file mode 100644
index 7ffa84a278..0000000000
--- a/frappe/patches/v4_1/enable_outgoing_email_settings.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doc("core", "doctype", "outgoing_email_settings")
- if (frappe.db.get_value("Outgoing Email Settings", "Outgoing Email Settings", "mail_server") or "").strip():
- frappe.db.set_value("Outgoing Email Settings", "Outgoing Email Settings", "enabled", 1)
diff --git a/frappe/patches/v4_1/enable_print_as_pdf.py b/frappe/patches/v4_1/enable_print_as_pdf.py
deleted file mode 100644
index 74db9f72ca..0000000000
--- a/frappe/patches/v4_1/enable_print_as_pdf.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doc("core", "doctype", "print_settings")
- print_settings = frappe.get_doc("Print Settings")
- print_settings.print_style = "Modern"
-
- try:
- import pdfkit
- except ImportError:
- pass
- else:
- # if someone has already configured in Outgoing Email Settings
- outgoing_email_settings = frappe.db.get_singles_dict("Outgoing Email Settings")
- if "send_print_as_pdf" in outgoing_email_settings:
- print_settings.send_print_as_pdf = outgoing_email_settings.send_print_as_pdf
- print_settings.pdf_page_size = outgoing_email_settings.pdf_page_size
-
- else:
- print_settings.send_print_as_pdf = 1
-
- print_settings.save()
diff --git a/frappe/patches/v4_1/file_manager_fix.py b/frappe/patches/v4_1/file_manager_fix.py
deleted file mode 100644
index cd30c94177..0000000000
--- a/frappe/patches/v4_1/file_manager_fix.py
+++ /dev/null
@@ -1,101 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-from __future__ import unicode_literals, print_function
-
-import frappe
-import os
-from frappe.core.doctype.file.file import get_content_hash, get_file_name
-from frappe.utils import get_files_path, get_site_path
-
-# The files missed by the previous patch might have been replaced with new files
-# with the same filename
-#
-# This patch does the following,
-# * Detect which files were replaced and rename them with name{hash:5}.extn and
-# update filedata record for the new file
-#
-# * make missing_files.txt in site dir with files that should be recovered from
-# a backup from a time before version 3 migration
-#
-# * Patch remaining unpatched File records.
-from six import iteritems
-
-
-def execute():
- frappe.db.auto_commit_on_many_writes = True
- rename_replacing_files()
- for name, file_name, file_url in frappe.db.sql(
- """select name, file_name, file_url from `tabFile`
- where ifnull(file_name, '')!='' and ifnull(content_hash, '')=''"""):
- b = frappe.get_doc('File', name)
- old_file_name = b.file_name
- b.file_name = os.path.basename(old_file_name)
- if old_file_name.startswith('files/') or old_file_name.startswith('/files/'):
- b.file_url = os.path.normpath('/' + old_file_name)
- else:
- b.file_url = os.path.normpath('/files/' + old_file_name)
- try:
- _file = frappe.get_doc("File", {"file_name": name})
- content = _file.get_content()
- b.content_hash = get_content_hash(content)
- except IOError:
- print('Warning: Error processing ', name)
- b.content_hash = None
- b.flags.ignore_duplicate_entry_error = True
- b.save()
- frappe.db.auto_commit_on_many_writes = False
-
-def get_replaced_files():
- ret = []
- new_files = dict(frappe.db.sql("select name, file_name from `tabFile` where file_name not like 'files/%'"))
- old_files = dict(frappe.db.sql("select name, file_name from `tabFile` where ifnull(content_hash, '')=''"))
- invfiles = invert_dict(new_files)
-
- for nname, nfilename in iteritems(new_files):
- if 'files/' + nfilename in old_files.values():
- ret.append((nfilename, invfiles[nfilename]))
- return ret
-
-def rename_replacing_files():
- replaced_files = get_replaced_files()
- if len(replaced_files):
- missing_files = [v[0] for v in replaced_files]
- with open(get_site_path('missing_files.txt'), 'w') as f:
- f.write(('\n'.join(missing_files) + '\n').encode('utf-8'))
-
- for file_name, file_datas in replaced_files:
- print ('processing ' + file_name)
- content_hash = frappe.db.get_value('File', file_datas[0], 'content_hash')
- if not content_hash:
- continue
- new_file_name = get_file_name(file_name, content_hash)
- if os.path.exists(get_files_path(new_file_name)):
- continue
- print('skipping ' + file_name)
- try:
- os.rename(get_files_path(file_name), get_files_path(new_file_name))
- except OSError:
- print('Error renaming ', file_name)
- for name in file_datas:
- f = frappe.get_doc('File', name)
- f.file_name = new_file_name
- f.file_url = '/files/' + new_file_name
- f.save()
-
-def invert_dict(ddict):
- ret = {}
- for k,v in iteritems(ddict):
- if not ret.get(v):
- ret[v] = [k]
- else:
- ret[v].append(k)
- return ret
-
-def get_file_name(fname, hash):
- if '.' in fname:
- partial, extn = fname.rsplit('.', 1)
- else:
- partial = fname
- extn = ''
- return '{partial}{suffix}.{extn}'.format(partial=partial, extn=extn, suffix=hash[:5])
diff --git a/frappe/patches/v4_2/print_with_letterhead.py b/frappe/patches/v4_2/print_with_letterhead.py
deleted file mode 100644
index 3e611ce073..0000000000
--- a/frappe/patches/v4_2/print_with_letterhead.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doc("core", "doctype", "print_settings")
- print_settings = frappe.get_doc("Print Settings")
- print_settings.with_letterhead = 1
- print_settings.save()
diff --git a/frappe/patches/v4_2/refactor_website_routing.py b/frappe/patches/v4_2/refactor_website_routing.py
deleted file mode 100644
index a5856db1c9..0000000000
--- a/frappe/patches/v4_2/refactor_website_routing.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- # clear all static web pages
- frappe.delete_doc("DocType", "Website Route", force=1)
- frappe.delete_doc("Page", "sitemap-browser", force=1)
- frappe.db.sql("drop table if exists `tabWebsite Route`")
diff --git a/frappe/patches/v4_2/set_assign_in_doc.py b/frappe/patches/v4_2/set_assign_in_doc.py
deleted file mode 100644
index a6a06492a0..0000000000
--- a/frappe/patches/v4_2/set_assign_in_doc.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- for name in frappe.db.sql_list("""select name from `tabToDo`
- where ifnull(reference_type, '')!='' and ifnull(reference_name, '')!=''"""):
- try:
- frappe.get_doc("ToDo", name).on_update()
- except Exception as e:
- if not frappe.db.is_table_missing(e):
- raise
diff --git a/frappe/patches/v4_3/remove_allow_on_submit_customization.py b/frappe/patches/v4_3/remove_allow_on_submit_customization.py
deleted file mode 100644
index af6ade68e6..0000000000
--- a/frappe/patches/v4_3/remove_allow_on_submit_customization.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- for d in frappe.get_all("Property Setter", fields=["name", "doc_type"],
- filters={"doctype_or_field": "DocField", "property": "allow_on_submit", "value": "1"}):
- frappe.delete_doc("Property Setter", d.name)
- frappe.clear_cache(doctype=d.doc_type)
diff --git a/frappe/patches/v5_0/bookmarks_to_stars.py b/frappe/patches/v5_0/bookmarks_to_stars.py
deleted file mode 100644
index 048d059701..0000000000
--- a/frappe/patches/v5_0/bookmarks_to_stars.py
+++ /dev/null
@@ -1,33 +0,0 @@
-from __future__ import unicode_literals
-import json
-import frappe
-import frappe.defaults
-from frappe.desk.like import _toggle_like
-from six import string_types
-
-def execute():
- for user in frappe.get_all("User"):
- username = user["name"]
- bookmarks = frappe.db.get_default("_bookmarks", username)
-
- if not bookmarks:
- continue
-
- if isinstance(bookmarks, string_types):
- bookmarks = json.loads(bookmarks)
-
- for opts in bookmarks:
- route = (opts.get("route") or "").strip("#/ ")
-
- if route and route.startswith("Form"):
- try:
- view, doctype, docname = opts["route"].split("/")
- except ValueError:
- continue
-
- if frappe.db.exists(doctype, docname):
- if (doctype=="DocType"
- or int(frappe.db.get_value("DocType", doctype, "issingle") or 0)
- or not frappe.db.table_exists(doctype)):
- continue
- _toggle_like(doctype, docname, add="Yes", user=username)
diff --git a/frappe/patches/v5_0/clear_website_group_and_notifications.py b/frappe/patches/v5_0/clear_website_group_and_notifications.py
deleted file mode 100644
index bad50222a3..0000000000
--- a/frappe/patches/v5_0/clear_website_group_and_notifications.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.delete_doc("DocType", "Post")
- frappe.delete_doc("DocType", "Website Group")
- frappe.delete_doc("DocType", "Website Route Permission")
- frappe.delete_doc("DocType", "User Vote")
- frappe.delete_doc("DocType", "Notification Count")
diff --git a/frappe/patches/v5_0/communication_parent.py b/frappe/patches/v5_0/communication_parent.py
deleted file mode 100644
index 2ea3b401c6..0000000000
--- a/frappe/patches/v5_0/communication_parent.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doc("core", "doctype", "communication")
- frappe.db.sql("""update tabCommunication set reference_doctype = parenttype, reference_name = parent""")
diff --git a/frappe/patches/v5_0/convert_to_barracuda_and_utf8mb4.py b/frappe/patches/v5_0/convert_to_barracuda_and_utf8mb4.py
deleted file mode 100644
index 0ea2ee2387..0000000000
--- a/frappe/patches/v5_0/convert_to_barracuda_and_utf8mb4.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-from frappe.database.mariadb.setup_db import check_database_settings
-from frappe.model.meta import trim_tables
-
-def execute():
- check_database_settings()
-
- for table in frappe.db.get_tables():
- frappe.db.sql_ddl("""alter table `{0}` ENGINE=InnoDB ROW_FORMAT=COMPRESSED""".format(table))
- try:
- frappe.db.sql_ddl("""alter table `{0}` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci""".format(table))
- except:
- # if row size gets too large, let it be old charset!
- pass
-
diff --git a/frappe/patches/v5_0/expire_old_scheduler_logs.py b/frappe/patches/v5_0/expire_old_scheduler_logs.py
deleted file mode 100644
index 8b65ed5fb1..0000000000
--- a/frappe/patches/v5_0/expire_old_scheduler_logs.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doctype("Error Log")
-
- from frappe.core.doctype.error_log.error_log import set_old_logs_as_seen
- set_old_logs_as_seen()
diff --git a/frappe/patches/v5_0/fix_email_alert.py b/frappe/patches/v5_0/fix_email_alert.py
deleted file mode 100644
index 0676f50a9c..0000000000
--- a/frappe/patches/v5_0/fix_email_alert.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from __future__ import unicode_literals
-
-import frappe
-
-def execute():
- frappe.reload_doctype("Notification")
- for e in frappe.get_all("Notification"):
- notification = frappe.get_doc("Notification", e.name)
- if notification.event == "Date Change":
- if notification.days_in_advance < 0:
- notification.event = "Days After"
- notification.days_in_advance = -email_alert.days_in_advance
- else:
- notification.event = "Days Before"
-
- notification.save()
diff --git a/frappe/patches/v5_0/fix_null_date_datetime.py b/frappe/patches/v5_0/fix_null_date_datetime.py
deleted file mode 100644
index e4f4e9e8b9..0000000000
--- a/frappe/patches/v5_0/fix_null_date_datetime.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- for table in frappe.db.get_tables():
- changed = False
- desc = frappe.db.sql("desc `{table}`".format(table=table), as_dict=True)
- for field in desc:
- if field["Type"] == "date":
- frappe.db.sql("""update `{table}` set `{fieldname}`=null where `{fieldname}`='0000-00-00'""".format(
- table=table, fieldname=field["Field"]))
- changed = True
-
- elif field["Type"] == "datetime(6)":
- frappe.db.sql("""update `{table}` set `{fieldname}`=null where `{fieldname}`='0000-00-00 00:00:00.000000'""".format(
- table=table, fieldname=field["Field"]))
- changed = True
-
- if changed:
- frappe.db.commit()
diff --git a/frappe/patches/v5_0/fix_text_editor_file_urls.py b/frappe/patches/v5_0/fix_text_editor_file_urls.py
deleted file mode 100644
index a6d7d2fb9a..0000000000
--- a/frappe/patches/v5_0/fix_text_editor_file_urls.py
+++ /dev/null
@@ -1,44 +0,0 @@
-from __future__ import unicode_literals, print_function
-import frappe
-import re
-
-def execute():
- """Fix relative urls for image src="files/" to src="/files/" in DocTypes with text editor fields"""
- doctypes_with_text_fields = frappe.get_all("DocField", fields=["parent", "fieldname"],
- filters={"fieldtype": "Text Editor"})
-
- done = []
- for opts in doctypes_with_text_fields:
- if opts in done:
- continue
-
- try:
- result = frappe.get_all(opts.parent, fields=["name", opts.fieldname])
- except frappe.db.SQLError:
- # bypass single tables
- continue
-
- for data in result:
- old_value = data[opts.fieldname]
- if not old_value:
- continue
-
- html = scrub_relative_urls(old_value)
- if html != old_value:
- # print_diff(html, old_value)
- frappe.db.set_value(opts.parent, data.name, opts.fieldname, html, update_modified=False)
-
- done.append(opts)
-
-def scrub_relative_urls(html):
- """prepend a slash before a relative url"""
- try:
- return re.sub(r'src[\s]*=[\s]*[\'"]files/([^\'"]*)[\'"]', r'src="/files/\g<1>"', html)
- except:
- print("Error", html)
- raise
-
-def print_diff(html, old_value):
- import difflib
- diff = difflib.unified_diff(old_value.splitlines(1), html.splitlines(1), lineterm='')
- print('\n'.join(list(diff)))
diff --git a/frappe/patches/v5_0/force_sync_website.py b/frappe/patches/v5_0/force_sync_website.py
deleted file mode 100644
index 5dcd0d79c7..0000000000
--- a/frappe/patches/v5_0/force_sync_website.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- pass
diff --git a/frappe/patches/v5_0/modify_session.py b/frappe/patches/v5_0/modify_session.py
deleted file mode 100644
index f0e247a633..0000000000
--- a/frappe/patches/v5_0/modify_session.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- if "device" not in frappe.db.get_table_columns("Sessions"):
- frappe.db.sql("alter table tabSessions add column `device` varchar(255) default 'desktop'")
diff --git a/frappe/patches/v5_0/move_scheduler_last_event_to_system_settings.py b/frappe/patches/v5_0/move_scheduler_last_event_to_system_settings.py
deleted file mode 100644
index 0fa1dad1e5..0000000000
--- a/frappe/patches/v5_0/move_scheduler_last_event_to_system_settings.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doctype('System Settings')
- last = frappe.db.get_global('scheduler_last_event')
- frappe.db.set_value('System Settings', 'System Settings', 'scheduler_last_event', last)
-
diff --git a/frappe/patches/v5_0/remove_shopping_cart_app.py b/frappe/patches/v5_0/remove_shopping_cart_app.py
deleted file mode 100644
index babde585a1..0000000000
--- a/frappe/patches/v5_0/remove_shopping_cart_app.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
-
-def execute():
- from frappe.installer import remove_from_installed_apps
- remove_from_installed_apps("shopping_cart")
diff --git a/frappe/patches/v5_0/rename_ref_type_fieldnames.py b/frappe/patches/v5_0/rename_ref_type_fieldnames.py
deleted file mode 100644
index dd24f6e5b5..0000000000
--- a/frappe/patches/v5_0/rename_ref_type_fieldnames.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- try:
- frappe.db.sql("alter table `tabEmail Queue` change `ref_docname` `reference_name` varchar(255)")
- except Exception as e:
- if not frappe.db.is_table_or_column_missing(e):
- raise
-
- try:
- frappe.db.sql("alter table `tabEmail Queue` change `ref_doctype` `reference_doctype` varchar(255)")
- except Exception as e:
- if not frappe.db.is_table_or_column_missing(e):
- raise
- frappe.reload_doctype("Email Queue")
diff --git a/frappe/patches/v5_0/rename_table_fieldnames.py b/frappe/patches/v5_0/rename_table_fieldnames.py
deleted file mode 100644
index b716599f28..0000000000
--- a/frappe/patches/v5_0/rename_table_fieldnames.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe.model.utils.rename_field import rename_field
-from frappe.modules import scrub, get_doctype_module
-
-rename_map = {
- "Customize Form": [
- ["customize_form_fields", "fields"]
- ],
- "Email Alert": [
- ["email_alert_recipients", "recipients"]
- ],
- "Workflow": [
- ["workflow_document_states", "states"],
- ["workflow_transitions", "transitions"]
- ]
-}
-
-def execute():
- frappe.reload_doc("custom", "doctype", "customize_form")
- frappe.reload_doc("email", "doctype", "notification")
- frappe.reload_doc("desk", "doctype", "event")
- frappe.reload_doc("workflow", "doctype", "workflow")
-
- for dt, field_list in rename_map.items():
- for field in field_list:
- rename_field(dt, field[0], field[1])
diff --git a/frappe/patches/v5_0/style_settings_to_website_theme.py b/frappe/patches/v5_0/style_settings_to_website_theme.py
deleted file mode 100644
index 40414d4e20..0000000000
--- a/frappe/patches/v5_0/style_settings_to_website_theme.py
+++ /dev/null
@@ -1,59 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-from frappe import _
-from frappe.utils import cint
-
-def execute():
- frappe.reload_doc("website", "doctype", "website_theme")
- frappe.reload_doc("website", "website_theme", "standard")
- frappe.reload_doctype("Website Settings")
- migrate_style_settings()
- frappe.delete_doc("website", "doctype", "style_settings")
-
-def migrate_style_settings():
- style_settings = frappe.db.get_singles_dict("Style Settings")
- standard_website_theme = frappe.get_doc("Website Theme", "Standard")
-
- website_theme = frappe.copy_doc(standard_website_theme)
- website_theme.custom = 1
- website_theme.theme = _("Custom")
-
- if style_settings:
- map_color_fields(style_settings, website_theme)
- map_other_fields(style_settings, website_theme)
-
- website_theme.no_sidebar = cint(frappe.db.get_single_value("Website Settings", "no_sidebar"))
-
- website_theme.save()
- website_theme.set_as_default()
-
-def map_color_fields(style_settings, website_theme):
- color_fields_map = {
- "page_text": "text_color",
- "page_links": "link_color",
- "top_bar_background": "top_bar_color",
- "top_bar_foreground": "top_bar_text_color",
- "footer_background": "footer_color",
- "footer_color": "footer_text_color",
- }
-
- for from_fieldname, to_fieldname in color_fields_map.items():
- from_value = style_settings.get(from_fieldname)
-
- if from_value:
- website_theme.set(to_fieldname, "#{0}".format(from_value))
-
-def map_other_fields(style_settings, website_theme):
- other_fields_map = {
- "heading_text_as": "heading_style",
- "google_web_font_for_heading": "heading_webfont",
- "google_web_font_for_text": "text_webfont",
- "add_css": "css"
- }
-
- for from_fieldname, to_fieldname in other_fields_map.items():
- website_theme.set(to_fieldname, style_settings.get(from_fieldname))
-
- for fieldname in ("apply_style", "background_image", "background_color",
- "font_size"):
- website_theme.set(fieldname, style_settings.get(fieldname))
diff --git a/frappe/patches/v5_0/update_shared.py b/frappe/patches/v5_0/update_shared.py
deleted file mode 100644
index f2b77895d8..0000000000
--- a/frappe/patches/v5_0/update_shared.py
+++ /dev/null
@@ -1,37 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-import frappe.share
-
-def execute():
- frappe.reload_doc("core", "doctype", "docperm")
- frappe.reload_doc("core", "doctype", "docshare")
- frappe.reload_doc('email', 'doctype', 'email_account')
-
- # default share to all writes
- frappe.db.sql("""update tabDocPerm set `share`=1 where ifnull(`write`,0)=1 and ifnull(`permlevel`,0)=0""")
-
- # every user must have access to his / her own detail
- users = frappe.get_all("User", filters={"user_type": "System User"})
- usernames = [user.name for user in users]
- for user in usernames:
- frappe.share.add("User", user, user, write=1, share=1)
-
- # move event user to shared
- if frappe.db.exists("DocType", "Event User"):
- for event in frappe.get_all("Event User", fields=["parent", "person"]):
- if event.person in usernames:
- if not frappe.db.exists("Event", event.parent):
- frappe.db.sql("delete from `tabEvent User` where parent = %s",event.parent)
- else:
- frappe.share.add("Event", event.parent, event.person, write=1)
-
- frappe.delete_doc("DocType", "Event User")
-
- # move note user to shared
- if frappe.db.exists("DocType", "Note User"):
- for note in frappe.get_all("Note User", fields=["parent", "user", "permission"]):
- perm = {"read": 1} if note.permission=="Read" else {"write": 1}
- if note.user in usernames:
- frappe.share.add("Note", note.parent, note.user, **perm)
-
- frappe.delete_doc("DocType", "Note User")
diff --git a/frappe/patches/v5_0/v4_to_v5.py b/frappe/patches/v5_0/v4_to_v5.py
deleted file mode 100644
index cd34f04c97..0000000000
--- a/frappe/patches/v5_0/v4_to_v5.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- changed = (
- ("desk", ("feed", "event", "todo", "note")),
- ("custom", ("custom_field", "custom_script", "customize_form",
- "customize_form_field", "property_setter")),
- ("email", ("email_queue", "notification", "notification_recipient", "standard_reply")),
- ("geo", ("country", "currency")),
- ("print", ("letter_head", "print_format", "print_settings"))
- )
- for module in changed:
- for doctype in module[1]:
- frappe.reload_doc(module[0], "doctype", doctype)
diff --git a/frappe/patches/v5_2/change_checks_to_not_null.py b/frappe/patches/v5_2/change_checks_to_not_null.py
deleted file mode 100644
index 23f5d659b5..0000000000
--- a/frappe/patches/v5_2/change_checks_to_not_null.py
+++ /dev/null
@@ -1,34 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-from frappe.utils import cint
-from frappe.model import default_fields
-
-def execute():
- for table in frappe.db.get_tables():
- doctype = table[3:]
- if frappe.db.exists("DocType", doctype):
- fieldnames = [df["fieldname"] for df in
- frappe.get_all("DocField", fields=["fieldname"], filters={"parent": doctype})]
- custom_fieldnames = [df["fieldname"] for df in
- frappe.get_all("Custom Field", fields=["fieldname"], filters={"dt": doctype})]
-
- else:
- fieldnames = custom_fieldnames = []
-
- for column in frappe.db.sql("""desc `{0}`""".format(table), as_dict=True):
- if column["Type"]=="int(1)":
- fieldname = column["Field"]
-
- # only change for defined fields, ignore old fields that don't exist in meta
- if not (fieldname in default_fields or fieldname in fieldnames or fieldname in custom_fieldnames):
- continue
-
- # set 0
- frappe.db.sql("""update `{table}` set `{column}`=0 where `{column}` is null"""\
- .format(table=table, column=fieldname))
- frappe.db.commit()
-
- # change definition
- frappe.db.sql_ddl("""alter table `{table}`
- modify `{column}` int(1) not null default {default}"""\
- .format(table=table, column=fieldname, default=cint(column["Default"])))
diff --git a/frappe/patches/v5_3/rename_chinese_languages.py b/frappe/patches/v5_3/rename_chinese_languages.py
deleted file mode 100644
index 8bc954c04c..0000000000
--- a/frappe/patches/v5_3/rename_chinese_languages.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-import frappe
-from frappe.translate import rename_language
-
-def execute():
- language_map = {
- "中国(简体)": "簡體中文",
- "中國(繁體)": "正體中文"
- }
-
- for old_name, new_name in language_map.items():
- rename_language(old_name, new_name)
diff --git a/frappe/patches/v6_0/communication_status_and_permission.py b/frappe/patches/v6_0/communication_status_and_permission.py
deleted file mode 100644
index c68ed9b4d6..0000000000
--- a/frappe/patches/v6_0/communication_status_and_permission.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-from frappe.permissions import reset_perms
-
-def execute():
- frappe.reload_doctype("Communication")
-
- # set status = "Linked"
- frappe.db.sql("""update `tabCommunication` set status='Linked'
- where ifnull(reference_doctype, '')!='' and ifnull(reference_name, '')!=''""")
-
- frappe.db.sql("""update `tabCommunication` set status='Closed'
- where status='Archived'""")
-
- # reset permissions if owner of all DocPerms is Administrator
- if not frappe.db.sql("""select name from `tabDocPerm`
- where parent='Communication' and ifnull(owner, '')!='Administrator'"""):
-
- reset_perms("Communication")
diff --git a/frappe/patches/v6_0/document_type_rename.py b/frappe/patches/v6_0/document_type_rename.py
deleted file mode 100644
index 16c7d34286..0000000000
--- a/frappe/patches/v6_0/document_type_rename.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.db.sql("""update tabDocType set document_type='Document'
- where document_type='Transaction'""")
- frappe.db.sql("""update tabDocType set document_type='Setup'
- where document_type='Master'""")
diff --git a/frappe/patches/v6_0/fix_ghana_currency.py b/frappe/patches/v6_0/fix_ghana_currency.py
deleted file mode 100644
index 67f740d240..0000000000
--- a/frappe/patches/v6_0/fix_ghana_currency.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from __future__ import unicode_literals
-
-def execute():
- from frappe.geo.country_info import get_all
- import frappe.utils.install
-
- countries = get_all()
- frappe.utils.install.add_country_and_currency("Ghana", frappe._dict(countries["Ghana"]))
diff --git a/frappe/patches/v6_0/make_task_log_folder.py b/frappe/patches/v6_0/make_task_log_folder.py
deleted file mode 100644
index 87d6e4126f..0000000000
--- a/frappe/patches/v6_0/make_task_log_folder.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from __future__ import unicode_literals
-import frappe.utils, os
-
-def execute():
- path = frappe.utils.get_site_path('task-logs')
- if not os.path.exists(path):
- os.makedirs(path)
diff --git a/frappe/patches/v6_1/__init__.py b/frappe/patches/v6_1/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/patches/v6_1/rename_file_data.py b/frappe/patches/v6_1/rename_file_data.py
deleted file mode 100644
index 83152271eb..0000000000
--- a/frappe/patches/v6_1/rename_file_data.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from __future__ import print_function, unicode_literals
-import frappe
-
-def execute():
- from frappe.core.doctype.file.file import make_home_folder
-
- if not frappe.db.exists("DocType", "File"):
- frappe.rename_doc("DocType", "File Data", "File")
- frappe.reload_doctype("File")
-
- if not frappe.db.exists("File", {"is_home_folder": 1}):
- make_home_folder()
-
- # make missing folders and set parent folder
- for file in frappe.get_all("File", filters={"is_folder": 0}):
- file = frappe.get_doc("File", file.name)
- file.flags.ignore_folder_validate = True
- file.flags.ignore_file_validate = True
- file.flags.ignore_duplicate_entry_error = True
- file.flags.ignore_links = True
- file.set_folder_name()
- try:
- file.save()
- except:
- print(frappe.get_traceback())
- raise
-
- from frappe.utils.nestedset import rebuild_tree
- rebuild_tree("File", "folder")
-
- # reset file size
- for folder in frappe.db.sql("""select name from tabFile f1 where is_folder = 1 and
- (select count(*) from tabFile f2 where f2.folder = f1.name and f2.is_folder = 1) = 0"""):
- folder = frappe.get_doc("File", folder[0])
- folder.save()
-
-
-
diff --git a/frappe/patches/v6_11/__init__.py b/frappe/patches/v6_11/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/patches/v6_11/rename_field_in_email_account.py b/frappe/patches/v6_11/rename_field_in_email_account.py
deleted file mode 100644
index 319b569802..0000000000
--- a/frappe/patches/v6_11/rename_field_in_email_account.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doc("email", "doctype", "email_account")
- if frappe.db.has_column('Email Account', 'pop3_server'):
- frappe.db.sql("update `tabEmail Account` set email_server = pop3_server")
diff --git a/frappe/patches/v6_15/__init__.py b/frappe/patches/v6_15/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/patches/v6_15/remove_property_setter_for_previous_field.py b/frappe/patches/v6_15/remove_property_setter_for_previous_field.py
deleted file mode 100644
index b24bf38442..0000000000
--- a/frappe/patches/v6_15/remove_property_setter_for_previous_field.py
+++ /dev/null
@@ -1,87 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-from __future__ import unicode_literals
-import frappe, json
-from frappe.utils import cstr
-
-def execute():
- # deprecated on 2016-03-09
- # using insert_after instead
- return
-
- frappe.db.sql("""delete from `tabProperty Setter` where property='previous_field'""")
-
- all_custom_fields = frappe._dict()
- for d in frappe.db.sql("""select name, dt, fieldname, insert_after from `tabCustom Field`
- where insert_after is not null and insert_after != ''""", as_dict=1):
- all_custom_fields.setdefault(d.dt, frappe._dict()).setdefault(d.fieldname, d.insert_after)
-
- for dt, custom_fields in all_custom_fields.items():
- _idx = []
- existing_ps = frappe.db.get_value("Property Setter",
- {"doc_type": dt, "property": "_idx"}, ["name", "value", "creation"], as_dict=1)
-
- # if no existsing property setter, build based on meta
- if not existing_ps:
- _idx = get_sorted_fields(dt, custom_fields)
- else:
- _idx = json.loads(existing_ps.value)
-
- idx_needs_to_be_fixed = False
- for fieldname, insert_after in custom_fields.items():
- # Delete existing property setter if field is not there
- if fieldname not in _idx:
- idx_needs_to_be_fixed = True
- break
- else:
- previous_field = _idx[_idx.index(fieldname) - 1]
-
- if previous_field != insert_after and cstr(existing_ps.creation) >= "2015-12-28":
- idx_needs_to_be_fixed = True
- break
-
- if idx_needs_to_be_fixed:
- frappe.delete_doc("Property Setter", existing_ps.name)
- _idx = get_sorted_fields(dt, custom_fields)
-
- if _idx:
- frappe.make_property_setter({
- "doctype":dt,
- "doctype_or_field": "DocType",
- "property": "_idx",
- "value": json.dumps(_idx),
- "property_type": "Text"
- }, validate_fields_for_doctype=False)
-
-
-def get_sorted_fields(doctype, custom_fields):
- """sort on basis of insert_after"""
- fields_dict = frappe.get_meta(doctype).get("fields")
-
- standard_fields_count = frappe.db.sql("""select count(name) from `tabDocField`
- where parent=%s""", doctype)[0][0]
-
- newlist = []
- pending = [d.fieldname for d in fields_dict]
-
- maxloops = len(custom_fields) + 20
- while (pending and maxloops>0):
- maxloops -= 1
- for fieldname in pending[:]:
- if fieldname in custom_fields and len(newlist) >= standard_fields_count:
- # field already added
- for n in newlist:
- if n==custom_fields.get(fieldname):
- newlist.insert(newlist.index(n)+1, fieldname)
- pending.remove(fieldname)
- break
- else:
- newlist.append(fieldname)
- pending.remove(fieldname)
-
- # recurring at end
- if pending:
- newlist += pending
-
- return newlist
diff --git a/frappe/patches/v6_15/set_username.py b/frappe/patches/v6_15/set_username.py
deleted file mode 100644
index 513ff3301d..0000000000
--- a/frappe/patches/v6_15/set_username.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doctype("User")
-
- # give preference to System Users
- users = frappe.db.sql_list("""select name from `tabUser` order by if(user_type='System User', 0, 1)""")
- for name in users:
- user = frappe.get_doc("User", name)
- if user.username or not user.first_name:
- continue
-
- username = user.suggest_username()
- if username:
- user.db_set("username", username, update_modified=False)
diff --git a/frappe/patches/v6_16/__init__.py b/frappe/patches/v6_16/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/patches/v6_16/feed_doc_owner.py b/frappe/patches/v6_16/feed_doc_owner.py
deleted file mode 100644
index 2dac9a143d..0000000000
--- a/frappe/patches/v6_16/feed_doc_owner.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doctype("Communication")
-
- for doctype, name in frappe.db.sql("""select distinct reference_doctype, reference_name
- from `tabCommunication`
- where
- (reference_doctype is not null and reference_doctype != '')
- and (reference_name is not null and reference_name != '')
- and (reference_owner is null or reference_owner = '')
- for update"""):
-
- owner = frappe.db.get_value(doctype, name, "owner")
-
- if not owner:
- continue
-
- frappe.db.sql("""update `tabCommunication`
- set reference_owner=%(owner)s
- where
- reference_doctype=%(doctype)s
- and reference_name=%(name)s
- and (reference_owner is null or reference_owner = '')""".format(doctype=doctype), {
- "doctype": doctype,
- "name": name,
- "owner": owner
- })
-
- frappe.db.commit()
diff --git a/frappe/patches/v6_16/star_to_like.py b/frappe/patches/v6_16/star_to_like.py
deleted file mode 100644
index e859223d54..0000000000
--- a/frappe/patches/v6_16/star_to_like.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-from frappe.database.schema import add_column
-
-def execute():
- frappe.db.sql("""update `tabSingles` set field='_liked_by' where field='_starred_by'""")
- frappe.db.commit()
-
- for table in frappe.db.get_tables():
- columns = [r[0] for r in frappe.db.sql("DESC `{0}`".format(table))]
- if "_starred_by" in columns and '_liked_by' not in columns:
- frappe.db.sql_ddl("""alter table `{0}` change `_starred_by` `_liked_by` Text """.format(table))
-
- if not frappe.db.has_column("Communication", "_liked_by"):
- add_column("Communication", "_liked_by", "Text")
diff --git a/frappe/patches/v6_19/__init__.py b/frappe/patches/v6_19/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/patches/v6_19/comment_feed_communication.py b/frappe/patches/v6_19/comment_feed_communication.py
deleted file mode 100644
index a7503c08ab..0000000000
--- a/frappe/patches/v6_19/comment_feed_communication.py
+++ /dev/null
@@ -1,307 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-from frappe import _
-from frappe.model.rename_doc import get_link_fields
-from frappe.model.dynamic_links import dynamic_link_queries
-from frappe.permissions import reset_perms
-
-def execute():
- # comments stay comments in v12
- return
-
- frappe.reload_doctype("DocType")
- frappe.reload_doctype("Communication")
- reset_perms("Communication")
-
- migrate_comments()
- frappe.delete_doc("DocType", "Comment")
- # frappe.db.sql_ddl("drop table `tabComment`")
-
- migrate_feed()
- frappe.delete_doc("DocType", "Feed")
- # frappe.db.sql_ddl("drop table `tabFeed`")
-
- update_timeline_doc_for("Blogger")
-
-def migrate_comments():
- from_fields = ""
- to_fields = ""
-
- if "reference_doctype" in frappe.db.get_table_columns("Comment"):
- from_fields = "reference_doctype as link_doctype, reference_name as link_name,"
- to_fields = "link_doctype, link_name,"
-
- # comments
- frappe.db.sql("""insert ignore into `tabCommunication` (
- subject,
- content,
- sender,
- sender_full_name,
- comment_type,
- communication_date,
- reference_doctype,
- reference_name,
- {to_fields}
-
- name,
- user,
- owner,
- creation,
- modified_by,
- modified,
- status,
- sent_or_received,
- communication_type,
- seen
- )
- select
- substring(comment, 1, 100) as subject,
- comment as content,
- comment_by as sender,
- comment_by_fullname as sender_full_name,
- comment_type,
- ifnull(timestamp(comment_date, comment_time), creation) as communication_date,
- comment_doctype as reference_doctype,
- comment_docname as reference_name,
- {from_fields}
-
- name,
- owner as user,
- owner,
- creation,
- modified_by,
- modified,
- 'Linked' as status,
- 'Sent' as sent_or_received,
- 'Comment' as communication_type,
- 1 as seen
- from `tabComment` where comment_doctype is not null and comment_doctype not in ('Message', 'My Company')"""
- .format(to_fields=to_fields, from_fields=from_fields))
-
- # chat and assignment notifications
- frappe.db.sql("""insert ignore into `tabCommunication` (
- subject,
- content,
- sender,
- sender_full_name,
- comment_type,
- communication_date,
- reference_doctype,
- reference_name,
- {to_fields}
-
- name,
- user,
- owner,
- creation,
- modified_by,
- modified,
- status,
- sent_or_received,
- communication_type,
- seen
- )
- select
- case
- when parenttype='Assignment' then %(assignment)s
- else substring(comment, 1, 100)
- end
- as subject,
- comment as content,
- comment_by as sender,
- comment_by_fullname as sender_full_name,
- comment_type,
- ifnull(timestamp(comment_date, comment_time), creation) as communication_date,
- 'User' as reference_doctype,
- comment_docname as reference_name,
- {from_fields}
-
- name,
- owner as user,
- owner,
- creation,
- modified_by,
- modified,
- 'Linked' as status,
- 'Sent' as sent_or_received,
- case
- when parenttype='Assignment' then 'Notification'
- else 'Chat'
- end
- as communication_type,
- 1 as seen
- from `tabComment` where comment_doctype in ('Message', 'My Company')"""
- .format(to_fields=to_fields, from_fields=from_fields), {"assignment": _("Assignment")})
-
-def migrate_feed():
- # migrate delete feed
- for doctype in frappe.db.sql("""select distinct doc_type from `tabFeed` where subject=%(deleted)s""", {"deleted": _("Deleted")}):
- frappe.db.sql("""insert ignore into `tabCommunication` (
- subject,
- sender,
- sender_full_name,
- comment_type,
- communication_date,
- reference_doctype,
-
- name,
- user,
- owner,
- creation,
- modified_by,
- modified,
- status,
- sent_or_received,
- communication_type,
- seen
- )
- select
- concat_ws(" ", %(_doctype)s, doc_name) as subject,
- owner as sender,
- full_name as sender_full_name,
- 'Deleted' as comment_type,
- creation as communication_date,
- doc_type as reference_doctype,
-
- name,
- owner as user,
- owner,
- creation,
- modified_by,
- modified,
- 'Linked' as status,
- 'Sent' as sent_or_received,
- 'Comment' as communication_type,
- 1 as seen
- from `tabFeed` where subject=%(deleted)s and doc_type=%(doctype)s""", {
- "deleted": _("Deleted"),
- "doctype": doctype,
- "_doctype": _(doctype)
- })
-
- # migrate feed type login or empty
- frappe.db.sql("""insert ignore into `tabCommunication` (
- subject,
- sender,
- sender_full_name,
- comment_type,
- communication_date,
- reference_doctype,
- reference_name,
-
- name,
- user,
- owner,
- creation,
- modified_by,
- modified,
- status,
- sent_or_received,
- communication_type,
- seen
- )
- select
- subject,
- owner as sender,
- full_name as sender_full_name,
- case
- when feed_type='Login' then 'Info'
- else 'Updated'
- end as comment_type,
- creation as communication_date,
- doc_type as reference_doctype,
- doc_name as reference_name,
-
- name,
- owner as user,
- owner,
- creation,
- modified_by,
- modified,
- 'Linked' as status,
- 'Sent' as sent_or_received,
- 'Comment' as communication_type,
- 1 as seen
- from `tabFeed` where (feed_type in ('Login', '') or feed_type is null)""")
-
-def update_timeline_doc_for(timeline_doctype):
- """NOTE: This method may be used by other apps for patching. It also has COMMIT after each update."""
-
- # find linked doctypes
- # link fields
- update_for_linked_docs(timeline_doctype)
-
- # dynamic link fields
- update_for_dynamically_linked_docs(timeline_doctype)
-
-def update_for_linked_docs(timeline_doctype):
- for df in get_link_fields(timeline_doctype):
- if df.issingle:
- continue
-
- reference_doctype = df.parent
-
- if not is_valid_timeline_doctype(reference_doctype, timeline_doctype):
- continue
-
- for doc in frappe.get_all(reference_doctype, fields=["name", df.fieldname]):
- timeline_name = doc.get(df.fieldname)
- update_communication(timeline_doctype, timeline_name, reference_doctype, doc.name)
-
-def update_for_dynamically_linked_docs(timeline_doctype):
- dynamic_link_fields = []
- for query in dynamic_link_queries:
- for df in frappe.db.sql(query, as_dict=True):
- dynamic_link_fields.append(df)
-
- for df in dynamic_link_fields:
- reference_doctype = df.parent
-
- if not is_valid_timeline_doctype(reference_doctype, timeline_doctype):
- continue
-
- try:
- docs = frappe.get_all(reference_doctype, fields=["name", df.fieldname],
- filters={ df.options: timeline_doctype })
- except frappe.db.SQLError as e:
- if frappe.db.is_table_missing(e):
- # single
- continue
- else:
- raise
-
- for doc in docs:
- timeline_name = doc.get(df.fieldname)
- update_communication(timeline_doctype, timeline_name, reference_doctype, doc.name)
-
-def update_communication(timeline_doctype, timeline_name, reference_doctype, reference_name):
- if not timeline_name:
- return
-
- frappe.db.sql("""update `tabCommunication` set timeline_doctype=%(timeline_doctype)s, timeline_name=%(timeline_name)s
- where (reference_doctype=%(reference_doctype)s and reference_name=%(reference_name)s)
- and (timeline_doctype is null or timeline_doctype='')
- and (timeline_name is null or timeline_name='')""", {
- "timeline_doctype": timeline_doctype,
- "timeline_name": timeline_name,
- "reference_doctype": reference_doctype,
- "reference_name": reference_name
- })
-
- frappe.db.commit()
-
-def is_valid_timeline_doctype(reference_doctype, timeline_doctype):
- # for reloading timeline_field
- frappe.reload_doctype(reference_doctype)
-
- # make sure the timeline field's doctype is same as timeline doctype
- meta = frappe.get_meta(reference_doctype)
- if not meta.timeline_field:
- return False
-
- doctype = meta.get_link_doctype(meta.timeline_field)
- if doctype != timeline_doctype:
- return False
-
-
- return True
diff --git a/frappe/patches/v6_2/__init__.py b/frappe/patches/v6_2/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/patches/v6_2/ignore_user_permissions_if_missing.py b/frappe/patches/v6_2/ignore_user_permissions_if_missing.py
deleted file mode 100644
index 356d28989a..0000000000
--- a/frappe/patches/v6_2/ignore_user_permissions_if_missing.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doctype("System Settings")
- system_settings = frappe.get_doc("System Settings")
- system_settings.flags.ignore_mandatory = 1
- system_settings.save()
diff --git a/frappe/patches/v6_2/rename_backup_manager.py b/frappe/patches/v6_2/rename_backup_manager.py
deleted file mode 100644
index af02e55878..0000000000
--- a/frappe/patches/v6_2/rename_backup_manager.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- unset = False
- frappe.reload_doc("integrations", "doctype", "dropbox_backup")
-
- dropbox_backup = frappe.get_doc("Dropbox Backup", "Dropbox Backup")
- for df in dropbox_backup.meta.fields:
- value = frappe.db.get_single_value("Backup Manager", df.fieldname)
- if value:
- if df.fieldname=="upload_backups_to_dropbox" and value=="Never":
- value = "Daily"
- unset = True
- dropbox_backup.set(df.fieldname, value)
-
- if unset:
- dropbox_backup.set("send_backups_to_dropbox", 0)
-
- dropbox_backup.save()
diff --git a/frappe/patches/v6_20x/__init__.py b/frappe/patches/v6_20x/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/patches/v6_20x/remove_roles_from_website_user.py b/frappe/patches/v6_20x/remove_roles_from_website_user.py
deleted file mode 100644
index a4d579a1f0..0000000000
--- a/frappe/patches/v6_20x/remove_roles_from_website_user.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doc("core", "doctype", "user_email")
- frappe.reload_doc("core", "doctype", "user")
- for user_name in frappe.get_all('User', filters={'user_type': 'Website User'}):
- user = frappe.get_doc('User', user_name)
- if user.roles:
- user.roles = []
- user.save()
diff --git a/frappe/patches/v6_20x/set_allow_draft_for_print.py b/frappe/patches/v6_20x/set_allow_draft_for_print.py
deleted file mode 100644
index 90c15e22b2..0000000000
--- a/frappe/patches/v6_20x/set_allow_draft_for_print.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.db.set_value("Print Settings", "Print Settings", "allow_print_for_draft", 1)
\ No newline at end of file
diff --git a/frappe/patches/v6_20x/update_insert_after.py b/frappe/patches/v6_20x/update_insert_after.py
deleted file mode 100644
index 5ebec52fc9..0000000000
--- a/frappe/patches/v6_20x/update_insert_after.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from __future__ import unicode_literals
-import frappe, json
-
-def execute():
- for ps in frappe.get_all('Property Setter', filters={'property': '_idx'},
- fields = ['doc_type', 'value']):
- custom_fields = frappe.get_all('Custom Field',
- filters = {'dt': ps.doc_type}, fields=['name', 'fieldname'])
-
- if custom_fields:
- _idx = json.loads(ps.value)
-
- for custom_field in custom_fields:
- if custom_field.fieldname in _idx:
- custom_field_idx = _idx.index(custom_field.fieldname)
- if custom_field_idx == 0:
- prev_fieldname = ""
-
- else:
- prev_fieldname = _idx[custom_field_idx - 1]
-
- else:
- prev_fieldname = _idx[-1]
- custom_field_idx = len(_idx)
-
- frappe.db.set_value('Custom Field', custom_field.name, 'insert_after', prev_fieldname)
- frappe.db.set_value('Custom Field', custom_field.name, 'idx', custom_field_idx)
diff --git a/frappe/patches/v6_21/__init__.py b/frappe/patches/v6_21/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/patches/v6_21/print_settings_repeat_header_footer.py b/frappe/patches/v6_21/print_settings_repeat_header_footer.py
deleted file mode 100644
index 941a145a54..0000000000
--- a/frappe/patches/v6_21/print_settings_repeat_header_footer.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doctype('Print Settings')
- frappe.db.set_value('Print Settings', 'Print Settings', 'repeat_header_footer', 1)
diff --git a/frappe/patches/v6_24/__init__.py b/frappe/patches/v6_24/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/patches/v6_24/set_language_as_code.py b/frappe/patches/v6_24/set_language_as_code.py
deleted file mode 100644
index d685fd7d0e..0000000000
--- a/frappe/patches/v6_24/set_language_as_code.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-from frappe.translate import get_lang_dict
-
-# migrate language from name to code
-def execute():
- return
diff --git a/frappe/patches/v6_4/__init__.py b/frappe/patches/v6_4/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/patches/v6_4/reduce_varchar_length.py b/frappe/patches/v6_4/reduce_varchar_length.py
deleted file mode 100644
index 93a8be8c92..0000000000
--- a/frappe/patches/v6_4/reduce_varchar_length.py
+++ /dev/null
@@ -1,37 +0,0 @@
-from __future__ import unicode_literals, print_function
-import frappe
-
-def execute():
- for doctype in frappe.get_all("DocType", filters={"issingle": 0}):
- doctype = doctype.name
- if not frappe.db.table_exists(doctype):
- continue
-
- for column in frappe.db.sql("desc `tab{doctype}`".format(doctype=doctype), as_dict=True):
- fieldname = column["Field"]
- column_type = column["Type"]
-
- if not column_type.startswith("varchar"):
- continue
-
- max_length = frappe.db.sql("""select max(char_length(`{fieldname}`)) from `tab{doctype}`"""\
- .format(fieldname=fieldname, doctype=doctype))
-
- max_length = max_length[0][0] if max_length else None
-
- if max_length and 140 < max_length <= 255:
- print(
- "setting length of '{fieldname}' in '{doctype}' as {length}".format(
- fieldname=fieldname, doctype=doctype, length=max_length)
- )
-
- # create property setter for length
- frappe.make_property_setter({
- "doctype": doctype,
- "fieldname": fieldname,
- "property": "length",
- "value": max_length,
- "property_type": "Int"
- })
-
- frappe.clear_cache(doctype=doctype)
diff --git a/frappe/patches/v6_4/rename_bengali_language.py b/frappe/patches/v6_4/rename_bengali_language.py
deleted file mode 100644
index dbbcb62f8d..0000000000
--- a/frappe/patches/v6_4/rename_bengali_language.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-import frappe
-from frappe.translate import rename_language
-
-def execute():
- rename_language("বাঙালি", "বাংলা")
\ No newline at end of file
diff --git a/frappe/patches/v6_6/__init__.py b/frappe/patches/v6_6/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/patches/v6_6/fix_file_url.py b/frappe/patches/v6_6/fix_file_url.py
deleted file mode 100644
index 4f8956d343..0000000000
--- a/frappe/patches/v6_6/fix_file_url.py
+++ /dev/null
@@ -1,36 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-from frappe.model.meta import is_single
-
-def execute():
- """Fix old style file urls that start with files/"""
- fix_file_urls()
- fix_attach_field_urls()
-
-def fix_file_urls():
- for file in frappe.db.sql_list("""select name from `tabFile` where file_url like 'files/%'"""):
- file = frappe.get_doc("File", file)
- file.db_set("file_url", "/" + file.file_url, update_modified=False)
- try:
- file.validate_file()
- file.db_set("file_name", file.file_name, update_modified=False)
- if not file.content_hash:
- file.generate_content_hash()
- file.db_set("content_hash", file.content_hash, update_modified=False)
-
- except IOError:
- pass
-
-def fix_attach_field_urls():
- # taken from an old patch
- attach_fields = (frappe.db.sql("""select parent, fieldname from `tabDocField` where fieldtype in ('Attach', 'Attach Image')""") +
- frappe.db.sql("""select dt, fieldname from `tabCustom Field` where fieldtype in ('Attach', 'Attach Image')"""))
-
- for doctype, fieldname in attach_fields:
- if is_single(doctype):
- frappe.db.sql("""update `tabSingles` set value=concat("/", `value`)
- where doctype=%(doctype)s and field=%(fieldname)s
- and value like 'files/%%'""", {"doctype": doctype, "fieldname": fieldname})
- else:
- frappe.db.sql("""update `tab{doctype}` set `{fieldname}`=concat("/", `{fieldname}`)
- where `{fieldname}` like 'files/%'""".format(doctype=doctype, fieldname=fieldname))
diff --git a/frappe/patches/v6_6/rename_slovak_language.py b/frappe/patches/v6_6/rename_slovak_language.py
deleted file mode 100644
index a942543372..0000000000
--- a/frappe/patches/v6_6/rename_slovak_language.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-import frappe
-from frappe.translate import rename_language
-
-def execute():
- rename_language("slovenčina", "slovenčina (Slovak)")
diff --git a/frappe/patches/v6_6/user_last_active.py b/frappe/patches/v6_6/user_last_active.py
deleted file mode 100644
index fd55935174..0000000000
--- a/frappe/patches/v6_6/user_last_active.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doctype("User")
- frappe.db.sql("update `tabUser` set last_active=last_login")
diff --git a/frappe/patches/v6_9/__init__.py b/frappe/patches/v6_9/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/patches/v6_9/int_float_not_null.py b/frappe/patches/v6_9/int_float_not_null.py
deleted file mode 100644
index 97495f9077..0000000000
--- a/frappe/patches/v6_9/int_float_not_null.py
+++ /dev/null
@@ -1,30 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-from frappe.utils import cint, flt
-
-def execute():
- for doctype in frappe.get_all("DocType", filters={"issingle": 0}):
- doctype = doctype.name
- meta = frappe.get_meta(doctype)
-
- for column in frappe.db.sql("desc `tab{doctype}`".format(doctype=doctype), as_dict=True):
- fieldname = column["Field"]
- column_type = column["Type"]
-
- if not (column_type.startswith("int") or column_type.startswith("decimal")):
- continue
-
- frappe.db.sql("""update `tab{doctype}` set `{fieldname}`=0 where `{fieldname}` is null"""\
- .format(doctype=doctype, fieldname=fieldname))
-
- # alter table
- if column["Null"]=='YES':
- if not meta.get_field(fieldname):
- continue
-
- default = cint(column["Default"]) if column_type.startswith("int") else flt(column["Default"])
- frappe.db.sql_ddl("""alter table `tab{doctype}`
- change `{fieldname}` `{fieldname}` {column_type} not null default '{default}'""".format(
- doctype=doctype, fieldname=fieldname, column_type=column_type, default=default))
-
-
diff --git a/frappe/patches/v6_9/rename_burmese_language.py b/frappe/patches/v6_9/rename_burmese_language.py
deleted file mode 100644
index 66477f7efe..0000000000
--- a/frappe/patches/v6_9/rename_burmese_language.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-import frappe
-from frappe.translate import rename_language
-
-def execute():
- rename_language("Melayu", "မြန်မာ")
diff --git a/frappe/patches/v7_0/__init__.py b/frappe/patches/v7_0/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/patches/v7_0/add_communication_in_doc.py b/frappe/patches/v7_0/add_communication_in_doc.py
deleted file mode 100644
index 4db02c5bab..0000000000
--- a/frappe/patches/v7_0/add_communication_in_doc.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-from frappe.core.doctype.comment.comment import update_comment_in_doc
-
-def execute():
- for d in frappe.db.get_all("Communication",
- fields = ['name', 'reference_doctype', 'reference_name', 'SUBSTRING(content,1,102)', 'communication_type'],
- filters = {"reference_name":None,"reference_doctype":None,'communication_type': 'Communication'}):
-
- try:
- update_comment_in_doc(d)
- except frappe.ImplicitCommitError:
- pass
diff --git a/frappe/patches/v7_0/cleanup_list_settings.py b/frappe/patches/v7_0/cleanup_list_settings.py
deleted file mode 100644
index e03ff57406..0000000000
--- a/frappe/patches/v7_0/cleanup_list_settings.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from __future__ import unicode_literals
-import frappe, json
-
-def execute():
- if frappe.db.table_exists("__ListSettings"):
- list_settings = frappe.db.sql("select user, doctype, data from __ListSettings", as_dict=1)
- for ls in list_settings:
- if ls and ls.data:
- data = json.loads(ls.data)
- if "fields" not in data:
- continue
- fields = data["fields"]
- for field in fields:
- if "name as" in field:
- fields.remove(field)
- data["fields"] = fields
-
- frappe.db.sql("update __ListSettings set data = %s where user=%s and doctype=%s",
- (json.dumps(data), ls.user, ls.doctype))
-
diff --git a/frappe/patches/v7_0/create_private_file_folder.py b/frappe/patches/v7_0/create_private_file_folder.py
deleted file mode 100644
index bd26917a78..0000000000
--- a/frappe/patches/v7_0/create_private_file_folder.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from __future__ import unicode_literals
-import frappe, os
-
-def execute():
- if not os.path.exists(os.path.join(frappe.local.site_path, 'private', 'files')):
- frappe.create_folder(os.path.join(frappe.local.site_path, 'private', 'files'))
\ No newline at end of file
diff --git a/frappe/patches/v7_0/re_route.py b/frappe/patches/v7_0/re_route.py
deleted file mode 100644
index cc36594ae8..0000000000
--- a/frappe/patches/v7_0/re_route.py
+++ /dev/null
@@ -1,23 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-from frappe.model.base_document import get_controller
-
-def execute():
- update_routes(['Blog Post', 'Blog Category', 'Web Page'])
-
-def update_routes(doctypes):
- """Patch old routing system"""
- for d in doctypes:
- frappe.reload_doctype(d)
- c = get_controller(d)
-
- condition = ''
- if c.website.condition_field:
- condition = 'where {0}=1'.format(c.website.condition_field)
-
- try:
- frappe.db.sql("""update ignore `tab{0}` set route = concat(ifnull(parent_website_route, ""),
- if(ifnull(parent_website_route, "")="", "", "/"), page_name) {1}""".format(d, condition))
-
- except Exception as e:
- if not frappe.db.is_missing_column(e): raise
diff --git a/frappe/patches/v7_0/rename_bulk_email_to_email_queue.py b/frappe/patches/v7_0/rename_bulk_email_to_email_queue.py
deleted file mode 100644
index 9a7a756144..0000000000
--- a/frappe/patches/v7_0/rename_bulk_email_to_email_queue.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.rename_doc('DocType', 'Bulk Email', 'Email Queue')
\ No newline at end of file
diff --git a/frappe/patches/v7_0/rename_newsletter_list_to_email_group.py b/frappe/patches/v7_0/rename_newsletter_list_to_email_group.py
deleted file mode 100644
index 79061d383c..0000000000
--- a/frappe/patches/v7_0/rename_newsletter_list_to_email_group.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.rename_doc('DocType', 'Newsletter List', 'Email Group')
- frappe.rename_doc('DocType', 'Newsletter List Subscriber', 'Email Group Member')
\ No newline at end of file
diff --git a/frappe/patches/v7_0/set_email_group.py b/frappe/patches/v7_0/set_email_group.py
deleted file mode 100644
index e3dd66ebf3..0000000000
--- a/frappe/patches/v7_0/set_email_group.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doc("email", "doctype", "email_group_member")
- if "newsletter_list" in frappe.db.get_table_columns("Email Group Member"):
- frappe.db.sql("""update `tabEmail Group Member` set email_group = newsletter_list
- where email_group is null or email_group = ''""")
\ No newline at end of file
diff --git a/frappe/patches/v7_0/set_user_fullname.py b/frappe/patches/v7_0/set_user_fullname.py
deleted file mode 100644
index a7c6670f45..0000000000
--- a/frappe/patches/v7_0/set_user_fullname.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doc("Core", "DocType", "User")
-
- for user in frappe.db.get_all('User'):
- user = frappe.get_doc('User', user.name)
- user.set_full_name()
- user.db_set('full_name', user.full_name, update_modified = False)
\ No newline at end of file
diff --git a/frappe/patches/v7_0/update_auth.py b/frappe/patches/v7_0/update_auth.py
deleted file mode 100644
index 3d47edf4b5..0000000000
--- a/frappe/patches/v7_0/update_auth.py
+++ /dev/null
@@ -1,42 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-from frappe.utils.password import create_auth_table, set_encrypted_password
-
-def execute():
- if '__OldAuth' not in frappe.db.get_tables():
- frappe.db.sql_ddl('''alter table `__Auth` rename `__OldAuth`''')
-
- create_auth_table()
-
- # user passwords
- frappe.db.sql('''insert ignore into `__Auth` (doctype, name, fieldname, `password`)
- (select 'User', `name`, 'password', `password` from `__OldAuth`)''')
-
- frappe.db.commit()
-
- # other password fields
- for doctype in frappe.db.sql_list('''select distinct parent from `tabDocField`
- where fieldtype="Password" and parent != "User"'''):
-
- frappe.reload_doctype(doctype)
- meta = frappe.get_meta(doctype)
-
- for df in meta.get('fields', {'fieldtype': 'Password'}):
- if meta.issingle:
- password = frappe.db.get_value(doctype, doctype, df.fieldname)
- if password:
- set_encrypted_password(doctype, doctype, password, fieldname=df.fieldname)
- frappe.db.set_value(doctype, doctype, df.fieldname, '*'*len(password))
-
- else:
- for d in frappe.db.sql('''select name, `{fieldname}` from `tab{doctype}`
- where `{fieldname}` is not null'''.format(fieldname=df.fieldname, doctype=doctype), as_dict=True):
-
- set_encrypted_password(doctype, d.name, d.get(df.fieldname), fieldname=df.fieldname)
-
- frappe.db.sql('''update `tab{doctype}` set `{fieldname}`=repeat("*", char_length(`{fieldname}`))'''
- .format(doctype=doctype, fieldname=df.fieldname))
-
- frappe.db.commit()
-
- frappe.db.sql_ddl('''drop table `__OldAuth`''')
diff --git a/frappe/patches/v7_0/update_report_builder_json.py b/frappe/patches/v7_0/update_report_builder_json.py
deleted file mode 100644
index a344ca5412..0000000000
--- a/frappe/patches/v7_0/update_report_builder_json.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- for report in frappe.db.sql_list(""" select name from `tabReport` where report_type = 'Report Builder'
- and is_standard = 'No' and `json` != '' and `json` is not null """):
- doc = frappe.get_doc("Report", report)
- doc.update_report_json()
- doc.db_set("json", doc.json, update_modified=False)
\ No newline at end of file
diff --git a/frappe/patches/v7_0/update_send_after_in_bulk_email.py b/frappe/patches/v7_0/update_send_after_in_bulk_email.py
deleted file mode 100644
index 1b08309b6a..0000000000
--- a/frappe/patches/v7_0/update_send_after_in_bulk_email.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-from frappe.utils import now_datetime
-
-def execute():
- frappe.db.sql('update `tabEmail Queue` set send_after=%s where send_after is null', now_datetime())
\ No newline at end of file
diff --git a/frappe/patches/v7_1/__init__.py b/frappe/patches/v7_1/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/patches/v7_1/disabled_print_settings_for_custom_print_format.py b/frappe/patches/v7_1/disabled_print_settings_for_custom_print_format.py
deleted file mode 100644
index c74d2d98f9..0000000000
--- a/frappe/patches/v7_1/disabled_print_settings_for_custom_print_format.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doctype('Print Format')
- frappe.db.sql("""
- update
- `tabPrint Format`
- set
- align_labels_right = 0, line_breaks = 0, show_section_headings = 0
- where
- custom_format = 1
- """)
diff --git a/frappe/patches/v7_1/refactor_integration_broker.py b/frappe/patches/v7_1/refactor_integration_broker.py
deleted file mode 100644
index 8c9aaa6795..0000000000
--- a/frappe/patches/v7_1/refactor_integration_broker.py
+++ /dev/null
@@ -1,52 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-import json
-
-def execute():
- for doctype_name in ["Razorpay Log", "Razorpay Payment", "Razorpay Settings"]:
- delete_doc("DocType", doctype_name)
-
- reload_doctypes()
- setup_services()
-
-def delete_doc(doctype, doctype_name):
- frappe.delete_doc(doctype, doctype_name)
-
-def reload_doctypes():
- for doctype in ("razorpay_settings", "paypal_settings", "dropbox_settings", "ldap_settings"):
- frappe.reload_doc("integrations", "doctype", doctype)
-
-def setup_services():
- for service in [{"old_name": "Razorpay", "new_name": "Razorpay"},
- {"old_name": "PayPal", "new_name": "PayPal"},
- {"old_name": "Dropbox Integration", "new_name": "Dropbox"},
- {"old_name": "LDAP Auth", "new_name": "LDAP"}]:
-
- try:
- service_doc = frappe.get_doc("Integration Service", service["old_name"])
- settings = json.loads(service_doc.custom_settings_json)
-
- service_settings = frappe.new_doc("{0} Settings".format(service["new_name"]))
- service_settings.update(settings)
-
- service_settings.flags.ignore_mandatory = True
- service_settings.save(ignore_permissions=True)
-
- if service["old_name"] in ["Dropbox Integration", "LDAP Auth"]:
- delete_doc("Integration Service", service["old_name"])
-
- new_service_doc = frappe.get_doc({
- "doctype": "Integration Service",
- "service": service["new_name"],
- "enabled": 1
- })
-
- new_service_doc.flags.ignore_mandatory = True
- new_service_doc.save(ignore_permissions=True)
-
- except Exception:
- pass
diff --git a/frappe/patches/v7_1/rename_chinese_language_codes.py b/frappe/patches/v7_1/rename_chinese_language_codes.py
deleted file mode 100644
index 1ed25a4959..0000000000
--- a/frappe/patches/v7_1/rename_chinese_language_codes.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.rename_doc('Language', 'zh-cn', 'zh', force=True,
- merge=True if frappe.db.exists('Language', 'zh') else False)
- if frappe.db.get_value('Language', 'zh-tw') == 'zh-tw':
- frappe.rename_doc('Language', 'zh-tw', 'zh-TW', force=True)
-
- frappe.db.set_value('Language', 'zh', 'language_code', 'zh')
- frappe.db.set_value('Language', 'zh-TW', 'language_code', 'zh-TW')
\ No newline at end of file
diff --git a/frappe/patches/v7_1/rename_scheduler_log_to_error_log.py b/frappe/patches/v7_1/rename_scheduler_log_to_error_log.py
deleted file mode 100644
index 4d1a39538f..0000000000
--- a/frappe/patches/v7_1/rename_scheduler_log_to_error_log.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- if not 'tabError Log' in frappe.db.get_tables():
- frappe.rename_doc('DocType', 'Scheduler Log', 'Error Log')
- frappe.db.sql("""delete from `tabError Log` where datediff(curdate(), creation) > 30""")
- frappe.db.commit()
- frappe.db.sql('alter table `tabError Log` change column name name varchar(140)')
- frappe.db.sql('alter table `tabError Log` change column parent parent varchar(140)')
- frappe.db.sql('alter table `tabError Log` engine=MyISAM')
diff --git a/frappe/patches/v7_1/set_backup_limit.py b/frappe/patches/v7_1/set_backup_limit.py
deleted file mode 100644
index 7b0a344305..0000000000
--- a/frappe/patches/v7_1/set_backup_limit.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from __future__ import unicode_literals
-from frappe.utils import cint
-import frappe
-
-def execute():
- frappe.reload_doctype('System Settings')
- backup_limit = frappe.db.get_single_value('System Settings', 'backup_limit')
-
- if cint(backup_limit) == 0:
- frappe.db.set_value('System Settings', 'System Settings', 'backup_limit', 3)
diff --git a/frappe/patches/v7_1/setup_integration_services.py b/frappe/patches/v7_1/setup_integration_services.py
deleted file mode 100644
index 1c70b8e835..0000000000
--- a/frappe/patches/v7_1/setup_integration_services.py
+++ /dev/null
@@ -1,118 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-from frappe.exceptions import DataError
-from frappe.utils.password import get_decrypted_password
-from frappe.utils import cstr
-import os
-
-app_list = [
- {"app_name": "razorpay_integration", "service_name": "Razorpay", "doctype": "Razorpay Settings", "remove": True},
- {"app_name": "paypal_integration", "service_name": "PayPal", "doctype": "PayPal Settings", "remove": True},
- {"app_name": "frappe", "service_name": "Dropbox", "doctype": "Dropbox Backup", "remove": False}
-]
-
-def execute():
- installed_apps = frappe.get_installed_apps()
-
- for app_details in app_list:
- if app_details["app_name"] in installed_apps:
- settings = get_app_settings(app_details)
- if app_details["remove"]:
- uninstall_app(app_details["app_name"])
-
- try:
- setup_integration_service(app_details, settings)
- except DataError:
- pass
-
- frappe.delete_doc("DocType", "Dropbox Backup")
-
-def setup_integration_service(app_details, settings=None):
- if not settings:
- return
-
- setup_service_settings(app_details["service_name"], settings)
-
- doc_path = frappe.get_app_path("frappe", "integration_broker", "doctype",
- "integration_service", "integration_service.json")
-
- if not os.path.exists(doc_path):
- return
-
- frappe.reload_doc("integration_broker", "doctype", "integration_service")
-
- if frappe.db.exists("Integration Service", app_details["service_name"]):
- integration_service = frappe.get_doc("Integration Service", app_details["service_name"])
- else:
- integration_service = frappe.new_doc("Integration Service")
- integration_service.service = app_details["service_name"]
-
- integration_service.enabled = 1
- integration_service.flags.ignore_mandatory = True
- integration_service.save(ignore_permissions=True)
-
-def get_app_settings(app_details):
- parameters = {}
- doctype = docname = app_details["doctype"]
-
- app_settings = get_parameters(app_details)
- if app_settings:
- settings = app_settings["settings"]
- frappe.reload_doc("integrations", "doctype", "{0}_settings".format(app_details["service_name"].lower()))
- controller = frappe.get_meta("{0} Settings".format(app_details["service_name"]))
-
- for d in controller.fields:
- if settings.get(d.fieldname):
- if ''.join(set(cstr(settings.get(d.fieldname)))) == '*':
- setattr(settings, d.fieldname, get_decrypted_password(doctype, docname, d.fieldname, raise_exception=True))
-
- parameters.update({d.fieldname : settings.get(d.fieldname)})
-
- return parameters
-
-def uninstall_app(app_name):
- from frappe.installer import remove_from_installed_apps
- remove_from_installed_apps(app_name)
-
-def get_parameters(app_details):
- if app_details["service_name"] == "Razorpay":
- return {"settings": frappe.get_doc(app_details["doctype"])}
-
- elif app_details["service_name"] == "PayPal":
- if frappe.conf.paypal_username and frappe.conf.paypal_password and frappe.conf.paypal_signature:
- return {
- "settings": {
- "api_username": frappe.conf.paypal_username,
- "api_password": frappe.conf.paypal_password,
- "signature": frappe.conf.paypal_signature
- }
- }
- else:
- return {"settings": frappe.get_doc(app_details["doctype"])}
-
- elif app_details["service_name"] == "Dropbox":
- doc = frappe.db.get_value(app_details["doctype"], None,
- ["dropbox_access_key", "dropbox_access_secret", "upload_backups_to_dropbox"], as_dict=1)
-
- if not doc:
- return
-
- if not (frappe.conf.dropbox_access_key and frappe.conf.dropbox_secret_key):
- return
-
- return {
- "settings": {
- "app_access_key": frappe.conf.dropbox_access_key,
- "app_secret_key": frappe.conf.dropbox_secret_key,
- "dropbox_access_key": doc.dropbox_access_key,
- "dropbox_access_secret": doc.dropbox_access_secret,
- "backup_frequency": doc.upload_backups_to_dropbox,
- "enabled": doc.send_backups_to_dropbox
- }
- }
-
-def setup_service_settings(service_name, settings):
- service_doc = frappe.get_doc("{0} Settings".format(service_name))
- service_doc.update(settings)
- service_doc.flags.ignore_mandatory = True
- service_doc.save(ignore_permissions=True)
\ No newline at end of file
diff --git a/frappe/patches/v7_1/sync_language_doctype.py b/frappe/patches/v7_1/sync_language_doctype.py
deleted file mode 100644
index 83d1a4f5a6..0000000000
--- a/frappe/patches/v7_1/sync_language_doctype.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-from frappe.translate import get_lang_dict
-
-def execute():
- frappe.reload_doc('core', 'doctype', 'language')
-
- from frappe.core.doctype.language.language import sync_languages
- sync_languages()
-
- # move language from old style to new style for old accounts
- # i.e. from "english" to "en"
-
- lang_dict = get_lang_dict()
- language = frappe.db.get_value('System Settings', None, 'language')
- if language:
- frappe.db.set_value('System Settings', None, 'language', lang_dict.get('language') or 'en')
-
- for user in frappe.get_all('User', fields=['name', 'language']):
- if user.language:
- frappe.db.set_value('User', user.name, 'language',
- lang_dict.get('language') or 'en', update_modified=False)
diff --git a/frappe/patches/v7_2/__init__.py b/frappe/patches/v7_2/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/patches/v7_2/fix_email_queue_recipient.py b/frappe/patches/v7_2/fix_email_queue_recipient.py
deleted file mode 100644
index 645b17b5c9..0000000000
--- a/frappe/patches/v7_2/fix_email_queue_recipient.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doc('email', 'doctype', 'email_queue_recipient')
- frappe.db.sql('update `tabEmail Queue Recipient` set parenttype="recipients"')
\ No newline at end of file
diff --git a/frappe/patches/v7_2/merge_knowledge_base.py b/frappe/patches/v7_2/merge_knowledge_base.py
deleted file mode 100644
index 301d15e1dd..0000000000
--- a/frappe/patches/v7_2/merge_knowledge_base.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-from frappe.patches.v7_0.re_route import update_routes
-from frappe.installer import remove_from_installed_apps
-
-def execute():
- if 'knowledge_base' in frappe.get_installed_apps():
- frappe.reload_doc('website', 'doctype', 'help_category')
- frappe.reload_doc('website', 'doctype', 'help_article')
- update_routes(['Help Category', 'Help Article'])
- remove_from_installed_apps('knowledge_base')
-
- # remove module def
- if frappe.db.exists('Module Def', 'Knowledge Base'):
- frappe.delete_doc('Module Def', 'Knowledge Base')
-
- # set missing routes
- for doctype in ('Help Category', 'Help Article'):
- for d in frappe.get_all(doctype, fields=['name', 'route']):
- if not d.route:
- doc = frappe.get_doc(doctype, d.name)
- doc.set_route()
- doc.db_update()
\ No newline at end of file
diff --git a/frappe/patches/v7_2/remove_in_filter.py b/frappe/patches/v7_2/remove_in_filter.py
deleted file mode 100644
index 36556d7c13..0000000000
--- a/frappe/patches/v7_2/remove_in_filter.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- if frappe.db.has_column('DocField', 'in_filter'):
- frappe.db.sql('alter table tabDocField drop column in_filter')
- frappe.clear_cache(doctype="DocField")
\ No newline at end of file
diff --git a/frappe/patches/v7_2/set_doctype_engine.py b/frappe/patches/v7_2/set_doctype_engine.py
deleted file mode 100644
index 3a5cc384a2..0000000000
--- a/frappe/patches/v7_2/set_doctype_engine.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- for t in frappe.db.sql('show table status'):
- if t[0].startswith('tab'):
- frappe.db.sql('update tabDocType set engine=%s where name=%s', (t[1], t[0][3:]))
\ No newline at end of file
diff --git a/frappe/patches/v7_2/set_in_standard_filter_property.py b/frappe/patches/v7_2/set_in_standard_filter_property.py
deleted file mode 100644
index 12f97f7f8e..0000000000
--- a/frappe/patches/v7_2/set_in_standard_filter_property.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doc('custom', 'doctype', 'custom_field', force=True)
-
- try:
- frappe.db.sql('update `tabCustom Field` set in_standard_filter = in_filter_dash')
- except Exception as e:
- if not frappe.db.is_missing_column(e): raise e
-
- for doctype in frappe.get_all("DocType", {"istable": 0, "issingle": 0, "custom": 0}):
- try:
- frappe.reload_doctype(doctype.name, force=True)
- except KeyError:
- pass
- except frappe.db.DataError:
- pass
- except Exception:
- pass
diff --git a/frappe/patches/v7_2/setup_custom_perms.py b/frappe/patches/v7_2/setup_custom_perms.py
deleted file mode 100644
index 1b3b86236c..0000000000
--- a/frappe/patches/v7_2/setup_custom_perms.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-from frappe.permissions import setup_custom_perms
-from frappe.core.page.permission_manager.permission_manager import get_standard_permissions
-from frappe.utils.reset_doc import setup_perms_for
-
-'''
-Copy DocPerm to Custom DocPerm where permissions are set differently
-'''
-
-def execute():
- for d in frappe.db.get_all('DocType', dict(istable=0, issingle=0, custom=0)):
- setup_perms_for(d.name)
diff --git a/frappe/patches/v7_2/setup_ldap_config.py b/frappe/patches/v7_2/setup_ldap_config.py
deleted file mode 100644
index 31dd8ca6fe..0000000000
--- a/frappe/patches/v7_2/setup_ldap_config.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-from frappe.utils import cint
-
-def execute():
- frappe.reload_doc("integrations", "doctype", "ldap_settings")
-
- if not frappe.db.exists("DocType", "Integration Service"):
- return
-
- if not frappe.db.exists("Integration Service", "LDAP"):
- return
-
- if not cint(frappe.db.get_value("Integration Service", "LDAP", 'enabled')):
- return
-
- import ldap
- try:
- ldap_settings = frappe.get_doc("LDAP Settings")
- ldap_settings.save(ignore_permissions=True)
- except ldap.LDAPError:
- pass
diff --git a/frappe/patches/v7_2/update_communications.py b/frappe/patches/v7_2/update_communications.py
deleted file mode 100644
index f3d859b95a..0000000000
--- a/frappe/patches/v7_2/update_communications.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- """
- in communication move feedback details to content
- remove Guest None from sender full name
- setup feedback request trigger's is_manual field
- """
- return
diff --git a/frappe/patches/v7_2/update_feedback_request.py b/frappe/patches/v7_2/update_feedback_request.py
deleted file mode 100644
index 11e9eb8e92..0000000000
--- a/frappe/patches/v7_2/update_feedback_request.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- """
- rename feedback request documents,
- update the feedback request and save the rating and communication
- reference in Feedback Request document
- """
- return
diff --git a/frappe/patches/v8_0/__init__.py b/frappe/patches/v8_0/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/patches/v8_0/deprecate_integration_broker.py b/frappe/patches/v8_0/deprecate_integration_broker.py
deleted file mode 100644
index ad1a7d9571..0000000000
--- a/frappe/patches/v8_0/deprecate_integration_broker.py
+++ /dev/null
@@ -1,51 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-from frappe.integrations.utils import create_payment_gateway
-
-def execute():
- setup_enabled_integrations()
-
- for doctype in ["integration_request", "oauth_authorization_code", "oauth_bearer_token", "oauth_client"]:
- frappe.reload_doc('integrations', 'doctype', doctype)
-
- frappe.reload_doc("core", "doctype", "payment_gateway")
- update_doctype_module()
- create_payment_gateway_master_records()
-
- for doctype in ["Integration Service", "Integration Service Parameter"]:
- frappe.delete_doc("DocType", doctype)
-
- if not frappe.db.get_value("DocType", {"module": "Integration Broker"}, "name"):
- frappe.delete_doc("Module Def", "Integration Broker")
-
-def setup_enabled_integrations():
- if not frappe.db.exists("DocType", "Integration Service"):
- return
-
- for service in frappe.get_all("Integration Service",
- filters={"enabled": 1, "service": ('in', ("Dropbox", "LDAP"))}, fields=["name"]):
-
- doctype = "{0} Settings".format(service.name)
- frappe.db.set_value(doctype, doctype, 'enabled', 1)
-
-def update_doctype_module():
- frappe.db.sql("""update tabDocType set module='Integrations'
- where name in ('Integration Request', 'Oauth Authorization Code',
- 'Oauth Bearer Token', 'Oauth Client') """)
-
- frappe.db.sql(""" update tabDocType set module='Core' where name = 'Payment Gateway'""")
-
-def create_payment_gateway_master_records():
- for payment_gateway in ["Razorpay", "PayPal"]:
- doctype = "{0} Settings".format(payment_gateway)
- doc = frappe.get_doc(doctype)
- doc_meta = frappe.get_meta(doctype)
- all_mandatory_fields_has_value = True
-
- for d in doc_meta.fields:
- if d.reqd and not doc.get(d.fieldname):
- all_mandatory_fields_has_value = False
- break
-
- if all_mandatory_fields_has_value:
- create_payment_gateway(payment_gateway)
diff --git a/frappe/patches/v8_0/drop_in_dialog.py b/frappe/patches/v8_0/drop_in_dialog.py
deleted file mode 100644
index 231d757f26..0000000000
--- a/frappe/patches/v8_0/drop_in_dialog.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- if frappe.db.has_column('DocType', 'in_dialog'):
- frappe.db.sql('alter table tabDocType drop column in_dialog')
- frappe.clear_cache(doctype="DocType")
\ No newline at end of file
diff --git a/frappe/patches/v8_0/drop_is_custom_from_docperm.py b/frappe/patches/v8_0/drop_is_custom_from_docperm.py
deleted file mode 100644
index 4530dcd2e0..0000000000
--- a/frappe/patches/v8_0/drop_is_custom_from_docperm.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doctype('DocPerm')
- if frappe.db.has_column('DocPerm', 'is_custom'):
- frappe.db.commit()
- frappe.db.sql('alter table `tabDocPerm` drop column is_custom')
\ No newline at end of file
diff --git a/frappe/patches/v8_0/drop_unwanted_indexes.py b/frappe/patches/v8_0/drop_unwanted_indexes.py
deleted file mode 100644
index fc66ed43fd..0000000000
--- a/frappe/patches/v8_0/drop_unwanted_indexes.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# Copyright (c) 2017, Frappe and Contributors
-# License: GNU General Public License v3. See license.txt
-# -*- coding: utf-8 -*-
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- # communication
- unwanted_indexes = ["communication_date_index", "message_id_index", "modified_index",
- "creation_index", "reference_owner", "communication_date"]
-
- for k in unwanted_indexes:
- try:
- frappe.db.sql("drop index {0} on `tabCommunication`".format(k))
- except:
- pass
\ No newline at end of file
diff --git a/frappe/patches/v8_0/install_new_build_system_requirements.py b/frappe/patches/v8_0/install_new_build_system_requirements.py
deleted file mode 100644
index 536c2fcfb3..0000000000
--- a/frappe/patches/v8_0/install_new_build_system_requirements.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from __future__ import print_function, unicode_literals
-from subprocess import Popen, call, PIPE
-
-def execute():
- # update nodejs version if brew exists
- p = Popen(['which', 'brew'], stdout=PIPE, stderr=PIPE)
- output, err = p.communicate()
- if output:
- call(['brew', 'upgrade', 'node'])
- else:
- print('Please update your NodeJS version')
-
- call([
- 'npm', 'install',
- 'babel-core',
- 'less',
- 'chokidar',
- 'babel-preset-es2015',
- 'babel-preset-es2016',
- 'babel-preset-es2017',
- 'babel-preset-babili'
- ])
\ No newline at end of file
diff --git a/frappe/patches/v8_0/newsletter_childtable_migrate.py b/frappe/patches/v8_0/newsletter_childtable_migrate.py
deleted file mode 100644
index f652b37f56..0000000000
--- a/frappe/patches/v8_0/newsletter_childtable_migrate.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# Copyright (c) 2017, Frappe and Contributors
-# License: GNU General Public License v3. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doc('email', 'doctype', 'newsletter_email_group')
- frappe.reload_doctype('Newsletter')
-
- if "email_group" not in frappe.db.get_table_columns("Newsletter"):
- return
-
- newsletters = frappe.get_all("Newsletter", fields=["name", "email_group"])
- for newsletter in newsletters:
- if newsletter.email_group:
- newsletter_doc = frappe.get_doc("Newsletter", newsletter.name)
- if not newsletter_doc.get("email_group"):
- newsletter_doc.append("email_group", {
- "email_group": newsletter.email_group,
- })
- newsletter_doc.flags.ignore_validate = True
- newsletter_doc.flags.ignore_mandatory = True
- newsletter_doc.save()
diff --git a/frappe/patches/v8_0/rename_listsettings_to_usersettings.py b/frappe/patches/v8_0/rename_listsettings_to_usersettings.py
deleted file mode 100644
index 584e4a1111..0000000000
--- a/frappe/patches/v8_0/rename_listsettings_to_usersettings.py
+++ /dev/null
@@ -1,46 +0,0 @@
-from __future__ import unicode_literals
-from frappe.model.utils.user_settings import update_user_settings
-import frappe, json
-from six import iteritems
-
-
-def execute():
- if frappe.db.table_exists("__ListSettings"):
- for us in frappe.db.sql('''select user, doctype, data from __ListSettings''', as_dict=True):
- try:
- data = json.loads(us.data)
- except:
- continue
-
- if 'List' in data:
- continue
-
- if 'limit' in data:
- data['page_length'] = data['limit']
- del data['limit']
-
- new_data = dict(List=data)
- new_data = json.dumps(new_data)
-
- frappe.db.sql('''update __ListSettings
- set data=%(new_data)s
- where user=%(user)s
- and doctype=%(doctype)s''',
- {'new_data': new_data, 'user': us.user, 'doctype': us.doctype})
-
- frappe.db.sql("RENAME TABLE __ListSettings to __UserSettings")
- else:
- if not frappe.db.table_exists("__UserSettings"):
- frappe.db.create_user_settings_table()
-
- for user in frappe.db.get_all('User', {'user_type': 'System User'}):
- defaults = frappe.defaults.get_defaults_for(user.name)
- for key, value in iteritems(defaults):
- if key.startswith('_list_settings:'):
- doctype = key.replace('_list_settings:', '')
- columns = ['`tab{1}`.`{0}`'.format(*c) for c in json.loads(value)]
- for col in columns:
- if "name as" in col:
- columns.remove(col)
-
- update_user_settings(doctype, {'fields': columns})
\ No newline at end of file
diff --git a/frappe/patches/v8_0/rename_page_role_to_has_role.py b/frappe/patches/v8_0/rename_page_role_to_has_role.py
deleted file mode 100644
index 9c610d857d..0000000000
--- a/frappe/patches/v8_0/rename_page_role_to_has_role.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- if not frappe.db.exists('DocType', 'Has Role'):
- frappe.rename_doc('DocType', 'Page Role', 'Has Role')
- reload_doc()
- set_ref_doctype_roles_to_report()
- copy_user_roles_to_has_roles()
- remove_doctypes()
-
-def reload_doc():
- frappe.reload_doc("core", 'doctype', "page")
- frappe.reload_doc("core", 'doctype', "report")
- frappe.reload_doc("core", 'doctype', "user")
- frappe.reload_doc("core", 'doctype', "has_role")
-
-def set_ref_doctype_roles_to_report():
- for data in frappe.get_all('Report', fields=["name"]):
- doc = frappe.get_doc('Report', data.name)
- if frappe.db.exists("DocType", doc.ref_doctype):
- try:
- doc.set_doctype_roles()
- for row in doc.roles:
- row.db_update()
- except:
- pass
-
-def copy_user_roles_to_has_roles():
- if frappe.db.exists('DocType', 'UserRole'):
- for data in frappe.get_all('User', fields = ["name"]):
- doc = frappe.get_doc('User', data.name)
- doc.set('roles',[])
- for args in frappe.get_all('UserRole', fields = ["role"],
- filters = {'parent': data.name, 'parenttype': 'User'}):
- doc.append('roles', {
- 'role': args.role
- })
- for role in doc.roles:
- role.db_update()
-
-def remove_doctypes():
- for doctype in ['UserRole', 'Event Role']:
- if frappe.db.exists('DocType', doctype):
- frappe.delete_doc('DocType', doctype)
\ No newline at end of file
diff --git a/frappe/patches/v8_0/rename_print_to_printing.py b/frappe/patches/v8_0/rename_print_to_printing.py
deleted file mode 100644
index ecdbc3f7be..0000000000
--- a/frappe/patches/v8_0/rename_print_to_printing.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- if frappe.db.exists('Module Def', 'Print'):
- frappe.reload_doc('printing', 'doctype', 'print_format')
- frappe.reload_doc('printing', 'doctype', 'print_settings')
- frappe.reload_doc('printing', 'doctype', 'print_heading')
- frappe.reload_doc('printing', 'doctype', 'letter_head')
- frappe.reload_doc('printing', 'page', 'print_format_builder')
- frappe.db.sql("""update `tabPrint Format` set module='Printing' where module='Print'""")
-
- frappe.delete_doc('Module Def', 'Print')
\ No newline at end of file
diff --git a/frappe/patches/v8_0/set_allow_traceback.py b/frappe/patches/v8_0/set_allow_traceback.py
deleted file mode 100644
index 3eceb3e29c..0000000000
--- a/frappe/patches/v8_0/set_allow_traceback.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doc('core', 'doctype', 'system_settings')
- frappe.db.sql("update `tabSystem Settings` set allow_error_traceback=1")
diff --git a/frappe/patches/v8_0/set_currency_field_precision.py b/frappe/patches/v8_0/set_currency_field_precision.py
deleted file mode 100644
index 89835c8c1e..0000000000
--- a/frappe/patches/v8_0/set_currency_field_precision.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe.utils import get_number_format_info
-
-def execute():
- frappe.reload_doc('core', 'doctype', 'system_settings', force=True)
- if not frappe.db.get_value("System Settings", None, "currency_precision"):
- default_currency = frappe.db.get_default("currency")
- number_format = frappe.db.get_value("Currency", default_currency, "number_format", cache=True) \
- or frappe.db.get_default("number_format")
- if number_format:
- precision = get_number_format_info(number_format)[2]
- else:
- precision = 2
-
- ss = frappe.get_doc("System Settings")
- ss.currency_precision = precision
- ss.flags.ignore_mandatory = True
- ss.save()
diff --git a/frappe/patches/v8_0/set_doctype_values_in_custom_role.py b/frappe/patches/v8_0/set_doctype_values_in_custom_role.py
deleted file mode 100644
index 58cdc4497d..0000000000
--- a/frappe/patches/v8_0/set_doctype_values_in_custom_role.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doctype('Custom Role')
-
- # set ref doctype in custom role for reports
- frappe.db.sql(""" update `tabCustom Role` set
- `tabCustom Role`.ref_doctype = (select ref_doctype from `tabReport` where name = `tabCustom Role`.report)
- where `tabCustom Role`.report is not null""")
diff --git a/frappe/patches/v8_0/set_user_permission_for_page_and_report.py b/frappe/patches/v8_0/set_user_permission_for_page_and_report.py
deleted file mode 100644
index 560ea46db2..0000000000
--- a/frappe/patches/v8_0/set_user_permission_for_page_and_report.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- if not frappe.db.exists('DocType', 'Custom Role'):
- frappe.reload_doc("core", 'doctype', "custom_role")
- set_user_permission_for_page_and_report()
-
- update_ref_doctype_in_custom_role()
-
-def update_ref_doctype_in_custom_role():
- frappe.reload_doc("core", 'doctype', "custom_role")
- frappe.db.sql("""update `tabCustom Role`
- set
- ref_doctype = (select ref_doctype from tabReport where name = `tabCustom Role`.report)
- where report is not null""")
-
-def set_user_permission_for_page_and_report():
- make_custom_roles_for_page_and_report()
-
-def make_custom_roles_for_page_and_report():
- for doctype in ['Page', 'Report']:
- for data in get_data(doctype):
- doc = frappe.get_doc(doctype, data.name)
- roles = get_roles(doctype, data, doc)
- make_custom_roles(doctype, doc.name, roles)
-
-def get_data(doctype):
- fields = ["name"] if doctype == 'Page' else ["name", "ref_doctype"]
- return frappe.get_all(doctype, fields = fields)
-
-def get_roles(doctype, data, doc):
- roles = []
- if doctype == 'Page':
- for d in doc.roles:
- if frappe.db.exists('Role', d.role):
- roles.append({'role': d.role})
- else:
- out = frappe.get_all('Custom DocPerm', fields='distinct role', filters=dict(parent = data.ref_doctype))
- for d in out:
- roles.append({'role': d.role})
- return roles
-
-def make_custom_roles(doctype, name, roles):
- field = doctype.lower()
-
- if roles:
- custom_permission = frappe.get_doc({
- 'doctype': 'Custom Role',
- field : name,
- 'roles' : roles
- }).insert()
diff --git a/frappe/patches/v8_0/setup_email_inbox.py b/frappe/patches/v8_0/setup_email_inbox.py
deleted file mode 100644
index 1bfe3b0b74..0000000000
--- a/frappe/patches/v8_0/setup_email_inbox.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from __future__ import unicode_literals
-import frappe, json
-from frappe.core.doctype.user.user import ask_pass_update, setup_user_email_inbox
-
-def execute():
- """
- depricate email inbox page if exists
- remove desktop icon for email inbox page if exists
- patch to remove Custom DocPerm for communication
- add user inbox child table entry for existing email account in not exists
- """
-
- if frappe.db.exists("Page", "email_inbox"):
- frappe.delete_doc("Page", "email_inbox")
-
- frappe.db.sql("""update `tabCustom DocPerm` set `write`=0, email=1 where parent='Communication'""")
-
- frappe.reload_doc("core", "doctype", "user_email")
- frappe.reload_doc("email", "doctype", "email_account")
-
- email_accounts = frappe.get_all("Email Account", filters={"enable_incoming": 1},
- fields=["name", "email_id", "awaiting_password", "enable_outgoing"])
-
- for email_account in email_accounts:
- setup_user_email_inbox(email_account.get("name"), email_account.get("awaiting_password"),
- email_account.get("email_id"), email_account.get("enabled_outgoing"))
diff --git a/frappe/patches/v8_0/update_gender_and_salutation.py b/frappe/patches/v8_0/update_gender_and_salutation.py
deleted file mode 100644
index bcd9d4cbd7..0000000000
--- a/frappe/patches/v8_0/update_gender_and_salutation.py
+++ /dev/null
@@ -1,15 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-
-from __future__ import unicode_literals
-import frappe
-from frappe.desk.page.setup_wizard.install_fixtures import update_genders, update_salutations
-
-def execute():
- frappe.db.set_value("DocType", "Contact", "module", "Contacts")
- frappe.db.set_value("DocType", "Address", "module", "Contacts")
- frappe.db.set_value("DocType", "Address Template", "module", "Contacts")
- frappe.reload_doc('contacts', 'doctype', 'gender')
- frappe.reload_doc('contacts', 'doctype', 'salutation')
-
- update_genders()
- update_salutations()
\ No newline at end of file
diff --git a/frappe/patches/v8_0/update_global_search_table.py b/frappe/patches/v8_0/update_global_search_table.py
deleted file mode 100644
index 3c0a70155b..0000000000
--- a/frappe/patches/v8_0/update_global_search_table.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- if not 'published' in frappe.db.get_db_table_columns('__global_search'):
- frappe.db.sql('''alter table __global_search
- add column `title` varchar(140)''')
-
- frappe.db.sql('''alter table __global_search
- add column `route` varchar(140)''')
-
- frappe.db.sql('''alter table __global_search
- add column `published` int(1) not null default 0''')
diff --git a/frappe/patches/v8_0/update_published_in_global_search.py b/frappe/patches/v8_0/update_published_in_global_search.py
deleted file mode 100644
index a378f24732..0000000000
--- a/frappe/patches/v8_0/update_published_in_global_search.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- from frappe.website.router import get_doctypes_with_web_view
- from frappe.utils.global_search import rebuild_for_doctype
-
- for doctype in get_doctypes_with_web_view():
- try:
- rebuild_for_doctype(doctype)
- except frappe.DoesNotExistError:
- pass
diff --git a/frappe/patches/v8_0/update_records_in_global_search.py b/frappe/patches/v8_0/update_records_in_global_search.py
deleted file mode 100644
index dafa1e76d3..0000000000
--- a/frappe/patches/v8_0/update_records_in_global_search.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-from frappe.utils.global_search import get_doctypes_with_global_search, rebuild_for_doctype
-from frappe.utils import update_progress_bar
-
-def execute():
- frappe.cache().delete_value('doctypes_with_global_search')
- doctypes_with_global_search = get_doctypes_with_global_search(with_child_tables=False)
-
- for i, doctype in enumerate(doctypes_with_global_search):
- update_progress_bar("Updating Global Search", i, len(doctypes_with_global_search))
- rebuild_for_doctype(doctype)
diff --git a/frappe/patches/v8_1/__init__.py b/frappe/patches/v8_1/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/patches/v8_1/delete_custom_docperm_if_doctype_not_exists.py b/frappe/patches/v8_1/delete_custom_docperm_if_doctype_not_exists.py
deleted file mode 100644
index 92b54edfd4..0000000000
--- a/frappe/patches/v8_1/delete_custom_docperm_if_doctype_not_exists.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.db.sql("""delete from `tabCustom DocPerm`
- where parent not in ( select name from `tabDocType` )
- """)
diff --git a/frappe/patches/v8_1/enable_allow_error_traceback_in_system_settings.py b/frappe/patches/v8_1/enable_allow_error_traceback_in_system_settings.py
deleted file mode 100644
index 9bd9757a86..0000000000
--- a/frappe/patches/v8_1/enable_allow_error_traceback_in_system_settings.py
+++ /dev/null
@@ -1,15 +0,0 @@
-# Copyright (c) 2017, Frappe and Contributors
-# License: GNU General Public License v3. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- """ enable the allow_enable_traceback property in system settings """
-
- frappe.reload_doc("core", "doctype", "system_settings")
- doc = frappe.get_doc("System Settings", "System Settings")
- doc.allow_error_traceback = 1
- doc.flags.ignore_permissions=True
- doc.flags.ignore_mandatory=True
- doc.save()
\ No newline at end of file
diff --git a/frappe/patches/v8_1/update_format_options_in_auto_email_report.py b/frappe/patches/v8_1/update_format_options_in_auto_email_report.py
deleted file mode 100644
index 56609780cb..0000000000
--- a/frappe/patches/v8_1/update_format_options_in_auto_email_report.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright (c) 2017, Frappe and Contributors
-# License: GNU General Public License v3. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- """ change the XLS option as XLSX in the auto email report """
-
- frappe.reload_doc("email", "doctype", "auto_email_report")
-
- auto_email_list = frappe.get_all("Auto Email Report", filters={"format": "XLS"})
- for auto_email in auto_email_list:
- frappe.db.set_value("Auto Email Report", auto_email.name, "format", "XLSX")
diff --git a/frappe/patches/v8_10/__init__.py b/frappe/patches/v8_10/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/patches/v8_10/delete_static_web_page_from_global_search.py b/frappe/patches/v8_10/delete_static_web_page_from_global_search.py
deleted file mode 100644
index 336562c157..0000000000
--- a/frappe/patches/v8_10/delete_static_web_page_from_global_search.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.db.sql("""delete from `__global_search` where doctype='Static Web Page'""");
\ No newline at end of file
diff --git a/frappe/patches/v8_5/__init__.py b/frappe/patches/v8_5/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/patches/v8_5/delete_email_group_member_with_invalid_emails.py b/frappe/patches/v8_5/delete_email_group_member_with_invalid_emails.py
deleted file mode 100644
index 89a9a7a1b9..0000000000
--- a/frappe/patches/v8_5/delete_email_group_member_with_invalid_emails.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Copyright (c) 2017, Frappe and Contributors
-# License: GNU General Public License v3. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe.utils import validate_email_address
-
-def execute():
- ''' update/delete the email group member with the wrong email '''
-
- email_group_members = frappe.get_all("Email Group Member", fields=["name", "email"])
- for member in email_group_members:
- validated_email = validate_email_address(member.email)
- if (validated_email==member.email):
- pass
- else:
- try:
- frappe.db.set_value("Email Group Member", member.name, "email", validated_email)
- except Exception:
- frappe.delete_doc(doctype="Email Group Member", name=member.name, force=1, ignore_permissions=True)
\ No newline at end of file
diff --git a/frappe/patches/v8_5/patch_event_colors.py b/frappe/patches/v8_5/patch_event_colors.py
deleted file mode 100644
index 8ac7aec238..0000000000
--- a/frappe/patches/v8_5/patch_event_colors.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
-
- if not frappe.db.sql("SHOW COLUMNS FROM `tabEvent` LIKE 'color';"):
- return
-
- colors = ['red', 'green', 'blue', 'yellow', 'skyblue', 'orange']
- hex_colors = ['#ffc4c4', '#cef6d1', '#d2d2ff', '#fffacd', '#d2f1ff', '#ffd2c2']
-
- def get_hex_for_color(color):
- index = colors.index(color)
- return hex_colors[index]
-
- query = '''
- update tabEvent
- set color='{hex}'
- where color='{color}'
- '''
-
- for color in colors:
- frappe.db.sql(query.format(color=color, hex=get_hex_for_color(color)))
-
- frappe.db.commit()
diff --git a/frappe/patches/v8_7/__init__.py b/frappe/patches/v8_7/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/patches/v8_x/__init__.py b/frappe/patches/v8_x/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/patches/v8_x/update_user_permission.py b/frappe/patches/v8_x/update_user_permission.py
deleted file mode 100644
index 693b87c974..0000000000
--- a/frappe/patches/v8_x/update_user_permission.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doc('core', 'doctype', 'user_permission')
- frappe.delete_doc('core', 'page', 'user-permissions')
- for perm in frappe.db.sql("""
- select
- name, parent, defkey, defvalue
- from
- tabDefaultValue
- where
- parent not in ('__default', '__global')
- and
- substr(defkey,1,1)!='_'
- and
- parenttype='User Permission'
- """, as_dict=True):
- if frappe.db.exists(perm.defkey, perm.defvalue) and frappe.db.exists('User', perm.parent):
- frappe.get_doc(dict(
- doctype='User Permission',
- user=perm.parent,
- allow=perm.defkey,
- for_value=perm.defvalue,
- apply_for_all_roles=0,
- )).insert(ignore_permissions = True)
-
- frappe.db.sql('delete from tabDefaultValue where parenttype="User Permission"')
diff --git a/frappe/patches/v9_1/__init__.py b/frappe/patches/v9_1/__init__.py
deleted file mode 100644
index baffc48825..0000000000
--- a/frappe/patches/v9_1/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from __future__ import unicode_literals
diff --git a/frappe/patches/v9_1/add_sms_sender_name_as_parameters.py b/frappe/patches/v9_1/add_sms_sender_name_as_parameters.py
deleted file mode 100644
index 9d7c0f003f..0000000000
--- a/frappe/patches/v9_1/add_sms_sender_name_as_parameters.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (c) 2017, Frappe and Contributors
-# License: GNU General Public License v3. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- frappe.reload_doc("core", "doctype", "sms_parameter")
- sms_sender_name = frappe.db.get_single_value("SMS Settings", "sms_sender_name")
- if sms_sender_name:
- frappe.reload_doc("core", "doctype", "sms_settings")
- sms_settings = frappe.get_doc("SMS Settings")
- sms_settings.append("parameters", {
- "parameter": "sender_name",
- "value": sms_sender_name
- })
- sms_settings.flags.ignore_mandatory = True
- sms_settings.flags.ignore_permissions = True
- sms_settings.save()
diff --git a/frappe/patches/v9_1/move_feed_to_activity_log.py b/frappe/patches/v9_1/move_feed_to_activity_log.py
deleted file mode 100644
index db46b4e419..0000000000
--- a/frappe/patches/v9_1/move_feed_to_activity_log.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-from frappe.utils.background_jobs import enqueue
-
-def execute():
- comm_records_count = frappe.db.count("Communication", {"comment_type": "Updated"})
- if comm_records_count > 100000:
- enqueue(method=move_data_from_communication_to_activity_log, queue='short', now=True)
- else:
- move_data_from_communication_to_activity_log()
-
-def move_data_from_communication_to_activity_log():
- frappe.reload_doc("core", "doctype", "communication")
- frappe.reload_doc("core", "doctype", "activity_log")
-
- frappe.db.sql("""insert into `tabActivity Log` (name, owner, modified, creation, status, communication_date,
- reference_doctype, reference_name, timeline_doctype, timeline_name, link_doctype, link_name, subject, content, user)
- select name, owner, modified, creation, status, communication_date,
- reference_doctype, reference_name, timeline_doctype, timeline_name, link_doctype, link_name, subject, content, user
- from `tabCommunication`
- where comment_type = 'Updated'""")
-
- frappe.db.sql("""delete from `tabCommunication` where comment_type = 'Updated'""")
- frappe.delete_doc("DocType", "Authentication Log")
\ No newline at end of file
diff --git a/frappe/patches/v9_1/resave_domain_settings.py b/frappe/patches/v9_1/resave_domain_settings.py
deleted file mode 100644
index 1e54ad3aa5..0000000000
--- a/frappe/patches/v9_1/resave_domain_settings.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- domain_settings = frappe.get_doc('Domain Settings')
- active_domains = [d.domain for d in domain_settings.active_domains]
- try:
- for d in ('Education', 'Healthcare', 'Hospitality'):
- if d in active_domains and frappe.db.exists('Domain', d):
- domain = frappe.get_doc('Domain', d)
- domain.setup_domain()
- except frappe.LinkValidationError:
- pass
diff --git a/frappe/patches/v9_1/revert_domain_settings.py b/frappe/patches/v9_1/revert_domain_settings.py
deleted file mode 100644
index a14b48dae6..0000000000
--- a/frappe/patches/v9_1/revert_domain_settings.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-
-def execute():
- domain_settings = frappe.get_doc('Domain Settings')
- active_domains = [d.domain for d in domain_settings.active_domains]
-
- for domain_name in ('Education', 'Healthcare', 'Hospitality'):
- if frappe.db.exists('Domain', domain_name) and domain_name not in active_domains:
- domain = frappe.get_doc('Domain', domain_name)
- domain.remove_domain()
\ No newline at end of file
diff --git a/frappe/permissions.py b/frappe/permissions.py
index 19f101aab5..af7bc3b602 100644
--- a/frappe/permissions.py
+++ b/frappe/permissions.py
@@ -1,18 +1,16 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
+import copy
-from __future__ import unicode_literals, print_function
-from six import string_types
-import frappe, copy, json
+import frappe
+import frappe.share
from frappe import _, msgprint
from frappe.utils import cint
-import frappe.share
+
+
rights = ("select", "read", "write", "create", "delete", "submit", "cancel", "amend",
"print", "email", "report", "import", "export", "set_user_permissions", "share")
-# TODO:
-
-# optimize: meta.get_link_map (check if the doctype link exists for the given permission type)
def check_admin_or_system_manager(user=None):
if not user: user = frappe.session.user
@@ -58,7 +56,7 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None, ra
meta = frappe.get_meta(doctype)
if doc:
- if isinstance(doc, string_types):
+ if isinstance(doc, str):
doc = frappe.get_doc(meta.name, doc)
perm = get_doc_permissions(doc, user=user, ptype=ptype).get(ptype)
if not perm: push_perm_check_log(_('User {0} does not have access to this document').format(frappe.bold(user)))
@@ -159,7 +157,7 @@ def get_role_permissions(doctype_meta, user=None, is_owner=None):
}
}
"""
- if isinstance(doctype_meta, string_types):
+ if isinstance(doctype_meta, str):
doctype_meta = frappe.get_meta(doctype_meta) # assuming doctype name was passed
if not user: user = frappe.session.user
@@ -303,7 +301,7 @@ def has_controller_permissions(doc, ptype, user=None):
if not methods:
return None
- for method in methods:
+ for method in reversed(methods):
controller_permission = frappe.call(frappe.get_attr(method), doc=doc, ptype=ptype, user=user)
if controller_permission is not None:
return controller_permission
@@ -312,7 +310,7 @@ def has_controller_permissions(doc, ptype, user=None):
return None
def get_doctypes_with_read():
- return list(set([p.parent if type(p.parent) == str else p.parent.encode('UTF8') for p in get_valid_perms()]))
+ return list({p.parent if type(p.parent) == str else p.parent.encode('UTF8') for p in get_valid_perms()})
def get_valid_perms(doctype=None, user=None):
'''Get valid permissions for the current user from DocPerm and Custom DocPerm'''
@@ -520,8 +518,7 @@ def reset_perms(doctype):
"""Reset permissions for given doctype."""
from frappe.desk.notifications import delete_notification_count_for
delete_notification_count_for(doctype)
-
- frappe.db.sql("""delete from `tabCustom DocPerm` where parent=%s""", doctype)
+ frappe.db.delete("Custom DocPerm", {"parent": doctype})
def get_linked_doctypes(dt):
return list(set([dt] + [d.options for d in
@@ -534,7 +531,7 @@ def get_linked_doctypes(dt):
def get_doc_name(doc):
if not doc: return None
- return doc if isinstance(doc, string_types) else doc.name
+ return doc if isinstance(doc, str) else doc.name
def allow_everything():
'''
diff --git a/frappe/printing/doctype/letter_head/letter_head.py b/frappe/printing/doctype/letter_head/letter_head.py
index 3a3b14faad..948be60b88 100644
--- a/frappe/printing/doctype/letter_head/letter_head.py
+++ b/frappe/printing/doctype/letter_head/letter_head.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import frappe
from frappe.utils import is_image
from frappe.model.document import Document
@@ -19,7 +18,7 @@ class LetterHead(Document):
def validate_disabled_and_default(self):
if self.disabled and self.is_default:
frappe.throw(_("Letter Head cannot be both disabled and default"))
-
+
if not self.is_default and not self.disabled:
if not frappe.db.exists('Letter Head', dict(is_default=1)):
self.is_default = 1
diff --git a/frappe/printing/doctype/letter_head/test_letter_head.py b/frappe/printing/doctype/letter_head/test_letter_head.py
index b69e9924ea..96dfc68705 100644
--- a/frappe/printing/doctype/letter_head/test_letter_head.py
+++ b/frappe/printing/doctype/letter_head/test_letter_head.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
diff --git a/frappe/printing/doctype/print_format/print_format.py b/frappe/printing/doctype/print_format/print_format.py
index 1c11f2d519..5d4ff92fe2 100644
--- a/frappe/printing/doctype/print_format/print_format.py
+++ b/frappe/printing/doctype/print_format/print_format.py
@@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
import frappe.utils
import json
diff --git a/frappe/printing/doctype/print_format/test_print_format.py b/frappe/printing/doctype/print_format/test_print_format.py
index 121916ae5f..e65eb0183f 100644
--- a/frappe/printing/doctype/print_format/test_print_format.py
+++ b/frappe/printing/doctype/print_format/test_print_format.py
@@ -1,7 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-from __future__ import unicode_literals, print_function
-
import frappe
import unittest
import re
diff --git a/frappe/printing/doctype/print_heading/print_heading.py b/frappe/printing/doctype/print_heading/print_heading.py
index 1bb3e52dd5..f9955c019d 100644
--- a/frappe/printing/doctype/print_heading/print_heading.py
+++ b/frappe/printing/doctype/print_heading/print_heading.py
@@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
diff --git a/frappe/printing/doctype/print_heading/test_print_heading.py b/frappe/printing/doctype/print_heading/test_print_heading.py
index 1a6435e783..ce99cde607 100644
--- a/frappe/printing/doctype/print_heading/test_print_heading.py
+++ b/frappe/printing/doctype/print_heading/test_print_heading.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
diff --git a/frappe/printing/doctype/print_settings/print_settings.json b/frappe/printing/doctype/print_settings/print_settings.json
index d64cb4c6d3..31962be050 100644
--- a/frappe/printing/doctype/print_settings/print_settings.json
+++ b/frappe/printing/doctype/print_settings/print_settings.json
@@ -148,7 +148,7 @@
"label": "Print Style"
},
{
- "default": "Modern",
+ "default": "Redesign",
"fieldname": "print_style",
"fieldtype": "Link",
"in_list_view": 1,
@@ -183,7 +183,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2020-10-22 23:42:09.471022",
+ "modified": "2021-02-15 14:16:18.474254",
"modified_by": "Administrator",
"module": "Printing",
"name": "Print Settings",
diff --git a/frappe/printing/doctype/print_settings/print_settings.py b/frappe/printing/doctype/print_settings/print_settings.py
index cf6a71a8ac..610c083097 100644
--- a/frappe/printing/doctype/print_settings/print_settings.py
+++ b/frappe/printing/doctype/print_settings/print_settings.py
@@ -2,7 +2,6 @@
# Copyright (c) 2018, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import cint
diff --git a/frappe/printing/doctype/print_settings/test_print_settings.py b/frappe/printing/doctype/print_settings/test_print_settings.py
index b8ad70a681..d1dec861b2 100644
--- a/frappe/printing/doctype/print_settings/test_print_settings.py
+++ b/frappe/printing/doctype/print_settings/test_print_settings.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import unittest
class TestPrintSettings(unittest.TestCase):
diff --git a/frappe/printing/doctype/print_style/print_style.py b/frappe/printing/doctype/print_style/print_style.py
index 310babd5df..a91786795c 100644
--- a/frappe/printing/doctype/print_style/print_style.py
+++ b/frappe/printing/doctype/print_style/print_style.py
@@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
diff --git a/frappe/printing/doctype/print_style/test_print_style.py b/frappe/printing/doctype/print_style/test_print_style.py
index cee57f8826..b717b23df8 100644
--- a/frappe/printing/doctype/print_style/test_print_style.py
+++ b/frappe/printing/doctype/print_style/test_print_style.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
-from __future__ import unicode_literals
-
import frappe
import unittest
diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js
index 233bbe0ce7..ca2a340661 100644
--- a/frappe/printing/page/print/print.js
+++ b/frappe/printing/page/print/print.js
@@ -113,22 +113,20 @@ frappe.ui.form.PrintView = class {
},
).$input;
- this.letterhead_selector = this.add_sidebar_item(
+ this.letterhead_selector_df = this.add_sidebar_item(
{
- fieldtype: 'Select',
+ fieldtype: 'Autocomplete',
fieldname: 'letterhead',
label: __('Select Letterhead'),
- options: [
- this.get_default_option_for_select(__('Select Letterhead')),
- __('No Letterhead')
- ],
+ placeholder: __('Select Letterhead'),
+ options: [__('No Letterhead')],
change: () => this.preview(),
default: this.print_settings.with_letterhead
? __('No Letterhead')
: __('Select Letterhead')
},
- ).$input;
-
+ );
+ this.letterhead_selector = this.letterhead_selector_df.$input;
this.sidebar_dynamic_section = $(
``
).appendTo(this.sidebar);
@@ -336,23 +334,19 @@ frappe.ui.form.PrintView = class {
}
set_letterhead_options() {
- let letterhead_options = [
- this.get_default_option_for_select(__('Select Letterhead')),
- __('No Letterhead')
- ];
+ let letterhead_options = [__('No Letterhead')];
let default_letterhead;
let doc_letterhead = this.frm.doc.letter_head;
return frappe.db
- .get_list('Letter Head', { fields: ['name', 'is_default'] })
+ .get_list('Letter Head', { fields: ['name', 'is_default'], limit: 0 })
.then((letterheads) => {
- this.letterhead_selector.empty();
letterheads.map((letterhead) => {
if (letterhead.is_default) default_letterhead = letterhead.name;
return letterhead_options.push(letterhead.name);
});
- this.letterhead_selector.add_options(letterhead_options);
+ this.letterhead_selector_df.set_data(letterhead_options);
let selected_letterhead = doc_letterhead || default_letterhead;
if (selected_letterhead)
this.letterhead_selector.val(selected_letterhead);
@@ -409,19 +403,14 @@ frappe.ui.form.PrintView = class {
setup_print_format_dom(out, $print_format) {
this.print_wrapper.find('.print-format-skeleton').remove();
let base_url = frappe.urllib.get_base_url();
- let print_css = frappe.assets.bundled_asset('print.bundle.css');
+ let print_css = frappe.assets.bundled_asset('print.bundle.css', frappe.utils.is_rtl(this.lang_code));
+ this.$print_format_body.find('html').attr('dir', frappe.utils.is_rtl(this.lang_code) ? 'rtl': 'ltr');
+ this.$print_format_body.find('html').attr('lang', this.lang_code);
this.$print_format_body.find('head').html(
`
`
);
- if (frappe.utils.is_rtl(this.lang_code)) {
- let rtl_css = frappe.assets.bundled_asset('frappe-rtl.bundle.css');
- this.$print_format_body.find('head').append(
- ``
- );
- }
-
this.$print_format_body.find('body').html(
`${out.html}
`
);
diff --git a/frappe/public/build.json b/frappe/public/build.json
deleted file mode 100755
index 942871ee9b..0000000000
--- a/frappe/public/build.json
+++ /dev/null
@@ -1,299 +0,0 @@
-{
- "css/frappe-web-b4.css": "public/scss/website.scss",
- "css/frappe-chat-web.css": [
- "public/css/font-awesome.css",
- "public/css/octicons/octicons.css",
- "public/less/chat.less"
- ],
- "concat:js/moment-bundle.min.js": [
- "node_modules/moment/min/moment-with-locales.min.js",
- "node_modules/moment-timezone/builds/moment-timezone-with-data.min.js"
- ],
- "js/chat.js": "public/js/frappe/chat.js",
- "js/frappe-recorder.min.js": "public/js/frappe/recorder/recorder.js",
- "js/checkout.min.js": "public/js/integrations/razorpay.js",
- "js/frappe-web.min.js": [
- "public/js/frappe/class.js",
- "public/js/frappe/polyfill.js",
- "public/js/lib/md5.min.js",
- "public/js/frappe/provide.js",
- "public/js/frappe/format.js",
- "public/js/frappe/utils/number_format.js",
- "public/js/frappe/utils/utils.js",
- "public/js/frappe/utils/common.js",
- "public/js/frappe/ui/messages.js",
- "public/js/frappe/translate.js",
- "public/js/frappe/utils/pretty_date.js",
- "public/js/frappe/microtemplate.js",
- "public/js/frappe/query_string.js",
-
- "public/js/frappe/upload.js",
-
- "public/js/frappe/model/meta.js",
- "public/js/frappe/model/model.js",
- "public/js/frappe/model/perm.js",
-
- "website/js/website.js",
- "public/js/frappe/socketio_client.js"
- ],
- "js/bootstrap-4-web.min.js": "website/js/bootstrap-4.js",
- "js/control.min.js": [
- "node_modules/air-datepicker/dist/js/datepicker.min.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.cs.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.da.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.de.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.en.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.es.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.fi.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.fr.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.hu.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.nl.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.pl.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.pt-BR.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.pt.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.ro.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.sk.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.zh.js",
- "public/js/frappe/ui/capture.js",
- "public/js/frappe/form/controls/control.js"
- ],
- "js/dialog.min.js": [
- "public/js/frappe/dom.js",
- "public/js/frappe/form/formatters.js",
- "public/js/frappe/form/layout.js",
- "public/js/frappe/ui/field_group.js",
- "public/js/frappe/form/link_selector.js",
- "public/js/frappe/form/multi_select_dialog.js",
- "public/js/frappe/ui/dialog.js"
- ],
- "css/desk.min.css": [
- "public/js/lib/leaflet/leaflet.css",
- "public/js/lib/leaflet/leaflet.draw.css",
- "public/js/lib/leaflet/L.Control.Locate.css",
- "public/js/lib/leaflet/easy-button.css",
- "public/css/font-awesome.css",
- "public/css/octicons/octicons.css",
- "public/less/desk.less",
- "public/less/module.less",
- "public/less/mobile.less",
- "public/less/controls.less",
- "public/less/chat.less",
- "public/css/fonts/inter/inter.css",
- "node_modules/frappe-charts/dist/frappe-charts.min.css",
- "node_modules/plyr/dist/plyr.css",
- "public/scss/desk.scss"
- ],
- "css/frappe-rtl.css": [
- "public/css/bootstrap-rtl.css",
- "public/css/desk-rtl.css",
- "public/css/report-rtl.css"
- ],
- "css/printview.css": [
- "public/css/bootstrap.css",
- "public/scss/print.scss"
- ],
- "concat:js/libs.min.js": [
- "public/js/lib/Sortable.min.js",
- "public/js/lib/jquery/jquery.hotkeys.js",
- "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js",
- "node_modules/vue/dist/vue.min.js",
- "node_modules/moment/min/moment-with-locales.min.js",
- "node_modules/moment-timezone/builds/moment-timezone-with-data.min.js",
- "node_modules/socket.io-client/dist/socket.io.slim.js",
- "node_modules/localforage/dist/localforage.min.js",
- "public/js/lib/jSignature.min.js",
- "public/js/lib/leaflet/leaflet.js",
- "public/js/lib/leaflet/leaflet.draw.js",
- "public/js/lib/leaflet/L.Control.Locate.js",
- "public/js/lib/leaflet/easy-button.js"
- ],
- "js/desk.min.js": [
- "public/js/frappe/translate.js",
- "public/js/frappe/class.js",
- "public/js/frappe/polyfill.js",
- "public/js/frappe/provide.js",
- "public/js/frappe/assets.js",
- "public/js/frappe/format.js",
- "public/js/frappe/form/formatters.js",
- "public/js/frappe/dom.js",
- "public/js/frappe/ui/messages.js",
- "public/js/frappe/ui/keyboard.js",
- "public/js/frappe/ui/colors.js",
- "public/js/frappe/ui/sidebar.js",
- "public/js/frappe/ui/link_preview.js",
-
- "public/js/frappe/request.js",
- "public/js/frappe/socketio_client.js",
- "public/js/frappe/utils/utils.js",
- "public/js/frappe/event_emitter.js",
- "public/js/frappe/router.js",
- "public/js/frappe/router_history.js",
- "public/js/frappe/defaults.js",
- "public/js/frappe/roles_editor.js",
- "public/js/frappe/module_editor.js",
- "public/js/frappe/microtemplate.js",
-
- "public/js/frappe/ui/page.html",
- "public/js/frappe/ui/page.js",
- "public/js/frappe/ui/slides.js",
- "public/js/frappe/ui/onboarding_dialog.js",
- "public/js/frappe/ui/find.js",
- "public/js/frappe/ui/iconbar.js",
- "public/js/frappe/form/layout.js",
- "public/js/frappe/ui/field_group.js",
- "public/js/frappe/form/link_selector.js",
- "public/js/frappe/form/multi_select_dialog.js",
- "public/js/frappe/ui/dialog.js",
- "public/js/frappe/ui/capture.js",
- "public/js/frappe/ui/app_icon.js",
- "public/js/frappe/ui/theme_switcher.js",
-
- "public/js/frappe/model/model.js",
- "public/js/frappe/db.js",
- "public/js/frappe/model/meta.js",
- "public/js/frappe/model/sync.js",
- "public/js/frappe/model/create_new.js",
- "public/js/frappe/model/perm.js",
- "public/js/frappe/model/workflow.js",
- "public/js/frappe/model/user_settings.js",
-
- "public/js/lib/md5.min.js",
- "public/js/frappe/utils/user.js",
- "public/js/frappe/utils/common.js",
- "public/js/frappe/utils/urllib.js",
- "public/js/frappe/utils/pretty_date.js",
- "public/js/frappe/utils/tools.js",
- "public/js/frappe/utils/datetime.js",
- "public/js/frappe/utils/number_format.js",
- "public/js/frappe/utils/help.js",
- "public/js/frappe/utils/help_links.js",
- "public/js/frappe/utils/address_and_contact.js",
- "public/js/frappe/utils/preview_email.js",
- "public/js/frappe/utils/file_manager.js",
-
- "public/js/frappe/upload.js",
- "public/js/frappe/ui/tree.js",
-
- "public/js/frappe/views/container.js",
- "public/js/frappe/views/breadcrumbs.js",
- "public/js/frappe/views/factory.js",
- "public/js/frappe/views/pageview.js",
-
- "public/js/frappe/ui/toolbar/awesome_bar.js",
- "public/js/frappe/ui/toolbar/energy_points_notifications.js",
- "public/js/frappe/ui/notifications/notifications.js",
- "public/js/frappe/ui/toolbar/search.js",
- "public/js/frappe/ui/toolbar/tag_utils.js",
- "public/js/frappe/ui/toolbar/search.html",
- "public/js/frappe/ui/toolbar/search_utils.js",
- "public/js/frappe/ui/toolbar/about.js",
- "public/js/frappe/ui/toolbar/navbar.html",
- "public/js/frappe/ui/toolbar/toolbar.js",
- "public/js/frappe/ui/toolbar/notifications.js",
- "public/js/frappe/views/communication.js",
- "public/js/frappe/views/translation_manager.js",
- "public/js/frappe/views/workspace/workspace.js",
-
- "public/js/frappe/widgets/widget_group.js",
-
- "public/js/frappe/ui/sort_selector.html",
- "public/js/frappe/ui/sort_selector.js",
-
- "public/js/frappe/change_log.html",
- "public/js/frappe/ui/workspace_loading_skeleton.html",
- "public/js/frappe/desk.js",
- "public/js/frappe/query_string.js",
-
- "public/js/frappe/ui/comment.js",
-
- "public/js/frappe/chat.js",
- "public/js/frappe/utils/energy_point_utils.js",
- "public/js/frappe/utils/dashboard_utils.js",
- "public/js/frappe/ui/chart.js",
- "public/js/frappe/ui/datatable.js",
- "public/js/frappe/ui/driver.js",
- "public/js/frappe/ui/plyr.js",
- "public/js/frappe/barcode_scanner/index.js"
- ],
- "js/form.min.js": [
- "public/js/frappe/form/templates/**.html",
- "public/js/frappe/form/controls/control.js",
- "public/js/frappe/views/formview.js",
- "public/js/frappe/form/form.js",
- "public/js/frappe/meta_tag.js"
- ],
- "js/list.min.js": [
- "public/js/frappe/ui/listing.html",
-
- "public/js/frappe/model/indicator.js",
- "public/js/frappe/ui/filters/filter.js",
- "public/js/frappe/ui/filters/filter_list.js",
- "public/js/frappe/ui/filters/field_select.js",
- "public/js/frappe/ui/filters/edit_filter.html",
- "public/js/frappe/ui/tags.js",
- "public/js/frappe/ui/tag_editor.js",
- "public/js/frappe/ui/like.js",
- "public/js/frappe/ui/liked_by.html",
- "public/html/print_template.html",
-
- "public/js/frappe/list/base_list.js",
- "public/js/frappe/list/list_view.js",
- "public/js/frappe/list/list_factory.js",
-
- "public/js/frappe/list/list_view_select.js",
- "public/js/frappe/list/list_sidebar.js",
- "public/js/frappe/list/list_sidebar.html",
- "public/js/frappe/list/list_sidebar_stat.html",
- "public/js/frappe/list/list_sidebar_group_by.js",
- "public/js/frappe/list/list_view_permission_restrictions.html",
-
- "public/js/frappe/views/gantt/gantt_view.js",
- "public/js/frappe/views/calendar/calendar.js",
- "public/js/frappe/views/dashboard/dashboard_view.js",
- "public/js/frappe/views/image/image_view.js",
- "public/js/frappe/views/map/map_view.js",
- "public/js/frappe/views/kanban/kanban_view.js",
- "public/js/frappe/views/inbox/inbox_view.js",
- "public/js/frappe/views/file/file_view.js",
-
- "public/js/frappe/views/treeview.js",
- "public/js/frappe/views/interaction.js",
-
- "public/js/frappe/views/image/image_view_item_row.html",
- "public/js/frappe/views/image/photoswipe_dom.html",
-
- "public/js/frappe/views/kanban/kanban_board.html",
- "public/js/frappe/views/kanban/kanban_column.html",
- "public/js/frappe/views/kanban/kanban_card.html"
- ],
- "css/report.min.css": [
- "node_modules/frappe-datatable/dist/frappe-datatable.css",
- "public/css/tree_grid.css"
- ],
- "js/report.min.js": [
- "public/js/lib/clusterize.min.js",
- "public/js/frappe/views/reports/report_factory.js",
- "public/js/frappe/views/reports/report_view.js",
- "public/js/frappe/views/reports/query_report.js",
- "public/js/frappe/views/reports/print_grid.html",
- "public/js/frappe/views/reports/print_tree.html",
- "public/js/frappe/ui/group_by/group_by.html",
- "public/js/frappe/ui/group_by/group_by.js",
- "public/js/frappe/views/reports/report_utils.js"
- ],
- "js/web_form.min.js": [
- "public/js/frappe/utils/datetime.js",
- "public/js/frappe/web_form/webform_script.js"
- ],
- "css/web_form.css": [
- "website/css/web_form.css",
- "public/css/octicons/octicons.css",
- "public/scss/controls.scss",
- "node_modules/frappe-datatable/dist/frappe-datatable.css"
- ],
- "css/email.css": "public/scss/email.scss",
- "js/barcode_scanner.min.js": "public/js/frappe/barcode_scanner/quagga.js",
- "js/user_profile_controller.min.js": "desk/page/user_profile/user_profile_controller.js",
- "css/login.css": "public/scss/login.scss",
- "js/data_import_tools.min.js": "public/js/frappe/data_import/index.js"
-}
diff --git a/frappe/public/css/bootstrap-rtl.css b/frappe/public/css/bootstrap-rtl.css
deleted file mode 100644
index 5dfa46c055..0000000000
--- a/frappe/public/css/bootstrap-rtl.css
+++ /dev/null
@@ -1,1476 +0,0 @@
-/*******************************************************************************
- * bootstrap-rtl (version 3.3.4)
- * Author: Morteza Ansarinia (http://github.com/morteza)
- * Created on: August 13,2015
- * Project: bootstrap-rtl
- * Copyright: Unlicensed Public Domain
- *******************************************************************************/
-
-html {
- direction: rtl;
-}
-body {
- direction: rtl;
-}
-.flip.text-left {
- text-align: right;
-}
-.flip.text-right {
- text-align: left;
-}
-.list-unstyled {
- padding-right: 0;
- padding-left: initial;
-}
-.list-inline {
- padding-right: 0;
- padding-left: initial;
- margin-right: -5px;
- margin-left: 0;
-}
-dd {
- margin-right: 0;
- margin-left: initial;
-}
-@media (min-width: 768px) {
- .dl-horizontal dt {
- float: right;
- clear: right;
- text-align: left;
- }
- .dl-horizontal dd {
- margin-right: 180px;
- margin-left: 0;
- }
-}
-blockquote {
- border-right: 5px solid #eeeeee;
- border-left: 0;
-}
-.blockquote-reverse,
-blockquote.pull-left {
- padding-left: 15px;
- padding-right: 0;
- border-left: 5px solid #eeeeee;
- border-right: 0;
- text-align: left;
-}
-.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {
- position: relative;
- min-height: 1px;
- padding-left: 15px;
- padding-right: 15px;
-}
-.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {
- float: right;
-}
-.col-xs-12 {
- width: 100%;
-}
-.col-xs-11 {
- width: 91.66666667%;
-}
-.col-xs-10 {
- width: 83.33333333%;
-}
-.col-xs-9 {
- width: 75%;
-}
-.col-xs-8 {
- width: 66.66666667%;
-}
-.col-xs-7 {
- width: 58.33333333%;
-}
-.col-xs-6 {
- width: 50%;
-}
-.col-xs-5 {
- width: 41.66666667%;
-}
-.col-xs-4 {
- width: 33.33333333%;
-}
-.col-xs-3 {
- width: 25%;
-}
-.col-xs-2 {
- width: 16.66666667%;
-}
-.col-xs-1 {
- width: 8.33333333%;
-}
-.col-xs-pull-12 {
- left: 100%;
- right: auto;
-}
-.col-xs-pull-11 {
- left: 91.66666667%;
- right: auto;
-}
-.col-xs-pull-10 {
- left: 83.33333333%;
- right: auto;
-}
-.col-xs-pull-9 {
- left: 75%;
- right: auto;
-}
-.col-xs-pull-8 {
- left: 66.66666667%;
- right: auto;
-}
-.col-xs-pull-7 {
- left: 58.33333333%;
- right: auto;
-}
-.col-xs-pull-6 {
- left: 50%;
- right: auto;
-}
-.col-xs-pull-5 {
- left: 41.66666667%;
- right: auto;
-}
-.col-xs-pull-4 {
- left: 33.33333333%;
- right: auto;
-}
-.col-xs-pull-3 {
- left: 25%;
- right: auto;
-}
-.col-xs-pull-2 {
- left: 16.66666667%;
- right: auto;
-}
-.col-xs-pull-1 {
- left: 8.33333333%;
- right: auto;
-}
-.col-xs-pull-0 {
- left: auto;
- right: auto;
-}
-.col-xs-push-12 {
- right: 100%;
- left: 0;
-}
-.col-xs-push-11 {
- right: 91.66666667%;
- left: 0;
-}
-.col-xs-push-10 {
- right: 83.33333333%;
- left: 0;
-}
-.col-xs-push-9 {
- right: 75%;
- left: 0;
-}
-.col-xs-push-8 {
- right: 66.66666667%;
- left: 0;
-}
-.col-xs-push-7 {
- right: 58.33333333%;
- left: 0;
-}
-.col-xs-push-6 {
- right: 50%;
- left: 0;
-}
-.col-xs-push-5 {
- right: 41.66666667%;
- left: 0;
-}
-.col-xs-push-4 {
- right: 33.33333333%;
- left: 0;
-}
-.col-xs-push-3 {
- right: 25%;
- left: 0;
-}
-.col-xs-push-2 {
- right: 16.66666667%;
- left: 0;
-}
-.col-xs-push-1 {
- right: 8.33333333%;
- left: 0;
-}
-.col-xs-push-0 {
- right: auto;
- left: 0;
-}
-.col-xs-offset-12 {
- margin-right: 100%;
- margin-left: 0;
-}
-.col-xs-offset-11 {
- margin-right: 91.66666667%;
- margin-left: 0;
-}
-.col-xs-offset-10 {
- margin-right: 83.33333333%;
- margin-left: 0;
-}
-.col-xs-offset-9 {
- margin-right: 75%;
- margin-left: 0;
-}
-.col-xs-offset-8 {
- margin-right: 66.66666667%;
- margin-left: 0;
-}
-.col-xs-offset-7 {
- margin-right: 58.33333333%;
- margin-left: 0;
-}
-.col-xs-offset-6 {
- margin-right: 50%;
- margin-left: 0;
-}
-.col-xs-offset-5 {
- margin-right: 41.66666667%;
- margin-left: 0;
-}
-.col-xs-offset-4 {
- margin-right: 33.33333333%;
- margin-left: 0;
-}
-.col-xs-offset-3 {
- margin-right: 25%;
- margin-left: 0;
-}
-.col-xs-offset-2 {
- margin-right: 16.66666667%;
- margin-left: 0;
-}
-.col-xs-offset-1 {
- margin-right: 8.33333333%;
- margin-left: 0;
-}
-.col-xs-offset-0 {
- margin-right: 0%;
- margin-left: 0;
-}
-@media (min-width: 768px) {
- .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {
- float: right;
- }
- .col-sm-12 {
- width: 100%;
- }
- .col-sm-11 {
- width: 91.66666667%;
- }
- .col-sm-10 {
- width: 83.33333333%;
- }
- .col-sm-9 {
- width: 75%;
- }
- .col-sm-8 {
- width: 66.66666667%;
- }
- .col-sm-7 {
- width: 58.33333333%;
- }
- .col-sm-6 {
- width: 50%;
- }
- .col-sm-5 {
- width: 41.66666667%;
- }
- .col-sm-4 {
- width: 33.33333333%;
- }
- .col-sm-3 {
- width: 25%;
- }
- .col-sm-2 {
- width: 16.66666667%;
- }
- .col-sm-1 {
- width: 8.33333333%;
- }
- .col-sm-pull-12 {
- left: 100%;
- right: auto;
- }
- .col-sm-pull-11 {
- left: 91.66666667%;
- right: auto;
- }
- .col-sm-pull-10 {
- left: 83.33333333%;
- right: auto;
- }
- .col-sm-pull-9 {
- left: 75%;
- right: auto;
- }
- .col-sm-pull-8 {
- left: 66.66666667%;
- right: auto;
- }
- .col-sm-pull-7 {
- left: 58.33333333%;
- right: auto;
- }
- .col-sm-pull-6 {
- left: 50%;
- right: auto;
- }
- .col-sm-pull-5 {
- left: 41.66666667%;
- right: auto;
- }
- .col-sm-pull-4 {
- left: 33.33333333%;
- right: auto;
- }
- .col-sm-pull-3 {
- left: 25%;
- right: auto;
- }
- .col-sm-pull-2 {
- left: 16.66666667%;
- right: auto;
- }
- .col-sm-pull-1 {
- left: 8.33333333%;
- right: auto;
- }
- .col-sm-pull-0 {
- left: auto;
- right: auto;
- }
- .col-sm-push-12 {
- right: 100%;
- left: 0;
- }
- .col-sm-push-11 {
- right: 91.66666667%;
- left: 0;
- }
- .col-sm-push-10 {
- right: 83.33333333%;
- left: 0;
- }
- .col-sm-push-9 {
- right: 75%;
- left: 0;
- }
- .col-sm-push-8 {
- right: 66.66666667%;
- left: 0;
- }
- .col-sm-push-7 {
- right: 58.33333333%;
- left: 0;
- }
- .col-sm-push-6 {
- right: 50%;
- left: 0;
- }
- .col-sm-push-5 {
- right: 41.66666667%;
- left: 0;
- }
- .col-sm-push-4 {
- right: 33.33333333%;
- left: 0;
- }
- .col-sm-push-3 {
- right: 25%;
- left: 0;
- }
- .col-sm-push-2 {
- right: 16.66666667%;
- left: 0;
- }
- .col-sm-push-1 {
- right: 8.33333333%;
- left: 0;
- }
- .col-sm-push-0 {
- right: auto;
- left: 0;
- }
- .col-sm-offset-12 {
- margin-right: 100%;
- margin-left: 0;
- }
- .col-sm-offset-11 {
- margin-right: 91.66666667%;
- margin-left: 0;
- }
- .col-sm-offset-10 {
- margin-right: 83.33333333%;
- margin-left: 0;
- }
- .col-sm-offset-9 {
- margin-right: 75%;
- margin-left: 0;
- }
- .col-sm-offset-8 {
- margin-right: 66.66666667%;
- margin-left: 0;
- }
- .col-sm-offset-7 {
- margin-right: 58.33333333%;
- margin-left: 0;
- }
- .col-sm-offset-6 {
- margin-right: 50%;
- margin-left: 0;
- }
- .col-sm-offset-5 {
- margin-right: 41.66666667%;
- margin-left: 0;
- }
- .col-sm-offset-4 {
- margin-right: 33.33333333%;
- margin-left: 0;
- }
- .col-sm-offset-3 {
- margin-right: 25%;
- margin-left: 0;
- }
- .col-sm-offset-2 {
- margin-right: 16.66666667%;
- margin-left: 0;
- }
- .col-sm-offset-1 {
- margin-right: 8.33333333%;
- margin-left: 0;
- }
- .col-sm-offset-0 {
- margin-right: 0%;
- margin-left: 0;
- }
-}
-@media (min-width: 992px) {
- .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {
- float: right;
- }
- .col-md-12 {
- width: 100%;
- }
- .col-md-11 {
- width: 91.66666667%;
- }
- .col-md-10 {
- width: 83.33333333%;
- }
- .col-md-9 {
- width: 75%;
- }
- .col-md-8 {
- width: 66.66666667%;
- }
- .col-md-7 {
- width: 58.33333333%;
- }
- .col-md-6 {
- width: 50%;
- }
- .col-md-5 {
- width: 41.66666667%;
- }
- .col-md-4 {
- width: 33.33333333%;
- }
- .col-md-3 {
- width: 25%;
- }
- .col-md-2 {
- width: 16.66666667%;
- }
- .col-md-1 {
- width: 8.33333333%;
- }
- .col-md-pull-12 {
- left: 100%;
- right: auto;
- }
- .col-md-pull-11 {
- left: 91.66666667%;
- right: auto;
- }
- .col-md-pull-10 {
- left: 83.33333333%;
- right: auto;
- }
- .col-md-pull-9 {
- left: 75%;
- right: auto;
- }
- .col-md-pull-8 {
- left: 66.66666667%;
- right: auto;
- }
- .col-md-pull-7 {
- left: 58.33333333%;
- right: auto;
- }
- .col-md-pull-6 {
- left: 50%;
- right: auto;
- }
- .col-md-pull-5 {
- left: 41.66666667%;
- right: auto;
- }
- .col-md-pull-4 {
- left: 33.33333333%;
- right: auto;
- }
- .col-md-pull-3 {
- left: 25%;
- right: auto;
- }
- .col-md-pull-2 {
- left: 16.66666667%;
- right: auto;
- }
- .col-md-pull-1 {
- left: 8.33333333%;
- right: auto;
- }
- .col-md-pull-0 {
- left: auto;
- right: auto;
- }
- .col-md-push-12 {
- right: 100%;
- left: 0;
- }
- .col-md-push-11 {
- right: 91.66666667%;
- left: 0;
- }
- .col-md-push-10 {
- right: 83.33333333%;
- left: 0;
- }
- .col-md-push-9 {
- right: 75%;
- left: 0;
- }
- .col-md-push-8 {
- right: 66.66666667%;
- left: 0;
- }
- .col-md-push-7 {
- right: 58.33333333%;
- left: 0;
- }
- .col-md-push-6 {
- right: 50%;
- left: 0;
- }
- .col-md-push-5 {
- right: 41.66666667%;
- left: 0;
- }
- .col-md-push-4 {
- right: 33.33333333%;
- left: 0;
- }
- .col-md-push-3 {
- right: 25%;
- left: 0;
- }
- .col-md-push-2 {
- right: 16.66666667%;
- left: 0;
- }
- .col-md-push-1 {
- right: 8.33333333%;
- left: 0;
- }
- .col-md-push-0 {
- right: auto;
- left: 0;
- }
- .col-md-offset-12 {
- margin-right: 100%;
- margin-left: 0;
- }
- .col-md-offset-11 {
- margin-right: 91.66666667%;
- margin-left: 0;
- }
- .col-md-offset-10 {
- margin-right: 83.33333333%;
- margin-left: 0;
- }
- .col-md-offset-9 {
- margin-right: 75%;
- margin-left: 0;
- }
- .col-md-offset-8 {
- margin-right: 66.66666667%;
- margin-left: 0;
- }
- .col-md-offset-7 {
- margin-right: 58.33333333%;
- margin-left: 0;
- }
- .col-md-offset-6 {
- margin-right: 50%;
- margin-left: 0;
- }
- .col-md-offset-5 {
- margin-right: 41.66666667%;
- margin-left: 0;
- }
- .col-md-offset-4 {
- margin-right: 33.33333333%;
- margin-left: 0;
- }
- .col-md-offset-3 {
- margin-right: 25%;
- margin-left: 0;
- }
- .col-md-offset-2 {
- margin-right: 16.66666667%;
- margin-left: 0;
- }
- .col-md-offset-1 {
- margin-right: 8.33333333%;
- margin-left: 0;
- }
- .col-md-offset-0 {
- margin-right: 0%;
- margin-left: 0;
- }
-}
-@media (min-width: 1200px) {
- .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {
- float: right;
- }
- .col-lg-12 {
- width: 100%;
- }
- .col-lg-11 {
- width: 91.66666667%;
- }
- .col-lg-10 {
- width: 83.33333333%;
- }
- .col-lg-9 {
- width: 75%;
- }
- .col-lg-8 {
- width: 66.66666667%;
- }
- .col-lg-7 {
- width: 58.33333333%;
- }
- .col-lg-6 {
- width: 50%;
- }
- .col-lg-5 {
- width: 41.66666667%;
- }
- .col-lg-4 {
- width: 33.33333333%;
- }
- .col-lg-3 {
- width: 25%;
- }
- .col-lg-2 {
- width: 16.66666667%;
- }
- .col-lg-1 {
- width: 8.33333333%;
- }
- .col-lg-pull-12 {
- left: 100%;
- right: auto;
- }
- .col-lg-pull-11 {
- left: 91.66666667%;
- right: auto;
- }
- .col-lg-pull-10 {
- left: 83.33333333%;
- right: auto;
- }
- .col-lg-pull-9 {
- left: 75%;
- right: auto;
- }
- .col-lg-pull-8 {
- left: 66.66666667%;
- right: auto;
- }
- .col-lg-pull-7 {
- left: 58.33333333%;
- right: auto;
- }
- .col-lg-pull-6 {
- left: 50%;
- right: auto;
- }
- .col-lg-pull-5 {
- left: 41.66666667%;
- right: auto;
- }
- .col-lg-pull-4 {
- left: 33.33333333%;
- right: auto;
- }
- .col-lg-pull-3 {
- left: 25%;
- right: auto;
- }
- .col-lg-pull-2 {
- left: 16.66666667%;
- right: auto;
- }
- .col-lg-pull-1 {
- left: 8.33333333%;
- right: auto;
- }
- .col-lg-pull-0 {
- left: auto;
- right: auto;
- }
- .col-lg-push-12 {
- right: 100%;
- left: 0;
- }
- .col-lg-push-11 {
- right: 91.66666667%;
- left: 0;
- }
- .col-lg-push-10 {
- right: 83.33333333%;
- left: 0;
- }
- .col-lg-push-9 {
- right: 75%;
- left: 0;
- }
- .col-lg-push-8 {
- right: 66.66666667%;
- left: 0;
- }
- .col-lg-push-7 {
- right: 58.33333333%;
- left: 0;
- }
- .col-lg-push-6 {
- right: 50%;
- left: 0;
- }
- .col-lg-push-5 {
- right: 41.66666667%;
- left: 0;
- }
- .col-lg-push-4 {
- right: 33.33333333%;
- left: 0;
- }
- .col-lg-push-3 {
- right: 25%;
- left: 0;
- }
- .col-lg-push-2 {
- right: 16.66666667%;
- left: 0;
- }
- .col-lg-push-1 {
- right: 8.33333333%;
- left: 0;
- }
- .col-lg-push-0 {
- right: auto;
- left: 0;
- }
- .col-lg-offset-12 {
- margin-right: 100%;
- margin-left: 0;
- }
- .col-lg-offset-11 {
- margin-right: 91.66666667%;
- margin-left: 0;
- }
- .col-lg-offset-10 {
- margin-right: 83.33333333%;
- margin-left: 0;
- }
- .col-lg-offset-9 {
- margin-right: 75%;
- margin-left: 0;
- }
- .col-lg-offset-8 {
- margin-right: 66.66666667%;
- margin-left: 0;
- }
- .col-lg-offset-7 {
- margin-right: 58.33333333%;
- margin-left: 0;
- }
- .col-lg-offset-6 {
- margin-right: 50%;
- margin-left: 0;
- }
- .col-lg-offset-5 {
- margin-right: 41.66666667%;
- margin-left: 0;
- }
- .col-lg-offset-4 {
- margin-right: 33.33333333%;
- margin-left: 0;
- }
- .col-lg-offset-3 {
- margin-right: 25%;
- margin-left: 0;
- }
- .col-lg-offset-2 {
- margin-right: 16.66666667%;
- margin-left: 0;
- }
- .col-lg-offset-1 {
- margin-right: 8.33333333%;
- margin-left: 0;
- }
- .col-lg-offset-0 {
- margin-right: 0%;
- margin-left: 0;
- }
-}
-caption {
- text-align: right;
-}
-th {
- text-align: right;
-}
-@media screen and (max-width: 767px) {
- .table-responsive > .table-bordered {
- border: 0;
- }
- .table-responsive > .table-bordered > thead > tr > th:first-child,
- .table-responsive > .table-bordered > tbody > tr > th:first-child,
- .table-responsive > .table-bordered > tfoot > tr > th:first-child,
- .table-responsive > .table-bordered > thead > tr > td:first-child,
- .table-responsive > .table-bordered > tbody > tr > td:first-child,
- .table-responsive > .table-bordered > tfoot > tr > td:first-child {
- border-right: 0;
- border-left: initial;
- }
- .table-responsive > .table-bordered > thead > tr > th:last-child,
- .table-responsive > .table-bordered > tbody > tr > th:last-child,
- .table-responsive > .table-bordered > tfoot > tr > th:last-child,
- .table-responsive > .table-bordered > thead > tr > td:last-child,
- .table-responsive > .table-bordered > tbody > tr > td:last-child,
- .table-responsive > .table-bordered > tfoot > tr > td:last-child {
- border-left: 0;
- border-right: initial;
- }
-}
-.radio label,
-.checkbox label {
- padding-right: 20px;
- padding-left: initial;
-}
-.radio input[type="radio"],
-.radio-inline input[type="radio"],
-.checkbox input[type="checkbox"],
-.checkbox-inline input[type="checkbox"] {
- margin-right: -20px;
- margin-left: auto;
-}
-.radio-inline,
-.checkbox-inline {
- padding-right: 20px;
- padding-left: 0;
-}
-.radio-inline + .radio-inline,
-.checkbox-inline + .checkbox-inline {
- margin-right: 10px;
- margin-left: 0;
-}
-.has-feedback .form-control {
- padding-left: 42.5px;
- padding-right: 12px;
-}
-.form-control-feedback {
- left: 0;
- right: auto;
-}
-@media (min-width: 768px) {
- .form-inline label {
- padding-right: 0;
- padding-left: initial;
- }
- .form-inline .radio input[type="radio"],
- .form-inline .checkbox input[type="checkbox"] {
- margin-right: 0;
- margin-left: auto;
- }
-}
-@media (min-width: 768px) {
- .form-horizontal .control-label {
- text-align: left;
- }
-}
-.form-horizontal .has-feedback .form-control-feedback {
- left: 15px;
- right: auto;
-}
-.caret {
- margin-right: 2px;
- margin-left: 0;
-}
-.dropdown-menu {
- right: 0;
- left: auto;
- float: left;
- text-align: right;
-}
-.dropdown-menu.pull-right {
- left: 0;
- right: auto;
- float: right;
-}
-.dropdown-menu-right {
- left: auto;
- right: 0;
-}
-.dropdown-menu-left {
- left: 0;
- right: auto;
-}
-@media (min-width: 768px) {
- .navbar-right .dropdown-menu {
- left: auto;
- right: 0;
- }
- .navbar-right .dropdown-menu-left {
- left: 0;
- right: auto;
- }
-}
-.btn-group > .btn,
-.btn-group-vertical > .btn {
- float: right;
-}
-.btn-group .btn + .btn,
-.btn-group .btn + .btn-group,
-.btn-group .btn-group + .btn,
-.btn-group .btn-group + .btn-group {
- margin-right: -1px;
- margin-left: 0px;
-}
-.btn-toolbar {
- margin-right: -5px;
- margin-left: 0px;
-}
-.btn-toolbar .btn-group,
-.btn-toolbar .input-group {
- float: right;
-}
-.btn-toolbar > .btn,
-.btn-toolbar > .btn-group,
-.btn-toolbar > .input-group {
- margin-right: 5px;
- margin-left: 0px;
-}
-.btn-group > .btn:first-child {
- margin-right: 0;
-}
-.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {
- border-top-right-radius: 4px;
- border-bottom-right-radius: 4px;
- border-bottom-left-radius: 0;
- border-top-left-radius: 0;
-}
-.btn-group > .btn:last-child:not(:first-child),
-.btn-group > .dropdown-toggle:not(:first-child) {
- border-top-left-radius: 4px;
- border-bottom-left-radius: 4px;
- border-bottom-right-radius: 0;
- border-top-right-radius: 0;
-}
-.btn-group > .btn-group {
- float: right;
-}
-.btn-group.btn-group-justified > .btn,
-.btn-group.btn-group-justified > .btn-group {
- float: none;
-}
-.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {
- border-radius: 0;
-}
-.btn-group > .btn-group:first-child > .btn:last-child,
-.btn-group > .btn-group:first-child > .dropdown-toggle {
- border-top-right-radius: 4px;
- border-bottom-right-radius: 4px;
- border-bottom-left-radius: 0;
- border-top-left-radius: 0;
-}
-.btn-group > .btn-group:last-child > .btn:first-child {
- border-top-left-radius: 4px;
- border-bottom-left-radius: 4px;
- border-bottom-right-radius: 0;
- border-top-right-radius: 0;
-}
-.btn .caret {
- margin-right: 0;
-}
-.btn-group-vertical > .btn + .btn,
-.btn-group-vertical > .btn + .btn-group,
-.btn-group-vertical > .btn-group + .btn,
-.btn-group-vertical > .btn-group + .btn-group {
- margin-top: -1px;
- margin-right: 0;
-}
-.input-group .form-control {
- float: right;
-}
-.input-group .form-control:first-child,
-.input-group-addon:first-child,
-.input-group-btn:first-child > .btn,
-.input-group-btn:first-child > .btn-group > .btn,
-.input-group-btn:first-child > .dropdown-toggle,
-.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),
-.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {
- border-bottom-right-radius: 4px;
- border-top-right-radius: 4px;
- border-bottom-left-radius: 0;
- border-top-left-radius: 0;
-}
-.input-group-addon:first-child {
- border-left: 0px;
- border-right: 1px solid;
-}
-.input-group .form-control:last-child,
-.input-group-addon:last-child,
-.input-group-btn:last-child > .btn,
-.input-group-btn:last-child > .btn-group > .btn,
-.input-group-btn:last-child > .dropdown-toggle,
-.input-group-btn:first-child > .btn:not(:first-child),
-.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {
- border-bottom-left-radius: 4px;
- border-top-left-radius: 4px;
- border-bottom-right-radius: 0;
- border-top-right-radius: 0;
-}
-.input-group-addon:last-child {
- border-left-width: 1px;
- border-left-style: solid;
- border-right: 0px;
-}
-.input-group-btn > .btn + .btn {
- margin-right: -1px;
- margin-left: auto;
-}
-.input-group-btn:first-child > .btn,
-.input-group-btn:first-child > .btn-group {
- margin-left: -1px;
- margin-right: auto;
-}
-.input-group-btn:last-child > .btn,
-.input-group-btn:last-child > .btn-group {
- margin-right: -1px;
- margin-left: auto;
-}
-.nav {
- padding-right: 0;
- padding-left: initial;
-}
-.nav-tabs > li {
- float: right;
-}
-.nav-tabs > li > a {
- margin-left: auto;
- margin-right: -2px;
- border-radius: 4px 4px 0 0;
-}
-.nav-pills > li {
- float: right;
-}
-.nav-pills > li > a {
- border-radius: 4px;
-}
-.nav-pills > li + li {
- margin-right: 2px;
- margin-left: auto;
-}
-.nav-stacked > li {
- float: none;
-}
-.nav-stacked > li + li {
- margin-right: 0;
- margin-left: auto;
-}
-.nav-justified > .dropdown .dropdown-menu {
- right: auto;
-}
-.nav-tabs-justified > li > a {
- margin-left: 0;
- margin-right: auto;
-}
-@media (min-width: 768px) {
- .nav-tabs-justified > li > a {
- border-radius: 4px 4px 0 0;
- }
-}
-@media (min-width: 768px) {
- .navbar-header {
- float: right;
- }
-}
-.navbar-collapse {
- padding-right: 15px;
- padding-left: 15px;
-}
-.navbar-brand {
- float: right;
-}
-@media (min-width: 768px) {
- .navbar > .container .navbar-brand,
- .navbar > .container-fluid .navbar-brand {
- margin-right: -15px;
- margin-left: auto;
- }
-}
-.navbar-toggle {
- float: left;
- margin-left: 15px;
- margin-right: auto;
-}
-@media (max-width: 767px) {
- .navbar-nav .open .dropdown-menu > li > a,
- .navbar-nav .open .dropdown-menu .dropdown-header {
- padding: 5px 25px 5px 15px;
- }
-}
-@media (min-width: 768px) {
- .navbar-nav {
- float: right;
- }
- .navbar-nav > li {
- float: right;
- }
-}
-@media (min-width: 768px) {
- .navbar-left.flip {
- float: right !important;
- }
- .navbar-right:last-child {
- margin-left: -15px;
- margin-right: auto;
- }
- .navbar-right.flip {
- float: left !important;
- margin-left: -15px;
- margin-right: auto;
- }
- .navbar-right .dropdown-menu {
- left: 0;
- right: auto;
- }
-}
-@media (min-width: 768px) {
- .navbar-text {
- float: right;
- }
- .navbar-text.navbar-right:last-child {
- margin-left: 0;
- margin-right: auto;
- }
-}
-.pagination {
- padding-right: 0;
-}
-.pagination > li > a,
-.pagination > li > span {
- float: right;
- margin-right: -1px;
- margin-left: 0px;
-}
-.pagination > li:first-child > a,
-.pagination > li:first-child > span {
- margin-left: 0;
- border-bottom-right-radius: 4px;
- border-top-right-radius: 4px;
- border-bottom-left-radius: 0;
- border-top-left-radius: 0;
-}
-.pagination > li:last-child > a,
-.pagination > li:last-child > span {
- margin-right: -1px;
- border-bottom-left-radius: 4px;
- border-top-left-radius: 4px;
- border-bottom-right-radius: 0;
- border-top-right-radius: 0;
-}
-.pager {
- padding-right: 0;
- padding-left: initial;
-}
-.pager .next > a,
-.pager .next > span {
- float: left;
-}
-.pager .previous > a,
-.pager .previous > span {
- float: right;
-}
-.nav-pills > li > a > .badge {
- margin-left: 0px;
- margin-right: 3px;
-}
-.list-group-item > .badge {
- float: left;
-}
-.list-group-item > .badge + .badge {
- margin-left: 5px;
- margin-right: auto;
-}
-.alert-dismissable,
-.alert-dismissible {
- padding-left: 35px;
- padding-right: 15px;
-}
-.alert-dismissable .close,
-.alert-dismissible .close {
- right: auto;
- left: -21px;
-}
-.progress-bar {
- float: right;
-}
-.media > .pull-left {
- margin-right: 10px;
-}
-.media > .pull-left.flip {
- margin-right: 0;
- margin-left: 10px;
-}
-.media > .pull-right {
- margin-left: 10px;
-}
-.media > .pull-right.flip {
- margin-left: 0;
- margin-right: 10px;
-}
-.media-right,
-.media > .pull-right {
- padding-right: 10px;
- padding-left: initial;
-}
-.media-left,
-.media > .pull-left {
- padding-left: 10px;
- padding-right: initial;
-}
-.media-list {
- padding-right: 0;
- padding-left: initial;
- list-style: none;
-}
-.list-group {
- padding-right: 0;
- padding-left: initial;
-}
-.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,
-.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,
-.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,
-.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,
-.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,
-.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,
-.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,
-.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {
- border-top-right-radius: 3px;
- border-top-left-radius: 0;
-}
-.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,
-.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,
-.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,
-.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,
-.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,
-.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,
-.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,
-.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {
- border-top-left-radius: 3px;
- border-top-right-radius: 0;
-}
-.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,
-.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,
-.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,
-.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,
-.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,
-.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,
-.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,
-.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {
- border-bottom-left-radius: 3px;
- border-top-right-radius: 0;
-}
-.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,
-.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,
-.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,
-.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,
-.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,
-.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,
-.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,
-.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {
- border-bottom-right-radius: 3px;
- border-top-left-radius: 0;
-}
-.panel > .table-bordered > thead > tr > th:first-child,
-.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,
-.panel > .table-bordered > tbody > tr > th:first-child,
-.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,
-.panel > .table-bordered > tfoot > tr > th:first-child,
-.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,
-.panel > .table-bordered > thead > tr > td:first-child,
-.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,
-.panel > .table-bordered > tbody > tr > td:first-child,
-.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,
-.panel > .table-bordered > tfoot > tr > td:first-child,
-.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {
- border-right: 0;
- border-left: none;
-}
-.panel > .table-bordered > thead > tr > th:last-child,
-.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,
-.panel > .table-bordered > tbody > tr > th:last-child,
-.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,
-.panel > .table-bordered > tfoot > tr > th:last-child,
-.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,
-.panel > .table-bordered > thead > tr > td:last-child,
-.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,
-.panel > .table-bordered > tbody > tr > td:last-child,
-.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,
-.panel > .table-bordered > tfoot > tr > td:last-child,
-.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {
- border-right: none;
- border-left: 0;
-}
-.embed-responsive .embed-responsive-item,
-.embed-responsive iframe,
-.embed-responsive embed,
-.embed-responsive object {
- right: 0;
- left: auto;
-}
-.close {
- float: left;
-}
-.modal-footer {
- text-align: left;
-}
-.modal-footer.flip {
- text-align: right;
-}
-.modal-footer .btn + .btn {
- margin-left: auto;
- margin-right: 5px;
-}
-.modal-footer .btn-group .btn + .btn {
- margin-right: -1px;
- margin-left: auto;
-}
-.modal-footer .btn-block + .btn-block {
- margin-right: 0;
- margin-left: auto;
-}
-.popover {
- left: auto;
- text-align: right;
-}
-.popover.top > .arrow {
- right: 50%;
- left: auto;
- margin-right: -11px;
- margin-left: auto;
-}
-.popover.top > .arrow:after {
- margin-right: -10px;
- margin-left: auto;
-}
-.popover.bottom > .arrow {
- right: 50%;
- left: auto;
- margin-right: -11px;
- margin-left: auto;
-}
-.popover.bottom > .arrow:after {
- margin-right: -10px;
- margin-left: auto;
-}
-.carousel-control {
- right: 0;
- bottom: 0;
-}
-.carousel-control.left {
- right: auto;
- left: 0;
- background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, 0.5) 0%), color-stop(rgba(0, 0, 0, 0.0001) 100%));
- background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);
- background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);
- background-repeat: repeat-x;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);
-}
-.carousel-control.right {
- left: auto;
- right: 0;
- background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, 0.0001) 0%), color-stop(rgba(0, 0, 0, 0.5) 100%));
- background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);
- background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);
- background-repeat: repeat-x;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);
-}
-.carousel-control .icon-prev,
-.carousel-control .glyphicon-chevron-left {
- left: 50%;
- right: auto;
- margin-right: -10px;
-}
-.carousel-control .icon-next,
-.carousel-control .glyphicon-chevron-right {
- right: 50%;
- left: auto;
- margin-left: -10px;
-}
-.carousel-indicators {
- right: 50%;
- left: 0;
- margin-right: -30%;
- margin-left: 0;
- padding-left: 0;
-}
-@media screen and (min-width: 768px) {
- .carousel-control .glyphicon-chevron-left,
- .carousel-control .icon-prev {
- margin-left: 0;
- margin-right: -15px;
- }
- .carousel-control .glyphicon-chevron-right,
- .carousel-control .icon-next {
- margin-left: 0;
- margin-right: -15px;
- }
- .carousel-caption {
- left: 20%;
- right: 20%;
- padding-bottom: 30px;
- }
-}
-.pull-right.flip {
- float: left !important;
-}
-.pull-left.flip {
- float: right !important;
-}
-/*# sourceMappingURL=bootstrap-rtl.css.map */
\ No newline at end of file
diff --git a/frappe/public/css/desk-rtl.css b/frappe/public/css/desk-rtl.css
deleted file mode 100644
index a38f6864ff..0000000000
--- a/frappe/public/css/desk-rtl.css
+++ /dev/null
@@ -1,118 +0,0 @@
-.navbar .navbar-search-icon{
- right: auto;
- left: 24px;
-}
-.navbar > .container > .navbar-header{
- float: right !important;
-}
-body[data-sidebar="0"] .navbar-home {
- margin-left: auto !important;
- margin-right: 15px !important;
-}
-.navbar-desk ~ ul > li {
- float: right !important;
-}
-body.no-breadcrumbs .navbar .navbar-home:before {
- margin-right: auto;
- margin-left: 10px !important;
- -ms-transform:rotate(180deg); /* Internet Explorer 9 */
- -webkit-transform:rotate(180deg); /* Chrome, Safari, Opera */
- transform:rotate(180deg); /* Standard syntax */
-}
-.layout-side-section .overlay-sidebar {
- left: auto !important;
- right: 0 !important;
-}
-.layout-side-section .overlay-sidebar.opened {
- transform:translateX(0) !important;
-}
-.navbar-right {
- float: left !important;
-}
-#navbar-breadcrumbs > li > a:before {
- margin-right: auto;
- margin-left: 10px;
- top: 6px;
- -ms-transform:rotate(180deg); /* Internet Explorer 9 */
- -webkit-transform:rotate(180deg); /* Chrome, Safari, Opera */
- transform:rotate(180deg); /* Standard syntax */
-}
-.case-wrapper {
- float: right;
-}
-.link-btn {
- right: auto;
- left: 4px;
- transform:rotate(180deg); /* Rotate icon*/
-}
-.sidebar-menu .badge {
- right: auto;
- left: 0px;
-}
-.indicator::before {
- margin: 0 0 0 4px;
-}
-.pull-left {
- float: right !important;
-}
-.grid-row > .row .col:last-child {
- margin-right: auto;
- margin-left: -10px;
-}
-.text-right {
- text-align: left;
-}
-.list-row-head .octicon-heart {
- margin-right: auto;
- margin-left: 13px;
-}
-.list-id {
- margin-left: 7px !important;
-}
-.avatar-small .avatar-sm {
- margin-left: 5px;
- margin-right: auto;
-}
-.list-row-right .list-row-modified {
- margin-right: auto;
- margin-left: 9px;
-}
-.list-comment-count {
- text-align: right;
-}
-ul.tree-children {
- padding-right: 20px;
- padding-left: inherit !important;
-}
-.balance-area {
- float: left !important;
-}
-.tree.opened::before, .tree-node.opened::before, .tree:last-child::after, .tree-node:last-child::after {
- left: inherit !important;
- right: 8px;
-}
-.tree.opened > .tree-children > .tree-node > .tree-link::before, .tree-node.opened > .tree-children > .tree-node > .tree-link::before {
- left: inherit !important;
- right: -11px;
-}
-.tree:last-child::after, .tree-node:last-child::after {
- right: -13px !important;
-}
-.tree.opened::before {
- left: auto !important;
- right: 23px;
-}
-.results {
- direction: ltr;
-}
-.data-table {
- direction: ltr;
-}
-.section-header {
- direction: ltr;
-}
-
-.ql-editor {
- direction: rtl;
- text-align: right;
-}
\ No newline at end of file
diff --git a/frappe/public/css/report-rtl.css b/frappe/public/css/report-rtl.css
deleted file mode 100644
index 03e986c56b..0000000000
--- a/frappe/public/css/report-rtl.css
+++ /dev/null
@@ -1,15 +0,0 @@
-.grid-report {
- direction: ltr;
-}
-
-.page-form .awesomplete > ul {
- left: auto;
-}
-
-.chart_area{
- direction: ltr;
-}
-
-.grid-report .show-zero{
- direction: rtl;
-}
diff --git a/frappe/public/html/print_template.html b/frappe/public/html/print_template.html
index 721bec7fa7..e2ff9c9c76 100644
--- a/frappe/public/html/print_template.html
+++ b/frappe/public/html/print_template.html
@@ -1,5 +1,5 @@
-
+
@@ -7,7 +7,7 @@
{{ title }}
-
+
diff --git a/frappe/public/icons/social/google_drive.svg b/frappe/public/icons/social/google_drive.svg
new file mode 100644
index 0000000000..e1a9378f8d
--- /dev/null
+++ b/frappe/public/icons/social/google_drive.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frappe/public/icons/timeless/symbol-defs.svg b/frappe/public/icons/timeless/symbol-defs.svg
index 5e52336bfa..f216374526 100644
--- a/frappe/public/icons/timeless/symbol-defs.svg
+++ b/frappe/public/icons/timeless/symbol-defs.svg
@@ -1,4 +1,4 @@
-