Merge branch 'develop' into fix-dashboard
This commit is contained in:
commit
1655eaca1e
41 changed files with 468 additions and 161 deletions
|
|
@ -98,15 +98,17 @@ context("Kanban Board", () => {
|
|||
});
|
||||
|
||||
it("Checks if Kanban Board edits are blocked for non-System Manager and non-owner of the Board", () => {
|
||||
// create admin kanban board
|
||||
cy.call("frappe.tests.ui_test_helpers.create_todo", { description: "Frappe User ToDo" });
|
||||
|
||||
cy.switch_to_user("Administrator");
|
||||
cy.call("frappe.tests.ui_test_helpers.create_admin_kanban");
|
||||
// remove sys manager
|
||||
cy.remove_role("frappe@example.com", "System Manager");
|
||||
|
||||
cy.switch_to_user("frappe@example.com");
|
||||
const noSystemManager = "nosysmanager@example.com";
|
||||
cy.call("frappe.tests.ui_test_helpers.create_test_user", {
|
||||
username: noSystemManager,
|
||||
});
|
||||
cy.remove_role(noSystemManager, "System Manager");
|
||||
cy.call("frappe.tests.ui_test_helpers.create_todo", { description: "Frappe User ToDo" });
|
||||
cy.call("frappe.tests.ui_test_helpers.create_admin_kanban");
|
||||
|
||||
cy.switch_to_user(noSystemManager);
|
||||
|
||||
cy.visit("/app/todo/view/kanban/Admin Kanban");
|
||||
|
||||
|
|
@ -122,7 +124,8 @@ context("Kanban Board", () => {
|
|||
// Column actions should be hidden (dropdown for 'Archive' and indicators)
|
||||
cy.get(".kanban .column-options").should("have.length", 0);
|
||||
|
||||
cy.add_role("frappe@example.com", "System Manager");
|
||||
cy.switch_to_user("Administrator");
|
||||
cy.call("frappe.client.delete", { doctype: "User", name: noSystemManager });
|
||||
});
|
||||
|
||||
after(() => {
|
||||
|
|
|
|||
|
|
@ -1274,7 +1274,7 @@ def reload_doc(
|
|||
return frappe.modules.reload_doc(module, dt, dn, force=force, reset_permissions=reset_permissions)
|
||||
|
||||
|
||||
@whitelist()
|
||||
@whitelist(methods=["POST", "PUT"])
|
||||
def rename_doc(
|
||||
doctype: str,
|
||||
old: str,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,11 @@ EXTRA_ARGS_CTX = {"ignore_unknown_options": True, "allow_extra_args": True}
|
|||
@click.option("--app", help="Build assets for app")
|
||||
@click.option("--apps", help="Build assets for specific apps")
|
||||
@click.option(
|
||||
"--hard-link", is_flag=True, default=False, help="Copy the files instead of symlinking"
|
||||
"--hard-link",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Copy the files instead of symlinking",
|
||||
envvar="FRAPPE_HARD_LINK_ASSETS",
|
||||
)
|
||||
@click.option(
|
||||
"--make-copy",
|
||||
|
|
@ -908,7 +912,7 @@ def run_ui_tests(
|
|||
|
||||
os.chdir(app_base_path)
|
||||
|
||||
node_bin = subprocess.getoutput("yarn bin")
|
||||
node_bin = subprocess.getoutput("(cd ../frappe && yarn bin)")
|
||||
cypress_path = f"{node_bin}/cypress"
|
||||
drag_drop_plugin_path = f"{node_bin}/../@4tw/cypress-drag-drop"
|
||||
real_events_plugin_path = f"{node_bin}/../cypress-real-events"
|
||||
|
|
@ -935,7 +939,7 @@ def run_ui_tests(
|
|||
"@cypress/code-coverage@^3",
|
||||
]
|
||||
)
|
||||
frappe.commands.popen(f"yarn add {packages} --no-lockfile")
|
||||
frappe.commands.popen(f"(cd ../frappe && yarn add {packages} --no-lockfile)")
|
||||
|
||||
# run for headless mode
|
||||
run_or_open = "run --browser chrome --record" if headless else "open"
|
||||
|
|
|
|||
|
|
@ -254,19 +254,23 @@ def address_query(doctype, txt, searchfield, start, page_len, filters):
|
|||
"""select
|
||||
`tabAddress`.name, `tabAddress`.city, `tabAddress`.country
|
||||
from
|
||||
`tabAddress`, `tabDynamic Link`
|
||||
`tabAddress`
|
||||
join `tabDynamic Link`
|
||||
on (`tabDynamic Link`.parent = `tabAddress`.name and `tabDynamic Link`.parenttype = 'Address')
|
||||
where
|
||||
`tabDynamic Link`.parent = `tabAddress`.name and
|
||||
`tabDynamic Link`.parenttype = 'Address' and
|
||||
`tabDynamic Link`.link_doctype = %(link_doctype)s and
|
||||
`tabDynamic Link`.link_name = %(link_name)s and
|
||||
ifnull(`tabAddress`.disabled, 0) = 0 and
|
||||
({search_condition})
|
||||
{mcond} {condition}
|
||||
order by
|
||||
if(locate(%(_txt)s, `tabAddress`.name), locate(%(_txt)s, `tabAddress`.name), 99999),
|
||||
case
|
||||
when locate(%(_txt)s, `tabAddress`.name) != 0
|
||||
then locate(%(_txt)s, `tabAddress`.name)
|
||||
else 99999
|
||||
end,
|
||||
`tabAddress`.idx desc, `tabAddress`.name
|
||||
limit %(start)s, %(page_len)s """.format(
|
||||
limit %(page_len)s offset %(start)s""".format(
|
||||
mcond=get_match_cond(doctype),
|
||||
search_condition=search_condition,
|
||||
condition=condition or "",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
# Copyright (c) 2015, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
from functools import partial
|
||||
|
||||
import frappe
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
from frappe.contacts.doctype.address.address import address_query, get_address_display
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
|
|
@ -28,3 +30,29 @@ class TestAddress(FrappeTestCase):
|
|||
address = frappe.get_list("Address")[0].name
|
||||
display = get_address_display(frappe.get_doc("Address", address).as_dict())
|
||||
self.assertTrue(display)
|
||||
|
||||
def test_address_query(self):
|
||||
def query(doctype="Address", txt="", searchfield="name", start=0, page_len=20, filters=None):
|
||||
if filters is None:
|
||||
filters = {"link_doctype": "User", "link_name": "Administrator"}
|
||||
return address_query(doctype, txt, searchfield, start, page_len, filters)
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
"address_type": "Billing",
|
||||
"address_line1": "1",
|
||||
"city": "Mumbai",
|
||||
"state": "Maharashtra",
|
||||
"country": "India",
|
||||
"doctype": "Address",
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "User",
|
||||
"link_name": "Administrator",
|
||||
}
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
self.assertGreaterEqual(len(query(txt="Admin")), 1)
|
||||
self.assertEqual(len(query(txt="what_zyx")), 0)
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ def _make(
|
|||
if not comm.get_outgoing_email_account():
|
||||
frappe.throw(
|
||||
_(
|
||||
"Unable to send mail because of a missing email account. Please setup default Email Account from Setup > Email > Email Account"
|
||||
"Unable to send mail because of a missing email account. Please setup default Email Account from Settings > Email Account"
|
||||
),
|
||||
exc=frappe.OutgoingEmailError,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ class CommunicationEmailMixin:
|
|||
if include_sender:
|
||||
cc.append(self.sender_mailid)
|
||||
if is_inbound_mail_communcation:
|
||||
if (doc_owner := self.get_owner()) not in frappe.STANDARD_USERS:
|
||||
if (doc_owner := self.get_owner()) and (doc_owner not in frappe.STANDARD_USERS):
|
||||
cc.append(doc_owner)
|
||||
cc = set(cc) - {self.sender_mailid}
|
||||
cc.update(self.get_assignees())
|
||||
|
|
@ -216,7 +216,11 @@ class CommunicationEmailMixin:
|
|||
"reference_name": self.reference_name,
|
||||
"reference_type": self.reference_doctype,
|
||||
}
|
||||
return ToDo.get_owners(filters)
|
||||
|
||||
if self.reference_doctype and self.reference_name:
|
||||
return ToDo.get_owners(filters)
|
||||
else:
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def filter_thread_notification_disbled_users(emails):
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import frappe
|
||||
import frappe.share
|
||||
from frappe.automation.doctype.auto_repeat.test_auto_repeat import create_submittable_doctype
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
|
||||
test_dependencies = ["User"]
|
||||
|
||||
|
|
@ -139,3 +139,61 @@ class TestDocShare(FrappeTestCase):
|
|||
|
||||
test_doc.reload()
|
||||
self.assertTrue(test_doc.has_permission("read"))
|
||||
|
||||
@change_settings("System Settings", {"disable_document_sharing": 1})
|
||||
def test_share_disabled_add(self):
|
||||
"Test if user loses share access on disabling share globally."
|
||||
frappe.share.add("Event", self.event.name, self.user, share=1) # Share as admin
|
||||
frappe.set_user(self.user)
|
||||
|
||||
# User does not have share access although given to them
|
||||
self.assertFalse(self.event.has_permission("share"))
|
||||
self.assertRaises(
|
||||
frappe.PermissionError, frappe.share.add, "Event", self.event.name, "test1@example.com"
|
||||
)
|
||||
|
||||
@change_settings("System Settings", {"disable_document_sharing": 1})
|
||||
def test_share_disabled_add_with_ignore_permissions(self):
|
||||
frappe.share.add("Event", self.event.name, self.user, share=1)
|
||||
frappe.set_user(self.user)
|
||||
|
||||
# User does not have share access although given to them
|
||||
self.assertFalse(self.event.has_permission("share"))
|
||||
|
||||
# Test if behaviour is consistent for developer overrides
|
||||
frappe.share.add_docshare(
|
||||
"Event", self.event.name, "test1@example.com", flags={"ignore_share_permission": True}
|
||||
)
|
||||
|
||||
@change_settings("System Settings", {"disable_document_sharing": 1})
|
||||
def test_share_disabled_set_permission(self):
|
||||
frappe.share.add("Event", self.event.name, self.user, share=1)
|
||||
frappe.set_user(self.user)
|
||||
|
||||
# User does not have share access although given to them
|
||||
self.assertFalse(self.event.has_permission("share"))
|
||||
self.assertRaises(
|
||||
frappe.PermissionError,
|
||||
frappe.share.set_permission,
|
||||
"Event",
|
||||
self.event.name,
|
||||
"test1@example.com",
|
||||
"read",
|
||||
)
|
||||
|
||||
@change_settings("System Settings", {"disable_document_sharing": 1})
|
||||
def test_share_disabled_assign_to(self):
|
||||
"""
|
||||
Assigning a document to a user without access must not share the document,
|
||||
if sharing disabled.
|
||||
"""
|
||||
from frappe.desk.form.assign_to import add
|
||||
|
||||
frappe.share.add("Event", self.event.name, self.user, share=1)
|
||||
frappe.set_user(self.user)
|
||||
|
||||
self.assertRaises(
|
||||
frappe.ValidationError,
|
||||
add,
|
||||
{"doctype": "Event", "name": self.event.name, "assign_to": ["test1@example.com"]},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -169,7 +169,6 @@ class ServerScript(Document):
|
|||
return items
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def setup_scheduler_events(script_name, frequency):
|
||||
"""Creates or Updates Scheduled Job Type documents based on the specified script name and frequency
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"time_zone",
|
||||
"enable_onboarding",
|
||||
"setup_complete",
|
||||
"disable_document_sharing",
|
||||
"date_and_number_format",
|
||||
"date_format",
|
||||
"time_format",
|
||||
|
|
@ -528,12 +529,18 @@
|
|||
"fieldtype": "Select",
|
||||
"label": "Rounding Method",
|
||||
"options": "Banker's Rounding (legacy)\nBanker's Rounding\nCommercial Rounding"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disable_document_sharing",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Document Sharing"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-10 12:23:45.248125",
|
||||
"modified": "2023-03-14 11:30:56.465653",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "System Settings",
|
||||
|
|
@ -552,4 +559,4 @@
|
|||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -219,7 +219,10 @@ frappe.ui.form.on("User", {
|
|||
});
|
||||
}
|
||||
|
||||
if (frappe.session.user == doc.name || frappe.user.has_role("System Manager")) {
|
||||
if (
|
||||
cint(frappe.boot.sysdefaults.enable_two_factor_auth) &&
|
||||
(frappe.session.user == doc.name || frappe.user.has_role("System Manager"))
|
||||
) {
|
||||
frm.add_custom_button(
|
||||
__("Reset OTP Secret"),
|
||||
function () {
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
|
|||
return db_size[0].get("database_size")
|
||||
|
||||
def log_query(self, query, values, debug, explain):
|
||||
self.last_query = query = self._cursor._last_executed
|
||||
self.last_query = query = self._cursor._executed
|
||||
self._log_query(query, debug, explain)
|
||||
return self.last_query
|
||||
|
||||
|
|
|
|||
|
|
@ -16,38 +16,27 @@ frappe.ui.form.on("Bulk Update", {
|
|||
if (!frm.doc.update_value) {
|
||||
frappe.throw(__('Field "value" is mandatory. Please specify value to be updated'));
|
||||
} else {
|
||||
frappe
|
||||
.call({
|
||||
method: "frappe.desk.doctype.bulk_update.bulk_update.update",
|
||||
args: {
|
||||
doctype: frm.doc.document_type,
|
||||
field: frm.doc.field,
|
||||
value: frm.doc.update_value,
|
||||
condition: frm.doc.condition,
|
||||
limit: frm.doc.limit,
|
||||
},
|
||||
})
|
||||
.then((r) => {
|
||||
let failed = r.message;
|
||||
if (!failed) failed = [];
|
||||
frm.call("bulk_update").then((r) => {
|
||||
let failed = r.message;
|
||||
if (!failed) failed = [];
|
||||
|
||||
if (failed.length && !r._server_messages) {
|
||||
frappe.throw(
|
||||
__("Cannot update {0}", [
|
||||
failed.map((f) => (f.bold ? f.bold() : f)).join(", "),
|
||||
])
|
||||
);
|
||||
} else {
|
||||
frappe.msgprint({
|
||||
title: __("Success"),
|
||||
message: __("Updated Successfully"),
|
||||
indicator: "green",
|
||||
});
|
||||
}
|
||||
if (failed.length && !r._server_messages) {
|
||||
frappe.throw(
|
||||
__("Cannot update {0}", [
|
||||
failed.map((f) => (f.bold ? f.bold() : f)).join(", "),
|
||||
])
|
||||
);
|
||||
} else {
|
||||
frappe.msgprint({
|
||||
title: __("Success"),
|
||||
message: __("Updated Successfully"),
|
||||
indicator: "green",
|
||||
});
|
||||
}
|
||||
|
||||
frappe.hide_progress();
|
||||
frm.save();
|
||||
});
|
||||
frappe.hide_progress();
|
||||
frm.save();
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,26 +10,24 @@ from frappe.utils.scheduler import is_scheduler_inactive
|
|||
|
||||
|
||||
class BulkUpdate(Document):
|
||||
pass
|
||||
@frappe.whitelist()
|
||||
def bulk_update(self):
|
||||
self.check_permission("write")
|
||||
limit = self.limit if self.limit and cint(self.limit) < 500 else 500
|
||||
|
||||
condition = ""
|
||||
if self.condition:
|
||||
if ";" in self.condition:
|
||||
frappe.throw(_("; not allowed in condition"))
|
||||
|
||||
@frappe.whitelist()
|
||||
def update(doctype, field, value, condition="", limit=500):
|
||||
if not limit or cint(limit) > 500:
|
||||
limit = 500
|
||||
condition = f" where {self.condition}"
|
||||
|
||||
if condition:
|
||||
condition = " where " + condition
|
||||
|
||||
if ";" in condition:
|
||||
frappe.throw(_("; not allowed in condition"))
|
||||
|
||||
docnames = frappe.db.sql_list(
|
||||
f"""select name from `tab{doctype}`{condition} limit {limit} offset 0"""
|
||||
)
|
||||
data = {}
|
||||
data[field] = value
|
||||
return submit_cancel_or_update_docs(doctype, docnames, "update", data)
|
||||
docnames = frappe.db.sql_list(
|
||||
f"""select name from `tab{self.document_type}`{condition} limit {limit} offset 0"""
|
||||
)
|
||||
return submit_cancel_or_update_docs(
|
||||
self.document_type, docnames, "update", {self.field: self.update_value}
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -93,10 +93,17 @@ def add(args=None):
|
|||
|
||||
doc = frappe.get_doc(args["doctype"], args["name"])
|
||||
|
||||
# if assignee does not have permissions, share
|
||||
# if assignee does not have permissions, share or inform
|
||||
if not frappe.has_permission(doc=doc, user=assign_to):
|
||||
frappe.share.add(doc.doctype, doc.name, assign_to)
|
||||
shared_with_users.append(assign_to)
|
||||
if frappe.get_system_settings("disable_document_sharing"):
|
||||
msg = _("User {0} is not permitted to access this document.").format(frappe.bold(assign_to))
|
||||
msg += "<br>" + _(
|
||||
"As document sharing is disabled, please give them the required permissions before assigning."
|
||||
)
|
||||
frappe.throw(msg, title=_("Missing Permission"))
|
||||
else:
|
||||
frappe.share.add(doc.doctype, doc.name, assign_to)
|
||||
shared_with_users.append(assign_to)
|
||||
|
||||
# make this document followed by assigned user
|
||||
if frappe.get_cached_value("User", assign_to, "follow_assigned_documents"):
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ def sendmail_to_system_managers(subject, content):
|
|||
|
||||
@frappe.whitelist()
|
||||
def get_contact_list(txt, page_length=20) -> list[dict]:
|
||||
"""Returns contacts (from autosuggest)"""
|
||||
"""Return email ids for a multiselect field."""
|
||||
|
||||
if cached_contacts := get_cached_contacts(txt):
|
||||
return cached_contacts[:page_length]
|
||||
|
|
@ -19,11 +19,14 @@ def get_contact_list(txt, page_length=20) -> list[dict]:
|
|||
reportview_conditions = build_match_conditions("Contact")
|
||||
match_conditions = f"and {reportview_conditions}" if reportview_conditions else ""
|
||||
|
||||
# The multiselect field will store the `label` as the selected value.
|
||||
# The `value` is just used as a unique key to distinguish between the options.
|
||||
# https://github.com/frappe/frappe/blob/6c6a89bcdd9454060a1333e23b855d0505c9ebc2/frappe/public/js/frappe/form/controls/autocomplete.js#L29-L35
|
||||
out = frappe.db.sql(
|
||||
f"""select email_id as value,
|
||||
f"""select name as value, email_id as label,
|
||||
concat(first_name, ifnull(concat(' ',last_name), '' )) as description
|
||||
from tabContact
|
||||
where name like %(txt)s or email_id like %(txt)s
|
||||
where (name like %(txt)s or email_id like %(txt)s) and email_id != ''
|
||||
{match_conditions}
|
||||
limit %(page_length)s""",
|
||||
{"txt": f"%{txt}%", "page_length": page_length},
|
||||
|
|
|
|||
|
|
@ -325,7 +325,7 @@ class EmailAccount(Document):
|
|||
|
||||
if _raise_error:
|
||||
frappe.throw(
|
||||
_("Please setup default Email Account from Setup > Email > Email Account"),
|
||||
_("Please setup default Email Account from Settings > Email Account"),
|
||||
frappe.OutgoingEmailError,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -69,9 +69,7 @@ class SMTPServer:
|
|||
|
||||
if not self.server:
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Email Account not setup. Please create a new Email Account from Setup > Email > Email Account"
|
||||
),
|
||||
_("Email Account not setup. Please create a new Email Account from Settings > Email Account"),
|
||||
raise_exception=frappe.OutgoingEmailError,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -488,6 +488,9 @@ def update_options_for_fieldtype(fieldtype: str, old: str, new: str) -> None:
|
|||
|
||||
if frappe.conf.developer_mode:
|
||||
for name in frappe.get_all("DocField", filters={"options": old}, pluck="parent"):
|
||||
if name in (old, new):
|
||||
continue
|
||||
|
||||
doctype = frappe.get_doc("DocType", name)
|
||||
save = False
|
||||
for f in doctype.fields:
|
||||
|
|
@ -496,11 +499,11 @@ def update_options_for_fieldtype(fieldtype: str, old: str, new: str) -> None:
|
|||
save = True
|
||||
if save:
|
||||
doctype.save()
|
||||
else:
|
||||
DocField = frappe.qb.DocType("DocField")
|
||||
frappe.qb.update(DocField).set(DocField.options, new).where(
|
||||
(DocField.fieldtype == fieldtype) & (DocField.options == old)
|
||||
).run()
|
||||
|
||||
DocField = frappe.qb.DocType("DocField")
|
||||
frappe.qb.update(DocField).set(DocField.options, new).where(
|
||||
(DocField.fieldtype == fieldtype) & (DocField.options == old)
|
||||
).run()
|
||||
|
||||
frappe.qb.update(CustomField).set(CustomField.options, new).where(
|
||||
(CustomField.fieldtype == fieldtype) & (CustomField.options == old)
|
||||
|
|
|
|||
|
|
@ -77,6 +77,9 @@ def has_permission(
|
|||
if user == "Administrator":
|
||||
return True
|
||||
|
||||
if ptype == "share" and frappe.get_system_settings("disable_document_sharing"):
|
||||
return False
|
||||
|
||||
if not doc and hasattr(doctype, "doctype"):
|
||||
# first argument can be doc or doctype
|
||||
doc = doctype
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ frappe.ui.form.ControlBarcode = class ControlBarcode extends frappe.ui.form.Cont
|
|||
let svg = value;
|
||||
let barcode_value = "";
|
||||
|
||||
this.set_empty_description();
|
||||
if (value && value.startsWith("<svg")) {
|
||||
barcode_value = $(svg).attr("data-barcode-value");
|
||||
}
|
||||
|
|
@ -44,10 +45,14 @@ frappe.ui.form.ControlBarcode = class ControlBarcode extends frappe.ui.form.Cont
|
|||
if (value) {
|
||||
// Get svg
|
||||
const svg = this.barcode_area.find("svg")[0];
|
||||
JsBarcode(svg, value, this.get_options(value));
|
||||
$(svg).attr("data-barcode-value", value);
|
||||
$(svg).attr("width", "100%");
|
||||
return this.barcode_area.html();
|
||||
try {
|
||||
JsBarcode(svg, value, this.get_options(value));
|
||||
$(svg).attr("data-barcode-value", value);
|
||||
$(svg).attr("width", "100%");
|
||||
return this.barcode_area.html();
|
||||
} catch (e) {
|
||||
this.set_description(`Invalid Barcode: ${String(e)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ frappe.form.formatters = {
|
|||
return value;
|
||||
}
|
||||
if (value) {
|
||||
value = frappe.datetime.str_to_user(value);
|
||||
value = frappe.datetime.str_to_user(value, false, true);
|
||||
// handle invalid date
|
||||
if (value === "Invalid date") {
|
||||
value = null;
|
||||
|
|
|
|||
|
|
@ -1,49 +1,51 @@
|
|||
<div class="clearfix"></div>
|
||||
{% for(var i=0, l=contact_list.length; i<l; i++) { %}
|
||||
{% for(const contact of contact_list) { %}
|
||||
<div class="address-box">
|
||||
<p class="h6 flex align-center">
|
||||
{%= contact_list[i].first_name %} {%= contact_list[i].last_name %}
|
||||
{% if(contact_list[i].is_primary_contact) { %}
|
||||
{%= contact.first_name %} {%= contact.last_name %}
|
||||
{% if(contact.is_primary_contact) { %}
|
||||
<span class="text-muted"> ({%= __("Primary") %})</span>
|
||||
{% } %}
|
||||
{% if(contact_list[i].designation){ %}
|
||||
<span class="text-muted">– {%= contact_list[i].designation %}</span>
|
||||
{% if(contact.designation){ %}
|
||||
<span class="text-muted">– {%= contact.designation %}</span>
|
||||
{% } %}
|
||||
<a href="/app/Form/Contact/{%= encodeURIComponent(contact_list[i].name) %}"
|
||||
<a href="/app/Form/Contact/{%= encodeURIComponent(contact.name) %}"
|
||||
class="btn btn-xs btn-default ml-auto">
|
||||
{%= __("Edit") %}
|
||||
</a>
|
||||
</p>
|
||||
{% if (contact_list[i].phones || contact_list[i].email_ids) { %}
|
||||
{% if (contact.phone || contact.mobile_no || contact.phone_nos.length > 0) { %}
|
||||
<p>
|
||||
{% if(contact_list[i].phone) { %}
|
||||
{%= __("Phone") %}: {%= contact_list[i].phone %}<span class="text-muted"> ({%= __("Primary") %})</span><br>
|
||||
{% if(contact.phone) { %}
|
||||
<a href="tel:{%= frappe.utils.escape_html(contact.phone) %}">{%= frappe.utils.escape_html(contact.phone) %}</a> · <span class="text-muted">{%= __("Primary Phone") %}</span><br>
|
||||
{% endif %}
|
||||
{% if(contact_list[i].mobile_no) { %}
|
||||
{%= __("Mobile No") %}: {%= contact_list[i].mobile_no %}<span class="text-muted"> ({%= __("Primary") %})</span><br>
|
||||
{% if(contact.mobile_no) { %}
|
||||
<a href="tel:{%= frappe.utils.escape_html(contact.mobile_no) %}">{%= frappe.utils.escape_html(contact.mobile_no) %}</a> · <span class="text-muted">{%= __("Primary Mobile") %}</span><br>
|
||||
{% endif %}
|
||||
{% if(contact_list[i].phone_nos) { %}
|
||||
{% for(var j=0, k=contact_list[i].phone_nos.length; j<k; j++) { %}
|
||||
{%= __("Phone") %}: {%= contact_list[i].phone_nos[j].phone %}<br>
|
||||
{% } %}
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
{% if(contact_list[i].email_id) { %}
|
||||
{%= __("Email") %}: {%= contact_list[i].email_id %}<span class="text-muted"> ({%= __("Primary") %})</span><br>
|
||||
{% endif %}
|
||||
{% if(contact_list[i].email_ids) { %}
|
||||
{% for(var j=0, k=contact_list[i].email_ids.length; j<k; j++) { %}
|
||||
{%= __("Email") %}: {%= contact_list[i].email_ids[j].email_id %}<br>
|
||||
{% if(contact.phone_nos) { %}
|
||||
{% for(const phone_no of contact.phone_nos) { %}
|
||||
<a href="tel:{%= frappe.utils.escape_html(phone_no.phone) %}">{%= frappe.utils.escape_html(phone_no.phone) %}</a><br>
|
||||
{% } %}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if (contact.email_id || contact.email_ids.length > 0) { %}
|
||||
<p>
|
||||
{% if (contact_list[i].address) { %}
|
||||
{%= __("Address") %}: {%= contact_list[i].address %}<br>
|
||||
{% endif %}
|
||||
{% if(contact.email_id) { %}
|
||||
<a href="mailto:{%= frappe.utils.escape_html(contact.email_id) %}">{%= frappe.utils.escape_html(contact.email_id) %}</a> · <span class="text-muted">{%= __("Primary Email") %}</span><br>
|
||||
{% endif %}
|
||||
{% if(contact.email_ids) { %}
|
||||
{% for(const email_id of contact.email_ids) { %}
|
||||
<a href="mailto:{%= frappe.utils.escape_html(email_id.email_id) %}">{%= frappe.utils.escape_html(email_id.email_id) %}</a><br>
|
||||
{% } %}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if (contact.address) { %}
|
||||
<p>
|
||||
{%= contact.address %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% } %}
|
||||
{% if(!contact_list.length) { %}
|
||||
|
|
@ -51,4 +53,4 @@
|
|||
{% } %}
|
||||
<p><button class="btn btn-xs btn-default btn-contact">
|
||||
{{ __("New Contact") }}</button>
|
||||
</p>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ frappe.views.ListSidebar = class ListSidebar {
|
|||
this.reload_stats();
|
||||
});
|
||||
}
|
||||
|
||||
this.add_insights_banner();
|
||||
}
|
||||
|
||||
setup_views() {
|
||||
|
|
@ -239,4 +241,43 @@ frappe.views.ListSidebar = class ListSidebar {
|
|||
this.sidebar.find(".stat-no-records").remove();
|
||||
this.get_stats();
|
||||
}
|
||||
|
||||
add_insights_banner() {
|
||||
try {
|
||||
if (this.list_view.view != "Report") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (localStorage.getItem("show_insights_banner") == "false") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.insights_banner) {
|
||||
this.insights_banner.remove();
|
||||
}
|
||||
|
||||
const message = "Get more insights from your data with Frappe Insights.";
|
||||
const link = "https://frappe.io/s/insights";
|
||||
const cta = "Get Frappe Insights";
|
||||
|
||||
this.insights_banner = $(`
|
||||
<div style="position: relative;">
|
||||
<div class="">
|
||||
${message}
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<a href="${link}" target="_blank" style="color: var(--primary-color)">${cta} -> </a>
|
||||
</div>
|
||||
<div style="position: absolute; top: 0px; right: 0px; cursor: pointer;" title="Dismiss"
|
||||
onclick="localStorage.setItem('show_insights_banner', 'false') || this.parentElement.remove()">
|
||||
<svg class="icon icon-sm" style="">
|
||||
<use class="" href="#icon-close"></use>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`).appendTo(this.sidebar);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -447,6 +447,12 @@ $.extend(frappe.model, {
|
|||
},
|
||||
|
||||
can_share: function (doctype, frm) {
|
||||
let disable_sharing = cint(frappe.sys_defaults.disable_document_sharing);
|
||||
|
||||
if (disable_sharing && frappe.session.user !== "Administrator") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (frm) {
|
||||
return frm.perm[0].share === 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -208,7 +208,6 @@ body {
|
|||
// Overrides for each widgets
|
||||
&.dashboard-widget-box {
|
||||
min-height: 240px;
|
||||
padding: var(--padding-md) var(--padding-lg);
|
||||
|
||||
.filter-chart {
|
||||
background-color: var(--control-bg);
|
||||
|
|
@ -238,13 +237,16 @@ body {
|
|||
}
|
||||
|
||||
.widget-head {
|
||||
padding: var(--padding-sm);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.widget-body {
|
||||
padding-top: 7px;
|
||||
}
|
||||
|
||||
.widget-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -560,7 +562,7 @@ body {
|
|||
}
|
||||
|
||||
&.links-widget-box {
|
||||
padding: 18px 12px;
|
||||
padding: 12px 7px;
|
||||
|
||||
.link-item {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from frappe.utils import get_table_name
|
|||
class Base:
|
||||
terms = terms
|
||||
desc = Order.desc
|
||||
asc = Order.asc
|
||||
Schema = Schema
|
||||
Table = Table
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import unittest
|
|||
from io import StringIO
|
||||
from unittest.mock import patch
|
||||
|
||||
import git
|
||||
import yaml
|
||||
|
||||
import frappe
|
||||
|
|
@ -134,6 +135,9 @@ class TestBoilerPlate(unittest.TestCase):
|
|||
|
||||
self.check_parsable_python_files(new_app_dir)
|
||||
|
||||
app_repo = git.Repo(new_app_dir)
|
||||
self.assertEqual(app_repo.active_branch.name, "develop")
|
||||
|
||||
def test_create_app_without_git_init(self):
|
||||
app_name = "test_app_no_git"
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from unittest.mock import MagicMock
|
|||
import frappe
|
||||
from frappe.tests.test_api import FrappeAPITestCase
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils.caching import request_cache, site_cache
|
||||
from frappe.utils.caching import redis_cache, request_cache, site_cache
|
||||
|
||||
CACHE_TTL = 4
|
||||
external_service = MagicMock(return_value=30)
|
||||
|
|
@ -82,13 +82,84 @@ class TestSiteCache(FrappeAPITestCase):
|
|||
api_with_ttl = f"{module}.ping_with_ttl"
|
||||
api_without_ttl = f"{module}.ping"
|
||||
|
||||
start = time.monotonic()
|
||||
for _ in range(5):
|
||||
self.get(f"/api/method/{api_with_ttl}")
|
||||
self.get(f"/api/method/{api_without_ttl}")
|
||||
end = time.monotonic()
|
||||
|
||||
self.assertEqual(register_with_external_service.call_count, 2)
|
||||
time.sleep(CACHE_TTL - (end - start))
|
||||
time.sleep(CACHE_TTL)
|
||||
self.get(f"/api/method/{api_with_ttl}")
|
||||
self.assertEqual(register_with_external_service.call_count, 3)
|
||||
|
||||
|
||||
class TestRedisCache(FrappeAPITestCase):
|
||||
def test_redis_cache(self):
|
||||
function_call_count = 0
|
||||
|
||||
@redis_cache(ttl=CACHE_TTL)
|
||||
def calculate_area(radius: float) -> float:
|
||||
nonlocal function_call_count
|
||||
function_call_count += 1
|
||||
return 3.14 * radius**2
|
||||
|
||||
self.assertEqual(calculate_area(10), 314)
|
||||
self.assertEqual(function_call_count, 1)
|
||||
self.assertEqual(calculate_area(10), 314)
|
||||
self.assertEqual(function_call_count, 1)
|
||||
|
||||
time.sleep(CACHE_TTL)
|
||||
self.assertEqual(calculate_area(10), 314)
|
||||
self.assertEqual(function_call_count, 2)
|
||||
|
||||
calculate_area.clear_cache()
|
||||
self.assertEqual(calculate_area(10), 314)
|
||||
self.assertEqual(function_call_count, 3)
|
||||
calculate_area.clear_cache()
|
||||
|
||||
def test_redis_cache_without_params(self):
|
||||
function_call_count = 0
|
||||
|
||||
@redis_cache
|
||||
def calculate_area(radius: float) -> float:
|
||||
nonlocal function_call_count
|
||||
function_call_count += 1
|
||||
return 3.14 * radius**2
|
||||
|
||||
calculate_area.clear_cache()
|
||||
self.assertEqual(calculate_area(10), 314)
|
||||
self.assertEqual(function_call_count, 1)
|
||||
|
||||
calculate_area.clear_cache()
|
||||
self.assertEqual(calculate_area(10), 314)
|
||||
self.assertEqual(function_call_count, 2)
|
||||
|
||||
calculate_area.clear_cache()
|
||||
|
||||
def test_redis_cache_diff_args(self):
|
||||
function_call_count = 0
|
||||
|
||||
@redis_cache(ttl=CACHE_TTL)
|
||||
def calculate_area(radius: float) -> float:
|
||||
nonlocal function_call_count
|
||||
function_call_count += 1
|
||||
return 3.14 * radius**2
|
||||
|
||||
self.assertEqual(calculate_area(10), 314)
|
||||
self.assertEqual(function_call_count, 1)
|
||||
self.assertEqual(calculate_area(100), 31400)
|
||||
self.assertEqual(function_call_count, 2)
|
||||
|
||||
self.assertEqual(calculate_area(5), 25 * 3.14)
|
||||
self.assertEqual(function_call_count, 3)
|
||||
|
||||
calculate_area(10)
|
||||
# from cache now
|
||||
self.assertEqual(function_call_count, 3)
|
||||
|
||||
calculate_area(radius=10)
|
||||
# args, kwargs are treated differently
|
||||
self.assertEqual(function_call_count, 4)
|
||||
|
||||
calculate_area(radius=10)
|
||||
# kwargs should hit cache too
|
||||
self.assertEqual(function_call_count, 4)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from frappe.core.doctype.doctype.test_doctype import new_doctype
|
|||
from frappe.query_builder import Field
|
||||
from frappe.query_builder.functions import Max
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import random_string
|
||||
from frappe.utils.nestedset import (
|
||||
NestedSetChildExistsError,
|
||||
NestedSetInvalidMergeError,
|
||||
|
|
@ -213,6 +214,12 @@ class TestNestedSet(FrappeTestCase):
|
|||
remove_subtree("Test Tree DocType", "Parent 2")
|
||||
self.test_basic_tree()
|
||||
|
||||
def test_rename_nestedset(self):
|
||||
doctype = new_doctype(is_tree=True).insert()
|
||||
|
||||
# Rename doctype
|
||||
frappe.rename_doc("DocType", doctype.name, "Test " + random_string(10), force=True)
|
||||
|
||||
def test_merge_groups(self):
|
||||
global records
|
||||
el = {"some_fieldname": "Parent 2", "parent_test_tree_doctype": "Root Node", "is_group": 1}
|
||||
|
|
|
|||
|
|
@ -9,14 +9,9 @@ from unittest.mock import patch
|
|||
|
||||
import frappe
|
||||
from frappe.core.doctype.doctype.test_doctype import new_doctype
|
||||
from frappe.exceptions import DoesNotExistError, ValidationError
|
||||
from frappe.exceptions import DoesNotExistError
|
||||
from frappe.model.base_document import get_controller
|
||||
from frappe.model.rename_doc import (
|
||||
bulk_rename,
|
||||
get_fetch_fields,
|
||||
update_document_title,
|
||||
update_linked_doctypes,
|
||||
)
|
||||
from frappe.model.rename_doc import bulk_rename, update_document_title
|
||||
from frappe.modules.utils import get_doc_path
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_to_date, now
|
||||
|
|
@ -255,14 +250,16 @@ class TestRenameDoc(FrappeTestCase):
|
|||
)
|
||||
|
||||
def test_deprecated_utils(self):
|
||||
from frappe.model.rename_doc import get_fetch_fields, update_linked_doctypes
|
||||
|
||||
stdout = StringIO()
|
||||
|
||||
with redirect_stdout(stdout), patch_db(["set_value"]):
|
||||
get_fetch_fields("User", "ToDo", ["Activity Log"])
|
||||
self.assertTrue("Function frappe.model.rename_doc.get_fetch_fields" in stdout.getvalue())
|
||||
self.assertIn("Function frappe.model.rename_doc.get_fetch_fields", stdout.getvalue())
|
||||
|
||||
update_linked_doctypes("User", "ToDo", "str", "str")
|
||||
self.assertTrue("Function frappe.model.rename_doc.update_linked_doctypes" in stdout.getvalue())
|
||||
self.assertIn("Function frappe.model.rename_doc.update_linked_doctypes", stdout.getvalue())
|
||||
|
||||
def test_doc_rename_method(self):
|
||||
name = choice(self.available_documents)
|
||||
|
|
|
|||
|
|
@ -426,12 +426,15 @@ def create_blog_post():
|
|||
return doc
|
||||
|
||||
|
||||
def create_test_user():
|
||||
if frappe.db.exists("User", UI_TEST_USER):
|
||||
@whitelist_for_tests
|
||||
def create_test_user(username=None):
|
||||
name = username or UI_TEST_USER
|
||||
|
||||
if frappe.db.exists("User", name):
|
||||
return
|
||||
|
||||
user = frappe.new_doc("User")
|
||||
user.email = UI_TEST_USER
|
||||
user.email = name
|
||||
user.first_name = "Frappe"
|
||||
user.new_password = frappe.local.conf.admin_password
|
||||
user.send_welcome_email = 0
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ def change_settings(doctype, settings_dict):
|
|||
# change setting
|
||||
for key, value in settings_dict.items():
|
||||
setattr(settings, key, value)
|
||||
settings.save()
|
||||
settings.save(ignore_permissions=True)
|
||||
# singles are cached by default, clear to avoid flake
|
||||
frappe.db.value_cache[settings] = {}
|
||||
yield # yield control to calling function
|
||||
|
|
@ -153,7 +153,7 @@ def change_settings(doctype, settings_dict):
|
|||
settings = frappe.get_doc(doctype)
|
||||
for key, value in previous_settings.items():
|
||||
setattr(settings, key, value)
|
||||
settings.save()
|
||||
settings.save(ignore_permissions=True)
|
||||
|
||||
|
||||
def timeout(seconds=30, error_message="Test timed out."):
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ Content,Inhalt,
|
|||
Content Type,Inhaltstyp,
|
||||
Create,Erstellen,
|
||||
Created By,Erstellt von,
|
||||
Crop,Zuschneiden,
|
||||
Current,Laufend,
|
||||
Custom HTML,Benutzerdefiniertes HTML,
|
||||
Custom?,Benutzerdefiniert?,
|
||||
|
|
@ -64,6 +65,7 @@ Delivery Status,Lieferstatus,
|
|||
Department,Abteilung,
|
||||
Details,Details,
|
||||
Document Name,Dokumentenname,
|
||||
Document Naming Settings,Dokumentenbenennungseinstellungen,
|
||||
Document Status,Dokumentenstatus,
|
||||
Document Type,Dokumententyp,
|
||||
Domain,Domäne,
|
||||
|
|
@ -1383,6 +1385,7 @@ Inverse,Invertieren,
|
|||
Is,Ist,
|
||||
Is Attachments Folder,Ist Ordner für Anhänge,
|
||||
Is Child Table,Ist Untertabelle,
|
||||
Is Custom,Ist benutzerdefiniert,
|
||||
Is Custom Field,Ist benutzerdefiniertes Feld,
|
||||
Is First Startup,Ist Erstes Startup,
|
||||
Is Folder,Ist Ordner,
|
||||
|
|
@ -1613,6 +1616,7 @@ Naming,Bezeichnung,
|
|||
Naming Rule, Benennungsregel,
|
||||
"Naming Options:\n<ol><li><b>field:[fieldname]</b> - By Field</li><li><b>naming_series:</b> - By Naming Series (field called naming_series must be present</li><li><b>Prompt</b> - Prompt user for a name</li><li><b>[series]</b> - Series by prefix (separated by a dot); for example PRE.#####</li>\n<li><b>format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####}</b> - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.</li></ol>","Namensoptionen: <ol><li> <b>Feld: [Feldname]</b> - Nach Feld </li><li> <b>naming_series:</b> - Nach der <b>Namensreihe</b> (das Feld naming_series muss vorhanden sein) </li><li> <b>Eingabeaufforderung</b> - Benutzer nach einem Namen fragen </li><li> <b>[Serie]</b> - Reihe nach Präfix (getrennt durch einen Punkt); zum Beispiel PRE. ##### </li><li> <b>Format: BEISPIEL- {MM} morewords {Feldname1} - {Feldname2} - {#####}</b> - Ersetzt alle verspannten Wörter (Feldnamen, Datumsworte (DD, MM, YY), Serien) durch ihren Wert. Außerhalb von Klammern können beliebige Zeichen verwendet werden. </li></ol>",
|
||||
Naming Series mandatory,Nummernkreis zwingend erforderlich,
|
||||
Navigation Settings,Navigationseinstellungen,
|
||||
Nested set error. Please contact the Administrator.,Schachtelfehler. Bitte den Administrator kontaktieren.,
|
||||
New Activity,Neue Aktivität,
|
||||
New Chat,Neuer Chat,
|
||||
|
|
@ -1720,11 +1724,11 @@ Note: Changing the Page Name will break previous URL to this page.,"Hinweis: Wen
|
|||
Note: Multiple sessions will be allowed in case of mobile device,Hinweis: Mehrere Sitzungen wird im Falle einer mobilen Gerät erlaubt sein,
|
||||
Nothing to show,Nichts anzuzeigen,
|
||||
Nothing to update,Nichts zu aktualisieren,
|
||||
Notification,Mitteilung,
|
||||
Notification,Benachrichtigung,
|
||||
Notification Recipient,Benachrichtigungsempfänger,
|
||||
Notification Tones,Benachrichtigungstöne,
|
||||
Notifications,Benachrichtigungen,
|
||||
Notifications and bulk mails will be sent from this outgoing server.,Hinweise und Massen-E-Mails werden von diesem Postausgangsserver versendet.,
|
||||
Notifications and bulk mails will be sent from this outgoing server.,Benachrichtigungen und Massen-E-Mails werden von diesem Postausgangsserver versendet.,
|
||||
Notify Users On Every Login,Benutzer bei jeder Anmeldung benachrichtigen,
|
||||
Notify if unreplied,"Benachrichtigen, wenn unbeantwortet",
|
||||
Notify if unreplied for (in mins),"Benachrichtigen, wenn unbeantwortet für (in Minuten)",
|
||||
|
|
@ -2323,6 +2327,7 @@ Show more details,Weiteres,
|
|||
Show only errors,Zeige nur Fehler,
|
||||
"Show title in browser window as ""Prefix - title""","Diesen Eintrag im Browser-Fenster als ""Präfix - Titel"" anzeigen",
|
||||
Showing only Numeric fields from Report,Nur numerische Felder aus Bericht anzeigen,
|
||||
Sidebar,Seitenleiste,
|
||||
Sidebar Items,Elemente der Seitenleiste,
|
||||
Sidebar Settings,Sidebar-Einstellungen,
|
||||
Sidebar and Comments,Sidebar und Kommentare,
|
||||
|
|
|
|||
|
|
|
@ -450,12 +450,20 @@ def disable():
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def reset_otp_secret(user):
|
||||
def reset_otp_secret(user: str):
|
||||
if frappe.session.user != user:
|
||||
frappe.only_for("System Manager", message=True)
|
||||
|
||||
otp_issuer = frappe.db.get_single_value("System Settings", "otp_issuer_name")
|
||||
user_email = frappe.db.get_value("User", user, "email")
|
||||
settings = frappe.get_cached_doc("System Settings")
|
||||
|
||||
if not settings.enable_two_factor_auth:
|
||||
frappe.throw(
|
||||
_("You have to enable Two Factor Auth from System Settings."),
|
||||
title=_("Enable Two Factor Auth"),
|
||||
)
|
||||
|
||||
otp_issuer = settings.otp_issuer_name or "Frappe Framework"
|
||||
user_email = frappe.get_cached_value("User", user, "email")
|
||||
|
||||
clear_default(user + "_otplogin")
|
||||
clear_default(user + "_otpsecret")
|
||||
|
|
@ -463,10 +471,10 @@ def reset_otp_secret(user):
|
|||
email_args = {
|
||||
"recipients": user_email,
|
||||
"sender": None,
|
||||
"subject": _("OTP Secret Reset - {0}").format(otp_issuer or "Frappe Framework"),
|
||||
"subject": _("OTP Secret Reset - {0}").format(otp_issuer),
|
||||
"message": _(
|
||||
"<p>Your OTP secret on {0} has been reset. If you did not perform this reset and did not request it, please contact your System Administrator immediately.</p>"
|
||||
).format(otp_issuer or "Frappe Framework"),
|
||||
).format(otp_issuer),
|
||||
"delayed": False,
|
||||
"retry": 3,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1031,8 +1031,13 @@ def groupby_metric(iterable: dict[str, list], key: str):
|
|||
return records
|
||||
|
||||
|
||||
def get_table_name(table_name: str) -> str:
|
||||
return f"tab{table_name}" if not table_name.startswith("__") else table_name
|
||||
def get_table_name(table_name: str, wrap_in_backticks: bool = False) -> str:
|
||||
name = f"tab{table_name}" if not table_name.startswith("__") else table_name
|
||||
|
||||
if wrap_in_backticks:
|
||||
return f"`{name}`"
|
||||
|
||||
return name
|
||||
|
||||
|
||||
def squashify(what):
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ def _create_app_boilerplate(dest, hooks, no_git=False):
|
|||
f.write(frappe.as_unicode(gitignore_template.format(app_name=hooks.app_name)))
|
||||
|
||||
# initialize git repository
|
||||
app_repo = git.Repo.init(app_directory)
|
||||
app_repo = git.Repo.init(app_directory, initial_branch="develop")
|
||||
app_repo.git.add(A=True)
|
||||
app_repo.index.commit("feat: Initialize App")
|
||||
|
||||
|
|
|
|||
|
|
@ -128,3 +128,39 @@ def site_cache(ttl: int | None = None, maxsize: int | None = None) -> Callable:
|
|||
return time_cache_wrapper(ttl)
|
||||
|
||||
return time_cache_wrapper
|
||||
|
||||
|
||||
def redis_cache(ttl: int | None = 3600, user: str | bool | None = None) -> Callable:
|
||||
"""Decorator to cache method calls and its return values in Redis
|
||||
|
||||
args:
|
||||
ttl: time to expiry in seconds, defaults to 1 hour
|
||||
user: `true` should cache be specific to session user.
|
||||
"""
|
||||
|
||||
def wrapper(func: Callable = None) -> Callable:
|
||||
|
||||
func_key = f"{func.__module__}.{func.__qualname__}"
|
||||
|
||||
def clear_cache():
|
||||
frappe.cache().delete_keys(func_key)
|
||||
|
||||
func.clear_cache = clear_cache
|
||||
func.ttl = ttl if not callable(ttl) else 3600
|
||||
|
||||
@wraps(func)
|
||||
def redis_cache_wrapper(*args, **kwargs):
|
||||
func_call_key = func_key + str(__generate_request_cache_key(args, kwargs))
|
||||
if frappe.cache().exists(func_call_key):
|
||||
return frappe.cache().get_value(func_call_key, user=user)
|
||||
else:
|
||||
val = func(*args, **kwargs)
|
||||
ttl = getattr(func, "ttl", 3600)
|
||||
frappe.cache().set_value(func_call_key, val, expires_in_sec=ttl, user=user)
|
||||
return val
|
||||
|
||||
return redis_cache_wrapper
|
||||
|
||||
if callable(ttl):
|
||||
return wrapper(ttl)
|
||||
return wrapper
|
||||
|
|
|
|||
|
|
@ -195,6 +195,10 @@ class RedisWrapper(redis.Redis):
|
|||
except redis.exceptions.ConnectionError:
|
||||
return False
|
||||
|
||||
def exists(self, *names: str, user=None, shared=None) -> int:
|
||||
names = [self.make_key(n, user=user, shared=shared) for n in names]
|
||||
return super().exists(*names)
|
||||
|
||||
def hgetall(self, name):
|
||||
value = super().hgetall(self.make_key(name))
|
||||
return {key: pickle.loads(value) for key, value in value.items()}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import frappe
|
|||
from frappe import _, get_module_path
|
||||
from frappe.core.doctype.access_log.access_log import make_access_log
|
||||
from frappe.core.doctype.document_share_key.document_share_key import is_expired
|
||||
from frappe.utils import cint, sanitize_html, strip_html
|
||||
from frappe.utils import cint, escape_html, strip_html
|
||||
from frappe.utils.jinja_globals import is_rtl
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -27,12 +27,11 @@ def get_context(context):
|
|||
"""Build context for print"""
|
||||
if not ((frappe.form_dict.doctype and frappe.form_dict.name) or frappe.form_dict.doc):
|
||||
return {
|
||||
"body": sanitize_html(
|
||||
"""<h1>Error</h1>
|
||||
"body": f"""
|
||||
<h1>Error</h1>
|
||||
<p>Parameters doctype and name required</p>
|
||||
<pre>%s</pre>"""
|
||||
% repr(frappe.form_dict)
|
||||
)
|
||||
<pre>{escape_html(frappe.as_json(frappe.form_dict, indent=2))}</pre>
|
||||
"""
|
||||
}
|
||||
|
||||
if frappe.form_dict.doc:
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ dependencies = [
|
|||
"Jinja2~=3.1.2",
|
||||
"Pillow~=9.3.0",
|
||||
"PyJWT~=2.4.0",
|
||||
"PyMySQL~=1.0.2",
|
||||
"PyMySQL==1.0.3",
|
||||
"PyPDF2~=2.1.0",
|
||||
"PyPika~=0.48.9",
|
||||
"PyQRCode~=1.2.1",
|
||||
|
|
@ -57,7 +57,7 @@ dependencies = [
|
|||
"python-dateutil~=2.8.1",
|
||||
"pytz==2022.1",
|
||||
"rauth~=0.7.3",
|
||||
"redis~=4.3.4",
|
||||
"redis~=4.5.4",
|
||||
"hiredis~=2.0.0",
|
||||
"requests-oauthlib~=1.3.0",
|
||||
"requests~=2.27.1",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue