Merge branch 'frappe:develop' into fix/dynamic-filters-dialog-prepopulate
This commit is contained in:
commit
bb809338de
343 changed files with 55923 additions and 41187 deletions
|
|
@ -1,5 +1,7 @@
|
|||
**/hooks.py,frappe.gettext.extractors.navbar.extract
|
||||
**/doctype/*/*.json,frappe.gettext.extractors.doctype.extract
|
||||
**/desktop_icon/*.json,frappe.gettext.extractors.desktop_icon.extract
|
||||
**/workspace_sidebar/*.json,frappe.gettext.extractors.workspace_sidebar.extract
|
||||
**/workspace/*/*.json,frappe.gettext.extractors.workspace.extract
|
||||
**/web_form/*/*.json,frappe.gettext.extractors.web_form.extract
|
||||
**/onboarding_step/*/*.json,frappe.gettext.extractors.onboarding_step.extract
|
||||
|
|
|
|||
|
|
|
@ -72,10 +72,8 @@ context("Web Form", () => {
|
|||
|
||||
cy.call("logout");
|
||||
|
||||
cy.visit("/note");
|
||||
cy.get_open_dialog()
|
||||
.get(".modal-message")
|
||||
.contains("You are not permitted to access this page without login.");
|
||||
cy.visit("/note", { failOnStatusCode: false });
|
||||
cy.contains("You must be logged in to use this form.");
|
||||
});
|
||||
|
||||
it("Show List", () => {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import importlib
|
|||
import inspect
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
import warnings
|
||||
|
|
@ -76,6 +77,7 @@ local = Local()
|
|||
cache: "RedisWrapper" | None = None
|
||||
client_cache: "ClientCache" | None = None
|
||||
STANDARD_USERS = ("Guest", "Administrator")
|
||||
SITE_NAME_PATTERN = re.compile(r"^[a-zA-Z0-9._-]+$")
|
||||
|
||||
# this global may be subsequently changed by frappe.tests.utils.toggle_test_mode()
|
||||
in_test = False
|
||||
|
|
@ -144,6 +146,9 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force: bool =
|
|||
if getattr(local, "initialised", None) and not force:
|
||||
return
|
||||
|
||||
if site and not SITE_NAME_PATTERN.match(site):
|
||||
raise ValueError(f"Invalid site name `{site}`")
|
||||
|
||||
local.error_log = []
|
||||
local.message_log = []
|
||||
local.debug_log = []
|
||||
|
|
@ -1472,25 +1477,23 @@ def logger(
|
|||
|
||||
|
||||
def get_desk_link(doctype, name, show_title_with_name=False, open_in_new_tab=False):
|
||||
from urllib.parse import quote
|
||||
from frappe.desk.utils import slug
|
||||
from frappe.utils.data import quoted
|
||||
|
||||
meta = get_meta(doctype)
|
||||
title = get_value(doctype, name, meta.get_title_field())
|
||||
|
||||
target_attr = ' target="_blank"' if open_in_new_tab else ""
|
||||
|
||||
# encode for href
|
||||
encoded_name = quote(name)
|
||||
|
||||
if show_title_with_name and name != title:
|
||||
html = '<a href="/desk/Form/{doctype}/{encoded_name}"{target} style="font-weight: bold;">{doctype_local} {name}: {title_local}</a>'
|
||||
html = '<a href="/desk/{doctype}/{encoded_name}"{target} style="font-weight: bold;">{doctype_local} {name}: {title_local}</a>'
|
||||
else:
|
||||
html = '<a href="/desk/Form/{doctype}/{encoded_name}"{target} style="font-weight: bold;">{doctype_local} {title_local}</a>'
|
||||
html = '<a href="/desk/{doctype}/{encoded_name}"{target} style="font-weight: bold;">{doctype_local} {title_local}</a>'
|
||||
|
||||
return html.format(
|
||||
doctype=doctype,
|
||||
doctype=quoted(slug(doctype)),
|
||||
name=name,
|
||||
encoded_name=encoded_name,
|
||||
encoded_name=quoted(name),
|
||||
doctype_local=_(doctype),
|
||||
title_local=_(title),
|
||||
target=target_attr,
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ def get_default_path():
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_app_as_default(app_name):
|
||||
def set_app_as_default(app_name: str):
|
||||
if frappe.db.get_value("User", frappe.session.user, "default_app") == app_name:
|
||||
frappe.db.set_value("User", frappe.session.user, "default_app", "")
|
||||
else:
|
||||
|
|
@ -96,7 +96,7 @@ def set_app_as_default(app_name):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_incomplete_setup_route(current_app, app_route):
|
||||
def get_incomplete_setup_route(current_app: str, app_route: str):
|
||||
pending_apps = get_apps_with_incomplete_dependencies(current_app)
|
||||
|
||||
if not pending_apps:
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ class LoginManager:
|
|||
self.full_name = None
|
||||
self.user_type = None
|
||||
|
||||
if frappe.local.form_dict.get("cmd") == "login" or frappe.local.request.path == "/api/method/login":
|
||||
if frappe.local.request.path == "/api/method/login":
|
||||
if self.login() is False:
|
||||
return
|
||||
self.resume = False
|
||||
|
|
@ -176,9 +176,12 @@ class LoginManager:
|
|||
self.set_user_info()
|
||||
|
||||
def get_user_info(self):
|
||||
self.info = frappe.get_cached_value(
|
||||
result = frappe.get_cached_value(
|
||||
"User", self.user, ["user_type", "first_name", "last_name", "user_image"], as_dict=1
|
||||
)
|
||||
if result is None:
|
||||
frappe.throw(_("User does not exist"), frappe.DoesNotExistError)
|
||||
self.info = result
|
||||
self.user_type = self.info.user_type
|
||||
|
||||
def setup_boot_cache(self):
|
||||
|
|
@ -201,7 +204,7 @@ class LoginManager:
|
|||
frappe.local.cookie_manager.set_cookie("system_user", "yes", deduplicate=True)
|
||||
if not resume:
|
||||
frappe.local.response["message"] = "Logged In"
|
||||
frappe.local.response["home_page"] = get_default_path() or "/desk"
|
||||
frappe.local.response["home_page"] = get_home_page() or "/desk"
|
||||
|
||||
if not resume:
|
||||
frappe.response["full_name"] = self.full_name
|
||||
|
|
@ -250,7 +253,11 @@ class LoginManager:
|
|||
):
|
||||
return
|
||||
|
||||
clear_sessions(frappe.session.user, keep_current=True)
|
||||
clear_sessions(
|
||||
frappe.session.user,
|
||||
keep_current=True,
|
||||
force=frappe.session.user != "Administrator",
|
||||
)
|
||||
|
||||
def authenticate(self, user: str | None = None, pwd: str | None = None):
|
||||
from frappe.core.doctype.user.user import User
|
||||
|
|
|
|||
|
|
@ -199,7 +199,10 @@ def get_assignments(doc) -> list[dict]:
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def bulk_apply(doctype, docnames):
|
||||
def bulk_apply(doctype: str, docnames: str | list[str]):
|
||||
if not frappe.get_cached_value("User", frappe.session.user, "bulk_actions"):
|
||||
frappe.throw(_("You are not allowed to perform bulk actions"), frappe.PermissionError)
|
||||
|
||||
docnames = frappe.parse_json(docnames)
|
||||
background = len(docnames) > 5
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
|
|
@ -556,7 +556,13 @@ def get_auto_repeat_entries(date=None):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_auto_repeat(doctype, docname, frequency="Daily", start_date=None, end_date=None):
|
||||
def make_auto_repeat(
|
||||
doctype: str,
|
||||
docname: str | int,
|
||||
frequency: str = "Daily",
|
||||
start_date: str | datetime | None = None,
|
||||
end_date: str | datetime | None = None,
|
||||
):
|
||||
if not start_date:
|
||||
start_date = getdate(today())
|
||||
doc = frappe.new_doc("Auto Repeat")
|
||||
|
|
@ -573,7 +579,9 @@ def make_auto_repeat(doctype, docname, frequency="Daily", start_date=None, end_d
|
|||
# method for reference_doctype filter
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_auto_repeat_doctypes(doctype, txt, searchfield, start, page_len, filters):
|
||||
def get_auto_repeat_doctypes(
|
||||
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: str | dict | list
|
||||
):
|
||||
res = frappe.get_all(
|
||||
"Property Setter",
|
||||
{
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ from frappe.desk.doctype.form_tour.form_tour import get_onboarding_ui_tours
|
|||
from frappe.desk.doctype.route_history.route_history import frequently_visited_links
|
||||
from frappe.desk.form.load import get_meta_bundle
|
||||
from frappe.email.inbox import get_email_accounts
|
||||
from frappe.integrations.frappe_providers.frappecloud_billing import is_fc_site
|
||||
from frappe.integrations.frappe_providers.frappecloud_billing import current_site_info, is_fc_site
|
||||
from frappe.model.base_document import get_controller
|
||||
from frappe.permissions import has_permission
|
||||
from frappe.query_builder import DocType
|
||||
|
|
@ -125,6 +125,8 @@ def get_bootinfo():
|
|||
bootinfo.setup_wizard_completed_apps = get_setup_wizard_completed_apps() or []
|
||||
bootinfo.desktop_icon_urls = get_desktop_icon_urls()
|
||||
bootinfo.desktop_icon_style = get_icon_style() or "Subtle"
|
||||
if bootinfo.is_fc_site:
|
||||
bootinfo.site_info = current_site_info()
|
||||
return bootinfo
|
||||
|
||||
|
||||
|
|
@ -537,7 +539,9 @@ def get_sidebar_items(allowed_workspaces):
|
|||
from frappe import _
|
||||
from frappe.desk.doctype.workspace_sidebar.workspace_sidebar import auto_generate_sidebar_from_module
|
||||
|
||||
workspace_sidebars = frappe.get_all("Workspace Sidebar", fields=["name", "header_icon"])
|
||||
workspace_sidebars = frappe.get_all(
|
||||
"Workspace Sidebar", fields=["name", "header_icon", "module_onboarding"]
|
||||
)
|
||||
module_sidebars = auto_generate_sidebar_from_module()
|
||||
workspace_sidebars.extend(module_sidebars)
|
||||
sidebar_items = {}
|
||||
|
|
@ -559,6 +563,7 @@ def get_sidebar_items(allowed_workspaces):
|
|||
"label": sidebar_title,
|
||||
"items": [],
|
||||
"header_icon": sidebar.get("header_icon"),
|
||||
"module_onboarding": sidebar.get("module_onboarding"),
|
||||
"module": sidebar_doc.module,
|
||||
"app": sidebar_doc.app,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# License: MIT. See LICENSE
|
||||
import json
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import frappe
|
||||
import frappe.model
|
||||
|
|
@ -25,18 +25,18 @@ Requests via FrappeClient are also handled here.
|
|||
|
||||
@frappe.whitelist()
|
||||
def get_list(
|
||||
doctype,
|
||||
fields=None,
|
||||
filters=None,
|
||||
group_by=None,
|
||||
order_by=None,
|
||||
limit_start=None,
|
||||
limit_page_length=20,
|
||||
parent=None,
|
||||
debug: bool = False,
|
||||
as_dict: bool = True,
|
||||
or_filters=None,
|
||||
expand=None,
|
||||
doctype: str,
|
||||
fields: str | list[str | dict[str, Any]] | None = None,
|
||||
filters: str | list | dict[str, Any] | None = None,
|
||||
group_by: str | list[str] | None = None,
|
||||
order_by: str | list[str] | None = None,
|
||||
limit_start: int | str | None = None,
|
||||
limit_page_length: int | str = 20,
|
||||
parent: str | None = None,
|
||||
debug: bool | int = False,
|
||||
as_dict: bool | int = True,
|
||||
or_filters: str | list[list] | dict[str, Any] | None = None,
|
||||
expand: str | list[str] | None = None,
|
||||
):
|
||||
"""Return a list of records by filters, fields, ordering and limit.
|
||||
|
||||
|
|
@ -76,7 +76,12 @@ def get_list(
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_count(doctype, filters=None, debug=False, cache=False):
|
||||
def get_count(
|
||||
doctype: str,
|
||||
filters: str | list | dict[str, Any] | None = None,
|
||||
debug: int | bool = False,
|
||||
cache: int | bool = False,
|
||||
):
|
||||
from frappe.desk.reportview import get_count
|
||||
|
||||
frappe.form_dict.doctype = doctype
|
||||
|
|
@ -87,7 +92,12 @@ def get_count(doctype, filters=None, debug=False, cache=False):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get(doctype, name=None, filters=None, parent=None):
|
||||
def get(
|
||||
doctype: str,
|
||||
name: str | int | None = None,
|
||||
filters: str | list | dict[str, Any] | None = None,
|
||||
parent: str | None = None,
|
||||
):
|
||||
"""Return a document by name or filters.
|
||||
|
||||
:param doctype: DocType of the document to be returned
|
||||
|
|
@ -108,7 +118,14 @@ def get(doctype, name=None, filters=None, parent=None):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, parent=None):
|
||||
def get_value(
|
||||
doctype: str,
|
||||
fieldname: str | list[str] | dict[str, Any],
|
||||
filters: str | list | dict[str, Any] | None = None,
|
||||
as_dict: int | bool = True,
|
||||
debug: int | bool = False,
|
||||
parent: str | None = None,
|
||||
):
|
||||
"""Return a value from a document.
|
||||
|
||||
:param doctype: DocType to be queried
|
||||
|
|
@ -156,7 +173,7 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, paren
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_single_value(doctype, field):
|
||||
def get_single_value(doctype: str, field: str):
|
||||
if not frappe.has_permission(doctype):
|
||||
frappe.throw(_("No permission for {0}").format(_(doctype)), frappe.PermissionError)
|
||||
|
||||
|
|
@ -164,7 +181,7 @@ def get_single_value(doctype, field):
|
|||
|
||||
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def set_value(doctype, name, fieldname, value=None):
|
||||
def set_value(doctype: str, name: str | int, fieldname: str | dict[str, Any], value: Any | None = None):
|
||||
"""Set a value using get_doc, group of values
|
||||
|
||||
:param doctype: DocType of the document
|
||||
|
|
@ -201,7 +218,7 @@ def set_value(doctype, name, fieldname, value=None):
|
|||
|
||||
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def insert(doc=None):
|
||||
def insert(doc: str | dict[str, Any] | None = None):
|
||||
"""Insert a document
|
||||
|
||||
:param doc: JSON or dict object to be inserted"""
|
||||
|
|
@ -212,7 +229,7 @@ def insert(doc=None):
|
|||
|
||||
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def insert_many(docs=None):
|
||||
def insert_many(docs: str | list[dict[str, Any]] | None = None):
|
||||
"""Insert multiple documents
|
||||
|
||||
:param docs: JSON or list of dict objects to be inserted in one request"""
|
||||
|
|
@ -226,7 +243,7 @@ def insert_many(docs=None):
|
|||
|
||||
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def save(doc):
|
||||
def save(doc: str | dict[str, Any]):
|
||||
"""Update (save) an existing document
|
||||
|
||||
:param doc: JSON or dict object with the properties of the document to be updated"""
|
||||
|
|
@ -240,7 +257,7 @@ def save(doc):
|
|||
|
||||
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def rename_doc(doctype, old_name, new_name, merge=False):
|
||||
def rename_doc(doctype: str, old_name: str | int, new_name: str | int, merge: bool = False):
|
||||
"""Rename document
|
||||
|
||||
:param doctype: DocType of the document to be renamed
|
||||
|
|
@ -251,7 +268,7 @@ def rename_doc(doctype, old_name, new_name, merge=False):
|
|||
|
||||
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def submit(doc):
|
||||
def submit(doc: str | dict[str, Any]):
|
||||
"""Submit a document
|
||||
|
||||
:param doc: JSON or dict object to be submitted remotely"""
|
||||
|
|
@ -265,7 +282,7 @@ def submit(doc):
|
|||
|
||||
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def cancel(doctype, name):
|
||||
def cancel(doctype: str, name: str | int):
|
||||
"""Cancel a document
|
||||
|
||||
:param doctype: DocType of the document to be cancelled
|
||||
|
|
@ -277,7 +294,7 @@ def cancel(doctype, name):
|
|||
|
||||
|
||||
@frappe.whitelist(methods=["DELETE", "POST"])
|
||||
def delete(doctype, name):
|
||||
def delete(doctype: str, name: str | int):
|
||||
"""Delete a remote document
|
||||
|
||||
:param doctype: DocType of the document to be deleted
|
||||
|
|
@ -286,7 +303,7 @@ def delete(doctype, name):
|
|||
|
||||
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def bulk_update(docs):
|
||||
def bulk_update(docs: str):
|
||||
"""Bulk update documents
|
||||
|
||||
:param docs: JSON list of documents to be updated remotely. Each document must have `docname` property"""
|
||||
|
|
@ -305,7 +322,7 @@ def bulk_update(docs):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def has_permission(doctype: str, docname: str, perm_type: str = "read"):
|
||||
def has_permission(doctype: str, docname: str | int, perm_type: str = "read"):
|
||||
"""Return a JSON with data whether the document has the requested permission.
|
||||
|
||||
:param doctype: DocType of the document to be checked
|
||||
|
|
@ -316,7 +333,7 @@ def has_permission(doctype: str, docname: str, perm_type: str = "read"):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_doc_permissions(doctype: str, docname: str):
|
||||
def get_doc_permissions(doctype: str, docname: str | int):
|
||||
"""Return an evaluated document permissions dict like `{"read":1, "write":1}`.
|
||||
|
||||
:param doctype: DocType of the document to be evaluated
|
||||
|
|
@ -327,7 +344,7 @@ def get_doc_permissions(doctype: str, docname: str):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_password(doctype: str, name: str, fieldname: str):
|
||||
def get_password(doctype: str, name: str | int, fieldname: str):
|
||||
"""Return a password type property. Only applicable for System Managers
|
||||
|
||||
:param doctype: DocType of the document that holds the password
|
||||
|
|
@ -351,14 +368,14 @@ def get_time_zone():
|
|||
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def attach_file(
|
||||
filename=None,
|
||||
filedata=None,
|
||||
doctype=None,
|
||||
docname=None,
|
||||
folder=None,
|
||||
decode_base64=False,
|
||||
is_private=None,
|
||||
docfield=None,
|
||||
filename: str | None = None,
|
||||
filedata: str | None = None,
|
||||
doctype: str | None = None,
|
||||
docname: str | int | None = None,
|
||||
folder: str | None = None,
|
||||
decode_base64: int | bool = False,
|
||||
is_private: int | bool | None = None,
|
||||
docfield: str | None = None,
|
||||
):
|
||||
"""Attach a file to Document
|
||||
|
||||
|
|
@ -396,7 +413,7 @@ def attach_file(
|
|||
|
||||
@frappe.whitelist()
|
||||
@http_cache(max_age=10 * 60)
|
||||
def is_document_amended(doctype: str, docname: str):
|
||||
def is_document_amended(doctype: str, docname: str | int):
|
||||
if frappe.permissions.has_permission(doctype):
|
||||
try:
|
||||
return frappe.db.exists(doctype, {"amended_from": docname})
|
||||
|
|
@ -409,7 +426,7 @@ def is_document_amended(doctype: str, docname: str):
|
|||
@frappe.whitelist(methods=["GET", "POST"])
|
||||
def validate_link_and_fetch(
|
||||
doctype: str,
|
||||
docname: str,
|
||||
docname: str | int,
|
||||
fields_to_fetch: list[str] | str | None = None,
|
||||
# search_widget parameters
|
||||
query: str | None = None,
|
||||
|
|
|
|||
|
|
@ -1116,6 +1116,7 @@ class TestGunicornWorker(IntegrationTestCase):
|
|||
time.sleep(2)
|
||||
execute_in_shell("pgrep gunicorn | xargs -L1 kill -9")
|
||||
|
||||
@unittest.skip("Flaky test")
|
||||
def test_gunicorn_ping_sync(self):
|
||||
self.spawn_gunicorn()
|
||||
path = f"http://{self.TEST_SITE}:{self.port}/api/method/ping"
|
||||
|
|
@ -1126,6 +1127,7 @@ class TestGunicornWorker(IntegrationTestCase):
|
|||
path = f"http://{self.TEST_SITE}:{self.port}/api/method/ping"
|
||||
self.assertEqual(requests.get(path).status_code, 200)
|
||||
|
||||
@unittest.skip("Flaky test")
|
||||
def test_gunicorn_idle_cpu_usage(self):
|
||||
def get_total_usage():
|
||||
process = psutil.Process(self.handle.pid)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
from typing import Any
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
|
|
@ -110,7 +112,7 @@ def delete_contact_and_address(doctype: str, docname: str) -> None:
|
|||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def filter_dynamic_link_doctypes(
|
||||
doctype, txt: str, searchfield, start, page_len, filters: dict
|
||||
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict[str, Any]
|
||||
) -> list[list[str]]:
|
||||
from frappe.permissions import get_doctypes_with_read
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright (c) 2015, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
from typing import Any
|
||||
|
||||
from jinja2 import TemplateSyntaxError
|
||||
|
||||
import frappe
|
||||
|
|
@ -262,7 +264,9 @@ def get_company_address(company):
|
|||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def address_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
def address_query(
|
||||
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict[str, Any]
|
||||
):
|
||||
from frappe.desk.search import search_widget
|
||||
|
||||
_filters = []
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
from typing import Any
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.contacts.address_and_contact import set_link_title
|
||||
|
|
@ -312,7 +314,7 @@ def invite_user(contact: str):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_contact_details(contact):
|
||||
def get_contact_details(contact: str):
|
||||
contact = frappe.get_doc("Contact", contact)
|
||||
contact.check_permission()
|
||||
|
||||
|
|
@ -341,7 +343,9 @@ def update_contact(doc, method):
|
|||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def contact_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
def contact_query(
|
||||
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict[str, Any]
|
||||
):
|
||||
from frappe.desk.reportview import get_match_cond
|
||||
|
||||
doctype = "Contact"
|
||||
|
|
@ -379,7 +383,7 @@ def contact_query(doctype, txt, searchfield, start, page_len, filters):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def address_query(links):
|
||||
def address_query(links: str):
|
||||
import json
|
||||
|
||||
links = [
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
# Copyright (c) 2021, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
from typing import Any
|
||||
|
||||
from tenacity import retry, retry_if_exception_type, stop_after_attempt
|
||||
|
||||
import frappe
|
||||
|
|
@ -45,14 +47,14 @@ class AccessLog(Document):
|
|||
reraise=True,
|
||||
)
|
||||
def make_access_log(
|
||||
doctype=None,
|
||||
document=None,
|
||||
method=None,
|
||||
file_type=None,
|
||||
report_name=None,
|
||||
filters=None,
|
||||
page=None,
|
||||
columns=None,
|
||||
doctype: str | None = None,
|
||||
document: str | int | None = None,
|
||||
method: str | None = None,
|
||||
file_type: str | None = None,
|
||||
report_name: str | None = None,
|
||||
filters: str | list | dict[str, Any] | None = None,
|
||||
page: str | None = None,
|
||||
columns: str | None = None,
|
||||
):
|
||||
access_log = frappe.get_doc(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import time
|
|||
import frappe
|
||||
from frappe.auth import CookieManager, LoginManager
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import set_request
|
||||
|
||||
|
||||
class TestActivityLog(IntegrationTestCase):
|
||||
|
|
@ -15,12 +16,12 @@ class TestActivityLog(IntegrationTestCase):
|
|||
# test user login log
|
||||
frappe.local.form_dict = frappe._dict(
|
||||
{
|
||||
"cmd": "login",
|
||||
"sid": "Guest",
|
||||
"pwd": self.ADMIN_PASSWORD or "admin",
|
||||
"usr": "Administrator",
|
||||
}
|
||||
)
|
||||
set_request(method="POST", path="/api/method/login")
|
||||
|
||||
frappe.local.request_ip = "127.0.0.1"
|
||||
frappe.local.cookie_manager = CookieManager()
|
||||
|
|
@ -60,8 +61,9 @@ class TestActivityLog(IntegrationTestCase):
|
|||
update_system_settings({"allow_consecutive_login_attempts": 3, "allow_login_after_fail": 5})
|
||||
|
||||
frappe.local.form_dict = frappe._dict(
|
||||
{"cmd": "login", "sid": "Guest", "pwd": self.ADMIN_PASSWORD, "usr": "Administrator"}
|
||||
{"sid": "Guest", "pwd": self.ADMIN_PASSWORD, "usr": "Administrator"}
|
||||
)
|
||||
set_request(method="POST", path="/api/method/login")
|
||||
|
||||
frappe.local.request_ip = "127.0.0.1"
|
||||
frappe.local.cookie_manager = CookieManager()
|
||||
|
|
|
|||
|
|
@ -211,8 +211,7 @@ frappe.ui.form.on("Communication", {
|
|||
],
|
||||
primary_action_label: __("Move"),
|
||||
primary_action(values) {
|
||||
d.hide();
|
||||
frappe.call({
|
||||
return frappe.call({
|
||||
method: "frappe.email.inbox.move_email",
|
||||
args: {
|
||||
communication: frm.doc.name,
|
||||
|
|
@ -220,6 +219,7 @@ frappe.ui.form.on("Communication", {
|
|||
},
|
||||
freeze: true,
|
||||
callback: function () {
|
||||
d.hide();
|
||||
window.history.back();
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -419,7 +419,7 @@ class Communication(Document, CommunicationEmailMixin):
|
|||
# Skip timeline links if a "Sent" communication already exists
|
||||
# else will create duplicate timeline entries
|
||||
if self.sent_or_received == "Received" and self.find_one_by_filters(
|
||||
message_id=self.message_id, sent_or_received="Sent"
|
||||
message_id=self.message_id, email_account=self.email_account, sent_or_received="Sent"
|
||||
):
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
|
||||
import json
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import frappe
|
||||
import frappe.email.smtp
|
||||
|
|
@ -27,32 +28,32 @@ if TYPE_CHECKING:
|
|||
|
||||
@frappe.whitelist()
|
||||
def make(
|
||||
doctype=None,
|
||||
name=None,
|
||||
content=None,
|
||||
subject=None,
|
||||
sent_or_received="Sent",
|
||||
sender=None,
|
||||
sender_full_name=None,
|
||||
recipients=None,
|
||||
communication_medium="Email",
|
||||
send_email=False,
|
||||
print_html=None,
|
||||
print_format=None,
|
||||
attachments=None,
|
||||
send_me_a_copy=False,
|
||||
cc=None,
|
||||
bcc=None,
|
||||
read_receipt=None,
|
||||
print_letterhead=True,
|
||||
email_template=None,
|
||||
communication_type=None,
|
||||
send_after=None,
|
||||
print_language=None,
|
||||
now=False,
|
||||
raw_html=False,
|
||||
add_css=True,
|
||||
in_reply_to=None,
|
||||
doctype: str | None = None,
|
||||
name: str | int | None = None,
|
||||
content: str | None = None,
|
||||
subject: str | None = None,
|
||||
sent_or_received: str = "Sent",
|
||||
sender: str | None = None,
|
||||
sender_full_name: str | None = None,
|
||||
recipients: str | list[str] | None = None,
|
||||
communication_medium: str = "Email",
|
||||
send_email: str | bool | int = False,
|
||||
print_html: str | None = None,
|
||||
print_format: str | None = None,
|
||||
attachments: str | list[str | dict[str, Any]] | None = None,
|
||||
send_me_a_copy: str | int | bool = False,
|
||||
cc: str | list[str] | None = None,
|
||||
bcc: str | list[str] | None = None,
|
||||
read_receipt: str | int | bool | None = None,
|
||||
print_letterhead: int | bool = True,
|
||||
email_template: str | None = None,
|
||||
communication_type: str | None = None,
|
||||
send_after: str | datetime | None = None,
|
||||
print_language: str | None = None,
|
||||
now: int | bool = False,
|
||||
raw_html: int | bool = False,
|
||||
add_css: int | bool = True,
|
||||
in_reply_to: str | None = None,
|
||||
**kwargs,
|
||||
) -> dict[str, str]:
|
||||
"""Make a new communication. Checks for email permissions for specified Document.
|
||||
|
|
@ -88,10 +89,8 @@ def make(
|
|||
if doctype and name:
|
||||
frappe.has_permission(doctype, doc=name, ptype="email", throw=True)
|
||||
|
||||
if (
|
||||
raw_html
|
||||
and email_template
|
||||
and not frappe.get_cached_value("Email Template", email_template, "use_html")
|
||||
if raw_html and not (
|
||||
email_template and frappe.get_cached_value("Email Template", email_template, "use_html")
|
||||
):
|
||||
warn(
|
||||
_(
|
||||
|
|
@ -193,7 +192,7 @@ def _make(
|
|||
}
|
||||
)
|
||||
comm.flags.skip_add_signature = not add_signature or (
|
||||
raw_html and frappe.get_cached_value("Email Template", email_template, "use_html")
|
||||
raw_html and email_template and frappe.get_cached_value("Email Template", email_template, "use_html")
|
||||
)
|
||||
comm.insert(ignore_permissions=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import csv
|
||||
import os
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import frappe
|
||||
import frappe.permissions
|
||||
|
|
@ -30,15 +31,15 @@ def get_data_keys():
|
|||
|
||||
@frappe.whitelist()
|
||||
def export_data(
|
||||
doctype=None,
|
||||
parent_doctype=None,
|
||||
all_doctypes=True,
|
||||
with_data=False,
|
||||
select_columns=None,
|
||||
file_type="CSV",
|
||||
template=False,
|
||||
filters=None,
|
||||
export_without_column_meta=False,
|
||||
doctype: str | list[str | dict[str, Any]] | None = None,
|
||||
parent_doctype: str | None = None,
|
||||
all_doctypes: bool | int | str = True,
|
||||
with_data: bool | int | str = False,
|
||||
select_columns: str | dict[str, list[str]] | None = None,
|
||||
file_type: str = "CSV",
|
||||
template: bool | str = False,
|
||||
filters: str | dict[str, Any] | list | None = None,
|
||||
export_without_column_meta: bool | str = False,
|
||||
):
|
||||
_doctype = doctype
|
||||
if isinstance(_doctype, list):
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from rq.command import send_stop_job_command
|
||||
from rq.exceptions import InvalidJobOperation
|
||||
|
|
@ -102,7 +103,7 @@ class DataImport(Document):
|
|||
self.payload_count = len(payloads)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_preview_from_template(self, import_file=None, google_sheets_url=None):
|
||||
def get_preview_from_template(self, import_file: str | None = None, google_sheets_url: str | None = None):
|
||||
if import_file:
|
||||
self.import_file = import_file
|
||||
self.set_delimiters_flag()
|
||||
|
|
@ -203,7 +204,13 @@ def start_import(data_import):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_template(doctype, export_fields=None, export_records=None, export_filters=None, file_type="CSV"):
|
||||
def download_template(
|
||||
doctype: str,
|
||||
export_fields: str | dict[str, list[str]] | None = None,
|
||||
export_records: str | None = None,
|
||||
export_filters: str | dict[str, Any] | list[list[Any]] | None = None,
|
||||
file_type: str = "CSV",
|
||||
):
|
||||
"""
|
||||
Download template from Exporter
|
||||
:param doctype: Document Type
|
||||
|
|
|
|||
|
|
@ -93,15 +93,19 @@ class Importer:
|
|||
return
|
||||
|
||||
# setup import log
|
||||
import_log = (
|
||||
frappe.get_all(
|
||||
"Data Import Log",
|
||||
fields=["row_indexes", "success", "log_index"],
|
||||
filters={"data_import": self.data_import.name},
|
||||
order_by="log_index",
|
||||
# Only use import log for retry/resume when Data Import is persisted in DB.
|
||||
# For bench data-import (CLI), the doc is never inserted, so we must not reuse logs
|
||||
import_log = []
|
||||
if self.data_import.name and frappe.db.exists("Data Import", self.data_import.name):
|
||||
import_log = (
|
||||
frappe.get_all(
|
||||
"Data Import Log",
|
||||
fields=["row_indexes", "success", "log_index"],
|
||||
filters={"data_import": self.data_import.name},
|
||||
order_by="log_index",
|
||||
)
|
||||
or []
|
||||
)
|
||||
or []
|
||||
)
|
||||
|
||||
log_index = 0
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class DeletedDocument(Document):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def restore(name, alert=True):
|
||||
def restore(name: str | int, alert: bool = True):
|
||||
deleted = frappe.get_doc("Deleted Document", name)
|
||||
|
||||
if deleted.restored:
|
||||
|
|
@ -69,7 +69,7 @@ def restore(name, alert=True):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def bulk_restore(docnames):
|
||||
def bulk_restore(docnames: str | list[str]):
|
||||
docnames = frappe.parse_json(docnames)
|
||||
message = _("Restoring Deleted Document")
|
||||
restored, invalid, failed = [], [], []
|
||||
|
|
|
|||
|
|
@ -2,4 +2,9 @@
|
|||
// For license information, please see license.txt
|
||||
|
||||
frappe.views.calendar["{doctype}"] = {{
|
||||
// field_map: {{
|
||||
// start: "start_date",
|
||||
// end: "end_date",
|
||||
// }},
|
||||
// gantt: true
|
||||
}};
|
||||
|
|
@ -992,7 +992,13 @@ class DocType(Document):
|
|||
self.append("fields", {"label": "Is Group", "fieldtype": "Check", "fieldname": "is_group"})
|
||||
self.append(
|
||||
"fields",
|
||||
{"label": "Old Parent", "fieldtype": "Link", "options": self.name, "fieldname": "old_parent"},
|
||||
{
|
||||
"label": "Old Parent",
|
||||
"fieldtype": "Link",
|
||||
"options": self.name,
|
||||
"fieldname": "old_parent",
|
||||
"hidden": 1,
|
||||
},
|
||||
)
|
||||
|
||||
parent_field_label = f"Parent {self.name}"
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ frappe.listview_settings["DocType"] = {
|
|||
primary_action_label: __("Create & Continue"),
|
||||
primary_action(values) {
|
||||
if (!values.istable) values.editable_grid = 0;
|
||||
frappe.db
|
||||
return frappe.db
|
||||
.insert({
|
||||
doctype: "DocType",
|
||||
...values,
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ class DocumentNamingSettings(Document):
|
|||
NamingSeries(series).validate()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_options(self, doctype=None):
|
||||
def get_options(self, doctype: str | None = None):
|
||||
doctype = doctype or self.transaction_type
|
||||
if not doctype:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ frappe.ui.form.on("File", {
|
|||
if (frm.doc.file_url) {
|
||||
frm.add_custom_button(__("View File"), () => {
|
||||
if (!frappe.utils.is_url(frm.doc.file_url)) {
|
||||
window.open(window.location.origin + frm.doc.file_url);
|
||||
window.open(
|
||||
window.location.origin + frm.doc.file_url + "?fid=" + frm.doc.name
|
||||
);
|
||||
} else {
|
||||
window.open(frm.doc.file_url);
|
||||
}
|
||||
|
|
@ -41,25 +43,34 @@ frappe.ui.form.on("File", {
|
|||
if (!frappe.utils.can_upload_public_files() && frm.doc.is_private) {
|
||||
frm.set_df_property("is_private", "read_only", 1);
|
||||
}
|
||||
|
||||
if (frm.doc.attached_to_name) {
|
||||
const field = frm.get_field("attached_to_name");
|
||||
field.$input_wrapper
|
||||
.find(".control-value")
|
||||
.html(`${frappe.utils.get_form_link(frm.doctype, frm.docname, true)}`);
|
||||
}
|
||||
},
|
||||
|
||||
preview_file: function (frm) {
|
||||
let $preview = "";
|
||||
let file_extension = frm.doc.file_type.toLowerCase();
|
||||
const full_file_url = frm.doc.file_url + "?fid=" + frm.doc.name;
|
||||
const src_url = frappe.utils.escape_html(full_file_url);
|
||||
|
||||
if (frappe.utils.is_image_file(frm.doc.file_url)) {
|
||||
if (frappe.utils.is_image_file(full_file_url)) {
|
||||
$preview = $(`<div class="img_preview">
|
||||
<img
|
||||
class="img-responsive"
|
||||
style="max-width: 500px";
|
||||
src="${frappe.utils.escape_html(frm.doc.file_url)}"
|
||||
src="${src_url}"
|
||||
onerror="${frm.toggle_display("preview", false)}"
|
||||
/>
|
||||
</div>`);
|
||||
} else if (frappe.utils.is_video_file(frm.doc.file_url)) {
|
||||
} else if (frappe.utils.is_video_file(full_file_url)) {
|
||||
$preview = $(`<div class="img_preview">
|
||||
<video width="480" height="320" controls>
|
||||
<source src="${frappe.utils.escape_html(frm.doc.file_url)}">
|
||||
<source src="${src_url}">
|
||||
${__("Your browser does not support the video element.")}
|
||||
</video>
|
||||
</div>`);
|
||||
|
|
@ -70,14 +81,14 @@ frappe.ui.form.on("File", {
|
|||
style="background:#323639;"
|
||||
width="100%"
|
||||
height="1190"
|
||||
src="${frappe.utils.escape_html(frm.doc.file_url)}" type="application/pdf"
|
||||
src="${src_url}" type="application/pdf"
|
||||
>
|
||||
</object>
|
||||
</div>`);
|
||||
} else if (file_extension === "mp3") {
|
||||
$preview = $(`<div class="img_preview">
|
||||
<audio width="480" height="60" controls>
|
||||
<source src="${frappe.utils.escape_html(frm.doc.file_url)}" type="audio/mpeg">
|
||||
<source src="${src_url}" type="audio/mpeg">
|
||||
${__("Your browser does not support the audio element.")}
|
||||
</audio >
|
||||
</div>`);
|
||||
|
|
@ -90,7 +101,7 @@ frappe.ui.form.on("File", {
|
|||
},
|
||||
|
||||
download: function (frm) {
|
||||
let file_url = frm.doc.file_url;
|
||||
let file_url = frm.doc.file_url + "?fid=" + frm.doc.name;
|
||||
if (frm.doc.file_name) {
|
||||
file_url = file_url.replace(/#/g, "%23");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -890,6 +890,14 @@ def has_permission(doc, ptype=None, user=None, debug=False):
|
|||
|
||||
if user != "Guest" and doc.owner == user:
|
||||
return True
|
||||
if (
|
||||
user != "Guest"
|
||||
and ptype in ["read", "write", "share", "submit"]
|
||||
and frappe.share.get_shared(
|
||||
"File", filters=[["share_name", "=", doc.name]], rights=[ptype], user=user
|
||||
)
|
||||
):
|
||||
return True
|
||||
|
||||
if doc.attached_to_doctype and doc.attached_to_name:
|
||||
attached_to_doctype = doc.attached_to_doctype
|
||||
|
|
|
|||
|
|
@ -641,6 +641,77 @@ class TestAttachment(IntegrationTestCase):
|
|||
self.assertTrue(exists)
|
||||
|
||||
|
||||
class TestCopyAttachmentsFromAmendedFrom(IntegrationTestCase):
|
||||
"""Test that attached_to_field and folder are copied when amending a document."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
from frappe.core.doctype.doctype.test_doctype import new_doctype
|
||||
|
||||
cls.test_doctype = "Test Amendable Attachment"
|
||||
new_doctype(
|
||||
cls.test_doctype,
|
||||
is_submittable=1,
|
||||
fields=[
|
||||
{"label": "Title", "fieldname": "title", "fieldtype": "Data"},
|
||||
{"label": "Attachment", "fieldname": "attachment", "fieldtype": "Attach"},
|
||||
],
|
||||
).insert(ignore_if_duplicate=True)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
frappe.db.rollback()
|
||||
frappe.delete_doc_if_exists("DocType", cls.test_doctype)
|
||||
|
||||
def test_attached_to_field_and_folder_copied_on_amend(self):
|
||||
# Create custom folder
|
||||
custom_folder = frappe.get_doc(
|
||||
{"doctype": "File", "file_name": "Test Amend Folder", "is_folder": 1, "folder": "Home"}
|
||||
).insert()
|
||||
|
||||
# Create original document and attach file with attached_to_field and custom folder
|
||||
doc = frappe.get_doc(doctype=self.test_doctype, title="Original").insert()
|
||||
file = frappe.get_doc(
|
||||
{
|
||||
"doctype": "File",
|
||||
"file_name": "amend_test_attach.txt",
|
||||
"content": "Test Content",
|
||||
"attached_to_doctype": self.test_doctype,
|
||||
"attached_to_name": doc.name,
|
||||
"attached_to_field": "attachment",
|
||||
"folder": custom_folder.name,
|
||||
}
|
||||
).insert()
|
||||
|
||||
doc.attachment = file.file_url
|
||||
doc.save()
|
||||
|
||||
# Submit and cancel
|
||||
doc.submit()
|
||||
doc.cancel()
|
||||
|
||||
# Amend document
|
||||
amended_doc = frappe.copy_doc(doc)
|
||||
amended_doc.docstatus = 0
|
||||
amended_doc.amended_from = doc.name
|
||||
amended_doc.save()
|
||||
|
||||
# Verify copied file has attached_to_field and folder from original
|
||||
copied_files = frappe.get_all(
|
||||
"File",
|
||||
filters={
|
||||
"attached_to_doctype": self.test_doctype,
|
||||
"attached_to_name": amended_doc.name,
|
||||
"file_name": "amend_test_attach.txt",
|
||||
},
|
||||
fields=["name", "attached_to_field", "folder"],
|
||||
)
|
||||
self.assertEqual(len(copied_files), 1, "Exactly one file should be copied to amended doc")
|
||||
self.assertEqual(copied_files[0].attached_to_field, "attachment")
|
||||
self.assertEqual(copied_files[0].folder, custom_folder.name)
|
||||
|
||||
|
||||
class TestAttachmentsAccess(IntegrationTestCase):
|
||||
def setUp(self) -> None:
|
||||
frappe.db.delete("File", {"is_folder": 0})
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ def has_unseen_error_log():
|
|||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_log_doctypes(doctype, txt, searchfield, start, page_len, filters):
|
||||
def get_log_doctypes(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: list):
|
||||
filters = filters or []
|
||||
|
||||
filters.extend(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import os
|
|||
from pathlib import Path
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.modules.export_file import delete_folder
|
||||
|
||||
|
|
@ -89,6 +90,10 @@ class ModuleDef(Document):
|
|||
frappe.clear_cache()
|
||||
frappe.setup_module_map()
|
||||
|
||||
def before_rename(self, old, new, merge=False):
|
||||
if not self.custom:
|
||||
frappe.throw(_("Only Custom Modules can be renamed."))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_installed_apps():
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ def update_job_id(prepared_report):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_prepared_report(report_name, filters=None):
|
||||
def make_prepared_report(report_name: str, filters: dict[str, Any] | str | list | None = None):
|
||||
"""run reports in background"""
|
||||
prepared_report = frappe.get_doc(
|
||||
{
|
||||
|
|
@ -212,7 +212,7 @@ def process_filters_for_prepared_report(filters: dict[str, Any] | str) -> str:
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_reports_in_queued_state(report_name, filters):
|
||||
def get_reports_in_queued_state(report_name: str, filters: dict[str, Any] | str | list):
|
||||
return frappe.get_all(
|
||||
"Prepared Report",
|
||||
filters={
|
||||
|
|
@ -252,7 +252,7 @@ def expire_stalled_report():
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_prepared_reports(reports):
|
||||
def delete_prepared_reports(reports: str | list[dict[str, Any]]):
|
||||
reports = frappe.parse_json(reports)
|
||||
for report in reports:
|
||||
prepared_report = frappe.get_doc("Prepared Report", report["name"])
|
||||
|
|
@ -284,7 +284,7 @@ def create_json_gz_file(data, dt, dn, report_name):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_attachment(dn):
|
||||
def download_attachment(dn: str):
|
||||
pr = frappe.get_doc("Prepared Report", dn)
|
||||
if not pr.has_permission("read"):
|
||||
frappe.throw(frappe._("Cannot Download Report due to insufficient permissions"))
|
||||
|
|
@ -330,7 +330,7 @@ def has_permission(doc, user):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def enqueue_json_to_csv_conversion(prepared_report_name):
|
||||
def enqueue_json_to_csv_conversion(prepared_report_name: str):
|
||||
"""Call this to enqueue the conversion in background."""
|
||||
enqueue(method=convert_json_to_csv, queue="long", prepared_report_name=prepared_report_name)
|
||||
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ def serialize_request(request):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_indexes(indexes):
|
||||
def add_indexes(indexes: str):
|
||||
frappe.only_for("Administrator")
|
||||
indexes = json.loads(indexes)
|
||||
|
||||
|
|
|
|||
|
|
@ -93,15 +93,24 @@ class Report(Document):
|
|||
doc.prepared_report = 0
|
||||
|
||||
def on_trash(self):
|
||||
if (
|
||||
self.is_standard == "Yes"
|
||||
and not cint(getattr(frappe.local.conf, "developer_mode", 0))
|
||||
and not frappe.flags.in_migrate
|
||||
and not frappe.flags.in_patch
|
||||
):
|
||||
frappe.throw(_("You are not allowed to delete Standard Report"))
|
||||
if self.is_standard == "Yes":
|
||||
if (
|
||||
not cint(getattr(frappe.local.conf, "developer_mode", 0))
|
||||
and not frappe.flags.in_migrate
|
||||
and not frappe.flags.in_patch
|
||||
):
|
||||
frappe.throw(_("You are not allowed to delete Standard Report"))
|
||||
|
||||
if frappe.conf.developer_mode and not frappe.flags.in_test:
|
||||
frappe.db.after_commit(self.delete_report_folder)
|
||||
|
||||
delete_custom_role("report", self.name)
|
||||
|
||||
def delete_report_folder(self):
|
||||
from frappe.modules.export_file import delete_folder
|
||||
|
||||
delete_folder(self.module, "Report", self.name)
|
||||
|
||||
def get_permission_log_options(self, event=None):
|
||||
return {"fields": ["roles"]}
|
||||
|
||||
|
|
|
|||
|
|
@ -89,6 +89,12 @@ class Role(Document):
|
|||
|
||||
def get_info_based_on_role(role, field="email", ignore_permissions=False):
|
||||
"""Get information of all users that have been assigned this role"""
|
||||
# Administrator is a superuser account, not a typical role with assigned users
|
||||
# so we resolve it directly to the Administrator user
|
||||
if role == "Administrator":
|
||||
user = frappe.db.get_value("User", "Administrator", field)
|
||||
return [user] if user else []
|
||||
|
||||
users = frappe.get_list(
|
||||
"Has Role",
|
||||
filters={"role": role, "parenttype": "User"},
|
||||
|
|
@ -120,7 +126,9 @@ def get_users(role):
|
|||
# searches for active employees
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def role_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
def role_query(
|
||||
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: list | dict | str
|
||||
):
|
||||
return frappe.get_all(
|
||||
"Role",
|
||||
limit_start=start,
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@ def get_all_queued_jobs():
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def stop_job(job_id):
|
||||
def stop_job(job_id: str):
|
||||
frappe.get_doc("RQ Job", job_id).stop_job()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -211,16 +211,21 @@ class ServerScript(Document):
|
|||
|
||||
safe_exec(self.script, script_filename=self.name)
|
||||
|
||||
def get_permission_query_conditions(self, user: str) -> list[str]:
|
||||
def get_permission_query_conditions(self, user: str, active_child_tables=None) -> list[str]:
|
||||
"""Specific to Permission Query Server Scripts.
|
||||
|
||||
Args:
|
||||
user (str): Take user email to execute script and return list of conditions.
|
||||
active_child_tables (list, optional): A list of child table names involved in the current SQL query.
|
||||
|
||||
Return:
|
||||
list: Return list of conditions defined by rules in self.script.
|
||||
"""
|
||||
locals = {"user": user, "conditions": ""}
|
||||
locals = {
|
||||
"user": user,
|
||||
"conditions": "",
|
||||
"active_child_tables": active_child_tables or [],
|
||||
}
|
||||
safe_exec(self.script, None, locals, script_filename=self.name)
|
||||
if locals["conditions"]:
|
||||
return locals["conditions"]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
|
@ -43,7 +44,7 @@ def get_session_default_values():
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_session_default_values(default_values):
|
||||
def set_session_default_values(default_values: str | dict[str, Any]):
|
||||
default_values = frappe.parse_json(default_values)
|
||||
for entry in default_values:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ def validate_receiver_nos(receiver_list):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_contact_number(contact_name, ref_doctype, ref_name):
|
||||
def get_contact_number(contact_name: str, ref_doctype: str, ref_name: str):
|
||||
"Return mobile number of the given contact."
|
||||
number = frappe.db.sql(
|
||||
"""select mobile_no, phone from tabContact
|
||||
|
|
@ -62,7 +62,7 @@ def get_contact_number(contact_name, ref_doctype, ref_name):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def send_sms(receiver_list, msg, sender_name="", success_msg=True):
|
||||
def send_sms(receiver_list: str | list[str], msg: str, sender_name: str = "", success_msg: bool = True):
|
||||
send_sms_hook_methods = frappe.get_hooks("send_sms")
|
||||
if send_sms_hook_methods:
|
||||
return frappe.get_attr(send_sms_hook_methods[-1])(receiver_list, msg, sender_name, success_msg)
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ def queue_submission(doc: Document, action: str, alert: bool = True):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_latest_submissions(doctype, docname):
|
||||
def get_latest_submissions(doctype: str, docname: str | int):
|
||||
# NOTE: not used creation as orderby intentianlly as we have used update_modified=False everywhere
|
||||
# hence assuming modified will be equal to creation for submission queue documents
|
||||
|
||||
|
|
|
|||
|
|
@ -248,7 +248,6 @@
|
|||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Note: Multiple sessions will be allowed in case of mobile device",
|
||||
"fieldname": "deny_multiple_sessions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow only one session per user"
|
||||
|
|
@ -790,7 +789,7 @@
|
|||
"icon": "fa fa-cog",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-02 18:13:45.430712",
|
||||
"modified": "2026-02-24 14:27:04.763075",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "System Settings",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@
|
|||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Translation", {
|
||||
refresh: function () {
|
||||
//
|
||||
refresh: function (frm) {
|
||||
frm.set_intro(
|
||||
__(
|
||||
"Translations can be viewed by guests, avoid storing private details in translations."
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -201,18 +201,19 @@ frappe.ui.form.on("User", {
|
|||
},
|
||||
],
|
||||
primary_action: (values) => {
|
||||
d.hide();
|
||||
if (values.new_password !== values.confirm_password) {
|
||||
frappe.throw(__("Passwords do not match!"));
|
||||
}
|
||||
frappe.call(
|
||||
"frappe.integrations.doctype.ldap_settings.ldap_settings.reset_password",
|
||||
{
|
||||
user: frm.doc.email,
|
||||
password: values.new_password,
|
||||
logout: values.logout_sessions,
|
||||
}
|
||||
);
|
||||
return frappe
|
||||
.call(
|
||||
"frappe.integrations.doctype.ldap_settings.ldap_settings.reset_password",
|
||||
{
|
||||
user: frm.doc.email,
|
||||
password: values.new_password,
|
||||
logout: values.logout_sessions,
|
||||
}
|
||||
)
|
||||
.then(() => d.hide());
|
||||
},
|
||||
});
|
||||
d.show();
|
||||
|
|
@ -440,9 +441,6 @@ frappe.ui.form.on("User Role Profile", {
|
|||
frm.roles_editor.show();
|
||||
}
|
||||
});
|
||||
if (frm.roles_editor) {
|
||||
$(".deselect-all, .select-all").prop("disabled", true);
|
||||
}
|
||||
}
|
||||
},
|
||||
role_profiles_remove: function (frm) {
|
||||
|
|
@ -450,7 +448,6 @@ frappe.ui.form.on("User Role Profile", {
|
|||
if (frm.roles_editor) {
|
||||
frm.roles_editor.disable = 0;
|
||||
frm.roles_editor.show();
|
||||
$(".deselect-all, .select-all").prop("disabled", false);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -49,13 +49,12 @@
|
|||
"mute_sounds",
|
||||
"desk_theme",
|
||||
"code_editor_type",
|
||||
"banner_image",
|
||||
"navigation_settings_section",
|
||||
"search_bar",
|
||||
"notifications",
|
||||
"list_settings_section",
|
||||
"list_sidebar",
|
||||
"bulk_actions",
|
||||
"list_sidebar",
|
||||
"view_switcher",
|
||||
"form_settings_section",
|
||||
"form_sidebar",
|
||||
|
|
@ -298,11 +297,6 @@
|
|||
"label": "Location",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "banner_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Banner Image"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_22",
|
||||
"fieldtype": "Column Break"
|
||||
|
|
@ -604,7 +598,7 @@
|
|||
"unique": 1
|
||||
},
|
||||
{
|
||||
"description": "<a href=\"https://docs.frappe.io/framework/user/en/api/rest#1-token-based-authentication\" target=\"_blank\">\n Click here to learn about token-based authentication\n</a>",
|
||||
"description": "<a href=\"https://docs.frappe.io/framework/user/en/api/rest#1-token-based-authentication\" target=\"_blank\" rel=\"noopener noreferrer\">\n Click here to learn about token-based authentication\n</a>",
|
||||
"fieldname": "generate_keys",
|
||||
"fieldtype": "Button",
|
||||
"label": "Generate Keys",
|
||||
|
|
@ -777,13 +771,13 @@
|
|||
"default": "1",
|
||||
"fieldname": "search_bar",
|
||||
"fieldtype": "Check",
|
||||
"label": "Search Bar"
|
||||
"label": "Show search bar"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "notifications",
|
||||
"fieldtype": "Check",
|
||||
"label": "Notifications"
|
||||
"label": "Allow notifications"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
|
|
@ -795,19 +789,20 @@
|
|||
"default": "1",
|
||||
"fieldname": "list_sidebar",
|
||||
"fieldtype": "Check",
|
||||
"label": "Sidebar"
|
||||
"label": "Show sidebar"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "bulk_actions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bulk Actions"
|
||||
"label": "Allow bulk actions",
|
||||
"permlevel": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "view_switcher",
|
||||
"fieldtype": "Check",
|
||||
"label": "View Switcher"
|
||||
"label": "Show view switcher"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
|
|
@ -819,25 +814,25 @@
|
|||
"default": "1",
|
||||
"fieldname": "form_sidebar",
|
||||
"fieldtype": "Check",
|
||||
"label": "Sidebar"
|
||||
"label": "Show sidebar"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "timeline",
|
||||
"fieldtype": "Check",
|
||||
"label": "Timeline"
|
||||
"label": "Show timeline"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "dashboard",
|
||||
"fieldtype": "Check",
|
||||
"label": "Dashboard"
|
||||
"label": "Show dashboard"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_absolute_datetime_in_timeline",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Absolute Datetime in Timeline"
|
||||
"label": "Show absolute datetime in timeline"
|
||||
},
|
||||
{
|
||||
"fieldname": "sessions_tab",
|
||||
|
|
@ -856,7 +851,7 @@
|
|||
"default": "0",
|
||||
"fieldname": "form_navigation_buttons",
|
||||
"fieldtype": "Check",
|
||||
"label": "Navigation Buttons"
|
||||
"label": "Show navigation buttons"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-user",
|
||||
|
|
@ -910,7 +905,7 @@
|
|||
}
|
||||
],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2026-01-12 16:04:21.542524",
|
||||
"modified": "2026-02-22 13:44:36.317890",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "User",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
from collections.abc import Iterable
|
||||
from datetime import timedelta
|
||||
from functools import cached_property
|
||||
from typing import Any
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
|
|
@ -74,7 +75,6 @@ class User(Document):
|
|||
allowed_in_mentions: DF.Check
|
||||
api_key: DF.Data | None
|
||||
api_secret: DF.Password | None
|
||||
banner_image: DF.AttachImage | None
|
||||
bio: DF.SmallText | None
|
||||
birth_date: DF.Date | None
|
||||
block_modules: DF.Table[BlockModule]
|
||||
|
|
@ -393,6 +393,9 @@ class User(Document):
|
|||
"""Set as System User if any of the given roles has desk_access"""
|
||||
self.user_type = "System User" if self.has_desk_access() else "Website User"
|
||||
|
||||
if self.has_value_changed("user_type"):
|
||||
clear_sessions(user=self.name, force=True)
|
||||
|
||||
def set_roles_and_modules_based_on_user_type(self):
|
||||
user_type_doc = frappe.get_cached_doc("User Type", self.user_type)
|
||||
if user_type_doc.role:
|
||||
|
|
@ -427,15 +430,6 @@ class User(Document):
|
|||
self.doctype, self.name, self.name, write=1, share=1, flags={"ignore_share_permission": True}
|
||||
)
|
||||
|
||||
def validate_share(self, docshare):
|
||||
pass
|
||||
# if docshare.user == self.name:
|
||||
# if self.user_type=="System User":
|
||||
# if docshare.share != 1:
|
||||
# frappe.throw(_("Sorry! User should have complete access to their own record."))
|
||||
# else:
|
||||
# frappe.throw(_("Sorry! Sharing with Website User is prohibited."))
|
||||
|
||||
def send_password_notification(self, new_password):
|
||||
try:
|
||||
if self.flags.in_insert:
|
||||
|
|
@ -899,13 +893,7 @@ def get_all_roles():
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_roles(arg=None):
|
||||
"""get roles for a user"""
|
||||
return frappe.get_roles(frappe.form_dict.get("uid", frappe.session.user))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_perm_info(role):
|
||||
def get_perm_info(role: str):
|
||||
"""get permission info"""
|
||||
from frappe.permissions import get_all_perms
|
||||
|
||||
|
|
@ -966,7 +954,9 @@ def update_password(
|
|||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def test_password_strength(new_password: str, key=None, old_password=None, user_data: tuple | None = None):
|
||||
def test_password_strength(
|
||||
new_password: str, key: str | None = None, old_password: str | None = None, user_data: tuple | None = None
|
||||
):
|
||||
from frappe.utils.password_strength import test_password_strength as _test_password_strength
|
||||
|
||||
if key is not None or old_password is not None:
|
||||
|
|
@ -1008,7 +998,7 @@ def has_email_account(email: str):
|
|||
|
||||
|
||||
@frappe.whitelist(allow_guest=False)
|
||||
def get_email_awaiting(user):
|
||||
def get_email_awaiting(user: str):
|
||||
return frappe.get_all(
|
||||
"User Email",
|
||||
fields=["email_account", "email_id"],
|
||||
|
|
@ -1069,7 +1059,7 @@ def reset_user_data(user):
|
|||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
def verify_password(password):
|
||||
def verify_password(password: str):
|
||||
frappe.local.login_manager.check_password(frappe.session.user, password)
|
||||
|
||||
|
||||
|
|
@ -1151,7 +1141,7 @@ def reset_password(user: str) -> str:
|
|||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def user_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
def user_query(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict[str, Any]):
|
||||
doctype = "User"
|
||||
|
||||
list_filters = {
|
||||
|
|
@ -1426,7 +1416,7 @@ def generate_keys(user: str):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def switch_theme(theme):
|
||||
def switch_theme(theme: str):
|
||||
if theme in ["Dark", "Light", "Automatic"]:
|
||||
frappe.db.set_value("User", frappe.session.user, "desk_theme", theme)
|
||||
|
||||
|
|
@ -1463,7 +1453,8 @@ def impersonate(user: str, reason: str):
|
|||
notification.set("type", "Alert")
|
||||
notification.insert(ignore_permissions=True)
|
||||
# notify user via email too
|
||||
if not frappe.conf.get("developer_mode"): # bypass for testing locally
|
||||
outgoing_email_exists = frappe.db.exists("Email Account", {"default_outgoing": 1, "awaiting_password": 0})
|
||||
if outgoing_email_exists:
|
||||
user_email = frappe.db.get_value("User", user, "email")
|
||||
email_message = _(
|
||||
"User {0} has started an impersonation session as you. <br><br><b>Reason provided:</b> {1}"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
|
@ -85,7 +86,7 @@ def send_user_permissions(bootinfo):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_user_permissions(user=None):
|
||||
def get_user_permissions(user: str | None = None):
|
||||
"""Get all users permissions for the user as a dict of doctype"""
|
||||
# if this is called from client-side,
|
||||
# user can access only his/her user permissions
|
||||
|
|
@ -160,7 +161,9 @@ def user_permission_exists(user, allow, for_value, applicable_for=None):
|
|||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_applicable_for_doctype_list(doctype, txt, searchfield, start, page_len, filters):
|
||||
def get_applicable_for_doctype_list(
|
||||
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict[str, Any]
|
||||
):
|
||||
actual_doctype = filters.get("doctype")
|
||||
linked_doctypes_map = get_linked_doctypes(actual_doctype, True)
|
||||
|
||||
|
|
@ -192,7 +195,7 @@ def get_permitted_documents(doctype):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def check_applicable_doc_perm(user, doctype, docname):
|
||||
def check_applicable_doc_perm(user: str, doctype: str, docname: str | int):
|
||||
frappe.only_for("System Manager")
|
||||
applicable = []
|
||||
doc_exists = frappe.get_all(
|
||||
|
|
@ -224,7 +227,7 @@ def check_applicable_doc_perm(user, doctype, docname):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def clear_user_permissions(user, for_doctype):
|
||||
def clear_user_permissions(user: str, for_doctype: str):
|
||||
frappe.only_for("System Manager")
|
||||
total = frappe.db.count("User Permission", {"user": user, "allow": for_doctype})
|
||||
|
||||
|
|
@ -242,7 +245,7 @@ def clear_user_permissions(user, for_doctype):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_user_permissions(data):
|
||||
def add_user_permissions(data: str | dict[str, Any]):
|
||||
"""Add and update the user permissions"""
|
||||
frappe.only_for("System Manager")
|
||||
if isinstance(data, str):
|
||||
|
|
|
|||
|
|
@ -84,13 +84,14 @@ class UserType(Document):
|
|||
title=_("Permission Error"),
|
||||
)
|
||||
|
||||
if not limit:
|
||||
frappe.throw(
|
||||
if limit is None:
|
||||
frappe.msgprint(
|
||||
_("The limit has not set for the user type {0} in the site config file.").format(
|
||||
frappe.bold(self.name)
|
||||
),
|
||||
title=_("Set Limit"),
|
||||
)
|
||||
return
|
||||
|
||||
if self.user_doctypes and len(self.user_doctypes) > limit:
|
||||
frappe.throw(
|
||||
|
|
@ -218,7 +219,9 @@ def get_non_standard_user_types():
|
|||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_user_linked_doctypes(doctype, txt, searchfield, start, page_len, filters):
|
||||
def get_user_linked_doctypes(
|
||||
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict | list | str
|
||||
):
|
||||
modules = [d.get("module_name") for d in get_modules_from_app("frappe")]
|
||||
|
||||
filters = [
|
||||
|
|
@ -254,7 +257,7 @@ def get_user_linked_doctypes(doctype, txt, searchfield, start, page_len, filters
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_user_id(parent):
|
||||
def get_user_id(parent: str):
|
||||
data = (
|
||||
frappe.get_all(
|
||||
"DocField",
|
||||
|
|
|
|||
|
|
@ -113,13 +113,20 @@ def get_permissions(doctype: str | None = None, role: str | None = None):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add(parent, role, permlevel):
|
||||
def add(parent: str, role: str, permlevel: int):
|
||||
frappe.only_for("System Manager")
|
||||
add_permission(parent, role, permlevel)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update(doctype: str, role: str, permlevel: int, ptype: str, value=None, if_owner=0) -> str | None:
|
||||
def update(
|
||||
doctype: str,
|
||||
role: str,
|
||||
permlevel: int,
|
||||
ptype: str,
|
||||
value: str | int | None = None,
|
||||
if_owner: str | int = 0,
|
||||
) -> str | None:
|
||||
"""Update role permission params.
|
||||
|
||||
Args:
|
||||
|
|
@ -152,7 +159,7 @@ def update(doctype: str, role: str, permlevel: int, ptype: str, value=None, if_o
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def remove(doctype, role, permlevel, if_owner=0):
|
||||
def remove(doctype: str, role: str, permlevel: int, if_owner: str | int = 0):
|
||||
frappe.only_for("System Manager")
|
||||
setup_custom_perms(doctype)
|
||||
|
||||
|
|
@ -169,20 +176,20 @@ def remove(doctype, role, permlevel, if_owner=0):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def reset(doctype):
|
||||
def reset(doctype: str):
|
||||
frappe.only_for("System Manager")
|
||||
reset_perms(doctype)
|
||||
clear_permissions_cache(doctype)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_users_with_role(role):
|
||||
def get_users_with_role(role: str):
|
||||
frappe.only_for("System Manager")
|
||||
return _get_user_with_role(role)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_standard_permissions(doctype):
|
||||
def get_standard_permissions(doctype: str):
|
||||
frappe.only_for("System Manager")
|
||||
meta = frappe.get_meta(doctype)
|
||||
if meta.custom:
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ frappe.query_reports["Database Storage Usage By Tables"] = {
|
|||
size: "small",
|
||||
primary_action_label: "Optimize",
|
||||
primary_action(values) {
|
||||
frappe.call({
|
||||
return frappe.call({
|
||||
method: "frappe.core.report.database_storage_usage_by_tables.database_storage_usage_by_tables.optimize_doctype",
|
||||
args: {
|
||||
doctype_name: values.doctype_name,
|
||||
|
|
@ -38,9 +38,9 @@ frappe.query_reports["Database Storage Usage By Tables"] = {
|
|||
)
|
||||
);
|
||||
}
|
||||
d.hide();
|
||||
},
|
||||
});
|
||||
d.hide();
|
||||
},
|
||||
});
|
||||
d.show();
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ def execute(filters=None):
|
|||
round((data_length / 1024 / 1024), 2) as data_size,
|
||||
round((index_length / 1024 / 1024), 2) as index_size
|
||||
FROM information_schema.TABLES
|
||||
WHERE table_schema = DATABASE()
|
||||
ORDER BY (data_length + index_length) DESC;
|
||||
""",
|
||||
"postgres": """
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
from typing import Any
|
||||
|
||||
import frappe
|
||||
import frappe.utils.user
|
||||
from frappe.model import data_fieldtypes
|
||||
|
|
@ -44,7 +46,9 @@ def get_columns_and_fields(doctype):
|
|||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def query_doctypes(doctype, txt, searchfield, start, page_len, filters):
|
||||
def query_doctypes(
|
||||
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict[str, Any]
|
||||
):
|
||||
user = filters.get("user")
|
||||
user_perms = frappe.utils.user.UserPermissions(user)
|
||||
user_perms.build_permissions()
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ class CustomField(Document):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_fields_label(doctype=None):
|
||||
def get_fields_label(doctype: str | None = None):
|
||||
meta = frappe.get_meta(doctype)
|
||||
|
||||
if doctype in core_doctypes_list:
|
||||
|
|
|
|||
|
|
@ -710,7 +710,7 @@ def is_standard_or_system_generated_field(df):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_link_filters_from_doc_without_customisations(doctype, fieldname):
|
||||
def get_link_filters_from_doc_without_customisations(doctype: str, fieldname: str):
|
||||
"""Get the filters of a link field from a doc without customisations
|
||||
In backend the customisations are not applied.
|
||||
Customisations are applied in the client side.
|
||||
|
|
|
|||
|
|
@ -634,6 +634,9 @@ class Database:
|
|||
from frappe.model.utils import is_single_doctype
|
||||
|
||||
out = None
|
||||
if isinstance(fieldname, list):
|
||||
fieldname = tuple(fieldname)
|
||||
|
||||
if cache and isinstance(filters, str) and fieldname in self.value_cache[doctype][filters]:
|
||||
return self.value_cache[doctype][filters][fieldname]
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from pymysql.constants.ER import DUP_ENTRY
|
|||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.database.schema import DBTable
|
||||
from frappe.database.schema import DbColumn, DBTable
|
||||
from frappe.utils.defaults import get_not_null_defaults
|
||||
|
||||
|
||||
|
|
@ -96,6 +96,37 @@ class MariaDBTable(DBTable):
|
|||
):
|
||||
add_index_query.append("ADD INDEX `modified`(`modified`)")
|
||||
|
||||
# logic to drop unique constraint for fields deleted from a doctype
|
||||
meta_columns = set(self.columns.keys())
|
||||
db_columns = set(self.current_columns.keys())
|
||||
|
||||
for col in db_columns:
|
||||
if (
|
||||
col not in meta_columns
|
||||
and col not in frappe.db.DEFAULT_COLUMNS
|
||||
and col not in frappe.db.OPTIONAL_COLUMNS
|
||||
):
|
||||
has_unique = frappe.db.get_column_index(self.table_name, col, unique=True)
|
||||
|
||||
if not has_unique:
|
||||
continue
|
||||
|
||||
current_col = self.current_columns.get(col)
|
||||
|
||||
deleted_col = DbColumn(
|
||||
table=self,
|
||||
fieldname=current_col.name,
|
||||
fieldtype=current_col.type,
|
||||
length=None,
|
||||
default=None,
|
||||
set_index=current_col.index,
|
||||
options=None,
|
||||
unique=False,
|
||||
precision=None,
|
||||
not_nullable=current_col.not_nullable,
|
||||
)
|
||||
self.drop_unique.append(deleted_col)
|
||||
|
||||
drop_index_query = []
|
||||
|
||||
for col in {*self.drop_index, *self.drop_unique}:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import frappe
|
||||
from frappe import _
|
||||
from frappe.database.schema import DBTable, get_definition
|
||||
from frappe.database.schema import DbColumn, DBTable, get_definition
|
||||
from frappe.utils import cint, flt
|
||||
from frappe.utils.defaults import get_not_null_defaults
|
||||
|
||||
|
|
@ -131,6 +131,50 @@ class PostgresTable(DBTable):
|
|||
index_name=col.fieldname, table_name=self.table_name, field=col.fieldname
|
||||
)
|
||||
|
||||
# logic to drop unique constraint for fields deleted from a doctype
|
||||
meta_columns = set(self.columns.keys())
|
||||
db_columns = set(self.current_columns.keys())
|
||||
|
||||
for col in db_columns:
|
||||
if (
|
||||
col not in meta_columns
|
||||
and col not in frappe.db.DEFAULT_COLUMNS
|
||||
and col not in frappe.db.OPTIONAL_COLUMNS
|
||||
):
|
||||
has_unique_index = frappe.db.sql(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM pg_indexes
|
||||
WHERE tablename = %s
|
||||
AND indexname IN (%s, %s)
|
||||
LIMIT 1
|
||||
""",
|
||||
(
|
||||
self.table_name,
|
||||
f"{self.table_name}_{col}_key",
|
||||
f"unique_{col}",
|
||||
),
|
||||
)
|
||||
|
||||
if not has_unique_index:
|
||||
continue
|
||||
|
||||
current_col = self.current_columns.get(col)
|
||||
|
||||
deleted_col = DbColumn(
|
||||
table=self,
|
||||
fieldname=current_col.name,
|
||||
fieldtype=current_col.type,
|
||||
length=None,
|
||||
default=None,
|
||||
set_index=current_col.index,
|
||||
options=None,
|
||||
unique=False,
|
||||
precision=None,
|
||||
not_nullable=current_col.not_nullable,
|
||||
)
|
||||
self.drop_unique.append(deleted_col)
|
||||
|
||||
drop_contraint_query = ""
|
||||
for col in self.drop_index:
|
||||
# primary key
|
||||
|
|
@ -141,8 +185,35 @@ class PostgresTable(DBTable):
|
|||
for col in self.drop_unique:
|
||||
# primary key
|
||||
if col.fieldname != "name":
|
||||
# if index key exists
|
||||
drop_contraint_query += f'DROP INDEX IF EXISTS "unique_{col.fieldname}" ;'
|
||||
# drop unique constraint first if exists which automatically drops the underlying index also
|
||||
unique_constraint_exists = frappe.db.sql(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = %s
|
||||
""",
|
||||
(f"{self.table_name}_{col.fieldname}_key",),
|
||||
)
|
||||
|
||||
if unique_constraint_exists:
|
||||
drop_contraint_query += f'ALTER TABLE "{self.table_name}" DROP CONSTRAINT IF EXISTS "{self.table_name}_{col.fieldname}_key" ;'
|
||||
|
||||
# drop the unique index backed by no constraint directly
|
||||
unique_index_exists = frappe.db.sql(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM pg_indexes
|
||||
WHERE tablename = %s
|
||||
AND indexname = %s
|
||||
""",
|
||||
(
|
||||
self.table_name,
|
||||
f"unique_{col.fieldname}",
|
||||
),
|
||||
)
|
||||
|
||||
if unique_index_exists:
|
||||
drop_contraint_query += f'DROP INDEX IF EXISTS "unique_{col.fieldname}" ;'
|
||||
|
||||
change_nullability = []
|
||||
for col in self.change_nullability:
|
||||
|
|
|
|||
|
|
@ -262,6 +262,8 @@ class Engine:
|
|||
self.field_aliases = set()
|
||||
self.db_query_compat = db_query_compat
|
||||
self.permitted_fields_cache = {} # Cache for get_permitted_fields results
|
||||
self.is_aggregate_query = False
|
||||
self._grouped_queries = set()
|
||||
|
||||
if isinstance(table, Table):
|
||||
self.table = table
|
||||
|
|
@ -308,19 +310,24 @@ class Engine:
|
|||
if for_update:
|
||||
self.query = self.query.for_update(skip_locked=skip_locked, nowait=not wait)
|
||||
|
||||
if any(isinstance(f, functions.AggregateFunction) for f in getattr(self, "fields", [])):
|
||||
# check if any field in select is aggregated (done to prevent breaking queries in postgres due to order by rule)
|
||||
self.is_aggregate_query = True
|
||||
|
||||
if group_by:
|
||||
self.is_aggregate_query = True # for postgres (group by used with order by)
|
||||
self.apply_group_by(group_by)
|
||||
|
||||
if order_by:
|
||||
if not (
|
||||
self.is_postgres and is_select and (distinct or group_by)
|
||||
self.is_postgres and is_select and distinct
|
||||
): # ignore in Postgres since order by fields need to appear in select distinct
|
||||
self.apply_order_by(order_by)
|
||||
else:
|
||||
warnings.warn(
|
||||
(
|
||||
"ORDER BY fields have been ignored because PostgreSQL requires them to "
|
||||
"appear in the SELECT list when using DISTINCT or GROUP BY."
|
||||
"appear in the SELECT list when using with DISTINCT"
|
||||
),
|
||||
UserWarning,
|
||||
stacklevel=2,
|
||||
|
|
@ -340,7 +347,7 @@ class Engine:
|
|||
|
||||
# Track field aliases for use in group_by/order_by
|
||||
for field in self.fields:
|
||||
if isinstance(field, Field | DynamicTableField) and field.alias:
|
||||
if isinstance(field, Field | DynamicTableField | AggregateFunction) and field.alias:
|
||||
self.field_aliases.add(field.alias)
|
||||
|
||||
if self.apply_permissions:
|
||||
|
|
@ -812,7 +819,7 @@ class Engine:
|
|||
if parsed := self._parse_backtick_field_notation(field):
|
||||
table_name, field_name = parsed
|
||||
|
||||
self._check_field_permission(table_name, field_name)
|
||||
self.check_filter_field_permission(table_name, field_name)
|
||||
|
||||
# Return query builder field reference
|
||||
return frappe.qb.DocType(table_name)[field_name]
|
||||
|
|
@ -835,7 +842,7 @@ class Engine:
|
|||
parent_doctype_for_perm = (
|
||||
dynamic_field.parent_doctype if isinstance(dynamic_field, ChildTableField) else None
|
||||
)
|
||||
self._check_field_permission(target_doctype, target_fieldname, parent_doctype_for_perm)
|
||||
self.check_filter_field_permission(target_doctype, target_fieldname, parent_doctype_for_perm)
|
||||
|
||||
self.query = dynamic_field.apply_join(self.query, engine=self)
|
||||
# Return the pypika Field object associated with the dynamic field
|
||||
|
|
@ -879,7 +886,9 @@ class Engine:
|
|||
|
||||
# If it's not a child table, check permissions
|
||||
if not parent_fieldname:
|
||||
self._check_field_permission(target_doctype, target_fieldname, parent_doctype_for_perm)
|
||||
self.check_filter_field_permission(
|
||||
target_doctype, target_fieldname, parent_doctype_for_perm
|
||||
)
|
||||
return frappe.qb.DocType(target_doctype)[target_fieldname]
|
||||
|
||||
# Create a ChildTableField instance to handle join and field access
|
||||
|
|
@ -893,7 +902,7 @@ class Engine:
|
|||
|
||||
# For permission check, the parent is the main doctype
|
||||
parent_doctype_for_perm = self.doctype
|
||||
self._check_field_permission(target_doctype, target_fieldname, parent_doctype_for_perm)
|
||||
self.check_filter_field_permission(target_doctype, target_fieldname, parent_doctype_for_perm)
|
||||
|
||||
# Delegate join logic
|
||||
self.query = child_field_handler.apply_join(self.query, engine=self)
|
||||
|
|
@ -933,18 +942,32 @@ class Engine:
|
|||
parent_fieldname=df.fieldname,
|
||||
)
|
||||
parent_doctype_for_perm = self.doctype
|
||||
self._check_field_permission(
|
||||
self.check_filter_field_permission(
|
||||
df.options, target_fieldname, parent_doctype_for_perm
|
||||
)
|
||||
self.query = child_field_handler.apply_join(self.query, engine=self)
|
||||
return child_field_handler.field
|
||||
|
||||
self._check_field_permission(target_doctype, target_fieldname, parent_doctype_for_perm)
|
||||
self.check_filter_field_permission(target_doctype, target_fieldname, parent_doctype_for_perm)
|
||||
# Convert string field name to pypika Field object for the specified/current doctype
|
||||
return frappe.qb.DocType(target_doctype)[target_fieldname]
|
||||
|
||||
def _check_field_permission(self, doctype: str, fieldname: str, parent_doctype: str | None = None):
|
||||
"""Check if the user has permission to access the given field"""
|
||||
def check_select_field_permission(self, doctype: str, fieldname: str, parent_doctype: str | None = None):
|
||||
"""Check if the user has permission to select the given field."""
|
||||
self._check_field_permission(doctype, fieldname, parent_doctype, for_filtering=False)
|
||||
|
||||
def check_filter_field_permission(self, doctype: str, fieldname: str, parent_doctype: str | None = None):
|
||||
"""Check if the user has permission to filter/order/group by the given field.
|
||||
|
||||
It allows all permlevel 0 fields for users with select permission,
|
||||
and all permitted fields for users with read permission.
|
||||
"""
|
||||
self._check_field_permission(doctype, fieldname, parent_doctype, for_filtering=True)
|
||||
|
||||
def _check_field_permission(
|
||||
self, doctype: str, fieldname: str, parent_doctype: str | None = None, for_filtering: bool = False
|
||||
):
|
||||
"""Check if the user has permission to access the given field."""
|
||||
if not self.apply_permissions:
|
||||
return
|
||||
|
||||
|
|
@ -966,7 +989,10 @@ class Engine:
|
|||
frappe.PermissionError,
|
||||
)
|
||||
|
||||
permitted_fields = self._get_cached_permitted_fields(doctype, parent_doctype, permission_type)
|
||||
permission_source = (
|
||||
self._get_filterable_fields if for_filtering else self._get_cached_permitted_fields
|
||||
)
|
||||
permitted_fields = permission_source(doctype, parent_doctype, permission_type)
|
||||
|
||||
if fieldname not in permitted_fields:
|
||||
frappe.throw(
|
||||
|
|
@ -992,6 +1018,42 @@ class Engine:
|
|||
)
|
||||
return self.permitted_fields_cache[cache_key]
|
||||
|
||||
def _get_filterable_fields(
|
||||
self, doctype: str, parenttype: str | None = None, permission_type: str | None = None
|
||||
) -> set:
|
||||
"""Get fields that can be used in filters/order by/group by.
|
||||
|
||||
For users with only select permission on parent doctypes, this returns
|
||||
all permlevel 0 fields (not just search fields which are used for selected fields).
|
||||
For users with read permission, returns standard permitted fields.
|
||||
"""
|
||||
if permission_type is None:
|
||||
permission_type = self.get_permission_type(doctype, parenttype)
|
||||
|
||||
if permission_type == "select":
|
||||
meta = frappe.get_meta(doctype)
|
||||
|
||||
# Only allow filtering by all permlevel 0 fields for parent doctypes.
|
||||
if meta.istable:
|
||||
return set()
|
||||
|
||||
# for select permission on parent doctype, allow all permlevel 0 fields in filters
|
||||
cache_key = (doctype, None, "_filterable_select")
|
||||
if cache_key not in self.permitted_fields_cache:
|
||||
if doctype in CORE_DOCTYPES:
|
||||
# core doctypes have no restrictions - return all valid columns
|
||||
self.permitted_fields_cache[cache_key] = set(meta.get_valid_columns())
|
||||
else:
|
||||
permlevel_0_fields = set(meta.default_fields) | OPTIONAL_FIELDS
|
||||
for df in meta.get_fieldnames_with_value(with_field_meta=True, with_virtual_fields=False):
|
||||
if df.permlevel == 0:
|
||||
permlevel_0_fields.add(df.fieldname)
|
||||
self.permitted_fields_cache[cache_key] = permlevel_0_fields
|
||||
return self.permitted_fields_cache[cache_key]
|
||||
else:
|
||||
# for read permission, use standard permitted fields
|
||||
return self._get_cached_permitted_fields(doctype, parenttype, permission_type)
|
||||
|
||||
def parse_string_field(self, field: str):
|
||||
"""
|
||||
Parses a field string into a pypika Field object.
|
||||
|
|
@ -1135,8 +1197,24 @@ class Engine:
|
|||
# Note: Comma handling is done in parse_fields before this method is called
|
||||
return self.parse_string_field(field)
|
||||
|
||||
def _normalize_postgres_order_field(self, field):
|
||||
"""In PostgreSQL order_by fields need to either be in group_by or be aggregated
|
||||
when used with select and group_by"""
|
||||
current_sql = field.get_sql() if hasattr(field, "get_sql") else str(field)
|
||||
if current_sql in self._grouped_queries:
|
||||
return field
|
||||
clean_name = current_sql.strip('"')
|
||||
if clean_name in self.field_aliases:
|
||||
return field
|
||||
if not isinstance(field, functions.AggregateFunction):
|
||||
return functions.Max(field)
|
||||
return field
|
||||
|
||||
def apply_group_by(self, group_by: str | None = None):
|
||||
parsed_group_by_fields = self._validate_group_by(group_by)
|
||||
self._grouped_queries = {
|
||||
f.get_sql() if hasattr(f, "get_sql") else str(f) for f in parsed_group_by_fields
|
||||
}
|
||||
self.query = self.query.groupby(*parsed_group_by_fields)
|
||||
|
||||
def apply_order_by(self, order_by: str | None):
|
||||
|
|
@ -1146,7 +1224,12 @@ class Engine:
|
|||
|
||||
parsed_order_fields = self._validate_order_by(order_by)
|
||||
for order_field, order_direction in parsed_order_fields:
|
||||
self.query = self.query.orderby(order_field, order=order_direction)
|
||||
if self.is_postgres and self.is_aggregate_query:
|
||||
self.query = self.query.orderby(
|
||||
self._normalize_postgres_order_field(order_field), order=order_direction
|
||||
)
|
||||
else:
|
||||
self.query = self.query.orderby(order_field, order=order_direction)
|
||||
|
||||
def _apply_default_order_by(self):
|
||||
"""Apply default ordering based on configured DocType metadata"""
|
||||
|
|
@ -1165,14 +1248,24 @@ class Engine:
|
|||
order_direction = Order.desc if spec_order == "desc" else Order.asc
|
||||
else:
|
||||
order_direction = Order.asc if spec_order == "asc" else Order.desc
|
||||
self.query = self.query.orderby(field, order=order_direction)
|
||||
if self.is_postgres and self.is_aggregate_query:
|
||||
self.query = self.query.orderby(
|
||||
self._normalize_postgres_order_field(field), order=order_direction
|
||||
)
|
||||
else:
|
||||
self.query = self.query.orderby(field, order=order_direction)
|
||||
else:
|
||||
field = self.table[sort_field]
|
||||
if self.db_query_compat:
|
||||
order_direction = Order.desc if sort_order.lower() == "desc" else Order.asc
|
||||
else:
|
||||
order_direction = Order.asc if sort_order.lower() == "asc" else Order.desc
|
||||
self.query = self.query.orderby(field, order=order_direction)
|
||||
if self.is_postgres and self.is_aggregate_query:
|
||||
self.query = self.query.orderby(
|
||||
self._normalize_postgres_order_field(field), order=order_direction
|
||||
)
|
||||
else:
|
||||
self.query = self.query.orderby(field, order=order_direction)
|
||||
|
||||
def _parse_backtick_field_notation(self, field_name: str) -> tuple[str, str] | None:
|
||||
"""
|
||||
|
|
@ -1209,7 +1302,7 @@ class Engine:
|
|||
if "`" in field_name:
|
||||
if parsed := self._parse_backtick_field_notation(field_name):
|
||||
table_name, field_name = parsed
|
||||
self._check_field_permission(table_name, field_name)
|
||||
self.check_filter_field_permission(table_name, field_name)
|
||||
return frappe.qb.DocType(table_name)[field_name]
|
||||
|
||||
# If parsing failed, fall through to error handling below
|
||||
|
|
@ -1223,14 +1316,14 @@ class Engine:
|
|||
if dynamic_field:
|
||||
# Check permissions for dynamic field
|
||||
if isinstance(dynamic_field, ChildTableField):
|
||||
self._check_field_permission(
|
||||
self.check_filter_field_permission(
|
||||
dynamic_field.doctype, dynamic_field.fieldname, dynamic_field.parent_doctype
|
||||
)
|
||||
elif isinstance(dynamic_field, LinkTableField):
|
||||
# Check permission for the link field in parent doctype
|
||||
self._check_field_permission(self.doctype, dynamic_field.link_fieldname)
|
||||
self.check_filter_field_permission(self.doctype, dynamic_field.link_fieldname)
|
||||
# Check permission for the target field in linked doctype
|
||||
self._check_field_permission(dynamic_field.doctype, dynamic_field.fieldname)
|
||||
self.check_filter_field_permission(dynamic_field.doctype, dynamic_field.fieldname)
|
||||
|
||||
# Apply join for the dynamic field
|
||||
self.query = dynamic_field.apply_join(self.query, engine=self)
|
||||
|
|
@ -1246,7 +1339,7 @@ class Engine:
|
|||
)
|
||||
|
||||
# Check permissions for simple field
|
||||
self._check_field_permission(self.doctype, field_name)
|
||||
self.check_filter_field_permission(self.doctype, field_name)
|
||||
|
||||
# Create Field object for simple field
|
||||
return self.table[field_name]
|
||||
|
|
@ -1533,6 +1626,16 @@ class Engine:
|
|||
|
||||
return where_condition
|
||||
|
||||
def get_queried_tables(self) -> list[str]:
|
||||
"""Extract all table names involved in the current query."""
|
||||
tables = []
|
||||
for table in self.query._from:
|
||||
tables.append(table.get_sql())
|
||||
|
||||
for join in self.query._joins:
|
||||
tables.append(join.item.get_sql())
|
||||
return list(set(tables))
|
||||
|
||||
def get_permission_query_conditions(self, doctype: str | None = None) -> list["RawCriterion"]:
|
||||
"""Add permission query conditions from hooks and server scripts"""
|
||||
from frappe.core.doctype.server_script.server_script_utils import get_server_script_map
|
||||
|
|
@ -1546,10 +1649,20 @@ class Engine:
|
|||
if c := frappe.call(frappe.get_attr(method), self.user, doctype=doctype):
|
||||
conditions.append(RawCriterion(f"({c})"))
|
||||
|
||||
active_child_tables = []
|
||||
current_tables = self.get_queried_tables()
|
||||
if len(current_tables) > 1:
|
||||
main_table_name = f"tab{self.doctype}"
|
||||
for table_name in current_tables:
|
||||
if table_name != main_table_name:
|
||||
active_child_tables.append(table_name)
|
||||
|
||||
# Get conditions from server scripts
|
||||
if permission_script_name := get_server_script_map().get("permission_query", {}).get(doctype):
|
||||
script = frappe.get_doc("Server Script", permission_script_name)
|
||||
if condition := script.get_permission_query_conditions(self.user):
|
||||
if condition := script.get_permission_query_conditions(
|
||||
self.user, active_child_tables=active_child_tables
|
||||
):
|
||||
conditions.append(RawCriterion(f"({condition})"))
|
||||
return conditions
|
||||
|
||||
|
|
@ -2287,7 +2400,7 @@ class SQLFunctionParser:
|
|||
elif "`" in arg:
|
||||
if parsed := self.engine._parse_backtick_field_notation(arg):
|
||||
table_name, field_name = parsed
|
||||
self.engine._check_field_permission(table_name, field_name)
|
||||
self.engine.check_select_field_permission(table_name, field_name)
|
||||
return Table(f"tab{table_name}")[field_name]
|
||||
else:
|
||||
frappe.throw(
|
||||
|
|
@ -2336,4 +2449,4 @@ class SQLFunctionParser:
|
|||
|
||||
def _check_function_field_permission(self, field_name: str):
|
||||
if self.engine.apply_permissions and self.engine.doctype:
|
||||
self.engine._check_field_permission(self.engine.doctype, field_name)
|
||||
self.engine.check_select_field_permission(self.engine.doctype, field_name)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
import json
|
||||
from datetime import date
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
|
@ -10,7 +11,7 @@ from frappe.query_builder.terms import ValueWrapper
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_event(args, field_map):
|
||||
def update_event(args: str, field_map: str):
|
||||
"""Updates Event (called via calendar) based on passed `field_map`"""
|
||||
args = frappe._dict(json.loads(args))
|
||||
field_map = frappe._dict(json.loads(field_map))
|
||||
|
|
@ -31,7 +32,14 @@ def get_event_conditions(doctype, filters=None):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_events(doctype, start, end, field_map, filters=None, fields=None):
|
||||
def get_events(
|
||||
doctype: str,
|
||||
start: date,
|
||||
end: date,
|
||||
field_map: str,
|
||||
filters: str | None = None,
|
||||
fields: str | list[str] | None = None,
|
||||
):
|
||||
field_map = frappe._dict(json.loads(field_map))
|
||||
fields = frappe.parse_json(fields)
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@ class Workspace:
|
|||
self.onboarding_list = [
|
||||
x["data"]["onboarding_name"] for x in loads(self.doc.content) if x["type"] == "onboarding"
|
||||
]
|
||||
self.onboardings = []
|
||||
|
||||
self.table_counts = get_table_with_counts()
|
||||
self.restricted_doctypes = (
|
||||
|
|
@ -157,7 +156,7 @@ class Workspace:
|
|||
self.cards = {"items": self.get_links()}
|
||||
self.charts = {"items": self.get_charts()}
|
||||
self.shortcuts = {"items": self.get_shortcuts()}
|
||||
self.onboardings = {"items": self.get_onboardings()}
|
||||
self.onboardings = {"items": []}
|
||||
self.quick_lists = {"items": self.get_quick_lists()}
|
||||
self.number_cards = {"items": self.get_number_cards()}
|
||||
self.custom_blocks = {"items": self.get_custom_blocks()}
|
||||
|
|
@ -315,38 +314,6 @@ class Workspace:
|
|||
|
||||
return items
|
||||
|
||||
@handle_not_exist
|
||||
def get_onboardings(self):
|
||||
if self.onboarding_list:
|
||||
for onboarding in self.onboarding_list:
|
||||
onboarding_doc = self.get_onboarding_doc(onboarding)
|
||||
if onboarding_doc:
|
||||
item = {
|
||||
"label": _(onboarding),
|
||||
"title": _(onboarding_doc.title),
|
||||
"subtitle": _(onboarding_doc.subtitle),
|
||||
"success": _(onboarding_doc.success_message),
|
||||
"docs_url": onboarding_doc.documentation_url,
|
||||
"items": self.get_onboarding_steps(onboarding_doc),
|
||||
}
|
||||
self.onboardings.append(item)
|
||||
return self.onboardings
|
||||
|
||||
@handle_not_exist
|
||||
def get_onboarding_steps(self, onboarding_doc):
|
||||
steps = []
|
||||
for doc in onboarding_doc.get_steps():
|
||||
step = doc.as_dict().copy()
|
||||
step.label = _(doc.title)
|
||||
step.description = _(doc.description)
|
||||
if step.action == "Create Entry":
|
||||
step.is_submittable = frappe.db.get_value(
|
||||
"DocType", step.reference_document, "is_submittable", cache=True
|
||||
)
|
||||
steps.append(step)
|
||||
|
||||
return steps
|
||||
|
||||
@handle_not_exist
|
||||
def get_number_cards(self):
|
||||
all_number_cards = []
|
||||
|
|
@ -384,7 +351,7 @@ class Workspace:
|
|||
|
||||
@frappe.whitelist()
|
||||
@frappe.read_only()
|
||||
def get_desktop_page(page):
|
||||
def get_desktop_page(page: str):
|
||||
"""Apply permissions, customizations and return the configuration for a page on desk.
|
||||
|
||||
Args:
|
||||
|
|
@ -490,7 +457,9 @@ def get_workspace_sidebar_items():
|
|||
pages.extend(private_pages)
|
||||
|
||||
if len(pages) == 0:
|
||||
pages.append(next((x for x in all_pages if x["title"] == "Welcome Workspace"), None))
|
||||
welcome_workspace = next((x for x in all_pages if x["title"] == "Welcome Workspace"), None)
|
||||
if welcome_workspace:
|
||||
pages.append(welcome_workspace)
|
||||
|
||||
return {
|
||||
"pages": pages,
|
||||
|
|
@ -681,7 +650,7 @@ def prepare_widget(config, doctype, parentfield):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_onboarding_step(name, field, value):
|
||||
def update_onboarding_step(name: str | int, field: str, value: int | str):
|
||||
"""Update status of onboaridng step
|
||||
|
||||
Args:
|
||||
|
|
@ -700,3 +669,50 @@ def update_onboarding_step(name, field, value):
|
|||
@frappe.whitelist()
|
||||
def get_installed_apps():
|
||||
return frappe.get_installed_apps()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.read_only()
|
||||
def get_onboarding_data(module: str):
|
||||
"""Get onboarding data for a page
|
||||
|
||||
Args:
|
||||
page (string): page name
|
||||
|
||||
Return:
|
||||
dict: onboarding data
|
||||
"""
|
||||
onboardings = []
|
||||
onboarding_doc = frappe.get_doc("Module Onboarding", module)
|
||||
if onboarding_doc.is_complete:
|
||||
return []
|
||||
|
||||
# Check if user is allowed
|
||||
allowed_roles = set(onboarding_doc.get_allowed_roles())
|
||||
user_roles = set(frappe.get_roles())
|
||||
if not allowed_roles & user_roles:
|
||||
return None
|
||||
|
||||
item = {
|
||||
"label": _(module),
|
||||
"title": _(onboarding_doc.title),
|
||||
"items": [],
|
||||
}
|
||||
|
||||
maps = get_onboarding_step_maps(onboarding_doc.name)
|
||||
for step in maps:
|
||||
steps = frappe.get_all("Onboarding Step", filters={"name": step}, order_by="idx", fields=["*"])
|
||||
|
||||
if steps:
|
||||
item["items"].append(steps[0])
|
||||
|
||||
onboardings.append(item)
|
||||
|
||||
if all(step.get("is_complete") or step.get("is_skipped") for step in item["items"]):
|
||||
return []
|
||||
|
||||
return onboardings
|
||||
|
||||
|
||||
def get_onboarding_step_maps(onboarding):
|
||||
return frappe.get_all("Onboarding Step Map", filters={"parent": onboarding}, pluck="step", order_by="idx")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright (c) 2015, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
from typing import Any
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.core.doctype.submission_queue.submission_queue import queue_submission
|
||||
|
|
@ -46,7 +48,16 @@ class BulkUpdate(Document):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def submit_cancel_or_update_docs(doctype, docnames, action="submit", data=None, task_id=None):
|
||||
def submit_cancel_or_update_docs(
|
||||
doctype: str,
|
||||
docnames: str | list[str],
|
||||
action: str = "submit",
|
||||
data: str | dict[str, Any] | None = None,
|
||||
task_id: str | None = None,
|
||||
) -> list[str] | None:
|
||||
if not frappe.get_cached_value("User", frappe.session.user, "bulk_actions"):
|
||||
frappe.throw(_("You are not allowed to perform bulk actions."), frappe.PermissionError)
|
||||
|
||||
if isinstance(docnames, str):
|
||||
docnames = frappe.parse_json(docnames)
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ class CustomHTMLBlock(Document):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_custom_blocks_for_user(doctype, txt, searchfield, start, page_len, filters):
|
||||
def get_custom_blocks_for_user(
|
||||
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict | str | list
|
||||
):
|
||||
# return logged in users private blocks and all public blocks
|
||||
customHTMLBlock = DocType("Custom HTML Block")
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ def get_permission_query_conditions(user):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_permitted_charts(dashboard_name):
|
||||
def get_permitted_charts(dashboard_name: str):
|
||||
permitted_charts = []
|
||||
dashboard = frappe.get_doc("Dashboard", dashboard_name)
|
||||
for chart in dashboard.charts:
|
||||
|
|
@ -101,7 +101,7 @@ def get_permitted_charts(dashboard_name):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_permitted_cards(dashboard_name):
|
||||
def get_permitted_cards(dashboard_name: str):
|
||||
dashboard = frappe.get_doc("Dashboard", dashboard_name)
|
||||
return [card for card in dashboard.cards if frappe.has_permission("Number Card", doc=card.card)]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import datetime
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
|
@ -89,16 +90,16 @@ def has_permission(doc, ptype, user):
|
|||
@frappe.whitelist()
|
||||
@cache_source
|
||||
def get(
|
||||
chart_name=None,
|
||||
chart=None,
|
||||
no_cache=None,
|
||||
filters=None,
|
||||
from_date=None,
|
||||
to_date=None,
|
||||
timespan=None,
|
||||
time_interval=None,
|
||||
heatmap_year=None,
|
||||
refresh=None,
|
||||
chart_name: str | None = None,
|
||||
chart: str | dict[str, Any] | None = None,
|
||||
no_cache: bool | int | None = None,
|
||||
filters: str | list | dict[str, Any] | None = None,
|
||||
from_date: str | datetime | None = None,
|
||||
to_date: str | datetime | None = None,
|
||||
timespan: str | None = None,
|
||||
time_interval: str | None = None,
|
||||
heatmap_year: str | int | None = None,
|
||||
refresh: bool | int | None = None,
|
||||
):
|
||||
if chart_name:
|
||||
chart: DashboardChart = frappe.get_doc("Dashboard Chart", chart_name)
|
||||
|
|
@ -139,7 +140,7 @@ def get(
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_dashboard_chart(args):
|
||||
def create_dashboard_chart(args: str | dict[str, Any]):
|
||||
args = frappe.parse_json(args)
|
||||
doc = frappe.new_doc("Dashboard Chart")
|
||||
|
||||
|
|
@ -156,7 +157,7 @@ def create_dashboard_chart(args):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_report_chart(args):
|
||||
def create_report_chart(args: str | dict[str, Any]):
|
||||
doc = create_dashboard_chart(args)
|
||||
args = frappe.parse_json(args)
|
||||
args.chart_name = doc.chart_name
|
||||
|
|
@ -165,7 +166,7 @@ def create_report_chart(args):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_chart_to_dashboard(args):
|
||||
def add_chart_to_dashboard(args: str | dict[str, Any]):
|
||||
args = frappe.parse_json(args)
|
||||
|
||||
dashboard = frappe.get_doc("Dashboard", args.dashboard)
|
||||
|
|
@ -326,7 +327,9 @@ def get_result(data, timegrain, from_date, to_date, chart_type):
|
|||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_charts_for_user(doctype, txt, searchfield, start, page_len, filters):
|
||||
def get_charts_for_user(
|
||||
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: str | list | dict[str, Any]
|
||||
):
|
||||
or_filters = {"owner": frappe.session.user, "is_public": 1}
|
||||
return frappe.db.get_list(
|
||||
"Dashboard Chart", fields=["name"], filters=filters, or_filters=or_filters, as_list=1
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import frappe
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ class DashboardSettings(Document):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_dashboard_settings(user):
|
||||
def create_dashboard_settings(user: str):
|
||||
if not frappe.db.exists("Dashboard Settings", user):
|
||||
doc = frappe.new_doc("Dashboard Settings")
|
||||
doc.name = user
|
||||
|
|
@ -43,7 +44,7 @@ def get_permission_query_conditions(user):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_chart_config(reset, config, chart_name):
|
||||
def save_chart_config(reset: int | str | bool, config: str | dict[str, Any], chart_name: str):
|
||||
reset = frappe.parse_json(reset)
|
||||
doc = frappe.get_doc("Dashboard Settings", frappe.session.user)
|
||||
chart_config = frappe.parse_json(doc.chart_config) or {}
|
||||
|
|
|
|||
|
|
@ -147,11 +147,11 @@
|
|||
"fieldname": "bg_color",
|
||||
"fieldtype": "Select",
|
||||
"label": "Background Color",
|
||||
"options": "blue\ngray"
|
||||
"options": "gray\nblue"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2026-01-27 18:17:48.667070",
|
||||
"modified": "2026-02-04 13:59:30.578370",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Desktop Icon",
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class DesktopIcon(Document):
|
|||
from frappe.types import DF
|
||||
|
||||
app: DF.Autocomplete | None
|
||||
bg_color: DF.Literal["blue", "gray"]
|
||||
bg_color: DF.Literal["gray", "blue"]
|
||||
hidden: DF.Check
|
||||
icon_image: DF.Attach | None
|
||||
icon_type: DF.Literal["Link", "Folder", "App"]
|
||||
|
|
@ -320,3 +320,24 @@ def create_user_icons(user, data):
|
|||
frappe.cache.hset("_user_settings", f"{'Desktop Icon'}::{user}", json.dumps(user_settings))
|
||||
return json.dumps(user_settings)
|
||||
return data
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_workspace_to_desktop(workspace: str):
|
||||
sidebar = frappe.new_doc("Workspace Sidebar")
|
||||
sidebar_item = frappe.new_doc("Workspace Sidebar Item")
|
||||
sidebar_item.label = workspace
|
||||
sidebar_item.type = "Link"
|
||||
sidebar_item.link_to = workspace
|
||||
sidebar_item.link_type = "Workspace"
|
||||
sidebar.title = workspace
|
||||
sidebar.append("items", sidebar_item)
|
||||
sidebar.save()
|
||||
|
||||
new_icon = frappe.new_doc("Desktop Icon")
|
||||
new_icon.label = workspace
|
||||
new_icon.icon_type = "Link"
|
||||
new_icon.link_to = workspace
|
||||
new_icon.link_type = "Workspace Sidebar"
|
||||
new_icon.insert()
|
||||
return {"icon": new_icon.as_dict()}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.desk.doctype.desktop_icon.desktop_icon import add_workspace_to_desktop
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
|
|
@ -24,11 +25,10 @@ class DesktopLayout(Document):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_layout(user, layout, new_icons):
|
||||
def save_layout(user: str, layout: str, new_icons: str | None = None):
|
||||
if not user:
|
||||
user = frappe.session.user
|
||||
layout = json.loads(layout)
|
||||
new_icons = json.loads(new_icons)
|
||||
desktop_layout = None
|
||||
try:
|
||||
desktop_layout = frappe.get_doc("Desktop Layout", frappe.session.user)
|
||||
|
|
@ -40,16 +40,36 @@ def save_layout(user, layout, new_icons):
|
|||
if layout:
|
||||
desktop_layout.layout = json.dumps(layout)
|
||||
desktop_layout.save()
|
||||
|
||||
for icon in new_icons:
|
||||
desktop_icon = frappe.new_doc("Desktop Icon")
|
||||
desktop_icon.update(icon)
|
||||
desktop_icon.owner = frappe.session.user
|
||||
desktop_icon.save()
|
||||
if new_icons:
|
||||
new_icons = json.loads(new_icons)
|
||||
for icon in new_icons:
|
||||
workspace = icon.get("workspace")
|
||||
if workspace:
|
||||
new_workspace = frappe.new_doc("Workspace")
|
||||
new_workspace.update(workspace)
|
||||
new_workspace.title = new_workspace.label
|
||||
new_workspace.save()
|
||||
return add_workspace_to_desktop(new_workspace.name)
|
||||
desktop_icon = frappe.new_doc("Desktop Icon")
|
||||
desktop_icon.update(icon)
|
||||
desktop_icon.owner = frappe.session.user
|
||||
desktop_icon.save()
|
||||
|
||||
return {"layout": layout}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_layout():
|
||||
"""Return the current user's saved desktop layout. Used on desk load to avoid stale cached HTML."""
|
||||
try:
|
||||
doc = frappe.get_doc("Desktop Layout", frappe.session.user)
|
||||
if doc.layout:
|
||||
return json.loads(doc.layout)
|
||||
except frappe.DoesNotExistError:
|
||||
frappe.clear_last_message()
|
||||
return None
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_layout():
|
||||
return frappe.delete_doc_if_exists("Desktop Layout", frappe.session.user)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import json
|
||||
from datetime import date, datetime
|
||||
from typing import Any
|
||||
|
||||
import frappe
|
||||
import frappe.share
|
||||
|
|
@ -235,7 +236,7 @@ class Event(Document):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_attending_status(event_name, attendee, status):
|
||||
def update_attending_status(event_name: str, attendee: str, status: str):
|
||||
event_doc = frappe.get_doc("Event", event_name)
|
||||
|
||||
if event_doc.owner == attendee == frappe.session.user:
|
||||
|
|
@ -252,7 +253,7 @@ def update_attending_status(event_name, attendee, status):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_communication(event, reference_doctype, reference_docname):
|
||||
def delete_communication(event: str | dict[str, Any], reference_doctype: str, reference_docname: str | int):
|
||||
if isinstance(event, str):
|
||||
event = json.loads(event)
|
||||
|
||||
|
|
@ -332,7 +333,11 @@ def send_event_digest():
|
|||
@frappe.whitelist()
|
||||
@http_cache(max_age=5 * 60, stale_while_revalidate=60 * 60)
|
||||
def get_events(
|
||||
start: date, end: date, user: str | None = None, for_reminder: bool = False, filters=None
|
||||
start: date,
|
||||
end: date,
|
||||
user: str | None = None,
|
||||
for_reminder: bool = False,
|
||||
filters: str | list | dict[str, Any] | None = None,
|
||||
) -> list[frappe._dict]:
|
||||
user = user or frappe.session.user
|
||||
type EventLikeDict = Event | frappe._dict
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class FormTour(Document):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def reset_tour(tour_name):
|
||||
def reset_tour(tour_name: str):
|
||||
for user in frappe.get_all("User", pluck="name"):
|
||||
onboarding_status = frappe.parse_json(frappe.db.get_value("User", user, "onboarding_status"))
|
||||
onboarding_status.pop(tour_name, None)
|
||||
|
|
@ -88,7 +88,7 @@ def reset_tour(tour_name):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_user_status(value, step):
|
||||
def update_user_status(value: str, step: str):
|
||||
from frappe.utils.telemetry import capture
|
||||
|
||||
step = frappe.parse_json(step)
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ def has_permission(doc, ptype, user):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_kanban_boards(doctype):
|
||||
def get_kanban_boards(doctype: str):
|
||||
"""Get Kanban Boards for doctype to show in List View"""
|
||||
return frappe.get_list(
|
||||
"Kanban Board",
|
||||
|
|
@ -76,7 +76,7 @@ def get_kanban_boards(doctype):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_column(board_name, column_title):
|
||||
def add_column(board_name: str, column_title: str):
|
||||
"""Adds new column to Kanban Board"""
|
||||
doc = frappe.get_doc("Kanban Board", board_name)
|
||||
for col in doc.columns:
|
||||
|
|
@ -89,7 +89,7 @@ def add_column(board_name, column_title):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def archive_restore_column(board_name, column_title, status):
|
||||
def archive_restore_column(board_name: str, column_title: str, status: str):
|
||||
"""Set column's status to status"""
|
||||
doc = frappe.get_doc("Kanban Board", board_name)
|
||||
for col in doc.columns:
|
||||
|
|
@ -101,7 +101,7 @@ def archive_restore_column(board_name, column_title, status):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_order(board_name, order):
|
||||
def update_order(board_name: str, order: str):
|
||||
"""Save the order of cards in columns"""
|
||||
board = frappe.get_doc("Kanban Board", board_name)
|
||||
doctype = board.reference_doctype
|
||||
|
|
@ -129,7 +129,14 @@ def update_order(board_name, order):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_order_for_single_card(board_name, docname, from_colname, to_colname, old_index, new_index):
|
||||
def update_order_for_single_card(
|
||||
board_name: str,
|
||||
docname: str,
|
||||
from_colname: str,
|
||||
to_colname: str,
|
||||
old_index: str | int,
|
||||
new_index: str | int,
|
||||
):
|
||||
"""Save the order of cards in columns"""
|
||||
board = frappe.get_doc("Kanban Board", board_name)
|
||||
doctype = board.reference_doctype
|
||||
|
|
@ -171,7 +178,7 @@ def get_kanban_column_order_and_index(board, colname):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_card(board_name, docname, colname):
|
||||
def add_card(board_name: str, docname: str, colname: str):
|
||||
board = frappe.get_doc("Kanban Board", board_name)
|
||||
|
||||
frappe.has_permission(board.reference_doctype, "write", throw=True)
|
||||
|
|
@ -185,7 +192,7 @@ def add_card(board_name, docname, colname):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def quick_kanban_board(doctype, board_name, field_name, project=None):
|
||||
def quick_kanban_board(doctype: str, board_name: str, field_name: str, project: str | None = None):
|
||||
"""Create new KanbanBoard quickly with default options"""
|
||||
|
||||
doc = frappe.new_doc("Kanban Board")
|
||||
|
|
@ -228,7 +235,7 @@ def get_order_for_column(board, colname):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_column_order(board_name, order):
|
||||
def update_column_order(board_name: str, order: str):
|
||||
"""Set the order of columns in Kanban Board"""
|
||||
board = frappe.get_doc("Kanban Board", board_name)
|
||||
order = json.loads(order)
|
||||
|
|
@ -260,7 +267,7 @@ def update_column_order(board_name, order):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_indicator(board_name, column_name, indicator):
|
||||
def set_indicator(board_name: str, column_name: str, indicator: str):
|
||||
"""Set the indicator color of column"""
|
||||
board = frappe.get_doc("Kanban Board", board_name)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright (c) 2020, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
from typing import Any
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
|
@ -29,7 +31,9 @@ class ListViewSettings(Document):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_listview_settings(doctype, listview_settings, removed_listview_fields):
|
||||
def save_listview_settings(
|
||||
doctype: str, listview_settings: str | dict[str, Any], removed_listview_fields: str | list[str]
|
||||
):
|
||||
listview_settings = frappe.parse_json(listview_settings)
|
||||
removed_listview_fields = frappe.parse_json(removed_listview_fields)
|
||||
|
||||
|
|
@ -87,7 +91,7 @@ def set_in_list_view_property(doctype, field, value):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_default_listview_fields(doctype):
|
||||
def get_default_listview_fields(doctype: str):
|
||||
meta = frappe.get_meta(doctype)
|
||||
path = frappe.get_module_path(
|
||||
frappe.scrub(meta.module), "doctype", frappe.scrub(meta.name), frappe.scrub(meta.name) + ".json"
|
||||
|
|
|
|||
|
|
@ -7,12 +7,9 @@
|
|||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"subtitle",
|
||||
"module",
|
||||
"allow_roles",
|
||||
"column_break_4",
|
||||
"success_message",
|
||||
"documentation_url",
|
||||
"allow_roles",
|
||||
"is_complete",
|
||||
"section_break_6",
|
||||
"steps"
|
||||
|
|
@ -25,12 +22,6 @@
|
|||
"label": "Title",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subtitle",
|
||||
"fieldtype": "Data",
|
||||
"label": "Subtitle",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "module",
|
||||
"fieldtype": "Link",
|
||||
|
|
@ -46,18 +37,6 @@
|
|||
"fieldname": "section_break_6",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "success_message",
|
||||
"fieldtype": "Data",
|
||||
"label": "Success Message",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "documentation_url",
|
||||
"fieldtype": "Data",
|
||||
"label": "Documentation URL",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_complete",
|
||||
|
|
@ -82,7 +61,7 @@
|
|||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-03-23 16:03:30.074327",
|
||||
"modified": "2026-02-20 13:30:25.659490",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Module Onboarding",
|
||||
|
|
@ -111,8 +90,9 @@
|
|||
}
|
||||
],
|
||||
"read_only": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,12 +19,9 @@ class ModuleOnboarding(Document):
|
|||
from frappe.types import DF
|
||||
|
||||
allow_roles: DF.TableMultiSelect[OnboardingPermission]
|
||||
documentation_url: DF.Data
|
||||
is_complete: DF.Check
|
||||
module: DF.Link
|
||||
steps: DF.Table[OnboardingStepMap]
|
||||
subtitle: DF.Data
|
||||
success_message: DF.Data
|
||||
title: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
"default": "1",
|
||||
"fieldname": "enabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enabled"
|
||||
"label": "Enable System Notification"
|
||||
},
|
||||
{
|
||||
"fieldname": "subscribed_documents",
|
||||
|
|
@ -98,7 +98,7 @@
|
|||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-04-16 17:15:25.641232",
|
||||
"modified": "2026-02-24 11:06:24.112935",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Notification Settings",
|
||||
|
|
|
|||
|
|
@ -130,8 +130,8 @@ def has_permission(doc, ptype="read", user=None):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_seen_value(value, user):
|
||||
def set_seen_value(value: int, user: str):
|
||||
if frappe.flags.read_only:
|
||||
return
|
||||
|
||||
frappe.db.set_value("Notification Settings", user, "seen", value, update_modified=False)
|
||||
frappe.db.set_value("Notification Settings", frappe.session.user, "seen", value, update_modified=False)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
# Copyright (c) 2020, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
from datetime import date, datetime
|
||||
from typing import Any
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.boot import get_allowed_report_names
|
||||
|
|
@ -121,7 +124,11 @@ def has_permission(doc, ptype, user):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_result(doc, filters, to_date=None):
|
||||
def get_result(
|
||||
doc: str | dict[str, Any] | Document,
|
||||
filters: str | list | dict[str, Any],
|
||||
to_date: str | datetime | date | None = None,
|
||||
):
|
||||
doc = frappe.parse_json(doc)
|
||||
fields = []
|
||||
sql_function_map = {
|
||||
|
|
@ -158,7 +165,9 @@ def get_result(doc, filters, to_date=None):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_percentage_difference(doc, filters, result):
|
||||
def get_percentage_difference(
|
||||
doc: str | dict[str, Any], filters: str | list | dict[str, Any], result: float | int | str
|
||||
):
|
||||
doc = frappe.parse_json(doc)
|
||||
result = frappe.parse_json(result)
|
||||
|
||||
|
|
@ -194,7 +203,7 @@ def calculate_previous_result(doc, filters):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_number_card(args):
|
||||
def create_number_card(args: str | dict[str, Any]):
|
||||
args = frappe.parse_json(args)
|
||||
doc = frappe.new_doc("Number Card")
|
||||
|
||||
|
|
@ -205,7 +214,9 @@ def create_number_card(args):
|
|||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters):
|
||||
def get_cards_for_user(
|
||||
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: str | list | dict[str, Any]
|
||||
):
|
||||
doctype = "Number Card"
|
||||
meta = frappe.get_meta(doctype)
|
||||
searchfields = meta.get_search_fields()
|
||||
|
|
@ -229,7 +240,7 @@ def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_report_number_card(args):
|
||||
def create_report_number_card(args: str | dict[str, Any]):
|
||||
card = create_number_card(args)
|
||||
args = frappe.parse_json(args)
|
||||
args.name = card.name
|
||||
|
|
@ -238,7 +249,7 @@ def create_report_number_card(args):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_card_to_dashboard(args):
|
||||
def add_card_to_dashboard(args: str | dict[str, Any]):
|
||||
args = frappe.parse_json(args)
|
||||
|
||||
dashboard = frappe.get_doc("Dashboard", args.dashboard)
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@
|
|||
"validate_action",
|
||||
"field",
|
||||
"value_to_validate",
|
||||
"video_url"
|
||||
"video_url",
|
||||
"section_break_ajog",
|
||||
"route_options"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -61,7 +63,7 @@
|
|||
"fieldname": "action",
|
||||
"fieldtype": "Select",
|
||||
"label": "Action",
|
||||
"options": "Create Entry\nUpdate Settings\nShow Form Tour\nView Report\nGo to Page\nWatch Video",
|
||||
"options": "Create Entry\nUpdate Settings\nShow Form Tour\nView Report\nGo to Page\nView Docs",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -145,7 +147,7 @@
|
|||
"label": "Is Single"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.action == \"Go to Page\"",
|
||||
"depends_on": "eval:doc.action == \"Go to Page\" || doc.action === \"View Docs\"",
|
||||
"description": "Example: #Tree/Account",
|
||||
"fieldname": "path",
|
||||
"fieldtype": "Data",
|
||||
|
|
@ -214,10 +216,19 @@
|
|||
"fieldtype": "Link",
|
||||
"label": "Form Tour",
|
||||
"options": "Form Tour"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ajog",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "route_options",
|
||||
"fieldtype": "Code",
|
||||
"label": "Route Options"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-03-23 16:03:33.078443",
|
||||
"modified": "2026-02-23 21:03:51.131292",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Onboarding Step",
|
||||
|
|
@ -248,8 +259,9 @@
|
|||
],
|
||||
"quick_entry": 1,
|
||||
"read_only": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class OnboardingStep(Document):
|
|||
from frappe.types import DF
|
||||
|
||||
action: DF.Literal[
|
||||
"Create Entry", "Update Settings", "Show Form Tour", "View Report", "Go to Page", "Watch Video"
|
||||
"Create Entry", "Update Settings", "Show Form Tour", "View Report", "Go to Page", "View Docs"
|
||||
]
|
||||
action_label: DF.Data | None
|
||||
callback_message: DF.SmallText | None
|
||||
|
|
@ -36,6 +36,7 @@ class OnboardingStep(Document):
|
|||
report_description: DF.Data | None
|
||||
report_reference_doctype: DF.Data | None
|
||||
report_type: DF.Data | None
|
||||
route_options: DF.Code | None
|
||||
show_form_tour: DF.Check
|
||||
show_full_form: DF.Check
|
||||
title: DF.Data
|
||||
|
|
@ -50,7 +51,7 @@ class OnboardingStep(Document):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_onboarding_steps(ob_steps):
|
||||
def get_onboarding_steps(ob_steps: str):
|
||||
steps = []
|
||||
for s in json.loads(ob_steps):
|
||||
doc = frappe.get_doc("Onboarding Step", s.get("step"))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright (c) 2022, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
from typing import Any
|
||||
|
||||
import frappe
|
||||
from frappe.deferred_insert import deferred_insert as _deferred_insert
|
||||
from frappe.model.document import Document
|
||||
|
|
@ -29,7 +31,7 @@ class RouteHistory(Document):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def deferred_insert(routes):
|
||||
def deferred_insert(routes: str | list[dict[str, Any]]):
|
||||
routes = [
|
||||
{
|
||||
"user": frappe.session.user,
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class SidebarItemGroup(Document):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_reports(module_name=None):
|
||||
def get_reports(module_name: str | None = None):
|
||||
reports_info = []
|
||||
if module_name:
|
||||
sidebar_group = frappe.get_doc("Sidebar Item Group", module_name)
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class SystemConsole(Document):
|
|||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
def execute_code(doc):
|
||||
def execute_code(doc: str):
|
||||
console = frappe.get_doc(json.loads(doc))
|
||||
console.run()
|
||||
return console.as_dict()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.utils import unique
|
||||
|
|
@ -33,7 +34,7 @@ def check_user_tags(dt):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_tag(tag, dt, dn, color=None):
|
||||
def add_tag(tag: str, dt: str, dn: str, color: str | None = None):
|
||||
"adds a new tag to a record, and creates the Tag master"
|
||||
DocTags(dt).add(dn, tag)
|
||||
|
||||
|
|
@ -41,8 +42,12 @@ def add_tag(tag, dt, dn, color=None):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_tags(tags, dt, docs, color=None):
|
||||
def add_tags(tags: str | list[str], dt: str, docs: str | list[str], color: str | None = None):
|
||||
"adds a new tag to a record, and creates the Tag master"
|
||||
|
||||
if not frappe.get_cached_value("User", frappe.session.user, "bulk_actions"):
|
||||
frappe.throw(_("You are not allowed to perform bulk actions"), frappe.PermissionError)
|
||||
|
||||
tags = frappe.parse_json(tags)
|
||||
docs = frappe.parse_json(docs)
|
||||
for doc in docs:
|
||||
|
|
@ -51,20 +56,20 @@ def add_tags(tags, dt, docs, color=None):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def remove_tag(tag, dt, dn):
|
||||
def remove_tag(tag: str, dt: str, dn: str):
|
||||
"removes tag from the record"
|
||||
DocTags(dt).remove(dn, tag)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_tagged_docs(doctype, tag):
|
||||
def get_tagged_docs(doctype: str, tag: str):
|
||||
frappe.has_permission(doctype, throw=True)
|
||||
doctype = DocType(doctype)
|
||||
return (frappe.qb.from_(doctype).where(doctype._user_tags.like(tag)).select(doctype.name)).run()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_tags(doctype, txt):
|
||||
def get_tags(doctype: str, txt: str):
|
||||
tag = frappe.get_list("Tag", filters=[["name", "like", f"%{txt}%"]])
|
||||
tags = [t.name for t in tag]
|
||||
|
||||
|
|
@ -176,7 +181,7 @@ def update_tags(doc, tags):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_documents_for_tag(tag):
|
||||
def get_documents_for_tag(tag: str):
|
||||
"""Search for given text in Tag Link.
|
||||
|
||||
:param tag: tag to be searched
|
||||
|
|
|
|||
|
|
@ -174,5 +174,5 @@ def has_permission(doc, ptype="read", user=None):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def new_todo(description):
|
||||
def new_todo(description: str):
|
||||
frappe.get_doc({"doctype": "ToDo", "description": description}).insert()
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ frappe.views.calendar["ToDo"] = {
|
|||
id: "name",
|
||||
title: "description",
|
||||
allDay: "allDay",
|
||||
progress: "progress",
|
||||
},
|
||||
gantt: true,
|
||||
filters: [
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ frappe.ui.form.on("Workspace", {
|
|||
|
||||
refresh: function (frm) {
|
||||
frm.enable_save();
|
||||
|
||||
frm.trigger("add_to_desktop");
|
||||
let url = `/desk/${
|
||||
frm.doc.public
|
||||
? frappe.router.slug(frm.doc.title)
|
||||
|
|
@ -44,6 +44,26 @@ frappe.ui.form.on("Workspace", {
|
|||
frm.layout.show_message(message);
|
||||
},
|
||||
|
||||
add_to_desktop: function (frm) {
|
||||
if (frappe.app.sidebar.get_workspace_sidebars(frm.doc.title).length === 0) {
|
||||
frm.add_custom_button(__("Add to Desktop"), function () {
|
||||
frappe.call({
|
||||
method: "frappe.desk.doctype.desktop_icon.desktop_icon.add_workspace_to_desktop",
|
||||
args: {
|
||||
workspace: frm.doc.name,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message.status) {
|
||||
frappe.toast({
|
||||
message: __("Workspace added to desktop"),
|
||||
indicator: "green",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
disable_form: function (frm) {
|
||||
frm.fields
|
||||
.filter((field) => field.has_input)
|
||||
|
|
|
|||
|
|
@ -76,6 +76,18 @@ class Workspace(Document):
|
|||
|
||||
if self.public and not is_workspace_manager() and not disable_saving_as_public():
|
||||
frappe.throw(_("You need to be Workspace Manager to edit this document"))
|
||||
|
||||
if (
|
||||
not self.public
|
||||
and self.for_user
|
||||
and self.for_user != frappe.session.user
|
||||
and not is_workspace_manager()
|
||||
):
|
||||
frappe.throw(
|
||||
_("You are not allowed to edit this workspace"),
|
||||
frappe.PermissionError,
|
||||
)
|
||||
|
||||
if self.has_value_changed("title"):
|
||||
validate_route_conflict(self.doctype, self.title)
|
||||
else:
|
||||
|
|
@ -125,10 +137,19 @@ class Workspace(Document):
|
|||
self.name = doc.name = doc.label = doc.title
|
||||
|
||||
def on_trash(self):
|
||||
if not self.module:
|
||||
self.delete_sidebar()
|
||||
self.delete_desktop_icon()
|
||||
if self.public and not is_workspace_manager():
|
||||
frappe.throw(_("You need to be Workspace Manager to delete a public workspace."))
|
||||
self.delete_from_my_workspaces()
|
||||
|
||||
def delete_desktop_icon(self):
|
||||
frappe.delete_doc_if_exists("Desktop Icon", self.title)
|
||||
|
||||
def delete_sidebar(self):
|
||||
frappe.delete_doc_if_exists("Workspace Sidebar", self.title)
|
||||
|
||||
def delete_from_my_workspaces(self):
|
||||
if self.public:
|
||||
return
|
||||
|
|
@ -275,7 +296,7 @@ def get_report_type(report):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def new_page(new_page):
|
||||
def new_page(new_page: str):
|
||||
if not loads(new_page):
|
||||
return
|
||||
|
||||
|
|
@ -319,7 +340,7 @@ def new_page(new_page):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_page(name, public, new_widgets, blocks):
|
||||
def save_page(name: str, public: str | int, new_widgets: str, blocks: str):
|
||||
public = frappe.parse_json(public)
|
||||
|
||||
doc = frappe.get_doc("Workspace", name)
|
||||
|
|
@ -334,7 +355,7 @@ def save_page(name, public, new_widgets, blocks):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_page(name, title, icon, indicator_color, parent, public):
|
||||
def update_page(name: str, title: str, icon: str, indicator_color: str, parent: str, public: str | int):
|
||||
public = frappe.parse_json(public)
|
||||
doc = frappe.get_doc("Workspace", name)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"column_break_pukb",
|
||||
"standard",
|
||||
"app",
|
||||
"module_onboarding",
|
||||
"section_break_vdyo",
|
||||
"items"
|
||||
],
|
||||
|
|
@ -67,12 +68,18 @@
|
|||
{
|
||||
"fieldname": "section_break_vdyo",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "module_onboarding",
|
||||
"fieldtype": "Link",
|
||||
"label": "Module Onboarding",
|
||||
"options": "Module Onboarding"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-02 12:35:38.009501",
|
||||
"modified": "2026-02-20 15:19:27.520469",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Workspace Sidebar",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ class WorkspaceSidebar(Document):
|
|||
for_user: DF.Link | None
|
||||
items: DF.Table[WorkspaceSidebarItem]
|
||||
module: DF.Text | None
|
||||
module_onboarding: DF.Link | None
|
||||
standard: DF.Check
|
||||
title: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
|
@ -195,7 +196,7 @@ def create_workspace_sidebar_for_workspaces():
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_sidebar_items(sidebar_title, sidebar_items):
|
||||
def add_sidebar_items(sidebar_title: str, sidebar_items: str):
|
||||
sidebar_items = loads(sidebar_items)
|
||||
title = f"{sidebar_title}-{frappe.session.user}"
|
||||
w = frappe.get_doc("Workspace Sidebar", sidebar_title)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
"""assign/unassign to ToDo"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import frappe
|
||||
import frappe.share
|
||||
|
|
@ -40,7 +41,7 @@ def get(args=None):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add(args=None, *, ignore_permissions=False):
|
||||
def add(args: dict[str, Any] | None = None, *, ignore_permissions: bool | int = False):
|
||||
"""add in someone's to do list
|
||||
args = {
|
||||
"assign_to": [],
|
||||
|
|
@ -140,9 +141,11 @@ def add(args=None, *, ignore_permissions=False):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_multiple(args=None):
|
||||
if not args:
|
||||
args = frappe.local.form_dict
|
||||
def add_multiple() -> None:
|
||||
if not frappe.get_cached_value("User", frappe.session.user, "bulk_actions"):
|
||||
frappe.throw(_("You are not allowed to perform bulk actions"), frappe.PermissionError)
|
||||
|
||||
args = frappe.local.form_dict
|
||||
|
||||
docname_list = json.loads(args["name"])
|
||||
|
||||
|
|
@ -174,12 +177,15 @@ def close_all_assignments(doctype, name, ignore_permissions=False):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def remove(doctype, name, assign_to, ignore_permissions=False):
|
||||
def remove(doctype: str, name: str | int, assign_to: str, ignore_permissions: bool | int = False):
|
||||
return set_status(doctype, name, "", assign_to, status="Cancelled", ignore_permissions=ignore_permissions)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def remove_multiple(doctype, names, ignore_permissions=False):
|
||||
def remove_multiple(doctype: str, names: str, ignore_permissions: bool | int = False):
|
||||
if not frappe.get_cached_value("User", frappe.session.user, "bulk_actions"):
|
||||
frappe.throw(_("You are not allowed to perform bulk actions"), frappe.PermissionError)
|
||||
|
||||
docname_list = json.loads(names)
|
||||
|
||||
for name in docname_list:
|
||||
|
|
@ -193,7 +199,7 @@ def remove_multiple(doctype, names, ignore_permissions=False):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def close(doctype: str, name: str, assign_to: str, ignore_permissions=False):
|
||||
def close(doctype: str, name: str, assign_to: str, ignore_permissions: bool | int = False):
|
||||
if assign_to != frappe.session.user:
|
||||
frappe.throw(_("Only the assignee can complete this to-do."))
|
||||
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ def is_document_followed(doctype, doc_name, user):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_follow_users(doctype, doc_name):
|
||||
def get_follow_users(doctype: str, doc_name: str):
|
||||
return frappe.get_all(
|
||||
"Document Follow", filters={"ref_doctype": doctype, "ref_docname": doc_name}, fields=["user"]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ from frappe.modules import load_doctype_module
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_submitted_linked_docs(doctype: str, name: str, ignore_doctypes_on_cancel_all=None) -> list[tuple]:
|
||||
def get_submitted_linked_docs(
|
||||
doctype: str, name: str, ignore_doctypes_on_cancel_all: str | list[str] | None = None
|
||||
) -> list[tuple]:
|
||||
"""Get all the nested submitted documents those are present in referencing tables (dependent tables).
|
||||
|
||||
:param doctype: Document type
|
||||
|
|
@ -365,7 +367,7 @@ def get_referencing_documents(
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=None):
|
||||
def cancel_all_linked_docs(docs: str, ignore_doctypes_on_cancel_all: str | list[str] | None = None):
|
||||
"""
|
||||
Cancel all linked doctype, optionally ignore doctypes specified in a list.
|
||||
|
||||
|
|
@ -435,37 +437,19 @@ def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> di
|
|||
is_target_doctype_table = frappe.get_meta(doctype).istable
|
||||
|
||||
for linked_doctype, link_context in linkinfo.items():
|
||||
# Don't try to fetch linked documents if the user can't read the doctype
|
||||
if not frappe.has_permission(linked_doctype):
|
||||
continue
|
||||
|
||||
linked_doctype_meta = frappe.get_meta(linked_doctype)
|
||||
|
||||
if linked_doctype_meta.issingle:
|
||||
continue
|
||||
|
||||
has_permission = frappe.has_permission(linked_doctype)
|
||||
filters = []
|
||||
or_filters = []
|
||||
ret = None
|
||||
parent_info = None
|
||||
|
||||
fields = [
|
||||
d.fieldname
|
||||
for d in linked_doctype_meta.get(
|
||||
"fields",
|
||||
{
|
||||
"in_list_view": 1,
|
||||
"fieldtype": ["not in", ("Image", "HTML", "Button", *frappe.model.table_fields)],
|
||||
},
|
||||
)
|
||||
] + ["name", "modified", "docstatus"]
|
||||
|
||||
if add_fields := link_context.get("add_fields"):
|
||||
fields += add_fields
|
||||
|
||||
fields = [sf.strip() for sf in fields if sf]
|
||||
|
||||
if filters_ctx := link_context.get("filters"):
|
||||
ret = frappe.get_list(doctype=linked_doctype, fields=fields, filters=filters_ctx, order_by=None)
|
||||
filters = filters_ctx
|
||||
|
||||
elif link_context.get("get_parent"):
|
||||
# check for child table
|
||||
|
|
@ -476,13 +460,10 @@ def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> di
|
|||
doctype, name, ["parenttype", "parent"], as_dict=True, order_by=None
|
||||
)
|
||||
|
||||
if parent_info and parent_info.parenttype == linked_doctype:
|
||||
ret = frappe.get_list(
|
||||
doctype=linked_doctype,
|
||||
fields=fields,
|
||||
filters=[[linked_doctype, "name", "=", parent_info.parent]],
|
||||
order_by=None,
|
||||
)
|
||||
if not (parent_info and parent_info.parenttype == linked_doctype):
|
||||
continue
|
||||
|
||||
filters = [[linked_doctype, "name", "=", parent_info.parent]]
|
||||
|
||||
elif child_doctype := link_context.get("child_doctype"):
|
||||
or_filters = [
|
||||
|
|
@ -493,15 +474,6 @@ def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> di
|
|||
if doctype_fieldname := link_context.get("doctype_fieldname"):
|
||||
filters.append([child_doctype, doctype_fieldname, "=", doctype])
|
||||
|
||||
ret = frappe.get_list(
|
||||
doctype=linked_doctype,
|
||||
fields=fields,
|
||||
filters=filters,
|
||||
or_filters=or_filters,
|
||||
distinct=True,
|
||||
order_by=None,
|
||||
)
|
||||
|
||||
elif link_fieldnames := link_context.get("fieldname"):
|
||||
if isinstance(link_fieldnames, str):
|
||||
link_fieldnames = [link_fieldnames]
|
||||
|
|
@ -516,25 +488,64 @@ def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> di
|
|||
or frappe.db.exists(linked_doctype, {"parenttype": doctype, "parent": name})
|
||||
):
|
||||
continue
|
||||
|
||||
total_count = len(
|
||||
frappe.get_all(
|
||||
linked_doctype,
|
||||
filters=filters,
|
||||
or_filters=or_filters,
|
||||
fields=["name"],
|
||||
order_by=None,
|
||||
)
|
||||
)
|
||||
|
||||
if not total_count:
|
||||
continue
|
||||
|
||||
if has_permission:
|
||||
fields = [
|
||||
d.fieldname
|
||||
for d in linked_doctype_meta.get(
|
||||
"fields",
|
||||
{
|
||||
"in_list_view": 1,
|
||||
"fieldtype": ["not in", ("Image", "HTML", "Button", *frappe.model.table_fields)],
|
||||
},
|
||||
)
|
||||
] + ["name", "modified", "docstatus"]
|
||||
|
||||
if add_fields := link_context.get("add_fields"):
|
||||
fields += add_fields
|
||||
|
||||
fields = [sf.strip() for sf in fields if sf]
|
||||
|
||||
ret = frappe.get_list(
|
||||
doctype=linked_doctype, fields=fields, filters=filters, or_filters=or_filters, order_by=None
|
||||
doctype=linked_doctype,
|
||||
fields=fields,
|
||||
filters=filters,
|
||||
or_filters=or_filters,
|
||||
distinct=True,
|
||||
order_by=None,
|
||||
)
|
||||
|
||||
if ret:
|
||||
results[linked_doctype] = ret
|
||||
permitted_count = len(ret or [])
|
||||
results[linked_doctype] = {
|
||||
"docs": ret or [],
|
||||
"hidden_count": total_count - permitted_count,
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get(doctype, docname):
|
||||
def get(doctype: str, docname: str):
|
||||
frappe.has_permission(doctype, doc=docname, throw=True)
|
||||
linked_doctypes = get_linked_doctypes(doctype=doctype)
|
||||
return get_linked_docs(doctype=doctype, name=docname, linkinfo=linked_doctypes)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False):
|
||||
def get_linked_doctypes(doctype: str, without_ignore_user_permissions_enabled: int | bool = False):
|
||||
"""add list of doctypes this doctype is 'linked' with.
|
||||
|
||||
Example, for Customer:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import json
|
||||
import typing
|
||||
from typing import Any
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
import frappe
|
||||
|
|
@ -12,16 +13,14 @@ import frappe.utils
|
|||
from frappe import _, _dict
|
||||
from frappe.core.doctype.permission_type.permission_type import get_doctype_ptype_map
|
||||
from frappe.desk.form.document_follow import is_document_followed
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.utils.user_settings import get_user_settings
|
||||
from frappe.permissions import check_doctype_permission, get_doc_permissions, has_permission
|
||||
from frappe.utils.data import cstr
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def getdoc(doctype, name):
|
||||
def getdoc(doctype: str, name: str | int):
|
||||
"""
|
||||
Loads a doclist for a given document. This method is called directly from the client.
|
||||
Requires "doctype", "name" as form variables.
|
||||
|
|
@ -60,7 +59,7 @@ def getdoc(doctype, name):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def getdoctype(doctype, with_parent=False):
|
||||
def getdoctype(doctype: str, with_parent: int | bool = False):
|
||||
"""load doctype"""
|
||||
|
||||
docs = []
|
||||
|
|
@ -90,7 +89,11 @@ def get_meta_bundle(doctype):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_docinfo(doc=None, doctype=None, name=None):
|
||||
def get_docinfo(
|
||||
doc: Document | dict | str | None = None,
|
||||
doctype: str | None = None,
|
||||
name: str | int | None = None,
|
||||
):
|
||||
from frappe.share import _get_users as get_docshares
|
||||
|
||||
if not doc:
|
||||
|
|
@ -182,7 +185,7 @@ def get_milestones(doctype, name):
|
|||
def get_attachments(dt, dn):
|
||||
return frappe.get_all(
|
||||
"File",
|
||||
fields=["name", "file_name", "file_url", "is_private"],
|
||||
fields=["name", "file_name", "file_url", "is_private", "attached_to_field", "folder"],
|
||||
filters={"attached_to_name": str(dn), "attached_to_doctype": dt},
|
||||
)
|
||||
|
||||
|
|
@ -200,7 +203,7 @@ def get_versions(doc: "Document") -> list[dict]:
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_communications(doctype, name, start=0, limit=20):
|
||||
def get_communications(doctype: str, name: str | int, start: str | int = 0, limit: str | int = 20):
|
||||
from frappe.utils import cint
|
||||
|
||||
frappe.get_lazy_doc(doctype, name).check_permission()
|
||||
|
|
@ -500,7 +503,7 @@ def update_user_info(docinfo, doc=None):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_user_info_for_viewers(users):
|
||||
def get_user_info_for_viewers(users: str):
|
||||
user_info = {}
|
||||
for user in json.loads(users):
|
||||
frappe.utils.add_user_info(user, user_info)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from frappe.utils.telemetry import capture_doc
|
|||
|
||||
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def savedocs(doc, action):
|
||||
def savedocs(doc: str, action: str):
|
||||
"""save / submit / update doclist"""
|
||||
doc = frappe.get_doc(json.loads(doc))
|
||||
capture_doc(doc, action)
|
||||
|
|
@ -52,7 +52,12 @@ def savedocs(doc, action):
|
|||
|
||||
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def cancel(doctype=None, name=None, workflow_state_fieldname=None, workflow_state=None):
|
||||
def cancel(
|
||||
doctype: str | None = None,
|
||||
name: str | int | None = None,
|
||||
workflow_state_fieldname: str | None = None,
|
||||
workflow_state: str | None = None,
|
||||
):
|
||||
"""cancel a doclist"""
|
||||
doc = frappe.get_doc(doctype, name)
|
||||
capture_doc(doc, "Cancel")
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ def add_comment(
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_comment(name, content):
|
||||
def update_comment(name: str | int, content: str):
|
||||
"""allow only owner to update comment"""
|
||||
doc = frappe.get_doc("Comment", name)
|
||||
|
||||
|
|
@ -77,40 +77,51 @@ def update_comment_publicity(name: str, publish: bool):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_next(doctype, value, prev, filters=None, sort_order="desc", sort_field="creation"):
|
||||
def get_next(
|
||||
doctype: str,
|
||||
value: str,
|
||||
prev: str | int,
|
||||
filters: dict | str | None = None,
|
||||
sort_order: str = "desc",
|
||||
sort_field: str = "creation",
|
||||
):
|
||||
prev = int(prev)
|
||||
if not filters:
|
||||
filters = []
|
||||
if isinstance(filters, str):
|
||||
filters = json.loads(filters)
|
||||
|
||||
# # condition based on sort order
|
||||
condition = ">" if sort_order.lower() == "asc" else "<"
|
||||
table = frappe.qb.DocType(doctype)
|
||||
sort_column = table[sort_field]
|
||||
name_column = table.name
|
||||
current_sort_value = frappe.db.get_value(doctype, value, sort_field)
|
||||
|
||||
# switch the condition
|
||||
if prev:
|
||||
sort_order = "asc" if sort_order.lower() == "desc" else "desc"
|
||||
condition = "<" if condition == ">" else ">"
|
||||
is_ascending = sort_order.lower() == "asc"
|
||||
if prev == is_ascending:
|
||||
composite_condition = (sort_column < current_sort_value) | (
|
||||
(sort_column == current_sort_value) & (name_column < value)
|
||||
)
|
||||
order = frappe.qb.desc
|
||||
else:
|
||||
composite_condition = (sort_column > current_sort_value) | (
|
||||
(sort_column == current_sort_value) & (name_column > value)
|
||||
)
|
||||
order = frappe.qb.asc
|
||||
|
||||
# # add condition for next or prev item
|
||||
filters.append([doctype, sort_field, condition, frappe.get_value(doctype, value, sort_field)])
|
||||
|
||||
res = frappe.get_list(
|
||||
doctype,
|
||||
fields=["name"],
|
||||
filters=filters,
|
||||
order_by=f"{sort_field} {sort_order}",
|
||||
limit_start=0,
|
||||
limit_page_length=1,
|
||||
as_list=True,
|
||||
query = (
|
||||
frappe.qb.get_query(doctype, filters=filters, fields=["name"], ignore_permissions=False)
|
||||
.orderby(sort_column, order=order)
|
||||
.orderby(name_column, order=order)
|
||||
.where(composite_condition)
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
if not res:
|
||||
frappe.msgprint(_("No further records"))
|
||||
return None
|
||||
else:
|
||||
if res := query.run(as_list=True):
|
||||
return res[0][0]
|
||||
|
||||
frappe.msgprint(_("No further records"))
|
||||
return None
|
||||
|
||||
|
||||
def get_pdf_link(doctype, docname, print_format="Standard", no_letterhead=0):
|
||||
return f"/api/method/frappe.utils.print_format.download_pdf?doctype={doctype}&name={docname}&format={print_format}&no_letterhead={no_letterhead}"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import frappe
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_task(args, field_map):
|
||||
def update_task(args: str, field_map: str):
|
||||
"""Updates Doc (called via gantt) based on passed `field_map`"""
|
||||
args = frappe._dict(json.loads(args))
|
||||
field_map = frappe._dict(json.loads(field_map))
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from frappe.utils import get_link_to_form
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def toggle_like(doctype, name, add=False):
|
||||
def toggle_like(doctype: str, name: str, add: str | bool = False):
|
||||
"""Adds / removes the current user in the `__liked_by` property of the given document.
|
||||
If column does not exist, will add it in the database.
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from frappe.www.printview import set_title_values_for_link_and_dynamic_link_fiel
|
|||
|
||||
@frappe.whitelist()
|
||||
@http_cache(max_age=60 * 10)
|
||||
def get_preview_data(doctype, docname):
|
||||
def get_preview_data(doctype: str, docname: str | int):
|
||||
preview_fields = []
|
||||
meta = frappe.get_meta(doctype)
|
||||
if not meta.show_preview_popup:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
from typing import Any
|
||||
|
||||
import frappe
|
||||
from frappe.model import is_default_field
|
||||
from frappe.query_builder import Order
|
||||
|
|
@ -10,7 +12,7 @@ from frappe.query_builder.utils import DocType
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_list_settings(doctype):
|
||||
def get_list_settings(doctype: str):
|
||||
try:
|
||||
return frappe.get_cached_doc("List View Settings", doctype)
|
||||
except frappe.DoesNotExistError:
|
||||
|
|
@ -18,7 +20,7 @@ def get_list_settings(doctype):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_list_settings(doctype, values):
|
||||
def set_list_settings(doctype: str, values: str | dict[str, Any]):
|
||||
try:
|
||||
doc = frappe.get_doc("List View Settings", doctype)
|
||||
except frappe.DoesNotExistError:
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ def get_filters_for(doctype):
|
|||
|
||||
@frappe.whitelist()
|
||||
@frappe.read_only()
|
||||
def get_open_count(doctype: str, name: str, items=None):
|
||||
def get_open_count(doctype: str, name: str | int, items: str | list[str] | None = None):
|
||||
"""Get count for internal and external links for given transactions
|
||||
|
||||
:param doctype: Reference DocType
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
--desktop-blur: blur(10.2px);
|
||||
--desktop-modal-width: 590px;
|
||||
--desktop-modal-height: 450px;
|
||||
--folder-thumbnail-icon-height: 12px;
|
||||
--folder-thumbnail-icon-height: 16px;
|
||||
--desktop-icon-dimension: 54px;
|
||||
--folder-icon-background-color: var(--surface-gray-1);
|
||||
--desktop-modal-radius: 30px;
|
||||
|
|
@ -80,13 +80,24 @@
|
|||
margin-top: 60px;
|
||||
padding: 20px;
|
||||
}
|
||||
.icons-container:has(.sidebar-card){
|
||||
margin-top: 20px;
|
||||
.sidebar-card{
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
.modal
|
||||
.modal-body .icons-container,.folder-icon .icons-container {
|
||||
.modal-body .icons-container, .folder-icon .icons-container {
|
||||
padding:0px;
|
||||
margin: 0px;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.folder-icon .icons-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.icons{
|
||||
gap: 16px;
|
||||
display: grid;
|
||||
|
|
@ -103,6 +114,10 @@
|
|||
gap: 12px;
|
||||
padding: 13px 16px 12px 16px;
|
||||
position: relative;
|
||||
border-radius: 20px;
|
||||
border-width: 1px;
|
||||
border-style: dashed;
|
||||
border-color: transparent;
|
||||
}
|
||||
.desktop-icon.desktop-edit-mode .hide-button {
|
||||
display: flex;
|
||||
|
|
@ -123,6 +138,7 @@
|
|||
.icon-container{
|
||||
padding: 10px;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -248,13 +264,13 @@
|
|||
position: absolute;
|
||||
}
|
||||
|
||||
.folder-icon{
|
||||
border-radius: 10px;
|
||||
background-color: var(--folder-icon-background-color) !important;
|
||||
.folder-icon {
|
||||
border-radius: 16px;
|
||||
background-color: var(--folder-icon-background-color) !important;
|
||||
box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.14);
|
||||
padding: 7px;
|
||||
align-items: normal;
|
||||
box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.14), 0 1px 3px 0 rgba(0, 0, 0, 0.14);
|
||||
/* box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.14), 0 1px 3px 0 rgba(0, 0, 0, 0.14); */
|
||||
& .icons{
|
||||
gap: 2.1px;
|
||||
margin-top: 0px;
|
||||
|
|
@ -337,8 +353,7 @@
|
|||
}
|
||||
|
||||
.desktop-edit-mode{
|
||||
border: 1px dashed var(--outline-gray-2);
|
||||
border-radius: 20px;
|
||||
border-color: var(--outline-gray-2);
|
||||
}
|
||||
.edit-mode-buttons{
|
||||
display: none;
|
||||
|
|
@ -358,7 +373,7 @@
|
|||
:root {
|
||||
--desktop-icon-dimension: 50px;
|
||||
--desktop-icon-container: 117px;
|
||||
--folder-thumbnail-icon-height:17px;
|
||||
--folder-thumbnail-icon-height:15px;
|
||||
}
|
||||
|
||||
.desktop-container {
|
||||
|
|
@ -437,6 +452,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.icons-container {
|
||||
> .icons-container {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.desktop-edit{
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
|
|
@ -500,31 +521,72 @@
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
.title-widget{
|
||||
display: inline-block;
|
||||
.title-widget {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 1.5rem;
|
||||
cursor: text;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.title-input-label{
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
color: var(--neutral-white);
|
||||
line-height: 22px;
|
||||
z-index: 1;
|
||||
pointers-events: none;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.title-input-wrapper{
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
.title-widget--read-only {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.title-input-wrapper input{
|
||||
border: 1px solid transparent;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none;
|
||||
.title-widget--editable:hover .title-input-label {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.desktop-modal-heading .title-widget--read-only .title-input-label:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.desktop-modal-heading .title-widget .title-input-label {
|
||||
color: var(--neutral-white);
|
||||
font-size: var(--text-2xl);
|
||||
line-height: 1.3;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.desktop-modal-heading .title-widget--editable:hover .title-input-label {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.title-input-wrapper {
|
||||
display: inline-block;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.desktop-modal-heading .title-input-wrapper .title-input {
|
||||
color: var(--neutral-white);
|
||||
font-size: var(--text-2xl);
|
||||
line-height: 1.3;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
outline: none;
|
||||
min-width: 80px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.desktop-modal-heading .title-input-wrapper .title-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.desktop-modal-heading .title-input-wrapper .title-input:focus {
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
background-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.title-input-mirror {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
white-space: pre;
|
||||
font-size: var(--text-2xl);
|
||||
font-family: inherit;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue