Merge branch 'develop' into setup-postgres-ci
This commit is contained in:
commit
4e50fc2dcf
72 changed files with 10109 additions and 7233 deletions
|
|
@ -78,12 +78,13 @@ def read_doc(doctype: str, name: str):
|
|||
doc = frappe.get_doc(doctype, name)
|
||||
doc.check_permission("read")
|
||||
doc.apply_fieldlevel_read_permissions()
|
||||
doc_dict = doc.as_dict()
|
||||
if sbool(frappe.form_dict.get("expand_links")):
|
||||
doc_dict = doc.as_dict()
|
||||
get_values_for_link_and_dynamic_link_fields(doc_dict)
|
||||
get_values_for_table_and_multiselect_fields(doc_dict)
|
||||
return doc_dict
|
||||
|
||||
return doc_dict
|
||||
return doc
|
||||
|
||||
|
||||
def get_values_for_link_and_dynamic_link_fields(doc_dict):
|
||||
|
|
|
|||
|
|
@ -85,14 +85,17 @@ frappe.ui.form.on("Auto Repeat", {
|
|||
},
|
||||
|
||||
preview_message: function (frm) {
|
||||
if (frm.is_dirty()) {
|
||||
frappe.msgprint(__("Please save the form before previewing the message"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (frm.doc.message) {
|
||||
frappe.call({
|
||||
method: "frappe.automation.doctype.auto_repeat.auto_repeat.generate_message_preview",
|
||||
type: "POST",
|
||||
args: {
|
||||
reference_dt: frm.doc.reference_doctype,
|
||||
reference_doc: frm.doc.reference_document,
|
||||
subject: frm.doc.subject,
|
||||
message: frm.doc.message,
|
||||
name: frm.doc.name,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
|
|
|
|||
|
|
@ -605,14 +605,15 @@ def update_reference(docname: str, reference: str):
|
|||
return "success" # backward compatbility
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def generate_message_preview(reference_dt, reference_doc, message=None, subject=None):
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
def generate_message_preview(name: str):
|
||||
frappe.has_permission("Auto Repeat", "write", throw=True)
|
||||
doc = frappe.get_doc(reference_dt, reference_doc)
|
||||
auto_repeat = frappe.get_doc("Auto Repeat", str(name))
|
||||
doc = frappe.get_doc(auto_repeat.reference_doctype, auto_repeat.reference_document)
|
||||
doc.check_permission()
|
||||
subject_preview = _("Please add a subject to your email")
|
||||
msg_preview = frappe.render_template(message, {"doc": doc})
|
||||
if subject:
|
||||
subject_preview = frappe.render_template(subject, {"doc": doc})
|
||||
msg_preview = frappe.render_template(auto_repeat.message, {"doc": doc})
|
||||
if auto_repeat.subject:
|
||||
subject_preview = frappe.render_template(auto_repeat.subject, {"doc": doc})
|
||||
|
||||
return {"message": msg_preview, "subject": subject_preview}
|
||||
|
|
|
|||
|
|
@ -34,9 +34,10 @@
|
|||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-05-21 17:09:55.054044",
|
||||
"modified": "2025-10-29 11:26:28.177653",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "API Request Log",
|
||||
|
|
|
|||
|
|
@ -63,9 +63,12 @@ class TestPreparedReport(IntegrationTestCase):
|
|||
self.assertEqual(len(prepared_data["result"]), len(generated_data["result"]))
|
||||
self.assertEqual(len(prepared_data), len(generated_data))
|
||||
|
||||
@run_only_if(db_type_is.MARIADB)
|
||||
def test_start_status_and_kill_jobs(self):
|
||||
with test_report(report_type="Query Report", query="select sleep(10)") as report:
|
||||
if frappe.db.db_type == "postgres":
|
||||
query = "select pg_sleep(5)"
|
||||
elif frappe.db.db_type == "mariadb":
|
||||
query = "select sleep(5)"
|
||||
with test_report(report_type="Query Report", query=query) as report:
|
||||
doc = self.create_prepared_report(report.name)
|
||||
self.wait_for_status(doc, "Started")
|
||||
job_id = doc.job_id
|
||||
|
|
|
|||
|
|
@ -51,12 +51,15 @@
|
|||
"column_break_34",
|
||||
"allow_login_after_fail",
|
||||
"two_factor_authentication",
|
||||
"column_break_odhl",
|
||||
"enable_two_factor_auth",
|
||||
"bypass_2fa_for_retricted_ip_users",
|
||||
"bypass_restrict_ip_check_if_2fa_enabled",
|
||||
"column_break_bzfr",
|
||||
"two_factor_method",
|
||||
"lifespan_qrcode_image",
|
||||
"otp_issuer_name",
|
||||
"otp_sms_template",
|
||||
"password_tab",
|
||||
"password_settings",
|
||||
"logout_on_password_reset",
|
||||
|
|
@ -752,12 +755,27 @@
|
|||
"fieldtype": "Select",
|
||||
"label": "Show External Link Warning",
|
||||
"options": "Never\nAsk\nAlways"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_odhl",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.enable_two_factor_auth==1 && doc.two_factor_method==\"SMS\"",
|
||||
"description": "OTP placeholder should be defined as <code>{{ otp }}</code> ",
|
||||
"fieldname": "otp_sms_template",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "OTP SMS Template"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_bzfr",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-09-24 16:04:02.016562",
|
||||
"modified": "2025-11-04 16:47:54.230874",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "System Settings",
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ class SystemSettings(Document):
|
|||
"#,###",
|
||||
]
|
||||
otp_issuer_name: DF.Data | None
|
||||
otp_sms_template: DF.SmallText | None
|
||||
password_reset_limit: DF.Int
|
||||
rate_limit_email_link_login: DF.Int
|
||||
reset_password_link_expiry_duration: DF.Duration | None
|
||||
|
|
@ -145,6 +146,7 @@ class SystemSettings(Document):
|
|||
self.validate_user_pass_login()
|
||||
self.validate_backup_limit()
|
||||
self.validate_file_extensions()
|
||||
self.validate_otp_sms_template()
|
||||
|
||||
if not self.link_field_results_limit:
|
||||
self.link_field_results_limit = 10
|
||||
|
|
@ -156,6 +158,17 @@ class SystemSettings(Document):
|
|||
_("{0} can not be more than {1}").format(label, 50), alert=True, indicator="yellow"
|
||||
)
|
||||
|
||||
def validate_otp_sms_template(self):
|
||||
if not self.enable_two_factor_auth or self.two_factor_method != "SMS" or not self.otp_sms_template:
|
||||
return
|
||||
|
||||
if "{{otp}}" not in self.otp_sms_template.replace(" ", ""):
|
||||
frappe.throw(
|
||||
_("OTP SMS Template must contain <code>{0}</code> placeholder to insert the OTP.").format(
|
||||
"{{otp}}"
|
||||
)
|
||||
)
|
||||
|
||||
def validate_user_pass_login(self):
|
||||
if not self.disable_user_pass_login:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -171,9 +171,13 @@ frappe.ui.form.on("Auto Email Report", {
|
|||
.appendTo(row);
|
||||
});
|
||||
|
||||
// remove mandatory but hidden filters from dialog
|
||||
const dialog_filter_fields = report_filters.filter(
|
||||
(f) => !(f.hidden == 1 && f.reqd == 1)
|
||||
);
|
||||
table.on("click", function () {
|
||||
dialog = new frappe.ui.Dialog({
|
||||
fields: report_filters,
|
||||
fields: dialog_filter_fields,
|
||||
primary_action: function () {
|
||||
var values = this.get_values();
|
||||
if (values) {
|
||||
|
|
|
|||
|
|
@ -124,7 +124,9 @@ class AutoEmailReport(Document):
|
|||
filters = frappe.parse_json(self.filters) if self.filters else {}
|
||||
filter_meta = frappe.parse_json(self.filter_meta) if self.filter_meta else {}
|
||||
throw_list = [
|
||||
meta["label"] for meta in filter_meta if meta.get("reqd") and not filters.get(meta["fieldname"])
|
||||
meta["label"]
|
||||
for meta in filter_meta
|
||||
if (meta.get("reqd") and (not meta.get("hidden"))) and not filters.get(meta["fieldname"])
|
||||
]
|
||||
if throw_list:
|
||||
frappe.throw(
|
||||
|
|
|
|||
|
|
@ -105,6 +105,14 @@ frappe.notification = {
|
|||
"options",
|
||||
[""].concat(["owner"]).concat(receiver_fields)
|
||||
);
|
||||
|
||||
// set options for "From Attach Field"
|
||||
let attach_fields = fields.filter((d) => d.fieldtype === "Attach");
|
||||
let attach_options = $.map(attach_fields, function (d) {
|
||||
return get_select_options(d);
|
||||
});
|
||||
|
||||
frm.set_df_property("from_attach_field", "options", [""].concat(attach_options));
|
||||
});
|
||||
},
|
||||
setup_example_message: function (frm) {
|
||||
|
|
|
|||
|
|
@ -48,7 +48,10 @@
|
|||
"view_properties",
|
||||
"column_break_25",
|
||||
"attach_print",
|
||||
"print_format"
|
||||
"print_format",
|
||||
"column_break_llcs",
|
||||
"attach_files",
|
||||
"from_attach_field"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -260,7 +263,7 @@
|
|||
"collapsible_depends_on": "attach_print",
|
||||
"fieldname": "column_break_25",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Print Settings"
|
||||
"label": "Attachment Settings"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
|
|
@ -337,13 +340,30 @@
|
|||
"fieldname": "filters_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Filters"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_llcs",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "attach_files",
|
||||
"fieldtype": "Select",
|
||||
"label": "Attach Files",
|
||||
"options": "\nFrom Field\nAll"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.attach_files === \"From Field\"",
|
||||
"fieldname": "from_attach_field",
|
||||
"fieldtype": "Select",
|
||||
"label": "From Attach Field",
|
||||
"mandatory_depends_on": "eval:doc.attach_files === \"From Field\""
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "fa fa-envelope",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-21 18:54:38.454748",
|
||||
"modified": "2025-10-21 23:14:52.345857",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Notification",
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ class Notification(Document):
|
|||
from frappe.email.doctype.notification_recipient.notification_recipient import NotificationRecipient
|
||||
from frappe.types import DF
|
||||
|
||||
attach_files: DF.Literal["", "From Field", "All"]
|
||||
attach_print: DF.Check
|
||||
channel: DF.Literal["Email", "Slack", "System Notification", "SMS"]
|
||||
condition: DF.Code | None
|
||||
|
|
@ -59,6 +60,7 @@ class Notification(Document):
|
|||
"Custom",
|
||||
]
|
||||
filters: DF.Code | None
|
||||
from_attach_field: DF.Literal[None]
|
||||
is_standard: DF.Check
|
||||
message: DF.Code | None
|
||||
message_type: DF.Literal["Markdown", "HTML", "Plain Text"]
|
||||
|
|
@ -166,6 +168,9 @@ class Notification(Document):
|
|||
if self.event == "Value Change" and not self.value_changed:
|
||||
frappe.throw(_("Please specify which value field must be checked"))
|
||||
|
||||
if self.attach_files == "From Field" and not self.from_attach_field:
|
||||
frappe.throw(_("Please specify the field from which to attach files"))
|
||||
|
||||
self.validate_forbidden_document_types()
|
||||
self.validate_condition()
|
||||
self.validate_filters()
|
||||
|
|
@ -445,7 +450,7 @@ def get_context(context):
|
|||
"subject": subject,
|
||||
"from_user": doc.modified_by or doc.owner,
|
||||
"email_content": frappe.render_template(self.message, context),
|
||||
"attached_file": attachments and json.dumps(attachments[0]),
|
||||
"attached_file": json.dumps(attachments) if attachments else None,
|
||||
}
|
||||
enqueue_create_notification(users, notification_doc)
|
||||
|
||||
|
|
@ -490,6 +495,13 @@ def get_context(context):
|
|||
comm = frappe.get_lazy_doc("Communication", communication)
|
||||
comm.get_outgoing_email_account()
|
||||
|
||||
# We expect at most one print format attachment, but we don't know where it is.
|
||||
print_letterhead = any(
|
||||
attachment.get("print_letterhead")
|
||||
for attachment in attachments
|
||||
if attachment.get("print_format_attachment") == 1
|
||||
)
|
||||
|
||||
frappe.sendmail(
|
||||
recipients=recipients,
|
||||
subject=subject,
|
||||
|
|
@ -501,7 +513,7 @@ def get_context(context):
|
|||
reference_name=get_reference_name(doc),
|
||||
attachments=attachments,
|
||||
expose_recipients="header",
|
||||
print_letterhead=((attachments and attachments[0].get("print_letterhead")) or False),
|
||||
print_letterhead=print_letterhead,
|
||||
communication=communication,
|
||||
)
|
||||
|
||||
|
|
@ -624,10 +636,28 @@ def get_context(context):
|
|||
|
||||
return list(set(receiver_list))
|
||||
|
||||
def get_attachment(self, doc):
|
||||
"""check print settings are attach the pdf"""
|
||||
if not self.attach_print:
|
||||
return None
|
||||
def get_attachment(self, doc) -> list[dict]:
|
||||
"""Check Attachment Settings and return attachments accordingly"""
|
||||
attachments = []
|
||||
|
||||
if self.attach_print:
|
||||
attachments.append(self.get_print(doc))
|
||||
|
||||
if self.attach_files == "From Field" and self.from_attach_field:
|
||||
attachments.append({"file_url": doc.get(self.from_attach_field)})
|
||||
elif self.attach_files == "All":
|
||||
attachments.extend(
|
||||
frappe.get_all(
|
||||
"File",
|
||||
fields=["file_url"],
|
||||
filters={"attached_to_doctype": self.document_type, "attached_to_name": doc.name},
|
||||
)
|
||||
)
|
||||
|
||||
return attachments
|
||||
|
||||
def get_print(self, doc):
|
||||
"""check print settings and return dict with print info"""
|
||||
|
||||
print_settings = frappe.get_doc("Print Settings", "Print Settings")
|
||||
if (doc.docstatus == 0 and not print_settings.allow_print_for_draft) or (
|
||||
|
|
@ -642,18 +672,17 @@ def get_context(context):
|
|||
title=_("Error in Notification"),
|
||||
)
|
||||
else:
|
||||
return [
|
||||
{
|
||||
"print_format_attachment": 1,
|
||||
"doctype": doc.doctype,
|
||||
"name": doc.name,
|
||||
"print_format": self.print_format,
|
||||
"print_letterhead": print_settings.with_letterhead,
|
||||
"lang": frappe.db.get_value("Print Format", self.print_format, "default_print_language")
|
||||
if self.print_format
|
||||
else "en",
|
||||
}
|
||||
]
|
||||
return {
|
||||
"print_format_attachment": 1,
|
||||
"doctype": doc.doctype,
|
||||
"name": doc.name,
|
||||
"print_format": self.print_format,
|
||||
"print_letterhead": print_settings.with_letterhead,
|
||||
"lang": doc.get("language")
|
||||
or frappe.db.get_value("Print Format", self.print_format, "default_print_language")
|
||||
if self.print_format
|
||||
else "en",
|
||||
}
|
||||
|
||||
def get_template(self, md_as_html=False):
|
||||
module = get_doc_module(self.module, self.doctype, self.name)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,51 @@ def get_test_notification(config):
|
|||
frappe.db.commit()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_test_doctype_with_attach_field(doctype_name="Test Attach Doctype"):
|
||||
"""Create a temporary doctype with an attach field for testing."""
|
||||
try:
|
||||
# Create custom doctype with attach field
|
||||
if not frappe.db.exists("DocType", doctype_name):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"name": doctype_name,
|
||||
"module": "Core",
|
||||
"custom": 1,
|
||||
"fields": [
|
||||
{"label": "Title", "fieldname": "title", "fieldtype": "Data", "reqd": 1},
|
||||
{"label": "Attachment", "fieldname": "attachment", "fieldtype": "Attach"},
|
||||
],
|
||||
"permissions": [
|
||||
{"role": "System Manager", "read": 1, "write": 1, "create": 1, "delete": 1}
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
yield doctype_name
|
||||
|
||||
finally:
|
||||
frappe.db.delete(doctype_name)
|
||||
frappe.delete_doc_if_exists("DocType", doctype_name, force=1)
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def create_test_file(file_name="test_attachment.txt", content="Test file content"):
|
||||
"""Create a test file and return its File document."""
|
||||
try:
|
||||
file_doc = frappe.get_doc(
|
||||
{"doctype": "File", "file_name": file_name, "content": content, "is_private": 0}
|
||||
).insert()
|
||||
|
||||
yield file_doc
|
||||
|
||||
finally:
|
||||
frappe.delete_doc_if_exists("File", file_doc.name, force=1)
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
class TestNotification(IntegrationTestCase):
|
||||
def setUp(self):
|
||||
frappe.db.delete("Email Queue")
|
||||
|
|
@ -465,6 +510,159 @@ class TestNotification(IntegrationTestCase):
|
|||
user.save()
|
||||
self.assertEqual(1, frappe.db.count("Notification Log", {"subject": n.subject}))
|
||||
|
||||
def test_attach_files_from_field(self):
|
||||
"""Test notification with 'From Field' attachment option."""
|
||||
frappe.db.delete("Email Queue")
|
||||
|
||||
with get_test_doctype_with_attach_field("Test From Field Doctype") as test_doctype:
|
||||
with create_test_file("from_field_test.txt", "Content from specific field") as test_file:
|
||||
# Create notification with "From Field" attachment
|
||||
notification_config = {
|
||||
"name": "Test From Field Attachment",
|
||||
"subject": "Test From Field Attachment",
|
||||
"document_type": test_doctype,
|
||||
"event": "Save",
|
||||
"message": "Document saved with attachment",
|
||||
"channel": "Email",
|
||||
"attach_files": "From Field",
|
||||
"from_attach_field": "attachment",
|
||||
"recipients": [{"receiver_by_document_field": "owner"}],
|
||||
}
|
||||
|
||||
with get_test_notification(notification_config):
|
||||
# Create document with attachment
|
||||
test_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": test_doctype,
|
||||
"title": "Test Document with Attachment",
|
||||
"attachment": test_file.file_url,
|
||||
}
|
||||
).insert()
|
||||
|
||||
# Trigger save event to send notification
|
||||
test_doc.save()
|
||||
|
||||
# Verify Email Queue created
|
||||
email_queue = frappe.get_doc(
|
||||
"Email Queue", {"reference_doctype": test_doctype, "reference_name": test_doc.name}
|
||||
)
|
||||
|
||||
self.assertTrue(email_queue, "Email Queue not created")
|
||||
|
||||
# Verify attachment metadata in Email Queue
|
||||
attachments = json.loads(email_queue.attachments) if email_queue.attachments else []
|
||||
self.assertEqual(len(attachments), 1, "Expected exactly one attachment")
|
||||
self.assertEqual(
|
||||
attachments[0].get("file_url"), test_file.file_url, "Attachment URL doesn't match"
|
||||
)
|
||||
|
||||
# Clean up test document
|
||||
test_doc.delete()
|
||||
|
||||
def test_attach_files_all(self):
|
||||
"""Test notification with 'All' attachment option."""
|
||||
frappe.db.delete("Email Queue")
|
||||
|
||||
with get_test_doctype_with_attach_field("Test All Attachments Doctype") as test_doctype:
|
||||
with create_test_file("all_test_1.txt", "First file content") as test_file1:
|
||||
# Create notification with "All" attachment option
|
||||
notification_config = {
|
||||
"name": "Test All Attachments",
|
||||
"subject": "Test All Attachments",
|
||||
"document_type": test_doctype,
|
||||
"event": "Save",
|
||||
"message": "Document saved with all attachments",
|
||||
"channel": "Email",
|
||||
"attach_files": "All",
|
||||
"recipients": [{"receiver_by_document_field": "owner"}],
|
||||
}
|
||||
|
||||
with get_test_notification(notification_config):
|
||||
# Create document with one attachment
|
||||
test_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": test_doctype,
|
||||
"title": "Test Document with Multiple Attachments",
|
||||
"attachment": test_file1.file_url,
|
||||
}
|
||||
).insert()
|
||||
|
||||
# Upload additional file that is not attached to a specific field
|
||||
with create_test_file(
|
||||
"additional_file.txt", "Additional file content"
|
||||
) as additional_file:
|
||||
additional_file.attached_to_doctype = test_doctype
|
||||
additional_file.attached_to_name = test_doc.name
|
||||
additional_file.save()
|
||||
|
||||
# Trigger save event to send notification
|
||||
test_doc.save()
|
||||
|
||||
# Verify Email Queue created
|
||||
email_queue = frappe.get_doc(
|
||||
"Email Queue",
|
||||
{"reference_doctype": test_doctype, "reference_name": test_doc.name},
|
||||
)
|
||||
|
||||
self.assertTrue(email_queue, "Email Queue not created")
|
||||
|
||||
# Verify all attachments in Email Queue
|
||||
attachments = json.loads(email_queue.attachments) if email_queue.attachments else []
|
||||
|
||||
# Should have the additional file that's directly attached to the document
|
||||
self.assertEqual(len(attachments), 2, "Expected exactly two attachments")
|
||||
|
||||
# Verify the additional file is included
|
||||
attachment_urls = [att.get("file_url") for att in attachments]
|
||||
self.assertIn(
|
||||
test_file1.file_url, attachment_urls, "First file not found in attachments"
|
||||
)
|
||||
self.assertIn(
|
||||
additional_file.file_url,
|
||||
attachment_urls,
|
||||
"Additional file not found in attachments",
|
||||
)
|
||||
|
||||
test_doc.delete()
|
||||
|
||||
def test_attach_files_empty_option(self):
|
||||
"""Test notification with empty attachment option (no attachments)."""
|
||||
frappe.db.delete("Email Queue")
|
||||
|
||||
# Create notification with empty attach_files option
|
||||
notification_config = {
|
||||
"name": "Test No Attachments",
|
||||
"subject": "Test No Attachments",
|
||||
"document_type": "ToDo",
|
||||
"event": "Save",
|
||||
"message": "Todo saved without attachments",
|
||||
"channel": "Email",
|
||||
"attach_print": 0,
|
||||
"attach_files": "", # Empty option
|
||||
"recipients": [{"receiver_by_document_field": "owner"}],
|
||||
}
|
||||
|
||||
with get_test_notification(notification_config):
|
||||
# Create a ToDo
|
||||
todo = frappe.new_doc("ToDo")
|
||||
todo.description = "Test ToDo"
|
||||
todo.insert()
|
||||
todo.save()
|
||||
|
||||
# Verify Email Queue created
|
||||
email_queue = frappe.get_doc(
|
||||
"Email Queue", {"reference_doctype": "ToDo", "reference_name": todo.name}
|
||||
)
|
||||
|
||||
self.assertTrue(email_queue, "Email Queue not created")
|
||||
|
||||
# Verify no attachments in Email Queue (or empty list)
|
||||
attachments = json.loads(email_queue.attachments) if email_queue.attachments else []
|
||||
self.assertEqual(len(attachments), 0, "Expected no attachments")
|
||||
|
||||
# Clean up
|
||||
todo.delete(ignore_permissions=True)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
frappe.delete_doc_if_exists("Notification", "ToDo Status Update")
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from babel.messages.catalog import Catalog
|
|||
from babel.messages.extract import DEFAULT_KEYWORDS, extract_from_dir
|
||||
from babel.messages.mofile import read_mo, write_mo
|
||||
from babel.messages.pofile import read_po, write_po
|
||||
from click import secho
|
||||
|
||||
import frappe
|
||||
from frappe.utils import get_bench_path
|
||||
|
|
@ -136,6 +137,7 @@ def generate_pot(target_app: str | None = None):
|
|||
for app in apps:
|
||||
app_path = frappe.get_pymodule_path(app, "..")
|
||||
catalog = new_catalog(app)
|
||||
ignored_strings = _get_ignored_strings(app)
|
||||
|
||||
# Each file will only be processed by the first method that matches,
|
||||
# so more specific methods should come first.
|
||||
|
|
@ -148,12 +150,63 @@ def generate_pot(target_app: str | None = None):
|
|||
if not message:
|
||||
continue
|
||||
|
||||
if (message, context) in ignored_strings:
|
||||
continue
|
||||
|
||||
catalog.add(message, locations=[(filename, lineno)], auto_comments=comments, context=context)
|
||||
|
||||
pot_path = write_catalog(app, catalog)
|
||||
print(f"POT file created at {pot_path}")
|
||||
|
||||
|
||||
def _get_ignored_strings(app: str) -> set[tuple[str, str | None]]:
|
||||
"""Return a set of tuples (message, context) that should be excluded from the given app's POT file.
|
||||
|
||||
Example:
|
||||
If [app]/hooks.py contains:
|
||||
ignore_translatable_strings_from = ["frappe"]
|
||||
|
||||
Then this will return a set of tuples (message, context) with all
|
||||
entries from frappe's POT file.
|
||||
"""
|
||||
ignored_strings = set()
|
||||
for ignore_app in frappe.get_hooks("ignore_translatable_strings_from", [], app_name=app):
|
||||
if ignore_app == app:
|
||||
raise ValueError(
|
||||
f"Invalid configuration: App '{app}' cannot ignore its own translatable strings. "
|
||||
f"Remove '{app}' from the 'ignore_translatable_strings_from' hook in {app}/hooks.py to fix this."
|
||||
)
|
||||
|
||||
try:
|
||||
catalog = get_catalog(ignore_app)
|
||||
except ModuleNotFoundError:
|
||||
secho(
|
||||
f"App '{ignore_app}' specified in '{app}/hooks.py' 'ignore_translatable_strings_from' hook is not installed. Skipping",
|
||||
err=True,
|
||||
fg="yellow",
|
||||
)
|
||||
continue
|
||||
except ImportError:
|
||||
secho(
|
||||
f"App '{ignore_app}' specified in '{app}/hooks.py' 'ignore_translatable_strings_from' hook could not be imported. Skipping",
|
||||
err=True,
|
||||
fg="yellow",
|
||||
)
|
||||
continue
|
||||
except AttributeError:
|
||||
secho(
|
||||
f"Site not initialized. Cannot load app '{ignore_app}' specified in '{app}/hooks.py' 'ignore_translatable_strings_from' hook. Skipping",
|
||||
err=True,
|
||||
fg="yellow",
|
||||
)
|
||||
continue
|
||||
|
||||
for message in catalog:
|
||||
ignored_strings.add((message.id, message.context))
|
||||
|
||||
return ignored_strings
|
||||
|
||||
|
||||
def get_is_gitignored_function_for_app(app: str | None):
|
||||
"""
|
||||
Used to check if a directory is gitignored or not.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1111
frappe/locale/zh.po
1111
frappe/locale/zh.po
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,7 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from werkzeug.exceptions import NotFound
|
||||
from werkzeug.middleware.shared_data import SharedDataMiddleware
|
||||
|
|
@ -18,11 +18,12 @@ class StaticDataMiddleware(SharedDataMiddleware):
|
|||
def get_directory_loader(self, directory):
|
||||
def loader(path):
|
||||
site = get_site_name(frappe.app._site or self.environ.get("HTTP_HOST"))
|
||||
path = os.path.join(directory, site, "public", "files", cstr(path))
|
||||
if os.path.isfile(path):
|
||||
return os.path.basename(path), self._opener(path)
|
||||
else:
|
||||
files_path = Path(directory) / site / "public" / "files"
|
||||
requested_path = Path(cstr(path))
|
||||
path = (files_path / requested_path).resolve()
|
||||
if not path.is_relative_to(files_path) or not path.is_file():
|
||||
raise NotFound
|
||||
# return None, None
|
||||
|
||||
return path.name, self._opener(path)
|
||||
|
||||
return loader
|
||||
|
|
|
|||
|
|
@ -1703,10 +1703,14 @@ class Document(BaseDocument):
|
|||
|
||||
return view_log
|
||||
|
||||
def log_error(self, title=None, message=None):
|
||||
def log_error(self, title=None, message=None, *, defer_insert=False):
|
||||
"""Helper function to create an Error Log"""
|
||||
return frappe.log_error(
|
||||
message=message, title=title, reference_doctype=self.doctype, reference_name=self.name
|
||||
message=message,
|
||||
title=title,
|
||||
reference_doctype=self.doctype,
|
||||
reference_name=self.name,
|
||||
defer_insert=defer_insert,
|
||||
)
|
||||
|
||||
def get_signature(self):
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ Utilities for using modules
|
|||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from textwrap import dedent, indent
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
|
|
@ -200,7 +201,11 @@ def scrub_dt_dn(dt: str, dn: str) -> tuple[str, str]:
|
|||
|
||||
def get_doc_path(module: str, doctype: str, name: str) -> str:
|
||||
"""Return path of a doc in a module."""
|
||||
return os.path.join(get_module_path(module), *scrub_dt_dn(doctype, name))
|
||||
module_path = Path(get_module_path(module))
|
||||
path = module_path / Path(*scrub_dt_dn(doctype, name))
|
||||
if not path.resolve().is_relative_to(module_path.resolve()):
|
||||
raise ValueError(_("Path {0} is not within module {1}").format(path, module))
|
||||
return path.resolve()
|
||||
|
||||
|
||||
def reload_doc(
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ let display_checked = computed(() => {
|
|||
type="checkbox"
|
||||
:checked="display_checked"
|
||||
:disabled="read_only"
|
||||
@change="(event) => $emit('update:modelValue', event.target.checked)"
|
||||
@change="(event) => $emit('update:modelValue', event.target.checked ? 1 : 0)"
|
||||
/>
|
||||
<span class="label-area" :class="{ reqd: df.reqd }">{{ __(df.label) }}</span>
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -155,7 +155,11 @@ frappe.ui.form.ControlInput = class ControlInput extends frappe.ui.form.Control
|
|||
} else {
|
||||
value = this.value || value;
|
||||
}
|
||||
if (["Data", "Long Text", "Small Text", "Text", "Password"].includes(this.df.fieldtype)) {
|
||||
if (
|
||||
["Data", "Long Text", "Small Text", "Text", "Password", "MultiSelect"].includes(
|
||||
this.df.fieldtype
|
||||
)
|
||||
) {
|
||||
value = frappe.utils.escape_html(value);
|
||||
}
|
||||
let doc = this.doc || (this.frm && this.frm.doc);
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ frappe.ui.form.ControlDuration = class ControlDuration extends frappe.ui.form.Co
|
|||
});
|
||||
|
||||
this.$input.on("focus", () => {
|
||||
if (this.df.read_only) return;
|
||||
this.$picker.show();
|
||||
let is_picker_set = this.is_duration_picker_set(this.inputs);
|
||||
if (!is_picker_set) {
|
||||
|
|
|
|||
|
|
@ -1121,14 +1121,14 @@ export default class GridRow {
|
|||
|
||||
this.columns[df.fieldname] = $col;
|
||||
this.columns_list.push($col);
|
||||
if (ci == 0 && !this.header_row) {
|
||||
if (ci == 0 && this.header_row) {
|
||||
$col.attr("tabIndex", 0);
|
||||
$col.on("focus", function () {
|
||||
if (me.grid.grid_rows.length == 0) {
|
||||
me.grid.add_new_row();
|
||||
}
|
||||
me.grid.grid_rows[me.grid.grid_rows.length - 1].toggle_editable_row(true);
|
||||
me.grid.set_focus_on_row();
|
||||
me.grid.set_focus_on_row(0);
|
||||
$col.attr("tabIndex", "");
|
||||
});
|
||||
}
|
||||
|
|
@ -1296,9 +1296,13 @@ export default class GridRow {
|
|||
if (is_last_column) {
|
||||
// last row
|
||||
if (me.doc.idx === values.length) {
|
||||
me.grid.add_new_row(null, null, true);
|
||||
me.grid.grid_rows[me.grid.grid_rows.length - 1].toggle_editable_row();
|
||||
me.grid.set_focus_on_row();
|
||||
setTimeout(function () {
|
||||
me.grid.add_new_row(null, null, true);
|
||||
me.grid.grid_rows[
|
||||
me.grid.grid_rows.length - 1
|
||||
].toggle_editable_row();
|
||||
me.grid.set_focus_on_row();
|
||||
}, 100);
|
||||
} else {
|
||||
// last column before last row
|
||||
me.grid.grid_rows[me.doc.idx].toggle_editable_row();
|
||||
|
|
|
|||
|
|
@ -129,7 +129,9 @@ frappe.ui.form.Layout = class Layout {
|
|||
|
||||
// Add block color and append to parent container `form-message-container`
|
||||
const block_color =
|
||||
color && ["yellow", "blue", "red", "green", "orange"].includes(color) ? color : "blue";
|
||||
color && ["yellow", "blue", "red", "green", "orange", "white"].includes(color)
|
||||
? color
|
||||
: "blue";
|
||||
$html.addClass(block_color).appendTo(this.message);
|
||||
|
||||
// Show parent container if hidden
|
||||
|
|
|
|||
|
|
@ -26,7 +26,11 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
this.page.hide_menu();
|
||||
this.print_icon && this.print_icon.addClass("hide");
|
||||
} else {
|
||||
this.page.show_menu();
|
||||
if (this.page.menu.children().length > 0) {
|
||||
this.page.show_menu();
|
||||
} else {
|
||||
this.page.hide_menu();
|
||||
}
|
||||
this.print_icon && this.print_icon.removeClass("hide");
|
||||
}
|
||||
}
|
||||
|
|
@ -300,11 +304,33 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
this.page.clear_menu();
|
||||
|
||||
if (frappe.boot.desk_settings.form_sidebar) {
|
||||
// this.make_navigation();
|
||||
this.make_navigation();
|
||||
this.make_menu_items();
|
||||
}
|
||||
}
|
||||
|
||||
make_navigation() {
|
||||
// Navigate
|
||||
if (!this.frm.is_new() && !this.frm.meta.issingle) {
|
||||
this.page.add_action_icon(
|
||||
"es-line-left-chevron",
|
||||
() => {
|
||||
this.frm.navigate_records(1);
|
||||
},
|
||||
"prev-doc",
|
||||
__("Previous Document")
|
||||
);
|
||||
this.page.add_action_icon(
|
||||
"es-line-right-chevron",
|
||||
() => {
|
||||
this.frm.navigate_records(0);
|
||||
},
|
||||
"next-doc",
|
||||
__("Next Document")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
make_menu_items() {
|
||||
// Print
|
||||
this.add_discard();
|
||||
|
|
|
|||
|
|
@ -72,10 +72,8 @@
|
|||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="sidebar-label">
|
||||
<div class="sidebar-action show-tags">
|
||||
<a class="list-tag-preview">{{ __("Show Tags") }}</a>
|
||||
</div>
|
||||
<div class="sidebar-action show-tags">
|
||||
<a class="list-tag-preview">{{ __("Show Tags") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -997,7 +997,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
* If the length of the text is not available, it defaults to a length of 22.5.
|
||||
*/
|
||||
let textLength = $(column_html).text()?.trim()?.length || 22.5;
|
||||
let calculatedWidth = (textLength * 10) / 1.3;
|
||||
let calculatedWidth = (textLength * 10) / 1.3 + (col.type == "Subject" ? 30 : 0);
|
||||
|
||||
/**
|
||||
* Updates the `column_max_widths` object by setting the maximum width for a specific column (fieldname).
|
||||
|
|
|
|||
|
|
@ -60,8 +60,12 @@ $.extend(frappe.perm, {
|
|||
perm[0].read = 1;
|
||||
}
|
||||
|
||||
if (!meta) return perm;
|
||||
|
||||
if (!meta) {
|
||||
if (frappe.boot.user.can_read.includes(doctype)) {
|
||||
perm[0].read = 1;
|
||||
}
|
||||
return perm;
|
||||
}
|
||||
perm = frappe.perm.get_role_permissions(meta);
|
||||
const base_perm = perm[0];
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@
|
|||
</div>
|
||||
<button class="btn btn-secondary btn-default btn-sm hide"></button>
|
||||
<div class="actions-btn-group hide">
|
||||
<button type="button" class="btn btn-primary btn-sm" data-toggle="dropdown" aria-expanded="false">
|
||||
<button type="button" class="btn btn-primary btn-sm justify-center" data-toggle="dropdown" aria-expanded="false">
|
||||
<span>
|
||||
<span class="hidden-xs actions-btn-group-label">{%= __("Actions") %}</span>
|
||||
<svg class="icon icon-xs">
|
||||
|
|
|
|||
|
|
@ -56,9 +56,6 @@ frappe.views.CommunicationComposer = class {
|
|||
fieldname: "recipients",
|
||||
default: this.get_default_recipients("recipients"),
|
||||
ignore_validation: true,
|
||||
onchange: function () {
|
||||
me.sanitize_emails(this);
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Button",
|
||||
|
|
@ -79,9 +76,6 @@ frappe.views.CommunicationComposer = class {
|
|||
fieldname: "cc",
|
||||
default: this.get_default_recipients("cc"),
|
||||
ignore_validation: true,
|
||||
onchange: function () {
|
||||
me.sanitize_emails(this);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: __("BCC", null, "Email Recipients"),
|
||||
|
|
@ -89,9 +83,6 @@ frappe.views.CommunicationComposer = class {
|
|||
fieldname: "bcc",
|
||||
default: this.get_default_recipients("bcc"),
|
||||
ignore_validation: true,
|
||||
onchange: function () {
|
||||
me.sanitize_emails(this);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: __("Schedule Send At"),
|
||||
|
|
@ -986,16 +977,4 @@ frappe.views.CommunicationComposer = class {
|
|||
const text = frappe.utils.html2text(html);
|
||||
return text.replace(/\n{3,}/g, "\n\n");
|
||||
}
|
||||
|
||||
sanitize_emails(control) {
|
||||
let emails = control.get_value();
|
||||
if (!emails) return;
|
||||
let sanitized = emails
|
||||
.split(",")
|
||||
.map((email) => frappe.utils.xss_sanitise(email.trim()))
|
||||
.join(",");
|
||||
if (sanitized != emails) {
|
||||
control.set_value(sanitized);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -726,8 +726,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
)
|
||||
.map(({ fieldname, fieldtype, options }) => ({ fieldname, fieldtype, options }));
|
||||
|
||||
console.log(js_filters, "js_filters");
|
||||
|
||||
this.last_ajax = frappe.call({
|
||||
method: "frappe.desk.query_report.run",
|
||||
type: "GET",
|
||||
|
|
|
|||
|
|
@ -424,7 +424,7 @@ textarea.form-control {
|
|||
/* duration control */
|
||||
|
||||
.duration-picker {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow-sm);
|
||||
|
|
@ -470,7 +470,7 @@ textarea.form-control {
|
|||
width: 55px;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
background-color: var(--control-bg);
|
||||
background-color: var(--bg-gray);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
|
@ -486,6 +486,22 @@ textarea.form-control {
|
|||
.picker-row {
|
||||
display: flex;
|
||||
margin: var(--margin-sm);
|
||||
|
||||
.col {
|
||||
&:last-child {
|
||||
.grid-row & {
|
||||
// remove flex and justify content added by grid context
|
||||
display: block;
|
||||
justify-content: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-row & {
|
||||
// remove border and padding added by grid context
|
||||
border-right: none;
|
||||
padding: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
.for-description {
|
||||
@include get_textstyle("sm", "regular");
|
||||
}
|
||||
|
||||
min-height: var(--input-height);
|
||||
border-radius: var(--border-radius-sm);
|
||||
padding: 4px 8px;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ body {
|
|||
.checkbox {
|
||||
label {
|
||||
display: inline-flex;
|
||||
align-items: start;
|
||||
align-items: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
--checkbox-right-margin: 8px;
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@
|
|||
.grid-static-col,
|
||||
.row-check,
|
||||
.row-index {
|
||||
height: 32px;
|
||||
padding: 4px 8px !important;
|
||||
background-color: var(--subtle-fg);
|
||||
}
|
||||
|
|
@ -188,6 +187,7 @@
|
|||
input {
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: -3px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
&.search {
|
||||
|
|
@ -314,7 +314,6 @@
|
|||
|
||||
.form-control:focus {
|
||||
border-color: $text-muted;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.has-error .form-control {
|
||||
|
|
@ -687,9 +686,7 @@
|
|||
&:focus-visible {
|
||||
@include grid-focus();
|
||||
}
|
||||
.grid-static-col {
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.grid-static-col.col-xs-1 {
|
||||
flex: 1 0 60px;
|
||||
width: 60px;
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@
|
|||
}
|
||||
|
||||
.frappe-list {
|
||||
margin: var(--margin-xs) var(--margin-md);
|
||||
margin: 0 var(--margin-md);
|
||||
.result.has-assign-to {
|
||||
.list-row .level-right {
|
||||
flex: 0 0 180px;
|
||||
|
|
@ -620,7 +620,7 @@ input.list-header-checkbox {
|
|||
@media (max-width: map-get($grid-breakpoints, "sm")) {
|
||||
.layout-main-section .frappe-list .result-container {
|
||||
.result {
|
||||
overflow-x: hidden;
|
||||
overflow: hidden;
|
||||
input.list-row-checkbox,
|
||||
input.list-header-checkbox {
|
||||
width: 15px !important;
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@
|
|||
|
||||
.form-group {
|
||||
padding: 0 10px 0 0;
|
||||
margin: 5px 0;
|
||||
margin: 8px 0;
|
||||
}
|
||||
.checkbox {
|
||||
margin-top: 4px;
|
||||
|
|
|
|||
|
|
@ -87,7 +87,8 @@ body {
|
|||
|
||||
.field-icon {
|
||||
left: 9px;
|
||||
top: 5px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
fill: var(--text-light);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ from frappe.utils import (
|
|||
get_bench_path,
|
||||
get_file_timestamp,
|
||||
get_gravatar,
|
||||
get_link_to_report,
|
||||
get_site_info,
|
||||
get_sites,
|
||||
get_url,
|
||||
|
|
@ -343,6 +344,13 @@ class TestFilters(IntegrationTestCase):
|
|||
self.assertTrue(compare(None, "is", "Not Set"))
|
||||
self.assertTrue(compare(None, "is", "not set"))
|
||||
|
||||
def test_get_link_to_report_with_between_filter(self):
|
||||
filters = {
|
||||
"creation": [["between", ["2024-01-01", "2024-12-31"]]],
|
||||
}
|
||||
link = get_link_to_report(name="ToDo", filters=filters)
|
||||
self.assertIn('creation=["between",["2024-01-01","2024-12-31"]]', link)
|
||||
|
||||
|
||||
class TestMoney(IntegrationTestCase):
|
||||
def test_money_in_words(self):
|
||||
|
|
|
|||
|
|
@ -210,6 +210,8 @@ def get_translation_dict_from_file(path, lang, app, throw=False) -> dict[str, st
|
|||
csv_content = read_csv_file(path)
|
||||
|
||||
for item in csv_content:
|
||||
item[0] = item[0].replace("\\n", "\n")
|
||||
item[1] = item[1].replace("\\n", "\n")
|
||||
if len(item) == 3 and item[2]:
|
||||
key = item[0] + ":" + item[2]
|
||||
translation_map[key] = strip(item[1])
|
||||
|
|
|
|||
|
|
@ -320,7 +320,8 @@ def send_token_via_sms(otpsecret, token=None, phone_no=None):
|
|||
return False
|
||||
|
||||
hotp = pyotp.HOTP(otpsecret)
|
||||
args = {ss.message_parameter: f"Your verification code is {hotp.at(int(token))}"}
|
||||
otp = hotp.at(int(token))
|
||||
args = {ss.message_parameter: get_rendered_otp_message(otp)}
|
||||
|
||||
for d in ss.get("parameters"):
|
||||
args[d.parameter] = d.value
|
||||
|
|
@ -341,6 +342,14 @@ def send_token_via_sms(otpsecret, token=None, phone_no=None):
|
|||
return True
|
||||
|
||||
|
||||
def get_rendered_otp_message(otp: str) -> str:
|
||||
default_template = "Your verification code is {{otp}}"
|
||||
custom_template = frappe.get_system_settings("otp_sms_template")
|
||||
template = custom_template or default_template
|
||||
|
||||
return frappe.render_template(template, {"otp": otp})
|
||||
|
||||
|
||||
def send_token_via_email(user, token, otp_secret, otp_issuer, subject=None, message=None):
|
||||
"""Send token to user as email."""
|
||||
user_email = frappe.db.get_value("User", user, "email")
|
||||
|
|
|
|||
|
|
@ -643,6 +643,11 @@ app_license = "{app_license}"
|
|||
# "Logging DocType Name": 30 # days to retain logs
|
||||
# }}
|
||||
|
||||
# Translation
|
||||
# ------------
|
||||
# List of apps whose translatable strings should be excluded from this app's translations.
|
||||
# ignore_translatable_strings_from = []
|
||||
|
||||
"""
|
||||
|
||||
gitignore_template = """# Byte-compiled / optimized / DLL files
|
||||
|
|
|
|||
|
|
@ -1935,10 +1935,23 @@ def get_link_to_report(
|
|||
conditions = []
|
||||
for k, v in filters.items():
|
||||
if isinstance(v, list):
|
||||
conditions.extend(
|
||||
str(k) + "=" + '["' + str(value[0] + '"' + "," + '"' + str(value[1]) + '"]')
|
||||
for value in v
|
||||
)
|
||||
for value in v:
|
||||
if value[0] == "between":
|
||||
conditions.append(
|
||||
str(k)
|
||||
+ "="
|
||||
+ '["'
|
||||
+ str(value[0])
|
||||
+ '",["'
|
||||
+ str(value[1][0])
|
||||
+ '","'
|
||||
+ str(value[1][1])
|
||||
+ '"]]'
|
||||
)
|
||||
else:
|
||||
conditions.append(
|
||||
str(k) + "=" + '["' + str(value[0] + '"' + "," + '"' + str(value[1]) + '"]')
|
||||
)
|
||||
else:
|
||||
conditions.append(str(k) + "=" + quote(str(v)))
|
||||
|
||||
|
|
|
|||
|
|
@ -219,9 +219,15 @@ def insert_values_for_multiple_docs(all_contents):
|
|||
# ignoring duplicate keys for doctype_name
|
||||
frappe.db.multisql(
|
||||
{
|
||||
"mariadb": """INSERT IGNORE INTO `__global_search`
|
||||
"mariadb": """INSERT INTO `__global_search`
|
||||
(doctype, name, content, published, title, route)
|
||||
VALUES {} """.format(", ".join(batch_values)),
|
||||
VALUES {}
|
||||
ON DUPLICATE KEY UPDATE
|
||||
content=VALUE(content),
|
||||
published=VALUE(published),
|
||||
title=VALUE(title),
|
||||
route=VALUE(route)
|
||||
""".format(", ".join(batch_values)),
|
||||
"postgres": """INSERT INTO `__global_search`
|
||||
(doctype, name, content, published, title, route)
|
||||
VALUES {}
|
||||
|
|
@ -422,6 +428,7 @@ def sync_value_in_queue(value):
|
|||
frappe.cache.lpush("global_search_queue", json.dumps(value))
|
||||
except redis.exceptions.ConnectionError:
|
||||
# not connected, sync directly
|
||||
assert not frappe.flags.in_test, "Should not fail silently in tests"
|
||||
sync_value(value)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -182,14 +182,9 @@
|
|||
<div class="email-field">
|
||||
<input type="email" id="forgot_email" class="form-control"
|
||||
placeholder="{{ _('Email Address') }}" required autofocus autocomplete="username">
|
||||
<svg class="field-icon email-icon" width="20" height="20" viewBox="0 0 20 20" fill="none"
|
||||
<svg class="field-icon email-icon" width="16" height="16" viewBox="0 0 16 16" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2.5 7.65149V15.0757C2.5 15.4374 2.64367 15.7842 2.8994 16.04C3.15513 16.2957 3.50198 16.4394 3.86364 16.4394H16.1364C16.498 16.4394 16.8449 16.2957 17.1006 16.04C17.3563 15.7842 17.5 15.4374 17.5 15.0757V7.65149"
|
||||
stroke="#74808B" stroke-miterlimit="10" stroke-linecap="square" />
|
||||
<path
|
||||
d="M17.5 7.57572V5.53026C17.5 5.1686 17.3563 4.82176 17.1006 4.56603C16.8449 4.31029 16.498 4.16663 16.1364 4.16663H3.86364C3.50198 4.16663 3.15513 4.31029 2.8994 4.56603C2.64367 4.82176 2.5 5.1686 2.5 5.53026V7.57572L10 10.8333L17.5 7.57572Z"
|
||||
stroke="#74808B" stroke-miterlimit="10" stroke-linecap="square" />
|
||||
<use class="es-lock" href="#es-line-email"></use>
|
||||
</svg>
|
||||
|
||||
</div>
|
||||
|
|
@ -214,14 +209,9 @@
|
|||
<div class="email-field">
|
||||
<input type="email" id="login_with_email_link_email" class="form-control"
|
||||
placeholder="{{ _('Email Address') }}" required autofocus autocomplete="username">
|
||||
<svg class="field-icon email-icon" width="20" height="20" viewBox="0 0 20 20" fill="none"
|
||||
<svg class="field-icon email-icon" width="16" height="16" viewBox="0 0 16 16" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2.5 7.65149V15.0757C2.5 15.4374 2.64367 15.7842 2.8994 16.04C3.15513 16.2957 3.50198 16.4394 3.86364 16.4394H16.1364C16.498 16.4394 16.8449 16.2957 17.1006 16.04C17.3563 15.7842 17.5 15.4374 17.5 15.0757V7.65149"
|
||||
stroke="#74808B" stroke-miterlimit="10" stroke-linecap="square" />
|
||||
<path
|
||||
d="M17.5 7.57572V5.53026C17.5 5.1686 17.3563 4.82176 17.1006 4.56603C16.8449 4.31029 16.498 4.16663 16.1364 4.16663H3.86364C3.50198 4.16663 3.15513 4.31029 2.8994 4.56603C2.64367 4.82176 2.5 5.1686 2.5 5.53026V7.57572L10 10.8333L17.5 7.57572Z"
|
||||
stroke="#74808B" stroke-miterlimit="10" stroke-linecap="square" />
|
||||
<use class="es-lock" href="#es-line-email"></use>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue