Merge branch 'develop' of https://github.com/frappe/frappe into select-btn-in-doctype-list

This commit is contained in:
Shariq Ansari 2023-05-26 20:59:27 +05:30
commit 7bdd8ee003
11 changed files with 238 additions and 47 deletions

View file

@ -1119,21 +1119,6 @@ class Database:
"""Returns True if column exists in database."""
return column in self.get_table_columns(doctype)
def get_column_type(self, doctype, column):
"""Returns column type from database."""
information_schema = frappe.qb.Schema("information_schema")
table = get_table_name(doctype)
return (
frappe.qb.from_(information_schema.columns)
.select(information_schema.columns.column_type)
.where(
(information_schema.columns.table_name == table)
& (information_schema.columns.column_name == column)
)
.run(pluck=True)[0]
)
def has_index(self, table_name, index_name):
raise NotImplementedError

View file

@ -318,6 +318,21 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
as_dict=1,
)
def get_column_type(self, doctype, column):
"""Returns column type from database."""
information_schema = frappe.qb.Schema("information_schema")
table = get_table_name(doctype)
return (
frappe.qb.from_(information_schema.columns)
.select(information_schema.columns.column_type)
.where(
(information_schema.columns.table_name == table)
& (information_schema.columns.column_name == column)
)
.run(pluck=True)[0]
)
def has_index(self, table_name, index_name):
return self.sql(
"""SHOW INDEX FROM `{table_name}`

View file

@ -394,6 +394,21 @@ class PostgresDatabase(PostgresExceptionUtil, Database):
as_dict=1,
)
def get_column_type(self, doctype, column):
"""Returns column type from database."""
information_schema = frappe.qb.Schema("information_schema")
table = get_table_name(doctype)
return (
frappe.qb.from_(information_schema.columns)
.select(information_schema.columns.data_type)
.where(
(information_schema.columns.table_name == table)
& (information_schema.columns.column_name == column)
)
.run(pluck=True)[0]
)
def get_database_list(self):
return self.sql("SELECT datname FROM pg_database", pluck=True)

View file

@ -459,6 +459,10 @@ class Email:
if content_type == "text/plain":
self.text_content += self.get_payload(part)
# attach txt file from received email as well aside from saving to text_content if it has filename
if part.get_filename():
self.get_attachment(part)
elif content_type == "text/html":
self.html_content += self.get_payload(part)

View file

@ -11,6 +11,28 @@ from frappe.modules.import_file import import_file_by_path
from frappe.modules.patch_handler import _patch_mode
from frappe.utils import update_progress_bar
IMPORTABLE_DOCTYPES = [
("core", "doctype"),
("core", "page"),
("core", "report"),
("desk", "dashboard_chart_source"),
("printing", "print_format"),
("website", "web_page"),
("website", "website_theme"),
("website", "web_form"),
("website", "web_template"),
("email", "notification"),
("printing", "print_style"),
("desk", "workspace"),
("desk", "onboarding_step"),
("desk", "module_onboarding"),
("desk", "form_tour"),
("custom", "client_script"),
("core", "server_script"),
("custom", "custom_field"),
("custom", "property_setter"),
]
def sync_all(force=0, reset_permissions=False):
_patch_mode(True)
@ -71,6 +93,11 @@ def sync_for(app_name, force=0, reset_permissions=False):
]:
files.append(os.path.join(FRAPPE_PATH, "desk", "doctype", desk_module, f"{desk_module}.json"))
for module_name, document_type in IMPORTABLE_DOCTYPES:
file = os.path.join(FRAPPE_PATH, module_name, "doctype", document_type, f"{document_type}.json")
if file not in files:
files.append(file)
for module_name in frappe.local.app_modules.get(app_name) or []:
folder = os.path.dirname(frappe.get_module(app_name + "." + module_name).__file__)
files = get_doc_files(files=files, start_path=folder)
@ -97,29 +124,7 @@ def get_doc_files(files, start_path):
files = files or []
# load in sequence - warning for devs
document_types = [
"doctype",
"page",
"report",
"dashboard_chart_source",
"print_format",
"web_page",
"website_theme",
"web_form",
"web_template",
"notification",
"print_style",
"workspace",
"onboarding_step",
"module_onboarding",
"form_tour",
"client_script",
"server_script",
"custom_field",
"property_setter",
]
for doctype in document_types:
for _module, doctype in IMPORTABLE_DOCTYPES:
doctype_path = os.path.join(start_path, doctype)
if os.path.exists(doctype_path):
for docname in os.listdir(doctype_path):

View file

@ -1440,6 +1440,67 @@ Object.assign(frappe.utils, {
prepend && wrapper.prepend(button);
},
add_select_group_button(wrapper, actions, btn_type, icon = "", prepend) {
// actions = [{
// label: "Action 1",
// description: "Description 1", (optional)
// action: () => {},
// },
// {
// label: "Action 2",
// description: "Description 2", (optional)
// action: () => {},
// }]
let selected_action = actions[0];
let $select_group_button = $(`
<div class="btn-group select-group-btn">
<button type="button" class="btn ${btn_type} btn-sm selected-button">
<span class="left-icon">${icon && frappe.utils.icon(icon, "xs")}</span>
<span class="label">${selected_action.label}</span>
</button>
<button type="button" class="btn ${btn_type} btn-sm dropdown-toggle dropdown-toggle-split" data-toggle="dropdown">
${frappe.utils.icon("down", "xs")}
</button>
<ul class="dropdown-menu dropdown-menu-right" role="menu"></ul>
</div>
`);
actions.forEach((action) => {
$(`<li>
<a class="dropdown-item flex">
<div class="tick-icon mr-2">${frappe.utils.icon("check", "xs")}</div>
<div>
<div class="item-label">${action.label}</div>
<div class="item-description text-muted small">${action.description || ""}</div>
</div>
</a>
</li>`)
.appendTo($select_group_button.find(".dropdown-menu"))
.click((e) => {
selected_action = action;
$select_group_button.find(".selected-button .label").text(action.label);
$(e.currentTarget).find(".tick-icon").addClass("selected");
$(e.currentTarget).siblings().find(".tick-icon").removeClass("selected");
});
});
$select_group_button.find(".dropdown-menu li:first-child .tick-icon").addClass("selected");
$select_group_button.find(".selected-button").click((event) => {
event.stopPropagation();
selected_action.action && selected_action.action(event);
});
!prepend && $select_group_button.appendTo(wrapper);
prepend && wrapper.prepend($select_group_button);
return $select_group_button;
},
sleep(time) {
return new Promise((resolve) => setTimeout(resolve, time));
},

View file

@ -56,7 +56,7 @@ frappe.views.CommunicationComposer = class {
},
{
fieldtype: "Button",
label: frappe.utils.icon("down"),
label: frappe.utils.icon("down", "xs"),
fieldname: "option_toggle_button",
click: () => {
this.toggle_more_options();
@ -77,12 +77,22 @@ frappe.views.CommunicationComposer = class {
fieldtype: "MultiSelect",
fieldname: "bcc",
},
{
fieldtype: "Section Break",
fieldname: "email_template_section_break",
hidden: 1,
},
{
label: __("Email Template"),
fieldtype: "Link",
options: "Email Template",
fieldname: "email_template",
},
{
fieldtype: "HTML",
label: __("Clear & Add template"),
fieldname: "clear_and_add_template",
},
{ fieldtype: "Section Break" },
{
label: __("Subject"),
@ -170,8 +180,9 @@ frappe.views.CommunicationComposer = class {
toggle_more_options(show_options) {
show_options = show_options || this.dialog.fields_dict.more_options.df.hidden;
this.dialog.set_df_property("more_options", "hidden", !show_options);
this.dialog.set_df_property("email_template_section_break", "hidden", !show_options);
const label = frappe.utils.icon(show_options ? "up-line" : "down");
const label = frappe.utils.icon(show_options ? "up-line" : "down", "xs");
this.dialog.get_field("option_toggle_button").set_label(label);
}
@ -266,13 +277,14 @@ frappe.views.CommunicationComposer = class {
setup_email_template() {
const me = this;
this.dialog.fields_dict["email_template"].df.onchange = () => {
const fields = this.dialog.fields_dict;
const clear_and_add_template = $(fields.clear_and_add_template.wrapper);
function add_template() {
const email_template = me.dialog.fields_dict.email_template.get_value();
if (!email_template) return;
function prepend_reply(reply) {
if (me.reply_added === email_template) return;
const content_field = me.dialog.fields_dict.content;
const subject_field = me.dialog.fields_dict.subject;
@ -280,8 +292,6 @@ frappe.views.CommunicationComposer = class {
content_field.set_value(`${reply.message}<br>${content}`);
subject_field.set_value(reply.subject);
me.reply_added = email_template;
}
frappe.call({
@ -294,7 +304,25 @@ frappe.views.CommunicationComposer = class {
prepend_reply(r.message);
},
});
};
}
let email_template_actions = [
{
label: __("Add Template"),
description: __("Prepend the template to the email message"),
action: () => add_template(),
},
{
label: __("Clear & Add Template"),
description: __("Clear the email message and add the template"),
action: () => {
me.dialog.fields_dict.content.set_value("");
add_template();
},
},
];
frappe.utils.add_select_group_button(clear_and_add_template, email_template_actions);
}
setup_last_edited_communication() {

View file

@ -222,12 +222,30 @@ body.modal-open[style^="padding-right"] {
margin-bottom: -24px;
button {
// same as form-control input
height: calc(1.5em + .75rem + 2px);
height: calc(1.5em + .7rem);
}
}
}
}
.modal [data-fieldname="email_template_section_break"] {
form {
display: flex;
align-items: center;
.frappe-control:first-child {
&[data-fieldname="email_template"] {
margin-right: 10px;
}
flex: 1;
}
.frappe-control:last-child {
margin-bottom: -8px;
}
}
}
// modal is xs (for grids)
.modal .hidden-xs {
display: none !important;

View file

@ -249,6 +249,26 @@ h2 {
}
}
.select-group-btn {
.dropdown-toggle-split::after {
display: none;
}
.dropdown-item {
.tick-icon {
visibility: hidden;
&.selected {
visibility: visible;
}
}
.item-label {
font-weight: 500;
}
}
}
.btn-xs {
@extend .btn-sm;
line-height: 1.2;

View file

@ -41,6 +41,16 @@ class Timestamp(Function):
super().__init__("TIMESTAMP", term, alias=alias)
class Round(Function):
def __init__(self, term, decimal=0, **kwargs):
super().__init__("ROUND", term, decimal, **kwargs)
class Truncate(Function):
def __init__(self, term, decimal, **kwargs):
super().__init__("TRUNCATE", term, decimal, **kwargs)
GroupConcat = ImportMapper({db_type_is.MARIADB: GROUP_CONCAT, db_type_is.POSTGRES: STRING_AGG})
Match = ImportMapper({db_type_is.MARIADB: MATCH, db_type_is.POSTGRES: TO_TSVECTOR})

View file

@ -13,6 +13,8 @@ from frappe.query_builder.functions import (
Date,
GroupConcat,
Match,
Round,
Truncate,
UnixTimestamp,
)
from frappe.query_builder.utils import db_type_is
@ -153,6 +155,20 @@ class TestCustomFunctionsMariaDB(FrappeTestCase):
"SELECT `tabred`.`other`,CONCAT(`tabNote`.`name`,'') FROM `tabred`,`tabNote`",
)
def test_round(self):
note = frappe.qb.DocType("Note")
query = frappe.qb.from_(note).select(Round(note.price))
self.assertEqual("select round(`price`,0) from `tabnote`", str(query).lower())
query = frappe.qb.from_(note).select(Round(note.price, 3))
self.assertEqual("select round(`price`,3) from `tabnote`", str(query).lower())
def test_truncate(self):
note = frappe.qb.DocType("Note")
query = frappe.qb.from_(note).select(Truncate(note.price, 3))
self.assertEqual("select truncate(`price`,3) from `tabnote`", str(query).lower())
@run_only_if(db_type_is.POSTGRES)
class TestCustomFunctionsPostgres(FrappeTestCase):
@ -283,6 +299,20 @@ class TestCustomFunctionsPostgres(FrappeTestCase):
'SELECT "tabred"."other",CAST("tabNote"."name" AS VARCHAR) FROM "tabred","tabNote"',
)
def test_round(self):
note = frappe.qb.DocType("Note")
query = frappe.qb.from_(note).select(Round(note.price))
self.assertEqual('select round("price",0) from "tabnote"', str(query).lower())
query = frappe.qb.from_(note).select(Round(note.price, 3))
self.assertEqual('select round("price",3) from "tabnote"', str(query).lower())
def test_truncate(self):
note = frappe.qb.DocType("Note")
query = frappe.qb.from_(note).select(Truncate(note.price, 3))
self.assertEqual('select truncate("price",3) from "tabnote"', str(query).lower())
class TestBuilderBase:
def test_adding_tabs(self):