');
+ $wrapper.append($doctype_select, $field_select);
+ field.$input_wrapper.append($wrapper);
+ $doctype_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 doctypes = frm.doc.fields
+ .filter(df => df.fieldtype == "Link")
+ .filter(df => df.options && df.fieldname != row.fieldname)
+ .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();
+ }
+ },
+
+ fieldtype: function(frm) {
+ frm.trigger("max_attachments");
}
-})
+});
+
+extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({frm: cur_frm}));
diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json
index 3024fb32a2..e67b78aef5 100644
--- a/frappe/core/doctype/doctype/doctype.json
+++ b/frappe/core/doctype/doctype/doctype.json
@@ -26,8 +26,10 @@
"fields_section_break",
"fields",
"sb1",
+ "naming_rule",
"autoname",
"name_case",
+ "allow_rename",
"column_break_15",
"description",
"documentation",
@@ -39,7 +41,6 @@
"column_break_23",
"hide_toolbar",
"allow_copy",
- "allow_rename",
"allow_import",
"allow_events_in_timeline",
"allow_auto_repeat",
@@ -71,6 +72,8 @@
"actions",
"links_section",
"links",
+ "document_states_section",
+ "states",
"web_view",
"has_web_view",
"allow_guest_to_view",
@@ -79,7 +82,8 @@
"is_published_field",
"website_search_field",
"advanced",
- "engine"
+ "engine",
+ "migration_hash"
],
"fields": [
{
@@ -149,7 +153,7 @@
"fieldtype": "Column Break"
},
{
- "default": "1",
+ "default": "0",
"depends_on": "eval:!doc.istable",
"description": "If enabled, changes to the document are tracked and shown in timeline",
"fieldname": "track_changes",
@@ -277,7 +281,7 @@
"oldfieldtype": "Check"
},
{
- "default": "0",
+ "default": "1",
"fieldname": "allow_rename",
"fieldtype": "Check",
"label": "Allow Rename",
@@ -556,6 +560,30 @@
"fieldtype": "Data",
"label": "Website Search Field"
},
+ {
+ "fieldname": "naming_rule",
+ "fieldtype": "Select",
+ "label": "Naming Rule",
+ "length": 40,
+ "options": "\nSet by user\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script"
+ },
+ {
+ "fieldname": "migration_hash",
+ "fieldtype": "Data",
+ "hidden": 1
+ },
+ {
+ "fieldname": "states",
+ "fieldtype": "Table",
+ "label": "States",
+ "options": "DocType State"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "document_states_section",
+ "fieldtype": "Section Break",
+ "label": "Document States"
+ },
{
"default": "0",
"fieldname": "show_title_field_in_link",
@@ -642,7 +670,7 @@
"link_fieldname": "reference_doctype"
}
],
- "modified": "2021-08-03 13:41:50.319555",
+ "modified": "2021-12-15 14:53:10.717788",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index d2f62d0a15..a6a81cb195 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
# imports - standard imports
import re, copy, os, shutil
@@ -23,6 +23,7 @@ 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
+from frappe.query_builder.functions import Concat
class InvalidFieldNameError(frappe.ValidationError): pass
class UniqueFieldnameError(frappe.ValidationError): pass
@@ -74,6 +75,7 @@ class DocType(Document):
self.make_repeatable()
self.validate_nestedset()
self.validate_website()
+ self.ensure_minimum_max_attachment_limit()
validate_links_table_fieldnames(self)
if not self.is_new():
@@ -86,10 +88,6 @@ class DocType(Document):
if self.default_print_format and not self.custom:
frappe.throw(_('Standard DocType cannot have default print format, use Customize Form'))
- if frappe.conf.get('developer_mode'):
- self.owner = 'Administrator'
- self.modified_by = 'Administrator'
-
def validate_field_name_conflicts(self):
"""Check if field names dont conflict with controller properties and methods"""
core_doctypes = [
@@ -176,7 +174,6 @@ class DocType(Document):
if self.is_virtual and self.custom:
frappe.throw(_("Not allowed to create custom Virtual DocType."), CannotCreateStandardDoctypeError)
-
if frappe.conf.get('developer_mode'):
self.owner = 'Administrator'
self.modified_by = 'Administrator'
@@ -250,6 +247,22 @@ class DocType(Document):
# clear website cache
clear_cache()
+ def ensure_minimum_max_attachment_limit(self):
+ """Ensure that max_attachments is *at least* bigger than number of attach fields."""
+ from frappe.model import attachment_fieldtypes
+
+
+ if not self.max_attachments:
+ return
+
+ total_attach_fields = len([d for d in self.fields if d.fieldtype in attachment_fieldtypes])
+ if total_attach_fields > self.max_attachments:
+ self.max_attachments = total_attach_fields
+ field_label = frappe.bold(self.meta.get_field("max_attachments").label)
+ frappe.msgprint(_("Number of attachment fields are more than {}, limit updated to {}.")
+ .format(field_label, total_attach_fields),
+ title=_("Insufficient attachment limit"), alert=True)
+
def change_modified_of_parent(self):
"""Change the timestamp of parent DocType if the current one is a child to clear caches."""
if frappe.flags.in_import:
@@ -257,7 +270,7 @@ class DocType(Document):
parent_list = frappe.db.get_all('DocField', 'parent',
dict(fieldtype=['in', frappe.model.table_fields], options=self.name))
for p in parent_list:
- frappe.db.sql('UPDATE `tabDocType` SET modified=%s WHERE `name`=%s', (now(), p.parent))
+ frappe.db.update("DocType", p.parent, {}, for_update=False)
def scrub_field_names(self):
"""Sluggify fieldnames if not set from Label."""
@@ -274,6 +287,8 @@ class DocType(Document):
d.fieldname = d.fieldname + '_section'
elif d.fieldtype=='Column Break':
d.fieldname = d.fieldname + '_column'
+ elif d.fieldtype=='Tab Break':
+ d.fieldname = d.fieldname + '_tab'
else:
d.fieldname = d.fieldtype.lower().replace(" ","_") + "_" + str(d.idx)
else:
@@ -312,9 +327,7 @@ class DocType(Document):
if allow_doctype_export:
self.export_doc()
self.make_controller_template()
-
- if self.has_web_view:
- self.set_base_class_for_controller()
+ self.set_base_class_for_controller()
# update index
if not self.custom:
@@ -352,23 +365,49 @@ class DocType(Document):
now=now, doctype=self.name)
def set_base_class_for_controller(self):
- '''Updates the controller class to subclass from `WebsiteGenertor`,
- if it is a subclass of `Document`'''
- controller_path = frappe.get_module_path(frappe.scrub(self.module),
- 'doctype', frappe.scrub(self.name), frappe.scrub(self.name) + '.py')
+ """If DocType.has_web_view has been changed, updates the controller class and import
+ from `WebsiteGenertor` to `Document` or viceversa"""
- with open(controller_path, 'r') as f:
+ if not self.has_value_changed("has_web_view"):
+ return
+
+ despaced_name = self.name.replace(" ", "_")
+ scrubbed_name = frappe.scrub(self.name)
+ scrubbed_module = frappe.scrub(self.module)
+ controller_path = frappe.get_module_path(
+ scrubbed_module, "doctype", scrubbed_name, f"{scrubbed_name}.py"
+ )
+
+ document_cls_tag = f"class {despaced_name}(Document)"
+ document_import_tag = "from frappe.model.document import Document"
+ website_generator_cls_tag = f"class {despaced_name}(WebsiteGenerator)"
+ website_generator_import_tag = "from frappe.website.generators.website_generator import WebsiteGenerator"
+
+ with open(controller_path) as f:
code = f.read()
+ updated_code = code
- class_string = '\nclass {0}(Document)'.format(self.name.replace(' ', ''))
- if '\nfrom frappe.model.document import Document' in code and class_string in code:
- code = code.replace('from frappe.model.document import Document',
- 'from frappe.website.website_generator import WebsiteGenerator')
- code = code.replace('class {0}(Document)'.format(self.name.replace(' ', '')),
- 'class {0}(WebsiteGenerator)'.format(self.name.replace(' ', '')))
+ is_website_generator_class = all([
+ website_generator_cls_tag in code,
+ website_generator_import_tag in code
+ ])
- with open(controller_path, 'w') as f:
- f.write(code)
+ if self.has_web_view and not is_website_generator_class:
+ updated_code = updated_code.replace(
+ document_import_tag, website_generator_import_tag
+ ).replace(
+ document_cls_tag, website_generator_cls_tag
+ )
+ elif not self.has_web_view and is_website_generator_class:
+ updated_code = updated_code.replace(
+ website_generator_import_tag, document_import_tag
+ ).replace(
+ website_generator_cls_tag, document_cls_tag
+ )
+
+ if updated_code != code:
+ with open(controller_path, "w") as f:
+ f.write(updated_code)
def run_module_method(self, method):
from frappe.modules import load_doctype_module
@@ -463,7 +502,7 @@ class DocType(Document):
return
# check if atleast 1 record exists
- if not (frappe.db.table_exists(self.name) and frappe.db.sql("select name from `tab{}` limit 1".format(self.name))):
+ if not (frappe.db.table_exists(self.name) and frappe.get_all(self.name, fields=["name"], limit=1, as_list=True)):
return
existing_property_setter = frappe.db.get_value("Property Setter", {"doc_type": self.name,
@@ -493,6 +532,9 @@ class DocType(Document):
# retain order of 'fields' table and change order in 'field_order'
docdict["field_order"] = [f.fieldname for f in self.fields]
+ if self.custom:
+ return
+
path = get_file_path(self.module, "DocType", self.name)
if os.path.exists(path):
try:
@@ -566,17 +608,17 @@ class DocType(Document):
def make_amendable(self):
"""If is_submittable is set, add amended_from docfields."""
if self.is_submittable:
- if not frappe.db.sql("""select name from tabDocField
- where fieldname = 'amended_from' and parent = %s""", self.name):
- self.append("fields", {
- "label": "Amended From",
- "fieldtype": "Link",
- "fieldname": "amended_from",
- "options": self.name,
- "read_only": 1,
- "print_hide": 1,
- "no_copy": 1
- })
+ docfield_exists = frappe.get_all("DocField", filters={"fieldname": "amended_from", "parent": self.name}, pluck="name", limit=1)
+ if not docfield_exists:
+ self.append("fields", {
+ "label": "Amended From",
+ "fieldtype": "Link",
+ "fieldname": "amended_from",
+ "options": self.name,
+ "read_only": 1,
+ "print_hide": 1,
+ "no_copy": 1
+ })
def make_repeatable(self):
"""If allow_auto_repeat is set, add auto_repeat custom field."""
@@ -701,12 +743,13 @@ def validate_series(dt, autoname=None, name=None):
and (not autoname.startswith('format:')):
prefix = autoname.split('.')[0]
- used_in = frappe.db.sql("""
- SELECT `name`
- FROM `tabDocType`
- WHERE `autoname` LIKE CONCAT(%s, '.%%')
- AND `name`!=%s
- """, (prefix, name))
+ doctype = frappe.qb.DocType("DocType")
+ used_in = (frappe.qb
+ .from_(doctype)
+ .select(doctype.name)
+ .where(doctype.autoname.like(Concat(prefix,".%")))
+ .where(doctype.name != name)
+ ).run()
if used_in:
frappe.throw(_("Series {0} already used in {1}").format(prefix, used_in[0][0]))
@@ -719,9 +762,22 @@ def validate_links_table_fieldnames(meta):
for index, link in enumerate(meta.links):
link_meta = frappe.get_meta(link.link_doctype)
if not link_meta.get_field(link.link_fieldname):
- message = _("Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype))
+ message = _("Document Links 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 = _("Document Links 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 = _("Document Links 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 = _("Document Links 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)
@@ -931,6 +987,9 @@ def validate_fields(meta):
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
@@ -1011,6 +1070,9 @@ def validate_fields(meta):
frappe.throw(_('Option {0} for field {1} is not a child table')
.format(frappe.bold(doctype), frappe.bold(docfield.fieldname)), title=_("Invalid Option"))
+ def check_max_height(docfield):
+ if getattr(docfield, 'max_height', None) and (docfield.max_height[-2:] not in ('px', 'em')):
+ frappe.throw('Max for {} height must be in px, em, rem'.format(frappe.bold(docfield.fieldname)))
fields = meta.get("fields")
fieldname_list = [d.fieldname for d in fields]
@@ -1044,6 +1106,7 @@ def validate_fields(meta):
scrub_options_in_select(d)
scrub_fetch_from(d)
validate_data_field_type(d)
+ check_max_height(d)
check_fold(fields)
check_search_fields(meta, fields)
@@ -1200,8 +1263,14 @@ def make_module_and_roles(doc, perm_fieldname="permissions"):
if ("tabModule Def" in frappe.db.get_tables()
and not frappe.db.exists("Module Def", doc.module)):
m = frappe.get_doc({"doctype": "Module Def", "module_name": doc.module})
- m.app_name = frappe.local.module_app[frappe.scrub(doc.module)]
+ if frappe.scrub(doc.module) in frappe.local.module_app:
+ m.app_name = frappe.local.module_app[frappe.scrub(doc.module)]
+ else:
+ m.app_name = 'frappe'
m.flags.ignore_mandatory = m.flags.ignore_permissions = True
+ if frappe.flags.package:
+ m.package = frappe.flags.package.name
+ m.custom = 1
m.insert()
default_roles = ["Administrator", "Guest", "All"]
diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py
index 1e1a01a685..4362a52c34 100644
--- a/frappe/core/doctype/doctype/test_doctype.py
+++ b/frappe/core/doctype/doctype/test_doctype.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
from frappe.core.doctype.doctype.doctype import (UniqueFieldnameError,
@@ -348,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)
@@ -371,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')
@@ -386,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')
@@ -401,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')
@@ -432,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 203b06ec1b..807d1bf0b1 100644
--- a/frappe/core/doctype/doctype_action/doctype_action.py
+++ b/frappe/core/doctype/doctype_action/doctype_action.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# 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 07e0efdace..ca2c4efa16 100644
--- a/frappe/core/doctype/doctype_link/doctype_link.py
+++ b/frappe/core/doctype/doctype_link/doctype_link.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/chat/doctype/__init__.py b/frappe/core/doctype/doctype_state/__init__.py
similarity index 100%
rename from frappe/chat/doctype/__init__.py
rename to frappe/core/doctype/doctype_state/__init__.py
diff --git a/frappe/core/doctype/doctype_state/doctype_state.json b/frappe/core/doctype/doctype_state/doctype_state.json
new file mode 100644
index 0000000000..79797b41c5
--- /dev/null
+++ b/frappe/core/doctype/doctype_state/doctype_state.json
@@ -0,0 +1,50 @@
+{
+ "actions": [],
+ "creation": "2021-08-23 17:21:28.345841",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "title",
+ "color",
+ "custom"
+ ],
+ "fields": [
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Title",
+ "reqd": 1
+ },
+ {
+ "default": "Blue",
+ "fieldname": "color",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Color",
+ "options": "Blue\nCyan\nGray\nGreen\nLight Blue\nOrange\nPink\nPurple\nRed\nYellow",
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "custom",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Custom"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-12-14 14:14:55.716378",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "DocType State",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/doctype_state/doctype_state.py b/frappe/core/doctype/doctype_state/doctype_state.py
new file mode 100644
index 0000000000..3172834180
--- /dev/null
+++ b/frappe/core/doctype/doctype_state/doctype_state.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 DocTypeState(Document):
+ pass
diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.json b/frappe/core/doctype/document_naming_rule/document_naming_rule.json
index 4a88e3be6e..4e6f3f3fd1 100644
--- a/frappe/core/doctype/document_naming_rule/document_naming_rule.json
+++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.json
@@ -41,6 +41,7 @@
"fieldname": "counter",
"fieldtype": "Int",
"label": "Counter",
+ "no_copy": 1,
"read_only": 1
},
{
@@ -79,7 +80,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-11-04 14:38:14.836056",
+ "modified": "2021-09-13 20:07:47.617615",
"modified_by": "Administrator",
"module": "Core",
"name": "Document Naming Rule",
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 10099bd19a..8013f9df6f 100644
--- a/frappe/core/doctype/document_naming_rule/document_naming_rule.py
+++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
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 2206d173d7..50f1386758 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,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
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 dfca052d95..4706492cea 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
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# 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 643e963bd7..3d0565234c 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,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/core/doctype/domain/domain.py b/frappe/core/doctype/domain/domain.py
index bbd20f3b70..ebd6e3ac9e 100644
--- a/frappe/core/doctype/domain/domain.py
+++ b/frappe/core/doctype/domain/domain.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/core/doctype/domain/test_domain.py b/frappe/core/doctype/domain/test_domain.py
index c2686a7566..d7924ebc90 100644
--- a/frappe/core/doctype/domain/test_domain.py
+++ b/frappe/core/doctype/domain/test_domain.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
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 a8c7c6a747..276411c2ab 100644
--- a/frappe/core/doctype/domain_settings/domain_settings.py
+++ b/frappe/core/doctype/domain_settings/domain_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/dynamic_link/dynamic_link.py b/frappe/core/doctype/dynamic_link/dynamic_link.py
index a7adb9ae72..c0502824c6 100644
--- a/frappe/core/doctype/dynamic_link/dynamic_link.py
+++ b/frappe/core/doctype/dynamic_link/dynamic_link.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/error_log/error_log.json b/frappe/core/doctype/error_log/error_log.json
index cdc7a63001..35ca3ceeef 100644
--- a/frappe/core/doctype/error_log/error_log.json
+++ b/frappe/core/doctype/error_log/error_log.json
@@ -112,7 +112,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "modified": "2017-03-14 12:21:44.292471",
+ "modified": "2021-10-25 12:21:44.292471",
"modified_by": "Administrator",
"module": "Core",
"name": "Error Log",
@@ -144,6 +144,5 @@
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_order": "ASC",
- "track_changes": 1,
"track_seen": 0
-}
\ No newline at end of file
+}
diff --git a/frappe/core/doctype/error_log/error_log.py b/frappe/core/doctype/error_log/error_log.py
index 3d66253b08..39c307520f 100644
--- a/frappe/core/doctype/error_log/error_log.py
+++ b/frappe/core/doctype/error_log/error_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/error_log/test_error_log.py b/frappe/core/doctype/error_log/test_error_log.py
index d7444ab2a7..54a41cd4a9 100644
--- a/frappe/core/doctype/error_log/test_error_log.py
+++ b/frappe/core/doctype/error_log/test_error_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/core/doctype/error_snapshot/error_snapshot.json b/frappe/core/doctype/error_snapshot/error_snapshot.json
index ea7a86d4f6..1333fe0d5b 100644
--- a/frappe/core/doctype/error_snapshot/error_snapshot.json
+++ b/frappe/core/doctype/error_snapshot/error_snapshot.json
@@ -359,7 +359,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "modified": "2016-12-29 14:40:38.619106",
+ "modified": "2021-10-25 14:40:38.619106",
"modified_by": "Administrator",
"module": "Core",
"name": "Error Snapshot",
@@ -394,6 +394,5 @@
"sort_field": "timestamp",
"sort_order": "DESC",
"title_field": "evalue",
- "track_changes": 1,
"track_seen": 0
-}
\ No newline at end of file
+}
diff --git a/frappe/core/doctype/error_snapshot/error_snapshot.py b/frappe/core/doctype/error_snapshot/error_snapshot.py
index 247a796a6b..85143b5aa6 100644
--- a/frappe/core/doctype/error_snapshot/error_snapshot.py
+++ b/frappe/core/doctype/error_snapshot/error_snapshot.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
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 135136294a..86928db9cc 100644
--- a/frappe/core/doctype/error_snapshot/test_error_snapshot.py
+++ b/frappe/core/doctype/error_snapshot/test_error_snapshot.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/core/doctype/feedback/feedback.json b/frappe/core/doctype/feedback/feedback.json
index cf8a180e27..f8380cfda6 100644
--- a/frappe/core/doctype/feedback/feedback.json
+++ b/frappe/core/doctype/feedback/feedback.json
@@ -8,40 +8,14 @@
"reference_doctype",
"reference_name",
"column_break_3",
- "email",
- "rating",
- "section_break_6",
- "feedback"
+ "like",
+ "ip_address"
],
"fields": [
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
- {
- "fieldname": "email",
- "fieldtype": "Data",
- "label": "Email",
- "reqd": 1
- },
- {
- "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",
@@ -56,11 +30,24 @@
"label": "Reference Name",
"options": "reference_doctype",
"reqd": 1
+ },
+ {
+ "fieldname": "ip_address",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "IP Address",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "like",
+ "fieldtype": "Check",
+ "label": "Like"
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-06-14 15:11:26.005805",
+ "modified": "2021-11-10 20:53:21.255593",
"modified_by": "Administrator",
"module": "Core",
"name": "Feedback",
diff --git a/frappe/core/doctype/feedback/feedback.py b/frappe/core/doctype/feedback/feedback.py
index 655bed6eb1..3704ee66e0 100644
--- a/frappe/core/doctype/feedback/feedback.py
+++ b/frappe/core/doctype/feedback/feedback.py
@@ -1,5 +1,5 @@
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/feedback/test_feedback.py b/frappe/core/doctype/feedback/test_feedback.py
index 702f9d8ac1..66f644ccd3 100644
--- a/frappe/core/doctype/feedback/test_feedback.py
+++ b/frappe/core/doctype/feedback/test_feedback.py
@@ -1,27 +1,39 @@
# Copyright (c) 2021, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
class TestFeedback(unittest.TestCase):
+ def tearDown(self):
+ frappe.form_dict.reference_doctype = None
+ frappe.form_dict.reference_name = None
+ frappe.form_dict.like = None
+ frappe.local.request_ip = None
+
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.sql("delete from `tabFeedback` where reference_doctype = 'Blog Post'")
+ 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','test@test.com')
+ from frappe.templates.includes.feedback.feedback import give_feedback
- self.assertEqual(feedback.feedback, 'New feedback')
- self.assertEqual(feedback.rating, 5)
+ frappe.form_dict.reference_doctype = 'Blog Post'
+ frappe.form_dict.reference_name = test_blog.name
+ frappe.form_dict.like = True
+ frappe.local.request_ip = '127.0.0.1'
- updated_feedback = update_feedback('Blog Post', test_blog.name, 6, 'Updated feedback', 'test@test.com')
+ feedback = give_feedback()
- self.assertEqual(updated_feedback.feedback, 'Updated feedback')
- self.assertEqual(updated_feedback.rating, 6)
+ self.assertEqual(feedback.like, True)
- frappe.db.sql("delete from `tabFeedback` where reference_doctype = 'Blog Post'")
+ frappe.form_dict.like = False
+
+ updated_feedback = give_feedback()
+
+ self.assertEqual(updated_feedback.like, False)
+
+ 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..d40328d3cd 100644
--- a/frappe/core/doctype/file/file.js
+++ b/frappe/core/doctype/file/file.js
@@ -23,6 +23,18 @@ 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..."));
+ frm.call("optimize_file").then(() => {
+ frappe.show_alert(__("Image optimized"));
+ });
+ });
+ }
+
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 b4bfe1d21b..91090bdd77 100755
--- a/frappe/core/doctype/file/file.py
+++ b/frappe/core/doctype/file/file.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
"""
record of files
@@ -21,14 +21,14 @@ import zipfile
import requests
import requests.exceptions
from PIL import Image, ImageFile, ImageOps
-from io import StringIO
+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
@@ -254,11 +254,11 @@ class File(Document):
return
file_name = self.file_url.split('/')[-1]
try:
- with open(get_files_path(file_name, is_private=self.is_private), "rb") as f:
+ file_path = get_files_path(file_name, is_private=self.is_private)
+ with open(file_path, "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(file_path))
def on_trash(self):
if self.is_home_folder or self.is_attachments_folder:
@@ -270,16 +270,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
@@ -289,16 +285,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
@@ -321,17 +314,23 @@ class File(Document):
self.delete_file_data_content(only_thumbnail=True)
def on_rollback(self):
- self.flags.on_rollback = True
- self.on_trash()
+ # if original_content flag is set, this rollback should revert the file to its original state
+ if self.flags.original_content:
+ file_path = self.get_full_path()
+ with open(file_path, "wb+") as f:
+ f.write(self.flags.original_content)
+
+ # following condition is only executed when an insert has been rolledback
+ else:
+ self.flags.on_rollback = True
+ self.on_trash()
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:
@@ -359,10 +358,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
@@ -431,47 +426,6 @@ class File(Document):
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
@@ -539,14 +493,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)
@@ -594,6 +540,53 @@ class File(Document):
if self.file_url:
self.is_private = cint(self.file_url.startswith('/private'))
+ @frappe.whitelist()
+ def optimize_file(self):
+ if self.is_folder:
+ raise TypeError('Folders cannot be optimized')
+
+ content_type = mimetypes.guess_type(self.file_name)[0]
+ is_local_image = content_type.startswith('image/') and self.file_size > 0
+ is_svg = content_type == 'image/svg+xml'
+
+ if not is_local_image:
+ raise NotImplementedError('Only local image files can be optimized')
+
+ if is_svg:
+ raise TypeError('Optimization of SVG images is not supported')
+
+ content = self.get_content()
+ file_path = self.get_full_path()
+ optimized_content = optimize_image(content, content_type)
+
+ with open(file_path, 'wb+') as f:
+ f.write(optimized_content)
+
+ self.file_size = len(optimized_content)
+ self.content_hash = get_content_hash(optimized_content)
+ # if rolledback, revert back to original
+ self.flags.original_content = content
+ frappe.local.rollback_observers.append(self)
+ self.save()
+
+ @staticmethod
+ def zip_files(files):
+ from six import string_types
+
+ zip_file = io.BytesIO()
+ zf = zipfile.ZipFile(zip_file, "w", zipfile.ZIP_DEFLATED)
+ for _file in files:
+ if isinstance(_file, string_types):
+ _file = frappe.get_doc("File", _file)
+ if not isinstance(_file, File):
+ continue
+ if _file.is_folder:
+ continue
+ zf.writestr(_file.file_name, _file.get_content())
+ zf.close()
+ return zip_file.getvalue()
+
+
def on_doctype_update():
frappe.db.add_index("File", ["attached_to_doctype", "attached_to_name"])
@@ -621,7 +614,8 @@ 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):
@@ -636,6 +630,16 @@ def move_file(file_list, new_parent, old_parent):
frappe.get_doc("File", old_parent).save()
frappe.get_doc("File", new_parent).save()
+
+@frappe.whitelist()
+def zip_files(files):
+ files = frappe.parse_json(files)
+ zipped_files = File.zip_files(files)
+ frappe.response["filename"] = "files.zip"
+ frappe.response["filecontent"] = zipped_files
+ frappe.response["type"] = "download"
+
+
def setup_folder_path(filename, new_parent):
file = frappe.get_doc("File", filename)
file.folder = new_parent
@@ -672,7 +676,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
@@ -703,7 +707,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)
@@ -737,49 +744,11 @@ 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
-
-
+@frappe.whitelist()
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
user = user or frappe.session.user
@@ -824,6 +793,7 @@ 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)
@@ -869,22 +839,28 @@ def extract_images_from_doc(doc, fieldname):
doc.set(fieldname, content)
-def extract_images_from_html(doc, content):
+def extract_images_from_html(doc, content, is_private=False):
frappe.flags.has_dataurl = False
def _save_file(match):
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, str):
- filename = str(filename, 'utf-8')
else:
- mtype = headers.split(";")[0]
filename = get_random_filename(content_type=mtype)
doctype = doc.parenttype if doc.parent else doc.doctype
@@ -896,7 +872,8 @@ def extract_images_from_html(doc, content):
"attached_to_doctype": doctype,
"attached_to_name": name,
"content": content,
- "decode": True
+ "decode": False,
+ "is_private": is_private
})
_file.save(ignore_permissions=True)
file_url = _file.file_url
@@ -911,12 +888,9 @@ def extract_images_from_html(doc, 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 "")
@@ -927,7 +901,7 @@ 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()
@@ -952,13 +926,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)
@@ -1001,20 +968,14 @@ def get_files_by_search_text(text):
def update_existing_file_docs(doc):
# Update is private and file url of all file docs that point to the same file
- frappe.db.sql("""
- UPDATE `tabFile`
- SET
- file_url = %(file_url)s,
- is_private = %(is_private)s
- WHERE
- content_hash = %(content_hash)s
- and name != %(file_name)s
- """, dict(
- file_url=doc.file_url,
- is_private=doc.is_private,
- content_hash=doc.content_hash,
- file_name=doc.name
- ))
+ file_doctype = frappe.qb.DocType("File")
+ (
+ frappe.qb.update(file_doctype)
+ .set(file_doctype.file_url, doc.file_url)
+ .set(file_doctype.is_private, doc.is_private)
+ .where(file_doctype.content_hash == doc.content_hash)
+ .where(file_doctype.name != doc.name)
+ ).run()
def attach_files_to_document(doc, event):
""" Runs on on_update hook of all documents.
diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py
index 649010c468..9a758b53f5 100644
--- a/frappe/core/doctype/file/test_file.py
+++ b/frappe/core/doctype/file/test_file.py
@@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
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')
@@ -203,10 +204,14 @@ class TestFile(unittest.TestCase):
def delete_test_data(self):
- for f in frappe.db.sql('''select name, file_name from tabFile where
- is_home_folder = 0 and is_attachments_folder = 0 order by creation desc'''):
- frappe.delete_doc("File", f[0])
-
+ test_file_data = frappe.db.get_all(
+ "File",
+ pluck="name",
+ filters={"is_home_folder": 0, "is_attachments_folder": 0},
+ order_by="creation desc",
+ )
+ for f in test_file_data:
+ frappe.delete_doc("File", f)
def upload_file(self):
_file = frappe.get_doc({
@@ -365,6 +370,81 @@ 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'
@@ -469,3 +549,93 @@ 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'
Error Logs ')
}
- if frappe.db.sql_list("select name from `tabError Log` where seen = 0 limit 1"):
+ if frappe.get_all("Error Log", filters={"seen": 0}, limit=1):
log_settings = frappe.get_cached_doc('Log Settings')
if log_settings.users_to_notify:
diff --git a/frappe/core/doctype/log_settings/test_log_settings.py b/frappe/core/doctype/log_settings/test_log_settings.py
index 8e0c9c3f23..40287948fd 100644
--- a/frappe/core/doctype/log_settings/test_log_settings.py
+++ b/frappe/core/doctype/log_settings/test_log_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/core/doctype/module_def/__init__.py b/frappe/core/doctype/module_def/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/core/doctype/module_def/__init__.py
+++ b/frappe/core/doctype/module_def/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/core/doctype/module_def/module_def.js b/frappe/core/doctype/module_def/module_def.js
index c7a6cf85f9..73d2d6562c 100644
--- a/frappe/core/doctype/module_def/module_def.js
+++ b/frappe/core/doctype/module_def/module_def.js
@@ -5,6 +5,9 @@ frappe.ui.form.on('Module Def', {
refresh: function(frm) {
frappe.xcall('frappe.core.doctype.module_def.module_def.get_installed_apps').then(r => {
frm.set_df_property('app_name', 'options', JSON.parse(r));
+ if (!frm.doc.app_name) {
+ frm.set_value('app_name', 'frappe');
+ }
});
}
});
diff --git a/frappe/core/doctype/module_def/module_def.json b/frappe/core/doctype/module_def/module_def.json
index 4de046bbb6..7ddc55fce5 100644
--- a/frappe/core/doctype/module_def/module_def.json
+++ b/frappe/core/doctype/module_def/module_def.json
@@ -8,6 +8,7 @@
"field_order": [
"module_name",
"custom",
+ "package",
"app_name",
"restrict_to_domain"
],
@@ -23,6 +24,7 @@
"unique": 1
},
{
+ "depends_on": "eval:!doc.custom",
"fieldname": "app_name",
"fieldtype": "Select",
"in_list_view": 1,
@@ -41,24 +43,84 @@
"fieldname": "custom",
"fieldtype": "Check",
"label": "Custom"
+ },
+ {
+ "depends_on": "custom",
+ "fieldname": "package",
+ "fieldtype": "Link",
+ "label": "Package",
+ "options": "Package"
}
],
"icon": "fa fa-sitemap",
"idx": 1,
"links": [
{
+ "group": "DocType",
"link_doctype": "DocType",
"link_fieldname": "module"
},
{
+ "group": "DocType",
+ "link_doctype": "Client Script",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "DocType",
+ "link_doctype": "Server Script",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Website",
+ "link_doctype": "Web Page",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Website",
+ "link_doctype": "Web Template",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Website",
+ "link_doctype": "Website Theme",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Website",
+ "link_doctype": "Web Form",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Customization",
"link_doctype": "Workspace",
"link_fieldname": "module"
+ },
+ {
+ "group": "Customization",
+ "link_doctype": "Custom Field",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Customization",
+ "link_doctype": "Property Setter",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Customization",
+ "link_doctype": "Print Format",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Customization",
+ "link_doctype": "Notification",
+ "link_fieldname": "module"
}
],
- "modified": "2021-06-02 13:04:53.118716",
+ "modified": "2021-09-05 21:58:40.253909",
"modified_by": "Administrator",
"module": "Core",
"name": "Module Def",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/core/doctype/module_def/module_def.py b/frappe/core/doctype/module_def/module_def.py
index 68025c83bb..6b420430b8 100644
--- a/frappe/core/doctype/module_def/module_def.py
+++ b/frappe/core/doctype/module_def/module_def.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe, os, json
diff --git a/frappe/core/doctype/module_def/test_module_def.py b/frappe/core/doctype/module_def/test_module_def.py
index 3a3ceb4b57..69a114d765 100644
--- a/frappe/core/doctype/module_def/test_module_def.py
+++ b/frappe/core/doctype/module_def/test_module_def.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/core/doctype/module_profile/module_profile.js b/frappe/core/doctype/module_profile/module_profile.js
index 9c92042dda..3714d31ade 100644
--- a/frappe/core/doctype/module_profile/module_profile.js
+++ b/frappe/core/doctype/module_profile/module_profile.js
@@ -1,19 +1,23 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Module Profile', {
- refresh: function(frm) {
+frappe.ui.form.on("Module Profile", {
+ refresh: function (frm) {
if (has_common(frappe.user_roles, ["Administrator", "System Manager"])) {
if (!frm.module_editor && frm.doc.__onload && frm.doc.__onload.all_modules) {
- let module_area = $('
')
- .appendTo(frm.fields_dict.module_html.wrapper);
-
+ const module_area = $(frm.fields_dict.module_html.wrapper);
frm.module_editor = new frappe.ModuleEditor(frm, module_area);
}
}
if (frm.module_editor) {
- frm.module_editor.refresh();
+ frm.module_editor.show();
+ }
+ },
+
+ validate: function (frm) {
+ if (frm.module_editor) {
+ frm.module_editor.set_modules_in_table();
}
}
});
diff --git a/frappe/core/doctype/module_profile/module_profile.json b/frappe/core/doctype/module_profile/module_profile.json
index 0e4e56962e..32bc757427 100644
--- a/frappe/core/doctype/module_profile/module_profile.json
+++ b/frappe/core/doctype/module_profile/module_profile.json
@@ -34,11 +34,17 @@
}
],
"index_web_pages_for_search": 1,
- "links": [],
- "modified": "2021-01-03 15:36:52.622696",
+ "links": [
+ {
+ "link_doctype": "User",
+ "link_fieldname": "module_profile"
+ }
+ ],
+ "modified": "2021-12-03 15:47:21.296443",
"modified_by": "Administrator",
"module": "Core",
"name": "Module Profile",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/core/doctype/module_profile/module_profile.py b/frappe/core/doctype/module_profile/module_profile.py
index 373e5078d0..930c3879b6 100644
--- a/frappe/core/doctype/module_profile/module_profile.py
+++ b/frappe/core/doctype/module_profile/module_profile.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from frappe.model.document import Document
diff --git a/frappe/core/doctype/module_profile/test_module_profile.py b/frappe/core/doctype/module_profile/test_module_profile.py
index e0d9c13371..e676767db6 100644
--- a/frappe/core/doctype/module_profile/test_module_profile.py
+++ b/frappe/core/doctype/module_profile/test_module_profile.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/core/doctype/navbar_item/navbar_item.py b/frappe/core/doctype/navbar_item/navbar_item.py
index a8fa611374..d4952a75f2 100644
--- a/frappe/core/doctype/navbar_item/navbar_item.py
+++ b/frappe/core/doctype/navbar_item/navbar_item.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/navbar_item/test_navbar_item.py b/frappe/core/doctype/navbar_item/test_navbar_item.py
index 85852a45e8..bb4b2a837a 100644
--- a/frappe/core/doctype/navbar_item/test_navbar_item.py
+++ b/frappe/core/doctype/navbar_item/test_navbar_item.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/core/doctype/navbar_settings/navbar_settings.py b/frappe/core/doctype/navbar_settings/navbar_settings.py
index 60aec67a00..c46d0081b6 100644
--- a/frappe/core/doctype/navbar_settings/navbar_settings.py
+++ b/frappe/core/doctype/navbar_settings/navbar_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
@@ -13,6 +13,9 @@ class NavbarSettings(Document):
def validate_standard_navbar_items(self):
doc_before_save = self.get_doc_before_save()
+ if not doc_before_save:
+ return
+
before_save_items = [item for item in \
doc_before_save.help_dropdown + doc_before_save.settings_dropdown if item.is_standard]
@@ -22,7 +25,6 @@ class NavbarSettings(Document):
if not frappe.flags.in_patch and (len(before_save_items) > len(after_save_items)):
frappe.throw(_("Please hide the standard navbar items instead of deleting them"))
-@frappe.whitelist(allow_guest=True)
def get_app_logo():
app_logo = frappe.db.get_single_value('Navbar Settings', 'app_logo', cache=True)
if not app_logo:
@@ -33,7 +35,3 @@ def get_app_logo():
def get_navbar_settings():
navbar_settings = frappe.get_single('Navbar Settings')
return navbar_settings
-
-
-
-
diff --git a/frappe/core/doctype/navbar_settings/test_navbar_settings.py b/frappe/core/doctype/navbar_settings/test_navbar_settings.py
index 4d1ee72815..01497d9035 100644
--- a/frappe/core/doctype/navbar_settings/test_navbar_settings.py
+++ b/frappe/core/doctype/navbar_settings/test_navbar_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/chat/doctype/chat_message/__init__.py b/frappe/core/doctype/package/__init__.py
similarity index 100%
rename from frappe/chat/doctype/chat_message/__init__.py
rename to frappe/core/doctype/package/__init__.py
diff --git a/frappe/core/doctype/package/licenses/GNU Affero General Public License.md b/frappe/core/doctype/package/licenses/GNU Affero General Public License.md
new file mode 100644
index 0000000000..c7f159aed8
--- /dev/null
+++ b/frappe/core/doctype/package/licenses/GNU Affero General Public License.md
@@ -0,0 +1,614 @@
+### GNU AFFERO GENERAL PUBLIC LICENSE
+
+Version 3, 19 November 2007
+
+Copyright (C) 2007 Free Software Foundation, Inc.
+
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+### Preamble
+
+The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains
+free software for all its users.
+
+When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing
+under this license.
+
+The precise terms and conditions for copying, distribution and
+modification follow.
+
+### TERMS AND CONDITIONS
+
+#### 0. Definitions.
+
+"This License" refers to version 3 of the GNU Affero General Public
+License.
+
+"Copyright" also means copyright-like laws that apply to other kinds
+of works, such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of
+an exact copy. The resulting work is called a "modified version" of
+the earlier work or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user
+through a computer network, with no transfer of a copy, is not
+conveying.
+
+An interactive user interface displays "Appropriate Legal Notices" to
+the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+#### 1. Source Code.
+
+The "source code" for a work means the preferred form of the work for
+making modifications to it. "Object code" means any non-source form of
+a work.
+
+A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users can
+regenerate automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same
+work.
+
+#### 2. Basic Permissions.
+
+All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+You may make, run and propagate covered works that you do not convey,
+without conditions so long as your license otherwise remains in force.
+You may convey covered works to others for the sole purpose of having
+them make modifications exclusively for you, or provide you with
+facilities for running those works, provided that you comply with the
+terms of this License in conveying all material for which you do not
+control copyright. Those thus making or running the covered works for
+you must do so exclusively on your behalf, under your direction and
+control, on terms that prohibit them from making any copies of your
+copyrighted material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the
+conditions stated below. Sublicensing is not allowed; section 10 makes
+it unnecessary.
+
+#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such
+circumvention is effected by exercising rights under this License with
+respect to the covered work, and you disclaim any intention to limit
+operation or modification of the work as a means of enforcing, against
+the work's users, your or third parties' legal rights to forbid
+circumvention of technological measures.
+
+#### 4. Conveying Verbatim Copies.
+
+You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+#### 5. Conveying Modified Source Versions.
+
+You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these
+conditions:
+
+- a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+- b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under
+ section 7. This requirement modifies the requirement in section 4
+ to "keep intact all notices".
+- c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+- d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+#### 6. Conveying Non-Source Forms.
+
+You may convey a covered work in object code form under the terms of
+sections 4 and 5, provided that you also convey the machine-readable
+Corresponding Source under the terms of this License, in one of these
+ways:
+
+- a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+- b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the Corresponding
+ Source from a network server at no charge.
+- c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+- d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+- e) Convey the object code using peer-to-peer transmission,
+ provided you inform other peers where the object code and
+ Corresponding Source of the work are being offered to the general
+ public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal,
+family, or household purposes, or (2) anything designed or sold for
+incorporation into a dwelling. In determining whether a product is a
+consumer product, doubtful cases shall be resolved in favor of
+coverage. For a particular product received by a particular user,
+"normally used" refers to a typical or common use of that class of
+product, regardless of the status of the particular user or of the way
+in which the particular user actually uses, or expects or is expected
+to use, the product. A product is a consumer product regardless of
+whether the product has substantial commercial, industrial or
+non-consumer uses, unless such uses represent the only significant
+mode of use of the product.
+
+"Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to
+install and execute modified versions of a covered work in that User
+Product from a modified version of its Corresponding Source. The
+information must suffice to ensure that the continued functioning of
+the modified object code is in no case prevented or interfered with
+solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or
+updates for a work that has been modified or installed by the
+recipient, or for the User Product in which it has been modified or
+installed. Access to a network may be denied when the modification
+itself materially and adversely affects the operation of the network
+or violates the rules and protocols for communication across the
+network.
+
+Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+#### 7. Additional Terms.
+
+"Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders
+of that material) supplement the terms of this License with terms:
+
+- a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+- b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+- c) Prohibiting misrepresentation of the origin of that material,
+ or requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+- d) Limiting the use for publicity purposes of names of licensors
+ or authors of the material; or
+- e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+- f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions
+ of it) with contractual assumptions of liability to the recipient,
+ for any liability that these contractual assumptions directly
+ impose on those licensors and authors.
+
+All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions; the
+above requirements apply either way.
+
+#### 8. Termination.
+
+You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+However, if you cease all violation of this License, then your license
+from a particular copyright holder is reinstated (a) provisionally,
+unless and until the copyright holder explicitly and finally
+terminates your license, and (b) permanently, if the copyright holder
+fails to notify you of the violation by some reasonable means prior to
+60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+#### 9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run
+a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+#### 10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+#### 11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims owned
+or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+A patent license is "discriminatory" if it does not include within the
+scope of its coverage, prohibits the exercise of, or is conditioned on
+the non-exercise of one or more of the rights that are specifically
+granted under this License. You may not convey a covered work if you
+are a party to an arrangement with a third party that is in the
+business of distributing software, under which you make payment to the
+third party based on the extent of your activity of conveying the
+work, and under which the third party grants, to any of the parties
+who would receive the covered work from you, a discriminatory patent
+license (a) in connection with copies of the covered work conveyed by
+you (or copies made from those copies), or (b) primarily for and in
+connection with specific products or compilations that contain the
+covered work, unless you entered into that arrangement, or that patent
+license was granted, prior to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+#### 12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under
+this License and any other pertinent obligations, then as a
+consequence you may not convey it at all. For example, if you agree to
+terms that obligate you to collect a royalty for further conveying
+from those to whom you convey the Program, the only way you could
+satisfy both those terms and this License would be to refrain entirely
+from conveying the Program.
+
+#### 13. Remote Network Interaction; Use with the GNU General Public License.
+
+Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your
+version supports such interaction) an opportunity to receive the
+Corresponding Source of your version by providing access to the
+Corresponding Source from a network server at no charge, through some
+standard or customary means of facilitating copying of software. This
+Corresponding Source shall include the Corresponding Source for any
+work covered by version 3 of the GNU General Public License that is
+incorporated pursuant to the following paragraph.
+
+Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+#### 14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions
+of the GNU Affero General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever
+published by the Free Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions
+of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+#### 15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
+WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
+PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
+DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
+CORRECTION.
+
+#### 16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
+CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
+ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
+NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
+LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
+TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
+PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+#### 17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
diff --git a/frappe/core/doctype/package/licenses/GNU General Public License.md b/frappe/core/doctype/package/licenses/GNU General Public License.md
new file mode 100644
index 0000000000..c4580f2eb6
--- /dev/null
+++ b/frappe/core/doctype/package/licenses/GNU General Public License.md
@@ -0,0 +1,617 @@
+### GNU GENERAL PUBLIC LICENSE
+
+Version 3, 29 June 2007
+
+Copyright (C) 2007 Free Software Foundation, Inc.
+
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+### Preamble
+
+The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom
+to share and change all versions of a program--to make sure it remains
+free software for all its users. We, the Free Software Foundation, use
+the GNU General Public License for most of our software; it applies
+also to any other work released this way by its authors. You can apply
+it to your programs, too.
+
+When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you
+have certain responsibilities if you distribute copies of the
+software, or if you modify it: responsibilities to respect the freedom
+of others.
+
+For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the
+manufacturer can do so. This is fundamentally incompatible with the
+aim of protecting users' freedom to change the software. The
+systematic pattern of such abuse occurs in the area of products for
+individuals to use, which is precisely where it is most unacceptable.
+Therefore, we have designed this version of the GPL to prohibit the
+practice for those products. If such problems arise substantially in
+other domains, we stand ready to extend this provision to those
+domains in future versions of the GPL, as needed to protect the
+freedom of users.
+
+Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish
+to avoid the special danger that patents applied to a free program
+could make it effectively proprietary. To prevent this, the GPL
+assures that patents cannot be used to render the program non-free.
+
+The precise terms and conditions for copying, distribution and
+modification follow.
+
+### TERMS AND CONDITIONS
+
+#### 0. Definitions.
+
+"This License" refers to version 3 of the GNU General Public License.
+
+"Copyright" also means copyright-like laws that apply to other kinds
+of works, such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of
+an exact copy. The resulting work is called a "modified version" of
+the earlier work or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user
+through a computer network, with no transfer of a copy, is not
+conveying.
+
+An interactive user interface displays "Appropriate Legal Notices" to
+the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+#### 1. Source Code.
+
+The "source code" for a work means the preferred form of the work for
+making modifications to it. "Object code" means any non-source form of
+a work.
+
+A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users can
+regenerate automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same
+work.
+
+#### 2. Basic Permissions.
+
+All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+You may make, run and propagate covered works that you do not convey,
+without conditions so long as your license otherwise remains in force.
+You may convey covered works to others for the sole purpose of having
+them make modifications exclusively for you, or provide you with
+facilities for running those works, provided that you comply with the
+terms of this License in conveying all material for which you do not
+control copyright. Those thus making or running the covered works for
+you must do so exclusively on your behalf, under your direction and
+control, on terms that prohibit them from making any copies of your
+copyrighted material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the
+conditions stated below. Sublicensing is not allowed; section 10 makes
+it unnecessary.
+
+#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such
+circumvention is effected by exercising rights under this License with
+respect to the covered work, and you disclaim any intention to limit
+operation or modification of the work as a means of enforcing, against
+the work's users, your or third parties' legal rights to forbid
+circumvention of technological measures.
+
+#### 4. Conveying Verbatim Copies.
+
+You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+#### 5. Conveying Modified Source Versions.
+
+You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these
+conditions:
+
+- a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+- b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under
+ section 7. This requirement modifies the requirement in section 4
+ to "keep intact all notices".
+- c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+- d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+#### 6. Conveying Non-Source Forms.
+
+You may convey a covered work in object code form under the terms of
+sections 4 and 5, provided that you also convey the machine-readable
+Corresponding Source under the terms of this License, in one of these
+ways:
+
+- a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+- b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the Corresponding
+ Source from a network server at no charge.
+- c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+- d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+- e) Convey the object code using peer-to-peer transmission,
+ provided you inform other peers where the object code and
+ Corresponding Source of the work are being offered to the general
+ public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal,
+family, or household purposes, or (2) anything designed or sold for
+incorporation into a dwelling. In determining whether a product is a
+consumer product, doubtful cases shall be resolved in favor of
+coverage. For a particular product received by a particular user,
+"normally used" refers to a typical or common use of that class of
+product, regardless of the status of the particular user or of the way
+in which the particular user actually uses, or expects or is expected
+to use, the product. A product is a consumer product regardless of
+whether the product has substantial commercial, industrial or
+non-consumer uses, unless such uses represent the only significant
+mode of use of the product.
+
+"Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to
+install and execute modified versions of a covered work in that User
+Product from a modified version of its Corresponding Source. The
+information must suffice to ensure that the continued functioning of
+the modified object code is in no case prevented or interfered with
+solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or
+updates for a work that has been modified or installed by the
+recipient, or for the User Product in which it has been modified or
+installed. Access to a network may be denied when the modification
+itself materially and adversely affects the operation of the network
+or violates the rules and protocols for communication across the
+network.
+
+Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+#### 7. Additional Terms.
+
+"Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders
+of that material) supplement the terms of this License with terms:
+
+- a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+- b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+- c) Prohibiting misrepresentation of the origin of that material,
+ or requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+- d) Limiting the use for publicity purposes of names of licensors
+ or authors of the material; or
+- e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+- f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions
+ of it) with contractual assumptions of liability to the recipient,
+ for any liability that these contractual assumptions directly
+ impose on those licensors and authors.
+
+All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions; the
+above requirements apply either way.
+
+#### 8. Termination.
+
+You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+However, if you cease all violation of this License, then your license
+from a particular copyright holder is reinstated (a) provisionally,
+unless and until the copyright holder explicitly and finally
+terminates your license, and (b) permanently, if the copyright holder
+fails to notify you of the violation by some reasonable means prior to
+60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+#### 9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run
+a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+#### 10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+#### 11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims owned
+or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+A patent license is "discriminatory" if it does not include within the
+scope of its coverage, prohibits the exercise of, or is conditioned on
+the non-exercise of one or more of the rights that are specifically
+granted under this License. You may not convey a covered work if you
+are a party to an arrangement with a third party that is in the
+business of distributing software, under which you make payment to the
+third party based on the extent of your activity of conveying the
+work, and under which the third party grants, to any of the parties
+who would receive the covered work from you, a discriminatory patent
+license (a) in connection with copies of the covered work conveyed by
+you (or copies made from those copies), or (b) primarily for and in
+connection with specific products or compilations that contain the
+covered work, unless you entered into that arrangement, or that patent
+license was granted, prior to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+#### 12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under
+this License and any other pertinent obligations, then as a
+consequence you may not convey it at all. For example, if you agree to
+terms that obligate you to collect a royalty for further conveying
+from those to whom you convey the Program, the only way you could
+satisfy both those terms and this License would be to refrain entirely
+from conveying the Program.
+
+#### 13. Use with the GNU Affero General Public License.
+
+Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+#### 14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions
+of the GNU General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in
+detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies that a certain numbered version of the GNU General Public
+License "or any later version" applies to it, you have the option of
+following the terms and conditions either of that numbered version or
+of any later version published by the Free Software Foundation. If the
+Program does not specify a version number of the GNU General Public
+License, you may choose any version ever published by the Free
+Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions
+of the GNU General Public License can be used, that proxy's public
+statement of acceptance of a version permanently authorizes you to
+choose that version for the Program.
+
+Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+#### 15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
+WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
+PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
+DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
+CORRECTION.
+
+#### 16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
+CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
+ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
+NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
+LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
+TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
+PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+#### 17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
diff --git a/frappe/core/doctype/package/licenses/MIT License.md b/frappe/core/doctype/package/licenses/MIT License.md
new file mode 100644
index 0000000000..c038ee76ae
--- /dev/null
+++ b/frappe/core/doctype/package/licenses/MIT License.md
@@ -0,0 +1,17 @@
+### MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this
+software and associated documentation files (the "Software"), to deal in the Software
+without restriction, including without limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies
+or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/frappe/core/doctype/package/package.js b/frappe/core/doctype/package/package.js
new file mode 100644
index 0000000000..90e2eed1e3
--- /dev/null
+++ b/frappe/core/doctype/package/package.js
@@ -0,0 +1,17 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Package', {
+ validate: function(frm) {
+ if (!frm.doc.package_name) {
+ frm.set_value('package_name', frm.doc.name.toLowerCase().replace(' ', '-'));
+ }
+ },
+
+ license_type: function(frm) {
+ frappe.call('frappe.core.doctype.package.package.get_license_text',
+ {'license_type': frm.doc.license_type}).then(r => {
+ frm.set_value('license', r.message);
+ });
+ }
+});
diff --git a/frappe/core/doctype/package/package.json b/frappe/core/doctype/package/package.json
new file mode 100644
index 0000000000..285e17a5bb
--- /dev/null
+++ b/frappe/core/doctype/package/package.json
@@ -0,0 +1,76 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "Prompt",
+ "creation": "2021-09-04 11:54:35.155687",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "package_name",
+ "readme",
+ "license_type",
+ "license"
+ ],
+ "fields": [
+ {
+ "fieldname": "readme",
+ "fieldtype": "Markdown Editor",
+ "label": "Readme"
+ },
+ {
+ "fieldname": "package_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Package Name",
+ "reqd": 1
+ },
+ {
+ "fieldname": "license_type",
+ "fieldtype": "Select",
+ "label": "License Type",
+ "options": "\nMIT License\nGNU General Public License\nGNU Affero General Public License"
+ },
+ {
+ "fieldname": "license",
+ "fieldtype": "Markdown Editor",
+ "label": "License"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [
+ {
+ "group": "Modules",
+ "link_doctype": "Module Def",
+ "link_fieldname": "package"
+ },
+ {
+ "group": "Release",
+ "link_doctype": "Package Release",
+ "link_fieldname": "package"
+ }
+ ],
+ "modified": "2021-09-05 13:15:01.130982",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Package",
+ "naming_rule": "Set by user",
+ "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/core/doctype/package/package.py b/frappe/core/doctype/package/package.py
new file mode 100644
index 0000000000..aa9735c061
--- /dev/null
+++ b/frappe/core/doctype/package/package.py
@@ -0,0 +1,18 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+import frappe
+import os
+from frappe.model.document import Document
+
+class Package(Document):
+ def validate(self):
+ if not self.package_name:
+ self.package_name = self.name.lower().replace(' ', '-')
+
+@frappe.whitelist()
+def get_license_text(license_type):
+ with open(os.path.join(os.path.dirname(__file__), 'licenses',
+ license_type + '.md'), 'r') as textfile:
+ return textfile.read()
+
diff --git a/frappe/core/doctype/package/test_package.py b/frappe/core/doctype/package/test_package.py
new file mode 100644
index 0000000000..3fb8d48274
--- /dev/null
+++ b/frappe/core/doctype/package/test_package.py
@@ -0,0 +1,89 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+
+import frappe
+import os
+import json
+import unittest
+
+class TestPackage(unittest.TestCase):
+ def test_package_release(self):
+ make_test_package()
+ make_test_module()
+ make_test_doctype()
+ make_test_server_script()
+ make_test_web_page()
+
+ # make release
+ frappe.get_doc(dict(
+ doctype = 'Package Release',
+ package = 'Test Package',
+ publish = 1
+ )).insert()
+
+ self.assertTrue(os.path.exists(frappe.get_site_path('packages', 'test-package')))
+ self.assertTrue(os.path.exists(frappe.get_site_path('packages', 'test-package', 'test_module_for_package')))
+ self.assertTrue(os.path.exists(frappe.get_site_path('packages', 'test-package', 'test_module_for_package', 'doctype', 'test_doctype_for_package')))
+ with open(frappe.get_site_path('packages', 'test-package', 'test_module_for_package',
+ 'doctype', 'test_doctype_for_package', 'test_doctype_for_package.json')) as f:
+ doctype = json.loads(f.read())
+ self.assertEqual(doctype['doctype'], 'DocType')
+ self.assertEqual(doctype['name'], 'Test DocType for Package')
+ self.assertEqual(doctype['fields'][0]['fieldname'], 'test_field')
+
+
+def make_test_package():
+ if not frappe.db.exists('Package', 'Test Package'):
+ frappe.get_doc(dict(
+ doctype = 'Package',
+ name = 'Test Package',
+ package_name = 'test-package',
+ readme = '# Test Package'
+ )).insert()
+
+def make_test_module():
+ if not frappe.db.exists('Module Def', 'Test Module for Package'):
+ frappe.get_doc(dict(
+ doctype = 'Module Def',
+ module_name = 'Test Module for Package',
+ custom = 1,
+ app_name = 'frappe',
+ package = 'Test Package'
+ )).insert()
+
+def make_test_doctype():
+ if not frappe.db.exists('DocType', 'Test DocType for Package'):
+ frappe.get_doc(dict(
+ doctype = 'DocType',
+ name = 'Test DocType for Package',
+ custom = 1,
+ module = 'Test Module for Package',
+ autoname = 'Prompt',
+ fields = [dict(
+ fieldname = 'test_field',
+ fieldtype = 'Data',
+ label = 'Test Field'
+ )]
+ )).insert()
+
+def make_test_server_script():
+ if not frappe.db.exists('Server Script', 'Test Script for Package'):
+ frappe.get_doc(dict(
+ doctype = 'Server Script',
+ name = 'Test Script for Package',
+ module = 'Test Module for Package',
+ script_type = 'DocType Event',
+ reference_doctype = 'Test DocType for Package',
+ doctype_event = 'Before Save',
+ script = 'frappe.msgprint("Test")'
+ )).insert()
+
+def make_test_web_page():
+ if not frappe.db.exists('Web Page', 'test-web-page-for-package'):
+ frappe.get_doc(dict(
+ doctype = "Web Page",
+ module = 'Test Module for Package',
+ main_section = "Some content",
+ published = 1,
+ title = "Test Web Page for Package"
+ )).insert()
diff --git a/frappe/chat/doctype/chat_profile/__init__.py b/frappe/core/doctype/package_import/__init__.py
similarity index 100%
rename from frappe/chat/doctype/chat_profile/__init__.py
rename to frappe/core/doctype/package_import/__init__.py
diff --git a/frappe/core/doctype/package_import/package_import.js b/frappe/core/doctype/package_import/package_import.js
new file mode 100644
index 0000000000..c01a6266cc
--- /dev/null
+++ b/frappe/core/doctype/package_import/package_import.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Package Import', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/core/doctype/package_import/package_import.json b/frappe/core/doctype/package_import/package_import.json
new file mode 100644
index 0000000000..f3c6168f8d
--- /dev/null
+++ b/frappe/core/doctype/package_import/package_import.json
@@ -0,0 +1,65 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "format:Package Import at {creation}",
+ "creation": "2021-09-05 16:36:46.680094",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "attach_package",
+ "activate",
+ "force",
+ "log"
+ ],
+ "fields": [
+ {
+ "fieldname": "attach_package",
+ "fieldtype": "Attach",
+ "label": "Attach Package"
+ },
+ {
+ "default": "0",
+ "fieldname": "activate",
+ "fieldtype": "Check",
+ "label": "Activate"
+ },
+ {
+ "fieldname": "log",
+ "fieldtype": "Code",
+ "label": "Log",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "force",
+ "fieldtype": "Check",
+ "label": "Force"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-09-05 21:30:04.796090",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Package Import",
+ "naming_rule": "Expression",
+ "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/core/doctype/package_import/package_import.py b/frappe/core/doctype/package_import/package_import.py
new file mode 100644
index 0000000000..f4a2d666dd
--- /dev/null
+++ b/frappe/core/doctype/package_import/package_import.py
@@ -0,0 +1,58 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+import frappe
+import os
+import json
+import subprocess
+from frappe.model.document import Document
+from frappe.desk.form.load import get_attachments
+from frappe.model.sync import get_doc_files
+from frappe.modules.import_file import import_file_by_path, import_doc
+
+class PackageImport(Document):
+ def validate(self):
+ if self.activate:
+ self.import_package()
+
+ def import_package(self):
+ attachment = get_attachments(self.doctype, self.name)
+
+ if not attachment:
+ frappe.throw(frappe._('Please attach the package'))
+
+ attachment = attachment[0]
+
+ # get package_name from file (package_name-0.0.0.tar.gz)
+ package_name = attachment.file_name.split('.')[0].rsplit('-', 1)[0]
+ if not os.path.exists(frappe.get_site_path('packages')):
+ os.makedirs(frappe.get_site_path('packages'))
+
+ # extract
+ subprocess.check_output(['tar', 'xzf',
+ frappe.get_site_path(attachment.file_url.strip('/')), '-C',
+ frappe.get_site_path('packages')])
+
+ package_path = frappe.get_site_path('packages', package_name)
+
+ # import Package
+ with open(os.path.join(package_path, package_name + '.json'), 'r') as packagefile:
+ doc_dict = json.loads(packagefile.read())
+
+ frappe.flags.package = import_doc(doc_dict)
+
+ # collect modules
+ files = []
+ log = []
+ for module in os.listdir(package_path):
+ module_path = os.path.join(package_path, module)
+ if os.path.isdir(module_path):
+ get_doc_files(files, module_path)
+
+ # import files
+ for file in files:
+ import_file_by_path(file, force=self.force, ignore_version=True,
+ for_sync=True)
+ log.append('Imported {}'.format(file))
+
+ self.log = '\n'.join(log)
diff --git a/frappe/core/doctype/package_import/test_package_import.py b/frappe/core/doctype/package_import/test_package_import.py
new file mode 100644
index 0000000000..04628fed93
--- /dev/null
+++ b/frappe/core/doctype/package_import/test_package_import.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+class TestPackageImport(unittest.TestCase):
+ pass
diff --git a/frappe/chat/doctype/chat_room/__init__.py b/frappe/core/doctype/package_release/__init__.py
similarity index 100%
rename from frappe/chat/doctype/chat_room/__init__.py
rename to frappe/core/doctype/package_release/__init__.py
diff --git a/frappe/core/doctype/package_release/package_release.js b/frappe/core/doctype/package_release/package_release.js
new file mode 100644
index 0000000000..9eabe36839
--- /dev/null
+++ b/frappe/core/doctype/package_release/package_release.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Package Release', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/core/doctype/package_release/package_release.json b/frappe/core/doctype/package_release/package_release.json
new file mode 100644
index 0000000000..b651d699c4
--- /dev/null
+++ b/frappe/core/doctype/package_release/package_release.json
@@ -0,0 +1,95 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2021-09-05 12:59:01.932327",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "package",
+ "publish",
+ "path",
+ "column_break_3",
+ "major",
+ "minor",
+ "patch",
+ "section_break_7",
+ "release_notes"
+ ],
+ "fields": [
+ {
+ "fieldname": "package",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Package",
+ "options": "Package",
+ "reqd": 1
+ },
+ {
+ "fieldname": "major",
+ "fieldtype": "Int",
+ "label": "Major"
+ },
+ {
+ "fieldname": "minor",
+ "fieldtype": "Int",
+ "label": "Minor"
+ },
+ {
+ "fieldname": "patch",
+ "fieldtype": "Int",
+ "label": "Patch",
+ "no_copy": 1
+ },
+ {
+ "fieldname": "path",
+ "fieldtype": "Small Text",
+ "label": "Path",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_7",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "release_notes",
+ "fieldtype": "Markdown Editor",
+ "label": "Release Notes"
+ },
+ {
+ "default": "0",
+ "fieldname": "publish",
+ "fieldtype": "Check",
+ "label": "Publish"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-09-05 16:04:32.860988",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Package Release",
+ "naming_rule": "By script",
+ "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/core/doctype/package_release/package_release.py b/frappe/core/doctype/package_release/package_release.py
new file mode 100644
index 0000000000..d23ae917c4
--- /dev/null
+++ b/frappe/core/doctype/package_release/package_release.py
@@ -0,0 +1,92 @@
+# 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_doc
+import os
+import subprocess
+from frappe.query_builder.functions import Max
+
+
+class PackageRelease(Document):
+ def set_version(self):
+ # set the next patch release by default
+ doctype = frappe.qb.DocType("Package Release")
+ if not self.major:
+ self.major = frappe.qb.from_(doctype) \
+ .where(doctype.package == self.package) \
+ .select(Max(doctype.minor)).run()[0][0] or 0
+
+ if not self.minor:
+ self.minor = frappe.qb.from_(doctype) \
+ .where(doctype.package == self.package) \
+ .select(Max("minor")).run()[0][0] or 0
+ if not self.patch:
+ value = frappe.qb.from_(doctype) \
+ .where(doctype.package == self.package) \
+ .select(Max("patch")).run()[0][0] or 0
+ self.patch = value + 1
+
+ def autoname(self):
+ self.set_version()
+ self.name = '{}-{}.{}.{}'.format(
+ frappe.db.get_value('Package', self.package, 'package_name'),
+ self.major, self.minor, self.patch)
+
+ def validate(self):
+ if self.publish:
+ self.export_files()
+
+ def export_files(self):
+ '''Export all the documents in this package to site/packages folder'''
+ package = frappe.get_doc('Package', self.package)
+
+ self.export_modules()
+ self.export_package_files(package)
+ self.make_tarfile(package)
+
+ def export_modules(self):
+ for m in frappe.db.get_all('Module Def', dict(package=self.package)):
+ module = frappe.get_doc('Module Def', m.name)
+ for l in module.meta.links:
+ if l.link_doctype == 'Module Def':
+ continue
+ # all documents of the type in the module
+ for d in frappe.get_all(l.link_doctype, dict(module=m.name)):
+ export_doc(frappe.get_doc(l.link_doctype, d.name))
+
+ def export_package_files(self, package):
+ # write readme
+ with open(frappe.get_site_path('packages', package.package_name, 'README.md'), 'w') as readme:
+ readme.write(package.readme)
+
+ # write license
+ if package.license:
+ with open(frappe.get_site_path('packages', package.package_name, 'LICENSE.md'), 'w') as license:
+ license.write(package.license)
+
+ # write package.json as `frappe_package.json`
+ with open(frappe.get_site_path('packages', package.package_name, package.package_name + '.json'), 'w') as packagefile:
+ packagefile.write(frappe.as_json(package.as_dict(no_nulls=True)))
+
+ def make_tarfile(self, package):
+ # make tarfile
+ filename = '{}.tar.gz'.format(self.name)
+ subprocess.check_output(['tar', 'czf', filename, package.package_name],
+ cwd=frappe.get_site_path('packages'))
+
+ # move file
+ subprocess.check_output(['mv', frappe.get_site_path('packages', filename),
+ frappe.get_site_path('public', 'files')])
+
+ # make attachment
+ file = frappe.get_doc(dict(
+ doctype = 'File',
+ file_url = '/' + os.path.join('files', filename),
+ attached_to_doctype = self.doctype,
+ attached_to_name = self.name
+ ))
+
+ file.flags.ignore_duplicate_entry_error = True
+ file.insert()
diff --git a/frappe/core/doctype/package_release/test_package_release.py b/frappe/core/doctype/package_release/test_package_release.py
new file mode 100644
index 0000000000..6a15e8625b
--- /dev/null
+++ b/frappe/core/doctype/package_release/test_package_release.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+class TestPackageRelease(unittest.TestCase):
+ pass
diff --git a/frappe/core/doctype/page/__init__.py b/frappe/core/doctype/page/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/core/doctype/page/__init__.py
+++ b/frappe/core/doctype/page/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/core/doctype/page/page.py b/frappe/core/doctype/page/page.py
index 0ba0e309dd..894e180bb1 100644
--- a/frappe/core/doctype/page/page.py
+++ b/frappe/core/doctype/page/page.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
import os
@@ -109,6 +109,7 @@ class Page(Document):
if os.path.exists(fpath):
with open(fpath, 'r') as f:
self.script = render_include(f.read())
+ self.script += f"\n\n//# sourceURL={page_name}.js"
# css
fpath = os.path.join(path, page_name + '.css')
diff --git a/frappe/core/doctype/page/test_page.py b/frappe/core/doctype/page/test_page.py
index 18b4aea2c8..7db32497a8 100644
--- a/frappe/core/doctype/page/test_page.py
+++ b/frappe/core/doctype/page/test_page.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/core/doctype/patch_log/__init__.py b/frappe/core/doctype/patch_log/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/core/doctype/patch_log/__init__.py
+++ b/frappe/core/doctype/patch_log/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/core/doctype/patch_log/patch_log.py b/frappe/core/doctype/patch_log/patch_log.py
index cc66955eb8..9a5da24e37 100644
--- a/frappe/core/doctype/patch_log/patch_log.py
+++ b/frappe/core/doctype/patch_log/patch_log.py
@@ -1,7 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/core/doctype/patch_log/test_patch_log.py b/frappe/core/doctype/patch_log/test_patch_log.py
index d0690ecee0..df1ca16b22 100644
--- a/frappe/core/doctype/patch_log/test_patch_log.py
+++ b/frappe/core/doctype/patch_log/test_patch_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/core/doctype/payment_gateway/payment_gateway.py b/frappe/core/doctype/payment_gateway/payment_gateway.py
index 1459635b01..d0fa550ea1 100644
--- a/frappe/core/doctype/payment_gateway/payment_gateway.py
+++ b/frappe/core/doctype/payment_gateway/payment_gateway.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/payment_gateway/test_payment_gateway.py b/frappe/core/doctype/payment_gateway/test_payment_gateway.py
index 66f899bd27..e2ad081cfa 100644
--- a/frappe/core/doctype/payment_gateway/test_payment_gateway.py
+++ b/frappe/core/doctype/payment_gateway/test_payment_gateway.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/core/doctype/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py
index c68bb6a4f1..2d1b026572 100644
--- a/frappe/core/doctype/prepared_report/prepared_report.py
+++ b/frappe/core/doctype/prepared_report/prepared_report.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import json
@@ -11,8 +11,6 @@ from frappe.desk.query_report import generate_report_result
from frappe.model.document import Document
from frappe.utils import gzip_compress, gzip_decompress
from frappe.utils.background_jobs import enqueue
-from frappe.core.doctype.file.file import remove_all
-
class PreparedReport(Document):
def before_insert(self):
diff --git a/frappe/core/doctype/prepared_report/test_prepared_report.py b/frappe/core/doctype/prepared_report/test_prepared_report.py
index ef324dd01a..5b12990f64 100644
--- a/frappe/core/doctype/prepared_report/test_prepared_report.py
+++ b/frappe/core/doctype/prepared_report/test_prepared_report.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
import json
diff --git a/frappe/core/doctype/report/__init__.py b/frappe/core/doctype/report/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/core/doctype/report/__init__.py
+++ b/frappe/core/doctype/report/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/core/doctype/report/boilerplate/controller.py b/frappe/core/doctype/report/boilerplate/controller.py
index b8e9cb7467..ccf732a405 100644
--- a/frappe/core/doctype/report/boilerplate/controller.py
+++ b/frappe/core/doctype/report/boilerplate/controller.py
@@ -1,5 +1,5 @@
# Copyright (c) 2013, {app_publisher} and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py
index a5c61fa436..266017dd71 100644
--- a/frappe/core/doctype/report/report.py
+++ b/frappe/core/doctype/report/report.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
import json, datetime
from frappe import _, scrub
@@ -51,6 +51,14 @@ class Report(Document):
and not frappe.flags.in_patch):
frappe.throw(_("You are not allowed to delete Standard Report"))
delete_custom_role('report', self.name)
+ self.delete_prepared_reports()
+
+ def delete_prepared_reports(self):
+ prepared_reports = frappe.get_all("Prepared Report", filters={'ref_report_doctype': self.name}, pluck='name')
+
+ for report in prepared_reports:
+ frappe.delete_doc("Prepared Report", report, ignore_missing=True, force=True,
+ delete_permanently=True)
def get_columns(self):
return [d.as_dict(no_default_fields = True) for d in self.columns]
@@ -105,7 +113,7 @@ class Report(Document):
if not self.query.lower().startswith("select"):
frappe.throw(_("Query must be a SELECT"), title=_('Report Document Error'))
- result = [list(t) for t in frappe.db.sql(self.query, filters, debug=True)]
+ result = [list(t) for t in frappe.db.sql(self.query, filters)]
columns = self.get_columns() or [cstr(c[0]) for c in frappe.db.get_description()]
return [columns, result]
diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py
index 9c953db1f0..36e3b09254 100644
--- a/frappe/core/doctype/report/test_report.py
+++ b/frappe/core/doctype/report/test_report.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe, json, os
import unittest
diff --git a/frappe/core/doctype/report_column/report_column.py b/frappe/core/doctype/report_column/report_column.py
index f9078d820d..3b2c1e130b 100644
--- a/frappe/core/doctype/report_column/report_column.py
+++ b/frappe/core/doctype/report_column/report_column.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/report_filter/report_filter.py b/frappe/core/doctype/report_filter/report_filter.py
index ccdcc0eb6f..b325985308 100644
--- a/frappe/core/doctype/report_filter/report_filter.py
+++ b/frappe/core/doctype/report_filter/report_filter.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/role/__init__.py b/frappe/core/doctype/role/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/core/doctype/role/__init__.py
+++ b/frappe/core/doctype/role/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/core/doctype/role/patches/v13_set_default_desk_properties.py b/frappe/core/doctype/role/patches/v13_set_default_desk_properties.py
index 375ea02e0e..dc17526047 100644
--- a/frappe/core/doctype/role/patches/v13_set_default_desk_properties.py
+++ b/frappe/core/doctype/role/patches/v13_set_default_desk_properties.py
@@ -2,9 +2,10 @@ import frappe
from ..role import desk_properties
def execute():
+ frappe.reload_doctype('user')
frappe.reload_doctype('role')
for role in frappe.get_all('Role', ['name', 'desk_access']):
role_doc = frappe.get_doc('Role', role.name)
for key in desk_properties:
role_doc.set(key, role_doc.desk_access)
- role_doc.save()
\ No newline at end of file
+ role_doc.save()
diff --git a/frappe/core/doctype/role/role.json b/frappe/core/doctype/role/role.json
index 0135cbf9e8..ba82e023a9 100644
--- a/frappe/core/doctype/role/role.json
+++ b/frappe/core/doctype/role/role.json
@@ -17,7 +17,6 @@
"navigation_settings_section",
"search_bar",
"notifications",
- "chat",
"list_settings_section",
"list_sidebar",
"bulk_actions",
@@ -85,12 +84,6 @@
"fieldtype": "Check",
"label": "Search Bar"
},
- {
- "default": "1",
- "fieldname": "chat",
- "fieldtype": "Check",
- "label": "Chat"
- },
{
"fieldname": "list_settings_section",
"fieldtype": "Section Break",
@@ -155,10 +148,11 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-01-27 10:35:37.638350",
+ "modified": "2021-10-08 14:06:55.729364",
"modified_by": "Administrator",
"module": "Core",
"name": "Role",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py
index 28b444e1e7..389e18dd4c 100644
--- a/frappe/core/doctype/role/role.py
+++ b/frappe/core/doctype/role/role.py
@@ -1,16 +1,23 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
-
from frappe.model.document import Document
-desk_properties = ("search_bar", "notifications", "chat", "list_sidebar",
+desk_properties = ("search_bar", "notifications", "list_sidebar",
"bulk_actions", "view_switcher", "form_sidebar", "timeline", "dashboard")
+STANDARD_ROLES = (
+ "Administrator",
+ "System Manager",
+ "Script Manager",
+ "All",
+ "Guest"
+)
+
class Role(Document):
def before_rename(self, old, new, merge=False):
- if old in ("Guest", "Administrator", "System Manager", "All"):
+ if old in STANDARD_ROLES:
frappe.throw(frappe._("Standard roles cannot be renamed"))
def after_insert(self):
@@ -23,7 +30,7 @@ class Role(Document):
self.set_desk_properties()
def disable_role(self):
- if self.name in ("Guest", "Administrator", "System Manager", "All"):
+ if self.name in STANDARD_ROLES:
frappe.throw(frappe._("Standard roles cannot be disabled"))
else:
self.remove_roles()
@@ -82,4 +89,4 @@ def role_query(doctype, txt, searchfield, start, page_len, filters):
report_filters.extend(filters)
return frappe.get_all('Role', limit_start=start, limit_page_length=page_len,
- filters=report_filters, as_list=1)
\ No newline at end of file
+ filters=report_filters, as_list=1)
diff --git a/frappe/core/doctype/role/test_role.py b/frappe/core/doctype/role/test_role.py
index 471f6cac43..1671f9a9c8 100644
--- a/frappe/core/doctype/role/test_role.py
+++ b/frappe/core/doctype/role/test_role.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py b/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py
index 59f34a1483..cd9a6dc0fa 100644
--- a/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py
+++ b/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.core.doctype.report.report import is_prepared_report_disabled
diff --git a/frappe/core/doctype/role_profile/role_profile.json b/frappe/core/doctype/role_profile/role_profile.json
index 4b3f35aa57..7cd60a16d1 100644
--- a/frappe/core/doctype/role_profile/role_profile.json
+++ b/frappe/core/doctype/role_profile/role_profile.json
@@ -1,175 +1,80 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "role_profile",
- "beta": 0,
- "creation": "2017-08-31 04:16:38.764465",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "autoname": "role_profile",
+ "creation": "2017-08-31 04:16:38.764465",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "role_profile",
+ "roles_html",
+ "roles"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "role_profile",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Role Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
+ "fieldname": "role_profile",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Role Name",
+ "reqd": 1,
"unique": 1
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "roles_html",
- "fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Roles HTML",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "roles_html",
+ "fieldtype": "HTML",
+ "label": "Roles HTML",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "roles",
- "fieldtype": "Table",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Roles Assigned",
- "length": 0,
- "no_copy": 0,
- "options": "Has Role",
- "permlevel": 1,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "roles",
+ "fieldtype": "Table",
+ "hidden": 1,
+ "label": "Roles Assigned",
+ "options": "Has Role",
+ "permlevel": 1,
+ "print_hide": 1,
+ "read_only": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2017-10-17 11:05:11.183066",
- "modified_by": "Administrator",
- "module": "Core",
- "name": "Role Profile",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "links": [
+ {
+ "link_doctype": "User",
+ "link_fieldname": "role_profile_name"
+ }
+ ],
+ "modified": "2021-12-03 15:45:45.270963",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Role Profile",
+ "naming_rule": "Expression (old style)",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "email": 1,
+ "export": 1,
+ "permlevel": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "title_field": "role_profile",
- "track_changes": 1,
- "track_seen": 0
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "role_profile",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/core/doctype/role_profile/role_profile.py b/frappe/core/doctype/role_profile/role_profile.py
index 0f58da5b5e..cb0a43d68f 100644
--- a/frappe/core/doctype/role_profile/role_profile.py
+++ b/frappe/core/doctype/role_profile/role_profile.py
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from frappe.model.document import Document
+import frappe
class RoleProfile(Document):
def autoname(self):
@@ -11,5 +12,9 @@ class RoleProfile(Document):
def on_update(self):
""" Changes in role_profile reflected across all its user """
- from frappe.core.doctype.user.user import update_roles
- update_roles(self.name)
+ users = frappe.get_all('User', filters={'role_profile_name': self.name})
+ roles = [role.role for role in self.roles]
+ for d in users:
+ user = frappe.get_doc('User', d)
+ user.set('roles', [])
+ user.add_roles(*roles)
diff --git a/frappe/core/doctype/role_profile/test_role_profile.py b/frappe/core/doctype/role_profile/test_role_profile.py
index 53e0a1b043..b208a186de 100644
--- a/frappe/core/doctype/role_profile/test_role_profile.py
+++ b/frappe/core/doctype/role_profile/test_role_profile.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
@@ -8,6 +8,7 @@ test_dependencies = ['Role']
class TestRoleProfile(unittest.TestCase):
def test_make_new_role_profile(self):
+ frappe.delete_doc_if_exists('Role Profile', 'Test 1', force=1)
new_role_profile = frappe.get_doc(dict(doctype='Role Profile', role_profile='Test 1')).insert()
self.assertEqual(new_role_profile.role_profile, 'Test 1')
@@ -19,7 +20,25 @@ class TestRoleProfile(unittest.TestCase):
new_role_profile.save()
self.assertEqual(new_role_profile.roles[0].role, '_Test Role 2')
+ # user with a role profile
+ random_user = frappe.mock("email")
+ random_user_name = frappe.mock("name")
+
+ random_user = frappe.get_doc({
+ "doctype": "User",
+ "email": random_user,
+ "enabled": 1,
+ "first_name": random_user_name,
+ "new_password": "Eastern_43A1W",
+ "role_profile_name": 'Test 1'
+ }).insert(ignore_permissions=True, ignore_if_duplicate=True)
+ self.assertListEqual([role.role for role in random_user.roles], [role.role for role in new_role_profile.roles])
+
# clear roles
new_role_profile.roles = []
new_role_profile.save()
self.assertEqual(new_role_profile.roles, [])
+
+ # user roles with the role profile should also be updated
+ random_user.reload()
+ self.assertListEqual(random_user.roles, [])
\ No newline at end of file
diff --git a/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json
index f86a4c8884..396b32bdf9 100644
--- a/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json
+++ b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json
@@ -38,7 +38,7 @@
}
],
"links": [],
- "modified": "2020-01-22 00:00:00.000000",
+ "modified": "2021-10-25 00:00:00.000000",
"modified_by": "Administrator",
"module": "Core",
"name": "Scheduled Job Log",
@@ -59,6 +59,5 @@
],
"quick_entry": 1,
"sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
+ "sort_order": "DESC"
}
diff --git a/frappe/core/doctype/scheduled_job_log/scheduled_job_log.py b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.py
index 7f54a3b6ae..bd5c15bc31 100644
--- a/frappe/core/doctype/scheduled_job_log/scheduled_job_log.py
+++ b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/scheduled_job_log/test_scheduled_job_log.py b/frappe/core/doctype/scheduled_job_log/test_scheduled_job_log.py
index 85471d0d71..9957f6c34c 100644
--- a/frappe/core/doctype/scheduled_job_log/test_scheduled_job_log.py
+++ b/frappe/core/doctype/scheduled_job_log/test_scheduled_job_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
index b6515b1e79..1a795bab82 100644
--- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
+++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
@@ -1,5 +1,5 @@
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import json
from datetime import datetime
diff --git a/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py
index a071cfe9a9..a11966c47e 100644
--- a/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py
+++ b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
from frappe.utils import get_datetime
@@ -10,7 +10,7 @@ from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
class TestScheduledJobType(unittest.TestCase):
def setUp(self):
frappe.db.rollback()
- frappe.db.sql('truncate `tabScheduled Job Type`')
+ frappe.db.truncate("Scheduled Job Type")
sync_jobs()
frappe.db.commit()
diff --git a/frappe/core/doctype/server_script/server_script.js b/frappe/core/doctype/server_script/server_script.js
index dda39115bf..ca34af11ab 100644
--- a/frappe/core/doctype/server_script/server_script.js
+++ b/frappe/core/doctype/server_script/server_script.js
@@ -10,6 +10,13 @@ frappe.ui.form.on('Server Script', {
frm.dashboard.hide();
}
+ if (!frm.is_new()) {
+ frm.add_custom_button(__('Compare Versions'), () => {
+ new frappe.ui.DiffView("Server Script", "script", frm.doc.name);
+ });
+ }
+
+
frm.call('get_autocompletion_items')
.then(r => r.message)
.then(items => {
diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json
index b7e49673f8..520c0008c5 100644
--- a/frappe/core/doctype/server_script/server_script.json
+++ b/frappe/core/doctype/server_script/server_script.json
@@ -13,6 +13,7 @@
"api_method",
"allow_guest",
"column_break_3",
+ "module",
"disabled",
"section_break_8",
"script",
@@ -93,6 +94,12 @@
"label": "Event Frequency",
"mandatory_depends_on": "eval:doc.script_type == \"Scheduler Event\"",
"options": "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long"
+ },
+ {
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module (for export)",
+ "options": "Module Def"
}
],
"index_web_pages_for_search": 1,
@@ -102,7 +109,7 @@
"link_fieldname": "server_script"
}
],
- "modified": "2021-02-18 12:36:19.803425",
+ "modified": "2021-09-04 12:02:43.671240",
"modified_by": "Administrator",
"module": "Core",
"name": "Server Script",
diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py
index d26fe5a188..5b1aab1241 100644
--- a/frappe/core/doctype/server_script/server_script.py
+++ b/frappe/core/doctype/server_script/server_script.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import ast
from types import FunctionType, MethodType, ModuleType
@@ -15,7 +15,6 @@ from frappe import _
class ServerScript(Document):
def validate(self):
frappe.only_for("Script Manager", True)
- self.validate_script()
self.sync_scheduled_jobs()
self.clear_scheduled_events()
@@ -28,6 +27,11 @@ class ServerScript(Document):
for job in self.scheduled_jobs:
frappe.delete_doc("Scheduled Job Type", job.name)
+ def get_code_fields(self):
+ return {
+ 'script': 'py'
+ }
+
@property
def scheduled_jobs(self) -> List[Dict[str, str]]:
return frappe.get_all(
@@ -36,10 +40,6 @@ class ServerScript(Document):
fields=["name", "stopped"],
)
- def validate_script(self):
- """Utilizes the ast module to check for syntax errors
- """
- ast.parse(self.script)
def sync_scheduled_jobs(self):
"""Sync Scheduled Job Type statuses if Server Script's disabled status is changed
@@ -94,7 +94,7 @@ class ServerScript(Document):
Args:
doc (Document): Executes script with for a certain document's events
"""
- safe_exec(self.script, _locals={"doc": doc})
+ safe_exec(self.script, _locals={"doc": doc}, restrict_commit_rollback=True)
def execute_scheduled_method(self):
"""Specific to Scheduled Jobs via Server Scripts
diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py
index c39fcfa0d0..bc92061f42 100644
--- a/frappe/core/doctype/server_script/test_server_script.py
+++ b/frappe/core/doctype/server_script/test_server_script.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
import requests
@@ -59,6 +59,16 @@ conditions = '1 = 1'
reference_doctype = 'Note',
script = '''
frappe.method_that_doesnt_exist("do some magic")
+'''
+ ),
+ dict(
+ name='test_todo_commit',
+ script_type = 'DocType Event',
+ doctype_event = 'Before Save',
+ reference_doctype = 'ToDo',
+ disabled = 1,
+ script = '''
+frappe.db.commit()
'''
)
]
@@ -66,7 +76,7 @@ class TestServerScript(unittest.TestCase):
@classmethod
def setUpClass(cls):
frappe.db.commit()
- frappe.db.sql('truncate `tabServer Script`')
+ frappe.db.truncate("Server Script")
frappe.get_doc('User', 'Administrator').add_roles('Script Manager')
for script in scripts:
script_doc = frappe.get_doc(doctype ='Server Script')
@@ -78,7 +88,7 @@ class TestServerScript(unittest.TestCase):
@classmethod
def tearDownClass(cls):
frappe.db.commit()
- frappe.db.sql('truncate `tabServer Script`')
+ frappe.db.truncate("Server Script")
frappe.cache().delete_value('server_script_map')
def setUp(self):
@@ -102,10 +112,30 @@ class TestServerScript(unittest.TestCase):
self.assertEqual(frappe.get_doc('Server Script', 'test_return_value').execute_method(), 'hello')
def test_permission_query(self):
- self.assertTrue('where (1 = 1)' in frappe.db.get_list('ToDo', return_query=1))
+ self.assertTrue('where (1 = 1)' in frappe.db.get_list('ToDo', run=False))
self.assertTrue(isinstance(frappe.db.get_list('ToDo'), list))
def test_attribute_error(self):
"""Raise AttributeError if method not found in Namespace"""
note = frappe.get_doc({"doctype": "Note", "title": "Test Note: Server Script"})
self.assertRaises(AttributeError, note.insert)
+
+ def test_syntax_validation(self):
+ server_script = scripts[0]
+ server_script["script"] = "js || code.?"
+
+ with self.assertRaises(frappe.ValidationError) as se:
+ frappe.get_doc(doctype="Server Script", **server_script).insert()
+
+ self.assertTrue("invalid python code" in str(se.exception).lower(),
+ msg="Python code validation not working")
+
+ def test_commit_in_doctype_event(self):
+ server_script = frappe.get_doc('Server Script', 'test_todo_commit')
+ server_script.disabled = 0
+ server_script.save()
+
+ self.assertRaises(AttributeError, frappe.get_doc(dict(doctype='ToDo', description='test me')).insert)
+
+ server_script.disabled = 1
+ server_script.save()
diff --git a/frappe/core/doctype/session_default/session_default.py b/frappe/core/doctype/session_default/session_default.py
index 70ff103111..9470a1bb38 100644
--- a/frappe/core/doctype/session_default/session_default.py
+++ b/frappe/core/doctype/session_default/session_default.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/session_default_settings/session_default_settings.py b/frappe/core/doctype/session_default_settings/session_default_settings.py
index 25f7522c86..52c917223e 100644
--- a/frappe/core/doctype/session_default_settings/session_default_settings.py
+++ b/frappe/core/doctype/session_default_settings/session_default_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
diff --git a/frappe/core/doctype/session_default_settings/test_session_default_settings.py b/frappe/core/doctype/session_default_settings/test_session_default_settings.py
index 7d20015b66..7a7e971aed 100644
--- a/frappe/core/doctype/session_default_settings/test_session_default_settings.py
+++ b/frappe/core/doctype/session_default_settings/test_session_default_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
from frappe.core.doctype.session_default_settings.session_default_settings import set_session_default_values, clear_session_defaults
diff --git a/frappe/core/doctype/sms_parameter/sms_parameter.py b/frappe/core/doctype/sms_parameter/sms_parameter.py
index d1fb1c53db..fb8466eac6 100644
--- a/frappe/core/doctype/sms_parameter/sms_parameter.py
+++ b/frappe/core/doctype/sms_parameter/sms_parameter.py
@@ -1,5 +1,5 @@
-# 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
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/core/doctype/sms_settings/sms_settings.json b/frappe/core/doctype/sms_settings/sms_settings.json
index 073fb88bc7..d29949af45 100755
--- a/frappe/core/doctype/sms_settings/sms_settings.json
+++ b/frappe/core/doctype/sms_settings/sms_settings.json
@@ -1,238 +1,80 @@
{
- "allow_copy": 1,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2013-01-10 16:34:24",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "editable_grid": 0,
+ "actions": [],
+ "allow_copy": 1,
+ "creation": "2013-01-10 16:34:24",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "sms_gateway_url",
+ "message_parameter",
+ "receiver_parameter",
+ "static_parameters_section",
+ "parameters",
+ "use_post"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "Eg. smsgateway.com/api/send_sms.cgi",
- "fieldname": "sms_gateway_url",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "SMS Gateway URL",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "description": "Eg. smsgateway.com/api/send_sms.cgi",
+ "fieldname": "sms_gateway_url",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "SMS Gateway URL",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "Enter url parameter for message",
- "fieldname": "message_parameter",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Message Parameter",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "description": "Enter url parameter for message",
+ "fieldname": "message_parameter",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Message Parameter",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "Enter url parameter for receiver nos",
- "fieldname": "receiver_parameter",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Receiver Parameter",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "description": "Enter url parameter for receiver nos",
+ "fieldname": "receiver_parameter",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Receiver Parameter",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "static_parameters_section",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "fieldname": "static_parameters_section",
+ "fieldtype": "Column Break",
"width": "50%"
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "Enter static url parameters here (Eg. sender=ERPNext, username=ERPNext, password=1234 etc.)",
- "fieldname": "parameters",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Static Parameters",
- "length": 0,
- "no_copy": 0,
- "options": "SMS Parameter",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "description": "Enter static url parameters here (Eg. sender=ERPNext, username=ERPNext, password=1234 etc.)",
+ "fieldname": "parameters",
+ "fieldtype": "Table",
+ "label": "Static Parameters",
+ "options": "SMS Parameter"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "use_post",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Use POST",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "default": "0",
+ "fieldname": "use_post",
+ "fieldtype": "Check",
+ "label": "Use POST"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-cog",
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 1,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2021-03-02 18:06:00.868688",
- "modified_by": "Administrator",
- "module": "Core",
- "name": "SMS Settings",
- "owner": "Administrator",
+ ],
+ "icon": "fa fa-cog",
+ "idx": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2021-09-21 19:45:26.809793",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "SMS Settings",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 0,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "track_changes": 1,
- "track_seen": 0
-}
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/sms_settings/sms_settings.py b/frappe/core/doctype/sms_settings/sms_settings.py
index 58a0ff08f6..f15ba7e4f6 100644
--- a/frappe/core/doctype/sms_settings/sms_settings.py
+++ b/frappe/core/doctype/sms_settings/sms_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# 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
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/core/doctype/sms_settings/test_sms_settings.py b/frappe/core/doctype/sms_settings/test_sms_settings.py
index 862f5e3965..b3be912f9e 100644
--- a/frappe/core/doctype/sms_settings/test_sms_settings.py
+++ b/frappe/core/doctype/sms_settings/test_sms_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/core/doctype/success_action/success_action.py b/frappe/core/doctype/success_action/success_action.py
index 4ebd3d250b..afb3a87485 100644
--- a/frappe/core/doctype/success_action/success_action.py
+++ b/frappe/core/doctype/success_action/success_action.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from frappe.model.document import Document
diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index 4b53983702..dcec9b13c2 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -23,6 +23,7 @@
"currency_precision",
"sec_backup_limit",
"backup_limit",
+ "encrypt_backup",
"background_workers",
"enable_scheduler",
"dormant_days",
@@ -66,8 +67,8 @@
"prepared_report_section",
"enable_prepared_report_auto_deletion",
"prepared_report_expiry_period",
- "chat",
- "enable_chat"
+ "system_updates_section",
+ "disable_system_update_notification"
],
"fields": [
{
@@ -381,18 +382,6 @@
"fieldtype": "Check",
"label": "Hide footer in auto email reports"
},
- {
- "collapsible": 1,
- "fieldname": "chat",
- "fieldtype": "Section Break",
- "label": "Chat"
- },
- {
- "default": "1",
- "fieldname": "enable_chat",
- "fieldtype": "Check",
- "label": "Enable Chat"
- },
{
"fieldname": "column_break_21",
"fieldtype": "Column Break"
@@ -469,12 +458,30 @@
"fieldname": "strip_exif_metadata_from_uploaded_images",
"fieldtype": "Check",
"label": "Strip EXIF tags from uploaded images"
+ },
+ {
+ "default": "0",
+ "fieldname": "encrypt_backup",
+ "fieldtype": "Check",
+ "label": "Encrypt Backups"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "system_updates_section",
+ "fieldtype": "Section Break",
+ "label": "System Updates"
+ },
+ {
+ "default": "0",
+ "fieldname": "disable_system_update_notification",
+ "fieldtype": "Check",
+ "label": "Disable System Update Notification"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
- "modified": "2021-03-30 11:47:47.330437",
+ "modified": "2021-11-29 18:09:53.601629",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
@@ -492,4 +499,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py
index 466914569f..1ae8e9e79e 100644
--- a/frappe/core/doctype/system_settings/system_settings.py
+++ b/frappe/core/doctype/system_settings/system_settings.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
diff --git a/frappe/core/doctype/system_settings/test_system_settings.py b/frappe/core/doctype/system_settings/test_system_settings.py
index a65c602abe..f95e26b793 100644
--- a/frappe/core/doctype/system_settings/test_system_settings.py
+++ b/frappe/core/doctype/system_settings/test_system_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/core/doctype/test/test.py b/frappe/core/doctype/test/test.py
index 98e36e6a30..4cb088c117 100644
--- a/frappe/core/doctype/test/test.py
+++ b/frappe/core/doctype/test/test.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/test/test_test.py b/frappe/core/doctype/test/test_test.py
index d8ca975d63..d8508b8651 100644
--- a/frappe/core/doctype/test/test_test.py
+++ b/frappe/core/doctype/test/test_test.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/core/doctype/transaction_log/test_transaction_log.py b/frappe/core/doctype/transaction_log/test_transaction_log.py
index 0d9b9353d0..c332a82f65 100644
--- a/frappe/core/doctype/transaction_log/test_transaction_log.py
+++ b/frappe/core/doctype/transaction_log/test_transaction_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
import hashlib
diff --git a/frappe/core/doctype/transaction_log/transaction_log.py b/frappe/core/doctype/transaction_log/transaction_log.py
index 58d0b3d176..0a480f6660 100644
--- a/frappe/core/doctype/transaction_log/transaction_log.py
+++ b/frappe/core/doctype/transaction_log/transaction_log.py
@@ -1,12 +1,14 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2018, Frappe Technologies and contributors
-# For license information, please see license.txt
+# Copyright (c) 2021, Frappe Technologies and contributors
+# License: MIT. See LICENSE
+
+import hashlib
import frappe
from frappe import _
from frappe.model.document import Document
+from frappe.query_builder import DocType
from frappe.utils import cint, now_datetime
-import hashlib
+
class TransactionLog(Document):
def before_insert(self):
@@ -14,10 +16,9 @@ class TransactionLog(Document):
self.row_index = index
self.timestamp = now_datetime()
if index != 1:
- prev_hash = frappe.db.sql(
- "SELECT `chaining_hash` FROM `tabTransaction Log` WHERE `row_index` = '{0}'".format(index - 1))
+ prev_hash = frappe.get_all("Transaction Log", filters={"row_index":str(index-1)}, pluck="chaining_hash", limit=1)
if prev_hash:
- self.previous_hash = prev_hash[0][0]
+ self.previous_hash = prev_hash[0]
else:
self.previous_hash = "Indexing broken"
else:
@@ -29,26 +30,27 @@ class TransactionLog(Document):
def hash_line(self):
sha = hashlib.sha256()
sha.update(
- frappe.safe_encode(str(self.row_index)) + \
- frappe.safe_encode(str(self.timestamp)) + \
- frappe.safe_encode(str(self.data))
+ frappe.safe_encode(str(self.row_index))
+ + frappe.safe_encode(str(self.timestamp))
+ + frappe.safe_encode(str(self.data))
)
return sha.hexdigest()
def hash_chain(self):
sha = hashlib.sha256()
- sha.update(
- frappe.safe_encode(str(self.transaction_hash)) + \
- frappe.safe_encode(str(self.previous_hash))
- )
+ sha.update(frappe.safe_encode(str(self.transaction_hash)) + frappe.safe_encode(str(self.previous_hash)))
return sha.hexdigest()
def get_current_index():
- current = frappe.db.sql("""SELECT `current`
- FROM `tabSeries`
- WHERE `name` = 'TRANSACTLOG'
- FOR UPDATE""")
+ series = DocType("Series")
+ current = (
+ frappe.qb.from_(series)
+ .where(series.name == "TRANSACTLOG")
+ .for_update()
+ .select("current")
+ ).run()
+
if current and current[0][0] is not None:
current = current[0][0]
diff --git a/frappe/core/doctype/translation/test_translation.py b/frappe/core/doctype/translation/test_translation.py
index ae1293b38f..982d9bf976 100644
--- a/frappe/core/doctype/translation/test_translation.py
+++ b/frappe/core/doctype/translation/test_translation.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
@@ -8,7 +8,7 @@ from frappe import _
class TestTranslation(unittest.TestCase):
def setUp(self):
- frappe.db.sql('delete from tabTranslation')
+ frappe.db.delete("Translation")
def tearDown(self):
frappe.local.lang = 'en'
diff --git a/frappe/core/doctype/translation/translation.py b/frappe/core/doctype/translation/translation.py
index b1f4642791..a01552903c 100644
--- a/frappe/core/doctype/translation/translation.py
+++ b/frappe/core/doctype/translation/translation.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/user/test_records.json b/frappe/core/doctype/user/test_records.json
index f9033d4660..21fe3ff69d 100644
--- a/frappe/core/doctype/user/test_records.json
+++ b/frappe/core/doctype/user/test_records.json
@@ -70,5 +70,19 @@
"role": "System Manager"
}
]
- }
+ },
+ {
+ "doctype": "User",
+ "email": "testpassword@example.com",
+ "enabled": 1,
+ "first_name": "_Test",
+ "new_password": "Eastern_43A1W",
+ "roles": [
+ {
+ "doctype": "Has Role",
+ "parentfield": "roles",
+ "role": "System Manager"
+ }
+ ]
+ }
]
diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py
index 392128834d..b3c85b22a1 100644
--- a/frappe/core/doctype/user/test_user.py
+++ b/frappe/core/doctype/user/test_user.py
@@ -1,16 +1,18 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-import frappe, unittest, uuid
+# License: MIT. See LICENSE
+import json
+import unittest
+from unittest.mock import patch
-from frappe.model.delete_doc import delete_doc
-from frappe.utils.data import today, add_to_date
-from frappe import _dict
-from frappe.utils import get_url
-from frappe.core.doctype.user.user import get_total_users
-from frappe.core.doctype.user.user import MaxUsersReachedError, test_password_strength
-from frappe.core.doctype.user.user import extract_mentions
+import frappe
+import frappe.exceptions
+from frappe.core.doctype.user.user import (extract_mentions, reset_password,
+ sign_up, test_password_strength, update_password, verify_password)
from frappe.frappeclient import FrappeClient
+from frappe.model.delete_doc import delete_doc
+from frappe.utils import get_url
+user_module = frappe.core.doctype.user.user
test_records = frappe.get_test_records('User')
class TestUser(unittest.TestCase):
@@ -23,7 +25,7 @@ class TestUser(unittest.TestCase):
def test_user_type(self):
new_user = frappe.get_doc(dict(doctype='User', email='test-for-type@example.com',
- first_name='Tester')).insert()
+ first_name='Tester')).insert(ignore_if_duplicate=True)
self.assertEqual(new_user.user_type, 'Website User')
# social login userid for frappe
@@ -52,7 +54,7 @@ class TestUser(unittest.TestCase):
def test_delete(self):
frappe.get_doc("User", "test@example.com").add_roles("_Test Role 2")
self.assertRaises(frappe.LinkExistsError, delete_doc, "Role", "_Test Role 2")
- frappe.db.sql("""delete from `tabHas Role` where role='_Test Role 2'""")
+ frappe.db.delete("Has Role", {"role": "_Test Role 2"})
delete_doc("Role","_Test Role 2")
if frappe.db.exists("User", "_test@example.com"):
@@ -119,40 +121,9 @@ class TestUser(unittest.TestCase):
# system manager now added by Administrator
self.assertTrue("System Manager" in [d.role for d in me.get("roles")])
- # def test_deny_multiple_sessions(self):
- # from frappe.installer import update_site_config
- # clear_limit('users')
- #
- # # allow one session
- # user = frappe.get_doc('User', 'test@example.com')
- # user.simultaneous_sessions = 1
- # user.new_password = 'Eastern_43A1W'
- # user.save()
- #
- # def test_request(conn):
- # value = conn.get_value('User', 'first_name', {'name': 'test@example.com'})
- # self.assertTrue('first_name' in value)
- #
- # from frappe.frappeclient import FrappeClient
- # update_site_config('deny_multiple_sessions', 0)
- #
- # conn1 = FrappeClient(get_url(), "test@example.com", "Eastern_43A1W", verify=False)
- # test_request(conn1)
- #
- # conn2 = FrappeClient(get_url(), "test@example.com", "Eastern_43A1W", verify=False)
- # test_request(conn2)
- #
- # update_site_config('deny_multiple_sessions', 1)
- # conn3 = FrappeClient(get_url(), "test@example.com", "Eastern_43A1W", verify=False)
- # test_request(conn3)
- #
- # # first connection should fail
- # test_request(conn1)
-
-
def test_delete_user(self):
new_user = frappe.get_doc(dict(doctype='User', email='test-for-delete@example.com',
- first_name='Tester Delete User')).insert()
+ first_name='Tester Delete User')).insert(ignore_if_duplicate=True)
self.assertEqual(new_user.user_type, 'Website User')
# role with desk access
@@ -174,7 +145,7 @@ class TestUser(unittest.TestCase):
self.assertFalse(frappe.db.exists('User', new_user.name))
def test_password_strength(self):
- # Test Password without Password Strenth Policy
+ # Test Password without Password Strength Policy
frappe.db.set_value("System Settings", "System Settings", "enable_password_policy", 0)
# password policy is disabled, test_password_strength should be ignored
@@ -193,6 +164,17 @@ class TestUser(unittest.TestCase):
result = test_password_strength("Eastern_43A1W")
self.assertEqual(result['feedback']['password_policy_validation_passed'], True)
+
+ # test password strength while saving user with new password
+ user = frappe.get_doc("User", "test@example.com")
+ frappe.flags.in_test = False
+ user.new_password = "password"
+ self.assertRaisesRegex(frappe.exceptions.ValidationError, "Invalid Password", user.save)
+ user.reload()
+ user.new_password = "Eastern_43A1W"
+ user.save()
+ frappe.flags.in_test = True
+
def test_comment_mentions(self):
comment = '''
@@ -227,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',
@@ -236,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
'''
@@ -264,35 +251,128 @@ class TestUser(unittest.TestCase):
c = FrappeClient(url)
res1 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
res2 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
- self.assertEqual(res1.status_code, 200)
+ self.assertEqual(res1.status_code, 400)
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..5b3a1affd9 100644
--- a/frappe/core/doctype/user/user.js
+++ b/frappe/core/doctype/user/user.js
@@ -50,7 +50,7 @@ frappe.ui.form.on('User', {
let d = frm.add_child("block_modules");
d.module = v.module;
});
- frm.module_editor && frm.module_editor.refresh();
+ frm.module_editor && frm.module_editor.show();
}
});
}
@@ -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
}
@@ -180,7 +180,7 @@ frappe.ui.form.on('User', {
frm.roles_editor.show();
}
- frm.module_editor && frm.module_editor.refresh();
+ frm.module_editor && frm.module_editor.show();
if(frappe.session.user==doc.name) {
// update display settings
@@ -263,6 +263,7 @@ frappe.ui.form.on('User', {
callback: function(r) {
if (r.message) {
frappe.msgprint(__("Save API Secret: {0}", [r.message.api_secret]));
+ frm.reload_doc();
}
}
});
diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json
index 1d5f89897d..cf05ce0c15 100644
--- a/frappe/core/doctype/user/user.json
+++ b/frappe/core/doctype/user/user.json
@@ -202,7 +202,8 @@
"fieldname": "role_profile_name",
"fieldtype": "Link",
"label": "Role Profile",
- "options": "Role Profile"
+ "options": "Role Profile",
+ "permlevel": 1
},
{
"fieldname": "roles_html",
@@ -554,20 +555,22 @@
"collapsible": 1,
"fieldname": "api_access",
"fieldtype": "Section Break",
- "label": "Api Access"
+ "label": "API Access"
},
{
- "description": "API Key cannot be regenerated",
+ "description": "API Key cannot be regenerated",
"fieldname": "api_key",
"fieldtype": "Data",
"label": "API Key",
+ "permlevel": 1,
"read_only": 1,
"unique": 1
},
{
"fieldname": "generate_keys",
"fieldtype": "Button",
- "label": "Generate Keys"
+ "label": "Generate Keys",
+ "permlevel": 1
},
{
"fieldname": "column_break_65",
@@ -577,6 +580,7 @@
"fieldname": "api_secret",
"fieldtype": "Password",
"label": "API Secret",
+ "permlevel": 1,
"read_only": 1
},
{
@@ -595,7 +599,7 @@
"fieldname": "desk_theme",
"fieldtype": "Select",
"label": "Desk Theme",
- "options": "Light\nDark"
+ "options": "Light\nDark\nAutomatic"
},
{
"fieldname": "module_profile",
@@ -613,11 +617,6 @@
"link_doctype": "Contact",
"link_fieldname": "user"
},
- {
- "group": "Profile",
- "link_doctype": "Chat Profile",
- "link_fieldname": "user"
- },
{
"group": "Profile",
"link_doctype": "Blogger",
@@ -670,7 +669,7 @@
}
],
"max_attachments": 5,
- "modified": "2021-02-02 16:11:06.037543",
+ "modified": "2021-11-17 17:17:16.098457",
"modified_by": "Administrator",
"module": "Core",
"name": "User",
@@ -705,4 +704,4 @@
"sort_order": "DESC",
"title_field": "full_name",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 5d799f8ee9..6c729901e5 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
from bs4 import BeautifulSoup
import frappe
import frappe.share
@@ -15,17 +15,12 @@ from frappe.desk.doctype.notification_settings.notification_settings import crea
from frappe.utils.user import get_system_managers
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
+from frappe.query_builder import DocType
STANDARD_USERS = ("Guest", "Administrator")
-
-class MaxUsersReachedError(frappe.ValidationError):
- pass
-
-
class User(Document):
__new_password = None
@@ -56,8 +51,6 @@ class User(Document):
frappe.cache().delete_key('enabled_users')
def validate(self):
- self.check_demo()
-
# clear new password
self.__new_password = self.new_password
self.new_password = ""
@@ -137,10 +130,6 @@ class User(Document):
"""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]))
@@ -224,15 +213,12 @@ class User(Document):
user_type_doc.update_modules_in_user(self)
def has_desk_access(self):
- '''Return true if any of the set roles has desk access'''
+ """Return true if any of the set roles has desk access"""
if not self.roles:
return False
- return len(frappe.db.sql("""select name
- from `tabRole` where desk_access=1
- and name in ({0}) limit 1""".format(', '.join(['%s'] * len(self.roles))),
- [d.role for d in self.roles]))
-
+ role_table = DocType("Role")
+ return frappe.db.count(role_table, ((role_table.desk_access == 1) & (role_table.name.isin([d.role for d in self.roles]))))
def share_with_self(self):
frappe.share.add(self.doctype, self.name, self.name, write=1, share=1,
@@ -290,12 +276,20 @@ class User(Document):
return link
def get_other_system_managers(self):
- return frappe.db.sql("""select distinct `user`.`name` from `tabHas Role` as `user_role`, `tabUser` as `user`
- where user_role.role='System Manager'
- and `user`.docstatus<2
- and `user`.enabled=1
- and `user_role`.parent = `user`.name
- and `user_role`.parent not in ('Administrator', %s) limit 1""", (self.name,))
+ user_doctype = DocType("User").as_("user")
+ user_role_doctype = DocType("Has Role").as_("user_role")
+ return (
+ frappe.qb.from_(user_doctype)
+ .from_(user_role_doctype)
+ .select(user_doctype.name)
+ .where(user_role_doctype.role == 'System Manager')
+ .where(user_doctype.docstatus < 2)
+ .where(user_doctype.enabled == 1)
+ .where(user_role_doctype.parent == user_doctype.name)
+ .where(user_role_doctype.parent.notin(["Administrator", self.name]))
+ .limit(1)
+ .distinct()
+ ).run()
def get_fullname(self):
"""get first_name space last_name"""
@@ -369,8 +363,12 @@ class User(Document):
# delete todos
frappe.db.delete("ToDo", {"owner": self.name})
- frappe.db.sql("""UPDATE `tabToDo` SET `assigned_by`=NULL WHERE `assigned_by`=%s""",
- (self.name,))
+ todo_table = DocType("ToDo")
+ (
+ frappe.qb.update(todo_table)
+ .set(todo_table.assigned_by, None)
+ .where(todo_table.assigned_by == self.name)
+ ).run()
# delete events
frappe.db.delete("Event", {"owner": self.name, "event_type": "Private"})
@@ -378,15 +376,21 @@ class User(Document):
# delete shares
frappe.db.delete("DocShare", {"user": self.name})
# delete messages
- frappe.db.sql("""delete from `tabCommunication`
- where communication_type in ('Chat', 'Notification')
- and reference_doctype='User'
- and (reference_name=%s or owner=%s)""", (self.name, self.name))
-
+ table = DocType("Communication")
+ frappe.db.delete(
+ table,
+ filters=(
+ (table.communication_type.isin(["Chat", "Notification"]))
+ & (table.reference_doctype == "User")
+ & ((table.reference_name == self.name) | table.owner == self.name)
+ ),
+ run=False,
+ )
# unlink contact
- frappe.db.sql("""update `tabContact`
- set `user`=null
- where `user`=%s""", (self.name))
+ table = DocType("Contact")
+ frappe.qb.update(table).where(
+ table.user == self.name
+ ).set(table.user, None).run()
# delete notification settings
frappe.delete_doc("Notification Settings", self.name, ignore_permissions=True)
@@ -398,7 +402,6 @@ class User(Document):
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)
@@ -427,16 +430,11 @@ class User(Document):
WHERE `%s` = %s""" %
(tab, field, '%s', field, '%s'), (new_name, old_name))
- if frappe.db.exists("Chat Profile", old_name):
- frappe.rename_doc("Chat Profile", old_name, new_name, force=True, show_alert=False)
-
if frappe.db.exists("Notification Settings", old_name):
frappe.rename_doc("Notification Settings", old_name, new_name, force=True, show_alert=False)
# set email
- frappe.db.sql("""UPDATE `tabUser`
- SET email = %s
- WHERE name = %s""", (new_name, new_name))
+ frappe.db.update("User", new_name, "email", new_name)
def append_roles(self, *roles):
"""Add roles to user"""
@@ -706,127 +704,36 @@ def has_email_account(email):
@frappe.whitelist(allow_guest=False)
def get_email_awaiting(user):
- waiting = frappe.db.sql("""select email_account,email_id
- from `tabUser Email`
- where awaiting_password = 1
- and parent = %(user)s""", {"user":user}, as_dict=1)
+ waiting = frappe.get_all("User Email", fields=["email_account", "email_id"], filters={"awaiting_password": 1, "parent": user})
if waiting:
return waiting
else:
- frappe.db.sql("""update `tabUser Email`
- set awaiting_password =0
- where parent = %(user)s""",{"user":user})
+ user_email_table = DocType("User Email")
+ frappe.qb.update(user_email_table).set(user_email_table.user_email_table, 0).where(user_email_table.parent == user).run()
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
- users = frappe.db.sql("""SELECT DISTINCT(parent) as user FROM `tabUser Email`
- WHERE awaiting_password = 1""", as_dict=True)
-
- password_list = [ user.get("user") for user in users ]
+ password_list = frappe.get_all("User Email", filters={"awaiting_password": True}, pluck="parent", distinct=True)
set_default("email_user_password", u','.join(password_list))
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)
@@ -848,14 +755,12 @@ def sign_up(email, full_name, redirect_to):
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)
@@ -887,7 +792,7 @@ def sign_up(email, full_name, redirect_to):
return 2, _("Please ask your administrator to verify your sign-up")
@frappe.whitelist(allow_guest=True)
-@rate_limit(key='user', limit=get_password_reset_limit, seconds = 24*60*60, methods=['POST'])
+@rate_limit(limit=get_password_reset_limit, seconds = 24*60*60, methods=['POST'])
def reset_password(user):
if user=="Administrator":
return 'not allowed'
@@ -903,6 +808,7 @@ def reset_password(user):
return frappe.msgprint(_("Password reset instructions have been sent to your email"))
except frappe.DoesNotExistError:
+ frappe.local.response['http_status_code'] = 400
frappe.clear_messages()
return 'not found'
@@ -979,8 +885,7 @@ def get_active_users():
def get_website_users():
"""Returns total no. of website users"""
- return frappe.db.sql("""select count(*) from `tabUser`
- where enabled = 1 and user_type = 'Website User'""")[0][0]
+ return frappe.db.count("User", filters={"enabled": True, "user_type": "Website User"})
def get_active_website_users():
"""Returns No. of website users who logged in, in the last 3 days"""
@@ -1048,91 +953,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 +970,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,22 +1028,22 @@ 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"]:
+ if theme in ["Dark", "Light", "Automatic"]:
frappe.db.set_value("User", frappe.session.user, "desk_theme", theme)
def get_enabled_users():
@@ -1240,4 +1051,4 @@ 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
+ return frappe.cache().get_value("enabled_users", _get_enabled_users)
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 48dbf87b3d..a14d735e6a 100644
--- a/frappe/core/doctype/user_document_type/user_document_type.py
+++ b/frappe/core/doctype/user_document_type/user_document_type.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# 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 729aa03444..daad083577 100644
--- a/frappe/core/doctype/user_email/user_email.py
+++ b/frappe/core/doctype/user_email/user_email.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
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 2f89d032e1..b5d642ae9c 100644
--- a/frappe/core/doctype/user_group/test_user_group.py
+++ b/frappe/core/doctype/user_group/test_user_group.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# 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 178775d407..05ff71e353 100644
--- a/frappe/core/doctype/user_group/user_group.py
+++ b/frappe/core/doctype/user_group/user_group.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
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 8dbaed9e65..6d4650a3d0 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,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# 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 f85ddc3209..69718d8d91 100644
--- a/frappe/core/doctype/user_group_member/user_group_member.py
+++ b/frappe/core/doctype/user_group_member/user_group_member.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# 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 85db846982..cf905c2ce2 100644
--- a/frappe/core/doctype/user_permission/test_user_permission.py
+++ b/frappe/core/doctype/user_permission/test_user_permission.py
@@ -73,7 +73,7 @@ class TestUserPermission(unittest.TestCase):
def test_for_applicable_on_update_from_apply_to_all(self):
''' Update User Permission from all to some applicable Doctypes'''
user = create_user('test_bulk_creation_update@example.com')
- param = get_params(user,'User', user.name, applicable = ["Chat Room", "Chat Message"])
+ param = get_params(user,'User', user.name, applicable = ["Comment", "Contact"])
# Initially create User Permission document with apply_to_all checked
is_created = add_user_permissions(get_params(user, 'User', user.name))
@@ -84,8 +84,8 @@ class TestUserPermission(unittest.TestCase):
frappe.db.commit()
removed_apply_to_all = frappe.db.exists("User Permission", get_exists_param(user))
- is_created_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Room"))
- is_created_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Message"))
+ is_created_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Comment"))
+ is_created_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Contact"))
# Check that apply_to_all is removed
self.assertIsNone(removed_apply_to_all)
@@ -101,14 +101,14 @@ class TestUserPermission(unittest.TestCase):
param = get_params(user, 'User', user.name)
# create User permissions that with applicable
- is_created = add_user_permissions(get_params(user, 'User', user.name, applicable = ["Chat Room", "Chat Message"]))
+ is_created = add_user_permissions(get_params(user, 'User', user.name, applicable = ["Comment", "Contact"]))
self.assertEqual(is_created, 1)
is_created = add_user_permissions(param)
is_created_apply_to_all = frappe.db.exists("User Permission", get_exists_param(user))
- removed_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Room"))
- removed_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Message"))
+ removed_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Comment"))
+ removed_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Contact"))
# To check that a User permission with apply_to_all exists
self.assertIsNotNone(is_created_apply_to_all)
diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py
index 5201ffef8d..1366ace115 100644
--- a/frappe/core/doctype/user_permission/user_permission.py
+++ b/frappe/core/doctype/user_permission/user_permission.py
@@ -1,5 +1,5 @@
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe, json
from frappe.model.document import Document
@@ -54,7 +54,7 @@ class UserPermission(Document):
ref_link = frappe.get_desk_link(self.doctype, overlap_exists[0].name)
frappe.throw(_("{0} has already assigned default value for {1}.").format(ref_link, self.allow))
-@frappe.whitelist(allow_guest=True)
+@frappe.whitelist()
def get_user_permissions(user=None):
'''Get all users permissions for the user as a dict of doctype'''
# if this is called from client-side,
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 13e3f0d351..18a21931e5 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
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# 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 4a34006d2b..80c0c89383 100644
--- a/frappe/core/doctype/user_social_login/user_social_login.py
+++ b/frappe/core/doctype/user_social_login/user_social_login.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from frappe.model.document import Document
diff --git a/frappe/core/doctype/user_type/test_user_type.py b/frappe/core/doctype/user_type/test_user_type.py
index 1c47f02bbb..7080e1830b 100644
--- a/frappe/core/doctype/user_type/test_user_type.py
+++ b/frappe/core/doctype/user_type/test_user_type.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# 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 82ffb090f1..c1fd678141 100644
--- a/frappe/core/doctype/user_type/user_type.py
+++ b/frappe/core/doctype/user_type/user_type.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
@@ -36,8 +36,11 @@ class UserType(Document):
if not self.user_doctypes:
return
- modules = frappe.get_all('DocType', fields=['distinct module as module'],
- filters={'name': ('in', [d.document_type for d in self.user_doctypes])})
+ modules = frappe.get_all("DocType",
+ fields=["module"],
+ filters={"name": ("in", [d.document_type for d in self.user_doctypes])},
+ distinct=True,
+ )
self.set('user_type_modules', [])
for row in modules:
@@ -192,7 +195,7 @@ def get_user_linked_doctypes(doctype, txt, searchfield, start, page_len, filters
['DocType', 'read_only', '=', 0], ['DocType', 'name', 'like', '%{0}%'.format(txt)]]
doctypes = frappe.get_all('DocType', fields = ['`tabDocType`.`name`'], filters=filters,
- order_by = '`tabDocType`.`idx` desc', limit_start=start, limit_page_length=page_len, as_list=1, debug=1)
+ order_by = '`tabDocType`.`idx` desc', limit_start=start, limit_page_length=page_len, as_list=1)
custom_dt_filters = [['Custom Field', 'dt', 'like', '%{0}%'.format(txt)],
['Custom Field', 'options', '=', 'User'], ['Custom Field', 'fieldtype', '=', 'Link']]
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 9afbcd294d..d25479f869 100644
--- a/frappe/core/doctype/user_type_module/user_type_module.py
+++ b/frappe/core/doctype/user_type_module/user_type_module.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# 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 f6c099c4ea..608dc9f0ab 100644
--- a/frappe/core/doctype/version/test_version.py
+++ b/frappe/core/doctype/version/test_version.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest, copy
from frappe.test_runner import make_test_objects
diff --git a/frappe/core/doctype/version/version.css b/frappe/core/doctype/version/version.css
deleted file mode 100644
index 769b352585..0000000000
--- a/frappe/core/doctype/version/version.css
+++ /dev/null
@@ -1,21 +0,0 @@
-.version-info {
- overflow: auto;
-}
-
-.version-info pre {
- border: 0px;
- margin: 0px;
- background-color: inherit;
-}
-
-.version-info .table {
- background-color: inherit;
-}
-
-.version-info .success {
- background-color: #dff0d8 !important;
-}
-
-.version-info .danger {
- background-color: #f2dede !important;
-}
diff --git a/frappe/core/doctype/version/version.py b/frappe/core/doctype/version/version.py
index a1bd851346..fcb558650a 100644
--- a/frappe/core/doctype/version/version.py
+++ b/frappe/core/doctype/version/version.py
@@ -1,7 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe, json
diff --git a/frappe/core/doctype/view_log/test_view_log.py b/frappe/core/doctype/view_log/test_view_log.py
index 025f3d8ad9..efa9538fbf 100644
--- a/frappe/core/doctype/view_log/test_view_log.py
+++ b/frappe/core/doctype/view_log/test_view_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/core/doctype/view_log/view_log.json b/frappe/core/doctype/view_log/view_log.json
index 6c3247c58f..3c4486c944 100644
--- a/frappe/core/doctype/view_log/view_log.json
+++ b/frappe/core/doctype/view_log/view_log.json
@@ -125,7 +125,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "modified": "2019-09-05 14:22:27.664645",
+ "modified": "2021-10-25 14:22:27.664645",
"modified_by": "Administrator",
"module": "Core",
"name": "View Log",
@@ -158,7 +158,6 @@
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
- "track_changes": 1,
"track_seen": 0,
"track_views": 0
-}
\ No newline at end of file
+}
diff --git a/frappe/core/doctype/view_log/view_log.py b/frappe/core/doctype/view_log/view_log.py
index 242250be8b..fbbd6e1154 100644
--- a/frappe/core/doctype/view_log/view_log.py
+++ b/frappe/core/doctype/view_log/view_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/core/form_tour/doctype/doctype.json b/frappe/core/form_tour/doctype/doctype.json
new file mode 100644
index 0000000000..391d3ecf40
--- /dev/null
+++ b/frappe/core/form_tour/doctype/doctype.json
@@ -0,0 +1,56 @@
+{
+ "creation": "2021-11-23 12:38:52.807353",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "first_document": 0,
+ "idx": 0,
+ "include_name_field": 1,
+ "is_standard": 1,
+ "modified": "2021-11-25 17:03:01.646360",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Doctype",
+ "owner": "Administrator",
+ "reference_doctype": "DocType",
+ "save_on_complete": 1,
+ "steps": [
+ {
+ "description": "Select a Module to which this DocType would belong",
+ "field": "",
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Module",
+ "parent_field": "",
+ "position": "Right",
+ "title": "Module"
+ },
+ {
+ "description": "Check this to make the DocType as Custom",
+ "field": "",
+ "fieldname": "custom",
+ "fieldtype": "Check",
+ "has_next_condition": 1,
+ "is_table_field": 0,
+ "label": "Custom?",
+ "next_step_condition": "eval: doc.custom",
+ "parent_field": "",
+ "position": "Left",
+ "title": "Custom "
+ },
+ {
+ "description": "A Field (or a docfield) defines a property of a DocType. You can define the column name, label, datatype and more for DocFields. For instance, a ToDo doctype has fields description, status and priority. These ultimately become columns in the database table tabToDo.",
+ "field": "",
+ "fieldname": "fields",
+ "fieldtype": "Table",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Fields",
+ "parent_field": "",
+ "position": "Top",
+ "title": "Fields"
+ }
+ ],
+ "title": "Doctype"
+}
\ No newline at end of file
diff --git a/frappe/core/notifications.py b/frappe/core/notifications.py
index 707de43f28..939cf52911 100644
--- a/frappe/core/notifications.py
+++ b/frappe/core/notifications.py
@@ -1,7 +1,10 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
+from frappe.query_builder import DocType, Interval
+from frappe.query_builder.functions import Now
+
def get_notification_config():
return {
@@ -39,28 +42,40 @@ def get_todays_events(as_list=False):
def get_unseen_likes():
"""Returns count of unseen likes"""
- return frappe.db.sql("""select count(*) from `tabComment`
- where
- comment_type='Like'
- and modified >= (NOW() - INTERVAL '1' YEAR)
- and owner is not null and owner!=%(user)s
- and reference_owner=%(user)s
- and seen=0
- """, {"user": frappe.session.user})[0][0]
+
+ comment_doctype = DocType("Comment")
+ return frappe.db.count(comment_doctype,
+ filters=(
+ (comment_doctype.comment_type == "Like")
+ & (comment_doctype.modified >= Now() - Interval(years=1))
+ & (comment_doctype.owner.notnull())
+ & (comment_doctype.owner != frappe.session.user)
+ & (comment_doctype.reference_owner == frappe.session.user)
+ & (comment_doctype.seen == 0)
+ )
+ )
+
def get_unread_emails():
- "returns unread emails for a user"
+ "returns count of unread emails for a user"
- return frappe.db.sql("""\
- SELECT count(*)
- FROM `tabCommunication`
- WHERE communication_type='Communication'
- AND communication_medium='Email'
- AND sent_or_received='Received'
- AND email_status not in ('Spam', 'Trash')
- AND email_account in (
- SELECT distinct email_account from `tabUser Email` WHERE parent=%(user)s
+ communication_doctype = DocType("Communication")
+ user_doctype = DocType("User")
+ distinct_email_accounts = (
+ frappe.qb.from_(user_doctype)
+ .select(user_doctype.email_account)
+ .where(user_doctype.parent == frappe.session.user)
+ .distinct()
+ )
+
+ return frappe.db.count(communication_doctype,
+ filters=(
+ (communication_doctype.communication_type == "Communication")
+ & (communication_doctype.communication_medium == "Email")
+ & (communication_doctype.sent_or_received == "Received")
+ & (communication_doctype.email_status.notin(["spam", "Trash"]))
+ & (communication_doctype.email_account.isin(distinct_email_accounts))
+ & (communication_doctype.modified >= Now() - Interval(years=1))
+ & (communication_doctype.seen == 0)
)
- AND modified >= (NOW() - INTERVAL '1' YEAR)
- AND seen=0
- """, {"user": frappe.session.user})[0][0]
+ )
diff --git a/frappe/core/page/__init__.py b/frappe/core/page/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/core/page/__init__.py
+++ b/frappe/core/page/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py
index 1f3555e351..4d9deca526 100644
--- a/frappe/core/page/background_jobs/background_jobs.py
+++ b/frappe/core/page/background_jobs/background_jobs.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import json
from typing import TYPE_CHECKING, Dict, List
@@ -67,7 +67,8 @@ def get_info(show_failed=False) -> List[Dict]:
fail_registry = queue.failed_job_registry
for job_id in fail_registry.get_job_ids():
job = queue.fetch_job(job_id)
- add_job(job, queue.name)
+ if job:
+ add_job(job, queue.name)
return jobs
diff --git a/frappe/core/page/permission_manager/__init__.py b/frappe/core/page/permission_manager/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/core/page/permission_manager/__init__.py
+++ b/frappe/core/page/permission_manager/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js
index 41cc900a97..6b427fdebf 100644
--- a/frappe/core/page/permission_manager/permission_manager.js
+++ b/frappe/core/page/permission_manager/permission_manager.js
@@ -325,15 +325,15 @@ frappe.PermissionEngine = class PermissionEngine {
.attr("data-doctype", d.parent)
.attr("data-role", d.role)
.attr("data-permlevel", d.permlevel)
- .click(function () {
+ .on("click", () => {
return frappe.call({
module: "frappe.core",
page: "permission_manager",
method: "remove",
args: {
- doctype: $(this).attr("data-doctype"),
- role: $(this).attr("data-role"),
- permlevel: $(this).attr("data-permlevel")
+ doctype: d.parent,
+ role: d.role,
+ permlevel: d.permlevel
},
callback: (r) => {
if (r.exc) {
diff --git a/frappe/core/page/permission_manager/permission_manager.py b/frappe/core/page/permission_manager/permission_manager.py
index 2a99283dda..08642c599e 100644
--- a/frappe/core/page/permission_manager/permission_manager.py
+++ b/frappe/core/page/permission_manager/permission_manager.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
diff --git a/frappe/core/report/__init__.py b/frappe/core/report/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/core/report/__init__.py
+++ b/frappe/core/report/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
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 13602ca777..535d354250 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,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _, throw
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 ff8d8345d6..e9c68cb0c7 100644
--- a/frappe/core/report/transaction_log_report/transaction_log_report.py
+++ b/frappe/core/report/transaction_log_report/transaction_log_report.py
@@ -1,5 +1,5 @@
-# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# Copyright (c) 2021, Frappe Technologies and contributors
+# License: MIT. See LICENSE
import frappe
import hashlib
@@ -12,13 +12,17 @@ def execute(filters=None):
return columns, data
def get_data(filters=None):
-
- logs = frappe.db.sql("SELECT * FROM `tabTransaction Log` order by creation desc ", as_dict=1)
result = []
+ logs = frappe.get_all("Transaction Log", fields=["*"], order_by="creation desc")
+
for l in logs:
row_index = int(l.row_index)
if row_index > 1:
- previous_hash = frappe.db.sql("SELECT chaining_hash FROM `tabTransaction Log` WHERE row_index = {0}".format(row_index - 1))
+ previous_hash = frappe.get_all(
+ "Transaction Log",
+ fields=["chaining_hash"],
+ filters={"row_index": row_index - 1},
+ )
if not previous_hash:
integrity = False
else:
diff --git a/frappe/core/utils.py b/frappe/core/utils.py
index 9b8ee3a326..d4690cae89 100644
--- a/frappe/core/utils.py
+++ b/frappe/core/utils.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/core/workspace/build/build.json b/frappe/core/workspace/build/build.json
index aefda698b1..aabb4f9d1c 100644
--- a/frappe/core/workspace/build/build.json
+++ b/frappe/core/workspace/build/build.json
@@ -1,24 +1,20 @@
{
- "cards_label": "Elements",
- "category": "Modules",
"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}},{\"type\":\"card\",\"data\":{\"card_name\":\"Packages\",\"col\":4}}]",
"creation": "2021-01-02 10:51:16.579957",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends_another_page": 0,
+ "for_user": "",
"hide_custom": 0,
"icon": "tool",
"idx": 0,
- "is_default": 0,
- "is_standard": 1,
"label": "Build",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Modules",
+ "link_count": 0,
"link_type": "DocType",
"onboard": 0,
"only_for": "",
@@ -28,6 +24,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Module Def",
+ "link_count": 0,
"link_to": "Module Def",
"link_type": "DocType",
"onboard": 0,
@@ -38,6 +35,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Workspace",
+ "link_count": 0,
"link_to": "Workspace",
"link_type": "DocType",
"onboard": 0,
@@ -48,6 +46,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Module Onboarding",
+ "link_count": 0,
"link_to": "Module Onboarding",
"link_type": "DocType",
"onboard": 0,
@@ -58,6 +57,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Block Module",
+ "link_count": 0,
"link_to": "Block Module",
"link_type": "DocType",
"onboard": 0,
@@ -68,6 +68,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Models",
+ "link_count": 0,
"link_type": "DocType",
"onboard": 0,
"only_for": "",
@@ -77,6 +78,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "DocType",
+ "link_count": 0,
"link_to": "DocType",
"link_type": "DocType",
"onboard": 0,
@@ -87,6 +89,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Workflow",
+ "link_count": 0,
"link_to": "Workflow",
"link_type": "DocType",
"onboard": 0,
@@ -97,6 +100,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Views",
+ "link_count": 0,
"link_type": "DocType",
"onboard": 0,
"only_for": "",
@@ -106,6 +110,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Report",
+ "link_count": 0,
"link_to": "Report",
"link_type": "DocType",
"onboard": 0,
@@ -116,6 +121,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Print Format",
+ "link_count": 0,
"link_to": "Print Format",
"link_type": "DocType",
"onboard": 0,
@@ -126,6 +132,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Workspace",
+ "link_count": 0,
"link_to": "Workspace",
"link_type": "DocType",
"onboard": 0,
@@ -136,6 +143,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Dashboard",
+ "link_count": 0,
"link_to": "Dashboard",
"link_type": "DocType",
"onboard": 0,
@@ -146,6 +154,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Scripting",
+ "link_count": 0,
"link_type": "DocType",
"onboard": 0,
"only_for": "",
@@ -155,6 +164,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Server Script",
+ "link_count": 0,
"link_to": "Server Script",
"link_type": "DocType",
"onboard": 0,
@@ -165,6 +175,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Client Script",
+ "link_count": 0,
"link_to": "Client Script",
"link_type": "DocType",
"onboard": 0,
@@ -175,20 +186,52 @@
"hidden": 0,
"is_query_report": 0,
"label": "Scheduled Job Type",
+ "link_count": 0,
"link_to": "Scheduled Job Type",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Packages",
+ "link_count": 2,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Package",
+ "link_count": 0,
+ "link_to": "Package",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Package Import",
+ "link_count": 0,
+ "link_to": "Package Import",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
}
],
- "modified": "2021-02-04 13:48:48.493146",
+ "modified": "2021-09-05 21:14:52.384816",
"modified_by": "Administrator",
"module": "Core",
"name": "Build",
"owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
+ "parent_page": "",
+ "public": 1,
+ "restrict_to_domain": "",
+ "roles": [],
+ "sequence_id": 5,
"shortcuts": [
{
"doc_view": "",
@@ -208,5 +251,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..917ce2cbdc 100644
--- a/frappe/core/workspace/settings/settings.json
+++ b/frappe/core/workspace/settings/settings.json
@@ -1,22 +1,20 @@
{
- "category": "Modules",
"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,
"docstatus": 0,
"doctype": "Workspace",
- "extends_another_page": 0,
+ "for_user": "",
"hide_custom": 0,
"icon": "setting",
"idx": 0,
- "is_standard": 1,
"label": "Settings",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Data",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -25,6 +23,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Import Data",
+ "link_count": 0,
"link_to": "Data Import",
"link_type": "DocType",
"onboard": 0,
@@ -35,6 +34,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Export Data",
+ "link_count": 0,
"link_to": "Data Export",
"link_type": "DocType",
"onboard": 0,
@@ -45,6 +45,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Bulk Update",
+ "link_count": 0,
"link_to": "Bulk Update",
"link_type": "DocType",
"onboard": 0,
@@ -55,6 +56,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Download Backups",
+ "link_count": 0,
"link_to": "backups",
"link_type": "Page",
"onboard": 0,
@@ -65,6 +67,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Deleted Documents",
+ "link_count": 0,
"link_to": "Deleted Document",
"link_type": "DocType",
"onboard": 0,
@@ -74,6 +77,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Email / Notifications",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -82,6 +86,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Email Account",
+ "link_count": 0,
"link_to": "Email Account",
"link_type": "DocType",
"onboard": 0,
@@ -92,6 +97,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Email Domain",
+ "link_count": 0,
"link_to": "Email Domain",
"link_type": "DocType",
"onboard": 0,
@@ -102,6 +108,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Notification",
+ "link_count": 0,
"link_to": "Notification",
"link_type": "DocType",
"onboard": 0,
@@ -112,6 +119,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Email Template",
+ "link_count": 0,
"link_to": "Email Template",
"link_type": "DocType",
"onboard": 0,
@@ -122,6 +130,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 +141,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Newsletter",
+ "link_count": 0,
"link_to": "Newsletter",
"link_type": "DocType",
"onboard": 0,
@@ -142,6 +152,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Notification Settings",
+ "link_count": 0,
"link_to": "Notification Settings",
"link_type": "DocType",
"onboard": 0,
@@ -151,6 +162,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Website",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -159,6 +171,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Website Settings",
+ "link_count": 0,
"link_to": "Website Settings",
"link_type": "DocType",
"onboard": 1,
@@ -169,6 +182,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Website Theme",
+ "link_count": 0,
"link_to": "Website Theme",
"link_type": "DocType",
"onboard": 1,
@@ -179,6 +193,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Website Script",
+ "link_count": 0,
"link_to": "Website Script",
"link_type": "DocType",
"onboard": 0,
@@ -189,6 +204,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 +215,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 +225,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Core",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -216,6 +234,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "System Settings",
+ "link_count": 0,
"link_to": "System Settings",
"link_type": "DocType",
"onboard": 0,
@@ -226,6 +245,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Error Log",
+ "link_count": 0,
"link_to": "Error Log",
"link_type": "DocType",
"onboard": 0,
@@ -236,6 +256,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Error Snapshot",
+ "link_count": 0,
"link_to": "Error Snapshot",
"link_type": "DocType",
"onboard": 0,
@@ -246,6 +267,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Domain Settings",
+ "link_count": 0,
"link_to": "Domain Settings",
"link_type": "DocType",
"onboard": 0,
@@ -255,6 +277,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Printing",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -263,6 +286,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 +297,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Print Settings",
+ "link_count": 0,
"link_to": "Print Settings",
"link_type": "DocType",
"onboard": 0,
@@ -283,6 +308,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Print Format",
+ "link_count": 0,
"link_to": "Print Format",
"link_type": "DocType",
"onboard": 0,
@@ -293,6 +319,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Print Style",
+ "link_count": 0,
"link_to": "Print Style",
"link_type": "DocType",
"onboard": 0,
@@ -302,6 +329,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Workflow",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -310,6 +338,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Workflow",
+ "link_count": 0,
"link_to": "Workflow",
"link_type": "DocType",
"onboard": 0,
@@ -320,6 +349,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Workflow State",
+ "link_count": 0,
"link_to": "Workflow State",
"link_type": "DocType",
"onboard": 0,
@@ -330,19 +360,23 @@
"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.456174",
"modified_by": "Administrator",
"module": "Core",
"name": "Settings",
"owner": "Administrator",
- "pin_to_bottom": 1,
- "pin_to_top": 0,
+ "parent_page": "",
+ "public": 1,
+ "restrict_to_domain": "",
+ "roles": [],
+ "sequence_id": 29,
"shortcuts": [
{
"icon": "setting",
@@ -363,5 +397,5 @@
"type": "DocType"
}
],
- "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..85c110151b 100644
--- a/frappe/core/workspace/users/users.json
+++ b/frappe/core/workspace/users/users.json
@@ -1,23 +1,20 @@
{
- "category": "Administration",
"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_another_page": 0,
+ "for_user": "",
"hide_custom": 0,
"icon": "users",
"idx": 0,
- "is_default": 0,
- "is_standard": 1,
"label": "Users",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Users",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -26,6 +23,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "User",
+ "link_count": 0,
"link_to": "User",
"link_type": "DocType",
"onboard": 0,
@@ -36,6 +34,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Role",
+ "link_count": 0,
"link_to": "Role",
"link_type": "DocType",
"onboard": 0,
@@ -46,6 +45,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Role Profile",
+ "link_count": 0,
"link_to": "Role Profile",
"link_type": "DocType",
"onboard": 0,
@@ -55,6 +55,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Logs",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -63,6 +64,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Activity Log",
+ "link_count": 0,
"link_to": "Activity Log",
"link_type": "DocType",
"onboard": 0,
@@ -73,6 +75,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Access Log",
+ "link_count": 0,
"link_to": "Access Log",
"link_type": "DocType",
"onboard": 0,
@@ -82,6 +85,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Permissions",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -90,6 +94,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 +105,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "User Permissions",
+ "link_count": 0,
"link_to": "User Permission",
"link_type": "DocType",
"onboard": 0,
@@ -110,6 +116,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 +127,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 +138,23 @@
"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.010205",
"modified_by": "Administrator",
"module": "Core",
"name": "Users",
"owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
+ "parent_page": "",
+ "public": 1,
+ "restrict_to_domain": "",
+ "roles": [],
+ "sequence_id": 27,
"shortcuts": [
{
"label": "User",
@@ -170,5 +182,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
index a59c24a714..1969cae141 100644
--- a/frappe/coverage.py
+++ b/frappe/coverage.py
@@ -33,3 +33,30 @@ FRAPPE_EXCLUSIONS = [
"*/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()
+ self.coverage.xml_report()
\ 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 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/custom/doctype/client_script/__init__.py
+++ b/frappe/custom/doctype/client_script/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/custom/doctype/client_script/client_script.js b/frappe/custom/doctype/client_script/client_script.js
index 27d11af4d1..ad9c9e4e42 100644
--- a/frappe/custom/doctype/client_script/client_script.js
+++ b/frappe/custom/doctype/client_script/client_script.js
@@ -43,6 +43,12 @@ frappe.ui.form.on('Client Script', {
d.show();
});
});
+
+ if (!frm.is_new()) {
+ frm.add_custom_button(__('Compare Versions'), () => {
+ new frappe.ui.DiffView("Client Script", "script", frm.doc.name);
+ });
+ }
}
frm.set_query('dt', {
diff --git a/frappe/custom/doctype/client_script/client_script.json b/frappe/custom/doctype/client_script/client_script.json
index db02d8d4bc..50f6bf3cc4 100644
--- a/frappe/custom/doctype/client_script/client_script.json
+++ b/frappe/custom/doctype/client_script/client_script.json
@@ -9,7 +9,10 @@
"field_order": [
"dt",
"view",
+ "column_break_3",
+ "module",
"enabled",
+ "section_break_6",
"script",
"sample"
],
@@ -53,13 +56,27 @@
"label": "Apply To",
"options": "List\nForm",
"set_only_once": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module (for export)",
+ "options": "Module Def"
+ },
+ {
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
}
],
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-03-16 20:33:51.400191",
+ "modified": "2021-09-04 12:03:27.029815",
"modified_by": "Administrator",
"module": "Custom",
"name": "Client Script",
diff --git a/frappe/custom/doctype/client_script/client_script.py b/frappe/custom/doctype/client_script/client_script.py
index 9c098fe8c9..fd6bc9accd 100644
--- a/frappe/custom/doctype/client_script/client_script.py
+++ b/frappe/custom/doctype/client_script/client_script.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
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 b8358468b9..4887956001 100644
--- a/frappe/custom/doctype/client_script/test_client_script.py
+++ b/frappe/custom/doctype/client_script/test_client_script.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/custom/doctype/custom_field/__init__.py b/frappe/custom/doctype/custom_field/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/custom/doctype/custom_field/__init__.py
+++ b/frappe/custom/doctype/custom_field/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json
index 19462e79de..235f11aad8 100644
--- a/frappe/custom/doctype/custom_field/custom_field.json
+++ b/frappe/custom/doctype/custom_field/custom_field.json
@@ -1,453 +1,458 @@
{
- "actions": [],
- "allow_import": 1,
- "creation": "2013-01-10 16:34:01",
- "description": "Adds a custom field to a DocType",
- "doctype": "DocType",
- "document_type": "Setup",
- "engine": "InnoDB",
- "field_order": [
- "dt",
- "label",
- "label_help",
- "fieldname",
- "insert_after",
- "length",
- "column_break_6",
- "fieldtype",
- "precision",
- "hide_seconds",
- "hide_days",
- "options",
- "fetch_from",
- "fetch_if_empty",
- "options_help",
- "section_break_11",
- "collapsible",
- "collapsible_depends_on",
- "default",
- "depends_on",
- "mandatory_depends_on",
- "read_only_depends_on",
- "properties",
- "non_negative",
- "reqd",
- "unique",
- "read_only",
- "ignore_user_permissions",
- "hidden",
- "print_hide",
- "print_hide_if_no_value",
- "print_width",
- "no_copy",
- "allow_on_submit",
- "in_list_view",
- "in_standard_filter",
- "in_global_search",
- "in_preview",
- "bold",
- "report_hide",
- "search_index",
- "allow_in_quick_entry",
- "ignore_xss_filter",
- "translatable",
- "hide_border",
- "description",
- "permlevel",
- "width",
- "columns"
- ],
- "fields": [
- {
- "bold": 1,
- "fieldname": "dt",
- "fieldtype": "Link",
- "in_filter": 1,
- "in_list_view": 1,
- "label": "Document",
- "oldfieldname": "dt",
- "oldfieldtype": "Link",
- "options": "DocType",
- "reqd": 1,
- "search_index": 1
- },
- {
- "bold": 1,
- "fieldname": "label",
- "fieldtype": "Data",
- "in_filter": 1,
- "label": "Label",
- "no_copy": 1,
- "oldfieldname": "label",
- "oldfieldtype": "Data"
- },
- {
- "fieldname": "label_help",
- "fieldtype": "HTML",
- "label": "Label Help",
- "oldfieldtype": "HTML"
- },
- {
- "fieldname": "fieldname",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Fieldname",
- "no_copy": 1,
- "oldfieldname": "fieldname",
- "oldfieldtype": "Data",
- "read_only": 1
- },
- {
- "description": "Select the label after which you want to insert new field.",
- "fieldname": "insert_after",
- "fieldtype": "Select",
- "label": "Insert After",
- "no_copy": 1,
- "oldfieldname": "insert_after",
- "oldfieldtype": "Select"
- },
- {
- "fieldname": "column_break_6",
- "fieldtype": "Column Break"
- },
- {
- "bold": 1,
- "default": "Data",
- "fieldname": "fieldtype",
- "fieldtype": "Select",
- "in_filter": 1,
- "in_list_view": 1,
- "label": "Field Type",
- "oldfieldname": "fieldtype",
- "oldfieldtype": "Select",
- "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\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
- },
- {
- "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
- "description": "Set non-standard precision for a Float or Currency field",
- "fieldname": "precision",
- "fieldtype": "Select",
- "label": "Precision",
- "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
- },
- {
- "fieldname": "options",
- "fieldtype": "Small Text",
- "in_list_view": 1,
- "label": "Options",
- "oldfieldname": "options",
- "oldfieldtype": "Text"
- },
- {
- "fieldname": "fetch_from",
- "fieldtype": "Small Text",
- "label": "Fetch From"
- },
- {
- "default": "0",
- "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
- "fieldname": "fetch_if_empty",
- "fieldtype": "Check",
- "label": "Fetch If Empty"
- },
- {
- "fieldname": "options_help",
- "fieldtype": "HTML",
- "label": "Options Help",
- "oldfieldtype": "HTML"
- },
- {
- "fieldname": "section_break_11",
- "fieldtype": "Section Break"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype==\"Section Break\"",
- "fieldname": "collapsible",
- "fieldtype": "Check",
- "label": "Collapsible"
- },
- {
- "depends_on": "eval:doc.fieldtype==\"Section Break\"",
- "fieldname": "collapsible_depends_on",
- "fieldtype": "Code",
- "label": "Collapsible Depends On"
- },
- {
- "fieldname": "default",
- "fieldtype": "Text",
- "label": "Default Value",
- "oldfieldname": "default",
- "oldfieldtype": "Text"
- },
- {
- "fieldname": "depends_on",
- "fieldtype": "Code",
- "label": "Depends On",
- "length": 255
- },
- {
- "fieldname": "description",
- "fieldtype": "Text",
- "label": "Field Description",
- "oldfieldname": "description",
- "oldfieldtype": "Text",
- "print_width": "300px",
- "width": "300px"
- },
- {
- "default": "0",
- "fieldname": "permlevel",
- "fieldtype": "Int",
- "label": "Permission Level",
- "oldfieldname": "permlevel",
- "oldfieldtype": "Int"
- },
- {
- "fieldname": "width",
- "fieldtype": "Data",
- "label": "Width",
- "oldfieldname": "width",
- "oldfieldtype": "Data"
- },
- {
- "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
- "fieldname": "columns",
- "fieldtype": "Int",
- "label": "Columns"
- },
- {
- "fieldname": "properties",
- "fieldtype": "Column Break",
- "oldfieldtype": "Column Break",
- "print_width": "50%",
- "width": "50%"
- },
- {
- "default": "0",
- "fieldname": "reqd",
- "fieldtype": "Check",
- "in_list_view": 1,
- "label": "Is Mandatory Field",
- "oldfieldname": "reqd",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "fieldname": "unique",
- "fieldtype": "Check",
- "label": "Unique"
- },
- {
- "default": "0",
- "fieldname": "read_only",
- "fieldtype": "Check",
- "label": "Read Only"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype===\"Link\"",
- "fieldname": "ignore_user_permissions",
- "fieldtype": "Check",
- "label": "Ignore User Permissions"
- },
- {
- "default": "0",
- "fieldname": "hidden",
- "fieldtype": "Check",
- "label": "Hidden"
- },
- {
- "default": "0",
- "fieldname": "print_hide",
- "fieldtype": "Check",
- "label": "Print Hide",
- "oldfieldname": "print_hide",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
- "fieldname": "print_hide_if_no_value",
- "fieldtype": "Check",
- "label": "Print Hide If No Value"
- },
- {
- "fieldname": "print_width",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Print Width",
- "no_copy": 1,
- "print_hide": 1
- },
- {
- "default": "0",
- "fieldname": "no_copy",
- "fieldtype": "Check",
- "label": "No Copy",
- "oldfieldname": "no_copy",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "fieldname": "allow_on_submit",
- "fieldtype": "Check",
- "label": "Allow on Submit",
- "oldfieldname": "allow_on_submit",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "fieldname": "in_list_view",
- "fieldtype": "Check",
- "label": "In List View"
- },
- {
- "default": "0",
- "fieldname": "in_standard_filter",
- "fieldtype": "Check",
- "label": "In Standard Filter"
- },
- {
- "default": "0",
- "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
- "fieldname": "in_global_search",
- "fieldtype": "Check",
- "label": "In Global Search"
- },
- {
- "default": "0",
- "fieldname": "bold",
- "fieldtype": "Check",
- "label": "Bold"
- },
- {
- "default": "0",
- "fieldname": "report_hide",
- "fieldtype": "Check",
- "label": "Report Hide",
- "oldfieldname": "report_hide",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "fieldname": "search_index",
- "fieldtype": "Check",
- "hidden": 1,
- "label": "Index",
- "no_copy": 1,
- "print_hide": 1
- },
- {
- "default": "0",
- "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
- "fieldname": "ignore_xss_filter",
- "fieldtype": "Check",
- "label": "Ignore XSS Filter"
- },
- {
- "default": "1",
- "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
- "fieldname": "translatable",
- "fieldtype": "Check",
- "label": "Translatable"
- },
- {
- "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
- "fieldname": "length",
- "fieldtype": "Int",
- "label": "Length"
- },
- {
- "fieldname": "mandatory_depends_on",
- "fieldtype": "Code",
- "label": "Mandatory Depends On",
- "length": 255
- },
- {
- "fieldname": "read_only_depends_on",
- "fieldtype": "Code",
- "label": "Read Only Depends On",
- "length": 255
- },
- {
- "default": "0",
- "fieldname": "allow_in_quick_entry",
- "fieldtype": "Check",
- "label": "Allow in Quick Entry"
- },
- {
- "default": "0",
- "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
- "fieldname": "in_preview",
- "fieldtype": "Check",
- "label": "In Preview"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype=='Duration'",
- "fieldname": "hide_seconds",
- "fieldtype": "Check",
- "label": "Hide Seconds"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype=='Duration'",
- "fieldname": "hide_days",
- "fieldtype": "Check",
- "label": "Hide Days"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype=='Section Break'",
- "fieldname": "hide_border",
- "fieldtype": "Check",
- "label": "Hide Border"
- },
- {
- "default": "0",
- "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
- "fieldname": "non_negative",
- "fieldtype": "Check",
- "label": "Non Negative"
- }
- ],
- "icon": "fa fa-glass",
- "idx": 1,
- "index_web_pages_for_search": 1,
- "links": [],
- "modified": "2021-07-12 04:54:12.042319",
- "modified_by": "Administrator",
- "module": "Custom",
- "name": "Custom Field",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Administrator",
- "share": 1,
- "write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "search_fields": "dt,label,fieldtype,options",
- "sort_field": "modified",
- "sort_order": "ASC",
- "track_changes": 1
+ "actions": [],
+ "allow_import": 1,
+ "creation": "2013-01-10 16:34:01",
+ "description": "Adds a custom field to a DocType",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "dt",
+ "module",
+ "label",
+ "label_help",
+ "fieldname",
+ "insert_after",
+ "length",
+ "column_break_6",
+ "fieldtype",
+ "precision",
+ "hide_seconds",
+ "hide_days",
+ "options",
+ "fetch_from",
+ "fetch_if_empty",
+ "options_help",
+ "section_break_11",
+ "collapsible",
+ "collapsible_depends_on",
+ "default",
+ "depends_on",
+ "mandatory_depends_on",
+ "read_only_depends_on",
+ "properties",
+ "non_negative",
+ "reqd",
+ "unique",
+ "read_only",
+ "ignore_user_permissions",
+ "hidden",
+ "print_hide",
+ "print_hide_if_no_value",
+ "print_width",
+ "no_copy",
+ "allow_on_submit",
+ "in_list_view",
+ "in_standard_filter",
+ "in_global_search",
+ "in_preview",
+ "bold",
+ "report_hide",
+ "search_index",
+ "allow_in_quick_entry",
+ "ignore_xss_filter",
+ "translatable",
+ "hide_border",
+ "description",
+ "permlevel",
+ "width",
+ "columns"
+ ],
+ "fields": [{
+ "bold": 1,
+ "fieldname": "dt",
+ "fieldtype": "Link",
+ "in_filter": 1,
+ "in_list_view": 1,
+ "label": "Document",
+ "oldfieldname": "dt",
+ "oldfieldtype": "Link",
+ "options": "DocType",
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "bold": 1,
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "in_filter": 1,
+ "label": "Label",
+ "no_copy": 1,
+ "oldfieldname": "label",
+ "oldfieldtype": "Data"
+ },
+ {
+ "fieldname": "label_help",
+ "fieldtype": "HTML",
+ "label": "Label Help",
+ "oldfieldtype": "HTML"
+ },
+ {
+ "fieldname": "fieldname",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Fieldname",
+ "no_copy": 1,
+ "oldfieldname": "fieldname",
+ "oldfieldtype": "Data",
+ "read_only": 1
+ },
+ {
+ "description": "Select the label after which you want to insert new field.",
+ "fieldname": "insert_after",
+ "fieldtype": "Select",
+ "label": "Insert After",
+ "no_copy": 1,
+ "oldfieldname": "insert_after",
+ "oldfieldtype": "Select"
+ },
+ {
+ "fieldname": "column_break_6",
+ "fieldtype": "Column Break"
+ },
+ {
+ "bold": 1,
+ "default": "Data",
+ "fieldname": "fieldtype",
+ "fieldtype": "Select",
+ "in_filter": 1,
+ "in_list_view": 1,
+ "label": "Field Type",
+ "oldfieldname": "fieldtype",
+ "oldfieldtype": "Select",
+ "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break",
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
+ "description": "Set non-standard precision for a Float or Currency field",
+ "fieldname": "precision",
+ "fieldtype": "Select",
+ "label": "Precision",
+ "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
+ },
+ {
+ "fieldname": "options",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Options",
+ "oldfieldname": "options",
+ "oldfieldtype": "Text"
+ },
+ {
+ "fieldname": "fetch_from",
+ "fieldtype": "Small Text",
+ "label": "Fetch From"
+ },
+ {
+ "default": "0",
+ "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
+ "fieldname": "fetch_if_empty",
+ "fieldtype": "Check",
+ "label": "Fetch If Empty"
+ },
+ {
+ "fieldname": "options_help",
+ "fieldtype": "HTML",
+ "label": "Options Help",
+ "oldfieldtype": "HTML"
+ },
+ {
+ "fieldname": "section_break_11",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype==\"Section Break\"",
+ "fieldname": "collapsible",
+ "fieldtype": "Check",
+ "label": "Collapsible"
+ },
+ {
+ "depends_on": "eval:doc.fieldtype==\"Section Break\"",
+ "fieldname": "collapsible_depends_on",
+ "fieldtype": "Code",
+ "label": "Collapsible Depends On"
+ },
+ {
+ "fieldname": "default",
+ "fieldtype": "Text",
+ "label": "Default Value",
+ "oldfieldname": "default",
+ "oldfieldtype": "Text"
+ },
+ {
+ "fieldname": "depends_on",
+ "fieldtype": "Code",
+ "label": "Depends On",
+ "length": 255
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "label": "Field Description",
+ "oldfieldname": "description",
+ "oldfieldtype": "Text",
+ "print_width": "300px",
+ "width": "300px"
+ },
+ {
+ "default": "0",
+ "fieldname": "permlevel",
+ "fieldtype": "Int",
+ "label": "Permission Level",
+ "oldfieldname": "permlevel",
+ "oldfieldtype": "Int"
+ },
+ {
+ "fieldname": "width",
+ "fieldtype": "Data",
+ "label": "Width",
+ "oldfieldname": "width",
+ "oldfieldtype": "Data"
+ },
+ {
+ "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
+ "fieldname": "columns",
+ "fieldtype": "Int",
+ "label": "Columns"
+ },
+ {
+ "fieldname": "properties",
+ "fieldtype": "Column Break",
+ "oldfieldtype": "Column Break",
+ "print_width": "50%",
+ "width": "50%"
+ },
+ {
+ "default": "0",
+ "fieldname": "reqd",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Is Mandatory Field",
+ "oldfieldname": "reqd",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "unique",
+ "fieldtype": "Check",
+ "label": "Unique"
+ },
+ {
+ "default": "0",
+ "fieldname": "read_only",
+ "fieldtype": "Check",
+ "label": "Read Only"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype===\"Link\"",
+ "fieldname": "ignore_user_permissions",
+ "fieldtype": "Check",
+ "label": "Ignore User Permissions"
+ },
+ {
+ "default": "0",
+ "fieldname": "hidden",
+ "fieldtype": "Check",
+ "label": "Hidden"
+ },
+ {
+ "default": "0",
+ "fieldname": "print_hide",
+ "fieldtype": "Check",
+ "label": "Print Hide",
+ "oldfieldname": "print_hide",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
+ "fieldname": "print_hide_if_no_value",
+ "fieldtype": "Check",
+ "label": "Print Hide If No Value"
+ },
+ {
+ "fieldname": "print_width",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Print Width",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "no_copy",
+ "fieldtype": "Check",
+ "label": "No Copy",
+ "oldfieldname": "no_copy",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_on_submit",
+ "fieldtype": "Check",
+ "label": "Allow on Submit",
+ "oldfieldname": "allow_on_submit",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "in_list_view",
+ "fieldtype": "Check",
+ "label": "In List View"
+ },
+ {
+ "default": "0",
+ "fieldname": "in_standard_filter",
+ "fieldtype": "Check",
+ "label": "In Standard Filter"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
+ "fieldname": "in_global_search",
+ "fieldtype": "Check",
+ "label": "In Global Search"
+ },
+ {
+ "default": "0",
+ "fieldname": "bold",
+ "fieldtype": "Check",
+ "label": "Bold"
+ },
+ {
+ "default": "0",
+ "fieldname": "report_hide",
+ "fieldtype": "Check",
+ "label": "Report Hide",
+ "oldfieldname": "report_hide",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "search_index",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Index",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "default": "0",
+ "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
+ "fieldname": "ignore_xss_filter",
+ "fieldtype": "Check",
+ "label": "Ignore XSS Filter"
+ },
+ {
+ "default": "1",
+ "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
+ "fieldname": "translatable",
+ "fieldtype": "Check",
+ "label": "Translatable"
+ },
+ {
+ "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
+ "fieldname": "length",
+ "fieldtype": "Int",
+ "label": "Length"
+ },
+ {
+ "fieldname": "mandatory_depends_on",
+ "fieldtype": "Code",
+ "label": "Mandatory Depends On",
+ "length": 255
+ },
+ {
+ "fieldname": "read_only_depends_on",
+ "fieldtype": "Code",
+ "label": "Read Only Depends On",
+ "length": 255
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_in_quick_entry",
+ "fieldtype": "Check",
+ "label": "Allow in Quick Entry"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
+ "fieldname": "in_preview",
+ "fieldtype": "Check",
+ "label": "In Preview"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_seconds",
+ "fieldtype": "Check",
+ "label": "Hide Seconds"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_days",
+ "fieldtype": "Check",
+ "label": "Hide Days"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Section Break'",
+ "fieldname": "hide_border",
+ "fieldtype": "Check",
+ "label": "Hide Border"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
+ "fieldname": "non_negative",
+ "fieldtype": "Check",
+ "label": "Non Negative"
+ },
+ {
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module (for export)",
+ "options": "Module Def"
+ }
+ ],
+ "icon": "fa fa-glass",
+ "idx": 1,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-09-04 12:45:23.810120",
+ "modified_by": "Administrator",
+ "module": "Custom",
+ "name": "Custom Field",
+ "owner": "Administrator",
+ "permissions": [{
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Administrator",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "search_fields": "dt,label,fieldtype,options",
+ "sort_field": "modified",
+ "sort_order": "ASC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py
index e266455f7a..8f7b21dd24 100644
--- a/frappe/custom/doctype/custom_field/custom_field.py
+++ b/frappe/custom/doctype/custom_field/custom_field.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
import json
@@ -8,6 +8,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.model.docfield import supports_translation
from frappe.model import core_doctypes_list
+from frappe.query_builder.functions import IfNull
class CustomField(Document):
def autoname(self):
@@ -18,7 +19,7 @@ class CustomField(Document):
if not self.fieldname:
label = self.label
if not label:
- if self.fieldtype in ["Section Break", "Column Break"]:
+ if self.fieldtype in ["Section Break", "Column Break", "Tab Break"]:
label = self.fieldtype + "_" + str(self.idx)
else:
frappe.throw(_("Label is mandatory"))
@@ -115,9 +116,7 @@ def get_fields_label(doctype=None):
def create_custom_field_if_values_exist(doctype, df):
df = frappe._dict(df)
if df.fieldname in frappe.db.get_table_columns(doctype) and \
- frappe.db.sql("""select count(*) from `tab{doctype}`
- where ifnull({fieldname},'')!=''""".format(doctype=doctype, fieldname=df.fieldname))[0][0]:
-
+ frappe.db.count(dt=doctype, filters=IfNull(df.fieldname, "") != ""):
create_custom_field(doctype, df)
def create_custom_field(doctype, df, ignore_validate=False):
@@ -131,7 +130,7 @@ def create_custom_field(doctype, df, ignore_validate=False):
"permlevel": 0,
"fieldtype": 'Data',
"hidden": 0,
- # Looks like we always use this programatically?
+ # Looks like we always use this programatically?
# "is_standard": 1
})
custom_field.update(df)
@@ -146,24 +145,29 @@ def create_custom_fields(custom_fields, ignore_validate = False, update=True):
if not ignore_validate and frappe.flags.in_setup_wizard:
ignore_validate = True
- for doctype, fields in custom_fields.items():
+ for doctypes, fields in custom_fields.items():
if isinstance(fields, dict):
# only one field
fields = [fields]
- for df in fields:
- field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": df["fieldname"]})
- if not field:
- try:
- df["owner"] = "Administrator"
- create_custom_field(doctype, df, ignore_validate=ignore_validate)
- except frappe.exceptions.DuplicateEntryError:
- pass
- elif update:
- custom_field = frappe.get_doc("Custom Field", field)
- custom_field.flags.ignore_validate = ignore_validate
- custom_field.update(df)
- custom_field.save()
+ if isinstance(doctypes, str):
+ # only one doctype
+ doctypes = (doctypes,)
+
+ for doctype in doctypes:
+ for df in fields:
+ field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": df["fieldname"]})
+ if not field:
+ try:
+ df["owner"] = "Administrator"
+ create_custom_field(doctype, df, ignore_validate=ignore_validate)
+ except frappe.exceptions.DuplicateEntryError:
+ pass
+ elif update:
+ custom_field = frappe.get_doc("Custom Field", field)
+ custom_field.flags.ignore_validate = ignore_validate
+ custom_field.update(df)
+ custom_field.save()
frappe.clear_cache(doctype=doctype)
frappe.db.updatedb(doctype)
diff --git a/frappe/custom/doctype/custom_field/test_custom_field.py b/frappe/custom/doctype/custom_field/test_custom_field.py
index 3196b66ee8..ad3cf27eea 100644
--- a/frappe/custom/doctype/custom_field/test_custom_field.py
+++ b/frappe/custom/doctype/custom_field/test_custom_field.py
@@ -1,12 +1,47 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
-test_records = frappe.get_test_records('Custom Field')
+test_records = frappe.get_test_records("Custom Field")
+
class TestCustomField(unittest.TestCase):
- pass
+ def test_create_custom_fields(self):
+ from .custom_field import create_custom_fields
+
+ create_custom_fields(
+ {
+ "Address": [
+ {
+ "fieldname": "_test_custom_field_1",
+ "label": "_Test Custom Field 1",
+ "fieldtype": "Data",
+ "insert_after": "phone",
+ },
+ ],
+ ("Address", "Contact"): [
+ {
+ "fieldname": "_test_custom_field_2",
+ "label": "_Test Custom Field 2",
+ "fieldtype": "Data",
+ "insert_after": "phone",
+ },
+ ],
+ }
+ )
+
+ frappe.db.commit()
+
+ self.assertTrue(
+ frappe.db.exists("Custom Field", "Address-_test_custom_field_1")
+ )
+ self.assertTrue(
+ frappe.db.exists("Custom Field", "Address-_test_custom_field_2")
+ )
+ self.assertTrue(
+ frappe.db.exists("Custom Field", "Contact-_test_custom_field_2")
+ )
diff --git a/frappe/custom/doctype/customize_form/__init__.py b/frappe/custom/doctype/customize_form/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/custom/doctype/customize_form/__init__.py
+++ b/frappe/custom/doctype/customize_form/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js
index 4e00456f0d..4862185b99 100644
--- a/frappe/custom/doctype/customize_form/customize_form.js
+++ b/frappe/custom/doctype/customize_form/customize_form.js
@@ -114,6 +114,7 @@ frappe.ui.form.on("Customize Form", {
frm.page.clear_icons();
if (frm.doc.doc_type) {
+ frm.page.set_title(__('Customize Form - {0}', [frm.doc.doc_type]));
frappe.customize_form.set_primary_action(frm);
frm.add_custom_button(
@@ -276,6 +277,21 @@ frappe.ui.form.on("DocType Action", {
}
});
+// can't delete standard states
+frappe.ui.form.on("DocType State", {
+ before_states_remove: function(frm, doctype, name) {
+ let row = frappe.get_doc(doctype, name);
+ if (!(row.custom || row.__islocal)) {
+ frappe.msgprint(__("Cannot delete standard document state."));
+ throw "cannot delete standard document state";
+ }
+ },
+ states_add: function(frm, cdt, cdn) {
+ let f = frappe.model.get_doc(cdt, cdn);
+ f.custom = 1;
+ }
+});
+
frappe.customize_form.set_primary_action = function(frm) {
frm.page.set_primary_action(__("Update"), function() {
if (frm.doc.doc_type) {
@@ -332,3 +348,4 @@ frappe.customize_form.clear_locals_and_refresh = function(frm) {
frm.refresh();
}
+extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({frm: cur_frm}));
diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json
index cdcac1582a..d266b8688a 100644
--- a/frappe/custom/doctype/customize_form/customize_form.json
+++ b/frappe/custom/doctype/customize_form/customize_form.json
@@ -42,6 +42,8 @@
"actions",
"document_links_section",
"links",
+ "document_states_section",
+ "states",
"section_break_8",
"sort_field",
"column_break_10",
@@ -282,6 +284,20 @@
"fieldtype": "Data",
"label": "Auto Name"
},
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "states",
+ "depends_on": "doc_type",
+ "fieldname": "document_states_section",
+ "fieldtype": "Section Break",
+ "label": "Document States"
+ },
+ {
+ "fieldname": "states",
+ "fieldtype": "Table",
+ "label": "States",
+ "options": "DocType State"
+ },
{
"default": "0",
"fieldname": "show_title_field_in_link",
@@ -295,10 +311,11 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-08-03 13:43:27.938781",
+ "modified": "2021-12-15 16:45:04.308690",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -315,5 +332,6 @@
"search_fields": "doc_type",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index 1866f4d368..3d312d3de7 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -72,7 +72,7 @@ class CustomizeForm(Document):
new_d[prop] = d.get(prop)
self.append("fields", new_d)
- for fieldname in ('links', 'actions'):
+ for fieldname in ('links', 'actions', 'states'):
for d in meta.get(fieldname):
self.append(fieldname, d)
@@ -193,6 +193,16 @@ class CustomizeForm(Document):
if prop == "fieldtype":
self.validate_fieldtype_change(df, meta_df[0].get(prop), df.get(prop))
+ elif prop == "length":
+ old_value_length = cint(meta_df[0].get(prop))
+ new_value_length = cint(df.get(prop))
+
+ if new_value_length and (old_value_length > new_value_length):
+ self.check_length_for_fieldtypes.append({'df': df, 'old_value': meta_df[0].get(prop)})
+ self.validate_fieldtype_length()
+ else:
+ self.flags.update_db = True
+
elif prop == "allow_on_submit" and df.get(prop):
if not frappe.db.get_value("DocField",
{"parent": self.doc_type, "fieldname": df.fieldname}, "allow_on_submit"):
@@ -248,7 +258,8 @@ class CustomizeForm(Document):
'''
for doctype, fieldname, field_map in (
('DocType Link', 'links', doctype_link_properties),
- ('DocType Action', 'actions', doctype_action_properties)
+ ('DocType Action', 'actions', doctype_action_properties),
+ ('DocType State', 'states', doctype_state_properties),
):
has_custom = False
items = []
@@ -559,6 +570,11 @@ doctype_action_properties = {
'hidden': 'Check'
}
+doctype_state_properties = {
+ 'title': 'Data',
+ 'color': 'Select'
+}
+
ALLOWED_FIELDTYPE_CHANGE = (
('Currency', 'Float', 'Percent'),
diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py
index 58bdcf9a18..8a287b17e8 100644
--- a/frappe/custom/doctype/customize_form/test_customize_form.py
+++ b/frappe/custom/doctype/customize_form/test_customize_form.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe, unittest, json
from frappe.test_runner import make_test_records_for_doctype
@@ -188,6 +188,26 @@ class TestCustomizeForm(unittest.TestCase):
def test_core_doctype_customization(self):
self.assertRaises(frappe.ValidationError, self.get_customize_form, 'User')
+ def test_save_customization_length_field_property(self):
+ # Using Notification Log doctype as it doesn't have any other custom fields
+ d = self.get_customize_form("Notification Log")
+
+ document_name = d.get("fields", {"fieldname": "document_name"})[0]
+ document_name.length = 255
+ d.run_method("save_customization")
+
+ self.assertEqual(frappe.db.get_value("Property Setter",
+ {"doc_type": "Notification Log", "property": "length", "field_name": "document_name"}, "value"), '255')
+
+ self.assertTrue(d.flags.update_db)
+
+ length = frappe.db.sql("""SELECT character_maximum_length
+ FROM information_schema.columns
+ WHERE table_name = 'tabNotification Log'
+ AND column_name = 'document_name'""")[0][0]
+
+ self.assertEqual(length, 255)
+
def test_custom_link(self):
try:
# create a dummy doctype linked to Event
@@ -232,6 +252,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 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/custom/doctype/customize_form_field/__init__.py
+++ b/frappe/custom/doctype/customize_form_field/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
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..986b99a7af 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\nTab Break",
"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-11 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 f288e70754..67563cf048 100644
--- a/frappe/custom/doctype/customize_form_field/customize_form_field.py
+++ b/frappe/custom/doctype/customize_form_field/customize_form_field.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.py b/frappe/custom/doctype/doctype_layout/doctype_layout.py
index 0dc320353d..fa285ddb62 100644
--- a/frappe/custom/doctype/doctype_layout/doctype_layout.py
+++ b/frappe/custom/doctype/doctype_layout/doctype_layout.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from frappe.model.document import Document
diff --git a/frappe/custom/doctype/doctype_layout/test_doctype_layout.py b/frappe/custom/doctype/doctype_layout/test_doctype_layout.py
index dcde3c00a4..a63dd7ee16 100644
--- a/frappe/custom/doctype/doctype_layout/test_doctype_layout.py
+++ b/frappe/custom/doctype/doctype_layout/test_doctype_layout.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
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 c1e963602f..3f8487b659 100644
--- a/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py
+++ b/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# 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 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/custom/doctype/property_setter/__init__.py
+++ b/frappe/custom/doctype/property_setter/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/custom/doctype/property_setter/property_setter.json b/frappe/custom/doctype/property_setter/property_setter.json
index b318d92c5a..9707f1ee1c 100644
--- a/frappe/custom/doctype/property_setter/property_setter.json
+++ b/frappe/custom/doctype/property_setter/property_setter.json
@@ -13,6 +13,8 @@
"field_name",
"row_name",
"column_break0",
+ "module",
+ "section_break_9",
"property",
"property_type",
"value",
@@ -35,7 +37,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Applied On",
- "options": "\nDocField\nDocType\nDocType Link\nDocType Action",
+ "options": "\nDocField\nDocType\nDocType Link\nDocType Action\nDocType State",
"read_only_depends_on": "eval:!doc.__islocal",
"reqd": 1
},
@@ -91,13 +93,23 @@
"fieldname": "row_name",
"fieldtype": "Data",
"label": "Row Name"
+ },
+ {
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module (for export)",
+ "options": "Module Def"
+ },
+ {
+ "fieldname": "section_break_9",
+ "fieldtype": "Section Break"
}
],
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-09-24 14:42:38.599684",
+ "modified": "2021-12-14 14:15:41.929071",
"modified_by": "Administrator",
"module": "Custom",
"name": "Property Setter",
@@ -129,5 +141,6 @@
"search_fields": "doc_type,property",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/custom/doctype/property_setter/property_setter.py b/frappe/custom/doctype/property_setter/property_setter.py
index 2a6c06b70a..7f40be9725 100644
--- a/frappe/custom/doctype/property_setter/property_setter.py
+++ b/frappe/custom/doctype/property_setter/property_setter.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
@@ -34,7 +34,7 @@ class PropertySetter(Document):
fields=['fieldname', 'label', 'fieldtype'],
filters={
'parent': dt,
- 'fieldtype': ['not in', ('Section Break', 'Column Break', 'HTML', 'Read Only', 'Fold') + frappe.model.table_fields],
+ 'fieldtype': ['not in', ('Section Break', 'Column Break', 'Tab Break', 'HTML', 'Read Only', 'Fold') + frappe.model.table_fields],
'fieldname': ['!=', '']
},
order_by='label asc',
@@ -43,20 +43,28 @@ class PropertySetter(Document):
def get_setup_data(self):
return {
- 'doctypes': [d[0] for d in frappe.db.sql("select name from tabDocType")],
+ 'doctypes': frappe.get_all("DocType", pluck="name"),
'dt_properties': self.get_property_list('DocType'),
'df_properties': self.get_property_list('DocField')
}
def get_field_ids(self):
- return frappe.db.sql("select name, fieldtype, label, fieldname from tabDocField where parent=%s", self.doc_type, as_dict = 1)
+ return frappe.db.get_values(
+ "DocField",
+ filters={"parent": self.doc_type},
+ fieldname=["name", "fieldtype", "label", "fieldname"],
+ as_dict=True,
+ )
def get_defaults(self):
if not self.field_name:
- return frappe.db.sql("select * from `tabDocType` where name=%s", self.doc_type, as_dict = 1)[0]
+ return frappe.get_all("DocType", filters={"name": self.doc_type}, fields="*")[0]
else:
- return frappe.db.sql("select * from `tabDocField` where fieldname=%s and parent=%s",
- (self.field_name, self.doc_type), as_dict = 1)[0]
+ return frappe.db.get_values(
+ "DocField",
+ filters={"fieldname": self.field_name, "parent": self.doc_type},
+ fieldname="*",
+ )[0]
def on_update(self):
if frappe.flags.in_patch:
diff --git a/frappe/custom/doctype/property_setter/test_property_setter.py b/frappe/custom/doctype/property_setter/test_property_setter.py
index 4d4de66d51..1bbbe59a0f 100644
--- a/frappe/custom/doctype/property_setter/test_property_setter.py
+++ b/frappe/custom/doctype/property_setter/test_property_setter.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
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 32d2396b2b..fc4ab97cfe 100644
--- a/frappe/custom/doctype/test_rename_new/test_rename_new.py
+++ b/frappe/custom/doctype/test_rename_new/test_rename_new.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# 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 b3ea4818de..03202669ed 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,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/custom/form_tour/custom_field/custom_field.json b/frappe/custom/form_tour/custom_field/custom_field.json
new file mode 100644
index 0000000000..3279449e7c
--- /dev/null
+++ b/frappe/custom/form_tour/custom_field/custom_field.json
@@ -0,0 +1,79 @@
+{
+ "creation": "2021-11-23 12:22:32.922700",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "first_document": 0,
+ "idx": 0,
+ "include_name_field": 0,
+ "is_standard": 1,
+ "modified": "2021-11-24 19:15:34.244244",
+ "modified_by": "Administrator",
+ "module": "Custom",
+ "name": "Custom Field",
+ "owner": "Administrator",
+ "reference_doctype": "Custom Field",
+ "save_on_complete": 1,
+ "steps": [
+ {
+ "description": "Select a Document for which you want the Custom Field",
+ "field": "",
+ "fieldname": "dt",
+ "fieldtype": "Link",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Document",
+ "parent_field": "",
+ "position": "Right",
+ "title": "Document"
+ },
+ {
+ "description": "Enter a Label for this field",
+ "field": "",
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Label",
+ "parent_field": "",
+ "position": "Right",
+ "title": "Label"
+ },
+ {
+ "description": "Select the label after which you want to insert new field.",
+ "field": "",
+ "fieldname": "insert_after",
+ "fieldtype": "Select",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Insert After",
+ "parent_field": "",
+ "position": "Right",
+ "title": "Insert After"
+ },
+ {
+ "description": "Select an appropriate Field Type that suits your requirements",
+ "field": "",
+ "fieldname": "fieldtype",
+ "fieldtype": "Select",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Field Type",
+ "parent_field": "",
+ "position": "Left",
+ "title": "Field Type"
+ },
+ {
+ "description": "Check this to make it a mandatory field",
+ "field": "",
+ "fieldname": "reqd",
+ "fieldtype": "Check",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Is Mandatory Field",
+ "parent_field": "",
+ "position": "Left",
+ "title": "Is Mandatory Field"
+ }
+ ],
+ "title": "Custom Field"
+}
\ No newline at end of file
diff --git a/frappe/custom/module_onboarding/customization/customization.json b/frappe/custom/module_onboarding/customization/customization.json
new file mode 100644
index 0000000000..99b7cc1f2b
--- /dev/null
+++ b/frappe/custom/module_onboarding/customization/customization.json
@@ -0,0 +1,44 @@
+{
+ "allow_roles": [
+ {
+ "role": "All"
+ }
+ ],
+ "creation": "2021-11-23 12:21:11.384229",
+ "docstatus": 0,
+ "doctype": "Module Onboarding",
+ "documentation_url": "https://docs.erpnext.com/docs/v13/user/manual/en/customize-erpnext",
+ "idx": 0,
+ "is_complete": 0,
+ "modified": "2021-11-24 17:04:31.523715",
+ "modified_by": "Administrator",
+ "module": "Custom",
+ "name": "Customization",
+ "owner": "Administrator",
+ "steps": [
+ {
+ "step": "Custom Field"
+ },
+ {
+ "step": "Custom Doctype"
+ },
+ {
+ "step": "Naming Series"
+ },
+ {
+ "step": "Workflows"
+ },
+ {
+ "step": "Role Permissions"
+ },
+ {
+ "step": "Print Format"
+ },
+ {
+ "step": "Report Builder"
+ }
+ ],
+ "subtitle": "Custom Field, Custom Doctype, Naming Series, Role Permission, Workflow, Print Formats, Reports",
+ "success_message": "Customization onboarding is all done!",
+ "title": "Customization"
+}
\ No newline at end of file
diff --git a/frappe/custom/onboarding_step/custom_doctype/custom_doctype.json b/frappe/custom/onboarding_step/custom_doctype/custom_doctype.json
new file mode 100644
index 0000000000..1f8601abee
--- /dev/null
+++ b/frappe/custom/onboarding_step/custom_doctype/custom_doctype.json
@@ -0,0 +1,21 @@
+{
+ "action": "Create Entry",
+ "action_label": "Learn more about creating new DocTypes",
+ "creation": "2021-11-23 12:30:04.407568",
+ "description": "A DocType (Document Type) is used to insert forms in ERPNext. Forms such as Customer, Orders, and Invoices are Doctypes in the backend. You can also create new DocTypes to create new forms in ERPNext as per your business needs.",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-11-23 12:30:04.407568",
+ "modified_by": "Administrator",
+ "name": "Custom Doctype",
+ "owner": "Administrator",
+ "reference_document": "DocType",
+ "show_form_tour": 1,
+ "show_full_form": 1,
+ "title": "Custom Document Types",
+ "validate_action": 1
+}
\ No newline at end of file
diff --git a/frappe/custom/onboarding_step/custom_field/custom_field.json b/frappe/custom/onboarding_step/custom_field/custom_field.json
new file mode 100644
index 0000000000..4044cf2456
--- /dev/null
+++ b/frappe/custom/onboarding_step/custom_field/custom_field.json
@@ -0,0 +1,21 @@
+{
+ "action": "Create Entry",
+ "action_label": "Learn how to add Custom Fields",
+ "creation": "2021-11-23 12:21:09.479808",
+ "description": "Every form in ERPNext has a standard set of fields. If you need to capture some information, but there is no standard Field available for it, you can insert Custom Field for it.\n\nOnce custom fields are added, you can use them for reports and analytics charts as well.\n",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-11-23 12:21:09.479808",
+ "modified_by": "Administrator",
+ "name": "Custom Field",
+ "owner": "Administrator",
+ "reference_document": "Custom Field",
+ "show_form_tour": 1,
+ "show_full_form": 1,
+ "title": "Create Custom Fields",
+ "validate_action": 1
+}
\ No newline at end of file
diff --git a/frappe/custom/onboarding_step/naming_series/naming_series.json b/frappe/custom/onboarding_step/naming_series/naming_series.json
new file mode 100644
index 0000000000..3b15e4afde
--- /dev/null
+++ b/frappe/custom/onboarding_step/naming_series/naming_series.json
@@ -0,0 +1,20 @@
+{
+ "action": "Watch Video",
+ "creation": "2021-11-23 13:57:45.091427",
+ "description": "Each document created in ERPNext can have a unique ID generated for it, using a prefix defined for it. Though each document has some prefix pre-configured, you can further customize it using tools like Naming Series Tool and Document Naming Rule.\n",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-11-24 15:04:14.662684",
+ "modified_by": "Administrator",
+ "name": "Naming Series",
+ "owner": "Administrator",
+ "show_form_tour": 0,
+ "show_full_form": 0,
+ "title": "Setup Naming Series",
+ "validate_action": 1,
+ "video_url": "https://youtu.be/IGyISSfI1qU"
+}
\ No newline at end of file
diff --git a/frappe/custom/onboarding_step/print_format/print_format.json b/frappe/custom/onboarding_step/print_format/print_format.json
new file mode 100644
index 0000000000..681ef85b95
--- /dev/null
+++ b/frappe/custom/onboarding_step/print_format/print_format.json
@@ -0,0 +1,21 @@
+{
+ "action": "Create Entry",
+ "action_label": "Learn about Standard and Custom Print Formats",
+ "creation": "2021-11-23 15:04:12.728513",
+ "description": "Print Formats allow you can define looks for documents when printed or converted to PDF. You can also create a custom Print Format using drag-and-drop tools.",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-11-23 15:04:12.728513",
+ "modified_by": "Administrator",
+ "name": "Print Format",
+ "owner": "Administrator",
+ "reference_document": "Print Format",
+ "show_form_tour": 1,
+ "show_full_form": 1,
+ "title": "Customize Print Formats",
+ "validate_action": 1
+}
\ No newline at end of file
diff --git a/frappe/custom/onboarding_step/report_builder/report_builder.json b/frappe/custom/onboarding_step/report_builder/report_builder.json
new file mode 100644
index 0000000000..4a0b5f9130
--- /dev/null
+++ b/frappe/custom/onboarding_step/report_builder/report_builder.json
@@ -0,0 +1,22 @@
+{
+ "action": "Watch Video",
+ "action_label": "Learn more about Report Builders",
+ "creation": "2021-11-24 17:04:18.762838",
+ "description": "In each module, you will find a host of single-click reports, ranging from financial statements to sales and purchase analytics and stock tracking reports. If a required new report is not available out-of-the-box, you can create custom reports in ERPNext by pulling values from the same multiple ERPNext tables.\n",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-11-24 17:04:18.762838",
+ "modified_by": "Administrator",
+ "name": "Report Builder",
+ "owner": "Administrator",
+ "reference_document": "Report",
+ "show_form_tour": 0,
+ "show_full_form": 0,
+ "title": "Generate Custom Reports",
+ "validate_action": 1,
+ "video_url": "https://youtu.be/TxJGUNarcQs"
+}
\ No newline at end of file
diff --git a/frappe/custom/onboarding_step/role_permissions/role_permissions.json b/frappe/custom/onboarding_step/role_permissions/role_permissions.json
new file mode 100644
index 0000000000..a817126989
--- /dev/null
+++ b/frappe/custom/onboarding_step/role_permissions/role_permissions.json
@@ -0,0 +1,20 @@
+{
+ "action": "Watch Video",
+ "creation": "2021-11-23 14:00:27.208500",
+ "description": "In ERPNext, you can add your Employees as Users, and give them restricted access. Tools like Role Permission and User Permission allow you to define rules which give restricted access to the user to masters and transactions.",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-11-24 15:04:14.615232",
+ "modified_by": "Administrator",
+ "name": "Role Permissions",
+ "owner": "Administrator",
+ "show_form_tour": 0,
+ "show_full_form": 0,
+ "title": "Setup Limited Access for a User",
+ "validate_action": 1,
+ "video_url": "https://youtu.be/g3mk45o1zAg"
+}
\ No newline at end of file
diff --git a/frappe/custom/onboarding_step/workflows/workflows.json b/frappe/custom/onboarding_step/workflows/workflows.json
new file mode 100644
index 0000000000..683b7a398a
--- /dev/null
+++ b/frappe/custom/onboarding_step/workflows/workflows.json
@@ -0,0 +1,20 @@
+{
+ "action": "Watch Video",
+ "creation": "2021-11-23 13:58:58.530044",
+ "description": "Workflows allow you to define custom rules for the approval process of a particular document in ERPNext. You can also set complex Workflow Rules and set approval conditions.",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-11-24 15:04:14.632144",
+ "modified_by": "Administrator",
+ "name": "Workflows",
+ "owner": "Administrator",
+ "show_form_tour": 0,
+ "show_full_form": 0,
+ "title": "Setup Approval Workflows",
+ "validate_action": 1,
+ "video_url": "https://youtu.be/yObJUg9FxFs"
+}
\ No newline at end of file
diff --git a/frappe/custom/workspace/customization/customization.json b/frappe/custom/workspace/customization/customization.json
index cdc3b73366..8938bdec9c 100644
--- a/frappe/custom/workspace/customization/customization.json
+++ b/frappe/custom/workspace/customization/customization.json
@@ -1,23 +1,20 @@
{
- "category": "Administration",
"charts": [],
+ "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Customization\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"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\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"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_another_page": 0,
+ "for_user": "",
"hide_custom": 0,
"icon": "customization",
"idx": 0,
- "is_default": 0,
- "is_standard": 1,
"label": "Customization",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Dashboards",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -26,6 +23,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Dashboard",
+ "link_count": 0,
"link_to": "Dashboard",
"link_type": "DocType",
"onboard": 0,
@@ -36,6 +34,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Dashboard Chart",
+ "link_count": 0,
"link_to": "Dashboard Chart",
"link_type": "DocType",
"onboard": 0,
@@ -46,6 +45,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 +55,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Form Customization",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -63,6 +64,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Customize Form",
+ "link_count": 0,
"link_to": "Customize Form",
"link_type": "DocType",
"onboard": 0,
@@ -73,6 +75,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Custom Field",
+ "link_count": 0,
"link_to": "Custom Field",
"link_type": "DocType",
"onboard": 0,
@@ -83,6 +86,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Client Script",
+ "link_count": 0,
"link_to": "Client Script",
"link_type": "DocType",
"onboard": 0,
@@ -93,6 +97,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "DocType",
+ "link_count": 0,
"link_to": "DocType",
"link_type": "DocType",
"onboard": 0,
@@ -102,6 +107,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Other",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -110,19 +116,23 @@
"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-11-24 16:20:03.500885",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customization",
"owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
+ "parent_page": "",
+ "public": 1,
+ "restrict_to_domain": "",
+ "roles": [],
+ "sequence_id": 8,
"shortcuts": [
{
"label": "Customize Form",
@@ -145,5 +155,6 @@
"link_to": "Server Script",
"type": "DocType"
}
- ]
+ ],
+ "title": "Customization"
}
\ No newline at end of file
diff --git a/frappe/data/google_fonts.json b/frappe/data/google_fonts.json
new file mode 100644
index 0000000000..232e509e77
--- /dev/null
+++ b/frappe/data/google_fonts.json
@@ -0,0 +1,56 @@
+[
+ "Alegreya Sans",
+ "Alegreya",
+ "Andada Pro",
+ "Anton",
+ "Archivo Narrow",
+ "Archivo",
+ "BioRhyme",
+ "Cardo",
+ "Chivo",
+ "Cormorant",
+ "Crimson Text",
+ "DM Sans",
+ "Eczar",
+ "Encode Sans",
+ "Epilogue ",
+ "Fira Sans",
+ "Hahmlet",
+ "IBM Plex Sans",
+ "Inconsolata",
+ "Inknut Antiqua",
+ "Inter",
+ "JetBrains Mono",
+ "Karla",
+ "Lato",
+ "Libre Baskerville",
+ "Libre Franklin",
+ "Lora",
+ "Manrope",
+ "Merriweather",
+ "Montserrat",
+ "Neuton",
+ "Nunito",
+ "Old Standard TT",
+ "Open Sans",
+ "Oswald",
+ "Oxygen",
+ "Playfair Display",
+ "Poppins",
+ "Proza Libre",
+ "PT Sans",
+ "PT Serif",
+ "Raleway",
+ "Roboto Slab",
+ "Roboto",
+ "Rubik",
+ "Sora",
+ "Source Sans Pro",
+ "Source Serif Pro",
+ "Space Grotesk",
+ "Space Mono",
+ "Spectral",
+ "Syne",
+ "Work Sans"
+]
+
diff --git a/frappe/data/sample_site_config.json b/frappe/data/sample_site_config.json
deleted file mode 100644
index 715cd7b9fa..0000000000
--- a/frappe/data/sample_site_config.json
+++ /dev/null
@@ -1,45 +0,0 @@
-{
- "db_name": "testdb",
- "db_password": "password",
- "mute_emails": true,
-
- "limits": {
- "emails": 1500,
- "space": 0.157,
- "expiry": "2016-07-25",
- "users": 1
- },
-
- "developer_mode": 1,
- "auto_cache_clear": true,
- "disable_website_cache": true,
- "max_file_size": 1000000,
-
- "mail_server": "localhost",
- "mail_login": null,
- "mail_password": null,
- "mail_port": 25,
- "use_ssl": 0,
- "auto_email_id": "hello@example.com",
-
- "google_analytics_id": "google_analytics_id",
- "google_analytics_anonymize_ip": 1,
-
- "google_login": {
- "client_id": "google_client_id",
- "client_secret": "google_client_secret"
- },
- "github_login": {
- "client_id": "github_client_id",
- "client_secret": "github_client_secret"
- },
- "facebook_login": {
- "client_id": "facebook_client_id",
- "client_secret": "facebook_client_secret"
- },
-
- "celery_broker": "redis://localhost",
- "celery_result_backend": null,
- "scheduler_interval": 300,
- "celery_queue_per_site": true
-}
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 d1137f2e67..2e4e4d45b3 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
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe, os
from frappe.model.document import Document
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 fd45f86ec1..ffc96c8266 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,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
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 5cb20ba56c..46d33eaca9 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
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
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 df11fc0522..b1040aaa58 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,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
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 6d3ef50937..ce46f60f67 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
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from frappe.model.document import 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 a8d0e40a4c..d13912b431 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
@@ -1,6 +1,5 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# Copyright (c) 2021, Frappe Technologies and contributors
+# License: MIT. See LICENSE
import frappe
from frappe.modules import get_module_path, scrub_dt_dn
@@ -8,6 +7,20 @@ from frappe.modules.export_file import export_to_files, create_init_py
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.model.document import Document
+
+def get_mapping_module(module, mapping_name):
+ app_name = frappe.db.get_value("Module Def", module, "app_name")
+ mapping_name = frappe.scrub(mapping_name)
+ module = frappe.scrub(module)
+
+ try:
+ return frappe.get_module(
+ f"{app_name}.{module}.data_migration_mapping.{mapping_name}"
+ )
+ except ImportError:
+ return None
+
+
class DataMigrationPlan(Document):
def on_update(self):
# update custom fields in mappings
@@ -54,26 +67,14 @@ class DataMigrationPlan(Document):
frappe.flags.ignore_in_install = False
def pre_process_doc(self, mapping_name, doc):
- module = self.get_mapping_module(mapping_name)
+ module = get_mapping_module(self.module, mapping_name)
if module and hasattr(module, 'pre_process'):
return module.pre_process(doc)
return doc
def post_process_doc(self, mapping_name, local_doc=None, remote_doc=None):
- module = self.get_mapping_module(mapping_name)
+ module = get_mapping_module(self.module, mapping_name)
if module and hasattr(module, 'post_process'):
return module.post_process(local_doc=local_doc, remote_doc=remote_doc)
-
- def get_mapping_module(self, mapping_name):
- try:
- module_def = frappe.get_doc("Module Def", self.module)
- module = frappe.get_module('{app}.{module}.data_migration_mapping.{mapping_name}'.format(
- app= module_def.app_name,
- module=frappe.scrub(self.module),
- mapping_name=frappe.scrub(mapping_name)
- ))
- return module
- except ImportError:
- return None
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 14c585a82d..649f7db903 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,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
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 ba4cf28eb8..7939a68d97 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
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from frappe.model.document import 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 c35af5827b..deb14baf27 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
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe, json, math
from frappe.model.document import Document
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 ef7b70dca2..485f86a7f9 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,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe, unittest
class TestDataMigrationRun(unittest.TestCase):
diff --git a/frappe/database/__init__.py b/frappe/database/__init__.py
index a899bec3d1..b0e3183d4f 100644
--- a/frappe/database/__init__.py
+++ b/frappe/database/__init__.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
# Database Module
# --------------------
diff --git a/frappe/database/database.py b/frappe/database/database.py
index b1dec95139..0f325a746e 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
# Database Module
# --------------------
@@ -14,8 +14,13 @@ import frappe.model.meta
from frappe import _
from time import time
-from frappe.utils import now, getdate, cast_fieldtype, get_datetime, get_table_name
+from frappe.utils import now, getdate, cast, get_datetime
from frappe.model.utils.link_count import flush_local_link_count
+from frappe.query_builder.functions import Count
+from frappe.query_builder.functions import Min, Max, Avg, Sum
+from frappe.query_builder.utils import Column
+from .query import Query
+from pypika.terms import Criterion, PseudoColumn
class Database(object):
@@ -32,6 +37,7 @@ class Database(object):
STANDARD_VARCHAR_COLUMNS = ('name', 'owner', 'modified_by', 'parent', 'parentfield', 'parenttype')
DEFAULT_COLUMNS = ['name', 'creation', 'modified', 'modified_by', 'owner', 'docstatus', 'parent',
'parentfield', 'parenttype', 'idx']
+ MAX_WRITES_PER_TRANSACTION = 200_000
class InvalidColumnName(frappe.ValidationError): pass
@@ -55,6 +61,7 @@ class Database(object):
self.password = password or frappe.conf.db_password
self.value_cache = {}
+ self.query = Query()
def setup_type_map(self):
pass
@@ -77,7 +84,8 @@ class Database(object):
pass
def sql(self, query, values=(), as_dict = 0, as_list = 0, formatted = 0,
- debug=0, ignore_ddl=0, as_utf8=0, auto_commit=0, update=None, explain=False):
+ debug=0, ignore_ddl=0, as_utf8=0, auto_commit=0, update=None,
+ explain=False, run=True, pluck=False):
"""Execute a SQL query and fetch all rows.
:param query: SQL query.
@@ -90,7 +98,7 @@ class Database(object):
:param as_utf8: Encode values as UTF 8.
:param auto_commit: Commit after executing the query.
:param update: Update this dict to all rows (if returned `as_dict`).
-
+ :param run: Returns query without executing it if False.
Examples:
# return customer names as dicts
@@ -105,6 +113,9 @@ class Database(object):
"""
query = str(query)
+ if not run:
+ return 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)
@@ -159,6 +170,12 @@ class Database(object):
frappe.errprint('Syntax error in query:')
frappe.errprint(query)
+ elif self.is_deadlocked(e):
+ raise frappe.QueryDeadlockError(e)
+
+ elif self.is_timedout(e):
+ raise frappe.QueryTimeoutError(e)
+
if ignore_ddl and (self.is_missing_column(e) or self.is_missing_table(e) or self.cant_drop_field_or_key(e)):
pass
else:
@@ -169,6 +186,9 @@ class Database(object):
if not self._cursor.description:
return ()
+ if pluck:
+ return [r[0] for r in self._cursor.fetchall()]
+
# scrub output if required
if as_dict:
ret = self.fetch_as_dict(formatted, as_utf8)
@@ -224,7 +244,7 @@ class Database(object):
except Exception:
frappe.errprint("error in query explain")
- def sql_list(self, query, values=(), debug=False):
+ def sql_list(self, query, values=(), debug=False, **kwargs):
"""Return data as list of single elements (first column).
Example:
@@ -232,7 +252,7 @@ class Database(object):
# doctypes = ["DocType", "DocField", "User", ...]
doctypes = frappe.db.sql_list("select name from DocType")
"""
- return [r[0] for r in self.sql(query, values, debug=debug)]
+ return [r[0] for r in self.sql(query, values, **kwargs, debug=debug)]
def sql_ddl(self, query, values=(), debug=False):
"""Commit and execute a query. DDL (Data Definition Language) queries that alter schema
@@ -240,6 +260,7 @@ class Database(object):
self.commit()
self.sql(query, debug=debug)
+
def check_transaction_status(self, query):
"""Raises exception if more than 20,000 `INSERT`, `UPDATE` queries are
executed in one transaction. This is to ensure that writes are always flushed otherwise this
@@ -253,7 +274,7 @@ class Database(object):
if query[:6].lower() in ('update', 'insert', 'delete'):
self.transaction_writes += 1
- if self.transaction_writes > 200000:
+ if self.transaction_writes > self.MAX_WRITES_PER_TRANSACTION:
if self.auto_commit_on_many_writes:
self.commit()
else:
@@ -310,65 +331,12 @@ class Database(object):
nres.append(nr)
return nres
- def build_conditions(self, filters):
- """Convert filters sent as dict, lists to SQL conditions. filter's key
- is passed by map function, build conditions like:
-
- * ifnull(`fieldname`, default_value) = %(fieldname)s
- * `fieldname` [=, !=, >, >=, <, <=] %(fieldname)s
- """
- conditions = []
- values = {}
- def _build_condition(key):
- """
- filter's key is passed by map function
- build conditions like:
- * ifnull(`fieldname`, default_value) = %(fieldname)s
- * `fieldname` [=, !=, >, >=, <, <=] %(fieldname)s
- """
- _operator = "="
- _rhs = " %(" + key + ")s"
- value = filters.get(key)
- values[key] = value
- if isinstance(value, (list, tuple)):
- # value is a tuple like ("!=", 0)
- _operator = value[0]
- 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]))
- del values[key]
-
- if _operator not in ["=", "!=", ">", ">=", "<", "<=", "like", "in", "not in", "not like"]:
- _operator = "="
-
- if "[" in key:
- split_key = key.split("[")
- condition = "coalesce(`" + split_key[0] + "`, " + split_key[1][:-1] + ") " \
- + _operator + _rhs
- else:
- condition = "`" + key + "` " + _operator + _rhs
-
- conditions.append(condition)
-
- if isinstance(filters, int):
- # docname is a number, convert to string
- filters = str(filters)
-
- if isinstance(filters, str):
- filters = { "name": filters }
-
- for f in filters:
- _build_condition(f)
-
- return " and ".join(conditions), values
-
def get(self, doctype, filters=None, as_dict=True, cache=False):
"""Returns `get_value` with fieldname='*'"""
return self.get_value(doctype, filters, "*", as_dict=as_dict, cache=cache)
def get_value(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
- debug=False, order_by=None, cache=False, for_update=False):
+ debug=False, order_by="KEEP_DEFAULT_ORDERING", cache=False, for_update=False, run=True, pluck=False):
"""Returns a document property or list of properties.
:param doctype: DocType name.
@@ -395,12 +363,16 @@ class Database(object):
"""
ret = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug,
- order_by, cache=cache, for_update=for_update)
+ order_by, cache=cache, for_update=for_update, run=run, pluck=pluck)
+
+ if not run:
+ return ret
return ((len(ret[0]) > 1 or as_dict) and ret[0] or ret[0][0]) if ret else None
def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
- debug=False, order_by=None, update=None, cache=False, for_update=False):
+ debug=False, order_by="KEEP_DEFAULT_ORDERING", update=None, cache=False, for_update=False,
+ run=True, pluck=False):
"""Returns multiple document properties.
:param doctype: DocType name.
@@ -424,10 +396,8 @@ class Database(object):
(doctype, filters, fieldname) in self.value_cache:
return self.value_cache[(doctype, filters, fieldname)]
- if not order_by: order_by = 'modified desc'
-
if isinstance(filters, list):
- out = self._get_value_for_many_names(doctype, filters, fieldname, debug=debug)
+ out = self._get_value_for_many_names(doctype, filters, fieldname, order_by, debug=debug, run=run, pluck=pluck)
else:
fields = fieldname
@@ -439,26 +409,49 @@ class Database(object):
if (filters is not None) and (filters!=doctype or doctype=="DocType"):
try:
- out = self._get_values_from_table(fields, filters, doctype, as_dict, debug, order_by, update, for_update=for_update)
+ if order_by:
+ order_by = "modified" if order_by == "KEEP_DEFAULT_ORDERING" else order_by
+ out = self._get_values_from_table(
+ fields,
+ filters,
+ doctype,
+ as_dict,
+ debug,
+ order_by,
+ update,
+ for_update=for_update,
+ run=run,
+ pluck=pluck,
+ )
except Exception as e:
if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)):
# table or column not found, return None
out = None
elif (not ignore) and frappe.db.is_table_missing(e):
# table not found, look in singles
- out = self.get_values_from_single(fields, filters, doctype, as_dict, debug, update)
+ out = self.get_values_from_single(fields, filters, doctype, as_dict, debug, update, run=run)
else:
raise
else:
- out = self.get_values_from_single(fields, filters, doctype, as_dict, debug, update)
+ out = self.get_values_from_single(fields, filters, doctype, as_dict, debug, update, run=run, pluck=pluck)
if cache and isinstance(filters, str):
self.value_cache[(doctype, filters, fieldname)] = out
return out
- def get_values_from_single(self, fields, filters, doctype, as_dict=False, debug=False, update=None):
+ def get_values_from_single(
+ self,
+ fields,
+ filters,
+ doctype,
+ as_dict=False,
+ debug=False,
+ update=None,
+ run=True,
+ pluck=False,
+ ):
"""Get values from `tabSingles` (Single DocTypes) (internal).
:param fields: List of fields,
@@ -484,11 +477,18 @@ class Database(object):
return [map(values.get, fields)]
else:
- r = self.sql("""select field, value
+ r = self.sql(
+ """select field, value
from `tabSingles` where field in (%s) and doctype=%s"""
- % (', '.join(['%s'] * len(fields)), '%s'),
- tuple(fields) + (doctype,), as_dict=False, debug=debug)
-
+ % (", ".join(["%s"] * len(fields)), "%s"),
+ tuple(fields) + (doctype,),
+ as_dict=False,
+ debug=debug,
+ run=run,
+ pluck=pluck,
+ )
+ if not run:
+ return r
if as_dict:
if r:
r = frappe._dict(r)
@@ -511,15 +511,10 @@ class Database(object):
# Get coulmn and value of the single doctype Accounts Settings
account_settings = frappe.db.get_singles_dict("Accounts Settings")
"""
- result = self.sql("""
- SELECT field, value
- FROM `tabSingles`
- WHERE doctype = %s
- """, doctype)
- # result = _cast_result(doctype, result)
-
+ result = self.query.get_sql(
+ "Singles", filters={"doctype": doctype}, fields=["field", "value"]
+ ).run()
dict_ = frappe._dict(result)
-
return dict_
@staticmethod
@@ -543,13 +538,16 @@ 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]
- val = self.sql("""select `value` from
- `tabSingles` where `doctype`=%s and `field`=%s""", (doctype, fieldname))
+ val = self.query.get_sql(
+ table="Singles",
+ filters={"doctype": doctype, "field": fieldname},
+ fields="value",
+ ).run()
val = val[0][0] if val else None
df = frappe.get_meta(doctype).get_field(fieldname)
@@ -557,7 +555,7 @@ class Database(object):
if not df:
frappe.throw(_('Invalid field name: {0}').format(frappe.bold(fieldname)), self.InvalidColumnName)
- val = cast_fieldtype(df.fieldtype, val)
+ val = cast(df.fieldtype, val)
self.value_cache[doctype][fieldname] = val
@@ -567,44 +565,61 @@ class Database(object):
"""Alias for get_single_value"""
return self.get_single_value(*args, **kwargs)
- def _get_values_from_table(self, fields, filters, doctype, as_dict, debug, order_by=None, update=None, for_update=False):
- fl = []
- if isinstance(fields, (list, tuple)):
- for f in fields:
- if "(" in f or " as " in f: # function
- fl.append(f)
+ def _get_values_from_table(
+ self,
+ fields,
+ filters,
+ doctype,
+ as_dict,
+ debug,
+ order_by=None,
+ update=None,
+ for_update=False,
+ run=True,
+ pluck=False,
+ ):
+ field_objects = []
+
+ if not isinstance(fields, Criterion):
+ for field in fields:
+ if "(" in str(field) or " as " in str(field):
+ field_objects.append(PseudoColumn(field))
else:
- fl.append("`" + f + "`")
- fl = ", ".join(fl)
- else:
- fl = fields
- if fields=="*":
- as_dict = True
+ field_objects.append(field)
- conditions, values = self.build_conditions(filters)
-
- order_by = ("order by " + order_by) if order_by else ""
-
- r = self.sql("select {fields} from `tab{doctype}` {where} {conditions} {order_by} {for_update}"
- .format(
- for_update = 'for update' if for_update else '',
- fields = fl,
- doctype = doctype,
- where = "where" if conditions else "",
- conditions = conditions,
- order_by = order_by),
- values, as_dict=as_dict, debug=debug, update=update)
+ query = self.query.get_sql(
+ table=doctype,
+ filters=filters,
+ orderby=order_by,
+ for_update=for_update,
+ field_objects=field_objects,
+ fields=fields,
+ )
+ if (
+ fields == "*"
+ and not isinstance(fields, (list, tuple))
+ and not isinstance(fields, Criterion)
+ ):
+ as_dict = True
+ r = self.sql(
+ query, as_dict=as_dict, debug=debug, update=update, run=run, pluck=pluck
+ )
return r
- def _get_value_for_many_names(self, doctype, names, field, debug=False):
+ def _get_value_for_many_names(self, doctype, names, field, order_by, debug=False, run=True, pluck=False):
names = list(filter(None, names))
-
if names:
- return self.get_all(doctype,
- fields=['name', field],
- filters=[['name', 'in', names]],
- debug=debug, as_list=1)
+ return self.get_all(
+ doctype,
+ fields=field,
+ filters=names,
+ order_by=order_by,
+ pluck=pluck,
+ debug=debug,
+ as_list=1,
+ run=run,
+ )
else:
return {}
@@ -649,7 +664,7 @@ class Database(object):
for key in to_update:
set_values.append('`{0}`=%({0})s'.format(key))
- for name in self.get_values(dt, dn, 'name', for_update=for_update):
+ for name in self.get_values(dt, dn, 'name', for_update=for_update, debug=debug):
values = dict(name=name[0])
values.update(to_update)
@@ -826,18 +841,14 @@ class Database(object):
cache_count = frappe.cache().get_value('doctype:count:{}'.format(dt))
if cache_count is not None:
return cache_count
+ query = self.query.get_sql(table=dt, filters=filters, fields=Count("*"))
if filters:
- conditions, filters = self.build_conditions(filters)
- count = self.sql("""select count(*)
- from `tab%s` where %s""" % (dt, conditions), filters, debug=debug)[0][0]
+ count = self.sql(query, debug=debug)[0][0]
return count
else:
- count = self.sql("""select count(*)
- from `tab%s`""" % (dt,))[0][0]
-
+ count = self.sql(query, debug=debug)[0][0]
if cache:
frappe.cache().set_value('doctype:count:{}'.format(dt), count, expires_in_sec = 86400)
-
return count
@staticmethod
@@ -896,13 +907,13 @@ class Database(object):
WHERE table_name = 'tab{0}' AND column_name = '{1}' '''.format(doctype, column))[0][0]
def has_index(self, table_name, index_name):
- pass
+ raise NotImplementedError
def add_index(self, doctype, fields, index_name=None):
- pass
+ raise NotImplementedError
def add_unique(self, doctype, fields, constraint_name=None):
- pass
+ raise NotImplementedError
@staticmethod
def get_index_name(fields):
@@ -928,7 +939,7 @@ class Database(object):
def escape(s, percent=True):
"""Excape quotes and percent in given string."""
# implemented in specific class
- pass
+ raise NotImplementedError
@staticmethod
def is_column_missing(e):
@@ -961,16 +972,9 @@ class Database(object):
"""
values = ()
filters = filters or kwargs.get("conditions")
- table = get_table_name(doctype)
- query = f"DELETE FROM `{table}`"
-
+ query = self.query.build_conditions(table=doctype, filters=filters).delete()
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):
@@ -1052,19 +1056,3 @@ def enqueue_jobs_after_commit():
q.enqueue_call(execute_job, timeout=job.get("timeout"),
kwargs=job.get("queue_args"))
frappe.flags.enqueue_after_commit = []
-
-# Helpers
-def _cast_result(doctype, result):
- batch = [ ]
-
- try:
- for field, value in result:
- df = frappe.get_meta(doctype).get_field(field)
- if df:
- value = cast_fieldtype(df.fieldtype, value)
-
- batch.append(tuple([field, value]))
- except frappe.exceptions.DoesNotExistError:
- return result
-
- return tuple(batch)
diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py
index 5dd6d9e58a..2f6d640743 100644
--- a/frappe/database/mariadb/database.py
+++ b/frappe/database/mariadb/database.py
@@ -22,11 +22,11 @@ class MariaDBDatabase(Database):
def setup_type_map(self):
self.db_type = 'mariadb'
self.type_map = {
- 'Currency': ('decimal', '18,6'),
+ 'Currency': ('decimal', '21,9'),
'Int': ('int', '11'),
'Long Int': ('bigint', '20'),
- 'Float': ('decimal', '18,6'),
- 'Percent': ('decimal', '18,6'),
+ 'Float': ('decimal', '21,9'),
+ 'Percent': ('decimal', '21,9'),
'Check': ('int', '1'),
'Small Text': ('text', ''),
'Long Text': ('longtext', ''),
@@ -51,7 +51,8 @@ class MariaDBDatabase(Database):
'Color': ('varchar', self.VARCHAR_LEN),
'Barcode': ('longtext', ''),
'Geolocation': ('longtext', ''),
- 'Duration': ('decimal', '18,6')
+ 'Duration': ('decimal', '21,9'),
+ 'Icon': ('varchar', self.VARCHAR_LEN)
}
def get_connection(self):
@@ -134,8 +135,8 @@ class MariaDBDatabase(Database):
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)
+ def change_column_type(self, doctype: str, column: str, type: str) -> Union[List, Tuple]:
+ table_name = get_table_name(doctype)
return self.sql(f"ALTER TABLE `{table_name}` MODIFY `{column}` {type} NOT NULL")
# exception types
@@ -194,7 +195,7 @@ class MariaDBDatabase(Database):
`password` TEXT NOT NULL,
`encrypted` INT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`doctype`, `name`, `fieldname`)
- ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci""")
+ ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci""")
def create_global_search_table(self):
if not '__global_search' in self.get_tables():
@@ -255,11 +256,11 @@ class MariaDBDatabase(Database):
index_name=index_name
))
- def add_index(self, doctype, fields, index_name=None):
+ def add_index(self, doctype: str, fields: List, index_name: str = None):
"""Creates an index with given fields if not already created.
Index name will be `fieldname1_fieldname2_index`"""
index_name = index_name or self.get_index_name(fields)
- table_name = 'tab' + doctype
+ table_name = get_table_name(doctype)
if not self.has_index(table_name, index_name):
self.commit()
self.sql("""ALTER TABLE `%s`
diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql
index a74ece8478..0def9ef748 100644
--- a/frappe/database/mariadb/framework_mariadb.sql
+++ b/frappe/database/mariadb/framework_mariadb.sql
@@ -61,6 +61,7 @@ CREATE TABLE `tabDocField` (
`in_preview` int(1) NOT NULL DEFAULT 0,
`read_only` int(1) NOT NULL DEFAULT 0,
`precision` varchar(255) DEFAULT NULL,
+ `max_height` varchar(10) DEFAULT NULL,
`length` int(11) NOT NULL DEFAULT 0,
`translatable` int(1) NOT NULL DEFAULT 0,
`hide_border` int(1) NOT NULL DEFAULT 0,
@@ -71,7 +72,7 @@ CREATE TABLE `tabDocField` (
KEY `label` (`label`),
KEY `fieldtype` (`fieldtype`),
KEY `fieldname` (`fieldname`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
@@ -108,7 +109,7 @@ CREATE TABLE `tabDocPerm` (
`email` int(1) NOT NULL DEFAULT 1,
PRIMARY KEY (`name`),
KEY `parent` (`parent`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Table structure for table `tabDocType Action`
@@ -132,7 +133,7 @@ CREATE TABLE `tabDocType Action` (
PRIMARY KEY (`name`),
KEY `parent` (`parent`),
KEY `modified` (`modified`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
--
-- Table structure for table `tabDocType Action`
@@ -155,7 +156,7 @@ CREATE TABLE `tabDocType Link` (
PRIMARY KEY (`name`),
KEY `parent` (`parent`),
KEY `modified` (`modified`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
--
-- Table structure for table `tabDocType`
@@ -183,6 +184,7 @@ CREATE TABLE `tabDocType` (
`restrict_to_domain` varchar(255) DEFAULT NULL,
`app` varchar(255) DEFAULT NULL,
`autoname` varchar(255) DEFAULT NULL,
+ `naming_rule` varchar(40) DEFAULT NULL,
`name_case` varchar(255) DEFAULT NULL,
`title_field` varchar(255) DEFAULT NULL,
`image_field` varchar(255) DEFAULT NULL,
@@ -225,9 +227,10 @@ CREATE TABLE `tabDocType` (
`subject_field` varchar(255) DEFAULT NULL,
`sender_field` varchar(255) DEFAULT NULL,
`show_title_field_in_link` int(1) NOT NULL DEFAULT 0,
+ `migration_hash` varchar(255) DEFAULT NULL,
PRIMARY KEY (`name`),
KEY `parent` (`parent`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Table structure for table `tabSeries`
@@ -238,7 +241,7 @@ CREATE TABLE `tabSeries` (
`name` varchar(100),
`current` int(10) NOT NULL DEFAULT 0,
PRIMARY KEY(`name`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
@@ -255,7 +258,7 @@ CREATE TABLE `tabSessions` (
`device` varchar(255) DEFAULT 'desktop',
`status` varchar(20) DEFAULT NULL,
KEY `sid` (`sid`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
@@ -268,7 +271,7 @@ CREATE TABLE `tabSingles` (
`field` varchar(255) DEFAULT NULL,
`value` text,
KEY `singles_doctype_field_index` (`doctype`, `field`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Table structure for table `__Auth`
@@ -282,7 +285,7 @@ CREATE TABLE `__Auth` (
`password` TEXT NOT NULL,
`encrypted` INT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`doctype`, `name`, `fieldname`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Table structure for table `tabFile`
@@ -310,7 +313,7 @@ CREATE TABLE `tabFile` (
KEY `parent` (`parent`),
KEY `attached_to_name` (`attached_to_name`),
KEY `attached_to_doctype` (`attached_to_doctype`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Table structure for table `tabDefaultValue`
@@ -333,4 +336,4 @@ CREATE TABLE `tabDefaultValue` (
PRIMARY KEY (`name`),
KEY `parent` (`parent`),
KEY `defaultvalue_parent_defkey_index` (`parent`,`defkey`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py
index b40af59286..5768a2f23d 100644
--- a/frappe/database/mariadb/schema.py
+++ b/frappe/database/mariadb/schema.py
@@ -4,18 +4,22 @@ from frappe.database.schema import DBTable
class MariaDBTable(DBTable):
def create(self):
- add_text = ''
+ additional_definitions = ""
+ engine = self.meta.get("engine") or "InnoDB"
+ varchar_len = frappe.db.VARCHAR_LEN
# columns
column_defs = self.get_column_definitions()
- if column_defs: add_text += ',\n'.join(column_defs) + ',\n'
+ if column_defs:
+ additional_definitions += ',\n'.join(column_defs) + ',\n'
# index
index_defs = self.get_index_definitions()
- if index_defs: add_text += ',\n'.join(index_defs) + ',\n'
+ if index_defs:
+ additional_definitions += ',\n'.join(index_defs) + ',\n'
# create table
- frappe.db.sql("""create table `%s` (
+ query = f"""create table `{self.table_name}` (
name varchar({varchar_len}) not null primary key,
creation datetime(6),
modified datetime(6),
@@ -26,13 +30,15 @@ class MariaDBTable(DBTable):
parentfield varchar({varchar_len}),
parenttype varchar({varchar_len}),
idx int(8) not null default '0',
- %sindex parent(parent),
+ {additional_definitions}
+ index parent(parent),
index modified(modified))
ENGINE={engine}
- ROW_FORMAT=COMPRESSED
+ ROW_FORMAT=DYNAMIC
CHARACTER SET=utf8mb4
- COLLATE=utf8mb4_unicode_ci""".format(varchar_len=frappe.db.VARCHAR_LEN,
- engine=self.meta.get("engine") or 'InnoDB') % (self.table_name, add_text))
+ COLLATE=utf8mb4_unicode_ci"""
+
+ frappe.db.sql(query)
def alter(self):
for col in self.columns.values():
diff --git a/frappe/database/mariadb/setup_db.py b/frappe/database/mariadb/setup_db.py
index 6be08c66bb..8088cc2331 100644
--- a/frappe/database/mariadb/setup_db.py
+++ b/frappe/database/mariadb/setup_db.py
@@ -34,25 +34,23 @@ def setup_database(force, source_sql, verbose, no_mariadb_socket=False):
db_name = frappe.local.conf.db_name
root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password)
dbman = DbManager(root_conn)
+ dbman_kwargs = {}
+ if no_mariadb_socket:
+ dbman_kwargs["host"] = "%"
+
if force or (db_name not in dbman.get_database_list()):
- dbman.delete_user(db_name)
- if no_mariadb_socket:
- dbman.delete_user(db_name, host="%")
+ dbman.delete_user(db_name, **dbman_kwargs)
dbman.drop_database(db_name)
else:
raise Exception("Database %s already exists" % (db_name,))
- dbman.create_user(db_name, frappe.conf.db_password)
- if no_mariadb_socket:
- dbman.create_user(db_name, frappe.conf.db_password, host="%")
+ dbman.create_user(db_name, frappe.conf.db_password, **dbman_kwargs)
if verbose: print("Created user %s" % db_name)
dbman.create_database(db_name)
if verbose: print("Created database %s" % db_name)
- dbman.grant_all_privileges(db_name, db_name)
- if no_mariadb_socket:
- dbman.grant_all_privileges(db_name, db_name, host="%")
+ dbman.grant_all_privileges(db_name, db_name, **dbman_kwargs)
dbman.flush_privileges()
if verbose: print("Granted privileges to user %s and database %s" % (db_name, db_name))
diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py
index 0b73c8b44b..bfa5515111 100644
--- a/frappe/database/postgres/database.py
+++ b/frappe/database/postgres/database.py
@@ -4,6 +4,7 @@ from typing import List, Tuple, Union
import psycopg2
import psycopg2.extensions
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
+from psycopg2.errorcodes import STRING_DATA_RIGHT_TRUNCATION
import frappe
from frappe.database.database import Database
@@ -31,11 +32,11 @@ class PostgresDatabase(Database):
def setup_type_map(self):
self.db_type = 'postgres'
self.type_map = {
- 'Currency': ('decimal', '18,6'),
+ 'Currency': ('decimal', '21,9'),
'Int': ('bigint', None),
'Long Int': ('bigint', None),
- 'Float': ('decimal', '18,6'),
- 'Percent': ('decimal', '18,6'),
+ 'Float': ('decimal', '21,9'),
+ 'Percent': ('decimal', '21,9'),
'Check': ('smallint', None),
'Small Text': ('text', ''),
'Long Text': ('text', ''),
@@ -60,7 +61,8 @@ class PostgresDatabase(Database):
'Color': ('varchar', self.VARCHAR_LEN),
'Barcode': ('text', ''),
'Geolocation': ('text', ''),
- 'Duration': ('decimal', '18,6')
+ 'Duration': ('decimal', '21,9'),
+ 'Icon': ('varchar', self.VARCHAR_LEN)
}
def get_connection(self):
@@ -170,7 +172,7 @@ class PostgresDatabase(Database):
@staticmethod
def is_data_too_long(e):
- return e.pgcode == '22001'
+ return e.pgcode == STRING_DATA_RIGHT_TRUNCATION
def rename_table(self, old_name: str, new_name: str) -> Union[List, Tuple]:
old_name = get_table_name(old_name)
@@ -181,8 +183,8 @@ class PostgresDatabase(Database):
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)
+ def change_column_type(self, doctype: str, column: str, type: str) -> Union[List, Tuple]:
+ table_name = get_table_name(doctype)
return self.sql(f'ALTER TABLE "{table_name}" ALTER COLUMN "{column}" TYPE {type}')
def create_auth_table(self):
@@ -257,14 +259,14 @@ class PostgresDatabase(Database):
return self.sql("""SELECT 1 FROM pg_indexes WHERE tablename='{table_name}'
and indexname='{index_name}' limit 1""".format(table_name=table_name, index_name=index_name))
- def add_index(self, doctype, fields, index_name=None):
+ def add_index(self, doctype: str, fields: List, index_name: str = None):
"""Creates an index with given fields if not already created.
Index name will be `fieldname1_fieldname2_index`"""
+ table_name = get_table_name(doctype)
index_name = index_name or self.get_index_name(fields)
- table_name = 'tab' + doctype
+ fields_str = '", "'.join(re.sub(r"\(.*\)", "", field) for field in fields)
- self.commit()
- self.sql("""CREATE INDEX IF NOT EXISTS "{}" ON `{}`("{}")""".format(index_name, table_name, '", "'.join(fields)))
+ self.sql_ddl(f'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}` ("{fields_str}")')
def add_unique(self, doctype, fields, constraint_name=None):
if isinstance(fields, str):
diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql
index f65a832bc6..d90d26cb90 100644
--- a/frappe/database/postgres/framework_postgres.sql
+++ b/frappe/database/postgres/framework_postgres.sql
@@ -61,6 +61,7 @@ CREATE TABLE "tabDocField" (
"in_preview" smallint NOT NULL DEFAULT 0,
"read_only" smallint NOT NULL DEFAULT 0,
"precision" varchar(255) DEFAULT NULL,
+ "max_height" varchar(10) DEFAULT NULL,
"length" bigint NOT NULL DEFAULT 0,
"translatable" smallint NOT NULL DEFAULT 0,
"hide_border" smallint NOT NULL DEFAULT 0,
@@ -188,6 +189,7 @@ CREATE TABLE "tabDocType" (
"restrict_to_domain" varchar(255) DEFAULT NULL,
"app" varchar(255) DEFAULT NULL,
"autoname" varchar(255) DEFAULT NULL,
+ "naming_rule" varchar(40) DEFAULT NULL,
"name_case" varchar(255) DEFAULT NULL,
"title_field" varchar(255) DEFAULT NULL,
"image_field" varchar(255) DEFAULT NULL,
@@ -230,6 +232,7 @@ CREATE TABLE "tabDocType" (
"subject_field" varchar(255) DEFAULT NULL,
"sender_field" varchar(255) DEFAULT NULL,
"show_title_field_in_link" smallint NOT NULL DEFAULT 0,
+ "migration_hash" varchar(255) DEFAULT NULL,
PRIMARY KEY ("name")
) ;
diff --git a/frappe/database/query.py b/frappe/database/query.py
new file mode 100644
index 0000000000..6d2be5fa25
--- /dev/null
+++ b/frappe/database/query.py
@@ -0,0 +1,329 @@
+import operator
+import re
+from typing import Any, Dict, List, Tuple, Union
+
+import frappe
+from frappe import _
+from frappe.query_builder import Criterion, Field, Order
+
+
+def like(key: str, value: str) -> frappe.qb:
+ """Wrapper method for `LIKE`
+
+ Args:
+ key (str): field
+ value (str): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `LIKE`
+ """
+ return Field(key).like(value)
+
+
+def func_in(key: str, value: Union[List, Tuple]) -> frappe.qb:
+ """Wrapper method for `IN`
+
+ Args:
+ key (str): field
+ value (Union[int, str]): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `IN`
+ """
+ return Field(key).isin(value)
+
+
+def not_like(key: str, value: str) -> frappe.qb:
+ """Wrapper method for `NOT LIKE`
+
+ Args:
+ key (str): field
+ value (str): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `NOT LIKE`
+ """
+ return Field(key).not_like(value)
+
+
+def func_not_in(key: str, value: Union[List, Tuple]):
+ """Wrapper method for `NOT IN`
+
+ Args:
+ key (str): field
+ value (Union[int, str]): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `NOT IN`
+ """
+ return Field(key).notin(value)
+
+
+def func_regex(key: str, value: str) -> frappe.qb:
+ """Wrapper method for `REGEX`
+
+ Args:
+ key (str): field
+ value (str): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `REGEX`
+ """
+ return Field(key).regex(value)
+
+
+def func_between(key: str, value: Union[List, Tuple]) -> frappe.qb:
+ """Wrapper method for `BETWEEN`
+
+ Args:
+ key (str): field
+ value (Union[int, str]): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `BETWEEN`
+ """
+ return Field(key)[slice(*value)]
+
+def make_function(key: Any, value: Union[int, str]):
+ """returns fucntion query
+
+ Args:
+ key (Any): field
+ value (Union[int, str]): criterion
+
+ Returns:
+ frappe.qb: frappe.qb object
+ """
+ return OPERATOR_MAP[value[0]](key, value[1])
+
+
+def change_orderby(order: str):
+ """Convert orderby to standart Order object
+
+ Args:
+ order (str): Field, order
+
+ Returns:
+ tuple: field, order
+ """
+ order = order.split()
+ if order[1].lower() == "asc":
+ orderby, order = order[0], Order.asc
+ return orderby, order
+ orderby, order = order[0], Order.desc
+ return orderby, order
+
+
+OPERATOR_MAP = {
+ "+": operator.add,
+ "=": operator.eq,
+ "-": operator.sub,
+ "!=": operator.ne,
+ "<": operator.lt,
+ ">": operator.gt,
+ "<=": operator.le,
+ ">=": operator.ge,
+ "in": func_in,
+ "not in": func_not_in,
+ "like": like,
+ "not like": not_like,
+ "regex": func_regex,
+ "between": func_between
+ }
+
+
+class Query:
+ def get_condition(self, table: str, **kwargs) -> frappe.qb:
+ """Get initial table object
+
+ Args:
+ table (str): DocType
+
+ Returns:
+ frappe.qb: DocType with initial condition
+ """
+ if kwargs.get("update"):
+ return frappe.qb.update(table)
+ if kwargs.get("into"):
+ return frappe.qb.into(table)
+ return frappe.qb.from_(table)
+
+ def criterion_query(self, table: str, criterion: Criterion, **kwargs) -> frappe.qb:
+ """Generate filters from Criterion objects
+
+ Args:
+ table (str): DocType
+ criterion (Criterion): Filters
+
+ Returns:
+ frappe.qb: condition object
+ """
+ condition = self.add_conditions(self.get_condition(table, **kwargs), **kwargs)
+ return condition.where(criterion)
+
+ def add_conditions(self, conditions: frappe.qb, **kwargs):
+ """Adding additional conditions
+
+ Args:
+ conditions (frappe.qb): built conditions
+
+ Returns:
+ conditions (frappe.qb): frappe.qb object
+ """
+ if kwargs.get("orderby"):
+ orderby = kwargs.get("orderby")
+ order = kwargs.get("order") if kwargs.get("order") else Order.desc
+ if isinstance(orderby, str) and len(orderby.split()) > 1:
+ orderby, order = change_orderby(orderby)
+ conditions = conditions.orderby(orderby, order=order)
+
+ if kwargs.get("limit"):
+ conditions = conditions.limit(kwargs.get("limit"))
+
+ if kwargs.get("distinct"):
+ conditions = conditions.distinct()
+
+ if kwargs.get("for_update"):
+ conditions = conditions.for_update()
+
+ return conditions
+
+ def misc_query(self, table: str, filters: Union[List, Tuple] = None, **kwargs):
+ """Build conditions using the given Lists or Tuple filters
+
+ Args:
+ table (str): DocType
+ filters (Union[List, Tuple], optional): Filters. Defaults to None.
+ """
+ conditions = self.get_condition(table, **kwargs)
+ if not filters:
+ return conditions
+ if isinstance(filters, list):
+ for f in filters:
+ if not isinstance(f, (list, tuple)):
+ _operator = OPERATOR_MAP[filters[1]]
+ if not isinstance(filters[0], str):
+ conditions = make_function(filters[0], filters[2])
+ break
+ conditions = conditions.where(_operator(Field(filters[0]), filters[2]))
+ break
+ else:
+ _operator = OPERATOR_MAP[f[1]]
+ conditions = conditions.where(_operator(Field(f[0]), f[2]))
+
+ conditions = self.add_conditions(conditions, **kwargs)
+ return conditions
+
+ def dict_query(self, table: str, filters: Dict[str, Union[str, int]] = None, **kwargs) -> frappe.qb:
+ """Build conditions using the given dictionary filters
+
+ Args:
+ table (str): DocType
+ filters (Dict[str, Union[str, int]], optional): Filters. Defaults to None.
+
+ Returns:
+ frappe.qb: conditions object
+ """
+ conditions = self.get_condition(table, **kwargs)
+ if not filters:
+ conditions = self.add_conditions(conditions, **kwargs)
+ return conditions
+
+ for key in filters:
+ value = filters.get(key)
+ _operator = OPERATOR_MAP["="]
+
+ if not isinstance(key, str):
+ conditions = conditions.where(make_function(key, value))
+ continue
+ if isinstance(value, (list, tuple)):
+ if isinstance(value[1], (list, tuple)) or value[0] in list(OPERATOR_MAP.keys())[-4:]:
+ _operator = OPERATOR_MAP[value[0]]
+ conditions = conditions.where(_operator(key, value[1]))
+ else:
+ _operator = OPERATOR_MAP[value[0]]
+ conditions = conditions.where(_operator(Field(key), value[1]))
+ else:
+ conditions = conditions.where(_operator(Field(key), value))
+ conditions = self.add_conditions(conditions, **kwargs)
+ return conditions
+
+ def build_conditions(
+ self,
+ table: str,
+ filters: Union[Dict[str, Union[str, int]], str, int] = None,
+ **kwargs
+ ) -> frappe.qb:
+ """Build conditions for sql query
+
+ Args:
+ filters (Union[Dict[str, Union[str, int]], str, int]): conditions in Dict
+ table (str): DocType
+
+ Returns:
+ frappe.qb: frappe.qb conditions object
+ """
+ if isinstance(filters, int) or isinstance(filters, str):
+ filters = {"name": str(filters)}
+
+ if isinstance(filters, Criterion):
+ criterion = self.criterion_query(table, filters, **kwargs)
+
+ elif isinstance(filters, (list, tuple)):
+ criterion = self.misc_query(table, filters, **kwargs)
+
+ else:
+ criterion = self.dict_query(filters=filters, table=table, **kwargs)
+
+ return criterion
+
+ def get_sql(
+ self,
+ table: str,
+ fields: Union[List, Tuple],
+ filters: Union[Dict[str, Union[str, int]], str, int] = None,
+ **kwargs
+ ):
+ criterion = self.build_conditions(table, filters, **kwargs)
+ if isinstance(fields, (list, tuple)):
+ query = criterion.select(*kwargs.get("field_objects", fields))
+
+ elif isinstance(fields, Criterion):
+ query = criterion.select(fields)
+
+ else:
+ query = criterion.select(fields)
+
+ return query
+
+
+class Permission:
+ @classmethod
+ def check_permissions(cls, query, **kwargs):
+ if not isinstance(query, str):
+ query = query.get_sql()
+
+ doctype = cls.get_tables_from_query(query)
+ if isinstance(doctype, str):
+ doctype = [doctype]
+
+ for dt in doctype:
+ dt = re.sub("tab", "", dt)
+ if not frappe.has_permission(
+ dt,
+ "select",
+ user=kwargs.get("user"),
+ parent_doctype=kwargs.get("parent_doctype"),
+ ) and not frappe.has_permission(
+ dt,
+ "read",
+ user=kwargs.get("user"),
+ parent_doctype=kwargs.get("parent_doctype"),
+ ):
+ frappe.throw(
+ _("Insufficient Permission for {0}").format(frappe.bold(dt))
+ )
+
+ @staticmethod
+ def get_tables_from_query(query: str):
+ return [table for table in re.findall(r"\w+", query) if table.startswith("tab")]
diff --git a/frappe/database/schema.py b/frappe/database/schema.py
index 31f11dbd5e..ce9fcb4147 100644
--- a/frappe/database/schema.py
+++ b/frappe/database/schema.py
@@ -303,6 +303,8 @@ def get_definition(fieldtype, precision=None, length=None):
size = d[1] if d[1] else None
if size:
+ # This check needs to exist for backward compatibility.
+ # Till V13, default size used for float, currency and percent are (18, 6).
if fieldtype in ["Float", "Currency", "Percent"] and cint(precision) > 6:
size = '21,9'
diff --git a/frappe/defaults.py b/frappe/defaults.py
index d4c338388d..eb98db449f 100644
--- a/frappe/defaults.py
+++ b/frappe/defaults.py
@@ -1,9 +1,10 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.desk.notifications import clear_notifications
from frappe.cache_manager import clear_defaults_cache, common_default_keys
+from frappe.query_builder import DocType
# Note: DefaultValue records are identified by parenttype
# __default, __global or 'User Permission'
@@ -116,14 +117,11 @@ def set_default(key, value, parent, parenttype="__default"):
:param value: Default value.
:param parent: Usually, **User** to whom the default belongs.
:param parenttype: [optional] default is `__default`."""
- if frappe.db.sql('''
- select
- defkey
- from
- `tabDefaultValue`
- where
- defkey=%s and parent=%s
- for update''', (key, parent)):
+ table = DocType("DefaultValue")
+ key_exists = frappe.qb.from_(table).where(
+ (table.defkey == key) & (table.parent == parent)
+ ).select(table.defkey).for_update().run()
+ if key_exists:
frappe.db.delete("DefaultValue", {
"defkey": key,
"parent": parent
@@ -191,8 +189,12 @@ def get_defaults_for(parent="__default"):
if defaults==None:
# sort descending because first default must get precedence
- res = frappe.db.sql("""select defkey, defvalue from `tabDefaultValue`
- where parent = %s order by creation""", (parent,), as_dict=1)
+ table = DocType("DefaultValue")
+ res = frappe.qb.from_(table).where(
+ table.parent == parent
+ ).select(
+ table.defkey, table.defvalue
+ ).orderby("creation").run(as_dict=True)
defaults = frappe._dict({})
for d in res:
diff --git a/frappe/deferred_insert.py b/frappe/deferred_insert.py
index 499fc5e41b..b1338a73b0 100644
--- a/frappe/deferred_insert.py
+++ b/frappe/deferred_insert.py
@@ -5,7 +5,6 @@ from frappe.utils import cstr
queue_prefix = 'insert_queue_for_'
-@frappe.whitelist()
def deferred_insert(doctype, records):
frappe.cache().rpush(queue_prefix + doctype, records)
diff --git a/frappe/desk/__init__.py b/frappe/desk/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/desk/__init__.py
+++ b/frappe/desk/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/desk/calendar.py b/frappe/desk/calendar.py
index 273b2654bf..66e6dd8434 100644
--- a/frappe/desk/calendar.py
+++ b/frappe/desk/calendar.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
diff --git a/frappe/desk/desk_page.py b/frappe/desk/desk_page.py
index d373dbda0e..a01008280c 100644
--- a/frappe/desk/desk_page.py
+++ b/frappe/desk/desk_page.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.translate import send_translations
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index ca53e6cba4..e1789852f1 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -1,11 +1,12 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
# Author - Shivam Mishra
import frappe
from json import loads, dumps
from frappe import _, DoesNotExistError, ValidationError, _dict
from frappe.boot import get_allowed_pages, get_allowed_reports
+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,
@@ -27,18 +28,18 @@ def handle_not_exist(fn):
class Workspace:
- def __init__(self, page_name, minimal=False):
- self.page_name = page_name
- self.extended_links = []
- self.extended_charts = []
- self.extended_shortcuts = []
+ 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.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)
@@ -47,16 +48,17 @@ 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'), str) else section.get('links')
@@ -74,8 +76,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()
@@ -101,39 +123,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())
@@ -147,21 +148,6 @@ class Workspace:
return doc
- def get_pages_to_extend(self):
- pages = frappe.get_all("Workspace", filters={
- "extends": self.page_name,
- 'restrict_to_domain': ['in', frappe.get_active_domains()],
- 'for_user': '',
- 'module': ['in', self.allowed_modules]
- })
-
- pages = [frappe.get_cached_doc("Workspace", page['name']) for page in pages]
-
- for page in pages:
- self.extended_links = self.extended_links + page.get_link_groups()
- self.extended_charts = self.extended_charts + page.charts
- self.extended_shortcuts = self.extended_shortcuts + page.shortcuts
-
def is_item_allowed(self, name, item_type):
if frappe.session.user == "Administrator":
return True
@@ -183,28 +169,20 @@ class Workspace:
def build_workspace(self):
self.cards = {
- 'label': _(self.doc.cards_label),
'items': self.get_links()
}
self.charts = {
- 'label': _(self.doc.charts_label),
'items': self.get_charts()
}
self.shortcuts = {
- 'label': _(self.doc.shortcuts_label),
'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)
@@ -250,9 +228,6 @@ class Workspace:
if not self.doc.hide_custom:
cards = cards + get_custom_reports_and_doctypes(self.doc.module)
- if len(self.extended_links):
- cards = merge_cards_based_on_label(cards + self.extended_links)
-
default_country = frappe.db.get_default("country")
new_data = []
@@ -290,8 +265,6 @@ class Workspace:
all_charts = []
if frappe.has_permission("Dashboard Chart", throw=False):
charts = self.doc.charts
- if len(self.extended_charts):
- charts = charts + self.extended_charts
for chart in charts:
if frappe.has_permission('Dashboard Chart', doc=chart.chart_name):
@@ -312,8 +285,6 @@ class Workspace:
items = []
shortcuts = self.doc.shortcuts
- if len(self.extended_shortcuts):
- shortcuts = shortcuts + self.extended_shortcuts
for item in shortcuts:
new_item = item.as_dict().copy()
@@ -333,9 +304,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":
@@ -352,59 +340,64 @@ 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,
- 'allow_customization': not wspace.doc.disable_user_customization
+ 'onboardings': wspace.onboardings
}
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, True)
+ 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")
@@ -439,7 +432,6 @@ def get_custom_doctype_list(module):
return out
-
def get_custom_report_list(module):
"""Returns list on new style reports for modules."""
reports = frappe.get_all("Report", fields=["name", "ref_doctype", "report_type"], filters=
@@ -460,73 +452,26 @@ def get_custom_report_list(module):
return out
-def get_custom_workspace_for_user(page):
- """Get custom page from workspace if exists or create one
+def save_new_widget(doc, page, blocks, new_widgets):
- Args:
- page (stirng): Page name
+ widgets = _dict(loads(new_widgets))
- Returns:
- Object: Document object
- """
- filters = {
- 'extends': page,
- 'for_user': frappe.session.user
- }
- pages = frappe.get_list("Workspace", filters=filters)
- if pages:
- return frappe.get_doc("Workspace", pages[0])
- doc = frappe.new_doc("Workspace")
- doc.extends = page
- doc.for_user = frappe.session.user
- return doc
+ 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)
-
-@frappe.whitelist()
-def save_customization(page, config):
- """Save customizations as a separate doctype in Workspace per user
-
- Args:
- page (string): Name of the page to be edited
- config (dict): Dictionary config of al widgets
-
- Returns:
- Boolean: Customization saving status
- """
- original_page = frappe.get_doc("Workspace", page)
- page_doc = get_custom_workspace_for_user(page)
-
- # Update field values
- page_doc.update({
- "icon": original_page.icon,
- "charts_label": original_page.charts_label,
- "cards_label": original_page.cards_label,
- "shortcuts_label": original_page.shortcuts_label,
- "module": original_page.module,
- "onboarding": original_page.onboarding,
- "developer_mode_only": original_page.developer_mode_only,
- "category": original_page.category
- })
-
- config = _dict(loads(config))
- if config.charts:
- page_doc.charts = prepare_widget(config.charts, "Workspace Chart", "charts")
- if config.shortcuts:
- page_doc.shortcuts = prepare_widget(config.shortcuts, "Workspace Shortcut", "shortcuts")
- if config.cards:
- page_doc.build_links_table_from_cards(config.cards)
-
- # Set label
- page_doc.label = page + '-' + frappe.session.user
+ # remove duplicate and unwanted widgets
+ if widgets:
+ clean_up(doc, blocks)
try:
- if page_doc.is_new():
- page_doc.insert(ignore_permissions=True)
- else:
- page_doc.save(ignore_permissions=True)
+ doc.save(ignore_permissions=True)
except (ValidationError, TypeError) as e:
# Create a json string to log
- json_config = dumps(config, sort_keys=True, indent=4)
+ json_config = dumps(widgets, sort_keys=True, indent=4)
# Error log body
log = \
@@ -540,6 +485,48 @@ def save_customization(page, config):
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
@@ -575,40 +562,14 @@ def prepare_widget(config, doctype, parentfield):
prepare_widget_list.append(doc)
return prepare_widget_list
-
@frappe.whitelist()
def update_onboarding_step(name, field, value):
"""Update status of onboaridng step
Args:
- name (string): Name of the doc
- field (string): field to be updated
- value: Value to be updated
+ name (string): Name of the doc
+ field (string): field to be updated
+ value: Value to be updated
"""
frappe.db.set_value("Onboarding Step", name, field, value)
-
-@frappe.whitelist()
-def reset_customization(page):
- """Reset workspace customizations for a user
-
- Args:
- page (string): Name of the page to be reset
- """
- page_doc = get_custom_workspace_for_user(page)
- page_doc.delete()
-
-def merge_cards_based_on_label(cards):
- """Merge cards with common label."""
- cards_dict = {}
- for card in cards:
- label = card.get('label')
- if label in cards_dict:
- links = cards_dict[label].links + card.links
- cards_dict[label].update(dict(links=links))
- cards_dict[label] = cards_dict.pop(label)
- else:
- cards_dict[label] = card
-
- return list(cards_dict.values())
-
diff --git a/frappe/desk/doctype/bulk_update/bulk_update.py b/frappe/desk/doctype/bulk_update/bulk_update.py
index 469ee839f1..b512ca175c 100644
--- a/frappe/desk/doctype/bulk_update/bulk_update.py
+++ b/frappe/desk/doctype/bulk_update/bulk_update.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/calendar_view/calendar_view.py b/frappe/desk/doctype/calendar_view/calendar_view.py
index 3a986f3273..11612f5587 100644
--- a/frappe/desk/doctype/calendar_view/calendar_view.py
+++ b/frappe/desk/doctype/calendar_view/calendar_view.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/console_log/console_log.py b/frappe/desk/doctype/console_log/console_log.py
index 5d0f1cfa93..e0b552ebfd 100644
--- a/frappe/desk/doctype/console_log/console_log.py
+++ b/frappe/desk/doctype/console_log/console_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# 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 3bb1605204..c41b9d68c8 100644
--- a/frappe/desk/doctype/console_log/test_console_log.py
+++ b/frappe/desk/doctype/console_log/test_console_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py
index 1d333609db..0dfd458a37 100644
--- a/frappe/desk/doctype/dashboard/dashboard.py
+++ b/frappe/desk/doctype/dashboard/dashboard.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
diff --git a/frappe/desk/doctype/dashboard/test_dashboard.py b/frappe/desk/doctype/dashboard/test_dashboard.py
index dd1bc31d86..15c132c027 100644
--- a/frappe/desk/doctype/dashboard/test_dashboard.py
+++ b/frappe/desk/doctype/dashboard/test_dashboard.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
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..e0d2cab8ef 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
@@ -45,6 +45,7 @@ frappe.ui.form.on('Dashboard Chart', {
frm.set_df_property("filters_section", "hidden", 1);
frm.set_df_property("dynamic_filters_section", "hidden", 1);
+ frm.trigger('set_parent_document_type');
frm.trigger('set_time_series');
frm.set_query('document_type', function() {
return {
@@ -110,9 +111,11 @@ frappe.ui.form.on('Dashboard Chart', {
frm.set_value('source', '');
frm.set_value('based_on', '');
frm.set_value('value_based_on', '');
+ frm.set_value('parent_document_type', '');
frm.set_value('filters_json', '[]');
frm.set_value('dynamic_filters_json', '[]');
frm.trigger('update_options');
+ frm.trigger('set_parent_document_type');
},
report_name: function(frm) {
@@ -125,7 +128,6 @@ frappe.ui.form.on('Dashboard Chart', {
frm.trigger('set_chart_report_filters');
},
-
set_chart_report_filters: function(frm) {
let report_name = frm.doc.report_name;
@@ -148,6 +150,10 @@ frappe.ui.form.on('Dashboard Chart', {
}
},
+ use_report_chart: function(frm) {
+ !frm.doc.use_report_chart && frm.trigger('set_chart_field_options');
+ },
+
set_chart_field_options: function(frm) {
let filters = frm.doc.filters_json.length > 2 ? JSON.parse(frm.doc.filters_json) : null;
if (frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2) {
@@ -179,6 +185,9 @@ frappe.ui.form.on('Dashboard Chart', {
} else {
frappe.msgprint(__('Report has no data, please modify the filters or change the Report Name'));
}
+ } else {
+ frm.set_value('use_report_chart', 1);
+ frm.set_df_property('use_report_chart', 'hidden', false);
}
});
},
@@ -223,7 +232,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});
}
@@ -365,6 +374,7 @@ frappe.ui.form.on('Dashboard Chart', {
frm.filter_group = new frappe.ui.FilterGroup({
parent: dialog.get_field('filter_area').$wrapper,
doctype: frm.doc.document_type,
+ parent_doctype: frm.doc.parent_document_type,
on_change: () => {},
});
@@ -481,6 +491,36 @@ frappe.ui.form.on('Dashboard Chart', {
frm.dynamic_filter_table.find('tbody').html(filter_rows);
}
+ },
+
+ set_parent_document_type: async function(frm) {
+ let document_type = frm.doc.document_type;
+ let doc_is_table = document_type &&
+ (await frappe.db.get_value('DocType', document_type, 'istable')).message.istable;
+
+ frm.set_df_property('parent_document_type', 'hidden', !doc_is_table);
+
+ if (document_type && doc_is_table) {
+ let parent = await frappe.db.get_list('DocField', {
+ filters: {
+ 'fieldtype': 'Table',
+ 'options': document_type
+ },
+ fields: ['parent']
+ });
+
+ parent && frm.set_query('parent_document_type', function() {
+ return {
+ filters: {
+ "name": ['in', parent.map(({ parent }) => parent)]
+ }
+ };
+ });
+
+ if (parent.length === 1) {
+ frm.set_value('parent_document_type', parent[0].parent);
+ }
+ }
}
});
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
index d4bba53068..a5d30c10e5 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
@@ -17,6 +17,7 @@
"y_axis",
"source",
"document_type",
+ "parent_document_type",
"based_on",
"value_based_on",
"group_by_type",
@@ -268,10 +269,18 @@
"fieldname": "use_report_chart",
"fieldtype": "Check",
"label": "Use Report Chart"
+ },
+ {
+ "depends_on": "eval: doc.chart_type !== 'Custom' && doc.chart_type !== 'Report'",
+ "description": "The document type selected is a child table, so the parent document type is required.",
+ "fieldname": "parent_document_type",
+ "fieldtype": "Link",
+ "label": "Parent Document Type",
+ "options": "DocType"
}
],
"links": [],
- "modified": "2020-07-23 11:10:33.509497",
+ "modified": "2021-11-09 17:18:11.456145",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
index db5964e7b2..cb77ef7a1a 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
@@ -333,7 +333,10 @@ class DashboardChart(Document):
def check_required_field(self):
if not self.document_type:
- frappe.throw(_("Document type is required to create a dashboard chart"))
+ frappe.throw(_("Document type is required to create a dashboard chart"))
+
+ if self.document_type and frappe.get_meta(self.document_type).istable and not self.parent_document_type:
+ frappe.throw(_("Parent document type is required to create a dashboard chart"))
if self.chart_type == 'Group By':
if not self.group_by_based_on:
diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
index 9f10522b12..5562f2fc92 100644
--- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import unittest, frappe
from frappe.utils import getdate, formatdate, get_last_day
from frappe.utils.dateutils import get_period_ending, get_period
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 7d6f66daa2..8b2fba2e58 100644
--- a/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.py
+++ b/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# 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 359801a303..87d095d5d1 100644
--- a/frappe/desk/doctype/dashboard_chart_link/dashboard_chart_link.py
+++ b/frappe/desk/doctype/dashboard_chart_link/dashboard_chart_link.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# 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 791dbc563b..71ded32837 100644
--- a/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.py
+++ b/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe, os
from frappe import _
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 53fe127dfb..6d6773d52e 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,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
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 df61c52114..2f29b3e989 100644
--- a/frappe/desk/doctype/dashboard_settings/dashboard_settings.py
+++ b/frappe/desk/doctype/dashboard_settings/dashboard_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.py b/frappe/desk/doctype/desktop_icon/desktop_icon.py
index 28c5a670cb..194b0d0ca4 100644
--- a/frappe/desk/doctype/desktop_icon/desktop_icon.py
+++ b/frappe/desk/doctype/desktop_icon/desktop_icon.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
diff --git a/frappe/desk/doctype/event/__init__.py b/frappe/desk/doctype/event/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/desk/doctype/event/__init__.py
+++ b/frappe/desk/doctype/event/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/desk/doctype/event/event.json b/frappe/desk/doctype/event/event.json
index 5768f00f32..2f67c36fc0 100644
--- a/frappe/desk/doctype/event/event.json
+++ b/frappe/desk/doctype/event/event.json
@@ -53,7 +53,7 @@
},
{
"fieldname": "subject",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"in_global_search": 1,
"in_list_view": 1,
"label": "Subject",
@@ -277,10 +277,11 @@
"icon": "fa fa-calendar",
"idx": 1,
"links": [],
- "modified": "2020-01-14 21:47:15.825287",
+ "modified": "2021-11-18 05:06:24.881742",
"modified_by": "Administrator",
"module": "Desk",
"name": "Event",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py
index e7e7be530b..86f0656bc6 100644
--- a/frappe/desk/doctype/event/event.py
+++ b/frappe/desk/doctype/event/event.py
@@ -1,5 +1,5 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
@@ -11,6 +11,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils.user import get_enabled_system_users
from frappe.desk.reportview import get_filters_cond
+from frappe.desk.doctype.notification_settings.notification_settings import is_email_notifications_enabled_for_type
weekdays = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"]
communication_mapping = {"": "Event", "Event": "Event", "Meeting": "Meeting", "Call": "Phone", "Sent/Received Email": "Email", "Other": "Other"}
@@ -141,7 +142,12 @@ def has_permission(doc, user):
def send_event_digest():
today = nowdate()
- for user in get_enabled_system_users():
+
+ # select only those users that have event reminder email notifications enabled
+ users = [user for user in get_enabled_system_users() if
+ is_email_notifications_enabled_for_type(user.name, 'Event Reminders')]
+
+ for user in users:
events = get_events(today, today, user.name, for_reminder=True)
if events:
frappe.set_user_lang(user.name, user.language)
diff --git a/frappe/desk/doctype/event/test_event.py b/frappe/desk/doctype/event/test_event.py
index 77211946a9..6b7f6ee471 100644
--- a/frappe/desk/doctype/event/test_event.py
+++ b/frappe/desk/doctype/event/test_event.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
"""Use blog post test to test user permissions logic"""
import frappe
@@ -14,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 ca4fae9930..b834ba3a82 100644
--- a/frappe/desk/doctype/event_participants/event_participants.py
+++ b/frappe/desk/doctype/event_participants/event_participants.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from frappe.model.document import Document
class EventParticipants(Document):
diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js
index 8d70dcd3dc..6a7c736fac 100644
--- a/frappe/desk/doctype/form_tour/form_tour.js
+++ b/frappe/desk/doctype/form_tour/form_tour.js
@@ -15,10 +15,13 @@ frappe.ui.form.on('Form Tour', {
frm.add_custom_button(__('Show Tour'), async () => {
const issingle = await check_if_single(frm.doc.reference_doctype);
+ const name = await get_first_document(frm.doc.reference_doctype);
let route_changed = null;
if (issingle) {
route_changed = frappe.set_route('Form', frm.doc.reference_doctype);
+ } else if (frm.doc.first_document) {
+ route_changed = frappe.set_route('Form', frm.doc.reference_doctype, name);
} else {
route_changed = frappe.set_route('Form', frm.doc.reference_doctype, 'new');
}
@@ -120,4 +123,15 @@ function get_child_field(child_table, child_name, 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
+}
+
+async function get_first_document(doctype) {
+ let docname;
+
+ await frappe.db.get_list(doctype, { order_by: "creation" }).then(res => {
+ if (Array.isArray(res) && res.length)
+ docname = res[0].name;
+ });
+
+ return docname || 'new';
+}
diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json
index e4ea528fcc..6f3bd56a4e 100644
--- a/frappe/desk/doctype/form_tour/form_tour.json
+++ b/frappe/desk/doctype/form_tour/form_tour.json
@@ -9,8 +9,11 @@
"title",
"reference_doctype",
"module",
+ "column_break_6",
"is_standard",
"save_on_complete",
+ "first_document",
+ "include_name_field",
"section_break_3",
"steps"
],
@@ -62,14 +65,32 @@
"label": "Module",
"options": "Module Def",
"read_only": 1
+ },
+ {
+ "fieldname": "column_break_6",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "first_document",
+ "fieldtype": "Check",
+ "label": "Show First Document Tour"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!doc.first_document",
+ "fieldname": "include_name_field",
+ "fieldtype": "Check",
+ "label": "Include Name Field"
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-06-06 20:32:54.068774",
+ "modified": "2021-11-24 12:03:45.449311",
"modified_by": "Administrator",
"module": "Desk",
"name": "Form Tour",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py
index dbc667ce28..82d47224dd 100644
--- a/frappe/desk/doctype/form_tour/form_tour.py
+++ b/frappe/desk/doctype/form_tour/form_tour.py
@@ -1,5 +1,5 @@
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
@@ -25,7 +25,7 @@ class FormTour(Document):
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 = ""
diff --git a/frappe/desk/doctype/form_tour/test_form_tour.py b/frappe/desk/doctype/form_tour/test_form_tour.py
index a4a796ce41..3670cbc218 100644
--- a/frappe/desk/doctype/form_tour/test_form_tour.py
+++ b/frappe/desk/doctype/form_tour/test_form_tour.py
@@ -1,5 +1,5 @@
# Copyright (c) 2021, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/desk/doctype/form_tour_step/form_tour_step.py b/frappe/desk/doctype/form_tour_step/form_tour_step.py
index 0df5665c63..bbc8edea08 100644
--- a/frappe/desk/doctype/form_tour_step/form_tour_step.py
+++ b/frappe/desk/doctype/form_tour_step/form_tour_step.py
@@ -1,5 +1,5 @@
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
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 de8a48af01..30a31f959f 100644
--- a/frappe/desk/doctype/global_search_doctype/global_search_doctype.py
+++ b/frappe/desk/doctype/global_search_doctype/global_search_doctype.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# 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 9112349c1b..e9a47cecd1 100644
--- a/frappe/desk/doctype/global_search_settings/global_search_settings.py
+++ b/frappe/desk/doctype/global_search_settings/global_search_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
@@ -33,7 +33,7 @@ class GlobalSearchSettings(Document):
def get_doctypes_for_global_search():
def get_from_db():
- doctypes = frappe.get_list("Global Search DocType", fields=["document_type"], order_by="idx ASC")
+ doctypes = frappe.get_all("Global Search DocType", fields=["document_type"], order_by="idx ASC")
return [d.document_type for d in doctypes] or []
return frappe.cache().hget("global_search", "search_priorities", get_from_db)
diff --git a/frappe/desk/doctype/kanban_board/kanban_board.py b/frappe/desk/doctype/kanban_board/kanban_board.py
index 5100727f43..155a925fcf 100644
--- a/frappe/desk/doctype/kanban_board/kanban_board.py
+++ b/frappe/desk/doctype/kanban_board/kanban_board.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
import json
diff --git a/frappe/desk/doctype/kanban_board/test_kanban_board.py b/frappe/desk/doctype/kanban_board/test_kanban_board.py
index f9503d736a..f00446141a 100644
--- a/frappe/desk/doctype/kanban_board/test_kanban_board.py
+++ b/frappe/desk/doctype/kanban_board/test_kanban_board.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/desk/doctype/kanban_board_column/kanban_board_column.json b/frappe/desk/doctype/kanban_board_column/kanban_board_column.json
index 95d9294e9a..c0acde5da5 100644
--- a/frappe/desk/doctype/kanban_board_column/kanban_board_column.json
+++ b/frappe/desk/doctype/kanban_board_column/kanban_board_column.json
@@ -1,155 +1,55 @@
{
- "allow_copy": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2016-10-19 12:26:42.569185",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2016-10-19 12:26:42.569185",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "column_name",
+ "status",
+ "indicator",
+ "order"
+ ],
"fields": [
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_name",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Column Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "column_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Column Name"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "Active",
- "fieldname": "status",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Status",
- "length": 0,
- "no_copy": 0,
- "options": "Active\nArchived",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "Active",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Status",
+ "options": "Active\nArchived"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "darkgrey",
- "fieldname": "indicator",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Indicator",
- "length": 0,
- "no_copy": 0,
- "options": "blue\norange\nred\ngreen\ndarkgrey\npurple\nyellow\nlightblue",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "Gray",
+ "fieldname": "indicator",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Indicator",
+ "options": "Blue\nCyan\nGray\nGreen\nLight Blue\nOrange\nPink\nPurple\nRed\nRed\nYellow"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "order",
- "fieldtype": "Code",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Order",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "order",
+ "fieldtype": "Code",
+ "label": "Order"
}
- ],
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
-
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2017-01-17 15:23:43.520379",
- "modified_by": "Administrator",
- "module": "Desk",
- "name": "Kanban Board Column",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2021-12-14 13:13:38.804259",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Kanban Board Column",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
}
\ No newline at end of file
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 aebba3351c..d919fd6aed 100644
--- a/frappe/desk/doctype/kanban_board_column/kanban_board_column.py
+++ b/frappe/desk/doctype/kanban_board_column/kanban_board_column.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
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 2467ae40a4..d2b01d301e 100644
--- a/frappe/desk/doctype/list_filter/list_filter.py
+++ b/frappe/desk/doctype/list_filter/list_filter.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
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 f4a288b7ba..78b56fe7d5 100644
--- a/frappe/desk/doctype/list_view_settings/list_view_settings.py
+++ b/frappe/desk/doctype/list_view_settings/list_view_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
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 00010d7604..85872dd36e 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,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# 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 6f01e0fd8d..aa268c792c 100644
--- a/frappe/desk/doctype/module_onboarding/module_onboarding.py
+++ b/frappe/desk/doctype/module_onboarding/module_onboarding.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/module_onboarding/test_module_onboarding.py b/frappe/desk/doctype/module_onboarding/test_module_onboarding.py
index 39184401a1..42f472abc1 100644
--- a/frappe/desk/doctype/module_onboarding/test_module_onboarding.py
+++ b/frappe/desk/doctype/module_onboarding/test_module_onboarding.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/desk/doctype/note/note.css b/frappe/desk/doctype/note/note.css
deleted file mode 100644
index b5026d2e46..0000000000
--- a/frappe/desk/doctype/note/note.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.like-disabled-input{
- background-color: #fff;
-}
\ No newline at end of file
diff --git a/frappe/desk/doctype/note/note.json b/frappe/desk/doctype/note/note.json
index 8d476e83fe..69a9518ac4 100644
--- a/frappe/desk/doctype/note/note.json
+++ b/frappe/desk/doctype/note/note.json
@@ -1,322 +1,106 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 1,
- "beta": 0,
- "creation": "2013-05-24 13:41:00",
- "custom": 0,
- "description": "",
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Document",
- "editable_grid": 0,
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "title",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 1,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Title",
- "length": 0,
- "no_copy": 1,
- "permlevel": 0,
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "description": "",
- "fieldname": "public",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Public",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "public",
- "fieldname": "notify_on_login",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Notify users with a popup when they log in",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "default": "0",
- "depends_on": "notify_on_login",
- "description": "If enabled, users will be notified every time they login. If not enabled, users will only be notified once.",
- "fieldname": "notify_on_every_login",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Notify Users On Every Login",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.notify_on_login && doc.public",
- "fieldname": "expire_notification_on",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Expire Notification On",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 1,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "description": "Help: To link to another record in the system, use \"#Form/Note/[Note Name]\" as the Link URL. (don't use \"http://\")",
- "fieldname": "content",
- "fieldtype": "Text Editor",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 1,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Content",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 1,
- "columns": 0,
- "fieldname": "seen_by_section",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Seen By",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "seen_by",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Seen By Table",
- "length": 0,
- "no_copy": 0,
- "options": "Note Seen By",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-file-text",
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-09-21 15:15:44.909636",
- "modified_by": "Administrator",
- "module": "Desk",
- "name": "Note",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 0,
- "role": "All",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 1,
- "show_name_in_global_search": 0,
- "sort_order": "ASC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
- }
\ No newline at end of file
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2013-05-24 13:41:00",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "engine": "InnoDB",
+ "field_order": [
+ "title",
+ "public",
+ "notify_on_login",
+ "notify_on_every_login",
+ "expire_notification_on",
+ "content",
+ "seen_by_section",
+ "seen_by"
+ ],
+ "fields": [
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "in_global_search": 1,
+ "in_list_view": 1,
+ "label": "Title",
+ "no_copy": 1,
+ "print_hide": 1,
+ "reqd": 1
+ },
+ {
+ "bold": 1,
+ "default": "0",
+ "fieldname": "public",
+ "fieldtype": "Check",
+ "label": "Public",
+ "print_hide": 1
+ },
+ {
+ "bold": 1,
+ "default": "0",
+ "depends_on": "public",
+ "fieldname": "notify_on_login",
+ "fieldtype": "Check",
+ "label": "Notify users with a popup when they log in"
+ },
+ {
+ "bold": 1,
+ "default": "0",
+ "depends_on": "notify_on_login",
+ "description": "If enabled, users will be notified every time they login. If not enabled, users will only be notified once.",
+ "fieldname": "notify_on_every_login",
+ "fieldtype": "Check",
+ "label": "Notify Users On Every Login"
+ },
+ {
+ "depends_on": "eval:doc.notify_on_login && doc.public",
+ "fieldname": "expire_notification_on",
+ "fieldtype": "Date",
+ "label": "Expire Notification On",
+ "search_index": 1
+ },
+ {
+ "bold": 1,
+ "description": "Help: To link to another record in the system, use \"/app/note/[Note Name]\" as the Link URL. (don't use \"http://\")",
+ "fieldname": "content",
+ "fieldtype": "Text Editor",
+ "in_global_search": 1,
+ "label": "Content"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "seen_by_section",
+ "fieldtype": "Section Break",
+ "label": "Seen By"
+ },
+ {
+ "fieldname": "seen_by",
+ "fieldtype": "Table",
+ "label": "Seen By Table",
+ "options": "Note Seen By"
+ }
+ ],
+ "icon": "fa fa-file-text",
+ "idx": 1,
+ "links": [],
+ "modified": "2021-09-18 10:57:51.352643",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Note",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "All",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "ASC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/note/note.py b/frappe/desk/doctype/note/note.py
index 790f9a514c..ae7af07cd9 100644
--- a/frappe/desk/doctype/note/note.py
+++ b/frappe/desk/doctype/note/note.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: See license.txt
+# License: MIT. See LICENSE
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 1bb1730357..ac2116c38a 100644
--- a/frappe/desk/doctype/note/test_note.py
+++ b/frappe/desk/doctype/note/test_note.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
@@ -8,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 cec4628b20..01bee05a9f 100644
--- a/frappe/desk/doctype/note_seen_by/note_seen_by.py
+++ b/frappe/desk/doctype/note_seen_by/note_seen_by.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/notification_log/notification_log.json b/frappe/desk/doctype/notification_log/notification_log.json
index 9e802298e3..e188708277 100644
--- a/frappe/desk/doctype/notification_log/notification_log.json
+++ b/frappe/desk/doctype/notification_log/notification_log.json
@@ -120,7 +120,7 @@
"hide_toolbar": 1,
"in_create": 1,
"links": [],
- "modified": "2020-09-18 17:26:09.703215",
+ "modified": "2021-10-25 17:26:09.703215",
"modified_by": "Administrator",
"module": "Desk",
"name": "Notification Log",
@@ -139,6 +139,5 @@
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "subject",
- "track_changes": 1,
"track_seen": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py
index d7d7f68b74..12e628ada2 100644
--- a/frappe/desk/doctype/notification_log/notification_log.py
+++ b/frappe/desk/doctype/notification_log/notification_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
diff --git a/frappe/desk/doctype/notification_log/test_notification_log.py b/frappe/desk/doctype/notification_log/test_notification_log.py
index af4dee8df3..4c415a860c 100644
--- a/frappe/desk/doctype/notification_log/test_notification_log.py
+++ b/frappe/desk/doctype/notification_log/test_notification_log.py
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
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
@@ -54,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.json b/frappe/desk/doctype/notification_settings/notification_settings.json
index fc12022e89..1a6efd5a0d 100644
--- a/frappe/desk/doctype/notification_settings/notification_settings.json
+++ b/frappe/desk/doctype/notification_settings/notification_settings.json
@@ -14,8 +14,11 @@
"enable_email_assignment",
"enable_email_energy_point",
"enable_email_share",
+ "enable_email_event_reminders",
"user",
- "seen"
+ "seen",
+ "system_notifications_section",
+ "energy_points_system_notifications"
],
"fields": [
{
@@ -84,15 +87,34 @@
"fieldtype": "Check",
"hidden": 1,
"label": "Seen"
+ },
+ {
+ "fieldname": "system_notifications_section",
+ "fieldtype": "Section Break",
+ "label": "System Notifications"
+ },
+ {
+ "default": "1",
+ "fieldname": "energy_points_system_notifications",
+ "fieldtype": "Check",
+ "label": "Energy Points"
+ },
+ {
+ "default": "1",
+ "depends_on": "enable_email_notifications",
+ "fieldname": "enable_email_event_reminders",
+ "fieldtype": "Check",
+ "label": "Event Reminders"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-11-04 12:54:57.989317",
+ "modified": "2021-11-24 14:45:31.931154",
"modified_by": "Administrator",
"module": "Desk",
"name": "Notification Settings",
+ "naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py
index eb3a16435f..cf6bb2d78d 100644
--- a/frappe/desk/doctype/notification_settings/notification_settings.py
+++ b/frappe/desk/doctype/notification_settings/notification_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/notification_settings/test_notification_settings.py b/frappe/desk/doctype/notification_settings/test_notification_settings.py
new file mode 100644
index 0000000000..e3dac0af5f
--- /dev/null
+++ b/frappe/desk/doctype/notification_settings/test_notification_settings.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+class TestNotificationSettings(unittest.TestCase):
+ pass
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 6931e77754..1fdba22779 100644
--- a/frappe/desk/doctype/notification_subscribed_document/notification_subscribed_document.py
+++ b/frappe/desk/doctype/notification_subscribed_document/notification_subscribed_document.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# 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 d8d5fe0953..5662523a9d 100644
--- a/frappe/desk/doctype/number_card/number_card.py
+++ b/frappe/desk/doctype/number_card/number_card.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/number_card/test_number_card.py b/frappe/desk/doctype/number_card/test_number_card.py
index c395f5f915..cc92e63341 100644
--- a/frappe/desk/doctype/number_card/test_number_card.py
+++ b/frappe/desk/doctype/number_card/test_number_card.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# 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 6c16f45f4b..0b55ae6dcd 100644
--- a/frappe/desk/doctype/number_card_link/number_card_link.py
+++ b/frappe/desk/doctype/number_card_link/number_card_link.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# 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 40d3dc33b1..a0e87c3067 100644
--- a/frappe/desk/doctype/onboarding_permission/onboarding_permission.py
+++ b/frappe/desk/doctype/onboarding_permission/onboarding_permission.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# 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 80b166de0a..c13fb29678 100644
--- a/frappe/desk/doctype/onboarding_permission/test_onboarding_permission.py
+++ b/frappe/desk/doctype/onboarding_permission/test_onboarding_permission.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.js b/frappe/desk/doctype/onboarding_step/onboarding_step.js
index 793e044d98..3c9bbab9ac 100644
--- a/frappe/desk/doctype/onboarding_step/onboarding_step.js
+++ b/frappe/desk/doctype/onboarding_step/onboarding_step.js
@@ -2,6 +2,17 @@
// For license information, please see license.txt
frappe.ui.form.on("Onboarding Step", {
+
+ setup: function(frm) {
+ frm.set_query("form_tour", function() {
+ return {
+ filters: {
+ reference_doctype: frm.doc.reference_document
+ }
+ };
+ });
+ },
+
refresh: function(frm) {
frappe.boot.developer_mode &&
frm.set_intro(
diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.json b/frappe/desk/doctype/onboarding_step/onboarding_step.json
index f71e821f65..b5d7851eca 100644
--- a/frappe/desk/doctype/onboarding_step/onboarding_step.json
+++ b/frappe/desk/doctype/onboarding_step/onboarding_step.json
@@ -20,6 +20,7 @@
"reference_document",
"show_full_form",
"show_form_tour",
+ "form_tour",
"is_single",
"reference_report",
"report_reference_doctype",
@@ -206,13 +207,21 @@
"fieldname": "show_form_tour",
"fieldtype": "Check",
"label": "Show Form Tour"
+ },
+ {
+ "depends_on": "show_form_tour",
+ "fieldname": "form_tour",
+ "fieldtype": "Link",
+ "label": "Form Tour",
+ "options": "Form Tour"
}
],
"links": [],
- "modified": "2020-10-30 14:54:06.646513",
+ "modified": "2021-12-02 10:56:04.448580",
"modified_by": "Administrator",
"module": "Desk",
"name": "Onboarding Step",
+ "naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.py b/frappe/desk/doctype/onboarding_step/onboarding_step.py
index 10bd8926ce..45e0ca34fd 100644
--- a/frappe/desk/doctype/onboarding_step/onboarding_step.py
+++ b/frappe/desk/doctype/onboarding_step/onboarding_step.py
@@ -1,11 +1,27 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
-# 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 2425577478..b0651da4da 100644
--- a/frappe/desk/doctype/onboarding_step/test_onboarding_step.py
+++ b/frappe/desk/doctype/onboarding_step/test_onboarding_step.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# 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 c79244c4ad..7c20e220db 100644
--- a/frappe/desk/doctype/onboarding_step_map/onboarding_step_map.py
+++ b/frappe/desk/doctype/onboarding_step_map/onboarding_step_map.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/route_history/route_history.json b/frappe/desk/doctype/route_history/route_history.json
index 7390aa011b..09db2320ca 100644
--- a/frappe/desk/doctype/route_history/route_history.json
+++ b/frappe/desk/doctype/route_history/route_history.json
@@ -88,7 +88,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "modified": "2018-10-05 13:26:03.106050",
+ "modified": "2021-10-25 13:26:03.106050",
"modified_by": "Administrator",
"module": "Desk",
"name": "Route History",
@@ -121,7 +121,6 @@
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
- "track_changes": 1,
"track_seen": 0,
"track_views": 0
-}
\ No newline at end of file
+}
diff --git a/frappe/desk/doctype/route_history/route_history.py b/frappe/desk/doctype/route_history/route_history.py
index 95872440c7..a49d5d5418 100644
--- a/frappe/desk/doctype/route_history/route_history.py
+++ b/frappe/desk/doctype/route_history/route_history.py
@@ -1,9 +1,13 @@
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
+
+import json
import frappe
+from frappe.deferred_insert import deferred_insert as _deferred_insert
from frappe.model.document import Document
+
class RouteHistory(Document):
pass
@@ -35,3 +39,16 @@ def flush_old_route_records():
"modified": ("<=", last_record_to_keep[0].modified),
"user": user
})
+
+@frappe.whitelist()
+def deferred_insert(routes):
+ routes = [
+ {
+ "user": frappe.session.user,
+ "route": route.get("route"),
+ "creation": route.get("creation"),
+ }
+ for route in frappe.parse_json(routes)
+ ]
+
+ _deferred_insert("Route History", json.dumps(routes))
diff --git a/frappe/desk/doctype/system_console/system_console.js b/frappe/desk/doctype/system_console/system_console.js
index c7eac39490..0fe3932671 100644
--- a/frappe/desk/doctype/system_console/system_console.js
+++ b/frappe/desk/doctype/system_console/system_console.js
@@ -5,17 +5,100 @@ 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,
});
+ frm.set_value("type", "Python");
},
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(() => frm.trigger("render_sql_output"))
+ .finally(() => $btn.text(__("Execute")));
+ });
+ },
+
+ type: function(frm) {
+ if (frm.doc.type == "Python") {
+ frm.set_value("output", "");
+ if (frm.sql_output) {
+ frm.sql_output.destroy();
+ frm.get_field("sql_output").html("");
+ }
+ }
+ },
+
+ render_sql_output: function(frm) {
+ if (frm.doc.type !== "SQL") return;
+ if (frm.sql_output) {
+ frm.sql_output.destroy();
+ frm.get_field("sql_output").html("");
+ }
+
+ if (frm.doc.output.startsWith("Traceback")) {
+ return;
+ }
+
+ let result = JSON.parse(frm.doc.output);
+ frm.set_value("output", `${result.length} ${result.length == 1 ? 'row' : 'rows'}`);
+
+ if (result.length) {
+ let columns = Object.keys(result[0]);
+ frm.sql_output = new DataTable(
+ frm.get_field("sql_output").$wrapper.get(0),
+ {
+ columns,
+ data: result
+ }
+ );
+ }
+ },
+
+ show_processlist: function(frm) {
+ if (frm.doc.show_processlist) {
+ // keep refreshing every 5 seconds
+ frm.events.refresh_processlist(frm);
+ frm.processlist_interval = setInterval(() => frm.events.refresh_processlist(frm), 5000);
+ } else {
+ if (frm.processlist_interval) {
+
+ // end it
+ clearInterval(frm.processlist_interval);
+ frm.get_field("processlist").html('');
+ }
+ }
+ },
+
+ refresh_processlist: function(frm) {
+ let timestamp = new Date();
+ frappe.call('frappe.desk.doctype.system_console.system_console.show_processlist').then(r => {
+ let rows = '';
+ for (let row of r.message) {
+ rows += `
+ | ${row.Id} |
+ ${row.Time} |
+ ${row.State} |
+ ${row.Info} |
+ ${row.Progress} |
+
`
+ }
+ frm.get_field('processlist').html(`
+ Requested on: ${timestamp}
+
+
+ | Id
+ | Time
+ | State
+ | Info
+ | Progress
+ |
+ ${rows}`);
});
}
});
diff --git a/frappe/desk/doctype/system_console/system_console.json b/frappe/desk/doctype/system_console/system_console.json
index 14e36e6fd3..657e9df89d 100644
--- a/frappe/desk/doctype/system_console/system_console.json
+++ b/frappe/desk/doctype/system_console/system_console.json
@@ -17,9 +17,15 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
+ "execute_section",
+ "type",
"console",
"commit",
- "output"
+ "output",
+ "sql_output",
+ "database_processes_section",
+ "show_processlist",
+ "processlist"
],
"fields": [
{
@@ -40,13 +46,47 @@
"fieldname": "commit",
"fieldtype": "Check",
"label": "Commit"
+ },
+ {
+ "fieldname": "execute_section",
+ "fieldtype": "Section Break",
+ "label": "Execute"
+ },
+ {
+ "fieldname": "database_processes_section",
+ "fieldtype": "Section Break",
+ "label": "Database Processes"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_processlist",
+ "fieldtype": "Check",
+ "label": "Show Processlist"
+ },
+ {
+ "fieldname": "processlist",
+ "fieldtype": "HTML",
+ "label": "processlist"
+ },
+ {
+ "default": "Python",
+ "fieldname": "type",
+ "fieldtype": "Select",
+ "label": "Type",
+ "options": "Python\nSQL"
+ },
+ {
+ "depends_on": "eval:doc.type == 'SQL'",
+ "fieldname": "sql_output",
+ "fieldtype": "HTML",
+ "label": "SQL Output"
}
],
"hide_toolbar": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2020-08-21 14:44:35.296877",
+ "modified": "2021-09-15 17:17:44.844767",
"modified_by": "Administrator",
"module": "Desk",
"name": "System Console",
@@ -65,4 +105,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/system_console/system_console.py b/frappe/desk/doctype/system_console/system_console.py
index e2b5656bc0..107ab2f932 100644
--- a/frappe/desk/doctype/system_console/system_console.py
+++ b/frappe/desk/doctype/system_console/system_console.py
@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import json
import frappe
-from frappe.utils.safe_exec import safe_exec
+from frappe.utils.safe_exec import safe_exec, read_sql
from frappe.model.document import Document
class SystemConsole(Document):
@@ -13,8 +13,11 @@ class SystemConsole(Document):
frappe.only_for('System Manager')
try:
frappe.debug_log = []
- safe_exec(self.console)
- self.output = '\n'.join(frappe.debug_log)
+ if self.type == 'Python':
+ safe_exec(self.console)
+ self.output = '\n'.join(frappe.debug_log)
+ elif self.type == 'SQL':
+ self.output = frappe.as_json(read_sql(self.console, as_dict=1))
except: # noqa: E722
self.output = frappe.get_traceback()
@@ -33,4 +36,9 @@ class SystemConsole(Document):
def execute_code(doc):
console = frappe.get_doc(json.loads(doc))
console.run()
- return console.as_dict()
\ No newline at end of file
+ return console.as_dict()
+
+@frappe.whitelist()
+def show_processlist():
+ frappe.only_for('System Manager')
+ return frappe.db.sql('show full processlist', as_dict=1)
diff --git a/frappe/desk/doctype/system_console/test_system_console.py b/frappe/desk/doctype/system_console/test_system_console.py
index 743c2d6dde..fa7c577faa 100644
--- a/frappe/desk/doctype/system_console/test_system_console.py
+++ b/frappe/desk/doctype/system_console/test_system_console.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/desk/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py
index 2341d721e2..381c24a765 100644
--- a/frappe/desk/doctype/tag/tag.py
+++ b/frappe/desk/doctype/tag/tag.py
@@ -1,9 +1,10 @@
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
from frappe.utils import unique
+from frappe.query_builder import DocType
class Tag(Document):
pass
@@ -11,7 +12,8 @@ class Tag(Document):
def check_user_tags(dt):
"if the user does not have a tags column, then it creates one"
try:
- frappe.db.sql("select `_user_tags` from `tab%s` limit 1" % dt)
+ doctype = DocType(dt)
+ frappe.qb.from_(doctype).select(doctype._user_tags).limit(1).run()
except Exception as e:
if frappe.db.is_column_missing(e):
DocTags(dt).setup()
@@ -42,10 +44,12 @@ def remove_tag(tag, dt, dn):
@frappe.whitelist()
def get_tagged_docs(doctype, tag):
frappe.has_permission(doctype, throw=True)
-
- return frappe.db.sql("""SELECT name
- FROM `tab{0}`
- WHERE _user_tags LIKE '%{1}%'""".format(doctype, tag))
+ doctype = DocType(doctype)
+ return (
+ frappe.qb.from_(doctype)
+ .where(doctype._user_tags.like(tag))
+ .select(doctype.name)
+ ).run()
@frappe.whitelist()
def get_tags(doctype, txt):
@@ -128,46 +132,35 @@ def delete_tags_for_document(doc):
})
def update_tags(doc, tags):
- """
- Adds tags for documents
- :param doc: Document to be added to global tags
- """
+ """Adds tags for documents
+ :param doc: Document to be added to global tags
+ """
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}):
- frappe.get_doc({
- "doctype": "Tag Link",
- "document_type": doc.doctype,
- "document_name": doc.name,
- "parenttype": doc.doctype,
- "parent": doc.name,
- "title": doc.get_title() or '',
- "tag": tag
- }).insert(ignore_permissions=True)
-
existing_tags = [tag.tag for tag in frappe.get_list("Tag Link", filters={
"document_type": doc.doctype,
"document_name": doc.name
}, fields=["tag"])]
- deleted_tags = get_deleted_tags(new_tags, existing_tags)
+ added_tags = set(new_tags) - set(existing_tags)
+ for tag in added_tags:
+ frappe.get_doc({
+ "doctype": "Tag Link",
+ "document_type": doc.doctype,
+ "document_name": doc.name,
+ "parenttype": doc.doctype,
+ "parent": doc.name,
+ "title": doc.get_title() or '',
+ "tag": tag
+ }).insert(ignore_permissions=True)
- if deleted_tags:
- for tag in deleted_tags:
- delete_tag_for_document(doc.doctype, doc.name, tag)
-
-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.delete("Tag Link", {
- "document_type": dt,
- "document_name": dn,
- "tag": tag
- })
+ deleted_tags = list(set(existing_tags) - set(new_tags))
+ for tag in deleted_tags:
+ frappe.db.delete("Tag Link", {
+ "document_type": doc.doctype,
+ "document_name": doc.name,
+ "tag": tag
+ })
@frappe.whitelist()
def get_documents_for_tag(tag):
diff --git a/frappe/desk/doctype/tag/test_tag.py b/frappe/desk/doctype/tag/test_tag.py
index 6eb7219c26..b9c6e0b744 100644
--- a/frappe/desk/doctype/tag/test_tag.py
+++ b/frappe/desk/doctype/tag/test_tag.py
@@ -6,7 +6,7 @@ from frappe.desk.doctype.tag.tag import add_tag
class TestTag(unittest.TestCase):
def setUp(self) -> None:
- frappe.db.sql("DELETE from `tabTag`")
+ frappe.db.delete("Tag")
frappe.db.sql("UPDATE `tabDocType` set _user_tags=''")
def test_tag_count_query(self):
diff --git a/frappe/desk/doctype/tag_link/tag_link.json b/frappe/desk/doctype/tag_link/tag_link.json
index 00a7349c5c..9142279fa3 100644
--- a/frappe/desk/doctype/tag_link/tag_link.json
+++ b/frappe/desk/doctype/tag_link/tag_link.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2019-09-24 13:25:36.435685",
"doctype": "DocType",
"editable_grid": 1,
@@ -44,7 +45,8 @@
"read_only": 1
}
],
- "modified": "2019-10-03 16:42:35.932409",
+ "links": [],
+ "modified": "2021-09-20 16:53:37.217998",
"modified_by": "Administrator",
"module": "Desk",
"name": "Tag Link",
@@ -61,6 +63,17 @@
"role": "System Manager",
"share": 1,
"write": 1
+ },
+ {
+ "create": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "All",
+ "share": 1,
+ "write": 1
}
],
"read_only": 1,
diff --git a/frappe/desk/doctype/tag_link/tag_link.py b/frappe/desk/doctype/tag_link/tag_link.py
index 4c5149f42c..d07894989d 100644
--- a/frappe/desk/doctype/tag_link/tag_link.py
+++ b/frappe/desk/doctype/tag_link/tag_link.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# 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 297ee3cc96..fa6a22903f 100644
--- a/frappe/desk/doctype/tag_link/test_tag_link.py
+++ b/frappe/desk/doctype/tag_link/test_tag_link.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/desk/doctype/todo/__init__.py b/frappe/desk/doctype/todo/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/desk/doctype/todo/__init__.py
+++ b/frappe/desk/doctype/todo/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/desk/doctype/todo/test_todo.py b/frappe/desk/doctype/todo/test_todo.py
index b38e4a059a..34d3cee191 100644
--- a/frappe/desk/doctype/todo/test_todo.py
+++ b/frappe/desk/doctype/todo/test_todo.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
from frappe.model.db_query import DatabaseQuery
@@ -14,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))
@@ -27,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 = ''
@@ -104,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')
@@ -122,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 09297b4e5e..6f3f4160e6 100644
--- a/frappe/desk/doctype/todo/todo.py
+++ b/frappe/desk/doctype/todo/todo.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
import json
@@ -29,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"
}
diff --git a/frappe/desk/doctype/workspace/test_workspace.py b/frappe/desk/doctype/workspace/test_workspace.py
index 619b3608eb..6c16e69afe 100644
--- a/frappe/desk/doctype/workspace/test_workspace.py
+++ b/frappe/desk/doctype/workspace/test_workspace.py
@@ -1,8 +1,95 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
-# import frappe
+# License: MIT. See LICENSE
+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.js b/frappe/desk/doctype/workspace/workspace.js
index 19d429f9f6..5377470343 100644
--- a/frappe/desk/doctype/workspace/workspace.js
+++ b/frappe/desk/doctype/workspace/workspace.js
@@ -8,14 +8,9 @@ frappe.ui.form.on('Workspace', {
refresh: function(frm) {
frm.enable_save();
- frm.get_field("is_standard").toggle(frappe.boot.developer_mode);
- frm.get_field("developer_mode_only").toggle(frappe.boot.developer_mode);
- if (frm.doc.for_user) {
- frm.set_df_property("extends", "read_only", true);
- }
-
- if (frm.doc.for_user || (frm.doc.is_standard && !frappe.boot.developer_mode)) {
+ if (frm.doc.for_user || (frm.doc.public && !frm.has_perm('write') &&
+ !frappe.user.has_role('Workspace Manager'))) {
frm.trigger('disable_form');
}
},
diff --git a/frappe/desk/doctype/workspace/workspace.json b/frappe/desk/doctype/workspace/workspace.json
index 386267b699..04975c69e3 100644
--- a/frappe/desk/doctype/workspace/workspace.json
+++ b/frappe/desk/doctype/workspace/workspace.json
@@ -8,37 +8,32 @@
"engine": "InnoDB",
"field_order": [
"label",
+ "title",
+ "sequence_id",
"for_user",
- "extends",
+ "parent_page",
"module",
- "category",
+ "column_break_3",
"icon",
"restrict_to_domain",
- "onboarding",
- "column_break_3",
- "extends_another_page",
- "is_default",
- "is_standard",
- "developer_mode_only",
- "disable_user_customization",
- "pin_to_top",
- "pin_to_bottom",
"hide_custom",
+ "public",
+ "content",
"section_break_2",
- "charts_label",
"charts",
"section_break_15",
- "shortcuts_label",
"shortcuts",
"section_break_18",
- "cards_label",
- "links"
+ "links",
+ "roles_section",
+ "roles"
],
"fields": [
{
"fieldname": "label",
"fieldtype": "Data",
"label": "Name",
+ "reqd": 1,
"unique": 1
},
{
@@ -55,7 +50,6 @@
"options": "Workspace Chart"
},
{
- "depends_on": "eval:!doc.extends_another_page || !doc.is_standard || frappe.boot.developer_mode",
"fieldname": "shortcuts",
"fieldtype": "Table",
"label": "Shortcuts",
@@ -66,7 +60,6 @@
"fieldtype": "Link",
"label": "Restrict to Domain",
"options": "Domain",
- "read_only_depends_on": "eval:doc.extends_another_page == 0",
"search_index": 1
},
{
@@ -81,64 +74,6 @@
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
- {
- "fieldname": "category",
- "fieldtype": "Select",
- "label": "Category",
- "options": "Modules\nDomains\nPlaces\nAdministration",
- "read_only_depends_on": "eval:doc.extends_another_page == 1",
- "search_index": 1
- },
- {
- "default": "0",
- "depends_on": "eval:doc.extends_another_page == 0",
- "fieldname": "developer_mode_only",
- "fieldtype": "Check",
- "label": "Developer Mode Only",
- "search_index": 1
- },
- {
- "default": "0",
- "depends_on": "eval:doc.pin_to_bottom!=1 && doc.extends_another_page == 0",
- "fieldname": "pin_to_top",
- "fieldtype": "Check",
- "label": "Pin To Top",
- "search_index": 1
- },
- {
- "default": "0",
- "depends_on": "eval:doc.extends_another_page == 0",
- "fieldname": "disable_user_customization",
- "fieldtype": "Check",
- "label": "Disable User Customization",
- "search_index": 1
- },
- {
- "default": "0",
- "depends_on": "eval:doc.pin_to_top!=1 && doc.extends_another_page == 0",
- "fieldname": "pin_to_bottom",
- "fieldtype": "Check",
- "label": "Pin To Bottom",
- "search_index": 1
- },
- {
- "depends_on": "eval:!doc.extends_another_page || !doc.is_standard",
- "fieldname": "charts_label",
- "fieldtype": "Data",
- "label": "Label"
- },
- {
- "depends_on": "eval:!doc.extends_another_page || !doc.is_standard",
- "fieldname": "shortcuts_label",
- "fieldtype": "Data",
- "label": "Label"
- },
- {
- "depends_on": "eval:!doc.extends_another_page || !doc.is_standard",
- "fieldname": "cards_label",
- "fieldtype": "Data",
- "label": "Label"
- },
{
"collapsible": 1,
"collapsible_depends_on": "shortcuts",
@@ -153,43 +88,12 @@
"fieldtype": "Section Break",
"label": "Link Cards"
},
- {
- "default": "0",
- "fieldname": "is_standard",
- "fieldtype": "Check",
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Is Standard",
- "search_index": 1
- },
- {
- "default": "0",
- "fieldname": "extends_another_page",
- "fieldtype": "Check",
- "label": "Extends Another Page",
- "search_index": 1
- },
- {
- "depends_on": "eval:doc.extends_another_page == 1 || doc.for_user",
- "fieldname": "extends",
- "fieldtype": "Link",
- "in_standard_filter": 1,
- "label": "Extends",
- "options": "Workspace",
- "search_index": 1
- },
{
"fieldname": "for_user",
"fieldtype": "Data",
"label": "For User",
"read_only": 1
},
- {
- "fieldname": "onboarding",
- "fieldtype": "Link",
- "label": "Onboarding",
- "options": "Module Onboarding"
- },
{
"default": "0",
"description": "Checking this will hide custom doctypes and reports cards in Links section",
@@ -199,7 +103,7 @@
},
{
"fieldname": "icon",
- "fieldtype": "Data",
+ "fieldtype": "Icon",
"label": "Icon"
},
{
@@ -209,19 +113,56 @@
"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",
+ "fieldname": "public",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Public",
+ "search_index": 1
+ },
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "label": "Title",
+ "reqd": 1
+ },
+ {
+ "fieldname": "parent_page",
+ "fieldtype": "Data",
+ "label": "Parent Page"
+ },
+ {
+ "default": "[]",
+ "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"
+ }
],
+ "in_create": 1,
"links": [],
- "modified": "2021-01-21 12:09:36.156614",
+ "modified": "2021-09-16 12:01:06.450622",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -232,7 +173,7 @@
"print": 1,
"read": 1,
"report": 1,
- "role": "System Manager",
+ "role": "Workspace Manager",
"share": 1,
"write": 1
},
@@ -248,4 +189,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 41b0227f2a..94114e3918 100644
--- a/frappe/desk/doctype/workspace/workspace.py
+++ b/frappe/desk/doctype/workspace/workspace.py
@@ -1,43 +1,38 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
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
class Workspace(Document):
def validate(self):
- if (self.is_standard and not frappe.conf.developer_mode and not disable_saving_as_standard()):
- frappe.throw(_("You need to be in developer mode to edit this document"))
+ if (self.public and not is_workspace_manager() and not disable_saving_as_public()):
+ frappe.throw(_("You need to be Workspace Manager to edit this document"))
validate_route_conflict(self.doctype, self.name)
- duplicate_exists = frappe.db.exists("Workspace", {
- "name": ["!=", self.name], 'is_default': 1, 'extends': self.extends
- })
-
- if self.is_default and self.name and duplicate_exists:
- frappe.throw(_("You can only have one default page that extends a particular standard page."))
+ try:
+ if not isinstance(loads(self.content), list):
+ raise
+ except Exception:
+ frappe.throw(_("Content data shoud be a list"))
def on_update(self):
- if disable_saving_as_standard():
+ if disable_saving_as_public():
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
def get_module_page_map():
- filters = {
- 'extends_another_page': 0,
- 'for_user': '',
- }
-
- pages = frappe.get_all("Workspace", fields=["name", "module"], filters=filters, as_list=1)
+ pages = frappe.get_all("Workspace", fields=["name", "module"], filters={'for_user': ''}, as_list=1)
return { page[1]: page[0] for page in pages if page[1] }
@@ -55,7 +50,7 @@ class Workspace(Document):
for link in self.links:
link = link.as_dict()
if link.type == "Card Break":
- if card_links and (not current_card.only_for or current_card.only_for == frappe.get_system_settings('country')):
+ if card_links and (not current_card.get('only_for') or current_card.get('only_for') == frappe.get_system_settings('country')):
current_card['links'] = card_links
cards.append(current_card)
@@ -69,21 +64,23 @@ class Workspace(Document):
return cards
- def build_links_table_from_cards(self, config):
- # Empty links table
- self.links = []
- order = config.get('order')
- widgets = config.get('widgets')
+ def build_links_table_from_card(self, config):
- for idx, name in enumerate(order):
- card = widgets[name].copy()
+ 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
+ "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:
@@ -95,11 +92,11 @@ class Workspace(Document):
"onboard": link.get('onboard'),
"only_for": link.get('only_for'),
"dependencies": link.get('dependencies'),
- "is_query_report": link.get('is_query_report')
+ "is_query_report": link.get('is_query_report'),
+ "idx": self.links[-1].idx + 1
})
-
-def disable_saving_as_standard():
+def disable_saving_as_public():
return frappe.flags.in_install or \
frappe.flags.in_patch or \
frappe.flags.in_test or \
@@ -123,3 +120,87 @@ 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, "label": doc.label}
+
+def delete_pages(deleted_pages):
+ for page in deleted_pages:
+ if page.get("public") and not is_workspace_manager():
+ return {"name": page.get("title"), "public": 1, "label": page.get("label")}
+
+ if frappe.db.exists("Workspace", page.get("name")):
+ frappe.get_doc("Workspace", page.get("name")).delete(ignore_permissions=True)
+
+ return {"name": "Home", "public": 1, "label": "Home"}
+
+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 is_workspace_manager():
+ 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')
+
+def is_workspace_manager():
+ return "Workspace Manager" in frappe.get_roles()
diff --git a/frappe/desk/doctype/workspace_chart/workspace_chart.py b/frappe/desk/doctype/workspace_chart/workspace_chart.py
index 6ec7abfd3c..a3b66d99ab 100644
--- a/frappe/desk/doctype/workspace_chart/workspace_chart.py
+++ b/frappe/desk/doctype/workspace_chart/workspace_chart.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# 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 d6ccc5306a..72256ba490 100644
--- a/frappe/desk/doctype/workspace_link/workspace_link.py
+++ b/frappe/desk/doctype/workspace_link/workspace_link.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# 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 83b446e454..1dad4cca05 100644
--- a/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.py
+++ b/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/form/__init__.py b/frappe/desk/form/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/desk/form/__init__.py
+++ b/frappe/desk/form/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/desk/form/assign_to.py b/frappe/desk/form/assign_to.py
index 3eda291d1e..bf77170eeb 100644
--- a/frappe/desk/form/assign_to.py
+++ b/frappe/desk/form/assign_to.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
"""assign/unassign to ToDo"""
diff --git a/frappe/desk/form/document_follow.py b/frappe/desk/form/document_follow.py
index 7f65f76a58..14970092d0 100644
--- a/frappe/desk/form/document_follow.py
+++ b/frappe/desk/form/document_follow.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
import frappe.utils
diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py
index ae48b7fc6b..14ea2712e2 100644
--- a/frappe/desk/form/linked_with.py
+++ b/frappe/desk/form/linked_with.py
@@ -1,7 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import json
from collections import defaultdict
+import itertools
+from typing import List
import frappe
import frappe.desk.form.load
@@ -12,72 +14,299 @@ from frappe.modules import load_doctype_module
@frappe.whitelist()
-def get_submitted_linked_docs(doctype, name, docs=None, visited=None):
+def get_submitted_linked_docs(doctype: str, name: str) -> List[tuple]:
+ """ Get all the nested submitted documents those are present in referencing tables (dependent tables).
+
+ :param doctype: Document type
+ :param name: Name of the document
+
+ Usecase:
+ * User should be able to cancel the linked documents along with the one user trying to cancel.
+
+ Case1: If document sd1-n1 (document name n1 from sumittable doctype sd1) is linked to sd2-n2 and sd2-n2 is linked to sd3-n3,
+ Getting submittable linked docs of `sd1-n1`should give both sd2-n2 and sd3-n3.
+ Case2: If document sd1-n1 (document name n1 from sumittable doctype sd1) is linked to d2-n2 and d2-n2 is linked to sd3-n3,
+ Getting submittable linked docs of `sd1-n1`should give None. (because d2-n2 is not a submittable doctype)
+ Case3: If document sd1-n1 (document name n1 from submittable doctype sd1) is linked to d2-n2 & sd2-n2. d2-n2 is linked to sd3-n3.
+ Getting submittable linked docs of `sd1-n1`should give sd2-n2.
+
+ Logic:
+ -----
+ 1. We can find linked documents only if we know how the doctypes are related.
+ 2. As we need only submittable documents, we can limit doctype relations search to submittable doctypes by
+ finding the relationships(Foreign key references) across submittable doctypes.
+ 3. Searching for links is going to be a tree like structure where at every level,
+ you will be finding documents using parent document and parent document links.
"""
- Get all nested submitted linked doctype linkinfo
+ tree = SubmittableDocumentTree(doctype, name)
+ visited_documents = tree.get_all_children()
+ docs = []
- Arguments:
- doctype (str) - The doctype for which get all linked doctypes
- name (str) - The docname for which get all linked doctypes
+ for dt, names in visited_documents.items():
+ docs.extend([{'doctype': dt, 'name': name, 'docstatus': 1} for name in names])
- Keyword Arguments:
- docs (list of dict) - (Optional) Get list of dictionary for linked doctype.
-
- Returns:
- dict - Return list of documents and link count
- """
-
- if not docs:
- docs = []
-
- if not visited:
- visited = {}
-
- if doctype not in visited:
- visited[doctype] = []
-
- if name in visited[doctype]:
- return
-
- linkinfo = get_linked_doctypes(doctype)
- linked_docs = get_linked_docs(doctype, name, linkinfo)
-
- link_count = 0
- visited[doctype].append(name)
-
- for link_doctype, link_names in linked_docs.items():
-
- for link in link_names:
- if link['name'] == name:
- continue
-
- docinfo = link.update({"doctype": link_doctype})
- validated_doc = validate_linked_doc(docinfo)
-
- if not validated_doc:
- continue
-
- link_count += 1
-
- links = get_submitted_linked_docs(link_doctype, link.name, docs, visited)
- if links:
- docs.append({
- "doctype": link_doctype,
- "name": link.name,
- "docstatus": link.docstatus,
- "link_count": links.get("count")
- })
-
- # sort linked documents by ascending number of links
- docs.sort(key=lambda doc: doc.get("link_count"))
return {
"docs": docs,
- "count": link_count
+ "count": len(docs)
}
+class SubmittableDocumentTree:
+ def __init__(self, doctype: str, name: str):
+ """Construct a tree for the submitable linked documents.
+
+ * Node has properties like doctype and docnames. Represented as Node(doctype, docnames).
+ * Nodes are linked by doctype relationships like table, link and dynamic links.
+ * Node is referenced(linked) by many other documents and those are the child nodes.
+
+ NOTE: child document is a property of child node (not same as Frappe child docs of a table field).
+ """
+ self.root_doctype = doctype
+ self.root_docname = name
+
+ # Documents those are yet to be visited for linked documents.
+ self.to_be_visited_documents = {doctype: [name]}
+ self.visited_documents = defaultdict(list)
+
+ self._submittable_doctypes = None # All submittable doctypes in the system
+ self._references_across_doctypes = None # doctype wise links/references
+
+ def get_all_children(self):
+ """Get all nodes of a tree except the root node (all the nested submitted
+ documents those are present in referencing tables (dependent tables).
+ """
+ while self.to_be_visited_documents:
+ next_level_children = defaultdict(list)
+ for parent_dt in list(self.to_be_visited_documents):
+ parent_docs = self.to_be_visited_documents.get(parent_dt)
+ if not parent_docs:
+ del self.to_be_visited_documents[parent_dt]
+ continue
+
+ child_docs = self.get_next_level_children(parent_dt, parent_docs)
+ self.visited_documents[parent_dt].extend(parent_docs)
+ for linked_dt, linked_names in child_docs.items():
+ not_visited_child_docs = set(linked_names) - set(self.visited_documents.get(linked_dt, []))
+ next_level_children[linked_dt].extend(not_visited_child_docs)
+
+ self.to_be_visited_documents = next_level_children
+
+ # Remove root node from visited documents
+ if self.root_docname in self.visited_documents.get(self.root_doctype, []):
+ self.visited_documents[self.root_doctype].remove(self.root_docname)
+
+ return self.visited_documents
+
+ def get_next_level_children(self, parent_dt, parent_names):
+ """Get immediate children of a Node(parent_dt, parent_names)
+ """
+ referencing_fields = self.get_doctype_references(parent_dt)
+
+ child_docs = defaultdict(list)
+ for field in referencing_fields:
+ links = get_referencing_documents(parent_dt, parent_names.copy(), field, get_parent_if_child_table_doc=True,
+ parent_filters=[('docstatus', '=', 1)], allowed_parents=self.get_link_sources()) or {}
+ for dt, names in links.items():
+ child_docs[dt].extend(names)
+ return child_docs
+
+ def get_doctype_references(self, doctype):
+ """Get references for a given document.
+ """
+ if self._references_across_doctypes is None:
+ get_links_to = self.get_document_sources()
+ limit_link_doctypes = self.get_link_sources()
+ self._references_across_doctypes = get_references_across_doctypes(
+ get_links_to, limit_link_doctypes)
+ return self._references_across_doctypes.get(doctype, [])
+
+ def get_document_sources(self):
+ """Returns list of doctypes from where we access submittable documents.
+ """
+ return list(set(self.get_link_sources() + [self.root_doctype]))
+
+ def get_link_sources(self):
+ """limit doctype links to these doctypes.
+ """
+ return list(set(self.get_submittable_doctypes()) - set(get_exempted_doctypes() or []))
+
+ def get_submittable_doctypes(self) -> List[str]:
+ """Returns list of submittable doctypes.
+ """
+ if not self._submittable_doctypes:
+ self._submittable_doctypes = frappe.db.get_list('DocType', {'is_submittable': 1}, pluck='name')
+ return self._submittable_doctypes
+
+
+def get_child_tables_of_doctypes(doctypes: List[str]=None):
+ """Returns child tables by doctype.
+ """
+ filters=[['fieldtype','=', 'Table']]
+ filters_for_docfield = filters
+ filters_for_customfield = filters
+
+ if doctypes:
+ filters_for_docfield = filters + [['parent', 'in', tuple(doctypes)]]
+ filters_for_customfield = filters + [['dt', 'in', tuple(doctypes)]]
+
+ links = frappe.get_all("DocField",
+ fields=["parent", "fieldname", "options as child_table"],
+ filters=filters_for_docfield,
+ as_list=1)
+
+ links+= frappe.get_all("Custom Field",
+ fields=["dt as parent", "fieldname", "options as child_table"],
+ filters=filters_for_customfield,
+ as_list=1)
+
+ child_tables_by_doctype = defaultdict(list)
+ for doctype, fieldname, child_table in links:
+ child_tables_by_doctype[doctype].append(
+ {'doctype': doctype, 'fieldname': fieldname, 'child_table': child_table})
+ return child_tables_by_doctype
+
+
+def get_references_across_doctypes(to_doctypes: List[str]=None, limit_link_doctypes: List[str]=None) -> List:
+ """Find doctype wise foreign key references.
+
+ :param to_doctypes: Get links of these doctypes.
+ :param limit_link_doctypes: limit links to these doctypes.
+
+ * Include child table, link and dynamic link references.
+ """
+ if limit_link_doctypes:
+ child_tables_by_doctype = get_child_tables_of_doctypes(limit_link_doctypes)
+ all_child_tables = [each['child_table'] for each in itertools.chain(*child_tables_by_doctype.values())]
+ limit_link_doctypes = limit_link_doctypes + all_child_tables
+ else:
+ child_tables_by_doctype = get_child_tables_of_doctypes()
+ all_child_tables = [each['child_table'] for each in itertools.chain(*child_tables_by_doctype.values())]
+
+ references_by_link_fields = get_references_across_doctypes_by_link_field(to_doctypes, limit_link_doctypes)
+ references_by_dlink_fields = get_references_across_doctypes_by_dynamic_link_field(to_doctypes, limit_link_doctypes)
+
+ references = references_by_link_fields.copy()
+ for k, v in references_by_dlink_fields.items():
+ references.setdefault(k, []).extend(v)
+
+ for doctype, links in references.items():
+ for link in links:
+ link['is_child'] = (link['doctype'] in all_child_tables)
+ return references
+
+
+def get_references_across_doctypes_by_link_field(to_doctypes: List[str]=None, limit_link_doctypes: List[str]=None):
+ """Find doctype wise foreign key references based on link fields.
+
+ :param to_doctypes: Get links to these doctypes.
+ :param limit_link_doctypes: limit links to these doctypes.
+ """
+ filters=[['fieldtype','=', 'Link']]
+
+ if to_doctypes:
+ filters += [['options', 'in', tuple(to_doctypes)]]
+
+ filters_for_docfield = filters[:]
+ filters_for_customfield = filters[:]
+
+ if limit_link_doctypes:
+ filters_for_docfield += [['parent', 'in', tuple(limit_link_doctypes)]]
+ filters_for_customfield += [['dt', 'in', tuple(limit_link_doctypes)]]
+
+ links = frappe.get_all("DocField",
+ fields=["parent", "fieldname", "options as linked_to"],
+ filters=filters_for_docfield,
+ as_list=1)
+
+ links+= frappe.get_all("Custom Field",
+ fields=["dt as parent", "fieldname", "options as linked_to"],
+ filters=filters_for_customfield,
+ as_list=1)
+
+ links_by_doctype = defaultdict(list)
+ for doctype, fieldname, linked_to in links:
+ links_by_doctype[linked_to].append({'doctype': doctype, 'fieldname': fieldname})
+ return links_by_doctype
+
+
+def get_references_across_doctypes_by_dynamic_link_field(to_doctypes: List[str]=None, limit_link_doctypes: List[str]=None):
+ """Find doctype wise foreign key references based on dynamic link fields.
+
+ :param to_doctypes: Get links to these doctypes.
+ :param limit_link_doctypes: limit links to these doctypes.
+ """
+
+ filters=[['fieldtype','=', 'Dynamic Link']]
+
+ filters_for_docfield = filters[:]
+ filters_for_customfield = filters[:]
+
+ if limit_link_doctypes:
+ filters_for_docfield += [['parent', 'in', tuple(limit_link_doctypes)]]
+ filters_for_customfield += [['dt', 'in', tuple(limit_link_doctypes)]]
+
+ # find dynamic links of parents
+ links = frappe.get_all("DocField",
+ fields=["parent as doctype", "fieldname", "options as doctype_fieldname"],
+ filters=filters_for_docfield,
+ as_list=1)
+
+ links += frappe.get_all("Custom Field",
+ fields=["dt as doctype", "fieldname", "options as doctype_fieldname"],
+ filters=filters_for_customfield,
+ as_list=1)
+
+ links_by_doctype = defaultdict(list)
+ for doctype, fieldname, doctype_fieldname in links:
+ try:
+ filters = [[doctype_fieldname, 'in', to_doctypes]] if to_doctypes else []
+ for linked_to in frappe.db.get_all(doctype, pluck=doctype_fieldname, filters = filters, distinct=1):
+ if linked_to:
+ links_by_doctype[linked_to].append({'doctype': doctype, 'fieldname': fieldname, 'doctype_fieldname': doctype_fieldname})
+ except frappe.db.ProgrammingError:
+ # TODO: FIXME
+ continue
+ return links_by_doctype
+
+def get_referencing_documents(reference_doctype: str, reference_names: List[str],
+ link_info: dict, get_parent_if_child_table_doc: bool=True,
+ parent_filters: List[list]=None, child_filters=None, allowed_parents=None):
+ """Get linked documents based on link_info.
+
+ :param reference_doctype: reference doctype to find links
+ :param reference_names: reference document names to find links for
+ :param link_info: linking details to get the linked documents
+ Ex: {'doctype': 'Purchase Invoice Advance', 'fieldname': 'reference_name',
+ 'doctype_fieldname': 'reference_type', 'is_child': True}
+ :param get_parent_if_child_table_doc: Get parent record incase linked document is a child table record.
+ :param parent_filters: filters to apply on if not a child table.
+ :param child_filters: apply filters if it is a child table.
+ :param allowed_parents: list of parents allowed in case of get_parent_if_child_table_doc
+ is enabled.
+ """
+ from_table = link_info['doctype']
+ filters = [[link_info['fieldname'], 'in', tuple(reference_names)]]
+ if link_info.get('doctype_fieldname'):
+ filters.append([link_info['doctype_fieldname'], '=', reference_doctype])
+
+ if not link_info.get('is_child'):
+ filters.extend(parent_filters or [])
+ return {from_table: frappe.db.get_all(from_table, filters, pluck='name')}
+
+
+ filters.extend(child_filters or [])
+ res = frappe.db.get_all(from_table, filters = filters, fields = ['name', 'parenttype', 'parent'])
+ documents = defaultdict(list)
+
+ for parent, rows in itertools.groupby(res, key = lambda row: row['parenttype']):
+ if allowed_parents and parent not in allowed_parents:
+ continue
+ filters = (parent_filters or []) + [['name', 'in', tuple([row.parent for row in rows])]]
+ documents[parent].extend(frappe.db.get_all(parent, filters=filters, pluck='name') or [])
+ return documents
+
@frappe.whitelist()
-def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=[]):
+def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=None):
"""
Cancel all linked doctype, optionally ignore doctypes specified in a list.
@@ -85,6 +314,8 @@ def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=[]):
docs (json str) - It contains list of dictionaries of a linked documents.
ignore_doctypes_on_cancel_all (list) - List of doctypes to ignore while cancelling.
"""
+ if ignore_doctypes_on_cancel_all is None:
+ ignore_doctypes_on_cancel_all = []
docs = json.loads(docs)
if isinstance(ignore_doctypes_on_cancel_all, str):
@@ -96,7 +327,7 @@ def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=[]):
frappe.publish_progress(percent=i/len(docs) * 100, title=_("Cancelling documents"))
-def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=[]):
+def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=None):
"""
Validate a document to be submitted and non-exempted from auto-cancel.
@@ -107,9 +338,8 @@ def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=[]):
Returns:
bool: True if linked document passes all validations, else False
"""
-
#ignore doctype to cancel
- if docinfo.get("doctype") in ignore_doctypes_on_cancel_all:
+ if docinfo.get("doctype") in (ignore_doctypes_on_cancel_all or []):
return False
# skip non-submittable doctypes since they don't need to be cancelled
@@ -130,7 +360,6 @@ def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=[]):
def get_exempted_doctypes():
""" Get list of doctypes exempted from being auto-cancelled """
-
auto_cancel_exempt_doctypes = []
for doctypes in frappe.get_hooks('auto_cancel_exempted_doctypes'):
auto_cancel_exempt_doctypes.append(doctypes)
@@ -181,11 +410,11 @@ def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None):
try:
if link.get("filters"):
- ret = frappe.get_list(doctype=dt, fields=fields, filters=link.get("filters"))
+ ret = frappe.get_all(doctype=dt, fields=fields, filters=link.get("filters"))
elif link.get("get_parent"):
if me and me.parent and me.parenttype == dt:
- ret = frappe.get_list(doctype=dt, fields=fields,
+ ret = frappe.get_all(doctype=dt, fields=fields,
filters=[[dt, "name", '=', me.parent]])
else:
ret = None
@@ -197,7 +426,7 @@ def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None):
if link.get("doctype_fieldname"):
filters.append([link.get('child_doctype'), link.get("doctype_fieldname"), "=", doctype])
- ret = frappe.get_list(doctype=dt, fields=fields, filters=filters, or_filters=or_filters, distinct=True)
+ ret = frappe.get_all(doctype=dt, fields=fields, filters=filters, or_filters=or_filters, distinct=True)
else:
link_fieldnames = link.get("fieldname")
@@ -208,7 +437,7 @@ def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None):
# dynamic link
if link.get("doctype_fieldname"):
filters.append([dt, link.get("doctype_fieldname"), "=", doctype])
- ret = frappe.get_list(doctype=dt, fields=fields, filters=filters, or_filters=or_filters)
+ ret = frappe.get_all(doctype=dt, fields=fields, filters=filters, or_filters=or_filters)
else:
ret = None
diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py
index 994a50f938..949bbb59db 100644
--- a/frappe/desk/form/load.py
+++ b/frappe/desk/form/load.py
@@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
+from typing import Dict, List, Union
import frappe, json
import frappe.utils
import frappe.share
@@ -12,7 +13,7 @@ from frappe.desk.form.document_follow import is_document_followed
from frappe import _
from urllib.parse import quote
-@frappe.whitelist(allow_guest=True)
+@frappe.whitelist()
def getdoc(doctype, name, user=None):
"""
Loads a doclist for a given document. This method is called directly from the client.
@@ -51,7 +52,7 @@ def getdoc(doctype, name, user=None):
set_link_titles(doc)
frappe.response.docs.append(doc)
-@frappe.whitelist(allow_guest=True)
+@frappe.whitelist()
def getdoctype(doctype, with_parent=False, cached_timestamp=None):
"""load doctype"""
@@ -105,9 +106,10 @@ def get_docinfo(doc=None, doctype=None, name=None):
"assignment_logs": get_comments(doc.doctype, doc.name, 'assignment'),
"permissions": get_doc_permissions(doc),
"shared": frappe.share.get_users(doc.doctype, doc.name),
- "info_logs": get_comments(doc.doctype, doc.name, 'Info'),
+ "info_logs": get_comments(doc.doctype, doc.name, comment_type=['Info', 'Edit', 'Label']),
"share_logs": get_comments(doc.doctype, doc.name, 'share'),
"like_logs": get_comments(doc.doctype, doc.name, 'Like'),
+ "workflow_logs": get_comments(doc.doctype, doc.name, comment_type="Workflow"),
"views": get_view_logs(doc.doctype, doc.name),
"energy_point_logs": get_point_logs(doc.doctype, doc.name),
"additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name),
@@ -138,10 +140,11 @@ def get_communications(doctype, name, start=0, limit=20):
return _get_communications(doctype, name, start, limit)
-def get_comments(doctype, name, comment_type='Comment'):
- comment_types = [comment_type]
+def get_comments(doctype: str, name: str, comment_type : Union[str, List[str]] = "Comment") -> List[frappe._dict]:
+ if isinstance(comment_type, list):
+ comment_types = comment_type
- if comment_type == 'share':
+ elif comment_type == 'share':
comment_types = ['Shared', 'Unshared']
elif comment_type == 'assignment':
@@ -150,15 +153,21 @@ def get_comments(doctype, name, comment_type='Comment'):
elif comment_type == 'attachment':
comment_types = ['Attachment', 'Attachment Removed']
- comments = frappe.get_all('Comment', fields = ['name', 'creation', 'content', 'owner', 'comment_type'], filters=dict(
- reference_doctype = doctype,
- reference_name = name,
- comment_type = ['in', comment_types]
- ))
+ else:
+ comment_types = [comment_type]
+
+ comments = frappe.get_all("Comment",
+ fields=["name", "creation", "content", "owner", "comment_type"],
+ filters={
+ "reference_doctype": doctype,
+ "reference_name": name,
+ "comment_type": ['in', comment_types],
+ }
+ )
# convert to markdown (legacy ?)
- if comment_type == 'Comment':
- for c in comments:
+ for c in comments:
+ if c.comment_type == "Comment":
c.content = frappe.utils.markdown(c.content)
return comments
diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py
index cf3606e785..b91dd3d481 100644
--- a/frappe/desk/form/meta.py
+++ b/frappe/desk/form/meta.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import io
import os
diff --git a/frappe/desk/form/save.py b/frappe/desk/form/save.py
index a7a4b829d8..b580e2c769 100644
--- a/frappe/desk/form/save.py
+++ b/frappe/desk/form/save.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
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 f3c4132777..86c3aba29a 100644
--- a/frappe/desk/form/test_form.py
+++ b/frappe/desk/form/test_form.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe, unittest
diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py
index bfceee6ea2..291767de10 100644
--- a/frappe/desk/form/utils.py
+++ b/frappe/desk/form/utils.py
@@ -1,11 +1,11 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
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 _
@@ -16,44 +16,6 @@ def remove_attach():
file_name = frappe.form_dict.get('file_name')
frappe.delete_doc('File', fid)
-@frappe.whitelist()
-def validate_link():
- """validate link when updated by user"""
- import frappe
- import frappe.utils
-
- value, options, fetch = frappe.form_dict.get('value'), frappe.form_dict.get('options'), frappe.form_dict.get('fetch')
-
- # no options, don't validate
- if not options or options=='null' or options=='undefined':
- frappe.response['message'] = 'Ok'
- return
-
- valid_value = frappe.db.get_all(options, filters=dict(name=value), as_list=1, limit=1)
-
- if valid_value:
- valid_value = valid_value[0][0]
-
- # get fetch values
- if fetch:
- # escape with "`"
- fetch = ", ".join(("`{0}`".format(f.strip()) for f in fetch.split(",")))
- fetch_value = None
- try:
- fetch_value = frappe.db.sql("select %s from `tab%s` where name=%s"
- % (fetch, options, '%s'), (value,))[0]
- except Exception as e:
- error_message = str(e).split("Unknown column '")
- fieldname = None if len(error_message)<=1 else error_message[1].split("'")[0]
- frappe.msgprint(_("Wrong fieldname {0} in add_fetch configuration of custom client script").format(fieldname))
- frappe.errprint(frappe.get_traceback())
-
- if fetch_value:
- frappe.response['fetch_values'] = [frappe.utils.parse_val(c) for c in fetch_value]
-
- frappe.response['valid_value'] = valid_value
- frappe.response['message'] = 'Ok'
-
@frappe.whitelist()
def add_comment(reference_doctype, reference_name, content, comment_email, comment_by):
@@ -66,7 +28,8 @@ def add_comment(reference_doctype, reference_name, content, comment_email, comme
comment_type='Comment',
comment_by=comment_by
))
- doc.content = extract_images_from_html(doc, content)
+ reference_doc = frappe.get_doc(reference_doctype, reference_name)
+ doc.content = extract_images_from_html(reference_doc, content, is_private=True)
doc.insert(ignore_permissions=True)
follow_document(doc.reference_doctype, doc.reference_name, frappe.session.user)
diff --git a/frappe/desk/gantt.py b/frappe/desk/gantt.py
index 7f0889c751..58ef3b836e 100644
--- a/frappe/desk/gantt.py
+++ b/frappe/desk/gantt.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe, json
diff --git a/frappe/desk/like.py b/frappe/desk/like.py
index d44d58a761..4480ed8a1e 100644
--- a/frappe/desk/like.py
+++ b/frappe/desk/like.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
"""Allow adding of likes to documents"""
diff --git a/frappe/desk/link_preview.py b/frappe/desk/link_preview.py
index 9b4471aa8d..03f8368a3a 100644
--- a/frappe/desk/link_preview.py
+++ b/frappe/desk/link_preview.py
@@ -40,6 +40,10 @@ def get_preview_data(doctype, docname):
for key, val in preview_data.items():
if val and meta.has_field(key) and key not in [image_field, title_field, 'name']:
- formatted_preview_data[meta.get_field(key).label] = frappe.format(val, meta.get_field(key).fieldtype)
+ formatted_preview_data[meta.get_field(key).label] = frappe.format(
+ val,
+ meta.get_field(key).fieldtype,
+ translated=True,
+ )
return formatted_preview_data
diff --git a/frappe/desk/listview.py b/frappe/desk/listview.py
index d2c84d36bf..43ad104f0d 100644
--- a/frappe/desk/listview.py
+++ b/frappe/desk/listview.py
@@ -1,8 +1,8 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
-@frappe.whitelist(allow_guest=True)
+@frappe.whitelist()
def get_list_settings(doctype):
try:
return frappe.get_cached_doc("List View Settings", doctype)
@@ -26,7 +26,7 @@ def get_group_by_count(doctype, current_filters, field):
current_filters = frappe.parse_json(current_filters)
subquery_condition = ''
- subquery = frappe.get_all(doctype, filters=current_filters, return_query = True)
+ subquery = frappe.get_all(doctype, filters=current_filters, run=False)
if field == 'assigned_to':
subquery_condition = ' and `tabToDo`.reference_name in ({subquery})'.format(subquery = subquery)
return frappe.db.sql("""select `tabToDo`.owner as name, count(*) as count
diff --git a/frappe/desk/moduleview.py b/frappe/desk/moduleview.py
index 021698ac92..e2e2c4c155 100644
--- a/frappe/desk/moduleview.py
+++ b/frappe/desk/moduleview.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
import json
diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py
index c84027928e..3fa41790b4 100644
--- a/frappe/desk/notifications.py
+++ b/frappe/desk/notifications.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.desk.doctype.notification_settings.notification_settings import get_subscribed_documents
@@ -216,7 +216,7 @@ def get_filters_for(doctype):
@frappe.whitelist()
@frappe.read_only()
-def get_open_count(doctype, name, items=[]):
+def get_open_count(doctype, name, items=None):
'''Get open count for given transactions and filters
:param doctype: Reference DocType
@@ -235,7 +235,8 @@ def get_open_count(doctype, name, items=[]):
links = meta.get_dashboard_data()
# compile all items in a list
- if not items:
+ if items is None:
+ items = []
for group in links.transactions:
items.extend(group.get("items"))
diff --git a/frappe/desk/page/activity/activity.py b/frappe/desk/page/activity/activity.py
index 3abc8e0ea5..71130f2304 100644
--- a/frappe/desk/page/activity/activity.py
+++ b/frappe/desk/page/activity/activity.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.utils import cint
diff --git a/frappe/desk/page/backups/backups.html b/frappe/desk/page/backups/backups.html
index e63481487c..ff10f1bd06 100644
--- a/frappe/desk/page/backups/backups.html
+++ b/frappe/desk/page/backups/backups.html
@@ -1,20 +1,27 @@
- {% for f in files %}
-
- {% endfor %}
+ {% for f in files %}
+
+ {% endfor %}
\ No newline at end of file
diff --git a/frappe/desk/page/backups/backups.js b/frappe/desk/page/backups/backups.js
index 337ad33f43..d6cab750f0 100644
--- a/frappe/desk/page/backups/backups.js
+++ b/frappe/desk/page/backups/backups.js
@@ -1,4 +1,4 @@
-frappe.pages['backups'].on_page_load = function(wrapper) {
+frappe.pages['backups'].on_page_load = function (wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: __('Download Backups'),
@@ -11,12 +11,35 @@ frappe.pages['backups'].on_page_load = function(wrapper) {
page.add_inner_button(__("Download Files Backup"), function () {
frappe.call({
- method:"frappe.desk.page.backups.backups.schedule_files_backup",
- args: {"user_email": frappe.session.user_email}
+ method: "frappe.desk.page.backups.backups.schedule_files_backup",
+ args: { "user_email": frappe.session.user_email }
});
});
+ page.add_inner_button(__("Get Backup Encryption Key"), function () {
+ if (frappe.user.has_role("System Manager")) {
+ frappe.verify_password(function () {
+ frappe.call({
+ method: "frappe.utils.backups.get_backup_encryption_key",
+ callback: function (r) {
+ frappe.msgprint({
+ title: __('Backup Encryption Key'),
+ message: __(r.message),
+ indicator: 'blue'
+ });
+ }
+ });
+ });
+ } else {
+ frappe.msgprint({
+ title: __('Error'),
+ message: __('System Manager privileges required.'),
+ indicator: 'red'
+ });
+ }
+ });
+
frappe.breadcrumbs.add("Setup");
$(frappe.render_template("backups")).appendTo(page.body.addClass("no-border"));
-}
+};
diff --git a/frappe/desk/page/backups/backups.py b/frappe/desk/page/backups/backups.py
index 2229a6d89e..14ed025e08 100644
--- a/frappe/desk/page/backups/backups.py
+++ b/frappe/desk/page/backups/backups.py
@@ -11,6 +11,10 @@ def get_context(context):
dt = os.path.getmtime(path)
return convert_utc_to_user_timezone(datetime.datetime.utcfromtimestamp(dt)).strftime('%a %b %d %H:%M %Y')
+ def get_encrytion_status(path):
+ if "-enc" in path:
+ return True
+
def get_size(path):
size = os.path.getsize(path)
if size > 1048576:
@@ -26,8 +30,9 @@ def get_context(context):
cleanup_old_backups(path, files, backup_limit)
files = [('/backups/' + _file,
- get_time(os.path.join(path, _file)),
- get_size(os.path.join(path, _file))) for _file in files if _file.endswith('sql.gz')]
+ get_time(os.path.join(path, _file)),
+ get_encrytion_status(os.path.join(path, _file)),
+ get_size(os.path.join(path, _file))) for _file in files if _file.endswith('sql.gz')]
files.sort(key=lambda x: x[1], reverse=True)
return {"files": files[:backup_limit]}
diff --git a/frappe/desk/page/leaderboard/leaderboard.js b/frappe/desk/page/leaderboard/leaderboard.js
index b3fccf84f9..076d672db5 100644
--- a/frappe/desk/page/leaderboard/leaderboard.js
+++ b/frappe/desk/page/leaderboard/leaderboard.js
@@ -141,7 +141,7 @@ class Leaderboard {
}
create_date_range_field() {
- let timespan_field = $(this.parent).find(`.frappe-control[data-original-title=${__('Timespan')}]`);
+ let timespan_field = $(this.parent).find(`.frappe-control[data-original-title="${__('Timespan')}"]`);
this.date_range_field = $(``).insertAfter(timespan_field).hide();
let date_field = frappe.ui.form.make_control({
diff --git a/frappe/desk/page/leaderboard/leaderboard.py b/frappe/desk/page/leaderboard/leaderboard.py
index 9469096f50..ad22eb9199 100644
--- a/frappe/desk/page/leaderboard/leaderboard.py
+++ b/frappe/desk/page/leaderboard/leaderboard.py
@@ -1,5 +1,5 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
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 06301cdeaf..1ef83f7ba0 100644
--- a/frappe/desk/page/setup_wizard/install_fixtures.py
+++ b/frappe/desk/page/setup_wizard/install_fixtures.py
@@ -1,5 +1,5 @@
-# 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
+# License: MIT. See LICENSE
import frappe
from frappe import _
diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js
index f44a57e339..7e90bc01ad 100644
--- a/frappe/desk/page/setup_wizard/setup_wizard.js
+++ b/frappe/desk/page/setup_wizard/setup_wizard.js
@@ -197,6 +197,8 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {
callback: (r) => {
if (r.message.status === 'ok') {
this.post_setup_success();
+ } else if (r.message.status === 'registered') {
+ this.update_setup_message(__("starting the setup..."));
} else if (r.message.fail !== undefined) {
this.abort_setup(r.message.fail);
}
@@ -238,6 +240,9 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {
if (data.fail_msg) {
this.abort_setup(data.fail_msg);
}
+ if (data.status === 'ok') {
+ this.post_setup_success();
+ }
})
}
diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py
index 5edb44e182..83a5e16009 100755
--- a/frappe/desk/page/setup_wizard/setup_wizard.py
+++ b/frappe/desk/page/setup_wizard/setup_wizard.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: See license.txt
+# License: MIT. See LICENSE
import frappe, json, os
from frappe.utils import strip, cint
@@ -54,9 +54,17 @@ def setup_complete(args):
return {'status': 'ok'}
args = parse_args(args)
-
stages = get_setup_stages(args)
+ is_background_task = frappe.conf.get('trigger_site_setup_in_background')
+ if is_background_task:
+ process_setup_stages.enqueue(stages=stages, user_input=args, is_background_task=True)
+ return {'status': 'registered'}
+ else:
+ return process_setup_stages(stages, args)
+
+@frappe.task()
+def process_setup_stages(stages, user_input, is_background_task=False):
try:
frappe.flags.in_setup_wizard = True
current_task = None
@@ -68,11 +76,16 @@ def setup_complete(args):
current_task = task
task.get('fn')(task.get('args'))
except Exception:
- handle_setup_exception(args)
- return {'status': 'fail', 'fail': current_task.get('fail_msg')}
+ handle_setup_exception(user_input)
+ if not is_background_task:
+ return {'status': 'fail', 'fail': current_task.get('fail_msg')}
+ frappe.publish_realtime('setup_task',
+ {'status': 'fail', "fail_msg": current_task.get('fail_msg')}, user=frappe.session.user)
else:
- run_setup_success(args)
- return {'status': 'ok'}
+ run_setup_success(user_input)
+ if not is_background_task:
+ return {'status': 'ok'}
+ frappe.publish_realtime('setup_task', {"status": 'ok'}, user=frappe.session.user)
finally:
frappe.flags.in_setup_wizard = False
diff --git a/frappe/desk/page/user_profile/user_profile_controller.js b/frappe/desk/page/user_profile/user_profile_controller.js
index c1a89f316e..40b542d5c3 100644
--- a/frappe/desk/page/user_profile/user_profile_controller.js
+++ b/frappe/desk/page/user_profile/user_profile_controller.js
@@ -17,21 +17,15 @@ class UserProfile {
show() {
let route = frappe.get_route();
this.user_id = route[1] || frappe.session.user;
-
- //validate if user
- if (route.length > 1) {
- frappe.dom.freeze(__('Loading user profile') + '...');
- frappe.db.exists('User', this.user_id).then(exists => {
- frappe.dom.unfreeze();
- if (exists) {
- this.make_user_profile();
- } else {
- frappe.msgprint(__('User does not exist'));
- }
- });
- } else {
- frappe.set_route('user-profile', frappe.session.user);
- }
+ frappe.dom.freeze(__('Loading user profile') + '...');
+ frappe.db.exists('User', this.user_id).then(exists => {
+ frappe.dom.unfreeze();
+ if (exists) {
+ this.make_user_profile();
+ } else {
+ frappe.msgprint(__('User does not exist'));
+ }
+ });
}
make_user_profile() {
@@ -74,8 +68,7 @@ class UserProfile {
primary_action_label: __('Go'),
primary_action: ({ user }) => {
dialog.hide();
- this.user_id = user;
- this.make_user_profile();
+ frappe.set_route('user-profile', user);
}
});
dialog.show();
diff --git a/frappe/desk/page/user_profile/user_profile_sidebar.html b/frappe/desk/page/user_profile/user_profile_sidebar.html
index 4a35c6cf9c..9f8889fd03 100644
--- a/frappe/desk/page/user_profile/user_profile_sidebar.html
+++ b/frappe/desk/page/user_profile/user_profile_sidebar.html
@@ -51,10 +51,10 @@
{%=__("Edit Profile") %}
{%=__("User Settings") %}
- {%=__("Leaderboard") %}
-
\ No newline at end of file
+
diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py
index 3c0ebf11c1..97bceeb725 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
import os
@@ -59,6 +59,20 @@ def get_report_doc(report_name):
return doc
+def get_report_result(report, filters):
+ if report.report_type == "Query Report":
+ res = report.execute_query_report(filters)
+
+ elif report.report_type == "Script Report":
+ res = report.execute_script_report(filters)
+
+ elif report.report_type == "Custom Report":
+ ref_report = get_report_doc(report.report_name)
+ res = get_report_result(ref_report, filters)
+
+ return res
+
+@frappe.read_only()
def generate_report_result(report, filters=None, user=None, custom_columns=None):
user = user or frappe.session.user
filters = filters or []
@@ -66,13 +80,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
if filters and isinstance(filters, str):
filters = json.loads(filters)
- res = []
-
- if report.report_type == "Query Report":
- res = report.execute_query_report(filters)
-
- elif report.report_type == "Script Report":
- res = report.execute_script_report(filters)
+ res = get_report_result(report, filters) or []
columns, result, message, chart, report_summary, skip_total_row = ljust_list(res, 6)
columns = [get_column_as_dict(col) for col in columns]
@@ -177,11 +185,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
@@ -389,14 +399,14 @@ def handle_duration_fieldtype_values(result, columns):
return result
-def build_xlsx_data(columns, data, visible_idx, include_indentation):
+def build_xlsx_data(columns, data, visible_idx, include_indentation, ignore_visible_idx=False):
result = [[]]
column_widths = []
for column in data.columns:
if column.get("hidden"):
continue
- result[0].append(column["label"])
+ result[0].append(column.get("label"))
column_width = cint(column.get('width', 0))
# to convert into scale accepted by openpyxl
column_width /= 10
@@ -405,7 +415,7 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation):
# build table from result
for row_idx, row in enumerate(data.result):
# only pick up rows that are visible in the report
- if row_idx in visible_idx:
+ if ignore_visible_idx or row_idx in visible_idx:
row_data = []
if isinstance(row, dict):
for col_idx, column in enumerate(data.columns):
diff --git a/frappe/desk/report/todo/todo.py b/frappe/desk/report/todo/todo.py
index 6bd22b843e..b1e49bc95d 100644
--- a/frappe/desk/report/todo/todo.py
+++ b/frappe/desk/report/todo/todo.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
diff --git a/frappe/desk/report_dump.py b/frappe/desk/report_dump.py
index b2d3ca3443..f57ed97fa5 100644
--- a/frappe/desk/report_dump.py
+++ b/frappe/desk/report_dump.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py
index 1dbc52eb5b..fb150e4bea 100644
--- a/frappe/desk/reportview.py
+++ b/frappe/desk/reportview.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
"""build query for doclistview and return results"""
@@ -14,7 +14,7 @@ from frappe.utils import cstr, format_duration
from frappe.model.base_document import get_controller
-@frappe.whitelist(allow_guest=True)
+@frappe.whitelist()
@frappe.read_only()
def get():
args = get_form_params()
@@ -121,12 +121,14 @@ def validate_filters(data, filters):
def setup_group_by(data):
'''Add columns for aggregated values e.g. count(name)'''
- if data.group_by:
+ if data.group_by and data.aggregate_function:
if data.aggregate_function.lower() not in ('count', 'sum', 'avg'):
frappe.throw(_('Invalid aggregate function'))
if frappe.db.has_column(data.aggregate_on_doctype, data.aggregate_on_field):
data.fields.append('{aggregate_function}(`tab{aggregate_on_doctype}`.`{aggregate_on_field}`) AS _aggregate_column'.format(**data))
+ if data.aggregate_on_field:
+ data.fields.append(f"`tab{data.aggregate_on_doctype}`.`{data.aggregate_on_field}`")
else:
raise_invalid_field(data.aggregate_on_field)
@@ -178,15 +180,16 @@ def update_wildcard_field_param(data):
def clean_params(data):
- data.pop('cmd', None)
- data.pop('data', None)
- data.pop('ignore_permissions', None)
- data.pop('view', None)
- data.pop('user', None)
-
- if "csrf_token" in data:
- del data["csrf_token"]
-
+ for param in (
+ "cmd",
+ "data",
+ "ignore_permissions",
+ "view",
+ "user",
+ "csrf_token",
+ "join"
+ ):
+ data.pop(param, None)
def parse_json(data):
if isinstance(data.get("filters"), str):
@@ -212,11 +215,13 @@ def get_parenttype_and_fieldname(field, data):
return parenttype, fieldname
-def compress(data, args = {}):
+def compress(data, args=None):
"""separate keys and values"""
from frappe.desk.query_report import add_total_row
if not data: return data
+ if args is None:
+ args = {}
values = []
keys = list(data[0])
for row in data:
@@ -421,15 +426,20 @@ def delete_bulk(doctype, items):
@frappe.whitelist()
@frappe.read_only()
-def get_sidebar_stats(stats, doctype, filters=[]):
+def get_sidebar_stats(stats, doctype, filters=None):
+ if filters is None:
+ filters = []
return {"stats": get_stats(stats, doctype, filters)}
@frappe.whitelist()
@frappe.read_only()
-def get_stats(stats, doctype, filters=[]):
+def get_stats(stats, doctype, filters=None):
"""get tag info"""
import json
+
+ if filters is None:
+ filters = []
tags = json.loads(stats)
if filters:
filters = json.loads(filters)
@@ -478,12 +488,11 @@ def get_stats(stats, doctype, filters=[]):
return stats
@frappe.whitelist()
-def get_filter_dashboard_data(stats, doctype, filters=[]):
+def get_filter_dashboard_data(stats, doctype, filters=None):
"""get tags info"""
import json
tags = json.loads(stats)
- if filters:
- filters = json.loads(filters)
+ filters = json.loads(filters or [])
stats = {}
columns = frappe.db.get_table_columns(doctype)
diff --git a/frappe/desk/search.py b/frappe/desk/search.py
index 6d259e3a3d..23b5c47fd1 100644
--- a/frappe/desk/search.py
+++ b/frappe/desk/search.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
# Search
import frappe, json
@@ -299,6 +299,7 @@ def get_users_for_mentions():
'name': ['not in', ('Administrator', 'Guest')],
'allowed_in_mentions': True,
'user_type': 'System User',
+ 'enabled': True,
})
def get_user_groups():
diff --git a/frappe/desk/treeview.py b/frappe/desk/treeview.py
index 66acde4cb2..f40c135653 100644
--- a/frappe/desk/treeview.py
+++ b/frappe/desk/treeview.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
@@ -69,13 +69,11 @@ def make_tree_args(**kwarg):
doctype = kwarg['doctype']
parent_field = 'parent_' + doctype.lower().replace(' ', '_')
- name_field = kwarg.get('name_field', doctype.lower().replace(' ', '_') + '_name')
if kwarg['is_root'] == 'false': kwarg['is_root'] = False
if kwarg['is_root'] == 'true': kwarg['is_root'] = True
kwarg.update({
- name_field: kwarg[name_field],
parent_field: kwarg.get("parent") or kwarg.get(parent_field)
})
diff --git a/frappe/desk/utils.py b/frappe/desk/utils.py
index 01b47ac106..5908277386 100644
--- a/frappe/desk/utils.py
+++ b/frappe/desk/utils.py
@@ -1,5 +1,5 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/email/__init__.py b/frappe/email/__init__.py
index 3fb539398a..79dec977b7 100644
--- a/frappe/email/__init__.py
+++ b/frappe/email/__init__.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
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 f30279e308..34728375cd 100644
--- a/frappe/email/doctype/auto_email_report/auto_email_report.py
+++ b/frappe/email/doctype/auto_email_report/auto_email_report.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import calendar
from datetime import timedelta
@@ -13,6 +13,7 @@ from frappe.utils import (format_time, get_link_to_form, get_url_to_report,
from frappe.model.naming import append_number_if_name_exists
from frappe.utils.csvutils import to_csv
from frappe.utils.xlsxutils import make_xlsx
+from frappe.desk.query_report import build_xlsx_data
max_reports_per_user = frappe.local.conf.max_reports_per_user or 3
@@ -99,13 +100,21 @@ class AutoEmailReport(Document):
return self.get_html_table(columns, data)
elif self.format == 'XLSX':
- spreadsheet_data = self.get_spreadsheet_data(columns, data)
- xlsx_file = make_xlsx(spreadsheet_data, "Auto Email Report")
+ report_data = frappe._dict()
+ report_data['columns'] = columns
+ report_data['result'] = data
+
+ xlsx_data, column_widths = build_xlsx_data(columns, report_data, [], 1, ignore_visible_idx=True)
+ xlsx_file = make_xlsx(xlsx_data, "Auto Email Report", column_widths=column_widths)
return xlsx_file.getvalue()
elif self.format == 'CSV':
- spreadsheet_data = self.get_spreadsheet_data(columns, data)
- return to_csv(spreadsheet_data)
+ report_data = frappe._dict()
+ report_data['columns'] = columns
+ report_data['result'] = data
+
+ xlsx_data, column_widths = build_xlsx_data(columns, report_data, [], 1, ignore_visible_idx=True)
+ return to_csv(xlsx_data)
else:
frappe.throw(_('Invalid Output Format'))
@@ -126,18 +135,6 @@ class AutoEmailReport(Document):
'edit_report_settings': get_link_to_form('Auto Email Report', self.name)
})
- @staticmethod
- def get_spreadsheet_data(columns, data):
- out = [[_(df.label) for df in columns], ]
- for row in data:
- new_row = []
- out.append(new_row)
- for df in columns:
- if df.fieldname not in row: continue
- new_row.append(frappe.format(row[df.fieldname], df, row))
-
- return out
-
def get_file_name(self):
return "{0}.{1}".format(self.report.replace(" ", "-").replace("/", "-"), self.format.lower())
@@ -245,14 +242,17 @@ 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):
+ if not row.get(col.fieldname):
+ continue
+
+ if col.fieldtype == "Link":
+ if col.options and col.options != "Currency":
row[col.fieldname] = get_link_to_form(col.options, row[col.fieldname])
elif col.fieldtype == "Dynamic Link":
- if col.options and row.get(col.fieldname) and row.get(col.options):
+ if col.options 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):
- doc = frappe.get_doc(col.parent, doc_name) if doc_name else None
+ elif col.fieldtype == "Currency":
+ doc = frappe.get_doc(col.parent, doc_name) if doc_name and col.parent 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
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 211a141ec0..559adfbe1a 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,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
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 a04f8ef4c2..97f8237736 100644
--- a/frappe/email/doctype/document_follow/document_follow.py
+++ b/frappe/email/doctype/document_follow/document_follow.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from frappe.model.document import Document
diff --git a/frappe/email/doctype/document_follow/test_document_follow.py b/frappe/email/doctype/document_follow/test_document_follow.py
index 456c0931f8..050add65e9 100644
--- a/frappe/email/doctype/document_follow/test_document_follow.py
+++ b/frappe/email/doctype/document_follow/test_document_follow.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
import frappe.desk.form.document_follow as document_follow
diff --git a/frappe/email/doctype/email_account/email_account.js b/frappe/email/doctype/email_account/email_account.js
index 83896e0af7..54f0d2372d 100644
--- a/frappe/email/doctype/email_account/email_account.js
+++ b/frappe/email/doctype/email_account/email_account.js
@@ -109,6 +109,15 @@ frappe.ui.form.on("Email Account", {
onload: function(frm) {
frm.set_df_property("append_to", "only_select", true);
frm.set_query("append_to", "frappe.email.doctype.email_account.email_account.get_append_to");
+ frm.set_query("append_to", "imap_folder", function() {
+ return {
+ query: "frappe.email.doctype.email_account.email_account.get_append_to"
+ };
+ });
+ if (frm.doc.__islocal) {
+ frm.add_child("imap_folder", {"folder_name": "INBOX"});
+ frm.refresh_field("imap_folder");
+ }
},
refresh: function(frm) {
@@ -117,7 +126,7 @@ frappe.ui.form.on("Email Account", {
frm.events.notify_if_unreplied(frm);
frm.events.show_gmail_message_for_less_secure_apps(frm);
- if(frappe.route_flags.delete_user_from_locals && frappe.route_flags.linked_user) {
+ if (frappe.route_flags.delete_user_from_locals && frappe.route_flags.linked_user) {
delete frappe.route_flags.delete_user_from_locals;
delete locals['User'][frappe.route_flags.linked_user];
}
@@ -125,7 +134,7 @@ frappe.ui.form.on("Email Account", {
show_gmail_message_for_less_secure_apps: function(frm) {
frm.dashboard.clear_headline();
- if(frm.doc.service==="GMail") {
+ if (frm.doc.service==="GMail") {
frm.dashboard.set_headline_alert('Gmail will only work if you allow access for less secure \
apps in Gmail settings. Read this for details');
@@ -137,8 +146,8 @@ frappe.ui.form.on("Email Account", {
frm.events.update_domain(frm);
},
- update_domain: function(frm){
- if (!frm.doc.email_id && !frm.doc.service){
+ update_domain: function(frm) {
+ if (!frm.doc.email_id && !frm.doc.service) {
return;
}
@@ -148,28 +157,16 @@ frappe.ui.form.on("Email Account", {
args: {
"email_id": frm.doc.email_id
},
- callback: function (r) {
+ callback: function(r) {
if (r.message) {
frm.events.set_domain_fields(frm, r.message);
- } else {
- frm.set_value("domain", "");
- frappe.confirm(__('Email Domain not configured for this account, Create one?'),
- function () {
- frappe.model.with_doctype("Email Domain", function() {
- frappe.route_options = { email_id: frm.doc.email_id };
- frappe.route_flags.return_to_email_account = 1;
- var doc = frappe.model.get_new_doc("Email Domain");
- frappe.set_route("Form", "Email Domain", doc.name);
- });
- }
- );
}
}
});
},
set_domain_fields: function(frm, args) {
- if(!args){
+ if (!args) {
args = frappe.route_flags.set_domain_values? frappe.route_options: {};
}
@@ -184,10 +181,8 @@ frappe.ui.form.on("Email Account", {
email_sync_option: function(frm) {
// confirm if the ALL sync option is selected
- if(frm.doc.email_sync_option == "ALL"){
- var msg = __("You are selecting Sync Option as ALL, It will resync all \
- read as well as unread message from server. This may also cause the duplication\
- of Communication (emails).");
+ if (frm.doc.email_sync_option == "ALL") {
+ var msg = __("You are selecting Sync Option as ALL, It will resync all read as well as unread message from server. This may also cause the duplication of Communication (emails).");
frappe.confirm(msg, null, function() {
frm.set_value("email_sync_option", "UNSEEN");
});
@@ -196,8 +191,7 @@ frappe.ui.form.on("Email Account", {
warn_autoreply_on_incoming: function(frm) {
if (frm.doc.enable_incoming && frm.doc.enable_auto_reply && frm.doc.__islocal) {
- var msg = __("Enabling auto reply on an incoming email account will send automated replies \
- to all the synchronized emails. Do you wish to continue?");
+ var msg = __("Enabling auto reply on an incoming email account will send automated replies to all the synchronized emails. Do you wish to continue?");
frappe.confirm(msg, null, function() {
frm.set_value("enable_auto_reply", 0);
frappe.show_alert({message: __("Disabled Auto Reply"), indicator: "blue"});
diff --git a/frappe/email/doctype/email_account/email_account.json b/frappe/email/doctype/email_account/email_account.json
index 6d811b801f..65053bab3d 100644
--- a/frappe/email/doctype/email_account/email_account.json
+++ b/frappe/email/doctype/email_account/email_account.json
@@ -7,30 +7,36 @@
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
+ "account_section",
"email_id",
- "login_id_is_different",
- "login_id",
+ "email_account_name",
+ "column_break_3",
+ "domain",
+ "service",
+ "authentication_column",
"password",
"awaiting_password",
"ascii_encode_password",
- "email_account_name",
- "email_settings",
- "domain",
- "service",
+ "column_break_10",
+ "login_id_is_different",
+ "login_id",
"mailbox_settings",
"enable_incoming",
- "use_imap",
- "email_server",
- "use_ssl",
- "append_emails_to_sent_folder",
- "incoming_port",
- "attachment_limit",
- "append_to",
"default_incoming",
+ "use_imap",
+ "use_ssl",
+ "email_server",
+ "incoming_port",
+ "column_break_18",
+ "attachment_limit",
"email_sync_option",
"initial_sync_count",
- "create_contact",
+ "section_break_25",
+ "imap_folder",
"section_break_12",
+ "append_emails_to_sent_folder",
+ "append_to",
+ "create_contact",
"enable_automatic_linking",
"section_break_13",
"notify_if_unreplied",
@@ -42,6 +48,7 @@
"use_tls",
"use_ssl_for_outgoing",
"smtp_port",
+ "column_break_38",
"default_outgoing",
"always_use_account_email_id_as_sender",
"always_use_account_name_as_sender_name",
@@ -80,7 +87,7 @@
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
- "label": "Use Different Email Login ID"
+ "label": "Use different login"
},
{
"depends_on": "login_id_is_different",
@@ -122,12 +129,6 @@
"label": "Email Account Name",
"unique": 1
},
- {
- "fieldname": "email_settings",
- "fieldtype": "Section Break",
- "hide_days": 1,
- "hide_seconds": 1
- },
{
"depends_on": "eval:!doc.service",
"fieldname": "domain",
@@ -136,7 +137,7 @@
"hide_seconds": 1,
"in_list_view": 1,
"in_standard_filter": 1,
- "label": "Domain",
+ "label": "Domain (optional)",
"options": "Email Domain"
},
{
@@ -145,18 +146,18 @@
"fieldtype": "Select",
"hide_days": 1,
"hide_seconds": 1,
- "label": "Service",
+ "label": "Service (optional)",
"options": "\nGMail\nSendgrid\nSparkPost\nYahoo Mail\nOutlook.com\nYandex.Mail"
},
{
"fieldname": "mailbox_settings",
"fieldtype": "Section Break",
"hide_days": 1,
- "hide_seconds": 1
+ "hide_seconds": 1,
+ "label": "Incoming (POP/IMAP) Settings"
},
{
"default": "0",
- "description": "Check this to pull emails from your mailbox",
"fieldname": "enable_incoming",
"fieldtype": "Check",
"hide_days": 1,
@@ -205,7 +206,7 @@
"label": "Attachment Limit (MB)"
},
{
- "depends_on": "enable_incoming",
+ "depends_on": "eval: doc.enable_incoming && !doc.use_imap",
"description": "Append as communication against this DocType (must have fields, \"Status\", \"Subject\")",
"fieldname": "append_to",
"fieldtype": "Link",
@@ -227,7 +228,7 @@
},
{
"default": "UNSEEN",
- "depends_on": "eval: doc.enable_incoming",
+ "depends_on": "eval: doc.enable_incoming && doc.use_imap",
"fieldname": "email_sync_option",
"fieldtype": "Select",
"hide_days": 1,
@@ -237,6 +238,7 @@
},
{
"default": "250",
+ "depends_on": "eval: doc.enable_incoming && doc.use_imap",
"description": "Total number of emails to sync in initial sync process ",
"fieldname": "initial_sync_count",
"fieldtype": "Select",
@@ -248,7 +250,7 @@
{
"depends_on": "enable_incoming",
"fieldname": "section_break_13",
- "fieldtype": "Section Break",
+ "fieldtype": "Column Break",
"hide_days": 1,
"hide_seconds": 1
},
@@ -282,7 +284,8 @@
"fieldname": "outgoing_mail_settings",
"fieldtype": "Section Break",
"hide_days": 1,
- "hide_seconds": 1
+ "hide_seconds": 1,
+ "label": "Outgoing (SMTP) Settings"
},
{
"default": "0",
@@ -336,22 +339,20 @@
{
"default": "0",
"depends_on": "enable_outgoing",
- "description": "Uses the Email Address mentioned in this Account as the Sender for all emails sent using this Account. ",
"fieldname": "always_use_account_email_id_as_sender",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
- "label": "Always use Account's Email Address as Sender"
+ "label": "Always use this email address as sender address"
},
{
"default": "0",
"depends_on": "enable_outgoing",
- "description": "Uses the Email Address Name mentioned in this Account as the Sender's Name for all emails sent using this Account.",
"fieldname": "always_use_account_name_as_sender_name",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
- "label": "Always use Account's Name as Sender's Name"
+ "label": "Always use this name as sender name"
},
{
"default": "1",
@@ -379,10 +380,13 @@
"label": "Disable SMTP server authentication"
},
{
+ "collapsible": 1,
+ "collapsible_depends_on": "add_signature",
"fieldname": "signature_section",
"fieldtype": "Section Break",
"hide_days": 1,
- "hide_seconds": 1
+ "hide_seconds": 1,
+ "label": "Signature"
},
{
"default": "0",
@@ -401,10 +405,13 @@
"label": "Signature"
},
{
+ "collapsible": 1,
+ "collapsible_depends_on": "enable_auto_reply",
"fieldname": "auto_reply",
"fieldtype": "Section Break",
"hide_days": 1,
- "hide_seconds": 1
+ "hide_seconds": 1,
+ "label": "Auto Reply"
},
{
"default": "0",
@@ -424,17 +431,20 @@
"label": "Auto Reply Message"
},
{
+ "collapsible": 1,
+ "collapsible_depends_on": "eval:frappe.utils.html2text(doc.footer || '')!=''",
"fieldname": "set_footer",
"fieldtype": "Section Break",
"hide_days": 1,
- "hide_seconds": 1
+ "hide_seconds": 1,
+ "label": "Footer"
},
{
"fieldname": "footer",
"fieldtype": "Text Editor",
"hide_days": 1,
"hide_seconds": 1,
- "label": "Footer"
+ "label": "Footer Content"
},
{
"fieldname": "uidvalidity",
@@ -477,7 +487,8 @@
"fieldname": "section_break_12",
"fieldtype": "Section Break",
"hide_days": 1,
- "hide_seconds": 1
+ "hide_seconds": 1,
+ "label": "Document Linking"
},
{
"default": "0",
@@ -527,15 +538,54 @@
"fieldname": "brand_logo",
"fieldtype": "Attach Image",
"label": "Brand Logo"
+ },
+ {
+ "fieldname": "authentication_column",
+ "fieldtype": "Section Break",
+ "label": "Authentication"
+ },
+ {
+ "fieldname": "column_break_10",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_18",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_38",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "account_section",
+ "fieldtype": "Section Break",
+ "label": "Account"
+ },
+ {
+ "depends_on": "eval: doc.use_imap && doc.enable_incoming",
+ "fieldname": "imap_folder",
+ "fieldtype": "Table",
+ "label": "IMAP Folder",
+ "options": "IMAP Folder"
+ },
+ {
+ "fieldname": "section_break_25",
+ "fieldtype": "Section Break",
+ "label": "IMAP Details"
}
],
"icon": "fa fa-inbox",
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-01-21 10:05:24.820597",
+ "modified": "2021-11-30 09:03:25.728637",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Account",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -554,4 +604,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index ecd59f42bb..ef1d49302f 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import email.utils
import functools
import imaplib
@@ -67,6 +67,10 @@ class EmailAccount(Document):
else:
self.login_id = None
+ # validate the imap settings
+ if self.enable_incoming and self.use_imap and len(self.imap_folder) <= 0:
+ frappe.throw(_("You need to set one IMAP folder for {0}").format(frappe.bold(self.email_id)))
+
duplicate_email_account = frappe.get_all("Email Account", filters={
"email_id": self.email_id,
"name": ("!=", self.name)
@@ -100,10 +104,11 @@ class EmailAccount(Document):
for e in self.get_unreplied_notification_emails():
validate_email_address(e, True)
- if self.enable_incoming and self.append_to:
- valid_doctypes = [d[0] for d in get_append_to()]
- if self.append_to not in valid_doctypes:
- frappe.throw(_("Append To can be one of {0}").format(comma_or(valid_doctypes)))
+ for folder in self.imap_folder:
+ if self.enable_incoming and folder.append_to:
+ valid_doctypes = [d[0] for d in get_append_to()]
+ if folder.append_to not in valid_doctypes:
+ frappe.throw(_("Append To can be one of {0}").format(comma_or(valid_doctypes)))
def validate_smtp_conn(self):
if not self.smtp_server:
@@ -137,8 +142,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,
@@ -179,13 +182,13 @@ class EmailAccount(Document):
return None
args = frappe._dict({
+ "email_account_name": self.email_account_name,
"email_account": self.name,
"host": self.email_server,
"use_ssl": self.use_ssl,
"username": getattr(self, "login_id", None) or self.email_id,
"use_imap": self.use_imap,
"email_sync_rule": email_sync_rule,
- "uid_validity": self.uidvalidity,
"incoming_port": get_port(self),
"initial_sync_count": self.initial_sync_count or 100
})
@@ -459,6 +462,14 @@ class EmailAccount(Document):
"""retrive and return inbound mails.
"""
+ mails = []
+
+ def process_mail(messages):
+ 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))
+
if frappe.local.flags.in_test:
return [InboundMail(msg, self) for msg in test_mails or []]
@@ -468,17 +479,23 @@ class EmailAccount(Document):
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 {}
+ if self.use_imap:
+ # process all given imap folder
+ for folder in self.imap_folder:
+ email_server.select_imap_folder(folder.folder_name)
+ email_server.settings['uid_validity'] = folder.uidvalidity
+ messages = email_server.get_messages(folder=folder.folder_name) or {}
+ process_mail(messages)
+ else:
+ # process the pop3 account
+ messages = email_server.get_messages() or {}
+ process_mail(messages)
+ # close connection to mailserver
+ email_server.logout()
except Exception:
frappe.log_error(title=_("Error while connecting to email account {0}").format(self.name))
return []
- 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))
-
return mails
def handle_bad_emails(self, uid, raw, reason):
@@ -532,9 +549,11 @@ 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
+ Communication = frappe.qb.DocType("Communication")
+ frappe.qb.update(Communication) \
+ .set(Communication.email_account, "") \
+ .where(Communication.email_account == self.name).run()
- frappe.db.sql("update `tabCommunication` set email_account='' where email_account=%s", self.name)
remove_user_email_inbox(email_account=self.name)
def after_rename(self, old, new, merge=False):
@@ -551,23 +570,26 @@ class EmailAccount(Document):
else:
return self.email_sync_option or "UNSEEN"
- def mark_emails_as_read_unread(self):
+ def mark_emails_as_read_unread(self, email_server=None, folder_name="INBOX"):
""" mark Email Flag Queue of self.email_account mails as read"""
-
if not self.use_imap:
return
- flags = frappe.db.sql("""select name, communication, uid, action from
- `tabEmail Flag Queue` where is_completed=0 and email_account={email_account}
- """.format(email_account=frappe.db.escape(self.name)), as_dict=True)
+ EmailFlagQ = frappe.qb.DocType("Email Flag Queue")
+ flags = (
+ frappe.qb.from_(EmailFlagQ)
+ .select(EmailFlagQ.name, EmailFlagQ.communication, EmailFlagQ.uid, EmailFlagQ.action)
+ .where(EmailFlagQ.is_completed == 0)
+ .where(EmailFlagQ.email_account == frappe.db.escape(self.name))
+ ).run(as_dict=True)
uid_list = { flag.get("uid", None): flag.get("action", "Read") for flag in flags }
if flags and uid_list:
- email_server = self.get_incoming_server()
+ if not email_server:
+ email_server = self.get_incoming_server()
if not email_server:
return
-
- email_server.update_flag(uid_list=uid_list)
+ email_server.update_flag(folder_name, uid_list=uid_list)
# mark communication as read
docnames = ",".join("'%s'"%flag.get("communication") for flag in flags \
@@ -580,16 +602,20 @@ class EmailAccount(Document):
self.set_communication_seen_status(docnames, seen=0)
docnames = ",".join([ "'%s'"%flag.get("name") for flag in flags ])
- frappe.db.sql(""" update `tabEmail Flag Queue` set is_completed=1
- where name in ({docnames})""".format(docnames=docnames))
+
+ EmailFlagQueue = frappe.qb.DocType("Email Flag Queue")
+ frappe.qb.update(EmailFlagQueue) \
+ .set(EmailFlagQueue.is_completed, 1) \
+ .where(EmailFlagQueue.name.isin(docnames)).run()
def set_communication_seen_status(self, docnames, seen=0):
""" mark Email Flag Queue of self.email_account mails as read"""
if not docnames:
return
-
- frappe.db.sql(""" update `tabCommunication` set seen={seen}
- where name in ({docnames})""".format(docnames=docnames, seen=seen))
+ Communication = frappe.qb.from_("Communication")
+ frappe.qb.update(Communication) \
+ .set(Communication.seen == seen) \
+ .where(Communication.name.isin(docnames)).run()
def check_automatic_linking_email_account(self):
if self.enable_automatic_linking:
@@ -655,15 +681,19 @@ def test_internet(host="8.8.8.8", port=53, timeout=3):
def notify_unreplied():
"""Sends email notifications if there are unreplied Communications
and `notify_if_unreplied` is set as true."""
-
for email_account in frappe.get_all("Email Account", "name", filters={"enable_incoming": 1, "notify_if_unreplied": 1}):
email_account = frappe.get_doc("Email Account", email_account.name)
- if email_account.append_to:
+ if email_account.use_imap:
+ append_to = [folder.get("append_to") for folder in email_account.imap_folder]
+ else:
+ append_to = email_account.append_to
+
+ if append_to:
# get open communications younger than x mins, for given doctype
for comm in frappe.get_all("Communication", "name", filters=[
{"sent_or_received": "Received"},
- {"reference_doctype": email_account.append_to},
+ {"reference_doctype": ("in", append_to)},
{"unread_notification_sent": 0},
{"email_account":email_account.name},
{"creation": ("<", datetime.now() - timedelta(seconds = (email_account.unreplied_for_mins or 30) * 60))},
@@ -706,9 +736,6 @@ def pull_from_email_account(email_account):
email_account = frappe.get_doc("Email Account", email_account)
email_account.receive()
- # mark Email Flag Queue mail as read
- email_account.mark_emails_as_read_unread()
-
def get_max_email_uid(email_account):
# get maximum uid of emails
max_uid = 1
@@ -724,3 +751,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:
+ UserEmail = frappe.qb.from_("User Email")
+ frappe.qb.update(UserEmail) \
+ .set(UserEmail.awaiting_password == awaiting_password or 0) \
+ .set(UserEmail.enable_outgoing == enable_outgoing) \
+ .where(UserEmail.email_account == email_account).run()
+
+ 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
diff --git a/frappe/email/doctype/email_account/test_email_account.py b/frappe/email/doctype/email_account/test_email_account.py
index 35cacac45a..6d26f9f070 100644
--- a/frappe/email/doctype/email_account/test_email_account.py
+++ b/frappe/email/doctype/email_account/test_email_account.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import os
import email
@@ -25,6 +25,7 @@ class TestEmailAccount(unittest.TestCase):
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)
+ email_account.db_set("use_imap", 1)
@classmethod
def tearDownClass(cls):
@@ -34,8 +35,8 @@ class TestEmailAccount(unittest.TestCase):
def setUp(self):
frappe.flags.mute_emails = False
frappe.flags.sent_mail = None
- frappe.db.sql('delete from `tabEmail Queue`')
- frappe.db.sql('delete from `tabUnhandled Email`')
+ frappe.db.delete("Email Queue")
+ frappe.db.delete("Unhandled Email")
def get_test_mail(self, fname):
with open(os.path.join(os.path.dirname(__file__), "test_mails", fname), "r") as f:
@@ -60,7 +61,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"}))
@@ -183,7 +184,7 @@ class TestEmailAccount(unittest.TestCase):
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()
@@ -229,6 +230,22 @@ class TestEmailAccount(unittest.TestCase):
email_account.handle_bad_emails(uid=-1, raw=mail_content, reason="Testing")
self.assertTrue(frappe.db.get_value("Unhandled Email", {'message_id': message_id}))
+ def test_imap_folder(self):
+ # assert tests if imap_folder >= 1 and imap is checked
+ email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
+
+ self.assertTrue(email_account.use_imap)
+ self.assertTrue(email_account.enable_incoming)
+ self.assertTrue(len(email_account.imap_folder) > 0)
+
+ def test_imap_folder_missing(self):
+ # Test the Exception in validate() that verifies the imap_folder list
+ email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
+ email_account.imap_folder = []
+
+ with self.assertRaises(Exception):
+ email_account.validate()
+
class TestInboundMail(unittest.TestCase):
@classmethod
def setUpClass(cls):
@@ -242,8 +259,8 @@ class TestInboundMail(unittest.TestCase):
def setUp(self):
cleanup()
- frappe.db.sql('delete from `tabEmail Queue`')
- frappe.db.sql('delete from `tabToDo`')
+ 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:
diff --git a/frappe/email/doctype/email_account/test_records.json b/frappe/email/doctype/email_account/test_records.json
index 15ca2a886e..450895d7a6 100644
--- a/frappe/email/doctype/email_account/test_records.json
+++ b/frappe/email/doctype/email_account/test_records.json
@@ -4,7 +4,6 @@
"is_global": 1,
"doctype": "Email Account",
"domain":"example.com",
- "append_to": "ToDo",
"email_account_name": "_Test Email Account 1",
"enable_outgoing": 1,
"smtp_server": "test.example.com",
@@ -20,6 +19,8 @@
"send_notification_to": "test_unreplied@example.com",
"pop3_server": "pop.test.example.com",
"no_remaining":"0",
+ "append_to": "ToDo",
+ "imap_folder": [{"folder_name": "INBOX", "append_to": "ToDo"}],
"track_email_status": 1
},
{
diff --git a/frappe/email/doctype/email_domain/email_domain.py b/frappe/email/doctype/email_domain/email_domain.py
index 0856549eb7..1611d32351 100644
--- a/frappe/email/doctype/email_domain/email_domain.py
+++ b/frappe/email/doctype/email_domain/email_domain.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
diff --git a/frappe/email/doctype/email_domain/test_email_domain.py b/frappe/email/doctype/email_domain/test_email_domain.py
index 8607151ca8..1064c7684a 100644
--- a/frappe/email/doctype/email_domain/test_email_domain.py
+++ b/frappe/email/doctype/email_domain/test_email_domain.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
from frappe.test_runner import make_test_objects
diff --git a/frappe/email/doctype/email_flag_queue/email_flag_queue.json b/frappe/email/doctype/email_flag_queue/email_flag_queue.json
index 165e8f9ea9..14b1ec4f53 100644
--- a/frappe/email/doctype/email_flag_queue/email_flag_queue.json
+++ b/frappe/email/doctype/email_flag_queue/email_flag_queue.json
@@ -1,213 +1,67 @@
{
- "allow_copy": 1,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2016-04-20 15:29:39.785172",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 0,
+ "actions": [],
+ "allow_copy": 1,
+ "creation": "2016-04-20 15:29:39.785172",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "is_completed",
+ "communication",
+ "action",
+ "email_account",
+ "uid"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "is_completed",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Is Completed",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "0",
+ "fieldname": "is_completed",
+ "fieldtype": "Check",
+ "label": "Is Completed",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "communication",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Communication",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "communication",
+ "fieldtype": "Data",
+ "label": "Communication"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "action",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Action",
- "length": 0,
- "no_copy": 0,
- "options": "Read\nUnread",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "action",
+ "fieldtype": "Select",
+ "label": "Action",
+ "options": "Read\nUnread"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "email_account",
- "fieldtype": "Data",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Email Account",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "email_account",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Email Account"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "uid",
- "fieldtype": "Data",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "UID",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "uid",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "UID"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 1,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2017-09-20 15:27:12.142079",
- "modified_by": "Administrator",
- "module": "Email",
- "name": "Email Flag Queue",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "in_create": 1,
+ "links": [],
+ "modified": "2021-11-30 09:51:34.489932",
+ "modified_by": "Administrator",
+ "module": "Email",
+ "name": "Email Flag Queue",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 0,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 0
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 0,
- "track_seen": 0
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC"
}
\ No newline at end of file
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 9bb30f08b2..886cf3c24b 100644
--- a/frappe/email/doctype/email_flag_queue/email_flag_queue.py
+++ b/frappe/email/doctype/email_flag_queue/email_flag_queue.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
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 d09b823ce6..b0e17b3b85 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,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/email/doctype/email_group/email_group.py b/frappe/email/doctype/email_group/email_group.py
index 2679353edf..ad52d9a9ec 100755
--- a/frappe/email/doctype/email_group/email_group.py
+++ b/frappe/email/doctype/email_group/email_group.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
diff --git a/frappe/email/doctype/email_group/test_email_group.py b/frappe/email/doctype/email_group/test_email_group.py
index 3e894118df..06341c128e 100644
--- a/frappe/email/doctype/email_group/test_email_group.py
+++ b/frappe/email/doctype/email_group/test_email_group.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
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 1f9303b83e..a9fd26f710 100644
--- a/frappe/email/doctype/email_group_member/email_group_member.py
+++ b/frappe/email/doctype/email_group_member/email_group_member.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
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 829d686400..de006dccb9 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,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
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 e1e332f978..d89a3d83be 100644
--- a/frappe/email/doctype/email_queue/email_queue.py
+++ b/frappe/email/doctype/email_queue/email_queue.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import traceback
import json
@@ -18,7 +18,7 @@ from frappe import _, safe_encode, task
from frappe.model.document import Document
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.utils import cint, split_emails, add_days, nowdate, cstr, get_hook_method
from frappe.email.doctype.email_account.email_account import EmailAccount
@@ -121,9 +121,13 @@ class EmailQueue(Document):
continue
message = ctx.build_message(recipient.recipient)
- if not frappe.flags.in_test:
- ctx.smtp_session.sendmail(from_addr=self.sender, to_addrs=recipient.recipient, msg=message)
- ctx.add_to_sent_list(recipient)
+ method = get_hook_method('override_email_send')
+ if method:
+ method(self, self.sender, recipient.recipient, message)
+ else:
+ if not frappe.flags.in_test:
+ ctx.smtp_session.sendmail(from_addr=self.sender, to_addrs=recipient.recipient, msg=message)
+ ctx.add_to_sent_list(recipient)
if frappe.flags.in_test:
frappe.flags.sent_mail = message
@@ -283,9 +287,14 @@ class SendMailContext:
if attachment.get('fcontent'):
continue
- fid = attachment.get("fid")
- if fid:
- _file = frappe.get_doc("File", fid)
+ file_filters = {}
+ if attachment.get('fid'):
+ file_filters['name'] = attachment.get('fid')
+ elif attachment.get('file_url'):
+ file_filters['file_url'] = attachment.get('file_url')
+
+ if file_filters:
+ _file = frappe.get_doc("File", file_filters)
fcontent = _file.get_content()
attachment.update({
'fname': _file.file_name,
@@ -293,6 +302,7 @@ class SendMailContext:
'parent': message_obj
})
attachment.pop("fid", None)
+ attachment.pop("file_url", None)
add_attachment(**attachment)
elif attachment.get("print_format_attachment") == 1:
@@ -503,7 +513,7 @@ class QueueBuilder:
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'):
+ if att.get('fid') or att.get('file_url'):
attachments.append(att)
elif att.get("print_format_attachment") == 1:
if not att.get('lang', None):
diff --git a/frappe/email/doctype/email_queue/test_email_queue.py b/frappe/email/doctype/email_queue/test_email_queue.py
index b76d6347b9..8ebcb68a38 100644
--- a/frappe/email/doctype/email_queue/test_email_queue.py
+++ b/frappe/email/doctype/email_queue/test_email_queue.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
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 055bdb3fc1..95b8593c4c 100644
--- a/frappe/email/doctype/email_queue_recipient/email_queue_recipient.py
+++ b/frappe/email/doctype/email_queue_recipient/email_queue_recipient.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
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 9807724ef1..b2a4be5421 100644
--- a/frappe/email/doctype/email_rule/email_rule.py
+++ b/frappe/email/doctype/email_rule/email_rule.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
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 b2213f7405..eef5448e57 100644
--- a/frappe/email/doctype/email_rule/test_email_rule.py
+++ b/frappe/email/doctype/email_rule/test_email_rule.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
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 4711451fd2..c51c46d72d 100644
--- a/frappe/email/doctype/email_template/email_template.py
+++ b/frappe/email/doctype/email_template/email_template.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe, json
from frappe.model.document import Document
diff --git a/frappe/email/doctype/email_template/test_email_template.py b/frappe/email/doctype/email_template/test_email_template.py
index 5a9ee969c6..a92ee9f9c3 100644
--- a/frappe/email/doctype/email_template/test_email_template.py
+++ b/frappe/email/doctype/email_template/test_email_template.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
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 6c47d8c538..d2ee828a55 100644
--- a/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py
+++ b/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/email/doctype/email_unsubscribe/test_email_unsubscribe.py b/frappe/email/doctype/email_unsubscribe/test_email_unsubscribe.py
index 602840fe3b..fdea802fdf 100644
--- a/frappe/email/doctype/email_unsubscribe/test_email_unsubscribe.py
+++ b/frappe/email/doctype/email_unsubscribe/test_email_unsubscribe.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/chat/doctype/chat_room_user/__init__.py b/frappe/email/doctype/imap_folder/__init__.py
similarity index 100%
rename from frappe/chat/doctype/chat_room_user/__init__.py
rename to frappe/email/doctype/imap_folder/__init__.py
diff --git a/frappe/email/doctype/imap_folder/imap_folder.json b/frappe/email/doctype/imap_folder/imap_folder.json
new file mode 100644
index 0000000000..bab50dea39
--- /dev/null
+++ b/frappe/email/doctype/imap_folder/imap_folder.json
@@ -0,0 +1,53 @@
+{
+ "actions": [],
+ "creation": "2021-09-21 11:38:13.521979",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "folder_name",
+ "append_to",
+ "uidvalidity",
+ "uidnext"
+ ],
+ "fields": [
+ {
+ "fieldname": "folder_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Folder Name",
+ "reqd": 1
+ },
+ {
+ "fieldname": "append_to",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Append To",
+ "options": "DocType"
+ },
+ {
+ "fieldname": "uidvalidity",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "UIDVALIDITY"
+ },
+ {
+ "fieldname": "uidnext",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "UIDNEXT"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-09-21 11:53:00.811236",
+ "modified_by": "Administrator",
+ "module": "Email",
+ "name": "IMAP Folder",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
diff --git a/frappe/email/doctype/imap_folder/imap_folder.py b/frappe/email/doctype/imap_folder/imap_folder.py
new file mode 100644
index 0000000000..b0bb36b677
--- /dev/null
+++ b/frappe/email/doctype/imap_folder/imap_folder.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 IMAPFolder(Document):
+ pass
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.js b/frappe/email/doctype/newsletter/newsletter.js
index 3277d8e9ee..55805ad485 100644
--- a/frappe/email/doctype/newsletter/newsletter.js
+++ b/frappe/email/doctype/newsletter/newsletter.js
@@ -4,69 +4,137 @@
frappe.ui.form.on('Newsletter', {
refresh(frm) {
let doc = frm.doc;
- if (!doc.__islocal && !cint(doc.email_sent) && !doc.__unsaved
- && in_list(frappe.boot.user.can_write, doc.doctype)) {
- frm.add_custom_button(__('Send Now'), function() {
- frappe.confirm(__("Do you really want to send this email newsletter?"), function() {
- frm.call('send_emails').then(() => {
- frm.refresh();
- });
+ let can_write = in_list(frappe.boot.user.can_write, doc.doctype);
+ if (!frm.is_new() && !frm.is_dirty() && !doc.email_sent && can_write) {
+ frm.add_custom_button(__('Send a test email'), () => {
+ frm.events.send_test_email(frm);
+ }, __('Preview'));
+
+ frm.add_custom_button(__('Check broken links'), () => {
+ frm.dashboard.set_headline(__('Checking broken links...'));
+ frm.call('find_broken_links').then(r => {
+ frm.dashboard.set_headline('');
+ let links = r.message;
+ if (links && links.length) {
+ let html = '' + links.map(link => `- ${link}
`).join('') + '
';
+ frm.dashboard.set_headline(__("Following links are broken in the email content: {0}", [html]));
+ } else {
+ frm.dashboard.set_headline(__("No broken links found in the email content"));
+ setTimeout(() => {
+ frm.dashboard.set_headline('');
+ }, 3000);
+ }
});
- }, "fa fa-play", "btn-success");
+ }, __('Preview'));
+
+ frm.add_custom_button(__('Send now'), () => {
+ if (frm.doc.schedule_send) {
+ frappe.confirm(__("This newsletter was scheduled to send on a later date. Are you sure you want to send it now?"), function () {
+ frm.call('send_emails').then(() => frm.refresh());
+ });
+ return;
+ }
+ frappe.confirm(__("Are you sure you want to send this newsletter now?"), function () {
+ frm.call('send_emails').then(() => frm.refresh());
+ });
+ }, __('Send'));
+
+ frm.add_custom_button(__('Schedule sending'), () => {
+ frm.events.schedule_send_dialog(frm);
+ }, __('Send'));
}
frm.events.setup_dashboard(frm);
+ frm.events.setup_sending_status(frm);
- if (doc.__islocal && !doc.send_from) {
+ if (frm.is_new() && !doc.sender_email) {
let { fullname, email } = frappe.user_info(doc.owner);
- frm.set_value('send_from', `${fullname} <${email}>`);
+ frm.set_value('sender_email', email);
+ frm.set_value('sender_name', fullname);
}
+
+ frm.trigger('update_schedule_message');
},
- onload_post_render(frm) {
- frm.trigger('setup_schedule_send');
- },
-
- setup_schedule_send(frm) {
- let today = new Date();
-
- // setting datepicker options to set min date & min time
- today.setHours(today.getHours() + 1 );
- frm.get_field('schedule_send').$input.datepicker({
- maxMinutes: 0,
- minDate: today,
- timeFormat: 'hh:00:00',
- onSelect: function (fd, d, picker) {
- if (!d) return;
- var date = d.toDateString();
- if (date === today.toDateString()) {
- picker.update({
- minHours: (today.getHours() + 1)
- });
- } else {
- picker.update({
- minHours: 0
- });
- }
- frm.get_field('schedule_send').$input.trigger('change');
+ schedule_send_dialog(frm) {
+ let hours = frappe.utils.range(24);
+ let time_slots = hours.map(hour => {
+ return `${(hour + '').padStart(2, '0')}:00`;
+ });
+ let d = new frappe.ui.Dialog({
+ title: __('Schedule Newsletter'),
+ fields: [
+ {
+ label: __('Date'),
+ fieldname: 'date',
+ fieldtype: 'Date',
+ options: {
+ minDate: new Date()
+ }
+ },
+ {
+ label: __('Time'),
+ fieldname: 'time',
+ fieldtype: 'Select',
+ options: time_slots,
+ },
+ ],
+ primary_action_label: __('Schedule'),
+ primary_action({ date, time }) {
+ frm.set_value('schedule_sending', 1);
+ frm.set_value('schedule_send', `${date} ${time}:00`);
+ d.hide();
+ frm.save();
+ },
+ secondary_action_label: __('Cancel Scheduling'),
+ secondary_action() {
+ frm.set_value('schedule_sending', 0);
+ frm.set_value('schedule_send', '');
+ d.hide();
+ frm.save();
}
});
+ if (frm.doc.schedule_sending) {
+ let parts = frm.doc.schedule_send.split(' ');
+ if (parts.length === 2) {
+ let [date, time] = parts;
+ d.set_value('date', date);
+ d.set_value('time', time.slice(0, 5));
+ }
+ }
+ d.show();
+ },
-
- const $tp = frm.get_field('schedule_send').datepicker.timepicker;
- $tp.$minutes.parent().css('display', 'none');
- $tp.$minutesText.css('display', 'none');
- $tp.$minutesText.prev().css('display', 'none');
- $tp.$seconds.parent().css('display', 'none');
+ send_test_email(frm) {
+ let d = new frappe.ui.Dialog({
+ title: __('Send Test Email'),
+ fields: [
+ {
+ label: __('Email'),
+ fieldname: 'email',
+ fieldtype: 'Data',
+ options: 'Email',
+ }
+ ],
+ primary_action_label: __('Send'),
+ primary_action({ email }) {
+ d.get_primary_btn().text(__('Sending...')).prop('disabled', true);
+ frm.call('send_test_email', { email })
+ .then(() => {
+ d.get_primary_btn().text(__('Send again')).prop('disabled', false);
+ });
+ }
+ });
+ d.show();
},
setup_dashboard(frm) {
- if(!frm.doc.__islocal && cint(frm.doc.email_sent)
+ if (!frm.doc.__islocal && cint(frm.doc.email_sent)
&& frm.doc.__onload && frm.doc.__onload.status_count) {
var stat = frm.doc.__onload.status_count;
var total = frm.doc.scheduled_to_send;
- if(total) {
- $.each(stat, function(k, v) {
+ if (total) {
+ $.each(stat, function (k, v) {
stat[k] = flt(v * 100 / total, 2) + '%';
});
@@ -94,5 +162,58 @@ frappe.ui.form.on('Newsletter', {
]);
}
}
+ },
+
+ setup_sending_status(frm) {
+ frm.call('get_sending_status').then(r => {
+ if (r.message) {
+ frm.events.update_sending_progress(frm, r.message.sent, r.message.total);
+ }
+ if (r.message.sent >= r.message.total) {
+ return;
+ }
+ if (frm.sending_status) return;
+
+ frm.sending_status = setInterval(() => {
+ if (frm.doc.email_sent && frm.$wrapper.is(':visible')) {
+ frm.call('get_sending_status').then(r => {
+ if (r.message) {
+ let { sent, total } = r.message;
+ frm.events.update_sending_progress(frm, sent, total);
+
+ if (sent >= total) {
+ clearInterval(frm.sending_status);
+ frm.sending_status = null;
+ return;
+ }
+ }
+ });
+ }
+ }, 5000);
+ });
+ },
+
+ update_sending_progress(frm, sent, total) {
+ if (sent >= total) {
+ frm.dashboard.hide_progress();
+ return;
+ }
+ frm.dashboard.show_progress(__('Sending emails'), sent * 100 / total, __("{0} of {1} sent", [sent, total]));
+ },
+
+ on_hide(frm) {
+ if (frm.sending_status) {
+ clearInterval(frm.sending_status);
+ frm.sending_status = null;
+ }
+ },
+
+ update_schedule_message(frm) {
+ if (!frm.doc.email_sent && frm.doc.schedule_send) {
+ let datetime = frappe.datetime.global_date_format(frm.doc.schedule_send);
+ frm.dashboard.set_headline_alert(__('This newsletter is scheduled to be sent on {0}', [datetime.bold()]));
+ } else {
+ frm.dashboard.clear_headline();
+ }
}
});
diff --git a/frappe/email/doctype/newsletter/newsletter.json b/frappe/email/doctype/newsletter/newsletter.json
index dcd19ed33c..baabd4991e 100644
--- a/frappe/email/doctype/newsletter/newsletter.json
+++ b/frappe/email/doctype/newsletter/newsletter.json
@@ -7,48 +7,59 @@
"document_type": "Other",
"engine": "InnoDB",
"field_order": [
+ "status_section",
+ "email_sent_at",
+ "column_break_3",
+ "total_recipients",
+ "column_break_12",
+ "email_sent",
+ "from_section",
+ "sender_name",
+ "column_break_5",
+ "sender_email",
+ "column_break_7",
"send_from",
- "schedule_sending",
- "schedule_send",
"recipients",
"email_group",
- "email_sent",
- "newsletter_content",
+ "subject_section",
"subject",
+ "newsletter_content",
"content_type",
"message",
"message_md",
"message_html",
- "section_break_13",
+ "attachments",
"send_unsubscribe_link",
- "send_attachments",
- "column_break_9",
- "published",
"send_webview_link",
- "route",
- "test_the_newsletter",
- "test_email_id",
- "test_send",
- "scheduled_to_send"
+ "schedule_settings_section",
+ "scheduled_to_send",
+ "schedule_sending",
+ "schedule_send",
+ "publish_as_a_web_page_section",
+ "published",
+ "route"
],
"fields": [
{
"fieldname": "email_group",
"fieldtype": "Table",
"in_standard_filter": 1,
- "label": "Email Group",
- "options": "Newsletter Email Group"
+ "label": "Audience",
+ "options": "Newsletter Email Group",
+ "reqd": 1
},
{
"fieldname": "send_from",
"fieldtype": "Data",
"ignore_xss_filter": 1,
- "label": "Sender"
+ "label": "Sender",
+ "read_only": 1
},
{
"default": "0",
"fieldname": "email_sent",
"fieldtype": "Check",
+ "hidden": 1,
"label": "Email Sent",
"no_copy": 1,
"read_only": 1
@@ -87,32 +98,12 @@
"label": "Published"
},
{
+ "depends_on": "published",
"fieldname": "route",
"fieldtype": "Data",
- "hidden": 1,
"label": "Route",
"read_only": 1
},
- {
- "collapsible": 1,
- "fieldname": "test_the_newsletter",
- "fieldtype": "Section Break",
- "label": "Testing"
- },
- {
- "description": "A Lead with this Email Address should exist",
- "fieldname": "test_email_id",
- "fieldtype": "Data",
- "label": "Test Email Address",
- "options": "Email"
- },
- {
- "depends_on": "eval: doc.test_email_id",
- "fieldname": "test_send",
- "fieldtype": "Button",
- "label": "Test",
- "options": "test_send"
- },
{
"fieldname": "scheduled_to_send",
"fieldtype": "Int",
@@ -122,21 +113,16 @@
{
"fieldname": "recipients",
"fieldtype": "Section Break",
- "label": "Recipients"
+ "label": "To"
},
{
"depends_on": "eval: doc.schedule_sending",
"fieldname": "schedule_send",
"fieldtype": "Datetime",
- "label": "Schedule Send",
+ "label": "Send Email At",
+ "read_only": 1,
"read_only_depends_on": "eval: doc.email_sent"
},
- {
- "default": "0",
- "fieldname": "send_attachments",
- "fieldtype": "Check",
- "label": "Send Attachments"
- },
{
"fieldname": "content_type",
"fieldtype": "Select",
@@ -161,23 +147,87 @@
"default": "0",
"fieldname": "schedule_sending",
"fieldtype": "Check",
- "label": "Schedule Sending",
+ "label": "Schedule sending at a later time",
"read_only_depends_on": "eval: doc.email_sent"
},
- {
- "fieldname": "column_break_9",
- "fieldtype": "Column Break"
- },
{
"default": "0",
- "depends_on": "published",
"fieldname": "send_webview_link",
"fieldtype": "Check",
"label": "Send Web View Link"
},
{
- "fieldname": "section_break_13",
- "fieldtype": "Section Break"
+ "fieldname": "from_section",
+ "fieldtype": "Section Break",
+ "label": "From"
+ },
+ {
+ "fieldname": "sender_name",
+ "fieldtype": "Data",
+ "label": "Sender Name"
+ },
+ {
+ "fieldname": "sender_email",
+ "fieldtype": "Data",
+ "label": "Sender Email",
+ "options": "Email",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_5",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_7",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "subject_section",
+ "fieldtype": "Section Break",
+ "label": "Subject"
+ },
+ {
+ "fieldname": "publish_as_a_web_page_section",
+ "fieldtype": "Section Break",
+ "label": "Publish as a web page"
+ },
+ {
+ "depends_on": "schedule_sending",
+ "fieldname": "schedule_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Scheduled Sending"
+ },
+ {
+ "fieldname": "attachments",
+ "fieldtype": "Table",
+ "label": "Attachments",
+ "options": "Newsletter Attachment"
+ },
+ {
+ "fieldname": "email_sent_at",
+ "fieldtype": "Datetime",
+ "label": "Email Sent At",
+ "read_only": 1
+ },
+ {
+ "fieldname": "total_recipients",
+ "fieldtype": "Int",
+ "label": "Total Recipients",
+ "read_only": 1
+ },
+ {
+ "depends_on": "email_sent",
+ "fieldname": "status_section",
+ "fieldtype": "Section Break",
+ "label": "Status"
+ },
+ {
+ "fieldname": "column_break_12",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
}
],
"has_web_view": 1,
@@ -187,7 +237,7 @@
"is_published_field": "published",
"links": [],
"max_attachments": 3,
- "modified": "2021-02-22 14:33:56.095380",
+ "modified": "2021-12-06 20:09:37.963141",
"modified_by": "Administrator",
"module": "Email",
"name": "Newsletter",
diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py
old mode 100755
new mode 100644
index 97d77549b7..aa6fa2c40a
--- a/frappe/email/doctype/newsletter/newsletter.py
+++ b/frappe/email/doctype/newsletter/newsletter.py
@@ -1,279 +1,355 @@
-# 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 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.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
-
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()
+ self.validate_publishing()
+
+ @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)
- frappe.msgprint(_("Test email sent to {0}").format(self.test_email_id))
+ def get_sending_status(self):
+ count_by_status = frappe.get_all("Email Queue",
+ filters={"reference_doctype": self.doctype, "reference_name": self.name},
+ fields=["status", "count(name) as count"],
+ group_by="status",
+ order_by="status"
+ )
+ sent = 0
+ total = 0
+ for row in count_by_status:
+ if row.status == "Sent":
+ sent = row.count
+ total += row.count
+
+ return {'sent': sent, 'total': total}
+
+ @frappe.whitelist()
+ def send_test_email(self, email):
+ test_emails = frappe.utils.validate_email_address(email, throw=True)
+ self.send_newsletter(emails=test_emails)
+ frappe.msgprint(_("Test email sent to {0}").format(email), alert=True)
+
+ @frappe.whitelist()
+ def find_broken_links(self):
+ from bs4 import BeautifulSoup
+ import requests
+
+ html = self.get_message()
+ soup = BeautifulSoup(html, "html.parser")
+ links = soup.find_all("a")
+ images = soup.find_all("img")
+ broken_links = []
+ for el in links + images:
+ url = el.attrs.get("href") or el.attrs.get("src")
+ try:
+ response = requests.head(url, verify=False, timeout=5)
+ if response.status_code >= 400:
+ broken_links.append(url)
+ except:
+ broken_links.append(url)
+ return broken_links
@frappe.whitelist()
def send_emails(self):
- """send emails to leads and customers"""
- 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))
+ """queue sending emails to recipients"""
+ self.schedule_sending = False
+ self.schedule_send = None
+ self.queue_all()
+ frappe.msgprint(_("Email queued to {0} recipients").format(self.total_recipients))
def validate_send(self):
+ """Validate if Newsletter can be sent.
+ """
+ self.validate_newsletter_status()
+ self.validate_newsletter_recipients()
+
+ def validate_newsletter_status(self):
+ if self.email_sent:
+ frappe.throw(_("Newsletter has already been sent"), exc=NewsletterAlreadySentError)
+
if self.get("__islocal"):
- throw(_("Please save the Newsletter before sending"))
+ frappe.throw(_("Please save the Newsletter before sending"), exc=NewsletterNotSavedError)
- if not self.recipients:
- frappe.throw(_("Newsletter should have at least one recipient"))
+ 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 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.local.flags.redirect_location = frappe.local.response.location
- raise frappe.Redirect
- else:
- context.attachments = get_attachments(self.name)
- context.no_cache = 1
- context.show_sidebar = True
+ def validate_sender_address(self):
+ """Validate self.send_from is a valid email address or not.
+ """
+ if self.sender_email:
+ frappe.utils.validate_email_address(self.sender_email, throw=True)
+ self.send_from = f"{self.sender_name} <{self.sender_email}>" if self.sender_name else self.sender_email
+ 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_attachments(name):
- return frappe.get_all("File",
+ def validate_publishing(self):
+ if self.send_webview_link and not self.published:
+ frappe.throw(_("Newsletter must be published to send webview link in email"))
+
+ 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):
+ """Queue Newsletter to all the recipients generated from the `Email Group` table
+ """
+ self.validate()
+ self.validate_send()
+
+ recipients = self.get_pending_recipients()
+ self.send_newsletter(emails=recipients)
+
+ self.email_sent = True
+ self.email_sent_at = frappe.utils.now()
+ self.total_recipients = len(recipients)
+ self.save()
+
+ def get_newsletter_attachments(self) -> List[Dict[str, str]]:
+ """Get list of attachments on current Newsletter
+ """
+ return [{"file_url": row.attachment} for row in self.attachments]
+
+ def send_newsletter(self, emails: List[str]):
+ """Trigger email generation for `emails` and add it in Email Queue.
+ """
+ attachments = self.get_newsletter_attachments()
+ sender = self.send_from or frappe.utils.get_formatted_email(self.owner)
+ args = self.as_dict()
+ args["message"] = self.get_message()
+
+ 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,
+ 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:
+ message = self.message
+ if self.content_type == "Markdown":
+ message = frappe.utils.md_to_html(self.message_md)
+ if self.content_type == "HTML":
+ message = self.message_html
+
+ return frappe.render_template(message, {"doc": self.as_dict()})
+
+ 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": 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"})
+ filters={
+ "attached_to_name": self.name,
+ "attached_to_doctype": "Newsletter",
+ "is_private": 0,
+ },
+ )
@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):
context.update({
- "show_sidebar": True,
"show_search": True,
- 'no_breadcrumbs': True,
- "title": _("Newsletter"),
- "get_list": get_newsletter_list,
+ "no_breadcrumbs": True,
+ "title": _("Newsletters"),
+ "filters": {"published": 1},
"row_template": "email/doctype/newsletter/templates/newsletter_row.html",
})
-def get_newsletter_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"):
- email_group_list = frappe.db.sql('''SELECT eg.name
- FROM `tabEmail Group` eg, `tabEmail Group Member` egm
- WHERE egm.unsubscribed=0
- AND eg.name=egm.email_group
- AND egm.email = %s''', frappe.session.user)
- email_group_list = [d[0] for d in email_group_list]
-
- if email_group_list:
- return frappe.db.sql('''SELECT n.name, n.subject, n.message, n.modified
- FROM `tabNewsletter` n, `tabNewsletter Email Group` neg
- WHERE n.name = neg.parent
- AND n.email_sent=1
- AND n.published=1
- AND neg.email_group in ({0})
- ORDER BY n.modified DESC LIMIT {1} OFFSET {2}
- '''.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/templates/newsletter.html b/frappe/email/doctype/newsletter/templates/newsletter.html
index 733c7df6af..1244f4c49a 100644
--- a/frappe/email/doctype/newsletter/templates/newsletter.html
+++ b/frappe/email/doctype/newsletter/templates/newsletter.html
@@ -1,6 +1,6 @@
{% extends "templates/web.html" %}
-{% block title %} {{ _("Newsletter") }} {% endblock %}
+{% block title %} {{ doc.subject }} {% endblock %}
{% block page_content %}
diff --git a/frappe/public/js/frappe/file_uploader/FileUploader.vue b/frappe/public/js/frappe/file_uploader/FileUploader.vue
index 06f9275711..167b4955fa 100644
--- a/frappe/public/js/frappe/file_uploader/FileUploader.vue
+++ b/frappe/public/js/frappe/file_uploader/FileUploader.vue
@@ -46,7 +46,7 @@
{{ __('Library') }}
-