From 850cd54b890856af30403542289facdeec42fad4 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Fri, 3 Dec 2021 17:07:16 +0100
Subject: [PATCH 01/67] refactor: module profile
---
.../doctype/module_profile/module_profile.js | 17 +-
.../module_profile/module_profile.json | 10 +-
.../doctype/role_profile/role_profile.json | 227 +++++-------------
frappe/core/doctype/user/user.js | 4 +-
frappe/public/js/frappe/module_editor.js | 76 +++---
5 files changed, 133 insertions(+), 201 deletions(-)
diff --git a/frappe/core/doctype/module_profile/module_profile.js b/frappe/core/doctype/module_profile/module_profile.js
index 9c92042dda..57b563157c 100644
--- a/frappe/core/doctype/module_profile/module_profile.js
+++ b/frappe/core/doctype/module_profile/module_profile.js
@@ -1,19 +1,24 @@
// 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) {
+ debugger;
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/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/user/user.js b/frappe/core/doctype/user/user.js
index 2ce7413aa7..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();
}
});
}
@@ -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
diff --git a/frappe/public/js/frappe/module_editor.js b/frappe/public/js/frappe/module_editor.js
index 5e2ca4bc83..ff0cfc2426 100644
--- a/frappe/public/js/frappe/module_editor.js
+++ b/frappe/public/js/frappe/module_editor.js
@@ -1,38 +1,54 @@
frappe.ModuleEditor = class ModuleEditor {
constructor(frm, wrapper) {
- this.wrapper = $('
').appendTo(wrapper);
this.frm = frm;
- this.make();
- }
- make() {
- var me = this;
- this.frm.doc.__onload.all_modules.forEach(function(m) {
- $(repl('
', {module: m})).appendTo(me.wrapper);
- });
- this.bind();
- }
- refresh() {
- var me = this;
- this.wrapper.find(".block-module-check").prop("checked", true);
- $.each(this.frm.doc.block_modules, function(i, d) {
- me.wrapper.find(".block-module-check[data-module='"+ d.module +"']").prop("checked", false);
+ this.wrapper = wrapper;
+ const block_modules = this.frm.doc.block_modules.map(row => row.module);
+ this.multicheck = frappe.ui.form.make_control({
+ parent: wrapper,
+ df: {
+ fieldname: "block_modules",
+ fieldtype: "MultiCheck",
+ select_all: true,
+ columns: 3,
+ get_data: () => {
+ return this.frm.doc.__onload.all_modules.map(module => {
+ return {
+ label: __(module),
+ value: module,
+ checked: !block_modules.includes(module),
+ };
+ });
+ },
+ on_change: () => {
+ this.set_modules_in_table();
+ this.frm.dirty();
+ }
+ },
+ render_input: true
});
}
- bind() {
- var me = this;
- this.wrapper.on("change", ".block-module-check", function() {
- var module = $(this).attr('data-module');
- if ($(this).prop("checked")) {
- // remove from block_modules
- me.frm.doc.block_modules = $.map(me.frm.doc.block_modules || [], function(d) {
- if (d.module != module) {
- return d;
- }
- });
- } else {
- me.frm.add_child("block_modules", {"module": module});
+
+ show() {
+ const block_modules = this.frm.doc.block_modules.map(row => row.module);
+ const all_modules = this.frm.doc.__onload.all_modules;
+ this.multicheck.selected_options = all_modules.filter(m => !block_modules.includes(m));
+ this.multicheck.refresh_input();
+ }
+
+ set_modules_in_table() {
+ let block_modules = this.frm.doc.block_modules || [];
+ let unchecked_options = this.multicheck.get_unchecked_options();
+
+ block_modules.map(module_doc => {
+ if (!unchecked_options.includes(module_doc.module)) {
+ frappe.model.clear_doc(module_doc.doctype, module_doc.name);
+ }
+ });
+
+ unchecked_options.map(module => {
+ if (!block_modules.find(d => d.module === module)) {
+ let module_doc = frappe.model.add_child(this.frm.doc, "Block Module", "block_modules");
+ module_doc.module = module;
}
});
}
From 504f8743c9ac818f1cacc7c1424340ebcc24c5a9 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Fri, 3 Dec 2021 17:24:57 +0100
Subject: [PATCH 02/67] fix: remove debugger
---
frappe/core/doctype/module_profile/module_profile.js | 1 -
1 file changed, 1 deletion(-)
diff --git a/frappe/core/doctype/module_profile/module_profile.js b/frappe/core/doctype/module_profile/module_profile.js
index 57b563157c..3714d31ade 100644
--- a/frappe/core/doctype/module_profile/module_profile.js
+++ b/frappe/core/doctype/module_profile/module_profile.js
@@ -3,7 +3,6 @@
frappe.ui.form.on("Module Profile", {
refresh: function (frm) {
- debugger;
if (has_common(frappe.user_roles, ["Administrator", "System Manager"])) {
if (!frm.module_editor && frm.doc.__onload && frm.doc.__onload.all_modules) {
const module_area = $(frm.fields_dict.module_html.wrapper);
From 3243fb2083593fdc42b59f609935a758ae399bad Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Mon, 6 Dec 2021 13:04:27 +0530
Subject: [PATCH 03/67] fix: misc fixes
---
frappe/database/database.py | 17 ++++++++---------
frappe/database/query.py | 5 ++---
frappe/query_builder/builder.py | 33 +++++++++++----------------------
3 files changed, 21 insertions(+), 34 deletions(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 6a4e781b44..0f325a746e 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -511,14 +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 = self.query.get_sql(
+ "Singles", filters={"doctype": doctype}, fields=["field", "value"]
+ ).run()
dict_ = frappe._dict(result)
-
return dict_
@staticmethod
@@ -547,8 +543,11 @@ class Database(object):
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)
diff --git a/frappe/database/query.py b/frappe/database/query.py
index 69328cb206..6d2be5fa25 100644
--- a/frappe/database/query.py
+++ b/frappe/database/query.py
@@ -286,14 +286,13 @@ class Query:
):
criterion = self.build_conditions(table, filters, **kwargs)
if isinstance(fields, (list, tuple)):
- query = criterion.select(*kwargs.get("field_objects"))
+ query = criterion.select(*kwargs.get("field_objects", fields))
elif isinstance(fields, Criterion):
query = criterion.select(fields)
else:
- if fields=="*":
- query = criterion.select(fields)
+ query = criterion.select(fields)
return query
diff --git a/frappe/query_builder/builder.py b/frappe/query_builder/builder.py
index 630cfea222..a65d50fdeb 100644
--- a/frappe/query_builder/builder.py
+++ b/frappe/query_builder/builder.py
@@ -18,16 +18,6 @@ class Base:
table_name = get_table_name(table_name)
return Table(table_name, *args, **kwargs)
-
-class MariaDB(Base, MySQLQuery):
- Field = terms.Field
-
- @classmethod
- def from_(cls, table, *args, **kwargs):
- if isinstance(table, str):
- table = cls.DocType(table)
- return super().from_(table, *args, **kwargs)
-
@classmethod
def into(cls, table, *args, **kwargs):
if isinstance(table, str):
@@ -40,6 +30,17 @@ class MariaDB(Base, MySQLQuery):
table = cls.DocType(table)
return super().update(table, *args, **kwargs)
+
+class MariaDB(Base, MySQLQuery):
+ Field = terms.Field
+
+ @classmethod
+ def from_(cls, table, *args, **kwargs):
+ if isinstance(table, str):
+ table = cls.DocType(table)
+ return super().from_(table, *args, **kwargs)
+
+
class Postgres(Base, PostgreSQLQuery):
field_translation = {"table_name": "relname", "table_rows": "n_tup_ins"}
schema_translation = {"tables": "pg_stat_all_tables"}
@@ -69,15 +70,3 @@ class Postgres(Base, PostgreSQLQuery):
table = cls.DocType(table)
return super().from_(table, *args, **kwargs)
-
- @classmethod
- def into(cls, table, *args, **kwargs):
- if isinstance(table, str):
- table = cls.DocType(table)
- return super().into(table, *args, **kwargs)
-
- @classmethod
- def update(cls, table, *args, **kwargs):
- if isinstance(table, str):
- table = cls.DocType(table)
- return super().update(table, *args, **kwargs)
\ No newline at end of file
From a574c1ba88cd76d74065bfbcbf2c9c78dd208d0b Mon Sep 17 00:00:00 2001
From: saxenabhishek
Date: Thu, 2 Dec 2021 23:20:40 +0530
Subject: [PATCH 04/67] chore: patching ValueWrapper
---
frappe/query_builder/terms.py | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)
create mode 100644 frappe/query_builder/terms.py
diff --git a/frappe/query_builder/terms.py b/frappe/query_builder/terms.py
new file mode 100644
index 0000000000..c221dcb28e
--- /dev/null
+++ b/frappe/query_builder/terms.py
@@ -0,0 +1,27 @@
+from typing import Any, Optional, Dict
+from pypika.terms import ValueWrapper
+from pypika.utils import format_alias_sql
+
+
+class NamedParameterWrapper():
+ def __init__(self, parameters: Dict[str, Any]):
+ self.parameters = parameters
+
+ def update_parameters(self, param_key: Any, param_value: Any, **kwargs):
+ self.parameters[param_key[1:]] = param_value
+
+ def get_sql(self, **kwargs):
+ return f'@param{len(self.parameters) + 1}'
+
+
+class ParameterizedValueWrapper(ValueWrapper):
+ def get_sql(self, quote_char: Optional[str] = None, secondary_quote_char: str = "'", param_wrapper= None, **kwargs: Any) -> str:
+ if param_wrapper is None:
+ sql = self.get_value_sql(quote_char=quote_char, secondary_quote_char=secondary_quote_char, **kwargs)
+ return format_alias_sql(sql, self.alias, quote_char=quote_char, **kwargs)
+ else:
+ value_sql = self.get_value_sql(quote_char=quote_char, **kwargs)
+ param_sql = param_wrapper.get_sql(**kwargs)
+ param_wrapper.update_parameters(param_key=param_sql, param_value=value_sql, **kwargs)
+
+ return format_alias_sql(param_sql, self.alias, quote_char=quote_char, **kwargs)
\ No newline at end of file
From 9fdacedfc80889c81c4887c2f2f2581e5d0d56f6 Mon Sep 17 00:00:00 2001
From: saxenabhishek
Date: Thu, 2 Dec 2021 23:23:25 +0530
Subject: [PATCH 05/67] feat: sanitise frappe.qb
---
frappe/query_builder/__init__.py | 5 +++++
frappe/query_builder/terms.py | 8 ++++----
frappe/query_builder/utils.py | 14 ++++++++++----
3 files changed, 19 insertions(+), 8 deletions(-)
diff --git a/frappe/query_builder/__init__.py b/frappe/query_builder/__init__.py
index 9c7432142f..06d499678f 100644
--- a/frappe/query_builder/__init__.py
+++ b/frappe/query_builder/__init__.py
@@ -1,2 +1,7 @@
+from frappe.query_builder.terms import ParameterizedValueWrapper
+import pypika
+
+pypika.terms.ValueWrapper = ParameterizedValueWrapper
+
from pypika import *
from frappe.query_builder.utils import Column, DocType, get_query_builder, patch_query_execute, patch_query_aggregation
diff --git a/frappe/query_builder/terms.py b/frappe/query_builder/terms.py
index c221dcb28e..c09d9595fb 100644
--- a/frappe/query_builder/terms.py
+++ b/frappe/query_builder/terms.py
@@ -1,4 +1,4 @@
-from typing import Any, Optional, Dict
+from typing import Any, List, Optional, Dict
from pypika.terms import ValueWrapper
from pypika.utils import format_alias_sql
@@ -8,10 +8,10 @@ class NamedParameterWrapper():
self.parameters = parameters
def update_parameters(self, param_key: Any, param_value: Any, **kwargs):
- self.parameters[param_key[1:]] = param_value
+ self.parameters[param_key[2:-2]] = param_value
def get_sql(self, **kwargs):
- return f'@param{len(self.parameters) + 1}'
+ return f'%(param{len(self.parameters) + 1})s'
class ParameterizedValueWrapper(ValueWrapper):
@@ -20,7 +20,7 @@ class ParameterizedValueWrapper(ValueWrapper):
sql = self.get_value_sql(quote_char=quote_char, secondary_quote_char=secondary_quote_char, **kwargs)
return format_alias_sql(sql, self.alias, quote_char=quote_char, **kwargs)
else:
- value_sql = self.get_value_sql(quote_char=quote_char, **kwargs)
+ value_sql = self.get_value_sql(quote_char=quote_char, **kwargs) if not isinstance(self.value,int) else self.value
param_sql = param_wrapper.get_sql(**kwargs)
param_wrapper.update_parameters(param_key=param_sql, param_value=value_sql, **kwargs)
diff --git a/frappe/query_builder/utils.py b/frappe/query_builder/utils.py
index a7f52df012..7922825725 100644
--- a/frappe/query_builder/utils.py
+++ b/frappe/query_builder/utils.py
@@ -10,6 +10,7 @@ import frappe
from .builder import MariaDB, Postgres
from pypika.terms import PseudoColumn
+from frappe.query_builder.terms import NamedParameterWrapper
class db_type_is(Enum):
MARIADB = "mariadb"
@@ -53,12 +54,16 @@ def patch_query_execute():
This excludes the use of `frappe.db.sql` method while
executing the query object
"""
-
def execute_query(query, *args, **kwargs):
- query = str(query)
+ query, params = prepare_query(query)
+ return frappe.db.sql(query, params, *args, **kwargs)
+
+ def prepare_query(query):
+ params = {}
+ query = query.get_sql(param_wrapper = NamedParameterWrapper(params))
if frappe.flags.in_safe_exec and not query.lower().strip().startswith("select"):
raise frappe.PermissionError('Only SELECT SQL allowed in scripting')
- return frappe.db.sql(query, *args, **kwargs)
+ return query, params
query_class = get_attr(str(frappe.qb).split("'")[1])
builder_class = get_type_hints(query_class._builder).get('return')
@@ -67,6 +72,7 @@ def patch_query_execute():
raise BuilderIdentificationFailed
builder_class.run = execute_query
+ builder_class.walk = prepare_query
def patch_query_aggregation():
@@ -77,4 +83,4 @@ def patch_query_aggregation():
frappe.qb.max = _max
frappe.qb.min = _min
frappe.qb.avg = _avg
- frappe.qb.sum = _sum
\ No newline at end of file
+ frappe.qb.sum = _sum
From 6120b4b3c1dbf17bc783be16ba41a5c73d5c5df1 Mon Sep 17 00:00:00 2001
From: saxenabhishek
Date: Sat, 4 Dec 2021 20:12:48 +0530
Subject: [PATCH 06/67] fix: extend named parameters to frappe.qb.function
---
frappe/query_builder/__init__.py | 3 ++-
frappe/query_builder/terms.py | 28 +++++++++++++++++++++++++---
2 files changed, 27 insertions(+), 4 deletions(-)
diff --git a/frappe/query_builder/__init__.py b/frappe/query_builder/__init__.py
index 06d499678f..bf7be84c51 100644
--- a/frappe/query_builder/__init__.py
+++ b/frappe/query_builder/__init__.py
@@ -1,7 +1,8 @@
-from frappe.query_builder.terms import ParameterizedValueWrapper
+from frappe.query_builder.terms import ParameterizedValueWrapper, ParameterizedFunction
import pypika
pypika.terms.ValueWrapper = ParameterizedValueWrapper
+pypika.terms.Function = ParameterizedFunction
from pypika import *
from frappe.query_builder.utils import Column, DocType, get_query_builder, patch_query_execute, patch_query_aggregation
diff --git a/frappe/query_builder/terms.py b/frappe/query_builder/terms.py
index c09d9595fb..2032cd8497 100644
--- a/frappe/query_builder/terms.py
+++ b/frappe/query_builder/terms.py
@@ -1,5 +1,6 @@
-from typing import Any, List, Optional, Dict
-from pypika.terms import ValueWrapper
+from typing import Any, Dict, Optional
+
+from pypika.terms import Function, ValueWrapper
from pypika.utils import format_alias_sql
@@ -23,5 +24,26 @@ class ParameterizedValueWrapper(ValueWrapper):
value_sql = self.get_value_sql(quote_char=quote_char, **kwargs) if not isinstance(self.value,int) else self.value
param_sql = param_wrapper.get_sql(**kwargs)
param_wrapper.update_parameters(param_key=param_sql, param_value=value_sql, **kwargs)
+ return format_alias_sql(param_sql, self.alias, quote_char=quote_char, **kwargs)
- return format_alias_sql(param_sql, self.alias, quote_char=quote_char, **kwargs)
\ No newline at end of file
+
+class ParameterizedFunction(Function):
+ def get_sql(self, **kwargs: Any) -> str:
+ with_alias = kwargs.pop("with_alias", False)
+ with_namespace = kwargs.pop("with_namespace", False)
+ quote_char = kwargs.pop("quote_char", None)
+ dialect = kwargs.pop("dialect", None)
+ param_wrapper = kwargs.pop("param_wrapper", None)
+
+ function_sql = self.get_function_sql(with_namespace=with_namespace, quote_char=quote_char, param_wrapper=param_wrapper, dialect=dialect)
+
+ if self.schema is not None:
+ function_sql = "{schema}.{function}".format(
+ schema=self.schema.get_sql(quote_char=quote_char, dialect=dialect, **kwargs),
+ function=function_sql,
+ )
+
+ if with_alias:
+ return format_alias_sql(function_sql, self.alias, quote_char=quote_char, **kwargs)
+
+ return function_sql
From aa855afe089d209d2a4796c8497d21ed5d12578a Mon Sep 17 00:00:00 2001
From: saxenabhishek
Date: Mon, 6 Dec 2021 12:12:56 +0530
Subject: [PATCH 07/67] test: test for patches through walk
---
frappe/tests/test_query_builder.py | 22 ++++++++++++++++++++--
1 file changed, 20 insertions(+), 2 deletions(-)
diff --git a/frappe/tests/test_query_builder.py b/frappe/tests/test_query_builder.py
index 7a0935a63b..1d63d2041c 100644
--- a/frappe/tests/test_query_builder.py
+++ b/frappe/tests/test_query_builder.py
@@ -2,7 +2,7 @@ import unittest
from typing import Callable
import frappe
-from frappe.query_builder.functions import GroupConcat, Match
+from frappe.query_builder.functions import Coalesce, GroupConcat, Match
from frappe.query_builder.utils import db_type_is
@@ -49,6 +49,25 @@ class TestBuilderBase(object):
self.assertIsInstance(query.run, Callable)
self.assertIsInstance(data, list)
+ def test_walk(self):
+ DocType = frappe.qb.DocType('DocType')
+ query = (
+ frappe.qb.from_(DocType)
+ .select(DocType.name)
+ .where((DocType.owner == "Administrator' --")
+ & (Coalesce(DocType.search_fields == "subject"))
+ )
+ )
+ self.assertTrue("walk" in dir(query))
+ query, params = query.walk()
+
+ self.assertIn("%(param1)s", query)
+ self.assertIn("%(param2)s", query)
+ self.assertIn("param1",params)
+ self.assertEqual(params["param1"],"Administrator' --")
+ self.assertEqual(params["param2"],"subject")
+
+
@run_only_if(db_type_is.MARIADB)
class TestBuilderMaria(unittest.TestCase, TestBuilderBase):
def test_adding_tabs_in_from(self):
@@ -59,7 +78,6 @@ class TestBuilderMaria(unittest.TestCase, TestBuilderBase):
"SELECT * FROM `__Auth`", frappe.qb.from_("__Auth").select("*").get_sql()
)
-
@run_only_if(db_type_is.POSTGRES)
class TestBuilderPostgres(unittest.TestCase, TestBuilderBase):
def test_adding_tabs_in_from(self):
From da4160e2dd0c9c47e207b51689d9a276221b50af Mon Sep 17 00:00:00 2001
From: Faris Ansari
Date: Fri, 3 Dec 2021 18:22:25 +0530
Subject: [PATCH 08/67] fix: Newsletter enhancements and fixes
- Organize fields into sections
- Buttons for Send now and Scheduled sending
- Buttons to Send test email and to Check broken links
- Remove Test section
---
frappe/email/doctype/newsletter/newsletter.js | 144 ++++++++++++------
.../email/doctype/newsletter/newsletter.json | 110 +++++++------
frappe/email/doctype/newsletter/newsletter.py | 31 +++-
.../newsletter/templates/newsletter.html | 4 +-
4 files changed, 194 insertions(+), 95 deletions(-)
diff --git a/frappe/email/doctype/newsletter/newsletter.js b/frappe/email/doctype/newsletter/newsletter.js
index 3277d8e9ee..a7cbcf702a 100644
--- a/frappe/email/doctype/newsletter/newsletter.js
+++ b/frappe/email/doctype/newsletter/newsletter.js
@@ -4,69 +4,123 @@
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.call('find_broken_links').then(r => {
+ let links = r.message;
+ if (links) {
+ let html = '' + links.map(link => `- ${link}
`).join('') + '
';
+ frappe.msgprint({
+ title: __("Broken Links"),
+ message: __("Following links are broken in the email content: {0}", [html]),
+ indicator: "red"
+ })
+ } else {
+ frappe.msgprint({
+ title: _("No Broken Links"),
+ message: _("No broken links found in the email content"),
+ indicator: "green"
+ })
+ }
+ })
+ }, __('Preview'));
+
+ frm.add_custom_button(__('Send now'), () => {
+ frappe.confirm(__("Do you really want to send this email newsletter?"), function () {
+ frm.call('send_emails').then(() => frm.refresh());
});
- }, "fa fa-play", "btn-success");
+ }, __('Send'));
+
+ frm.add_custom_button(__('Schedule sending'), () => {
+ frm.events.schedule_send_dialog(frm);
+ }, __('Send'));
}
frm.events.setup_dashboard(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);
}
},
- 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}`);
+ d.hide();
}
});
+ 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);
+ }
+ }
+ 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) + '%';
});
diff --git a/frappe/email/doctype/newsletter/newsletter.json b/frappe/email/doctype/newsletter/newsletter.json
index dcd19ed33c..ccb2ca8181 100644
--- a/frappe/email/doctype/newsletter/newsletter.json
+++ b/frappe/email/doctype/newsletter/newsletter.json
@@ -7,29 +7,33 @@
"document_type": "Other",
"engine": "InnoDB",
"field_order": [
+ "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",
+ "preview_text",
+ "newsletter_content",
"content_type",
"message",
"message_md",
"message_html",
- "section_break_13",
"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": [
{
@@ -43,7 +47,8 @@
"fieldname": "send_from",
"fieldtype": "Data",
"ignore_xss_filter": 1,
- "label": "Sender"
+ "label": "Sender",
+ "read_only": 1
},
{
"default": "0",
@@ -89,30 +94,9 @@
{
"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,13 +106,14 @@
{
"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"
},
{
@@ -161,13 +146,9 @@
"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",
@@ -176,8 +157,51 @@
"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"
+ },
+ {
+ "description": "Preview Text appears in the inbox after the subject line",
+ "fieldname": "preview_text",
+ "fieldtype": "Data",
+ "label": "Preview Text"
+ },
+ {
+ "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"
}
],
"has_web_view": 1,
@@ -187,7 +211,7 @@
"is_published_field": "published",
"links": [],
"max_attachments": 3,
- "modified": "2021-02-22 14:33:56.095380",
+ "modified": "2021-12-03 17:50:12.028162",
"modified_by": "Administrator",
"module": "Email",
"name": "Newsletter",
diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py
index 12fe160c9d..acaa1dbcc1 100644
--- a/frappe/email/doctype/newsletter/newsletter.py
+++ b/frappe/email/doctype/newsletter/newsletter.py
@@ -30,10 +30,30 @@ class Newsletter(WebsiteGenerator):
return self._recipients
@frappe.whitelist()
- def test_send(self):
- test_emails = frappe.utils.split_emails(self.test_email_id)
+ def send_test_email(self, email):
+ test_emails = frappe.utils.split_emails(email)
self.queue_all(test_emails=test_emails)
- frappe.msgprint(_("Test email sent to {0}").format(self.test_email_id))
+ 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):
@@ -75,8 +95,9 @@ class Newsletter(WebsiteGenerator):
def validate_sender_address(self):
"""Validate self.send_from is a valid email address or not.
"""
- if self.send_from:
- frappe.utils.validate_email_address(self.send_from, throw=True)
+ 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.
diff --git a/frappe/email/doctype/newsletter/templates/newsletter.html b/frappe/email/doctype/newsletter/templates/newsletter.html
index 733c7df6af..11ee19a550 100644
--- a/frappe/email/doctype/newsletter/templates/newsletter.html
+++ b/frappe/email/doctype/newsletter/templates/newsletter.html
@@ -36,7 +36,7 @@
- {{ doc.message }}
+ {{ doc.get_message() }}
@@ -51,7 +51,7 @@