diff --git a/.github/helper/documentation.py b/.github/helper/documentation.py
index 8156137e3f..b541583fd6 100644
--- a/.github/helper/documentation.py
+++ b/.github/helper/documentation.py
@@ -1,50 +1,73 @@
import sys
+import requests
from urllib.parse import urlparse
-import requests
-docs_repos = [
- "frappe_docs",
- "erpnext_documentation",
+WEBSITE_REPOS = [
"erpnext_com",
"frappe_io",
]
+DOCUMENTATION_DOMAINS = [
+ "docs.erpnext.com",
+ "frappeframework.com",
+]
-def uri_validator(x):
- result = urlparse(x)
- return all([result.scheme, result.netloc, result.path])
-def docs_link_exists(body):
- for line in body.splitlines():
- for word in line.split():
- if word.startswith('http') and uri_validator(word):
- parsed_url = urlparse(word)
- if parsed_url.netloc == "github.com":
- parts = parsed_url.path.split('/')
- if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
- return True
- if parsed_url.netloc in ["docs.erpnext.com", "frappeframework.com"]:
- return True
+def is_valid_url(url: str) -> bool:
+ parts = urlparse(url)
+ return all((parts.scheme, parts.netloc, parts.path))
+
+
+def is_documentation_link(word: str) -> bool:
+ if not word.startswith("http") or not is_valid_url(word):
+ return False
+
+ parsed_url = urlparse(word)
+ if parsed_url.netloc in DOCUMENTATION_DOMAINS:
+ return True
+
+ if parsed_url.netloc == "github.com":
+ parts = parsed_url.path.split("/")
+ if len(parts) == 5 and parts[1] == "frappe" and parts[2] in WEBSITE_REPOS:
+ return True
+
+ return False
+
+
+def contains_documentation_link(body: str) -> bool:
+ return any(
+ is_documentation_link(word)
+ for line in body.splitlines()
+ for word in line.split()
+ )
+
+
+def check_pull_request(number: str) -> "tuple[int, str]":
+ response = requests.get(f"https://api.github.com/repos/frappe/frappe/pulls/{number}")
+ if not response.ok:
+ return 1, "Pull Request Not Found! ⚠️"
+
+ payload = response.json()
+ title = (payload.get("title") or "").lower().strip()
+ head_sha = (payload.get("head") or {}).get("sha")
+ body = (payload.get("body") or "").lower()
+
+ if (
+ not title.startswith("feat")
+ or not head_sha
+ or "no-docs" in body
+ or "backport" in body
+ ):
+ return 0, "Skipping documentation checks... 🏃"
+
+ if contains_documentation_link(body):
+ return 0, "Documentation Link Found. You're Awesome! 🎉"
+
+ return 1, "Documentation Link Not Found! ⚠️"
if __name__ == "__main__":
- pr = sys.argv[1]
- response = requests.get(f"https://api.github.com/repos/frappe/frappe/pulls/{pr}")
-
- if response.ok:
- payload = response.json()
- title = (payload.get("title") or "").lower()
- head_sha = (payload.get("head") or {}).get("sha")
- body = (payload.get("body") or "").lower()
-
- if title.startswith("feat") and head_sha and "no-docs" not in body:
- if docs_link_exists(body):
- print("Documentation Link Found. You're Awesome! 🎉")
-
- else:
- print("Documentation Link Not Found! ⚠️")
- sys.exit(1)
-
- else:
- print("Skipping documentation checks... 🏃")
+ exit_code, message = check_pull_request(sys.argv[1])
+ print(message)
+ sys.exit(exit_code)
diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml
index d050ecb6bc..be343c1254 100644
--- a/.github/workflows/linters.yml
+++ b/.github/workflows/linters.yml
@@ -92,7 +92,8 @@ jobs:
${{ runner.os }}-pip-
${{ runner.os }}-
- - run: |
+ - name: Install and run pip-audit
+ run: |
pip install pip-audit
cd ${GITHUB_WORKSPACE}
sed -i '/dropbox/d' pyproject.toml # Remove dropbox temporarily https://github.com/dropbox/dropbox-sdk-python/pull/456
diff --git a/cypress/integration/kanban.js b/cypress/integration/kanban.js
index 04a72a9436..f14c991c7c 100644
--- a/cypress/integration/kanban.js
+++ b/cypress/integration/kanban.js
@@ -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(() => {
diff --git a/cypress/integration/list_view.js b/cypress/integration/list_view.js
index 1fed62d678..3fa0758f0c 100644
--- a/cypress/integration/list_view.js
+++ b/cypress/integration/list_view.js
@@ -54,12 +54,8 @@ context("List View", () => {
method: "POST",
url: "api/method/frappe.model.workflow.bulk_workflow_approval",
}).as("bulk-approval");
- cy.intercept({
- method: "POST",
- url: "api/method/frappe.desk.reportview.get",
- }).as("real-time-update");
cy.wrap(elements).contains("Approve").click();
- cy.wait(["@bulk-approval", "@real-time-update"]);
+ cy.wait("@bulk-approval");
cy.wait(300);
cy.get_open_dialog().find(".btn-modal-close").click();
cy.reload();
diff --git a/cypress/integration/list_view_drag_select.js b/cypress/integration/list_view_drag_select.js
new file mode 100644
index 0000000000..d481390d89
--- /dev/null
+++ b/cypress/integration/list_view_drag_select.js
@@ -0,0 +1,48 @@
+context("List View", () => {
+ before(() => {
+ cy.login();
+ cy.go_to_list("DocType");
+ });
+
+ it("List view check rows on drag", () => {
+ cy.get(".list-row-checkbox").then(($checkbox) => {
+ cy.wrap($checkbox).first().trigger("mousedown");
+ cy.get(".level.list-row").each(($ele) => {
+ cy.wrap($ele).trigger("mousemove");
+ });
+ cy.document().trigger("mouseup");
+ });
+
+ cy.get(".level.list-row .list-row-checkbox").each(($checkbox) => {
+ cy.wrap($checkbox).should("be.checked");
+ });
+ });
+
+ it("Check all rows are checked", () => {
+ cy.get(".level.list-row .list-row-checkbox")
+ .its("length")
+ .then((len) => {
+ cy.get(".level-item.list-header-meta")
+ .should("be.visible")
+ .should("contain.text", `${len} items selected`);
+ });
+ });
+
+ it("List view uncheck rows on drag", () => {
+ cy.get(".list-row-checkbox").then(($checkbox) => {
+ cy.wrap($checkbox).first().trigger("mousedown");
+ cy.get(".level.list-row").each(($ele) => {
+ cy.wrap($ele).trigger("mousemove");
+ });
+ cy.document().trigger("mouseup");
+ });
+
+ cy.get(".level.list-row .list-row-checkbox").each(($checkbox) => {
+ cy.wrap($checkbox).should("not.be.checked");
+ });
+ });
+
+ it("Check all rows are unchecked", () => {
+ cy.get(".level-item.list-header-meta").should("not.be.visible");
+ });
+});
diff --git a/cypress/integration/rounding.js b/cypress/integration/rounding.js
index 647b32a6a5..1a9cfa685b 100644
--- a/cypress/integration/rounding.js
+++ b/cypress/integration/rounding.js
@@ -4,11 +4,11 @@ context("Rounding behaviour", () => {
cy.visit("/app/");
});
- it("Rounds floats accurately", () => {
+ it("Commercial Rounding", () => {
cy.window()
.its("flt")
.then((flt) => {
- let rounding_method = "Rounding Half Away From Zero";
+ let rounding_method = "Commercial Rounding";
expect(flt("0.5", 0, null, rounding_method)).eq(1);
expect(flt("0.3", null, null, rounding_method)).eq(0.3);
@@ -41,4 +41,64 @@ context("Rounding behaviour", () => {
expect(flt(-0.15, 1, null, rounding_method)).eq(-0.2);
});
});
+
+ it("Banker's Rounding", () => {
+ cy.window()
+ .its("flt")
+ .then((flt) => {
+ let rounding_method = "Banker's Rounding";
+
+ expect(flt("0.5", 0, null, rounding_method)).eq(0);
+ expect(flt("0.3", null, rounding_method)).eq(0.3);
+
+ expect(flt("1.5", 0, null, rounding_method)).eq(2);
+
+ // positive rounding to integers
+ expect(flt(0.4, 0, null, rounding_method)).eq(0);
+ expect(flt(0.5, 0, null, rounding_method)).eq(0);
+ expect(flt(1.455, 0, null, rounding_method)).eq(1);
+ expect(flt(1.5, 0, null, rounding_method)).eq(2);
+
+ // negative rounding to integers
+ expect(flt(-0.5, 0, null, rounding_method)).eq(0);
+ expect(flt(-1.5, 0, null, rounding_method)).eq(-2);
+
+ // negative precision i.e. round to nearest 10th
+ expect(flt(123, -1, null, rounding_method)).eq(120);
+ expect(flt(125, -1, null, rounding_method)).eq(120);
+ expect(flt(134.45, -1, null, rounding_method)).eq(130);
+ expect(flt(135, -1, null, rounding_method)).eq(140);
+
+ // positive multiple digit rounding
+ expect(flt(1.25, 1, null, rounding_method)).eq(1.2);
+ expect(flt(0.15, 1, null, rounding_method)).eq(0.2);
+ expect(flt(2.675, 2, null, rounding_method)).eq(2.68);
+ expect(flt(-2.675, 2, null, rounding_method)).eq(-2.68);
+
+ // negative multiple digit rounding
+ expect(flt(-1.25, 1, null, rounding_method)).eq(-1.2);
+ expect(flt(-0.15, 1, null, rounding_method)).eq(-0.2);
+
+ // Nearest number and not even (the default behaviour)
+ expect(flt(0.5, 0, null, rounding_method)).eq(0);
+ expect(flt(1.5, 0, null, rounding_method)).eq(2);
+ expect(flt(2.5, 0, null, rounding_method)).eq(2);
+ expect(flt(3.5, 0, null, rounding_method)).eq(4);
+
+ expect(flt(0.05, 1, null, rounding_method)).eq(0.0);
+ expect(flt(1.15, 1, null, rounding_method)).eq(1.2);
+ expect(flt(2.25, 1, null, rounding_method)).eq(2.2);
+ expect(flt(3.35, 1, null, rounding_method)).eq(3.4);
+
+ expect(flt(-0.5, 0, null, rounding_method)).eq(0);
+ expect(flt(-1.5, 0, null, rounding_method)).eq(-2);
+ expect(flt(-2.5, 0, null, rounding_method)).eq(-2);
+ expect(flt(-3.5, 0, null, rounding_method)).eq(-4);
+
+ expect(flt(-0.05, 1, null, rounding_method)).eq(0.0);
+ expect(flt(-1.15, 1, null, rounding_method)).eq(-1.2);
+ expect(flt(-2.25, 1, null, rounding_method)).eq(-2.2);
+ expect(flt(-3.35, 1, null, rounding_method)).eq(-3.4);
+ });
+ });
});
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 332224f989..9d7befe2d1 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -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,
diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py
index 24a4c6a271..12e829ff09 100644
--- a/frappe/cache_manager.py
+++ b/frappe/cache_manager.py
@@ -63,6 +63,7 @@ user_cache_keys = (
"has_role:Page",
"has_role:Report",
"desk_sidebar_items",
+ "contacts",
)
doctype_cache_keys = (
diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py
index f41cca3c57..03374986d4 100644
--- a/frappe/commands/utils.py
+++ b/frappe/commands/utils.py
@@ -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"
diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py
index 5fe22eb7f2..965425019c 100644
--- a/frappe/contacts/doctype/address/address.py
+++ b/frappe/contacts/doctype/address/address.py
@@ -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 "",
diff --git a/frappe/contacts/doctype/address/test_address.py b/frappe/contacts/doctype/address/test_address.py
index 1d11c5efef..ecb95f9e0c 100644
--- a/frappe/contacts/doctype/address/test_address.py
+++ b/frappe/contacts/doctype/address/test_address.py
@@ -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)
diff --git a/frappe/core/doctype/access_log/access_log_list.js b/frappe/core/doctype/access_log/access_log_list.js
new file mode 100644
index 0000000000..dab5f083cb
--- /dev/null
+++ b/frappe/core/doctype/access_log/access_log_list.js
@@ -0,0 +1,7 @@
+frappe.listview_settings["Access Log"] = {
+ onload: function (list_view) {
+ frappe.require("logtypes.bundle.js", () => {
+ frappe.utils.logtypes.show_log_retention_message(list_view.doctype);
+ });
+ },
+};
diff --git a/frappe/core/doctype/communication/communication.json b/frappe/core/doctype/communication/communication.json
index 293a6b2c87..e5f090e2f7 100644
--- a/frappe/core/doctype/communication/communication.json
+++ b/frappe/core/doctype/communication/communication.json
@@ -318,7 +318,7 @@
},
{
"fieldname": "message_id",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"ignore_xss_filter": 1,
"label": "Message ID",
"length": 995,
@@ -395,7 +395,7 @@
"icon": "fa fa-comment",
"idx": 1,
"links": [],
- "modified": "2022-05-09 00:13:45.310564",
+ "modified": "2023-03-16 12:04:18.113817",
"modified_by": "Administrator",
"module": "Core",
"name": "Communication",
diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py
index adeac35204..067cba59b2 100644
--- a/frappe/core/doctype/communication/communication.py
+++ b/frappe/core/doctype/communication/communication.py
@@ -397,6 +397,7 @@ def on_doctype_update():
"""Add indexes in `tabCommunication`"""
frappe.db.add_index("Communication", ["reference_doctype", "reference_name"])
frappe.db.add_index("Communication", ["status", "communication_type"])
+ frappe.db.add_index("Communication", ["message_id(140)"])
def has_permission(doc, ptype, user):
diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py
index 2e199e014d..1733b7b716 100755
--- a/frappe/core/doctype/communication/email.py
+++ b/frappe/core/doctype/communication/email.py
@@ -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,
)
diff --git a/frappe/core/doctype/communication/mixins.py b/frappe/core/doctype/communication/mixins.py
index 7b6427d1c2..24b6a8fafb 100644
--- a/frappe/core/doctype/communication/mixins.py
+++ b/frappe/core/doctype/communication/mixins.py
@@ -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):
diff --git a/frappe/core/doctype/deleted_document/deleted_document.py b/frappe/core/doctype/deleted_document/deleted_document.py
index 14b9bb5c11..9aa8e41708 100644
--- a/frappe/core/doctype/deleted_document/deleted_document.py
+++ b/frappe/core/doctype/deleted_document/deleted_document.py
@@ -7,6 +7,7 @@ import frappe
from frappe import _
from frappe.desk.doctype.bulk_update.bulk_update import show_progress
from frappe.model.document import Document
+from frappe.model.workflow import get_workflow_name
class DeletedDocument(Document):
@@ -27,6 +28,11 @@ def restore(name, alert=True):
except frappe.DocstatusTransitionError:
frappe.msgprint(_("Cancelled Document restored as Draft"))
doc.docstatus = 0
+ active_workflow = get_workflow_name(doc.doctype)
+ if active_workflow:
+ workflow_state_fieldname = frappe.get_value("Workflow", active_workflow, "workflow_state_field")
+ if doc.get(workflow_state_fieldname):
+ doc.set(workflow_state_fieldname, None)
doc.insert()
doc.add_comment("Edit", _("restored {0} as {1}").format(deleted.deleted_name, doc.name))
diff --git a/frappe/core/doctype/docshare/test_docshare.py b/frappe/core/doctype/docshare/test_docshare.py
index b874042d15..e080b0d4ff 100644
--- a/frappe/core/doctype/docshare/test_docshare.py
+++ b/frappe/core/doctype/docshare/test_docshare.py
@@ -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"]},
+ )
diff --git a/frappe/core/doctype/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js
index d92277152c..a8eed4270d 100644
--- a/frappe/core/doctype/doctype/doctype.js
+++ b/frappe/core/doctype/doctype/doctype.js
@@ -42,12 +42,14 @@ frappe.ui.form.on("DocType", {
if (!frappe.boot.developer_mode && !frm.doc.custom) {
// make the document read-only
frm.set_read_only();
+ frm.dashboard.clear_comment();
frm.dashboard.add_comment(
__("DocTypes can not be modified, please use {0} instead", [customize_form_link]),
"blue",
true
);
} else if (frappe.boot.developer_mode) {
+ frm.dashboard.clear_comment();
let msg = __(
"This site is running in developer mode. Any change made here will be updated in code."
);
diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json
index 671a6e86e6..b3196158f5 100644
--- a/frappe/core/doctype/doctype/doctype.json
+++ b/frappe/core/doctype/doctype/doctype.json
@@ -189,6 +189,7 @@
},
{
"default": "0",
+ "depends_on": "eval:!doc.istable",
"fieldname": "beta",
"fieldtype": "Check",
"label": "Beta"
@@ -463,6 +464,7 @@
},
{
"default": "0",
+ "depends_on": "eval:!doc.istable",
"description": "Tree structures are implemented using Nested Set",
"fieldname": "is_tree",
"fieldtype": "Check",
@@ -539,6 +541,7 @@
},
{
"default": "0",
+ "depends_on": "eval:!doc.istable",
"fieldname": "is_virtual",
"fieldtype": "Check",
"label": "Is Virtual"
@@ -622,6 +625,7 @@
},
{
"default": "0",
+ "depends_on": "eval:!doc.istable",
"description": "Enables Calendar and Gantt views.",
"fieldname": "is_calendar_and_gantt",
"fieldtype": "Check",
@@ -708,7 +712,7 @@
"link_fieldname": "reference_doctype"
}
],
- "modified": "2023-01-04 17:23:09.206018",
+ "modified": "2023-03-23 16:15:51.067267",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
diff --git a/frappe/core/doctype/log_settings/log_settings.py b/frappe/core/doctype/log_settings/log_settings.py
index 4a3b457bcc..832be49f3c 100644
--- a/frappe/core/doctype/log_settings/log_settings.py
+++ b/frappe/core/doctype/log_settings/log_settings.py
@@ -20,6 +20,7 @@ DEFAULT_LOGTYPES_RETENTION = {
"Submission Queue": 30,
"Prepared Report": 30,
"Webhook Request Log": 30,
+ "Integration Request": 90,
"Reminder": 30,
}
diff --git a/frappe/core/doctype/prepared_report/prepared_report_list.js b/frappe/core/doctype/prepared_report/prepared_report_list.js
new file mode 100644
index 0000000000..d0565fe826
--- /dev/null
+++ b/frappe/core/doctype/prepared_report/prepared_report_list.js
@@ -0,0 +1,7 @@
+frappe.listview_settings["Prepared Report"] = {
+ onload: function (list_view) {
+ frappe.require("logtypes.bundle.js", () => {
+ frappe.utils.logtypes.show_log_retention_message(list_view.doctype);
+ });
+ },
+};
diff --git a/frappe/core/doctype/role/patches/v13_set_default_desk_properties.py b/frappe/core/doctype/role/patches/v13_set_default_desk_properties.py
index 87de6ac79a..22854fa5f8 100644
--- a/frappe/core/doctype/role/patches/v13_set_default_desk_properties.py
+++ b/frappe/core/doctype/role/patches/v13_set_default_desk_properties.py
@@ -4,8 +4,6 @@ from ..role import desk_properties
def execute():
- frappe.reload_doctype("user")
- frappe.reload_doctype("role")
for role in frappe.get_all("Role", ["name", "desk_access"]):
role_doc = frappe.get_doc("Role", role.name)
for key in desk_properties:
diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py
index 97a0e9b581..31b82501cb 100644
--- a/frappe/core/doctype/role/role.py
+++ b/frappe/core/doctype/role/role.py
@@ -64,13 +64,14 @@ class Role(Document):
user.save()
-def get_info_based_on_role(role, field="email"):
+def get_info_based_on_role(role, field="email", ignore_permissions=False):
"""Get information of all users that have been assigned this role"""
users = frappe.get_list(
"Has Role",
filters={"role": role, "parenttype": "User"},
parent_doctype="User",
fields=["parent as user_name"],
+ ignore_permissions=ignore_permissions,
)
return get_user_info(users, field)
diff --git a/frappe/core/doctype/rq_job/rq_job_list.js b/frappe/core/doctype/rq_job/rq_job_list.js
index 5f6646cd65..fed56a16fe 100644
--- a/frappe/core/doctype/rq_job/rq_job_list.js
+++ b/frappe/core/doctype/rq_job/rq_job_list.js
@@ -4,11 +4,15 @@ frappe.listview_settings["RQ Job"] = {
onload(listview) {
if (!has_common(frappe.user_roles, ["Administrator", "System Manager"])) return;
- listview.page.add_inner_button(__("Remove Failed Jobs"), () => {
- frappe.confirm(__("Are you sure you want to remove all failed jobs?"), () => {
- frappe.xcall("frappe.core.doctype.rq_job.rq_job.remove_failed_jobs");
- });
- });
+ listview.page.add_inner_button(
+ __("Remove Failed Jobs"),
+ () => {
+ frappe.confirm(__("Are you sure you want to remove all failed jobs?"), () => {
+ frappe.xcall("frappe.core.doctype.rq_job.rq_job.remove_failed_jobs");
+ });
+ },
+ __("Actions")
+ );
if (listview.list_view_settings) {
listview.list_view_settings.disable_count = 1;
@@ -20,6 +24,25 @@ frappe.listview_settings["RQ Job"] = {
listview.page.set_indicator(__("Scheduler: Active"), "green");
} else {
listview.page.set_indicator(__("Scheduler: Inactive"), "red");
+ listview.page.add_inner_button(
+ __("Enable Scheduler"),
+ () => {
+ frappe.confirm(__("Are you sure you want to re-enable scheduler?"), () => {
+ frappe
+ .xcall("frappe.utils.scheduler.activate_scheduler")
+ .then(() => {
+ frappe.show_alert(__("Enabled Scheduler"));
+ })
+ .catch((e) => {
+ frappe.show_alert({
+ message: __("Failed to enable scheduler: {0}", e),
+ indicator: "error",
+ });
+ });
+ });
+ },
+ __("Actions")
+ );
}
});
diff --git a/frappe/core/doctype/rq_job/test_rq_job.py b/frappe/core/doctype/rq_job/test_rq_job.py
index 2fbefda056..3b8e2d5cc3 100644
--- a/frappe/core/doctype/rq_job/test_rq_job.py
+++ b/frappe/core/doctype/rq_job/test_rq_job.py
@@ -44,6 +44,12 @@ class TestRQJob(FrappeTestCase):
)
self.check_status(job, "finished")
+ def test_configurable_ttl(self):
+ frappe.conf.rq_job_failure_ttl = 600
+ job = frappe.enqueue(method=self.BG_JOB, queue="short")
+
+ self.assertEqual(job.failure_ttl, 600)
+
def test_func_obj_serialization(self):
job = frappe.enqueue(method=test_func, queue="short")
rq_job = frappe.get_doc("RQ Job", job.id)
diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py
index dc502e4683..17cddee1e8 100644
--- a/frappe/core/doctype/server_script/server_script.py
+++ b/frappe/core/doctype/server_script/server_script.py
@@ -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
diff --git a/frappe/core/doctype/system_settings/system_settings.js b/frappe/core/doctype/system_settings/system_settings.js
index 1d5ba7ddb0..bf8988d64c 100644
--- a/frappe/core/doctype/system_settings/system_settings.js
+++ b/frappe/core/doctype/system_settings/system_settings.js
@@ -16,6 +16,8 @@ frappe.ui.form.on("System Settings", {
}
},
});
+
+ frm.trigger("set_rounding_method_options");
},
enable_password_policy: function (frm) {
if (frm.doc.enable_password_policy == 0) {
@@ -56,4 +58,17 @@ frappe.ui.form.on("System Settings", {
}
);
},
+
+ set_rounding_method_options: function (frm) {
+ if (frm.doc.rounding_method != "Banker's Rounding (legacy)") {
+ let field = frm.fields_dict.rounding_method;
+
+ field.df.options = field.df.options
+ .split("\n")
+ .filter((o) => o != "Banker's Rounding (legacy)")
+ .join("\n");
+
+ field.refresh();
+ }
+ },
});
diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index 2c9e92d943..102a0a76c2 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -13,6 +13,7 @@
"time_zone",
"enable_onboarding",
"setup_complete",
+ "disable_document_sharing",
"date_and_number_format",
"date_format",
"time_format",
@@ -523,17 +524,23 @@
"label": "Login with email link expiry (in minutes)"
},
{
- "default": "Round Half Even",
+ "default": "Banker's Rounding (legacy)",
"fieldname": "rounding_method",
"fieldtype": "Select",
"label": "Rounding Method",
- "options": "Round Half Even\nRounding Half Away From Zero"
+ "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-06 11:31:19.144956",
+ "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
-}
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js
index 18e8651819..918a9ee37c 100644
--- a/frappe/core/doctype/user/user.js
+++ b/frappe/core/doctype/user/user.js
@@ -114,12 +114,16 @@ frappe.ui.form.on("User", {
return;
}
+ function hasChanged(doc_attr, boot_attr) {
+ return (doc_attr || boot_attr) && doc_attr !== boot_attr;
+ }
+
if (
doc.name === frappe.session.user &&
!doc.__unsaved &&
frappe.all_timezones &&
- (doc.language || frappe.boot.user.language) &&
- doc.language !== frappe.boot.user.language
+ (hasChanged(doc.language, frappe.boot.user.language) ||
+ hasChanged(doc.time_zone, frappe.boot.time_zone.user))
) {
frappe.msgprint(__("Refreshing..."));
window.location.reload();
@@ -215,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 () {
diff --git a/frappe/custom/doctype/custom_field/custom_field.js b/frappe/custom/doctype/custom_field/custom_field.js
index fba19ca45e..be416cb49a 100644
--- a/frappe/custom/doctype/custom_field/custom_field.js
+++ b/frappe/custom/doctype/custom_field/custom_field.js
@@ -25,6 +25,27 @@ frappe.ui.form.on("Custom Field", {
frm.toggle_enable("dt", frm.doc.__islocal);
frm.trigger("dt");
frm.toggle_reqd("label", !frm.doc.fieldname);
+
+ if (frm.doc.is_system_generated) {
+ frm.dashboard.add_comment(
+ __(
+ "Warning: This field is system generated and may be overwritten by a future update. Modify it using {0} instead.",
+ [
+ frappe.utils.get_form_link(
+ "Customize Form",
+ "Customize Form",
+ true,
+ __("Customize Form"),
+ {
+ doc_type: frm.doc.dt,
+ }
+ ),
+ ]
+ ),
+ "yellow",
+ true
+ );
+ }
},
dt: function (frm) {
if (!frm.doc.dt) {
diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js
index 4ab693b415..c420dca3e4 100644
--- a/frappe/custom/doctype/customize_form/customize_form.js
+++ b/frappe/custom/doctype/customize_form/customize_form.js
@@ -89,7 +89,7 @@ frappe.ui.form.on("Customize Form", {
setup_sortable: function (frm) {
frm.doc.fields.forEach(function (f) {
- if (!f.is_custom_field) {
+ if (!f.is_custom_field || f.is_system_generated) {
f._sortable = false;
}
@@ -251,10 +251,23 @@ frappe.ui.form.on("Customize Form", {
// can't delete standard fields
frappe.ui.form.on("Customize Form Field", {
before_fields_remove: function (frm, doctype, name) {
- var row = frappe.get_doc(doctype, name);
+ const row = frappe.get_doc(doctype, name);
+
+ if (row.is_system_generated) {
+ frappe.throw(
+ __(
+ "Cannot delete system generated field {0}. You can hide it instead.",
+ [__(row.label) || row.fieldname]
+ )
+ );
+ }
+
if (!(row.is_custom_field || row.__islocal)) {
- frappe.msgprint(__("Cannot delete standard field. You can hide it if you want"));
- throw "cannot delete standard field";
+ frappe.throw(
+ __("Cannot delete standard field {0}. You can hide it instead.", [
+ __(row.label) || row.fieldname,
+ ])
+ );
}
},
fields_add: function (frm, cdt, cdn) {
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index bdd18cddfa..42cbf33f4f 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -193,8 +193,9 @@ class CustomizeForm(Document):
# docfield
for df in self.get("fields"):
meta_df = meta.get("fields", {"fieldname": df.fieldname})
- if not meta_df or meta_df[0].get("is_custom_field"):
+ if not meta_df or not is_standard_or_system_generated_field(meta_df[0]):
continue
+
self.set_property_setters_for_docfield(meta, df, meta_df)
# action and links
@@ -350,12 +351,14 @@ class CustomizeForm(Document):
def update_custom_fields(self):
for i, df in enumerate(self.get("fields")):
- if df.get("is_custom_field"):
- if not frappe.db.exists("Custom Field", {"dt": self.doc_type, "fieldname": df.fieldname}):
- self.add_custom_field(df, i)
- self.flags.update_db = True
- else:
- self.update_in_custom_field(df, i)
+ if is_standard_or_system_generated_field(df):
+ continue
+
+ if not frappe.db.exists("Custom Field", {"dt": self.doc_type, "fieldname": df.fieldname}):
+ self.add_custom_field(df, i)
+ self.flags.update_db = True
+ else:
+ self.update_in_custom_field(df, i)
self.delete_custom_fields()
@@ -380,7 +383,7 @@ class CustomizeForm(Document):
def update_in_custom_field(self, df, i):
meta = frappe.get_meta(self.doc_type)
meta_df = meta.get("fields", {"fieldname": df.fieldname})
- if not (meta_df and meta_df[0].get("is_custom_field")):
+ if not meta_df or is_standard_or_system_generated_field(meta_df[0]):
# not a custom field
return
@@ -416,7 +419,7 @@ class CustomizeForm(Document):
}
for fieldname in fields_to_remove:
df = meta.get("fields", {"fieldname": fieldname})[0]
- if df.get("is_custom_field"):
+ if not is_standard_or_system_generated_field(df):
frappe.delete_doc("Custom Field", df.name)
def make_property_setter(
@@ -561,6 +564,10 @@ def reset_customization(doctype):
frappe.clear_cache(doctype=doctype)
+def is_standard_or_system_generated_field(df):
+ return not df.get("is_custom_field") or df.get("is_system_generated")
+
+
doctype_properties = {
"search_fields": "Data",
"title_field": "Data",
diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py
index 661652c74c..8d98dc4149 100644
--- a/frappe/custom/doctype/customize_form/test_customize_form.py
+++ b/frappe/custom/doctype/customize_form/test_customize_form.py
@@ -403,3 +403,25 @@ class TestCustomizeForm(FrappeTestCase):
with self.assertRaises(frappe.ValidationError):
d.run_method("save_customization")
+
+ def test_system_generated_fields(self):
+ doctype = "Event"
+ custom_field_name = "test_custom_field"
+
+ custom_field = frappe.get_doc("Custom Field", {"dt": doctype, "fieldname": custom_field_name})
+ custom_field.is_system_generated = 1
+ custom_field.save()
+
+ d = self.get_customize_form(doctype)
+ custom_field = d.getone("fields", {"fieldname": custom_field_name})
+ custom_field.description = "Test Description"
+ d.run_method("save_customization")
+
+ property_setter_filters = {
+ "doc_type": doctype,
+ "field_name": custom_field_name,
+ "property": "description",
+ }
+ self.assertEqual(
+ frappe.db.get_value("Property Setter", property_setter_filters, "value"), "Test Description"
+ )
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 7e0cb83454..c51a8f10a7 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -1300,8 +1300,8 @@ def enqueue_jobs_after_commit():
execute_job,
timeout=job.get("timeout"),
kwargs=job.get("queue_args"),
- failure_ttl=RQ_JOB_FAILURE_TTL,
- result_ttl=RQ_RESULTS_TTL,
+ failure_ttl=frappe.conf.get("rq_job_failure_ttl") or RQ_JOB_FAILURE_TTL,
+ result_ttl=frappe.conf.get("rq_results_ttl") or RQ_RESULTS_TTL,
)
frappe.flags.enqueue_after_commit = []
diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py
index 7f9846d6c4..43540956e0 100644
--- a/frappe/database/mariadb/database.py
+++ b/frappe/database/mariadb/database.py
@@ -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
diff --git a/frappe/desk/doctype/bulk_update/bulk_update.js b/frappe/desk/doctype/bulk_update/bulk_update.js
index 017eee1480..d8a2b89cf3 100644
--- a/frappe/desk/doctype/bulk_update/bulk_update.js
+++ b/frappe/desk/doctype/bulk_update/bulk_update.js
@@ -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();
+ });
}
});
},
diff --git a/frappe/desk/doctype/bulk_update/bulk_update.py b/frappe/desk/doctype/bulk_update/bulk_update.py
index 5521d9583f..535be8155f 100644
--- a/frappe/desk/doctype/bulk_update/bulk_update.py
+++ b/frappe/desk/doctype/bulk_update/bulk_update.py
@@ -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()
diff --git a/frappe/desk/form/assign_to.py b/frappe/desk/form/assign_to.py
index 72265dce1f..2b17d38371 100644
--- a/frappe/desk/form/assign_to.py
+++ b/frappe/desk/form/assign_to.py
@@ -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 += "
" + _(
+ "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"):
diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py
index 9e10ced912..28377572c3 100644
--- a/frappe/desk/form/utils.py
+++ b/frappe/desk/form/utils.py
@@ -57,7 +57,14 @@ def update_comment(name, content):
if frappe.session.user not in ["Administrator", doc.owner]:
frappe.throw(_("Comment can only be edited by the owner"), frappe.PermissionError)
- doc.content = content
+ if doc.reference_doctype and doc.reference_name:
+ reference_doc = frappe.get_doc(doc.reference_doctype, doc.reference_name)
+ reference_doc.check_permission()
+
+ doc.content = extract_images_from_html(reference_doc, content, is_private=True)
+ else:
+ doc.content = content
+
doc.save(ignore_permissions=True)
diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py
index 4d4d76207a..cdb25b81ba 100755
--- a/frappe/desk/page/setup_wizard/setup_wizard.py
+++ b/frappe/desk/page/setup_wizard/setup_wizard.py
@@ -165,6 +165,7 @@ def update_system_settings(args):
"language": get_language_code(args.get("language")) or "en",
"time_zone": args.get("timezone"),
"float_precision": 3,
+ "rounding_method": "Banker's Rounding",
"date_format": frappe.db.get_value("Country", args.get("country"), "date_format"),
"time_format": frappe.db.get_value("Country", args.get("country"), "time_format"),
"number_format": number_format,
diff --git a/frappe/email/__init__.py b/frappe/email/__init__.py
index 1f6af4a3e7..486db2a784 100644
--- a/frappe/email/__init__.py
+++ b/frappe/email/__init__.py
@@ -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},
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index aa0935e028..faf28afdb3 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -19,7 +19,6 @@ from frappe.email.utils import get_port
from frappe.model.document import Document
from frappe.utils import cint, comma_or, cstr, parse_addr, validate_email_address
from frappe.utils.background_jobs import enqueue, get_jobs
-from frappe.utils.error import raise_error_on_no_output
from frappe.utils.jinja import render_template
from frappe.utils.user import get_system_managers
@@ -301,11 +300,6 @@ class EmailAccount(Document):
return cls.from_record({"sender": "notifications@example.com"})
@classmethod
- @raise_error_on_no_output(
- keep_quiet=lambda: not cint(frappe.get_system_settings("setup_complete")),
- error_message=_("Please setup default Email Account from Setup > Email > Email Account"),
- error_type=frappe.OutgoingEmailError,
- ) # noqa
@cache_email_account("outgoing_email_account")
def find_outgoing(cls, match_by_email=None, match_by_doctype=None, _raise_error=False):
"""Find the outgoing Email account to use.
@@ -329,6 +323,12 @@ class EmailAccount(Document):
if doc:
return {"default": doc}
+ if _raise_error:
+ frappe.throw(
+ _("Please setup default Email Account from Settings > Email Account"),
+ frappe.OutgoingEmailError,
+ )
+
@classmethod
def find_default_outgoing(cls):
"""Find default outgoing account."""
diff --git a/frappe/email/doctype/email_queue/email_queue.json b/frappe/email/doctype/email_queue/email_queue.json
index c9ec374687..ac8d656678 100644
--- a/frappe/email/doctype/email_queue/email_queue.json
+++ b/frappe/email/doctype/email_queue/email_queue.json
@@ -67,10 +67,9 @@
},
{
"fieldname": "message_id",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"label": "Message ID",
- "read_only": 1,
- "search_index": 1
+ "read_only": 1
},
{
"fieldname": "reference_doctype",
@@ -153,7 +152,7 @@
"idx": 1,
"in_create": 1,
"links": [],
- "modified": "2022-07-12 15:17:37.934316",
+ "modified": "2023-03-16 12:15:17.850292",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Queue",
diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py
index 17fdddfaf3..c10494b0d9 100644
--- a/frappe/email/doctype/email_queue/email_queue.py
+++ b/frappe/email/doctype/email_queue/email_queue.py
@@ -5,6 +5,7 @@ import json
import quopri
import smtplib
import traceback
+from contextlib import suppress
from email.parser import Parser
from email.policy import SMTPUTF8
@@ -407,6 +408,8 @@ def on_doctype_update():
"Email Queue", ("status", "send_after", "priority", "creation"), "index_bulk_flush"
)
+ frappe.db.add_index("Email Queue", ["message_id(140)"])
+
def get_email_retry_limit():
return cint(frappe.db.get_system_setting("email_retry_limit")) or 3
@@ -704,7 +707,10 @@ class QueueBuilder:
if not smtp_server_instance:
email_account = q.get_email_account()
smtp_server_instance = email_account.get_smtp_server()
- q.send(smtp_server_instance=smtp_server_instance)
+
+ with suppress(Exception):
+ q.send(smtp_server_instance=smtp_server_instance)
+
smtp_server_instance.quit()
def as_dict(self, include_recipients=True):
diff --git a/frappe/email/doctype/email_queue/patches/__init__.py b/frappe/email/doctype/email_queue/patches/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/email/doctype/email_queue/patches/drop_search_index_on_message_id.py b/frappe/email/doctype/email_queue/patches/drop_search_index_on_message_id.py
new file mode 100644
index 0000000000..7c4baf5a2a
--- /dev/null
+++ b/frappe/email/doctype/email_queue/patches/drop_search_index_on_message_id.py
@@ -0,0 +1,11 @@
+import frappe
+
+
+def execute():
+ """Drop search index on message_id"""
+
+ if frappe.db.get_column_type("Email Queue", "message_id") == "text":
+ return
+
+ if index := frappe.db.get_column_index("tabEmail Queue", "message_id", unique=False):
+ frappe.db.sql(f"ALTER TABLE `tabEmail Queue` DROP INDEX `{index.Key_name}`")
diff --git a/frappe/email/doctype/newsletter/newsletter.json b/frappe/email/doctype/newsletter/newsletter.json
index 2b692ced04..7ac6203ada 100644
--- a/frappe/email/doctype/newsletter/newsletter.json
+++ b/frappe/email/doctype/newsletter/newsletter.json
@@ -29,6 +29,7 @@
"message",
"message_md",
"message_html",
+ "campaign",
"attachments",
"send_unsubscribe_link",
"send_webview_link",
@@ -237,6 +238,13 @@
"label": "Total Views",
"no_copy": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "campaign",
+ "fieldtype": "Link",
+ "label": "Campaign",
+ "options": "Marketing Campaign",
+ "reqd": 0
}
],
"has_web_view": 1,
@@ -245,7 +253,7 @@
"index_web_pages_for_search": 1,
"is_published_field": "published",
"links": [],
- "modified": "2023-02-23 12:53:18.478018",
+ "modified": "2023-03-20 22:45:59.129630",
"modified_by": "Administrator",
"module": "Email",
"name": "Newsletter",
@@ -270,4 +278,4 @@
"states": [],
"title_field": "subject",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py
index b5f07aa59c..4a2f69a44c 100644
--- a/frappe/email/doctype/newsletter/newsletter.py
+++ b/frappe/email/doctype/newsletter/newsletter.py
@@ -167,7 +167,7 @@ class Newsletter(WebsiteGenerator):
attachments = self.get_newsletter_attachments()
sender = self.send_from or frappe.utils.get_formatted_email(self.owner)
args = self.as_dict()
- args["message"] = self.get_message()
+ args["message"] = self.get_message(medium="email")
is_auto_commit_set = bool(frappe.db.auto_commit_on_many_writes)
frappe.db.auto_commit_on_many_writes = not frappe.flags.in_test
@@ -193,7 +193,7 @@ class Newsletter(WebsiteGenerator):
frappe.db.auto_commit_on_many_writes = is_auto_commit_set
- def get_message(self) -> str:
+ def get_message(self, medium=None) -> str:
message = self.message
if self.content_type == "Markdown":
message = frappe.utils.md_to_html(self.message_md)
@@ -202,9 +202,9 @@ class Newsletter(WebsiteGenerator):
html = frappe.render_template(message, {"doc": self.as_dict()})
- return self.add_source(html)
+ return self.add_source(html, medium=medium)
- def add_source(self, html: str) -> str:
+ def add_source(self, html: str, medium="None") -> str:
"""Add source to the site links in the newsletter content."""
from bs4 import BeautifulSoup
@@ -216,8 +216,8 @@ class Newsletter(WebsiteGenerator):
if href and not href.startswith("#"):
if not frappe.utils.is_site_link(href):
continue
- new_href = frappe.utils.add_source_to_url(
- href, reference_doctype=self.doctype, reference_docname=self.name
+ new_href = frappe.utils.add_trackers_to_url(
+ href, source="Newsletter", campaign=self.campaign, medium=medium
)
link["href"] = new_href
@@ -377,14 +377,17 @@ def send_scheduled_email():
def newsletter_email_read(recipient_email, reference_doctype, reference_name):
verify_request()
try:
- doc = frappe.get_doc(reference_doctype, reference_name)
+ doc = frappe.get_cached_doc("Newsletter", reference_name)
if doc.add_viewed(recipient_email, force=True, unique_views=True):
- doc.db_set("total_views", frappe.utils.cint(doc.total_views) + 1, update_modified=False)
+ newsletter = frappe.qb.DocType("Newsletter")
+ (
+ frappe.qb.update(newsletter)
+ .set(newsletter.total_views, newsletter.total_views + 1)
+ .where(newsletter.name == doc.name)
+ ).run()
except Exception:
- frappe.log_error(
- f"Unable to mark as viewed for {recipient_email}", None, reference_doctype, reference_name
- )
+ doc.log_error(f"Unable to mark as viewed for {recipient_email}")
finally:
frappe.response.update(frappe.utils.get_imaginary_pixel_response())
diff --git a/frappe/email/doctype/newsletter/templates/newsletter.html b/frappe/email/doctype/newsletter/templates/newsletter.html
index 1244f4c49a..05f3560648 100644
--- a/frappe/email/doctype/newsletter/templates/newsletter.html
+++ b/frappe/email/doctype/newsletter/templates/newsletter.html
@@ -36,7 +36,7 @@
- {{ doc.get_message() }}
+ {{ doc.get_message(medium="web_page") }}
diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py
index d5071e23a0..2efbf597ec 100644
--- a/frappe/email/doctype/notification/notification.py
+++ b/frappe/email/doctype/notification/notification.py
@@ -298,7 +298,7 @@ def get_context(context):
# For sending emails to specified role
if recipient.receiver_by_role:
- emails = get_info_based_on_role(recipient.receiver_by_role, "email")
+ emails = get_info_based_on_role(recipient.receiver_by_role, "email", ignore_permissions=True)
for email in emails:
recipients = recipients + email.split("\n")
diff --git a/frappe/email/receive.py b/frappe/email/receive.py
index 538ab7738d..59d41b543f 100644
--- a/frappe/email/receive.py
+++ b/frappe/email/receive.py
@@ -782,7 +782,7 @@ class InboundMail(Email):
Here are the cases to handle:
1. If mail is a reply to already sent mail, then we can get parent communicaion from
- Email Queue record.
+ Email Queue record or message_id on communication.
2. Sometimes we send communication name in message-ID directly, use that to get parent communication.
3. Sender sent a reply but reply is on top of what (s)he sent before,
then parent record exists directly in communication.
@@ -795,17 +795,15 @@ class InboundMail(Email):
if not self.is_reply():
return ""
- if not self.is_reply_to_system_sent_mail():
- communication = Communication.find_one_by_filters(
- message_id=self.in_reply_to, creation=[">=", self.get_relative_dt(-30)]
- )
- elif self.parent_email_queue() and self.parent_email_queue().communication:
- communication = Communication.find(self.parent_email_queue().communication, ignore_error=True)
- else:
- reference = self.in_reply_to
- if "@" in self.in_reply_to:
- reference, _ = self.in_reply_to.split("@", 1)
- communication = Communication.find(reference, ignore_error=True)
+ communication = Communication.find_one_by_filters(message_id=self.in_reply_to)
+ if not communication:
+ if self.parent_email_queue() and self.parent_email_queue().communication:
+ communication = Communication.find(self.parent_email_queue().communication, ignore_error=True)
+ else:
+ reference = self.in_reply_to
+ if "@" in self.in_reply_to:
+ reference, _ = self.in_reply_to.split("@", 1)
+ communication = Communication.find(reference, ignore_error=True)
self._parent_communication = communication or ""
return self._parent_communication
diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py
index 028b21b0ae..3b22bc4ce4 100644
--- a/frappe/email/smtp.py
+++ b/frappe/email/smtp.py
@@ -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,
)
diff --git a/frappe/handler.py b/frappe/handler.py
index 0a25f329c7..58241c1223 100644
--- a/frappe/handler.py
+++ b/frappe/handler.py
@@ -198,7 +198,7 @@ def upload_file():
filename = file.filename
content_type = guess_type(filename)[0]
- if optimize and content_type.startswith("image/"):
+ if optimize and content_type and content_type.startswith("image/"):
args = {"content": content, "content_type": content_type}
if frappe.form_dict.max_width:
args["max_width"] = int(frappe.form_dict.max_width)
diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js
index 9a5e9a4dc7..6fee47f1bd 100644
--- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js
+++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js
@@ -5,50 +5,43 @@ frappe.ui.form.on("Dropbox Settings", {
refresh: function (frm) {
frm.toggle_display(
["app_access_key", "app_secret_key"],
- !(frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config)
+ !frm.doc.__onload?.dropbox_setup_via_site_config
);
- frm.clear_custom_buttons();
frm.events.take_backup(frm);
},
+ are_keys_present: function (frm) {
+ return (
+ (frm.doc.app_access_key && frm.doc.app_secret_key) ||
+ frm.doc.__onload?.dropbox_setup_via_site_config
+ );
+ },
+
allow_dropbox_access: function (frm) {
- if (frm.doc.app_access_key && frm.doc.app_secret_key) {
- frappe.call({
- method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.get_dropbox_authorize_url",
- freeze: true,
- callback: function (r) {
- if (!r.exc) {
- window.open(r.message.auth_url);
- }
- },
- });
- } else if (frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config) {
- frappe.call({
- method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.get_redirect_url",
- freeze: true,
- callback: function (r) {
- if (!r.exc) {
- window.open(r.message.auth_url);
- }
- },
- });
- } else {
- frappe.msgprint(__("Please enter values for App Access Key and App Secret Key"));
+ if (!frm.events.are_keys_present(frm)) {
+ frappe.msgprint(__("App Access Key and/or Secret Key are not present."));
+ return;
}
+
+ frappe.call({
+ method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.get_dropbox_authorize_url",
+ freeze: true,
+ callback: function (r) {
+ if (!r.exc) {
+ window.open(r.message.auth_url);
+ }
+ },
+ });
},
take_backup: function (frm) {
- if (
- frm.doc.enabled &&
- ((frm.doc.app_access_key && frm.doc.app_secret_key) ||
- (frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config))
- ) {
- frm.add_custom_button(__("Take Backup Now"), function (frm) {
+ if (frm.doc.enabled && (frm.doc.dropbox_refresh_token || frm.doc.dropbox_access_token)) {
+ frm.add_custom_button(__("Take Backup Now"), function () {
frappe.call({
method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backup",
freeze: true,
});
- }).addClass("btn-primary");
+ });
}
},
});
diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json
index 858469647a..15535f08c4 100644
--- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json
+++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json
@@ -1,8 +1,10 @@
{
+ "actions": [],
"creation": "2016-09-21 10:12:57.399174",
"doctype": "DocType",
"document_type": "System",
"editable_grid": 1,
+ "engine": "InnoDB",
"field_order": [
"enabled",
"send_notifications_to",
@@ -14,8 +16,7 @@
"app_access_key",
"app_secret_key",
"allow_dropbox_access",
- "dropbox_access_key",
- "dropbox_access_secret",
+ "dropbox_refresh_token",
"dropbox_access_token"
],
"fields": [
@@ -82,17 +83,11 @@
"label": "Allow Dropbox Access"
},
{
- "fieldname": "dropbox_access_key",
+ "fieldname": "dropbox_refresh_token",
"fieldtype": "Password",
"hidden": 1,
- "label": "Dropbox Access Key",
- "read_only": 1
- },
- {
- "fieldname": "dropbox_access_secret",
- "fieldtype": "Password",
- "hidden": 1,
- "label": "Dropbox Access Secret",
+ "label": "Dropbox Refresh Token",
+ "no_copy": 1,
"read_only": 1
},
{
@@ -104,7 +99,8 @@
],
"in_create": 1,
"issingle": 1,
- "modified": "2019-08-22 16:26:44.468391",
+ "links": [],
+ "modified": "2023-03-20 14:20:19.180611",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Dropbox Settings",
@@ -125,5 +121,6 @@
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
index e6998a9d6d..c213ba8340 100644
--- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
+++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# License: MIT. See LICENSE
-import json
import os
from urllib.parse import parse_qs, urlparse
@@ -16,16 +15,8 @@ from frappe.integrations.offsite_backup_utils import (
send_email,
validate_file_size,
)
-from frappe.integrations.utils import make_post_request
from frappe.model.document import Document
-from frappe.utils import (
- cint,
- encode,
- get_backups_path,
- get_files_path,
- get_request_site_address,
- get_url,
-)
+from frappe.utils import cint, encode, get_backups_path, get_files_path, get_request_site_address
from frappe.utils.background_jobs import enqueue
from frappe.utils.backups import new_backup
@@ -101,27 +92,9 @@ def take_backup_to_dropbox(retry_count=0, upload_db_backup=True):
def backup_to_dropbox(upload_db_backup=True):
- if not frappe.db:
- frappe.connect()
-
# upload database
dropbox_settings = get_dropbox_settings()
-
- if not dropbox_settings["access_token"]:
- access_token = generate_oauth2_access_token_from_oauth1_token(dropbox_settings)
-
- if not access_token.get("oauth2_token"):
- return (
- "Failed backup upload",
- "No Access Token exists! Please generate the access token for Dropbox.",
- )
-
- dropbox_settings["access_token"] = access_token["oauth2_token"]
- set_dropbox_access_token(access_token["oauth2_token"])
-
- dropbox_client = dropbox.Dropbox(
- oauth2_access_token=dropbox_settings["access_token"], timeout=None
- )
+ dropbox_client = get_dropbox_client(dropbox_settings)
if upload_db_backup:
if frappe.flags.create_new_backup:
@@ -267,24 +240,36 @@ def get_uploaded_files_meta(dropbox_folder, dropbox_client):
# folder not found
if isinstance(e.error, dropbox.files.ListFolderError):
return frappe._dict({"entries": []})
- else:
- raise
+ raise
+
+
+def get_dropbox_client(dropbox_settings):
+ dropbox_client = dropbox.Dropbox(
+ oauth2_access_token=dropbox_settings["access_token"],
+ oauth2_refresh_token=dropbox_settings["refresh_token"],
+ app_key=dropbox_settings["app_key"],
+ app_secret=dropbox_settings["app_secret"],
+ timeout=None,
+ )
+
+ # checking if the access token has expired
+ dropbox_client.files_list_folder("")
+ if dropbox_settings["access_token"] != dropbox_client._oauth2_access_token:
+ set_dropbox_token(dropbox_client._oauth2_access_token)
+
+ return dropbox_client
def get_dropbox_settings(redirect_uri=False):
- if not frappe.conf.dropbox_broker_site:
- frappe.conf.dropbox_broker_site = "https://dropbox.erpnext.com"
+ # NOTE: access token is kept for legacy dropbox apps
settings = frappe.get_doc("Dropbox Settings")
app_details = {
"app_key": settings.app_access_key or frappe.conf.dropbox_access_key,
"app_secret": settings.get_password(fieldname="app_secret_key", raise_exception=False)
if settings.app_secret_key
else frappe.conf.dropbox_secret_key,
- "access_token": settings.get_password("dropbox_access_token", raise_exception=False)
- if settings.dropbox_access_token
- else "",
- "access_key": settings.get_password("dropbox_access_key", raise_exception=False),
- "access_secret": settings.get_password("dropbox_access_secret", raise_exception=False),
+ "refresh_token": settings.get_password("dropbox_refresh_token", raise_exception=False),
+ "access_token": settings.get_password("dropbox_access_token", raise_exception=False),
"file_backup": settings.file_backup,
"no_of_backups": settings.no_of_backups if settings.limit_no_of_backups else None,
}
@@ -294,14 +279,11 @@ def get_dropbox_settings(redirect_uri=False):
{
"redirect_uri": get_request_site_address(True)
+ "/api/method/frappe.integrations.doctype.dropbox_settings.dropbox_settings.dropbox_auth_finish"
- if settings.app_secret_key
- else frappe.conf.dropbox_broker_site
- + "/api/method/dropbox_erpnext_broker.www.setup_dropbox.generate_dropbox_access_token",
}
)
- if not app_details["app_key"] or not app_details["app_secret"]:
- raise Exception(_("Please set Dropbox access keys in your site config"))
+ if not (app_details["app_key"] and app_details["app_secret"]):
+ raise Exception(_("Please set Dropbox access keys in site config or doctype"))
return app_details
@@ -321,28 +303,6 @@ def delete_older_backups(dropbox_client, folder_path, to_keep):
dropbox_client.files_delete(os.path.join(folder_path, f.name))
-@frappe.whitelist()
-def get_redirect_url():
- if not frappe.conf.dropbox_broker_site:
- frappe.conf.dropbox_broker_site = "https://dropbox.erpnext.com"
- url = "{}/api/method/dropbox_erpnext_broker.www.setup_dropbox.get_authotize_url".format(
- frappe.conf.dropbox_broker_site
- )
-
- try:
- response = make_post_request(url, data={"site": get_url()})
- if response.get("message"):
- return response["message"]
-
- except Exception:
- frappe.log_error()
- frappe.throw(
- _(
- "Something went wrong while generating dropbox access token. Please check error log for more details."
- )
- )
-
-
@frappe.whitelist()
def get_dropbox_authorize_url():
app_details = get_dropbox_settings(redirect_uri=True)
@@ -352,6 +312,7 @@ def get_dropbox_authorize_url():
session={},
csrf_token_session_key="dropbox-auth-csrf-token",
consumer_secret=app_details["app_secret"],
+ token_access_type="offline",
)
auth_url = dropbox_oauth_flow.start()
@@ -360,11 +321,20 @@ def get_dropbox_authorize_url():
@frappe.whitelist()
-def dropbox_auth_finish(return_access_token=False):
+def dropbox_auth_finish():
app_details = get_dropbox_settings(redirect_uri=True)
callback = frappe.form_dict
close = '' + _("Please close this window") + "
"
+ if not callback.state or not callback.code:
+ frappe.respond_as_web_page(
+ _("Dropbox Setup"),
+ _("Illegal Access Token. Please try again") + close,
+ indicator_color="red",
+ http_status_code=frappe.AuthenticationError.http_status_code,
+ )
+ return
+
dropbox_oauth_flow = dropbox.DropboxOAuth2Flow(
consumer_key=app_details["app_key"],
redirect_uri=app_details["redirect_uri"],
@@ -373,40 +343,20 @@ def dropbox_auth_finish(return_access_token=False):
consumer_secret=app_details["app_secret"],
)
- if callback.state or callback.code:
- token = dropbox_oauth_flow.finish({"state": callback.state, "code": callback.code})
- if return_access_token and token.access_token:
- return token.access_token, callback.state
+ token = dropbox_oauth_flow.finish({"state": callback.state, "code": callback.code})
+ set_dropbox_token(token.access_token, token.refresh_token)
- set_dropbox_access_token(token.access_token)
- else:
- frappe.respond_as_web_page(
- _("Dropbox Setup"),
- _("Illegal Access Token. Please try again") + close,
- indicator_color="red",
- http_status_code=frappe.AuthenticationError.http_status_code,
- )
-
- frappe.respond_as_web_page(
- _("Dropbox Setup"), _("Dropbox access is approved!") + close, indicator_color="green"
- )
+ frappe.local.response["type"] = "redirect"
+ frappe.local.response["location"] = "/app/dropbox-settings"
-def set_dropbox_access_token(access_token):
- frappe.db.set_single_value("Dropbox Settings", "dropbox_access_token", access_token)
+def set_dropbox_token(access_token, refresh_token=None):
+ # NOTE: used doc object instead of db.set_value so that password field is set properly
+ dropbox_settings = frappe.get_single("Dropbox Settings")
+ dropbox_settings.dropbox_access_token = access_token
+ if refresh_token:
+ dropbox_settings.dropbox_refresh_token = refresh_token
+
+ dropbox_settings.save()
+
frappe.db.commit()
-
-
-def generate_oauth2_access_token_from_oauth1_token(dropbox_settings=None):
- if not dropbox_settings.get("access_key") or not dropbox_settings.get("access_secret"):
- return {}
-
- url = "https://api.dropboxapi.com/2/auth/token/from_oauth1"
- headers = {"Content-Type": "application/json"}
- auth = (dropbox_settings["app_key"], dropbox_settings["app_secret"])
- data = {
- "oauth1_token": dropbox_settings["access_key"],
- "oauth1_token_secret": dropbox_settings["access_secret"],
- }
-
- return make_post_request(url, auth=auth, headers=headers, data=json.dumps(data))
diff --git a/frappe/integrations/doctype/google_calendar/google_calendar.py b/frappe/integrations/doctype/google_calendar/google_calendar.py
index a663c9c593..65a4a2bccd 100644
--- a/frappe/integrations/doctype/google_calendar/google_calendar.py
+++ b/frappe/integrations/doctype/google_calendar/google_calendar.py
@@ -403,7 +403,7 @@ def insert_event_in_google_calendar(doc, method=None):
event = {"summary": doc.subject, "description": doc.description, "google_calendar_event": 1}
event.update(
format_date_according_to_google_calendar(
- doc.all_day, get_datetime(doc.starts_on), get_datetime(doc.ends_on)
+ doc.all_day, get_datetime(doc.starts_on), get_datetime(doc.ends_on) if doc.ends_on else None
)
)
@@ -484,7 +484,7 @@ def update_event_in_google_calendar(doc, method=None):
)
event.update(
format_date_according_to_google_calendar(
- doc.all_day, get_datetime(doc.starts_on), get_datetime(doc.ends_on)
+ doc.all_day, get_datetime(doc.starts_on), get_datetime(doc.ends_on) if doc.ends_on else None
)
)
diff --git a/frappe/integrations/doctype/integration_request/integration_request.py b/frappe/integrations/doctype/integration_request/integration_request.py
index 334736bc9b..7ca185bd70 100644
--- a/frappe/integrations/doctype/integration_request/integration_request.py
+++ b/frappe/integrations/doctype/integration_request/integration_request.py
@@ -13,6 +13,13 @@ class IntegrationRequest(Document):
if self.flags._name:
self.name = self.flags._name
+ def clear_old_logs(days=30):
+ from frappe.query_builder import Interval
+ from frappe.query_builder.functions import Now
+
+ table = frappe.qb.DocType("Integration Request")
+ frappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days))))
+
def update_status(self, params, status):
data = json.loads(self.data)
data.update(params)
diff --git a/frappe/integrations/doctype/integration_request/integration_request_list.js b/frappe/integrations/doctype/integration_request/integration_request_list.js
new file mode 100644
index 0000000000..9aede34f29
--- /dev/null
+++ b/frappe/integrations/doctype/integration_request/integration_request_list.js
@@ -0,0 +1,7 @@
+frappe.listview_settings["Integration Request"] = {
+ onload: function (list_view) {
+ frappe.require("logtypes.bundle.js", () => {
+ frappe.utils.logtypes.show_log_retention_message(list_view.doctype);
+ });
+ },
+};
diff --git a/frappe/integrations/doctype/webhook_request_log/webhook_request_log_list.js b/frappe/integrations/doctype/webhook_request_log/webhook_request_log_list.js
new file mode 100644
index 0000000000..dd4e215157
--- /dev/null
+++ b/frappe/integrations/doctype/webhook_request_log/webhook_request_log_list.js
@@ -0,0 +1,7 @@
+frappe.listview_settings["Webhook Request Log"] = {
+ onload: function (list_view) {
+ frappe.require("logtypes.bundle.js", () => {
+ frappe.utils.logtypes.show_log_retention_message(list_view.doctype);
+ });
+ },
+};
diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py
index f6c6ee0a21..f171e4b1be 100644
--- a/frappe/model/__init__.py
+++ b/frappe/model/__init__.py
@@ -200,6 +200,10 @@ def get_permitted_fields(
if doctype in core_doctypes_list:
return valid_columns
+ # DocType has only fields of type Table (Table, Table MultiSelect)
+ if set(valid_columns).issubset(default_fields):
+ return valid_columns
+
if permitted_fields := meta.get_permitted_fieldnames(parenttype=parenttype, user=user):
meta_fields = meta.default_fields.copy()
optional_meta_fields = [x for x in optional_fields if x in valid_columns]
diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py
index 420cbee091..3908365291 100644
--- a/frappe/model/rename_doc.py
+++ b/frappe/model/rename_doc.py
@@ -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)
diff --git a/frappe/patches.txt b/frappe/patches.txt
index 9ebb32fea0..da83094961 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -162,7 +162,6 @@ execute:frappe.delete_doc("DocType", "Footer Item")
execute:frappe.reload_doctype('user')
execute:frappe.reload_doctype('docperm')
frappe.patches.v13_0.replace_field_target_with_open_in_new_tab
-frappe.core.doctype.role.patches.v13_set_default_desk_properties
frappe.patches.v13_0.add_switch_theme_to_navbar_settings
frappe.patches.v13_0.update_icons_in_customized_desk_pages
execute:frappe.db.set_default('desktop:home_page', 'space')
@@ -184,6 +183,7 @@ frappe.patches.v13_0.encrypt_2fa_secrets
frappe.patches.v13_0.reset_corrupt_defaults
frappe.patches.v13_0.remove_share_for_std_users
execute:frappe.reload_doc('custom', 'doctype', 'custom_field')
+frappe.email.doctype.email_queue.patches.drop_search_index_on_message_id
frappe.patches.v14_0.update_workspace2 # 20.09.2021
frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021
frappe.patches.v14_0.transform_todo_schema
@@ -199,6 +199,7 @@ frappe.patches.v15_0.remove_event_streaming
frappe.patches.v15_0.copy_disable_prepared_report_to_prepared_report
[post_model_sync]
+frappe.core.doctype.role.patches.v13_set_default_desk_properties
frappe.patches.v14_0.drop_data_import_legacy
frappe.patches.v14_0.copy_mail_data #08.03.21
frappe.patches.v14_0.update_github_endpoints #08-11-2021
diff --git a/frappe/permissions.py b/frappe/permissions.py
index 75a940233e..97badea500 100644
--- a/frappe/permissions.py
+++ b/frappe/permissions.py
@@ -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
diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js
index 54704467c0..8e5e165c78 100644
--- a/frappe/printing/page/print/print.js
+++ b/frappe/printing/page/print/print.js
@@ -347,7 +347,7 @@ frappe.ui.form.PrintView = class {
set_default_print_language() {
let print_format = this.get_print_format();
this.lang_code =
- print_format.default_print_language || this.frm.doc.language || frappe.boot.lang;
+ this.frm.doc.language || print_format.default_print_language || frappe.boot.lang;
this.language_selector.val(this.lang_code);
}
@@ -657,7 +657,7 @@ frappe.ui.form.PrintView = class {
}
get_letterhead() {
- return this.letterhead_selector.val();
+ return this.letterhead_selector.val() || __("No Letterhead");
}
get_no_preview_html() {
diff --git a/frappe/public/js/frappe/form/column.js b/frappe/public/js/frappe/form/column.js
index 3ea4296a94..317679f829 100644
--- a/frappe/public/js/frappe/form/column.js
+++ b/frappe/public/js/frappe/form/column.js
@@ -38,7 +38,12 @@ export default class Column {
resize_all_columns() {
// distribute all columns equally
- let colspan = cint(12 / this.section.wrapper.find(".form-column").length);
+ let columns = this.section.wrapper.find(".form-column").length;
+ let colspan = cint(12 / columns);
+
+ if (columns == 5) {
+ colspan = 20;
+ }
this.section.wrapper
.find(".form-column")
diff --git a/frappe/public/js/frappe/form/controls/barcode.js b/frappe/public/js/frappe/form/controls/barcode.js
index c130ecc039..a819384773 100644
--- a/frappe/public/js/frappe/form/controls/barcode.js
+++ b/frappe/public/js/frappe/form/controls/barcode.js
@@ -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("