Merge branch 'develop' into develop
This commit is contained in:
commit
e5372a147f
28 changed files with 271 additions and 64 deletions
2
.github/helper/install.sh
vendored
2
.github/helper/install.sh
vendored
|
|
@ -17,7 +17,7 @@ if [ "$TYPE" == "server" ]; then
|
|||
fi
|
||||
|
||||
if [ "$DB" == "mariadb" ];then
|
||||
sudo apt install mariadb-client-10.3
|
||||
sudo apt update && sudo apt install mariadb-client-10.3
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'";
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'";
|
||||
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -11,6 +11,7 @@ dist/
|
|||
frappe/docs/current
|
||||
frappe/public/dist
|
||||
.vscode
|
||||
.vs
|
||||
node_modules
|
||||
.kdev4/
|
||||
*.kdev4
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ class Importer:
|
|||
new_doc = frappe.new_doc(self.doctype)
|
||||
new_doc.update(doc)
|
||||
|
||||
if (meta.autoname or "").lower() != "prompt":
|
||||
if not doc.name and (meta.autoname or "").lower() != "prompt":
|
||||
# name can only be set directly if autoname is prompt
|
||||
new_doc.set("name", None)
|
||||
|
||||
|
|
|
|||
|
|
@ -569,6 +569,24 @@ class File(Document):
|
|||
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"])
|
||||
|
||||
|
|
@ -612,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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -410,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
|
||||
|
|
@ -426,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")
|
||||
|
|
@ -437,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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -51,10 +51,10 @@
|
|||
<p><a class="edit-profile-link">{%=__("Edit Profile") %}</a></p>
|
||||
<p><a class="user-settings-link">{%=__("User Settings") %}</a></p>
|
||||
<p>
|
||||
<a class="leaderboard-link" href="#leaderboard/User"
|
||||
<a class="leaderboard-link" href="/app/leaderboard/User"
|
||||
>{%=__("Leaderboard") %}</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ def update_modified(original_modified, doc):
|
|||
).set(
|
||||
singles_table.value,original_modified
|
||||
).where(
|
||||
singles_table.field == "modified"
|
||||
singles_table["field"] == "modified", # singles_table.field is a method of pypika Selectable
|
||||
).where(
|
||||
singles_table.doctype == doc["name"]
|
||||
).run()
|
||||
|
|
|
|||
|
|
@ -2,4 +2,4 @@ import frappe
|
|||
|
||||
def execute():
|
||||
frappe.reload_doc('website', 'doctype', 'web_page_view', force=True)
|
||||
frappe.db.sql("""UPDATE `tabWeb Page View` set path="/" where path=''""")
|
||||
frappe.db.sql("""UPDATE `tabWeb Page View` set path='/' where path=''""")
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ def execute():
|
|||
select
|
||||
* from `__UserSettings`
|
||||
where
|
||||
user="{user}"
|
||||
user='{user}'
|
||||
'''.format(user = user.user), as_dict=True)
|
||||
|
||||
for setting in user_settings:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import frappe
|
|||
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("email", "doctype", "imap_folder")
|
||||
frappe.reload_doc("email", "doctype", "email_account")
|
||||
|
||||
# patch for all Email Account with the flag use_imap
|
||||
for email_account in frappe.get_list("Email Account", filters={"enable_incoming": 1, "use_imap": 1}):
|
||||
# get all data from Email Account
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ def get_doc_permissions(doc, user=None, ptype=None):
|
|||
meta = frappe.get_meta(doc.doctype)
|
||||
|
||||
def is_user_owner():
|
||||
return (doc.get("owner") or "").lower() == frappe.session.user.lower()
|
||||
return (doc.get("owner") or "").lower() == user.lower()
|
||||
|
||||
if has_controller_permissions(doc, ptype, user=user) is False:
|
||||
push_perm_check_log('Not allowed via controller permission check')
|
||||
|
|
|
|||
|
|
@ -172,9 +172,11 @@ class FormTimeline extends BaseTimeline {
|
|||
|
||||
get_communication_timeline_contents() {
|
||||
let communication_timeline_contents = [];
|
||||
let icon_set = {Email: "mail", Phone: "call", Meeting: "calendar", Other: "dot-horizontal"};
|
||||
(this.doc_info.communications|| []).forEach(communication => {
|
||||
let medium = communication.communication_medium;
|
||||
communication_timeline_contents.push({
|
||||
icon: 'mail',
|
||||
icon: icon_set[medium],
|
||||
icon_size: 'sm',
|
||||
creation: communication.creation,
|
||||
is_card: true,
|
||||
|
|
|
|||
|
|
@ -481,6 +481,24 @@ frappe.request.report_error = function(xhr, request_opts) {
|
|||
exc = "";
|
||||
}
|
||||
|
||||
const copy_markdown_to_clipboard = () => {
|
||||
const code_block = snippet => '```\n' + snippet + '\n```';
|
||||
const traceback_info = [
|
||||
'### App Versions',
|
||||
code_block(JSON.stringify(frappe.boot.versions, null, "\t")),
|
||||
'### Route',
|
||||
code_block(frappe.get_route_str()),
|
||||
'### Trackeback',
|
||||
code_block(exc),
|
||||
'### Request Data',
|
||||
code_block(JSON.stringify(request_opts, null, "\t")),
|
||||
'### Response Data',
|
||||
code_block(JSON.stringify(data, null, '\t')),
|
||||
].join("\n");
|
||||
frappe.utils.copy_to_clipboard(traceback_info);
|
||||
};
|
||||
|
||||
|
||||
var show_communication = function() {
|
||||
var error_report_message = [
|
||||
'<h5>Please type some additional information that could help us reproduce this issue:</h5>',
|
||||
|
|
@ -532,6 +550,11 @@ frappe.request.report_error = function(xhr, request_opts) {
|
|||
frappe.msgprint(__('Support Email Address Not Specified'));
|
||||
}
|
||||
frappe.error_dialog.hide();
|
||||
},
|
||||
secondary_action_label: __('Copy error to clipboard'),
|
||||
secondary_action: () => {
|
||||
copy_markdown_to_clipboard();
|
||||
frappe.error_dialog.hide();
|
||||
}
|
||||
});
|
||||
frappe.error_dialog.wrapper.classList.add('msgprint-dialog');
|
||||
|
|
|
|||
|
|
@ -316,7 +316,7 @@ Object.assign(frappe.utils, {
|
|||
}
|
||||
},
|
||||
get_scroll_position: function(element, additional_offset) {
|
||||
let header_offset = $(".navbar").height() + $(".page-head:visible").height();
|
||||
let header_offset = $(".navbar").height() + $(".page-head:visible").height() || $(".navbar").height();
|
||||
let scroll_top = $(element).offset().top - header_offset - cint(additional_offset);
|
||||
return scroll_top;
|
||||
},
|
||||
|
|
@ -957,17 +957,24 @@ Object.assign(frappe.utils, {
|
|||
return decoded;
|
||||
},
|
||||
copy_to_clipboard(string) {
|
||||
let input = $("<input>");
|
||||
$("body").append(input);
|
||||
input.val(string).select();
|
||||
const show_success_alert = () => {
|
||||
frappe.show_alert({
|
||||
indicator: 'green',
|
||||
message: __('Copied to clipboard.')
|
||||
});
|
||||
};
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(string).then(show_success_alert);
|
||||
} else {
|
||||
let input = $("<textarea>");
|
||||
$("body").append(input);
|
||||
input.val(string).select();
|
||||
|
||||
document.execCommand("copy");
|
||||
input.remove();
|
||||
document.execCommand("copy");
|
||||
show_success_alert();
|
||||
input.remove();
|
||||
}
|
||||
|
||||
frappe.show_alert({
|
||||
indicator: 'green',
|
||||
message: __('Copied to clipboard.')
|
||||
});
|
||||
},
|
||||
is_rtl(lang=null) {
|
||||
return ["ar", "he", "fa", "ps"].includes(lang || frappe.boot.lang);
|
||||
|
|
|
|||
|
|
@ -351,7 +351,7 @@ frappe.views.CommunicationComposer = class {
|
|||
}
|
||||
|
||||
async set_values_from_last_edited_communication() {
|
||||
if (this.txt) return;
|
||||
if (this.txt || this.message) return;
|
||||
|
||||
const last_edited = this.get_last_edited_communication();
|
||||
if (!last_edited.content) return;
|
||||
|
|
@ -713,7 +713,7 @@ frappe.views.CommunicationComposer = class {
|
|||
async set_content() {
|
||||
if (this.content_set) return;
|
||||
|
||||
let message = this.txt || "";
|
||||
let message = this.txt || this.message || "";
|
||||
if (!message && this.frm) {
|
||||
const { doctype, docname } = this.frm;
|
||||
message = await localforage.getItem(doctype + docname) || "";
|
||||
|
|
@ -727,7 +727,7 @@ frappe.views.CommunicationComposer = class {
|
|||
|
||||
const SALUTATION_END_COMMENT = "<!-- salutation-ends -->";
|
||||
if (this.real_name && !message.includes(SALUTATION_END_COMMENT)) {
|
||||
this.message = `
|
||||
message = `
|
||||
<p>${__('Dear {0},', [this.real_name], 'Salutation in new email')},</p>
|
||||
${SALUTATION_END_COMMENT}<br>
|
||||
${message}
|
||||
|
|
|
|||
|
|
@ -169,6 +169,15 @@ frappe.views.FileView = class FileView extends frappe.views.ListView {
|
|||
frappe.file_manager.paste(this.current_folder)
|
||||
)
|
||||
.hide();
|
||||
|
||||
this.page.add_actions_menu_item(__('Export as zip'), () => {
|
||||
let docnames = this.get_checked_items(true);
|
||||
if (docnames.length) {
|
||||
open_url_post('/api/method/frappe.core.doctype.file.file.zip_files', {
|
||||
files: JSON.stringify(docnames)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
set_fields() {
|
||||
|
|
|
|||
|
|
@ -225,4 +225,7 @@
|
|||
--checkbox-right-margin: var(--margin-xs);
|
||||
--checkbox-size: 14px;
|
||||
--checkbox-focus-shadow: 0 0 0 2px var(--gray-300);
|
||||
|
||||
--right-arrow-svg: url("data: image/svg+xml;utf8, <svg width='6' height='8' viewBox='0 0 6 8' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M1.25 7.5L4.75 4L1.25 0.5' stroke='%231F272E' stroke-linecap='round' stroke-linejoin='round'/></svg>");
|
||||
--left-arrow-svg: url("data: image/svg+xml;utf8, <svg width='6' height='8' viewBox='0 0 6 8' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M7.5 9.5L4 6l3.5-3.5' stroke='%231F272E' stroke-linecap='round' stroke-linejoin='round'></path></svg>");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,4 @@ $input-height: 28px !default;
|
|||
// skeleton
|
||||
--skeleton-bg: var(--gray-100);
|
||||
|
||||
--right-arrow-svg: url("data: image/svg+xml;utf8, <svg width='6' height='8' viewBox='0 0 6 8' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M1.25 7.5L4.75 4L1.25 0.5' stroke='%231F272E' stroke-linecap='round' stroke-linejoin='round'/></svg>");
|
||||
|
||||
--left-arrow-svg: url("data: image/svg+xml;utf8, <svg width='6' height='8' viewBox='0 0 6 8' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M7.5 9.5L4 6l3.5-3.5' stroke='%231F272E' stroke-linecap='round' stroke-linejoin='round'></path></svg>");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,9 +112,30 @@
|
|||
}
|
||||
|
||||
.breadcrumb {
|
||||
padding: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
font-size: $font-size-sm;
|
||||
background-color: $breadcrumb-bg;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
+ .breadcrumb-item
|
||||
{
|
||||
font-size: $font-size-sm;
|
||||
&::before {
|
||||
content: #{"/*!rtl:var(--left-arrow-svg);*/"}var(--right-arrow-svg);
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
a {
|
||||
color: var(--text-color)
|
||||
}
|
||||
|
||||
li.disabled {
|
||||
a {
|
||||
color: var(--text-muted);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.card {
|
||||
|
|
@ -196,11 +217,14 @@ h5.modal-title {
|
|||
|
||||
.btn-xs {
|
||||
@extend .btn-sm;
|
||||
|
||||
}
|
||||
|
||||
.hidden-xs {
|
||||
@extend .d-block;
|
||||
@extend .d-sm-none;
|
||||
@include media-breakpoint-between(xs, sm) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.visible-xs {
|
||||
|
|
|
|||
|
|
@ -1,2 +1,8 @@
|
|||
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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
from typing import Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
from pypika.functions import DistinctOptionFunction
|
||||
from pypika.utils import builder
|
||||
from pypika.terms import Term
|
||||
from pypika.utils import builder, format_alias_sql, format_quotes
|
||||
|
||||
import frappe
|
||||
|
||||
|
|
@ -81,3 +82,23 @@ class TO_TSVECTOR(DistinctOptionFunction):
|
|||
text (str): [ the text string that we match it against ]
|
||||
"""
|
||||
self._PLAINTO_TSQUERY = text
|
||||
|
||||
|
||||
class ConstantColumn(Term):
|
||||
alias = None
|
||||
|
||||
def __init__(self, value: str) -> None:
|
||||
"""[ Returns a pseudo column with a constant value in all the rows]
|
||||
|
||||
Args:
|
||||
value (str): [ Value of the column ]
|
||||
"""
|
||||
self.value = value
|
||||
|
||||
def get_sql(self, quote_char: Optional[str] = None, **kwargs: Any) -> str:
|
||||
return format_alias_sql(
|
||||
format_quotes(self.value, kwargs.get("secondary_quote_char") or ""),
|
||||
self.alias or self.value,
|
||||
quote_char=quote_char,
|
||||
**kwargs
|
||||
)
|
||||
|
|
|
|||
49
frappe/query_builder/terms.py
Normal file
49
frappe/query_builder/terms.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
from typing import Any, Dict, Optional
|
||||
|
||||
from pypika.terms import Function, 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[2:-2]] = param_value
|
||||
|
||||
def get_sql(self, **kwargs):
|
||||
return f'%(param{len(self.parameters) + 1})s'
|
||||
|
||||
|
||||
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) 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)
|
||||
|
||||
|
||||
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
|
||||
|
|
@ -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) # nosemgrep
|
||||
|
||||
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
|
||||
frappe.qb.sum = _sum
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import unittest
|
|||
from typing import Callable
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder.functions import GroupConcat, Match
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Coalesce, GroupConcat, Match
|
||||
from frappe.query_builder.utils import db_type_is
|
||||
|
||||
|
||||
|
|
@ -23,7 +24,9 @@ class TestCustomFunctionsMariaDB(unittest.TestCase):
|
|||
" MATCH('Notes') AGAINST ('+text*' IN BOOLEAN MODE)", query.get_sql()
|
||||
)
|
||||
|
||||
|
||||
def test_constant_column(self):
|
||||
query = frappe.qb.from_("DocType").select("name", ConstantColumn("John").as_("User"))
|
||||
self.assertEqual(query.get_sql(), "SELECT `name`,'John' `User` FROM `tabDocType`")
|
||||
@run_only_if(db_type_is.POSTGRES)
|
||||
class TestCustomFunctionsPostgres(unittest.TestCase):
|
||||
def test_concat(self):
|
||||
|
|
@ -35,6 +38,9 @@ class TestCustomFunctionsPostgres(unittest.TestCase):
|
|||
"TO_TSVECTOR('Notes') @@ PLAINTO_TSQUERY('text')", query.get_sql()
|
||||
)
|
||||
|
||||
def test_constant_column(self):
|
||||
query = frappe.qb.from_("DocType").select("name", ConstantColumn("John").as_("User"))
|
||||
self.assertEqual(query.get_sql(), 'SELECT "name",\'John\' "User" FROM "tabDocType"')
|
||||
|
||||
class TestBuilderBase(object):
|
||||
def test_adding_tabs(self):
|
||||
|
|
@ -49,6 +55,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 +84,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):
|
||||
|
|
|
|||
|
|
@ -56,6 +56,12 @@ def get_email_address(user=None):
|
|||
def get_formatted_email(user, mail=None):
|
||||
"""get Email Address of user formatted as: `John Doe <johndoe@example.com>`"""
|
||||
fullname = get_fullname(user)
|
||||
|
||||
method = get_hook_method('get_sender_details')
|
||||
if method:
|
||||
sender_name, mail = method()
|
||||
# if method exists but sender_name is ""
|
||||
fullname = sender_name or fullname
|
||||
|
||||
if not mail:
|
||||
mail = get_email_address(user) or validate_email_address(user)
|
||||
|
|
@ -240,7 +246,9 @@ def get_traceback() -> str:
|
|||
return ""
|
||||
|
||||
trace_list = traceback.format_exception(exc_type, exc_value, exc_tb)
|
||||
return "".join(cstr(t) for t in trace_list)
|
||||
bench_path = get_bench_path() + "/"
|
||||
|
||||
return "".join(cstr(t) for t in trace_list).replace(bench_path, "")
|
||||
|
||||
def log(event, details):
|
||||
frappe.logger().info(details)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue