Merge branch 'develop' into fix_child_table_in_dialog_sortable

This commit is contained in:
Shariq Ansari 2023-01-05 13:38:55 +05:30 committed by GitHub
commit 94669b13a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 372 additions and 165 deletions

View file

@ -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");
});
});

View file

@ -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");
});
});

View file

@ -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);

View file

@ -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():

View file

@ -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

View file

@ -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):

View file

@ -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}>"

View file

@ -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):

View file

@ -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()

View file

@ -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]

View file

@ -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()

View file

@ -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

View file

@ -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 = [

View file

@ -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()

View file

@ -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.")

View file

@ -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))

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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")

View file

@ -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},
)

View file

@ -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)

View file

@ -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() {

View file

@ -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) {

View file

@ -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 {

View file

@ -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)

View file

@ -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""",

View file

@ -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()

View file

@ -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()

View file

@ -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():

View file

@ -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)

View file

@ -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")},
)

View file

@ -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)