Merge branch 'develop' into fix_child_table_in_dialog_sortable
This commit is contained in:
commit
94669b13a1
33 changed files with 372 additions and 165 deletions
|
|
@ -1,6 +1,6 @@
|
|||
context("Kanban Board", () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.login("frappe@example.com");
|
||||
cy.visit("/app");
|
||||
});
|
||||
|
||||
|
|
@ -96,4 +96,36 @@ context("Kanban Board", () => {
|
|||
.first()
|
||||
.should("not.contain", "ID:");
|
||||
});
|
||||
|
||||
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");
|
||||
|
||||
cy.visit("/app/todo/view/kanban/Admin Kanban");
|
||||
|
||||
// Menu button should be hidden (dropdown for 'Save Filters' and 'Delete Kanban Board')
|
||||
cy.get(".no-list-sidebar .menu-btn-group .btn-default[data-original-title='Menu']").should(
|
||||
"have.length",
|
||||
0
|
||||
);
|
||||
// Kanban Columns should be visible (read-only)
|
||||
cy.get(".kanban .kanban-column").should("have.length", 2);
|
||||
// User should be able to add card (has access to ToDo)
|
||||
cy.get(".kanban .add-card").should("have.length", 2);
|
||||
// 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");
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.call("logout");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,16 +1,7 @@
|
|||
context("Permissions API", () => {
|
||||
before(() => {
|
||||
cy.visit("/login");
|
||||
|
||||
cy.login("Administrator");
|
||||
cy.call("frappe.tests.ui_test_helpers.add_remove_role", {
|
||||
action: "remove",
|
||||
user: "frappe@example.com",
|
||||
role: "System Manager",
|
||||
});
|
||||
cy.call("logout");
|
||||
|
||||
cy.login("frappe@example.com");
|
||||
cy.remove_role("frappe@example.com", "System Manager");
|
||||
cy.visit("/app");
|
||||
});
|
||||
|
||||
|
|
@ -44,14 +35,7 @@ context("Permissions API", () => {
|
|||
});
|
||||
|
||||
after(() => {
|
||||
cy.call("logout");
|
||||
|
||||
cy.login("Administrator");
|
||||
cy.call("frappe.tests.ui_test_helpers.add_remove_role", {
|
||||
action: "add",
|
||||
user: "frappe@example.com",
|
||||
role: "System Manager",
|
||||
});
|
||||
cy.add_role("frappe@example.com", "System Manager");
|
||||
cy.call("logout");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -371,6 +371,45 @@ Cypress.Commands.add("update_doc", (doctype, docname, args) => {
|
|||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("switch_to_user", (user) => {
|
||||
cy.call("logout");
|
||||
cy.login(user);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("add_role", (user, role) => {
|
||||
cy.window()
|
||||
.its("frappe")
|
||||
.then((frappe) => {
|
||||
const session_user = frappe.session.user;
|
||||
add_remove_role("add", user, role, session_user);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("remove_role", (user, role) => {
|
||||
cy.window()
|
||||
.its("frappe")
|
||||
.then((frappe) => {
|
||||
const session_user = frappe.session.user;
|
||||
add_remove_role("remove", user, role, session_user);
|
||||
});
|
||||
});
|
||||
|
||||
const add_remove_role = (action, user, role, session_user) => {
|
||||
if (session_user !== "Administrator") {
|
||||
cy.switch_to_user("Administrator");
|
||||
}
|
||||
|
||||
cy.call("frappe.tests.ui_test_helpers.add_remove_role", {
|
||||
action: action,
|
||||
user: user,
|
||||
role: role,
|
||||
});
|
||||
|
||||
if (session_user !== "Administrator") {
|
||||
cy.switch_to_user(session_user);
|
||||
}
|
||||
};
|
||||
|
||||
Cypress.Commands.add("open_list_filter", () => {
|
||||
cy.get(".filter-section .filter-button").click();
|
||||
cy.wait(300);
|
||||
|
|
|
|||
|
|
@ -1907,7 +1907,7 @@ def get_value(*args, **kwargs):
|
|||
return db.get_value(*args, **kwargs)
|
||||
|
||||
|
||||
def as_json(obj: dict | list, indent=1, separators=None) -> str:
|
||||
def as_json(obj: dict | list, indent=1, separators=None, ensure_ascii=True) -> str:
|
||||
from frappe.utils.response import json_handler
|
||||
|
||||
if separators is None:
|
||||
|
|
@ -1915,13 +1915,24 @@ def as_json(obj: dict | list, indent=1, separators=None) -> str:
|
|||
|
||||
try:
|
||||
return json.dumps(
|
||||
obj, indent=indent, sort_keys=True, default=json_handler, separators=separators
|
||||
obj,
|
||||
indent=indent,
|
||||
sort_keys=True,
|
||||
default=json_handler,
|
||||
separators=separators,
|
||||
ensure_ascii=ensure_ascii,
|
||||
)
|
||||
except TypeError:
|
||||
# this would break in case the keys are not all os "str" type - as defined in the JSON
|
||||
# adding this to ensure keys are sorted (expected behaviour)
|
||||
sorted_obj = dict(sorted(obj.items(), key=lambda kv: str(kv[0])))
|
||||
return json.dumps(sorted_obj, indent=indent, default=json_handler, separators=separators)
|
||||
return json.dumps(
|
||||
sorted_obj,
|
||||
indent=indent,
|
||||
default=json_handler,
|
||||
separators=separators,
|
||||
ensure_ascii=ensure_ascii,
|
||||
)
|
||||
|
||||
|
||||
def are_emails_muted():
|
||||
|
|
|
|||
|
|
@ -303,6 +303,17 @@ def has_permission(doctype, docname, perm_type="read"):
|
|||
return {"has_permission": frappe.has_permission(doctype, perm_type.lower(), docname)}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_doc_permissions(doctype, docname):
|
||||
"""Returns an evaluated document permissions dict like `{"read":1, "write":1}`
|
||||
|
||||
:param doctype: DocType of the document to be evaluated
|
||||
:param docname: `name` of the document to be evaluated
|
||||
"""
|
||||
doc = frappe.get_doc(doctype, docname)
|
||||
return {"permissions": frappe.permissions.get_doc_permissions(doc)}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_password(doctype, name, fieldname):
|
||||
"""Return a password type property. Only applicable for System Managers
|
||||
|
|
|
|||
|
|
@ -260,7 +260,7 @@ def export_json(doctype, path, filters=None, or_filters=None, name=None, order_b
|
|||
path = os.path.join("..", path)
|
||||
|
||||
with open(path, "w") as outfile:
|
||||
outfile.write(frappe.as_json(out))
|
||||
outfile.write(frappe.as_json(out, ensure_ascii=False))
|
||||
|
||||
|
||||
def export_csv(doctype, path):
|
||||
|
|
|
|||
|
|
@ -29,3 +29,12 @@ class DocField(Document):
|
|||
if self.fieldtype == "Select":
|
||||
options = self.options or ""
|
||||
return [d for d in options.split("\n") if d]
|
||||
|
||||
def __repr__(self):
|
||||
unsaved = "unsaved" if not self.name else ""
|
||||
doctype = self.__class__.__name__
|
||||
|
||||
docstatus = f" docstatus={self.docstatus}" if self.docstatus else ""
|
||||
parent = f" parent={self.parent}" if getattr(self, "parent", None) else ""
|
||||
|
||||
return f"<{self.fieldtype}{doctype}: {self.fieldname}{docstatus}{parent}{unsaved}>"
|
||||
|
|
|
|||
|
|
@ -85,8 +85,8 @@ class Domain(Document):
|
|||
def set_default_portal_role(self):
|
||||
"""Set default portal role based on domain"""
|
||||
if self.data.get("default_portal_role"):
|
||||
frappe.db.set_value(
|
||||
"Portal Settings", None, "default_role", self.data.get("default_portal_role")
|
||||
frappe.db.set_single_value(
|
||||
"Portal Settings", "default_role", self.data.get("default_portal_role")
|
||||
)
|
||||
|
||||
def setup_properties(self):
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ test_records = frappe.get_test_records("User")
|
|||
class TestUser(FrappeTestCase):
|
||||
def tearDown(self):
|
||||
# disable password strength test
|
||||
frappe.db.set_value("System Settings", "System Settings", "enable_password_policy", 0)
|
||||
frappe.db.set_value("System Settings", "System Settings", "minimum_password_score", "")
|
||||
frappe.db.set_value("System Settings", "System Settings", "password_reset_limit", 3)
|
||||
frappe.db.set_single_value("System Settings", "enable_password_policy", 0)
|
||||
frappe.db.set_single_value("System Settings", "minimum_password_score", "")
|
||||
frappe.db.set_single_value("System Settings", "password_reset_limit", 3)
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def test_user_type(self):
|
||||
|
|
@ -111,7 +111,7 @@ class TestUser(FrappeTestCase):
|
|||
|
||||
self.assertEqual(frappe.db.get_value("User", "xxxtest@example.com"), None)
|
||||
|
||||
frappe.db.set_value("Website Settings", "Website Settings", "_test", "_test_val")
|
||||
frappe.db.set_single_value("Website Settings", "_test", "_test_val")
|
||||
self.assertEqual(frappe.db.get_value("Website Settings", None, "_test"), "_test_val")
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Website Settings", "Website Settings", "_test"), "_test_val"
|
||||
|
|
@ -179,15 +179,15 @@ class TestUser(FrappeTestCase):
|
|||
|
||||
def test_password_strength(self):
|
||||
# Test Password without Password Strength Policy
|
||||
frappe.db.set_value("System Settings", "System Settings", "enable_password_policy", 0)
|
||||
frappe.db.set_single_value("System Settings", "enable_password_policy", 0)
|
||||
|
||||
# password policy is disabled, test_password_strength should be ignored
|
||||
result = test_password_strength("test_password")
|
||||
self.assertFalse(result.get("feedback", None))
|
||||
|
||||
# Test Password with Password Strenth Policy Set
|
||||
frappe.db.set_value("System Settings", "System Settings", "enable_password_policy", 1)
|
||||
frappe.db.set_value("System Settings", "System Settings", "minimum_password_score", 2)
|
||||
frappe.db.set_single_value("System Settings", "enable_password_policy", 1)
|
||||
frappe.db.set_single_value("System Settings", "minimum_password_score", 2)
|
||||
|
||||
# Score 1; should now fail
|
||||
result = test_password_strength("bee2ve")
|
||||
|
|
@ -275,7 +275,7 @@ class TestUser(FrappeTestCase):
|
|||
|
||||
def test_rate_limiting_for_reset_password(self):
|
||||
# Allow only one reset request for a day
|
||||
frappe.db.set_value("System Settings", "System Settings", "password_reset_limit", 1)
|
||||
frappe.db.set_single_value("System Settings", "password_reset_limit", 1)
|
||||
frappe.db.commit()
|
||||
|
||||
url = get_url()
|
||||
|
|
@ -443,9 +443,7 @@ class TestUser(FrappeTestCase):
|
|||
def test_reset_password_link_expiry(self):
|
||||
new_password = "new_password"
|
||||
# set the reset password expiry to 1 second
|
||||
frappe.db.set_value(
|
||||
"System Settings", "System Settings", "reset_password_link_expiry_duration", 1
|
||||
)
|
||||
frappe.db.set_single_value("System Settings", "reset_password_link_expiry_duration", 1)
|
||||
frappe.set_user("testpassword@example.com")
|
||||
test_user = frappe.get_doc("User", "testpassword@example.com")
|
||||
test_user.reset_password()
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ from frappe.model.utils.link_count import flush_local_link_count
|
|||
from frappe.query_builder.functions import Count
|
||||
from frappe.utils import cast as cast_fieldtype
|
||||
from frappe.utils import cint, get_datetime, get_table_name, getdate, now, sbool
|
||||
from frappe.utils.deprecations import deprecated
|
||||
from frappe.utils.deprecations import deprecated, deprecation_warning
|
||||
|
||||
IFNULL_PATTERN = re.compile(r"ifnull\(", flags=re.IGNORECASE)
|
||||
INDEX_PATTERN = re.compile(r"\s*\([^)]+\)\s*")
|
||||
|
|
@ -689,13 +689,30 @@ class Database:
|
|||
def get_list(*args, **kwargs):
|
||||
return frappe.get_list(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _get_update_dict(
|
||||
fieldname: str | dict, value: Any, *, modified: str, modified_by: str, update_modified: bool
|
||||
) -> dict[str, Any]:
|
||||
"""Create update dict that represents column-values to be updated."""
|
||||
update_dict = fieldname if isinstance(fieldname, dict) else {fieldname: value}
|
||||
|
||||
if update_modified:
|
||||
modified = modified or now()
|
||||
modified_by = modified_by or frappe.session.user
|
||||
update_dict.update({"modified": modified, "modified_by": modified_by})
|
||||
|
||||
return update_dict
|
||||
|
||||
def set_single_value(
|
||||
self,
|
||||
doctype: str,
|
||||
fieldname: str | dict,
|
||||
value: str | int | None = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
*,
|
||||
modified=None,
|
||||
modified_by=None,
|
||||
update_modified=True,
|
||||
debug=False,
|
||||
):
|
||||
"""Set field value of Single DocType.
|
||||
|
||||
|
|
@ -708,7 +725,23 @@ class Database:
|
|||
# Update the `deny_multiple_sessions` field in System Settings DocType.
|
||||
company = frappe.db.set_single_value("System Settings", "deny_multiple_sessions", True)
|
||||
"""
|
||||
return self.set_value(doctype, doctype, fieldname, value, *args, **kwargs)
|
||||
|
||||
to_update = self._get_update_dict(
|
||||
fieldname, value, modified=modified, modified_by=modified_by, update_modified=update_modified
|
||||
)
|
||||
|
||||
frappe.db.delete(
|
||||
"Singles", filters={"field": ("in", tuple(to_update)), "doctype": doctype}, debug=debug
|
||||
)
|
||||
|
||||
singles_data = ((doctype, key, sbool(value)) for key, value in to_update.items())
|
||||
frappe.qb.into("Singles").columns("doctype", "field", "value").insert(*singles_data).run(
|
||||
debug=debug
|
||||
)
|
||||
frappe.clear_document_cache(doctype, doctype)
|
||||
|
||||
if doctype in self.value_cache:
|
||||
del self.value_cache[doctype]
|
||||
|
||||
def get_single_value(self, doctype, fieldname, cache=True):
|
||||
"""Get property of Single DocType. Cache locally by default
|
||||
|
|
@ -834,40 +867,40 @@ class Database:
|
|||
:param update_modified: default True. Set as false, if you don't want to update the timestamp.
|
||||
:param debug: Print the query in the developer / js console.
|
||||
"""
|
||||
is_single_doctype = not (dn and dt != dn)
|
||||
to_update = field if isinstance(field, dict) else {field: val}
|
||||
|
||||
if update_modified:
|
||||
modified = modified or now()
|
||||
modified_by = modified_by or frappe.session.user
|
||||
to_update.update({"modified": modified, "modified_by": modified_by})
|
||||
|
||||
if is_single_doctype:
|
||||
frappe.db.delete(
|
||||
"Singles", filters={"field": ("in", tuple(to_update)), "doctype": dt}, debug=debug
|
||||
if _is_single_doctype := not (dn and dt != dn):
|
||||
deprecation_warning(
|
||||
"Calling db.set_value on single doctype is deprecated. This behaviour will be removed in version 15. Use db.set_single_value instead."
|
||||
)
|
||||
self.set_single_value(
|
||||
doctype=dt,
|
||||
fieldname=field,
|
||||
value=val,
|
||||
debug=debug,
|
||||
update_modified=update_modified,
|
||||
modified=modified,
|
||||
modified_by=modified_by,
|
||||
)
|
||||
return
|
||||
|
||||
singles_data = ((dt, key, sbool(value)) for key, value in to_update.items())
|
||||
query = (
|
||||
frappe.qb.into("Singles").columns("doctype", "field", "value").insert(*singles_data)
|
||||
).run(debug=debug)
|
||||
frappe.clear_document_cache(dt, dt)
|
||||
to_update = self._get_update_dict(
|
||||
field, val, modified=modified, modified_by=modified_by, update_modified=update_modified
|
||||
)
|
||||
|
||||
query = frappe.qb.engine.build_conditions(table=dt, filters=dn, update=True)
|
||||
|
||||
if isinstance(dn, str):
|
||||
frappe.clear_document_cache(dt, dn)
|
||||
else:
|
||||
query = frappe.qb.engine.build_conditions(table=dt, filters=dn, update=True)
|
||||
# TODO: Fix this; doesn't work rn - gavin@frappe.io
|
||||
# frappe.cache().hdel_keys(dt, "document_cache")
|
||||
# Workaround: clear all document caches
|
||||
frappe.cache().delete_value("document_cache")
|
||||
|
||||
if isinstance(dn, str):
|
||||
frappe.clear_document_cache(dt, dn)
|
||||
else:
|
||||
# TODO: Fix this; doesn't work rn - gavin@frappe.io
|
||||
# frappe.cache().hdel_keys(dt, "document_cache")
|
||||
# Workaround: clear all document caches
|
||||
frappe.cache().delete_value("document_cache")
|
||||
for column, value in to_update.items():
|
||||
query = query.set(column, value)
|
||||
|
||||
for column, value in to_update.items():
|
||||
query = query.set(column, value)
|
||||
|
||||
query.run(debug=debug)
|
||||
query.run(debug=debug)
|
||||
|
||||
if dt in self.value_cache:
|
||||
del self.value_cache[dt]
|
||||
|
|
|
|||
|
|
@ -88,10 +88,15 @@ def update_order(board_name, order):
|
|||
"""Save the order of cards in columns"""
|
||||
board = frappe.get_doc("Kanban Board", board_name)
|
||||
doctype = board.reference_doctype
|
||||
updated_cards = []
|
||||
|
||||
if not frappe.has_permission(doctype, "write"):
|
||||
# Return board data from db
|
||||
return board, updated_cards
|
||||
|
||||
fieldname = board.field_name
|
||||
order_dict = json.loads(order)
|
||||
|
||||
updated_cards = []
|
||||
for col_name, cards in order_dict.items():
|
||||
for card in cards:
|
||||
column = frappe.get_value(doctype, {"name": card}, fieldname)
|
||||
|
|
@ -103,8 +108,7 @@ def update_order(board_name, order):
|
|||
if column.column_name == col_name:
|
||||
column.order = json.dumps(cards)
|
||||
|
||||
board.save()
|
||||
return board, updated_cards
|
||||
return board.save(ignore_permissions=True), updated_cards
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -114,6 +118,9 @@ def update_order_for_single_card(
|
|||
"""Save the order of cards in columns"""
|
||||
board = frappe.get_doc("Kanban Board", board_name)
|
||||
doctype = board.reference_doctype
|
||||
|
||||
frappe.has_permission(doctype, "write", throw=True)
|
||||
|
||||
fieldname = board.field_name
|
||||
old_index = frappe.parse_json(old_index)
|
||||
new_index = frappe.parse_json(new_index)
|
||||
|
|
@ -130,7 +137,7 @@ def update_order_for_single_card(
|
|||
# save updated order
|
||||
board.columns[from_col_idx].order = frappe.as_json(from_col_order)
|
||||
board.columns[to_col_idx].order = frappe.as_json(to_col_order)
|
||||
board.save()
|
||||
board.save(ignore_permissions=True)
|
||||
|
||||
# update changed value in doc
|
||||
frappe.set_value(doctype, docname, fieldname, to_colname)
|
||||
|
|
@ -151,13 +158,14 @@ def get_kanban_column_order_and_index(board, colname):
|
|||
def add_card(board_name, docname, colname):
|
||||
board = frappe.get_doc("Kanban Board", board_name)
|
||||
|
||||
frappe.has_permission(board.reference_doctype, "write", throw=True)
|
||||
|
||||
col_order, col_idx = get_kanban_column_order_and_index(board, colname)
|
||||
col_order.insert(0, docname)
|
||||
|
||||
board.columns[col_idx].order = frappe.as_json(col_order)
|
||||
|
||||
board.save()
|
||||
return board
|
||||
return board.save(ignore_permissions=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -267,10 +267,10 @@ def add_all_roles_to(name):
|
|||
|
||||
def disable_future_access():
|
||||
frappe.db.set_default("desktop:home_page", "workspace")
|
||||
frappe.db.set_value("System Settings", "System Settings", "setup_complete", 1)
|
||||
frappe.db.set_single_value("System Settings", "setup_complete", 1)
|
||||
|
||||
# Enable onboarding after install
|
||||
frappe.db.set_value("System Settings", "System Settings", "enable_onboarding", 1)
|
||||
frappe.db.set_single_value("System Settings", "enable_onboarding", 1)
|
||||
|
||||
if not frappe.flags.in_test:
|
||||
# remove all roles and add 'Administrator' to prevent future access
|
||||
|
|
|
|||
|
|
@ -356,6 +356,7 @@ global_search_doctypes = {
|
|||
}
|
||||
|
||||
override_whitelisted_methods = {
|
||||
# Legacy File APIs
|
||||
"frappe.core.doctype.file.file.download_file": "download_file",
|
||||
"frappe.core.doctype.file.file.unzip_file": "frappe.core.api.file.unzip_file",
|
||||
"frappe.core.doctype.file.file.get_attached_images": "frappe.core.api.file.get_attached_images",
|
||||
|
|
@ -365,6 +366,14 @@ override_whitelisted_methods = {
|
|||
"frappe.core.doctype.file.file.create_new_folder": "frappe.core.api.file.create_new_folder",
|
||||
"frappe.core.doctype.file.file.move_file": "frappe.core.api.file.move_file",
|
||||
"frappe.core.doctype.file.file.zip_files": "frappe.core.api.file.zip_files",
|
||||
# Legacy (& Consistency) OAuth2 APIs
|
||||
"frappe.www.login.login_via_google": "frappe.integrations.oauth2_logins.login_via_google",
|
||||
"frappe.www.login.login_via_github": "frappe.integrations.oauth2_logins.login_via_github",
|
||||
"frappe.www.login.login_via_facebook": "frappe.integrations.oauth2_logins.login_via_facebook",
|
||||
"frappe.www.login.login_via_frappe": "frappe.integrations.oauth2_logins.login_via_frappe",
|
||||
"frappe.www.login.login_via_office365": "frappe.integrations.oauth2_logins.login_via_office365",
|
||||
"frappe.www.login.login_via_salesforce": "frappe.integrations.oauth2_logins.login_via_salesforce",
|
||||
"frappe.www.login.login_via_fairlogin": "frappe.integrations.oauth2_logins.login_via_fairlogin",
|
||||
}
|
||||
|
||||
ignore_links_on_delete = [
|
||||
|
|
|
|||
|
|
@ -393,7 +393,7 @@ def dropbox_auth_finish(return_access_token=False):
|
|||
|
||||
|
||||
def set_dropbox_access_token(access_token):
|
||||
frappe.db.set_value("Dropbox Settings", None, "dropbox_access_token", access_token)
|
||||
frappe.db.set_single_value("Dropbox Settings", "dropbox_access_token", access_token)
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ def authorize_access(reauthorize=False, code=None):
|
|||
|
||||
if not oauth_code or reauthorize:
|
||||
if reauthorize:
|
||||
frappe.db.set_value("Google Drive", None, "backup_folder_id", "")
|
||||
frappe.db.set_single_value("Google Drive", "backup_folder_id", "")
|
||||
return oauth_obj.get_authentication_url(
|
||||
{
|
||||
"redirect": f"/app/Form/{quote('Google Drive')}",
|
||||
|
|
@ -62,8 +62,7 @@ def authorize_access(reauthorize=False, code=None):
|
|||
)
|
||||
|
||||
r = oauth_obj.authorize(oauth_code)
|
||||
frappe.db.set_value(
|
||||
"Google Drive",
|
||||
frappe.db.set_single_value(
|
||||
"Google Drive",
|
||||
{"authorization_code": oauth_code, "refresh_token": r.get("refresh_token")},
|
||||
)
|
||||
|
|
@ -95,7 +94,7 @@ def check_for_folder_in_google_drive():
|
|||
|
||||
try:
|
||||
folder = google_drive.files().create(body=file_metadata, fields="id").execute()
|
||||
frappe.db.set_value("Google Drive", None, "backup_folder_id", folder.get("id"))
|
||||
frappe.db.set_single_value("Google Drive", "backup_folder_id", folder.get("id"))
|
||||
frappe.db.commit()
|
||||
except HttpError as e:
|
||||
frappe.throw(
|
||||
|
|
@ -120,7 +119,7 @@ def check_for_folder_in_google_drive():
|
|||
|
||||
for f in google_drive_folders.get("files"):
|
||||
if f.get("name") == account.backup_folder_name:
|
||||
frappe.db.set_value("Google Drive", None, "backup_folder_id", f.get("id"))
|
||||
frappe.db.set_single_value("Google Drive", "backup_folder_id", f.get("id"))
|
||||
frappe.db.commit()
|
||||
backup_folder_exists = True
|
||||
break
|
||||
|
|
@ -186,7 +185,7 @@ def upload_system_backup_to_google_drive():
|
|||
send_email(False, "Google Drive", "Google Drive", "email", error_status=e)
|
||||
|
||||
set_progress(3, "Uploading successful.")
|
||||
frappe.db.set_value("Google Drive", None, "last_backup_on", frappe.utils.now_datetime())
|
||||
frappe.db.set_single_value("Google Drive", "last_backup_on", frappe.utils.now_datetime())
|
||||
send_email(True, "Google Drive", "Google Drive", "email")
|
||||
return _("Google Drive Backup Successful.")
|
||||
|
||||
|
|
|
|||
|
|
@ -17,24 +17,24 @@ class TestGoogleSettings(FrappeTestCase):
|
|||
|
||||
def test_picker_disabled(self):
|
||||
"""Google Drive Picker should be disabled if it is not enabled in Google Settings."""
|
||||
frappe.db.set_value("Google Settings", None, "enable", 1)
|
||||
frappe.db.set_value("Google Settings", None, "google_drive_picker_enabled", 0)
|
||||
frappe.db.set_single_value("Google Settings", "enable", 1)
|
||||
frappe.db.set_single_value("Google Settings", "google_drive_picker_enabled", 0)
|
||||
settings = get_file_picker_settings()
|
||||
|
||||
self.assertEqual(settings, {})
|
||||
|
||||
def test_google_disabled(self):
|
||||
"""Google Drive Picker should be disabled if Google integration is not enabled."""
|
||||
frappe.db.set_value("Google Settings", None, "enable", 0)
|
||||
frappe.db.set_value("Google Settings", None, "google_drive_picker_enabled", 1)
|
||||
frappe.db.set_single_value("Google Settings", "enable", 0)
|
||||
frappe.db.set_single_value("Google Settings", "google_drive_picker_enabled", 1)
|
||||
settings = get_file_picker_settings()
|
||||
|
||||
self.assertEqual(settings, {})
|
||||
|
||||
def test_picker_enabled(self):
|
||||
"""If picker is enabled, get_file_picker_settings should return the credentials."""
|
||||
frappe.db.set_value("Google Settings", None, "enable", 1)
|
||||
frappe.db.set_value("Google Settings", None, "google_drive_picker_enabled", 1)
|
||||
frappe.db.set_single_value("Google Settings", "enable", 1)
|
||||
frappe.db.set_single_value("Google Settings", "google_drive_picker_enabled", 1)
|
||||
settings = get_file_picker_settings()
|
||||
|
||||
self.assertEqual(True, settings.get("enabled", False))
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ import frappe
|
|||
|
||||
def execute():
|
||||
frappe.reload_doc("core", "doctype", "system_settings")
|
||||
frappe.db.set_value("System Settings", None, "allow_login_after_fail", 60)
|
||||
frappe.db.set_single_value("System Settings", "allow_login_after_fail", 60)
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ def execute():
|
|||
frappe.reload_doctype("Dropbox Settings")
|
||||
check_dropbox_enabled = cint(frappe.db.get_single_value("Dropbox Settings", "enabled"))
|
||||
if check_dropbox_enabled == 1:
|
||||
frappe.db.set_value("Dropbox Settings", None, "file_backup", 1)
|
||||
frappe.db.set_single_value("Dropbox Settings", "file_backup", 1)
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ import frappe
|
|||
|
||||
def execute():
|
||||
frappe.reload_doc("core", "doctype", "system_settings", force=1)
|
||||
frappe.db.set_value("System Settings", None, "password_reset_limit", 3)
|
||||
frappe.db.set_single_value("System Settings", "password_reset_limit", 3)
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@ def execute():
|
|||
frappe.reload_doctype("System Settings")
|
||||
# setting first_day_of_the_week value as "Monday" to avoid breaking change
|
||||
# because before the configuration was introduced, system used to consider "Monday" as start of the week
|
||||
frappe.db.set_value("System Settings", "System Settings", "first_day_of_the_week", "Monday")
|
||||
frappe.db.set_single_value("System Settings", "first_day_of_the_week", "Monday")
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ import frappe
|
|||
|
||||
|
||||
def execute():
|
||||
frappe.db.set_value(
|
||||
"System Settings",
|
||||
frappe.db.set_single_value(
|
||||
"System Settings",
|
||||
{"document_share_key_expiry": 30, "allow_older_web_view_links": 1},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@ import frappe
|
|||
|
||||
def execute():
|
||||
days = frappe.db.get_single_value("Website Settings", "auto_account_deletion")
|
||||
frappe.db.set_value("Website Settings", None, "auto_account_deletion", days * 24)
|
||||
frappe.db.set_single_value("Website Settings", "auto_account_deletion", days * 24)
|
||||
|
|
|
|||
|
|
@ -191,12 +191,16 @@ frappe.views.ListViewSelect = class ListViewSelect {
|
|||
);
|
||||
});
|
||||
|
||||
this.page.add_custom_menu_item(
|
||||
kanban_switcher,
|
||||
__("Create New Kanban Board"),
|
||||
() => frappe.views.KanbanView.show_kanban_dialog(this.doctype),
|
||||
true
|
||||
);
|
||||
let perms = this.list_view.board_perms;
|
||||
let can_create = perms ? perms.create : true;
|
||||
if (can_create) {
|
||||
this.page.add_custom_menu_item(
|
||||
kanban_switcher,
|
||||
__("Create New Kanban Board"),
|
||||
() => frappe.views.KanbanView.show_kanban_dialog(this.doctype),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get_page_name() {
|
||||
|
|
|
|||
|
|
@ -297,6 +297,7 @@ frappe.provide("frappe.views");
|
|||
self.wrapper = opts.wrapper;
|
||||
self.cur_list = opts.cur_list;
|
||||
self.board_name = opts.board_name;
|
||||
self.board_perms = self.cur_list.board_perms;
|
||||
|
||||
self.update = function (cards) {
|
||||
// update cards internally
|
||||
|
|
@ -325,6 +326,7 @@ frappe.provide("frappe.views");
|
|||
store.watch((state, getters) => {
|
||||
return state.empty_state;
|
||||
}, show_empty_state);
|
||||
|
||||
store.dispatch("update_order");
|
||||
}
|
||||
|
||||
|
|
@ -346,7 +348,7 @@ frappe.provide("frappe.views");
|
|||
var columns = store.state.columns;
|
||||
|
||||
columns.filter(is_active_column).map(function (col) {
|
||||
frappe.views.KanbanBoardColumn(col, self.$kanban_board);
|
||||
frappe.views.KanbanBoardColumn(col, self.$kanban_board, self.board_perms);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -356,6 +358,9 @@ frappe.provide("frappe.views");
|
|||
}
|
||||
|
||||
function setup_sortable() {
|
||||
// If no write access to board, editing board (by dragging column) should be blocked
|
||||
if (!self.board_perms.write) return;
|
||||
|
||||
var sortable = new Sortable(self.$kanban_board.get(0), {
|
||||
group: "columns",
|
||||
animation: 150,
|
||||
|
|
@ -371,6 +376,12 @@ frappe.provide("frappe.views");
|
|||
}
|
||||
|
||||
function bind_add_column() {
|
||||
if (!self.board_perms.write) {
|
||||
// If no write access to board, editing board (by adding column) should be blocked
|
||||
self.$kanban_board.find(".add-new-column").remove();
|
||||
return;
|
||||
}
|
||||
|
||||
var $add_new_column = self.$kanban_board.find(".add-new-column"),
|
||||
$compose_column = $add_new_column.find(".compose-column"),
|
||||
$compose_column_form = $add_new_column.find(".compose-column-form").hide();
|
||||
|
|
@ -512,7 +523,7 @@ frappe.provide("frappe.views");
|
|||
return self;
|
||||
};
|
||||
|
||||
frappe.views.KanbanBoardColumn = function (column, wrapper) {
|
||||
frappe.views.KanbanBoardColumn = function (column, wrapper, board_perms) {
|
||||
var self = {};
|
||||
var filtered_cards = [];
|
||||
|
||||
|
|
@ -535,6 +546,7 @@ frappe.provide("frappe.views");
|
|||
indicator: frappe.scrub(column.indicator, "-"),
|
||||
})
|
||||
).appendTo(wrapper);
|
||||
// add task, archive
|
||||
self.$kanban_cards = self.$kanban_column.find(".kanban-cards");
|
||||
}
|
||||
|
||||
|
|
@ -565,6 +577,9 @@ frappe.provide("frappe.views");
|
|||
}
|
||||
|
||||
function setup_sortable() {
|
||||
// Block card dragging/record editing without 'write' access to reference doctype
|
||||
if (!frappe.model.can_write(store.state.doctype)) return;
|
||||
|
||||
Sortable.create(self.$kanban_cards.get(0), {
|
||||
group: "cards",
|
||||
animation: 150,
|
||||
|
|
@ -598,6 +613,14 @@ frappe.provide("frappe.views");
|
|||
var $wrapper = self.$kanban_column;
|
||||
var $btn_add = $wrapper.find(".add-card");
|
||||
var $new_card_area = $wrapper.find(".new-card-area");
|
||||
|
||||
if (!frappe.model.can_create(store.state.doctype)) {
|
||||
// Block record/card creation without 'create' access to reference doctype
|
||||
$btn_add.remove();
|
||||
$new_card_area.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
var $textarea = $new_card_area.find("textarea");
|
||||
|
||||
//Add card button
|
||||
|
|
@ -639,6 +662,12 @@ frappe.provide("frappe.views");
|
|||
}
|
||||
|
||||
function bind_options() {
|
||||
if (!board_perms.write) {
|
||||
// If no write access to board, column options should be hidden
|
||||
self.$kanban_column.find(".column-options").remove();
|
||||
return;
|
||||
}
|
||||
|
||||
self.$kanban_column
|
||||
.find(".column-options .dropdown-menu")
|
||||
.on("click", "[data-action]", function () {
|
||||
|
|
@ -652,6 +681,7 @@ frappe.provide("frappe.views");
|
|||
store.dispatch("set_indicator", { column, color });
|
||||
}
|
||||
});
|
||||
|
||||
get_column_indicators(function (indicators) {
|
||||
let html = `<li class="button-group">${indicators
|
||||
.map((indicator) => {
|
||||
|
|
@ -687,6 +717,11 @@ frappe.provide("frappe.views");
|
|||
};
|
||||
|
||||
self.$card = $(frappe.render_template("kanban_card", opts)).appendTo(wrapper);
|
||||
|
||||
if (!frappe.model.can_write(card.doctype)) {
|
||||
// Undraggable card without 'write' access to reference doctype
|
||||
self.$card.find(".kanban-card-body").css("cursor", "default");
|
||||
}
|
||||
}
|
||||
|
||||
function get_doc_content(card) {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,16 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
|
|||
});
|
||||
}
|
||||
|
||||
init() {
|
||||
return super.init().then(() => {
|
||||
let menu_length = this.page.menu.find(".dropdown-item").length;
|
||||
if (menu_length === 1) {
|
||||
// Only 'Refresh' (hidden) is present (always), dropdown is visibly empty
|
||||
this.page.hide_menu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setup_defaults() {
|
||||
return super.setup_defaults().then(() => {
|
||||
let get_board_name = () => {
|
||||
|
|
@ -56,32 +66,53 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
|
|||
this.card_meta = this.get_card_meta();
|
||||
this.page_length = 0;
|
||||
|
||||
this.menu_items.push(
|
||||
...[
|
||||
{
|
||||
label: __("Save filters"),
|
||||
action: () => {
|
||||
this.save_kanban_board_filters();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: __("Delete Kanban Board"),
|
||||
action: () => {
|
||||
frappe.confirm("Are you sure you want to proceed?", () => {
|
||||
frappe.db.delete_doc("Kanban Board", this.board_name).then(() => {
|
||||
frappe.show_alert(`Kanban Board ${this.board_name} deleted.`);
|
||||
frappe.set_route("List", this.doctype, "List");
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
return this.get_board();
|
||||
return frappe.run_serially([
|
||||
() => this.set_board_perms_and_push_menu_items(),
|
||||
() => this.get_board(),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
set_board_perms_and_push_menu_items() {
|
||||
// needs server-side call as client-side document instance is absent before kanban render
|
||||
return frappe.call({
|
||||
method: "frappe.client.get_doc_permissions",
|
||||
args: {
|
||||
doctype: "Kanban Board",
|
||||
docname: this.board_name,
|
||||
},
|
||||
callback: (result) => {
|
||||
this.board_perms = result.message.permissions || {};
|
||||
this.push_menu_items();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
push_menu_items() {
|
||||
if (this.board_perms.write) {
|
||||
this.menu_items.push({
|
||||
label: __("Save filters"),
|
||||
action: () => {
|
||||
this.save_kanban_board_filters();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (this.board_perms.delete) {
|
||||
this.menu_items.push({
|
||||
label: __("Delete Kanban Board"),
|
||||
action: () => {
|
||||
frappe.confirm("Are you sure you want to proceed?", () => {
|
||||
frappe.db.delete_doc("Kanban Board", this.board_name).then(() => {
|
||||
frappe.show_alert(`Kanban Board ${this.board_name} deleted.`);
|
||||
frappe.set_route("List", this.doctype, "List");
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setup_paging_area() {
|
||||
// pass
|
||||
}
|
||||
|
|
@ -103,6 +134,7 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
|
|||
this.hide_sidebar = true;
|
||||
this.hide_page_form = true;
|
||||
this.hide_card_layout = true;
|
||||
this.hide_sort_selector = true;
|
||||
super.setup_page();
|
||||
}
|
||||
|
||||
|
|
@ -129,6 +161,8 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
|
|||
render_list() {}
|
||||
|
||||
on_filter_change() {
|
||||
if (!this.board_perms.write) return; // avoid misleading ux
|
||||
|
||||
if (JSON.stringify(this.board.filters_array) !== JSON.stringify(this.filter_area.get())) {
|
||||
this.page.set_indicator(__("Not Saved"), "orange");
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -581,6 +581,40 @@ def create_kanban():
|
|||
).insert()
|
||||
|
||||
|
||||
@whitelist_for_tests
|
||||
def create_todo(description):
|
||||
frappe.get_doc({"doctype": "ToDo", "description": description}).insert()
|
||||
|
||||
|
||||
@whitelist_for_tests
|
||||
def create_admin_kanban():
|
||||
if not frappe.db.exists("Kanban Board", "Admin Kanban"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Kanban Board",
|
||||
"name": "Admin Kanban",
|
||||
"owner": "Administrator",
|
||||
"kanban_board_name": "Admin Kanban",
|
||||
"reference_doctype": "ToDo",
|
||||
"field_name": "status",
|
||||
"private": 0,
|
||||
"show_labels": 0,
|
||||
"columns": [
|
||||
{
|
||||
"column_name": "Open",
|
||||
"status": "Active",
|
||||
"indicator": "Gray",
|
||||
},
|
||||
{
|
||||
"column_name": "Closed",
|
||||
"status": "Active",
|
||||
"indicator": "Gray",
|
||||
},
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
@whitelist_for_tests
|
||||
def add_remove_role(action, user, role):
|
||||
user_doc = frappe.get_doc("User", user)
|
||||
|
|
|
|||
|
|
@ -1176,7 +1176,7 @@ def rename_language(old_name, new_name):
|
|||
|
||||
language_in_system_settings = frappe.db.get_single_value("System Settings", "language")
|
||||
if language_in_system_settings == old_name:
|
||||
frappe.db.set_value("System Settings", "System Settings", "language", new_name)
|
||||
frappe.db.set_single_value("System Settings", "language", new_name)
|
||||
|
||||
frappe.db.sql(
|
||||
"""update `tabUser` set language=%(new_name)s where language=%(old_name)s""",
|
||||
|
|
|
|||
|
|
@ -446,7 +446,7 @@ def should_remove_barcode_image(barcode):
|
|||
|
||||
|
||||
def disable():
|
||||
frappe.db.set_value("System Settings", None, "enable_two_factor_auth", 0)
|
||||
frappe.db.set_single_value("System Settings", "enable_two_factor_auth", 0)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ def before_tests():
|
|||
if not int(frappe.db.get_single_value("System Settings", "setup_complete") or 0):
|
||||
complete_setup_wizard()
|
||||
|
||||
frappe.db.set_value("Website Settings", "Website Settings", "disable_signup", 0)
|
||||
frappe.db.set_single_value("Website Settings", "disable_signup", 0)
|
||||
frappe.db.commit()
|
||||
frappe.clear_cache()
|
||||
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ def is_scheduler_disabled() -> bool:
|
|||
|
||||
|
||||
def toggle_scheduler(enable):
|
||||
frappe.db.set_value("System Settings", None, "enable_scheduler", 1 if enable else 0)
|
||||
frappe.db.set_single_value("System Settings", "enable_scheduler", int(enable))
|
||||
|
||||
|
||||
def enable_scheduler():
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class TestPersonalDataDeletionRequest(FrappeTestCase):
|
|||
self.assertFalse(frappe.db.exists("Personal Data Deletion Request", self.delete_request.name))
|
||||
|
||||
def test_process_auto_request(self):
|
||||
frappe.db.set_value("Website Settings", None, "auto_account_deletion", "1")
|
||||
frappe.db.set_single_value("Website Settings", "auto_account_deletion", "1")
|
||||
date_time_obj = datetime.strptime(
|
||||
self.delete_request.creation, "%Y-%m-%d %H:%M:%S.%f"
|
||||
) + timedelta(hours=-2)
|
||||
|
|
|
|||
|
|
@ -31,8 +31,7 @@ def authorize_access(reauthorize=False, code=None):
|
|||
)
|
||||
|
||||
res = oauth_obj.authorize(oauth_code)
|
||||
frappe.db.set_value(
|
||||
"Website Settings",
|
||||
frappe.db.set_single_value(
|
||||
"Website Settings",
|
||||
{"indexing_authorization_code": oauth_code, "indexing_refresh_token": res.get("refresh_token")},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,13 +11,7 @@ from frappe.rate_limiter import rate_limit
|
|||
from frappe.utils import cint, get_url
|
||||
from frappe.utils.html_utils import get_icon_html
|
||||
from frappe.utils.jinja import guess_is_path
|
||||
from frappe.utils.oauth import (
|
||||
get_oauth2_authorize_url,
|
||||
get_oauth_keys,
|
||||
login_via_oauth2,
|
||||
login_via_oauth2_id_token,
|
||||
redirect_post_login,
|
||||
)
|
||||
from frappe.utils.oauth import get_oauth2_authorize_url, get_oauth_keys, redirect_post_login
|
||||
from frappe.utils.password import get_decrypted_password
|
||||
from frappe.website.utils import get_home_page
|
||||
|
||||
|
|
@ -108,31 +102,6 @@ def get_context(context):
|
|||
return context
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def login_via_google(code: str, state: str):
|
||||
login_via_oauth2("google", code, state, decoder=decoder_compat)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def login_via_github(code: str, state: str):
|
||||
login_via_oauth2("github", code, state)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def login_via_facebook(code: str, state: str):
|
||||
login_via_oauth2("facebook", code, state, decoder=decoder_compat)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def login_via_frappe(code: str, state: str):
|
||||
login_via_oauth2("frappe", code, state, decoder=decoder_compat)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def login_via_office365(code: str, state: str):
|
||||
login_via_oauth2_id_token("office_365", code, state, decoder=decoder_compat)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def login_via_token(login_token: str):
|
||||
sid = frappe.cache().get_value(f"login_token:{login_token}", expires=True)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue