Merge branch 'develop' into app-switcher

This commit is contained in:
Shariq Ansari 2024-08-12 16:29:34 +05:30 committed by GitHub
commit 11b699c60d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 174 additions and 132 deletions

View file

@ -239,8 +239,7 @@
"options": "Has Role",
"permlevel": 1,
"print_hide": 1,
"read_only": 1,
"show_on_timeline": 1
"read_only": 1
},
{
"collapsible": 1,
@ -431,8 +430,7 @@
"hidden": 1,
"label": "Block Modules",
"options": "Block Module",
"permlevel": 1,
"show_on_timeline": 1
"permlevel": 1
},
{
"fieldname": "home_settings",
@ -813,7 +811,7 @@
"link_fieldname": "user"
}
],
"modified": "2024-08-08 19:09:17.399748",
"modified": "2024-08-12 15:23:38.996646",
"modified_by": "Administrator",
"module": "Core",
"name": "User",

View file

@ -144,7 +144,7 @@ class Database:
def _transform_query(self, query: Query, values: QueryValues) -> tuple:
return query, values
def _transform_result(self, result: list[tuple]) -> list[tuple]:
def _transform_result(self, result: list[tuple] | tuple[tuple]) -> tuple[tuple]:
return result
def _clean_up(self):

View file

@ -225,6 +225,9 @@ class PostgresDatabase(PostgresExceptionUtil, Database):
)
return db_size[0].get("database_size")
def _transform_result(self, result: list[tuple] | tuple[tuple]) -> tuple[tuple]:
return tuple(result) if isinstance(result, list) else result
# pylint: disable=W0221
def sql(self, query, values=EmptyQueryValues, *args, **kwargs):
return super().sql(modify_query(query), modify_values(values), *args, **kwargs)

View file

@ -175,11 +175,17 @@ frappe.ui.form.on("Email Account", {
delete locals["User"][frappe.route_flags.linked_user];
}
if (frappe.boot.developer_mode && !frm.is_dirty() && frm.doc.enable_incoming) {
if (!frm.is_dirty() && frm.doc.enable_incoming) {
frm.add_custom_button(__("Pull Emails"), () => {
frappe.dom.freeze(__("Pulling emails..."));
frm.call({
method: "pull_emails",
args: { email_account: frm.doc.name },
}).then((r) => {
frappe.dom.unfreeze();
if (!(r._server_messages && r._server_messages.length)) {
frappe.show_alert({ message: __("Emails Pulled"), indicator: "green" });
}
});
});
}

View file

@ -916,7 +916,13 @@ def pull_emails(email_account: str) -> None:
"""Pull emails from given email account."""
frappe.has_permission("Email Account", "read", throw=True)
pull_from_email_account(email_account)
job_name = f"pull_from_email_account|{email_account}"
queued_jobs = get_jobs(site=frappe.local.site, key="job_name")[frappe.local.site]
if job_name not in queued_jobs:
pull_from_email_account(email_account)
else:
frappe.msgprint(_("Emails are already being pulled from this account."))
def pull_from_email_account(email_account):

View file

@ -164,7 +164,6 @@ frappe.ui.form.on("Notification", {
},
};
});
frm.preview_fields = frm.doc.__onload.preview_fields;
},
refresh: function (frm) {
frappe.notification.setup_fieldname_select(frm);
@ -185,7 +184,15 @@ frappe.ui.form.on("Notification", {
const args = {
doc: frm.doc,
doctype: frm.doc.document_type,
preview_fields: frm.preview_fields,
preview_fields: [
{
label: __("Meets Condition?"),
fieldtype: "Data",
method: "preview_meets_condition",
},
{ label: __("Subject"), fieldtype: "Data", method: "preview_subject" },
{ label: __("Message"), fieldtype: "Code", method: "preview_message" },
],
};
let dialog = new frappe.views.RenderPreviewer(args);
return dialog;

View file

@ -73,14 +73,6 @@ class Notification(Document):
"""load message"""
if self.is_standard:
self.message = self.get_template()
self.set_onload(
"preview_fields",
[
{"label": _("Meets Condition?"), "fieldtype": "Data", "method": "preview_meets_condition"},
{"label": _("Subject"), "fieldtype": "Data", "method": "preview_subject"},
{"label": _("Message"), "fieldtype": "Code", "method": "preview_message"},
],
)
def autoname(self):
if not self.name:

View file

@ -85,6 +85,29 @@ frappe.ui.form.on("Webhook", {
"background_jobs_queue",
"frappe.integrations.doctype.webhook.webhook.get_all_queues"
);
if (frm.doc.webhook_doctype) {
frm.add_custom_button(__("Preview"), () => {
const args = {
doc: frm.doc,
doctype: frm.doc.webhook_doctype,
preview_fields: [
{
label: __("Meets Condition?"),
fieldtype: "Data",
method: "preview_meets_condition",
},
{
label: __("Request Body"),
fieldtype: "Code",
method: "preview_request_body",
},
],
};
let dialog = new frappe.views.RenderPreviewer(args);
return dialog;
});
}
},
request_structure: (frm) => {
@ -98,17 +121,6 @@ frappe.ui.form.on("Webhook", {
enable_security: (frm) => {
frm.toggle_reqd("webhook_secret", frm.doc.enable_security);
},
preview_document: (frm) => {
frappe.call({
method: "generate_preview",
doc: frm.doc,
callback: (r) => {
frm.refresh_field("meets_condition");
frm.refresh_field("preview_request_body");
},
});
},
});
frappe.ui.form.on("Webhook Data", {

View file

@ -30,13 +30,7 @@
"webhook_headers",
"sb_webhook_data",
"webhook_data",
"webhook_json",
"preview_tab",
"preview_document",
"column_break_26",
"meets_condition",
"section_break_28",
"preview_request_body"
"webhook_json"
],
"fields": [
{
@ -169,37 +163,6 @@
"options": "POST\nPUT\nDELETE",
"reqd": 1
},
{
"fieldname": "preview_tab",
"fieldtype": "Tab Break",
"label": "Preview"
},
{
"fieldname": "preview_document",
"fieldtype": "Dynamic Link",
"label": "Select Document",
"options": "webhook_doctype"
},
{
"fieldname": "preview_request_body",
"fieldtype": "Code",
"is_virtual": 1,
"label": "Request Body"
},
{
"fieldname": "meets_condition",
"fieldtype": "Data",
"is_virtual": 1,
"label": "Meets Condition?"
},
{
"fieldname": "column_break_26",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_28",
"fieldtype": "Section Break"
},
{
"default": "0",
"description": "On checking this option, URL will be treated like a jinja template string",
@ -226,7 +189,7 @@
"link_fieldname": "webhook"
}
],
"modified": "2024-03-23 16:04:03.108172",
"modified": "2024-07-22 09:23:32.642172",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Webhook",
@ -250,4 +213,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View file

@ -36,9 +36,6 @@ class Webhook(Document):
enable_security: DF.Check
enabled: DF.Check
is_dynamic_url: DF.Check
meets_condition: DF.Data | None
preview_document: DF.DynamicLink | None
preview_request_body: DF.Code | None
request_method: DF.Literal["POST", "PUT", "DELETE"]
request_structure: DF.Literal["", "Form URL-Encoded", "JSON"]
request_url: DF.SmallText
@ -119,35 +116,24 @@ class Webhook(Document):
frappe.throw(_("Invalid Webhook Secret"))
@frappe.whitelist()
def generate_preview(self):
# This function doesn't need to do anything specific as virtual fields
# get evaluated automatically.
pass
@property
def meets_condition(self):
def preview_meets_condition(self, preview_document):
if not self.condition:
return _("Yes")
if not (self.preview_document and self.webhook_doctype):
return _("Select a document to check if it meets conditions.")
try:
doc = frappe.get_cached_doc(self.webhook_doctype, self.preview_document)
doc = frappe.get_cached_doc(self.webhook_doctype, preview_document)
met_condition = frappe.safe_eval(self.condition, eval_locals=get_context(doc))
except Exception as e:
frappe.local.message_log = []
return _("Failed to evaluate conditions: {}").format(e)
return _("Yes") if met_condition else _("No")
@property
def preview_request_body(self):
if not (self.preview_document and self.webhook_doctype):
return _("Select a document to preview request data")
@frappe.whitelist()
def preview_request_body(self, preview_document):
try:
doc = frappe.get_cached_doc(self.webhook_doctype, self.preview_document)
doc = frappe.get_cached_doc(self.webhook_doctype, preview_document)
return frappe.as_json(get_webhook_data(doc, self))
except Exception as e:
frappe.local.message_log = []
return _("Failed to compute request body: {}").format(e)

View file

@ -25,13 +25,15 @@ frappe.ui.form.make_quick_entry = (doctype, after_insert, init_callback, doc, fo
return frappe.quick_entry.setup();
};
frappe.ui.form.QuickEntryForm = class QuickEntryForm {
frappe.ui.form.QuickEntryForm = class QuickEntryForm extends frappe.ui.Dialog {
constructor(doctype, after_insert, init_callback, doc, force) {
super({ auto_make: false });
this.doctype = doctype;
this.after_insert = after_insert;
this.init_callback = init_callback;
this.doc = doc;
this.force = force ? force : false;
this.dialog = this; // for backward compatibility
}
setup() {
@ -132,29 +134,39 @@ frappe.ui.form.QuickEntryForm = class QuickEntryForm {
this.script_manager.setup();
}
get mandatory() {
// Backwards compatibility
console.warn("QuickEntryForm: .mandatory is deprecated, use .docfields instead");
return this.docfields;
}
set mandatory(value) {
// Backwards compatibility
console.warn("QuickEntryForm: .mandatory is deprecated, use .docfields instead");
this.docfields = value;
}
render_dialog() {
var me = this;
this.dialog = new frappe.ui.Dialog({
title: this.get_title(),
fields: this.docfields,
doc: this.doc,
});
this.fields = this.docfields;
this.title = this.get_title();
super.make();
this.register_primary_action();
this.render_edit_in_full_page_link();
this.setup_cmd_enter_for_save();
this.dialog.onhide = () => (frappe.quick_entry = null);
this.dialog.show();
this.onhide = () => (frappe.quick_entry = null);
this.show();
this.dialog.refresh_dependency();
this.refresh_dependency();
this.set_defaults();
this.script_manager.trigger("refresh");
if (this.init_callback) {
this.init_callback(this.dialog);
this.init_callback(this);
}
}
@ -170,7 +182,7 @@ frappe.ui.form.QuickEntryForm = class QuickEntryForm {
register_primary_action() {
var me = this;
this.dialog.set_primary_action(__("Save"), function () {
this.set_primary_action(__("Save"), function () {
if (me.dialog.working) {
return;
}
@ -247,14 +259,14 @@ frappe.ui.form.QuickEntryForm = class QuickEntryForm {
process_after_insert(r) {
// delete the old doc
frappe.model.clear_doc(this.dialog.doc.doctype, this.dialog.doc.name);
this.dialog.doc = r.message;
frappe.model.clear_doc(this.doc.doctype, this.doc.name);
this.doc = r.message;
if (this.script_manager.has_handler("after_save")) {
return this.script_manager.trigger("after_save");
} else if (frappe._from_link) {
frappe.ui.form.update_calling_link(this.dialog.doc);
frappe.ui.form.update_calling_link(this.doc);
} else if (this.after_insert) {
this.after_insert(this.dialog.doc);
this.after_insert(this.doc);
} else {
this.open_form_if_not_list();
}
@ -263,7 +275,7 @@ frappe.ui.form.QuickEntryForm = class QuickEntryForm {
setup_cmd_enter_for_save() {
var me = this;
// ctrl+enter to save
this.dialog.wrapper.keydown(function (e) {
this.wrapper.keydown(function (e) {
if ((e.ctrlKey || e.metaKey) && e.which == 13) {
if (!frappe.request.ajax_count) {
// not already working -- double entry
@ -278,7 +290,7 @@ frappe.ui.form.QuickEntryForm = class QuickEntryForm {
open_form_if_not_list() {
if (this.meta.issingle) return;
let route = frappe.get_route();
let doc = this.dialog.doc;
let doc = this.doc;
if (route && !(route[0] === "List" && route[1] === doc.doctype)) {
frappe.run_serially([() => frappe.set_route("Form", doc.doctype, doc.name)]);
}
@ -286,17 +298,17 @@ frappe.ui.form.QuickEntryForm = class QuickEntryForm {
update_doc() {
var me = this;
var data = this.dialog.get_values(true);
var data = this.get_values(true);
$.each(data, function (key, value) {
if (!is_null(value)) {
me.dialog.doc[key] = value;
}
});
return this.dialog.doc;
return this.doc;
}
open_doc(set_hooks) {
this.dialog.hide();
this.hide();
this.update_doc();
if (set_hooks && this.after_insert) {
frappe.route_options = frappe.route_options || {};
@ -309,13 +321,13 @@ frappe.ui.form.QuickEntryForm = class QuickEntryForm {
render_edit_in_full_page_link() {
if (this.force || this.hide_full_form_button) return;
this.dialog.add_custom_action(__("Edit Full Form"), () => this.open_doc(true));
this.add_custom_action(__("Edit Full Form"), () => this.open_doc(true));
}
set_defaults() {
var me = this;
// set defaults
$.each(this.dialog.fields_dict, function (fieldname, field) {
$.each(this.fields_dict, function (fieldname, field) {
field.doctype = me.doc.doctype;
field.docname = me.doc.name;

View file

@ -245,6 +245,7 @@ frappe.ui.form.ScriptManager = class ScriptManager {
this.trigger("setup");
}
log_error(caller, e) {
frappe.show_alert({ message: __("Error in Client Script."), indicator: "error" });
console.group && console.group();

View file

@ -790,7 +790,9 @@ $.extend(frappe.model, {
}
for (var i = 0, j = fieldnames.length; i < j; i++) {
var fieldname = fieldnames[i];
doc[fieldname] = flt(doc[fieldname], precision(fieldname, doc));
if (doc[fieldname]) {
doc[fieldname] = flt(doc[fieldname], precision(fieldname, doc));
}
}
},

View file

@ -13,8 +13,10 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
this.display = false;
this.is_dialog = true;
$.extend(this, { animate: true, size: null }, opts);
this.make();
$.extend(this, { animate: true, size: null, auto_make: true }, opts);
if (this.auto_make) {
this.make();
}
}
make() {
@ -127,10 +129,6 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
});
}
get $backdrop() {
return $(this.$wrapper.data("bs.modal")?._backdrop);
}
set_modal_size() {
if (!this.fields) {
this.size = "";
@ -259,7 +257,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
this.$wrapper.removeClass("modal-minimize");
if (this.minimizable && this.is_minimized) {
this.$backdrop.show();
$(".modal-backdrop").toggle();
this.is_minimized = false;
}
@ -272,10 +270,6 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
}
hide() {
if (this.animate && this.animation_speed === "slow") {
this.$wrapper.addClass("slow");
this.$backdrop.addClass("slow");
}
this.$wrapper.modal("hide");
this.is_visible = false;
}
@ -297,7 +291,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
}
toggle_minimize() {
this.$backdrop.toggle();
$(".modal-backdrop").toggle();
let modal = this.$wrapper.closest(".modal").toggleClass("modal-minimize");
modal.attr("tabindex") ? modal.removeAttr("tabindex") : modal.attr("tabindex", -1);
this.is_minimized = !this.is_minimized;
@ -323,6 +317,8 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
action && action_button.click(action);
}
add_custom_button() {}
};
frappe.ui.hide_open_dialog = () => {

View file

@ -6,6 +6,8 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
constructor(opts) {
super(opts);
this.dirty = false;
this.fetch_dict = {};
$.each(this.fields || [], function (i, f) {
if (!f.fieldname && f.label) {
f.fieldname = f.label.replace(/ /g, "_").toLowerCase();
@ -197,4 +199,43 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
field.df[prop] = value;
field.refresh();
}
set_query(fieldname, opt1, opt2) {
if (opt2) {
// on child table
// set_query(fieldname, parent fieldname, query)
if (this.fields_dict[opt1])
this.fields_dict[opt1].grid.get_field(fieldname).get_query = opt2;
} else {
// on parent table
// set_query(fieldname, query)
if (this.fields_dict[fieldname]) {
this.fields_dict[fieldname].get_query = opt1;
}
}
}
// UTILITIES
add_fetch(link_field, source_field, target_field, target_doctype) {
/*
Example fetch dict to get sender_email from email_id field in sender:
{
"Notification": {
"sender": {
"sender_email": "email_id"
}
}
}
*/
if (!target_doctype) target_doctype = "*";
// Target field kept as key because source field could be non-unique
this.fetch_dict.setDefault(target_doctype, {}).setDefault(link_field, {})[target_field] =
source_field;
}
is_new() {
return this.doc.__islocal;
}
};

View file

@ -572,6 +572,20 @@ class TestDB(FrappeTestCase):
frappe.db.rollback()
def test_get_list_return_value_data_type(self):
frappe.db.delete("Note")
frappe.get_doc(doctype="Note", title="note1", content="something").insert()
frappe.get_doc(doctype="Note", title="note2", content="someting else").insert()
note_docs = frappe.db.sql("select * from `tabNote`")
# should return both records
self.assertEqual(len(note_docs), 2)
# data-type should be list
self.assertIsInstance(note_docs, tuple)
@run_only_if(db_type_is.POSTGRES)
def test_modify_query(self):
from frappe.database.postgres.database import modify_query
@ -1111,9 +1125,9 @@ class TestPostgresSchemaQueryIndependence(ExtFrappeTestCase):
if frappe.db.sql(
"""SELECT 1
FROM information_schema.schemata
WHERE schema_name = 'alt_schema'
limit 1 """
FROM information_schema.schemata
WHERE schema_name = 'alt_schema'
LIMIT 1 """
):
self.cleanup()
@ -1244,19 +1258,19 @@ class TestPostgresSchemaQueryIndependence(ExtFrappeTestCase):
rows = frappe.db.sql(f'select * from "tab{self.test_table_name}"')
self.assertEqual(
rows,
[
(
(
"a",
"b",
)
],
),
),
) # there should be a single row in the public table
# when schema is changed to alt_schema, the alt_schema tables should be addressed by search path
frappe.conf["db_schema"] = "alt_schema"
frappe.db.connect()
rows = frappe.db.sql(f'select * from "tab{self.test_table_name}"')
self.assertEqual(rows, []) # there are no records in the alt_schema table
self.assertEqual(rows, ()) # there are no records in the alt_schema table
del frappe.conf["db_schema"]

View file

@ -202,6 +202,9 @@ def schedule_jobs_based_on_activity(check_time=None):
def is_dormant(check_time=None):
# Assume never dormant if developer_mode is enabled
if frappe.conf.developer_mode:
return False
last_activity_log_timestamp = _get_last_creation_timestamp("Activity Log")
since = (frappe.get_system_settings("dormant_days") or 4) * 86400
if not last_activity_log_timestamp: