Merge branch 'develop' of https://github.com/frappe/frappe into select-btn-in-doctype-list
This commit is contained in:
commit
7bdd8ee003
11 changed files with 238 additions and 47 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue