").appendTo(frm.fields_dict.modules_html.wrapper);
frm.module_editor = new frappe.ModuleEditor(frm, module_area);
}
} else {
@@ -80,111 +97,147 @@ frappe.ui.form.on('User', {
}
}
},
- refresh: function(frm) {
+ refresh: function (frm) {
let doc = frm.doc;
if (frm.is_new()) {
frm.set_value("time_zone", frappe.sys_defaults.time_zone);
}
- if (in_list(['System User', 'Website User'], frm.doc.user_type)
- && !frm.is_new() && !frm.roles_editor && frm.can_edit_roles) {
+ if (
+ in_list(["System User", "Website User"], frm.doc.user_type) &&
+ !frm.is_new() &&
+ !frm.roles_editor &&
+ frm.can_edit_roles
+ ) {
frm.reload_doc();
return;
}
- if(doc.name===frappe.session.user && !doc.__unsaved
- && frappe.all_timezones
- && (doc.language || frappe.boot.user.language)
- && doc.language !== frappe.boot.user.language) {
+ const hasChanged = (doc_attr, boot_attr) => {
+ return doc_attr && boot_attr && doc_attr !== boot_attr;
+ };
+
+ if (
+ doc.name === frappe.session.user &&
+ !doc.__unsaved &&
+ frappe.all_timezones &&
+ (hasChanged(doc.language, frappe.boot.user.language) ||
+ hasChanged(doc.time_zone, frappe.boot.time_zone.user))
+ ) {
frappe.msgprint(__("Refreshing..."));
window.location.reload();
}
- frm.toggle_display(['sb1', 'sb3', 'modules_access'], false);
+ frm.toggle_display(["sb1", "sb3", "modules_access"], false);
- if(!frm.is_new()) {
- if(has_access_to_edit_user()) {
+ if (!frm.is_new()) {
+ if (has_access_to_edit_user()) {
+ frm.add_custom_button(
+ __("Set User Permissions"),
+ function () {
+ frappe.route_options = {
+ user: doc.name,
+ };
+ frappe.set_route("List", "User Permission");
+ },
+ __("Permissions")
+ );
- frm.add_custom_button(__("Set User Permissions"), function() {
- frappe.route_options = {
- "user": doc.name
- };
- frappe.set_route('List', 'User Permission');
- }, __("Permissions"));
+ frm.add_custom_button(
+ __("View Permitted Documents"),
+ () =>
+ frappe.set_route("query-report", "Permitted Documents For User", {
+ user: frm.doc.name,
+ }),
+ __("Permissions")
+ );
- frm.add_custom_button(__('View Permitted Documents'),
- () => frappe.set_route('query-report', 'Permitted Documents For User',
- {user: frm.doc.name}), __("Permissions"));
-
- frm.toggle_display(['sb1', 'sb3', 'modules_access'], true);
+ frm.toggle_display(["sb1", "sb3", "modules_access"], true);
}
- frm.add_custom_button(__("Reset Password"), function() {
- frappe.call({
- method: "frappe.core.doctype.user.user.reset_password",
- args: {
- "user": frm.doc.name
- }
- });
- }, __("Password"));
+ frm.add_custom_button(
+ __("Reset Password"),
+ function () {
+ frappe.call({
+ method: "frappe.core.doctype.user.user.reset_password",
+ args: {
+ user: frm.doc.name,
+ },
+ });
+ },
+ __("Password")
+ );
if (frappe.user.has_role("System Manager")) {
frappe.db.get_single_value("LDAP Settings", "enabled").then((value) => {
if (value === 1 && frm.doc.name != "Administrator") {
- frm.add_custom_button(__("Reset LDAP Password"), function() {
- const d = new frappe.ui.Dialog({
- title: __("Reset LDAP Password"),
- fields: [
- {
- label: __("New Password"),
- fieldtype: "Password",
- fieldname: "new_password",
- reqd: 1
+ frm.add_custom_button(
+ __("Reset LDAP Password"),
+ function () {
+ const d = new frappe.ui.Dialog({
+ title: __("Reset LDAP Password"),
+ fields: [
+ {
+ label: __("New Password"),
+ fieldtype: "Password",
+ fieldname: "new_password",
+ reqd: 1,
+ },
+ {
+ label: __("Confirm New Password"),
+ fieldtype: "Password",
+ fieldname: "confirm_password",
+ reqd: 1,
+ },
+ {
+ label: __("Logout All Sessions"),
+ fieldtype: "Check",
+ fieldname: "logout_sessions",
+ },
+ ],
+ 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,
+ }
+ );
},
- {
- label: __("Confirm New Password"),
- fieldtype: "Password",
- fieldname: "confirm_password",
- reqd: 1
- },
- {
- label: __("Logout All Sessions"),
- fieldtype: "Check",
- fieldname: "logout_sessions"
- }
- ],
- 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
- });
- }
- });
- d.show();
- }, __("Password"));
+ });
+ d.show();
+ },
+ __("Password")
+ );
}
});
}
- if (frappe.session.user == doc.name || frappe.user.has_role("System Manager")) {
- frm.add_custom_button(__("Reset OTP Secret"), function() {
- frappe.call({
- method: "frappe.twofactor.reset_otp_secret",
- args: {
- "user": frm.doc.name
- }
- });
- }, __("Password"));
+ if (
+ cint(frappe.boot.sysdefaults.enable_two_factor_auth) &&
+ (frappe.session.user == doc.name || frappe.user.has_role("System Manager"))
+ ) {
+ frm.add_custom_button(
+ __("Reset OTP Secret"),
+ function () {
+ frappe.call({
+ method: "frappe.twofactor.reset_otp_secret",
+ args: {
+ user: frm.doc.name,
+ },
+ });
+ },
+ __("Password")
+ );
}
- frm.trigger('enabled');
+ frm.trigger("enabled");
if (frm.roles_editor && frm.can_edit_roles) {
frm.roles_editor.disable = frm.doc.role_profile_name ? 1 : 0;
@@ -193,10 +246,12 @@ frappe.ui.form.on('User', {
frm.module_editor && frm.module_editor.show();
- if(frappe.session.user==doc.name) {
+ if (frappe.session.user == doc.name) {
// update display settings
- if(doc.user_image) {
- frappe.boot.user_info[frappe.session.user].image = frappe.utils.get_file_link(doc.user_image);
+ if (doc.user_image) {
+ frappe.boot.user_info[frappe.session.user].image = frappe.utils.get_file_link(
+ doc.user_image
+ );
}
}
}
@@ -208,51 +263,51 @@ frappe.ui.form.on('User', {
}
}
if (!found) {
- frm.add_custom_button(__("Create User Email"), function() {
+ frm.add_custom_button(__("Create User Email"), function () {
frm.events.create_user_email(frm);
});
}
}
- if (frappe.route_flags.unsaved===1){
+ if (frappe.route_flags.unsaved === 1) {
delete frappe.route_flags.unsaved;
- for ( var i=0;i
{
+ child_row.used_oauth = value.auth_method === "OAuth";
+ frm.refresh_field("user_emails", cdn, "used_oauth");
+ }
+ );
+ },
});
function has_access_to_edit_user() {
@@ -292,7 +362,10 @@ function has_access_to_edit_user() {
}
function get_roles_for_editing_user() {
- return frappe.get_meta('User').permissions
- .filter(perm => perm.permlevel >= 1 && perm.write)
- .map(perm => perm.role) || ['System Manager'];
+ return (
+ frappe
+ .get_meta("User")
+ .permissions.filter((perm) => perm.permlevel >= 1 && perm.write)
+ .map((perm) => perm.role) || ["System Manager"]
+ );
}
diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json
index 82e3fa71f3..00e1cffa88 100644
--- a/frappe/core/doctype/user/user.json
+++ b/frappe/core/doctype/user/user.json
@@ -7,6 +7,7 @@
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
+ "user_details_tab",
"enabled",
"section_break_3",
"email",
@@ -22,23 +23,31 @@
"send_welcome_email",
"unsubscribed",
"user_image",
+ "roles_permissions_tab",
"sb1",
"role_profile_name",
"roles_html",
"roles",
+ "sb_allow_modules",
+ "module_profile",
+ "modules_html",
+ "block_modules",
+ "home_settings",
"short_bio",
"gender",
"birth_date",
"interest",
- "banner_image",
- "desk_theme",
"column_break_26",
"phone",
"location",
"bio",
- "mute_sounds",
"column_break_22",
"mobile_no",
+ "settings_tab",
+ "desk_settings_section",
+ "mute_sounds",
+ "desk_theme",
+ "banner_image",
"change_password",
"new_password",
"logout_all_sessions",
@@ -61,11 +70,6 @@
"send_me_a_copy",
"allowed_in_mentions",
"user_emails",
- "sb_allow_modules",
- "module_profile",
- "modules_html",
- "block_modules",
- "home_settings",
"sb2",
"defaults",
"sb3",
@@ -87,7 +91,8 @@
"api_key",
"generate_keys",
"column_break_65",
- "api_secret"
+ "api_secret",
+ "connections_tab"
],
"fields": [
{
@@ -126,7 +131,7 @@
{
"fieldname": "middle_name",
"fieldtype": "Data",
- "label": "Middle Name (Optional)",
+ "label": "Middle Name",
"oldfieldname": "middle_name",
"oldfieldtype": "Data"
},
@@ -232,7 +237,7 @@
"collapsible": 1,
"depends_on": "enabled",
"fieldname": "short_bio",
- "fieldtype": "Section Break",
+ "fieldtype": "Tab Break",
"label": "More Information"
},
{
@@ -398,7 +403,6 @@
"permlevel": 1
},
{
- "collapsible": 1,
"depends_on": "eval:in_list(['System User'], doc.user_type)",
"fieldname": "sb_allow_modules",
"fieldtype": "Section Break",
@@ -492,7 +496,7 @@
{
"description": "Restrict user from this IP address only. Multiple IP addresses can be added by separating with commas. Also accepts partial IP addresses like (111.111.111)",
"fieldname": "restrict_ip",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"label": "Restrict IP",
"permlevel": 1
},
@@ -615,13 +619,13 @@
"options": "Module Profile"
},
{
- "description": "Stores the datetime when the last reset password key was generated.",
- "fieldname": "last_reset_password_key_generated_on",
- "fieldtype": "Datetime",
- "hidden": 1,
- "label": "Last Reset Password Key Generated On",
- "read_only": 1
- },
+ "description": "Stores the datetime when the last reset password key was generated.",
+ "fieldname": "last_reset_password_key_generated_on",
+ "fieldtype": "Datetime",
+ "hidden": 1,
+ "label": "Last Reset Password Key Generated On",
+ "read_only": 1
+ },
{
"fieldname": "column_break_75",
"fieldtype": "Column Break"
@@ -648,18 +652,45 @@
"label": "Auto follow documents that you Like"
},
{
- "default": "0",
- "depends_on": "eval:(doc.document_follow_notify== 1)",
- "fieldname": "follow_shared_documents",
- "fieldtype": "Check",
- "label": "Auto follow documents that are shared with you"
- },
+ "default": "0",
+ "depends_on": "eval:(doc.document_follow_notify== 1)",
+ "fieldname": "follow_shared_documents",
+ "fieldtype": "Check",
+ "label": "Auto follow documents that are shared with you"
+ },
{
"default": "0",
"depends_on": "eval:(doc.document_follow_notify== 1)",
"fieldname": "follow_assigned_documents",
"fieldtype": "Check",
"label": "Auto follow documents that are assigned to you"
+ },
+ {
+ "fieldname": "user_details_tab",
+ "fieldtype": "Tab Break",
+ "label": "User Details"
+ },
+ {
+ "fieldname": "roles_permissions_tab",
+ "fieldtype": "Tab Break",
+ "label": "Roles & Permissions"
+ },
+ {
+ "fieldname": "settings_tab",
+ "fieldtype": "Tab Break",
+ "label": "Settings"
+ },
+ {
+ "fieldname": "connections_tab",
+ "fieldtype": "Tab Break",
+ "label": "Connections",
+ "show_dashboard": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "desk_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Desk Settings"
}
],
"icon": "fa fa-user",
@@ -722,7 +753,7 @@
"link_fieldname": "user"
}
],
- "modified": "2022-05-25 01:00:51.345319",
+ "modified": "2022-09-19 16:05:46.485242",
"modified_by": "Administrator",
"module": "Core",
"name": "User",
@@ -738,7 +769,6 @@
"read": 1,
"report": 1,
"role": "System Manager",
- "set_user_permissions": 1,
"share": 1,
"write": 1
},
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 12a48afe7e..14266e4cd8 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -1,14 +1,13 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
from datetime import timedelta
-
-from bs4 import BeautifulSoup
+from typing import Optional, Sequence
import frappe
import frappe.defaults
import frappe.permissions
import frappe.share
-from frappe import _, msgprint, throw
+from frappe import STANDARD_USERS, _, msgprint, throw
from frappe.core.doctype.user_type.user_type import user_linked_with_permission_on_doctype
from frappe.desk.doctype.notification_settings.notification_settings import (
create_notification_settings,
@@ -24,7 +23,7 @@ from frappe.utils import (
flt,
format_datetime,
get_formatted_email,
- get_time_zone,
+ get_system_timezone,
has_gravatar,
now_datetime,
today,
@@ -34,8 +33,6 @@ from frappe.utils.password import update_password as _update_password
from frappe.utils.user import get_system_managers
from frappe.website.utils import is_signup_disabled
-STANDARD_USERS = frappe.STANDARD_USERS
-
class User(Document):
__new_password = None
@@ -55,7 +52,7 @@ class User(Document):
def onload(self):
from frappe.config import get_modules_from_all_apps
- self.set_onload("all_modules", [m.get("module_name") for m in get_modules_from_all_apps()])
+ self.set_onload("all_modules", sorted(m.get("module_name") for m in get_modules_from_all_apps()))
def before_insert(self):
self.flags.in_insert = True
@@ -125,10 +122,20 @@ class User(Document):
now = frappe.flags.in_test or frappe.flags.in_install
self.send_password_notification(self.__new_password)
frappe.enqueue(
- "frappe.core.doctype.user.user.create_contact", user=self, ignore_mandatory=True, now=now
+ "frappe.core.doctype.user.user.create_contact",
+ user=self,
+ ignore_mandatory=True,
+ now=now,
+ enqueue_after_commit=True,
)
- if self.name not in ("Administrator", "Guest") and not self.user_image:
- frappe.enqueue("frappe.core.doctype.user.user.update_gravatar", name=self.name, now=now)
+
+ if self.name not in STANDARD_USERS and not self.user_image:
+ frappe.enqueue(
+ "frappe.core.doctype.user.user.update_gravatar",
+ name=self.name,
+ now=now,
+ enqueue_after_commit=True,
+ )
# Set user selected timezone
if self.time_zone:
@@ -239,7 +246,10 @@ class User(Document):
)
def share_with_self(self):
- frappe.share.add(
+ if self.name in STANDARD_USERS:
+ return
+
+ frappe.share.add_docshare(
self.doctype, self.name, self.name, write=1, share=1, flags={"ignore_share_permission": True}
)
@@ -304,12 +314,10 @@ class User(Document):
.from_(user_role_doctype)
.select(user_doctype.name)
.where(user_role_doctype.role == "System Manager")
- .where(user_doctype.docstatus < 2)
.where(user_doctype.enabled == 1)
.where(user_role_doctype.parent == user_doctype.name)
.where(user_role_doctype.parent.notin(["Administrator", self.name]))
.limit(1)
- .distinct()
).run()
def get_fullname(self):
@@ -473,7 +481,7 @@ class User(Document):
frappe.rename_doc("Notification Settings", old_name, new_name, force=True, show_alert=False)
# set email
- frappe.db.update("User", new_name, "email", new_name)
+ frappe.db.set_value("User", new_name, "email", new_name)
def append_roles(self, *roles):
"""Add roles to user"""
@@ -538,11 +546,11 @@ class User(Document):
if self.__new_password:
user_data = (self.first_name, self.middle_name, self.last_name, self.email, self.birth_date)
- result = test_password_strength(self.__new_password, "", None, user_data)
+ result = test_password_strength(self.__new_password, user_data=user_data)
feedback = result.get("feedback", None)
if feedback and not feedback.get("password_policy_validation_passed", False):
- handle_password_test_fail(result)
+ handle_password_test_fail(feedback)
def suggest_username(self):
def _check_suggestion(suggestion):
@@ -581,7 +589,7 @@ class User(Document):
if len(email_accounts) != len(set(email_accounts)):
frappe.throw(_("Email Account added multiple times"))
- def get_social_login_userid(self, provider):
+ def get_social_login_userid(self, provider: str):
try:
for p in self.social_logins:
if p.provider == provider:
@@ -611,10 +619,10 @@ class User(Document):
"""
login_with_mobile = cint(
- frappe.db.get_value("System Settings", "System Settings", "allow_login_using_mobile_number")
+ frappe.db.get_single_value("System Settings", "allow_login_using_mobile_number")
)
login_with_username = cint(
- frappe.db.get_value("System Settings", "System Settings", "allow_login_using_user_name")
+ frappe.db.get_single_value("System Settings", "allow_login_using_user_name")
)
or_filters = [{"name": user_name}]
@@ -623,7 +631,7 @@ class User(Document):
if login_with_username:
or_filters.append({"username": user_name})
- users = frappe.db.get_all("User", fields=["name", "enabled"], or_filters=or_filters, limit=1)
+ users = frappe.get_all("User", fields=["name", "enabled"], or_filters=or_filters, limit=1)
if not users:
return
@@ -639,7 +647,7 @@ class User(Document):
def set_time_zone(self):
if not self.time_zone:
- self.time_zone = get_time_zone()
+ self.time_zone = get_system_timezone()
@frappe.whitelist()
@@ -679,16 +687,23 @@ def get_perm_info(role):
@frappe.whitelist(allow_guest=True)
-def update_password(new_password, logout_all_sessions=0, key=None, old_password=None):
- # validate key to avoid key input like ['like', '%'], '', ['in', ['']]
- if key and not isinstance(key, str):
- frappe.throw(_("Invalid key type"))
+def update_password(
+ new_password: str, logout_all_sessions: int = 0, key: str = None, old_password: str = None
+):
+ """Update password for the current user.
- result = test_password_strength(new_password, key, old_password)
+ Args:
+ new_password (str): New password.
+ logout_all_sessions (int, optional): If set to 1, all other sessions will be logged out. Defaults to 0.
+ key (str, optional): Password reset key. Defaults to None.
+ old_password (str, optional): Old password. Defaults to None.
+ """
+
+ result = test_password_strength(new_password)
feedback = result.get("feedback", None)
if feedback and not feedback.get("password_policy_validation_passed", False):
- handle_password_test_fail(result)
+ handle_password_test_fail(feedback)
res = _get_user_for_update_password(key, old_password)
if res.get("message"):
@@ -718,22 +733,22 @@ def update_password(new_password, logout_all_sessions=0, key=None, old_password=
if user_doc.user_type == "System User":
return "/app"
else:
- return redirect_url if redirect_url else "/"
+ return redirect_url or "/"
@frappe.whitelist(allow_guest=True)
-def test_password_strength(new_password, key=None, old_password=None, user_data=None):
+def test_password_strength(
+ new_password: str, key=None, old_password=None, user_data: tuple | None = None
+):
+ from frappe.utils.deprecations import deprecation_warning
from frappe.utils.password_strength import test_password_strength as _test_password_strength
- password_policy = (
- frappe.db.get_value(
- "System Settings", None, ["enable_password_policy", "minimum_password_score"], as_dict=True
+ if key is not None or old_password is not None:
+ deprecation_warning(
+ "Arguments `key` and `old_password` are deprecated in function `test_password_strength`."
)
- or {}
- )
- enable_password_policy = cint(password_policy.get("enable_password_policy", 0))
- minimum_password_score = cint(password_policy.get("minimum_password_score", 0))
+ enable_password_policy = frappe.get_system_settings("enable_password_policy") or 0
if not enable_password_policy:
return {}
@@ -746,6 +761,7 @@ def test_password_strength(new_password, key=None, old_password=None, user_data=
if new_password:
result = _test_password_strength(new_password, user_inputs=user_data)
password_policy_validation_passed = False
+ minimum_password_score = cint(frappe.get_system_settings("minimum_password_score")) or 0
# score should be greater than 0 and minimum_password_score
if result.get("score") and result.get("score") >= minimum_password_score:
@@ -755,9 +771,8 @@ def test_password_strength(new_password, key=None, old_password=None, user_data=
return result
-# for login
@frappe.whitelist()
-def has_email_account(email):
+def has_email_account(email: str):
return frappe.get_list("Email Account", filters={"email_id": email})
@@ -824,7 +839,7 @@ def verify_password(password):
@frappe.whitelist(allow_guest=True)
-def sign_up(email, full_name, redirect_to):
+def sign_up(email: str, full_name: str, redirect_to: str) -> tuple[int, str]:
if is_signup_disabled():
frappe.throw(_("Sign Up is disabled"), title=_("Not Allowed"))
@@ -861,7 +876,7 @@ def sign_up(email, full_name, redirect_to):
user.insert()
# set default signup role as per Portal Settings
- default_role = frappe.db.get_value("Portal Settings", None, "default_role")
+ default_role = frappe.db.get_single_value("Portal Settings", "default_role")
if default_role:
user.add_roles(default_role)
@@ -876,12 +891,12 @@ def sign_up(email, full_name, redirect_to):
@frappe.whitelist(allow_guest=True)
@rate_limit(limit=get_password_reset_limit, seconds=24 * 60 * 60, methods=["POST"])
-def reset_password(user):
+def reset_password(user: str) -> str:
if user == "Administrator":
return "not allowed"
try:
- user = frappe.get_doc("User", user)
+ user: User = frappe.get_doc("User", user)
if not user.enabled:
return "disabled"
@@ -893,7 +908,7 @@ def reset_password(user):
title=_("Password Email Sent"),
)
except frappe.DoesNotExistError:
- frappe.local.response["http_status_code"] = 400
+ frappe.local.response["http_status_code"] = 404
frappe.clear_messages()
return "not found"
@@ -903,6 +918,7 @@ def reset_password(user):
def user_query(doctype, txt, searchfield, start, page_len, filters):
from frappe.desk.reportview import get_filters_cond, get_match_cond
+ doctype = "User"
conditions = []
user_type_condition = "and user_type != 'Website User'"
@@ -1044,31 +1060,15 @@ def notify_admin_access_to_system_manager(login_manager=None):
)
-def extract_mentions(txt):
- """Find all instances of @mentions in the html."""
- soup = BeautifulSoup(txt, "html.parser")
- emails = []
- for mention in soup.find_all(class_="mention"):
- if mention.get("data-is-group") == "true":
- try:
- user_group = frappe.get_cached_doc("User Group", mention["data-id"])
- emails += [d.user for d in user_group.user_group_members]
- except frappe.DoesNotExistError:
- pass
- continue
- email = mention["data-id"]
- emails.append(email)
+def handle_password_test_fail(feedback: dict):
+ # Backward compatibility
+ if "feedback" in feedback:
+ feedback = feedback["feedback"]
- return emails
+ suggestions = feedback.get("suggestions", [])
+ warning = feedback.get("warning", "")
-
-def handle_password_test_fail(result):
- suggestions = result["feedback"]["suggestions"][0] if result["feedback"]["suggestions"] else ""
- warning = result["feedback"]["warning"] if "warning" in result["feedback"] else ""
- suggestions += (
- " " + _("Hint: Include symbols, numbers and capital letters in the password") + " "
- )
- frappe.throw(" ".join([_("Invalid Password:"), warning, suggestions]))
+ frappe.throw(msg=" ".join([warning] + suggestions), title=_("Invalid Password"))
def update_gravatar(name):
@@ -1086,13 +1086,12 @@ def throttle_user_creation():
@frappe.whitelist()
-def get_role_profile(role_profile):
- roles = frappe.get_doc("Role Profile", {"role_profile": role_profile})
- return roles.roles
+def get_role_profile(role_profile: str):
+ return frappe.get_doc("Role Profile", {"role_profile": role_profile}).roles
@frappe.whitelist()
-def get_module_profile(module_profile):
+def get_module_profile(module_profile: str):
module_profile = frappe.get_doc("Module Profile", {"module_profile_name": module_profile})
return module_profile.get("block_modules")
@@ -1165,14 +1164,14 @@ def get_restricted_ip_list(user):
@frappe.whitelist()
-def generate_keys(user):
+def generate_keys(user: str):
"""
generate api key and api secret
:param user: str
"""
frappe.only_for("System Manager")
- user_details = frappe.get_doc("User", user)
+ user_details: User = frappe.get_doc("User", user)
api_secret = frappe.generate_hash(length=15)
# if api key is not set generate api key
if not user_details.api_key:
diff --git a/frappe/core/doctype/user/user_list.js b/frappe/core/doctype/user/user_list.js
index 5632edf0cc..334ed0b370 100644
--- a/frappe/core/doctype/user/user_list.js
+++ b/frappe/core/doctype/user/user_list.js
@@ -1,19 +1,19 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
-frappe.listview_settings['User'] = {
+frappe.listview_settings["User"] = {
add_fields: ["enabled", "user_type", "user_image"],
- filters: [["enabled","=",1]],
- prepare_data: function(data) {
+ filters: [["enabled", "=", 1]],
+ prepare_data: function (data) {
data["user_for_avatar"] = data["name"];
},
- get_indicator: function(doc) {
- if(doc.enabled) {
+ get_indicator: function (doc) {
+ if (doc.enabled) {
return [__("Active"), "green", "enabled,=,1"];
} else {
return [__("Disabled"), "grey", "enabled,=,0"];
}
- }
+ },
};
frappe.help.youtube_id["User"] = "8Slw1hsTmUI";
diff --git a/frappe/core/doctype/user_group/test_user_group.py b/frappe/core/doctype/user_group/test_user_group.py
index 368f4eaef2..79e013672d 100644
--- a/frappe/core/doctype/user_group/test_user_group.py
+++ b/frappe/core/doctype/user_group/test_user_group.py
@@ -1,8 +1,8 @@
# Copyright (c) 2021, Frappe Technologies and Contributors
# License: MIT. See LICENSE
# import frappe
-import unittest
+from frappe.tests.utils import FrappeTestCase
-class TestUserGroup(unittest.TestCase):
+class TestUserGroup(FrappeTestCase):
pass
diff --git a/frappe/core/doctype/user_group/user_group.js b/frappe/core/doctype/user_group/user_group.js
index 2aa9b68658..cab1f5dff1 100644
--- a/frappe/core/doctype/user_group/user_group.js
+++ b/frappe/core/doctype/user_group/user_group.js
@@ -1,8 +1,7 @@
// Copyright (c) 2021, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('User Group', {
+frappe.ui.form.on("User Group", {
// refresh: function(frm) {
-
// }
});
diff --git a/frappe/core/doctype/user_group_member/test_user_group_member.py b/frappe/core/doctype/user_group_member/test_user_group_member.py
index 5d709d0bec..a2eb5c7bfc 100644
--- a/frappe/core/doctype/user_group_member/test_user_group_member.py
+++ b/frappe/core/doctype/user_group_member/test_user_group_member.py
@@ -1,8 +1,8 @@
# Copyright (c) 2021, Frappe Technologies and Contributors
# License: MIT. See LICENSE
# import frappe
-import unittest
+from frappe.tests.utils import FrappeTestCase
-class TestUserGroupMember(unittest.TestCase):
+class TestUserGroupMember(FrappeTestCase):
pass
diff --git a/frappe/core/doctype/user_group_member/user_group_member.js b/frappe/core/doctype/user_group_member/user_group_member.js
index 0b2dbe0d46..4c4011c8b4 100644
--- a/frappe/core/doctype/user_group_member/user_group_member.js
+++ b/frappe/core/doctype/user_group_member/user_group_member.js
@@ -1,8 +1,7 @@
// Copyright (c) 2021, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('User Group Member', {
+frappe.ui.form.on("User Group Member", {
// refresh: function(frm) {
-
// }
});
diff --git a/frappe/core/doctype/user_permission/test_user_permission.py b/frappe/core/doctype/user_permission/test_user_permission.py
index b963da2f49..8742d2e040 100644
--- a/frappe/core/doctype/user_permission/test_user_permission.py
+++ b/frappe/core/doctype/user_permission/test_user_permission.py
@@ -1,7 +1,5 @@
# Copyright (c) 2021, Frappe Technologies and Contributors
# See LICENSE
-import unittest
-
import frappe
from frappe.core.doctype.doctype.test_doctype import new_doctype
from frappe.core.doctype.user_permission.user_permission import (
@@ -9,10 +7,11 @@ from frappe.core.doctype.user_permission.user_permission import (
remove_applicable,
)
from frappe.permissions import has_user_permission
+from frappe.tests.utils import FrappeTestCase
from frappe.website.doctype.blog_post.test_blog_post import make_test_blog
-class TestUserPermission(unittest.TestCase):
+class TestUserPermission(FrappeTestCase):
def setUp(self):
test_users = (
"test_bulk_creation_update@example.com",
@@ -278,7 +277,7 @@ def create_user(email, *roles):
user = frappe.new_doc("User")
user.email = email
- user.first_name = email.split("@")[0]
+ user.first_name = email.split("@", 1)[0]
if not roles:
roles = ("System Manager",)
diff --git a/frappe/core/doctype/user_permission/user_permission.js b/frappe/core/doctype/user_permission/user_permission.js
index f6989db5d8..39ee4348b9 100644
--- a/frappe/core/doctype/user_permission/user_permission.js
+++ b/frappe/core/doctype/user_permission/user_permission.js
@@ -1,59 +1,58 @@
// Copyright (c) 2017, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('User Permission', {
- setup: frm => {
+frappe.ui.form.on("User Permission", {
+ setup: (frm) => {
frm.set_query("allow", () => {
return {
- "filters": {
+ filters: {
issingle: 0,
- istable: 0
- }
+ istable: 0,
+ },
};
});
- frm.set_query('applicable_for', () => {
+ frm.set_query("applicable_for", () => {
return {
- 'query': 'frappe.core.doctype.user_permission.user_permission.get_applicable_for_doctype_list',
- 'doctype': frm.doc.allow
+ query: "frappe.core.doctype.user_permission.user_permission.get_applicable_for_doctype_list",
+ doctype: frm.doc.allow,
};
});
-
},
- refresh: frm => {
- frm.add_custom_button(__('View Permitted Documents'),
- () => frappe.set_route('query-report', 'Permitted Documents For User',
- { user: frm.doc.user }));
- frm.trigger('set_applicable_for_constraint');
- frm.trigger('toggle_hide_descendants');
+ refresh: (frm) => {
+ frm.add_custom_button(__("View Permitted Documents"), () =>
+ frappe.set_route("query-report", "Permitted Documents For User", {
+ user: frm.doc.user,
+ })
+ );
+ frm.trigger("set_applicable_for_constraint");
+ frm.trigger("toggle_hide_descendants");
},
- allow: frm => {
+ allow: (frm) => {
if (frm.doc.allow) {
if (frm.doc.for_value) {
- frm.set_value('for_value', null);
+ frm.set_value("for_value", null);
}
- frm.trigger('toggle_hide_descendants');
+ frm.trigger("toggle_hide_descendants");
}
},
- apply_to_all_doctypes: frm => {
- frm.trigger('set_applicable_for_constraint');
+ apply_to_all_doctypes: (frm) => {
+ frm.trigger("set_applicable_for_constraint");
},
- set_applicable_for_constraint: frm => {
- frm.toggle_reqd('applicable_for', !frm.doc.apply_to_all_doctypes);
+ set_applicable_for_constraint: (frm) => {
+ frm.toggle_reqd("applicable_for", !frm.doc.apply_to_all_doctypes);
if (frm.doc.apply_to_all_doctypes && frm.doc.applicable_for) {
- frm.set_value('applicable_for', null, null, true);
+ frm.set_value("applicable_for", null, null, true);
}
},
- toggle_hide_descendants: frm => {
+ toggle_hide_descendants: (frm) => {
let show = frappe.boot.nested_set_doctypes.includes(frm.doc.allow);
- frm.toggle_display('hide_descendants', show);
- }
-
-
+ frm.toggle_display("hide_descendants", show);
+ },
});
diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py
index 2dfd7863b1..63c1f40512 100644
--- a/frappe/core/doctype/user_permission/user_permission.py
+++ b/frappe/core/doctype/user_permission/user_permission.py
@@ -18,16 +18,16 @@ class UserPermission(Document):
def on_update(self):
frappe.cache().hdel("user_permissions", self.user)
- frappe.publish_realtime("update_user_permissions")
+ frappe.publish_realtime("update_user_permissions", user=self.user, after_commit=True)
- def on_trash(self): # pylint: disable=no-self-use
+ def on_trash(self):
frappe.cache().hdel("user_permissions", self.user)
- frappe.publish_realtime("update_user_permissions")
+ frappe.publish_realtime("update_user_permissions", user=self.user, after_commit=True)
def validate_user_permission(self):
"""checks for duplicate user permission records"""
- duplicate_exists = frappe.db.get_all(
+ duplicate_exists = frappe.get_all(
self.doctype,
filters={
"allow": self.allow,
diff --git a/frappe/core/doctype/user_permission/user_permission_list.js b/frappe/core/doctype/user_permission/user_permission_list.js
index 0ce66fa8e3..ce5e624403 100644
--- a/frappe/core/doctype/user_permission/user_permission_list.js
+++ b/frappe/core/doctype/user_permission/user_permission_list.js
@@ -1,18 +1,17 @@
-frappe.listview_settings['User Permission'] = {
-
- onload: function(list_view) {
+frappe.listview_settings["User Permission"] = {
+ onload: function (list_view) {
var me = this;
- list_view.page.add_inner_button( __("Add / Update"), function() {
- let dialog =new frappe.ui.Dialog({
- title : __('Add User Permissions'),
+ list_view.page.add_inner_button(__("Add / Update"), function () {
+ let dialog = new frappe.ui.Dialog({
+ title: __("Add User Permissions"),
fields: [
{
- fieldname: 'user',
- label: __('For User'),
- fieldtype: 'Link',
- options: 'User',
+ fieldname: "user",
+ label: __("For User"),
+ fieldtype: "Link",
+ options: "User",
reqd: 1,
- onchange: function() {
+ onchange: function () {
dialog.fields_dict.doctype.set_input(undefined);
dialog.fields_dict.docname.set_input(undefined);
dialog.set_df_property("docname", "hidden", 1);
@@ -20,77 +19,87 @@ frappe.listview_settings['User Permission'] = {
dialog.set_df_property("apply_to_all_doctypes", "hidden", 1);
dialog.set_df_property("applicable_doctypes", "hidden", 1);
dialog.set_df_property("hide_descendants", "hidden", 1);
- }
+ },
},
{
- fieldname: 'doctype',
- label: __('Document Type'),
- fieldtype: 'Link',
- options: 'DocType',
+ fieldname: "doctype",
+ label: __("Document Type"),
+ fieldtype: "Link",
+ options: "DocType",
reqd: 1,
- onchange: function() {
+ onchange: function () {
me.on_doctype_change(dialog);
- }
+ },
},
{
- fieldname: 'docname',
- label: __('Document Name'),
- fieldtype: 'Dynamic Link',
- options: 'doctype',
+ fieldname: "docname",
+ label: __("Document Name"),
+ fieldtype: "Dynamic Link",
+ options: "doctype",
hidden: 1,
- onchange: function() {
+ onchange: function () {
let field = dialog.fields_dict["docname"];
- if(field.value != field.last_value) {
- if(dialog.fields_dict.doctype.value && dialog.fields_dict.docname.value && dialog.fields_dict.user.value){
- me.get_applicable_doctype(dialog).then(applicable => {
- me.get_multi_select_options(dialog, applicable).then(options => {
- me.applicable_options = options;
- me.on_docname_change(dialog, options, applicable);
- if(options.length > 5){
- dialog.fields_dict.applicable_doctypes.setup_select_all();
+ if (field.value != field.last_value) {
+ if (
+ dialog.fields_dict.doctype.value &&
+ dialog.fields_dict.docname.value &&
+ dialog.fields_dict.user.value
+ ) {
+ me.get_applicable_doctype(dialog).then((applicable) => {
+ me.get_multi_select_options(dialog, applicable).then(
+ (options) => {
+ me.applicable_options = options;
+ me.on_docname_change(dialog, options, applicable);
+ if (options.length > 5) {
+ dialog.fields_dict.applicable_doctypes.setup_select_all();
+ }
}
- });
+ );
});
}
}
- }
+ },
},
{
fieldtype: "Section Break",
- hide_border: 1
+ hide_border: 1,
},
{
- fieldname: 'is_default',
- label: __('Is Default'),
- fieldtype: 'Check',
- hidden: 1
- },
- {
- fieldname: 'apply_to_all_doctypes',
- label: __('Apply to all Documents Types'),
- fieldtype: 'Check',
+ fieldname: "is_default",
+ label: __("Is Default"),
+ fieldtype: "Check",
hidden: 1,
- onchange: function() {
- if(dialog.fields_dict.doctype.value && dialog.fields_dict.docname.value && dialog.fields_dict.user.value){
+ },
+ {
+ fieldname: "apply_to_all_doctypes",
+ label: __("Apply to all Documents Types"),
+ fieldtype: "Check",
+ hidden: 1,
+ onchange: function () {
+ if (
+ dialog.fields_dict.doctype.value &&
+ dialog.fields_dict.docname.value &&
+ dialog.fields_dict.user.value
+ ) {
me.on_apply_to_all_doctypes_change(dialog, me.applicable_options);
- if(me.applicable_options.length > 5){
+ if (me.applicable_options.length > 5) {
dialog.fields_dict.applicable_doctypes.setup_select_all();
}
}
- }
+ },
},
{
- fieldtype: "Column Break"
+ fieldtype: "Column Break",
},
{
- fieldname: 'hide_descendants',
- label: __('Hide Descendants'),
- fieldtype: 'Check',
- hidden: 1
+ fieldname: "hide_descendants",
+ label: __("Hide Descendants"),
+ fieldtype: "Check",
+ hidden: 1,
},
{
fieldtype: "Section Break",
- hide_border: 1
+ hide_border: 1,
},
{
label: __("Applicable Document Types"),
@@ -98,7 +107,7 @@ frappe.listview_settings['User Permission'] = {
fieldtype: "MultiCheck",
options: [],
columns: 2,
- hidden: 1
+ hidden: 1,
},
],
primary_action: (data) => {
@@ -107,126 +116,137 @@ frappe.listview_settings['User Permission'] = {
async: false,
method: "frappe.core.doctype.user_permission.user_permission.add_user_permissions",
args: {
- data : data
+ data: data,
},
- callback: function(r) {
- if(r.message === 1) {
- frappe.show_alert({message:__("User Permissions created sucessfully"), indicator:'blue'});
+ callback: function (r) {
+ if (r.message === 1) {
+ frappe.show_alert({
+ message: __("User Permissions created sucessfully"),
+ indicator: "blue",
+ });
} else {
- frappe.show_alert({message:__("Nothing to update"), indicator:'red'});
-
+ frappe.show_alert({
+ message: __("Nothing to update"),
+ indicator: "red",
+ });
}
- }
+ },
});
dialog.hide();
list_view.refresh();
},
- primary_action_label: __('Submit')
+ primary_action_label: __("Submit"),
});
dialog.show();
});
- list_view.page.add_inner_button( __("Bulk Delete"), function() {
+ list_view.page.add_inner_button(__("Bulk Delete"), function () {
const dialog = new frappe.ui.Dialog({
- title: __('Clear User Permissions'),
+ title: __("Clear User Permissions"),
fields: [
{
- fieldname: 'user',
- label: __('For User'),
- fieldtype: 'Link',
- options: 'User',
- reqd: 1
+ fieldname: "user",
+ label: __("For User"),
+ fieldtype: "Link",
+ options: "User",
+ reqd: 1,
},
{
- fieldname: 'for_doctype',
- label: __('For Document Type'),
- fieldtype: 'Link',
- options: 'DocType',
- reqd: 1
+ fieldname: "for_doctype",
+ label: __("For Document Type"),
+ fieldtype: "Link",
+ options: "DocType",
+ reqd: 1,
},
],
primary_action: (data) => {
// mandatory not filled
if (!data) return;
- frappe.confirm(__('Are you sure?'), () => {
+ frappe.confirm(__("Are you sure?"), () => {
frappe
- .xcall('frappe.core.doctype.user_permission.user_permission.clear_user_permissions', data)
- .then(data => {
+ .xcall(
+ "frappe.core.doctype.user_permission.user_permission.clear_user_permissions",
+ data
+ )
+ .then((data) => {
dialog.hide();
- let message = '';
+ let message = "";
if (data === 0) {
- message = __('No records deleted');
- } else if(data === 1) {
- message = __('{0} record deleted', [data]);
+ message = __("No records deleted");
+ } else if (data === 1) {
+ message = __("{0} record deleted", [data]);
} else {
- message = __('{0} records deleted', [data]);
+ message = __("{0} records deleted", [data]);
}
frappe.show_alert({
message,
- indicator: 'info'
+ indicator: "info",
});
list_view.refresh();
});
});
-
},
- primary_action_label: __('Delete')
+ primary_action_label: __("Delete"),
});
dialog.show();
});
},
- validate: function(dialog, data) {
- if(dialog.fields_dict.applicable_doctypes.get_unchecked_options().length == 0) {
+ validate: function (dialog, data) {
+ if (dialog.fields_dict.applicable_doctypes.get_unchecked_options().length == 0) {
data.apply_to_all_doctypes = 1;
data.applicable_doctypes = [];
return data;
}
- if(data.apply_to_all_doctypes == 0 && !("applicable_doctypes" in data)) {
+ if (data.apply_to_all_doctypes == 0 && !("applicable_doctypes" in data)) {
frappe.throw(__("Please select applicable Doctypes"));
}
return data;
},
- get_applicable_doctype: function(dialog) {
- return new Promise(resolve => {
- frappe.call({
- method: 'frappe.core.doctype.user_permission.user_permission.check_applicable_doc_perm',
- async: false,
- args:{
- user: dialog.fields_dict.user.value,
- doctype: dialog.fields_dict.doctype.value,
- docname: dialog.fields_dict.docname.value
- }
- }).then(r => {
- resolve(r.message);
- });
+ get_applicable_doctype: function (dialog) {
+ return new Promise((resolve) => {
+ frappe
+ .call({
+ method: "frappe.core.doctype.user_permission.user_permission.check_applicable_doc_perm",
+ async: false,
+ args: {
+ user: dialog.fields_dict.user.value,
+ doctype: dialog.fields_dict.doctype.value,
+ docname: dialog.fields_dict.docname.value,
+ },
+ })
+ .then((r) => {
+ resolve(r.message);
+ });
});
},
- get_multi_select_options: function(dialog, applicable){
- return new Promise(resolve => {
- frappe.call({
- method: 'frappe.desk.form.linked_with.get_linked_doctypes',
- async: false,
- args:{
- user: dialog.fields_dict.user.value,
- doctype: dialog.fields_dict.doctype.value,
- docname: dialog.fields_dict.docname.value
- }
- }).then(r => {
- var options = [];
- for(var d in r.message){
- var checked = ($.inArray(d, applicable) != -1) ? 1 : 0;
- options.push({ "label":d, "value": d , "checked": checked});
- }
- resolve(options);
- });
+ get_multi_select_options: function (dialog, applicable) {
+ return new Promise((resolve) => {
+ frappe
+ .call({
+ method: "frappe.desk.form.linked_with.get_linked_doctypes",
+ async: false,
+ args: {
+ user: dialog.fields_dict.user.value,
+ doctype: dialog.fields_dict.doctype.value,
+ docname: dialog.fields_dict.docname.value,
+ },
+ })
+ .then((r) => {
+ var options = [];
+ for (var d in r.message) {
+ var checked = $.inArray(d, applicable) != -1 ? 1 : 0;
+ options.push({ label: d, value: d, checked: checked });
+ }
+ resolve(options);
+ });
});
},
- on_doctype_change: function(dialog) {
+ on_doctype_change: function (dialog) {
dialog.set_df_property("docname", "hidden", 0);
dialog.set_df_property("docname", "reqd", 1);
dialog.set_df_property("is_default", "hidden", 0);
@@ -237,12 +257,15 @@ frappe.listview_settings['User Permission'] = {
dialog.refresh();
},
- on_docname_change: function(dialog, options, applicable) {
- if(applicable.length != 0 ) {
+ on_docname_change: function (dialog, options, applicable) {
+ if (applicable.length != 0) {
dialog.set_primary_action("Update");
dialog.set_title("Update User Permissions");
dialog.set_df_property("applicable_doctypes", "options", options);
- if(dialog.fields_dict.applicable_doctypes.get_checked_options().length == options.length) {
+ if (
+ dialog.fields_dict.applicable_doctypes.get_checked_options().length ==
+ options.length
+ ) {
dialog.set_df_property("applicable_doctypes", "hidden", 1);
} else {
dialog.set_df_property("applicable_doctypes", "hidden", 0);
@@ -257,8 +280,8 @@ frappe.listview_settings['User Permission'] = {
dialog.refresh();
},
- on_apply_to_all_doctypes_change: function(dialog, options) {
- if(dialog.fields_dict.apply_to_all_doctypes.get_value() == 0) {
+ on_apply_to_all_doctypes_change: function (dialog, options) {
+ if (dialog.fields_dict.apply_to_all_doctypes.get_value() == 0) {
dialog.set_df_property("applicable_doctypes", "hidden", 0);
dialog.set_df_property("applicable_doctypes", "options", options);
} else {
@@ -266,5 +289,5 @@ frappe.listview_settings['User Permission'] = {
dialog.set_df_property("applicable_doctypes", "hidden", 1);
}
dialog.refresh_sections();
- }
+ },
};
diff --git a/frappe/core/doctype/user_social_login/user_social_login.json b/frappe/core/doctype/user_social_login/user_social_login.json
index 3cac838016..6b4b1822d1 100644
--- a/frappe/core/doctype/user_social_login/user_social_login.json
+++ b/frappe/core/doctype/user_social_login/user_social_login.json
@@ -1,189 +1,58 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-12-02 13:01:20.507112",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2017-12-02 13:01:20.507112",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "provider",
+ "section_break_0",
+ "username",
+ "column_break_0",
+ "userid"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "provider",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Provider",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "provider",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Provider",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_0",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "section_break_0",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "username",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Username",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "username",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Username",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_0",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "column_break_0",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "userid",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "User ID",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "userid",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "User ID",
+ "read_only": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2017-12-02 15:37:58.397062",
- "modified_by": "Administrator",
- "module": "Core",
- "name": "User Social Login",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2022-08-03 12:20:53.800689",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "User Social Login",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/core/doctype/user_type/test_user_type.py b/frappe/core/doctype/user_type/test_user_type.py
index 235881517a..ec1a5e3bfc 100644
--- a/frappe/core/doctype/user_type/test_user_type.py
+++ b/frappe/core/doctype/user_type/test_user_type.py
@@ -1,12 +1,11 @@
# Copyright (c) 2021, Frappe Technologies and Contributors
# License: MIT. See LICENSE
-import unittest
-
import frappe
from frappe.installer import update_site_config
+from frappe.tests.utils import FrappeTestCase
-class TestUserType(unittest.TestCase):
+class TestUserType(FrappeTestCase):
def setUp(self):
create_role()
diff --git a/frappe/core/doctype/user_type/user_type.js b/frappe/core/doctype/user_type/user_type.js
index 6b53248fd4..5cf0dbb25f 100644
--- a/frappe/core/doctype/user_type/user_type.js
+++ b/frappe/core/doctype/user_type/user_type.js
@@ -1,72 +1,71 @@
// Copyright (c) 2021, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('User Type', {
- refresh: function(frm) {
- if (frm.is_new() && !frappe.boot.developer_mode)
- frm.set_value('is_standard', 1);
+frappe.ui.form.on("User Type", {
+ refresh: function (frm) {
+ if (frm.is_new() && !frappe.boot.developer_mode) frm.set_value("is_standard", 1);
- frm.set_query('document_type', 'user_doctypes', function() {
+ frm.set_query("document_type", "user_doctypes", function () {
return {
filters: {
- istable: 0
- }
+ istable: 0,
+ },
};
});
- frm.set_query('document_type', 'select_doctypes', function() {
+ frm.set_query("document_type", "select_doctypes", function () {
return {
filters: {
- istable: 0
- }
+ istable: 0,
+ },
};
});
- frm.set_query('document_type', 'custom_select_doctypes', function() {
+ frm.set_query("document_type", "custom_select_doctypes", function () {
return {
filters: {
- istable: 0
- }
+ istable: 0,
+ },
};
});
- frm.set_query('role', function() {
+ frm.set_query("role", function () {
return {
filters: {
is_custom: 1,
disabled: 0,
- desk_access: 1
- }
+ desk_access: 1,
+ },
};
});
- frm.set_query('apply_user_permission_on', function() {
+ frm.set_query("apply_user_permission_on", function () {
return {
- query: "frappe.core.doctype.user_type.user_type.get_user_linked_doctypes"
+ query: "frappe.core.doctype.user_type.user_type.get_user_linked_doctypes",
};
});
},
- onload: function(frm) {
- frm.trigger('get_user_id_fields');
+ onload: function (frm) {
+ frm.trigger("get_user_id_fields");
},
- apply_user_permission_on: function(frm) {
- frm.set_value('user_id_field', '');
- frm.trigger('get_user_id_fields');
+ apply_user_permission_on: function (frm) {
+ frm.set_value("user_id_field", "");
+ frm.trigger("get_user_id_fields");
},
- get_user_id_fields: function(frm) {
+ get_user_id_fields: function (frm) {
if (frm.doc.apply_user_permission_on) {
frappe.call({
- method: 'frappe.core.doctype.user_type.user_type.get_user_id',
+ method: "frappe.core.doctype.user_type.user_type.get_user_id",
args: {
- parent: frm.doc.apply_user_permission_on
+ parent: frm.doc.apply_user_permission_on,
+ },
+ callback: function (r) {
+ set_field_options("user_id_field", [""].concat(r.message));
},
- callback: function(r) {
- set_field_options('user_id_field', [""].concat(r.message));
- }
});
}
- }
+ },
});
diff --git a/frappe/core/doctype/user_type/user_type.py b/frappe/core/doctype/user_type/user_type.py
index 369e70bf56..39d9133412 100644
--- a/frappe/core/doctype/user_type/user_type.py
+++ b/frappe/core/doctype/user_type/user_type.py
@@ -14,6 +14,12 @@ class UserType(Document):
self.set_modules()
self.add_select_perm_doctypes()
+ def clear_cache(self):
+ super().clear_cache()
+
+ if not self.is_standard:
+ frappe.cache().delete_value("non_standard_user_types")
+
def on_update(self):
if self.is_standard:
return
@@ -24,7 +30,6 @@ class UserType(Document):
self.add_role_permissions_for_select_doctypes()
self.add_role_permissions_for_file()
self.update_users()
- get_non_standard_user_type_details()
self.remove_permission_for_deleted_doctypes()
def on_trash(self):
@@ -184,19 +189,14 @@ def add_role_permissions(doctype, role):
return name
-def get_non_standard_user_type_details():
+def get_non_standard_user_types():
user_types = frappe.get_all(
"User Type",
fields=["apply_user_permission_on", "name", "user_id_field"],
filters={"is_standard": 0},
)
- if user_types:
- user_type_details = {d.name: [d.apply_user_permission_on, d.user_id_field] for d in user_types}
-
- frappe.cache().set_value("non_standard_user_types", user_type_details)
-
- return user_type_details
+ return {d.name: [d.apply_user_permission_on, d.user_id_field] for d in user_types}
@frappe.whitelist()
@@ -287,13 +287,13 @@ def user_linked_with_permission_on_doctype(doc, user):
def apply_permissions_for_non_standard_user_type(doc, method=None):
"""Create user permission for the non standard user type"""
- if not frappe.db.table_exists("User Type"):
+ if not frappe.db.table_exists("User Type") or frappe.flags.in_migrate:
return
- user_types = frappe.cache().get_value("non_standard_user_types")
-
- if not user_types:
- user_types = get_non_standard_user_type_details()
+ user_types = frappe.cache().get_value(
+ "non_standard_user_types",
+ get_non_standard_user_types,
+ )
if not user_types:
return
diff --git a/frappe/core/doctype/user_type/user_type_list.js b/frappe/core/doctype/user_type/user_type_list.js
index 9a9ef417ac..856fe8985e 100644
--- a/frappe/core/doctype/user_type/user_type_list.js
+++ b/frappe/core/doctype/user_type/user_type_list.js
@@ -1,4 +1,4 @@
-frappe.listview_settings['User Type'] = {
+frappe.listview_settings["User Type"] = {
add_fields: ["is_standard"],
get_indicator: function (doc) {
if (doc.is_standard) {
@@ -6,5 +6,5 @@ frappe.listview_settings['User Type'] = {
} else {
return [__("Custom"), "blue", "is_standard,=,0"];
}
- }
+ },
};
diff --git a/frappe/core/doctype/version/test_version.py b/frappe/core/doctype/version/test_version.py
index 3e82f30f06..ce8e0e8b89 100644
--- a/frappe/core/doctype/version/test_version.py
+++ b/frappe/core/doctype/version/test_version.py
@@ -1,14 +1,14 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import copy
-import unittest
import frappe
from frappe.core.doctype.version.version import get_diff
from frappe.test_runner import make_test_objects
+from frappe.tests.utils import FrappeTestCase
-class TestVersion(unittest.TestCase):
+class TestVersion(FrappeTestCase):
def test_get_diff(self):
frappe.set_user("Administrator")
test_records = make_test_objects("Event", reset=True)
diff --git a/frappe/core/doctype/version/version.js b/frappe/core/doctype/version/version.js
index d39d2eac03..1e26e5f748 100644
--- a/frappe/core/doctype/version/version.js
+++ b/frappe/core/doctype/version/version.js
@@ -1,9 +1,12 @@
-frappe.ui.form.on("Version", "refresh", function(frm) {
- $(frappe.render_template('version_view', {doc:frm.doc, data:JSON.parse(frm.doc.data)}))
- .appendTo(frm.fields_dict.table_html.$wrapper.empty());
+frappe.ui.form.on("Version", "refresh", function (frm) {
+ $(
+ frappe.render_template("version_view", { doc: frm.doc, data: JSON.parse(frm.doc.data) })
+ ).appendTo(frm.fields_dict.table_html.$wrapper.empty());
- frm.add_custom_button(__('Show all Versions'), function() {
- frappe.set_route('List', 'Version',
- {ref_doctype: frm.doc.ref_doctype, docname: frm.doc.docname});
+ frm.add_custom_button(__("Show all Versions"), function () {
+ frappe.set_route("List", "Version", {
+ ref_doctype: frm.doc.ref_doctype,
+ docname: frm.doc.docname,
+ });
});
});
diff --git a/frappe/core/doctype/version/version.json b/frappe/core/doctype/version/version.json
index 463a7d3cba..13c82fa2b2 100644
--- a/frappe/core/doctype/version/version.json
+++ b/frappe/core/doctype/version/version.json
@@ -1,247 +1,81 @@
{
- "allow_copy": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "hash",
- "beta": 0,
- "creation": "2014-02-20 17:22:37",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 0,
- "engine": "InnoDB",
+ "actions": [],
+ "autoname": "hash",
+ "creation": "2014-02-20 17:22:37",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "ref_doctype",
+ "column_break_3",
+ "docname",
+ "data",
+ "section_break_4",
+ "table_html"
+ ],
"fields": [
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "ref_doctype",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "DocType",
- "length": 0,
- "no_copy": 0,
- "options": "DocType",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "ref_doctype",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "DocType",
+ "options": "DocType",
+ "reqd": 1
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_3",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "docname",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Document Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "docname",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Document Name",
+ "reqd": 1
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "data",
- "fieldtype": "Code",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Data",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "data",
+ "fieldtype": "Code",
+ "hidden": 1,
+ "label": "Data"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_4",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "table_html",
- "fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Table HTML",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "table_html",
+ "fieldtype": "HTML",
+ "label": "Table HTML"
}
- ],
- "hide_heading": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-copy",
- "idx": 1,
- "image_view": 0,
- "in_create": 1,
-
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-04-10 14:39:45.926836",
- "modified_by": "Administrator",
- "module": "Core",
- "name": "Version",
- "owner": "Administrator",
+ ],
+ "icon": "fa fa-copy",
+ "idx": 1,
+ "in_create": 1,
+ "links": [],
+ "modified": "2022-08-03 12:20:53.929691",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Version",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 0,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "is_custom": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
- },
+ "export": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager"
+ },
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 0,
- "delete": 1,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "is_custom": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 0,
- "role": "Administrator",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "delete": 1,
+ "read": 1,
+ "role": "Administrator"
}
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "sort_order": "ASC",
- "title_field": "docname",
- "track_changes": 1,
- "track_seen": 0
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "ASC",
+ "states": [],
+ "title_field": "docname",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/core/doctype/version/version_view.html b/frappe/core/doctype/version/version_view.html
index a17460ccc7..c6473b6a42 100644
--- a/frappe/core/doctype/version/version_view.html
+++ b/frappe/core/doctype/version/version_view.html
@@ -18,8 +18,8 @@
{% for item in data.changed %}
{{ frappe.meta.get_label(doc.ref_doctype, item[0]) }}
- {{ item[1] }}
- {{ item[2] }}
+ {{ frappe.utils.escape_html(item[1]) }}
+ {{ frappe.utils.escape_html(item[2]) }}
{% endfor %}
@@ -50,7 +50,7 @@
{% for row_key in item_keys %}
{{ row_key }}
- {{ item[1][row_key] }}
+ {{ frappe.utils.escape_html(item[1][row_key]) }}
{% endfor %}
@@ -85,8 +85,8 @@
{{ frappe.meta.get_label(doc.ref_doctype, table_info[0]) }}
{{ table_info[1] }}
{{ item[0] }}
- {{ item[1] }}
- {{ item[2] }}
+ {{ frappe.utils.escape_html(item[1]) }}
+ {{ frappe.utils.escape_html(item[2]) }}
{% endfor %}
{% endfor %}
diff --git a/frappe/core/doctype/view_log/test_view_log.py b/frappe/core/doctype/view_log/test_view_log.py
index 5a88269028..d1596d84a4 100644
--- a/frappe/core/doctype/view_log/test_view_log.py
+++ b/frappe/core/doctype/view_log/test_view_log.py
@@ -1,11 +1,10 @@
# Copyright (c) 2018, Frappe Technologies and Contributors
# License: MIT. See LICENSE
-import unittest
-
import frappe
+from frappe.tests.utils import FrappeTestCase
-class TestViewLog(unittest.TestCase):
+class TestViewLog(FrappeTestCase):
def tearDown(self):
frappe.set_user("Administrator")
diff --git a/frappe/core/doctype/view_log/view_log.js b/frappe/core/doctype/view_log/view_log.js
index a8c95b01e8..06d23802be 100644
--- a/frappe/core/doctype/view_log/view_log.js
+++ b/frappe/core/doctype/view_log/view_log.js
@@ -1,8 +1,6 @@
// Copyright (c) 2018, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('View Log', {
- refresh: function(frm) {
-
- }
+frappe.ui.form.on("View Log", {
+ refresh: function (frm) {},
});
diff --git a/frappe/core/doctype/view_log/view_log.json b/frappe/core/doctype/view_log/view_log.json
index 3c4486c944..a350ae835c 100644
--- a/frappe/core/doctype/view_log/view_log.json
+++ b/frappe/core/doctype/view_log/view_log.json
@@ -1,163 +1,56 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
+ "actions": [],
"creation": "2018-05-27 02:20:11.193944",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
- "document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
+ "field_order": [
+ "viewed_by",
+ "reference_doctype",
+ "reference_name"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "viewed_by",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Viewed By",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 1,
- "set_only_once": 1,
- "translatable": 0,
- "unique": 0
+ "set_only_once": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "reference_doctype",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Reference Document Type",
- "length": 0,
- "no_copy": 0,
"options": "DocType",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 1,
- "set_only_once": 1,
- "translatable": 0,
- "unique": 0
+ "set_only_once": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Reference name",
- "length": 0,
- "no_copy": 0,
"options": "reference_doctype",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
"search_index": 1,
- "set_only_once": 1,
- "translatable": 0,
- "unique": 0
+ "set_only_once": 1
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2021-10-25 14:22:27.664645",
+ "links": [],
+ "modified": "2022-09-07 05:16:14.587628",
"modified_by": "Administrator",
"module": "Core",
"name": "View Log",
- "name_case": "",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 0
+ "share": 1
}
],
"quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
- "track_seen": 0,
- "track_views": 0
-}
+ "states": []
+}
\ No newline at end of file
diff --git a/frappe/core/notifications.py b/frappe/core/notifications.py
index 9a127e567e..093418e345 100644
--- a/frappe/core/notifications.py
+++ b/frappe/core/notifications.py
@@ -32,11 +32,10 @@ def get_things_todo(as_list=False):
if as_list:
return data
- else:
- return data[0][0]
+ return data[0][0]
-def get_todays_events(as_list=False):
+def get_todays_events(as_list: bool = False):
"""Returns a count of todays events in calendar"""
from frappe.desk.doctype.event.event import get_events
from frappe.utils import nowdate
diff --git a/frappe/core/page/background_jobs/background_jobs.css b/frappe/core/page/background_jobs/background_jobs.css
deleted file mode 100644
index 7716519113..0000000000
--- a/frappe/core/page/background_jobs/background_jobs.css
+++ /dev/null
@@ -1,47 +0,0 @@
-
-.table-background-jobs {
- margin-bottom: 0px;
- margin-top: 0px;
- font-size: var(--text-md);
- table-layout: fixed;
-}
-
-.table-background-jobs th {
- font-weight: normal;
- color: var(--text-muted);
-}
-
-.table-background-jobs td {
- color: var(--text-light);
-}
-
-.table-background-jobs th, .table-background-jobs td {
- padding: var(--padding-sm) var(--padding-md);
-}
-
-.table-background-jobs tbody tr:hover {
- background-color: var(--highlight-color);
-}
-
-.job-name {
- font-size: var(--text-md);
- font-family: var(--font-family-monospace);
- word-break: break-word;
-}
-
-.no-background-jobs {
- min-height: 320px;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-direction: column;
-}
-
-.no-background-jobs > img {
- margin-bottom: var(--margin-md);
- max-height: 100px;
-}
-
-.footer {
- padding: var(--padding-md);
-}
diff --git a/frappe/core/page/background_jobs/background_jobs.html b/frappe/core/page/background_jobs/background_jobs.html
deleted file mode 100644
index e0c1a8f633..0000000000
--- a/frappe/core/page/background_jobs/background_jobs.html
+++ /dev/null
@@ -1,58 +0,0 @@
-{% if jobs.length %}
-
-
-
- {{ __("Queue") }}
- {{ __("Job") }}
- {{ __("Status") }}
- {{ __("Created") }}
-
-
-
- {% for j in jobs %}
-
-
- {{ toTitle(j.queue.split(":").slice(-1)[0]) }}
-
-
-
-
- {{ frappe.utils.encode_tags(j.job_name) }}
-
-
- {% if j.exc_info %}
-
- {{ __("Exception") }}
-
-
{{ frappe.utils.encode_tags(j.exc_info) }}
-
-
- {% endif %}
-
-
-
- {{ toTitle(j.status) }}
-
-
-
- {{ frappe.datetime.prettyDate(j.creation) }}
-
-
- {% endfor %}
-
-
-{% else %}
-
-
-
{{ __("No jobs found on this site") }}
-
-{% endif %}
-
diff --git a/frappe/core/page/background_jobs/background_jobs.js b/frappe/core/page/background_jobs/background_jobs.js
deleted file mode 100644
index 7334bfd5dd..0000000000
--- a/frappe/core/page/background_jobs/background_jobs.js
+++ /dev/null
@@ -1,149 +0,0 @@
-frappe.pages["background_jobs"].on_page_load = wrapper => {
- const background_job = new BackgroundJobs(wrapper);
-
- $(wrapper).bind("show", () => {
- background_job.show();
- });
-
- window.background_jobs = background_job;
-};
-
-class BackgroundJobs {
- constructor(wrapper) {
- this.page = frappe.ui.make_app_page({
- parent: wrapper,
- title: __("Background Jobs"),
- single_column: true
- });
-
- this.page.add_inner_button(__("Remove Failed Jobs"), () => {
- frappe.confirm(
- __("Are you sure you want to remove all failed jobs?"),
- () => {
- frappe
- .call(
- "frappe.core.page.background_jobs.background_jobs.remove_failed_jobs"
- )
- .then(() => this.refresh_jobs());
- }
- );
- });
-
- this.page.main.addClass("frappe-card");
- this.page.body.append('
');
- this.$content = $(this.page.body).find(".table-area");
-
- this.make_filters();
- this.refresh_jobs = frappe.utils.throttle(
- this.refresh_jobs.bind(this),
- 1000
- );
- }
-
- make_filters() {
- this.view = this.page.add_field({
- label: __("View"),
- fieldname: "view",
- fieldtype: "Select",
- options: ["Jobs", "Workers"],
- default: "Jobs",
- change: () => {
- this.queue_timeout.toggle(this.view.get_value() === "Jobs");
- this.job_status.toggle(this.view.get_value() === "Jobs");
- }
- });
- this.queue_timeout = this.page.add_field({
- label: __("Queue"),
- fieldname: "queue_timeout",
- fieldtype: "Select",
- options: [
- { label: "All Queues", value: "all" },
- { label: "Default", value: "default" },
- { label: "Short", value: "short" },
- { label: "Long", value: "long" }
- ],
- default: "all"
- });
- this.job_status = this.page.add_field({
- label: __("Job Status"),
- fieldname: "job_status",
- fieldtype: "Select",
- options: [
- { label: "All Jobs", value: "all" },
- { label: "Queued", value: "queued" },
- { label: "Deferred", value: "deferred" },
- { label: "Started", value: "started" },
- { label: "Finished", value: "finished" },
- { label: "Failed", value: "failed" }
- ],
- default: "all"
- });
- this.auto_refresh = this.page.add_field({
- label: __("Auto Refresh"),
- fieldname: "auto_refresh",
- fieldtype: "Check",
- default: 1,
- change: () => {
- if (this.auto_refresh.get_value()) {
- this.refresh_jobs();
- }
- }
- });
- }
-
- show() {
- this.refresh_jobs();
- this.update_scheduler_status();
- }
-
- update_scheduler_status() {
- frappe.call({
- method:
- "frappe.core.page.background_jobs.background_jobs.get_scheduler_status",
- callback: r => {
- let { status } = r.message;
- if (status === "active") {
- this.page.set_indicator(__("Scheduler: Active"), "green");
- } else {
- this.page.set_indicator(__("Scheduler: Inactive"), "red");
- }
- }
- });
- }
-
- refresh_jobs() {
- let view = this.view.get_value();
- let args;
- let { queue_timeout, job_status } = this.page.get_form_values();
- if (view === "Jobs") {
- args = { view, queue_timeout, job_status };
- } else {
- args = { view };
- }
-
- this.page.add_inner_message(__("Refreshing..."));
- frappe.call({
- method: "frappe.core.page.background_jobs.background_jobs.get_info",
- args,
- callback: res => {
- this.page.add_inner_message("");
-
- let template =
- view === "Jobs" ? "background_jobs" : "background_workers";
- this.$content.html(
- frappe.render_template(template, {
- jobs: res.message || []
- })
- );
-
- let auto_refresh = this.auto_refresh.get_value();
- if (
- frappe.get_route()[0] === "background_jobs" &&
- auto_refresh
- ) {
- setTimeout(() => this.refresh_jobs(), 2000);
- }
- }
- });
- }
-}
diff --git a/frappe/core/page/background_jobs/background_jobs.json b/frappe/core/page/background_jobs/background_jobs.json
deleted file mode 100644
index 6701cc54bc..0000000000
--- a/frappe/core/page/background_jobs/background_jobs.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "content": null,
- "creation": "2016-08-18 16:44:14.322642",
- "docstatus": 0,
- "doctype": "Page",
- "idx": 0,
- "modified": "2016-08-18 16:48:11.577611",
- "modified_by": "Administrator",
- "module": "Core",
- "name": "background_jobs",
- "owner": "Administrator",
- "page_name": "background_jobs",
- "roles": [
- {
- "role": "System Manager"
- }
- ],
- "script": null,
- "standard": "Yes",
- "style": null,
- "title": "Background Jobs"
-}
\ No newline at end of file
diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py
deleted file mode 100644
index 8ef15b65eb..0000000000
--- a/frappe/core/page/background_jobs/background_jobs.py
+++ /dev/null
@@ -1,78 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: MIT. See LICENSE
-
-from typing import TYPE_CHECKING
-
-import frappe
-from frappe.utils import convert_utc_to_user_timezone
-from frappe.utils.background_jobs import get_queues, get_workers
-from frappe.utils.scheduler import is_scheduler_inactive
-
-if TYPE_CHECKING:
- from rq.job import Job
-
-JOB_COLORS = {"queued": "orange", "failed": "red", "started": "blue", "finished": "green"}
-
-
-@frappe.whitelist()
-def get_info(view=None, queue_timeout=None, job_status=None) -> list[dict]:
- jobs = []
-
- def add_job(job: "Job", queue: str) -> None:
-
- if job.kwargs.get("site") == frappe.local.site:
- job_info = {
- "job_name": job.kwargs.get("kwargs", {}).get("playbook_method")
- or job.kwargs.get("kwargs", {}).get("job_type")
- or str(job.kwargs.get("job_name")),
- "status": job.get_status(),
- "queue": queue,
- "creation": convert_utc_to_user_timezone(job.created_at),
- "color": JOB_COLORS[job.get_status()],
- }
-
- if job.exc_info:
- job_info["exc_info"] = job.exc_info
-
- jobs.append(job_info)
-
- if view == "Jobs":
- queues = get_queues()
- for queue in queues:
- for job in queue.jobs:
- if job_status != "all" and job.get_status() != job_status:
- return
- if queue_timeout != "all" and not queue.name.endswith(f":{queue_timeout}"):
- return
- add_job(job, queue.name)
-
- elif view == "Workers":
- workers = get_workers()
- for worker in workers:
- current_job = worker.get_current_job()
- if current_job:
- if hasattr(current_job, "kwargs") and current_job.kwargs.get("site") == frappe.local.site:
- add_job(current_job, current_job.origin)
- else:
- jobs.append({"queue": worker.name, "job_name": "busy", "status": "", "creation": ""})
- else:
- jobs.append({"queue": worker.name, "job_name": "idle", "status": "", "creation": ""})
-
- return jobs
-
-
-@frappe.whitelist()
-def remove_failed_jobs():
- queues = get_queues()
- for queue in queues:
- fail_registry = queue.failed_job_registry
- for job_id in fail_registry.get_job_ids():
- job = queue.fetch_job(job_id)
- fail_registry.remove(job, delete_job=True)
-
-
-@frappe.whitelist()
-def get_scheduler_status():
- if is_scheduler_inactive():
- return {"status": "inactive"}
- return {"status": "active"}
diff --git a/frappe/core/page/background_jobs/background_workers.html b/frappe/core/page/background_jobs/background_workers.html
deleted file mode 100644
index 1647cea4b4..0000000000
--- a/frappe/core/page/background_jobs/background_workers.html
+++ /dev/null
@@ -1,51 +0,0 @@
-{% if jobs.length %}
-
-
-
- {{ __("Worker") }}
- {{ __("Current Job") }}
- {{ __("Status") }}
- {{ __("Created") }}
-
-
-
- {% for j in jobs %}
-
-
- {{ j.queue }}
-
-
-
-
- {{ frappe.utils.encode_tags(j.job_name) }}
-
-
- {% if j.exc_info %}
-
- {{ __("Exception") }}
-
-
{{ frappe.utils.encode_tags(j.exc_info) }}
-
-
- {% endif %}
-
-
- {{ toTitle(j.status) }}
-
- {{ frappe.datetime.prettyDate(j.creation) }}
-
- {% endfor %}
-
-
-{% else %}
-
-
-
{{ __("No workers online on this site") }}
-
-{% endif %}
-
\ No newline at end of file
diff --git a/frappe/core/page/dashboard_view/dashboard_view.js b/frappe/core/page/dashboard_view/dashboard_view.js
index bf9fb2a286..0ee697dfc1 100644
--- a/frappe/core/page/dashboard_view/dashboard_view.js
+++ b/frappe/core/page/dashboard_view/dashboard_view.js
@@ -1,19 +1,18 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
-frappe.provide('frappe.dashboards');
-frappe.provide('frappe.dashboards.chart_sources');
+frappe.provide("frappe.dashboards");
+frappe.provide("frappe.dashboards.chart_sources");
-
-frappe.pages['dashboard-view'].on_page_load = function(wrapper) {
+frappe.pages["dashboard-view"].on_page_load = function (wrapper) {
frappe.ui.make_app_page({
parent: wrapper,
title: __("Dashboard"),
- single_column: true
+ single_column: true,
});
frappe.dashboard = new Dashboard(wrapper);
- $(wrapper).bind('show', function() {
+ $(wrapper).bind("show", function () {
frappe.dashboard.show();
});
};
@@ -37,20 +36,20 @@ class Dashboard {
} else {
// last opened
if (frappe.last_dashboard) {
- frappe.set_re_route('dashboard-view', frappe.last_dashboard);
+ frappe.set_re_route("dashboard-view", frappe.last_dashboard);
} else {
// default dashboard
- frappe.db.get_list('Dashboard', {filters: {is_default: 1}}).then(data => {
+ frappe.db.get_list("Dashboard", { filters: { is_default: 1 } }).then((data) => {
if (data && data.length) {
- frappe.set_re_route('dashboard-view', data[0].name);
+ frappe.set_re_route("dashboard-view", data[0].name);
} else {
// no default, get the latest one
- frappe.db.get_list('Dashboard', {limit: 1}).then(data => {
+ frappe.db.get_list("Dashboard", { limit: 1 }).then((data) => {
if (data && data.length) {
- frappe.set_re_route('dashboard-view', data[0].name);
+ frappe.set_re_route("dashboard-view", data[0].name);
} else {
// create a new dashboard!
- frappe.new_doc('Dashboard');
+ frappe.new_doc("Dashboard");
}
});
}
@@ -63,11 +62,11 @@ class Dashboard {
if (this.dashboard_name !== current_dashboard_name) {
this.dashboard_name = current_dashboard_name;
let title = this.dashboard_name;
- if (!this.dashboard_name.toLowerCase().includes(__('dashboard'))) {
+ if (!this.dashboard_name.toLowerCase().includes(__("dashboard"))) {
// ensure dashboard title has "dashboard"
- title = __('{0} Dashboard', [title]);
+ title = __("{0} Dashboard", [__(title)]);
}
- this.page.set_title(title);
+ this.page.set_title(__(title));
this.set_dropdown();
this.container.empty();
this.refresh();
@@ -81,31 +80,30 @@ class Dashboard {
}
refresh() {
- frappe.run_serially([
- () => this.render_cards(),
- () => this.render_charts()
- ]);
+ frappe.run_serially([() => this.render_cards(), () => this.render_charts()]);
}
render_charts() {
return this.get_permitted_items(
- 'frappe.desk.doctype.dashboard.dashboard.get_permitted_charts'
- ).then(charts => {
+ "frappe.desk.doctype.dashboard.dashboard.get_permitted_charts"
+ ).then((charts) => {
if (!charts.length) {
- frappe.msgprint(__('No Permitted Charts on this Dashboard'), __('No Permitted Charts'))
+ frappe.msgprint(
+ __("No Permitted Charts on this Dashboard"),
+ __("No Permitted Charts")
+ );
}
frappe.dashboard_utils.get_dashboard_settings().then((settings) => {
- let chart_config = settings.chart_config? JSON.parse(settings.chart_config): {};
- this.charts =
- charts.map(chart => {
- return {
- chart_name: chart.chart,
- label: chart.chart,
- chart_settings: chart_config[chart.chart] || {},
- ...chart
- }
- });
+ let chart_config = settings.chart_config ? JSON.parse(settings.chart_config) : {};
+ this.charts = charts.map((chart) => {
+ return {
+ chart_name: chart.chart,
+ label: chart.chart,
+ chart_settings: chart_config[chart.chart] || {},
+ ...chart,
+ };
+ });
this.chart_group = new frappe.widget.WidgetGroup({
title: null,
@@ -121,24 +119,23 @@ class Dashboard {
},
widgets: this.charts,
});
- })
+ });
});
}
render_cards() {
return this.get_permitted_items(
- 'frappe.desk.doctype.dashboard.dashboard.get_permitted_cards'
- ).then(cards => {
+ "frappe.desk.doctype.dashboard.dashboard.get_permitted_cards"
+ ).then((cards) => {
if (!cards.length) {
return;
}
- this.number_cards =
- cards.map(card => {
- return {
- name: card.card,
- };
- });
+ this.number_cards = cards.map((card) => {
+ return {
+ name: card.card,
+ };
+ });
this.number_card_group = new frappe.widget.WidgetGroup({
container: this.container,
@@ -157,41 +154,43 @@ class Dashboard {
}
get_permitted_items(method) {
- return frappe.xcall(
- method,
- {
- dashboard_name: this.dashboard_name
- }
- ).then(items => {
- return items;
- });
+ return frappe
+ .xcall(method, {
+ dashboard_name: this.dashboard_name,
+ })
+ .then((items) => {
+ return items;
+ });
}
set_dropdown() {
this.page.clear_menu();
- this.page.add_menu_item(__('Edit'), () => {
- frappe.set_route('Form', 'Dashboard', frappe.dashboard.dashboard_name);
+ this.page.add_menu_item(__("Edit"), () => {
+ frappe.set_route("Form", "Dashboard", frappe.dashboard.dashboard_name);
});
- this.page.add_menu_item(__('New'), () => {
- frappe.new_doc('Dashboard');
+ this.page.add_menu_item(__("New"), () => {
+ frappe.new_doc("Dashboard");
});
- this.page.add_menu_item(__('Refresh All'), () => {
- this.chart_group &&
- this.chart_group.widgets_list.forEach(chart => chart.refresh());
+ this.page.add_menu_item(__("Refresh All"), () => {
+ this.chart_group && this.chart_group.widgets_list.forEach((chart) => chart.refresh());
this.number_card_group &&
- this.number_card_group.widgets_list.forEach(card => card.render_card());
+ this.number_card_group.widgets_list.forEach((card) => card.render_card());
});
- frappe.db.get_list('Dashboard').then(dashboards => {
- dashboards.map(dashboard => {
+ frappe.db.get_list("Dashboard").then((dashboards) => {
+ dashboards.map((dashboard) => {
let name = dashboard.name;
if (name != this.dashboard_name) {
- this.page.add_menu_item(name, () => frappe.set_route("dashboard-view", name), 1);
+ this.page.add_menu_item(
+ name,
+ () => frappe.set_route("dashboard-view", name),
+ 1
+ );
}
});
});
}
-}
\ No newline at end of file
+}
diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js
index 8a06a9aac5..bc27106068 100644
--- a/frappe/core/page/permission_manager/permission_manager.js
+++ b/frappe/core/page/permission_manager/permission_manager.js
@@ -1,20 +1,21 @@
-frappe.pages['permission-manager'].on_page_load = (wrapper) => {
+frappe.pages["permission-manager"].on_page_load = (wrapper) => {
let page = frappe.ui.make_app_page({
parent: wrapper,
- title: __('Role Permissions Manager'),
+ title: __("Role Permissions Manager"),
card_layout: true,
- single_column: true
+ single_column: true,
});
frappe.breadcrumbs.add("Setup");
- $("
").appendTo(page.main);
+ $("
").appendTo(
+ page.main
+ );
$(frappe.render_template("permission_manager_help", {})).appendTo(page.main);
wrapper.permission_engine = new frappe.PermissionEngine(wrapper);
-
};
-frappe.pages['permission-manager'].refresh = function (wrapper) {
+frappe.pages["permission-manager"].refresh = function (wrapper) {
wrapper.permission_engine.set_from_route();
};
@@ -30,33 +31,38 @@ frappe.PermissionEngine = class PermissionEngine {
make() {
this.make_reset_button();
- frappe.call({
- module: "frappe.core",
- page: "permission_manager",
- method: "get_roles_and_doctypes"
- }).then((res) => {
- this.options = res.message;
- this.setup_page();
- });
+ frappe
+ .call({
+ module: "frappe.core",
+ page: "permission_manager",
+ method: "get_roles_and_doctypes",
+ })
+ .then((res) => {
+ this.options = res.message;
+ this.setup_page();
+ });
}
setup_page() {
- this.doctype_select
- = this.wrapper.page.add_select(__("Document Type"),
- [{ value: "", label: __("Select Document Type") + "..." }].concat(this.options.doctypes))
- .change(function () {
- frappe.set_route("permission-manager", $(this).val());
- });
+ this.doctype_select = this.wrapper.page
+ .add_select(
+ __("Document Type"),
+ [{ value: "", label: __("Select Document Type") + "..." }].concat(
+ this.options.doctypes
+ )
+ )
+ .change(function () {
+ frappe.set_route("permission-manager", $(this).val());
+ });
- this.role_select
- = this.wrapper.page.add_select(__("Roles"),
- [__("Select Role") + "..."].concat(this.options.roles))
- .change(() => {
- this.refresh();
- });
+ this.role_select = this.wrapper.page
+ .add_select(__("Roles"), [__("Select Role") + "..."].concat(this.options.roles))
+ .change(() => {
+ this.refresh();
+ });
- this.page.add_inner_button(__('Set User Permissions'), () => {
- return frappe.set_route('List', 'User Permission');
+ this.page.add_inner_button(__("Set User Permissions"), () => {
+ return frappe.set_route("List", "User Permission");
});
this.set_from_route();
}
@@ -91,7 +97,7 @@ frappe.PermissionEngine = class PermissionEngine {
page: "permission_manager",
method: "get_standard_permissions",
args: { doctype: doctype },
- callback: callback
+ callback: callback,
});
}
return false;
@@ -100,18 +106,22 @@ frappe.PermissionEngine = class PermissionEngine {
reset_std_permissions(data) {
let doctype = this.get_doctype();
let d = frappe.confirm(__("Reset Permissions for {0}?", [doctype]), () => {
- return frappe.call({
- module: "frappe.core",
- page: "permission_manager",
- method: "reset",
- args: { doctype }
- }).then(() => {
- this.refresh();
- });
+ return frappe
+ .call({
+ module: "frappe.core",
+ page: "permission_manager",
+ method: "reset",
+ args: { doctype },
+ })
+ .then(() => {
+ this.refresh();
+ });
});
// show standard permissions
- let $d = $(d.wrapper).find(".frappe-confirm-message").append("Standard Permissions: ");
+ let $d = $(d.wrapper)
+ .find(".frappe-confirm-message")
+ .append("Standard Permissions: ");
let $wrapper = $("
").appendTo($d);
data.message.forEach((d) => {
let rights = this.rights
@@ -164,14 +174,16 @@ frappe.PermissionEngine = class PermissionEngine {
}
// get permissions
- frappe.call({
- module: "frappe.core",
- page: "permission_manager",
- method: "get_permissions",
- args: { doctype, role }
- }).then((r) => {
- this.render(r.message);
- });
+ frappe
+ .call({
+ module: "frappe.core",
+ page: "permission_manager",
+ method: "get_permissions",
+ args: { doctype, role },
+ })
+ .then((r) => {
+ this.render(r.message);
+ });
}
render(perm_list) {
@@ -187,19 +199,21 @@ frappe.PermissionEngine = class PermissionEngine {
}
show_permission_table(perm_list) {
- this.table = $("\
+ this.table = $(
+ "
").appendTo(this.body);
+
"
+ ).appendTo(this.body);
const table_columns = [
[__("Document Type"), 150],
[__("Role"), 170],
[__("Level"), 40],
[__("Permissions"), 350],
- ["", 40]
+ ["", 40],
];
table_columns.forEach((col) => {
@@ -236,9 +250,9 @@ frappe.PermissionEngine = class PermissionEngine {
let perm_cell = this.add_cell(row, d, "permissions");
let perm_container = $("
").appendTo(perm_cell);
- this.rights.forEach(r => {
- if (!d.is_submittable && ['submit', 'cancel', 'amend'].includes(r)) return;
- if (d.in_create && ['create', 'write', 'delete'].includes(r)) return;
+ this.rights.forEach((r) => {
+ if (!d.is_submittable && ["submit", "cancel", "amend"].includes(r)) return;
+ if (d.in_create && ["create", "write", "delete"].includes(r)) return;
this.add_check(perm_container, d, r);
});
@@ -248,7 +262,8 @@ frappe.PermissionEngine = class PermissionEngine {
}
add_cell(row, d, fieldname) {
- return $("").appendTo(row)
+ return $(" ")
+ .appendTo(row)
.attr("data-fieldname", fieldname)
.addClass("pt-4")
.html(__(d[fieldname]));
@@ -266,19 +281,20 @@ frappe.PermissionEngine = class PermissionEngine {
${__(label)}
${__(description)}
- `)
+ `
+ )
.appendTo(cell)
.attr("data-fieldname", fieldname);
- checkbox.find("input")
+ checkbox
+ .find("input")
.prop("checked", d[fieldname] ? true : false)
.attr("data-ptype", fieldname)
.attr("data-role", d.role)
.attr("data-permlevel", d.permlevel)
.attr("data-doctype", d.parent);
- checkbox.find("label")
- .css("text-transform", "capitalize");
+ checkbox.find("label").css("text-transform", "capitalize");
return checkbox;
}
@@ -290,8 +306,22 @@ frappe.PermissionEngine = class PermissionEngine {
}
get rights() {
- return ["select", "read", "write", "create", "delete", "submit", "cancel", "amend",
- "print", "email", "report", "import", "export", "set_user_permissions", "share"];
+ return [
+ "select",
+ "read",
+ "write",
+ "create",
+ "delete",
+ "submit",
+ "cancel",
+ "amend",
+ "print",
+ "email",
+ "report",
+ "import",
+ "export",
+ "share",
+ ];
}
set_show_users(cell, role) {
@@ -305,22 +335,29 @@ frappe.PermissionEngine = class PermissionEngine {
page: "permission_manager",
method: "get_users_with_role",
args: {
- role: role
+ role: role,
},
callback: function (r) {
r.message = $.map(r.message, function (p) {
return $.format('{1} ', [p, p]);
});
- frappe.msgprint(__("Users with role {0}:", [__(role)])
- + " " + r.message.join(" "));
- }
+ frappe.msgprint(
+ __("Users with role {0}:", [__(role)]) +
+ " " +
+ r.message.join(" ")
+ );
+ },
});
return false;
});
}
add_delete_button(row, d) {
- $(`${frappe.utils.icon('delete')} `)
+ $(
+ `${frappe.utils.icon(
+ "delete"
+ )} `
+ )
.appendTo($(` `).appendTo(row))
.attr("data-doctype", d.parent)
.attr("data-role", d.role)
@@ -333,7 +370,7 @@ frappe.PermissionEngine = class PermissionEngine {
args: {
doctype: d.parent,
role: d.role,
- permlevel: d.permlevel
+ permlevel: d.permlevel,
},
callback: (r) => {
if (r.exc) {
@@ -341,7 +378,7 @@ frappe.PermissionEngine = class PermissionEngine {
} else {
this.refresh();
}
- }
+ },
});
});
}
@@ -350,7 +387,7 @@ frappe.PermissionEngine = class PermissionEngine {
let me = this;
this.body.on("click", ".show-user-permissions", () => {
frappe.route_options = { allow: this.get_doctype() || "" };
- frappe.set_route('List', 'User Permission');
+ frappe.set_route("List", "User Permission");
});
this.body.on("click", "input[type='checkbox']", function () {
@@ -361,7 +398,7 @@ frappe.PermissionEngine = class PermissionEngine {
permlevel: chk.attr("data-permlevel"),
doctype: chk.attr("data-doctype"),
ptype: chk.attr("data-ptype"),
- value: chk.prop("checked") ? 1 : 0
+ value: chk.prop("checked") ? 1 : 0,
};
return frappe.call({
module: "frappe.core",
@@ -376,7 +413,7 @@ frappe.PermissionEngine = class PermissionEngine {
} else {
me.get_perm(args.role)[args.ptype] = args.value;
}
- }
+ },
});
});
}
@@ -389,19 +426,30 @@ frappe.PermissionEngine = class PermissionEngine {
title: __("Add New Permission Rule"),
fields: [
{
- fieldtype: "Select", label: __("Document Type"),
- options: this.options.doctypes, reqd: 1, fieldname: "parent"
+ fieldtype: "Select",
+ label: __("Document Type"),
+ options: this.options.doctypes,
+ reqd: 1,
+ fieldname: "parent",
},
{
- fieldtype: "Select", label: __("Role"),
- options: this.options.roles, reqd: 1, fieldname: "role"
+ fieldtype: "Select",
+ label: __("Role"),
+ options: this.options.roles,
+ reqd: 1,
+ fieldname: "role",
},
{
- fieldtype: "Select", label: __("Permission Level"),
- options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], reqd: 1, fieldname: "permlevel",
- description: __("Level 0 is for document level permissions, higher levels for field level permissions.")
- }
- ]
+ fieldtype: "Select",
+ label: __("Permission Level"),
+ options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ reqd: 1,
+ fieldname: "permlevel",
+ description: __(
+ "Level 0 is for document level permissions, higher levels for field level permissions."
+ ),
+ },
+ ],
});
if (this.get_doctype()) {
d.set_value("parent", this.get_doctype());
@@ -412,7 +460,7 @@ frappe.PermissionEngine = class PermissionEngine {
d.get_input("role").prop("disabled", true);
}
d.set_value("permlevel", "0");
- d.set_primary_action(__('Add'), () => {
+ d.set_primary_action(__("Add"), () => {
let args = d.get_values();
if (!args) {
return;
@@ -428,7 +476,7 @@ frappe.PermissionEngine = class PermissionEngine {
} else {
this.refresh();
}
- }
+ },
});
d.hide();
});
@@ -439,13 +487,11 @@ frappe.PermissionEngine = class PermissionEngine {
}
make_reset_button() {
- this.page.set_secondary_action(
- __("Restore Original Permissions"),
- () => {
- this.get_standard_permissions((data) => {
- this.reset_std_permissions(data);
- });
+ this.page.set_secondary_action(__("Restore Original Permissions"), () => {
+ this.get_standard_permissions((data) => {
+ this.reset_std_permissions(data);
});
+ });
}
get_perm(role) {
@@ -455,7 +501,9 @@ frappe.PermissionEngine = class PermissionEngine {
}
get_link_fields(doctype) {
- return frappe.get_children("DocType", doctype, "fields",
- { fieldtype: "Link", options: ["not in", ["User", '[Select]']] });
+ return frappe.get_children("DocType", doctype, "fields", {
+ fieldtype: "Link",
+ options: ["not in", ["User", "[Select]"]],
+ });
}
};
diff --git a/frappe/core/page/permission_manager/permission_manager.py b/frappe/core/page/permission_manager/permission_manager.py
index 46c9e0aca2..5ed3014778 100644
--- a/frappe/core/page/permission_manager/permission_manager.py
+++ b/frappe/core/page/permission_manager/permission_manager.py
@@ -19,7 +19,6 @@ from frappe.permissions import (
setup_custom_perms,
update_permission_property,
)
-from frappe.translate import send_translations
from frappe.utils.user import get_users_with_role as _get_user_with_role
not_allowed_in_permission_manager = ["DocType", "Patch Log", "Module Def", "Transaction Log"]
@@ -28,7 +27,6 @@ not_allowed_in_permission_manager = ["DocType", "Patch Log", "Module Def", "Tran
@frappe.whitelist()
def get_roles_and_doctypes():
frappe.only_for("System Manager")
- send_translations(frappe.get_lang_dict("doctype", "DocPerm"))
active_domains = frappe.get_active_domains()
@@ -64,8 +62,8 @@ def get_roles_and_doctypes():
roles_list = [{"label": _(d.get("name")), "value": d.get("name")} for d in roles]
return {
- "doctypes": sorted(doctypes_list, key=lambda d: d["label"]),
- "roles": sorted(roles_list, key=lambda d: d["label"]),
+ "doctypes": sorted(doctypes_list, key=lambda d: d["label"].casefold()),
+ "roles": sorted(roles_list, key=lambda d: d["label"].casefold()),
}
diff --git a/frappe/core/page/recorder/recorder.js b/frappe/core/page/recorder/recorder.js
index f1f74daf71..1f004915fe 100644
--- a/frappe/core/page/recorder/recorder.js
+++ b/frappe/core/page/recorder/recorder.js
@@ -1,28 +1,28 @@
-frappe.pages['recorder'].on_page_load = function(wrapper) {
+frappe.pages["recorder"].on_page_load = function (wrapper) {
frappe.ui.make_app_page({
parent: wrapper,
- title: __('Recorder'),
+ title: __("Recorder"),
single_column: true,
- card_layout: true
+ card_layout: true,
});
frappe.recorder = new Recorder(wrapper);
- $(wrapper).bind('show', function() {
+ $(wrapper).bind("show", function () {
frappe.recorder.show();
});
- frappe.require('recorder.bundle.js');
+ frappe.require("recorder.bundle.js");
};
class Recorder {
constructor(wrapper) {
this.wrapper = $(wrapper);
- this.container = this.wrapper.find('.layout-main-section');
+ this.container = this.wrapper.find(".layout-main-section");
this.container.append($('
'));
}
show() {
- if (!this.view || this.view.$route.name == "recorder-detail") return;
- this.view.$router.replace({name: "recorder-detail"});
+ if (!this.route || this.route.name == "RecorderDetail") return;
+ this.router?.replace({ name: "RecorderDetail" });
}
}
diff --git a/frappe/custom/doctype/test_rename_new/__init__.py b/frappe/core/report/database_storage_usage_by_tables/__init__.py
similarity index 100%
rename from frappe/custom/doctype/test_rename_new/__init__.py
rename to frappe/core/report/database_storage_usage_by_tables/__init__.py
diff --git a/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.js b/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.js
new file mode 100644
index 0000000000..b2cf268b36
--- /dev/null
+++ b/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.js
@@ -0,0 +1,7 @@
+// Copyright (c) 2022, Frappe Technologies and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Database Storage Usage By Tables"] = {
+ filters: [],
+};
diff --git a/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.json b/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.json
new file mode 100644
index 0000000000..20deb78ad6
--- /dev/null
+++ b/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.json
@@ -0,0 +1,28 @@
+{
+ "add_total_row": 1,
+ "columns": [],
+ "creation": "2022-10-19 02:25:24.326791",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "letter_head": "abc",
+ "modified": "2022-10-19 02:59:00.365307",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Database Storage Usage By Tables",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "query": "",
+ "ref_doctype": "Error Log",
+ "report_name": "Database Storage Usage By Tables",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "System Manager"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.py b/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.py
new file mode 100644
index 0000000000..c88262552e
--- /dev/null
+++ b/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.py
@@ -0,0 +1,40 @@
+# Copyright (c) 2022, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+import frappe
+
+COLUMNS = [
+ {"label": "Table", "fieldname": "table", "fieldtype": "Data", "width": 200},
+ {"label": "Size (MB)", "fieldname": "size", "fieldtype": "Float"},
+ {"label": "Data (MB)", "fieldname": "data_size", "fieldtype": "Float"},
+ {"label": "Index (MB)", "fieldname": "index_size", "fieldtype": "Float"},
+]
+
+
+def execute(filters=None):
+ frappe.only_for("System Manager")
+
+ data = frappe.db.multisql(
+ {
+ "mariadb": """
+ SELECT table_name AS `table`,
+ round(((data_length + index_length) / 1024 / 1024), 2) `size`,
+ round((data_length / 1024 / 1024), 2) as data_size,
+ round((index_length / 1024 / 1024), 2) as index_size
+ FROM information_schema.TABLES
+ ORDER BY (data_length + index_length) DESC;
+ """,
+ "postgres": """
+ SELECT
+ table_name as "table",
+ round(pg_total_relation_size(quote_ident(table_name)) / 1024 / 1024, 2) as "size",
+ round(pg_relation_size(quote_ident(table_name)) / 1024 / 1024, 2) as "data_size",
+ round(pg_indexes_size(quote_ident(table_name)) / 1024 / 1024, 2) as "index_size"
+ FROM information_schema.tables
+ WHERE table_schema = 'public'
+ ORDER BY 2 DESC;
+ """,
+ },
+ as_dict=1,
+ )
+ return COLUMNS, data
diff --git a/frappe/core/report/database_storage_usage_by_tables/test_database_storage_usage_by_tables.py b/frappe/core/report/database_storage_usage_by_tables/test_database_storage_usage_by_tables.py
new file mode 100644
index 0000000000..e82cbe9caf
--- /dev/null
+++ b/frappe/core/report/database_storage_usage_by_tables/test_database_storage_usage_by_tables.py
@@ -0,0 +1,15 @@
+# Copyright (c) 2022, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+
+from frappe.core.report.database_storage_usage_by_tables.database_storage_usage_by_tables import (
+ execute,
+)
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestDBUsageReport(FrappeTestCase):
+ def test_basic_query(self):
+ _, data = execute()
+ tables = [d.table for d in data]
+ self.assertFalse({"tabUser", "tabDocField"}.difference(tables))
diff --git a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.js b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.js
index 195f25f533..f840a49c92 100644
--- a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.js
+++ b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.js
@@ -2,33 +2,33 @@
// MIT License. See license.txt
frappe.query_reports["Permitted Documents For User"] = {
- "filters": [
+ filters: [
{
- "fieldname": "user",
- "label": __("User"),
- "fieldtype": "Link",
- "options": "User",
- "reqd": 1
+ fieldname: "user",
+ label: __("User"),
+ fieldtype: "Link",
+ options: "User",
+ reqd: 1,
},
{
- "fieldname": "doctype",
- "label": __("DocType"),
- "fieldtype": "Link",
- "options": "DocType",
- "reqd": 1,
- "get_query": function () {
+ fieldname: "doctype",
+ label: __("DocType"),
+ fieldtype: "Link",
+ options: "DocType",
+ reqd: 1,
+ get_query: function () {
return {
- "query": "frappe.core.report.permitted_documents_for_user.permitted_documents_for_user.query_doctypes",
- "filters": {
- "user": frappe.query_report.get_filter_value('user')
- }
- }
- }
+ query: "frappe.core.report.permitted_documents_for_user.permitted_documents_for_user.query_doctypes",
+ filters: {
+ user: frappe.query_report.get_filter_value("user"),
+ },
+ };
+ },
},
{
- "fieldname": "show_permissions",
- "label": __("Show Permissions"),
- "fieldtype": "Check"
- }
- ]
-}
+ fieldname: "show_permissions",
+ label: __("Show Permissions"),
+ fieldtype: "Check",
+ },
+ ],
+};
diff --git a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py
index 362cc6b105..2c92a72ab3 100644
--- a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py
+++ b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py
@@ -4,19 +4,18 @@
import frappe
import frappe.utils.user
from frappe.model import data_fieldtypes
-from frappe.permissions import check_admin_or_system_manager, rights
+from frappe.permissions import rights
def execute(filters=None):
+ frappe.only_for("System Manager")
+
user, doctype, show_permissions = (
filters.get("user"),
filters.get("doctype"),
filters.get("show_permissions"),
)
- if not validate(user, doctype):
- return [], []
-
columns, fields = get_columns_and_fields(doctype)
data = frappe.get_list(doctype, fields=fields, as_list=True, user=user)
@@ -30,15 +29,9 @@ def execute(filters=None):
return columns, data
-def validate(user, doctype):
- # check if current user is System Manager
- check_admin_or_system_manager()
- return user and doctype
-
-
def get_columns_and_fields(doctype):
columns = [f"Name:Link/{doctype}:200"]
- fields = ["`name`"]
+ fields = ["name"]
for df in frappe.get_meta(doctype).fields:
if df.in_list_view and df.fieldtype in data_fieldtypes:
fields.append(f"`{df.fieldname}`")
diff --git a/frappe/core/report/transaction_log_report/transaction_log_report.js b/frappe/core/report/transaction_log_report/transaction_log_report.js
index 54ecf3fcf1..3c7261306d 100644
--- a/frappe/core/report/transaction_log_report/transaction_log_report.js
+++ b/frappe/core/report/transaction_log_report/transaction_log_report.js
@@ -3,9 +3,9 @@
/* eslint-disable */
frappe.query_reports["Transaction Log Report"] = {
- onload: function(query_report) {
- query_report.add_make_chart_button = function() {
+ onload: function (query_report) {
+ query_report.add_make_chart_button = function () {
//
};
- }
-}
+ },
+};
diff --git a/frappe/core/web_form/edit_profile/edit_profile.js b/frappe/core/web_form/edit_profile/edit_profile.js
index 699703c579..8f56ebb353 100644
--- a/frappe/core/web_form/edit_profile/edit_profile.js
+++ b/frappe/core/web_form/edit_profile/edit_profile.js
@@ -1,3 +1,3 @@
-frappe.ready(function() {
+frappe.ready(function () {
// bind events here
-})
\ No newline at end of file
+});
diff --git a/frappe/core/web_form/edit_profile/edit_profile.json b/frappe/core/web_form/edit_profile/edit_profile.json
index c04e705820..9a38b29b68 100644
--- a/frappe/core/web_form/edit_profile/edit_profile.json
+++ b/frappe/core/web_form/edit_profile/edit_profile.json
@@ -1,37 +1,33 @@
{
- "accept_payment": 0,
"allow_comments": 0,
"allow_delete": 0,
"allow_edit": 1,
"allow_incomplete": 0,
"allow_multiple": 0,
"allow_print": 0,
- "amount": 0.0,
- "amount_based_on_field": 0,
"apply_document_permissions": 0,
"breadcrumbs": "[{\"title\": _(\"My Account\"), \"route\": \"me\"}]",
+ "client_script": "frappe.web_form.after_load = () => {\n if (window.location.pathname.endsWith(\"/new\") && frappe.session.user) {\n let current_path = window.location.href;\n window.location.href = current_path.replace(\"/new\", \"/\" + frappe.session.user);\n }\n}",
"creation": "2016-09-19 05:16:59.242754",
"doc_type": "User",
"docstatus": 0,
"doctype": "Web Form",
"idx": 0,
"introduction_text": "",
- "is_multi_step_form": 0,
"is_standard": 1,
+ "list_columns": [],
"login_required": 1,
"max_attachment_size": 0,
- "modified": "2022-03-22 15:00:43.456738",
+ "modified": "2023-01-18 10:26:26.766414",
"modified_by": "Administrator",
"module": "Core",
"name": "edit-profile",
"owner": "Administrator",
"published": 1,
"route": "update-profile",
- "route_to_success_link": 0,
"show_attachments": 0,
- "show_in_grid": 0,
+ "show_list": 0,
"show_sidebar": 0,
- "sidebar_items": [],
"success_message": "Profile updated successfully.",
"success_url": "/me",
"title": "Update Profile",
diff --git a/frappe/core/workspace/build/build.json b/frappe/core/workspace/build/build.json
index c1c506ae3a..67dfae650f 100644
--- a/frappe/core/workspace/build/build.json
+++ b/frappe/core/workspace/build/build.json
@@ -1,6 +1,6 @@
{
"charts": [],
- "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"DocType\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Workspace\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Report\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Elements \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Modules\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Models\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Views\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Scripting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Packages\",\"col\":4}}]",
+ "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"DocType\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Workspace\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Report\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Elements \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Modules\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Models\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Views\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Scripting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Packages\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"System Logs\",\"col\":4}}]",
"creation": "2021-01-02 10:51:16.579957",
"docstatus": 0,
"doctype": "Workspace",
@@ -10,60 +10,6 @@
"idx": 0,
"label": "Build",
"links": [
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Modules",
- "link_count": 0,
- "link_type": "DocType",
- "onboard": 0,
- "only_for": "",
- "type": "Card Break"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Module Def",
- "link_count": 0,
- "link_to": "Module Def",
- "link_type": "DocType",
- "onboard": 0,
- "only_for": "",
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Workspace",
- "link_count": 0,
- "link_to": "Workspace",
- "link_type": "DocType",
- "onboard": 0,
- "only_for": "",
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Module Onboarding",
- "link_count": 0,
- "link_to": "Module Onboarding",
- "link_type": "DocType",
- "onboard": 0,
- "only_for": "",
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Block Module",
- "link_count": 0,
- "link_to": "Block Module",
- "link_type": "DocType",
- "onboard": 0,
- "only_for": "",
- "type": "Link"
- },
{
"hidden": 0,
"is_query_report": 0,
@@ -96,60 +42,6 @@
"only_for": "",
"type": "Link"
},
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Views",
- "link_count": 0,
- "link_type": "DocType",
- "onboard": 0,
- "only_for": "",
- "type": "Card Break"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Report",
- "link_count": 0,
- "link_to": "Report",
- "link_type": "DocType",
- "onboard": 0,
- "only_for": "",
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Print Format",
- "link_count": 0,
- "link_to": "Print Format",
- "link_type": "DocType",
- "onboard": 0,
- "only_for": "",
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Workspace",
- "link_count": 0,
- "link_to": "Workspace",
- "link_type": "DocType",
- "onboard": 0,
- "only_for": "",
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Dashboard",
- "link_count": 0,
- "link_to": "Dashboard",
- "link_type": "DocType",
- "onboard": 0,
- "only_for": "",
- "type": "Link"
- },
{
"hidden": 0,
"is_query_report": 0,
@@ -220,15 +112,175 @@
"link_type": "DocType",
"onboard": 0,
"type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Modules",
+ "link_count": 3,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Module Def",
+ "link_count": 0,
+ "link_to": "Module Def",
+ "link_type": "DocType",
+ "onboard": 0,
+ "only_for": "",
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Module Onboarding",
+ "link_count": 0,
+ "link_to": "Module Onboarding",
+ "link_type": "DocType",
+ "onboard": 0,
+ "only_for": "",
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Module Profile",
+ "link_count": 0,
+ "link_to": "Module Profile",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Views",
+ "link_count": 4,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Report",
+ "link_count": 0,
+ "link_to": "Report",
+ "link_type": "DocType",
+ "onboard": 0,
+ "only_for": "",
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Print Format",
+ "link_count": 0,
+ "link_to": "Print Format",
+ "link_type": "DocType",
+ "onboard": 0,
+ "only_for": "",
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Dashboard",
+ "link_count": 0,
+ "link_to": "Dashboard",
+ "link_type": "DocType",
+ "onboard": 0,
+ "only_for": "",
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Workspace",
+ "link_count": 0,
+ "link_to": "Workspace",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "System Logs",
+ "link_count": 6,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Background Jobs",
+ "link_count": 0,
+ "link_to": "RQ Job",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Scheduled Jobs Logs",
+ "link_count": 0,
+ "link_to": "Scheduled Job Log",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Error Logs",
+ "link_count": 0,
+ "link_to": "Error Log",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Error Snapshot",
+ "link_count": 0,
+ "link_to": "Error Snapshot",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Communication Logs",
+ "link_count": 0,
+ "link_to": "Communication",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Activity Log",
+ "link_count": 0,
+ "link_to": "Activity Log",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
}
],
- "modified": "2022-01-13 17:26:02.736366",
+ "modified": "2022-09-11 06:41:31.095300",
"modified_by": "Administrator",
"module": "Core",
"name": "Build",
"owner": "Administrator",
"parent_page": "",
"public": 1,
+ "quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 5.0,
diff --git a/frappe/core/workspace/settings/settings.json b/frappe/core/workspace/settings/settings.json
index 5aadbc42d5..1469892bd8 100644
--- a/frappe/core/workspace/settings/settings.json
+++ b/frappe/core/workspace/settings/settings.json
@@ -1,6 +1,6 @@
{
"charts": [],
- "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Settings \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"System Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Print Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Website Settings\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Email / Notifications\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Website\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Core\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Printing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Workflow\",\"col\":4}}]",
+ "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Settings \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"System Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Print Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Website Settings\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Email / Notifications\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Website\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Core\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Printing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Workflow\",\"col\":4}}]",
"creation": "2020-03-02 15:09:40.527211",
"docstatus": 0,
"doctype": "Workspace",
@@ -221,58 +221,6 @@
"onboard": 0,
"type": "Link"
},
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Core",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "System Settings",
- "link_count": 0,
- "link_to": "System Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Error Log",
- "link_count": 0,
- "link_to": "Error Log",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Error Snapshot",
- "link_count": 0,
- "link_to": "Error Snapshot",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Domain Settings",
- "link_count": 0,
- "link_to": "Domain Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
{
"hidden": 0,
"is_query_report": 0,
@@ -365,15 +313,46 @@
"link_type": "DocType",
"onboard": 0,
"type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Core",
+ "link_count": 2,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "System Settings",
+ "link_count": 0,
+ "link_to": "System Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Domain Settings",
+ "link_count": 0,
+ "link_to": "Domain Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
}
],
- "modified": "2022-01-13 17:49:59.586909",
+ "modified": "2022-08-28 21:41:28.065190",
"modified_by": "Administrator",
"module": "Core",
"name": "Settings",
"owner": "Administrator",
"parent_page": "",
"public": 1,
+ "quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 29.0,
diff --git a/frappe/coverage.py b/frappe/coverage.py
index ffa3576818..a1f3e26585 100644
--- a/frappe/coverage.py
+++ b/frappe/coverage.py
@@ -24,6 +24,15 @@ STANDARD_EXCLUSIONS = [
"*/patches/*",
]
+# tested via commands' test suite
+TESTED_VIA_CLI = [
+ "*/frappe/installer.py",
+ "*/frappe/build.py",
+ "*/frappe/database/__init__.py",
+ "*/frappe/database/db_manager.py",
+ "*/frappe/database/**/setup_db.py",
+]
+
FRAPPE_EXCLUSIONS = [
"*/tests/*",
"*/commands/*",
@@ -33,7 +42,7 @@ FRAPPE_EXCLUSIONS = [
"*frappe/setup.py",
"*/doctype/*/*_dashboard.py",
"*/patches/*",
-]
+] + TESTED_VIA_CLI
class CodeCoverage:
@@ -64,3 +73,4 @@ class CodeCoverage:
self.coverage.stop()
self.coverage.save()
self.coverage.xml_report()
+ print("Saved Coverage")
diff --git a/frappe/custom/doctype/client_script/client_script.js b/frappe/custom/doctype/client_script/client_script.js
index 18786c62cf..67bb0083c8 100644
--- a/frappe/custom/doctype/client_script/client_script.js
+++ b/frappe/custom/doctype/client_script/client_script.js
@@ -1,46 +1,49 @@
// Copyright (c) 2016, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Client Script', {
+frappe.ui.form.on("Client Script", {
setup(frm) {
frm.get_field("sample").html(SAMPLE_HTML);
},
refresh(frm) {
if (frm.doc.dt && frm.doc.script) {
- frm.add_custom_button(__('Go to {0}', [frm.doc.dt]),
- () => frappe.set_route('List', frm.doc.dt, 'List'));
+ frm.add_custom_button(__("Go to {0}", [frm.doc.dt]), () =>
+ frappe.set_route("List", frm.doc.dt, "List")
+ );
}
- if (frm.doc.view == 'Form') {
- frm.add_custom_button(__('Add script for Child Table'), () => {
+ if (frm.doc.view == "Form") {
+ frm.add_custom_button(__("Add script for Child Table"), () => {
frappe.model.with_doctype(frm.doc.dt, () => {
- const child_tables = frappe.meta.get_docfields(frm.doc.dt, null, {
- fieldtype: 'Table'
- }).map(df => df.options);
+ const child_tables = frappe.meta
+ .get_docfields(frm.doc.dt, null, {
+ fieldtype: "Table",
+ })
+ .map((df) => df.options);
const d = new frappe.ui.Dialog({
- title: __('Select Child Table'),
+ title: __("Select Child Table"),
fields: [
{
- label: __('Select Child Table'),
- fieldtype: 'Link',
- fieldname: 'cdt',
- options: 'DocType',
+ label: __("Select Child Table"),
+ fieldtype: "Link",
+ fieldname: "cdt",
+ options: "DocType",
get_query: () => {
return {
filters: {
istable: 1,
- name: ['in', child_tables]
- }
+ name: ["in", child_tables],
+ },
};
- }
- }
+ },
+ },
],
primary_action: ({ cdt }) => {
- cdt = d.get_field('cdt').value;
+ cdt = d.get_field("cdt").value;
frm.events.add_script_for_doctype(frm, cdt);
d.hide();
- }
+ },
});
d.show();
@@ -48,39 +51,39 @@ frappe.ui.form.on('Client Script', {
});
if (!frm.is_new()) {
- frm.add_custom_button(__('Compare Versions'), () => {
+ frm.add_custom_button(__("Compare Versions"), () => {
new frappe.ui.DiffView("Client Script", "script", frm.doc.name);
});
}
}
- frm.set_query('dt', {
+ frm.set_query("dt", {
filters: {
- istable: 0
- }
+ istable: 0,
+ },
});
},
dt(frm) {
- frm.toggle_display('view', !frappe.boot.single_types.includes(frm.doc.dt));
+ frm.toggle_display("view", !frappe.boot.single_types.includes(frm.doc.dt));
if (!frm.doc.script) {
frm.events.add_script_for_doctype(frm, frm.doc.dt);
}
if (frm.doc.script && !frm.doc.script.includes(frm.doc.dt)) {
- frm.doc.script = '';
+ frm.doc.script = "";
frm.events.add_script_for_doctype(frm, frm.doc.dt);
}
},
view(frm) {
- let has_form_boilerplate = frm.doc.script.includes('frappe.ui.form.on')
- if (frm.doc.view === 'List' && has_form_boilerplate) {
- frm.set_value('script', '');
+ let has_form_boilerplate = frm.doc.script.includes("frappe.ui.form.on");
+ if (frm.doc.view === "List" && has_form_boilerplate) {
+ frm.set_value("script", "");
}
- if (frm.doc.view === 'Form' && !has_form_boilerplate) {
- frm.trigger('dt');
+ if (frm.doc.view === "Form" && !has_form_boilerplate) {
+ frm.trigger("dt");
}
},
@@ -93,12 +96,12 @@ frappe.ui.form.on('${doctype}', {
}
})
`.trim();
- let script = (frm.doc.script || '');
+ let script = frm.doc.script || "";
if (script) {
- script += '\n\n';
+ script += "\n\n";
}
- frm.set_value('script', script + boilerplate);
- }
+ frm.set_value("script", script + boilerplate);
+ },
});
const SAMPLE_HTML = `Client Script Help
diff --git a/frappe/custom/doctype/client_script/test_client_script.py b/frappe/custom/doctype/client_script/test_client_script.py
index c93df04c98..9b50a3d0b0 100644
--- a/frappe/custom/doctype/client_script/test_client_script.py
+++ b/frappe/custom/doctype/client_script/test_client_script.py
@@ -1,9 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
-import unittest
+from frappe.tests.utils import FrappeTestCase
# test_records = frappe.get_test_records('Client Script')
-class TestClientScript(unittest.TestCase):
+class TestClientScript(FrappeTestCase):
pass
diff --git a/frappe/custom/doctype/client_script/ui_test_client_script.js b/frappe/custom/doctype/client_script/ui_test_client_script.js
index 022f677151..0d202d697c 100644
--- a/frappe/custom/doctype/client_script/ui_test_client_script.js
+++ b/frappe/custom/doctype/client_script/ui_test_client_script.js
@@ -12,14 +12,14 @@ context("Client Script", () => {
dt: "ToDo",
view: "Form",
enabled: 1,
- script: `console.log('todo form script')`
+ script: `console.log('todo form script')`,
},
true
);
cy.visit("/app/todo/new", {
onBeforeLoad(win) {
cy.spy(win.console, "log").as("consoleLog");
- }
+ },
});
cy.get("@consoleLog").should("be.calledWith", "todo form script");
});
@@ -32,14 +32,14 @@ context("Client Script", () => {
dt: "ToDo",
view: "List",
enabled: 1,
- script: `console.log('todo list script')`
+ script: `console.log('todo list script')`,
},
true
);
cy.visit("/app/todo", {
onBeforeLoad(win) {
cy.spy(win.console, "log").as("consoleLog");
- }
+ },
});
cy.get("@consoleLog").should("be.calledWith", "todo list script");
});
@@ -52,19 +52,16 @@ context("Client Script", () => {
dt: "ToDo",
view: "List",
enabled: 0,
- script: `console.log('todo disabled script')`
+ script: `console.log('todo disabled script')`,
},
true
);
cy.visit("/app/todo", {
onBeforeLoad(win) {
cy.spy(win.console, "log").as("consoleLog");
- }
+ },
});
- cy.get("@consoleLog").should(
- "not.be.calledWith",
- "todo disabled script"
- );
+ cy.get("@consoleLog").should("not.be.calledWith", "todo disabled script");
});
it("should run multiple scripts", () => {
@@ -75,7 +72,7 @@ context("Client Script", () => {
dt: "ToDo",
view: "Form",
enabled: 1,
- script: `console.log('todo form script 1')`
+ script: `console.log('todo form script 1')`,
},
true
);
@@ -86,14 +83,14 @@ context("Client Script", () => {
dt: "ToDo",
view: "Form",
enabled: 1,
- script: `console.log('todo form script 2')`
+ script: `console.log('todo form script 2')`,
},
true
);
cy.visit("/app/todo/new", {
onBeforeLoad(win) {
cy.spy(win.console, "log").as("consoleLog");
- }
+ },
});
cy.get("@consoleLog").should("be.calledWith", "todo form script 1");
cy.get("@consoleLog").should("be.calledWith", "todo form script 2");
diff --git a/frappe/custom/doctype/custom_field/custom_field.js b/frappe/custom/doctype/custom_field/custom_field.js
index c59fabeaa6..be416cb49a 100644
--- a/frappe/custom/doctype/custom_field/custom_field.js
+++ b/frappe/custom/doctype/custom_field/custom_field.js
@@ -4,88 +4,120 @@
// Refresh
// --------
-frappe.ui.form.on('Custom Field', {
- setup: function(frm) {
- frm.set_query('dt', function(doc) {
+frappe.ui.form.on("Custom Field", {
+ setup: function (frm) {
+ frm.set_query("dt", function (doc) {
var filters = [
- ['DocType', 'issingle', '=', 0],
- ['DocType', 'custom', '=', 0],
- ['DocType', 'name', 'not in', frappe.model.core_doctypes_list],
- ['DocType', 'restrict_to_domain', 'in', frappe.boot.active_domains]
+ ["DocType", "issingle", "=", 0],
+ ["DocType", "custom", "=", 0],
+ ["DocType", "name", "not in", frappe.model.core_doctypes_list],
+ ["DocType", "restrict_to_domain", "in", frappe.boot.active_domains],
];
- if(frappe.session.user!=="Administrator") {
- filters.push(['DocType', 'module', 'not in', ['Core', 'Custom']])
+ if (frappe.session.user !== "Administrator") {
+ filters.push(["DocType", "module", "not in", ["Core", "Custom"]]);
}
return {
- "filters": filters
- }
+ filters: filters,
+ };
});
},
- refresh: function(frm) {
- frm.toggle_enable('dt', frm.doc.__islocal);
- frm.trigger('dt');
- frm.toggle_reqd('label', !frm.doc.fieldname);
+ refresh: function (frm) {
+ frm.toggle_enable("dt", frm.doc.__islocal);
+ frm.trigger("dt");
+ frm.toggle_reqd("label", !frm.doc.fieldname);
+
+ if (frm.doc.is_system_generated) {
+ frm.dashboard.add_comment(
+ __(
+ "Warning: This field is system generated and may be overwritten by a future update. Modify it using {0} instead.",
+ [
+ frappe.utils.get_form_link(
+ "Customize Form",
+ "Customize Form",
+ true,
+ __("Customize Form"),
+ {
+ doc_type: frm.doc.dt,
+ }
+ ),
+ ]
+ ),
+ "yellow",
+ true
+ );
+ }
},
- dt: function(frm) {
- if(!frm.doc.dt) {
- set_field_options('insert_after', '');
+ dt: function (frm) {
+ if (!frm.doc.dt) {
+ set_field_options("insert_after", "");
return;
}
var insert_after = frm.doc.insert_after || null;
return frappe.call({
- method: 'frappe.custom.doctype.custom_field.custom_field.get_fields_label',
+ method: "frappe.custom.doctype.custom_field.custom_field.get_fields_label",
args: { doctype: frm.doc.dt, fieldname: frm.doc.fieldname },
- callback: function(r) {
- if(r) {
- if(r._server_messages && r._server_messages.length) {
+ callback: function (r) {
+ if (r) {
+ if (r._server_messages && r._server_messages.length) {
frm.set_value("dt", "");
} else {
- set_field_options('insert_after', r.message);
- var fieldnames = $.map(r.message, function(v) { return v.value; });
+ set_field_options("insert_after", r.message);
+ var fieldnames = $.map(r.message, function (v) {
+ return v.value;
+ });
- if(insert_after==null || !in_list(fieldnames, insert_after)) {
+ if (insert_after == null || !in_list(fieldnames, insert_after)) {
insert_after = fieldnames[-1];
}
- frm.set_value('insert_after', insert_after);
+ frm.set_value("insert_after", insert_after);
}
}
- }
+ },
});
-
},
- label: function(frm) {
- if(frm.doc.label && frappe.utils.has_special_chars(frm.doc.label)) {
- frm.fields_dict['label_help'].disp_area.innerHTML =
- ''+__('Special Characters are not allowed')+' ';
- frm.set_value('label', '');
+ label: function (frm) {
+ if (frm.doc.label && frappe.utils.has_special_chars(frm.doc.label)) {
+ frm.fields_dict["label_help"].disp_area.innerHTML =
+ '' + __("Special Characters are not allowed") + " ";
+ frm.set_value("label", "");
} else {
- frm.fields_dict['label_help'].disp_area.innerHTML = '';
+ frm.fields_dict["label_help"].disp_area.innerHTML = "";
}
},
- fieldtype: function(frm) {
- if(frm.doc.fieldtype == 'Link') {
- frm.fields_dict['options_help'].disp_area.innerHTML =
- __('Name of the Document Type (DocType) you want this field to be linked to. e.g. Customer');
- } else if(frm.doc.fieldtype == 'Select') {
- frm.fields_dict['options_help'].disp_area.innerHTML =
- __('Options for select. Each option on a new line.')+' '+__('e.g.:')+' '+__('Option 1')+' '+__('Option 2')+' '+__('Option 3')+' ';
- } else if(frm.doc.fieldtype == 'Dynamic Link') {
- frm.fields_dict['options_help'].disp_area.innerHTML =
- __('Fieldname which will be the DocType for this link field.');
+ fieldtype: function (frm) {
+ if (frm.doc.fieldtype == "Link") {
+ frm.fields_dict["options_help"].disp_area.innerHTML = __(
+ "Name of the Document Type (DocType) you want this field to be linked to. e.g. Customer"
+ );
+ } else if (frm.doc.fieldtype == "Select") {
+ frm.fields_dict["options_help"].disp_area.innerHTML =
+ __("Options for select. Each option on a new line.") +
+ " " +
+ __("e.g.:") +
+ " " +
+ __("Option 1") +
+ " " +
+ __("Option 2") +
+ " " +
+ __("Option 3") +
+ " ";
+ } else if (frm.doc.fieldtype == "Dynamic Link") {
+ frm.fields_dict["options_help"].disp_area.innerHTML = __(
+ "Fieldname which will be the DocType for this link field."
+ );
} else {
- frm.fields_dict['options_help'].disp_area.innerHTML = '';
+ frm.fields_dict["options_help"].disp_area.innerHTML = "";
}
- }
+ },
});
-
-frappe.utils.has_special_chars = function(t) {
- var iChars = "!@#$%^&*()+=-[]\\\';,./{}|\":<>?";
+frappe.utils.has_special_chars = function (t) {
+ var iChars = "!@#$%^&*()+=-[]\\';,./{}|\":<>?";
for (var i = 0; i < t.length; i++) {
if (iChars.indexOf(t.charAt(i)) != -1) {
return true;
}
}
return false;
-}
+};
diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py
index 8a2a2663de..8953153be6 100644
--- a/frappe/custom/doctype/custom_field/custom_field.py
+++ b/frappe/custom/doctype/custom_field/custom_field.py
@@ -9,7 +9,7 @@ from frappe.model import core_doctypes_list
from frappe.model.docfield import supports_translation
from frappe.model.document import Document
from frappe.query_builder.functions import IfNull
-from frappe.utils import cstr
+from frappe.utils import cstr, random_string
class CustomField(Document):
@@ -18,11 +18,23 @@ class CustomField(Document):
self.name = self.dt + "-" + self.fieldname
def set_fieldname(self):
+ restricted = (
+ "name",
+ "parent",
+ "creation",
+ "modified",
+ "modified_by",
+ "parentfield",
+ "parenttype",
+ "file_list",
+ "flags",
+ "docstatus",
+ )
if not self.fieldname:
label = self.label
if not label:
if self.fieldtype in ["Section Break", "Column Break", "Tab Break"]:
- label = self.fieldtype + "_" + str(self.idx)
+ label = self.fieldtype + "_" + str(random_string(5))
else:
frappe.throw(_("Label is mandatory"))
@@ -34,6 +46,9 @@ class CustomField(Document):
# fieldnames should be lowercase
self.fieldname = self.fieldname.lower()
+ if self.fieldname in restricted:
+ self.fieldname = self.fieldname + "1"
+
def before_insert(self):
self.set_fieldname()
@@ -80,17 +95,15 @@ class CustomField(Document):
check_fieldname_conflicts(self)
def on_update(self):
- if not frappe.flags.in_setup_wizard:
- frappe.clear_cache(doctype=self.dt)
-
+ # validate field
if not self.flags.ignore_validate:
- # validate field
from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype
validate_fields_for_doctype(self.dt)
- # update the schema
- if not frappe.db.get_value("DocType", self.dt, "issingle") and not frappe.flags.in_setup_wizard:
+ # clear cache and update the schema
+ if not frappe.flags.in_create_custom_fields:
+ frappe.clear_cache(doctype=self.dt)
frappe.db.updatedb(self.dt)
def on_trash(self):
@@ -104,6 +117,20 @@ class CustomField(Document):
# delete property setter entries
frappe.db.delete("Property Setter", {"doc_type": self.dt, "field_name": self.fieldname})
+
+ # update doctype layouts
+ doctype_layouts = frappe.get_all(
+ "DocType Layout", filters={"document_type": self.dt}, pluck="name"
+ )
+
+ for layout in doctype_layouts:
+ layout_doc = frappe.get_doc("DocType Layout", layout)
+ for field in layout_doc.fields:
+ if field.fieldname == self.fieldname:
+ layout_doc.remove(field)
+ layout_doc.save()
+ break
+
frappe.clear_cache(doctype=self.dt)
def validate_insert_after(self, meta):
@@ -130,7 +157,7 @@ def get_fields_label(doctype=None):
return frappe.msgprint(_("Custom Fields can only be added to a standard DocType."))
return [
- {"value": df.fieldname or "", "label": _(df.label or "")}
+ {"value": df.fieldname or "", "label": _(df.label) if df.label else ""}
for df in frappe.get_meta(doctype).get("fields")
]
@@ -169,38 +196,45 @@ def create_custom_fields(custom_fields, ignore_validate=False, update=True):
:param custom_fields: example `{'Sales Invoice': [dict(fieldname='test')]}`"""
- if not ignore_validate and frappe.flags.in_setup_wizard:
- ignore_validate = True
+ try:
+ frappe.flags.in_create_custom_fields = True
+ doctypes_to_update = set()
- for doctypes, fields in custom_fields.items():
- if isinstance(fields, dict):
- # only one field
- fields = [fields]
+ if frappe.flags.in_setup_wizard:
+ ignore_validate = True
- if isinstance(doctypes, str):
- # only one doctype
- doctypes = (doctypes,)
+ for doctypes, fields in custom_fields.items():
+ if isinstance(fields, dict):
+ # only one field
+ fields = [fields]
- for doctype in doctypes:
- for df in fields:
- field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": df["fieldname"]})
- if not field:
- try:
- df["owner"] = "Administrator"
- create_custom_field(doctype, df, ignore_validate=ignore_validate)
- except frappe.exceptions.DuplicateEntryError:
- pass
- elif update:
- custom_field = frappe.get_doc("Custom Field", field)
- custom_field.flags.ignore_validate = ignore_validate
- custom_field.update(df)
- custom_field.save()
+ if isinstance(doctypes, str):
+ # only one doctype
+ doctypes = (doctypes,)
- frappe.clear_cache(doctype=doctype)
- frappe.db.updatedb(doctype)
+ for doctype in doctypes:
+ doctypes_to_update.add(doctype)
+ for df in fields:
+ field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": df["fieldname"]})
+ if not field:
+ try:
+ df = df.copy()
+ df["owner"] = "Administrator"
+ create_custom_field(doctype, df, ignore_validate=ignore_validate)
-@frappe.whitelist()
-def add_custom_field(doctype, df):
- df = json.loads(df)
- return create_custom_field(doctype, df)
+ except frappe.exceptions.DuplicateEntryError:
+ pass
+
+ elif update:
+ custom_field = frappe.get_doc("Custom Field", field)
+ custom_field.flags.ignore_validate = ignore_validate
+ custom_field.update(df)
+ custom_field.save()
+
+ for doctype in doctypes_to_update:
+ frappe.clear_cache(doctype=doctype)
+ frappe.db.updatedb(doctype)
+
+ finally:
+ frappe.flags.in_create_custom_fields = False
diff --git a/frappe/custom/doctype/custom_field/test_custom_field.py b/frappe/custom/doctype/custom_field/test_custom_field.py
index 34223315c5..cf64e4495b 100644
--- a/frappe/custom/doctype/custom_field/test_custom_field.py
+++ b/frappe/custom/doctype/custom_field/test_custom_field.py
@@ -1,17 +1,15 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
-import unittest
-
import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+from frappe.tests.utils import FrappeTestCase
test_records = frappe.get_test_records("Custom Field")
-class TestCustomField(unittest.TestCase):
+class TestCustomField(FrappeTestCase):
def test_create_custom_fields(self):
- from .custom_field import create_custom_fields
-
create_custom_fields(
{
"Address": [
@@ -38,3 +36,48 @@ class TestCustomField(unittest.TestCase):
self.assertTrue(frappe.db.exists("Custom Field", "Address-_test_custom_field_1"))
self.assertTrue(frappe.db.exists("Custom Field", "Address-_test_custom_field_2"))
self.assertTrue(frappe.db.exists("Custom Field", "Contact-_test_custom_field_2"))
+
+ def test_custom_field_sorting(self):
+ try:
+ custom_fields = {
+ "ToDo": [
+ {"fieldname": "a_test_field", "insert_after": "b_test_field"},
+ {"fieldname": "b_test_field", "insert_after": "status"},
+ {"fieldname": "c_test_field", "insert_after": "unknown_custom_field"},
+ {"fieldname": "d_test_field", "insert_after": "status"},
+ ]
+ }
+
+ create_custom_fields(custom_fields, ignore_validate=True)
+
+ fields = frappe.get_meta("ToDo", cached=False).fields
+
+ for i, field in enumerate(fields):
+ if field.fieldname == "b_test_field":
+ self.assertEqual(fields[i - 1].fieldname, "status")
+
+ if field.fieldname == "d_test_field":
+ self.assertEqual(fields[i - 1].fieldname, "a_test_field")
+
+ self.assertEqual(fields[-1].fieldname, "c_test_field")
+
+ finally:
+ frappe.db.delete(
+ "Custom Field",
+ {
+ "dt": "ToDo",
+ "fieldname": (
+ "in",
+ (
+ "a_test_field",
+ "b_test_field",
+ "c_test_field",
+ "d_test_field",
+ ),
+ ),
+ },
+ )
+
+ # undo changes commited by DDL
+ # nosemgrep
+ frappe.db.commit()
diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js
index 3ec6795f0e..8549c239e5 100644
--- a/frappe/custom/doctype/customize_form/customize_form.js
+++ b/frappe/custom/doctype/customize_form/customize_form.js
@@ -4,7 +4,7 @@
frappe.provide("frappe.customize_form");
frappe.ui.form.on("Customize Form", {
- setup: function(frm) {
+ setup: function (frm) {
// save the last setting if refreshing
window.addEventListener("beforeunload", () => {
if (frm.doc.doc_type && frm.doc.doc_type != "undefined") {
@@ -13,93 +13,90 @@ frappe.ui.form.on("Customize Form", {
});
},
- onload: function(frm) {
- frm.set_query("doc_type", function() {
+ onload: function (frm) {
+ frm.set_query("doc_type", function () {
return {
filters: [
["DocType", "issingle", "=", 0],
["DocType", "custom", "=", 0],
- [
- "DocType",
- "name",
- "not in",
- frappe.model.core_doctypes_list
- ],
- [
- "DocType",
- "restrict_to_domain",
- "in",
- frappe.boot.active_domains
- ]
- ]
+ ["DocType", "name", "not in", frappe.model.core_doctypes_list],
+ ["DocType", "restrict_to_domain", "in", frappe.boot.active_domains],
+ ],
};
});
- frm.set_query("default_print_format", function() {
+ frm.set_query("default_print_format", function () {
return {
filters: {
print_format_type: ["!=", "JS"],
- doc_type: ["=", frm.doc.doc_type]
- }
+ doc_type: ["=", frm.doc.doc_type],
+ },
};
});
- $(frm.wrapper).on("grid-row-render", function(e, grid_row) {
+ $(frm.wrapper).on("grid-row-render", function (e, grid_row) {
if (grid_row.doc && grid_row.doc.fieldtype == "Section Break") {
$(grid_row.row).css({ "font-weight": "bold" });
}
grid_row.row.removeClass("highlight");
- if (grid_row.doc.is_custom_field &&
- !grid_row.row.hasClass('highlight') &&
- !grid_row.doc.is_system_generated) {
+ if (
+ grid_row.doc.is_custom_field &&
+ !grid_row.row.hasClass("highlight") &&
+ !grid_row.doc.is_system_generated
+ ) {
grid_row.row.addClass("highlight");
}
});
- $(frm.wrapper).on("grid-make-sortable", function(e, frm) {
+ $(frm.wrapper).on("grid-make-sortable", function (e, frm) {
frm.trigger("setup_sortable");
});
- $(frm.wrapper).on("grid-move-row", function(e, frm) {
+ $(frm.wrapper).on("grid-move-row", function (e, frm) {
frm.trigger("setup_sortable");
});
},
- doc_type: function(frm) {
+ doc_type: function (frm) {
if (frm.doc.doc_type) {
return frm.call({
method: "fetch_to_customize",
doc: frm.doc,
freeze: true,
- callback: function(r) {
+ callback: function (r) {
if (r) {
if (r._server_messages && r._server_messages.length) {
frm.set_value("doc_type", "");
} else {
frm.refresh();
frm.trigger("setup_sortable");
+ frm.trigger("setup_default_views");
}
}
localStorage["customize_doctype"] = frm.doc.doc_type;
- }
+ },
});
} else {
frm.refresh();
}
},
- setup_sortable: function(frm) {
- frm.doc.fields.forEach(function(f, i) {
- if (!f.is_custom_field) {
+ is_calendar_and_gantt: function (frm) {
+ frm.trigger("setup_default_views");
+ },
+
+ setup_sortable: function (frm) {
+ frm.doc.fields.forEach(function (f) {
+ if (!f.is_custom_field || f.is_system_generated) {
f._sortable = false;
}
if (f.fieldtype == "Table") {
frm.add_custom_button(
f.options,
- function() {
+ function () {
frm.set_value("doc_type", f.options);
},
__("Customize Child Table")
@@ -109,52 +106,57 @@ frappe.ui.form.on("Customize Form", {
frm.fields_dict.fields.grid.refresh();
},
- refresh: function(frm) {
+ refresh: function (frm) {
frm.disable_save(true);
frm.page.clear_icons();
if (frm.doc.doc_type) {
- frm.page.set_title(__('Customize Form - {0}', [frm.doc.doc_type]));
- frappe.customize_form.set_primary_action(frm);
+ frappe.model.with_doctype(frm.doc.doc_type).then(() => {
+ frm.page.set_title(__("Customize Form - {0}", [frm.doc.doc_type]));
+ frappe.customize_form.set_primary_action(frm);
- frm.add_custom_button(
- __("Go to {0} List", [__(frm.doc.doc_type)]),
- function() {
- frappe.set_route("List", frm.doc.doc_type);
- },
- __("Actions")
- );
+ render_form_builder_message(frm);
- frm.add_custom_button(
- __("Reload"),
- function() {
- frm.script_manager.trigger("doc_type");
- },
- __("Actions")
- );
+ frm.add_custom_button(
+ __("Go to {0} List", [__(frm.doc.doc_type)]),
+ function () {
+ frappe.set_route("List", frm.doc.doc_type);
+ },
+ __("Actions")
+ );
- frm.add_custom_button(
- __("Reset to defaults"),
- function() {
- frappe.customize_form.confirm(
- __("Remove all customizations?"),
- frm
- );
- },
- __("Actions")
- );
+ frm.add_custom_button(
+ __("Reload"),
+ function () {
+ frm.script_manager.trigger("doc_type");
+ },
+ __("Actions")
+ );
- frm.add_custom_button(
- __("Set Permissions"),
- function() {
- frappe.set_route("permission-manager", frm.doc.doc_type);
- },
- __("Actions")
- );
+ frm.add_custom_button(
+ __("Reset to defaults"),
+ function () {
+ frappe.customize_form.confirm(__("Remove all customizations?"), frm);
+ },
+ __("Actions")
+ );
- const is_autoname_autoincrement = frm.doc.autoname === 'autoincrement';
- frm.set_df_property("naming_rule", "hidden", is_autoname_autoincrement);
- frm.set_df_property("autoname", "read_only", is_autoname_autoincrement);
+ frm.add_custom_button(
+ __("Set Permissions"),
+ function () {
+ frappe.set_route("permission-manager", frm.doc.doc_type);
+ },
+ __("Actions")
+ );
+
+ const is_autoname_autoincrement = frm.doc.autoname === "autoincrement";
+ frm.set_df_property("naming_rule", "hidden", is_autoname_autoincrement);
+ frm.set_df_property("autoname", "read_only", is_autoname_autoincrement);
+ frm.toggle_display(
+ ["queue_in_background"],
+ frappe.get_meta(frm.doc.doc_type).is_submittable || 0
+ );
+ });
}
frm.events.setup_export(frm);
@@ -181,37 +183,38 @@ frappe.ui.form.on("Customize Form", {
if (frappe.boot.developer_mode) {
frm.add_custom_button(
__("Export Customizations"),
- function() {
+ function () {
frappe.prompt(
[
{
fieldtype: "Link",
fieldname: "module",
options: "Module Def",
- label: __("Module to Export")
+ label: __("Module to Export"),
+ reqd: 1,
},
{
fieldtype: "Check",
fieldname: "sync_on_migrate",
label: __("Sync on Migrate"),
- default: 1
+ default: 1,
},
{
fieldtype: "Check",
fieldname: "with_permissions",
label: __("Export Custom Permissions"),
- default: 1
- }
+ default: 1,
+ },
],
- function(data) {
+ function (data) {
frappe.call({
method: "frappe.modules.utils.export_customizations",
args: {
doctype: frm.doc.doc_type,
module: data.module,
sync_on_migrate: data.sync_on_migrate,
- with_permissions: data.with_permissions
- }
+ with_permissions: data.with_permissions,
+ },
});
},
__("Select Module")
@@ -225,127 +228,190 @@ frappe.ui.form.on("Customize Form", {
setup_sort_order(frm) {
// sort order select
if (frm.doc.doc_type) {
- var fields = $.map(frm.doc.fields, function(df) {
- return frappe.model.is_value_type(df.fieldtype)
- ? df.fieldname
- : null;
+ var fields = $.map(frm.doc.fields, function (df) {
+ return frappe.model.is_value_type(df.fieldtype) ? df.fieldname : null;
});
fields = ["", "name", "modified"].concat(fields);
frm.set_df_property("sort_field", "options", fields);
}
- }
+ },
+
+ setup_default_views(frm) {
+ frappe.model.set_default_views_for_doctype(frm.doc.doc_type, frm);
+ },
});
// can't delete standard fields
frappe.ui.form.on("Customize Form Field", {
- before_fields_remove: function(frm, doctype, name) {
- var row = frappe.get_doc(doctype, name);
+ before_fields_remove: function (frm, doctype, name) {
+ const row = frappe.get_doc(doctype, name);
+
+ if (row.is_system_generated) {
+ frappe.throw(
+ __(
+ "Cannot delete system generated field {0} . You can hide it instead.",
+ [__(row.label) || row.fieldname]
+ )
+ );
+ }
+
if (!(row.is_custom_field || row.__islocal)) {
- frappe.msgprint(__("Cannot delete standard field. You can hide it if you want"));
- throw "cannot delete standard field";
+ frappe.throw(
+ __("Cannot delete standard field {0} . You can hide it instead.", [
+ __(row.label) || row.fieldname,
+ ])
+ );
}
},
- fields_add: function(frm, cdt, cdn) {
+ fields_add: function (frm, cdt, cdn) {
var f = frappe.model.get_doc(cdt, cdn);
f.is_system_generated = false;
f.is_custom_field = true;
- }
+ frm.trigger("setup_default_views");
+ },
+
+ form_render(frm, doctype, docname) {
+ frm.trigger("setup_fetch_from_fields", doctype, docname);
+ },
});
// can't delete standard links
frappe.ui.form.on("DocType Link", {
- before_links_remove: function(frm, doctype, name) {
+ before_links_remove: function (frm, doctype, name) {
let row = frappe.get_doc(doctype, name);
if (!(row.custom || row.__islocal)) {
frappe.msgprint(__("Cannot delete standard link. You can hide it if you want"));
throw "cannot delete standard link";
}
},
- links_add: function(frm, cdt, cdn) {
+ links_add: function (frm, cdt, cdn) {
let f = frappe.model.get_doc(cdt, cdn);
f.custom = 1;
- }
+ },
});
// can't delete standard actions
frappe.ui.form.on("DocType Action", {
- before_actions_remove: function(frm, doctype, name) {
+ before_actions_remove: function (frm, doctype, name) {
let row = frappe.get_doc(doctype, name);
if (!(row.custom || row.__islocal)) {
frappe.msgprint(__("Cannot delete standard action. You can hide it if you want"));
throw "cannot delete standard action";
}
},
- actions_add: function(frm, cdt, cdn) {
+ actions_add: function (frm, cdt, cdn) {
let f = frappe.model.get_doc(cdt, cdn);
f.custom = 1;
- }
+ },
});
// can't delete standard states
frappe.ui.form.on("DocType State", {
- before_states_remove: function(frm, doctype, name) {
+ before_states_remove: function (frm, doctype, name) {
let row = frappe.get_doc(doctype, name);
if (!(row.custom || row.__islocal)) {
frappe.msgprint(__("Cannot delete standard document state."));
throw "cannot delete standard document state";
}
},
- states_add: function(frm, cdt, cdn) {
+ states_add: function (frm, cdt, cdn) {
let f = frappe.model.get_doc(cdt, cdn);
f.custom = 1;
- }
+ },
});
-frappe.customize_form.set_primary_action = function(frm) {
- frm.page.set_primary_action(__("Update"), function() {
- if (frm.doc.doc_type) {
- return frm.call({
- doc: frm.doc,
- freeze: true,
- btn: frm.page.btn_primary,
- method: "save_customization",
- callback: function(r) {
- if (!r.exc) {
- frappe.customize_form.clear_locals_and_refresh(frm);
- frm.script_manager.trigger("doc_type");
- }
- }
- });
+frappe.customize_form.validate_fieldnames = async function (frm) {
+ for (let i = 0; i < frm.doc.fields.length; i++) {
+ let field = frm.doc.fields[i];
+
+ let fieldname = field.label && frappe.model.scrub(field.label).toLowerCase();
+ if (
+ field.label &&
+ !field.fieldname &&
+ in_list(frappe.model.restricted_fields, fieldname)
+ ) {
+ let message = __(
+ "For field {0} in row {1} , fieldname {2} is restricted it will be renamed as {2}1 . Do you want to continue?",
+ [field.label, field.idx, fieldname]
+ );
+ await pause_to_confirm(message);
}
+ }
+
+ function pause_to_confirm(message) {
+ return new Promise((resolve) => {
+ frappe.confirm(
+ message,
+ () => resolve(),
+ () => {
+ frm.page.btn_primary.prop("disabled", false);
+ }
+ );
+ });
+ }
+};
+
+frappe.customize_form.save_customization = function (frm) {
+ if (frm.doc.doc_type) {
+ return frm.call({
+ doc: frm.doc,
+ freeze: true,
+ freeze_message: __("Saving Customization..."),
+ btn: frm.page.btn_primary,
+ method: "save_customization",
+ callback: function (r) {
+ if (!r.exc) {
+ frappe.customize_form.clear_locals_and_refresh(frm);
+ frm.script_manager.trigger("doc_type");
+ }
+ },
+ });
+ }
+};
+
+frappe.customize_form.set_primary_action = function (frm) {
+ frm.page.set_primary_action(__("Update"), async () => {
+ await this.validate_fieldnames(frm);
+ this.save_customization(frm);
});
};
-frappe.customize_form.confirm = function(msg, frm) {
+frappe.customize_form.confirm = function (msg, frm) {
if (!frm.doc.doc_type) return;
var d = new frappe.ui.Dialog({
- title: 'Reset To Defaults',
+ title: "Reset To Defaults",
fields: [
- {fieldtype:"HTML", options:__("All customizations will be removed. Please confirm.")},
+ {
+ fieldtype: "HTML",
+ options: __("All customizations will be removed. Please confirm."),
+ },
],
- primary_action: function() {
+ primary_action: function () {
return frm.call({
doc: frm.doc,
method: "reset_to_defaults",
- callback: function(r) {
+ callback: function (r) {
if (r.exc) {
frappe.msgprint(r.exc);
} else {
d.hide();
- frappe.show_alert({message:__('Customizations Reset'), indicator:'green'});
+ frappe.show_alert({
+ message: __("Customizations Reset"),
+ indicator: "green",
+ });
frappe.customize_form.clear_locals_and_refresh(frm);
}
- }
+ },
});
- }
+ },
});
frappe.customize_form.confirm.dialog = d;
d.show();
-}
+};
-frappe.customize_form.clear_locals_and_refresh = function(frm) {
+frappe.customize_form.clear_locals_and_refresh = function (frm) {
delete frm.doc.__unsaved;
// clear doctype from locals
frappe.model.clear_doc("DocType", frm.doc.doc_type);
@@ -353,4 +419,31 @@ frappe.customize_form.clear_locals_and_refresh = function(frm) {
frm.refresh();
};
-extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({frm: cur_frm}));
+function render_form_builder_message(frm) {
+ $(frm.fields_dict["try_form_builder_html"].wrapper).empty();
+ if (!frm.is_new() && frm.fields_dict["try_form_builder_html"]) {
+ let title = __("Use Form Builder to visually customize your form layout");
+ let msg = __(
+ "You can drag and drop fields to create your form layout, add tabs, sections and columns to organize your form and update field properties all from one screen."
+ );
+
+ let message = `
+
+ `;
+
+ $(frm.fields_dict["try_form_builder_html"].wrapper).html(message);
+ }
+}
+
+extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({ frm: cur_frm }));
diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json
index 0011f51af4..e0d822eb61 100644
--- a/frappe/custom/doctype/customize_form/customize_form.json
+++ b/frappe/custom/doctype/customize_form/customize_form.json
@@ -10,28 +10,36 @@
"doc_type",
"properties",
"label",
- "max_attachments",
"search_fields",
"column_break_5",
- "allow_copy",
"istable",
+ "is_calendar_and_gantt",
"editable_grid",
"quick_entry",
"track_changes",
"track_views",
"allow_auto_repeat",
"allow_import",
+ "queue_in_background",
"fields_section_break",
+ "try_form_builder_html",
"fields",
"naming_section",
"naming_rule",
"autoname",
+ "form_settings_section",
+ "image_field",
+ "max_attachments",
+ "column_break_21",
+ "allow_copy",
+ "make_attachments_public",
"view_settings_section",
"title_field",
"show_title_field_in_link",
- "translate_link_fields",
- "image_field",
+ "translated_doctype",
"default_print_format",
+ "default_view",
+ "force_re_route_to_default_view",
"column_break_29",
"show_preview_popup",
"email_settings_section",
@@ -315,9 +323,55 @@
},
{
"default": "0",
- "fieldname": "translate_link_fields",
+ "fieldname": "translated_doctype",
"fieldtype": "Check",
"label": "Translate Link Fields"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "form_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Form Settings"
+ },
+ {
+ "fieldname": "column_break_21",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "make_attachments_public",
+ "fieldtype": "Check",
+ "label": "Make Attachments Public by Default"
+ },
+ {
+ "default": "0",
+ "fieldname": "queue_in_background",
+ "fieldtype": "Check",
+ "label": "Queue in Background"
+ },
+ {
+ "fieldname": "default_view",
+ "fieldtype": "Select",
+ "label": "Default View"
+ },
+ {
+ "default": "0",
+ "depends_on": "default_view",
+ "fieldname": "force_re_route_to_default_view",
+ "fieldtype": "Check",
+ "label": "Force Re-route to Default View"
+ },
+ {
+ "default": "0",
+ "description": "Enables Calendar and Gantt views.",
+ "fieldname": "is_calendar_and_gantt",
+ "fieldtype": "Check",
+ "label": "Is Calendar and Gantt"
+ },
+ {
+ "fieldname": "try_form_builder_html",
+ "fieldtype": "HTML",
+ "label": "Try Form Builder HTML"
}
],
"hide_toolbar": 1,
@@ -326,7 +380,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2022-05-13 15:36:16.772277",
+ "modified": "2023-05-15 16:03:19.872532",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index 4923bfc525..42cbf33f4f 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -193,8 +193,9 @@ class CustomizeForm(Document):
# docfield
for df in self.get("fields"):
meta_df = meta.get("fields", {"fieldname": df.fieldname})
- if not meta_df or meta_df[0].get("is_custom_field"):
+ if not meta_df or not is_standard_or_system_generated_field(meta_df[0]):
continue
+
self.set_property_setters_for_docfield(meta, df, meta_df)
# action and links
@@ -350,12 +351,14 @@ class CustomizeForm(Document):
def update_custom_fields(self):
for i, df in enumerate(self.get("fields")):
- if df.get("is_custom_field"):
- if not frappe.db.exists("Custom Field", {"dt": self.doc_type, "fieldname": df.fieldname}):
- self.add_custom_field(df, i)
- self.flags.update_db = True
- else:
- self.update_in_custom_field(df, i)
+ if is_standard_or_system_generated_field(df):
+ continue
+
+ if not frappe.db.exists("Custom Field", {"dt": self.doc_type, "fieldname": df.fieldname}):
+ self.add_custom_field(df, i)
+ self.flags.update_db = True
+ else:
+ self.update_in_custom_field(df, i)
self.delete_custom_fields()
@@ -374,10 +377,13 @@ class CustomizeForm(Document):
d.insert()
df.fieldname = d.fieldname
+ if df.get("in_global_search"):
+ self.flags.rebuild_doctype_for_global_search = True
+
def update_in_custom_field(self, df, i):
meta = frappe.get_meta(self.doc_type)
meta_df = meta.get("fields", {"fieldname": df.fieldname})
- if not (meta_df and meta_df[0].get("is_custom_field")):
+ if not meta_df or is_standard_or_system_generated_field(meta_df[0]):
# not a custom field
return
@@ -387,6 +393,8 @@ class CustomizeForm(Document):
if df.get(prop) != custom_field.get(prop):
if prop == "fieldtype":
self.validate_fieldtype_change(df, meta_df[0].get(prop), df.get(prop))
+ if prop == "in_global_search":
+ self.flags.rebuild_doctype_for_global_search = True
custom_field.set(prop, df.get(prop))
changed = True
@@ -411,7 +419,7 @@ class CustomizeForm(Document):
}
for fieldname in fields_to_remove:
df = meta.get("fields", {"fieldname": fieldname})[0]
- if df.get("is_custom_field"):
+ if not is_standard_or_system_generated_field(df):
frappe.delete_doc("Custom Field", df.name)
def make_property_setter(
@@ -556,6 +564,10 @@ def reset_customization(doctype):
frappe.clear_cache(doctype=doctype)
+def is_standard_or_system_generated_field(df):
+ return not df.get("is_custom_field") or df.get("is_system_generated")
+
+
doctype_properties = {
"search_fields": "Data",
"title_field": "Data",
@@ -566,8 +578,10 @@ doctype_properties = {
"allow_copy": "Check",
"istable": "Check",
"quick_entry": "Check",
+ "queue_in_background": "Check",
"editable_grid": "Check",
"max_attachments": "Int",
+ "make_attachments_public": "Check",
"track_changes": "Check",
"track_views": "Check",
"allow_auto_repeat": "Check",
@@ -581,6 +595,10 @@ doctype_properties = {
"autoname": "Data",
"show_title_field_in_link": "Check",
"translate_link_fields": "Check",
+ "is_calendar_and_gantt": "Check",
+ "default_view": "Select",
+ "force_re_route_to_default_view": "Check",
+ "translated_doctype": "Check",
}
docfield_properties = {
diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py
index b00f45f5d2..8d98dc4149 100644
--- a/frappe/custom/doctype/customize_form/test_customize_form.py
+++ b/frappe/custom/doctype/customize_form/test_customize_form.py
@@ -2,17 +2,17 @@
# License: MIT. See LICENSE
import json
-import unittest
import frappe
from frappe.core.doctype.doctype.doctype import InvalidFieldNameError
from frappe.core.doctype.doctype.test_doctype import new_doctype
from frappe.test_runner import make_test_records_for_doctype
+from frappe.tests.utils import FrappeTestCase
test_dependencies = ["Custom Field", "Property Setter"]
-class TestCustomizeForm(unittest.TestCase):
+class TestCustomizeForm(FrappeTestCase):
def insert_custom_field(self):
frappe.delete_doc_if_exists("Custom Field", "Event-test_custom_field")
frappe.get_doc(
@@ -54,7 +54,7 @@ class TestCustomizeForm(unittest.TestCase):
d = self.get_customize_form("Event")
self.assertEqual(d.doc_type, "Event")
- self.assertEqual(len(d.get("fields")), 36)
+ self.assertEqual(len(d.get("fields")), 38)
d = self.get_customize_form("Event")
self.assertEqual(d.doc_type, "Event")
@@ -403,3 +403,25 @@ class TestCustomizeForm(unittest.TestCase):
with self.assertRaises(frappe.ValidationError):
d.run_method("save_customization")
+
+ def test_system_generated_fields(self):
+ doctype = "Event"
+ custom_field_name = "test_custom_field"
+
+ custom_field = frappe.get_doc("Custom Field", {"dt": doctype, "fieldname": custom_field_name})
+ custom_field.is_system_generated = 1
+ custom_field.save()
+
+ d = self.get_customize_form(doctype)
+ custom_field = d.getone("fields", {"fieldname": custom_field_name})
+ custom_field.description = "Test Description"
+ d.run_method("save_customization")
+
+ property_setter_filters = {
+ "doc_type": doctype,
+ "field_name": custom_field_name,
+ "property": "description",
+ }
+ self.assertEqual(
+ frappe.db.get_value("Property Setter", property_setter_filters, "value"), "Test Description"
+ )
diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json
index 8fa054894f..d8da44101b 100644
--- a/frappe/custom/doctype/customize_form_field/customize_form_field.json
+++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json
@@ -212,6 +212,7 @@
},
{
"default": "0",
+ "depends_on": "eval:!in_list(['Section Break', 'Column Break', 'Tab Break'], doc.fieldtype)",
"fieldname": "permlevel",
"fieldtype": "Int",
"in_list_view": 1,
@@ -348,7 +349,7 @@
"width": "50px"
},
{
- "depends_on": "eval:cur_frm.doc.istable",
+ "depends_on": "eval:parent.istable",
"description": "Number of columns for a field in a Grid (Total Columns in a grid should be less than 11)",
"fieldname": "columns",
"fieldtype": "Int",
@@ -467,7 +468,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2022-04-13 22:31:14.162661",
+ "modified": "2023-02-20 12:07:40.242470",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",
@@ -477,4 +478,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"states": []
-}
+}
\ No newline at end of file
diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.js b/frappe/custom/doctype/doctype_layout/doctype_layout.js
index 533efea9b8..b212b79a5b 100644
--- a/frappe/custom/doctype/doctype_layout/doctype_layout.js
+++ b/frappe/custom/doctype/doctype_layout/doctype_layout.js
@@ -1,30 +1,105 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('DocType Layout', {
- refresh: function(frm) {
- frm.trigger('document_type');
- frm.events.set_button(frm);
- },
+frappe.ui.form.on("DocType Layout", {
+ onload_post_render(frm) {
+ // disallow users from manually adding/deleting rows; this doctype should only
+ // be used for managing layout, and docfields and custom fields should be used
+ // to manage other field metadata (hidden, etc.)
+ frm.set_df_property("fields", "cannot_add_rows", true);
+ frm.set_df_property("fields", "cannot_delete_rows", true);
- document_type(frm) {
- frm.set_fields_as_options('fields', frm.doc.document_type, null, [], 'fieldname').then(() => {
- // child table empty? then show all fields as default
- if (frm.doc.document_type) {
- if (!(frm.doc.fields || []).length) {
- for (let f of frappe.get_doc('DocType', frm.doc.document_type).fields) {
- frm.add_child('fields', { fieldname: f.fieldname, label: f.label });
- }
- }
- }
+ $(frm.wrapper).on("grid-move-row", (e, frm) => {
+ // refresh the layout after moving a row
+ frm.dirty();
});
},
- set_button(frm) {
+ refresh(frm) {
+ frm.events.add_buttons(frm);
+ },
+
+ async document_type(frm) {
+ if (frm.doc.document_type) {
+ // refreshing the doctype fields resets the new name input field;
+ // once the fields are set, reset the name to the original input
+ if (frm.is_new()) {
+ const document_name = frm.doc.__newname || frm.doc.name;
+ }
+
+ frm.set_value("fields", []);
+ await frm.events.sync_fields(frm, false);
+
+ if (frm.is_new()) {
+ frm.doc.__newname = document_name;
+ frm.refresh_field("__newname");
+ }
+ }
+ },
+
+ add_buttons(frm) {
if (!frm.is_new()) {
- frm.add_custom_button(__('Go to {0} List', [frm.doc.name]), () => {
+ frm.add_custom_button(__("Go to {0} List", [frm.doc.name]), () => {
window.open(`/app/${frappe.router.slug(frm.doc.name)}`);
});
+
+ frm.add_custom_button(__("Sync {0} Fields", [frm.doc.name]), async () => {
+ await frm.events.sync_fields(frm, true);
+ });
}
- }
+ },
+
+ async sync_fields(frm, notify) {
+ frappe.dom.freeze("Fetching fields...");
+ const response = await frm.call({ doc: frm.doc, method: "sync_fields" });
+ frm.refresh_field("fields");
+ frappe.dom.unfreeze();
+
+ if (!response.message) {
+ frappe.msgprint(__("No changes to sync"));
+ return;
+ }
+
+ frm.dirty();
+ if (notify) {
+ const addedFields = response.message.added;
+ const removedFields = response.message.removed;
+
+ const getChangedMessage = (fields) => {
+ let changes = "";
+ for (const field of fields) {
+ if (field.label) {
+ changes += `Row #${field.idx}: ${field.fieldname.bold()} (${
+ field.label
+ }) `;
+ } else {
+ changes += `Row #${field.idx}: ${field.fieldname.bold()} `;
+ }
+ }
+ return changes;
+ };
+
+ let message = "";
+
+ if (addedFields.length) {
+ message += `The following fields have been added:${getChangedMessage(
+ addedFields
+ )} `;
+ }
+
+ if (removedFields.length) {
+ message += `The following fields have been removed:${getChangedMessage(
+ removedFields
+ )} `;
+ }
+
+ if (message) {
+ frappe.msgprint({
+ message: __(message),
+ indicator: "green",
+ title: __("Synced Fields"),
+ });
+ }
+ }
+ },
});
diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.json b/frappe/custom/doctype/doctype_layout/doctype_layout.json
index e47c9e03e0..ffb5cdae31 100644
--- a/frappe/custom/doctype/doctype_layout/doctype_layout.json
+++ b/frappe/custom/doctype/doctype_layout/doctype_layout.json
@@ -1,7 +1,7 @@
{
"actions": [],
"allow_rename": 1,
- "autoname": "Prompt",
+ "autoname": "prompt",
"creation": "2020-11-16 17:05:35.306846",
"doctype": "DocType",
"editable_grid": 1,
@@ -19,7 +19,8 @@
"in_list_view": 1,
"label": "Document Type",
"options": "DocType",
- "reqd": 1
+ "reqd": 1,
+ "set_only_once": 1
},
{
"fieldname": "fields",
@@ -42,10 +43,11 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-12-10 15:01:04.352184",
+ "modified": "2023-02-14 17:53:24.486171",
"modified_by": "Administrator",
"module": "Custom",
"name": "DocType Layout",
+ "naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
@@ -62,11 +64,12 @@
},
{
"read": 1,
- "role": "Guest"
+ "role": "All"
}
],
"route": "doctype-layout",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.py b/frappe/custom/doctype/doctype_layout/doctype_layout.py
index ea8e9acc99..f712853ccd 100644
--- a/frappe/custom/doctype/doctype_layout/doctype_layout.py
+++ b/frappe/custom/doctype/doctype_layout/doctype_layout.py
@@ -1,11 +1,77 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# License: MIT. See LICENSE
+from typing import TYPE_CHECKING
+
+import frappe
from frappe.desk.utils import slug
from frappe.model.document import Document
+if TYPE_CHECKING:
+ from frappe.core.doctype.docfield.docfield import DocField
+
class DocTypeLayout(Document):
def validate(self):
if not self.route:
self.route = slug(self.name)
+
+ @frappe.whitelist()
+ def sync_fields(self):
+ doctype_fields = frappe.get_meta(self.document_type).fields
+
+ if self.is_new():
+ added_fields = [field.fieldname for field in doctype_fields]
+ removed_fields = []
+ else:
+ doctype_fieldnames = {field.fieldname for field in doctype_fields}
+ layout_fieldnames = {field.fieldname for field in self.fields}
+ added_fields = list(doctype_fieldnames - layout_fieldnames)
+ removed_fields = list(layout_fieldnames - doctype_fieldnames)
+
+ if not (added_fields or removed_fields):
+ return
+
+ added = self.add_fields(added_fields, doctype_fields)
+ removed = self.remove_fields(removed_fields)
+
+ for index, field in enumerate(self.fields):
+ field.idx = index + 1
+
+ return {"added": added, "removed": removed}
+
+ def add_fields(self, added_fields: list[str], doctype_fields: list["DocField"]) -> list[dict]:
+ added = []
+ for field in added_fields:
+ field_details = next((f for f in doctype_fields if f.fieldname == field), None)
+ if not field_details:
+ continue
+
+ # remove 'doctype' data from the DocField to allow adding it to the layout
+ row = self.append("fields", field_details.as_dict(no_default_fields=True))
+ row_data = row.as_dict()
+
+ if field_details.get("insert_after"):
+ insert_after = next(
+ (f for f in self.fields if f.fieldname == field_details.insert_after),
+ None,
+ )
+
+ # initialize new row to just after the insert_after field
+ if insert_after:
+ self.fields.insert(insert_after.idx, row)
+ self.fields.pop()
+
+ row_data = {"idx": insert_after.idx + 1, "fieldname": row.fieldname, "label": row.label}
+
+ added.append(row_data)
+ return added
+
+ def remove_fields(self, removed_fields: list[str]) -> list[dict]:
+ removed = []
+ for field in removed_fields:
+ field_details = next((f for f in self.fields if f.fieldname == field), None)
+ if field_details:
+ self.remove(field_details)
+ removed.append(field_details.as_dict())
+ return removed
diff --git a/frappe/custom/doctype/doctype_layout/patches/convert_web_forms_to_doctype_layout.py b/frappe/custom/doctype/doctype_layout/patches/convert_web_forms_to_doctype_layout.py
index 59cdfffb21..7d22ee3c7d 100644
--- a/frappe/custom/doctype/doctype_layout/patches/convert_web_forms_to_doctype_layout.py
+++ b/frappe/custom/doctype/doctype_layout/patches/convert_web_forms_to_doctype_layout.py
@@ -2,7 +2,7 @@ import frappe
def execute():
- for web_form_name in frappe.db.get_all("Web Form", pluck="name"):
+ for web_form_name in frappe.get_all("Web Form", pluck="name"):
web_form = frappe.get_doc("Web Form", web_form_name)
doctype_layout = frappe.get_doc(
dict(
diff --git a/frappe/custom/doctype/doctype_layout/test_doctype_layout.py b/frappe/custom/doctype/doctype_layout/test_doctype_layout.py
index 0e64a9e727..c568cd4df7 100644
--- a/frappe/custom/doctype/doctype_layout/test_doctype_layout.py
+++ b/frappe/custom/doctype/doctype_layout/test_doctype_layout.py
@@ -1,8 +1,8 @@
# Copyright (c) 2020, Frappe Technologies and Contributors
# License: MIT. See LICENSE
# import frappe
-import unittest
+from frappe.tests.utils import FrappeTestCase
-class TestDocTypeLayout(unittest.TestCase):
+class TestDocTypeLayout(FrappeTestCase):
pass
diff --git a/frappe/custom/doctype/property_setter/property_setter.js b/frappe/custom/doctype/property_setter/property_setter.js
index bff5ad0e63..955e01c33e 100644
--- a/frappe/custom/doctype/property_setter/property_setter.js
+++ b/frappe/custom/doctype/property_setter/property_setter.js
@@ -1,10 +1,10 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
-frappe.ui.form.on('Property Setter', {
- validate: function(frm) {
- if(frm.doc.property_type=='Check' && !in_list(['0','1'], frm.doc.value)) {
- frappe.throw(__('Value for a check field can be either 0 or 1'));
+frappe.ui.form.on("Property Setter", {
+ validate: function (frm) {
+ if (frm.doc.property_type == "Check" && !in_list(["0", "1"], frm.doc.value)) {
+ frappe.throw(__("Value for a check field can be either 0 or 1"));
}
- }
+ },
});
diff --git a/frappe/custom/doctype/property_setter/property_setter.py b/frappe/custom/doctype/property_setter/property_setter.py
index 3034904381..bac616356d 100644
--- a/frappe/custom/doctype/property_setter/property_setter.py
+++ b/frappe/custom/doctype/property_setter/property_setter.py
@@ -21,6 +21,9 @@ class PropertySetter(Document):
delete_property_setter(self.doc_type, self.property, self.field_name, self.row_name)
frappe.clear_cache(doctype=self.doc_type)
+ def on_trash(self):
+ frappe.clear_cache(doctype=self.doc_type)
+
def validate_fieldtype_change(self):
if self.property == "fieldtype" and self.field_name in not_allowed_fieldtype_change:
frappe.throw(_("Field type cannot be changed for {0}").format(self.field_name))
diff --git a/frappe/custom/doctype/property_setter/test_property_setter.py b/frappe/custom/doctype/property_setter/test_property_setter.py
index 1fa2d2cefb..dc74a69161 100644
--- a/frappe/custom/doctype/property_setter/test_property_setter.py
+++ b/frappe/custom/doctype/property_setter/test_property_setter.py
@@ -1,9 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
-import unittest
+from frappe.tests.utils import FrappeTestCase
# test_records = frappe.get_test_records('Property Setter')
-class TestPropertySetter(unittest.TestCase):
+class TestPropertySetter(FrappeTestCase):
pass
diff --git a/frappe/custom/doctype/test_rename_new/test_rename_new.json b/frappe/custom/doctype/test_rename_new/test_rename_new.json
deleted file mode 100644
index 0b089091a1..0000000000
--- a/frappe/custom/doctype/test_rename_new/test_rename_new.json
+++ /dev/null
@@ -1,42 +0,0 @@
-{
- "actions": [],
- "creation": "2021-01-13 12:47:03.572640",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "random"
- ],
- "fields": [
- {
- "fieldname": "random",
- "fieldtype": "Data",
- "label": "random"
- }
- ],
- "index_web_pages_for_search": 1,
- "links": [],
- "modified": "2021-01-13 12:47:03.572640",
- "modified_by": "Administrator",
- "module": "Custom",
- "name": "Test rename new",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "route": "test-rename",
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/frappe/custom/doctype/test_rename_new/test_rename_new.py b/frappe/custom/doctype/test_rename_new/test_rename_new.py
deleted file mode 100644
index ed89d1fad1..0000000000
--- a/frappe/custom/doctype/test_rename_new/test_rename_new.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2021, Frappe Technologies and contributors
-# License: MIT. See LICENSE
-
-# import frappe
-from frappe.model.document import Document
-
-
-class Testrenamenew(Document):
- pass
diff --git a/frappe/custom/doctype/test_rename_new/test_test_rename_new.py b/frappe/custom/doctype/test_rename_new/test_test_rename_new.py
deleted file mode 100644
index f1ccf42ede..0000000000
--- a/frappe/custom/doctype/test_rename_new/test_test_rename_new.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2021, Frappe Technologies and Contributors
-# License: MIT. See LICENSE
-# import frappe
-import unittest
-
-
-class Testrenamenew(unittest.TestCase):
- pass
diff --git a/frappe/custom/fixtures/temp_doctype.json b/frappe/custom/fixtures/temp_doctype.json
index 343aa2cb37..20b3d9caa9 100644
--- a/frappe/custom/fixtures/temp_doctype.json
+++ b/frappe/custom/fixtures/temp_doctype.json
@@ -18,7 +18,6 @@
"beta": 0,
"is_virtual": 0,
"naming_rule": "",
- "name_case": "",
"allow_rename": 1,
"hide_toolbar": 0,
"allow_copy": 0,
@@ -58,7 +57,6 @@
"report": 1,
"export": 1,
"import": 0,
- "set_user_permissions": 0,
"share": 1,
"print": 1,
"email": 1,
diff --git a/frappe/custom/fixtures/temp_singles.json b/frappe/custom/fixtures/temp_singles.json
index b7e2536f25..723f47d7ac 100644
--- a/frappe/custom/fixtures/temp_singles.json
+++ b/frappe/custom/fixtures/temp_singles.json
@@ -18,7 +18,6 @@
"beta": 0,
"is_virtual": 0,
"naming_rule": "",
- "name_case": "",
"allow_rename": 1,
"hide_toolbar": 0,
"allow_copy": 0,
@@ -58,7 +57,6 @@
"report": 1,
"export": 1,
"import": 0,
- "set_user_permissions": 0,
"share": 1,
"print": 1,
"email": 1,
diff --git a/frappe/desk/page/activity/__init__.py b/frappe/custom/report/__init__.py
similarity index 100%
rename from frappe/desk/page/activity/__init__.py
rename to frappe/custom/report/__init__.py
diff --git a/frappe/desk/page/translation_tool/__init__.py b/frappe/custom/report/audit_system_hooks/__init__.py
similarity index 100%
rename from frappe/desk/page/translation_tool/__init__.py
rename to frappe/custom/report/audit_system_hooks/__init__.py
diff --git a/frappe/custom/report/audit_system_hooks/audit_system_hooks.js b/frappe/custom/report/audit_system_hooks/audit_system_hooks.js
new file mode 100644
index 0000000000..a78464f3da
--- /dev/null
+++ b/frappe/custom/report/audit_system_hooks/audit_system_hooks.js
@@ -0,0 +1,7 @@
+// Copyright (c) 2023, Frappe Technologies and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Audit System Hooks"] = {
+ filters: [],
+};
diff --git a/frappe/custom/report/audit_system_hooks/audit_system_hooks.json b/frappe/custom/report/audit_system_hooks/audit_system_hooks.json
new file mode 100644
index 0000000000..b13a43a0c5
--- /dev/null
+++ b/frappe/custom/report/audit_system_hooks/audit_system_hooks.json
@@ -0,0 +1,27 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2023-01-25 15:02:21.896117",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "letter_head": "",
+ "modified": "2023-01-31 14:53:37.778576",
+ "modified_by": "Administrator",
+ "module": "Custom",
+ "name": "Audit System Hooks",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "query": "",
+ "ref_doctype": "Property Setter",
+ "report_name": "Audit System Hooks",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "System Manager"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/frappe/custom/report/audit_system_hooks/audit_system_hooks.py b/frappe/custom/report/audit_system_hooks/audit_system_hooks.py
new file mode 100644
index 0000000000..a42c5c361a
--- /dev/null
+++ b/frappe/custom/report/audit_system_hooks/audit_system_hooks.py
@@ -0,0 +1,70 @@
+# Copyright (c) 2023, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+import frappe
+
+
+def execute(filters=None):
+ return get_columns(), get_data()
+
+
+def get_columns():
+ values_field_type = "Data" # TODO: better text wrapping in reportview
+ columns = [
+ {"label": "Hook name", "fieldname": "hook_name", "fieldtype": "Data", "width": 200},
+ {"label": "Hook key (optional)", "fieldname": "hook_key", "fieldtype": "Data", "width": 200},
+ {"label": "Hook Values (resolved)", "fieldname": "hook_values", "fieldtype": values_field_type},
+ ]
+
+ # Each app is shown in order as a column
+ installed_apps = frappe.get_installed_apps(_ensure_on_bench=True)
+ columns += [
+ {"label": app, "fieldname": app, "fieldtype": values_field_type} for app in installed_apps
+ ]
+
+ return columns
+
+
+def get_data():
+ hooks = frappe.get_hooks()
+ installed_apps = frappe.get_installed_apps(_ensure_on_bench=True)
+
+ def fmt_hook_values(v):
+ """Improve readability by discarding falsy values and removing containers when only 1
+ value is in container"""
+ if not v:
+ return ""
+
+ v = delist(v)
+
+ if isinstance(v, (dict, list)):
+ try:
+ return frappe.as_json(v)
+ except Exception:
+ pass
+
+ return str(v)
+
+ data = []
+ for hook, values in hooks.items():
+ if isinstance(values, dict):
+ for k, v in values.items():
+ row = {"hook_name": hook, "hook_key": fmt_hook_values(k), "hook_values": fmt_hook_values(v)}
+ for app in installed_apps:
+ if app_hooks := delist(frappe.get_hooks(hook, app_name=app)):
+ row[app] = fmt_hook_values(app_hooks.get(k))
+ data.append(row)
+ else:
+ row = {"hook_name": hook, "hook_values": fmt_hook_values(values)}
+ for app in installed_apps:
+ row[app] = fmt_hook_values(frappe.get_hooks(hook, app_name=app))
+
+ data.append(row)
+
+ return data
+
+
+def delist(val):
+ if isinstance(val, list) and len(val) == 1:
+ return val[0]
+ return val
diff --git a/frappe/custom/report/audit_system_hooks/test_audit_system_hooks.py b/frappe/custom/report/audit_system_hooks/test_audit_system_hooks.py
new file mode 100644
index 0000000000..cd3edffc77
--- /dev/null
+++ b/frappe/custom/report/audit_system_hooks/test_audit_system_hooks.py
@@ -0,0 +1,17 @@
+# Copyright (c) 2022, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+
+from frappe.custom.report.audit_system_hooks.audit_system_hooks import execute
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestAuditSystemHooksReport(FrappeTestCase):
+ def test_basic_query(self):
+ _, data = execute()
+ for row in data:
+ if row.get("hook_name") == "app_name":
+ self.assertEqual(row.get("hook_values"), "frappe")
+ break
+ else:
+ self.fail("Failed to generate hooks report")
diff --git a/frappe/custom/workspace/customization/customization.json b/frappe/custom/workspace/customization/customization.json
index 1756abcb1d..8985bf54ed 100644
--- a/frappe/custom/workspace/customization/customization.json
+++ b/frappe/custom/workspace/customization/customization.json
@@ -1,6 +1,6 @@
{
"charts": [],
- "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Customization\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customize Form\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Custom Role\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Client Script\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Server Script\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Dashboards\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Form Customization\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other\",\"col\":4}}]",
+ "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Customization\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customize Form\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Custom Role\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Client Script\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Server Script\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Dashboards\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Form Customization\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other\",\"col\":4}}]",
"creation": "2020-03-02 15:15:03.839594",
"docstatus": 0,
"doctype": "Workspace",
@@ -107,7 +107,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Other",
- "link_count": 0,
+ "link_count": 2,
"onboard": 0,
"type": "Card Break"
},
@@ -121,15 +121,26 @@
"link_type": "DocType",
"onboard": 0,
"type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Navbar Settings",
+ "link_count": 0,
+ "link_to": "Navbar Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
}
],
- "modified": "2022-01-13 17:28:08.345794",
+ "modified": "2022-08-28 20:56:24.980719",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customization",
"owner": "Administrator",
"parent_page": "",
"public": 1,
+ "quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 8.0,
diff --git a/frappe/database/__init__.py b/frappe/database/__init__.py
index 7de3fabf01..76ad24b6e6 100644
--- a/frappe/database/__init__.py
+++ b/frappe/database/__init__.py
@@ -50,16 +50,3 @@ def get_db(host=None, user=None, password=None, port=None):
import frappe.database.mariadb.database
return frappe.database.mariadb.database.MariaDBDatabase(host, user, password, port=port)
-
-
-def setup_help_database(help_db_name):
- import frappe
-
- if frappe.conf.db_type == "postgres":
- import frappe.database.postgres.setup_db
-
- return frappe.database.postgres.setup_db.setup_help_database(help_db_name)
- else:
- import frappe.database.mariadb.setup_db
-
- return frappe.database.mariadb.setup_db.setup_help_database(help_db_name)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index a7906bc3fb..2d38a6dea8 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -1,28 +1,39 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
-# Database Module
-# --------------------
-
import datetime
+import itertools
+import json
import random
import re
import string
-from contextlib import contextmanager
+import traceback
+from contextlib import contextmanager, suppress
from time import time
+from typing import Any, Iterable, Sequence
+from pypika.dialects import MySQLQueryBuilder, PostgreSQLQueryBuilder
from pypika.terms import Criterion, NullValue
import frappe
import frappe.defaults
import frappe.model.meta
from frappe import _
-from frappe.exceptions import DoesNotExistError
+from frappe.database.utils import (
+ DefaultOrderBy,
+ EmptyQueryValues,
+ FallBackDateTimeStr,
+ LazyMogrify,
+ Query,
+ QueryValues,
+ is_query_type,
+)
+from frappe.exceptions import DoesNotExistError, ImplicitCommitError
from frappe.model.utils.link_count import flush_local_link_count
from frappe.query_builder.functions import Count
-from frappe.query_builder.utils import DocType
from frappe.utils import cast as cast_fieldtype
-from frappe.utils import get_datetime, get_table_name, getdate, now, sbool
+from frappe.utils import cint, get_datetime, get_table_name, getdate, now, sbool
+from frappe.utils.deprecations import deprecated, deprecation_warning
IFNULL_PATTERN = re.compile(r"ifnull\(", flags=re.IGNORECASE)
INDEX_PATTERN = re.compile(r"\s*\([^)]+\)\s*")
@@ -30,10 +41,6 @@ SINGLE_WORD_PATTERN = re.compile(r'([`"]?)(tab([A-Z]\w+))\1')
MULTI_WORD_PATTERN = re.compile(r'([`"])(tab([A-Z]\w+)( [A-Z]\w+)+)\1')
-def is_query_type(query: str, query_type: str | tuple[str]) -> bool:
- return query.lstrip().split(maxsplit=1)[0].lower().startswith(query_type)
-
-
class Database:
"""
Open a database connection with the given parmeters, if use_default is True, use the
@@ -51,10 +58,35 @@ class Database:
CHILD_TABLE_COLUMNS = ("parent", "parenttype", "parentfield")
MAX_WRITES_PER_TRANSACTION = 200_000
+ # NOTE:
+ # FOR MARIADB - using no cache - as during backup, if the sequence was used in anyform,
+ # it drops the cache and uses the next non cached value in setval query and
+ # puts that in the backup file, which will start the counter
+ # from that value when inserting any new record in the doctype.
+ # By default the cache is 1000 which will mess up the sequence when
+ # using the system after a restore.
+ #
+ # Another case could be if the cached values expire then also there is a chance of
+ # the cache being skipped.
+ #
+ # FOR POSTGRES - The sequence cache for postgres is per connection.
+ # Since we're opening and closing connections for every request this results in skipping the cache
+ # to the next non-cached value hence not using cache in postgres.
+ # ref: https://stackoverflow.com/questions/21356375/postgres-9-0-4-sequence-skipping-numbers
+ SEQUENCE_CACHE = 0
+
class InvalidColumnName(frappe.ValidationError):
pass
- def __init__(self, host=None, user=None, password=None, ac_name=None, use_default=0, port=None):
+ def __init__(
+ self,
+ host=None,
+ user=None,
+ password=None,
+ ac_name=None,
+ use_default=0,
+ port=None,
+ ):
self.setup_type_map()
self.host = host or frappe.conf.db_host or "127.0.0.1"
self.port = port or frappe.conf.db_port or ""
@@ -73,6 +105,8 @@ class Database:
self.password = password or frappe.conf.db_password
self.value_cache = {}
+ # self.db_type: str
+ # self.last_query (lazy) attribute of last sql query executed
def setup_type_map(self):
pass
@@ -84,26 +118,43 @@ class Database:
self._cursor = self._conn.cursor()
frappe.local.rollback_observers = []
+ try:
+ if execution_timeout := get_query_execution_timeout():
+ self.set_execution_timeout(execution_timeout)
+ except Exception as e:
+ frappe.logger("database").warning(f"Couldn't set execution timeout {e}")
+
+ def set_execution_timeout(self, seconds: int):
+ """Set session speicifc timeout on exeuction of statements.
+ If any statement takes more time it will be killed along with entire transaction."""
+ raise NotImplementedError
+
def use(self, db_name):
"""`USE` db_name."""
self._conn.select_db(db_name)
def get_connection(self):
- pass
+ """Returns a Database connection object that conforms with https://peps.python.org/pep-0249/#connection-objects"""
+ raise NotImplementedError
def get_database_size(self):
- pass
+ raise NotImplementedError
+
+ def _transform_query(self, query: Query, values: QueryValues) -> tuple:
+ return query, values
+
+ def _transform_result(self, result: list[tuple]) -> list[tuple]:
+ return result
def sql(
self,
- query,
- values=(),
+ query: Query,
+ values: QueryValues = EmptyQueryValues,
+ *,
as_dict=0,
as_list=0,
- formatted=0,
debug=0,
ignore_ddl=0,
- as_utf8=0,
auto_commit=0,
update=None,
explain=False,
@@ -113,13 +164,11 @@ class Database:
"""Execute a SQL query and fetch all rows.
:param query: SQL query.
- :param values: List / dict of values to be escaped and substituted in the query.
+ :param values: Tuple / List / Dict of values to be escaped and substituted in the query.
:param as_dict: Return as a dictionary.
:param as_list: Always return as a list.
- :param formatted: Format values like date etc.
:param debug: Print query and `EXPLAIN` in debug log.
:param ignore_ddl: Catch exception if table, column missing.
- :param as_utf8: Encode values as UTF 8.
:param auto_commit: Commit after executing the query.
:param update: Update this dict to all rows (if returned `as_dict`).
:param run: Returns query without executing it if False.
@@ -136,6 +185,9 @@ class Database:
{"name": "a%", "owner":"test@example.com"})
"""
+ if isinstance(query, (MySQLQueryBuilder, PostgreSQLQueryBuilder)):
+ frappe.errprint("Use run method to execute SQL queries generated by Query Engine")
+
debug = debug or getattr(self, "debug", False)
query = str(query)
if not run:
@@ -152,132 +204,147 @@ class Database:
# in transaction validations
self.check_transaction_status(query)
-
self.clear_db_table_cache(query)
- # autocommit
if auto_commit:
self.commit()
- # execute
+ if debug:
+ time_start = time()
+
+ if values == EmptyQueryValues:
+ values = None
+ elif not isinstance(values, (tuple, dict, list)):
+ values = (values,)
+ query, values = self._transform_query(query, values)
+
try:
- if debug:
- time_start = time()
-
- self.log_query(query, values, debug, explain)
-
- if values != ():
-
- # MySQL-python==1.2.5 hack!
- if not isinstance(values, (dict, tuple, list)):
- values = (values,)
-
- self._cursor.execute(query, values)
-
- if frappe.flags.in_migrate:
- self.log_touched_tables(query, values)
-
- else:
- self._cursor.execute(query)
-
- if frappe.flags.in_migrate:
- self.log_touched_tables(query)
-
- if debug:
- time_end = time()
- frappe.errprint(("Execution time: {} sec").format(round(time_end - time_start, 2)))
-
+ self._cursor.execute(query, values)
except Exception as e:
if self.is_syntax_error(e):
- # only for mariadb
- frappe.errprint("Syntax error in query:")
- frappe.errprint(query)
+ frappe.errprint(f"Syntax error in query:\n{query} {values or ''}")
elif self.is_deadlocked(e):
- raise frappe.QueryDeadlockError(e)
+ raise frappe.QueryDeadlockError(e) from e
elif self.is_timedout(e):
- raise frappe.QueryTimeoutError(e)
+ raise frappe.QueryTimeoutError(e) from e
- elif frappe.conf.db_type == "postgres":
- # TODO: added temporarily
- import traceback
+ elif self.is_read_only_mode_error(e):
+ frappe.throw(
+ _(
+ "Site is running in read only mode for maintenance or site update, this action can not be performed right now. Please try again later."
+ ),
+ title=_("In Read Only Mode"),
+ exc=frappe.InReadOnlyMode,
+ )
+ # TODO: added temporarily
+ elif self.db_type == "postgres":
traceback.print_stack()
- print(e)
+ frappe.errprint(f"Error in query:\n{e}")
raise
- if ignore_ddl and (
- self.is_missing_column(e) or self.is_table_missing(e) or self.cant_drop_field_or_key(e)
- ):
- pass
- else:
+ elif isinstance(e, self.ProgrammingError):
+ if frappe.conf.developer_mode:
+ traceback.print_stack()
+ frappe.errprint(f"Error in query:\n{query, values}")
raise
+ if not (
+ ignore_ddl
+ and (self.is_missing_column(e) or self.is_table_missing(e) or self.cant_drop_field_or_key(e))
+ ):
+ raise
+
+ if debug:
+ time_end = time()
+ frappe.errprint(f"Execution time: {time_end - time_start:.2f} sec")
+
+ self.log_query(query, values, debug, explain)
+
if auto_commit:
self.commit()
if not self._cursor.description:
return ()
+ self.last_result = self._transform_result(self._cursor.fetchall())
+
if pluck:
- return [r[0] for r in self._cursor.fetchall()]
+ return [r[0] for r in self.last_result]
# scrub output if required
if as_dict:
- ret = self.fetch_as_dict(formatted, as_utf8)
+ ret = self.fetch_as_dict()
if update:
for r in ret:
r.update(update)
return ret
elif as_list:
- return self.convert_to_lists(self._cursor.fetchall(), formatted, as_utf8)
- elif as_utf8:
- return self.convert_to_lists(self._cursor.fetchall(), formatted, as_utf8)
- else:
- return self._cursor.fetchall()
+ return self.convert_to_lists(self.last_result)
+ return self.last_result
- def log_query(self, query, values, debug, explain):
- # for debugging in tests
- if frappe.conf.get("allow_tests") and frappe.cache().get_value("flag_print_sql"):
- print(self.mogrify(query, values))
+ def _log_query(self, mogrified_query: str, debug: bool = False, explain: bool = False) -> None:
+ """Takes the query and logs it to various interfaces according to the settings."""
+ _query = None
+
+ if frappe.conf.allow_tests and frappe.cache().get_value("flag_print_sql"):
+ _query = _query or str(mogrified_query)
+ print(_query)
- # debug
if debug:
- if explain and is_query_type(query, "select"):
- self.explain_query(query, values)
- frappe.errprint(self.mogrify(query, values))
+ _query = _query or str(mogrified_query)
+ if explain and is_query_type(_query, "select"):
+ self.explain_query(_query)
+ frappe.errprint(_query)
- # info
- if (frappe.conf.get("logging") or False) == 2:
- frappe.log("<<<< query")
- frappe.log(self.mogrify(query, values))
- frappe.log(">>>>")
+ if frappe.conf.logging == 2:
+ _query = _query or str(mogrified_query)
+ frappe.log(f"<<<< query\n{_query}\n>>>>")
- def mogrify(self, query, values):
+ if frappe.flags.in_migrate:
+ _query = _query or str(mogrified_query)
+ self.log_touched_tables(_query)
+
+ def log_query(
+ self, query: str, values: QueryValues = None, debug: bool = False, explain: bool = False
+ ) -> str:
+ # TODO: Use mogrify until MariaDB Connector/C 1.1 is released and we can fetch something
+ # like cursor._transformed_statement from the cursor object. We can also avoid setting
+ # mogrified_query if we don't need to log it.
+ mogrified_query = self.lazy_mogrify(query, values)
+ self._log_query(mogrified_query, debug, explain)
+ return mogrified_query
+
+ def mogrify(self, query: Query, values: QueryValues):
"""build the query string with values"""
if not values:
return query
- else:
- try:
- return self._cursor.mogrify(query, values)
- except Exception:
- return (query, values)
+
+ try:
+ return self._cursor.mogrify(query, values)
+ except AttributeError:
+ if isinstance(values, dict):
+ return query % {k: frappe.db.escape(v) if isinstance(v, str) else v for k, v in values.items()}
+ elif isinstance(values, (list, tuple)):
+ return query % tuple(frappe.db.escape(v) if isinstance(v, str) else v for v in values)
+ return query, values
+
+ def lazy_mogrify(self, query: Query, values: QueryValues) -> LazyMogrify:
+ """Wrap the object with str to generate mogrified query."""
+ return LazyMogrify(query, values)
def explain_query(self, query, values=None):
"""Print `EXPLAIN` in error log."""
+ frappe.errprint("--- query explain ---")
try:
- frappe.errprint("--- query explain ---")
- if values is None:
- self._cursor.execute("explain " + query)
- else:
- self._cursor.execute("explain " + query, values)
- import json
-
+ self._cursor.execute(f"EXPLAIN {query}", values)
+ except Exception as e:
+ frappe.errprint(f"error in query explain: {e}")
+ else:
frappe.errprint(json.dumps(self.fetch_as_dict(), indent=1))
frappe.errprint("--- query explain end ---")
- except Exception:
- frappe.errprint("error in query explain")
def sql_list(self, query, values=(), debug=False, **kwargs):
"""Return data as list of single elements (first column).
@@ -296,7 +363,7 @@ class Database:
self.sql(query, debug=debug)
def check_transaction_status(self, query):
- """Raises exception if more than 20,000 `INSERT`, `UPDATE` queries are
+ """Raises exception if more than 200,000 `INSERT`, `UPDATE` queries are
executed in one transaction. This is to ensure that writes are always flushed otherwise this
could cause the system to hang."""
self.check_implicit_commit(query)
@@ -320,58 +387,29 @@ class Database:
and query
and is_query_type(query, ("start", "alter", "drop", "create", "begin", "truncate"))
):
- raise Exception("This statement can cause implicit commit")
+ raise ImplicitCommitError("This statement can cause implicit commit")
- def fetch_as_dict(self, formatted=0, as_utf8=0):
+ def fetch_as_dict(self) -> list[frappe._dict]:
"""Internal. Converts results to dict."""
- result = self._cursor.fetchall()
- ret = []
+ result = self.last_result
if result:
keys = [column[0] for column in self._cursor.description]
- for r in result:
- values = []
- for value in r:
- if as_utf8 and isinstance(value, str):
- value = value.encode("utf-8")
- values.append(value)
-
- ret.append(frappe._dict(zip(keys, values)))
- return ret
+ return [frappe._dict(zip(keys, row)) for row in result]
@staticmethod
def clear_db_table_cache(query):
if query and is_query_type(query, ("drop", "create")):
frappe.cache().delete_key("db_tables")
- @staticmethod
- def needs_formatting(result, formatted):
- """Returns true if the first row in the result has a Date, Datetime, Long Int."""
- if result and result[0]:
- for v in result[0]:
- if isinstance(v, (datetime.date, datetime.timedelta, datetime.datetime, int)):
- return True
- if formatted and isinstance(v, (int, float)):
- return True
-
- return False
-
def get_description(self):
"""Returns result metadata."""
return self._cursor.description
@staticmethod
- def convert_to_lists(res, formatted=0, as_utf8=0):
+ def convert_to_lists(res):
"""Convert tuple output to lists (internal)."""
- nres = []
- for r in res:
- nr = []
- for val in r:
- if as_utf8 and isinstance(val, str):
- val = val.encode("utf-8")
- nr.append(val)
- nres.append(nr)
- return nres
+ return [[value for value in row] for row in res]
def get(self, doctype, filters=None, as_dict=True, cache=False):
"""Returns `get_value` with fieldname='*'"""
@@ -385,7 +423,7 @@ class Database:
ignore=None,
as_dict=False,
debug=False,
- order_by="KEEP_DEFAULT_ORDERING",
+ order_by=DefaultOrderBy,
cache=False,
for_update=False,
*,
@@ -444,9 +482,8 @@ class Database:
if len(row) > 1 or as_dict:
return row
- else:
- # single field is requested, send it without wrapping in containers
- return row[0]
+ # single field is requested, send it without wrapping in containers
+ return row[0]
def get_values(
self,
@@ -456,7 +493,7 @@ class Database:
ignore=None,
as_dict=False,
debug=False,
- order_by="KEEP_DEFAULT_ORDERING",
+ order_by=DefaultOrderBy,
update=None,
cache=False,
for_update=False,
@@ -515,7 +552,7 @@ class Database:
if (filters is not None) and (filters != doctype or doctype == "DocType"):
try:
if order_by:
- order_by = "modified" if order_by == "KEEP_DEFAULT_ORDERING" else order_by
+ order_by = "modified" if order_by == DefaultOrderBy else order_by
out = self._get_values_from_table(
fields=fields,
filters=filters,
@@ -571,10 +608,6 @@ class Database:
:param filters: Filters (dict).
:param doctype: DocType name.
"""
- # TODO
- # if not frappe.model.meta.is_single(doctype):
- # raise frappe.DoesNotExistError("DocType", doctype)
-
if fields == "*" or isinstance(filters, dict):
# check if single doc matches with filters
values = self.get_singles_dict(doctype)
@@ -590,7 +623,7 @@ class Database:
return [map(values.get, fields)]
else:
- r = frappe.qb.engine.get_query(
+ r = frappe.qb.get_query(
"Singles",
filters={"field": ("in", tuple(fields)), "doctype": doctype},
fields=["field", "value"],
@@ -623,7 +656,7 @@ class Database:
# Get coulmn and value of the single doctype Accounts Settings
account_settings = frappe.db.get_singles_dict("Accounts Settings")
"""
- queried_result = frappe.qb.engine.get_query(
+ queried_result = frappe.qb.get_query(
"Singles",
filters={"doctype": doctype},
fields=["field", "value"],
@@ -657,13 +690,30 @@ class Database:
def get_list(*args, **kwargs):
return frappe.get_list(*args, **kwargs)
+ @staticmethod
+ def _get_update_dict(
+ fieldname: str | dict, value: Any, *, modified: str, modified_by: str, update_modified: bool
+ ) -> dict[str, Any]:
+ """Create update dict that represents column-values to be updated."""
+ update_dict = fieldname if isinstance(fieldname, dict) else {fieldname: value}
+
+ if update_modified:
+ modified = modified or now()
+ modified_by = modified_by or frappe.session.user
+ update_dict.update({"modified": modified, "modified_by": modified_by})
+
+ return update_dict
+
def set_single_value(
self,
doctype: str,
fieldname: str | dict,
value: str | int | None = None,
- *args,
- **kwargs,
+ *,
+ modified=None,
+ modified_by=None,
+ update_modified=True,
+ debug=False,
):
"""Set field value of Single DocType.
@@ -676,7 +726,23 @@ class Database:
# Update the `deny_multiple_sessions` field in System Settings DocType.
company = frappe.db.set_single_value("System Settings", "deny_multiple_sessions", True)
"""
- return self.set_value(doctype, doctype, fieldname, value, *args, **kwargs)
+
+ to_update = self._get_update_dict(
+ fieldname, value, modified=modified, modified_by=modified_by, update_modified=update_modified
+ )
+
+ frappe.db.delete(
+ "Singles", filters={"field": ("in", tuple(to_update)), "doctype": doctype}, debug=debug
+ )
+
+ singles_data = ((doctype, key, sbool(value)) for key, value in to_update.items())
+ frappe.qb.into("Singles").columns("doctype", "field", "value").insert(*singles_data).run(
+ debug=debug
+ )
+ frappe.clear_document_cache(doctype, doctype)
+
+ if doctype in self.value_cache:
+ del self.value_cache[doctype]
def get_single_value(self, doctype, fieldname, cache=True):
"""Get property of Single DocType. Cache locally by default
@@ -696,7 +762,7 @@ class Database:
if cache and fieldname in self.value_cache[doctype]:
return self.value_cache[doctype][fieldname]
- val = frappe.qb.engine.get_query(
+ val = frappe.qb.get_query(
table="Singles",
filters={"doctype": doctype, "field": fieldname},
fields="value",
@@ -707,7 +773,9 @@ class Database:
if not df:
frappe.throw(
- _("Invalid field name: {0}").format(frappe.bold(fieldname)), self.InvalidColumnName
+ _("Field {0} does not exist on {1}").format(
+ frappe.bold(fieldname), frappe.bold(doctype), self.InvalidColumnName
+ )
)
val = cast_fieldtype(df.fieldtype, val)
@@ -736,23 +804,19 @@ class Database:
distinct=False,
limit=None,
):
- field_objects = []
-
- query = frappe.qb.engine.get_query(
+ query = frappe.qb.get_query(
table=doctype,
filters=filters,
- orderby=order_by,
+ order_by=order_by,
for_update=for_update,
- field_objects=field_objects,
fields=fields,
distinct=distinct,
limit=limit,
)
- if fields == "*" and not isinstance(fields, (list, tuple)) and not isinstance(fields, Criterion):
+ if isinstance(fields, str) and fields == "*":
as_dict = True
- r = self.sql(query, as_dict=as_dict, debug=debug, update=update, run=run, pluck=pluck)
- return r
+ return query.run(as_dict=as_dict, debug=debug, update=update, run=run, pluck=pluck)
def _get_value_for_many_names(
self,
@@ -768,26 +832,16 @@ class Database:
limit=None,
as_dict=False,
):
- names = list(filter(None, names))
- if names:
- return self.get_all(
+ if names := list(filter(None, names)):
+ return frappe.qb.get_query(
doctype,
fields=field,
filters=names,
order_by=order_by,
- pluck=pluck,
- debug=debug,
- as_list=not as_dict,
- run=run,
distinct=distinct,
- limit_page_length=limit,
- )
- else:
- return {}
-
- def update(self, *args, **kwargs):
- """Update multiple values. Alias for `set_value`."""
- return self.set_value(*args, **kwargs)
+ limit=limit,
+ ).run(debug=debug, run=run, as_dict=as_dict, pluck=pluck)
+ return {}
def set_value(
self,
@@ -807,90 +861,52 @@ class Database:
**Warning:** this function will not call Document events and should be avoided in normal cases.
:param dt: DocType name.
- :param dn: Document name.
+ :param dn: Document name for updating single record or filters for updating many records.
:param field: Property / field name or dictionary of values to be updated
:param value: Value to be updated.
:param modified: Use this as the `modified` timestamp.
:param modified_by: Set this user as `modified_by`.
:param update_modified: default True. Set as false, if you don't want to update the timestamp.
:param debug: Print the query in the developer / js console.
- :param for_update: Will add a row-level lock to the value that is being set so that it can be released on commit.
"""
- is_single_doctype = not (dn and dt != dn)
- to_update = field if isinstance(field, dict) else {field: val}
- if update_modified:
- modified = modified or now()
- modified_by = modified_by or frappe.session.user
- to_update.update({"modified": modified, "modified_by": modified_by})
-
- if is_single_doctype:
- frappe.db.delete(
- "Singles", filters={"field": ("in", tuple(to_update)), "doctype": dt}, debug=debug
+ if _is_single_doctype := not (dn and dt != dn):
+ deprecation_warning(
+ "Calling db.set_value on single doctype is deprecated. This behaviour will be removed in version 15. Use db.set_single_value instead."
)
+ self.set_single_value(
+ doctype=dt,
+ fieldname=field,
+ value=val,
+ debug=debug,
+ update_modified=update_modified,
+ modified=modified,
+ modified_by=modified_by,
+ )
+ return
- singles_data = ((dt, key, sbool(value)) for key, value in to_update.items())
- query = (
- frappe.qb.into("Singles").columns("doctype", "field", "value").insert(*singles_data)
- ).run(debug=debug)
- frappe.clear_document_cache(dt, dt)
+ to_update = self._get_update_dict(
+ field, val, modified=modified, modified_by=modified_by, update_modified=update_modified
+ )
+ query = frappe.qb.get_query(table=dt, filters=dn, update=True)
+
+ if isinstance(dn, str):
+ frappe.clear_document_cache(dt, dn)
else:
- table = DocType(dt)
+ # TODO: Fix this; doesn't work rn - gavin@frappe.io
+ # frappe.cache().hdel_keys(dt, "document_cache")
+ # Workaround: clear all document caches
+ frappe.cache().delete_value("document_cache")
- if for_update:
- docnames = tuple(
- self.get_values(dt, dn, "name", debug=debug, for_update=for_update, pluck=True)
- ) or (NullValue(),)
- query = frappe.qb.update(table).where(table.name.isin(docnames))
+ for column, value in to_update.items():
+ query = query.set(column, value)
- for docname in docnames:
- frappe.clear_document_cache(dt, docname)
-
- else:
- query = frappe.qb.engine.build_conditions(table=dt, filters=dn, update=True)
- # TODO: Fix this; doesn't work rn - gavin@frappe.io
- # frappe.cache().hdel_keys(dt, "document_cache")
- # Workaround: clear all document caches
- frappe.cache().delete_value("document_cache")
-
- for column, value in to_update.items():
- query = query.set(column, value)
-
- query.run(debug=debug)
+ query.run(debug=debug)
if dt in self.value_cache:
del self.value_cache[dt]
- @staticmethod
- def set(doc, field, val):
- """Set value in document. **Avoid**"""
- doc.db_set(field, val)
-
- def touch(self, doctype, docname):
- """Update the modified timestamp of this document."""
- modified = now()
- self.sql(
- """update `tab{doctype}` set `modified`=%s
- where name=%s""".format(
- doctype=doctype
- ),
- (modified, docname),
- )
- return modified
-
- @staticmethod
- def set_temp(value):
- """Set a temperory value and return a key."""
- key = frappe.generate_hash()
- frappe.cache().hset("temp", key, value)
- return key
-
- @staticmethod
- def get_temp(key):
- """Return the temperory value and delete it."""
- return frappe.cache().hget("temp", key)
-
def set_global(self, key, val, user="__global"):
"""Save a global key value. Global values will be automatically set if they match fieldname."""
self.set_default(key, val, user)
@@ -926,8 +942,10 @@ class Database:
return defaults.get(frappe.scrub(key))
- def begin(self):
- self.sql("START TRANSACTION")
+ def begin(self, *, read_only=False):
+ read_only = read_only or frappe.flags.read_only
+ mode = "READ ONLY" if read_only else ""
+ self.sql(f"START TRANSACTION {mode}")
def commit(self):
"""Commit current transaction. Calls SQL `COMMIT`."""
@@ -935,9 +953,7 @@ class Database:
frappe.call(method[0], *(method[1] or []), **(method[2] or {}))
self.sql("commit")
- if frappe.conf.db_type == "postgres":
- # Postgres requires explicitly starting new transaction
- self.begin()
+ self.begin() # explicitly start a new transaction
frappe.local.rollback_observers = []
self.flush_realtime_log()
@@ -979,34 +995,26 @@ class Database:
obj.on_rollback()
frappe.local.rollback_observers = []
+ frappe.local.realtime_log = []
+ frappe.flags.enqueue_after_commit = []
+
def field_exists(self, dt, fn):
"""Return true of field exists."""
return self.exists("DocField", {"fieldname": fn, "parent": dt})
def table_exists(self, doctype, cached=True):
"""Returns True if table for given doctype exists."""
- return ("tab" + doctype) in self.get_tables(cached=cached)
+ return f"tab{doctype}" in self.get_tables(cached=cached)
def has_table(self, doctype):
return self.table_exists(doctype)
def get_tables(self, cached=True):
- tables = frappe.cache().get_value("db_tables")
- if not tables or not cached:
- table_rows = self.sql(
- """
- SELECT table_name
- FROM information_schema.tables
- WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
- """
- )
- tables = {d[0] for d in table_rows}
- frappe.cache().set_value("db_tables", tables)
- return tables
+ raise NotImplementedError
def a_row_exists(self, doctype):
"""Returns True if atleast one row exists."""
- return self.sql(f"select name from `tab{doctype}` limit 1")
+ return frappe.get_all(doctype, limit=1, order_by=None, as_list=True)
def exists(self, dt, dn=None, cache=False):
"""Return the document name of a matching document, or None.
@@ -1041,7 +1049,7 @@ class Database:
dt = dt.copy() # don't modify the original dict
dt, dn = dt.pop("doctype"), dt
- return self.get_value(dt, dn, ignore=True, cache=cache)
+ return self.get_value(dt, dn, ignore=True, cache=cache, order_by=None)
def count(self, dt, filters=None, debug=False, cache=False, distinct: bool = True):
"""Returns `COUNT(*)` for given DocType and filters."""
@@ -1049,10 +1057,9 @@ class Database:
cache_count = frappe.cache().get_value(f"doctype:count:{dt}")
if cache_count is not None:
return cache_count
- query = frappe.qb.engine.get_query(
- table=dt, filters=filters, fields=Count("*"), distinct=distinct
- )
- count = self.sql(query, debug=debug)[0][0]
+ count = frappe.qb.get_query(table=dt, filters=filters, fields=Count("*"), distinct=distinct).run(
+ debug=debug
+ )[0][0]
if not filters and cache:
frappe.cache().set_value(f"doctype:count:{dt}", count, expires_in_sec=86400)
return count
@@ -1062,17 +1069,11 @@ class Database:
return getdate(date).strftime("%Y-%m-%d")
@staticmethod
- def format_datetime(datetime):
+ def format_datetime(datetime): # noqa: F811
if not datetime:
- return "0001-01-01 00:00:00.000000"
+ return FallBackDateTimeStr
- if isinstance(datetime, str):
- if ":" not in datetime:
- datetime = datetime + " 00:00:00.000000"
- else:
- datetime = datetime.strftime("%Y-%m-%d %H:%M:%S.%f")
-
- return datetime
+ return get_datetime(datetime).strftime("%Y-%m-%d %H:%M:%S.%f")
def get_creation_count(self, doctype, minutes):
"""Get count of records created in the last x minutes"""
@@ -1080,28 +1081,27 @@ class Database:
from frappe.utils import now_datetime
- return self.sql(
- """select count(name) from `tab{doctype}`
- where creation >= %s""".format(
- doctype=doctype
- ),
- now_datetime() - relativedelta(minutes=minutes),
- )[0][0]
+ Table = frappe.qb.DocType(doctype)
+
+ return (
+ frappe.qb.from_(Table)
+ .select(Count(Table.name))
+ .where(Table.creation >= now_datetime() - relativedelta(minutes=minutes))
+ .run()[0][0]
+ )
def get_db_table_columns(self, table) -> list[str]:
"""Returns list of column names from given table."""
columns = frappe.cache().hget("table_columns", table)
if columns is None:
- columns = [
- r[0]
- for r in self.sql(
- """
- select column_name
- from information_schema.columns
- where table_name = %s """,
- table,
- )
- ]
+ information_schema = frappe.qb.Schema("information_schema")
+
+ columns = (
+ frappe.qb.from_(information_schema.columns)
+ .select(information_schema.columns.column_name)
+ .where(information_schema.columns.table_name == table)
+ .run(pluck=True)
+ )
if columns:
frappe.cache().hset("table_columns", table, columns)
@@ -1120,12 +1120,19 @@ class Database:
return column in self.get_table_columns(doctype)
def get_column_type(self, doctype, column):
- return self.sql(
- """SELECT column_type FROM INFORMATION_SCHEMA.COLUMNS
- WHERE table_name = 'tab{}' AND column_name = '{}' """.format(
- doctype, column
+ """Returns column type from database."""
+ information_schema = frappe.qb.Schema("information_schema")
+ table = get_table_name(doctype)
+
+ return (
+ frappe.qb.from_(information_schema.columns)
+ .select(information_schema.columns.column_type)
+ .where(
+ (information_schema.columns.table_name == table)
+ & (information_schema.columns.column_name == column)
)
- )[0][0]
+ .run(pluck=True)[0]
+ )
def has_index(self, table_name, index_name):
raise NotImplementedError
@@ -1148,7 +1155,6 @@ class Database:
def close(self):
"""Close database connection."""
if self._conn:
- # self._cursor.close()
self._conn.close()
self._cursor = None
self._conn = None
@@ -1177,7 +1183,7 @@ class Database:
return self.is_missing_column(e) or self.is_table_missing(e)
def multisql(self, sql_dict, values=(), **kwargs):
- current_dialect = frappe.db.db_type or "mariadb"
+ current_dialect = self.db_type or "mariadb"
query = sql_dict.get(current_dialect)
return self.sql(query, values, **kwargs)
@@ -1188,7 +1194,7 @@ class Database:
Doctype name can be passed directly, it will be pre-pended with `tab`.
"""
filters = filters or kwargs.get("conditions")
- query = frappe.qb.engine.build_conditions(table=doctype, filters=filters).delete()
+ query = frappe.qb.get_query(table=doctype, filters=filters, delete=True)
if "debug" not in kwargs:
kwargs["debug"] = debug
return query.run(**kwargs)
@@ -1201,9 +1207,6 @@ class Database:
"""
return self.sql_ddl(f"truncate `{get_table_name(doctype)}`")
- def clear_table(self, doctype):
- return self.truncate(doctype)
-
def get_last_created(self, doctype):
last_record = self.get_all(doctype, ("creation"), limit=1, order_by="creation desc")
if last_record:
@@ -1211,9 +1214,7 @@ class Database:
else:
return None
- def log_touched_tables(self, query, values=None):
- if values:
- query = frappe.safe_decode(self._cursor.mogrify(query, values))
+ def log_touched_tables(self, query):
if is_query_type(query, ("insert", "delete", "update", "alter", "drop", "rename")):
# single_word_regex is designed to match following patterns
# `tabXxx`, tabXxx and "tabXxx"
@@ -1221,7 +1222,7 @@ class Database:
# multi_word_regex is designed to match following patterns
# `tabXxx Xxx` and "tabXxx Xxx"
- # ([`"]?) Captures " or ` at the begining of the table name (if provided)
+ # ([`"]?) Captures " or ` at the beginning of the table name (if provided)
# \1 matches the first captured group (quote character) at the end of the table name
# multi word table name must have surrounding quotes.
@@ -1237,28 +1238,36 @@ class Database:
frappe.flags.touched_tables = set()
frappe.flags.touched_tables.update(tables)
- def bulk_insert(self, doctype, fields, values, ignore_duplicates=False, *, chunk_size=10_000):
+ def bulk_insert(
+ self,
+ doctype: str,
+ fields: list[str],
+ values: Iterable[Sequence[Any]],
+ ignore_duplicates=False,
+ *,
+ chunk_size=10_000,
+ ):
"""
Insert multiple records at a time
:param doctype: Doctype name
:param fields: list of fields
- :params values: list of list of values
+ :params values: iterable of values
"""
- values = list(values)
table = frappe.qb.DocType(doctype)
- for start_index in range(0, len(values), chunk_size):
- query = frappe.qb.into(table)
- if ignore_duplicates:
- # Pypika does not have same api for ignoring duplicates
- if frappe.conf.db_type == "mariadb":
- query = query.ignore()
- elif frappe.conf.db_type == "postgres":
- query = query.on_conflict().do_nothing()
+ query = frappe.qb.into(table).columns(fields)
- values_to_insert = values[start_index : start_index + chunk_size]
- query.columns(fields).insert(*values_to_insert).run()
+ if ignore_duplicates:
+ # Pypika does not have same api for ignoring duplicates
+ if frappe.conf.db_type == "mariadb":
+ query = query.ignore()
+ elif frappe.conf.db_type == "postgres":
+ query = query.on_conflict().do_nothing()
+
+ value_iterator = iter(values)
+ while value_chunk := tuple(itertools.islice(value_iterator, chunk_size)):
+ query.insert(*value_chunk).run()
def create_sequence(self, *args, **kwargs):
from frappe.database.sequence import create_sequence
@@ -1277,12 +1286,24 @@ class Database:
def enqueue_jobs_after_commit():
- from frappe.utils.background_jobs import execute_job, get_queue
+ from frappe.utils.background_jobs import (
+ RQ_JOB_FAILURE_TTL,
+ RQ_RESULTS_TTL,
+ execute_job,
+ get_queue,
+ )
if frappe.flags.enqueue_after_commit and len(frappe.flags.enqueue_after_commit) > 0:
for job in frappe.flags.enqueue_after_commit:
q = get_queue(job.get("queue"), is_async=job.get("is_async"))
- q.enqueue_call(execute_job, timeout=job.get("timeout"), kwargs=job.get("queue_args"))
+ q.enqueue_call(
+ execute_job,
+ timeout=job.get("timeout"),
+ kwargs=job.get("queue_args"),
+ failure_ttl=frappe.conf.get("rq_job_failure_ttl") or RQ_JOB_FAILURE_TTL,
+ result_ttl=frappe.conf.get("rq_results_ttl") or RQ_RESULTS_TTL,
+ job_id=job.get("job_id"),
+ )
frappe.flags.enqueue_after_commit = []
@@ -1310,3 +1331,28 @@ def savepoint(catch: type | tuple[type, ...] = Exception):
frappe.db.rollback(save_point=savepoint)
else:
frappe.db.release_savepoint(savepoint)
+
+
+def get_query_execution_timeout() -> int:
+ """Get execution timeout based on current timeout in different contexts.
+
+ HTTP requests: HTTP timeout or a default (300)
+ Background jobs: Job timeout
+ Console/Commands: No timeout = 0.
+
+ Note: Timeout adds 1.5x as "safety factor"
+ """
+ from rq import get_current_job
+
+ if not frappe.conf.get("enable_db_statement_timeout"):
+ return 0
+
+ # Zero means no timeout, which is the default value in db.
+ timeout = 0
+ with suppress(Exception):
+ if getattr(frappe.local, "request", None):
+ timeout = frappe.conf.http_timeout or 300
+ elif job := get_current_job():
+ timeout = job.timeout
+
+ return int(cint(timeout) * 1.5)
diff --git a/frappe/database/db_manager.py b/frappe/database/db_manager.py
index 796f23a054..5840158fa1 100644
--- a/frappe/database/db_manager.py
+++ b/frappe/database/db_manager.py
@@ -1,5 +1,3 @@
-import os
-
import frappe
@@ -15,63 +13,51 @@ class DbManager:
return self.db.sql("select user()")[0][0].split("@")[1]
def create_user(self, user, password, host=None):
- # Create user if it doesn't exist.
- if not host:
- host = self.get_current_host()
-
- if password:
- self.db.sql(f"CREATE USER '{user}'@'{host}' IDENTIFIED BY '{password}';")
- else:
- self.db.sql(f"CREATE USER '{user}'@'{host}';")
+ host = host or self.get_current_host()
+ password_predicate = f" IDENTIFIED BY '{password}'" if password else ""
+ self.db.sql(f"CREATE USER '{user}'@'{host}'{password_predicate}")
def delete_user(self, target, host=None):
- if not host:
- host = self.get_current_host()
- try:
- self.db.sql(f"DROP USER '{target}'@'{host}';")
- except Exception as e:
- if e.args[0] == 1396:
- pass
- else:
- raise
+ host = host or self.get_current_host()
+ self.db.sql(f"DROP USER IF EXISTS '{target}'@'{host}'")
def create_database(self, target):
if target in self.get_database_list():
self.drop_database(target)
-
- self.db.sql("CREATE DATABASE `%s` ;" % target)
+ self.db.sql(f"CREATE DATABASE `{target}`")
def drop_database(self, target):
- self.db.sql("DROP DATABASE IF EXISTS `%s`;" % target)
+ self.db.sql_ddl(f"DROP DATABASE IF EXISTS `{target}`")
def grant_all_privileges(self, target, user, host=None):
- if not host:
- host = self.get_current_host()
-
- if frappe.conf.get("rds_db", 0) == 1:
- self.db.sql(
- "GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES, CREATE VIEW, EVENT, TRIGGER, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EXECUTE, LOCK TABLES ON `%s`.* TO '%s'@'%s';"
- % (target, user, host)
+ host = host or self.get_current_host()
+ permissions = (
+ (
+ "SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, "
+ "CREATE TEMPORARY TABLES, CREATE VIEW, EVENT, TRIGGER, SHOW VIEW, "
+ "CREATE ROUTINE, ALTER ROUTINE, EXECUTE, LOCK TABLES"
)
- else:
- self.db.sql(f"GRANT ALL PRIVILEGES ON `{target}`.* TO '{user}'@'{host}';")
+ if frappe.conf.rds_db
+ else "ALL PRIVILEGES"
+ )
+ self.db.sql(f"GRANT {permissions} ON `{target}`.* TO '{user}'@'{host}'")
def flush_privileges(self):
self.db.sql("FLUSH PRIVILEGES")
def get_database_list(self):
- """get list of databases"""
- return [d[0] for d in self.db.sql("SHOW DATABASES")]
+ return self.db.sql("SHOW DATABASES", pluck=True)
@staticmethod
def restore_database(target, source, user, password):
+ import os
+ from shutil import which
+
from frappe.utils import make_esc
esc = make_esc("$ ")
+ pv = which("pv")
- from distutils.spawn import find_executable
-
- pv = find_executable("pv")
if pv:
pipe = f"{pv} {source} |"
source = ""
diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py
index 047039b0df..43540956e0 100644
--- a/frappe/database/mariadb/database.py
+++ b/frappe/database/mariadb/database.py
@@ -1,3 +1,5 @@
+import re
+
import pymysql
from pymysql.constants import ER, FIELD_TYPE
from pymysql.converters import conversions, escape_string
@@ -7,24 +9,142 @@ from frappe.database.database import Database
from frappe.database.mariadb.schema import MariaDBTable
from frappe.utils import UnicodeWithAttrs, cstr, get_datetime, get_table_name
+_PARAM_COMP = re.compile(r"%\([\w]*\)s")
-class MariaDBDatabase(Database):
- ProgrammingError = pymysql.err.ProgrammingError
- TableMissingError = pymysql.err.ProgrammingError
- OperationalError = pymysql.err.OperationalError
- InternalError = pymysql.err.InternalError
- SQLError = pymysql.err.ProgrammingError
- DataError = pymysql.err.DataError
+
+class MariaDBExceptionUtil:
+ ProgrammingError = pymysql.ProgrammingError
+ TableMissingError = pymysql.ProgrammingError
+ OperationalError = pymysql.OperationalError
+ InternalError = pymysql.InternalError
+ SQLError = pymysql.ProgrammingError
+ DataError = pymysql.DataError
+
+ # match ER_SEQUENCE_RUN_OUT - https://mariadb.com/kb/en/mariadb-error-codes/
+ SequenceGeneratorLimitExceeded = pymysql.OperationalError
+ SequenceGeneratorLimitExceeded.errno = 4084
+
+ @staticmethod
+ def is_deadlocked(e: pymysql.Error) -> bool:
+ return e.args[0] == ER.LOCK_DEADLOCK
+
+ @staticmethod
+ def is_timedout(e: pymysql.Error) -> bool:
+ return e.args[0] == ER.LOCK_WAIT_TIMEOUT
+
+ @staticmethod
+ def is_read_only_mode_error(e: pymysql.Error) -> bool:
+ return e.args[0] == 1792
+
+ @staticmethod
+ def is_table_missing(e: pymysql.Error) -> bool:
+ return e.args[0] == ER.NO_SUCH_TABLE
+
+ @staticmethod
+ def is_missing_table(e: pymysql.Error) -> bool:
+ return MariaDBDatabase.is_table_missing(e)
+
+ @staticmethod
+ def is_missing_column(e: pymysql.Error) -> bool:
+ return e.args[0] == ER.BAD_FIELD_ERROR
+
+ @staticmethod
+ def is_duplicate_fieldname(e: pymysql.Error) -> bool:
+ return e.args[0] == ER.DUP_FIELDNAME
+
+ @staticmethod
+ def is_duplicate_entry(e: pymysql.Error) -> bool:
+ return e.args[0] == ER.DUP_ENTRY
+
+ @staticmethod
+ def is_access_denied(e: pymysql.Error) -> bool:
+ return e.args[0] == ER.ACCESS_DENIED_ERROR
+
+ @staticmethod
+ def cant_drop_field_or_key(e: pymysql.Error) -> bool:
+ return e.args[0] == ER.CANT_DROP_FIELD_OR_KEY
+
+ @staticmethod
+ def is_syntax_error(e: pymysql.Error) -> bool:
+ return e.args[0] == ER.PARSE_ERROR
+
+ @staticmethod
+ def is_statement_timeout(e: pymysql.Error) -> bool:
+ return e.args[0] == 1969
+
+ @staticmethod
+ def is_data_too_long(e: pymysql.Error) -> bool:
+ return e.args[0] == ER.DATA_TOO_LONG
+
+ @staticmethod
+ def is_primary_key_violation(e: pymysql.Error) -> bool:
+ return (
+ MariaDBDatabase.is_duplicate_entry(e)
+ and "PRIMARY" in cstr(e.args[1])
+ and isinstance(e, pymysql.IntegrityError)
+ )
+
+ @staticmethod
+ def is_unique_key_violation(e: pymysql.Error) -> bool:
+ return (
+ MariaDBDatabase.is_duplicate_entry(e)
+ and "Duplicate" in cstr(e.args[1])
+ and isinstance(e, pymysql.IntegrityError)
+ )
+
+
+class MariaDBConnectionUtil:
+ def get_connection(self):
+ conn = self._get_connection()
+ conn.auto_reconnect = True
+ return conn
+
+ def _get_connection(self):
+ """Return MariaDB connection object."""
+ return self.create_connection()
+
+ def create_connection(self):
+ return pymysql.connect(**self.get_connection_settings())
+
+ def set_execution_timeout(self, seconds: int):
+ self.sql("set session max_statement_time = %s", int(seconds))
+
+ def get_connection_settings(self) -> dict:
+ conn_settings = {
+ "host": self.host,
+ "user": self.user,
+ "password": self.password,
+ "conv": self.CONVERSION_MAP,
+ "charset": "utf8mb4",
+ "use_unicode": True,
+ }
+
+ if self.user not in (frappe.flags.root_login, "root"):
+ conn_settings["database"] = self.user
+
+ if self.port:
+ conn_settings["port"] = int(self.port)
+
+ if frappe.conf.local_infile:
+ conn_settings["local_infile"] = frappe.conf.local_infile
+
+ if frappe.conf.db_ssl_ca and frappe.conf.db_ssl_cert and frappe.conf.db_ssl_key:
+ conn_settings["ssl"] = {
+ "ca": frappe.conf.db_ssl_ca,
+ "cert": frappe.conf.db_ssl_cert,
+ "key": frappe.conf.db_ssl_key,
+ }
+ return conn_settings
+
+
+class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
REGEX_CHARACTER = "regexp"
-
- # NOTE: using a very small cache - as during backup, if the sequence was used in anyform,
- # it drops the cache and uses the next non cached value in setval query and
- # puts that in the backup file, which will start the counter
- # from that value when inserting any new record in the doctype.
- # By default the cache is 1000 which will mess up the sequence when
- # using the system after a restore.
- # issue link: https://jira.mariadb.org/browse/MDEV-21786
- SEQUENCE_CACHE = 50
+ CONVERSION_MAP = conversions | {
+ FIELD_TYPE.NEWDECIMAL: float,
+ FIELD_TYPE.DATETIME: get_datetime,
+ UnicodeWithAttrs: escape_string,
+ }
+ default_port = "3306"
def setup_type_map(self):
self.db_type = "mariadb"
@@ -65,44 +185,6 @@ class MariaDBDatabase(Database):
"JSON": ("json", ""),
}
- def get_connection(self):
- usessl = 0
- if frappe.conf.db_ssl_ca and frappe.conf.db_ssl_cert and frappe.conf.db_ssl_key:
- usessl = 1
- ssl_params = {
- "ca": frappe.conf.db_ssl_ca,
- "cert": frappe.conf.db_ssl_cert,
- "key": frappe.conf.db_ssl_key,
- }
-
- conversions.update(
- {
- FIELD_TYPE.NEWDECIMAL: float,
- FIELD_TYPE.DATETIME: get_datetime,
- UnicodeWithAttrs: conversions[str],
- }
- )
-
- conn = pymysql.connect(
- user=self.user or "",
- password=self.password or "",
- host=self.host,
- port=self.port,
- charset="utf8mb4",
- use_unicode=True,
- ssl=ssl_params if usessl else None,
- conv=conversions,
- local_infile=frappe.conf.local_infile,
- )
-
- # MYSQL_OPTION_MULTI_STATEMENTS_OFF = 1
- # # self._conn.set_server_option(MYSQL_OPTION_MULTI_STATEMENTS_OFF)
-
- if self.user != "root":
- conn.select_db(self.user)
-
- return conn
-
def get_database_size(self):
"""'Returns database size in MB"""
db_size = self.sql(
@@ -117,9 +199,18 @@ class MariaDBDatabase(Database):
return db_size[0].get("database_size")
+ def log_query(self, query, values, debug, explain):
+ self.last_query = query = self._cursor._executed
+ self._log_query(query, debug, explain)
+ return self.last_query
+
@staticmethod
def escape(s, percent=True):
"""Excape quotes and percent in given string."""
+ # Update: We've scrapped PyMySQL in favour of MariaDB's official Python client
+ # Also, given we're promoting use of the PyPika builder via frappe.qb, the use
+ # of this method should be limited.
+
# pymysql expects unicode argument to escape_string with Python 3
s = frappe.as_unicode(escape_string(frappe.as_unicode(s)), "utf-8").replace("`", "\\`")
@@ -140,7 +231,7 @@ class MariaDBDatabase(Database):
@staticmethod
def is_type_datetime(code):
- return code in (pymysql.DATE, pymysql.DATETIME)
+ return code == pymysql.DATETIME
def rename_table(self, old_name: str, new_name: str) -> list | tuple:
old_name = get_table_name(old_name)
@@ -158,57 +249,6 @@ class MariaDBDatabase(Database):
null_constraint = "NOT NULL" if not nullable else ""
return self.sql_ddl(f"ALTER TABLE `{table_name}` MODIFY `{column}` {type} {null_constraint}")
- # exception types
- @staticmethod
- def is_deadlocked(e):
- return e.args[0] == ER.LOCK_DEADLOCK
-
- @staticmethod
- def is_timedout(e):
- return e.args[0] == ER.LOCK_WAIT_TIMEOUT
-
- @staticmethod
- def is_table_missing(e):
- return e.args[0] == ER.NO_SUCH_TABLE
-
- @staticmethod
- def is_missing_table(e):
- return MariaDBDatabase.is_table_missing(e)
-
- @staticmethod
- def is_missing_column(e):
- return e.args[0] == ER.BAD_FIELD_ERROR
-
- @staticmethod
- def is_duplicate_fieldname(e):
- return e.args[0] == ER.DUP_FIELDNAME
-
- @staticmethod
- def is_duplicate_entry(e):
- return e.args[0] == ER.DUP_ENTRY
-
- @staticmethod
- def is_access_denied(e):
- return e.args[0] == ER.ACCESS_DENIED_ERROR
-
- @staticmethod
- def cant_drop_field_or_key(e):
- return e.args[0] == ER.CANT_DROP_FIELD_OR_KEY
-
- @staticmethod
- def is_syntax_error(e):
- return e.args[0] == ER.PARSE_ERROR
-
- @staticmethod
- def is_data_too_long(e):
- return e.args[0] == ER.DATA_TOO_LONG
-
- def is_primary_key_violation(self, e):
- return self.is_duplicate_entry(e) and "PRIMARY" in cstr(e.args[1])
-
- def is_unique_key_violation(self, e):
- return self.is_duplicate_entry(e) and "Duplicate" in cstr(e.args[1])
-
def create_auth_table(self):
self.sql_ddl(
"""create table if not exists `__Auth` (
@@ -250,22 +290,6 @@ class MariaDBDatabase(Database):
) ENGINE=InnoDB DEFAULT CHARSET=utf8"""
)
- def create_help_table(self):
- self.sql(
- """create table help(
- path varchar(255),
- content text,
- title text,
- intro text,
- full_path text,
- fulltext(title),
- fulltext(content),
- index (path))
- COLLATE=utf8mb4_unicode_ci
- ENGINE=MyISAM
- CHARACTER SET=utf8mb4"""
- )
-
@staticmethod
def get_on_duplicate_update(key=None):
return "ON DUPLICATE key UPDATE "
@@ -283,6 +307,7 @@ class MariaDBDatabase(Database):
where table_name="{table_name}"
and column_name=columns.column_name
and NON_UNIQUE=1
+ and Seq_in_index = 1
limit 1
), 0) as 'index',
column_key = 'UNI' as 'unique'
@@ -301,6 +326,37 @@ class MariaDBDatabase(Database):
)
)
+ def get_column_index(
+ self, table_name: str, fieldname: str, unique: bool = False
+ ) -> frappe._dict | None:
+ """Check if column exists for a specific fields in specified order.
+
+ This differs from db.has_index because it doesn't rely on index name but columns inside an
+ index.
+ """
+
+ indexes = self.sql(
+ f"""SHOW INDEX FROM `{table_name}`
+ WHERE Column_name = "{fieldname}"
+ AND Seq_in_index = 1
+ AND Non_unique={int(not unique)}
+ """,
+ as_dict=True,
+ )
+
+ # Same index can be part of clustered index which contains more fields
+ # We don't want those.
+ for index in indexes:
+ clustered_index = self.sql(
+ f"""SHOW INDEX FROM `{table_name}`
+ WHERE Key_name = "{index.Key_name}"
+ AND Seq_in_index = 2
+ """,
+ as_dict=True,
+ )
+ if not clustered_index:
+ return index
+
def add_index(self, doctype: str, fields: list, index_name: str = None):
"""Creates an index with given fields if not already created.
Index name will be `fieldname1_fieldname2_index`"""
@@ -351,5 +407,26 @@ class MariaDBDatabase(Database):
db_table.sync()
self.begin()
- def get_database_list(self, target):
- return [d[0] for d in self.sql("SHOW DATABASES;")]
+ def get_database_list(self):
+ return self.sql("SHOW DATABASES", pluck=True)
+
+ def get_tables(self, cached=True):
+ """Returns list of tables"""
+ to_query = not cached
+
+ if cached:
+ tables = frappe.cache().get_value("db_tables")
+ to_query = not tables
+
+ if to_query:
+ information_schema = frappe.qb.Schema("information_schema")
+
+ tables = (
+ frappe.qb.from_(information_schema.tables)
+ .select(information_schema.tables.table_name)
+ .where(information_schema.tables.table_schema != "information_schema")
+ .run(pluck=True)
+ )
+ frappe.cache().set_value("db_tables", tables)
+
+ return tables
diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql
index dc91873a82..9507a48b91 100644
--- a/frappe/database/mariadb/framework_mariadb.sql
+++ b/frappe/database/mariadb/framework_mariadb.sql
@@ -116,6 +116,7 @@ CREATE TABLE `tabDocPerm` (
-- Table structure for table `tabDocType Action`
--
+DROP TABLE IF EXISTS `tabDocType Action`;
CREATE TABLE `tabDocType Action` (
`name` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL,
`creation` datetime(6) DEFAULT NULL,
@@ -137,9 +138,10 @@ CREATE TABLE `tabDocType Action` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
--
--- Table structure for table `tabDocType Action`
+-- Table structure for table `tabDocType Link`
--
+DROP TABLE IF EXISTS `tabDocType Link`;
CREATE TABLE `tabDocType Link` (
`name` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL,
`creation` datetime(6) DEFAULT NULL,
@@ -183,7 +185,6 @@ CREATE TABLE `tabDocType` (
`app` varchar(255) DEFAULT NULL,
`autoname` varchar(255) DEFAULT NULL,
`naming_rule` varchar(40) DEFAULT NULL,
- `name_case` varchar(255) DEFAULT NULL,
`title_field` varchar(255) DEFAULT NULL,
`image_field` varchar(255) DEFAULT NULL,
`timeline_field` varchar(255) DEFAULT NULL,
@@ -226,7 +227,7 @@ CREATE TABLE `tabDocType` (
`sender_field` varchar(255) DEFAULT NULL,
`show_title_field_in_link` int(1) NOT NULL DEFAULT 0,
`migration_hash` varchar(255) DEFAULT NULL,
- `translate_link_fields` int(1) NOT NULL DEFAULT 0,
+ `translated_doctype` int(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`name`)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -253,7 +254,6 @@ CREATE TABLE `tabSessions` (
`sessiondata` longtext,
`ipaddress` varchar(16) DEFAULT NULL,
`lastupdate` datetime(6) DEFAULT NULL,
- `device` varchar(255) DEFAULT 'desktop',
`status` varchar(20) DEFAULT NULL,
KEY `sid` (`sid`)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py
index 24a78012e1..bbdd95d921 100644
--- a/frappe/database/mariadb/schema.py
+++ b/frappe/database/mariadb/schema.py
@@ -1,3 +1,5 @@
+from pymysql.constants.ER import DUP_ENTRY
+
import frappe
from frappe import _
from frappe.database.schema import DBTable
@@ -6,7 +8,7 @@ from frappe.model import log_types
class MariaDBTable(DBTable):
def create(self):
- additional_definitions = ""
+ additional_definitions = []
engine = self.meta.get("engine") or "InnoDB"
varchar_len = frappe.db.VARCHAR_LEN
name_column = f"name varchar({varchar_len}) primary key"
@@ -14,26 +16,24 @@ class MariaDBTable(DBTable):
# columns
column_defs = self.get_column_definitions()
if column_defs:
- additional_definitions += ",\n".join(column_defs) + ",\n"
+ additional_definitions += column_defs
# index
index_defs = self.get_index_definitions()
if index_defs:
- additional_definitions += ",\n".join(index_defs) + ",\n"
+ additional_definitions += index_defs
# child table columns
if self.meta.get("istable") or 0:
- additional_definitions += (
- ",\n".join(
- (
- f"parent varchar({varchar_len})",
- f"parentfield varchar({varchar_len})",
- f"parenttype varchar({varchar_len})",
- "index parent(parent)",
- )
- )
- + ",\n"
- )
+ additional_definitions += [
+ f"parent varchar({varchar_len})",
+ f"parentfield varchar({varchar_len})",
+ f"parenttype varchar({varchar_len})",
+ "index parent(parent)",
+ ]
+ else:
+ # parent types
+ additional_definitions.append("index modified(modified)")
# creating sequence(s)
if (
@@ -44,9 +44,11 @@ class MariaDBTable(DBTable):
# NOTE: not used nextval func as default as the ability to restore
# database with sequences has bugs in mariadb and gives a scary error.
- # issue link: https://jira.mariadb.org/browse/MDEV-21786
+ # issue link: https://jira.mariadb.org/browse/MDEV-20070
name_column = "name bigint primary key"
+ additional_definitions = ",\n".join(additional_definitions)
+
# create table
query = f"""create table `{self.table_name}` (
{name_column},
@@ -56,8 +58,7 @@ class MariaDBTable(DBTable):
owner varchar({varchar_len}),
docstatus int(1) not null default '0',
idx int(8) not null default '0',
- {additional_definitions}
- index modified(modified))
+ {additional_definitions})
ENGINE={engine}
ROW_FORMAT=DYNAMIC
CHARACTER SET=utf8mb4
@@ -74,55 +75,39 @@ class MariaDBTable(DBTable):
add_index_query = []
drop_index_query = []
- columns_to_modify = set(self.change_type + self.add_unique + self.set_default)
-
for col in self.add_column:
add_column_query.append(f"ADD COLUMN `{col.fieldname}` {col.get_definition()}")
+ columns_to_modify = set(self.change_type + self.set_default)
for col in columns_to_modify:
- modify_column_query.append(f"MODIFY `{col.fieldname}` {col.get_definition()}")
+ modify_column_query.append(
+ f"MODIFY `{col.fieldname}` {col.get_definition(for_modification=True)}"
+ )
+
+ for col in self.add_unique:
+ modify_column_query.append(
+ f"ADD UNIQUE INDEX IF NOT EXISTS {col.fieldname} (`{col.fieldname}`)"
+ )
for col in self.add_index:
# if index key does not exists
- if not frappe.db.has_index(self.table_name, col.fieldname + "_index"):
+ if not frappe.db.get_column_index(self.table_name, col.fieldname, unique=False):
add_index_query.append(f"ADD INDEX `{col.fieldname}_index`(`{col.fieldname}`)")
- for col in self.drop_index + self.drop_unique:
- if col.fieldname != "name": # primary key
- current_column = self.current_columns.get(col.fieldname.lower())
- unique_constraint_changed = current_column.unique != col.unique
- if unique_constraint_changed and not col.unique:
- # nosemgrep
- unique_index_record = frappe.db.sql(
- """
- SHOW INDEX FROM `{}`
- WHERE Key_name=%s
- AND Non_unique=0
- """.format(
- self.table_name
- ),
- (col.fieldname),
- as_dict=1,
- )
- if unique_index_record:
- drop_index_query.append(f"DROP INDEX `{unique_index_record[0].Key_name}`")
- index_constraint_changed = current_column.index != col.set_index
- # if index key exists
- if index_constraint_changed and not col.set_index:
- # nosemgrep
- index_record = frappe.db.sql(
- """
- SHOW INDEX FROM `{}`
- WHERE Key_name=%s
- AND Non_unique=1
- """.format(
- self.table_name
- ),
- (col.fieldname + "_index"),
- as_dict=1,
- )
- if index_record:
- drop_index_query.append(f"DROP INDEX `{index_record[0].Key_name}`")
+ for col in {*self.drop_index, *self.drop_unique}:
+ if col.fieldname == "name":
+ continue
+
+ current_column = self.current_columns.get(col.fieldname.lower())
+ unique_constraint_changed = current_column.unique != col.unique
+ if unique_constraint_changed and not col.unique:
+ if unique_index := frappe.db.get_column_index(self.table_name, col.fieldname, unique=True):
+ drop_index_query.append(f"DROP INDEX `{unique_index.Key_name}`")
+
+ index_constraint_changed = current_column.index != col.set_index
+ if index_constraint_changed and not col.set_index:
+ if index_record := frappe.db.get_column_index(self.table_name, col.fieldname, unique=False):
+ drop_index_query.append(f"DROP INDEX `{index_record.Key_name}`")
try:
for query_parts in [add_column_query, modify_column_query, add_index_query, drop_index_query]:
@@ -132,17 +117,15 @@ class MariaDBTable(DBTable):
frappe.db.sql(query)
except Exception as e:
- # sanitize
- if e.args[0] == 1060:
- frappe.throw(str(e))
- elif e.args[0] == 1062:
+ if query := locals().get("query"): # this weirdness is to avoid potentially unbounded vars
+ print(f"Failed to alter schema using query: {query}")
+
+ if e.args[0] == DUP_ENTRY:
fieldname = str(e).split("'")[-2]
frappe.throw(
_("{0} field cannot be set as unique in {1}, as there are non-unique existing values").format(
fieldname, self.table_name
)
)
- elif e.args[0] == 1067:
- frappe.throw(str(e.args[1]))
- else:
- raise e
+
+ raise
diff --git a/frappe/database/mariadb/setup_db.py b/frappe/database/mariadb/setup_db.py
index 5eef0ef2c6..add7fa373f 100644
--- a/frappe/database/mariadb/setup_db.py
+++ b/frappe/database/mariadb/setup_db.py
@@ -1,31 +1,26 @@
import os
+import click
+
import frappe
from frappe.database.db_manager import DbManager
-expected_settings_10_2_earlier = {
- "innodb_file_format": "Barracuda",
- "innodb_file_per_table": "ON",
- "innodb_large_prefix": "ON",
- "character_set_server": "utf8mb4",
- "collation_server": "utf8mb4_unicode_ci",
-}
-
-expected_settings_10_3_later = {
+REQUIRED_MARIADB_CONFIG = {
"character_set_server": "utf8mb4",
"collation_server": "utf8mb4_unicode_ci",
}
-def get_mariadb_versions():
+def get_mariadb_variables():
+ return frappe._dict(frappe.db.sql("show variables"))
+
+
+def get_mariadb_version(version_string: str = ""):
# MariaDB classifies their versions as Major (1st and 2nd number), and Minor (3rd number)
# Example: Version 10.3.13 is Major Version = 10.3, Minor Version = 13
- mariadb_variables = frappe._dict(frappe.db.sql("""show variables"""))
- version_string = mariadb_variables.get("version").split("-")[0]
- versions = {}
- versions["major"] = version_string.split(".")[0] + "." + version_string.split(".")[1]
- versions["minor"] = version_string.split(".")[2]
- return versions
+ version_string = version_string or get_mariadb_variables().get("version")
+ version = version_string.split("-", 1)[0]
+ return version.rsplit(".", 1)
def setup_database(force, source_sql, verbose, no_mariadb_socket=False):
@@ -63,29 +58,12 @@ def setup_database(force, source_sql, verbose, no_mariadb_socket=False):
bootstrap_database(db_name, verbose, source_sql)
-def setup_help_database(help_db_name):
- dbman = DbManager(get_root_connection(frappe.flags.root_login, frappe.flags.root_password))
- dbman.drop_database(help_db_name)
-
- # make database
- if not help_db_name in dbman.get_database_list():
- try:
- dbman.create_user(help_db_name, help_db_name)
- except Exception as e:
- # user already exists
- if e.args[0] != 1396:
- raise
- dbman.create_database(help_db_name)
- dbman.grant_all_privileges(help_db_name, help_db_name)
- dbman.flush_privileges()
-
-
def drop_user_and_database(db_name, root_login, root_password):
frappe.local.db = get_root_connection(root_login, root_password)
dbman = DbManager(frappe.local.db)
+ dbman.drop_database(db_name)
dbman.delete_user(db_name, host="%")
dbman.delete_user(db_name)
- dbman.drop_database(db_name)
def bootstrap_database(db_name, verbose, source_sql=None):
@@ -125,36 +103,55 @@ def import_db_from_sql(source_sql=None, verbose=False):
def check_database_settings():
- versions = get_mariadb_versions()
- if versions["major"] <= "10.2":
- expected_variables = expected_settings_10_2_earlier
- else:
- expected_variables = expected_settings_10_3_later
- mariadb_variables = frappe._dict(frappe.db.sql("""show variables"""))
+ check_compatible_versions()
+
# Check each expected value vs. actuals:
+ mariadb_variables = get_mariadb_variables()
result = True
- for key, expected_value in expected_variables.items():
+ for key, expected_value in REQUIRED_MARIADB_CONFIG.items():
if mariadb_variables.get(key) != expected_value:
print(
"For key %s. Expected value %s, found value %s"
% (key, expected_value, mariadb_variables.get(key))
)
result = False
+
if not result:
- site = frappe.local.site
- msg = (
- "Creation of your site - {x} failed because MariaDB is not properly {sep}"
- "configured. If using version 10.2.x or earlier, make sure you use the {sep}"
- "the Barracuda storage engine. {sep}{sep}"
- "Please verify the settings above in MariaDB's my.cnf. Restart MariaDB. And {sep}"
- "then run `bench new-site {x}` again.{sep2}"
- ""
- ).format(x=site, sep2="\n" * 2, sep="\n")
- print_db_config(msg)
+ print(
+ (
+ "{sep2}Creation of your site - {site} failed because MariaDB is not properly {sep}"
+ "configured.{sep2}"
+ "Please verify the above settings in MariaDB's my.cnf. Restart MariaDB.{sep}"
+ "And then run `bench new-site {site}` again.{sep2}"
+ ).format(site=frappe.local.site, sep2="\n\n", sep="\n")
+ )
+
return result
+def check_compatible_versions():
+ try:
+ version = get_mariadb_version()
+ version_tuple = tuple(int(v) for v in version[0].split("."))
+
+ if version_tuple < (10, 6):
+ click.secho(
+ f"Warning: MariaDB version {version} is less than 10.6 which is not supported by Frappe",
+ fg="yellow",
+ )
+ elif version_tuple >= (10, 9):
+ click.secho(
+ f"Warning: MariaDB version {version} is more than 10.8 which is not yet tested with Frappe Framework.",
+ fg="yellow",
+ )
+ except Exception:
+ click.secho(
+ "MariaDB version compatibility checks failed, make sure you're running a supported version.",
+ fg="yellow",
+ )
+
+
def get_root_connection(root_login, root_password):
import getpass
@@ -173,9 +170,3 @@ def get_root_connection(root_login, root_password):
)
return frappe.local.flags.root_connection
-
-
-def print_db_config(explanation):
- print("=" * 80)
- print(explanation)
- print("=" * 80)
diff --git a/frappe/database/operator_map.py b/frappe/database/operator_map.py
new file mode 100644
index 0000000000..2c8b53dae3
--- /dev/null
+++ b/frappe/database/operator_map.py
@@ -0,0 +1,138 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+import operator
+from typing import Callable
+
+import frappe
+from frappe.database.utils import NestedSetHierarchy
+from frappe.model.db_query import get_timespan_date_range
+from frappe.query_builder import Field
+
+
+def like(key: Field, value: str) -> frappe.qb:
+ """Wrapper method for `LIKE`
+
+ Args:
+ key (str): field
+ value (str): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `LIKE`
+ """
+ return key.like(value)
+
+
+def func_in(key: Field, value: list | tuple) -> frappe.qb:
+ """Wrapper method for `IN`
+
+ Args:
+ key (str): field
+ value (Union[int, str]): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `IN`
+ """
+ if isinstance(value, str):
+ value = value.split(",")
+ return key.isin(value)
+
+
+def not_like(key: Field, value: str) -> frappe.qb:
+ """Wrapper method for `NOT LIKE`
+
+ Args:
+ key (str): field
+ value (str): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `NOT LIKE`
+ """
+ return key.not_like(value)
+
+
+def func_not_in(key: Field, value: list | tuple | str):
+ """Wrapper method for `NOT IN`
+
+ Args:
+ key (str): field
+ value (Union[int, str]): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `NOT IN`
+ """
+ if isinstance(value, str):
+ value = value.split(",")
+ return key.notin(value)
+
+
+def func_regex(key: Field, value: str) -> frappe.qb:
+ """Wrapper method for `REGEX`
+
+ Args:
+ key (str): field
+ value (str): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `REGEX`
+ """
+ return key.regex(value)
+
+
+def func_between(key: Field, value: list | tuple) -> frappe.qb:
+ """Wrapper method for `BETWEEN`
+
+ Args:
+ key (str): field
+ value (Union[int, str]): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `BETWEEN`
+ """
+ return key[slice(*value)]
+
+
+def func_is(key, value):
+ "Wrapper for IS"
+ return key.isnotnull() if value.lower() == "set" else key.isnull()
+
+
+def func_timespan(key: Field, value: str) -> frappe.qb:
+ """Wrapper method for `TIMESPAN`
+
+ Args:
+ key (str): field
+ value (str): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `TIMESPAN`
+ """
+
+ return func_between(key, get_timespan_date_range(value))
+
+
+# default operators
+OPERATOR_MAP: dict[str, Callable] = {
+ "+": operator.add,
+ "=": operator.eq,
+ "-": operator.sub,
+ "!=": operator.ne,
+ "<": operator.lt,
+ ">": operator.gt,
+ "<=": operator.le,
+ "=<": operator.le,
+ ">=": operator.ge,
+ "=>": operator.ge,
+ "/": operator.truediv,
+ "*": operator.mul,
+ "in": func_in,
+ "not in": func_not_in,
+ "like": like,
+ "not like": not_like,
+ "regex": func_regex,
+ "between": func_between,
+ "is": func_is,
+ "timespan": func_timespan,
+ "nested_set": NestedSetHierarchy,
+ # TODO: Add support for custom operators (WIP) - via filters_config hooks
+}
diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py
index 2553ebaa26..d082afceaf 100644
--- a/frappe/database/postgres/database.py
+++ b/frappe/database/postgres/database.py
@@ -2,12 +2,23 @@ import re
import psycopg2
import psycopg2.extensions
-from psycopg2.errorcodes import STRING_DATA_RIGHT_TRUNCATION
+from psycopg2.errorcodes import (
+ CLASS_INTEGRITY_CONSTRAINT_VIOLATION,
+ DEADLOCK_DETECTED,
+ DUPLICATE_COLUMN,
+ INSUFFICIENT_PRIVILEGE,
+ STRING_DATA_RIGHT_TRUNCATION,
+ UNDEFINED_COLUMN,
+ UNDEFINED_TABLE,
+ UNIQUE_VIOLATION,
+)
+from psycopg2.errors import ReadOnlySqlTransaction, SequenceGeneratorLimitExceeded, SyntaxError
from psycopg2.extensions import ISOLATION_LEVEL_REPEATABLE_READ
import frappe
from frappe.database.database import Database
from frappe.database.postgres.schema import PostgresTable
+from frappe.database.utils import EmptyQueryValues, LazyDecode
from frappe.utils import cstr, get_table_name
# cast decimals as floats
@@ -25,7 +36,7 @@ PG_TRANSFORM_PATTERN = re.compile(r"([=><]+)\s*([+-]?\d+)(\.0)?(?![a-zA-Z\.\d])"
FROM_TAB_PATTERN = re.compile(r"from tab([\w-]*)", flags=re.IGNORECASE)
-class PostgresDatabase(Database):
+class PostgresExceptionUtil:
ProgrammingError = psycopg2.ProgrammingError
TableMissingError = psycopg2.ProgrammingError
OperationalError = psycopg2.OperationalError
@@ -33,13 +44,73 @@ class PostgresDatabase(Database):
SQLError = psycopg2.ProgrammingError
DataError = psycopg2.DataError
InterfaceError = psycopg2.InterfaceError
- REGEX_CHARACTER = "~"
+ SequenceGeneratorLimitExceeded = SequenceGeneratorLimitExceeded
- # NOTE; The sequence cache for postgres is per connection.
- # Since we're opening and closing connections for every transaction this results in skipping the cache
- # to the next non-cached value hence not using cache in postgres.
- # ref: https://stackoverflow.com/questions/21356375/postgres-9-0-4-sequence-skipping-numbers
- SEQUENCE_CACHE = 0
+ @staticmethod
+ def is_deadlocked(e):
+ return getattr(e, "pgcode", None) == DEADLOCK_DETECTED
+
+ @staticmethod
+ def is_timedout(e):
+ # http://initd.org/psycopg/docs/extensions.html?highlight=datatype#psycopg2.extensions.QueryCanceledError
+ return isinstance(e, psycopg2.extensions.QueryCanceledError)
+
+ @staticmethod
+ def is_read_only_mode_error(e) -> bool:
+ return isinstance(e, ReadOnlySqlTransaction)
+
+ @staticmethod
+ def is_syntax_error(e):
+ return isinstance(e, SyntaxError)
+
+ @staticmethod
+ def is_table_missing(e):
+ return getattr(e, "pgcode", None) == UNDEFINED_TABLE
+
+ @staticmethod
+ def is_missing_table(e):
+ return PostgresDatabase.is_table_missing(e)
+
+ @staticmethod
+ def is_missing_column(e):
+ return getattr(e, "pgcode", None) == UNDEFINED_COLUMN
+
+ @staticmethod
+ def is_access_denied(e):
+ return getattr(e, "pgcode", None) == INSUFFICIENT_PRIVILEGE
+
+ @staticmethod
+ def cant_drop_field_or_key(e):
+ return getattr(e, "pgcode", None) == CLASS_INTEGRITY_CONSTRAINT_VIOLATION
+
+ @staticmethod
+ def is_duplicate_entry(e):
+ return getattr(e, "pgcode", None) == UNIQUE_VIOLATION
+
+ @staticmethod
+ def is_primary_key_violation(e):
+ return getattr(e, "pgcode", None) == UNIQUE_VIOLATION and "_pkey" in cstr(e.args[0])
+
+ @staticmethod
+ def is_unique_key_violation(e):
+ return getattr(e, "pgcode", None) == UNIQUE_VIOLATION and "_key" in cstr(e.args[0])
+
+ @staticmethod
+ def is_duplicate_fieldname(e):
+ return getattr(e, "pgcode", None) == DUPLICATE_COLUMN
+
+ @staticmethod
+ def is_statement_timeout(e):
+ return PostgresDatabase.is_timedout(e) or isinstance(e, frappe.QueryTimeoutError)
+
+ @staticmethod
+ def is_data_too_long(e):
+ return getattr(e, "pgcode", None) == STRING_DATA_RIGHT_TRUNCATION
+
+
+class PostgresDatabase(PostgresExceptionUtil, Database):
+ REGEX_CHARACTER = "~"
+ default_port = "5432"
def setup_type_map(self):
self.db_type = "postgres"
@@ -80,6 +151,10 @@ class PostgresDatabase(Database):
"JSON": ("json", ""),
}
+ @property
+ def last_query(self):
+ return LazyDecode(self._cursor.query)
+
def get_connection(self):
conn = psycopg2.connect(
"host='{}' dbname='{}' user='{}' password='{}' port={}".format(
@@ -90,6 +165,10 @@ class PostgresDatabase(Database):
return conn
+ def set_execution_timeout(self, seconds: int):
+ # Postgres expects milliseconds as input
+ self.sql("set local statement_timeout = %s", int(seconds) * 1000)
+
def escape(self, s, percent=True):
"""Escape quotes and percent in given string."""
if isinstance(s, bytes):
@@ -116,9 +195,12 @@ class PostgresDatabase(Database):
return db_size[0].get("database_size")
# pylint: disable=W0221
- def sql(self, query, values=(), *args, **kwargs):
+ def sql(self, query, values=EmptyQueryValues, *args, **kwargs):
return super().sql(modify_query(query), modify_values(values), *args, **kwargs)
+ def lazy_mogrify(self, *args, **kwargs) -> str:
+ return self.last_query
+
def get_tables(self, cached=True):
return [
d[0]
@@ -151,60 +233,6 @@ class PostgresDatabase(Database):
def is_type_datetime(code):
return code == psycopg2.DATETIME
- # exception type
- @staticmethod
- def is_deadlocked(e):
- return e.pgcode == "40P01"
-
- @staticmethod
- def is_timedout(e):
- # http://initd.org/psycopg/docs/extensions.html?highlight=datatype#psycopg2.extensions.QueryCanceledError
- return isinstance(e, psycopg2.extensions.QueryCanceledError)
-
- @staticmethod
- def is_syntax_error(e):
- return isinstance(e, psycopg2.errors.SyntaxError)
-
- @staticmethod
- def is_table_missing(e):
- return getattr(e, "pgcode", None) == "42P01"
-
- @staticmethod
- def is_missing_table(e):
- return PostgresDatabase.is_table_missing(e)
-
- @staticmethod
- def is_missing_column(e):
- return getattr(e, "pgcode", None) == "42703"
-
- @staticmethod
- def is_access_denied(e):
- return e.pgcode == "42501"
-
- @staticmethod
- def cant_drop_field_or_key(e):
- return e.pgcode.startswith("23")
-
- @staticmethod
- def is_duplicate_entry(e):
- return e.pgcode == "23505"
-
- @staticmethod
- def is_primary_key_violation(e):
- return getattr(e, "pgcode", None) == "23505" and "_pkey" in cstr(e.args[0])
-
- @staticmethod
- def is_unique_key_violation(e):
- return getattr(e, "pgcode", None) == "23505" and "_key" in cstr(e.args[0])
-
- @staticmethod
- def is_duplicate_fieldname(e):
- return e.pgcode == "42701"
-
- @staticmethod
- def is_data_too_long(e):
- return e.pgcode == STRING_DATA_RIGHT_TRUNCATION
-
def rename_table(self, old_name: str, new_name: str) -> list | tuple:
old_name = get_table_name(old_name)
new_name = get_table_name(new_name)
@@ -269,17 +297,6 @@ class PostgresDatabase(Database):
)"""
)
- def create_help_table(self):
- self.sql(
- """CREATE TABLE "help"(
- "path" varchar(255),
- "content" text,
- "title" text,
- "intro" text,
- "full_path" text)"""
- )
- self.sql("""CREATE INDEX IF NOT EXISTS "help_index" ON "help" ("path")""")
-
def updatedb(self, doctype, meta=None):
"""
Syncs a `DocType` to the table
@@ -377,8 +394,8 @@ class PostgresDatabase(Database):
as_dict=1,
)
- def get_database_list(self, target):
- return [d[0] for d in self.sql("SELECT datname FROM pg_database;")]
+ def get_database_list(self):
+ return self.sql("SELECT datname FROM pg_database", pluck=True)
def modify_query(query):
@@ -409,7 +426,7 @@ def modify_values(values):
return value
- if not values:
+ if not values or values == EmptyQueryValues:
return values
if isinstance(values, dict):
diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql
index 99e94a226f..37605be0f6 100644
--- a/frappe/database/postgres/framework_postgres.sql
+++ b/frappe/database/postgres/framework_postgres.sql
@@ -188,7 +188,6 @@ CREATE TABLE "tabDocType" (
"app" varchar(255) DEFAULT NULL,
"autoname" varchar(255) DEFAULT NULL,
"naming_rule" varchar(40) DEFAULT NULL,
- "name_case" varchar(255) DEFAULT NULL,
"title_field" varchar(255) DEFAULT NULL,
"image_field" varchar(255) DEFAULT NULL,
"timeline_field" varchar(255) DEFAULT NULL,
@@ -231,7 +230,7 @@ CREATE TABLE "tabDocType" (
"sender_field" varchar(255) DEFAULT NULL,
"show_title_field_in_link" smallint NOT NULL DEFAULT 0,
"migration_hash" varchar(255) DEFAULT NULL,
- "translate_link_fields" smallint NOT NULL DEFAULT 0,
+ "translated_doctype" smallint NOT NULL DEFAULT 0,
PRIMARY KEY ("name")
) ;
@@ -257,7 +256,6 @@ CREATE TABLE "tabSessions" (
"sessiondata" text,
"ipaddress" varchar(16) DEFAULT NULL,
"lastupdate" timestamp(6) DEFAULT NULL,
- "device" varchar(255) DEFAULT 'desktop',
"status" varchar(20) DEFAULT NULL
);
diff --git a/frappe/database/postgres/setup_db.py b/frappe/database/postgres/setup_db.py
index 7eee8081c0..ff14510c9c 100644
--- a/frappe/database/postgres/setup_db.py
+++ b/frappe/database/postgres/setup_db.py
@@ -75,15 +75,6 @@ def import_db_from_sql(source_sql=None, verbose=False):
)
-def setup_help_database(help_db_name):
- root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password)
- root_conn.sql(f"DROP DATABASE IF EXISTS `{help_db_name}`")
- root_conn.sql(f"DROP USER IF EXISTS {help_db_name}")
- root_conn.sql(f"CREATE DATABASE `{help_db_name}`")
- root_conn.sql(f"CREATE user {help_db_name} password '{help_db_name}'")
- root_conn.sql("GRANT ALL PRIVILEGES ON DATABASE `{0}` TO {0}".format(help_db_name))
-
-
def get_root_connection(root_login=None, root_password=None):
if not frappe.local.flags.root_connection:
if not root_login:
diff --git a/frappe/database/query.py b/frappe/database/query.py
index 9bb5383b24..595bd5a3ff 100644
--- a/frappe/database/query.py
+++ b/frappe/database/query.py
@@ -1,15 +1,23 @@
-import operator
+import itertools
import re
from ast import literal_eval
-from functools import cached_property
from types import BuiltinFunctionType
-from typing import TYPE_CHECKING, Any, Callable
+from typing import TYPE_CHECKING
+
+import sqlparse
+from pypika.queries import QueryBuilder, Table
import frappe
from frappe import _
-from frappe.model.db_query import get_timespan_date_range
-from frappe.query_builder import Criterion, Field, Order, Table, functions
+from frappe.database.operator_map import OPERATOR_MAP
+from frappe.database.utils import DefaultOrderBy, get_doctype_name
+from frappe.query_builder import Criterion, Field, Order, functions
from frappe.query_builder.functions import Function, SqlFunctions
+from frappe.query_builder.utils import PseudoColumnMapper
+from frappe.utils.data import MARIADB_SPECIFIC_COMMENT
+
+if TYPE_CHECKING:
+ from frappe.query_builder import DocType
TAB_PATTERN = re.compile("^tab")
WORDS_PATTERN = re.compile(r"\w+")
@@ -18,364 +26,181 @@ SQL_FUNCTIONS = [sql_function.value for sql_function in SqlFunctions]
COMMA_PATTERN = re.compile(r",\s*(?![^()]*\))")
-def like(key: Field, value: str) -> frappe.qb:
- """Wrapper method for `LIKE`
-
- Args:
- key (str): field
- value (str): criterion
-
- Returns:
- frappe.qb: `frappe.qb object with `LIKE`
- """
- return key.like(value)
-
-
-def func_in(key: Field, value: list | tuple) -> frappe.qb:
- """Wrapper method for `IN`
-
- Args:
- key (str): field
- value (Union[int, str]): criterion
-
- Returns:
- frappe.qb: `frappe.qb object with `IN`
- """
- return key.isin(value)
-
-
-def not_like(key: Field, value: str) -> frappe.qb:
- """Wrapper method for `NOT LIKE`
-
- Args:
- key (str): field
- value (str): criterion
-
- Returns:
- frappe.qb: `frappe.qb object with `NOT LIKE`
- """
- return key.not_like(value)
-
-
-def func_not_in(key: Field, value: list | tuple):
- """Wrapper method for `NOT IN`
-
- Args:
- key (str): field
- value (Union[int, str]): criterion
-
- Returns:
- frappe.qb: `frappe.qb object with `NOT IN`
- """
- return key.notin(value)
-
-
-def func_regex(key: Field, value: str) -> frappe.qb:
- """Wrapper method for `REGEX`
-
- Args:
- key (str): field
- value (str): criterion
-
- Returns:
- frappe.qb: `frappe.qb object with `REGEX`
- """
- return key.regex(value)
-
-
-def func_between(key: Field, value: list | tuple) -> frappe.qb:
- """Wrapper method for `BETWEEN`
-
- Args:
- key (str): field
- value (Union[int, str]): criterion
-
- Returns:
- frappe.qb: `frappe.qb object with `BETWEEN`
- """
- return key[slice(*value)]
-
-
-def func_is(key, value):
- "Wrapper for IS"
- return key.isnotnull() if value.lower() == "set" else key.isnull()
-
-
-def func_timespan(key: Field, value: str) -> frappe.qb:
- """Wrapper method for `TIMESPAN`
-
- Args:
- key (str): field
- value (str): criterion
-
- Returns:
- frappe.qb: `frappe.qb object with `TIMESPAN`
- """
-
- return func_between(key, get_timespan_date_range(value))
-
-
-def make_function(key: Any, value: int | str):
- """returns fucntion query
-
- Args:
- key (Any): field
- value (Union[int, str]): criterion
-
- Returns:
- frappe.qb: frappe.qb object
- """
- return OPERATOR_MAP[value[0].casefold()](key, value[1])
-
-
-def change_orderby(order: str):
- """Convert orderby to standart Order object
-
- Args:
- order (str): Field, order
-
- Returns:
- tuple: field, order
- """
- order = order.split()
-
- try:
- if order[1].lower() == "asc":
- return order[0], Order.asc
- except IndexError:
- pass
-
- return order[0], Order.desc
-
-
-def literal_eval_(literal):
- try:
- return literal_eval(literal)
- except (ValueError, SyntaxError):
- return literal
-
-
-# default operators
-OPERATOR_MAP: dict[str, Callable] = {
- "+": operator.add,
- "=": operator.eq,
- "-": operator.sub,
- "!=": operator.ne,
- "<": operator.lt,
- ">": operator.gt,
- "<=": operator.le,
- "=<": operator.le,
- ">=": operator.ge,
- "=>": operator.ge,
- "/": operator.truediv,
- "*": operator.mul,
- "in": func_in,
- "not in": func_not_in,
- "like": like,
- "not like": not_like,
- "regex": func_regex,
- "between": func_between,
- "is": func_is,
- "timespan": func_timespan,
- # TODO: Add support for nested set
- # TODO: Add support for custom operators (WIP) - via filters_config hooks
-}
-
-
class Engine:
- tables: dict[str, str] = {}
+ def get_query(
+ self,
+ table: str | Table,
+ fields: list | tuple | None = None,
+ filters: dict[str, str | int] | str | int | list[list | str | int] | None = None,
+ order_by: str | None = None,
+ group_by: str | None = None,
+ limit: int | None = None,
+ offset: int | None = None,
+ distinct: bool = False,
+ for_update: bool = False,
+ update: bool = False,
+ into: bool = False,
+ delete: bool = False,
+ ) -> QueryBuilder:
+ self.is_mariadb = frappe.db.db_type == "mariadb"
+ self.is_postgres = frappe.db.db_type == "postgres"
- @cached_property
- def OPERATOR_MAP(self):
- from frappe.boot import get_additional_filters_from_hooks
+ if isinstance(table, Table):
+ self.table = table
+ self.doctype = get_doctype_name(table.get_sql())
+ else:
+ self.doctype = table
+ self.table = frappe.qb.DocType(table)
- # default operators
- all_operators = OPERATOR_MAP.copy()
+ if update:
+ self.query = frappe.qb.update(self.table)
+ elif into:
+ self.query = frappe.qb.into(self.table)
+ elif delete:
+ self.query = frappe.qb.from_(self.table).delete()
+ else:
+ self.query = frappe.qb.from_(self.table)
+ self.apply_fields(fields)
- # update with site-specific custom operators
- additional_filters_config = get_additional_filters_from_hooks()
+ self.apply_filters(filters)
+ self.apply_order_by(order_by)
- if additional_filters_config:
- from frappe.utils.commands import warn
+ if limit:
+ self.query = self.query.limit(limit)
- warn("'filters_config' hook is not completely implemented yet in frappe.db.query engine")
+ if offset:
+ self.query = self.query.offset(offset)
- for _operator, function in additional_filters_config.items():
- if callable(function):
- all_operators.update({_operator.casefold(): function})
- elif isinstance(function, dict):
- all_operators[_operator.casefold()] = frappe.get_attr(function.get("get_field"))()["operator"]
+ if distinct:
+ self.query = self.query.distinct()
- return all_operators
+ if for_update:
+ self.query = self.query.for_update()
- def get_condition(self, table: str | Table, **kwargs) -> frappe.qb:
- """Get initial table object
+ if group_by:
+ self.query = self.query.groupby(group_by)
- Args:
- table (str): DocType
+ return self.query
- Returns:
- frappe.qb: DocType with initial condition
- """
- table_object = self.get_table(table)
- if kwargs.get("update"):
- return frappe.qb.update(table_object)
- if kwargs.get("into"):
- return frappe.qb.into(table_object)
- return frappe.qb.from_(table_object)
+ def apply_fields(self, fields):
+ # add fields
+ self.fields = self.parse_fields(fields)
+ if not self.fields:
+ self.fields = [getattr(self.table, "name")]
- def get_table(self, table_name: str | Table) -> Table:
- if isinstance(table_name, Table):
- return table_name
- table_name = table_name.strip('"').strip("'")
- if table_name not in self.tables:
- self.tables[table_name] = frappe.qb.DocType(table_name)
- return self.tables[table_name]
-
- def criterion_query(self, table: str, criterion: Criterion, **kwargs) -> frappe.qb:
- """Generate filters from Criterion objects
-
- Args:
- table (str): DocType
- criterion (Criterion): Filters
-
- Returns:
- frappe.qb: condition object
- """
- condition = self.add_conditions(self.get_condition(table, **kwargs), **kwargs)
- return condition.where(criterion)
-
- def add_conditions(self, conditions: frappe.qb, **kwargs):
- """Adding additional conditions
-
- Args:
- conditions (frappe.qb): built conditions
-
- Returns:
- conditions (frappe.qb): frappe.qb object
- """
- if kwargs.get("orderby") and kwargs.get("orderby") != "KEEP_DEFAULT_ORDERING":
- orderby = kwargs.get("orderby")
- if isinstance(orderby, str) and len(orderby.split()) > 1:
- for ordby in orderby.split(","):
- if ordby := ordby.strip():
- orderby, order = change_orderby(ordby)
- conditions = conditions.orderby(orderby, order=order)
+ self.query._child_queries = []
+ for field in self.fields:
+ if isinstance(field, DynamicTableField):
+ self.query = field.apply_select(self.query)
+ elif isinstance(field, ChildQuery):
+ self.query._child_queries.append(field)
else:
- conditions = conditions.orderby(orderby, order=kwargs.get("order") or Order.desc)
+ self.query = self.query.select(field)
- if kwargs.get("limit"):
- conditions = conditions.limit(kwargs.get("limit"))
- conditions = conditions.offset(kwargs.get("offset", 0))
+ def apply_filters(
+ self,
+ filters: dict[str, str | int] | str | int | list[list | str | int] | None = None,
+ ):
+ if filters is None:
+ return
- if kwargs.get("distinct"):
- conditions = conditions.distinct()
-
- if kwargs.get("for_update"):
- conditions = conditions.for_update()
-
- if kwargs.get("groupby"):
- conditions = conditions.groupby(kwargs.get("groupby"))
-
- return conditions
-
- def misc_query(self, table: str, filters: list | tuple = None, **kwargs):
- """Build conditions using the given Lists or Tuple filters
-
- Args:
- table (str): DocType
- filters (Union[List, Tuple], optional): Filters. Defaults to None.
- """
- conditions = self.get_condition(table, **kwargs)
- if not filters:
- return conditions
- if isinstance(filters, list):
- for f in filters:
- if isinstance(f, (list, tuple)):
- _operator = self.OPERATOR_MAP[f[-2].casefold()]
- if len(f) == 4:
- table_object = self.get_table(f[0])
- _field = table_object[f[1]]
- else:
- _field = Field(f[0])
- conditions = conditions.where(_operator(_field, f[-1]))
- elif isinstance(f, dict):
- conditions = self.dict_query(table, f, **kwargs)
- else:
- _operator = self.OPERATOR_MAP[filters[1].casefold()]
- if not isinstance(filters[0], str):
- conditions = make_function(filters[0], filters[2])
- break
- conditions = conditions.where(_operator(Field(filters[0]), filters[2]))
- break
-
- return self.add_conditions(conditions, **kwargs)
-
- def dict_query(self, table: str, filters: dict[str, str | int] = None, **kwargs) -> frappe.qb:
- """Build conditions using the given dictionary filters
-
- Args:
- table (str): DocType
- filters (Dict[str, Union[str, int]], optional): Filters. Defaults to None.
-
- Returns:
- frappe.qb: conditions object
- """
- conditions = self.get_condition(table, **kwargs)
- if not filters:
- conditions = self.add_conditions(conditions, **kwargs)
- return conditions
-
- for key, value in filters.items():
- if isinstance(value, bool):
- filters.update({key: str(int(value))})
-
- for key in filters:
- value = filters.get(key)
- _operator = self.OPERATOR_MAP["="]
-
- if not isinstance(key, str):
- conditions = conditions.where(make_function(key, value))
- continue
- if isinstance(value, (list, tuple)):
- _operator = self.OPERATOR_MAP[value[0].casefold()]
- _value = value[1] if value[1] else ("",)
- conditions = conditions.where(_operator(Field(key), _value))
- else:
- if value is not None:
- conditions = conditions.where(_operator(Field(key), value))
- else:
- _table = conditions._from[0]
- field = getattr(_table, key)
- conditions = conditions.where(field.isnull())
-
- return self.add_conditions(conditions, **kwargs)
-
- def build_conditions(
- self, table: str, filters: dict[str, str | int] | str | int = None, **kwargs
- ) -> frappe.qb:
- """Build conditions for sql query
-
- Args:
- filters (Union[Dict[str, Union[str, int]], str, int]): conditions in Dict
- table (str): DocType
-
- Returns:
- frappe.qb: frappe.qb conditions object
- """
- if isinstance(filters, int) or isinstance(filters, str):
+ if isinstance(filters, (str, int)):
filters = {"name": str(filters)}
if isinstance(filters, Criterion):
- criterion = self.criterion_query(table, filters, **kwargs)
+ self.query = self.query.where(filters)
+
+ elif isinstance(filters, dict):
+ self.apply_dict_filters(filters)
elif isinstance(filters, (list, tuple)):
- criterion = self.misc_query(table, filters, **kwargs)
+ if all(isinstance(d, (str, int)) for d in filters) and len(filters) > 0:
+ self.apply_dict_filters({"name": ("in", filters)})
+ else:
+ for filter in filters:
+ if isinstance(filter, (str, int, Criterion, dict)):
+ self.apply_filters(filter)
+ elif isinstance(filter, (list, tuple)):
+ self.apply_list_filters(filter)
+ def apply_list_filters(self, filter: list):
+ if len(filter) == 2:
+ field, value = filter
+ self._apply_filter(field, value)
+ elif len(filter) == 3:
+ field, operator, value = filter
+ self._apply_filter(field, value, operator)
+ elif len(filter) == 4:
+ doctype, field, operator, value = filter
+ self._apply_filter(field, value, operator, doctype)
+
+ def apply_dict_filters(self, filters: dict[str, str | int | list]):
+ for key in filters:
+ value = filters.get(key)
+ self._apply_filter(key, value)
+
+ def _apply_filter(
+ self, field: str, value: str | int | list | None, operator: str = "=", doctype: str | None = None
+ ):
+ _field = field
+ _value = value
+ _operator = operator
+
+ if isinstance(_field, Field):
+ pass
+ elif dynamic_field := DynamicTableField.parse(field, self.doctype):
+ # apply implicit join if link field's field is referenced
+ self.query = dynamic_field.apply_join(self.query)
+ _field = dynamic_field.field
+ elif has_function(field):
+ _field = self.get_function_object(field)
+ elif not doctype or doctype == self.doctype:
+ _field = self.table[field]
+ elif doctype:
+ _field = frappe.qb.DocType(doctype)[field]
+
+ # apply implicit join if child table is referenced
+ if doctype and doctype != self.doctype:
+ meta = frappe.get_meta(doctype)
+ table = frappe.qb.DocType(doctype)
+ if meta.istable and not self.query.is_joined(table):
+ self.query = self.query.left_join(table).on(
+ (table.parent == self.table.name) & (table.parenttype == self.doctype)
+ )
+
+ if isinstance(_value, (list, tuple)):
+ _operator, _value = _value
+ elif isinstance(_value, bool):
+ _value = int(_value)
+
+ if isinstance(_value, str) and has_function(_value):
+ _value = self.get_function_object(_value)
+
+ if isinstance(_value, (list, tuple)) and not _value:
+ _value = ("",)
+
+ # Nested set
+ if _operator in OPERATOR_MAP["nested_set"]:
+ hierarchy = _operator
+ docname = _value
+ result = get_nested_set_hierarchy_result(self.doctype, docname, hierarchy)
+ operator_fn = (
+ OPERATOR_MAP["not in"]
+ if hierarchy in ("not ancestors of", "not descendants of")
+ else OPERATOR_MAP["in"]
+ )
+ if result:
+ result = list(itertools.chain.from_iterable(result))
+ self.query = self.query.where(operator_fn(_field, result))
+ else:
+ self.query = self.query.where(operator_fn(_field, ("",)))
+ return
+
+ operator_fn = OPERATOR_MAP[_operator.casefold()]
+ if _value is None and isinstance(_field, Field):
+ self.query = self.query.where(_field.isnull())
else:
- criterion = self.dict_query(filters=filters, table=table, **kwargs)
-
- return criterion
+ self.query = self.query.where(operator_fn(_field, _value))
def get_function_object(self, field: str) -> "Function":
"""Expects field to look like 'SUM(*)' or 'name' or something similar. Returns PyPika Function object"""
@@ -399,147 +224,110 @@ class Engine:
if isinstance(operator_mapping, BuiltinFunctionType):
has_primitive_operator = True
field = operator_mapping(
- *map(lambda field: Field(field.strip()), arg.split(_operator)),
+ *map(
+ lambda field: Field(field.strip())
+ if "`" not in field
+ else PseudoColumnMapper(field.strip()),
+ arg.split(_operator),
+ ),
)
- field = Field(initial_fields) if not has_primitive_operator else field
+ field = (
+ (Field(initial_fields) if "`" not in initial_fields else PseudoColumnMapper(initial_fields))
+ if not has_primitive_operator
+ else field
+ )
else:
field = initial_fields
_args.append(field)
+
+ if alias and "`" in alias:
+ alias = alias.replace("`", "")
try:
+ if func.casefold() == "now":
+ return getattr(functions, func)()
return getattr(functions, func)(*_args, alias=alias or None)
except AttributeError:
# Fall back for functions not present in `SqlFunctions``
return Function(func, *_args, alias=alias or None)
- def function_objects_from_string(self, fields):
- fields = list(map(lambda str: str.strip(), COMMA_PATTERN.split(fields)))
- return self.function_objects_from_list(fields=fields)
-
- def function_objects_from_list(self, fields):
- functions = []
- for field in fields:
- field = field.casefold() if isinstance(field, str) else field
- if not issubclass(type(field), Criterion):
- if any([f"{func}(" in field for func in SQL_FUNCTIONS]) or "(" in field:
- functions.append(field)
-
- return [self.get_function_object(function) for function in functions]
-
- def remove_string_functions(self, fields, function_objects):
- """Remove string functions from fields which have already been converted to function objects"""
- for function in function_objects:
- if isinstance(fields, str):
- if function.alias:
- fields = fields.replace(" as " + function.alias.casefold(), "")
- fields = BRACKETS_PATTERN.sub("", fields.replace(function.name.casefold(), ""))
- # Check if only comma is left in fields after stripping functions.
- if "," in fields and (len(fields.strip()) == 1):
- fields = ""
- else:
- updated_fields = []
- for field in fields:
- if isinstance(field, str):
- if function.alias:
- field = field.replace(" as " + function.alias.casefold(), "")
- field = (
- BRACKETS_PATTERN.sub("", field).strip().casefold().replace(function.name.casefold(), "")
- )
- updated_fields.append(field)
-
- fields = [field for field in updated_fields if field]
-
- return fields
-
- def set_fields(self, fields, **kwargs):
- fields = kwargs.get("pluck") if kwargs.get("pluck") else fields or "name"
- if isinstance(fields, list) and None in fields and Field not in fields:
- return None
-
- function_objects = []
-
- is_list = isinstance(fields, (list, tuple, set))
- if is_list and len(fields) == 1:
- fields = fields[0]
- is_list = False
-
- if is_list:
- function_objects += self.function_objects_from_list(fields=fields)
-
- is_str = isinstance(fields, str)
- if is_str:
- fields = fields.casefold()
- function_objects += self.function_objects_from_string(fields=fields)
-
- fields = self.remove_string_functions(fields, function_objects)
-
- if is_str and "," in fields:
- fields = [field.replace(" ", "") if "as" not in field else field for field in fields.split(",")]
- is_list, is_str = True, False
-
- if is_str:
- if fields == "*":
- return fields
- if " as " in fields:
- fields, reference = fields.split(" as ")
- fields = Field(fields).as_(reference)
-
- if not is_str and fields:
- if issubclass(type(fields), Criterion):
- return fields
- updated_fields = []
- if "*" in fields:
- return fields
- for field in fields:
- if not isinstance(field, Criterion) and field:
- if " as " in field:
- field, reference = field.split(" as ")
- updated_fields.append(Field(field.strip()).as_(reference))
- else:
- updated_fields.append(Field(field))
-
- fields = updated_fields
-
- # Need to check instance again since fields modified.
- if not isinstance(fields, (list, tuple, set)):
- fields = [fields] if fields else []
-
- fields.extend(function_objects)
- return fields
-
- def get_query(
- self,
- table: str,
- fields: list | tuple,
- filters: dict[str, str | int] | str | int | list[list | str | int] = None,
- **kwargs,
- ):
- # Clean up state before each query
- self.tables = {}
- criterion = self.build_conditions(table, filters, **kwargs)
- fields = self.set_fields(kwargs.get("field_objects") or fields, **kwargs)
-
- join = kwargs.get("join").replace(" ", "_") if kwargs.get("join") else "left_join"
-
- if len(self.tables) > 1:
- primary_table = self.tables[table]
- del self.tables[table]
- for table_object in self.tables.values():
- criterion = getattr(criterion, join)(table_object).on(
- table_object.parent == primary_table.name
- )
+ def sanitize_fields(self, fields: str | list | tuple):
+ def _sanitize_field(field: str):
+ if not isinstance(field, str):
+ return field
+ stripped_field = sqlparse.format(field, strip_comments=True, keyword_case="lower")
+ if self.is_mariadb:
+ return MARIADB_SPECIFIC_COMMENT.sub("", stripped_field)
+ return stripped_field
if isinstance(fields, (list, tuple)):
- query = criterion.select(*fields)
+ return [_sanitize_field(field) for field in fields]
+ elif isinstance(fields, str):
+ return _sanitize_field(fields)
- elif isinstance(fields, Criterion):
- query = criterion.select(fields)
+ return fields
- else:
- query = criterion.select(fields)
+ def parse_string_field(self, field: str):
+ if field == "*":
+ return self.table.star
+ alias = None
+ if " as " in field:
+ field, alias = field.split(" as ")
+ if "`" in field:
+ if alias:
+ return PseudoColumnMapper(f"{field} {alias}")
+ return PseudoColumnMapper(field)
+ if alias:
+ return self.table[field].as_(alias)
+ return self.table[field]
- return query
+ def parse_fields(self, fields: str | list | tuple | None) -> list:
+ if not fields:
+ return []
+ fields = self.sanitize_fields(fields)
+ if isinstance(fields, (list, tuple, set)) and None in fields and Field not in fields:
+ return []
+
+ if not isinstance(fields, (list, tuple)):
+ fields = [fields]
+
+ def parse_field(field: str):
+ if has_function(field):
+ return self.get_function_object(field)
+ elif parsed := DynamicTableField.parse(field, self.doctype):
+ return parsed
+ else:
+ return self.parse_string_field(field)
+
+ _fields = []
+ for field in fields:
+ if isinstance(field, Criterion):
+ _fields.append(field)
+ elif isinstance(field, dict):
+ for child_field, fields in field.items():
+ _fields.append(ChildQuery(child_field, fields, self.doctype))
+ elif isinstance(field, str):
+ if "," in field:
+ field = field.casefold() if "`" not in field else field
+ field_list = COMMA_PATTERN.split(field)
+ for field in field_list:
+ if _field := field.strip():
+ _fields.append(parse_field(_field))
+ else:
+ _fields.append(parse_field(field))
+
+ return _fields
+
+ def apply_order_by(self, order_by: str | None):
+ if not order_by or order_by == DefaultOrderBy:
+ return
+ for declaration in order_by.split(","):
+ if _order_by := declaration.strip():
+ parts = _order_by.split(" ")
+ order_field, order_direction = parts[0], parts[1] if len(parts) > 1 else "desc"
+ order_direction = Order.asc if order_direction.lower() == "asc" else Order.desc
+ self.query = self.query.orderby(order_field, order=order_direction)
class Permission:
@@ -570,3 +358,178 @@ class Permission:
@staticmethod
def get_tables_from_query(query: str):
return [table for table in WORDS_PATTERN.findall(query) if table.startswith("tab")]
+
+
+class DynamicTableField:
+ def __init__(
+ self,
+ doctype: str,
+ fieldname: str,
+ parent_doctype: str,
+ alias: str | None = None,
+ ) -> None:
+ self.doctype = doctype
+ self.fieldname = fieldname
+ self.alias = alias
+ self.parent_doctype = parent_doctype
+
+ def __str__(self) -> str:
+ table_name = f"`tab{self.doctype}`"
+ fieldname = f"`{self.fieldname}`"
+ if frappe.db.db_type == "postgres":
+ table_name = table_name.replace("`", '"')
+ fieldname = fieldname.replace("`", '"')
+ alias = f"AS {self.alias}" if self.alias else ""
+ return f"{table_name}.{fieldname} {alias}".strip()
+
+ @staticmethod
+ def parse(field: str, doctype: str):
+ if "." in field:
+ alias = None
+ if " as " in field:
+ field, alias = field.split(" as ")
+ if field.startswith("`tab") or field.startswith('"tab'):
+ _, child_doctype, child_field = re.search(r'([`"])tab(.+?)\1.\1(.+)\1', field).groups()
+ if child_doctype == doctype:
+ return
+ return ChildTableField(child_doctype, child_field, doctype, alias=alias)
+ else:
+ linked_fieldname, fieldname = field.split(".")
+ linked_field = frappe.get_meta(doctype).get_field(linked_fieldname)
+ linked_doctype = linked_field.options
+ if linked_field.fieldtype == "Link":
+ return LinkTableField(linked_doctype, fieldname, doctype, linked_fieldname, alias=alias)
+ elif linked_field.fieldtype in frappe.model.table_fields:
+ return ChildTableField(linked_doctype, fieldname, doctype, alias=alias)
+
+ def apply_select(self, query: QueryBuilder) -> QueryBuilder:
+ raise NotImplementedError
+
+
+class ChildTableField(DynamicTableField):
+ def __init__(
+ self,
+ doctype: str,
+ fieldname: str,
+ parent_doctype: str,
+ alias: str | None = None,
+ ) -> None:
+ self.doctype = doctype
+ self.fieldname = fieldname
+ self.alias = alias
+ self.parent_doctype = parent_doctype
+ self.table = frappe.qb.DocType(self.doctype)
+ self.field = self.table[self.fieldname]
+
+ def apply_select(self, query: QueryBuilder) -> QueryBuilder:
+ table = frappe.qb.DocType(self.doctype)
+ query = self.apply_join(query)
+ return query.select(getattr(table, self.fieldname).as_(self.alias or None))
+
+ def apply_join(self, query: QueryBuilder) -> QueryBuilder:
+ table = frappe.qb.DocType(self.doctype)
+ main_table = frappe.qb.DocType(self.parent_doctype)
+ if not query.is_joined(table):
+ query = query.left_join(table).on(
+ (table.parent == main_table.name) & (table.parenttype == self.parent_doctype)
+ )
+ return query
+
+
+class LinkTableField(DynamicTableField):
+ def __init__(
+ self,
+ doctype: str,
+ fieldname: str,
+ parent_doctype: str,
+ link_fieldname: str,
+ alias: str | None = None,
+ ) -> None:
+ super().__init__(doctype, fieldname, parent_doctype, alias=alias)
+ self.link_fieldname = link_fieldname
+ self.table = frappe.qb.DocType(self.doctype)
+ self.field = self.table[self.fieldname]
+
+ def apply_select(self, query: QueryBuilder) -> QueryBuilder:
+ table = frappe.qb.DocType(self.doctype)
+ query = self.apply_join(query)
+ return query.select(getattr(table, self.fieldname).as_(self.alias or None))
+
+ def apply_join(self, query: QueryBuilder) -> QueryBuilder:
+ table = frappe.qb.DocType(self.doctype)
+ main_table = frappe.qb.DocType(self.parent_doctype)
+ if not query.is_joined(table):
+ query = query.left_join(table).on(table.name == getattr(main_table, self.link_fieldname))
+ return query
+
+
+class ChildQuery:
+ def __init__(
+ self,
+ fieldname: str,
+ fields: list,
+ parent_doctype: str,
+ ) -> None:
+ field = frappe.get_meta(parent_doctype).get_field(fieldname)
+ if field.fieldtype not in frappe.model.table_fields:
+ return
+ self.fieldname = fieldname
+ self.fields = fields
+ self.parent_doctype = parent_doctype
+ self.doctype = field.options
+
+ def get_query(self, parent_names=None) -> QueryBuilder:
+ filters = {
+ "parenttype": self.parent_doctype,
+ "parentfield": self.fieldname,
+ "parent": ["in", parent_names],
+ }
+ return frappe.qb.get_query(
+ self.doctype,
+ fields=self.fields + ["parent", "parentfield"],
+ filters=filters,
+ order_by="idx asc",
+ )
+
+
+def literal_eval_(literal):
+ try:
+ return literal_eval(literal)
+ except (ValueError, SyntaxError):
+ return literal
+
+
+def has_function(field):
+ _field = field.casefold() if (isinstance(field, str) and "`" not in field) else field
+ if not issubclass(type(_field), Criterion):
+ if any([f"{func}(" in _field for func in SQL_FUNCTIONS]):
+ return True
+
+
+def get_nested_set_hierarchy_result(doctype: str, name: str, hierarchy: str):
+ table = frappe.qb.DocType(doctype)
+ try:
+ lft, rgt = frappe.qb.from_(table).select("lft", "rgt").where(table.name == name).run()[0]
+ except IndexError:
+ lft, rgt = None, None
+
+ if hierarchy in ("descendants of", "not descendants of"):
+ result = (
+ frappe.qb.from_(table)
+ .select(table.name)
+ .where(table.lft > lft)
+ .where(table.rgt < rgt)
+ .orderby(table.lft, order=Order.asc)
+ .run()
+ )
+ else:
+ # Get ancestor elements of a DocType with a tree structure
+ result = (
+ frappe.qb.from_(table)
+ .select(table.name)
+ .where(table.lft < lft)
+ .where(table.rgt > rgt)
+ .orderby(table.lft, order=Order.desc)
+ .run()
+ )
+ return result
diff --git a/frappe/database/schema.py b/frappe/database/schema.py
index 5920d14c3d..7a8330595e 100644
--- a/frappe/database/schema.py
+++ b/frappe/database/schema.py
@@ -17,18 +17,18 @@ class DBTable:
self.doctype = doctype
self.table_name = f"tab{doctype}"
self.meta = meta or frappe.get_meta(doctype, False)
- self.columns = {}
+ self.columns: dict[str, DbColumn] = {}
self.current_columns = {}
# lists for change
- self.add_column = []
- self.change_type = []
- self.change_name = []
- self.add_unique = []
- self.add_index = []
- self.drop_unique = []
- self.drop_index = []
- self.set_default = []
+ self.add_column: list[DbColumn] = []
+ self.change_type: list[DbColumn] = []
+ self.change_name: list[DbColumn] = []
+ self.add_unique: list[DbColumn] = []
+ self.add_index: list[DbColumn] = []
+ self.drop_unique: list[DbColumn] = []
+ self.drop_index: list[DbColumn] = []
+ self.set_default: list[DbColumn] = []
# load
self.get_columns_from_docfields()
@@ -187,7 +187,7 @@ class DbColumn:
self.unique = unique
self.precision = precision
- def get_definition(self, with_default=1):
+ def get_definition(self, for_modification=False):
column_def = get_definition(self.fieldtype, precision=self.precision, length=self.length)
if not column_def:
@@ -209,7 +209,7 @@ class DbColumn:
):
column_def += f" default {frappe.db.escape(self.default)}"
- if self.unique and (column_def not in ("text", "longtext")):
+ if self.unique and not for_modification and (column_def not in ("text", "longtext")):
column_def += " unique"
return column_def
diff --git a/frappe/database/sequence.py b/frappe/database/sequence.py
index 6a352d20d1..54362a5895 100644
--- a/frappe/database/sequence.py
+++ b/frappe/database/sequence.py
@@ -57,12 +57,17 @@ def create_sequence(
def get_next_val(doctype_name: str, slug: str = "_id_seq") -> int:
- return db.multisql(
- {
- "postgres": f"select nextval('\"{scrub(doctype_name + slug)}\"')",
- "mariadb": f"select nextval(`{scrub(doctype_name + slug)}`)",
- }
- )[0][0]
+ sequence_name = scrub(f"{doctype_name}{slug}")
+
+ if db.db_type == "postgres":
+ sequence_name = f"'\"{sequence_name}\"'"
+ elif db.db_type == "mariadb":
+ sequence_name = f"`{sequence_name}`"
+
+ try:
+ return db.sql(f"SELECT nextval({sequence_name})")[0][0]
+ except IndexError:
+ raise db.SequenceGeneratorLimitExceeded
def set_next_val(
diff --git a/frappe/database/utils.py b/frappe/database/utils.py
new file mode 100644
index 0000000000..d1030ca6d7
--- /dev/null
+++ b/frappe/database/utils.py
@@ -0,0 +1,78 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# License: MIT. See LICENSE
+
+import typing
+from functools import cached_property
+from types import NoneType
+
+import frappe
+from frappe.query_builder.builder import MariaDB, Postgres
+from frappe.query_builder.functions import Function
+
+if typing.TYPE_CHECKING:
+ from frappe.query_builder import DocType
+
+Query = str | MariaDB | Postgres
+QueryValues = tuple | list | dict | NoneType
+
+EmptyQueryValues = object()
+FallBackDateTimeStr = "0001-01-01 00:00:00.000000"
+DefaultOrderBy = "KEEP_DEFAULT_ORDERING"
+NestedSetHierarchy = (
+ "ancestors of",
+ "descendants of",
+ "not ancestors of",
+ "not descendants of",
+)
+
+
+def is_query_type(query: str, query_type: str | tuple[str]) -> bool:
+ return query.lstrip().split(maxsplit=1)[0].lower().startswith(query_type)
+
+
+def is_pypika_function_object(field: str) -> bool:
+ return getattr(field, "__module__", None) == "pypika.functions" or isinstance(field, Function)
+
+
+def get_doctype_name(table_name: str) -> str:
+ if table_name.startswith(("tab", "`tab", '"tab')):
+ table_name = table_name.replace("tab", "", 1)
+ table_name = table_name.replace("`", "")
+ table_name = table_name.replace('"', "")
+ return table_name
+
+
+class LazyString:
+ def _setup(self) -> None:
+ raise NotImplementedError
+
+ @cached_property
+ def value(self) -> str:
+ return self._setup()
+
+ def __str__(self) -> str:
+ return self.value
+
+ def __repr__(self) -> str:
+ return f"'{self.value}'"
+
+
+class LazyDecode(LazyString):
+ __slots__ = ()
+
+ def __init__(self, value: str) -> None:
+ self._value = value
+
+ def _setup(self) -> None:
+ return self._value.decode()
+
+
+class LazyMogrify(LazyString):
+ __slots__ = ()
+
+ def __init__(self, query, values) -> None:
+ self.query = query
+ self.values = values
+
+ def _setup(self) -> str:
+ return frappe.db.mogrify(self.query, self.values)
diff --git a/frappe/defaults.py b/frappe/defaults.py
index 02076b1fda..edbf784200 100644
--- a/frappe/defaults.py
+++ b/frappe/defaults.py
@@ -6,8 +6,7 @@ from frappe.cache_manager import clear_defaults_cache, common_default_keys
from frappe.desk.notifications import clear_notifications
from frappe.query_builder import DocType
-# Note: DefaultValue records are identified by parenttype
-# __default, __global or 'User Permission'
+# Note: DefaultValue records are identified by parent (e.g. __default, __global)
def set_user_default(key, value, user=None, parenttype=None):
@@ -26,9 +25,12 @@ def get_user_default(key, user=None):
if d and isinstance(d, (list, tuple)) and len(d) == 1:
# Use User Permission value when only when it has a single value
d = d[0]
-
else:
d = user_defaults.get(frappe.scrub(key), None)
+ user_permission_default = get_user_permission_default(key, user_defaults)
+ if not d:
+ # If no default value is found, use the User Permission value
+ d = user_permission_default
value = isinstance(d, (list, tuple)) and d[0] or d
if not_in_user_permission(key, value, user):
@@ -37,6 +39,24 @@ def get_user_default(key, user=None):
return value
+def get_user_permission_default(key, defaults):
+ permissions = get_user_permissions()
+ user_default = ""
+ if permissions.get(key):
+ # global default in user permission
+ for item in permissions.get(key):
+ doc = item.get("doc")
+ if defaults.get(key) == doc:
+ user_default = doc
+
+ for item in permissions.get(key):
+ if item.get("is_default"):
+ user_default = item.get("doc")
+ break
+
+ return user_default
+
+
def get_user_default_as_list(key, user=None):
user_defaults = get_defaults(user or frappe.session.user)
d = user_defaults.get(key, None)
@@ -242,4 +262,6 @@ def get_defaults_for(parent="__default"):
def _clear_cache(parent):
+ if frappe.flags.in_install:
+ return
frappe.clear_cache(user=parent if parent not in common_default_keys else None)
diff --git a/frappe/desk/desk_page.py b/frappe/desk/desk_page.py
index ad0bd549d8..182c2f9ef7 100644
--- a/frappe/desk/desk_page.py
+++ b/frappe/desk/desk_page.py
@@ -2,7 +2,6 @@
# License: MIT. See LICENSE
import frappe
-from frappe.translate import send_translations
@frappe.whitelist()
@@ -31,28 +30,4 @@ def getpage():
page = frappe.form_dict.get("name")
doc = get(page)
- # load translations
- if frappe.lang != "en":
- send_translations(frappe.get_lang_dict("page", page))
-
frappe.response.docs.append(doc)
-
-
-def has_permission(page):
- if frappe.session.user == "Administrator" or "System Manager" in frappe.get_roles():
- return True
-
- page_roles = [d.role for d in page.get("roles")]
- if page_roles:
- if frappe.session.user == "Guest" and "Guest" not in page_roles:
- return False
- elif not set(page_roles).intersection(set(frappe.get_roles())):
- # check if roles match
- return False
-
- if not frappe.has_permission("Page", ptype="read", doc=page):
- # check if there are any user_permissions
- return False
- else:
- # hack for home pages! if no Has Roles, allow everyone to see!
- return True
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index e2be2656a9..46cda8fe5d 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -40,7 +40,6 @@ class Workspace:
self.allowed_modules = self.get_cached("user_allowed_modules", self.get_allowed_modules)
self.doc = frappe.get_cached_doc("Workspace", self.page_name)
-
if (
self.doc
and self.doc.module
@@ -154,26 +153,26 @@ class Workspace:
return True
if item_type == "dashboard":
return True
+ if item_type == "url":
+ return True
return False
def build_workspace(self):
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.quick_lists = {"items": self.get_quick_lists()}
+ self.number_cards = {"items": self.get_number_cards()}
+ self.custom_blocks = {"items": self.get_custom_blocks()}
def _doctype_contains_a_record(self, name):
exists = self.table_counts.get(name, False)
if not exists and frappe.db.exists(name):
if not frappe.db.get_value("DocType", name, "issingle"):
- exists = bool(frappe.db.get_all(name, limit=1))
+ exists = bool(frappe.get_all(name, limit=1))
else:
exists = True
self.table_counts[name] = exists
@@ -205,6 +204,24 @@ class Workspace:
return item
+ def is_custom_block_permitted(self, custom_block_name):
+ from frappe.utils import has_common
+
+ allowed = [
+ d.role
+ for d in frappe.get_all("Has Role", fields=["role"], filters={"parent": custom_block_name})
+ ]
+
+ if not allowed:
+ return True
+
+ roles = frappe.get_roles()
+
+ if has_common(roles, allowed):
+ return True
+
+ return False
+
@handle_not_exist
def get_links(self):
cards = self.doc.get_link_groups()
@@ -292,12 +309,13 @@ class Workspace:
quick_lists = self.doc.quick_lists
for item in quick_lists:
- new_item = item.as_dict().copy()
+ if self.is_item_allowed(item.document_type, "doctype"):
+ new_item = item.as_dict().copy()
- # Translate label
- new_item["label"] = _(item.label) if item.label else _(item.document_type)
+ # Translate label
+ new_item["label"] = _(item.label) if item.label else _(item.document_type)
- items.append(new_item)
+ items.append(new_item)
return items
@@ -332,6 +350,40 @@ class Workspace:
return steps
+ @handle_not_exist
+ def get_number_cards(self):
+ all_number_cards = []
+ if frappe.has_permission("Number Card", throw=False):
+ number_cards = self.doc.number_cards
+ for number_card in number_cards:
+ if frappe.has_permission("Number Card", doc=number_card.number_card_name):
+ # Translate label
+ number_card.label = (
+ _(number_card.label) if number_card.label else _(number_card.number_card_name)
+ )
+ all_number_cards.append(number_card)
+
+ return all_number_cards
+
+ @handle_not_exist
+ def get_custom_blocks(self):
+ all_custom_blocks = []
+ if frappe.has_permission("Custom HTML Block", throw=False):
+ custom_blocks = self.doc.custom_blocks
+
+ for custom_block in custom_blocks:
+ if frappe.has_permission("Custom HTML Block", doc=custom_block.custom_block_name):
+ if not self.is_custom_block_permitted(custom_block.custom_block_name):
+ continue
+
+ # Translate label
+ custom_block.label = (
+ _(custom_block.label) if custom_block.label else _(custom_block.custom_block_name)
+ )
+ all_custom_blocks.append(custom_block)
+
+ return all_custom_blocks
+
@frappe.whitelist()
@frappe.read_only()
@@ -354,6 +406,8 @@ def get_desktop_page(page):
"cards": workspace.cards,
"onboardings": workspace.onboardings,
"quick_lists": workspace.quick_lists,
+ "number_cards": workspace.number_cards,
+ "custom_blocks": workspace.custom_blocks,
}
except DoesNotExistError:
frappe.log_error("Workspace Missing")
@@ -379,7 +433,17 @@ def get_workspace_sidebar_items():
# pages sorted based on sequence id
order_by = "sequence_id asc"
- fields = ["name", "title", "for_user", "parent_page", "content", "public", "module", "icon"]
+ fields = [
+ "name",
+ "title",
+ "for_user",
+ "parent_page",
+ "content",
+ "public",
+ "module",
+ "icon",
+ "is_hidden",
+ ]
all_pages = frappe.get_all(
"Workspace", fields=fields, filters=filters, order_by=order_by, ignore_permissions=True
)
@@ -391,7 +455,7 @@ def get_workspace_sidebar_items():
try:
workspace = Workspace(page, True)
if has_access or workspace.is_permitted():
- if page.public:
+ if page.public and (has_access or not page.is_hidden):
pages.append(page)
elif page.for_user == frappe.session.user:
private_pages.append(page)
@@ -472,6 +536,14 @@ def save_new_widget(doc, page, blocks, new_widgets):
doc.shortcuts.extend(new_widget(widgets.shortcut, "Workspace Shortcut", "shortcuts"))
if widgets.quick_list:
doc.quick_lists.extend(new_widget(widgets.quick_list, "Workspace Quick List", "quick_lists"))
+ if widgets.custom_block:
+ doc.custom_blocks.extend(
+ new_widget(widgets.custom_block, "Workspace Custom Block", "custom_blocks")
+ )
+ if widgets.number_card:
+ doc.number_cards.extend(
+ new_widget(widgets.number_card, "Workspace Number Card", "number_cards")
+ )
if widgets.card:
doc.build_links_table_from_card(widgets.card)
@@ -501,12 +573,12 @@ def save_new_widget(doc, page, blocks, new_widgets):
def clean_up(original_page, blocks):
page_widgets = {}
- for wid in ["shortcut", "card", "chart", "quick_list"]:
+ for wid in ["shortcut", "card", "chart", "quick_list", "number_card", "custom_block"]:
# get list of widget's name from blocks
page_widgets[wid] = [x["data"][wid + "_name"] for x in loads(blocks) if x["type"] == wid]
- # shortcut, chart & quick_list cleanup
- for wid in ["shortcut", "chart", "quick_list"]:
+ # shortcut, chart, quick_list, number_card & custom_block cleanup
+ for wid in ["shortcut", "chart", "quick_list", "number_card", "custom_block"]:
updated_widgets = []
original_page.get(wid + "s").reverse()
@@ -588,4 +660,8 @@ def update_onboarding_step(name, field, value):
value: Value to be updated
"""
+ from frappe.utils.telemetry import capture
+
frappe.db.set_value("Onboarding Step", name, field, value)
+
+ capture(frappe.scrub(name), app="frappe_onboarding", properties={field: value})
diff --git a/frappe/desk/doctype/bulk_update/bulk_update.js b/frappe/desk/doctype/bulk_update/bulk_update.js
index bb9cf2af51..d8a2b89cf3 100644
--- a/frappe/desk/doctype/bulk_update/bulk_update.js
+++ b/frappe/desk/doctype/bulk_update/bulk_update.js
@@ -1,41 +1,36 @@
// Copyright (c) 2016, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Bulk Update', {
- refresh: function(frm) {
- frm.set_query("document_type", function() {
+frappe.ui.form.on("Bulk Update", {
+ refresh: function (frm) {
+ frm.set_query("document_type", function () {
return {
filters: [
- ['DocType', 'issingle', '=', 0],
- ['DocType', 'name', 'not in', frappe.model.core_doctypes_list]
- ]
+ ["DocType", "issingle", "=", 0],
+ ["DocType", "name", "not in", frappe.model.core_doctypes_list],
+ ],
};
});
- frm.page.set_primary_action(__('Update'), function() {
+ frm.page.set_primary_action(__("Update"), function () {
if (!frm.doc.update_value) {
frappe.throw(__('Field "value" is mandatory. Please specify value to be updated'));
} else {
- frappe.call({
- method: 'frappe.desk.doctype.bulk_update.bulk_update.update',
- args: {
- doctype: frm.doc.document_type,
- field: frm.doc.field,
- value: frm.doc.update_value,
- condition: frm.doc.condition,
- limit: frm.doc.limit
- },
- }).then(r => {
+ frm.call("bulk_update").then((r) => {
let failed = r.message;
if (!failed) failed = [];
if (failed.length && !r._server_messages) {
- frappe.throw(__('Cannot update {0}', [failed.map(f => f.bold ? f.bold(): f).join(', ')]));
+ frappe.throw(
+ __("Cannot update {0}", [
+ failed.map((f) => (f.bold ? f.bold() : f)).join(", "),
+ ])
+ );
} else {
frappe.msgprint({
- title: __('Success'),
- message: __('Updated Successfully'),
- indicator: 'green'
+ title: __("Success"),
+ message: __("Updated Successfully"),
+ indicator: "green",
});
}
@@ -46,20 +41,18 @@ frappe.ui.form.on('Bulk Update', {
});
},
- document_type: function(frm) {
+ document_type: function (frm) {
// set field options
- if(!frm.doc.document_type) return;
+ if (!frm.doc.document_type) return;
- frappe.model.with_doctype(frm.doc.document_type, function() {
- var options = $.map(frappe.get_meta(frm.doc.document_type).fields,
- function(d) {
- if(d.fieldname && frappe.model.no_value_type.indexOf(d.fieldtype)===-1) {
- return d.fieldname;
- }
- return null;
+ frappe.model.with_doctype(frm.doc.document_type, function () {
+ var options = $.map(frappe.get_meta(frm.doc.document_type).fields, function (d) {
+ if (d.fieldname && frappe.model.no_value_type.indexOf(d.fieldtype) === -1) {
+ return d.fieldname;
}
- );
- frm.set_df_property('field', 'options', options);
+ return null;
+ });
+ frm.set_df_property("field", "options", options);
});
- }
+ },
});
diff --git a/frappe/desk/doctype/bulk_update/bulk_update.json b/frappe/desk/doctype/bulk_update/bulk_update.json
index 0ec29a0dda..93458516fd 100644
--- a/frappe/desk/doctype/bulk_update/bulk_update.json
+++ b/frappe/desk/doctype/bulk_update/bulk_update.json
@@ -1,204 +1,77 @@
{
- "allow_copy": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2016-07-15 05:51:29.224123",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
+ "actions": [],
+ "creation": "2016-07-15 05:51:29.224123",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "document_type",
+ "field",
+ "update_value",
+ "condition",
+ "limit"
+ ],
"fields": [
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "document_type",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Document Type",
- "length": 0,
- "no_copy": 0,
- "options": "DocType",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "document_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Document Type",
+ "options": "DocType",
+ "reqd": 1
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "field",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Field",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "field",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Field",
+ "reqd": 1
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "update_value",
- "fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Update Value",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "update_value",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Update Value",
+ "reqd": 1
+ },
{
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "description": "SQL Conditions. Example: status=\"Open\"",
- "fieldname": "condition",
- "fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Condition",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "bold": 1,
+ "description": "SQL Conditions. Example: status=\"Open\"",
+ "fieldname": "condition",
+ "fieldtype": "Small Text",
+ "label": "Condition"
+ },
{
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "default": "500",
- "description": "Max 500 records at a time",
- "fieldname": "limit",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Limit",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "bold": 1,
+ "default": "500",
+ "description": "Max 500 records at a time",
+ "fieldname": "limit",
+ "fieldtype": "Int",
+ "label": "Limit"
}
- ],
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
-
- "is_submittable": 0,
- "issingle": 1,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2016-12-29 14:40:31.929701",
- "modified_by": "Administrator",
- "module": "Desk",
- "name": "Bulk Update",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "issingle": 1,
+ "links": [],
+ "modified": "2022-08-03 12:20:50.742376",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Bulk Update",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "is_custom": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 0,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/desk/doctype/bulk_update/bulk_update.py b/frappe/desk/doctype/bulk_update/bulk_update.py
index 1e515bbc47..535be8155f 100644
--- a/frappe/desk/doctype/bulk_update/bulk_update.py
+++ b/frappe/desk/doctype/bulk_update/bulk_update.py
@@ -3,31 +3,31 @@
import frappe
from frappe import _
+from frappe.core.doctype.submission_queue.submission_queue import queue_submission
from frappe.model.document import Document
from frappe.utils import cint
+from frappe.utils.scheduler import is_scheduler_inactive
class BulkUpdate(Document):
- pass
+ @frappe.whitelist()
+ def bulk_update(self):
+ self.check_permission("write")
+ limit = self.limit if self.limit and cint(self.limit) < 500 else 500
+ condition = ""
+ if self.condition:
+ if ";" in self.condition:
+ frappe.throw(_("; not allowed in condition"))
-@frappe.whitelist()
-def update(doctype, field, value, condition="", limit=500):
- if not limit or cint(limit) > 500:
- limit = 500
+ condition = f" where {self.condition}"
- if condition:
- condition = " where " + condition
-
- if ";" in condition:
- frappe.throw(_("; not allowed in condition"))
-
- docnames = frappe.db.sql_list(
- f"""select name from `tab{doctype}`{condition} limit {limit} offset 0"""
- )
- data = {}
- data[field] = value
- return submit_cancel_or_update_docs(doctype, docnames, "update", data)
+ docnames = frappe.db.sql_list(
+ f"""select name from `tab{self.document_type}`{condition} limit {limit} offset 0"""
+ )
+ return submit_cancel_or_update_docs(
+ self.document_type, docnames, "update", {self.field: self.update_value}
+ )
@frappe.whitelist()
@@ -44,8 +44,12 @@ def submit_cancel_or_update_docs(doctype, docnames, action="submit", data=None):
try:
message = ""
if action == "submit" and doc.docstatus.is_draft():
- doc.submit()
- message = _("Submitting {0}").format(doctype)
+ if doc.meta.queue_in_background and not is_scheduler_inactive():
+ queue_submission(doc, action)
+ message = _("Queuing {0} for Submission").format(doctype)
+ else:
+ doc.submit()
+ message = _("Submitting {0}").format(doctype)
elif action == "cancel" and doc.docstatus.is_submitted():
doc.cancel()
message = _("Cancelling {0}").format(doctype)
diff --git a/frappe/desk/doctype/calendar_view/calendar_view.js b/frappe/desk/doctype/calendar_view/calendar_view.js
index a58a9555db..c302c1a4d8 100644
--- a/frappe/desk/doctype/calendar_view/calendar_view.js
+++ b/frappe/desk/doctype/calendar_view/calendar_view.js
@@ -1,35 +1,36 @@
// Copyright (c) 2017, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Calendar View', {
- onload: function(frm) {
- frm.trigger('reference_doctype');
+frappe.ui.form.on("Calendar View", {
+ onload: function (frm) {
+ frm.trigger("reference_doctype");
},
- refresh: function(frm) {
+ refresh: function (frm) {
if (!frm.is_new()) {
- frm.add_custom_button(__('Show Calendar'),
- () => frappe.set_route('List', frm.doc.reference_doctype, 'Calendar', frm.doc.name));
+ frm.add_custom_button(__("Show Calendar"), () =>
+ frappe.set_route("List", frm.doc.reference_doctype, "Calendar", frm.doc.name)
+ );
}
},
- reference_doctype: function(frm) {
+ reference_doctype: function (frm) {
const { reference_doctype } = frm.doc;
if (!reference_doctype) return;
frappe.model.with_doctype(reference_doctype, () => {
const meta = frappe.get_meta(reference_doctype);
- const subject_options = meta.fields.filter(
- df => !frappe.model.no_value_type.includes(df.fieldtype)
- ).map(df => df.fieldname);
+ const subject_options = meta.fields
+ .filter((df) => !frappe.model.no_value_type.includes(df.fieldtype))
+ .map((df) => df.fieldname);
- const date_options = meta.fields.filter(
- df => ['Date', 'Datetime'].includes(df.fieldtype)
- ).map(df => df.fieldname);
+ const date_options = meta.fields
+ .filter((df) => ["Date", "Datetime"].includes(df.fieldtype))
+ .map((df) => df.fieldname);
- frm.set_df_property('subject_field', 'options', subject_options);
- frm.set_df_property('start_date_field', 'options', date_options);
- frm.set_df_property('end_date_field', 'options', date_options);
+ frm.set_df_property("subject_field", "options", subject_options);
+ frm.set_df_property("start_date_field", "options", date_options);
+ frm.set_df_property("end_date_field", "options", date_options);
frm.refresh();
});
- }
+ },
});
diff --git a/frappe/desk/doctype/calendar_view/calendar_view_list.js b/frappe/desk/doctype/calendar_view/calendar_view_list.js
new file mode 100644
index 0000000000..aa55a8ebbb
--- /dev/null
+++ b/frappe/desk/doctype/calendar_view/calendar_view_list.js
@@ -0,0 +1,16 @@
+frappe.listview_settings["Calendar View"] = {
+ button: {
+ show(doc) {
+ return doc.name;
+ },
+ get_label() {
+ return frappe.utils.icon("calendar", "sm");
+ },
+ get_description(doc) {
+ return __("View {0}", [`${doc.name}`]);
+ },
+ action(doc) {
+ frappe.set_route("List", doc.reference_doctype, "Calendar", doc.name);
+ },
+ },
+};
diff --git a/frappe/desk/doctype/console_log/console_log.js b/frappe/desk/doctype/console_log/console_log.js
index 1ef4fdce59..9a980667ac 100644
--- a/frappe/desk/doctype/console_log/console_log.js
+++ b/frappe/desk/doctype/console_log/console_log.js
@@ -1,8 +1,7 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Console Log', {
+frappe.ui.form.on("Console Log", {
// refresh: function(frm) {
-
// }
});
diff --git a/frappe/desk/doctype/console_log/test_console_log.py b/frappe/desk/doctype/console_log/test_console_log.py
index 0579098382..9ae8d1f1e8 100644
--- a/frappe/desk/doctype/console_log/test_console_log.py
+++ b/frappe/desk/doctype/console_log/test_console_log.py
@@ -1,8 +1,8 @@
# Copyright (c) 2020, Frappe Technologies and Contributors
# License: MIT. See LICENSE
# import frappe
-import unittest
+from frappe.tests.utils import FrappeTestCase
-class TestConsoleLog(unittest.TestCase):
+class TestConsoleLog(FrappeTestCase):
pass
diff --git a/frappe/event_streaming/__init__.py b/frappe/desk/doctype/custom_html_block/__init__.py
similarity index 100%
rename from frappe/event_streaming/__init__.py
rename to frappe/desk/doctype/custom_html_block/__init__.py
diff --git a/frappe/desk/doctype/custom_html_block/custom_html_block.js b/frappe/desk/doctype/custom_html_block/custom_html_block.js
new file mode 100644
index 0000000000..727a73c92f
--- /dev/null
+++ b/frappe/desk/doctype/custom_html_block/custom_html_block.js
@@ -0,0 +1,49 @@
+// Copyright (c) 2023, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Custom HTML Block", {
+ refresh(frm) {
+ render_custom_html_block(frm);
+ },
+});
+
+function render_custom_html_block(frm) {
+ let wrapper = frm.fields_dict["preview"].wrapper;
+ wrapper.classList.add("mb-3");
+
+ let random_id = "custom-block-" + frappe.utils.get_random(5).toLowerCase();
+
+ class CustomBlock extends HTMLElement {
+ constructor() {
+ super();
+
+ // html
+ let div = document.createElement("div");
+ div.innerHTML = frappe.dom.remove_script_and_style(frm.doc.html);
+
+ // css
+ let style = document.createElement("style");
+ style.textContent = frm.doc.style;
+
+ // javascript
+ let script = document.createElement("script");
+ script.textContent = `
+ (function() {
+ let cname = ${JSON.stringify(random_id)};
+ let root_element = document.querySelector(cname).shadowRoot;
+ ${frm.doc.script}
+ })();
+ `;
+
+ this.attachShadow({ mode: "open" });
+ this.shadowRoot?.appendChild(div);
+ this.shadowRoot?.appendChild(style);
+ this.shadowRoot?.appendChild(script);
+ }
+ }
+
+ if (!customElements.get(random_id)) {
+ customElements.define(random_id, CustomBlock);
+ }
+ wrapper.innerHTML = `<${random_id}>${random_id}>`;
+}
diff --git a/frappe/desk/doctype/custom_html_block/custom_html_block.json b/frappe/desk/doctype/custom_html_block/custom_html_block.json
new file mode 100644
index 0000000000..6c3d80fba9
--- /dev/null
+++ b/frappe/desk/doctype/custom_html_block/custom_html_block.json
@@ -0,0 +1,152 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "prompt",
+ "creation": "2023-05-17 13:58:37.311045",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "preview_section",
+ "preview",
+ "html_section",
+ "html_message",
+ "html",
+ "javascript_section",
+ "js_message",
+ "script",
+ "css_section",
+ "style",
+ "roles_section",
+ "roles"
+ ],
+ "fields": [
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "eval:true;",
+ "fieldname": "html_section",
+ "fieldtype": "Section Break",
+ "label": "HTML"
+ },
+ {
+ "fieldname": "html",
+ "fieldtype": "Code",
+ "options": "HTML"
+ },
+ {
+ "fieldname": "preview_section",
+ "fieldtype": "Section Break",
+ "label": "Preview"
+ },
+ {
+ "fieldname": "preview",
+ "fieldtype": "HTML"
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "eval:true;",
+ "fieldname": "javascript_section",
+ "fieldtype": "Section Break",
+ "label": "Javascript"
+ },
+ {
+ "fieldname": "script",
+ "fieldtype": "Code",
+ "options": "JS"
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "eval:true;",
+ "fieldname": "css_section",
+ "fieldtype": "Section Break",
+ "label": "CSS"
+ },
+ {
+ "fieldname": "style",
+ "fieldtype": "Code",
+ "options": "CSS"
+ },
+ {
+ "fieldname": "js_message",
+ "fieldtype": "HTML",
+ "label": "JS Message",
+ "options": "To interact with above HTML you will have to use `root_element` as a parent selector.
For example:
// here root_element is provided by default\nlet some_class_element = root_element.querySelector('.some-class');\nsome_class_element.textContent = \"New content\";\n "
+ },
+ {
+ "fieldname": "html_message",
+ "fieldtype": "HTML",
+ "label": "HTML Message",
+ "options": "You cannot use global class on elements. The css for those classes will not be applied on this HTML, you will have to rewrite styles again in CSS field
For Example:
\n// style for class m-3 will not work\n <div class=\"m-3\"></div> \n // You will have to add style of m-3 in CSS field below like\n .m-3 {\n margin: 14px!important\n }\n "
+ },
+ {
+ "fieldname": "roles_section",
+ "fieldtype": "Section Break",
+ "label": "Roles"
+ },
+ {
+ "fieldname": "roles",
+ "fieldtype": "Table",
+ "label": "Roles",
+ "options": "Has Role"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2023-05-17 17:17:04.232519",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Custom HTML Block",
+ "naming_rule": "Set by user",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "All",
+ "share": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "if_owner": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "All",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Workspace Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/custom_html_block/custom_html_block.py b/frappe/desk/doctype/custom_html_block/custom_html_block.py
new file mode 100644
index 0000000000..7f85c2db5f
--- /dev/null
+++ b/frappe/desk/doctype/custom_html_block/custom_html_block.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class CustomHTMLBlock(Document):
+ pass
diff --git a/frappe/desk/doctype/custom_html_block/test_custom_html_block.py b/frappe/desk/doctype/custom_html_block/test_custom_html_block.py
new file mode 100644
index 0000000000..fbef9af45b
--- /dev/null
+++ b/frappe/desk/doctype/custom_html_block/test_custom_html_block.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestCustomHTMLBlock(FrappeTestCase):
+ pass
diff --git a/frappe/desk/doctype/dashboard/dashboard.js b/frappe/desk/doctype/dashboard/dashboard.js
index c640259cf2..9f584ca552 100644
--- a/frappe/desk/doctype/dashboard/dashboard.js
+++ b/frappe/desk/doctype/dashboard/dashboard.js
@@ -1,30 +1,30 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Dashboard', {
- refresh: function(frm) {
- frm.add_custom_button(__("Show Dashboard"),
- () => frappe.set_route('dashboard-view', frm.doc.name)
+frappe.ui.form.on("Dashboard", {
+ refresh: function (frm) {
+ frm.add_custom_button(__("Show Dashboard"), () =>
+ frappe.set_route("dashboard-view", frm.doc.name)
);
if (!frappe.boot.developer_mode && frm.doc.is_standard) {
frm.disable_form();
}
- frm.set_query("chart", "charts", function() {
+ frm.set_query("chart", "charts", function () {
return {
filters: {
is_public: 1,
- }
+ },
};
});
- frm.set_query("card", "cards", function() {
+ frm.set_query("card", "cards", function () {
return {
filters: {
is_public: 1,
- }
+ },
};
});
- }
+ },
});
diff --git a/frappe/desk/doctype/dashboard/dashboard_list.js b/frappe/desk/doctype/dashboard/dashboard_list.js
index d60a324048..80ebbd4355 100644
--- a/frappe/desk/doctype/dashboard/dashboard_list.js
+++ b/frappe/desk/doctype/dashboard/dashboard_list.js
@@ -1,4 +1,4 @@
-frappe.listview_settings['Dashboard'] = {
+frappe.listview_settings["Dashboard"] = {
button: {
show(doc) {
return doc.name;
@@ -7,10 +7,10 @@ frappe.listview_settings['Dashboard'] = {
return frappe.utils.icon("dashboard-list", "sm");
},
get_description(doc) {
- return __('View {0}', [`${doc.name}`]);
+ return __("View {0}", [`${doc.name}`]);
},
action(doc) {
- frappe.set_route('dashboard-view', doc.name);
- }
+ frappe.set_route("dashboard-view", doc.name);
+ },
},
-};
\ No newline at end of file
+};
diff --git a/frappe/desk/doctype/dashboard/test_dashboard.py b/frappe/desk/doctype/dashboard/test_dashboard.py
index d2ba871509..99aeecaee6 100644
--- a/frappe/desk/doctype/dashboard/test_dashboard.py
+++ b/frappe/desk/doctype/dashboard/test_dashboard.py
@@ -1,7 +1,7 @@
# Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE
-import unittest
+from frappe.tests.utils import FrappeTestCase
-class TestDashboard(unittest.TestCase):
+class TestDashboard(FrappeTestCase):
pass
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
index 0b93786e8e..6d23be79d7 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
@@ -1,42 +1,44 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.provide('frappe.dashboards.chart_sources');
+frappe.provide("frappe.dashboards.chart_sources");
-frappe.ui.form.on('Dashboard Chart', {
- setup: function(frm) {
+frappe.ui.form.on("Dashboard Chart", {
+ setup: function (frm) {
// fetch timeseries from source
- frm.add_fetch('source', 'timeseries', 'timeseries');
+ frm.add_fetch("source", "timeseries", "timeseries");
},
- before_save: function(frm) {
- let dynamic_filters = JSON.parse(frm.doc.dynamic_filters_json || 'null');
- let static_filters = JSON.parse(frm.doc.filters_json || 'null');
- static_filters =
- frappe.dashboard_utils.remove_common_static_filter_values(static_filters, dynamic_filters);
+ before_save: function (frm) {
+ let dynamic_filters = JSON.parse(frm.doc.dynamic_filters_json || "null");
+ let static_filters = JSON.parse(frm.doc.filters_json || "null");
+ static_filters = frappe.dashboard_utils.remove_common_static_filter_values(
+ static_filters,
+ dynamic_filters
+ );
- frm.set_value('filters_json', JSON.stringify(static_filters));
- frm.trigger('show_filters');
+ frm.set_value("filters_json", JSON.stringify(static_filters));
+ frm.trigger("show_filters");
},
- refresh: function(frm) {
+ refresh: function (frm) {
frm.chart_filters = null;
frm.is_disabled = !frappe.boot.developer_mode && frm.doc.is_standard;
if (frm.is_disabled) {
- !frm.doc.custom_options && frm.set_df_property('chart_options_section', 'hidden', 1);
+ !frm.doc.custom_options && frm.set_df_property("chart_options_section", "hidden", 1);
frm.disable_form();
}
- frm.add_custom_button('Add Chart to Dashboard', () => {
+ frm.add_custom_button("Add Chart to Dashboard", () => {
const dialog = frappe.dashboard_utils.get_add_to_dashboard_dialog(
frm.doc.name,
- 'Dashboard Chart',
- 'frappe.desk.doctype.dashboard_chart.dashboard_chart.add_chart_to_dashboard'
+ "Dashboard Chart",
+ "frappe.desk.doctype.dashboard_chart.dashboard_chart.add_chart_to_dashboard"
);
if (!frm.doc.chart_name) {
- frappe.msgprint(__('Please create chart first'));
+ frappe.msgprint(__("Please create chart first"));
} else {
dialog.show();
}
@@ -45,199 +47,227 @@ frappe.ui.form.on('Dashboard Chart', {
frm.set_df_property("filters_section", "hidden", 1);
frm.set_df_property("dynamic_filters_section", "hidden", 1);
- frm.trigger('set_parent_document_type');
- frm.trigger('set_time_series');
- frm.set_query('document_type', function() {
+ frm.trigger("set_parent_document_type");
+ frm.trigger("set_time_series");
+ frm.set_query("document_type", function () {
return {
filters: {
- 'issingle': false
- }
- }
+ issingle: false,
+ },
+ };
});
- frm.trigger('update_options');
- frm.trigger('set_heatmap_year_options');
+ frm.trigger("update_options");
+ frm.trigger("set_heatmap_year_options");
if (frm.doc.report_name) {
- frm.trigger('set_chart_report_filters');
+ frm.trigger("set_chart_report_filters");
}
},
- is_standard: function(frm) {
+ is_standard: function (frm) {
if (frappe.boot.developer_mode && frm.doc.is_standard) {
- frm.trigger('render_dynamic_filters_table');
+ frm.trigger("render_dynamic_filters_table");
} else {
frm.set_df_property("dynamic_filters_section", "hidden", 1);
}
},
- source: function(frm) {
+ source: function (frm) {
frm.trigger("show_filters");
},
- set_heatmap_year_options: function(frm) {
- if (frm.doc.type == 'Heatmap') {
- frappe.db.get_doc('System Settings').then(doc => {
+ set_heatmap_year_options: function (frm) {
+ if (frm.doc.type == "Heatmap") {
+ frappe.db.get_doc("System Settings").then((doc) => {
const creation_date = doc.creation;
- frm.set_df_property('heatmap_year', 'options', frappe.dashboard_utils.get_years_since_creation(creation_date));
+ frm.set_df_property(
+ "heatmap_year",
+ "options",
+ frappe.dashboard_utils.get_years_since_creation(creation_date)
+ );
});
}
},
- chart_type: function(frm) {
- frm.trigger('set_time_series');
- if (frm.doc.chart_type == 'Report') {
- frm.set_query('report_name', () => {
+ chart_type: function (frm) {
+ frm.trigger("set_time_series");
+ if (frm.doc.chart_type == "Report") {
+ frm.set_query("report_name", () => {
return {
filters: {
- 'report_type': ['!=', 'Report Builder']
- }
- }
+ report_type: ["!=", "Report Builder"],
+ },
+ };
});
} else {
- frm.set_value('document_type', '');
+ frm.set_value("document_type", "");
}
},
- set_time_series: function(frm) {
+ set_time_series: function (frm) {
// set timeseries based on chart type
- if (['Count', 'Average', 'Sum'].includes(frm.doc.chart_type)) {
- frm.set_value('timeseries', 1);
+ if (["Count", "Average", "Sum"].includes(frm.doc.chart_type)) {
+ frm.set_value("timeseries", 1);
} else {
- frm.set_value('timeseries', 0);
+ frm.set_value("timeseries", 0);
}
},
- document_type: function(frm) {
+ document_type: function (frm) {
// update `based_on` options based on date / datetime fields
- frm.set_value('source', '');
- frm.set_value('based_on', '');
- frm.set_value('value_based_on', '');
- frm.set_value('parent_document_type', '');
- frm.set_value('filters_json', '[]');
- frm.set_value('dynamic_filters_json', '[]');
- frm.trigger('update_options');
- frm.trigger('set_parent_document_type');
+ frm.set_value("source", "");
+ frm.set_value("based_on", "");
+ frm.set_value("value_based_on", "");
+ frm.set_value("parent_document_type", "");
+ frm.set_value("filters_json", "[]");
+ frm.set_value("dynamic_filters_json", "[]");
+ frm.trigger("update_options");
+ frm.trigger("set_parent_document_type");
},
- report_name: function(frm) {
- frm.set_value('x_field', '');
- frm.set_value('y_axis', []);
- frm.set_df_property('x_field', 'options', []);
- frm.set_value('filters_json', '{}');
- frm.set_value('dynamic_filters_json', '{}');
- frm.set_value('use_report_chart', 0);
- frm.trigger('set_chart_report_filters');
+ report_name: function (frm) {
+ frm.set_value("x_field", "");
+ frm.set_value("y_axis", []);
+ frm.set_df_property("x_field", "options", []);
+ frm.set_value("filters_json", "{}");
+ frm.set_value("dynamic_filters_json", "{}");
+ frm.set_value("use_report_chart", 0);
+ frm.trigger("set_chart_report_filters");
},
- set_chart_report_filters: function(frm) {
+ set_chart_report_filters: function (frm) {
let report_name = frm.doc.report_name;
if (report_name) {
if (frm.doc.filters_json.length > 2) {
- frm.trigger('show_filters');
- frm.trigger('set_chart_field_options');
+ frm.trigger("show_filters");
+ frm.trigger("set_chart_field_options");
} else {
- frappe.report_utils.get_report_filters(report_name).then(filters => {
+ frappe.report_utils.get_report_filters(report_name).then((filters) => {
if (filters) {
frm.chart_filters = filters;
let filter_values = frappe.report_utils.get_filter_values(filters);
- frm.set_value('filters_json', JSON.stringify(filter_values));
+ frm.set_value("filters_json", JSON.stringify(filter_values));
}
- frm.trigger('show_filters');
- frm.trigger('set_chart_field_options');
+ frm.trigger("show_filters");
+ frm.trigger("set_chart_field_options");
});
}
-
}
},
- use_report_chart: function(frm) {
- !frm.doc.use_report_chart && frm.trigger('set_chart_field_options');
+ use_report_chart: function (frm) {
+ !frm.doc.use_report_chart && frm.trigger("set_chart_field_options");
},
- set_chart_field_options: function(frm) {
+ set_chart_field_options: function (frm) {
let filters = frm.doc.filters_json.length > 2 ? JSON.parse(frm.doc.filters_json) : null;
if (frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2) {
filters = frappe.dashboard_utils.get_all_filters(frm.doc);
}
- frappe.xcall(
- 'frappe.desk.query_report.run',
- {
+ frappe
+ .xcall("frappe.desk.query_report.run", {
report_name: frm.doc.report_name,
filters: filters,
- ignore_prepared_report: 1
- }
- ).then(data => {
- frm.report_data = data;
- let report_has_chart = Boolean(data.chart);
+ ignore_prepared_report: 1,
+ })
+ .then((data) => {
+ frm.report_data = data;
+ let report_has_chart = Boolean(data.chart);
- frm.set_df_property('use_report_chart', 'hidden', !report_has_chart);
+ frm.set_df_property("use_report_chart", "hidden", !report_has_chart);
- if (!frm.doc.use_report_chart) {
- if (data.result.length) {
- frm.field_options = frappe.report_utils.get_field_options_from_report(data.columns, data);
- frm.set_df_property('x_field', 'options', frm.field_options.non_numeric_fields);
- if (!frm.field_options.numeric_fields.length) {
- frappe.msgprint(__("Report has no numeric fields, please change the Report Name"));
+ if (!frm.doc.use_report_chart) {
+ if (data.result.length) {
+ frm.field_options = frappe.report_utils.get_field_options_from_report(
+ data.columns,
+ data
+ );
+ frm.set_df_property(
+ "x_field",
+ "options",
+ frm.field_options.non_numeric_fields
+ );
+ if (!frm.field_options.numeric_fields.length) {
+ frappe.msgprint(
+ __("Report has no numeric fields, please change the Report Name")
+ );
+ } else {
+ let y_field_df = frappe.meta.get_docfield(
+ "Dashboard Chart Field",
+ "y_field",
+ frm.doc.name
+ );
+ y_field_df.options = frm.field_options.numeric_fields;
+ }
} else {
- let y_field_df = frappe.meta.get_docfield('Dashboard Chart Field', 'y_field', frm.doc.name);
- y_field_df.options = frm.field_options.numeric_fields;
+ frappe.msgprint(
+ __(
+ "Report has no data, please modify the filters or change the Report Name"
+ )
+ );
}
} else {
- frappe.msgprint(__('Report has no data, please modify the filters or change the Report Name'));
+ frm.set_value("use_report_chart", 1);
+ frm.set_df_property("use_report_chart", "hidden", false);
}
- } else {
- frm.set_value('use_report_chart', 1);
- frm.set_df_property('use_report_chart', 'hidden', false);
- }
- });
+ });
},
- timespan: function(frm) {
+ timespan: function (frm) {
const time_interval_options = {
"Select Date Range": ["Quarterly", "Monthly", "Weekly", "Daily"],
"All Time": ["Yearly", "Monthly"],
"Last Year": ["Quarterly", "Monthly", "Weekly", "Daily"],
"Last Quarter": ["Monthly", "Weekly", "Daily"],
"Last Month": ["Weekly", "Daily"],
- "Last Week": ["Daily"]
+ "Last Week": ["Daily"],
};
if (frm.doc.timespan) {
- frm.set_df_property('time_interval', 'options', time_interval_options[frm.doc.timespan]);
+ frm.set_df_property(
+ "time_interval",
+ "options",
+ time_interval_options[frm.doc.timespan]
+ );
}
},
- update_options: function(frm) {
+ update_options: function (frm) {
let doctype = frm.doc.document_type;
let date_fields = [
- {label: __('Created On'), value: 'creation'},
- {label: __('Last Modified On'), value: 'modified'}
+ { label: __("Created On"), value: "creation" },
+ { label: __("Last Modified On"), value: "modified" },
];
let value_fields = [];
- let group_by_fields = [{label: 'Created By', value: 'owner'}];
+ let group_by_fields = [{ label: "Created By", value: "owner" }];
let aggregate_function_fields = [];
- let update_form = function() {
+ let update_form = function () {
// update select options
- frm.set_df_property('based_on', 'options', date_fields);
- frm.set_df_property('value_based_on', 'options', value_fields);
- frm.set_df_property('group_by_based_on', 'options', group_by_fields);
- frm.set_df_property('aggregate_function_based_on', 'options', aggregate_function_fields);
+ frm.set_df_property("based_on", "options", date_fields);
+ frm.set_df_property("value_based_on", "options", value_fields);
+ frm.set_df_property("group_by_based_on", "options", group_by_fields);
+ frm.set_df_property(
+ "aggregate_function_based_on",
+ "options",
+ aggregate_function_fields
+ );
frm.trigger("show_filters");
- }
-
+ };
if (doctype) {
frappe.model.with_doctype(doctype, () => {
// get all date and datetime fields
- frappe.get_meta(doctype).fields.map(df => {
- if (['Date', 'Datetime'].includes(df.fieldtype)) {
- date_fields.push({label: df.label, value: df.fieldname});
+ frappe.get_meta(doctype).fields.map((df) => {
+ if (["Date", "Datetime"].includes(df.fieldtype)) {
+ date_fields.push({ label: df.label, value: df.fieldname });
}
- if (['Int', 'Float', 'Currency', 'Percent', 'Duration'].includes(df.fieldtype)) {
- value_fields.push({label: df.label, value: df.fieldname});
- aggregate_function_fields.push({label: df.label, value: df.fieldname});
+ if (
+ ["Int", "Float", "Currency", "Percent", "Duration"].includes(df.fieldtype)
+ ) {
+ value_fields.push({ label: df.label, value: df.fieldname });
+ aggregate_function_fields.push({ label: df.label, value: df.fieldname });
}
- if (['Link', 'Select'].includes(df.fieldtype)) {
- group_by_fields.push({label: df.label, value: df.fieldname});
+ if (["Link", "Select"].includes(df.fieldtype)) {
+ group_by_fields.push({ label: df.label, value: df.fieldname });
}
});
update_form();
@@ -246,92 +276,89 @@ frappe.ui.form.on('Dashboard Chart', {
// update select options
update_form();
}
-
},
- show_filters: function(frm) {
+ show_filters: function (frm) {
frm.chart_filters = [];
- frappe.dashboard_utils.get_filters_for_chart_type(frm.doc).then(filters => {
+ frappe.dashboard_utils.get_filters_for_chart_type(frm.doc).then((filters) => {
if (filters) {
frm.chart_filters = filters;
}
- frm.trigger('render_filters_table');
+ frm.trigger("render_filters_table");
if (frappe.boot.developer_mode && frm.doc.is_standard) {
- frm.trigger('render_dynamic_filters_table');
+ frm.trigger("render_dynamic_filters_table");
}
});
},
- render_filters_table: function(frm) {
+ render_filters_table: function (frm) {
frm.set_df_property("filters_section", "hidden", 0);
- let is_document_type = frm.doc.chart_type!== 'Report' && frm.doc.chart_type!=='Custom';
- let is_dynamic_filter = f => ['Date', 'DateRange'].includes(f.fieldtype) && f.default;
+ let is_document_type = frm.doc.chart_type !== "Report" && frm.doc.chart_type !== "Custom";
+ let is_dynamic_filter = (f) => ["Date", "DateRange"].includes(f.fieldtype) && f.default;
- let wrapper = $(frm.get_field('filters_json').wrapper).empty();
+ let wrapper = $(frm.get_field("filters_json").wrapper).empty();
let table = $(`
- ${__('Filter')}
- ${__('Condition')}
- ${__('Value')}
+ ${__("Filter")}
+ ${__("Condition")}
+ ${__("Value")}
`).appendTo(wrapper);
$(`${__("Click table to edit")}
`).appendTo(wrapper);
- let filters = JSON.parse(frm.doc.filters_json || '[]');
+ let filters = JSON.parse(frm.doc.filters_json || "[]");
var filters_set = false;
// Set dynamic filters for reports
- if (frm.doc.chart_type == 'Report') {
+ if (frm.doc.chart_type == "Report") {
let set_filters = false;
- frm.chart_filters.forEach(f => {
+ frm.chart_filters.forEach((f) => {
if (is_dynamic_filter(f)) {
filters[f.fieldname] = f.default;
set_filters = true;
}
});
- set_filters && frm.set_value('filters_json', JSON.stringify(filters));
+ set_filters && frm.set_value("filters_json", JSON.stringify(filters));
}
let fields = [];
if (is_document_type) {
fields = [
{
- fieldtype: 'HTML',
- fieldname: 'filter_area',
- }
+ fieldtype: "HTML",
+ fieldname: "filter_area",
+ },
];
if (filters.length > 0) {
- filters.forEach( filter => {
- const filter_row =
- $(`
+ filters.forEach((filter) => {
+ const filter_row = $(`
${filter[1]}
${filter[2] || ""}
${filter[3]}
`);
- table.find('tbody').append(filter_row);
+ table.find("tbody").append(filter_row);
filters_set = true;
});
}
} else if (frm.chart_filters.length) {
- fields = frm.chart_filters.filter(f => f.fieldname);
+ fields = frm.chart_filters.filter((f) => f.fieldname);
- fields.map(f => {
+ fields.map((f) => {
if (filters[f.fieldname]) {
- let condition = '=';
- const filter_row =
- $(`
+ let condition = "=";
+ const filter_row = $(`
${f.label}
${condition}
${filters[f.fieldname] || ""}
`);
- table.find('tbody').append(filter_row);
+ table.find("tbody").append(filter_row);
filters_set = true;
}
});
@@ -340,39 +367,39 @@ frappe.ui.form.on('Dashboard Chart', {
if (!filters_set) {
const filter_row = $(`
${__("Click to Set Filters")} `);
- table.find('tbody').append(filter_row);
+ table.find("tbody").append(filter_row);
}
- table.on('click', () => {
- frm.is_disabled && frappe.throw(__('Cannot edit filters for standard charts'));
+ table.on("click", () => {
+ frm.is_disabled && frappe.throw(__("Cannot edit filters for standard charts"));
let dialog = new frappe.ui.Dialog({
- title: __('Set Filters'),
- fields: fields.filter(f => !is_dynamic_filter(f)),
- primary_action: function() {
+ title: __("Set Filters"),
+ fields: fields.filter((f) => !is_dynamic_filter(f)),
+ primary_action: function () {
let values = this.get_values();
if (values) {
this.hide();
if (is_document_type) {
let filters = frm.filter_group.get_filters();
- frm.set_value('filters_json', JSON.stringify(filters));
+ frm.set_value("filters_json", JSON.stringify(filters));
} else {
- frm.set_value('filters_json', JSON.stringify(values));
+ frm.set_value("filters_json", JSON.stringify(values));
}
- frm.trigger('show_filters');
- if (frm.doc.chart_type == 'Report') {
- frm.trigger('set_chart_report_filters');
+ frm.trigger("show_filters");
+ if (frm.doc.chart_type == "Report") {
+ frm.trigger("set_chart_report_filters");
}
}
},
- primary_action_label: "Set"
+ primary_action_label: "Set",
});
frappe.dashboards.filters_dialog = dialog;
if (is_document_type) {
frm.filter_group = new frappe.ui.FilterGroup({
- parent: dialog.get_field('filter_area').$wrapper,
+ parent: dialog.get_field("filter_area").$wrapper,
doctype: frm.doc.document_type,
parent_doctype: frm.doc.parent_document_type,
on_change: () => {},
@@ -383,12 +410,14 @@ frappe.ui.form.on('Dashboard Chart', {
dialog.show();
- if (frm.doc.chart_type == 'Report') {
+ if (frm.doc.chart_type == "Report") {
//Set query report object so that it can be used while fetching filter values in the report
- frappe.query_report = new frappe.views.QueryReport({'filters': dialog.fields_list});
- frappe.query_reports[frm.doc.report_name]
- && frappe.query_reports[frm.doc.report_name].onload
- && frappe.query_reports[frm.doc.report_name].onload(frappe.query_report);
+ frappe.query_report = new frappe.views.QueryReport({
+ filters: dialog.fields_list,
+ });
+ frappe.query_reports[frm.doc.report_name] &&
+ frappe.query_reports[frm.doc.report_name].onload &&
+ frappe.query_reports[frm.doc.report_name].onload(frappe.query_report);
}
dialog.set_values(filters);
@@ -398,37 +427,40 @@ frappe.ui.form.on('Dashboard Chart', {
render_dynamic_filters_table(frm) {
frm.set_df_property("dynamic_filters_section", "hidden", 0);
- let is_document_type = frm.doc.chart_type !== 'Report'
- && frm.doc.chart_type !== 'Custom';
+ let is_document_type = frm.doc.chart_type !== "Report" && frm.doc.chart_type !== "Custom";
- let wrapper = $(frm.get_field('dynamic_filters_json').wrapper).empty();
+ let wrapper = $(frm.get_field("dynamic_filters_json").wrapper).empty();
- frm.dynamic_filter_table = $(`
+ frm.dynamic_filter_table =
+ $(`
- ${__('Filter')}
- ${__('Condition')}
- ${__('Value')}
+ ${__("Filter")}
+ ${__("Condition")}
+ ${__("Value")}
`).appendTo(wrapper);
- frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
- ? JSON.parse(frm.doc.dynamic_filters_json)
- : null;
+ frm.dynamic_filters =
+ frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
+ ? JSON.parse(frm.doc.dynamic_filters_json)
+ : null;
- frm.trigger('set_dynamic_filters_in_table');
+ frm.trigger("set_dynamic_filters_in_table");
- let filters = JSON.parse(frm.doc.filters_json || '[]');
+ let filters = JSON.parse(frm.doc.filters_json || "[]");
let fields = frappe.dashboard_utils.get_fields_for_dynamic_filter_dialog(
- is_document_type, filters, frm.dynamic_filters
+ is_document_type,
+ filters,
+ frm.dynamic_filters
);
- frm.dynamic_filter_table.on('click', () => {
+ frm.dynamic_filter_table.on("click", () => {
let dialog = new frappe.ui.Dialog({
- title: __('Set Dynamic Filters'),
+ title: __("Set Dynamic Filters"),
fields: fields,
primary_action: () => {
let values = dialog.get_values();
@@ -436,19 +468,19 @@ frappe.ui.form.on('Dashboard Chart', {
let dynamic_filters = [];
for (let key of Object.keys(values)) {
if (is_document_type) {
- let [doctype, fieldname] = key.split(':');
- dynamic_filters.push([doctype, fieldname, '=', values[key]]);
+ let [doctype, fieldname] = key.split(":");
+ dynamic_filters.push([doctype, fieldname, "=", values[key]]);
}
}
if (is_document_type) {
- frm.set_value('dynamic_filters_json', JSON.stringify(dynamic_filters));
+ frm.set_value("dynamic_filters_json", JSON.stringify(dynamic_filters));
} else {
- frm.set_value('dynamic_filters_json', JSON.stringify(values));
+ frm.set_value("dynamic_filters_json", JSON.stringify(values));
}
- frm.trigger('set_dynamic_filters_in_table');
+ frm.trigger("set_dynamic_filters_in_table");
},
- primary_action_label: "Set"
+ primary_action_label: "Set",
});
dialog.show();
@@ -456,71 +488,66 @@ frappe.ui.form.on('Dashboard Chart', {
});
},
- set_dynamic_filters_in_table: function(frm) {
- frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
- ? JSON.parse(frm.doc.dynamic_filters_json)
- : null;
+ set_dynamic_filters_in_table: function (frm) {
+ frm.dynamic_filters =
+ frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
+ ? JSON.parse(frm.doc.dynamic_filters_json)
+ : null;
if (!frm.dynamic_filters) {
const filter_row = $(`
${__("Click to Set Dynamic Filters")} `);
- frm.dynamic_filter_table.find('tbody').html(filter_row);
+ frm.dynamic_filter_table.find("tbody").html(filter_row);
} else {
- let filter_rows = '';
+ let filter_rows = "";
if ($.isArray(frm.dynamic_filters)) {
- frm.dynamic_filters.forEach(filter => {
- filter_rows +=
- `
+ frm.dynamic_filters.forEach((filter) => {
+ filter_rows += `
${filter[1]}
${filter[2] || ""}
${filter[3]}
`;
});
} else {
- let condition = '=';
+ let condition = "=";
for (let [key, val] of Object.entries(frm.dynamic_filters)) {
- filter_rows +=
- `
+ filter_rows += `
${key}
${condition}
${val || ""}
- `
- ;
+ `;
}
}
- frm.dynamic_filter_table.find('tbody').html(filter_rows);
+ frm.dynamic_filter_table.find("tbody").html(filter_rows);
}
},
- set_parent_document_type: async function(frm) {
+ set_parent_document_type: async function (frm) {
let document_type = frm.doc.document_type;
- let doc_is_table = document_type &&
- (await frappe.db.get_value('DocType', document_type, 'istable')).message.istable;
+ let doc_is_table =
+ document_type &&
+ (await frappe.db.get_value("DocType", document_type, "istable")).message.istable;
- frm.set_df_property('parent_document_type', 'hidden', !doc_is_table);
+ frm.set_df_property("parent_document_type", "hidden", !doc_is_table);
if (document_type && doc_is_table) {
- let parent = await frappe.db.get_list('DocField', {
- filters: {
- 'fieldtype': 'Table',
- 'options': document_type
- },
- fields: ['parent']
- });
+ let parents = await frappe.xcall(
+ "frappe.desk.doctype.dashboard_chart.dashboard_chart.get_parent_doctypes",
+ { child_type: document_type }
+ );
- parent && frm.set_query('parent_document_type', function() {
+ frm.set_query("parent_document_type", function () {
return {
filters: {
- "name": ['in', parent.map(({ parent }) => parent)]
- }
+ name: ["in", parents],
+ },
};
});
- if (parent.length === 1) {
- frm.set_value('parent_document_type', parent[0].parent);
+ if (parents.length === 1) {
+ frm.set_value("parent_document_type", parents[0]);
}
}
- }
-
+ },
});
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
index a5d30c10e5..a5aa6cc20a 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
@@ -42,7 +42,8 @@
"column_break_2",
"color",
"section_break_10",
- "last_synced_on"
+ "last_synced_on",
+ "roles"
],
"fields": [
{
@@ -277,13 +278,20 @@
"fieldtype": "Link",
"label": "Parent Document Type",
"options": "DocType"
+ },
+ {
+ "fieldname": "roles",
+ "fieldtype": "Table",
+ "label": "Roles",
+ "options": "Has Role"
}
],
"links": [],
- "modified": "2021-11-09 17:18:11.456145",
+ "modified": "2022-07-27 11:09:09.203236",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -323,5 +331,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
index 75f230a901..5cbeb06e33 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
@@ -11,7 +11,7 @@ from frappe.config import get_modules_from_all_apps_for_user
from frappe.model.document import Document
from frappe.model.naming import append_number_if_name_exists
from frappe.modules.export_file import export_to_files
-from frappe.utils import cint, get_datetime, getdate, now_datetime, nowdate
+from frappe.utils import cint, get_datetime, getdate, has_common, now_datetime, nowdate
from frappe.utils.dashboard import cache_source
from frappe.utils.data import format_date
from frappe.utils.dateutils import (
@@ -87,6 +87,11 @@ def has_permission(doc, ptype, user):
if doc.document_type in allowed_doctypes:
return True
+ if doc.roles:
+ allowed = [d.role for d in doc.roles]
+ if has_common(roles, allowed):
+ return True
+
return False
@@ -210,14 +215,13 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
group_by="_unit",
order_by="_unit asc",
as_list=True,
- ignore_ifnull=True,
)
result = get_result(data, timegrain, from_date, to_date, chart.chart_type)
return {
"labels": [
- format_date(get_period(r[0], timegrain))
+ format_date(get_period(r[0], timegrain), parse_day_first=True)
if timegrain in ("Daily", "Weekly")
else get_period(r[0], timegrain)
for r in result
@@ -244,7 +248,7 @@ def get_heatmap_chart_config(chart, filters, heatmap_year):
timestamp_field = f"extract(epoch from timestamp {datefield})"
data = dict(
- frappe.db.get_all(
+ frappe.get_all(
doctype,
fields=[
timestamp_field,
@@ -274,28 +278,20 @@ def get_group_by_chart_config(chart, filters):
group_by_field = chart.group_by_based_on
doctype = chart.document_type
- data = frappe.db.get_list(
+ data = frappe.get_list(
doctype,
fields=[
f"{group_by_field} as name",
- "{aggregate_function}({value_field}) as count".format(
- aggregate_function=aggregate_function, value_field=value_field
- ),
+ f"{aggregate_function}({value_field}) as count",
],
filters=filters,
+ parent_doctype=chart.parent_document_type,
group_by=group_by_field,
order_by="count desc",
ignore_ifnull=True,
)
if data:
- if chart.number_of_groups and chart.number_of_groups < len(data):
- other_count = 0
- for i in range(chart.number_of_groups - 1, len(data)):
- other_count += data[i]["count"]
- data = data[0 : chart.number_of_groups - 1]
- data.append({"name": "Other", "count": other_count})
-
chart_config = {
"labels": [item["name"] if item["name"] else "Not Specified" for item in data],
"datasets": [{"name": chart.name, "values": [item["count"] for item in data]}],
@@ -387,3 +383,25 @@ class DashboardChart(Document):
json.loads(self.custom_options)
except ValueError as error:
frappe.throw(_("Invalid json added in the custom options: {0}").format(error))
+
+
+@frappe.whitelist()
+def get_parent_doctypes(child_type: str) -> list[str]:
+ """Get all parent doctypes that have the child doctype."""
+ assert isinstance(child_type, str)
+
+ standard = frappe.get_all(
+ "DocField",
+ fields="parent",
+ filters={"fieldtype": "Table", "options": child_type},
+ pluck="parent",
+ )
+
+ custom = frappe.get_all(
+ "Custom Field",
+ fields="dt",
+ filters={"fieldtype": "Table", "options": child_type},
+ pluck="dt",
+ )
+
+ return standard + custom
diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
index 820f3c0555..ddbabedcb4 100644
--- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
@@ -173,7 +173,7 @@ class TestDashboardChart(FrappeTestCase):
self.assertEqual(result.get("datasets")[0].get("values"), [200.0, 400.0, 300.0, 0.0, 100.0, 0.0])
self.assertEqual(
result.get("labels"),
- ["06-01-2019", "07-01-2019", "08-01-2019", "09-01-2019", "10-01-2019", "11-01-2019"],
+ ["01-06-2019", "01-07-2019", "01-08-2019", "01-09-2019", "01-10-2019", "01-11-2019"],
)
def test_weekly_dashboard_chart(self):
@@ -203,7 +203,7 @@ class TestDashboardChart(FrappeTestCase):
result = get(chart_name="Test Weekly Dashboard Chart", refresh=1)
self.assertEqual(result.get("datasets")[0].get("values"), [50.0, 300.0, 800.0, 0.0])
- self.assertEqual(result.get("labels"), ["12-30-2018", "06-01-2019", "01-13-2019", "01-20-2019"])
+ self.assertEqual(result.get("labels"), ["12-30-2018", "01-06-2019", "01-13-2019", "01-20-2019"])
def test_avg_dashboard_chart(self):
insert_test_records()
@@ -230,7 +230,7 @@ class TestDashboardChart(FrappeTestCase):
with patch.object(frappe.utils.data, "get_first_day_of_the_week", return_value="Monday"):
result = get(chart_name="Test Average Dashboard Chart", refresh=1)
- self.assertEqual(result.get("labels"), ["12-30-2018", "06-01-2019", "01-13-2019", "01-20-2019"])
+ self.assertEqual(result.get("labels"), ["12-30-2018", "01-06-2019", "01-13-2019", "01-20-2019"])
self.assertEqual(result.get("datasets")[0].get("values"), [50.0, 150.0, 266.6666666666667, 0.0])
def test_user_date_label_dashboard_chart(self):
@@ -255,13 +255,13 @@ class TestDashboardChart(FrappeTestCase):
with patch.object(frappe.utils.data, "get_user_date_format", return_value="dd.mm.yyyy"):
result = get(chart_name="Test Dashboard Chart Date Label")
self.assertEqual(
- sorted(result.get("labels")), sorted(["01.05.2019", "01.12.2019", "19.01.2019"])
+ sorted(result.get("labels")), sorted(["05.01.2019", "12.01.2019", "19.01.2019"])
)
with patch.object(frappe.utils.data, "get_user_date_format", return_value="mm-dd-yyyy"):
result = get(chart_name="Test Dashboard Chart Date Label")
self.assertEqual(
- sorted(result.get("labels")), sorted(["01-19-2019", "05-01-2019", "12-01-2019"])
+ sorted(result.get("labels")), sorted(["01-19-2019", "01-05-2019", "01-12-2019"])
)
diff --git a/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.js b/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.js
index 96dd40d3c1..6f1fa36ffd 100644
--- a/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.js
+++ b/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.js
@@ -1,5 +1,4 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Dashboard Chart Source', {
-});
+frappe.ui.form.on("Dashboard Chart Source", {});
diff --git a/frappe/desk/doctype/dashboard_chart_source/test_dashboard_chart_source.py b/frappe/desk/doctype/dashboard_chart_source/test_dashboard_chart_source.py
index 457487bb6d..5b4c114287 100644
--- a/frappe/desk/doctype/dashboard_chart_source/test_dashboard_chart_source.py
+++ b/frappe/desk/doctype/dashboard_chart_source/test_dashboard_chart_source.py
@@ -1,7 +1,7 @@
# Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE
-import unittest
+from frappe.tests.utils import FrappeTestCase
-class TestDashboardChartSource(unittest.TestCase):
+class TestDashboardChartSource(FrappeTestCase):
pass
diff --git a/frappe/desk/doctype/dashboard_settings/dashboard_settings.js b/frappe/desk/doctype/dashboard_settings/dashboard_settings.js
index 8e7966366d..aa5be2b1a5 100644
--- a/frappe/desk/doctype/dashboard_settings/dashboard_settings.js
+++ b/frappe/desk/doctype/dashboard_settings/dashboard_settings.js
@@ -1,8 +1,7 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Dashboard Settings', {
+frappe.ui.form.on("Dashboard Settings", {
// refresh: function(frm) {
-
// }
});
diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.js b/frappe/desk/doctype/desktop_icon/desktop_icon.js
index 58ea09e732..72ef1f7a12 100644
--- a/frappe/desk/doctype/desktop_icon/desktop_icon.js
+++ b/frappe/desk/doctype/desktop_icon/desktop_icon.js
@@ -1,8 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Desktop Icon', {
- refresh: function(frm) {
-
- }
+frappe.ui.form.on("Desktop Icon", {
+ refresh: function (frm) {},
});
diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.json b/frappe/desk/doctype/desktop_icon/desktop_icon.json
index 59c95953ad..ef88346f53 100644
--- a/frappe/desk/doctype/desktop_icon/desktop_icon.json
+++ b/frappe/desk/doctype/desktop_icon/desktop_icon.json
@@ -1,736 +1,175 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2016-02-22 03:47:45.387068",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 0,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2016-02-22 03:47:45.387068",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "module_name",
+ "label",
+ "standard",
+ "custom",
+ "column_break_3",
+ "app",
+ "description",
+ "category",
+ "hidden",
+ "blocked",
+ "force_show",
+ "section_break_7",
+ "type",
+ "_doctype",
+ "_report",
+ "link",
+ "column_break_10",
+ "color",
+ "icon",
+ "reverse",
+ "idx"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "module_name",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Module Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "module_name",
+ "fieldtype": "Data",
+ "label": "Module Name"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "label",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Label",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "label": "Label"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "standard",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Standard",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "fieldname": "standard",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Standard"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "custom",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Custom",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "fieldname": "custom",
+ "fieldtype": "Check",
+ "label": "Custom",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_3",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "app",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "App",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "app",
+ "fieldtype": "Data",
+ "label": "App",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "description",
- "fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Description",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "label": "Description"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "category",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Category",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "category",
+ "fieldtype": "Data",
+ "label": "Category"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "hidden",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Hidden",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "fieldname": "hidden",
+ "fieldtype": "Check",
+ "label": "Hidden"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "blocked",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Blocked",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "fieldname": "blocked",
+ "fieldtype": "Check",
+ "label": "Blocked"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "force_show",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Force Show",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "fieldname": "force_show",
+ "fieldtype": "Check",
+ "label": "Force Show",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_7",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_7",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "type",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Type",
- "length": 0,
- "no_copy": 0,
- "options": "module\nlist\nlink\npage\nquery-report",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "type",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Type",
+ "options": "module\nlist\nlink\npage\nquery-report"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "_doctype",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "_doctype",
- "length": 0,
- "no_copy": 0,
- "options": "DocType",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "_doctype",
+ "fieldtype": "Link",
+ "label": "_doctype",
+ "options": "DocType"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "_report",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "_report",
- "length": 0,
- "no_copy": 0,
- "options": "Report",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "_report",
+ "fieldtype": "Link",
+ "label": "_report",
+ "options": "Report"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "link",
- "fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Link",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "link",
+ "fieldtype": "Small Text",
+ "label": "Link"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_10",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_10",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "color",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Color",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "color",
+ "fieldtype": "Data",
+ "label": "Color"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "icon",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Icon",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "icon",
+ "fieldtype": "Data",
+ "label": "Icon"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "reverse",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Reverse Icon Color",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "fieldname": "reverse",
+ "fieldtype": "Check",
+ "label": "Reverse Icon Color"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "idx",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Idx",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "idx",
+ "fieldtype": "Int",
+ "label": "Idx"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 1,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-01-24 04:58:58.720618",
- "modified_by": "Administrator",
- "module": "Desk",
- "name": "Desktop Icon",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "in_create": 1,
+ "links": [],
+ "modified": "2022-08-03 12:20:50.577580",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Desktop Icon",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 1,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "title_field": "module_name",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "read_only": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "title_field": "module_name",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.py b/frappe/desk/doctype/desktop_icon/desktop_icon.py
index 5602f4da24..63fa12b8fb 100644
--- a/frappe/desk/doctype/desktop_icon/desktop_icon.py
+++ b/frappe/desk/doctype/desktop_icon/desktop_icon.py
@@ -61,7 +61,7 @@ def get_desktop_icons(user=None):
blocked_doctypes = [d.get("name") for d in blocked_doctypes]
- standard_icons = frappe.db.get_all("Desktop Icon", fields=fields, filters={"standard": 1})
+ standard_icons = frappe.get_all("Desktop Icon", fields=fields, filters={"standard": 1})
standard_map = {}
for icon in standard_icons:
@@ -69,7 +69,7 @@ def get_desktop_icons(user=None):
icon.blocked = 1
standard_map[icon.module_name] = icon
- user_icons = frappe.db.get_all(
+ user_icons = frappe.get_all(
"Desktop Icon", fields=fields, filters={"standard": 0, "owner": user}
)
@@ -430,7 +430,7 @@ def get_context(context):
context.user = frappe.session.user
if "System Manager" in frappe.get_roles():
- context.users = frappe.db.get_all(
+ context.users = frappe.get_all(
"User",
filters={"user_type": "System User", "enabled": 1},
fields=["name", "first_name", "last_name"],
@@ -443,7 +443,7 @@ def get_module_icons(user=None):
frappe.only_for("System Manager")
if not user:
- icons = frappe.db.get_all("Desktop Icon", fields="*", filters={"standard": 1}, order_by="idx")
+ icons = frappe.get_all("Desktop Icon", fields="*", filters={"standard": 1}, order_by="idx")
else:
frappe.cache().hdel("desktop_icons", user)
icons = get_user_icons(user)
diff --git a/frappe/desk/doctype/event/event.js b/frappe/desk/doctype/event/event.js
index 87d78bae94..299cbe5cc3 100644
--- a/frappe/desk/doctype/event/event.js
+++ b/frappe/desk/doctype/event/event.js
@@ -3,70 +3,91 @@
frappe.provide("frappe.desk");
frappe.ui.form.on("Event", {
- onload: function(frm) {
- frm.set_query('reference_doctype', "event_participants", function() {
- return {
- "filters": {
- "issingle": 0,
- }
- };
- });
- frm.set_query('google_calendar', function() {
+ onload: function (frm) {
+ frm.set_query("reference_doctype", "event_participants", function () {
return {
filters: {
- "owner": frappe.session.user
- }
+ issingle: 0,
+ },
+ };
+ });
+ frm.set_query("google_calendar", function () {
+ return {
+ filters: {
+ owner: frappe.session.user,
+ },
};
});
},
- refresh: function(frm) {
- if(frm.doc.event_participants) {
- frm.doc.event_participants.forEach(value => {
- frm.add_custom_button(__(value.reference_docname), function() {
- frappe.set_route("Form", value.reference_doctype, value.reference_docname);
- }, __("Participants"));
- })
+ refresh: function (frm) {
+ if (frm.doc.event_participants) {
+ frm.doc.event_participants.forEach((value) => {
+ frm.add_custom_button(
+ __(value.reference_docname),
+ function () {
+ frappe.set_route("Form", value.reference_doctype, value.reference_docname);
+ },
+ __("Participants")
+ );
+ });
}
frm.page.set_inner_btn_group_as_primary(__("Add Participants"));
- frm.add_custom_button(__('Add Contacts'), function() {
- new frappe.desk.eventParticipants(frm, "Contact");
- }, __("Add Participants"));
- },
- repeat_on: function(frm) {
- if(frm.doc.repeat_on==="Every Day") {
- ["monday", "tuesday", "wednesday", "thursday",
- "friday", "saturday", "sunday"].map(function(v) {
- frm.set_value(v, 1);
- });
+ frm.add_custom_button(
+ __("Add Contacts"),
+ function () {
+ new frappe.desk.eventParticipants(frm, "Contact");
+ },
+ __("Add Participants")
+ );
+
+ const [ends_on_date] = frm.doc.ends_on
+ ? frm.doc.ends_on.split(" ")
+ : frm.doc.starts_on.split(" ");
+
+ if (frm.doc.google_meet_link && frappe.datetime.now_date() <= ends_on_date) {
+ frm.dashboard.set_headline(
+ __("Join video conference with {0}", [
+ `Google Meet `,
+ ])
+ );
}
- }
+ },
+ repeat_on: function (frm) {
+ if (frm.doc.repeat_on === "Every Day") {
+ ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"].map(
+ function (v) {
+ frm.set_value(v, 1);
+ }
+ );
+ }
+ },
});
frappe.ui.form.on("Event Participants", {
- event_participants_remove: function(frm, cdt, cdn) {
- if (cdt&&!cdn.includes("New Event Participants")){
+ event_participants_remove: function (frm, cdt, cdn) {
+ if (cdt && !cdn.includes("New Event Participants")) {
frappe.call({
type: "POST",
method: "frappe.desk.doctype.event.event.delete_communication",
args: {
- "event": frm.doc,
- "reference_doctype": cdt,
- "reference_docname": cdn
+ event: frm.doc,
+ reference_doctype: cdt,
+ reference_docname: cdn,
},
freeze: true,
- callback: function(r) {
- if(r.exc) {
+ callback: function (r) {
+ if (r.exc) {
frappe.show_alert({
message: __("{0}", [r.exc]),
- indicator: 'orange'
+ indicator: "orange",
});
}
- }
+ },
});
}
- }
+ },
});
frappe.desk.eventParticipants = class eventParticipants {
@@ -86,7 +107,7 @@ frappe.desk.eventParticipants = class eventParticipants {
dynamic_link_reference: me.doctype,
fieldname: "reference_docname",
target: table,
- txt: ""
+ txt: "",
});
}
};
diff --git a/frappe/desk/doctype/event/event.json b/frappe/desk/doctype/event/event.json
index bce3b1e65a..5ca49f3831 100644
--- a/frappe/desk/doctype/event/event.json
+++ b/frappe/desk/doctype/event/event.json
@@ -22,12 +22,14 @@
"sender",
"all_day",
"sync_with_google_calendar",
+ "add_video_conferencing",
"sb_00",
"google_calendar",
- "pulled_from_google_calendar",
- "cb_00",
"google_calendar_id",
+ "cb_00",
"google_calendar_event_id",
+ "google_meet_link",
+ "pulled_from_google_calendar",
"section_break_13",
"repeat_on",
"repeat_till",
@@ -221,11 +223,11 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
- "options": "Open\nClosed"
+ "options": "Open\nCompleted\nClosed"
},
{
"collapsible": 1,
- "depends_on": "eval:doc.sync_with_google_calendar",
+ "depends_on": "eval:doc.sync_with_google_calendar || doc.pulled_from_google_calendar",
"fieldname": "sb_00",
"fieldtype": "Section Break",
"label": "Google Calendar"
@@ -245,6 +247,7 @@
"fieldname": "google_calendar_event_id",
"fieldtype": "Data",
"label": "Google Calendar Event ID",
+ "no_copy": 1,
"read_only": 1
},
{
@@ -272,12 +275,27 @@
"label": "Sender",
"options": "Email",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.sync_with_google_calendar",
+ "description": "via Google Meet",
+ "fieldname": "add_video_conferencing",
+ "fieldtype": "Check",
+ "label": "Add Video Conferencing"
+ },
+ {
+ "fieldname": "google_meet_link",
+ "fieldtype": "Data",
+ "label": "Google Meet Link",
+ "no_copy": 1,
+ "read_only": 1
}
],
"icon": "fa fa-calendar",
"idx": 1,
"links": [],
- "modified": "2022-05-12 05:43:27.935510",
+ "modified": "2022-08-12 19:24:34.794098",
"modified_by": "Administrator",
"module": "Desk",
"name": "Event",
@@ -318,4 +336,4 @@
"track_changes": 1,
"track_seen": 1,
"track_views": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py
index e9104ef897..5013c4fb3d 100644
--- a/frappe/desk/doctype/event/event.py
+++ b/frappe/desk/doctype/event/event.py
@@ -6,6 +6,7 @@ import json
import frappe
from frappe import _
+from frappe.contacts.doctype.contact.contact import get_default_contact
from frappe.desk.doctype.notification_settings.notification_settings import (
is_email_notifications_enabled_for_type,
)
@@ -55,6 +56,12 @@ class Event(Document):
if self.sync_with_google_calendar and not self.google_calendar:
frappe.throw(_("Select Google Calendar to which event should be synced."))
+ if not self.sync_with_google_calendar:
+ self.add_video_conferencing = 0
+
+ def before_save(self):
+ self.set_participants_email()
+
def on_update(self):
self.sync_communication()
@@ -131,6 +138,22 @@ class Event(Document):
for participant in participants:
self.add_participant(participant["doctype"], participant["docname"])
+ def set_participants_email(self):
+ for participant in self.event_participants:
+ if participant.email:
+ continue
+
+ if participant.reference_doctype != "Contact":
+ participant_contact = get_default_contact(
+ participant.reference_doctype, participant.reference_docname
+ )
+ else:
+ participant_contact = participant.reference_docname
+
+ participant.email = (
+ frappe.get_value("Contact", participant_contact, "email_id") if participant_contact else None
+ )
+
@frappe.whitelist()
def delete_communication(event, reference_doctype, reference_docname):
@@ -205,7 +228,7 @@ def send_event_digest():
@frappe.whitelist()
-def get_events(start, end, user=None, for_reminder=False, filters=None):
+def get_events(start, end, user=None, for_reminder=False, filters=None) -> list[frappe._dict]:
if not user:
user = frappe.session.user
@@ -283,8 +306,8 @@ def get_events(start, end, user=None, for_reminder=False, filters=None):
)
# process recurring events
- start = start.split(" ")[0]
- end = end.split(" ")[0]
+ start = start.split(" ", 1)[0]
+ end = end.split(" ", 1)[0]
add_events = []
remove_events = []
@@ -292,7 +315,7 @@ def get_events(start, end, user=None, for_reminder=False, filters=None):
new_event = e.copy()
enddate = (
- add_days(date, int(date_diff(e.ends_on.split(" ")[0], e.starts_on.split(" ")[0])))
+ add_days(date, int(date_diff(e.ends_on.split(" ", 1)[0], e.starts_on.split(" ", 1)[0])))
if (e.starts_on and e.ends_on)
else date
)
@@ -314,8 +337,8 @@ def get_events(start, end, user=None, for_reminder=False, filters=None):
repeat = "3000-01-01" if cstr(e.repeat_till) == "" else e.repeat_till
if e.repeat_on == "Yearly":
- start_year = cint(start.split("-")[0])
- end_year = cint(end.split("-")[0])
+ start_year = cint(start.split("-", 1)[0])
+ end_year = cint(end.split("-", 1)[0])
# creates a string with date (27) and month (07) eg: 07-27
event_start = "-".join(event_start.split("-")[1:])
@@ -334,12 +357,13 @@ def get_events(start, end, user=None, for_reminder=False, filters=None):
if e.repeat_on == "Monthly":
# creates a string with date (27) and month (07) and year (2019) eg: 2019-07-27
- date = start.split("-")[0] + "-" + start.split("-")[1] + "-" + event_start.split("-")[2]
+ year, month = start.split("-", maxsplit=2)[:2]
+ date = f"{year}-{month}-" + event_start.split("-", maxsplit=3)[2]
# last day of month issue, start from prev month!
try:
getdate(date)
- except ValueError:
+ except Exception:
date = date.split("-")
date = date[0] + "-" + str(cint(date[1]) - 1) + "-" + date[2]
diff --git a/frappe/desk/doctype/event/event_calendar.js b/frappe/desk/doctype/event/event_calendar.js
index df474a0258..bfdd09b864 100644
--- a/frappe/desk/doctype/event/event_calendar.js
+++ b/frappe/desk/doctype/event/event_calendar.js
@@ -1,16 +1,16 @@
frappe.views.calendar["Event"] = {
field_map: {
- "start": "starts_on",
- "end": "ends_on",
- "id": "name",
- "allDay": "all_day",
- "title": "subject",
- "status": "event_type",
- "color": "color"
+ start: "starts_on",
+ end: "ends_on",
+ id: "name",
+ allDay: "all_day",
+ title: "subject",
+ status: "event_type",
+ color: "color",
},
style_map: {
- "Public": "success",
- "Private": "info"
+ Public: "success",
+ Private: "info",
},
- get_events_method: "frappe.desk.doctype.event.event.get_events"
-}
\ No newline at end of file
+ get_events_method: "frappe.desk.doctype.event.event.get_events",
+};
diff --git a/frappe/desk/doctype/event/event_list.js b/frappe/desk/doctype/event/event_list.js
index 5d73d9dd1a..f6460288d8 100644
--- a/frappe/desk/doctype/event/event_list.js
+++ b/frappe/desk/doctype/event/event_list.js
@@ -1,8 +1,8 @@
-frappe.listview_settings['Event'] = {
+frappe.listview_settings["Event"] = {
add_fields: ["starts_on", "ends_on"],
- onload: function() {
+ onload: function () {
frappe.route_options = {
- "status": "Open"
+ status: "Open",
};
- }
-}
\ No newline at end of file
+ },
+};
diff --git a/frappe/desk/doctype/event/test_event.py b/frappe/desk/doctype/event/test_event.py
index efbd54fb09..72eab8f416 100644
--- a/frappe/desk/doctype/event/test_event.py
+++ b/frappe/desk/doctype/event/test_event.py
@@ -3,17 +3,17 @@
"""Use blog post test to test user permissions logic"""
import json
-import unittest
import frappe
import frappe.defaults
from frappe.desk.doctype.event.event import get_events
from frappe.test_runner import make_test_objects
+from frappe.tests.utils import FrappeTestCase
test_records = frappe.get_test_records("Event")
-class TestEvent(unittest.TestCase):
+class TestEvent(FrappeTestCase):
def setUp(self):
frappe.db.delete("Event")
make_test_objects("Event", reset=True)
diff --git a/frappe/desk/doctype/event_participants/event_participants.json b/frappe/desk/doctype/event_participants/event_participants.json
index 86cf2670c9..bbb0a24f3e 100644
--- a/frappe/desk/doctype/event_participants/event_participants.json
+++ b/frappe/desk/doctype/event_participants/event_participants.json
@@ -1,108 +1,49 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2018-09-21 15:44:58.836156",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "reference_doctype",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Reference Document Type",
- "length": 0,
- "no_copy": 0,
- "options": "DocType",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "reference_docname",
- "fieldtype": "Dynamic Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Reference Name",
- "length": 0,
- "no_copy": 0,
- "options": "reference_doctype",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2019-09-05 14:22:27.664645",
- "modified_by": "Administrator",
- "module": "Desk",
- "name": "Event Participants",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
- }
\ No newline at end of file
+ "actions": [],
+ "creation": "2018-09-21 15:44:58.836156",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "reference_doctype",
+ "reference_docname",
+ "email"
+ ],
+ "fields": [
+ {
+ "fieldname": "reference_doctype",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Reference Document Type",
+ "options": "DocType",
+ "reqd": 1
+ },
+ {
+ "fieldname": "reference_docname",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "label": "Reference Name",
+ "options": "reference_doctype",
+ "reqd": 1
+ },
+ {
+ "fieldname": "email",
+ "fieldtype": "Data",
+ "label": "Email",
+ "options": "Email"
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2022-10-18 17:49:33.549459",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Event Participants",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js
index 3f3fc0ff8a..1e67e10779 100644
--- a/frappe/desk/doctype/form_tour/form_tour.js
+++ b/frappe/desk/doctype/form_tour/form_tour.js
@@ -1,10 +1,10 @@
// Copyright (c) 2021, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Form Tour', {
- setup: function(frm) {
+frappe.ui.form.on("Form Tour", {
+ setup: function (frm) {
if (!frm.doc.is_standard || frappe.boot.developer_mode) {
- frm.trigger('setup_queries');
+ frm.trigger("setup_queries");
}
},
@@ -13,28 +13,26 @@ frappe.ui.form.on('Form Tour', {
frm.trigger("disable_form");
}
- frm.add_custom_button(__('Show Tour'), async () => {
+ frm.add_custom_button(__("Show Tour"), async () => {
const issingle = await check_if_single(frm.doc.reference_doctype);
let route_changed = null;
if (issingle) {
- route_changed = frappe.set_route('Form', frm.doc.reference_doctype);
+ route_changed = frappe.set_route("Form", frm.doc.reference_doctype);
} else if (frm.doc.first_document) {
const name = await get_first_document(frm.doc.reference_doctype);
- route_changed = frappe.set_route('Form', frm.doc.reference_doctype, name);
+ route_changed = frappe.set_route("Form", frm.doc.reference_doctype, name);
} else {
- route_changed = frappe.set_route('Form', frm.doc.reference_doctype, 'new');
+ route_changed = frappe.set_route("Form", frm.doc.reference_doctype, "new");
}
route_changed.then(() => {
const tour_name = frm.doc.name;
- cur_frm.tour
- .init({ tour_name })
- .then(() => cur_frm.tour.start());
+ cur_frm.tour.init({ tour_name }).then(() => cur_frm.tour.start());
});
});
},
- disable_form: function(frm) {
+ disable_form: function (frm) {
frm.set_read_only();
frm.fields
.filter((field) => field.has_input)
@@ -45,51 +43,48 @@ frappe.ui.form.on('Form Tour', {
},
setup_queries(frm) {
- frm.set_query("reference_doctype", function() {
+ frm.set_query("reference_doctype", function () {
return {
filters: {
- istable: 0
- }
+ istable: 0,
+ },
};
});
- frm.trigger('reference_doctype');
+ frm.trigger("reference_doctype");
},
reference_doctype(frm) {
if (!frm.doc.reference_doctype) return;
- frm.set_fields_as_options(
- "fieldname",
- frm.doc.reference_doctype,
- df => !df.hidden
- ).then(options => {
- frm.fields_dict.steps.grid.update_docfield_property(
- "fieldname",
- "options",
- [""].concat(options)
- );
- });
+ frm.set_fields_as_options("fieldname", frm.doc.reference_doctype, (df) => !df.hidden).then(
+ (options) => {
+ frm.fields_dict.steps.grid.update_docfield_property(
+ "fieldname",
+ "options",
+ [""].concat(options)
+ );
+ }
+ );
frm.set_fields_as_options(
- 'parent_fieldname',
+ "parent_fieldname",
frm.doc.reference_doctype,
- (df) => df.fieldtype == "Table" && !df.hidden,
- ).then(options => {
+ (df) => df.fieldtype == "Table" && !df.hidden
+ ).then((options) => {
frm.fields_dict.steps.grid.update_docfield_property(
"parent_fieldname",
"options",
[""].concat(options)
);
});
-
- }
+ },
});
-frappe.ui.form.on('Form Tour Step', {
+frappe.ui.form.on("Form Tour Step", {
form_render(frm, cdt, cdn) {
if (locals[cdt][cdn].is_table_field) {
- frm.trigger('parent_fieldname', cdt, cdn);
+ frm.trigger("parent_fieldname", cdt, cdn);
}
},
parent_fieldname(frm, cdt, cdn) {
@@ -97,37 +92,36 @@ frappe.ui.form.on('Form Tour Step', {
const parent_fieldname_df = frappe
.get_meta(frm.doc.reference_doctype)
- .fields.find(df => df.fieldname == child_row.parent_fieldname);
+ .fields.find((df) => df.fieldname == child_row.parent_fieldname);
frm.set_fields_as_options(
- 'fieldname',
+ "fieldname",
parent_fieldname_df.options,
- (df) => !df.hidden,
- ).then(options => {
+ (df) => !df.hidden
+ ).then((options) => {
frm.fields_dict.steps.grid.update_docfield_property(
"fieldname",
"options",
[""].concat(options)
);
if (child_row.fieldname) {
- frappe.model.set_value(cdt, cdn, 'fieldname', child_row.fieldname);
+ frappe.model.set_value(cdt, cdn, "fieldname", child_row.fieldname);
}
});
- }
+ },
});
async function check_if_single(doctype) {
- const { message } = await frappe.db.get_value('DocType', doctype, 'issingle');
+ const { message } = await frappe.db.get_value("DocType", doctype, "issingle");
return message.issingle || 0;
}
async function get_first_document(doctype) {
let docname;
- await frappe.db.get_list(doctype, { order_by: "creation" }).then(res => {
- if (Array.isArray(res) && res.length)
- docname = res[0].name;
+ await frappe.db.get_list(doctype, { order_by: "creation" }).then((res) => {
+ if (Array.isArray(res) && res.length) docname = res[0].name;
});
- return docname || 'new';
+ return docname || "new";
}
diff --git a/frappe/desk/doctype/form_tour/test_form_tour.py b/frappe/desk/doctype/form_tour/test_form_tour.py
index cb0c4ef33a..3fdcdf95a6 100644
--- a/frappe/desk/doctype/form_tour/test_form_tour.py
+++ b/frappe/desk/doctype/form_tour/test_form_tour.py
@@ -2,8 +2,8 @@
# License: MIT. See LICENSE
# import frappe
-import unittest
+from frappe.tests.utils import FrappeTestCase
-class TestFormTour(unittest.TestCase):
+class TestFormTour(FrappeTestCase):
pass
diff --git a/frappe/desk/doctype/global_search_settings/global_search_settings.js b/frappe/desk/doctype/global_search_settings/global_search_settings.js
index c333f83585..147a72eef1 100644
--- a/frappe/desk/doctype/global_search_settings/global_search_settings.js
+++ b/frappe/desk/doctype/global_search_settings/global_search_settings.js
@@ -1,14 +1,17 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Global Search Settings', {
- refresh: function(frm) {
-
- frappe.realtime.on('global_search_settings', (data) => {
+frappe.ui.form.on("Global Search Settings", {
+ refresh: function (frm) {
+ frappe.realtime.on("global_search_settings", (data) => {
if (data.progress) {
- frm.dashboard.show_progress('Setting up Global Search', data.progress / data.total * 100, data.msg);
+ frm.dashboard.show_progress(
+ "Setting up Global Search",
+ (data.progress / data.total) * 100,
+ data.msg
+ );
if (data.progress === data.total) {
- frm.dashboard.hide_progress('Setting up Global Search');
+ frm.dashboard.hide_progress("Setting up Global Search");
}
}
});
@@ -16,14 +19,14 @@ frappe.ui.form.on('Global Search Settings', {
frm.add_custom_button(__("Reset"), function () {
frappe.call({
method: "frappe.desk.doctype.global_search_settings.global_search_settings.reset_global_search_settings_doctypes",
- callback: function() {
+ callback: function () {
frappe.show_alert({
message: __("Global Search Document Types Reset."),
- indicator: "green"
+ indicator: "green",
});
frm.refresh();
- }
+ },
});
});
- }
+ },
});
diff --git a/frappe/desk/doctype/kanban_board/kanban_board.js b/frappe/desk/doctype/kanban_board/kanban_board.js
index ff80a58fa0..3b815fef0e 100644
--- a/frappe/desk/doctype/kanban_board/kanban_board.js
+++ b/frappe/desk/doctype/kanban_board/kanban_board.js
@@ -1,43 +1,45 @@
// Copyright (c) 2016, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Kanban Board', {
- onload: function(frm) {
- frm.trigger('reference_doctype');
+frappe.ui.form.on("Kanban Board", {
+ onload: function (frm) {
+ frm.trigger("reference_doctype");
},
- refresh: function(frm) {
- if(frm.is_new()) return;
- frm.add_custom_button("Show Board", function() {
+ refresh: function (frm) {
+ if (frm.is_new()) return;
+ frm.add_custom_button("Show Board", function () {
frappe.set_route("List", frm.doc.reference_doctype, "Kanban", frm.doc.name);
});
},
- reference_doctype: function(frm) {
-
+ reference_doctype: function (frm) {
// set field options
- if(!frm.doc.reference_doctype) return;
+ if (!frm.doc.reference_doctype) return;
- frappe.model.with_doctype(frm.doc.reference_doctype, function() {
- var options = $.map(frappe.get_meta(frm.doc.reference_doctype).fields,
- function(d) {
- if(d.fieldname && d.fieldtype === 'Select' &&
- frappe.model.no_value_type.indexOf(d.fieldtype)===-1) {
- return d.fieldname;
- }
- return null;
- });
- frm.set_df_property('field_name', 'options', options);
- frm.get_field('field_name').refresh();
+ frappe.model.with_doctype(frm.doc.reference_doctype, function () {
+ var options = $.map(frappe.get_meta(frm.doc.reference_doctype).fields, function (d) {
+ if (
+ d.fieldname &&
+ d.fieldtype === "Select" &&
+ frappe.model.no_value_type.indexOf(d.fieldtype) === -1
+ ) {
+ return d.fieldname;
+ }
+ return null;
+ });
+ frm.set_df_property("field_name", "options", options);
+ frm.get_field("field_name").refresh();
});
},
- field_name: function(frm) {
+ field_name: function (frm) {
var field = frappe.meta.get_field(frm.doc.reference_doctype, frm.doc.field_name);
frm.doc.columns = [];
- field.options && field.options.split('\n').forEach(function(o) {
- o = o.trim();
- if(!o) return;
- var d = frm.add_child('columns');
- d.column_name = o;
- });
+ field.options &&
+ field.options.split("\n").forEach(function (o) {
+ o = o.trim();
+ if (!o) return;
+ var d = frm.add_child("columns");
+ d.column_name = o;
+ });
frm.refresh();
- }
+ },
});
diff --git a/frappe/desk/doctype/kanban_board/kanban_board.py b/frappe/desk/doctype/kanban_board/kanban_board.py
index 83f0f46df0..e3257e25be 100644
--- a/frappe/desk/doctype/kanban_board/kanban_board.py
+++ b/frappe/desk/doctype/kanban_board/kanban_board.py
@@ -88,10 +88,15 @@ def update_order(board_name, order):
"""Save the order of cards in columns"""
board = frappe.get_doc("Kanban Board", board_name)
doctype = board.reference_doctype
+ updated_cards = []
+
+ if not frappe.has_permission(doctype, "write"):
+ # Return board data from db
+ return board, updated_cards
+
fieldname = board.field_name
order_dict = json.loads(order)
- updated_cards = []
for col_name, cards in order_dict.items():
for card in cards:
column = frappe.get_value(doctype, {"name": card}, fieldname)
@@ -103,8 +108,7 @@ def update_order(board_name, order):
if column.column_name == col_name:
column.order = json.dumps(cards)
- board.save()
- return board, updated_cards
+ return board.save(ignore_permissions=True), updated_cards
@frappe.whitelist()
@@ -114,6 +118,9 @@ def update_order_for_single_card(
"""Save the order of cards in columns"""
board = frappe.get_doc("Kanban Board", board_name)
doctype = board.reference_doctype
+
+ frappe.has_permission(doctype, "write", throw=True)
+
fieldname = board.field_name
old_index = frappe.parse_json(old_index)
new_index = frappe.parse_json(new_index)
@@ -130,7 +137,7 @@ def update_order_for_single_card(
# save updated order
board.columns[from_col_idx].order = frappe.as_json(from_col_order)
board.columns[to_col_idx].order = frappe.as_json(to_col_order)
- board.save()
+ board.save(ignore_permissions=True)
# update changed value in doc
frappe.set_value(doctype, docname, fieldname, to_colname)
@@ -151,13 +158,14 @@ def get_kanban_column_order_and_index(board, colname):
def add_card(board_name, docname, colname):
board = frappe.get_doc("Kanban Board", board_name)
+ frappe.has_permission(board.reference_doctype, "write", throw=True)
+
col_order, col_idx = get_kanban_column_order_and_index(board, colname)
col_order.insert(0, docname)
board.columns[col_idx].order = frappe.as_json(col_order)
- board.save()
- return board
+ return board.save(ignore_permissions=True)
@frappe.whitelist()
diff --git a/frappe/desk/doctype/kanban_board/test_kanban_board.py b/frappe/desk/doctype/kanban_board/test_kanban_board.py
index 73f566b906..05cd5723c4 100644
--- a/frappe/desk/doctype/kanban_board/test_kanban_board.py
+++ b/frappe/desk/doctype/kanban_board/test_kanban_board.py
@@ -1,9 +1,9 @@
# Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE
-import unittest
+from frappe.tests.utils import FrappeTestCase
# test_records = frappe.get_test_records('Kanban Board')
-class TestKanbanBoard(unittest.TestCase):
+class TestKanbanBoard(FrappeTestCase):
pass
diff --git a/frappe/desk/doctype/list_filter/list_filter.json b/frappe/desk/doctype/list_filter/list_filter.json
index dad62bf8d6..257bbc6d45 100644
--- a/frappe/desk/doctype/list_filter/list_filter.json
+++ b/frappe/desk/doctype/list_filter/list_filter.json
@@ -1,188 +1,62 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
+ "actions": [],
"creation": "2018-02-22 15:10:24.401801",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
- "document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
+ "field_order": [
+ "filter_name",
+ "reference_doctype",
+ "for_user",
+ "filters"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "filter_name",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Filter Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Filter Name"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "reference_doctype",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Reference Document Type",
- "length": 0,
- "no_copy": 0,
- "options": "DocType",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "DocType"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "for_user",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "For User",
- "length": 0,
- "no_copy": 0,
- "options": "User",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "User"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "filters",
"fieldtype": "Long Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Filters",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Filters"
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
"in_create": 1,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-09-05 14:22:27.664645",
+ "links": [],
+ "modified": "2022-08-03 12:20:50.889979",
"modified_by": "Administrator",
"module": "Desk",
"name": "List Filter",
- "name_case": "",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
}
],
- "quick_entry": 0,
"read_only": 1,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
- "track_changes": 0,
- "track_seen": 0
+ "states": []
}
\ No newline at end of file
diff --git a/frappe/desk/doctype/list_view_settings/list_view_settings.js b/frappe/desk/doctype/list_view_settings/list_view_settings.js
index db33f71675..007a242dd3 100644
--- a/frappe/desk/doctype/list_view_settings/list_view_settings.js
+++ b/frappe/desk/doctype/list_view_settings/list_view_settings.js
@@ -1,8 +1,7 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('List View Settings', {
+frappe.ui.form.on("List View Settings", {
// refresh: function(frm) {
-
// }
-});
\ No newline at end of file
+});
diff --git a/frappe/desk/doctype/list_view_settings/list_view_settings.json b/frappe/desk/doctype/list_view_settings/list_view_settings.json
index 44761992f1..69ea379e61 100644
--- a/frappe/desk/doctype/list_view_settings/list_view_settings.json
+++ b/frappe/desk/doctype/list_view_settings/list_view_settings.json
@@ -7,6 +7,7 @@
"engine": "InnoDB",
"field_order": [
"disable_count",
+ "disable_comment_count",
"disable_sidebar_stats",
"disable_auto_refresh",
"total_fields",
@@ -49,13 +50,20 @@
"hidden": 1,
"label": "Fields",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "disable_comment_count",
+ "fieldtype": "Check",
+ "label": "Disable Comment Count"
}
],
"links": [],
- "modified": "2020-05-12 18:27:15.568199",
+ "modified": "2023-02-14 14:46:43.764229",
"modified_by": "Administrator",
"module": "Desk",
"name": "List View Settings",
+ "naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
@@ -72,5 +80,6 @@
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/desk/doctype/list_view_settings/test_list_view_settings.py b/frappe/desk/doctype/list_view_settings/test_list_view_settings.py
index 0eab9cd7a6..8e27385230 100644
--- a/frappe/desk/doctype/list_view_settings/test_list_view_settings.py
+++ b/frappe/desk/doctype/list_view_settings/test_list_view_settings.py
@@ -1,8 +1,8 @@
# Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE
# import frappe
-import unittest
+from frappe.tests.utils import FrappeTestCase
-class TestListViewSettings(unittest.TestCase):
+class TestListViewSettings(FrappeTestCase):
pass
diff --git a/frappe/desk/doctype/module_onboarding/module_onboarding.js b/frappe/desk/doctype/module_onboarding/module_onboarding.js
index d95920e2ca..0e312025bf 100644
--- a/frappe/desk/doctype/module_onboarding/module_onboarding.js
+++ b/frappe/desk/doctype/module_onboarding/module_onboarding.js
@@ -2,7 +2,7 @@
// For license information, please see license.txt
frappe.ui.form.on("Module Onboarding", {
- refresh: function(frm) {
+ refresh: function (frm) {
frappe.boot.developer_mode &&
frm.set_intro(
__(
@@ -15,7 +15,7 @@ frappe.ui.form.on("Module Onboarding", {
}
},
- disable_form: function(frm) {
+ disable_form: function (frm) {
frm.set_read_only();
frm.fields
.filter((field) => field.has_input)
diff --git a/frappe/desk/doctype/module_onboarding/test_module_onboarding.py b/frappe/desk/doctype/module_onboarding/test_module_onboarding.py
index fa19794c1e..e84a5c4d2b 100644
--- a/frappe/desk/doctype/module_onboarding/test_module_onboarding.py
+++ b/frappe/desk/doctype/module_onboarding/test_module_onboarding.py
@@ -1,8 +1,8 @@
# Copyright (c) 2020, Frappe Technologies and Contributors
# License: MIT. See LICENSE
# import frappe
-import unittest
+from frappe.tests.utils import FrappeTestCase
-class TestModuleOnboarding(unittest.TestCase):
+class TestModuleOnboarding(FrappeTestCase):
pass
diff --git a/frappe/desk/doctype/note/note.js b/frappe/desk/doctype/note/note.js
index 5718180b70..567e7534ba 100644
--- a/frappe/desk/doctype/note/note.js
+++ b/frappe/desk/doctype/note/note.js
@@ -1,45 +1,45 @@
frappe.ui.form.on("Note", {
- refresh: function(frm) {
- if (frm.doc.__islocal) {
- frm.events.set_editable(frm, true);
- } else {
- if (!frm.doc.content) {
- frm.doc.content = " ";
- }
-
- // toggle edit
- frm.add_custom_button("Edit", function() {
- frm.events.set_editable(frm, !frm.is_note_editable);
- });
- frm.events.set_editable(frm, false);
+ refresh: function (frm) {
+ if (!frm.is_new()) {
+ frm.is_note_editable = false;
+ frm.events.set_editable(frm);
}
},
- set_editable: function(frm, editable) {
- // hide all fields other than content
-
- // no permission
- if (editable && !frm.perm[0].write) return;
+ set_editable: function (frm) {
+ if (frm.has_perm("write")) {
+ const read_label = __("Read mode");
+ const edit_label = __("Edit mode");
+ frm.remove_custom_button(frm.is_note_editable ? edit_label : read_label);
+ frm.add_custom_button(frm.is_note_editable ? read_label : edit_label, function () {
+ frm.is_note_editable = !frm.is_note_editable;
+ frm.events.set_editable(frm);
+ });
+ }
+ // toggle "read_only" for content and "hidden" of all other fields
// content read_only
- frm.set_df_property("content", "read_only", editable ? 0 : 1);
+ frm.set_df_property("content", "read_only", frm.is_note_editable ? 0 : 1);
// hide all other fields
- $.each(frm.fields_dict, function(fieldname) {
- if (fieldname !== "content") {
- frm.set_df_property(fieldname, "hidden", editable ? 0 : 1);
+ for (const field of frm.meta.fields) {
+ if (field.fieldname !== "content") {
+ frm.set_df_property(
+ field.fieldname,
+ "hidden",
+ frm.is_note_editable && !field.hidden && frm.get_perm(field.permlevel, "write")
+ ? 0
+ : 1
+ );
}
- });
+ }
// no label, description for content either
- frm.get_field("content").toggle_label(editable);
- frm.get_field("content").toggle_description(editable);
-
- // set flag for toggle
- frm.is_note_editable = editable;
- }
+ frm.get_field("content").toggle_label(frm.is_note_editable);
+ frm.get_field("content").toggle_description(frm.is_note_editable);
+ },
});
-frappe.tour['Note'] = [
+frappe.tour["Note"] = [
{
fieldname: "title",
title: "Title of the Note",
@@ -48,6 +48,7 @@ frappe.tour['Note'] = [
{
fieldname: "public",
title: "Sets the Note to Public",
- description: "You can change the visibility of the note with this, setting it to public will allow other users to view it.",
+ description:
+ "You can change the visibility of the note with this, setting it to public will allow other users to view it.",
},
-];
\ No newline at end of file
+];
diff --git a/frappe/desk/doctype/note/note.json b/frappe/desk/doctype/note/note.json
index 69a9518ac4..b297e2ada6 100644
--- a/frappe/desk/doctype/note/note.json
+++ b/frappe/desk/doctype/note/note.json
@@ -1,6 +1,6 @@
{
"actions": [],
- "allow_rename": 1,
+ "autoname": "hash",
"creation": "2013-05-24 13:41:00",
"doctype": "DocType",
"document_type": "Document",
@@ -19,11 +19,8 @@
{
"fieldname": "title",
"fieldtype": "Data",
- "in_global_search": 1,
"in_list_view": 1,
"label": "Title",
- "no_copy": 1,
- "print_hide": 1,
"reqd": 1
},
{
@@ -32,6 +29,7 @@
"fieldname": "public",
"fieldtype": "Check",
"label": "Public",
+ "permlevel": 1,
"print_hide": 1
},
{
@@ -40,7 +38,8 @@
"depends_on": "public",
"fieldname": "notify_on_login",
"fieldtype": "Check",
- "label": "Notify users with a popup when they log in"
+ "label": "Notify users with a popup when they log in",
+ "permlevel": 1
},
{
"bold": 1,
@@ -49,13 +48,15 @@
"description": "If enabled, users will be notified every time they login. If not enabled, users will only be notified once.",
"fieldname": "notify_on_every_login",
"fieldtype": "Check",
- "label": "Notify Users On Every Login"
+ "label": "Notify Users On Every Login",
+ "permlevel": 1
},
{
"depends_on": "eval:doc.notify_on_login && doc.public",
"fieldname": "expire_notification_on",
"fieldtype": "Date",
"label": "Expire Notification On",
+ "permlevel": 1,
"search_index": 1
},
{
@@ -68,39 +69,80 @@
},
{
"collapsible": 1,
+ "depends_on": "notify_on_login",
"fieldname": "seen_by_section",
"fieldtype": "Section Break",
- "label": "Seen By"
+ "label": "Seen By",
+ "permlevel": 1
},
{
"fieldname": "seen_by",
"fieldtype": "Table",
"label": "Seen By Table",
- "options": "Note Seen By"
+ "options": "Note Seen By",
+ "permlevel": 1
}
],
"icon": "fa fa-file-text",
"idx": 1,
"links": [],
- "modified": "2021-09-18 10:57:51.352643",
+ "modified": "2023-04-24 16:07:03.281184",
"modified_by": "Administrator",
"module": "Desk",
"name": "Note",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
+ "export": 1,
"print": 1,
"read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "permlevel": 1,
+ "read": 1,
+ "role": "System Manager",
+ "write": 1
+ },
+ {
+ "permlevel": 2,
+ "read": 1,
+ "role": "System Manager",
+ "write": 1
+ },
+ {
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "All"
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "if_owner": 1,
"role": "All",
"share": 1,
"write": 1
+ },
+ {
+ "permlevel": 1,
+ "read": 1,
+ "role": "All"
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC",
+ "states": [],
+ "title_field": "title",
"track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/desk/doctype/note/note.py b/frappe/desk/doctype/note/note.py
index c0a37d5f44..6aba6391cb 100644
--- a/frappe/desk/doctype/note/note.py
+++ b/frappe/desk/doctype/note/note.py
@@ -1,53 +1,39 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
-import re
-
import frappe
from frappe.model.document import Document
-NAME_PATTERN = re.compile("[%'\"#*?`]")
-
class Note(Document):
- def autoname(self):
- # replace forbidden characters
- self.name = NAME_PATTERN.sub("", self.title.strip())
-
def validate(self):
if self.notify_on_login and not self.expire_notification_on:
-
# expire this notification in a week (default)
self.expire_notification_on = frappe.utils.add_days(self.creation, 7)
+ if not self.content:
+ self.content = " "
+
def before_print(self, settings=None):
self.print_heading = self.name
self.sub_heading = ""
+ def mark_seen_by(self, user: str) -> None:
+ if user in [d.user for d in self.seen_by]:
+ return
+
+ self.append("seen_by", {"user": user})
+
@frappe.whitelist()
-def mark_as_seen(note):
- note = frappe.get_doc("Note", note)
- if frappe.session.user not in [d.user for d in note.seen_by]:
- note.append("seen_by", {"user": frappe.session.user})
- note.save(ignore_version=True)
+def mark_as_seen(note: str):
+ note: Note = frappe.get_doc("Note", note)
+ note.mark_seen_by(frappe.session.user)
+ note.save(ignore_permissions=True, ignore_version=True)
def get_permission_query_conditions(user):
if not user:
user = frappe.session.user
- if user == "Administrator":
- return ""
-
- return f"""(`tabNote`.public=1 or `tabNote`.owner={frappe.db.escape(user)})"""
-
-
-def has_permission(doc, ptype, user):
- if doc.public == 1 or user == "Administrator":
- return True
-
- if user == doc.owner:
- return True
-
- return False
+ return f"(`tabNote`.owner = {frappe.db.escape(user)} or `tabNote`.public = 1)"
diff --git a/frappe/desk/doctype/note/note_list.js b/frappe/desk/doctype/note/note_list.js
index f7f8d37dcf..a8e948bc94 100644
--- a/frappe/desk/doctype/note/note_list.js
+++ b/frappe/desk/doctype/note/note_list.js
@@ -1,13 +1,11 @@
-frappe.listview_settings['Note'] = {
- onload: function(me) {
- me.page.set_title(__("Notes"));
- },
- add_fields: ["title", "public"],
- get_indicator: function(doc) {
- if(doc.public) {
+frappe.listview_settings["Note"] = {
+ hide_name_column: true,
+ add_fields: ["public"],
+ get_indicator: function (doc) {
+ if (doc.public) {
return [__("Public"), "green", "public,=,Yes"];
} else {
return [__("Private"), "gray", "public,=,No"];
}
- }
-}
+ },
+};
diff --git a/frappe/desk/doctype/note/test_note.py b/frappe/desk/doctype/note/test_note.py
index d8bdb9efc4..426fb5a16e 100644
--- a/frappe/desk/doctype/note/test_note.py
+++ b/frappe/desk/doctype/note/test_note.py
@@ -1,14 +1,13 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# License: MIT. See LICENSE
-import unittest
-
import frappe
+from frappe.tests.utils import FrappeTestCase
test_records = frappe.get_test_records("Note")
-class TestNote(unittest.TestCase):
+class TestNote(FrappeTestCase):
def insert_note(self):
frappe.db.delete("Version")
frappe.db.delete("Note")
diff --git a/frappe/desk/doctype/note_seen_by/note_seen_by.json b/frappe/desk/doctype/note_seen_by/note_seen_by.json
index 7ee423e347..905a043284 100644
--- a/frappe/desk/doctype/note_seen_by/note_seen_by.json
+++ b/frappe/desk/doctype/note_seen_by/note_seen_by.json
@@ -1,64 +1,32 @@
{
- "allow_copy": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2016-08-29 05:29:16.726172",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
+ "actions": [],
+ "creation": "2016-08-29 05:29:16.726172",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "user"
+ ],
"fields": [
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "user",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "label": "User",
- "length": 0,
- "no_copy": 0,
- "options": "User",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "User",
+ "options": "User",
+ "permlevel": 2
}
- ],
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
-
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2016-08-29 06:02:41.531341",
- "modified_by": "Administrator",
- "module": "Desk",
- "name": "Note Seen By",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_seen": 0
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2023-04-24 16:14:53.684098",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Note Seen By",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/frappe/desk/doctype/notification_log/notification_log.js b/frappe/desk/doctype/notification_log/notification_log.js
index 1f381d115b..ea5fdc6400 100644
--- a/frappe/desk/doctype/notification_log/notification_log.js
+++ b/frappe/desk/doctype/notification_log/notification_log.js
@@ -1,25 +1,25 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Notification Log', {
- refresh: function(frm) {
+frappe.ui.form.on("Notification Log", {
+ refresh: function (frm) {
if (frm.doc.attached_file) {
- frm.trigger('set_attachment');
+ frm.trigger("set_attachment");
} else {
- frm.get_field('attachment_link').$wrapper.empty();
+ frm.get_field("attachment_link").$wrapper.empty();
}
},
- open_reference_document: function(frm) {
+ open_reference_document: function (frm) {
const dt = frm.doc.document_type;
const dn = frm.doc.document_name;
- frappe.set_route('Form', dt, dn);
+ frappe.set_route("Form", dt, dn);
},
- set_attachment: function(frm) {
+ set_attachment: function (frm) {
const attachment = JSON.parse(frm.doc.attached_file);
- const $wrapper = frm.get_field('attachment_link').$wrapper;
+ const $wrapper = frm.get_field("attachment_link").$wrapper;
$wrapper.html(`
@@ -41,5 +41,5 @@ frappe.ui.form.on('Notification Log', {
frappe.msgprint(__("Please enable pop-ups"));
}
});
- }
+ },
});
diff --git a/frappe/desk/doctype/notification_log/notification_log.json b/frappe/desk/doctype/notification_log/notification_log.json
index e188708277..f24a6447b4 100644
--- a/frappe/desk/doctype/notification_log/notification_log.json
+++ b/frappe/desk/doctype/notification_log/notification_log.json
@@ -22,18 +22,14 @@
"fieldname": "subject",
"fieldtype": "Text",
"in_list_view": 1,
- "label": "Subject",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Subject"
},
{
"fieldname": "for_user",
"fieldtype": "Link",
"hidden": 1,
"label": "For User",
- "options": "User",
- "show_days": 1,
- "show_seconds": 1
+ "options": "User"
},
{
"fieldname": "type",
@@ -42,36 +38,26 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Type",
- "options": "Mention\nEnergy Point\nAssignment\nShare\nAlert",
- "search_index": 1,
- "show_days": 1,
- "show_seconds": 1
+ "options": "Mention\nEnergy Point\nAssignment\nShare\nAlert"
},
{
"fieldname": "email_content",
"fieldtype": "Text Editor",
- "label": "Message",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Message"
},
{
"fieldname": "document_type",
"fieldtype": "Link",
"hidden": 1,
"label": "Document Type",
- "options": "DocType",
- "search_index": 1,
- "show_days": 1,
- "show_seconds": 1
+ "options": "DocType"
},
{
"fieldname": "document_name",
"fieldtype": "Data",
"hidden": 1,
"label": "Document Link",
- "search_index": 1,
- "show_days": 1,
- "show_seconds": 1
+ "search_index": 1
},
{
"fieldname": "from_user",
@@ -79,9 +65,7 @@
"hidden": 1,
"label": "From User",
"options": "User",
- "search_index": 1,
- "show_days": 1,
- "show_seconds": 1
+ "search_index": 1
},
{
"default": "0",
@@ -89,38 +73,30 @@
"fieldtype": "Check",
"hidden": 1,
"ignore_user_permissions": 1,
- "label": "Read",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Read"
},
{
"fieldname": "open_reference_document",
"fieldtype": "Button",
- "label": "Open Reference Document",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Open Reference Document"
},
{
"fieldname": "attached_file",
"fieldtype": "Code",
"hidden": 1,
"label": "Attached File",
- "options": "JSON",
- "show_days": 1,
- "show_seconds": 1
+ "options": "JSON"
},
{
"fieldname": "attachment_link",
"fieldtype": "HTML",
- "label": "Attachment Link",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Attachment Link"
}
],
"hide_toolbar": 1,
"in_create": 1,
"links": [],
- "modified": "2021-10-25 17:26:09.703215",
+ "modified": "2022-09-13 16:08:48.153934",
"modified_by": "Administrator",
"module": "Desk",
"name": "Notification Log",
@@ -138,6 +114,7 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "subject",
"track_seen": 1
-}
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py
index 17734635dd..4d82932555 100644
--- a/frappe/desk/doctype/notification_log/notification_log.py
+++ b/frappe/desk/doctype/notification_log/notification_log.py
@@ -20,6 +20,14 @@ class NotificationLog(Document):
except frappe.OutgoingEmailError:
self.log_error(_("Failed to send notification email"))
+ @staticmethod
+ def clear_old_logs(days=180):
+ from frappe.query_builder import Interval
+ from frappe.query_builder.functions import Now
+
+ table = frappe.qb.DocType("Notification Log")
+ frappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days))))
+
def get_permission_query_conditions(for_user):
if not for_user:
@@ -133,9 +141,25 @@ def get_email_header(doc):
return header_map[doc.type or "Default"]
+@frappe.whitelist()
+def get_notification_logs(limit=20):
+ notification_logs = frappe.db.get_list(
+ "Notification Log", fields=["*"], limit=limit, order_by="modified desc"
+ )
+
+ users = [log.from_user for log in notification_logs]
+ users = [*set(users)] # remove duplicates
+ user_info = frappe._dict()
+
+ for user in users:
+ frappe.utils.add_user_info(user, user_info)
+
+ return {"notification_logs": notification_logs, "user_info": user_info}
+
+
@frappe.whitelist()
def mark_all_as_read():
- unread_docs_list = frappe.db.get_all(
+ unread_docs_list = frappe.get_all(
"Notification Log", filters={"read": 0, "for_user": frappe.session.user}
)
unread_docnames = [doc.name for doc in unread_docs_list]
diff --git a/frappe/desk/doctype/notification_log/notification_log_list.js b/frappe/desk/doctype/notification_log/notification_log_list.js
new file mode 100644
index 0000000000..150ffabfa7
--- /dev/null
+++ b/frappe/desk/doctype/notification_log/notification_log_list.js
@@ -0,0 +1,7 @@
+frappe.listview_settings["Notification Log"] = {
+ onload: function (listview) {
+ frappe.require("logtypes.bundle.js", () => {
+ frappe.utils.logtypes.show_log_retention_message(cur_list.doctype);
+ });
+ },
+};
diff --git a/frappe/desk/doctype/notification_log/test_notification_log.py b/frappe/desk/doctype/notification_log/test_notification_log.py
index 532f05ab57..a43455149f 100644
--- a/frappe/desk/doctype/notification_log/test_notification_log.py
+++ b/frappe/desk/doctype/notification_log/test_notification_log.py
@@ -1,13 +1,12 @@
# Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE
-import unittest
-
import frappe
from frappe.core.doctype.user.user import get_system_users
from frappe.desk.form.assign_to import add as assign_task
+from frappe.tests.utils import FrappeTestCase
-class TestNotificationLog(unittest.TestCase):
+class TestNotificationLog(FrappeTestCase):
def test_assignment(self):
todo = get_todo()
user = get_user()
@@ -38,7 +37,7 @@ class TestNotificationLog(unittest.TestCase):
def get_last_email_queue():
- res = frappe.db.get_all("Email Queue", fields=["message"], order_by="creation desc", limit=1)
+ res = frappe.get_all("Email Queue", fields=["message"], order_by="creation desc", limit=1)
return res[0]
diff --git a/frappe/desk/doctype/notification_settings/notification_settings.js b/frappe/desk/doctype/notification_settings/notification_settings.js
index cc2fd95204..ba72369273 100644
--- a/frappe/desk/doctype/notification_settings/notification_settings.js
+++ b/frappe/desk/doctype/notification_settings/notification_settings.js
@@ -1,28 +1,27 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Notification Settings', {
+frappe.ui.form.on("Notification Settings", {
onload: (frm) => {
frappe.breadcrumbs.add({
- label: __('Settings'),
- route: '#modules/Settings',
- type: 'Custom'
+ label: __("Settings"),
+ route: "#modules/Settings",
+ type: "Custom",
});
- frm.set_query('subscribed_documents', () => {
+ frm.set_query("subscribed_documents", () => {
return {
filters: {
- istable: 0
- }
+ istable: 0,
+ },
};
});
},
refresh: (frm) => {
- if (frappe.user.has_role('System Manager')) {
- frm.add_custom_button(__('Go to Notification Settings List'), () => {
- frappe.set_route('List', 'Notification Settings');
+ if (frappe.user.has_role("System Manager")) {
+ frm.add_custom_button(__("Go to Notification Settings List"), () => {
+ frappe.set_route("List", "Notification Settings");
});
}
- }
-
+ },
});
diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py
index 801d512fe7..cd85c7d06d 100644
--- a/frappe/desk/doctype/notification_settings/notification_settings.py
+++ b/frappe/desk/doctype/notification_settings/notification_settings.py
@@ -92,4 +92,7 @@ def get_permission_query_conditions(user):
@frappe.whitelist()
def set_seen_value(value, user):
+ if frappe.flags.read_only:
+ return
+
frappe.db.set_value("Notification Settings", user, "seen", value, update_modified=False)
diff --git a/frappe/desk/doctype/notification_settings/test_notification_settings.py b/frappe/desk/doctype/notification_settings/test_notification_settings.py
index 966b923567..e88f98443b 100644
--- a/frappe/desk/doctype/notification_settings/test_notification_settings.py
+++ b/frappe/desk/doctype/notification_settings/test_notification_settings.py
@@ -2,8 +2,8 @@
# See license.txt
# import frappe
-import unittest
+from frappe.tests.utils import FrappeTestCase
-class TestNotificationSettings(unittest.TestCase):
+class TestNotificationSettings(FrappeTestCase):
pass
diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js
index 79ddb71187..b0c5456268 100644
--- a/frappe/desk/doctype/number_card/number_card.js
+++ b/frappe/desk/doctype/number_card/number_card.js
@@ -1,73 +1,75 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Number Card', {
- refresh: function(frm) {
+frappe.ui.form.on("Number Card", {
+ refresh: function (frm) {
if (!frappe.boot.developer_mode && frm.doc.is_standard) {
frm.disable_form();
}
frm.set_df_property("filters_section", "hidden", 1);
frm.set_df_property("dynamic_filters_section", "hidden", 1);
- frm.trigger('set_options');
+ frm.trigger("set_options");
if (!frm.doc.type) {
- frm.set_value('type', 'Document Type');
+ frm.set_value("type", "Document Type");
}
- if (frm.doc.type == 'Report' && frm.doc.report_name) {
- frm.trigger('set_report_filters');
+ if (frm.doc.type == "Report" && frm.doc.report_name) {
+ frm.trigger("set_report_filters");
}
- if (frm.doc.type == 'Custom') {
+ if (frm.doc.type == "Custom") {
if (!frappe.boot.developer_mode) {
frm.disable_form();
}
frm.filters = eval(frm.doc.filters_config);
- frm.trigger('set_filters_description');
- frm.trigger('set_method_description');
- frm.trigger('render_filters_table');
+ frm.trigger("set_filters_description");
+ frm.trigger("set_method_description");
+ frm.trigger("render_filters_table");
}
- frm.trigger('set_parent_document_type');
+ frm.trigger("set_parent_document_type");
if (!frm.is_new()) {
- frm.trigger('create_add_to_dashboard_button');
+ frm.trigger("create_add_to_dashboard_button");
}
},
- create_add_to_dashboard_button: function(frm) {
- frm.add_custom_button('Add Card to Dashboard', () => {
+ create_add_to_dashboard_button: function (frm) {
+ frm.add_custom_button("Add Card to Dashboard", () => {
const dialog = frappe.dashboard_utils.get_add_to_dashboard_dialog(
frm.doc.name,
- 'Number Card',
- 'frappe.desk.doctype.number_card.number_card.add_card_to_dashboard'
+ "Number Card",
+ "frappe.desk.doctype.number_card.number_card.add_card_to_dashboard"
);
if (!frm.doc.name) {
- frappe.msgprint(__('Please create Card first'));
+ frappe.msgprint(__("Please create Card first"));
} else {
dialog.show();
}
});
},
- before_save: function(frm) {
- let dynamic_filters = JSON.parse(frm.doc.dynamic_filters_json || 'null');
- let static_filters = JSON.parse(frm.doc.filters_json || 'null');
- static_filters =
- frappe.dashboard_utils.remove_common_static_filter_values(static_filters, dynamic_filters);
+ before_save: function (frm) {
+ let dynamic_filters = JSON.parse(frm.doc.dynamic_filters_json || "null");
+ let static_filters = JSON.parse(frm.doc.filters_json || "null");
+ static_filters = frappe.dashboard_utils.remove_common_static_filter_values(
+ static_filters,
+ dynamic_filters
+ );
- frm.set_value('filters_json', JSON.stringify(static_filters));
- frm.trigger('render_filters_table');
- frm.trigger('render_dynamic_filters_table');
+ frm.set_value("filters_json", JSON.stringify(static_filters));
+ frm.trigger("render_filters_table");
+ frm.trigger("render_dynamic_filters_table");
},
- is_standard: function(frm) {
- frm.trigger('render_dynamic_filters_table');
+ is_standard: function (frm) {
+ frm.trigger("render_dynamic_filters_table");
frm.set_df_property("dynamic_filters_section", "hidden", 1);
},
- set_filters_description: function(frm) {
- if (frm.doc.type == 'Custom') {
+ set_filters_description: function (frm) {
+ if (frm.doc.type == "Custom") {
frm.fields_dict.filters_config.set_description(`
Set the filters here. For example:
@@ -91,8 +93,8 @@ frappe.ui.form.on('Number Card', {
}
},
- set_method_description: function(frm) {
- if (frm.doc.type == 'Custom') {
+ set_method_description: function (frm) {
+ if (frm.doc.type == "Custom") {
frm.fields_dict.method.set_description(`
Set the path to a whitelisted function that will return the number on the card in the format:
@@ -105,53 +107,52 @@ frappe.ui.form.on('Number Card', {
}
},
- type: function(frm) {
- frm.trigger('set_filters_description');
- if (frm.doc.type == 'Report') {
- frm.set_query('report_name', () => {
+ type: function (frm) {
+ frm.trigger("set_filters_description");
+ if (frm.doc.type == "Report") {
+ frm.set_query("report_name", () => {
return {
filters: {
- 'report_type': ['!=', 'Report Builder']
- }
+ report_type: ["!=", "Report Builder"],
+ },
};
});
}
-
},
- report_name: function(frm) {
+ report_name: function (frm) {
frm.filters = [];
- frm.set_value('filters_json', '{}');
- frm.set_value('dynamic_filters_json', '{}');
- frm.set_df_property('report_field', 'options', []);
- frm.trigger('set_report_filters');
+ frm.set_value("filters_json", "{}");
+ frm.set_value("dynamic_filters_json", "{}");
+ frm.set_df_property("report_field", "options", []);
+ frm.trigger("set_report_filters");
},
- filters_config: function(frm) {
+ filters_config: function (frm) {
frm.filters = eval(frm.doc.filters_config);
const filter_values = frappe.report_utils.get_filter_values(frm.filters);
- frm.set_value('filters_json', JSON.stringify(filter_values));
- frm.trigger('render_filters_table');
+ frm.set_value("filters_json", JSON.stringify(filter_values));
+ frm.trigger("render_filters_table");
},
- document_type: function(frm) {
- frm.set_query('document_type', function() {
+ document_type: function (frm) {
+ frm.set_query("document_type", function () {
return {
filters: {
- 'issingle': false
- }
+ issingle: false,
+ },
};
});
- frm.set_value('filters_json', '[]');
- frm.set_value('dynamic_filters_json', '[]');
- frm.set_value('aggregate_function_based_on', '');
- frm.set_value('parent_document_type', '');
- frm.trigger('set_options');
- frm.trigger('set_parent_document_type');
+ frm.set_value("filters_json", "[]");
+ frm.set_value("dynamic_filters_json", "[]");
+ frm.set_value("aggregate_function_based_on", "");
+ frm.set_value("parent_document_type", "");
+ frm.trigger("set_options");
+ frm.trigger("set_parent_document_type");
},
- set_options: function(frm) {
- if (frm.doc.type !== 'Document Type') {
+ set_options: function (frm) {
+ if (frm.doc.type !== "Document Type") {
return;
}
@@ -160,134 +161,148 @@ frappe.ui.form.on('Number Card', {
if (doctype) {
frappe.model.with_doctype(doctype, () => {
- frappe.get_meta(doctype).fields.map(df => {
+ frappe.get_meta(doctype).fields.map((df) => {
if (frappe.model.numeric_fieldtypes.includes(df.fieldtype)) {
- if (df.fieldtype == 'Currency') {
- if (!df.options || df.options !== 'Company:company:default_currency') {
+ if (df.fieldtype == "Currency") {
+ if (!df.options || df.options !== "Company:company:default_currency") {
return;
}
}
- aggregate_based_on_fields.push({label: df.label, value: df.fieldname});
+ aggregate_based_on_fields.push({ label: df.label, value: df.fieldname });
}
});
- frm.set_df_property('aggregate_function_based_on', 'options', aggregate_based_on_fields);
+ frm.set_df_property(
+ "aggregate_function_based_on",
+ "options",
+ aggregate_based_on_fields
+ );
});
- frm.trigger('render_filters_table');
- frm.trigger('render_dynamic_filters_table');
+ frm.trigger("render_filters_table");
+ frm.trigger("render_dynamic_filters_table");
}
},
- set_report_filters: function(frm) {
+ set_report_filters: function (frm) {
const report_name = frm.doc.report_name;
if (report_name) {
- frappe.report_utils.get_report_filters(report_name).then(filters => {
+ frappe.report_utils.get_report_filters(report_name).then((filters) => {
if (filters) {
frm.filters = filters;
const filter_values = frappe.report_utils.get_filter_values(filters);
if (frm.doc.filters_json.length <= 2) {
- frm.set_value('filters_json', JSON.stringify(filter_values));
+ frm.set_value("filters_json", JSON.stringify(filter_values));
}
}
- frm.trigger('render_filters_table');
- frm.trigger('set_report_field_options');
- frm.trigger('render_dynamic_filters_table');
+ frm.trigger("render_filters_table");
+ frm.trigger("set_report_field_options");
+ frm.trigger("render_dynamic_filters_table");
});
}
},
- set_report_field_options: function(frm) {
+ set_report_field_options: function (frm) {
let filters = frm.doc.filters_json.length > 2 ? JSON.parse(frm.doc.filters_json) : null;
if (frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2) {
filters = frappe.dashboard_utils.get_all_filters(frm.doc);
}
- frappe.xcall(
- 'frappe.desk.query_report.run',
- {
+ frappe
+ .xcall("frappe.desk.query_report.run", {
report_name: frm.doc.report_name,
filters: filters,
- ignore_prepared_report: 1
- }
- ).then(data => {
- if (data.result.length) {
- frm.field_options = frappe.report_utils.get_field_options_from_report(data.columns, data);
- frm.set_df_property('report_field', 'options', frm.field_options.numeric_fields);
- if (!frm.field_options.numeric_fields.length) {
- frappe.msgprint(__("Report has no numeric fields, please change the Report Name"));
+ ignore_prepared_report: 1,
+ })
+ .then((data) => {
+ if (data.result.length) {
+ frm.field_options = frappe.report_utils.get_field_options_from_report(
+ data.columns,
+ data
+ );
+ frm.set_df_property(
+ "report_field",
+ "options",
+ frm.field_options.numeric_fields
+ );
+ if (!frm.field_options.numeric_fields.length) {
+ frappe.msgprint(
+ __("Report has no numeric fields, please change the Report Name")
+ );
+ }
+ } else {
+ frappe.msgprint(
+ __(
+ "Report has no data, please modify the filters or change the Report Name"
+ )
+ );
}
- } else {
- frappe.msgprint(__('Report has no data, please modify the filters or change the Report Name'));
- }
- });
+ });
},
- render_filters_table: function(frm) {
+ render_filters_table: function (frm) {
frm.set_df_property("filters_section", "hidden", 0);
- let is_document_type = frm.doc.type == 'Document Type';
- let is_dynamic_filter = f => ['Date', 'DateRange'].includes(f.fieldtype) && f.default;
+ let is_document_type = frm.doc.type == "Document Type";
+ let is_dynamic_filter = (f) => ["Date", "DateRange"].includes(f.fieldtype) && f.default;
- let wrapper = $(frm.get_field('filters_json').wrapper).empty();
+ let wrapper = $(frm.get_field("filters_json").wrapper).empty();
let table = $(`
- ${__('Filter')}
- ${__('Condition')}
- ${__('Value')}
+ ${__("Filter")}
+ ${__("Condition")}
+ ${__("Value")}
`).appendTo(wrapper);
$(`
${__("Click table to edit")}
`).appendTo(wrapper);
- let filters = JSON.parse(frm.doc.filters_json || '[]');
+ let filters = JSON.parse(frm.doc.filters_json || "[]");
let filters_set = false;
// Set dynamic filters for reports
- if (frm.doc.type == 'Report') {
+ if (frm.doc.type == "Report") {
let set_filters = false;
- frm.filters.forEach(f => {
+ frm.filters.forEach((f) => {
if (is_dynamic_filter(f)) {
filters[f.fieldname] = f.default;
set_filters = true;
}
});
- set_filters && frm.set_value('filters_json', JSON.stringify(filters));
+ set_filters && frm.set_value("filters_json", JSON.stringify(filters));
}
let fields = [];
if (is_document_type) {
fields = [
{
- fieldtype: 'HTML',
- fieldname: 'filter_area',
- }
+ fieldtype: "HTML",
+ fieldname: "filter_area",
+ },
];
if (filters.length) {
- filters.forEach(filter => {
- const filter_row =
- $(`
+ filters.forEach((filter) => {
+ const filter_row = $(`
${filter[1]}
${filter[2] || ""}
${filter[3]}
`);
- table.find('tbody').append(filter_row);
+ table.find("tbody").append(filter_row);
});
filters_set = true;
}
} else if (frm.filters.length) {
- fields = frm.filters.filter(f => f.fieldname);
- fields.map(f => {
+ fields = frm.filters.filter((f) => f.fieldname);
+ fields.map((f) => {
if (filters[f.fieldname]) {
- let condition = '=';
- const filter_row =
- $(`
+ let condition = "=";
+ const filter_row = $(`
${f.label}
${condition}
${filters[f.fieldname] || ""}
`);
- table.find('tbody').append(filter_row);
+ table.find("tbody").append(filter_row);
if (!filters_set) filters_set = true;
}
});
@@ -296,32 +311,32 @@ frappe.ui.form.on('Number Card', {
if (!filters_set) {
const filter_row = $(`
${__("Click to Set Filters")} `);
- table.find('tbody').append(filter_row);
+ table.find("tbody").append(filter_row);
}
- table.on('click', () => {
+ table.on("click", () => {
let dialog = new frappe.ui.Dialog({
- title: __('Set Filters'),
- fields: fields.filter(f => !is_dynamic_filter(f)),
- primary_action: function() {
+ title: __("Set Filters"),
+ fields: fields.filter((f) => !is_dynamic_filter(f)),
+ primary_action: function () {
let values = this.get_values();
if (values) {
this.hide();
if (is_document_type) {
let filters = frm.filter_group.get_filters();
- frm.set_value('filters_json', JSON.stringify(filters));
+ frm.set_value("filters_json", JSON.stringify(filters));
} else {
- frm.set_value('filters_json', JSON.stringify(values));
+ frm.set_value("filters_json", JSON.stringify(values));
}
- frm.trigger('render_filters_table');
+ frm.trigger("render_filters_table");
}
},
- primary_action_label: "Set"
+ primary_action_label: "Set",
});
if (is_document_type) {
frm.filter_group = new frappe.ui.FilterGroup({
- parent: dialog.get_field('filter_area').$wrapper,
+ parent: dialog.get_field("filter_area").$wrapper,
doctype: frm.doc.document_type,
parent_doctype: frm.doc.parent_document_type,
on_change: () => {},
@@ -331,56 +346,61 @@ frappe.ui.form.on('Number Card', {
dialog.show();
- if (frm.doc.type == 'Report') {
+ if (frm.doc.type == "Report") {
//Set query report object so that it can be used while fetching filter values in the report
- frappe.query_report = new frappe.views.QueryReport({'filters': dialog.fields_list});
- frappe.query_reports[frm.doc.report_name]
- && frappe.query_reports[frm.doc.report_name].onload
- && frappe.query_reports[frm.doc.report_name].onload(frappe.query_report);
+ frappe.query_report = new frappe.views.QueryReport({
+ filters: dialog.fields_list,
+ });
+ frappe.query_reports[frm.doc.report_name] &&
+ frappe.query_reports[frm.doc.report_name].onload &&
+ frappe.query_reports[frm.doc.report_name].onload(frappe.query_report);
}
dialog.set_values(filters);
});
-
},
render_dynamic_filters_table(frm) {
- if (!frappe.boot.developer_mode || !frm.doc.is_standard || frm.doc.type == 'Custom') {
+ if (!frappe.boot.developer_mode || !frm.doc.is_standard || frm.doc.type == "Custom") {
return;
}
frm.set_df_property("dynamic_filters_section", "hidden", 0);
- let is_document_type = frm.doc.type == 'Document Type';
+ let is_document_type = frm.doc.type == "Document Type";
- let wrapper = $(frm.get_field('dynamic_filters_json').wrapper).empty();
+ let wrapper = $(frm.get_field("dynamic_filters_json").wrapper).empty();
- frm.dynamic_filter_table = $(`
+ frm.dynamic_filter_table =
+ $(`
- ${__('Filter')}
- ${__('Condition')}
- ${__('Value')}
+ ${__("Filter")}
+ ${__("Condition")}
+ ${__("Value")}
`).appendTo(wrapper);
- frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
- ? JSON.parse(frm.doc.dynamic_filters_json)
- : null;
+ frm.dynamic_filters =
+ frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
+ ? JSON.parse(frm.doc.dynamic_filters_json)
+ : null;
- frm.trigger('set_dynamic_filters_in_table');
+ frm.trigger("set_dynamic_filters_in_table");
- let filters = JSON.parse(frm.doc.filters_json || '[]');
+ let filters = JSON.parse(frm.doc.filters_json || "[]");
let fields = frappe.dashboard_utils.get_fields_for_dynamic_filter_dialog(
- is_document_type, filters, frm.dynamic_filters
+ is_document_type,
+ filters,
+ frm.dynamic_filters
);
- frm.dynamic_filter_table.on('click', () => {
+ frm.dynamic_filter_table.on("click", () => {
let dialog = new frappe.ui.Dialog({
- title: __('Set Dynamic Filters'),
+ title: __("Set Dynamic Filters"),
fields: fields,
primary_action: () => {
let values = dialog.get_values();
@@ -388,19 +408,19 @@ frappe.ui.form.on('Number Card', {
let dynamic_filters = [];
for (let key of Object.keys(values)) {
if (is_document_type) {
- let [doctype, fieldname] = key.split(':');
- dynamic_filters.push([doctype, fieldname, '=', values[key]]);
+ let [doctype, fieldname] = key.split(":");
+ dynamic_filters.push([doctype, fieldname, "=", values[key]]);
}
}
if (is_document_type) {
- frm.set_value('dynamic_filters_json', JSON.stringify(dynamic_filters));
+ frm.set_value("dynamic_filters_json", JSON.stringify(dynamic_filters));
} else {
- frm.set_value('dynamic_filters_json', JSON.stringify(values));
+ frm.set_value("dynamic_filters_json", JSON.stringify(values));
}
- frm.trigger('set_dynamic_filters_in_table');
+ frm.trigger("set_dynamic_filters_in_table");
},
- primary_action_label: "Set"
+ primary_action_label: "Set",
});
dialog.show();
@@ -408,71 +428,66 @@ frappe.ui.form.on('Number Card', {
});
},
- set_dynamic_filters_in_table: function(frm) {
- frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
- ? JSON.parse(frm.doc.dynamic_filters_json)
- : null;
+ set_dynamic_filters_in_table: function (frm) {
+ frm.dynamic_filters =
+ frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
+ ? JSON.parse(frm.doc.dynamic_filters_json)
+ : null;
if (!frm.dynamic_filters) {
const filter_row = $(`
${__("Click to Set Dynamic Filters")} `);
- frm.dynamic_filter_table.find('tbody').html(filter_row);
+ frm.dynamic_filter_table.find("tbody").html(filter_row);
} else {
- let filter_rows = '';
+ let filter_rows = "";
if ($.isArray(frm.dynamic_filters)) {
- frm.dynamic_filters.forEach(filter => {
- filter_rows +=
- `
+ frm.dynamic_filters.forEach((filter) => {
+ filter_rows += `
${filter[1]}
${filter[2] || ""}
${filter[3]}
`;
});
} else {
- let condition = '=';
+ let condition = "=";
for (let [key, val] of Object.entries(frm.dynamic_filters)) {
- filter_rows +=
- `
+ filter_rows += `
${key}
${condition}
${val || ""}
- `
- ;
+ `;
}
}
- frm.dynamic_filter_table.find('tbody').html(filter_rows);
+ frm.dynamic_filter_table.find("tbody").html(filter_rows);
}
},
- set_parent_document_type: async function(frm) {
+ set_parent_document_type: async function (frm) {
let document_type = frm.doc.document_type;
- let doc_is_table = document_type &&
- (await frappe.db.get_value('DocType', document_type, 'istable')).message.istable;
+ let doc_is_table =
+ document_type &&
+ (await frappe.db.get_value("DocType", document_type, "istable")).message.istable;
- frm.set_df_property('parent_document_type', 'hidden', !doc_is_table);
+ frm.set_df_property("parent_document_type", "hidden", !doc_is_table);
if (document_type && doc_is_table) {
- let parent = await frappe.db.get_list('DocField', {
- filters: {
- 'fieldtype': 'Table',
- 'options': document_type
- },
- fields: ['parent']
- });
+ let parents = await frappe.xcall(
+ "frappe.desk.doctype.dashboard_chart.dashboard_chart.get_parent_doctypes",
+ { child_type: document_type }
+ );
- parent && frm.set_query('parent_document_type', function() {
+ frm.set_query("parent_document_type", function () {
return {
filters: {
- "name": ['in', parent.map(({ parent }) => parent)]
- }
+ name: ["in", parents],
+ },
};
});
- if (parent.length === 1) {
- frm.set_value('parent_document_type', parent[0].parent);
+ if (parents.length === 1) {
+ frm.set_value("parent_document_type", parents[0]);
}
}
- }
-
+ },
});
diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py
index 8e808ff635..451dc699fe 100644
--- a/frappe/desk/doctype/number_card/number_card.py
+++ b/frappe/desk/doctype/number_card/number_card.py
@@ -124,15 +124,17 @@ def get_result(doc, filters, to_date=None):
)
]
- filters = frappe.parse_json(filters)
-
if not filters:
filters = []
+ elif isinstance(filters, str):
+ filters = frappe.parse_json(filters)
if to_date:
filters.append([doc.document_type, "creation", "<", to_date])
- res = frappe.db.get_list(doc.document_type, fields=fields, filters=filters)
+ res = frappe.get_list(
+ doc.document_type, fields=fields, filters=filters, parent_doctype=doc.parent_document_type
+ )
number = res[0]["result"] if res else 0
return cint(number)
@@ -200,7 +202,7 @@ def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters):
if txt:
search_conditions = [numberCard[field].like(f"%{txt}%") for field in searchfields]
- condition_query = frappe.qb.engine.build_conditions(doctype, filters)
+ condition_query = frappe.qb.get_query(doctype, filters=filters)
return (
condition_query.select(numberCard.name, numberCard.label, numberCard.document_type)
diff --git a/frappe/desk/doctype/number_card/test_number_card.py b/frappe/desk/doctype/number_card/test_number_card.py
index c0dda40104..a002b8aad3 100644
--- a/frappe/desk/doctype/number_card/test_number_card.py
+++ b/frappe/desk/doctype/number_card/test_number_card.py
@@ -1,8 +1,8 @@
# Copyright (c) 2020, Frappe Technologies and Contributors
# License: MIT. See LICENSE
# import frappe
-import unittest
+from frappe.tests.utils import FrappeTestCase
-class TestNumberCard(unittest.TestCase):
+class TestNumberCard(FrappeTestCase):
pass
diff --git a/frappe/desk/doctype/onboarding_permission/onboarding_permission.js b/frappe/desk/doctype/onboarding_permission/onboarding_permission.js
index 752b8a02cc..ec2c8a03b0 100644
--- a/frappe/desk/doctype/onboarding_permission/onboarding_permission.js
+++ b/frappe/desk/doctype/onboarding_permission/onboarding_permission.js
@@ -1,8 +1,7 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Onboarding Permission', {
+frappe.ui.form.on("Onboarding Permission", {
// refresh: function(frm) {
-
// }
});
diff --git a/frappe/desk/doctype/onboarding_permission/test_onboarding_permission.py b/frappe/desk/doctype/onboarding_permission/test_onboarding_permission.py
index cdfe0d7890..d82cb3c346 100644
--- a/frappe/desk/doctype/onboarding_permission/test_onboarding_permission.py
+++ b/frappe/desk/doctype/onboarding_permission/test_onboarding_permission.py
@@ -1,8 +1,8 @@
# Copyright (c) 2020, Frappe Technologies and Contributors
# License: MIT. See LICENSE
# import frappe
-import unittest
+from frappe.tests.utils import FrappeTestCase
-class TestOnboardingPermission(unittest.TestCase):
+class TestOnboardingPermission(FrappeTestCase):
pass
diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.js b/frappe/desk/doctype/onboarding_step/onboarding_step.js
index 3c9bbab9ac..67b2ed0501 100644
--- a/frappe/desk/doctype/onboarding_step/onboarding_step.js
+++ b/frappe/desk/doctype/onboarding_step/onboarding_step.js
@@ -2,18 +2,17 @@
// For license information, please see license.txt
frappe.ui.form.on("Onboarding Step", {
-
- setup: function(frm) {
- frm.set_query("form_tour", function() {
+ setup: function (frm) {
+ frm.set_query("form_tour", function () {
return {
filters: {
- reference_doctype: frm.doc.reference_document
- }
+ reference_doctype: frm.doc.reference_document,
+ },
};
});
},
- refresh: function(frm) {
+ refresh: function (frm) {
frappe.boot.developer_mode &&
frm.set_intro(
__(
@@ -30,15 +29,16 @@ frappe.ui.form.on("Onboarding Step", {
}
},
- reference_document: function(frm) {
+ reference_document: function (frm) {
if (frm.doc.reference_document && frm.doc.action == "Update Settings") {
setup_fields(frm);
}
},
- action: function(frm) {
+ action: function (frm) {
if (frm.doc.action == "Show Form Tour") {
- frm.fields_dict.reference_document.set_description(`You need to add the steps in the contoller JS file. For example: note.js
+ frm.fields_dict.reference_document
+ .set_description(`You need to add the steps in the contoller JS file. For example: note.js
frappe.tour['Note'] = [
{
@@ -54,7 +54,7 @@ frappe.tour['Note'] = [
}
},
- disable_form: function(frm) {
+ disable_form: function (frm) {
frm.set_read_only();
frm.fields
.filter((field) => field.has_input)
@@ -71,9 +71,7 @@ function setup_fields(frm) {
let fields = frappe
.get_meta(frm.doc.reference_document)
.fields.filter((df) => {
- return ["Data", "Check", "Int", "Link", "Select"].includes(
- df.fieldtype
- );
+ return ["Data", "Check", "Int", "Link", "Select"].includes(df.fieldtype);
})
.map((df) => {
return {
diff --git a/frappe/desk/doctype/onboarding_step/test_onboarding_step.py b/frappe/desk/doctype/onboarding_step/test_onboarding_step.py
index d8bf55584c..73b9ab4ac3 100644
--- a/frappe/desk/doctype/onboarding_step/test_onboarding_step.py
+++ b/frappe/desk/doctype/onboarding_step/test_onboarding_step.py
@@ -1,8 +1,8 @@
# Copyright (c) 2020, Frappe Technologies and Contributors
# License: MIT. See LICENSE
# import frappe
-import unittest
+from frappe.tests.utils import FrappeTestCase
-class TestOnboardingStep(unittest.TestCase):
+class TestOnboardingStep(FrappeTestCase):
pass
diff --git a/frappe/desk/doctype/route_history/route_history.js b/frappe/desk/doctype/route_history/route_history.js
index 19689e406b..c68d4e2b54 100644
--- a/frappe/desk/doctype/route_history/route_history.js
+++ b/frappe/desk/doctype/route_history/route_history.js
@@ -1,8 +1,6 @@
// Copyright (c) 2018, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Route History', {
- refresh: function() {
-
- }
+frappe.ui.form.on("Route History", {
+ refresh: function () {},
});
diff --git a/frappe/desk/doctype/route_history/route_history_list.js b/frappe/desk/doctype/route_history/route_history_list.js
index 84a441852c..03bf86b9fd 100644
--- a/frappe/desk/doctype/route_history/route_history_list.js
+++ b/frappe/desk/doctype/route_history/route_history_list.js
@@ -1,7 +1,7 @@
frappe.listview_settings["Route History"] = {
- onload: function(listview) {
+ onload: function (listview) {
frappe.require("logtypes.bundle.js", () => {
frappe.utils.logtypes.show_log_retention_message(cur_list.doctype);
- })
+ });
},
};
diff --git a/frappe/desk/doctype/system_console/system_console.js b/frappe/desk/doctype/system_console/system_console.js
index 7751ffe860..dc73f33b67 100644
--- a/frappe/desk/doctype/system_console/system_console.js
+++ b/frappe/desk/doctype/system_console/system_console.js
@@ -1,21 +1,21 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('System Console', {
- onload: function(frm) {
+frappe.ui.form.on("System Console", {
+ onload: function (frm) {
frappe.ui.keys.add_shortcut({
- shortcut: 'shift+enter',
- action: () => frm.page.btn_primary.trigger('click'),
+ shortcut: "shift+enter",
+ action: () => frm.page.btn_primary.trigger("click"),
page: frm.page,
- description: __('Execute Console script'),
+ description: __("Execute Console script"),
ignore_inputs: true,
});
frm.set_value("type", "Python");
},
- refresh: function(frm) {
+ refresh: function (frm) {
frm.disable_save();
- frm.page.set_primary_action(__("Execute"), $btn => {
+ frm.page.set_primary_action(__("Execute"), ($btn) => {
$btn.text(__("Executing..."));
return frm
.execute_action("Execute")
@@ -24,7 +24,7 @@ frappe.ui.form.on('System Console', {
});
},
- type: function(frm) {
+ type: function (frm) {
if (frm.doc.type == "Python") {
frm.set_value("output", "");
if (frm.sql_output) {
@@ -34,7 +34,7 @@ frappe.ui.form.on('System Console', {
}
},
- render_sql_output: function(frm) {
+ render_sql_output: function (frm) {
if (frm.doc.type !== "SQL") return;
if (frm.sql_output) {
frm.sql_output.destroy();
@@ -46,50 +46,51 @@ frappe.ui.form.on('System Console', {
}
let result = JSON.parse(frm.doc.output);
- frm.set_value("output", `${result.length} ${result.length == 1 ? 'row' : 'rows'}`);
+ frm.set_value("output", `${result.length} ${result.length == 1 ? "row" : "rows"}`);
if (result.length) {
let columns = Object.keys(result[0]);
- frm.sql_output = new DataTable(
- frm.get_field("sql_output").$wrapper.get(0),
- {
- columns,
- data: result
- }
- );
+ frm.sql_output = new DataTable(frm.get_field("sql_output").$wrapper.get(0), {
+ columns,
+ data: result,
+ });
}
},
- show_processlist: function(frm) {
+ show_processlist: function (frm) {
if (frm.doc.show_processlist) {
// keep refreshing every 5 seconds
frm.events.refresh_processlist(frm);
- frm.processlist_interval = setInterval(() => frm.events.refresh_processlist(frm), 5000);
+ frm.processlist_interval = setInterval(
+ () => frm.events.refresh_processlist(frm),
+ 5000
+ );
} else {
if (frm.processlist_interval) {
-
// end it
clearInterval(frm.processlist_interval);
- frm.get_field("processlist").html('');
+ frm.get_field("processlist").html("");
}
}
},
- refresh_processlist: function(frm) {
+ refresh_processlist: function (frm) {
let timestamp = new Date();
- frappe.call('frappe.desk.doctype.system_console.system_console.show_processlist').then(r => {
- let rows = '';
- for (let row of r.message) {
- rows += `
+ frappe
+ .call("frappe.desk.doctype.system_console.system_console.show_processlist")
+ .then((r) => {
+ let rows = "";
+ for (let row of r.message) {
+ rows += `
${row.Id}
${row.Time}
${row.State}
${row.Info}
${row.Progress}
- `
- }
+ `;
+ }
- frm.get_field('processlist').html(`
+ frm.get_field("processlist").html(`
Requested on: ${timestamp}
@@ -100,6 +101,6 @@ frappe.ui.form.on('System Console', {
Progress / Wait Event
${rows}`);
- });
+ });
},
});
diff --git a/frappe/desk/doctype/system_console/test_system_console.py b/frappe/desk/doctype/system_console/test_system_console.py
index 96bf555f59..2664f7c925 100644
--- a/frappe/desk/doctype/system_console/test_system_console.py
+++ b/frappe/desk/doctype/system_console/test_system_console.py
@@ -1,11 +1,10 @@
# Copyright (c) 2020, Frappe Technologies and Contributors
# License: MIT. See LICENSE
-import unittest
-
import frappe
+from frappe.tests.utils import FrappeTestCase
-class TestSystemConsole(unittest.TestCase):
+class TestSystemConsole(FrappeTestCase):
def test_system_console(self):
system_console = frappe.get_doc("System Console")
system_console.console = 'log("hello")'
diff --git a/frappe/desk/doctype/tag/tag.js b/frappe/desk/doctype/tag/tag.js
index f55f98c3d0..1c60f417e0 100644
--- a/frappe/desk/doctype/tag/tag.js
+++ b/frappe/desk/doctype/tag/tag.js
@@ -1,8 +1,7 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Tag', {
+frappe.ui.form.on("Tag", {
// refresh: function(frm) {
-
// }
});
diff --git a/frappe/desk/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py
index ca167c148e..c5fe6407b7 100644
--- a/frappe/desk/doctype/tag/tag.py
+++ b/frappe/desk/doctype/tag/tag.py
@@ -59,7 +59,7 @@ def get_tags(doctype, txt):
tag = frappe.get_list("Tag", filters=[["name", "like", f"%{txt}%"]])
tags = [t.name for t in tag]
- return sorted(filter(lambda t: t and txt.lower() in t.lower(), list(set(tags))))
+ return sorted(filter(lambda t: t and txt.casefold() in t.casefold(), list(set(tags))))
class DocTags:
@@ -192,4 +192,4 @@ def get_documents_for_tag(tag):
@frappe.whitelist()
def get_tags_list_for_awesomebar():
- return [t.name for t in frappe.get_list("Tag")]
+ return frappe.get_list("Tag", pluck="name", order_by=None)
diff --git a/frappe/desk/doctype/tag/test_tag.py b/frappe/desk/doctype/tag/test_tag.py
index 8719da8c21..0c746e67ac 100644
--- a/frappe/desk/doctype/tag/test_tag.py
+++ b/frappe/desk/doctype/tag/test_tag.py
@@ -1,11 +1,10 @@
-import unittest
-
import frappe
from frappe.desk.doctype.tag.tag import add_tag
from frappe.desk.reportview import get_stats
+from frappe.tests.utils import FrappeTestCase
-class TestTag(unittest.TestCase):
+class TestTag(FrappeTestCase):
def setUp(self) -> None:
frappe.db.delete("Tag")
frappe.db.sql("UPDATE `tabDocType` set _user_tags=''")
diff --git a/frappe/desk/doctype/tag_link/tag_link.js b/frappe/desk/doctype/tag_link/tag_link.js
index d85655bb90..e2cb4fcd7f 100644
--- a/frappe/desk/doctype/tag_link/tag_link.js
+++ b/frappe/desk/doctype/tag_link/tag_link.js
@@ -1,8 +1,7 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Tag Link', {
+frappe.ui.form.on("Tag Link", {
// refresh: function(frm) {
-
// }
});
diff --git a/frappe/desk/doctype/tag_link/test_tag_link.py b/frappe/desk/doctype/tag_link/test_tag_link.py
index 59d7bcd2bc..10edb2859e 100644
--- a/frappe/desk/doctype/tag_link/test_tag_link.py
+++ b/frappe/desk/doctype/tag_link/test_tag_link.py
@@ -1,8 +1,8 @@
# Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE
# import frappe
-import unittest
+from frappe.tests.utils import FrappeTestCase
-class TestTagLink(unittest.TestCase):
+class TestTagLink(FrappeTestCase):
pass
diff --git a/frappe/desk/doctype/todo/test_todo.py b/frappe/desk/doctype/todo/test_todo.py
index 56ca1f30e7..4880fad9d5 100644
--- a/frappe/desk/doctype/todo/test_todo.py
+++ b/frappe/desk/doctype/todo/test_todo.py
@@ -1,16 +1,15 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
-import unittest
-
import frappe
from frappe.core.doctype.doctype.doctype import clear_permissions_cache
from frappe.model.db_query import DatabaseQuery
from frappe.permissions import add_permission, reset_perms
+from frappe.tests.utils import FrappeTestCase
test_dependencies = ["User"]
-class TestToDo(unittest.TestCase):
+class TestToDo(FrappeTestCase):
def test_delete(self):
todo = frappe.get_doc(
dict(doctype="ToDo", description="test todo", assigned_by="Administrator")
diff --git a/frappe/desk/doctype/todo/todo.js b/frappe/desk/doctype/todo/todo.js
index 0317281371..6c1af67d2b 100644
--- a/frappe/desk/doctype/todo/todo.js
+++ b/frappe/desk/doctype/todo/todo.js
@@ -1,40 +1,55 @@
// bind events
frappe.ui.form.on("ToDo", {
- onload: function(frm) {
- frm.set_query("reference_type", function(txt) {
+ onload: function (frm) {
+ frm.set_query("reference_type", function (txt) {
return {
- "filters": {
- "issingle": 0,
- }
+ filters: {
+ issingle: 0,
+ },
};
});
},
- refresh: function(frm) {
- if(frm.doc.reference_type && frm.doc.reference_name) {
- frm.add_custom_button(__(frm.doc.reference_name), function() {
+ refresh: function (frm) {
+ if (frm.doc.reference_type && frm.doc.reference_name) {
+ frm.add_custom_button(__(frm.doc.reference_name), function () {
frappe.set_route("Form", frm.doc.reference_type, frm.doc.reference_name);
});
}
if (!frm.doc.__islocal) {
- if(frm.doc.status!=="Closed") {
- frm.add_custom_button(__("Close"), function() {
- frm.set_value("status", "Closed");
- frm.save(null, function() {
- // back to list
- frappe.set_route("List", "ToDo");
- });
- }, "fa fa-check", "btn-success");
+ if (frm.doc.status !== "Closed") {
+ frm.add_custom_button(
+ __("Close"),
+ function () {
+ frm.set_value("status", "Closed");
+ frm.save(null, function () {
+ // back to list
+ frappe.set_route("List", "ToDo");
+ });
+ },
+ "fa fa-check",
+ "btn-success"
+ );
} else {
- frm.add_custom_button(__("Reopen"), function() {
- frm.set_value("status", "Open");
- frm.save();
- }, null, "btn-default");
+ frm.add_custom_button(
+ __("Reopen"),
+ function () {
+ frm.set_value("status", "Open");
+ frm.save();
+ },
+ null,
+ "btn-default"
+ );
}
- frm.add_custom_button(__("New"), function() {
- frappe.new_doc("ToDo")
- }, null, "btn-default");
+ frm.add_custom_button(
+ __("New"),
+ function () {
+ frappe.new_doc("ToDo");
+ },
+ null,
+ "btn-default"
+ );
}
- }
+ },
});
diff --git a/frappe/desk/doctype/todo/todo_calendar.js b/frappe/desk/doctype/todo/todo_calendar.js
index 8ba020fac1..f79243a86e 100644
--- a/frappe/desk/doctype/todo/todo_calendar.js
+++ b/frappe/desk/doctype/todo/todo_calendar.js
@@ -3,29 +3,27 @@
frappe.views.calendar["ToDo"] = {
field_map: {
- "start": "date",
- "end": "date",
- "id": "name",
- "title": "description",
- "allDay": "allDay",
- "progress": "progress"
+ start: "date",
+ end: "date",
+ id: "name",
+ title: "description",
+ allDay: "allDay",
+ progress: "progress",
},
gantt: true,
filters: [
{
- "fieldtype": "Link",
- "fieldname": "reference_type",
- "options": "Task",
- "label": __("Task")
+ fieldtype: "Link",
+ fieldname: "reference_type",
+ options: "Task",
+ label: __("Task"),
},
{
- "fieldtype": "Dynamic Link",
- "fieldname": "reference_name",
- "options": "reference_type",
- "label": __("Task")
- }
-
+ fieldtype: "Dynamic Link",
+ fieldname: "reference_name",
+ options: "reference_type",
+ label: __("Task"),
+ },
],
- get_events_method: "frappe.desk.calendar.get_events"
+ get_events_method: "frappe.desk.calendar.get_events",
};
-
diff --git a/frappe/desk/doctype/todo/todo_list.js b/frappe/desk/doctype/todo/todo_list.js
index 53564cc017..bf62088f1d 100644
--- a/frappe/desk/doctype/todo/todo_list.js
+++ b/frappe/desk/doctype/todo/todo_list.js
@@ -1,40 +1,44 @@
-frappe.listview_settings['ToDo'] = {
+frappe.listview_settings["ToDo"] = {
hide_name_column: true,
add_fields: ["reference_type", "reference_name"],
- onload: function(me) {
+ onload: function (me) {
if (!frappe.route_options) {
frappe.route_options = {
- "owner": frappe.session.user,
- "status": "Open"
+ owner: frappe.session.user,
+ status: "Open",
};
}
me.page.set_title(__("To Do"));
},
button: {
- show: function(doc) {
+ show: function (doc) {
return doc.reference_name;
},
- get_label: function() {
- return __('Open');
+ get_label: function () {
+ return __("Open", null, "Access");
},
- get_description: function(doc) {
- return __('Open {0}', [`${doc.reference_type} ${doc.reference_name}`])
+ get_description: function (doc) {
+ return __("Open {0}", [`${__(doc.reference_type)}: ${doc.reference_name}`]);
+ },
+ action: function (doc) {
+ frappe.set_route("Form", doc.reference_type, doc.reference_name);
},
- action: function(doc) {
- frappe.set_route('Form', doc.reference_type, doc.reference_name);
- }
},
- refresh: function(me) {
+ refresh: function (me) {
if (me.todo_sidebar_setup) return;
// add assigned by me
- me.page.add_sidebar_item(__("Assigned By Me"), function() {
- me.filter_area.add([[me.doctype, "assigned_by", '=', frappe.session.user]]);
- }, ('.list-link[data-view="Kanban"]'));
+ me.page.add_sidebar_item(
+ __("Assigned By Me"),
+ function () {
+ me.filter_area.add([[me.doctype, "assigned_by", "=", frappe.session.user]]);
+ },
+ '.list-link[data-view="Kanban"]'
+ );
me.todo_sidebar_setup = true;
},
-}
\ No newline at end of file
+};
diff --git a/frappe/desk/doctype/workspace/test_workspace.py b/frappe/desk/doctype/workspace/test_workspace.py
index d0b0eba9e2..22665e1dd2 100644
--- a/frappe/desk/doctype/workspace/test_workspace.py
+++ b/frappe/desk/doctype/workspace/test_workspace.py
@@ -1,11 +1,10 @@
# Copyright (c) 2020, Frappe Technologies and Contributors
# License: MIT. See LICENSE
-import unittest
-
import frappe
+from frappe.tests.utils import FrappeTestCase
-class TestWorkspace(unittest.TestCase):
+class TestWorkspace(FrappeTestCase):
def setUp(self):
create_module("Test Module")
diff --git a/frappe/desk/doctype/workspace/workspace.js b/frappe/desk/doctype/workspace/workspace.js
index 3f912127fc..b5f298c477 100644
--- a/frappe/desk/doctype/workspace/workspace.js
+++ b/frappe/desk/doctype/workspace/workspace.js
@@ -1,26 +1,59 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Workspace', {
- setup: function() {
- frappe.meta.get_field('Workspace Link', 'only_for').no_default = true;
+frappe.ui.form.on("Workspace", {
+ setup: function () {
+ frappe.meta.get_field("Workspace Link", "only_for").no_default = true;
},
- refresh: function(frm) {
+ refresh: function (frm) {
frm.enable_save();
- if (frm.doc.for_user || (frm.doc.public && !frm.has_perm('write') &&
- !frappe.user.has_role('Workspace Manager'))) {
- frm.trigger('disable_form');
+ let url = `/app/${
+ frm.doc.public
+ ? frappe.router.slug(frm.doc.title)
+ : "private/" + frappe.router.slug(frm.doc.title)
+ }`;
+ frm.sidebar
+ .add_user_action(__("Go to Workspace"))
+ .attr("href", url)
+ .attr("target", "_blank");
+
+ frm.layout.message.empty();
+ let message = __(
+ "This document allows you to edit limited fields. For all kinds of workspace customization, use the Edit button located on the workspace page"
+ );
+
+ if (
+ frm.doc.for_user ||
+ (frm.doc.public &&
+ !frm.has_perm("write") &&
+ !frappe.user.has_role("Workspace Manager"))
+ ) {
+ frm.trigger("disable_form");
+
+ if (frm.doc.public) {
+ message = __("Only Workspace Manager can edit public workspaces");
+ } else {
+ message = __(
+ "We do not allow editing of this document. Simply click the Edit button on the workspace page to make your workspace editable and customize it as you wish"
+ );
+ }
}
+
+ if (frappe.boot.developer_mode) {
+ frm.set_df_property("module", "read_only", 0);
+ }
+
+ frm.layout.show_message(message);
},
- disable_form: function(frm) {
+ disable_form: function (frm) {
frm.fields
- .filter(field => field.has_input)
- .forEach(field => {
+ .filter((field) => field.has_input)
+ .forEach((field) => {
frm.set_df_property(field.df.fieldname, "read_only", "1");
});
frm.disable_save();
- }
-});
\ No newline at end of file
+ },
+});
diff --git a/frappe/desk/doctype/workspace/workspace.json b/frappe/desk/doctype/workspace/workspace.json
index 032de9de4e..2759acd228 100644
--- a/frappe/desk/doctype/workspace/workspace.json
+++ b/frappe/desk/doctype/workspace/workspace.json
@@ -19,7 +19,10 @@
"restrict_to_domain",
"hide_custom",
"public",
+ "is_hidden",
"content",
+ "number_cards_tab",
+ "number_cards",
"tab_break_2",
"charts",
"tab_break_15",
@@ -28,6 +31,8 @@
"links",
"quick_lists_tab",
"quick_lists",
+ "custom_blocks_tab",
+ "custom_blocks",
"roles_tab",
"roles"
],
@@ -71,7 +76,8 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Module",
- "options": "Module Def"
+ "options": "Module Def",
+ "read_only": 1
},
{
"fieldname": "column_break_3",
@@ -107,7 +113,8 @@
{
"fieldname": "icon",
"fieldtype": "Icon",
- "label": "Icon"
+ "label": "Icon",
+ "read_only": 1
},
{
"fieldname": "links",
@@ -122,18 +129,21 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Public",
+ "read_only": 1,
"search_index": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
+ "read_only": 1,
"reqd": 1
},
{
"fieldname": "parent_page",
"fieldtype": "Data",
- "label": "Parent Page"
+ "label": "Parent Page",
+ "read_only": 1
},
{
"default": "[]",
@@ -145,7 +155,8 @@
{
"fieldname": "sequence_id",
"fieldtype": "Float",
- "label": "Sequence Id"
+ "label": "Sequence Id",
+ "read_only": 1
},
{
"fieldname": "roles",
@@ -168,11 +179,39 @@
"fieldtype": "Table",
"label": "Quick Lists",
"options": "Workspace Quick List"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_hidden",
+ "fieldtype": "Check",
+ "label": "Is Hidden"
+ },
+ {
+ "fieldname": "number_cards_tab",
+ "fieldtype": "Tab Break",
+ "label": "Number Cards"
+ },
+ {
+ "fieldname": "number_cards",
+ "fieldtype": "Table",
+ "label": "Number Cards",
+ "options": "Workspace Number Card"
+ },
+ {
+ "fieldname": "custom_blocks_tab",
+ "fieldtype": "Tab Break",
+ "label": "Custom Blocks"
+ },
+ {
+ "fieldname": "custom_blocks",
+ "fieldtype": "Table",
+ "label": "Custom Blocks",
+ "options": "Workspace Custom Block"
}
],
"in_create": 1,
"links": [],
- "modified": "2022-05-12 13:00:03.925605",
+ "modified": "2023-05-17 14:52:38.110224",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace",
@@ -190,18 +229,10 @@
"role": "Workspace Manager",
"share": 1,
"write": 1
- },
- {
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "All",
- "share": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
- "states": []
+ "states": [],
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py
index 9fa99884fb..7b5970a7d6 100644
--- a/frappe/desk/doctype/workspace/workspace.py
+++ b/frappe/desk/doctype/workspace/workspace.py
@@ -1,6 +1,7 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# License: MIT. See LICENSE
+from collections import defaultdict
from json import loads
import frappe
@@ -9,14 +10,17 @@ from frappe.desk.desktop import save_new_widget
from frappe.desk.utils import validate_route_conflict
from frappe.model.document import Document
from frappe.model.rename_doc import rename_doc
-from frappe.modules.export_file import export_to_files
+from frappe.modules.export_file import delete_folder, export_to_files
class Workspace(Document):
def validate(self):
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"))
- validate_route_conflict(self.doctype, self.name)
+ if self.has_value_changed("title"):
+ validate_route_conflict(self.doctype, self.title)
+ else:
+ validate_route_conflict(self.doctype, self.name)
try:
if not isinstance(loads(self.content), list):
@@ -28,16 +32,43 @@ class Workspace(Document):
if disable_saving_as_public():
return
- if frappe.conf.developer_mode and self.module and self.public:
- export_to_files(record_list=[["Workspace", self.name]], record_module=self.module)
+ if frappe.conf.developer_mode and self.public:
+ if self.module:
+ export_to_files(record_list=[["Workspace", self.name]], record_module=self.module)
+
+ if self.has_value_changed("title") or self.has_value_changed("module"):
+ previous = self.get_doc_before_save()
+ if previous and previous.get("module") and previous.get("title"):
+ delete_folder(previous.get("module"), "Workspace", previous.get("title"))
+
+ def before_export(self, doc):
+ if doc.title != doc.label and doc.label == doc.name:
+ self.name = doc.name = doc.label = doc.title
+
+ def after_delete(self):
+ if disable_saving_as_public():
+ return
+
+ if self.module and frappe.conf.developer_mode:
+ delete_folder(self.module, "Workspace", self.title)
@staticmethod
- def get_module_page_map():
- pages = frappe.get_all(
- "Workspace", fields=["name", "module"], filters={"for_user": ""}, as_list=1
+ def get_module_wise_workspaces():
+ workspaces = frappe.get_all(
+ "Workspace",
+ fields=["name", "module"],
+ filters={"for_user": "", "public": 1},
+ order_by="creation",
)
- return {page[1]: page[0] for page in pages if page[1]}
+ module_workspaces = defaultdict(list)
+
+ for workspace in workspaces:
+ if not workspace.module:
+ continue
+ module_workspaces[workspace.module].append(workspace.name)
+
+ return module_workspaces
def get_link_groups(self):
cards = []
@@ -121,6 +152,7 @@ class Workspace(Document):
def disable_saving_as_public():
return (
frappe.flags.in_install
+ or frappe.flags.in_uninstall
or frappe.flags.in_patch
or frappe.flags.in_test
or frappe.flags.in_fixtures
@@ -176,7 +208,7 @@ def save_page(title, public, new_widgets, blocks):
if not public:
filters = {"for_user": frappe.session.user, "label": title + "-" + frappe.session.user}
- pages = frappe.get_list("Workspace", filters=filters)
+ pages = frappe.get_all("Workspace", filters=filters)
if pages:
doc = frappe.get_doc("Workspace", pages[0])
@@ -191,12 +223,8 @@ def save_page(title, public, new_widgets, blocks):
@frappe.whitelist()
def update_page(name, title, icon, parent, public):
public = frappe.parse_json(public)
-
doc = frappe.get_doc("Workspace", name)
- filters = {"parent_page": doc.title, "public": doc.public}
- child_docs = frappe.get_list("Workspace", filters=filters)
-
if doc:
doc.title = title
doc.icon = icon
@@ -212,6 +240,9 @@ def update_page(name, title, icon, parent, public):
rename_doc("Workspace", name, new_name, force=True, ignore_permissions=True)
# update new name and public in child pages
+ child_docs = frappe.get_all(
+ "Workspace", filters={"parent_page": doc.title, "public": doc.public}
+ )
if child_docs:
for child in child_docs:
child_doc = frappe.get_doc("Workspace", child.name)
@@ -230,6 +261,32 @@ def update_page(name, title, icon, parent, public):
return {"name": title, "public": public, "label": new_name}
+def hide_unhide_page(page_name: str, is_hidden: bool):
+ page = frappe.get_doc("Workspace", page_name)
+
+ if page.get("public") and not is_workspace_manager():
+ frappe.throw(
+ _("Need Workspace Manager role to hide/unhide public workspaces"), frappe.PermissionError
+ )
+
+ if not page.get("public") and page.get("for_user") != frappe.session.user:
+ frappe.throw(_("Cannot update private workspace of other users"), frappe.PermissionError)
+
+ page.is_hidden = int(is_hidden)
+ page.save(ignore_permissions=True)
+ return True
+
+
+@frappe.whitelist()
+def hide_page(page_name: str):
+ return hide_unhide_page(page_name, 1)
+
+
+@frappe.whitelist()
+def unhide_page(page_name: str):
+ return hide_unhide_page(page_name, 0)
+
+
@frappe.whitelist()
def duplicate_page(page_name, new_page):
if not loads(new_page):
@@ -248,6 +305,7 @@ def duplicate_page(page_name, new_page):
doc.public = new_page.get("is_public")
doc.for_user = ""
doc.label = doc.title
+ doc.module = ""
if not doc.public:
doc.for_user = doc.for_user or frappe.session.user
doc.label = f"{doc.title}-{doc.for_user}"
@@ -319,7 +377,7 @@ def last_sequence_id(doc):
if not doc_exists:
return 0
- return frappe.db.get_list(
+ return frappe.get_all(
"Workspace",
fields=["sequence_id"],
filters={"public": doc.public, "for_user": doc.for_user},
@@ -328,7 +386,7 @@ def last_sequence_id(doc):
def get_page_list(fields, filters):
- return frappe.get_list("Workspace", fields=fields, filters=filters, order_by="sequence_id asc")
+ return frappe.get_all("Workspace", fields=fields, filters=filters, order_by="sequence_id asc")
def is_workspace_manager():
diff --git a/frappe/event_streaming/doctype/__init__.py b/frappe/desk/doctype/workspace_custom_block/__init__.py
similarity index 100%
rename from frappe/event_streaming/doctype/__init__.py
rename to frappe/desk/doctype/workspace_custom_block/__init__.py
diff --git a/frappe/desk/doctype/workspace_custom_block/workspace_custom_block.json b/frappe/desk/doctype/workspace_custom_block/workspace_custom_block.json
new file mode 100644
index 0000000000..090719de3a
--- /dev/null
+++ b/frappe/desk/doctype/workspace_custom_block/workspace_custom_block.json
@@ -0,0 +1,39 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2023-05-17 14:49:19.454932",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "custom_block_name",
+ "label"
+ ],
+ "fields": [
+ {
+ "fieldname": "custom_block_name",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Custom Block Name",
+ "options": "Custom HTML Block"
+ },
+ {
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Label"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2023-05-17 14:50:45.575609",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Workspace Custom Block",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/workspace_custom_block/workspace_custom_block.py b/frappe/desk/doctype/workspace_custom_block/workspace_custom_block.py
new file mode 100644
index 0000000000..745844f5c1
--- /dev/null
+++ b/frappe/desk/doctype/workspace_custom_block/workspace_custom_block.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class WorkspaceCustomBlock(Document):
+ pass
diff --git a/frappe/event_streaming/doctype/document_type_field_mapping/__init__.py b/frappe/desk/doctype/workspace_number_card/__init__.py
similarity index 100%
rename from frappe/event_streaming/doctype/document_type_field_mapping/__init__.py
rename to frappe/desk/doctype/workspace_number_card/__init__.py
diff --git a/frappe/desk/doctype/workspace_number_card/workspace_number_card.json b/frappe/desk/doctype/workspace_number_card/workspace_number_card.json
new file mode 100644
index 0000000000..f9e3865d74
--- /dev/null
+++ b/frappe/desk/doctype/workspace_number_card/workspace_number_card.json
@@ -0,0 +1,40 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2023-02-15 01:16:26.216201",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "number_card_name",
+ "label"
+ ],
+ "fields": [
+ {
+ "fieldname": "number_card_name",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Number Card Name",
+ "options": "Number Card",
+ "reqd": 1
+ },
+ {
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Label"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2023-02-15 01:16:26.216201",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Workspace Number Card",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/workspace_number_card/workspace_number_card.py b/frappe/desk/doctype/workspace_number_card/workspace_number_card.py
new file mode 100644
index 0000000000..e972f3f525
--- /dev/null
+++ b/frappe/desk/doctype/workspace_number_card/workspace_number_card.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class WorkspaceNumberCard(Document):
+ pass
diff --git a/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.json b/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.json
index 8673e93cf7..8832d9e1f4 100644
--- a/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.json
+++ b/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.json
@@ -7,6 +7,7 @@
"field_order": [
"type",
"link_to",
+ "url",
"doc_view",
"column_break_4",
"label",
@@ -24,16 +25,16 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
- "options": "DocType\nReport\nPage\nDashboard",
+ "options": "DocType\nReport\nPage\nDashboard\nURL",
"reqd": 1
},
{
+ "depends_on": "eval:doc.type != \"URL\"",
"fieldname": "link_to",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Link To",
- "options": "type",
- "reqd": 1
+ "options": "type"
},
{
"depends_on": "eval:doc.type == \"DocType\"",
@@ -94,12 +95,20 @@
"fieldname": "format",
"fieldtype": "Data",
"label": "Format"
+ },
+ {
+ "depends_on": "eval:doc.type == \"URL\"",
+ "fieldname": "url",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "URL",
+ "options": "URL"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-01-12 13:13:17.571324",
+ "modified": "2023-04-19 13:32:31.005443",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace Shortcut",
diff --git a/frappe/desk/form/assign_to.py b/frappe/desk/form/assign_to.py
index 7853e807b8..ce8bb444a1 100644
--- a/frappe/desk/form/assign_to.py
+++ b/frappe/desk/form/assign_to.py
@@ -93,10 +93,17 @@ def add(args=None):
doc = frappe.get_doc(args["doctype"], args["name"])
- # if assignee does not have permissions, share
+ # if assignee does not have permissions, share or inform
if not frappe.has_permission(doc=doc, user=assign_to):
- frappe.share.add(doc.doctype, doc.name, assign_to)
- shared_with_users.append(assign_to)
+ if frappe.get_system_settings("disable_document_sharing"):
+ msg = _("User {0} is not permitted to access this document.").format(frappe.bold(assign_to))
+ msg += " " + _(
+ "As document sharing is disabled, please give them the required permissions before assigning."
+ )
+ frappe.throw(msg, title=_("Missing Permission"))
+ else:
+ frappe.share.add(doc.doctype, doc.name, assign_to)
+ shared_with_users.append(assign_to)
# make this document followed by assigned user
if frappe.get_cached_value("User", assign_to, "follow_assigned_documents"):
@@ -138,37 +145,38 @@ def add_multiple(args=None):
def close_all_assignments(doctype, name):
- assignments = frappe.db.get_all(
+ assignments = frappe.get_all(
"ToDo",
- fields=["allocated_to"],
+ fields=["allocated_to", "name"],
filters=dict(reference_type=doctype, reference_name=name, status=("!=", "Cancelled")),
)
if not assignments:
return False
for assign_to in assignments:
- set_status(doctype, name, assign_to.allocated_to, status="Closed")
+ set_status(doctype, name, todo=assign_to.name, assign_to=assign_to.allocated_to, status="Closed")
return True
@frappe.whitelist()
def remove(doctype, name, assign_to):
- return set_status(doctype, name, assign_to, status="Cancelled")
+ return set_status(doctype, name, "", assign_to, status="Cancelled")
-def set_status(doctype, name, assign_to, status="Cancelled"):
+def set_status(doctype, name, todo=None, assign_to=None, status="Cancelled"):
"""remove from todo"""
try:
- todo = frappe.db.get_value(
- "ToDo",
- {
- "reference_type": doctype,
- "reference_name": name,
- "allocated_to": assign_to,
- "status": ("!=", status),
- },
- )
+ if not todo:
+ todo = frappe.db.get_value(
+ "ToDo",
+ {
+ "reference_type": doctype,
+ "reference_name": name,
+ "allocated_to": assign_to,
+ "status": ("!=", status),
+ },
+ )
if todo:
todo = frappe.get_doc("ToDo", todo)
todo.status = status
@@ -189,14 +197,18 @@ def clear(doctype, name):
"""
Clears assignments, return False if not assigned.
"""
- assignments = frappe.db.get_all(
- "ToDo", fields=["allocated_to"], filters=dict(reference_type=doctype, reference_name=name)
+ assignments = frappe.get_all(
+ "ToDo",
+ fields=["allocated_to", "name"],
+ filters=dict(reference_type=doctype, reference_name=name),
)
if not assignments:
return False
for assign_to in assignments:
- set_status(doctype, name, assign_to.allocated_to, "Cancelled")
+ set_status(
+ doctype, name, todo=assign_to.name, assign_to=assign_to.allocated_to, status="Cancelled"
+ )
return True
diff --git a/frappe/desk/form/document_follow.py b/frappe/desk/form/document_follow.py
index 2e4bcedf5a..f12e44fe61 100644
--- a/frappe/desk/form/document_follow.py
+++ b/frappe/desk/form/document_follow.py
@@ -161,9 +161,14 @@ def get_document_followed_by_user(user):
def get_version(doctype, doc_name, frequency, user):
timeline = []
- filters = get_filters("docname", doc_name, frequency, user)
version = frappe.get_all(
- "Version", filters=filters, fields=["ref_doctype", "data", "modified", "modified", "modified_by"]
+ "Version",
+ filters=[
+ ["ref_doctype", "=", doctype],
+ ["docname", "=", doc_name],
+ *_get_filters(frequency, user),
+ ],
+ fields=["data", "modified", "modified_by"],
)
if version:
for v in version:
@@ -186,9 +191,14 @@ def get_comments(doctype, doc_name, frequency, user):
from frappe.core.utils import html2text
timeline = []
- filters = get_filters("reference_name", doc_name, frequency, user)
comments = frappe.get_all(
- "Comment", filters=filters, fields=["content", "modified", "modified_by", "comment_type"]
+ "Comment",
+ filters=[
+ ["reference_doctype", "=", doctype],
+ ["reference_name", "=", doc_name],
+ *_get_filters(frequency, user),
+ ],
+ fields=["content", "modified", "modified_by", "comment_type"],
)
for comment in comments:
if comment.comment_type == "Like":
@@ -306,29 +316,27 @@ def send_weekly_updates():
send_document_follow_mails("Weekly")
-def get_filters(search_by, name, frequency, user):
- filters = []
+def _get_filters(frequency, user):
+ filters = [
+ ["modified_by", "!=", user],
+ ]
if frequency == "Weekly":
- filters = [
- [search_by, "=", name],
+ filters += [
["modified", ">", frappe.utils.add_days(frappe.utils.nowdate(), -7)],
["modified", "<", frappe.utils.nowdate()],
- ["modified_by", "!=", user],
]
+
elif frequency == "Daily":
- filters = [
- [search_by, "=", name],
+ filters += [
["modified", ">", frappe.utils.add_days(frappe.utils.nowdate(), -1)],
["modified", "<", frappe.utils.nowdate()],
- ["modified_by", "!=", user],
]
+
elif frequency == "Hourly":
- filters = [
- [search_by, "=", name],
+ filters += [
["modified", ">", frappe.utils.add_to_date(frappe.utils.now_datetime(), hours=-1)],
["modified", "<", frappe.utils.now_datetime()],
- ["modified_by", "!=", user],
]
return filters
diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py
index b60c11774f..9bc7b138dd 100644
--- a/frappe/desk/form/linked_with.py
+++ b/frappe/desk/form/linked_with.py
@@ -136,7 +136,9 @@ class SubmittableDocumentTree:
def get_submittable_doctypes(self) -> list[str]:
"""Returns list of submittable doctypes."""
if not self._submittable_doctypes:
- self._submittable_doctypes = frappe.db.get_all("DocType", {"is_submittable": 1}, pluck="name")
+ self._submittable_doctypes = frappe.get_all(
+ "DocType", {"is_submittable": 1}, pluck="name", order_by=None
+ )
return self._submittable_doctypes
@@ -155,6 +157,7 @@ def get_child_tables_of_doctypes(doctypes: list[str] = None):
fields=["parent", "fieldname", "options as child_table"],
filters=filters_for_docfield,
as_list=1,
+ order_by=None,
)
links += frappe.get_all(
@@ -162,6 +165,7 @@ def get_child_tables_of_doctypes(doctypes: list[str] = None):
fields=["dt as parent", "fieldname", "options as child_table"],
filters=filters_for_customfield,
as_list=1,
+ order_by=None,
)
child_tables_by_doctype = defaultdict(list)
@@ -275,6 +279,7 @@ def get_references_across_doctypes_by_dynamic_link_field(
fields=["parent as doctype", "fieldname", "options as doctype_fieldname"],
filters=filters_for_docfield,
as_list=1,
+ order_by=None,
)
links += frappe.get_all(
@@ -282,15 +287,14 @@ def get_references_across_doctypes_by_dynamic_link_field(
fields=["dt as doctype", "fieldname", "options as doctype_fieldname"],
filters=filters_for_customfield,
as_list=1,
+ order_by=None,
)
links_by_doctype = defaultdict(list)
for doctype, fieldname, doctype_fieldname in links:
try:
filters = [[doctype_fieldname, "in", to_doctypes]] if to_doctypes else []
- for linked_to in frappe.db.get_all(
- doctype, pluck=doctype_fieldname, filters=filters, distinct=1
- ):
+ for linked_to in frappe.get_all(doctype, pluck=doctype_fieldname, filters=filters, distinct=1):
if linked_to:
links_by_doctype[linked_to].append(
{"doctype": doctype, "fieldname": fieldname, "doctype_fieldname": doctype_fieldname}
@@ -330,17 +334,21 @@ def get_referencing_documents(
if not link_info.get("is_child"):
filters.extend(parent_filters or [])
- return {from_table: frappe.db.get_all(from_table, filters, pluck="name")}
+ return {from_table: frappe.get_all(from_table, filters, pluck="name", order_by=None)}
filters.extend(child_filters or [])
- res = frappe.db.get_all(from_table, filters=filters, fields=["name", "parenttype", "parent"])
+ res = frappe.get_all(
+ from_table, filters=filters, fields=["name", "parenttype", "parent"], order_by=None
+ )
documents = defaultdict(list)
for parent, rows in itertools.groupby(res, key=lambda row: row["parenttype"]):
if allowed_parents and parent not in allowed_parents:
continue
filters = (parent_filters or []) + [["name", "in", tuple(row.parent for row in rows)]]
- documents[parent].extend(frappe.db.get_all(parent, filters=filters, pluck="name") or [])
+ documents[parent].extend(
+ frappe.get_all(parent, filters=filters, pluck="name", order_by=None) or []
+ )
return documents
@@ -405,7 +413,6 @@ def get_exempted_doctypes():
return auto_cancel_exempt_doctypes
-@frappe.whitelist()
def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> dict[str, list]:
if isinstance(linkinfo, str):
# additional fields are added in linkinfo
@@ -428,9 +435,6 @@ def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> di
continue
linkmeta = link_meta_bundle[0]
- if not linkmeta.has_permission():
- continue
-
if not linkmeta.get("issingle"):
fields = [
d.fieldname
@@ -450,7 +454,7 @@ def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> di
try:
if link.get("filters"):
- ret = frappe.get_all(doctype=dt, fields=fields, filters=link.get("filters"))
+ ret = frappe.get_all(doctype=dt, fields=fields, filters=link.get("filters"), order_by=None)
elif link.get("get_parent"):
ret = None
@@ -459,9 +463,11 @@ def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> di
if not frappe.get_meta(doctype).istable:
continue
- me = frappe.db.get_value(doctype, name, ["parenttype", "parent"], as_dict=True)
+ me = frappe.db.get_value(doctype, name, ["parenttype", "parent"], as_dict=True, order_by=None)
if me and me.parenttype == dt:
- ret = frappe.get_all(doctype=dt, fields=fields, filters=[[dt, "name", "=", me.parent]])
+ ret = frappe.get_all(
+ doctype=dt, fields=fields, filters=[[dt, "name", "=", me.parent]], order_by=None
+ )
elif link.get("child_doctype"):
or_filters = [
@@ -474,7 +480,12 @@ def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> di
filters.append([link.get("child_doctype"), link.get("doctype_fieldname"), "=", doctype])
ret = frappe.get_all(
- doctype=dt, fields=fields, filters=filters, or_filters=or_filters, distinct=True
+ doctype=dt,
+ fields=fields,
+ filters=filters,
+ or_filters=or_filters,
+ distinct=True,
+ order_by=None,
)
else:
@@ -486,7 +497,9 @@ def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> di
# dynamic link
if link.get("doctype_fieldname"):
filters.append([dt, link.get("doctype_fieldname"), "=", doctype])
- ret = frappe.get_all(doctype=dt, fields=fields, filters=filters, or_filters=or_filters)
+ ret = frappe.get_all(
+ doctype=dt, fields=fields, filters=filters, or_filters=or_filters, order_by=None
+ )
else:
ret = None
diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py
index 898c6461e9..42109f8863 100644
--- a/frappe/desk/form/load.py
+++ b/frappe/desk/form/load.py
@@ -11,6 +11,7 @@ import frappe.share
import frappe.utils
from frappe import _, _dict
from frappe.desk.form.document_follow import is_document_followed
+from frappe.model.utils import is_virtual_doctype
from frappe.model.utils.user_settings import get_user_settings
from frappe.permissions import get_doc_permissions
from frappe.utils.data import cstr
@@ -27,14 +28,10 @@ def getdoc(doctype, name, user=None):
if not (doctype and name):
raise Exception("doctype and name required!")
- if not name:
- name = doctype
-
- if not frappe.db.exists(doctype, name):
+ if not is_virtual_doctype(doctype) and not frappe.db.exists(doctype, name):
return []
doc = frappe.get_doc(doctype, name)
- run_onload(doc)
if not doc.has_permission("read"):
frappe.flags.error_message = _("Insufficient Permission for {0}").format(
@@ -42,6 +39,7 @@ def getdoc(doctype, name, user=None):
)
raise frappe.PermissionError(("read", doctype, name))
+ run_onload(doc)
doc.apply_fieldlevel_read_permissions()
# add file list
@@ -63,11 +61,9 @@ def getdoctype(doctype, with_parent=False, cached_timestamp=None):
parent_dt = None
# with parent (called from report builder)
- if with_parent:
- parent_dt = frappe.model.meta.get_parent_dt(doctype)
- if parent_dt:
- docs = get_meta_bundle(parent_dt)
- frappe.response["parent_dt"] = parent_dt
+ if with_parent and (parent_dt := frappe.model.meta.get_parent_dt(doctype)):
+ docs = get_meta_bundle(parent_dt)
+ frappe.response["parent_dt"] = parent_dt
if not docs:
docs = get_meta_bundle(doctype)
@@ -109,6 +105,8 @@ def get_docinfo(doc=None, doctype=None, name=None):
docinfo.update(
{
+ "doctype": doc.doctype,
+ "name": doc.name,
"attachments": get_attachments(doc.doctype, doc.name),
"communications": communications_except_auto_messages,
"automated_messages": automated_messages,
@@ -177,7 +175,7 @@ def add_comments(doc, docinfo):
def get_milestones(doctype, name):
- return frappe.db.get_all(
+ return frappe.get_all(
"Milestone",
fields=["creation", "owner", "track_field", "value"],
filters=dict(reference_type=doctype, reference_name=name),
@@ -248,7 +246,7 @@ def get_comments(
def get_point_logs(doctype, docname):
- return frappe.db.get_all(
+ return frappe.get_all(
"Energy Point Log",
filters={"reference_doctype": doctype, "reference_name": docname, "type": ["!=", "Review"]},
fields=["*"],
@@ -372,7 +370,7 @@ def run_onload(doc):
def get_view_logs(doctype, docname):
"""get and return the latest view logs if available"""
logs = []
- if hasattr(frappe.get_meta(doctype), "track_views") and frappe.get_meta(doctype).track_views:
+ if getattr(frappe.get_meta(doctype), "track_views", None):
view_logs = frappe.get_all(
"View Log",
filters={
@@ -405,7 +403,7 @@ def get_document_email(doctype, name):
return None
email = email.split("@")
- return f"{email[0]}+{quote(doctype)}+{quote(cstr(name))}@{email[1]}"
+ return f"{email[0]}+{quote(doctype)}={quote(cstr(name))}@{email[1]}"
def get_automatic_email_link():
@@ -451,7 +449,9 @@ def get_title_values_for_link_and_dynamic_link_fields(doc, link_fields=None):
if not meta or not (meta.title_field and meta.show_title_field_in_link):
continue
- link_title = frappe.db.get_value(doctype, doc.get(field.fieldname), meta.title_field, cache=True)
+ link_title = frappe.db.get_value(
+ doctype, doc.get(field.fieldname), meta.title_field, cache=True, order_by=None
+ )
link_titles.update({doctype + "::" + doc.get(field.fieldname): link_title})
return link_titles
diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py
index 5a426b4c63..62a9c89c81 100644
--- a/frappe/desk/form/meta.py
+++ b/frappe/desk/form/meta.py
@@ -4,12 +4,14 @@ import io
import os
import frappe
+from frappe import _
from frappe.build import scrub_html_template
from frappe.model.meta import Meta
from frappe.model.utils import render_include
from frappe.modules import get_module_path, load_doctype_module, scrub
from frappe.translate import extract_messages_from_code, make_dict_from_messages
from frappe.utils import get_html_format
+from frappe.utils.data import get_link_to_form
ASSET_KEYS = (
"__js",
@@ -35,12 +37,10 @@ ASSET_KEYS = (
def get_meta(doctype, cached=True):
# don't cache for developer mode as js files, templates may be edited
if cached and not frappe.conf.developer_mode:
- meta = frappe.cache().hget("form_meta", doctype)
- if meta:
- meta = FormMeta(meta)
- else:
+ meta = frappe.cache().hget("doctype_form_meta", doctype)
+ if not meta:
meta = FormMeta(doctype)
- frappe.cache().hset("form_meta", doctype, meta.as_dict())
+ frappe.cache().hset("doctype_form_meta", doctype, meta)
else:
meta = FormMeta(doctype)
@@ -52,7 +52,7 @@ def get_meta(doctype, cached=True):
class FormMeta(Meta):
def __init__(self, doctype):
- super().__init__(doctype)
+ self.__dict__.update(frappe.get_meta(doctype).__dict__)
self.load_assets()
def load_assets(self):
@@ -134,7 +134,7 @@ class FormMeta(Meta):
for fname in os.listdir(path):
if fname.endswith(".html"):
with open(os.path.join(path, fname), encoding="utf-8") as f:
- templates[fname.split(".")[0]] = scrub_html_template(f.read())
+ templates[fname.split(".", 1)[0]] = scrub_html_template(f.read())
self.set("__templates", templates or None)
@@ -146,7 +146,7 @@ class FormMeta(Meta):
"""embed all require files"""
# custom script
client_scripts = (
- frappe.db.get_all(
+ frappe.get_all(
"Client Script",
filters={"dt": self.name, "enabled": 1},
fields=["name", "script", "view"],
@@ -158,6 +158,9 @@ class FormMeta(Meta):
list_script = ""
form_script = ""
for script in client_scripts:
+ if not script.script:
+ continue
+
if script.view == "List":
list_script += f"""
// {script.name}
@@ -165,7 +168,7 @@ class FormMeta(Meta):
"""
- if script.view == "Form":
+ elif script.view == "Form":
form_script += f"""
// {script.name}
{script.script}
@@ -183,19 +186,40 @@ class FormMeta(Meta):
"""add search fields found in the doctypes indicated by link fields' options"""
for df in self.get("fields", {"fieldtype": "Link", "options": ["!=", "[Select]"]}):
if df.options:
- search_fields = frappe.get_meta(df.options).search_fields
+ try:
+ search_fields = frappe.get_meta(df.options).search_fields
+ except frappe.DoesNotExistError:
+ self._show_missing_doctype_msg(df)
+
if search_fields:
search_fields = search_fields.split(",")
df.search_fields = [sf.strip() for sf in search_fields]
+ def _show_missing_doctype_msg(self, df):
+ # A link field is referring to non-existing doctype, this usually happens when
+ # customizations are removed or some custom app is removed but hasn't cleaned
+ # up after itself.
+ frappe.clear_last_message()
+
+ msg = _("Field {0} is referring to non-existing doctype {1}.").format(
+ frappe.bold(df.fieldname), frappe.bold(df.options)
+ )
+
+ if df.get("is_custom_field"):
+ custom_field_link = get_link_to_form("Custom Field", df.name)
+ msg += " " + _("Please delete the field from {0} or add the required doctype.").format(
+ custom_field_link
+ )
+
+ frappe.throw(msg, title=_("Missing DocType"))
+
def add_linked_document_type(self):
for df in self.get("fields", {"fieldtype": "Link"}):
if df.options:
try:
df.linked_document_type = frappe.get_meta(df.options).document_type
except frappe.DoesNotExistError:
- # edge case where options="[Select]"
- pass
+ self._show_missing_doctype_msg(df)
def load_print_formats(self):
print_formats = frappe.db.sql(
@@ -225,7 +249,7 @@ class FormMeta(Meta):
def load_templates(self):
if not self.custom:
module = load_doctype_module(self.name)
- app = module.__name__.split(".")[0]
+ app = module.__name__.split(".", 1)[0]
templates = {}
if hasattr(module, "form_grid_templates"):
for key, path in module.form_grid_templates.items():
diff --git a/frappe/desk/form/save.py b/frappe/desk/form/save.py
index f3e7b6294f..9ee2541a90 100644
--- a/frappe/desk/form/save.py
+++ b/frappe/desk/form/save.py
@@ -4,7 +4,11 @@
import json
import frappe
+from frappe.core.doctype.submission_queue.submission_queue import queue_submission
from frappe.desk.form.load import run_onload
+from frappe.model.docstatus import DocStatus
+from frappe.monitor import add_data_to_monitor
+from frappe.utils.scheduler import is_scheduler_inactive
@frappe.whitelist()
@@ -14,9 +18,17 @@ def savedocs(doc, action):
set_local_name(doc)
# action
- doc.docstatus = {"Save": 0, "Submit": 1, "Update": 1, "Cancel": 2}[action]
+ doc.docstatus = {
+ "Save": DocStatus.draft(),
+ "Submit": DocStatus.submitted(),
+ "Update": DocStatus.submitted(),
+ "Cancel": DocStatus.cancelled(),
+ }[action]
- if doc.docstatus == 1:
+ if doc.docstatus.is_submitted():
+ if action == "Submit" and doc.meta.queue_in_background and not is_scheduler_inactive():
+ queue_submission(doc, action)
+ return
doc.submit()
else:
doc.save()
@@ -25,6 +37,7 @@ def savedocs(doc, action):
run_onload(doc)
send_updated_docs(doc)
+ add_data_to_monitor(doctype=doc.doctype, action=action)
frappe.msgprint(frappe._("Saved"), indicator="green", alert=True)
diff --git a/frappe/desk/form/test_form.py b/frappe/desk/form/test_form.py
index c05a932241..f256b03d27 100644
--- a/frappe/desk/form/test_form.py
+++ b/frappe/desk/form/test_form.py
@@ -1,13 +1,12 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
-import unittest
-
import frappe
from frappe.desk.form.linked_with import get_linked_docs, get_linked_doctypes
+from frappe.tests.utils import FrappeTestCase
-class TestForm(unittest.TestCase):
+class TestForm(FrappeTestCase):
def test_linked_with(self):
results = get_linked_docs("Role", "System Manager", linkinfo=get_linked_doctypes("Role"))
self.assertTrue("User" in results)
@@ -15,5 +14,7 @@ class TestForm(unittest.TestCase):
if __name__ == "__main__":
+ import unittest
+
frappe.connect()
unittest.main()
diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py
index 9e10ced912..28377572c3 100644
--- a/frappe/desk/form/utils.py
+++ b/frappe/desk/form/utils.py
@@ -57,7 +57,14 @@ def update_comment(name, content):
if frappe.session.user not in ["Administrator", doc.owner]:
frappe.throw(_("Comment can only be edited by the owner"), frappe.PermissionError)
- doc.content = content
+ if doc.reference_doctype and doc.reference_name:
+ reference_doc = frappe.get_doc(doc.reference_doctype, doc.reference_name)
+ reference_doc.check_permission()
+
+ doc.content = extract_images_from_html(reference_doc, content, is_private=True)
+ else:
+ doc.content = content
+
doc.save(ignore_permissions=True)
diff --git a/frappe/desk/leaderboard.py b/frappe/desk/leaderboard.py
index a5f5de3117..65d6aaf785 100644
--- a/frappe/desk/leaderboard.py
+++ b/frappe/desk/leaderboard.py
@@ -16,7 +16,7 @@ def get_leaderboards():
@frappe.whitelist()
def get_energy_point_leaderboard(date_range, company=None, field=None, limit=None):
- all_users = frappe.db.get_all(
+ all_users = frappe.get_all(
"User",
filters={
"name": ["not in", ["Administrator", "Guest"]],
@@ -31,7 +31,7 @@ def get_energy_point_leaderboard(date_range, company=None, field=None, limit=Non
if date_range:
date_range = frappe.parse_json(date_range)
filters.append(["creation", "between", [date_range[0], date_range[1]]])
- energy_point_users = frappe.db.get_all(
+ energy_point_users = frappe.get_all(
"Energy Point Log",
fields=["user as name", "sum(points) as value"],
filters=filters,
diff --git a/frappe/desk/listview.py b/frappe/desk/listview.py
index ea6eb6259c..05d45ad9ac 100644
--- a/frappe/desk/listview.py
+++ b/frappe/desk/listview.py
@@ -36,7 +36,7 @@ def get_group_by_count(doctype: str, current_filters: str, field: str) -> list[d
ToDo = DocType("ToDo")
User = DocType("User")
count = Count("*").as_("count")
- filtered_records = frappe.qb.engine.build_conditions(doctype, current_filters).select("name")
+ filtered_records = frappe.qb.get_query(doctype, filters=current_filters, fields=["name"])
return (
frappe.qb.from_(ToDo)
diff --git a/frappe/desk/moduleview.py b/frappe/desk/moduleview.py
deleted file mode 100644
index 913b3406e3..0000000000
--- a/frappe/desk/moduleview.py
+++ /dev/null
@@ -1,615 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: MIT. See LICENSE
-
-import json
-
-import frappe
-from frappe import _
-from frappe.boot import get_allowed_pages, get_allowed_reports
-from frappe.cache_manager import (
- build_domain_restriced_doctype_cache,
- build_domain_restriced_page_cache,
- build_table_count_cache,
-)
-from frappe.desk.doctype.desktop_icon.desktop_icon import clear_desktop_icons_cache, set_hidden
-
-
-@frappe.whitelist()
-def get(module):
- """Returns data (sections, list of reports, counts) to render module view in desk:
- `/desk/#Module/[name]`."""
- data = get_data(module)
-
- out = {"data": data}
-
- return out
-
-
-@frappe.whitelist()
-def hide_module(module):
- set_hidden(module, frappe.session.user, 1)
- clear_desktop_icons_cache()
-
-
-def get_table_with_counts():
- counts = frappe.cache().get_value("information_schema:counts")
- if counts:
- return counts
- else:
- return build_table_count_cache()
-
-
-def get_data(module, build=True):
- """Get module data for the module view `desk/#Module/[name]`"""
- doctype_info = get_doctype_info(module)
- data = build_config_from_file(module)
-
- if not data:
- data = build_standard_config(module, doctype_info)
- else:
- add_custom_doctypes(data, doctype_info)
-
- add_section(data, _("Custom Reports"), "fa fa-list-alt", get_report_list(module))
-
- data = combine_common_sections(data)
- data = apply_permissions(data)
-
- # set_last_modified(data)
-
- if build:
- exists_cache = get_table_with_counts()
-
- def doctype_contains_a_record(name):
- exists = exists_cache.get(name)
- if not exists:
- if not frappe.db.get_value("DocType", name, "issingle"):
- exists = frappe.db.count(name)
- else:
- exists = True
- exists_cache[name] = exists
- return exists
-
- for section in data:
- for item in section["items"]:
- # Onboarding
-
- # First disable based on exists of depends_on list
- doctype = item.get("doctype")
- dependencies = item.get("dependencies") or None
- if not dependencies and doctype:
- item["dependencies"] = [doctype]
-
- dependencies = item.get("dependencies")
- if dependencies:
- incomplete_dependencies = [d for d in dependencies if not doctype_contains_a_record(d)]
- if len(incomplete_dependencies):
- item["incomplete_dependencies"] = incomplete_dependencies
-
- if item.get("onboard"):
- # Mark Spotlights for initial
- if item.get("type") == "doctype":
- name = item.get("name")
- count = doctype_contains_a_record(name)
-
- item["count"] = count
-
- return data
-
-
-def build_config_from_file(module):
- """Build module info from `app/config/desktop.py` files."""
- data = []
- module = frappe.scrub(module)
-
- for app in frappe.get_installed_apps():
- try:
- data += get_config(app, module)
- except ImportError:
- pass
-
- return filter_by_restrict_to_domain(data)
-
-
-def filter_by_restrict_to_domain(data):
- """filter Pages and DocType depending on the Active Module(s)"""
- doctypes = (
- frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache()
- )
- pages = frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache()
-
- for d in data:
- _items = []
- for item in d.get("items", []):
-
- item_type = item.get("type")
- item_name = item.get("name")
-
- if (item_name in pages) or (item_name in doctypes) or item_type == "report":
- _items.append(item)
-
- d.update({"items": _items})
-
- return data
-
-
-def build_standard_config(module, doctype_info):
- """Build standard module data from DocTypes."""
- if not frappe.db.get_value("Module Def", module):
- frappe.throw(_("Module Not Found"))
-
- data = []
-
- add_section(
- data,
- _("Documents"),
- "fa fa-star",
- [d for d in doctype_info if d.document_type in ("Document", "Transaction")],
- )
-
- add_section(
- data,
- _("Setup"),
- "fa fa-cog",
- [d for d in doctype_info if d.document_type in ("Master", "Setup", "")],
- )
-
- add_section(data, _("Standard Reports"), "fa fa-list", get_report_list(module, is_standard="Yes"))
-
- return data
-
-
-def add_section(data, label, icon, items):
- """Adds a section to the module data."""
- if not items:
- return
- data.append({"label": label, "icon": icon, "items": items})
-
-
-def add_custom_doctypes(data, doctype_info):
- """Adds Custom DocTypes to modules setup via `config/desktop.py`."""
- add_section(
- data,
- _("Documents"),
- "fa fa-star",
- [d for d in doctype_info if (d.custom and d.document_type in ("Document", "Transaction"))],
- )
-
- add_section(
- data,
- _("Setup"),
- "fa fa-cog",
- [d for d in doctype_info if (d.custom and d.document_type in ("Setup", "Master", ""))],
- )
-
-
-def get_doctype_info(module):
- """Returns list of non child DocTypes for given module."""
- active_domains = frappe.get_active_domains()
-
- doctype_info = frappe.get_all(
- "DocType",
- filters={"module": module, "istable": 0},
- or_filters={"ifnull(restrict_to_domain, '')": "", "restrict_to_domain": ("in", active_domains)},
- fields=["'doctype' as type", "name", "description", "document_type", "custom", "issingle"],
- order_by="custom asc, document_type desc, name asc",
- )
-
- for d in doctype_info:
- d.document_type = d.document_type or ""
- d.description = _(d.description or "")
-
- return doctype_info
-
-
-def combine_common_sections(data):
- """Combine sections declared in separate apps."""
- sections = []
- sections_dict = {}
- for each in data:
- if each["label"] not in sections_dict:
- sections_dict[each["label"]] = each
- sections.append(each)
- else:
- sections_dict[each["label"]]["items"] += each["items"]
-
- return sections
-
-
-def apply_permissions(data):
- default_country = frappe.db.get_default("country")
-
- user = frappe.get_user()
- user.build_permissions()
-
- allowed_pages = get_allowed_pages()
- allowed_reports = get_allowed_reports()
-
- new_data = []
- for section in data:
- new_items = []
-
- for item in section.get("items") or []:
- item = frappe._dict(item)
-
- if item.country and item.country != default_country:
- continue
-
- if (
- (item.type == "doctype" and item.name in user.can_read)
- or (item.type == "page" and item.name in allowed_pages)
- or (item.type == "report" and item.name in allowed_reports)
- or item.type == "help"
- ):
-
- new_items.append(item)
-
- if new_items:
- new_section = section.copy()
- new_section["items"] = new_items
- new_data.append(new_section)
-
- return new_data
-
-
-def get_disabled_reports():
- if not hasattr(frappe.local, "disabled_reports"):
- frappe.local.disabled_reports = {r.name for r in frappe.get_all("Report", {"disabled": 1})}
- return frappe.local.disabled_reports
-
-
-def get_config(app, module):
- """Load module info from `[app].config.[module]`."""
- config = frappe.get_module(f"{app}.config.{module}")
- config = config.get_data()
-
- sections = [s for s in config if s.get("condition", True)]
-
- disabled_reports = get_disabled_reports()
- for section in sections:
- items = []
- for item in section["items"]:
- if item["type"] == "report" and item["name"] in disabled_reports:
- continue
- # some module links might not have name
- if not item.get("name"):
- item["name"] = item.get("label")
- if not item.get("label"):
- item["label"] = _(item.get("name"))
- items.append(item)
- section["items"] = items
-
- return sections
-
-
-def config_exists(app, module):
- try:
- frappe.get_module(f"{app}.config.{module}")
- return True
- except ImportError:
- return False
-
-
-def add_setup_section(config, app, module, label, icon):
- """Add common sections to `/desk#Module/Setup`"""
- try:
- setup_section = get_setup_section(app, module, label, icon)
- if setup_section:
- config.append(setup_section)
- except ImportError:
- pass
-
-
-def get_setup_section(app, module, label, icon):
- """Get the setup section from each module (for global Setup page)."""
- config = get_config(app, module)
- for section in config:
- if section.get("label") == _("Setup"):
- return {"label": label, "icon": icon, "items": section["items"]}
-
-
-def get_onboard_items(app, module):
- try:
- sections = get_config(app, module)
- except ImportError:
- return []
-
- onboard_items = []
- fallback_items = []
-
- if not sections:
- doctype_info = get_doctype_info(module)
- sections = build_standard_config(module, doctype_info)
-
- for section in sections:
- for item in section["items"]:
- if item.get("onboard", 0) == 1:
- onboard_items.append(item)
-
- # in case onboard is not set
- fallback_items.append(item)
-
- if len(onboard_items) > 5:
- return onboard_items
-
- return onboard_items or fallback_items
-
-
-@frappe.whitelist()
-def get_links_for_module(app, module):
- return [{"value": l.get("name"), "label": l.get("label")} for l in get_links(app, module)]
-
-
-def get_links(app, module):
- try:
- sections = get_config(app, frappe.scrub(module))
- except ImportError:
- return []
-
- links = []
- for section in sections:
- for item in section["items"]:
- links.append(item)
- return links
-
-
-@frappe.whitelist()
-def get_desktop_settings():
- from frappe.config import get_modules_from_all_apps_for_user
-
- all_modules = get_modules_from_all_apps_for_user()
- home_settings = get_home_settings()
-
- modules_by_name = {}
- for m in all_modules:
- modules_by_name[m["module_name"]] = m
-
- module_categories = ["Modules", "Domains", "Places", "Administration"]
- user_modules_by_category = {}
-
- user_saved_modules_by_category = home_settings.modules_by_category or {}
- user_saved_links_by_module = home_settings.links_by_module or {}
-
- def apply_user_saved_links(module):
- module = frappe._dict(module)
- all_links = get_links(module.app, module.module_name)
- module_links_by_name = {}
- for link in all_links:
- module_links_by_name[link["name"]] = link
-
- if module.module_name in user_saved_links_by_module:
- user_links = frappe.parse_json(user_saved_links_by_module[module.module_name])
- module.links = [module_links_by_name[l] for l in user_links if l in module_links_by_name]
-
- return module
-
- for category in module_categories:
- if category in user_saved_modules_by_category:
- user_modules = user_saved_modules_by_category[category]
- user_modules_by_category[category] = [
- apply_user_saved_links(modules_by_name[m]) for m in user_modules if modules_by_name.get(m)
- ]
- else:
- user_modules_by_category[category] = [
- apply_user_saved_links(m) for m in all_modules if m.get("category") == category
- ]
-
- # filter out hidden modules
- if home_settings.hidden_modules:
- for category in user_modules_by_category:
- hidden_modules = home_settings.hidden_modules or []
- modules = user_modules_by_category[category]
- user_modules_by_category[category] = [
- module for module in modules if module.module_name not in hidden_modules
- ]
-
- return user_modules_by_category
-
-
-@frappe.whitelist()
-def update_hidden_modules(category_map):
- category_map = frappe.parse_json(category_map)
- home_settings = get_home_settings()
-
- saved_hidden_modules = home_settings.hidden_modules or []
-
- for category in category_map:
- config = frappe._dict(category_map[category])
- saved_hidden_modules += config.removed or []
- saved_hidden_modules = [d for d in saved_hidden_modules if d not in (config.added or [])]
-
- if home_settings.get("modules_by_category") and home_settings.modules_by_category.get(category):
- module_placement = [
- d for d in (config.added or []) if d not in home_settings.modules_by_category[category]
- ]
- home_settings.modules_by_category[category] += module_placement
-
- home_settings.hidden_modules = saved_hidden_modules
- set_home_settings(home_settings)
-
- return get_desktop_settings()
-
-
-@frappe.whitelist()
-def update_global_hidden_modules(modules):
- modules = frappe.parse_json(modules)
- frappe.only_for("System Manager")
-
- doc = frappe.get_doc("User", "Administrator")
- doc.set("block_modules", [])
- for module in modules:
- doc.append("block_modules", {"module": module})
-
- doc.save(ignore_permissions=True)
-
- return get_desktop_settings()
-
-
-@frappe.whitelist()
-def update_modules_order(module_category, modules):
- modules = frappe.parse_json(modules)
- home_settings = get_home_settings()
-
- home_settings.modules_by_category = home_settings.modules_by_category or {}
- home_settings.modules_by_category[module_category] = modules
-
- set_home_settings(home_settings)
-
-
-@frappe.whitelist()
-def update_links_for_module(module_name, links):
- links = frappe.parse_json(links)
- home_settings = get_home_settings()
-
- home_settings.setdefault("links_by_module", {})
- home_settings["links_by_module"].setdefault(module_name, None)
- home_settings["links_by_module"][module_name] = links
-
- set_home_settings(home_settings)
-
- return get_desktop_settings()
-
-
-@frappe.whitelist()
-def get_options_for_show_hide_cards():
- global_options = []
-
- if "System Manager" in frappe.get_roles():
- global_options = get_options_for_global_modules()
-
- return {"user_options": get_options_for_user_blocked_modules(), "global_options": global_options}
-
-
-@frappe.whitelist()
-def get_options_for_global_modules():
- from frappe.config import get_modules_from_all_apps
-
- all_modules = get_modules_from_all_apps()
-
- blocked_modules = frappe.get_doc("User", "Administrator").get_blocked_modules()
-
- options = []
- for module in all_modules:
- module = frappe._dict(module)
- options.append(
- {
- "category": module.category,
- "label": module.label,
- "value": module.module_name,
- "checked": module.module_name not in blocked_modules,
- }
- )
-
- return options
-
-
-@frappe.whitelist()
-def get_options_for_user_blocked_modules():
- from frappe.config import get_modules_from_all_apps_for_user
-
- all_modules = get_modules_from_all_apps_for_user()
- home_settings = get_home_settings()
-
- hidden_modules = home_settings.hidden_modules or []
-
- options = []
- for module in all_modules:
- module = frappe._dict(module)
- options.append(
- {
- "category": module.category,
- "label": module.label,
- "value": module.module_name,
- "checked": module.module_name not in hidden_modules,
- }
- )
-
- return options
-
-
-def set_home_settings(home_settings):
- frappe.cache().hset("home_settings", frappe.session.user, home_settings)
- frappe.db.set_value("User", frappe.session.user, "home_settings", json.dumps(home_settings))
-
-
-@frappe.whitelist()
-def get_home_settings():
- def get_from_db():
- settings = frappe.db.get_value("User", frappe.session.user, "home_settings")
- return frappe.parse_json(settings or "{}")
-
- home_settings = frappe.cache().hget("home_settings", frappe.session.user, get_from_db)
- return home_settings
-
-
-def get_module_link_items_from_list(app, module, list_of_link_names):
- try:
- sections = get_config(app, frappe.scrub(module))
- except ImportError:
- return []
-
- links = []
- for section in sections:
- for item in section["items"]:
- if item.get("label", "") in list_of_link_names:
- links.append(item)
-
- return links
-
-
-def set_last_modified(data):
- for section in data:
- for item in section["items"]:
- if item["type"] == "doctype":
- item["last_modified"] = get_last_modified(item["name"])
-
-
-def get_last_modified(doctype):
- def _get():
- try:
- last_modified = frappe.get_all(
- doctype, fields=["max(modified)"], as_list=True, limit_page_length=1
- )[0][0]
- except Exception as e:
- if frappe.db.is_table_missing(e):
- last_modified = None
- else:
- raise
-
- # hack: save as -1 so that it is cached
- if last_modified is None:
- last_modified = -1
-
- return last_modified
-
- last_modified = frappe.cache().hget("last_modified", doctype, _get)
-
- if last_modified == -1:
- last_modified = None
-
- return last_modified
-
-
-def get_report_list(module, is_standard="No"):
- """Returns list on new style reports for modules."""
- reports = frappe.get_list(
- "Report",
- fields=["name", "ref_doctype", "report_type"],
- filters={"is_standard": is_standard, "disabled": 0, "module": module},
- order_by="name",
- )
-
- out = []
- for r in reports:
- out.append(
- {
- "type": "report",
- "doctype": r.ref_doctype,
- "is_query_report": 1
- if r.report_type in ("Query Report", "Script Report", "Custom Report")
- else 0,
- "label": _(r.name),
- "name": r.name,
- }
- )
-
- return out
diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py
index 2a987f5539..271f2b4074 100644
--- a/frappe/desk/notifications.py
+++ b/frappe/desk/notifications.py
@@ -3,10 +3,19 @@
import json
+from bs4 import BeautifulSoup
+
import frappe
+from frappe import _
+from frappe.desk.doctype.notification_log.notification_log import (
+ enqueue_create_notification,
+ get_title,
+ get_title_html,
+)
from frappe.desk.doctype.notification_settings.notification_settings import (
get_subscribed_documents,
)
+from frappe.utils import get_fullname
@frappe.whitelist()
@@ -146,8 +155,6 @@ def clear_notifications(user=None):
else:
cache.delete_key("notification_count:" + name)
- frappe.publish_realtime("clear_notifications")
-
def clear_notification_config(user):
frappe.cache().hdel("notification_config", user)
@@ -155,7 +162,6 @@ def clear_notification_config(user):
def delete_notification_count_for(doctype):
frappe.cache().delete_key("notification_count:" + doctype)
- frappe.publish_realtime("clear_notifications")
def clear_doctype_notifications(doc, method=None, *args, **kwargs):
@@ -298,3 +304,56 @@ def get_open_count(doctype, name, items=None):
out["timeline_data"] = module.get_timeline_data(doctype, name)
return out
+
+
+def notify_mentions(ref_doctype, ref_name, content):
+ if ref_doctype and ref_name and content:
+ mentions = extract_mentions(content)
+
+ if not mentions:
+ return
+
+ sender_fullname = get_fullname(frappe.session.user)
+ title = get_title(ref_doctype, ref_name)
+
+ recipients = [
+ frappe.db.get_value(
+ "User",
+ {"enabled": 1, "name": name, "user_type": "System User", "allowed_in_mentions": 1},
+ "email",
+ )
+ for name in mentions
+ ]
+
+ notification_message = _("""{0} mentioned you in a comment in {1} {2}""").format(
+ frappe.bold(sender_fullname), frappe.bold(ref_doctype), get_title_html(title)
+ )
+
+ notification_doc = {
+ "type": "Mention",
+ "document_type": ref_doctype,
+ "document_name": ref_name,
+ "subject": notification_message,
+ "from_user": frappe.session.user,
+ "email_content": content,
+ }
+
+ enqueue_create_notification(recipients, notification_doc)
+
+
+def extract_mentions(txt):
+ """Find all instances of @mentions in the html."""
+ soup = BeautifulSoup(txt, "html.parser")
+ emails = []
+ for mention in soup.find_all(class_="mention"):
+ if mention.get("data-is-group") == "true":
+ try:
+ user_group = frappe.get_cached_doc("User Group", mention["data-id"])
+ emails += [d.user for d in user_group.user_group_members]
+ except frappe.DoesNotExistError:
+ pass
+ continue
+ email = mention["data-id"]
+ emails.append(email)
+
+ return emails
diff --git a/frappe/desk/page/activity/README.md b/frappe/desk/page/activity/README.md
deleted file mode 100644
index 59e0352d12..0000000000
--- a/frappe/desk/page/activity/README.md
+++ /dev/null
@@ -1 +0,0 @@
-List of latest activities based on Feed.
\ No newline at end of file
diff --git a/frappe/desk/page/activity/activity.css b/frappe/desk/page/activity/activity.css
deleted file mode 100644
index b2387135c7..0000000000
--- a/frappe/desk/page/activity/activity.css
+++ /dev/null
@@ -1,74 +0,0 @@
-#page-activity .label {
- display: inline-block;
- margin-right: 7px;
-}
-
-#page-activity .list-row {
- border: none;
- padding: 0px;
- height: auto;
- cursor: pointer;
-}
-
-#page-activity hr {
- border-top: 1px solid var(--dark-border-color);
-}
-
-.activity-label {
- max-width: 100px;
- margin-bottom: -4px;
-}
-
-.date-indicator {
- background: none;
- font-size: 12px;
- vertical-align: middle;
- font-weight: bold;
- color: var(--text-muted);
-}
-.date-indicator::after {
- margin: 0 -4px 0 12px;
- content: "";
- display: inline-block;
- height: 8px;
- width: 8px;
- border-radius: 8px;
- background: var(--dark-border-color);
-}
-
-.date-indicator.blue {
- color: var(--primary);
-}
-
-.date-indicator.blue::after {
- background: var(--primary);
-}
-
-.activity-message {
- border-left: 1px solid var(--dark-border-color);
- padding: 15px;
- padding-right: 30px;
-}
-
-.activity-date {
- padding: 15px;
- padding-right: 0px;
- z-index: 1;
-}
-
-#page-activity .list-filters {
- display: none !important;
-}
-
-#page-activity .octicon-heart {
- color: var(--red-500);
- margin: 0px 5px;
-}
-
-.heatmap {
- padding-top: 30px;
-}
-
-.heatmap svg {
- margin: auto;
-}
diff --git a/frappe/desk/page/activity/activity.js b/frappe/desk/page/activity/activity.js
deleted file mode 100644
index 7b4e8ddc1a..0000000000
--- a/frappe/desk/page/activity/activity.js
+++ /dev/null
@@ -1,221 +0,0 @@
-// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-// License: See license.txt
-
-frappe.provide("frappe.activity");
-
-frappe.pages['activity'].on_page_load = function (wrapper) {
- var me = this;
-
- frappe.ui.make_app_page({
- parent: wrapper,
- single_column: true
- });
-
- me.page = wrapper.page;
- me.page.set_title(__("Activity"));
-
- frappe.model.with_doctype("Communication", function () {
- me.page.list = new frappe.views.Activity({
- doctype: 'Communication',
- parent: wrapper
- });
- });
-
- frappe.activity.render_heatmap(me.page);
-
- me.page.main.on("click", ".activity-message", function () {
- var link_doctype = $(this).attr("data-link-doctype"),
- link_name = $(this).attr("data-link-name"),
- doctype = $(this).attr("data-doctype"),
- docname = $(this).attr("data-docname");
-
- [link_doctype, link_name, doctype, docname] =
- [link_doctype, link_name, doctype, docname].map(decodeURIComponent);
-
- link_doctype = link_doctype && link_doctype !== 'null' ? link_doctype : null;
- link_name = link_name && link_name !== 'null' ? link_name : null;
-
- if (doctype && docname) {
- if (link_doctype && link_name) {
- frappe.route_options = {
- scroll_to: { "doctype": doctype, "name": docname }
- }
- }
-
- frappe.set_route(["Form", link_doctype || doctype, link_name || docname]);
- }
- });
-
- // Build Report Button
- if (frappe.boot.user.can_get_report.indexOf("Feed") != -1) {
- this.page.add_menu_item(__('Build Report'), function () {
- frappe.set_route("List", "Feed", "Report");
- }, 'fa fa-th')
- }
-
- this.page.add_menu_item(__('Activity Log'), function () {
- frappe.route_options = {
- "user": frappe.session.user
- }
-
- frappe.set_route("List", "Activity Log", "Report");
- }, 'fa fa-th');
-};
-
-frappe.pages['activity'].on_page_show = function () {
- frappe.breadcrumbs.add("Desk");
-}
-
-frappe.activity.last_feed_date = false;
-frappe.activity.Feed = class Feed {
- constructor(row, data) {
- this.scrub_data(data);
- this.add_date_separator(row, data);
- if (!data.add_class)
- data.add_class = "label-default";
-
- data.link = "";
- if (data.link_doctype && data.link_name) {
- data.link = frappe.format(data.link_name, { fieldtype: "Link", options: data.link_doctype },
- { label: __(data.link_doctype) + " " + __(data.link_name) });
-
- } else if (data.feed_type === "Comment" && data.comment_type === "Comment") {
- // hack for backward compatiblity
- data.link_doctype = data.reference_doctype;
- data.link_name = data.reference_name;
- data.reference_doctype = "Communication";
- data.reference_name = data.name;
-
- data.link = frappe.format(data.link_name, { fieldtype: "Link", options: data.link_doctype },
- { label: __(data.link_doctype) + " " + __(data.link_name) });
-
- } else if (data.reference_doctype && data.reference_name) {
- data.link = frappe.format(data.reference_name, { fieldtype: "Link", options: data.reference_doctype },
- { label: __(data.reference_doctype) + " " + __(data.reference_name) });
- }
-
- $(row)
- .append(frappe.render_template("activity_row", data))
- .find("a").addClass("grey");
- }
-
- scrub_data(data) {
- data.by = frappe.user.full_name(data.owner);
- data.avatar = frappe.avatar(data.owner);
-
- data.icon = "fa fa-flag";
-
- // color for comment
- data.add_class = {
- "Comment": "label-danger",
- "Assignment": "label-warning",
- "Login": "label-default"
- }[data.comment_type || data.communication_medium] || "label-info"
-
- data.when = comment_when(data.creation);
- data.feed_type = data.comment_type || data.communication_medium;
- }
-
- add_date_separator(row, data) {
- var date = frappe.datetime.str_to_obj(data.creation);
- var last = frappe.activity.last_feed_date;
-
- if ((last && frappe.datetime.obj_to_str(last) != frappe.datetime.obj_to_str(date)) || (!last)) {
- var diff = frappe.datetime.get_day_diff(frappe.datetime.get_today(), frappe.datetime.obj_to_str(date));
- var pdate;
- if (diff < 1) {
- pdate = 'Today';
- } else if (diff < 2) {
- pdate = 'Yesterday';
- } else {
- pdate = frappe.datetime.global_date_format(date);
- }
- data.date_sep = pdate;
- data.date_class = pdate == 'Today' ? "date-indicator blue" : "date-indicator";
- } else {
- data.date_sep = null;
- data.date_class = "";
- }
- frappe.activity.last_feed_date = date;
- }
-};
-
-frappe.activity.render_heatmap = function (page) {
- $('\
- ').prependTo(page.main);
-
- frappe.call({
- method: "frappe.desk.page.activity.activity.get_heatmap_data",
- callback: function (r) {
- if (r.message) {
- new frappe.Chart(".heatmap", {
- type: 'heatmap',
- start: new Date(moment().subtract(1, 'year').toDate()),
- countLabel: "actions",
- discreteDomains: 1,
- radius: 3, // default 0
- data: {
- 'dataPoints': r.message
- }
- });
- }
- }
- });
-};
-
-frappe.views.Activity = class Activity extends frappe.views.BaseList {
- constructor(opts) {
- super(opts);
- this.show();
- }
-
- setup_defaults() {
- super.setup_defaults();
-
- this.page_title = __('Activity');
- this.doctype = 'Communication';
- this.method = 'frappe.desk.page.activity.activity.get_feed';
-
- }
-
- setup_filter_area() {
- //
- }
-
- setup_view_menu() {
- //
- }
-
- setup_sort_selector() {
-
- }
-
- setup_side_bar() {
-
- }
-
- get_args() {
- return {
- start: this.start,
- page_length: this.page_length
- };
- }
-
- update_data(r) {
- let data = r.message || [];
-
- if (this.start === 0) {
- this.data = data;
- } else {
- this.data = this.data.concat(data);
- }
- }
-
- render() {
- this.data.map(value => {
- const row = $('').data("data", value).appendTo(this.$result).get(0);
- new frappe.activity.Feed(row, value);
- });
- }
-};
diff --git a/frappe/desk/page/activity/activity.json b/frappe/desk/page/activity/activity.json
deleted file mode 100644
index aa195d9323..0000000000
--- a/frappe/desk/page/activity/activity.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "creation": "2013-04-09 11:45:31.000000",
- "docstatus": 0,
- "doctype": "Page",
- "icon": "fa fa-play",
- "idx": 1,
- "modified": "2013-07-11 14:40:20.000001",
- "modified_by": "Administrator",
- "module": "Desk",
- "name": "activity",
- "owner": "Administrator",
- "page_name": "activity",
- "roles": [
- {
- "role": "All"
- }
- ],
- "standard": "Yes",
- "title": "Activity"
-}
diff --git a/frappe/desk/page/activity/activity.py b/frappe/desk/page/activity/activity.py
deleted file mode 100644
index d22fa006a4..0000000000
--- a/frappe/desk/page/activity/activity.py
+++ /dev/null
@@ -1,65 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: MIT. See LICENSE
-
-import frappe
-from frappe.core.doctype.activity_log.feed import get_feed_match_conditions
-from frappe.utils import cint
-
-
-@frappe.whitelist()
-def get_feed(start, page_length):
- """get feed"""
- match_conditions_communication = get_feed_match_conditions(frappe.session.user, "Communication")
- match_conditions_comment = get_feed_match_conditions(frappe.session.user, "Comment")
-
- result = frappe.db.sql(
- """select X.*
- from (select name, owner, modified, creation, seen, comment_type,
- reference_doctype, reference_name, '' as link_doctype, '' as link_name, subject,
- communication_type, communication_medium, content
- from
- `tabCommunication`
- where
- communication_type = 'Communication'
- and communication_medium != 'Email'
- and {match_conditions_communication}
- UNION
- select name, owner, modified, creation, '0', 'Updated',
- reference_doctype, reference_name, link_doctype, link_name, subject,
- 'Comment', '', content
- from
- `tabActivity Log`
- UNION
- select name, owner, modified, creation, '0', comment_type,
- reference_doctype, reference_name, link_doctype, link_name, '',
- 'Comment', '', content
- from
- `tabComment`
- where
- {match_conditions_comment}
- ) X
- order by X.creation DESC
- LIMIT %(page_length)s
- OFFSET %(start)s""".format(
- match_conditions_comment=match_conditions_comment,
- match_conditions_communication=match_conditions_communication,
- ),
- {"user": frappe.session.user, "start": cint(start), "page_length": cint(page_length)},
- as_dict=True,
- )
-
- return result
-
-
-@frappe.whitelist()
-def get_heatmap_data():
- return dict(
- frappe.db.sql(
- """select unix_timestamp(date(creation)), count(name)
- from `tabActivity Log`
- where
- date(creation) > subdate(curdate(), interval 1 year)
- group by date(creation)
- order by creation asc"""
- )
- )
diff --git a/frappe/desk/page/activity/activity_row.html b/frappe/desk/page/activity/activity_row.html
deleted file mode 100644
index 4a15d3d9cd..0000000000
--- a/frappe/desk/page/activity/activity_row.html
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
- {%= date_sep || "" %}
-
- {{ avatar }}
-
- {% if (feed_type==="Login") { %}
- {%= __("Logged in") %}
- {% } else if (feed_type==="Label") { %}
- {%= __("{0} {1}", ["" + subject + " ", link]) %}
- {% } else if (reference_doctype && feed_type==="Comment") { %}
- {%= __("Commented on {0}: {1}", [link, "" + content + " "]) %}
- {% } else if (reference_doctype && communication_type==="Communication") { %}
- {%= __("Communicated via {0} on {1}: {2}", [__(feed_type), link, "" + subject + " "]) %}
- {% } else if (reference_doctype && !feed_type) { %}
- {%= __("Updated {0}: {1}", [link, "" + subject + " "]) %}
- {% } else if (feed_type==="Like" && reference_doctype) { %}
- {%= by %}
- {% if (in_list(["Comment", "Communication"], reference_doctype)) { %}
- {%= content %}
- {% } else { %}
- {%= link %}
- {% } %}
- {% } else if (in_list(["Created", "Submitted", "Cancelled", "Deleted"], feed_type)) { %}
- {%= __("{0} {1}", ["" + __(feed_type) + " ", feed_type==="Deleted" ? subject : link ]) %}
- {% } else if (feed_type==="Updated") { %}
- {%= __("Updated {0}: {1}", [link, "" + subject + " "]) %}
- {% } else if (feed_type==="Relinked") { %}
- {%= __("{0} {1} to {2}", [by, content,link]) %}
- {% } else if (reference_doctype && reference_name) { %}
- {%= __("{0}: {1}", [link, "" + content + " "]) %}
- {% } else { %}
- {%= subject %}
- {% } %}
-
-
-
diff --git a/frappe/desk/page/backups/backups.js b/frappe/desk/page/backups/backups.js
index d6cab750f0..08289cab2d 100644
--- a/frappe/desk/page/backups/backups.js
+++ b/frappe/desk/page/backups/backups.js
@@ -1,18 +1,18 @@
-frappe.pages['backups'].on_page_load = function (wrapper) {
+frappe.pages["backups"].on_page_load = function (wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
- title: __('Download Backups'),
- single_column: true
+ title: __("Download Backups"),
+ single_column: true,
});
page.add_inner_button(__("Set Number of Backups"), function () {
- frappe.set_route('Form', 'System Settings');
+ frappe.set_route("Form", "System Settings");
});
page.add_inner_button(__("Download Files Backup"), function () {
frappe.call({
method: "frappe.desk.page.backups.backups.schedule_files_backup",
- args: { "user_email": frappe.session.user_email }
+ args: { user_email: frappe.session.user_email },
});
});
@@ -23,18 +23,18 @@ frappe.pages['backups'].on_page_load = function (wrapper) {
method: "frappe.utils.backups.get_backup_encryption_key",
callback: function (r) {
frappe.msgprint({
- title: __('Backup Encryption Key'),
+ title: __("Backup Encryption Key"),
message: __(r.message),
- indicator: 'blue'
+ indicator: "blue",
});
- }
+ },
});
});
} else {
frappe.msgprint({
- title: __('Error'),
- message: __('System Manager privileges required.'),
- indicator: 'red'
+ title: __("Error"),
+ message: __("System Manager privileges required."),
+ indicator: "red",
});
}
});
diff --git a/frappe/desk/page/backups/backups.py b/frappe/desk/page/backups/backups.py
index 2ef09df900..9554c7b9b7 100644
--- a/frappe/desk/page/backups/backups.py
+++ b/frappe/desk/page/backups/backups.py
@@ -4,13 +4,13 @@ import os
import frappe
from frappe import _
from frappe.utils import cint, get_site_path, get_url
-from frappe.utils.data import convert_utc_to_user_timezone
+from frappe.utils.data import convert_utc_to_system_timezone
def get_context(context):
def get_time(path):
dt = os.path.getmtime(path)
- return convert_utc_to_user_timezone(datetime.datetime.utcfromtimestamp(dt)).strftime(
+ return convert_utc_to_system_timezone(datetime.datetime.utcfromtimestamp(dt)).strftime(
"%a %b %d %H:%M %Y"
)
diff --git a/frappe/event_streaming/doctype/document_type_mapping/__init__.py b/frappe/desk/page/form_builder/__init__.py
similarity index 100%
rename from frappe/event_streaming/doctype/document_type_mapping/__init__.py
rename to frappe/desk/page/form_builder/__init__.py
diff --git a/frappe/desk/page/form_builder/form_builder.js b/frappe/desk/page/form_builder/form_builder.js
new file mode 100644
index 0000000000..cc29084b69
--- /dev/null
+++ b/frappe/desk/page/form_builder/form_builder.js
@@ -0,0 +1,202 @@
+frappe.pages["form-builder"].on_page_load = function (wrapper) {
+ frappe.ui.make_app_page({
+ parent: wrapper,
+ title: __("Form Builder"),
+ single_column: true,
+ });
+
+ // hot reload in development
+ if (frappe.boot.developer_mode) {
+ frappe.hot_update = frappe.hot_update || [];
+ frappe.hot_update.push(() => load_form_builder(wrapper));
+ }
+};
+
+frappe.pages["form-builder"].on_page_show = function (wrapper) {
+ load_form_builder(wrapper);
+};
+
+function load_form_builder(wrapper) {
+ let route = frappe.get_route();
+ route = route.filter((a) => a);
+ if (route.length > 1) {
+ let doctype = route[1];
+ let is_customize_form = route[2] === "customize";
+
+ if (frappe.form_builder?.doctype) {
+ frappe.form_builder.doctype = frappe.form_builder.store.doctype = doctype;
+ frappe.form_builder.customize = frappe.form_builder.store.is_customize_form =
+ is_customize_form;
+ frappe.form_builder.init(true);
+ frappe.form_builder.store.fetch();
+ return;
+ }
+
+ let $parent = $(wrapper).find(".layout-main-section");
+ $parent.empty();
+
+ frappe.require("form_builder.bundle.js").then(() => {
+ frappe.form_builder = new frappe.ui.FormBuilder({
+ wrapper: $parent,
+ page: wrapper.page,
+ doctype: doctype,
+ customize: is_customize_form,
+ });
+ });
+ } else {
+ let d = new frappe.ui.Dialog({
+ title: __("Select DocType"),
+ fields: [
+ {
+ label: __("Select DocType"),
+ fieldname: "doctype",
+ fieldtype: "Link",
+ options: "DocType",
+ only_select: 1,
+ },
+ {
+ label: __("Customize"),
+ fieldname: "customize",
+ fieldtype: "Check",
+ },
+ ],
+ primary_action_label: __("Edit"),
+ primary_action({ doctype, customize }) {
+ if (customize) {
+ frappe.model.with_doctype(doctype).then(() => {
+ let meta = frappe.get_meta(doctype);
+ if (in_list(frappe.model.core_doctypes_list, this.doctype))
+ frappe.throw(__("Core DocTypes cannot be customized."));
+
+ if (meta.issingle)
+ frappe.throw(__("Single DocTypes cannot be customized."));
+
+ if (meta.custom)
+ frappe.throw(
+ __(
+ "Only standard DocTypes are allowed to be customized from Customize Form."
+ )
+ );
+ frappe.set_route("form-builder", doctype, "customize");
+ });
+ } else {
+ frappe.set_route("form-builder", doctype);
+ }
+ },
+ secondary_action_label: __("Create New DocType"),
+ secondary_action() {
+ let doctype = d.get_value("doctype") || "";
+ let non_developer =
+ frappe.session.user !== "Administrator" || !frappe.boot.developer_mode;
+ d.hide();
+ let new_d = new frappe.ui.Dialog({
+ title: __("Create New DocType"),
+ fields: [
+ {
+ label: __("DocType Name"),
+ fieldname: "doctype_name",
+ fieldtype: "Data",
+ default: doctype,
+ reqd: 1,
+ },
+ { fieldtype: "Column Break" },
+ {
+ label: __("Module"),
+ fieldname: "module",
+ fieldtype: "Link",
+ options: "Module Def",
+ reqd: 1,
+ },
+ { fieldtype: "Section Break" },
+ {
+ label: __("Is Submittable"),
+ fieldname: "is_submittable",
+ fieldtype: "Check",
+ description: __(
+ "Once submitted, submittable documents cannot be changed. They can only be Cancelled and Amended."
+ ),
+ depends_on: "eval:!doc.istable && !doc.issingle",
+ },
+ {
+ label: __("Is Child Table"),
+ fieldname: "istable",
+ fieldtype: "Check",
+ description: __("Child Tables are shown as a Grid in other DocTypes"),
+ depends_on: "eval:!doc.is_submittable && !doc.issingle",
+ },
+ {
+ label: __("Editable Grid"),
+ fieldname: "editable_grid",
+ fieldtype: "Check",
+ depends_on: "istable",
+ default: 1,
+ },
+ {
+ label: __("Is Single"),
+ fieldname: "issingle",
+ fieldtype: "Check",
+ description: __(
+ "Single Types have only one record no tables associated. Values are stored in tabSingles"
+ ),
+ depends_on: "eval:!doc.istable && !doc.is_submittable",
+ },
+ {
+ label: __("Custom?"),
+ fieldname: "custom",
+ fieldtype: "Check",
+ default: non_developer,
+ read_only: non_developer,
+ },
+ ],
+ primary_action_label: __("Create & Continue"),
+ primary_action(values) {
+ if (!values.istable) values.editable_grid = 0;
+ frappe.db
+ .insert({
+ doctype: "DocType",
+ name: values.doctype_name,
+ module: values.module,
+ istable: values.istable,
+ editable_grid: values.editable_grid,
+ issingle: values.issingle,
+ custom: values.custom,
+ is_submittable: values.is_submittable,
+ permissions: [
+ {
+ create: 1,
+ delete: 1,
+ email: 1,
+ export: 1,
+ print: 1,
+ read: 1,
+ report: 1,
+ role: "System Manager",
+ share: 1,
+ write: 1,
+ },
+ ],
+ fields: [
+ {
+ label: "Title",
+ fieldname: "title",
+ fieldtype: "Data",
+ },
+ ],
+ })
+ .then((doc) => {
+ frappe.set_route("form-builder", doc.name);
+ });
+ },
+ secondary_action_label: __("Back"),
+ secondary_action() {
+ new_d.hide();
+ d.show();
+ },
+ });
+ new_d.show();
+ },
+ });
+
+ d.show();
+ }
+}
diff --git a/frappe/desk/page/form_builder/form_builder.json b/frappe/desk/page/form_builder/form_builder.json
new file mode 100644
index 0000000000..afeacecd90
--- /dev/null
+++ b/frappe/desk/page/form_builder/form_builder.json
@@ -0,0 +1,19 @@
+{
+ "content": null,
+ "creation": "2022-10-10 22:42:53.597423",
+ "docstatus": 0,
+ "doctype": "Page",
+ "idx": 0,
+ "modified": "2022-10-10 22:42:53.597423",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "form-builder",
+ "owner": "Administrator",
+ "page_name": "form-builder",
+ "roles": [],
+ "script": null,
+ "standard": "Yes",
+ "style": null,
+ "system_page": 0,
+ "title": "Form Builder"
+}
\ No newline at end of file
diff --git a/frappe/desk/page/leaderboard/leaderboard.js b/frappe/desk/page/leaderboard/leaderboard.js
index aa1678af37..9f689b461e 100644
--- a/frappe/desk/page/leaderboard/leaderboard.js
+++ b/frappe/desk/page/leaderboard/leaderboard.js
@@ -1,7 +1,7 @@
frappe.pages["leaderboard"].on_page_load = (wrapper) => {
frappe.leaderboard = new Leaderboard(wrapper);
- $(wrapper).bind('show', ()=> {
+ $(wrapper).bind("show", () => {
// Get which leaderboard to show
let doctype = frappe.get_route()[1];
frappe.leaderboard.show_leaderboard(doctype);
@@ -9,7 +9,6 @@ frappe.pages["leaderboard"].on_page_load = (wrapper) => {
};
class Leaderboard {
-
constructor(parent) {
frappe.ui.make_app_page({
parent: parent,
@@ -20,11 +19,12 @@ class Leaderboard {
this.parent = parent;
this.page = this.parent.page;
- this.page.sidebar.html(`
`);
- this.$sidebar_list = this.page.sidebar.find('ul');
+ this.page.sidebar.html(
+ `
`
+ );
+ this.$sidebar_list = this.page.sidebar.find("ul");
this.get_leaderboard_config();
-
}
get_leaderboard_config() {
@@ -32,47 +32,57 @@ class Leaderboard {
this.filters = {};
this.leaderboard_limit = 20;
- frappe.xcall("frappe.desk.page.leaderboard.leaderboard.get_leaderboard_config").then(config => {
- this.leaderboard_config = config;
- for (let doctype in this.leaderboard_config) {
- this.doctypes.push(doctype);
- this.filters[doctype] = this.leaderboard_config[doctype].fields.map(field => {
- if (typeof field ==='object') {
- return field.label || field.fieldname;
- }
- return field;
- });
- }
+ frappe
+ .xcall("frappe.desk.page.leaderboard.leaderboard.get_leaderboard_config")
+ .then((config) => {
+ this.leaderboard_config = config;
+ for (let doctype in this.leaderboard_config) {
+ this.doctypes.push(doctype);
+ this.filters[doctype] = this.leaderboard_config[doctype].fields.map(
+ (field) => {
+ if (typeof field === "object") {
+ return field.label || field.fieldname;
+ }
+ return field;
+ }
+ );
+ }
- // For translation. Do not remove this
- // __("This Week"), __("This Month"), __("This Quarter"), __("This Year"),
- // __("Last Week"), __("Last Month"), __("Last Quarter"), __("Last Year"),
- // __("All Time"), __("Select From Date")
- this.timespans = [
- "This Week", "This Month", "This Quarter", "This Year",
- "Last Week", "Last Month", "Last Quarter", "Last Year",
- "All Time", "Select Date Range"
- ];
+ // For translation. Do not remove this
+ // __("This Week"), __("This Month"), __("This Quarter"), __("This Year"),
+ // __("Last Week"), __("Last Month"), __("Last Quarter"), __("Last Year"),
+ // __("All Time"), __("Select From Date")
+ this.timespans = [
+ "This Week",
+ "This Month",
+ "This Quarter",
+ "This Year",
+ "Last Week",
+ "Last Month",
+ "Last Quarter",
+ "Last Year",
+ "All Time",
+ "Select Date Range",
+ ];
- // for saving current selected filters
- const _initial_doctype = frappe.get_route()[1] || this.doctypes[0];
- const _initial_timespan = this.timespans[0];
- const _initial_filter = this.filters[_initial_doctype];
+ // for saving current selected filters
+ const _initial_doctype = frappe.get_route()[1] || this.doctypes[0];
+ const _initial_timespan = this.timespans[0];
+ const _initial_filter = this.filters[_initial_doctype];
- this.options = {
- selected_doctype: _initial_doctype,
- selected_filter: _initial_filter,
- selected_filter_item: _initial_filter[0],
- selected_timespan: _initial_timespan,
- };
+ this.options = {
+ selected_doctype: _initial_doctype,
+ selected_filter: _initial_filter,
+ selected_filter_item: _initial_filter[0],
+ selected_timespan: _initial_timespan,
+ };
- this.message = null;
- this.make();
- });
+ this.message = null;
+ this.make();
+ });
}
make() {
-
this.$container = $(`
@@ -80,7 +90,7 @@ class Leaderboard {
this.$graph_area = this.$container.find(".leaderboard-graph");
- this.doctypes.map(doctype => {
+ this.doctypes.map((doctype) => {
const icon = this.leaderboard_config[doctype].icon;
this.get_sidebar_item(doctype, icon).appendTo(this.$sidebar_list);
});
@@ -94,7 +104,6 @@ class Leaderboard {
// Get which leaderboard to show
let doctype = frappe.get_route()[1];
this.show_leaderboard(doctype);
-
}
setup_leaderboard_fields() {
@@ -106,27 +115,28 @@ class Leaderboard {
default: frappe.defaults.get_default("company"),
reqd: 1,
change: (e) => {
- this.options.selected_company = e.currentTarget.value;
this.make_request();
- }
+ },
});
- this.timespan_select = this.page.add_select(__("Timespan"),
- this.timespans.map(d => {
- return {"label": __(d), value: d };
+ this.timespan_select = this.page.add_select(
+ __("Timespan"),
+ this.timespans.map((d) => {
+ return { label: __(d), value: d };
})
);
this.create_date_range_field();
- this.type_select = this.page.add_select(__("Field"),
- this.options.selected_filter.map(d => {
- return {"label": __(frappe.model.unscrub(d)), value: d };
+ this.type_select = this.page.add_select(
+ __("Field"),
+ this.options.selected_filter.map((d) => {
+ return { label: __(frappe.model.unscrub(d)), value: d };
})
);
this.timespan_select.on("change", (e) => {
this.options.selected_timespan = e.currentTarget.value;
- if (this.options.selected_timespan === 'Select Date Range') {
+ if (this.options.selected_timespan === "Select Date Range") {
this.date_range_field.show();
} else {
this.date_range_field.hide();
@@ -141,41 +151,46 @@ class Leaderboard {
}
create_date_range_field() {
- let timespan_field = $(this.parent).find(`.frappe-control[data-original-title="${__('Timespan')}"]`);
- this.date_range_field = $(`
`).insertAfter(timespan_field).hide();
+ let timespan_field = $(this.parent).find(
+ `.frappe-control[data-original-title="${__("Timespan")}"]`
+ );
+ this.date_range_field = $(`
`)
+ .insertAfter(timespan_field)
+ .hide();
let date_field = frappe.ui.form.make_control({
df: {
- fieldtype: 'DateRange',
- fieldname: 'selected_date_range',
+ fieldtype: "DateRange",
+ fieldname: "selected_date_range",
placeholder: __("Date Range"),
default: [frappe.datetime.month_start(), frappe.datetime.now_date()],
- input_class: 'input-xs',
+ input_class: "input-xs",
reqd: 1,
change: () => {
this.selected_date_range = date_field.get_value();
if (this.selected_date_range) this.make_request();
- }
+ },
},
- parent: $(this.parent).find('.from-date-field'),
- render_input: 1
+ parent: $(this.parent).find(".from-date-field"),
+ render_input: 1,
});
}
render_selected_doctype() {
-
- this.$sidebar_list.on("click", "li", (e)=> {
+ this.$sidebar_list.on("click", "li", (e) => {
let $li = $(e.currentTarget);
let doctype = $li.find(".doctype-text").attr("doctype-value");
- this.options.selected_company = frappe.defaults.get_default("company");
+ this.company_select.set_value(
+ frappe.defaults.get_default("company") || this.company_select.get_value()
+ );
this.options.selected_doctype = doctype;
this.options.selected_filter = this.filters[doctype];
this.options.selected_filter_item = this.filters[doctype][0];
this.type_select.empty().add_options(
- this.options.selected_filter.map(d => {
- return {"label": __(frappe.model.unscrub(d)), value: d };
+ this.options.selected_filter.map((d) => {
+ return { label: __(frappe.model.unscrub(d)), value: d };
})
);
if (this.leaderboard_config[this.options.selected_doctype].company_disabled) {
@@ -193,10 +208,10 @@ class Leaderboard {
}
render_search_box() {
-
- this.$search_box =
- $(`
-
+ this.$search_box = $(`
+
`);
$(this.parent).find(".page-form").append(this.$search_box);
@@ -206,7 +221,9 @@ class Leaderboard {
if (this.doctypes.length) {
if (this.doctypes.includes(doctype)) {
this.options.selected_doctype = doctype;
- this.$sidebar_list.find(`[doctype-value = "${this.options.selected_doctype}"]`).trigger("click");
+ this.$sidebar_list
+ .find(`[doctype-value = "${this.options.selected_doctype}"]`)
+ .trigger("click");
}
this.$search_box.find(".leaderboard-search-input").val("");
@@ -215,43 +232,44 @@ class Leaderboard {
}
make_request() {
-
- frappe.model.with_doctype(this.options.selected_doctype, ()=> {
+ frappe.model.with_doctype(this.options.selected_doctype, () => {
this.get_leaderboard(this.get_leaderboard_data);
});
}
get_leaderboard(notify) {
- if (!this.options.selected_company) {
- frappe.throw(__("Please select Company"));
+ let company = this.company_select.get_value();
+ if (!company && !this.leaderboard_config[this.options.selected_doctype].company_disabled) {
+ notify(this, null);
+ frappe.show_alert(__("Please select Company"));
+ return;
}
- frappe.call(
- this.leaderboard_config[this.options.selected_doctype].method,
- {
- 'date_range': this.get_date_range(),
- 'company': this.options.selected_company,
- 'field': this.options.selected_filter_item,
- 'limit': this.leaderboard_limit,
- }
- ).then(r => {
- let results = r.message || [];
+ frappe
+ .call(this.leaderboard_config[this.options.selected_doctype].method, {
+ date_range: this.get_date_range(),
+ company: company,
+ field: this.options.selected_filter_item,
+ limit: this.leaderboard_limit,
+ })
+ .then((r) => {
+ let results = r.message || [];
- let graph_items = results.slice(0, 10);
+ let graph_items = results.slice(0, 10);
- this.$graph_area.show().empty();
+ this.$graph_area.show().empty();
- const custom_options = {
- data: {
- datasets: [{ values: graph_items.map(d => d.value) }],
- labels: graph_items.map(d => d.name)
- },
- format_tooltip_x: d => d[this.options.selected_filter_item],
- height: 140
- };
- frappe.utils.make_chart('.leaderboard-graph', custom_options);
+ const custom_options = {
+ data: {
+ datasets: [{ values: graph_items.map((d) => d.value) }],
+ labels: graph_items.map((d) => d.name),
+ },
+ format_tooltip_x: (d) => d[this.options.selected_filter_item],
+ height: 140,
+ };
+ frappe.utils.make_chart(".leaderboard-graph", custom_options);
- notify(this, r);
- });
+ notify(this, r);
+ });
}
get_leaderboard_data(me, res) {
@@ -267,9 +285,7 @@ class Leaderboard {
}
render_list_view(items = []) {
-
- var html =
- `${this.render_message()}
+ var html = `${this.render_message()}
${this.render_result(items)}
`;
@@ -278,47 +294,43 @@ class Leaderboard {
}
render_result(items) {
-
- var html =
- `${this.render_list_header()}
+ var html = `${this.render_list_header()}
${this.render_list_result(items)}`;
return html;
}
render_list_header() {
- const _selected_filter = this.options.selected_filter
- .map(i => frappe.model.unscrub(i));
+ const _selected_filter = this.options.selected_filter.map((i) => frappe.model.unscrub(i));
const fields = ["rank", "name", this.options.selected_filter_item];
- const filters = fields.map(filter => {
- const col = __(frappe.model.unscrub(filter));
- return (
- `
+ ${col && _selected_filter.indexOf(col) !== -1 ? "text-right" : ""}">
${col}
-
`
- );
- }).join("");
+
`;
+ })
+ .join("");
- const html =
- `
@@ -192,112 +190,113 @@
-
diff --git a/frappe/public/js/frappe/recorder/recorder.bundle.js b/frappe/public/js/frappe/recorder/recorder.bundle.js
new file mode 100644
index 0000000000..4f6fb59d26
--- /dev/null
+++ b/frappe/public/js/frappe/recorder/recorder.bundle.js
@@ -0,0 +1,8 @@
+import { createApp } from "vue";
+import RecorderRoot from "./RecorderRoot.vue";
+import router from "./router.js";
+
+let app = createApp(RecorderRoot).use(router);
+SetVueGlobals(app);
+app.mount(".recorder-container");
+frappe.recorder.view = app;
diff --git a/frappe/public/js/frappe/recorder/recorder.js b/frappe/public/js/frappe/recorder/recorder.js
deleted file mode 100644
index eed57ddd4f..0000000000
--- a/frappe/public/js/frappe/recorder/recorder.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import Vue from 'vue/dist/vue.js';
-import VueRouter from 'vue-router/dist/vue-router.js';
-
-import RecorderRoot from "./RecorderRoot.vue";
-
-import RecorderDetail from "./RecorderDetail.vue";
-import RequestDetail from "./RequestDetail.vue";
-
-Vue.prototype.__ = window.__;
-Vue.prototype.frappe = window.frappe;
-
-Vue.use(VueRouter);
-const routes = [
- {
- name: "recorder-detail",
- path: '/detail',
- component: RecorderDetail,
- },
- {
- name: "request-detail",
- path: '/request/:id',
- component: RequestDetail,
- },
- {
- path: '/',
- redirect: {
- name: "recorder-detail"
- }
- }
-];
-
-const router = new VueRouter({
- mode: 'history',
- base: "/app/recorder/",
- routes: routes,
-});
-
-frappe.recorder.view = new Vue({
- el: ".recorder-container",
- router: router,
- data: {
- page: frappe.pages["recorder"].page
- },
- template: " ",
- components: {
- RecorderRoot,
- }
-});
diff --git a/frappe/public/js/frappe/recorder/router.js b/frappe/public/js/frappe/recorder/router.js
new file mode 100644
index 0000000000..ebff6eba7c
--- /dev/null
+++ b/frappe/public/js/frappe/recorder/router.js
@@ -0,0 +1,28 @@
+import { createWebHistory, createRouter } from "vue-router";
+import RecorderDetail from "./RecorderDetail.vue";
+import RequestDetail from "./RequestDetail.vue";
+
+const routes = [
+ {
+ path: "/detail",
+ name: "RecorderDetail",
+ component: RecorderDetail,
+ },
+ {
+ path: "/request/:id",
+ name: "RequestDetail",
+ component: RequestDetail,
+ meta: { shouldFetch: true },
+ },
+ {
+ path: "/",
+ redirect: "/detail",
+ },
+];
+
+const router = createRouter({
+ history: createWebHistory("/app/recorder/"),
+ routes,
+});
+
+export default router;
diff --git a/frappe/public/js/frappe/request.js b/frappe/public/js/frappe/request.js
index dd4c352f41..47ca1f7548 100644
--- a/frappe/public/js/frappe/request.js
+++ b/frappe/public/js/frappe/request.js
@@ -3,14 +3,14 @@
// My HTTP Request
-frappe.provide('frappe.request');
-frappe.provide('frappe.request.error_handlers');
-frappe.request.url = '/';
+frappe.provide("frappe.request");
+frappe.provide("frappe.request.error_handlers");
+frappe.request.url = "/";
frappe.request.ajax_count = 0;
frappe.request.waiting_for_ajax = [];
frappe.request.logs = {};
-frappe.xcall = function(method, params) {
+frappe.xcall = function (method, params) {
return new Promise((resolve, reject) => {
frappe.call({
method: method,
@@ -20,32 +20,35 @@ frappe.xcall = function(method, params) {
},
error: (r) => {
reject(r.message);
- }
+ },
});
});
};
// generic server call (call page, object)
-frappe.call = function(opts) {
+frappe.call = function (opts) {
if (!frappe.is_online()) {
- frappe.show_alert({
- indicator: 'orange',
- message: __('Connection Lost'),
- subtitle: __('You are not connected to Internet. Retry after sometime.')
- }, 3);
+ frappe.show_alert(
+ {
+ indicator: "orange",
+ message: __("Connection Lost"),
+ subtitle: __("You are not connected to Internet. Retry after sometime."),
+ },
+ 3
+ );
opts.always && opts.always();
return $.ajax();
}
- if (typeof arguments[0]==='string') {
+ if (typeof arguments[0] === "string") {
opts = {
method: arguments[0],
args: arguments[1],
callback: arguments[2],
- headers: arguments[3]
- }
+ headers: arguments[3],
+ };
}
- if(opts.quiet) {
+ if (opts.quiet) {
opts.no_spinner = true;
}
var args = $.extend({}, opts.args);
@@ -56,37 +59,36 @@ frappe.call = function(opts) {
}
// cmd
- if(opts.module && opts.page) {
- args.cmd = opts.module+'.page.'+opts.page+'.'+opts.page+'.'+opts.method;
- } else if(opts.doc) {
+ if (opts.module && opts.page) {
+ args.cmd = opts.module + ".page." + opts.page + "." + opts.page + "." + opts.method;
+ } else if (opts.doc) {
$.extend(args, {
cmd: "run_doc_method",
docs: frappe.get_doc(opts.doc.doctype, opts.doc.name),
method: opts.method,
args: opts.args,
});
- } else if(opts.method) {
+ } else if (opts.method) {
args.cmd = opts.method;
}
- var callback = function(data, response_text) {
- if(data.task_id) {
+ var callback = function (data, response_text) {
+ if (data.task_id) {
// async call, subscribe
frappe.socketio.subscribe(data.task_id, opts);
- if(opts.queued) {
+ if (opts.queued) {
opts.queued(data);
}
- }
- else if (opts.callback) {
+ } else if (opts.callback) {
// ajax
return opts.callback(data, response_text);
}
- }
+ };
let url = opts.url;
if (!url) {
- url = '/api/method/' + args.cmd;
+ url = "/api/method/" + args.cmd;
if (window.cordova) {
let host = frappe.request.url;
host = host.slice(0, host.length - 1);
@@ -116,40 +118,38 @@ frappe.call = function(opts) {
silent: opts.silent,
url,
});
-}
+};
-
-frappe.request.call = function(opts) {
+frappe.request.call = function (opts) {
frappe.request.prepare(opts);
var statusCode = {
- 200: function(data, xhr) {
+ 200: function (data, xhr) {
opts.success_callback && opts.success_callback(data, xhr.responseText);
},
- 401: function(xhr) {
- if(frappe.app.session_expired_dialog && frappe.app.session_expired_dialog.display) {
+ 401: function (xhr) {
+ if (frappe.app.session_expired_dialog && frappe.app.session_expired_dialog.display) {
frappe.app.redirect_to_login();
} else {
frappe.app.handle_session_expired();
}
},
- 404: function(xhr) {
- if (frappe.flags.setting_original_route) {
- // original route is wrong, redirect to login
- frappe.app.redirect_to_login();
- } else {
- frappe.msgprint({title: __("Not found"), indicator: 'red',
- message: __('The resource you are looking for is not available')});
- }
+ 404: function (xhr) {
+ frappe.msgprint({
+ title: __("Not found"),
+ indicator: "red",
+ message: __("The resource you are looking for is not available"),
+ });
},
- 403: function(xhr) {
+ 403: function (xhr) {
if (frappe.session.user === "Guest" && frappe.session.logged_in_user !== "Guest") {
// session expired
frappe.app.handle_session_expired();
} else if (xhr.responseJSON && xhr.responseJSON._error_message) {
frappe.msgprint({
- title: __("Not permitted"), indicator: 'red',
- message: xhr.responseJSON._error_message
+ title: __("Not permitted"),
+ indicator: "red",
+ message: xhr.responseJSON._error_message,
});
xhr.responseJSON._server_messages = null;
@@ -162,22 +162,34 @@ frappe.request.call = function(opts) {
}
} else {
frappe.msgprint({
- title: __("Not permitted"), indicator: 'red',
- message: __('You do not have enough permissions to access this resource. Please contact your manager to get access.')});
+ title: __("Not permitted"),
+ indicator: "red",
+ message: __(
+ "You do not have enough permissions to access this resource. Please contact your manager to get access."
+ ),
+ });
}
-
-
},
- 508: function(xhr) {
+ 508: function (xhr) {
frappe.utils.play_sound("error");
- frappe.msgprint({title:__('Please try again'), indicator:'red',
- message:__("Another transaction is blocking this one. Please try again in a few seconds.")});
+ frappe.msgprint({
+ title: __("Please try again"),
+ indicator: "red",
+ message: __(
+ "Another transaction is blocking this one. Please try again in a few seconds."
+ ),
+ });
},
- 413: function(data, xhr) {
- frappe.msgprint({indicator:'red', title:__('File too big'), message:__("File size exceeded the maximum allowed size of {0} MB",
- [(frappe.boot.max_file_size || 5242880) / 1048576])});
+ 413: function (data, xhr) {
+ frappe.msgprint({
+ indicator: "red",
+ title: __("File too big"),
+ message: __("File size exceeded the maximum allowed size of {0} MB", [
+ (frappe.boot.max_file_size || 5242880) / 1048576,
+ ]),
+ });
},
- 417: function(xhr) {
+ 417: function (xhr) {
var r = xhr.responseJSON;
if (!r) {
try {
@@ -189,11 +201,11 @@ frappe.request.call = function(opts) {
opts.error_callback && opts.error_callback(r);
},
- 501: function(data, xhr) {
- if(typeof data === "string") data = JSON.parse(data);
+ 501: function (data, xhr) {
+ if (typeof data === "string") data = JSON.parse(data);
opts.error_callback && opts.error_callback(data, xhr.responseText);
},
- 500: function(xhr) {
+ 500: function (xhr) {
frappe.utils.play_sound("error");
try {
opts.error_callback && opts.error_callback();
@@ -202,46 +214,49 @@ frappe.request.call = function(opts) {
frappe.request.report_error(xhr, opts);
}
},
- 504: function(xhr) {
- frappe.msgprint(__("Request Timed Out"))
+ 504: function (xhr) {
+ frappe.msgprint(__("Request Timed Out"));
opts.error_callback && opts.error_callback();
},
- 502: function(xhr) {
+ 502: function (xhr) {
frappe.msgprint(__("Internal Server Error"));
- }
+ },
};
var exception_handlers = {
- 'QueryTimeoutError': function() {
+ QueryTimeoutError: function () {
frappe.utils.play_sound("error");
frappe.msgprint({
- title: __('Request Timeout'),
- indicator: 'red',
- message: __("Server was too busy to process this request. Please try again.")
+ title: __("Request Timeout"),
+ indicator: "red",
+ message: __("Server was too busy to process this request. Please try again."),
});
},
- 'QueryDeadlockError': function() {
+ QueryDeadlockError: function () {
frappe.utils.play_sound("error");
frappe.msgprint({
- title: __('Deadlock Occurred'),
- indicator: 'red',
- message: __("Server was too busy to process this request. Please try again.")
+ title: __("Deadlock Occurred"),
+ indicator: "red",
+ message: __("Server was too busy to process this request. Please try again."),
});
- }
+ },
};
var ajax_args = {
url: opts.url || frappe.request.url,
data: opts.args,
type: opts.type,
- dataType: opts.dataType || 'json',
+ dataType: opts.dataType || "json",
async: opts.async,
- headers: Object.assign({
- "X-Frappe-CSRF-Token": frappe.csrf_token,
- "Accept": "application/json",
- "X-Frappe-CMD": (opts.args && opts.args.cmd || '') || ''
- }, opts.headers),
- cache: false
+ headers: Object.assign(
+ {
+ "X-Frappe-CSRF-Token": frappe.csrf_token,
+ Accept: "application/json",
+ "X-Frappe-CMD": (opts.args && opts.args.cmd) || "" || "",
+ },
+ opts.headers
+ ),
+ cache: false,
};
if (opts.args && opts.args.doctype) {
@@ -251,17 +266,17 @@ frappe.request.call = function(opts) {
frappe.last_request = ajax_args.data;
return $.ajax(ajax_args)
- .done(function(data, textStatus, xhr) {
+ .done(function (data, textStatus, xhr) {
try {
- if(typeof data === "string") data = JSON.parse(data);
+ if (typeof data === "string") data = JSON.parse(data);
// sync attached docs
- if(data.docs || data.docinfo) {
+ if (data.docs || data.docinfo) {
frappe.model.sync(data);
}
// sync translated messages
- if(data.__messages) {
+ if (data.__messages) {
$.extend(frappe._messages, data.__messages);
}
@@ -278,33 +293,35 @@ frappe.request.call = function(opts) {
if (status_code_handler) {
status_code_handler(data, xhr);
}
- } catch(e) {
+ } catch (e) {
console.log("Unable to handle success response", data); // eslint-disable-line
console.error(e); // eslint-disable-line
}
-
})
- .always(function(data, textStatus, xhr) {
+ .always(function (data, textStatus, xhr) {
try {
- if(typeof data==="string") {
+ if (typeof data === "string") {
data = JSON.parse(data);
}
- if(data.responseText) {
+ if (data.responseText) {
var xhr = data;
data = JSON.parse(data.responseText);
}
- } catch(e) {
+ } catch (e) {
data = null;
// pass
}
frappe.request.cleanup(opts, data);
- if(opts.always) {
+ if (opts.always) {
opts.always(data);
}
})
- .fail(function(xhr, textStatus) {
+ .fail(function (xhr, textStatus) {
try {
- if (xhr.getResponseHeader('content-type') == 'application/json' && xhr.responseText) {
+ if (
+ xhr.getResponseHeader("content-type") == "application/json" &&
+ xhr.responseText
+ ) {
var data;
try {
data = JSON.parse(xhr.responseText);
@@ -315,7 +332,7 @@ frappe.request.call = function(opts) {
}
if (data && data.exception) {
// frappe.exceptions.CustomError: (1024, ...) -> CustomError
- var exception = data.exception.split('.').at(-1).split(':').at(0);
+ var exception = data.exception.split(".").at(-1).split(":").at(0);
var exception_handler = exception_handlers[exception];
if (exception_handler) {
exception_handler(data);
@@ -330,14 +347,14 @@ frappe.request.call = function(opts) {
}
// if not handled by error handler!
opts.error_callback && opts.error_callback(xhr);
- } catch(e) {
+ } catch (e) {
console.log("Unable to handle failed response"); // eslint-disable-line
console.error(e); // eslint-disable-line
}
});
-}
+};
-frappe.request.is_fresh = function(args, threshold) {
+frappe.request.is_fresh = function (args, threshold) {
// return true if a request with similar args has been sent recently
if (!frappe.request.logs[args.cmd]) {
frappe.request.logs[args.cmd] = [];
@@ -345,39 +362,41 @@ frappe.request.is_fresh = function(args, threshold) {
for (let past_request of frappe.request.logs[args.cmd]) {
// check if request has same args and was made recently
- if ((new Date() - past_request.timestamp) < threshold
- && frappe.utils.deep_equal(args, past_request.args)) {
+ if (
+ new Date() - past_request.timestamp < threshold &&
+ frappe.utils.deep_equal(args, past_request.args)
+ ) {
// eslint-disable-next-line no-console
- console.log('throttled');
+ console.log("throttled");
return true;
}
}
// log the request
- frappe.request.logs[args.cmd].push({args: args, timestamp: new Date()});
+ frappe.request.logs[args.cmd].push({ args: args, timestamp: new Date() });
return false;
};
// call execute serverside request
-frappe.request.prepare = function(opts) {
+frappe.request.prepare = function (opts) {
$("body").attr("data-ajax-state", "triggered");
// btn indicator
- if(opts.btn) $(opts.btn).prop("disabled", true);
+ if (opts.btn) $(opts.btn).prop("disabled", true);
// freeze page
- if(opts.freeze) frappe.dom.freeze(opts.freeze_message);
+ if (opts.freeze) frappe.dom.freeze(opts.freeze_message);
// stringify args if required
- for(var key in opts.args) {
- if(opts.args[key] && ($.isPlainObject(opts.args[key]) || $.isArray(opts.args[key]))) {
+ for (var key in opts.args) {
+ if (opts.args[key] && ($.isPlainObject(opts.args[key]) || $.isArray(opts.args[key]))) {
opts.args[key] = JSON.stringify(opts.args[key]);
}
}
// no cmd?
- if(!opts.args.cmd && !opts.url) {
- console.log(opts)
+ if (!opts.args.cmd && !opts.url) {
+ console.log(opts);
throw "Incomplete Request";
}
@@ -385,25 +404,25 @@ frappe.request.prepare = function(opts) {
opts.error_callback = opts.error;
delete opts.success;
delete opts.error;
+};
-}
-
-frappe.request.cleanup = function(opts, r) {
+frappe.request.cleanup = function (opts, r) {
// stop button indicator
- if(opts.btn) {
+ if (opts.btn) {
$(opts.btn).prop("disabled", false);
}
$("body").attr("data-ajax-state", "complete");
// un-freeze page
- if(opts.freeze) frappe.dom.unfreeze();
-
- if(r) {
+ if (opts.freeze) frappe.dom.unfreeze();
+ if (r) {
// session expired? - Guest has no business here!
- if (r.session_expired ||
- (frappe.session.user === 'Guest' && frappe.session.logged_in_user !== "Guest")) {
+ if (
+ r.session_expired ||
+ (frappe.session.user === "Guest" && frappe.session.logged_in_user !== "Guest")
+ ) {
frappe.app.handle_session_expired();
return;
}
@@ -414,13 +433,13 @@ frappe.request.cleanup = function(opts, r) {
let handlers = [].concat(global_handlers, request_handler).filter(Boolean);
if (r.exc_type) {
- handlers.forEach(handler => {
+ handlers.forEach((handler) => {
handler(r);
});
}
// show messages
- if(r._server_messages && !opts.silent) {
+ if (r._server_messages && !opts.silent) {
// show server messages if no handlers exist
if (handlers.length === 0) {
r._server_messages = JSON.parse(r._server_messages);
@@ -430,11 +449,11 @@ frappe.request.cleanup = function(opts, r) {
}
// show errors
- if(r.exc) {
+ if (r.exc) {
r.exc = JSON.parse(r.exc);
- if(r.exc instanceof Array) {
- r.exc.forEach(exc => {
- if(exc) {
+ if (r.exc instanceof Array) {
+ r.exc.forEach((exc) => {
+ if (exc) {
console.error(exc);
}
});
@@ -444,13 +463,15 @@ frappe.request.cleanup = function(opts, r) {
}
// debug messages
- if(r._debug_messages) {
- if(opts.args) {
+ if (r._debug_messages) {
+ if (opts.args) {
console.log("======== arguments ========");
console.log(opts.args);
- console.log("========")
+ console.log("========");
}
- $.each(JSON.parse(r._debug_messages), function(i, v) { console.log(v); });
+ $.each(JSON.parse(r._debug_messages), function (i, v) {
+ console.log(v);
+ });
console.log("======== response ========");
delete r._debug_messages;
console.log(r);
@@ -459,11 +480,11 @@ frappe.request.cleanup = function(opts, r) {
}
frappe.last_response = r;
-}
+};
frappe.after_server_call = () => {
- if(frappe.request.ajax_count) {
- return new Promise(resolve => {
+ if (frappe.request.ajax_count) {
+ return new Promise((resolve) => {
frappe.request.waiting_for_ajax.push(() => {
resolve();
});
@@ -473,21 +494,21 @@ frappe.after_server_call = () => {
}
};
-frappe.after_ajax = function(fn) {
- return new Promise(resolve => {
- if(frappe.request.ajax_count) {
+frappe.after_ajax = function (fn) {
+ return new Promise((resolve) => {
+ if (frappe.request.ajax_count) {
frappe.request.waiting_for_ajax.push(() => {
- if(fn) return resolve(fn());
+ if (fn) return resolve(fn());
resolve();
});
} else {
- if(fn) return resolve(fn());
+ if (fn) return resolve(fn());
resolve();
}
});
};
-frappe.request.report_error = function(xhr, request_opts) {
+frappe.request.report_error = function (xhr, request_opts) {
var data = JSON.parse(xhr.responseText);
var exc;
if (data.exc) {
@@ -502,55 +523,57 @@ frappe.request.report_error = function(xhr, request_opts) {
}
const copy_markdown_to_clipboard = () => {
- const code_block = snippet => '```\n' + snippet + '\n```';
+ const code_block = (snippet) => "```\n" + snippet + "\n```";
const traceback_info = [
- '### App Versions',
+ "### App Versions",
code_block(JSON.stringify(frappe.boot.versions, null, "\t")),
- '### Route',
+ "### Route",
code_block(frappe.get_route_str()),
- '### Trackeback',
+ "### Traceback",
code_block(exc),
- '### Request Data',
+ "### Request Data",
code_block(JSON.stringify(request_opts, null, "\t")),
- '### Response Data',
- code_block(JSON.stringify(data, null, '\t')),
+ "### Response Data",
+ code_block(JSON.stringify(data, null, "\t")),
].join("\n");
frappe.utils.copy_to_clipboard(traceback_info);
};
-
- var show_communication = function() {
+ var show_communication = function () {
var error_report_message = [
- 'Please type some additional information that could help us reproduce this issue: ',
+ "Please type some additional information that could help us reproduce this issue: ",
'
',
- ' ',
- 'App Versions ',
- '' + JSON.stringify(frappe.boot.versions, null, "\t") + ' ',
- 'Route ',
- '' + frappe.get_route_str() + ' ',
- ' ',
- 'Error Report ',
- '' + exc + ' ',
- ' ',
- 'Request Data ',
- '' + JSON.stringify(request_opts, null, "\t") + ' ',
- ' ',
- 'Response JSON ',
- '' + JSON.stringify(data, null, '\t')+ ' '
+ " ",
+ "App Versions ",
+ "" + JSON.stringify(frappe.boot.versions, null, "\t") + " ",
+ "Route ",
+ "" + frappe.get_route_str() + " ",
+ " ",
+ "Error Report ",
+ "" + exc + " ",
+ " ",
+ "Request Data ",
+ "" + JSON.stringify(request_opts, null, "\t") + " ",
+ " ",
+ "Response JSON ",
+ "" + JSON.stringify(data, null, "\t") + " ",
].join("\n");
var communication_composer = new frappe.views.CommunicationComposer({
- subject: 'Error Report [' + frappe.datetime.nowdate() + ']',
+ subject: "Error Report [" + frappe.datetime.nowdate() + "]",
recipients: error_report_email,
message: error_report_message,
doc: {
doctype: "User",
- name: frappe.session.user
- }
+ name: frappe.session.user,
+ },
});
- communication_composer.dialog.$wrapper.css("z-index", cint(frappe.msg_dialog.$wrapper.css("z-index")) + 1);
- }
+ communication_composer.dialog.$wrapper.css(
+ "z-index",
+ cint(frappe.msg_dialog.$wrapper.css("z-index")) + 1
+ );
+ };
if (exc) {
var error_report_email = frappe.boot.error_report_email;
@@ -561,40 +584,38 @@ frappe.request.report_error = function(xhr, request_opts) {
if (!frappe.error_dialog) {
frappe.error_dialog = new frappe.ui.Dialog({
- title: __('Server Error'),
- primary_action_label: __('Report'),
+ title: __("Server Error"),
+ primary_action_label: __("Report"),
primary_action: () => {
if (error_report_email) {
show_communication();
} else {
- frappe.msgprint(__('Support Email Address Not Specified'));
+ frappe.msgprint(__("Support Email Address Not Specified"));
}
frappe.error_dialog.hide();
},
- secondary_action_label: __('Copy error to clipboard'),
+ secondary_action_label: __("Copy error to clipboard"),
secondary_action: () => {
copy_markdown_to_clipboard();
frappe.error_dialog.hide();
- }
+ },
});
- frappe.error_dialog.wrapper.classList.add('msgprint-dialog');
-
+ frappe.error_dialog.wrapper.classList.add("msgprint-dialog");
}
- let parts = strip(exc).split('\n');
+ let parts = strip(exc).split("\n");
frappe.error_dialog.$body.html(parts[parts.length - 1]);
frappe.error_dialog.show();
-
}
};
-frappe.request.cleanup_request_opts = function(request_opts) {
+frappe.request.cleanup_request_opts = function (request_opts) {
var doc = (request_opts.args || {}).doc;
if (doc) {
doc = JSON.parse(doc);
- $.each(Object.keys(doc), function(i, key) {
- if (key.indexOf("password")!==-1 && doc[key]) {
+ $.each(Object.keys(doc), function (i, key) {
+ if (key.indexOf("password") !== -1 && doc[key]) {
// mask the password
doc[key] = "*****";
}
@@ -604,19 +625,19 @@ frappe.request.cleanup_request_opts = function(request_opts) {
return request_opts;
};
-frappe.request.on_error = function(error_type, handler) {
+frappe.request.on_error = function (error_type, handler) {
frappe.request.error_handlers[error_type] = frappe.request.error_handlers[error_type] || [];
frappe.request.error_handlers[error_type].push(handler);
-}
+};
-$(document).ajaxSend(function() {
+$(document).ajaxSend(function () {
frappe.request.ajax_count++;
});
-$(document).ajaxComplete(function() {
+$(document).ajaxComplete(function () {
frappe.request.ajax_count--;
- if(!frappe.request.ajax_count) {
- $.each(frappe.request.waiting_for_ajax || [], function(i, fn) {
+ if (!frappe.request.ajax_count) {
+ $.each(frappe.request.waiting_for_ajax || [], function (i, fn) {
fn();
});
frappe.request.waiting_for_ajax = [];
diff --git a/frappe/public/js/frappe/roles_editor.js b/frappe/public/js/frappe/roles_editor.js
index 05f58692f6..312a90a82c 100644
--- a/frappe/public/js/frappe/roles_editor.js
+++ b/frappe/public/js/frappe/roles_editor.js
@@ -3,7 +3,7 @@ frappe.RoleEditor = class {
this.frm = frm;
this.wrapper = wrapper;
this.disable = disable;
- let user_roles = this.frm.doc.roles.map(a => a.role);
+ let user_roles = this.frm.doc.roles.map((a) => a.role);
this.multicheck = frappe.ui.form.make_control({
parent: wrapper,
df: {
@@ -12,36 +12,40 @@ frappe.RoleEditor = class {
select_all: true,
columns: 3,
get_data: () => {
- return frappe.xcall('frappe.core.doctype.user.user.get_all_roles').then(roles => {
- return roles.map(role => {
- return {
- label: __(role),
- value: role,
- checked: user_roles.includes(role)
- };
+ return frappe
+ .xcall("frappe.core.doctype.user.user.get_all_roles")
+ .then((roles) => {
+ return roles.map((role) => {
+ return {
+ label: __(role),
+ value: role,
+ checked: user_roles.includes(role),
+ };
+ });
});
- });
},
on_change: () => {
this.set_roles_in_table();
this.frm.dirty();
- }
+ },
},
- render_input: true
+ render_input: true,
});
let original_func = this.multicheck.make_checkboxes;
this.multicheck.make_checkboxes = () => {
original_func.call(this.multicheck);
- this.multicheck.$wrapper.find('.label-area').click(e => {
- let role = $(e.target).data('unit');
+ this.multicheck.$wrapper.find(".label-area").click((e) => {
+ let role = $(e.target).data("unit");
role && this.show_permissions(role);
e.preventDefault();
});
};
}
set_enable_disable() {
- $(this.wrapper).find('input[type="checkbox"]').attr('disabled', this.disable ? true : false);
+ $(this.wrapper)
+ .find('input[type="checkbox"]')
+ .attr("disabled", this.disable ? true : false);
}
show_permissions(role) {
// show permissions for a role
@@ -49,49 +53,59 @@ frappe.RoleEditor = class {
this.make_perm_dialog();
}
$(this.perm_dialog.body).empty();
- return frappe.xcall('frappe.core.doctype.user.user.get_perm_info', { role })
- .then(permissions => {
+ return frappe
+ .xcall("frappe.core.doctype.user.user.get_perm_info", { role })
+ .then((permissions) => {
const $body = $(this.perm_dialog.body);
if (!permissions.length) {
$body.append(`
- ${__('{0} role does not have permission on any doctype', [role])}
+ ${__("{0} role does not have permission on any doctype", [__(role)])}
`);
} else {
$body.append(`
- ${__('Document Type')}
- ${__('Level')}
- ${frappe.perm.rights.map(p => ` ${frappe.unscrub(p)} `).join("")}
+ ${__("Document Type")}
+ ${__("Level")}
+ ${frappe.perm.rights.map((p) => ` ${__(frappe.unscrub(p))} `).join("")}
`);
- permissions.forEach(perm => {
- $body.find('tbody').append(`
+ permissions.forEach((perm) => {
+ $body.find("tbody").append(`
- ${perm.parent}
+ ${__(perm.parent)}
${perm.permlevel}
- ${frappe.perm.rights.map(p => `${perm[p] ? frappe.utils.icon('check', 'xs') : '-'} `).join("")}
+ ${frappe.perm.rights
+ .map(
+ (p) =>
+ `${
+ perm[p] ? frappe.utils.icon("check", "xs") : "-"
+ } `
+ )
+ .join("")}
`);
});
}
- this.perm_dialog.set_title(role);
+ this.perm_dialog.set_title(__(role));
this.perm_dialog.show();
});
}
make_perm_dialog() {
this.perm_dialog = new frappe.ui.Dialog({
- title: __('Role Permissions')
+ title: __("Role Permissions"),
});
this.perm_dialog.$wrapper
- .find('.modal-dialog')
- .css("width", "1200px")
- .css("max-width", "80vw");
+ .find(".modal-dialog")
+ .css("width", "auto")
+ .css("max-width", "1200px");
+
+ this.perm_dialog.$wrapper.find(".modal-body").css("overflow", "overlay");
}
show() {
this.reset();
@@ -99,20 +113,20 @@ frappe.RoleEditor = class {
}
reset() {
- let user_roles = (this.frm.doc.roles || []).map(a => a.role);
+ let user_roles = (this.frm.doc.roles || []).map((a) => a.role);
this.multicheck.selected_options = user_roles;
this.multicheck.refresh_input();
}
set_roles_in_table() {
let roles = this.frm.doc.roles || [];
let checked_options = this.multicheck.get_checked_options();
- roles.map(role_doc => {
+ roles.map((role_doc) => {
if (!checked_options.includes(role_doc.role)) {
frappe.model.clear_doc(role_doc.doctype, role_doc.name);
}
});
- checked_options.map(role => {
- if (!roles.find(d => d.role === role)) {
+ checked_options.map((role) => {
+ if (!roles.find((d) => d.role === role)) {
let role_doc = frappe.model.add_child(this.frm.doc, "Has Role", "roles");
role_doc.role = role;
}
@@ -121,7 +135,7 @@ frappe.RoleEditor = class {
get_roles() {
return {
checked_roles: this.multicheck.get_checked_options(),
- unchecked_roles: this.multicheck.get_unchecked_options()
+ unchecked_roles: this.multicheck.get_unchecked_options(),
};
}
-};
\ No newline at end of file
+};
diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js
index a6bff4d72f..c03bfc9b95 100644
--- a/frappe/public/js/frappe/router.js
+++ b/frappe/public/js/frappe/router.js
@@ -4,17 +4,18 @@
// route urls to their virtual pages
// re-route map (for rename)
-frappe.provide('frappe.views');
-frappe.re_route = {"#login": ""};
+frappe.provide("frappe.views");
+frappe.re_route = { "#login": "" };
frappe.route_titles = {};
frappe.route_flags = {};
frappe.route_history = [];
frappe.view_factory = {};
frappe.view_factories = [];
frappe.route_options = null;
+frappe.open_in_new_tab = false;
frappe.route_hooks = {};
-$(window).on('hashchange', function(e) {
+$(window).on("hashchange", function (e) {
// v1 style routing, route is in hash
if (window.location.hash && !frappe.router.is_app_route(e.currentTarget.pathname)) {
let sub_path = frappe.router.get_sub_path(window.location.hash);
@@ -23,7 +24,7 @@ $(window).on('hashchange', function(e) {
}
});
-window.addEventListener('popstate', (e) => {
+window.addEventListener("popstate", (e) => {
// forward-back button, just re-render based on current route
frappe.router.route();
e.preventDefault();
@@ -31,75 +32,106 @@ window.addEventListener('popstate', (e) => {
});
// routing v2, capture all clicks so that the target is managed with push-state
-$('body').on('click', 'a', function(e) {
+$("body").on("click", "a", function (e) {
let override = (route) => {
e.preventDefault();
frappe.set_route(route);
return false;
};
- const href = e.currentTarget.getAttribute('href');
+ const target_element = e.currentTarget;
+ const href = target_element.getAttribute("href");
+ const is_on_same_host = target_element.hostname === window.location.hostname;
// click handled, but not by href
- if (e.currentTarget.getAttribute('onclick') // has a handler
- || (e.ctrlKey || e.metaKey) // open in a new tab
- || href==='#') { // hash is home
+ if (
+ target_element.getAttribute("onclick") || // has a handler
+ e.ctrlKey ||
+ e.metaKey || // open in a new tab
+ href === "#" // hash is home
+ ) {
return;
}
- if (href === '') {
- return override('/app');
+ if (href === "") {
+ return override("/app");
}
- if (href && href.startsWith('#')) {
+ if (href && href.startsWith("#")) {
// target startswith "#", this is a v1 style route, so remake it.
- return override(e.currentTarget.hash);
+ return override(target_element.hash);
}
- if (frappe.router.is_app_route(e.currentTarget.pathname)) {
+ if (is_on_same_host && frappe.router.is_app_route(target_element.pathname)) {
// target has "/app, this is a v2 style route.
- if (e.currentTarget.search) {
+ if (target_element.search) {
frappe.route_options = {};
- let params = new URLSearchParams(e.currentTarget.search);
+ let params = new URLSearchParams(target_element.search);
for (const [key, value] of params) {
frappe.route_options[key] = value;
}
}
- return override(e.currentTarget.pathname + e.currentTarget.hash);
+ return override(target_element.pathname + target_element.hash);
}
-
});
frappe.router = {
current_route: null,
routes: {},
- factory_views: ['form', 'list', 'report', 'tree', 'print', 'dashboard'],
- list_views: ['list', 'kanban', 'report', 'calendar', 'tree', 'gantt', 'dashboard', 'image', 'inbox'],
+ factory_views: ["form", "list", "report", "tree", "print", "dashboard"],
+ list_views: [
+ "list",
+ "kanban",
+ "report",
+ "calendar",
+ "tree",
+ "gantt",
+ "dashboard",
+ "image",
+ "inbox",
+ "map",
+ ],
+ list_views_route: {
+ list: "List",
+ kanban: "Kanban",
+ report: "Report",
+ calendar: "Calendar",
+ tree: "Tree",
+ gantt: "Gantt",
+ dashboard: "Dashboard",
+ image: "Image",
+ inbox: "Inbox",
+ file: "Home",
+ map: "Map",
+ },
layout_mapped: {},
is_app_route(path) {
// desk paths must begin with /app or doctype route
- if (path.substr(0, 1) === '/') path = path.substr(1);
- path = path.split('/');
+ if (path.substr(0, 1) === "/") path = path.substr(1);
+ path = path.split("/");
if (path[0]) {
- return path[0]==='app';
+ return path[0] === "app";
}
},
setup() {
// setup the route names by forming slugs of the given doctypes
for (let doctype of frappe.boot.user.can_read) {
- this.routes[this.slug(doctype)] = {doctype: doctype};
+ this.routes[this.slug(doctype)] = { doctype: doctype };
}
if (frappe.boot.doctype_layouts) {
for (let doctype_layout of frappe.boot.doctype_layouts) {
- this.routes[this.slug(doctype_layout.name)] = {doctype: doctype_layout.document_type, doctype_layout: doctype_layout.name };
+ this.routes[this.slug(doctype_layout.name)] = {
+ doctype: doctype_layout.document_type,
+ doctype_layout: doctype_layout.name,
+ };
}
}
},
- route() {
+ async route() {
// resolve the route from the URL or hash
// translate it so the objects are well defined
// and render the page as required
@@ -110,22 +142,22 @@ frappe.router = {
if (this.re_route(sub_path)) return;
this.current_sub_path = sub_path;
- this.current_route = this.parse();
+ this.current_route = await this.parse();
this.set_history(sub_path);
this.render();
this.set_title(sub_path);
- this.trigger('change');
+ this.trigger("change");
},
- parse(route) {
- route = this.get_sub_path_string(route).split('/');
+ async parse(route) {
+ route = this.get_sub_path_string(route).split("/");
if (!route) return [];
route = $.map(route, this.decode_component);
this.set_route_options_from_url();
- return this.convert_to_standard_route(route);
+ return await this.convert_to_standard_route(route);
},
- convert_to_standard_route(route) {
+ async convert_to_standard_route(route) {
// /app/settings = ["Workspaces", "Settings"]
// /app/private/settings = ["Workspaces", "private", "Settings"]
// /app/user = ["List", "User"]
@@ -139,59 +171,104 @@ frappe.router = {
if (frappe.workspaces[route[0]]) {
// public workspace
- route = ['Workspaces', frappe.workspaces[route[0]].title];
- } else if (route[0] == 'private' && frappe.workspaces[private_workspace]) {
+ route = ["Workspaces", frappe.workspaces[route[0]].title];
+ } else if (route[0] == "private" && frappe.workspaces[private_workspace]) {
// private workspace
- route = ['Workspaces', 'private', frappe.workspaces[private_workspace].title];
+ route = ["Workspaces", "private", frappe.workspaces[private_workspace].title];
} else if (this.routes[route[0]]) {
// route
- route = this.set_doctype_route(route);
+ route = await this.set_doctype_route(route);
}
return route;
},
doctype_route_exist(route) {
- route = this.get_sub_path_string(route).split('/');
+ route = this.get_sub_path_string(route).split("/");
return this.routes[route[0]];
},
set_doctype_route(route) {
let doctype_route = this.routes[route[0]];
- // doctype route
- if (route[1]) {
- if (route[2] && route[1]==='view') {
- route = this.get_standard_route_for_list(route, doctype_route);
- } else {
+
+ return frappe.model.with_doctype(doctype_route.doctype).then(() => {
+ // doctype route
+ let meta = frappe.get_meta(doctype_route.doctype);
+
+ if (route[1] && route[1] === "view" && route[2]) {
+ route = this.get_standard_route_for_list(
+ route,
+ doctype_route,
+ meta.force_re_route_to_default_view && meta.default_view
+ ? meta.default_view
+ : null
+ );
+ } else if (route[1] && route[1] !== "view") {
let docname = route[1];
if (route.length > 2) {
- docname = route.slice(1).join('/');
+ docname = route.slice(1).join("/");
}
- route = ['Form', doctype_route.doctype, docname];
+ route = ["Form", doctype_route.doctype, docname];
+ } else if (frappe.model.is_single(doctype_route.doctype)) {
+ route = ["Form", doctype_route.doctype, doctype_route.doctype];
+ } else if (meta.default_view) {
+ route = [
+ "List",
+ doctype_route.doctype,
+ this.list_views_route[meta.default_view.toLowerCase()],
+ ];
+ } else {
+ route = ["List", doctype_route.doctype, "List"];
}
- } else if (frappe.model.is_single(doctype_route.doctype)) {
- route = ['Form', doctype_route.doctype, doctype_route.doctype];
- } else {
- route = ['List', doctype_route.doctype, 'List'];
- }
-
- if (doctype_route.doctype_layout) {
- // set the layout
+ // reset the layout to avoid using incorrect views
this.doctype_layout = doctype_route.doctype_layout;
- }
-
- return route;
+ return route;
+ });
},
- get_standard_route_for_list(route, doctype_route) {
+ get_standard_route_for_list(route, doctype_route, default_view) {
let standard_route;
- if (route[2].toLowerCase()==='tree') {
- standard_route = ['Tree', doctype_route.doctype];
+ let _route = default_view || route[2] || "";
+
+ if (_route.toLowerCase() === "tree") {
+ standard_route = ["Tree", doctype_route.doctype];
} else {
- standard_route = ['List', doctype_route.doctype, frappe.utils.to_title_case(route[2])];
+ let new_route = this.list_views_route[_route.toLowerCase()];
+ let re_route = route[2].toLowerCase() !== new_route.toLowerCase();
+
+ if (re_route) {
+ /**
+ * In case of force_re_route, the url of the route should change,
+ * if the _route and route[2] are different, it means there is a default_view
+ * with force_re_route enabled.
+ *
+ * To change the url, to the correct view, the route[2] is changed with default_view
+ *
+ * Eg: If default_view is set to Report with force_re_route enabled and user routes
+ * to List,
+ * route: [todo, view, list]
+ * default_view: report
+ *
+ * replaces the list to report and re-routes to the new route but should be replaced in
+ * the history since the list route should not exist in history as we are rerouting it to
+ * report
+ */
+ frappe.route_flags.replace_route = true;
+
+ route[2] = _route.toLowerCase();
+ this.set_route(route);
+ }
+
+ standard_route = [
+ "List",
+ doctype_route.doctype,
+ this.list_views_route[_route.toLowerCase()],
+ ];
+
// calendar / kanban / dashboard / folder
if (route[3]) standard_route.push(...route.slice(3, route.length));
}
+
return standard_route;
},
@@ -205,7 +282,7 @@ frappe.router = {
this.render_page();
} else {
// Show home
- frappe.views.pageview.show('');
+ frappe.views.pageview.show("");
}
},
@@ -265,44 +342,49 @@ frappe.router = {
// example 3: frappe.set_route('a/b/c');
let route = Array.from(arguments);
- return new Promise(resolve => {
+ return new Promise((resolve) => {
route = this.get_route_from_arguments(route);
route = this.convert_from_standard_route(route);
let sub_path = this.make_url(route);
// replace each # occurrences in the URL with encoded character except for last
// sub_path = sub_path.replace(/[#](?=.*[#])/g, "%23");
- this.push_state(sub_path);
-
+ if (frappe.open_in_new_tab) {
+ localStorage["route_options"] = JSON.stringify(frappe.route_options);
+ window.open(sub_path, "_blank");
+ frappe.open_in_new_tab = false;
+ } else {
+ this.push_state(sub_path);
+ }
setTimeout(() => {
- frappe.after_ajax && frappe.after_ajax(() => {
- resolve();
- });
+ frappe.after_ajax &&
+ frappe.after_ajax(() => {
+ resolve();
+ });
}, 100);
- }).finally(() => frappe.route_flags = {});
+ }).finally(() => (frappe.route_flags = {}));
},
get_route_from_arguments(route) {
- if (route.length===1 && $.isArray(route[0])) {
+ if (route.length === 1 && $.isArray(route[0])) {
// called as frappe.set_route(['a', 'b', 'c']);
route = route[0];
}
- if (route.length===1 && route[0] && route[0].includes('/')) {
+ if (route.length === 1 && route[0] && route[0].includes("/")) {
// called as frappe.set_route('a/b/c')
- route = $.map(route[0].split('/'), this.decode_component);
+ route = $.map(route[0].split("/"), this.decode_component);
}
- if (route && route[0] == '') {
+ if (route && route[0] == "") {
route.shift();
}
- if (route && ['desk', 'app'].includes(route[0])) {
+ if (route && ["desk", "app"].includes(route[0])) {
// we only need subpath, remove "app" (or "desk")
route.shift();
}
return route;
-
},
convert_from_standard_route(route) {
@@ -310,11 +392,11 @@ frappe.router = {
// ["Form", "Sales Order", "SO-0001"] => /sales-order/SO-0001
// ["Tree", "Account"] = /account/view/tree
- const view = route[0] ? route[0].toLowerCase() : '';
+ const view = route[0] ? route[0].toLowerCase() : "";
let new_route = route;
- if (view === 'list') {
- if (route[2] && route[2] !== 'list' && !$.isPlainObject(route[2])) {
- new_route = [this.slug(route[1]), 'view', route[2].toLowerCase()];
+ if (view === "list") {
+ if (route[2] && route[2] !== "list" && !$.isPlainObject(route[2])) {
+ new_route = [this.slug(route[1]), "view", route[2].toLowerCase()];
// calendar / inbox / file folder
if (route[3]) new_route.push(...route.slice(3, route.length));
@@ -324,15 +406,16 @@ frappe.router = {
}
new_route = [this.slug(route[1])];
}
- } else if (view === 'form') {
+ } else if (view === "form") {
new_route = [this.slug(route[1])];
if (route[2]) {
// if not single
new_route.push(route[2]);
}
- } else if (view === 'tree') {
- new_route = [this.slug(route[1]), 'view', 'tree'];
+ } else if (view === "tree") {
+ new_route = [this.slug(route[1]), "view", "tree"];
}
+
return new_route;
},
@@ -348,28 +431,26 @@ frappe.router = {
},
make_url(params) {
- let path_string = $.map(params, function(a) {
+ let path_string = $.map(params, function (a) {
if ($.isPlainObject(a)) {
frappe.route_options = a;
return null;
} else {
- a = String(a);
- if (a && a.match(/[%'"#\s\t]/)) {
- // if special chars, then encode
- a = encodeURIComponent(a);
- }
- return a;
+ return encodeURIComponent(String(a));
}
- }).join('/');
+ }).join("/");
let private_home = frappe.workspaces[`home-${frappe.user.name.toLowerCase()}`];
- let default_page = private_home ? 'private/home' : frappe.workspaces['home'] ? 'home' : Object.keys(frappe.workspaces)[0];
- return '/app/' + (path_string || default_page);
+ let default_page = private_home
+ ? "private/home"
+ : frappe.workspaces["home"]
+ ? "home"
+ : Object.keys(frappe.workspaces)[0];
+ return "/app/" + (path_string || default_page);
},
push_state(url) {
// change the URL and call the router
if (window.location.pathname !== url) {
-
// push/replace state so the browser looks fine
const method = frappe.route_flags.replace_route ? "replaceState" : "pushState";
history[method](null, null, url);
@@ -384,7 +465,7 @@ frappe.router = {
// supports both v1 and v2 routing
if (!route) {
route = window.location.pathname;
- if (route.includes('app#')) {
+ if (route.includes("app#")) {
// to support v1
route = window.location.hash;
}
@@ -394,18 +475,18 @@ frappe.router = {
},
strip_prefix(route) {
- if (route.substr(0, 1)=='/') route = route.substr(1); // for /app/sub
- if (route.startsWith('app/')) route = route.substr(4); // for desk/sub
- if (route == 'app') route = route.substr(4); // for /app
- if (route.substr(0, 1)=='/') route = route.substr(1);
- if (route.substr(0, 1)=='#') route = route.substr(1);
- if (route.substr(0, 1)=='!') route = route.substr(1);
+ if (route.substr(0, 1) == "/") route = route.substr(1); // for /app/sub
+ if (route.startsWith("app/")) route = route.substr(4); // for desk/sub
+ if (route == "app") route = route.substr(4); // for /app
+ if (route.substr(0, 1) == "/") route = route.substr(1);
+ if (route.substr(0, 1) == "#") route = route.substr(1);
+ if (route.substr(0, 1) == "!") route = route.substr(1);
return route;
},
get_sub_path(route) {
var sub_path = this.get_sub_path_string(route);
- route = $.map(sub_path.split('/'), this.decode_component).join('/');
+ route = $.map(sub_path.split("/"), this.decode_component).join("/");
return route;
},
@@ -418,6 +499,11 @@ frappe.router = {
frappe.route_options = {};
}
+ if (localStorage.getItem("route_options")) {
+ frappe.route_options = JSON.parse(localStorage.getItem("route_options"));
+ localStorage.removeItem("route_options");
+ }
+
let params = new URLSearchParams(query_string);
for (const [key, value] of params) {
frappe.route_options[key] = value;
@@ -438,18 +524,18 @@ frappe.router = {
},
slug(name) {
- return name.toLowerCase().replace(/ /g, '-');
- }
+ return name.toLowerCase().replace(/ /g, "-");
+ },
};
// global functions for backward compatibility
frappe.get_route = () => frappe.router.current_route;
-frappe.get_route_str = () => frappe.router.current_route.join('/');
-frappe.set_route = function() {
+frappe.get_route_str = () => frappe.router.current_route.join("/");
+frappe.set_route = function () {
return frappe.router.set_route.apply(frappe.router, arguments);
};
-frappe.get_prev_route = function() {
+frappe.get_prev_route = function () {
if (frappe.route_history && frappe.route_history.length > 1) {
return frappe.route_history[frappe.route_history.length - 2];
} else {
@@ -457,13 +543,13 @@ frappe.get_prev_route = function() {
}
};
-frappe.set_re_route = function() {
+frappe.set_re_route = function () {
var tmp = frappe.router.get_sub_path();
frappe.set_route.apply(null, arguments);
frappe.re_route[tmp] = frappe.router.get_sub_path();
};
-frappe.has_route_options = function() {
+frappe.has_route_options = function () {
return Boolean(Object.keys(frappe.route_options || {}).length);
};
diff --git a/frappe/public/js/frappe/router_history.js b/frappe/public/js/frappe/router_history.js
index 14b936f5e8..a5b344c083 100644
--- a/frappe/public/js/frappe/router_history.js
+++ b/frappe/public/js/frappe/router_history.js
@@ -1,27 +1,28 @@
frappe.route_history_queue = [];
-const routes_to_skip = ['Form', 'social', 'setup-wizard', 'recorder'];
+const routes_to_skip = ["Form", "social", "setup-wizard", "recorder"];
const save_routes = frappe.utils.debounce(() => {
- if (frappe.session.user === 'Guest') return;
+ if (frappe.session.user === "Guest") return;
const routes = frappe.route_history_queue;
if (!routes.length) return;
frappe.route_history_queue = [];
- frappe.xcall('frappe.desk.doctype.route_history.route_history.deferred_insert', {
- 'routes': routes
- }).catch(() => {
- frappe.route_history_queue.concat(routes);
- });
-
+ frappe
+ .xcall("frappe.desk.doctype.route_history.route_history.deferred_insert", {
+ routes: routes,
+ })
+ .catch(() => {
+ frappe.route_history_queue.concat(routes);
+ });
}, 10000);
-frappe.router.on('change', () => {
+frappe.router.on("change", () => {
const route = frappe.get_route();
if (is_route_useful(route)) {
frappe.route_history_queue.push({
- 'creation': frappe.datetime.now_datetime(),
- 'route': frappe.get_route_str()
+ creation: frappe.datetime.now_datetime(),
+ route: frappe.get_route_str(),
});
save_routes();
@@ -31,9 +32,9 @@ frappe.router.on('change', () => {
function is_route_useful(route) {
if (!route[1]) {
return false;
- } else if ((route[0] === 'List' && !route[2]) || routes_to_skip.includes(route[0])) {
+ } else if ((route[0] === "List" && !route[2]) || routes_to_skip.includes(route[0])) {
return false;
} else {
return true;
}
-}
\ No newline at end of file
+}
diff --git a/frappe/public/js/frappe/scanner/index.js b/frappe/public/js/frappe/scanner/index.js
index 0b9acf7f9e..a7b38cbde3 100644
--- a/frappe/public/js/frappe/scanner/index.js
+++ b/frappe/public/js/frappe/scanner/index.js
@@ -45,11 +45,12 @@ frappe.ui.Scanner = class Scanner {
this.hide_dialog();
}
},
- errorMessage => { // eslint-disable-line
+ (errorMessage) => {
+ // eslint-disable-line
// parse error, ignore it.
}
)
- .catch(err => {
+ .catch((err) => {
this.is_alive = false;
this.hide_dialog();
console.error(err); // eslint-disable-line
@@ -73,8 +74,8 @@ frappe.ui.Scanner = class Scanner {
fields: [
{
fieldtype: "HTML",
- fieldname: "scan_area"
- }
+ fieldname: "scan_area",
+ },
],
on_page_show: () => {
this.$scan_area = dialog.get_field("scan_area").$wrapper;
@@ -84,7 +85,7 @@ frappe.ui.Scanner = class Scanner {
},
on_hide: () => {
this.stop_scan();
- }
+ },
});
return dialog;
}
@@ -94,8 +95,6 @@ frappe.ui.Scanner = class Scanner {
}
load_lib() {
- return frappe.require(
- "/assets/frappe/node_modules/html5-qrcode/dist/html5-qrcode.min.js"
- );
+ return frappe.require("/assets/frappe/node_modules/html5-qrcode/dist/html5-qrcode.min.js");
}
};
diff --git a/frappe/public/js/frappe/socketio_client.js b/frappe/public/js/frappe/socketio_client.js
index 615b73e82a..f67da84ef4 100644
--- a/frappe/public/js/frappe/socketio_client.js
+++ b/frappe/public/js/frappe/socketio_client.js
@@ -3,7 +3,8 @@ frappe.socketio = {
open_tasks: {},
open_docs: [],
emit_queue: [],
- init: function(port = 3000) {
+
+ init: function (port = 3000) {
if (frappe.boot.disable_async) {
return;
}
@@ -14,27 +15,16 @@ frappe.socketio = {
// Enable secure option when using HTTPS
if (window.location.protocol == "https:") {
- frappe.socketio.socket = io.connect(
- frappe.socketio.get_host(port),
- {
- secure: true,
- withCredentials: true,
- }
- );
+ frappe.socketio.socket = io.connect(frappe.socketio.get_host(port), {
+ secure: true,
+ withCredentials: true,
+ reconnectionAttempts: 3,
+ });
} else if (window.location.protocol == "http:") {
- frappe.socketio.socket = io.connect(
- frappe.socketio.get_host(port),
- {
- withCredentials: true,
- }
- );
- } else if (window.location.protocol == "file:") {
- frappe.socketio.socket = io.connect(
- window.localStorage.server,
- {
- withCredentials: true,
- }
- );
+ frappe.socketio.socket = io.connect(frappe.socketio.get_host(port), {
+ withCredentials: true,
+ reconnectionAttempts: 3,
+ });
}
if (!frappe.socketio.socket) {
@@ -42,34 +32,36 @@ frappe.socketio = {
return;
}
- frappe.socketio.socket.on('msgprint', function(message) {
+ frappe.socketio.socket.on("msgprint", function (message) {
frappe.msgprint(message);
});
- frappe.socketio.socket.on('progress', function(data) {
- if(data.progress) {
- data.percent = flt(data.progress[0]) / data.progress[1] * 100;
+ frappe.socketio.socket.on("progress", function (data) {
+ if (data.progress) {
+ data.percent = (flt(data.progress[0]) / data.progress[1]) * 100;
}
- if(data.percent) {
- if(data.percent==100) {
- frappe.hide_progress();
- } else {
- frappe.show_progress(data.title || __("Progress"), data.percent, 100, data.description);
- }
+ if (data.percent) {
+ frappe.show_progress(
+ data.title || __("Progress"),
+ data.percent,
+ 100,
+ data.description,
+ true
+ );
}
});
frappe.socketio.setup_listeners();
frappe.socketio.setup_reconnect();
- $(document).on('form-load form-rename', function(e, frm) {
- if (frm.is_new()) {
+ $(document).on("form-load form-rename", function (e, frm) {
+ if (!frm.doc || frm.is_new()) {
return;
}
- for (var i=0, l=frappe.socketio.open_docs.length; i {
- if (!cur_frm || cur_frm.is_new()) {
+ window.addEventListener("beforeunload", () => {
+ if (!cur_frm || !cur_frm.doc || cur_frm.is_new()) {
return;
}
- // if tab/window is closed, notify other users
- if (cur_frm.doc) {
- frappe.socketio.doc_close(cur_frm.doctype, cur_frm.docname);
- }
+ frappe.socketio.doc_close(cur_frm.doctype, cur_frm.docname);
});
},
- get_host: function(port = 3000) {
+ get_host: function (port = 3000) {
var host = window.location.origin;
- if(window.dev_server) {
+ if (window.dev_server) {
var parts = host.split(":");
- port = frappe.boot.socketio_port || port.toString() || '3000';
- if(parts.length > 2) {
+ port = frappe.boot.socketio_port || port.toString() || "3000";
+ if (parts.length > 2) {
host = parts[0] + ":" + parts[1];
}
host = host + ":" + port;
}
return host;
},
- subscribe: function(task_id, opts) {
+ subscribe: function (task_id, opts) {
// TODO DEPRECATE
- frappe.socketio.socket.emit('task_subscribe', task_id);
- frappe.socketio.socket.emit('progress_subscribe', task_id);
+ frappe.socketio.socket.emit("task_subscribe", task_id);
+ frappe.socketio.socket.emit("progress_subscribe", task_id);
frappe.socketio.open_tasks[task_id] = opts;
},
- task_subscribe: function(task_id) {
- frappe.socketio.socket.emit('task_subscribe', task_id);
+ task_subscribe: function (task_id) {
+ frappe.socketio.socket.emit("task_subscribe", task_id);
},
- task_unsubscribe: function(task_id) {
- frappe.socketio.socket.emit('task_unsubscribe', task_id);
+ task_unsubscribe: function (task_id) {
+ frappe.socketio.socket.emit("task_unsubscribe", task_id);
},
- doc_subscribe: function(doctype, docname) {
+ doctype_subscribe: function (doctype) {
+ frappe.socketio.socket.emit("doctype_subscribe", doctype);
+ },
+ doctype_unsubscribe: function (doctype) {
+ frappe.socketio.socket.emit("doctype_unsubscribe", doctype);
+ },
+ doc_subscribe: function (doctype, docname) {
if (frappe.flags.doc_subscribe) {
- console.log('throttled');
+ console.log("throttled");
return;
}
frappe.flags.doc_subscribe = true;
// throttle to 1 per sec
- setTimeout(function() { frappe.flags.doc_subscribe = false }, 1000);
+ setTimeout(function () {
+ frappe.flags.doc_subscribe = false;
+ }, 1000);
- frappe.socketio.socket.emit('doc_subscribe', doctype, docname);
- frappe.socketio.open_docs.push({doctype: doctype, docname: docname});
+ frappe.socketio.socket.emit("doc_subscribe", doctype, docname);
+ frappe.socketio.open_docs.push({ doctype: doctype, docname: docname });
},
- doc_unsubscribe: function(doctype, docname) {
- frappe.socketio.socket.emit('doc_unsubscribe', doctype, docname);
- frappe.socketio.open_docs = $.filter(frappe.socketio.open_docs, function(d) {
- if(d.doctype===doctype && d.name===docname) {
+ doc_unsubscribe: function (doctype, docname) {
+ frappe.socketio.socket.emit("doc_unsubscribe", doctype, docname);
+ frappe.socketio.open_docs = $.filter(frappe.socketio.open_docs, function (d) {
+ if (d.doctype === doctype && d.name === docname) {
return null;
} else {
return d;
}
- })
+ });
},
- doc_open: function(doctype, docname) {
+ doc_open: function (doctype, docname) {
// notify that the user has opened this doc, if not already notified
- if (!frappe.socketio.last_doc
- || (frappe.socketio.last_doc[0] != doctype || frappe.socketio.last_doc[1] != docname)) {
- frappe.socketio.socket.emit('doc_open', doctype, docname);
+ if (
+ !frappe.socketio.last_doc ||
+ frappe.socketio.last_doc[0] != doctype ||
+ frappe.socketio.last_doc[1] != docname
+ ) {
+ frappe.socketio.socket.emit("doc_open", doctype, docname);
frappe.socketio.last_doc &&
- frappe.socketio.doc_close(frappe.socketio.last_doc[0], frappe.socketio.last_doc[1]);
+ frappe.socketio.doc_close(
+ frappe.socketio.last_doc[0],
+ frappe.socketio.last_doc[1]
+ );
}
frappe.socketio.last_doc = [doctype, docname];
},
- doc_close: function(doctype, docname) {
+ doc_close: function (doctype, docname) {
// notify that the user has closed this doc
- frappe.socketio.socket.emit('doc_close', doctype, docname);
+ frappe.socketio.socket.emit("doc_close", doctype, docname);
// if the doc is closed the user has also stopped typing
- frappe.socketio.socket.emit('doc_typing_stopped', doctype, docname);
+ frappe.socketio.socket.emit("doc_typing_stopped", doctype, docname);
},
- form_typing: function(doctype, docname) {
+ form_typing: function (doctype, docname) {
// notifiy that the user is typing on the doc
- frappe.socketio.socket.emit('doc_typing', doctype, docname);
+ frappe.socketio.socket.emit("doc_typing", doctype, docname);
},
- form_stopped_typing: function(doctype, docname) {
+ form_stopped_typing: function (doctype, docname) {
// notifiy that the user has stopped typing
- frappe.socketio.socket.emit('doc_typing_stopped', doctype, docname);
+ frappe.socketio.socket.emit("doc_typing_stopped", doctype, docname);
},
- setup_listeners: function() {
- frappe.socketio.socket.on('task_status_change', function(data) {
+ setup_listeners: function () {
+ frappe.socketio.socket.on("task_status_change", function (data) {
frappe.socketio.process_response(data, data.status.toLowerCase());
});
- frappe.socketio.socket.on('task_progress', function(data) {
+ frappe.socketio.socket.on("task_progress", function (data) {
frappe.socketio.process_response(data, "progress");
});
},
- setup_reconnect: function() {
+ setup_reconnect: function () {
// subscribe again to open_tasks
- frappe.socketio.socket.on("connect", function() {
+ frappe.socketio.socket.on("connect", function () {
// wait for 5 seconds before subscribing again
// because it takes more time to start python server than nodejs server
// and we use validation requests to python server for subscribing
- setTimeout(function() {
- $.each(frappe.socketio.open_tasks, function(task_id, opts) {
+ setTimeout(function () {
+ $.each(frappe.socketio.open_tasks, function (task_id, opts) {
frappe.socketio.subscribe(task_id, opts);
});
// re-connect open docs
- $.each(frappe.socketio.open_docs, function(d) {
- if(locals[d.doctype] && locals[d.doctype][d.name]) {
+ $.each(frappe.socketio.open_docs, function (d) {
+ if (locals[d.doctype] && locals[d.doctype][d.name]) {
frappe.socketio.doc_subscribe(d.doctype, d.name);
}
});
- if (cur_frm && cur_frm.doc) {
+ if (cur_frm && cur_frm.doc && !cur_frm.is_new()) {
frappe.socketio.doc_open(cur_frm.doc.doctype, cur_frm.doc.name);
}
}, 5000);
});
},
- process_response: function(data, method) {
- if(!data) {
+ process_response: function (data, method) {
+ if (!data) {
return;
}
// success
var opts = frappe.socketio.open_tasks[data.task_id];
- if(opts[method]) {
+ if (opts[method]) {
opts[method](data);
}
// "callback" is std frappe term
- if(method==="success") {
- if(opts.callback) opts.callback(data);
+ if (method === "success") {
+ if (opts.callback) opts.callback(data);
}
// always
frappe.request.cleanup(opts, data);
- if(opts.always) {
+ if (opts.always) {
opts.always(data);
}
// error
- if(data.status_code && data.status_code > 400 && opts.error) {
+ if (data.status_code && data.status_code > 400 && opts.error) {
opts.error(data);
}
- }
-}
+ },
+};
frappe.provide("frappe.realtime");
-frappe.realtime.on = function(event, callback) {
+frappe.realtime.on = function (event, callback) {
frappe.socketio.socket && frappe.socketio.socket.on(event, callback);
};
-frappe.realtime.off = function(event, callback) {
+frappe.realtime.off = function (event, callback) {
frappe.socketio.socket && frappe.socketio.socket.off(event, callback);
-}
+};
-frappe.realtime.publish = function(event, message) {
- if(frappe.socketio.socket) {
+frappe.realtime.publish = function (event, message) {
+ if (frappe.socketio.socket) {
frappe.socketio.socket.emit(event, message);
}
-}
-
+};
diff --git a/frappe/public/js/frappe/translate.js b/frappe/public/js/frappe/translate.js
index ca407329aa..de111c8e1e 100644
--- a/frappe/public/js/frappe/translate.js
+++ b/frappe/public/js/frappe/translate.js
@@ -2,13 +2,13 @@
// MIT License. See license.txt
// for translation
-frappe._ = function(txt, replace, context = null) {
+frappe._ = function (txt, replace, context = null) {
if (!txt) return txt;
if (typeof txt != "string") return txt;
- let translated_text = '';
+ let translated_text = "";
- let key = txt; // txt.replace(/\n/g, "");
+ let key = txt; // txt.replace(/\n/g, "");
if (context) {
translated_text = frappe._messages[`${key}:${context}`];
}
@@ -25,13 +25,13 @@ frappe._ = function(txt, replace, context = null) {
window.__ = frappe._;
-frappe.get_languages = function() {
+frappe.get_languages = function () {
if (!frappe.languages) {
frappe.languages = [];
- $.each(frappe.boot.lang_dict, function(lang, value) {
+ $.each(frappe.boot.lang_dict, function (lang, value) {
frappe.languages.push({ label: lang, value: value });
});
- frappe.languages = frappe.languages.sort(function(a, b) {
+ frappe.languages = frappe.languages.sort(function (a, b) {
return a.value < b.value ? -1 : 1;
});
}
diff --git a/frappe/public/js/frappe/ui/alt_keyboard_shortcuts.js b/frappe/public/js/frappe/ui/alt_keyboard_shortcuts.js
index c97c4c343d..389bba9ce7 100644
--- a/frappe/public/js/frappe/ui/alt_keyboard_shortcuts.js
+++ b/frappe/public/js/frappe/ui/alt_keyboard_shortcuts.js
@@ -1,4 +1,4 @@
-frappe.provide('frappe.ui.keys');
+frappe.provide("frappe.ui.keys");
let shortcut_groups = new WeakMap();
let shortcut_group_list = [];
@@ -22,23 +22,23 @@ frappe.ui.keys.bind_shortcut_group_event = () => {
function highlight_alt_shortcuts() {
if ($current_dropdown) {
- $current_dropdown.addClass('alt-pressed');
- $body.removeClass('alt-pressed');
+ $current_dropdown.addClass("alt-pressed");
+ $body.removeClass("alt-pressed");
} else {
- $body.addClass('alt-pressed');
- $current_dropdown && $current_dropdown.removeClass('alt-pressed');
+ $body.addClass("alt-pressed");
+ $current_dropdown && $current_dropdown.removeClass("alt-pressed");
}
}
function unhighlight_alt_shortcuts() {
- $current_dropdown && $current_dropdown.removeClass('alt-pressed');
- $body.removeClass('alt-pressed');
+ $current_dropdown && $current_dropdown.removeClass("alt-pressed");
+ $body.removeClass("alt-pressed");
}
- $(document).on('keydown', (e) => {
- let key = (frappe.ui.keys.key_map[e.which] || '').toLowerCase();
+ $(document).on("keydown", (e) => {
+ let key = (frappe.ui.keys.key_map[e.which] || "").toLowerCase();
- if (key === 'alt') {
+ if (key === "alt") {
highlight_alt_shortcuts();
}
@@ -55,12 +55,12 @@ frappe.ui.keys.bind_shortcut_group_event = () => {
highlight_alt_shortcuts();
}
});
- $(document).on('keyup', (e) => {
- if (e.key === 'Alt') {
+ $(document).on("keyup", (e) => {
+ if (e.key === "Alt") {
unhighlight_alt_shortcuts();
}
});
- $(document).on('mousemove', () => {
+ $(document).on("mousemove", () => {
unhighlight_alt_shortcuts();
});
};
@@ -71,22 +71,22 @@ function get_shortcut_for_key(key) {
// Priority 2: Current Page
let shortcuts = shortcut_group_list
- .filter(shortcut_group => key in shortcut_group.shortcuts_dict)
- .map(shortcut_group => shortcut_group.shortcuts_dict[key])
- .filter(shortcut => shortcut.$target.is(':visible'));
+ .filter((shortcut_group) => key in shortcut_group.shortcuts_dict)
+ .map((shortcut_group) => shortcut_group.shortcuts_dict[key])
+ .filter((shortcut) => shortcut.$target.is(":visible"));
let shortcut = null;
- if ($current_dropdown && $current_dropdown.is('.open')) {
- shortcut = shortcuts.find(
- shortcut => $.contains($current_dropdown[0], shortcut.$target[0])
+ if ($current_dropdown && $current_dropdown.is(".open")) {
+ shortcut = shortcuts.find((shortcut) =>
+ $.contains($current_dropdown[0], shortcut.$target[0])
);
}
if (shortcut) return shortcut;
- shortcut = shortcuts.find(
- shortcut => $.contains(window.cur_page.page.page.wrapper[0], shortcut.$target[0])
+ shortcut = shortcuts.find((shortcut) =>
+ $.contains(window.cur_page.page.page.wrapper[0], shortcut.$target[0])
);
return shortcut;
@@ -102,15 +102,15 @@ frappe.ui.keys.AltShortcutGroup = class AltShortcutGroup {
}
bind_events() {
- $(document).on('show.bs.dropdown', (e) => {
- $current_dropdown && $current_dropdown.removeClass('alt-pressed');
+ $(document).on("show.bs.dropdown", (e) => {
+ $current_dropdown && $current_dropdown.removeClass("alt-pressed");
let $target = $(e.target);
- if ($target.is('.dropdown, .btn-group')) {
+ if ($target.is(".dropdown, .btn-group")) {
$current_dropdown = $target;
}
});
- $(document).on('hide.bs.dropdown', () => {
- $current_dropdown && $current_dropdown.removeClass('alt-pressed');
+ $(document).on("hide.bs.dropdown", () => {
+ $current_dropdown && $current_dropdown.removeClass("alt-pressed");
$current_dropdown = null;
});
}
@@ -120,15 +120,15 @@ frappe.ui.keys.AltShortcutGroup = class AltShortcutGroup {
$text_el = $target;
}
let text_content = $text_el.text().trim();
- let letters = text_content.split('');
+ let letters = text_content.split("");
// first unused letter
- let shortcut_letter = letters.find(letter => {
+ let shortcut_letter = letters.find((letter) => {
letter = letter.toLowerCase();
- let is_valid_char = letter >= 'a' && letter <= 'z';
+ let is_valid_char = letter >= "a" && letter <= "z";
return !this.is_taken(letter) && is_valid_char;
});
if (!shortcut_letter) {
- $text_el.attr('data-label', encodeURIComponent(text_content));
+ $text_el.attr("data-label", encodeURIComponent(text_content));
return;
}
for (let key in this.shortcuts_dict) {
@@ -145,22 +145,25 @@ frappe.ui.keys.AltShortcutGroup = class AltShortcutGroup {
$target,
$text_el,
letter: shortcut_letter,
- text: text_content
+ text: text_content,
};
this.shortcuts_dict[shortcut_letter.toLowerCase()] = shortcut;
this.underline_text(shortcut);
}
underline_text(shortcut) {
- shortcut.$text_el.attr('data-label', encodeURIComponent(shortcut.text));
+ shortcut.$text_el.attr("data-label", encodeURIComponent(shortcut.text));
let underline_el_found = false;
- let text_html = shortcut.text.split('').map(letter => {
- if (letter === shortcut.letter && !underline_el_found) {
- letter = `${letter} `;
- underline_el_found = true;
- }
- return letter;
- }).join('');
+ let text_html = shortcut.text
+ .split("")
+ .map((letter) => {
+ if (letter === shortcut.letter && !underline_el_found) {
+ letter = `${letter} `;
+ underline_el_found = true;
+ }
+ return letter;
+ })
+ .join("");
let original_text_html = shortcut.$text_el.html();
text_html = original_text_html.replace(shortcut.text.trim(), text_html.trim());
shortcut.$text_el.html(text_html);
@@ -168,8 +171,8 @@ frappe.ui.keys.AltShortcutGroup = class AltShortcutGroup {
is_taken(letter) {
let is_in_global_shortcut = frappe.ui.keys.standard_shortcuts
- .filter(s => !s.page)
- .some(s => s.shortcut === `alt+${letter}`);
+ .filter((s) => !s.page)
+ .some((s) => s.shortcut === `alt+${letter}`);
return letter in this.shortcuts_dict || is_in_global_shortcut;
}
};
diff --git a/frappe/public/js/frappe/ui/app_icon.js b/frappe/public/js/frappe/ui/app_icon.js
index a2efb08aa4..840f423cb8 100644
--- a/frappe/public/js/frappe/ui/app_icon.js
+++ b/frappe/public/js/frappe/ui/app_icon.js
@@ -1,45 +1,62 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
-
frappe.provide("frappe.ui");
frappe.ui.app_icon = {
- get_html: function(module, small) {
+ get_html: function (module, small) {
var icon = module.icon;
var color = module.color;
- if (icon
- && icon.match(/([\uE000-\uF8FF]|\uD83C[\uDF00-\uDFFF]|\uD83D[\uDC00-\uDDFF])/g)) {
+ if (icon && icon.match(/([\uE000-\uF8FF]|\uD83C[\uDF00-\uDFFF]|\uD83D[\uDC00-\uDDFF])/g)) {
module.emoji = module.icon;
}
var icon_style = "";
- if(module.reverse) {
+ if (module.reverse) {
icon_style = "color: #36414C;";
}
- if(!color) {
- color = '#4aa3df';
+ if (!color) {
+ color = "#4aa3df";
}
// first letter
- if(!icon || module.emoji) {
- icon = '' + (module.emoji || module._label[0].toUpperCase()) + ' ';
- } else if(icon.split(".").slice(-1)[0]==="svg") {
+ if (!icon || module.emoji) {
+ icon =
+ '" +
+ (module.emoji || module._label[0].toUpperCase()) +
+ " ";
+ } else if (icon.split(".").slice(-1)[0] === "svg") {
$.ajax({
url: frappe.urllib.get_full_url(icon),
dataType: "text",
async: false,
- success: function(data) {
+ success: function (data) {
icon = data;
- }
+ },
});
- icon = ''+ icon+' ';
+ icon = '' + icon + " ";
} else {
- icon = ' ';
+ icon =
+ ' ';
}
- return ''+icon+'
';
- }
+ return (
+ '' +
+ icon +
+ "
"
+ );
+ },
};
diff --git a/frappe/public/js/frappe/ui/capture.js b/frappe/public/js/frappe/ui/capture.js
index eb444be4e9..bc7ce34149 100644
--- a/frappe/public/js/frappe/ui/capture.js
+++ b/frappe/public/js/frappe/ui/capture.js
@@ -11,19 +11,18 @@
* frappe._.get_data_uri(video)
* // returns "data:image/pngbase64,..."
*/
-frappe._.get_data_uri = element => {
-
+frappe._.get_data_uri = (element) => {
const width = element.videoWidth;
const height = element.videoHeight;
- const $canvas = $(' ');
+ const $canvas = $(" ");
$canvas[0].width = width;
$canvas[0].height = height;
- const context = $canvas[0].getContext('2d');
+ const context = $canvas[0].getContext("2d");
context.drawImage(element, 0, 0, width, height);
- const data_uri = $canvas[0].toDataURL('image/png');
+ const data_uri = $canvas[0].toDataURL("image/png");
return data_uri;
};
@@ -92,14 +91,14 @@ frappe.ui.Capture = class {
fields: [
{
fieldtype: "HTML",
- fieldname: "capture"
+ fieldname: "capture",
},
{
fieldtype: "HTML",
- fieldname: "total_count"
- }
+ fieldname: "total_count",
+ },
],
- on_hide: this.stop_media_stream()
+ on_hide: this.stop_media_stream(),
});
me.$template = $(frappe.ui.Capture.TEMPLATE);
@@ -107,7 +106,7 @@ frappe.ui.Capture = class {
let field = me.dialog.get_field("capture");
$(field.wrapper).html(me.$template);
- me.dialog.get_close_btn().on('click', () => {
+ me.dialog.get_close_btn().on("click", () => {
me.hide();
});
}
@@ -137,7 +136,7 @@ frappe.ui.Capture = class {
.then(() => {
me.dialog.show();
})
- .catch(err => {
+ .catch((err) => {
if (me.options.error) {
frappe.show_alert(frappe.ui.Capture.ERR_MESSAGE, 3);
}
@@ -150,21 +149,21 @@ frappe.ui.Capture = class {
let me = this;
let constraints = {
video: {
- facingMode: this.facing_mode
- }
+ facingMode: this.facing_mode,
+ },
};
- return navigator.mediaDevices.getUserMedia(constraints).then(stream => {
+ return navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
me.stream = stream;
me.dialog.custom_actions.empty();
- me.dialog.get_primary_btn().off('click');
+ me.dialog.get_primary_btn().off("click");
me.setup_take_photo_action();
me.setup_preview_action();
me.setup_toggle_camera();
- me.$template.find('.fc-stream-container').show();
- me.$template.find('.fc-preview-container').hide();
- me.video = me.$template.find('video')[0];
+ me.$template.find(".fc-stream-container").show();
+ me.$template.find(".fc-preview-container").hide();
+ me.video = me.$template.find("video")[0];
me.video.srcObject = me.stream;
me.video.load();
me.video.play();
@@ -173,9 +172,9 @@ frappe.ui.Capture = class {
render_preview() {
this.stop_media_stream();
- this.$template.find('.fc-stream-container').hide();
- this.$template.find('.fc-preview-container').show();
- this.dialog.get_primary_btn().off('click');
+ this.$template.find(".fc-stream-container").hide();
+ this.$template.find(".fc-preview-container").show();
+ this.dialog.get_primary_btn().off("click");
let images = ``;
@@ -190,8 +189,8 @@ frappe.ui.Capture = class {
`;
});
- this.$template.find('.fc-preview-container').empty();
- $(this.$template.find('.fc-preview-container')).html(
+ this.$template.find(".fc-preview-container").empty();
+ $(this.$template.find(".fc-preview-container")).html(
`
${images}
`
@@ -207,7 +206,7 @@ frappe.ui.Capture = class {
setup_take_photo_action() {
let me = this;
- this.dialog.set_primary_action(__('Take Photo'), () => {
+ this.dialog.set_primary_action(__("Take Photo"), () => {
const data_url = frappe._.get_data_uri(me.video);
me.images.push(data_url);
@@ -225,7 +224,7 @@ frappe.ui.Capture = class {
this.dialog.set_secondary_action_label(__("Preview"));
this.dialog.set_secondary_action(() => {
- me.dialog.get_primary_btn().off('click');
+ me.dialog.get_primary_btn().off("click");
me.render_preview();
});
}
@@ -234,7 +233,7 @@ frappe.ui.Capture = class {
let me = this;
let elements = this.$template[0].getElementsByClassName("capture-remove-btn");
- elements.forEach(el => {
+ elements.forEach((el) => {
el.onclick = () => {
let idx = parseInt(el.getAttribute("data-idx"));
@@ -262,16 +261,20 @@ frappe.ui.Capture = class {
setup_toggle_camera() {
let me = this;
- this.dialog.add_custom_action(__("Switch Camera"), () => {
- me.facing_mode = me.facing_mode == "environment" ? "user" : "environment";
+ this.dialog.add_custom_action(
+ __("Switch Camera"),
+ () => {
+ me.facing_mode = me.facing_mode == "environment" ? "user" : "environment";
- frappe.show_alert({
- message: __("Switching Camera")
- });
+ frappe.show_alert({
+ message: __("Switching Camera"),
+ });
- me.stop_media_stream();
- me.render_stream();
- }, "btn-switch");
+ me.stop_media_stream();
+ me.render_stream();
+ },
+ "btn-switch"
+ );
}
setup_capture_action() {
@@ -290,7 +293,7 @@ frappe.ui.Capture = class {
setup_submit_action() {
let me = this;
- this.dialog.set_primary_action(__('Submit'), () => {
+ this.dialog.set_primary_action(__("Submit"), () => {
me.hide();
if (me.callback) {
@@ -319,9 +322,9 @@ frappe.ui.Capture = class {
frappe.ui.Capture.OPTIONS = {
title: __("Camera"),
animate: false,
- error: false
+ error: false,
};
-frappe.ui.Capture.ERR_MESSAGE = __('Unable to load camera.');
+frappe.ui.Capture.ERR_MESSAGE = __("Unable to load camera.");
frappe.ui.Capture.TEMPLATE = `
diff --git a/frappe/public/js/frappe/ui/chart.js b/frappe/public/js/frappe/ui/chart.js
index 36e081a7f0..642598c564 100644
--- a/frappe/public/js/frappe/ui/chart.js
+++ b/frappe/public/js/frappe/ui/chart.js
@@ -17,17 +17,17 @@ frappe.ui.RealtimeChart = class RealtimeChart extends frappe.Chart {
this.socketEvent = socketEvent;
this.maxLabelPoints = maxLabelPoints;
- this.start_updating = function() {
- frappe.realtime.on(this.socketEvent, data => {
+ this.start_updating = function () {
+ frappe.realtime.on(this.socketEvent, (data) => {
this.update_chart(data.label, data.points);
});
};
- this.stop_updating = function() {
+ this.stop_updating = function () {
frappe.realtime.off(this.socketEvent);
};
- this.update_chart = function(label, data) {
+ this.update_chart = function (label, data) {
if (this.currentSize >= this.maxLabelPoints) {
this.removeDataPoint(0);
} else {
diff --git a/frappe/public/js/frappe/ui/colors.js b/frappe/public/js/frappe/ui/colors.js
index b26ca224d1..2694b013dc 100644
--- a/frappe/public/js/frappe/ui/colors.js
+++ b/frappe/public/js/frappe/ui/colors.js
@@ -4,14 +4,14 @@
frappe.provide("frappe.ui");
frappe.ui.color = {
- get: function(color_name, shade) {
- if(color_name && shade) return this.get_color_shade(color_name, shade);
- if(color_name) return this.get_color_shade(color_name, 'default');
+ get: function (color_name, shade) {
+ if (color_name && shade) return this.get_color_shade(color_name, shade);
+ if (color_name) return this.get_color_shade(color_name, "default");
return frappe.ui.color_map;
},
- get_color: function(color_name) {
+ get_color: function (color_name) {
const color_names = Object.keys(frappe.ui.color_map);
- if(color_names.includes(color_name)) {
+ if (color_names.includes(color_name)) {
return frappe.ui.color_map[color_name];
} else {
// eslint-disable-next-line
@@ -19,26 +19,36 @@ frappe.ui.color = {
}
},
get_color_map() {
- const colors = ['red', 'green', 'blue', 'dark-green', 'yellow', 'gray', 'purple', 'pink', 'orange'];
- const shades = ['100', '300', '500', '700'];
+ const colors = [
+ "red",
+ "green",
+ "blue",
+ "dark-green",
+ "yellow",
+ "gray",
+ "purple",
+ "pink",
+ "orange",
+ ];
+ const shades = ["100", "300", "500", "700"];
const style = getComputedStyle(document.body);
let color_map = {};
- colors.forEach(color => {
- color_map[color] = shades.map(shade =>
+ colors.forEach((color) => {
+ color_map[color] = shades.map((shade) =>
style.getPropertyValue(`--${color}-${shade}`).trim()
);
});
return color_map;
},
- get_color_shade: function(color_name, shade) {
+ get_color_shade: function (color_name, shade) {
const shades = {
- 'default': 2,
- 'light': 1,
- 'extra-light': 0,
- 'dark': 3
+ default: 2,
+ light: 1,
+ "extra-light": 0,
+ dark: 3,
};
- if(Object.keys(shades).includes(shade)) {
+ if (Object.keys(shades).includes(shade)) {
const color = this.get_color(color_name);
return color ? color[shades[shade]] : color_name;
} else {
@@ -46,33 +56,32 @@ frappe.ui.color = {
console.warn(`'shade' can be one of ${Object.keys(shades)} and not ${shade}`);
}
},
- all: function() {
- return Object.values(frappe.ui.color_map)
- .reduce((acc, curr) => acc.concat(curr) , []);
+ all: function () {
+ return Object.values(frappe.ui.color_map).reduce((acc, curr) => acc.concat(curr), []);
},
- names: function() {
+ names: function () {
return Object.keys(frappe.ui.color_map);
},
- is_standard: function(color_name) {
- if(!color_name) return false;
- if(color_name.startsWith('#')) {
+ is_standard: function (color_name) {
+ if (!color_name) return false;
+ if (color_name.startsWith("#")) {
return this.all().includes(color_name);
}
return this.names().includes(color_name);
},
- get_color_name: function(hex) {
+ get_color_name: function (hex) {
for (const key in frappe.ui.color_map) {
const colors = frappe.ui.color_map[key];
if (colors.includes(hex)) return key;
}
},
- get_contrast_color: function(hex) {
- if(!this.validate_hex(hex)) {
+ get_contrast_color: function (hex) {
+ if (!this.validate_hex(hex)) {
return;
}
- if(!this.is_standard(hex)) {
+ if (!this.is_standard(hex)) {
const brightness = this.brightness(hex);
- if(brightness < 128) {
+ if (brightness < 128) {
return this.lighten(hex, 0.5);
}
return this.lighten(hex, -0.5);
@@ -81,13 +90,13 @@ frappe.ui.color = {
const color_name = this.get_color_name(hex);
const colors = this.get_color(color_name);
const shade_value = colors.indexOf(hex);
- if(shade_value <= 1) {
- return this.get(color_name, 'dark');
+ if (shade_value <= 1) {
+ return this.get(color_name, "dark");
}
- return this.get(color_name, 'extra-light');
+ return this.get(color_name, "extra-light");
},
- validate_hex: function(hex) {
+ validate_hex: function (hex) {
// https://stackoverflow.com/a/8027444/5353542
return /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex);
},
@@ -98,25 +107,29 @@ frappe.ui.color = {
t = percent < 0 ? 0 : 255,
p = percent < 0 ? percent * -1 : percent,
R = f >> 16,
- G = f >> 8 & 0x00FF,
- B = f & 0x0000FF;
- return "#" +
- (0x1000000 +
- (Math.round((t - R) * p) + R) *
- 0x10000 +
- (Math.round((t - G) * p) + G) *
- 0x100 + (Math.round((t - B) * p) + B)
- ).toString(16).slice(1);
+ G = (f >> 8) & 0x00ff,
+ B = f & 0x0000ff;
+ return (
+ "#" +
+ (
+ 0x1000000 +
+ (Math.round((t - R) * p) + R) * 0x10000 +
+ (Math.round((t - G) * p) + G) * 0x100 +
+ (Math.round((t - B) * p) + B)
+ )
+ .toString(16)
+ .slice(1)
+ );
},
hex_to_rgb(hex) {
- if(hex.startsWith('#')) {
+ if (hex.startsWith("#")) {
hex = hex.substring(1);
}
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
- return {r, g, b};
+ return { r, g, b };
},
brightness(hex) {
@@ -125,7 +138,7 @@ frappe.ui.color = {
// 255 - brightest (#fff)
// 0 - darkest (#000)
return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
- }
+ },
};
frappe.ui.color_map = frappe.ui.color.get_color_map();
diff --git a/frappe/public/js/frappe/ui/datatable.js b/frappe/public/js/frappe/ui/datatable.js
index c71c285f3c..9ede568b36 100644
--- a/frappe/public/js/frappe/ui/datatable.js
+++ b/frappe/public/js/frappe/ui/datatable.js
@@ -1,3 +1,3 @@
import DataTable from "frappe-datatable";
-frappe.DataTable = DataTable;
\ No newline at end of file
+frappe.DataTable = DataTable;
diff --git a/frappe/public/js/frappe/ui/dialog.js b/frappe/public/js/frappe/ui/dialog.js
index 3e2d22ffa2..9ec6922306 100644
--- a/frappe/public/js/frappe/ui/dialog.js
+++ b/frappe/public/js/frappe/ui/dialog.js
@@ -1,8 +1,7 @@
+import "./field_group";
+import "../dom";
-import './field_group';
-import '../dom';
-
-frappe.provide('frappe.ui');
+frappe.provide("frappe.ui");
window.cur_dialog = null;
@@ -21,32 +20,30 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
make() {
this.$wrapper = frappe.get_modal("", "");
- if(this.static) {
+ if (this.static) {
this.$wrapper.modal({
- backdrop: 'static',
- keyboard: false
+ backdrop: "static",
+ keyboard: false,
});
this.get_close_btn().hide();
}
- this.wrapper = this.$wrapper.find('.modal-dialog')
- .get(0);
- if (this.size == "small" )
- $(this.wrapper).addClass("modal-sm");
- else if (this.size == "large" )
- $(this.wrapper).addClass("modal-lg");
- else if (this.size == "extra-large" )
- $(this.wrapper).addClass("modal-xl");
+ if (!this.size) this.set_modal_size();
+
+ this.wrapper = this.$wrapper.find(".modal-dialog").get(0);
+ if (this.size == "small") $(this.wrapper).addClass("modal-sm");
+ else if (this.size == "large") $(this.wrapper).addClass("modal-lg");
+ else if (this.size == "extra-large") $(this.wrapper).addClass("modal-xl");
this.make_head();
this.modal_body = this.$wrapper.find(".modal-body");
- this.$body = $('
').appendTo(this.modal_body);
+ this.$body = $("
").appendTo(this.modal_body);
this.body = this.$body.get(0);
this.$message = $('
').appendTo(this.modal_body);
this.header = this.$wrapper.find(".modal-header");
this.footer = this.$wrapper.find(".modal-footer");
- this.standard_actions = this.footer.find('.standard-actions');
- this.custom_actions = this.footer.find('.custom-actions');
+ this.standard_actions = this.footer.find(".standard-actions");
+ this.custom_actions = this.footer.find(".custom-actions");
this.set_indicator();
// make fields (if any)
@@ -55,10 +52,12 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
this.refresh_section_collapse();
// show footer
- this.action = this.action || { primary: { }, secondary: { } };
+ this.action = this.action || { primary: {}, secondary: {} };
if (this.primary_action || (this.action.primary && this.action.primary.onsubmit)) {
this.set_primary_action(
- this.primary_action_label || this.action.primary.label || __("Submit", null, "Primary action in dialog"),
+ this.primary_action_label ||
+ this.action.primary.label ||
+ __("Submit", null, "Primary action in dialog"),
this.primary_action || this.action.primary.onsubmit
);
}
@@ -67,26 +66,36 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
this.set_secondary_action(this.secondary_action);
}
- if (this.secondary_action_label || (this.action.secondary && this.action.secondary.label)) {
- this.set_secondary_action_label(this.secondary_action_label || this.action.secondary.label);
+ if (
+ this.secondary_action_label ||
+ (this.action.secondary && this.action.secondary.label)
+ ) {
+ this.set_secondary_action_label(
+ this.secondary_action_label || this.action.secondary.label
+ );
}
if (this.minimizable) {
- this.header.find('.title-section').click(() => this.is_minimized && this.toggle_minimize());
- this.get_minimize_btn().removeClass('hide').on('click', () => this.toggle_minimize());
+ this.header
+ .find(".title-section")
+ .click(() => this.is_minimized && this.toggle_minimize());
+ this.get_minimize_btn()
+ .removeClass("hide")
+ .on("click", () => this.toggle_minimize());
}
var me = this;
this.$wrapper
- .on("hide.bs.modal", function() {
+ .on("hide.bs.modal", function () {
me.display = false;
me.is_minimized = false;
me.hide_scrollbar(false);
- if(frappe.ui.open_dialogs[frappe.ui.open_dialogs.length-1]===me) {
+ if (frappe.ui.open_dialogs[frappe.ui.open_dialogs.length - 1] === me) {
frappe.ui.open_dialogs.pop();
- if(frappe.ui.open_dialogs.length) {
- window.cur_dialog = frappe.ui.open_dialogs[frappe.ui.open_dialogs.length-1];
+ if (frappe.ui.open_dialogs.length) {
+ window.cur_dialog =
+ frappe.ui.open_dialogs[frappe.ui.open_dialogs.length - 1];
} else {
window.cur_dialog = null;
}
@@ -94,7 +103,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
me.onhide && me.onhide();
me.on_hide && me.on_hide();
})
- .on("shown.bs.modal", function() {
+ .on("shown.bs.modal", function () {
// focus on first input
me.display = true;
window.cur_dialog = me;
@@ -102,16 +111,43 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
me.focus_on_first_input();
me.hide_scrollbar(true);
me.on_page_show && me.on_page_show();
- $(document).trigger('frappe.ui.Dialog:shown');
- $(document).off('focusin.modal');
+ $(document).trigger("frappe.ui.Dialog:shown");
+ $(document).off("focusin.modal");
})
- .on('scroll', function() {
- var $input = $('input:focus');
- if ($input.length && ['Date', 'Datetime', 'Time'].includes($input.attr('data-fieldtype'))) {
+ .on("scroll", function () {
+ var $input = $("input:focus");
+ if (
+ $input.length &&
+ ["Date", "Datetime", "Time"].includes($input.attr("data-fieldtype"))
+ ) {
$input.blur();
}
});
+ }
+ set_modal_size() {
+ if (!this.fields) {
+ this.size = "";
+ return;
+ }
+
+ let col_brk = 0;
+ let cur_col_brk = 0;
+
+ // if fields have more than 2 Column Breaks before encountering Section Break, make it large
+ this.fields.forEach((field) => {
+ if (field.fieldtype == "Column Break") {
+ cur_col_brk++;
+
+ if (cur_col_brk > col_brk) {
+ col_brk = cur_col_brk;
+ }
+ } else if (field.fieldtype == "Section Break") {
+ cur_col_brk = 0;
+ }
+ });
+
+ this.size = col_brk >= 4 ? "extra-large" : col_brk >= 2 ? "large" : "";
}
get_primary_btn() {
@@ -123,14 +159,14 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
}
set_message(text) {
- this.$message.removeClass('hide');
- this.$body.addClass('hide');
+ this.$message.removeClass("hide");
+ this.$body.addClass("hide");
this.$message.text(text);
}
clear_message() {
- this.$message.addClass('hide');
- this.$body.removeClass('hide');
+ this.$message.addClass("hide");
+ this.$body.removeClass("hide");
}
clear() {
@@ -139,41 +175,39 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
}
set_primary_action(label, click) {
- this.footer.removeClass('hide');
+ this.footer.removeClass("hide");
this.has_primary_action = true;
var me = this;
return this.get_primary_btn()
.removeClass("hide")
.html(label)
- .off('click')
- .on('click', function() {
+ .off("click")
+ .on("click", function () {
me.primary_action_fulfilled = true;
// get values and send it
// as first parameter to click callback
// if no values then return
var values = me.get_values();
- if(!values) return;
+ if (!values) return;
click && click.apply(me, [values]);
});
}
set_secondary_action(click) {
- this.footer.removeClass('hide');
- this.get_secondary_btn().removeClass('hide').off('click').on('click', click);
+ this.footer.removeClass("hide");
+ return this.get_secondary_btn().removeClass("hide").off("click").on("click", click);
}
set_secondary_action_label(label) {
- this.get_secondary_btn()
- .removeClass("hide")
- .html(label);
+ this.get_secondary_btn().removeClass("hide").html(label);
}
disable_primary_action() {
- this.get_primary_btn().addClass('disabled');
+ this.get_primary_btn().addClass("disabled");
}
enable_primary_action() {
- this.get_primary_btn().removeClass('disabled');
+ this.get_primary_btn().removeClass("disabled");
}
make_head() {
@@ -186,20 +220,23 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
set_indicator() {
if (this.indicator) {
- this.header.find('.indicator').removeClass().addClass('indicator ' + this.indicator);
+ this.header
+ .find(".indicator")
+ .removeClass()
+ .addClass("indicator " + this.indicator);
}
}
show() {
// show it
if (this.animate) {
- this.$wrapper.addClass('fade');
+ this.$wrapper.addClass("fade");
} else {
- this.$wrapper.removeClass('fade');
+ this.$wrapper.removeClass("fade");
}
this.$wrapper.modal("show");
- this.$wrapper.removeClass('modal-minimize');
+ this.$wrapper.removeClass("modal-minimize");
if (this.minimizable && this.is_minimized) {
$(".modal-backdrop").toggle();
@@ -236,25 +273,25 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
}
toggle_minimize() {
- $('.modal-backdrop').toggle();
- let modal = this.$wrapper.closest('.modal').toggleClass('modal-minimize');
- modal.attr('tabindex') ? modal.removeAttr('tabindex') : modal.attr('tabindex', -1);
+ $(".modal-backdrop").toggle();
+ let modal = this.$wrapper.closest(".modal").toggleClass("modal-minimize");
+ modal.attr("tabindex") ? modal.removeAttr("tabindex") : modal.attr("tabindex", -1);
this.is_minimized = !this.is_minimized;
- const icon = this.is_minimized ? 'expand' : 'collapse';
+ const icon = this.is_minimized ? "expand" : "collapse";
this.get_minimize_btn().html(frappe.utils.icon(icon));
this.on_minimize_toggle && this.on_minimize_toggle(this.is_minimized);
- this.header.find('.modal-title').toggleClass('cursor-pointer');
+ this.header.find(".modal-title").toggleClass("cursor-pointer");
this.hide_scrollbar(!this.is_minimized);
}
hide_scrollbar(bool) {
- $("body").css("overflow", bool ? "hidden" : "auto");
+ $("body").css("overflow", bool ? "hidden" : "auto");
}
- add_custom_action(label, action, css_class=null) {
- this.footer.removeClass('hide');
+ add_custom_action(label, action, css_class = null) {
+ this.footer.removeClass("hide");
let action_button = $(`
-
+
${label}
`);
diff --git a/frappe/public/js/frappe/ui/driver.js b/frappe/public/js/frappe/ui/driver.js
index 98ed49ec05..f3a848282b 100644
--- a/frappe/public/js/frappe/ui/driver.js
+++ b/frappe/public/js/frappe/ui/driver.js
@@ -1,3 +1,3 @@
-import Driver from 'driver.js';
+import Driver from "driver.js";
-frappe.Driver = Driver;
\ No newline at end of file
+frappe.Driver = Driver;
diff --git a/frappe/public/js/frappe/ui/field_group.js b/frappe/public/js/frappe/ui/field_group.js
index c226c4cbfb..20bb7aadbd 100644
--- a/frappe/public/js/frappe/ui/field_group.js
+++ b/frappe/public/js/frappe/ui/field_group.js
@@ -1,32 +1,32 @@
-import '../form/layout';
+import "../form/layout";
-frappe.provide('frappe.ui');
+frappe.provide("frappe.ui");
frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
constructor(opts) {
super(opts);
this.dirty = false;
- $.each(this.fields || [], function(i, f) {
- if(!f.fieldname && f.label) {
+ $.each(this.fields || [], function (i, f) {
+ if (!f.fieldname && f.label) {
f.fieldname = f.label.replace(/ /g, "_").toLowerCase();
}
- })
- if(this.values) {
+ });
+ if (this.values) {
this.set_values(this.values);
}
}
make() {
var me = this;
- if(this.fields) {
+ if (this.fields) {
super.make();
this.refresh();
// set default
- $.each(this.fields_list, function(i, field) {
+ $.each(this.fields_list, function (i, field) {
if (field.df["default"]) {
let def_value = field.df["default"];
- if (def_value == 'Today' && field.df["fieldtype"] == 'Date') {
+ if (def_value == "Today" && field.df["fieldtype"] == "Date") {
def_value = frappe.datetime.get_today();
}
@@ -34,30 +34,28 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
// if default and has depends_on, render its fields.
me.refresh_dependency();
}
- })
+ });
- if(!this.no_submit_on_enter) {
+ if (!this.no_submit_on_enter) {
this.catch_enter_as_submit();
}
- $(this.wrapper).find('input, select').on(
- 'change awesomplete-selectcomplete',
- () => {
+ $(this.wrapper)
+ .find("input, select")
+ .on("change awesomplete-selectcomplete", () => {
this.dirty = true;
frappe.run_serially([
() => frappe.timeout(0.1),
- () => me.refresh_dependency()
+ () => me.refresh_dependency(),
]);
- }
- );
-
+ });
}
}
focus_on_first_input() {
- if(this.no_focus) return;
- $.each(this.fields_list, function(i, f) {
- if(!in_list(['Date', 'Datetime', 'Time', 'Check'], f.df.fieldtype) && f.set_focus) {
+ if (this.no_focus) return;
+ $.each(this.fields_list, function (i, f) {
+ if (!in_list(["Date", "Datetime", "Time", "Check"], f.df.fieldtype) && f.set_focus) {
f.set_focus();
return false;
}
@@ -66,14 +64,16 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
catch_enter_as_submit() {
var me = this;
- $(this.body).find('input[type="text"], input[type="password"], select').keypress(function(e) {
- if(e.which==13) {
- if(me.has_primary_action) {
- e.preventDefault();
- me.get_primary_btn().trigger("click");
+ $(this.body)
+ .find('input[type="text"], input[type="password"], select')
+ .keypress(function (e) {
+ if (e.which == 13) {
+ if (me.has_primary_action) {
+ e.preventDefault();
+ me.get_primary_btn().trigger("click");
+ }
}
- }
- });
+ });
}
get_input(fieldname) {
@@ -85,22 +85,19 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
return this.fields_dict[fieldname];
}
- get_values(ignore_errors) {
+ get_values(ignore_errors, check_invalid) {
var ret = {};
var errors = [];
+ let invalid = [];
+
for (var key in this.fields_dict) {
var f = this.fields_dict[key];
if (f.get_value) {
var v = f.get_value();
- if (
- f.df.reqd &&
- is_null(typeof v === 'string' ? strip_html(v) : v)
- )
+ if (f.df.reqd && is_null(typeof v === "string" ? strip_html(v) : v))
errors.push(__(f.df.label));
- if (f.df.reqd
- && f.df.fieldtype === 'Text Editor'
- && is_null(strip_html(cstr(v))))
+ if (f.df.reqd && f.df.fieldtype === "Text Editor" && is_null(strip_html(cstr(v))))
errors.push(__(f.df.label));
if (!is_null(v)) ret[f.df.fieldname] = v;
@@ -109,13 +106,34 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
if (this.is_dialog && f.df.reqd && !f.value) {
f.refresh_input();
}
+
+ if (f.df.invalid) {
+ invalid.push(__(f.df.label));
+ }
}
+
if (errors.length && !ignore_errors) {
frappe.msgprint({
- title: __('Missing Values Required'),
- message: __('Following fields have missing values:') +
- ' ',
- indicator: 'orange'
+ title: __("Missing Values Required"),
+ message:
+ __("Following fields have missing values:") +
+ "" +
+ errors.join(" ") +
+ " ",
+ indicator: "orange",
+ });
+ return null;
+ }
+
+ if (invalid.length && check_invalid) {
+ frappe.msgprint({
+ title: __("Inavlid Values"),
+ message:
+ __("Following fields have invalid values:") +
+ "" +
+ invalid.join(" ") +
+ " ",
+ indicator: "orange",
});
return null;
}
@@ -128,7 +146,7 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
}
set_value(key, val) {
- return new Promise(resolve => {
+ return new Promise((resolve) => {
var f = this.fields_dict[key];
if (f) {
f.set_value(val).then(() => {
@@ -152,8 +170,8 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
set_values(dict) {
let promises = [];
- for(var key in dict) {
- if(this.fields_dict[key]) {
+ for (var key in dict) {
+ if (this.fields_dict[key]) {
promises.push(this.set_value(key, dict[key]));
}
}
@@ -162,15 +180,18 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
}
clear() {
- for(var key in this.fields_dict) {
+ for (var key in this.fields_dict) {
var f = this.fields_dict[key];
- if(f && f.set_input) {
- f.set_input(f.df['default'] || '');
+ if (f && f.set_input) {
+ f.set_input(f.df["default"] || "");
}
}
}
- set_df_property (fieldname, prop, value) {
+ set_df_property(fieldname, prop, value) {
+ if (!fieldname) {
+ return;
+ }
const field = this.get_field(fieldname);
field.df[prop] = value;
field.refresh();
diff --git a/frappe/public/js/frappe/ui/filters/field_select.js b/frappe/public/js/frappe/ui/filters/field_select.js
index 8f6d3ab89d..3e67dee232 100644
--- a/frappe/public/js/frappe/ui/filters/field_select.js
+++ b/frappe/public/js/frappe/ui/filters/field_select.js
@@ -8,7 +8,9 @@ frappe.ui.FieldSelect = class FieldSelect {
this.options = [];
this.$input = $(' ')
.appendTo(this.parent)
- .on("click", function () { $(this).select(); });
+ .on("click", function () {
+ $(this).select();
+ });
this.input_class && this.$input.addClass(this.input_class);
this.select_input = this.$input.get(0);
this.awesomplete = new Awesomplete(this.select_input, {
@@ -20,36 +22,37 @@ frappe.ui.FieldSelect = class FieldSelect {
return $(repl('%(label)s
', item))
.data("item.autocomplete", item)
.get(0);
- }
+ },
});
- this.$input.on("awesomplete-select", function(e) {
+ this.$input.on("awesomplete-select", function (e) {
var o = e.originalEvent;
var value = o.text.value;
var item = me.awesomplete.get_item(value);
me.selected_doctype = item.doctype;
me.selected_fieldname = item.fieldname;
- if(me.select) me.select(item.doctype, item.fieldname);
+ if (me.select) me.select(item.doctype, item.fieldname);
});
- this.$input.on("awesomplete-selectcomplete", function(e) {
+ this.$input.on("awesomplete-selectcomplete", function (e) {
var o = e.originalEvent;
var value = o.text.value;
var item = me.awesomplete.get_item(value);
me.$input.val(item.label);
});
- if(this.filter_fields) {
- for(var i in this.filter_fields)
- this.add_field_option(this.filter_fields[i]);
+ if (this.filter_fields) {
+ for (var i in this.filter_fields) this.add_field_option(this.filter_fields[i]);
} else {
this.build_options();
}
this.set_value(this.doctype, "name");
}
get_value() {
- return this.selected_doctype ? this.selected_doctype + "." + this.selected_fieldname : null;
+ return this.selected_doctype
+ ? this.selected_doctype + "." + this.selected_fieldname
+ : null;
}
val(value) {
- if(value===undefined) {
+ if (value === undefined) {
return this.get_value();
} else {
this.set_value(value);
@@ -63,17 +66,17 @@ frappe.ui.FieldSelect = class FieldSelect {
set_value(doctype, fieldname) {
var me = this;
this.clear();
- if(!doctype) return;
+ if (!doctype) return;
// old style
- if(doctype.indexOf(".")!==-1) {
+ if (doctype.indexOf(".") !== -1) {
var parts = doctype.split(".");
doctype = parts[0];
fieldname = parts[1];
}
- $.each(this.options, function(i, v) {
- if(v.doctype===doctype && v.fieldname===fieldname) {
+ $.each(this.options, function (i, v) {
+ if (v.doctype === doctype && v.fieldname === fieldname) {
me.selected_doctype = doctype;
me.selected_fieldname = fieldname;
me.$input.val(v.label);
@@ -84,56 +87,62 @@ frappe.ui.FieldSelect = class FieldSelect {
build_options() {
var me = this;
me.table_fields = [];
- var std_filters = $.map(frappe.model.std_fields, function(d) {
- var opts = {parent: me.doctype};
- if(d.fieldname=="name") opts.options = me.doctype;
+ var std_filters = $.map(frappe.model.std_fields, function (d) {
+ var opts = { parent: me.doctype };
+ if (d.fieldname == "name") opts.options = me.doctype;
return $.extend(copy_dict(d), opts);
});
// add parenttype column
- var doctype_obj = locals['DocType'][me.doctype];
- if(doctype_obj && cint(doctype_obj.istable)) {
- std_filters = std_filters.concat([{
- fieldname: 'parent',
- fieldtype: 'Data',
- label: 'Parent',
- parent: me.doctype,
- }]);
+ var doctype_obj = locals["DocType"][me.doctype];
+ if (doctype_obj && cint(doctype_obj.istable)) {
+ std_filters = std_filters.concat([
+ {
+ fieldname: "parent",
+ fieldtype: "Data",
+ label: "Parent",
+ parent: me.doctype,
+ },
+ ]);
}
// blank
- if(this.with_blank) {
+ if (this.with_blank) {
this.options.push({
- label:"",
- value:"",
+ label: "",
+ value: "",
});
}
// main table
var main_table_fields = std_filters.concat(frappe.meta.docfield_list[me.doctype]);
- $.each(frappe.utils.sort(main_table_fields, "label", "string"), function(i, df) {
- let doctype = frappe.get_meta(me.doctype).istable && me.parent_doctype ?
- me.parent_doctype : me.doctype;
+ $.each(frappe.utils.sort(main_table_fields, "label", "string"), function (i, df) {
+ let doctype =
+ frappe.get_meta(me.doctype).istable && me.parent_doctype
+ ? me.parent_doctype
+ : me.doctype;
// show fields where user has read access and if report hide flag is not set
- if (frappe.perm.has_perm(doctype, df.permlevel, "read"))
- me.add_field_option(df);
+ if (frappe.perm.has_perm(doctype, df.permlevel, "read")) me.add_field_option(df);
});
// child tables
- $.each(me.table_fields, function(i, table_df) {
- if(table_df.options) {
+ $.each(me.table_fields, function (i, table_df) {
+ if (table_df.options) {
let child_table_fields = [].concat(frappe.meta.docfield_list[table_df.options]);
if (table_df.fieldtype === "Table MultiSelect") {
- const link_field = frappe.meta.get_docfields(table_df.options)
- .find(df => df.fieldtype === 'Link');
+ const link_field = frappe.meta
+ .get_docfields(table_df.options)
+ .find((df) => df.fieldtype === "Link");
child_table_fields = link_field ? [link_field] : [];
}
- $.each(frappe.utils.sort(child_table_fields, "label", "string"), function(i, df) {
- let doctype = frappe.get_meta(me.doctype).istable && me.parent_doctype ?
- me.parent_doctype : me.doctype;
+ $.each(frappe.utils.sort(child_table_fields, "label", "string"), function (i, df) {
+ let doctype =
+ frappe.get_meta(me.doctype).istable && me.parent_doctype
+ ? me.parent_doctype
+ : me.doctype;
// show fields where user has read access and if report hide flag is not set
if (frappe.perm.has_perm(doctype, df.permlevel, "read"))
@@ -146,8 +155,7 @@ frappe.ui.FieldSelect = class FieldSelect {
add_field_option(df) {
let me = this;
- if (df.fieldname == 'docstatus' && !frappe.model.is_submittable(me.doctype))
- return;
+ if (df.fieldname == "docstatus" && !frappe.model.is_submittable(me.doctype)) return;
if (frappe.model.table_fields.includes(df.fieldtype)) {
me.table_fields.push(df);
@@ -157,23 +165,25 @@ frappe.ui.FieldSelect = class FieldSelect {
let label = null;
let table = null;
- if(me.doctype && df.parent==me.doctype) {
+ if (me.doctype && df.parent == me.doctype) {
label = __(df.label);
table = me.doctype;
} else {
- label = __(df.label) + ' (' + __(df.parent) + ')';
+ label = __(df.label) + " (" + __(df.parent) + ")";
table = df.parent;
}
- if(frappe.model.no_value_type.indexOf(df.fieldtype) == -1 &&
- !(me.fields_by_name[df.parent] && me.fields_by_name[df.parent][df.fieldname])) {
+ if (
+ frappe.model.no_value_type.indexOf(df.fieldtype) == -1 &&
+ !(me.fields_by_name[df.parent] && me.fields_by_name[df.parent][df.fieldname])
+ ) {
this.options.push({
label: label,
value: table + "." + df.fieldname,
fieldname: df.fieldname,
- doctype: df.parent
+ doctype: df.parent,
});
- if(!me.fields_by_name[df.parent]) me.fields_by_name[df.parent] = {};
+ if (!me.fields_by_name[df.parent]) me.fields_by_name[df.parent] = {};
me.fields_by_name[df.parent][df.fieldname] = df;
}
}
diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js
index c19d696008..5902c136bd 100644
--- a/frappe/public/js/frappe/ui/filters/filter.js
+++ b/frappe/public/js/frappe/ui/filters/filter.js
@@ -2,7 +2,7 @@ frappe.ui.Filter = class {
constructor(opts) {
$.extend(this, opts);
if (this.value === null || this.value === undefined) {
- this.value = '';
+ this.value = "";
}
this.utils = frappe.ui.filter_utils;
@@ -13,39 +13,44 @@ frappe.ui.Filter = class {
set_conditions() {
this.conditions = [
- ['=', __('Equals')],
- ['!=', __('Not Equals')],
- ['like', __('Like')],
- ['not like', __('Not Like')],
- ['in', __('In')],
- ['not in', __('Not In')],
- ['is', __('Is')],
- ['>', '>'],
- ['<', '<'],
- ['>=', '>='],
- ['<=', '<='],
- ['Between', __('Between')],
- ['Timespan', __('Timespan')],
+ ["=", __("Equals")],
+ ["!=", __("Not Equals")],
+ ["like", __("Like")],
+ ["not like", __("Not Like")],
+ ["in", __("In")],
+ ["not in", __("Not In")],
+ ["is", __("Is")],
+ [">", ">"],
+ ["<", "<"],
+ [">=", ">="],
+ ["<=", "<="],
+ ["Between", __("Between")],
+ ["Timespan", __("Timespan")],
];
this.nested_set_conditions = [
- ['descendants of', __('Descendants Of')],
- ['not descendants of', __('Not Descendants Of')],
- ['ancestors of', __('Ancestors Of')],
- ['not ancestors of', __('Not Ancestors Of')],
+ ["descendants of", __("Descendants Of")],
+ ["not descendants of", __("Not Descendants Of")],
+ ["ancestors of", __("Ancestors Of")],
+ ["not ancestors of", __("Not Ancestors Of")],
];
this.conditions.push(...this.nested_set_conditions);
this.invalid_condition_map = {
- Date: ['like', 'not like'],
- Datetime: ['like', 'not like'],
- Data: ['Between', 'Timespan'],
- Select: ['like', 'not like', 'Between', 'Timespan'],
- Link: ['Between', 'Timespan', '>', '<', '>=', '<='],
- Currency: ['Between', 'Timespan'],
- Color: ['Between', 'Timespan'],
- Check: this.conditions.map((c) => c[0]).filter((c) => c !== '='),
+ Date: ["like", "not like"],
+ Datetime: ["like", "not like", "in", "not in", "=", "!="],
+ Data: ["Between", "Timespan"],
+ Select: ["like", "not like", "Between", "Timespan"],
+ Link: ["Between", "Timespan", ">", "<", ">=", "<="],
+ Currency: ["Between", "Timespan"],
+ Color: ["Between", "Timespan"],
+ Check: this.conditions.map((c) => c[0]).filter((c) => c !== "="),
+ Code: ["Between", "Timespan", ">", "<", ">=", "<=", "in", "not in"],
+ "HTML Editor": ["Between", "Timespan", ">", "<", ">=", "<=", "in", "not in"],
+ "Markdown Editor": ["Between", "Timespan", ">", "<", ">=", "<=", "in", "not in"],
+ Password: ["Between", "Timespan", ">", "<", ">=", "<=", "in", "not in"],
+ Rating: ["like", "not like", "Between", "in", "not in", "Timespan"],
};
}
@@ -66,11 +71,11 @@ frappe.ui.Filter = class {
make() {
this.filter_edit_area = $(
- frappe.render_template('edit_filter', {
+ frappe.render_template("edit_filter", {
conditions: this.conditions,
})
);
- this.parent && this.filter_edit_area.appendTo(this.parent.find('.filter-edit-area'));
+ this.parent && this.filter_edit_area.appendTo(this.parent.find(".filter-edit-area"));
this.make_select();
this.set_events();
this.setup();
@@ -78,11 +83,11 @@ frappe.ui.Filter = class {
make_select() {
this.fieldselect = new frappe.ui.FieldSelect({
- parent: this.filter_edit_area.find('.fieldname-select-area'),
+ parent: this.filter_edit_area.find(".fieldname-select-area"),
doctype: this.parent_doctype,
parent_doctype: this._parent_doctype,
filter_fields: this.filter_fields,
- input_class: 'input-xs',
+ input_class: "input-xs",
select: (doctype, fieldname) => {
this.set_field(doctype, fieldname);
},
@@ -94,42 +99,37 @@ frappe.ui.Filter = class {
}
set_events() {
- this.filter_edit_area.find('.remove-filter').on('click', () => {
+ this.filter_edit_area.find(".remove-filter").on("click", () => {
this.remove();
this.on_change();
});
- this.filter_edit_area.find('.condition').change(() => {
+ this.filter_edit_area.find(".condition").change(() => {
if (!this.field) return;
let condition = this.get_condition();
let fieldtype = null;
- if (['in', 'like', 'not in', 'not like'].includes(condition)) {
- fieldtype = 'Data';
+ if (["in", "like", "not in", "not like"].includes(condition)) {
+ fieldtype = "Data";
this.add_condition_help(condition);
} else {
- this.filter_edit_area.find('.filter-description').empty();
+ this.filter_edit_area.find(".filter-description").empty();
}
if (
- ['Select', 'MultiSelect'].includes(this.field.df.fieldtype) &&
- ['in', 'not in'].includes(condition)
+ ["Select", "MultiSelect"].includes(this.field.df.fieldtype) &&
+ ["in", "not in"].includes(condition)
) {
- fieldtype = 'MultiSelect';
+ fieldtype = "MultiSelect";
}
- this.set_field(
- this.field.df.parent,
- this.field.df.fieldname,
- fieldtype,
- condition
- );
+ this.set_field(this.field.df.parent, this.field.df.fieldname, fieldtype, condition);
});
}
setup() {
- const fieldname = this.fieldname || 'name';
+ const fieldname = this.fieldname || "name";
// set the field
return this.set_values(this.doctype, fieldname, this.condition, this.value);
}
@@ -137,7 +137,7 @@ frappe.ui.Filter = class {
setup_state(is_new) {
let promise = Promise.resolve();
if (is_new) {
- this.filter_edit_area.addClass('new-filter');
+ this.filter_edit_area.addClass("new-filter");
} else {
promise = this.update_filter_tag();
}
@@ -174,22 +174,22 @@ frappe.ui.Filter = class {
return;
}
- if (this.field.df.original_type === 'Check') {
- value = value == 1 ? 'Yes' : 'No';
+ if (this.field.df.original_type === "Check") {
+ value = value == 1 ? "Yes" : "No";
}
if (condition) this.set_condition(condition, true);
// set value can be asynchronous, so update_filter_tag should happen after field is set
this._filter_value_set = Promise.resolve();
- if (['in', 'not in'].includes(condition) && Array.isArray(value)) {
- value = value.join(',');
+ if (["in", "not in"].includes(condition) && Array.isArray(value)) {
+ value = value.join(",");
}
if (Array.isArray(value)) {
this._filter_value_set = this.field.set_value(value);
} else if (value !== undefined || value !== null) {
- this._filter_value_set = this.field.set_value((value + '').trim());
+ this._filter_value_set = this.field.set_value((value + "").trim());
}
return this._filter_value_set;
}
@@ -199,9 +199,7 @@ frappe.ui.Filter = class {
let cur = {};
if (this.field) for (let k in this.field.df) cur[k] = this.field.df[k];
- let original_docfield = (this.fieldselect.fields_by_name[doctype] || {})[
- fieldname
- ];
+ let original_docfield = (this.fieldselect.fields_by_name[doctype] || {})[fieldname];
if (!original_docfield) {
console.warn(`Field ${fieldname} is not selectable.`);
@@ -254,12 +252,12 @@ frappe.ui.Filter = class {
df.options = field.options;
df.fieldname = fieldname;
this.make_field(df, cur.fieldtype);
- }
+ };
if (this.filters_config[condition].data) {
let field = this.filters_config[condition].data;
setup_field(field);
} else {
- frappe.xcall(this.filters_config[condition].get_field, args).then(field => {
+ frappe.xcall(this.filters_config[condition].get_field, args).then((field) => {
this.filters_config[condition].data = field;
setup_field(field);
});
@@ -273,11 +271,8 @@ frappe.ui.Filter = class {
let old_text = this.field ? this.field.get_value() : null;
this.hide_invalid_conditions(df.fieldtype, df.original_type);
this.toggle_nested_set_conditions(df);
- let field_area = this.filter_edit_area
- .find('.filter-field')
- .empty()
- .get(0);
- df.input_class = 'input-xs';
+ let field_area = this.filter_edit_area.find(".filter-field").empty().get(0);
+ df.input_class = "input-xs";
let f = frappe.ui.form.make_control({
df: df,
parent: field_area,
@@ -295,13 +290,13 @@ frappe.ui.Filter = class {
bind_filter_field_events() {
// Apply filter on input focus out
- this.field.$input.on('focusout', () => this.on_change());
+ this.field.$input.on("focusout", () => this.on_change());
// run on enter
$(this.field.wrapper)
- .find(':input')
- .keydown(e => {
- if (e.which == 13 && this.field.df.fieldtype !== 'MultiSelect') {
+ .find(":input")
+ .keydown((e) => {
+ if (e.which == 13 && this.field.df.fieldtype !== "MultiSelect") {
this.on_change();
}
});
@@ -326,47 +321,44 @@ frappe.ui.Filter = class {
}
get_condition() {
- return this.filter_edit_area.find('.condition').val();
+ return this.filter_edit_area.find(".condition").val();
}
set_condition(condition, trigger_change = false) {
- let $condition_field = this.filter_edit_area.find('.condition');
+ let $condition_field = this.filter_edit_area.find(".condition");
$condition_field.val(condition);
if (trigger_change) $condition_field.change();
}
add_condition_help(condition) {
- const description = ['in', 'not in'].includes(condition)
- ? __('values separated by commas')
- : __('use % as wildcard');
+ const description = ["in", "not in"].includes(condition)
+ ? __("values separated by commas")
+ : __("use % as wildcard");
- this.filter_edit_area.find('.filter-description').html(description);
+ this.filter_edit_area.find(".filter-description").html(description);
}
make_tag() {
if (!this.field) return;
this.$filter_tag = this.get_filter_tag_element().insertAfter(
- this.parent.find('.active-tag-filters .clear-filters')
+ this.parent.find(".active-tag-filters .clear-filters")
);
this.set_filter_button_text();
this.bind_tag();
}
bind_tag() {
- this.$filter_tag.find('.remove-filter').on('click', this.remove.bind(this));
+ this.$filter_tag.find(".remove-filter").on("click", this.remove.bind(this));
- let filter_button = this.$filter_tag.find('.toggle-filter');
- filter_button.on('click', () => {
- filter_button
- .closest('.tag-filters-area')
- .find('.filter-edit-area')
- .show();
+ let filter_button = this.$filter_tag.find(".toggle-filter");
+ filter_button.on("click", () => {
+ filter_button.closest(".tag-filters-area").find(".filter-edit-area").show();
this.filter_edit_area.toggle();
});
}
set_filter_button_text() {
- this.$filter_tag.find('.toggle-filter').html(this.get_filter_button_text());
+ this.$filter_tag.find(".toggle-filter").html(this.get_filter_button_text());
}
get_filter_button_text() {
@@ -374,19 +366,17 @@ frappe.ui.Filter = class {
this.field,
this.get_selected_label() || this.get_selected_value()
);
- return `${__(this.field.df.label)} ${__(this.get_condition())} ${__(
- value
- )}`;
+ return `${__(this.field.df.label)} ${__(this.get_condition())} ${__(value)}`;
}
get_filter_tag_element() {
return $(`
+ title="${__("Edit Filter")}">
- ${frappe.utils.icon('close')}
+ title="${__("Remove Filter")}">
+ ${frappe.utils.icon("close")}
`);
}
@@ -406,8 +396,7 @@ frappe.ui.Filter = class {
toggle_nested_set_conditions(df) {
let show_condition =
- df.fieldtype === 'Link' &&
- frappe.boot.nested_set_doctypes.includes(df.options);
+ df.fieldtype === "Link" && frappe.boot.nested_set_doctypes.includes(df.options);
this.nested_set_conditions.forEach((condition) => {
this.filter_edit_area
.find(`.condition option[value="${condition[0]}"]`)
@@ -418,10 +407,10 @@ frappe.ui.Filter = class {
frappe.ui.filter_utils = {
get_formatted_value(field, value) {
- if (field.df.fieldname === 'docstatus') {
- value = { 0: 'Draft', 1: 'Submitted', 2: 'Cancelled' }[value] || value;
- } else if (field.df.original_type === 'Check') {
- value = { 0: 'No', 1: 'Yes' }[cint(value)];
+ if (field.df.fieldname === "docstatus") {
+ value = { 0: "Draft", 1: "Submitted", 2: "Cancelled" }[value] || value;
+ } else if (field.df.original_type === "Check") {
+ value = { 0: "No", 1: "Yes" }[cint(value)];
}
return frappe.format(value, field.df, { only_value: 1 });
},
@@ -431,32 +420,32 @@ frappe.ui.filter_utils = {
let val = field.get_value();
- if (typeof val === 'string') {
+ if (typeof val === "string") {
val = strip(val);
}
- if (condition == 'is' && !val) {
+ if (condition == "is" && !val) {
val = field.df.options[0].value;
}
- if (field.df.original_type == 'Check') {
- val = val == 'Yes' ? 1 : 0;
+ if (field.df.original_type == "Check") {
+ val = val == "Yes" ? 1 : 0;
}
- if (['like', 'not like'].includes(condition)) {
+ if (["like", "not like"].includes(condition)) {
// automatically append wildcards
- if (val && !(val.startsWith('%') || val.endsWith('%'))) {
- val = '%' + val + '%';
+ if (val && !(val.startsWith("%") || val.endsWith("%"))) {
+ val = "%" + val + "%";
}
- } else if (['in', 'not in'].includes(condition)) {
+ } else if (["in", "not in"].includes(condition)) {
if (val) {
- val = val.split(',').map((v) => strip(v));
+ val = val.split(",").map((v) => strip(v));
}
} else if (frappe.boot.additional_filters_config[condition]) {
val = field.value || val;
}
- if (val === '%') {
- val = '';
+ if (val === "%") {
+ val = "";
}
return val;
@@ -469,12 +458,12 @@ frappe.ui.filter_utils = {
},
get_default_condition(df) {
- if (df.fieldtype == 'Data') {
- return 'like';
- } else if (df.fieldtype == 'Date' || df.fieldtype == 'Datetime') {
- return 'Between';
+ if (df.fieldtype == "Data") {
+ return "like";
+ } else if (df.fieldtype == "Date" || df.fieldtype == "Datetime") {
+ return "Between";
} else {
- return '=';
+ return "=";
}
},
@@ -483,7 +472,7 @@ frappe.ui.filter_utils = {
if (df.original_type) df.fieldtype = df.original_type;
else df.original_type = df.fieldtype;
- df.description = '';
+ df.description = "";
df.reqd = 0;
df.ignore_link_validation = true;
@@ -494,70 +483,79 @@ frappe.ui.filter_utils = {
}
// scrub
- if (df.fieldname == 'docstatus') {
- df.fieldtype = 'Select';
+ if (df.fieldname == "docstatus") {
+ df.fieldtype = "Select";
df.options = [
- { value: 0, label: __('Draft') },
- { value: 1, label: __('Submitted') },
- { value: 2, label: __('Cancelled') },
+ { value: 0, label: __("Draft") },
+ { value: 1, label: __("Submitted") },
+ { value: 2, label: __("Cancelled") },
+ ];
+ } else if (df.fieldtype == "Check") {
+ df.fieldtype = "Select";
+ df.options = [
+ { label: __("Yes", null, "Checkbox is checked"), value: "Yes" },
+ { label: __("No", null, "Checkbox is not checked"), value: "No" },
];
- } else if (df.fieldtype == 'Check') {
- df.fieldtype = 'Select';
- df.options = 'No\nYes';
} else if (
[
- 'Text',
- 'Small Text',
- 'Text Editor',
- 'Code',
- 'Markdown Editor',
- 'HTML Editor',
- 'Tag',
- 'Comments',
- 'Dynamic Link',
- 'Read Only',
- 'Assign',
- 'Color',
+ "Text",
+ "Small Text",
+ "Text Editor",
+ "Code",
+ "Attach",
+ "Attach Image",
+ "Markdown Editor",
+ "HTML Editor",
+ "Tag",
+ "Phone",
+ "JSON",
+ "Comments",
+ "Barcode",
+ "Dynamic Link",
+ "Read Only",
+ "Assign",
+ "Color",
].indexOf(df.fieldtype) != -1
) {
- df.fieldtype = 'Data';
+ df.fieldtype = "Data";
} else if (
- df.fieldtype == 'Link' &&
+ df.fieldtype == "Link" &&
[
- '=',
- '!=',
- 'descendants of',
- 'ancestors of',
- 'not descendants of',
- 'not ancestors of',
+ "=",
+ "!=",
+ "descendants of",
+ "ancestors of",
+ "not descendants of",
+ "not ancestors of",
].indexOf(condition) == -1
) {
- df.fieldtype = 'Data';
+ df.fieldtype = "Data";
}
- if (
- df.fieldtype === 'Data' &&
- (df.options || '').toLowerCase() === 'email'
- ) {
+ if (df.fieldtype === "Data" && (df.options || "").toLowerCase() === "email") {
df.options = null;
}
- if (
- condition == 'Between' &&
- (df.fieldtype == 'Date' || df.fieldtype == 'Datetime')
- ) {
- df.fieldtype = 'DateRange';
+ if (condition == "Between" && (df.fieldtype == "Date" || df.fieldtype == "Datetime")) {
+ df.fieldtype = "DateRange";
}
if (
- condition == 'Timespan' &&
- ['Date', 'Datetime', 'DateRange', 'Select'].includes(df.fieldtype)
+ condition == "Timespan" &&
+ ["Date", "Datetime", "DateRange", "Select"].includes(df.fieldtype)
) {
- df.fieldtype = 'Select';
- df.options = this.get_timespan_options(['Last', 'Yesterday', 'Today', 'Tomorrow', 'This', 'Next']);
+ df.fieldtype = "Select";
+ df.options = this.get_timespan_options([
+ "Last",
+ "Yesterday",
+ "Today",
+ "Tomorrow",
+ "This",
+ "Next",
+ ]);
}
- if (condition === 'is') {
- df.fieldtype = 'Select';
+ if (condition === "is") {
+ df.fieldtype = "Select";
df.options = [
- { label: __('Set', null, 'Field value is set'), value: 'set' },
- { label: __('Not Set', null, 'Field value is not set'), value: 'not set' },
+ { label: __("Set", null, "Field value is set"), value: "set" },
+ { label: __("Not Set", null, "Field value is not set"), value: "not set" },
];
}
return;
@@ -565,9 +563,9 @@ frappe.ui.filter_utils = {
get_timespan_options(periods) {
const period_map = {
- Last: ['Week', 'Month', 'Quarter', '6 months', 'Year'],
- This: ['Week', 'Month', 'Quarter', 'Year'],
- Next: ['Week', 'Month', 'Quarter', '6 months', 'Year'],
+ Last: ["Week", "Month", "Quarter", "6 months", "Year"],
+ This: ["Week", "Month", "Quarter", "Year"],
+ Next: ["Week", "Month", "Quarter", "6 months", "Year"],
};
let options = [];
periods.forEach((period) => {
diff --git a/frappe/public/js/frappe/ui/filters/filter_list.js b/frappe/public/js/frappe/ui/filters/filter_list.js
index 5e13f086cb..4691e05792 100644
--- a/frappe/public/js/frappe/ui/filters/filter_list.js
+++ b/frappe/public/js/frappe/ui/filters/filter_list.js
@@ -14,9 +14,31 @@ frappe.ui.FilterGroup = class {
make_popover() {
this.init_filter_popover();
+ this.set_clear_all_filters_event();
this.set_popover_events();
}
+ set_clear_all_filters_event() {
+ if (!this.filter_x_button) return;
+
+ this.filter_x_button.on("click", () => {
+ this.toggle_empty_filters(true);
+ if (typeof this.base_list !== "undefined") {
+ // It's a list view. Clear all the filters, also the ones in the
+ // FilterArea outside this FilterGroup
+ this.base_list.filter_area.clear();
+ } else {
+ // Not a list view, just clear the filters in this FilterGroup
+ this.clear_filters();
+ }
+ this.update_filter_button();
+ });
+ }
+
+ hide_popover() {
+ this.filter_button.popover("hide");
+ }
+
init_filter_popover() {
this.filter_button.popover({
content: this.get_filter_area_template(),
@@ -28,46 +50,46 @@ frappe.ui.FilterGroup = class {
`,
html: true,
- trigger: 'manual',
- container: 'body',
- placement: 'bottom',
- offset: '-100px 0'
+ trigger: "manual",
+ container: "body",
+ placement: "bottom",
+ offset: "-100px, 0",
});
}
toggle_empty_filters(show) {
- this.wrapper &&
- this.wrapper.find('.empty-filters').toggle(show);
+ this.wrapper && this.wrapper.find(".empty-filters").toggle(show);
}
set_popover_events() {
- $(document.body).on('click', (e) => {
- if (this.wrapper && this.wrapper.is(':visible')) {
- const in_datepicker = $(e.target).is('.datepicker--cell')
- || $(e.target).closest('.datepicker--nav-title').length !== 0
- || $(e.target).parents('.datepicker--nav-action').length !== 0;
+ $(document.body).on("click", (e) => {
+ if (this.wrapper && this.wrapper.is(":visible")) {
+ const in_datepicker =
+ $(e.target).is(".datepicker--cell") ||
+ $(e.target).closest(".datepicker--nav-title").length !== 0 ||
+ $(e.target).parents(".datepicker--nav-action").length !== 0;
if (
- $(e.target).parents('.filter-popover').length === 0
- && $(e.target).parents('.filter-box').length === 0
- && this.filter_button.find($(e.target)).length === 0
- && !$(e.target).is(this.filter_button)
- && !in_datepicker
+ $(e.target).parents(".filter-popover").length === 0 &&
+ $(e.target).parents(".filter-box").length === 0 &&
+ this.filter_button.find($(e.target)).length === 0 &&
+ !$(e.target).is(this.filter_button) &&
+ !in_datepicker
) {
- this.wrapper && this.filter_button.popover('hide');
+ this.wrapper && this.hide_popover();
}
}
});
- this.filter_button.on('click', () => {
- this.filter_button.popover('toggle');
+ this.filter_button.on("click", () => {
+ this.filter_button.popover("toggle");
});
- this.filter_button.on('shown.bs.popover', () => {
+ this.filter_button.on("shown.bs.popover", () => {
let hide_empty_filters = this.filters && this.filters.length > 0;
if (!this.wrapper) {
- this.wrapper = $('.filter-popover');
+ this.wrapper = $(".filter-popover");
if (hide_empty_filters) {
this.toggle_empty_filters(false);
this.add_filters_to_popover(this.filters);
@@ -75,23 +97,23 @@ frappe.ui.FilterGroup = class {
this.set_filter_events();
}
this.toggle_empty_filters(false);
- !hide_empty_filters && this.add_filter(this.doctype, 'name');
+ !hide_empty_filters && this.add_filter(this.doctype, "name");
});
- this.filter_button.on('hidden.bs.popover', () => {
+ this.filter_button.on("hidden.bs.popover", () => {
this.apply();
});
// REDESIGN-TODO: (Temporary) Review and find best solution for this
- frappe.router.on('change', () => {
- if (this.wrapper && this.wrapper.is(':visible')) {
- this.filter_button.popover('hide');
+ frappe.router.on("change", () => {
+ if (this.wrapper && this.wrapper.is(":visible")) {
+ this.hide_popover();
}
});
}
add_filters_to_popover(filters) {
- filters.forEach(filter => {
+ filters.forEach((filter) => {
filter.parent = this.wrapper;
filter.field = null;
filter.make();
@@ -109,34 +131,31 @@ frappe.ui.FilterGroup = class {
? this.filters.length > 1
? __("{0} filters", [this.filters.length])
: __("{0} filter", [this.filters.length])
- : __('Filter');
-
+ : __("Filter");
this.filter_button
- .toggleClass('btn-default', !filters_applied)
- .toggleClass('btn-primary-light', filters_applied);
+ .toggleClass("btn-default", !filters_applied)
+ .toggleClass("btn-primary-light", filters_applied);
- this.filter_button.find('.filter-icon')
- .toggleClass('active', filters_applied);
+ this.filter_button.find(".filter-icon").toggleClass("active", filters_applied);
- this.filter_button.find('.button-label').html(button_label);
+ this.filter_button.find(".button-label").html(button_label);
}
set_filter_events() {
- this.wrapper.find('.add-filter').on('click', () => {
+ this.wrapper.find(".add-filter").on("click", () => {
this.toggle_empty_filters(false);
- this.add_filter(this.doctype, 'name');
+ this.add_filter(this.doctype, "name");
});
- this.wrapper.find('.clear-filters').on('click', () => {
+ this.wrapper.find(".clear-filters").on("click", () => {
this.toggle_empty_filters(true);
this.clear_filters();
this.on_change();
+ this.hide_popover();
});
- this.wrapper.find('.apply-filters').on('click', () => {
- this.filter_button.popover('hide');
- });
+ this.wrapper.find(".apply-filters").on("click", () => this.hide_popover());
}
add_filters(filters) {
@@ -157,24 +176,26 @@ frappe.ui.FilterGroup = class {
if (!this.validate_args(doctype, fieldname)) return false;
const is_new_filter = arguments.length < 2;
- if (is_new_filter && this.wrapper.find('.new-filter:visible').length) {
+ if (is_new_filter && this.wrapper.find(".new-filter:visible").length) {
// only allow 1 new filter at a time!
return Promise.resolve();
} else {
let args = [doctype, fieldname, condition, value, hidden];
const promise = this.push_new_filter(args, is_new_filter);
- return (promise && promise.then) ? promise : Promise.resolve();
+ return promise && promise.then ? promise : Promise.resolve();
}
}
validate_args(doctype, fieldname) {
- if (doctype && fieldname
- && !frappe.meta.has_field(doctype, fieldname)
- && frappe.model.is_non_std_field(fieldname)) {
-
+ if (
+ doctype &&
+ fieldname &&
+ !frappe.meta.has_field(doctype, fieldname) &&
+ frappe.model.is_non_std_field(fieldname)
+ ) {
frappe.msgprint({
- message: __('Invalid filter: {0}', [fieldname.bold()]),
- indicator: 'red',
+ message: __("Invalid filter: {0}", [fieldname.bold()]),
+ indicator: "red",
});
return false;
@@ -222,7 +243,7 @@ frappe.ui.FilterGroup = class {
}
get_filter_value(fieldname) {
- let filter_obj = this.filters.find(f => f.fieldname == fieldname) || {};
+ let filter_obj = this.filters.find((f) => f.fieldname == fieldname) || {};
return filter_obj.value;
}
@@ -234,8 +255,7 @@ frappe.ui.FilterGroup = class {
.map((f) => {
let f_value = f.get_value();
if (filter_value.length === 2) {
- exists =
- filter_value[0] === f_value[0] && filter_value[1] === f_value[1];
+ exists = filter_value[0] === f_value[0] && filter_value[1] === f_value[1];
return;
}
@@ -264,11 +284,10 @@ frappe.ui.FilterGroup = class {
update_filters() {
// remove hidden filters and undefined filters
const filter_exists = (f) => ![undefined, null].includes(f.get_selected_value());
- this.filters.map(f => !filter_exists(f) && f.remove());
- this.filters = this.filters.filter(f => filter_exists(f) && f.field);
+ this.filters.map((f) => !filter_exists(f) && f.remove());
+ this.filters = this.filters.filter((f) => filter_exists(f) && f.field);
this.update_filter_button();
- this.filters.length === 0 &&
- this.toggle_empty_filters(true);
+ this.filters.length === 0 && this.toggle_empty_filters(true);
}
clear_filters() {
@@ -289,28 +308,28 @@ frappe.ui.FilterGroup = class {
- ${__('No filters selected')}
+ ${__("No filters selected")}
-
`
- );
+
`);
/* eslint-disable indent */
}
@@ -333,8 +352,7 @@ frappe.ui.FilterGroup = class {
}
add(filters, refresh = true) {
- if (!filters || (Array.isArray(filters) && filters.length === 0))
- return Promise.resolve();
+ if (!filters || (Array.isArray(filters) && filters.length === 0)) return Promise.resolve();
if (typeof filters[0] === "string") {
// passed in the format of doctype, field, condition, value
@@ -346,9 +364,7 @@ frappe.ui.FilterGroup = class {
return !this.exists(f);
});
- const { non_standard_filters, promise } = this.set_standard_filter(
- filters
- );
+ const { non_standard_filters, promise } = this.set_standard_filter(filters);
return promise
.then(() => {
diff --git a/frappe/public/js/frappe/ui/find.js b/frappe/public/js/frappe/ui/find.js
index 6b2756ab73..7ea6c57022 100644
--- a/frappe/public/js/frappe/ui/find.js
+++ b/frappe/public/js/frappe/ui/find.js
@@ -1,16 +1,18 @@
frappe.find = {
page_primary_action: () => {
- return $('.page-actions:visible .btn-primary');
+ return $(".page-actions:visible .btn-primary");
},
field: (fieldname, value) => {
- return new Promise(resolve => {
+ return new Promise((resolve) => {
let input = $(`[data-fieldname="${fieldname}"] :input`);
- if(value) {
- input.val(value).trigger('change');
- frappe.after_ajax(() => { resolve(input); });
+ if (value) {
+ input.val(value).trigger("change");
+ frappe.after_ajax(() => {
+ resolve(input);
+ });
} else {
resolve(input);
}
});
- }
-};
\ No newline at end of file
+ },
+};
diff --git a/frappe/public/js/frappe/ui/group_by/group_by.js b/frappe/public/js/frappe/ui/group_by/group_by.js
index 692d675c62..f48aba1b7f 100644
--- a/frappe/public/js/frappe/ui/group_by/group_by.js
+++ b/frappe/public/js/frappe/ui/group_by/group_by.js
@@ -1,4 +1,4 @@
-frappe.provide('frappe.views');
+frappe.provide("frappe.views");
frappe.ui.GroupBy = class {
constructor(report_view) {
@@ -16,13 +16,13 @@ frappe.ui.GroupBy = class {
init_group_by_popover() {
const sql_aggregate_functions = [
- {name: 'count', label: __('Count')},
- {name: 'sum', label: __('Sum')},
- {name: 'avg', label: __('Average')}
+ { name: "count", label: __("Count") },
+ { name: "sum", label: __("Sum") },
+ { name: "avg", label: __("Average") },
];
const group_by_template = $(
- frappe.render_template('group_by', {
+ frappe.render_template("group_by", {
doctype: this.doctype,
group_by_conditions: this.get_group_by_fields(),
aggregate_function_conditions: sql_aggregate_functions,
@@ -39,64 +39,62 @@ frappe.ui.GroupBy = class {
`,
html: true,
- trigger: 'manual',
- container: 'body',
- placement: 'bottom',
- offset: '-100px 0',
+ trigger: "manual",
+ container: "body",
+ placement: "bottom",
+ offset: "-100px, 0",
});
}
// TODO: make common with filter popover
set_popover_events() {
- $(document.body).on('click', (e) => {
- if (this.wrapper && this.wrapper.is(':visible')) {
+ $(document.body).on("click", (e) => {
+ if (this.wrapper && this.wrapper.is(":visible")) {
if (
- $(e.target).parents('.group-by-popover').length === 0 &&
- $(e.target).parents('.group-by-box').length === 0 &&
- $(e.target).parents('.group-by-button').length === 0 &&
+ $(e.target).parents(".group-by-popover").length === 0 &&
+ $(e.target).parents(".group-by-box").length === 0 &&
+ $(e.target).parents(".group-by-button").length === 0 &&
!$(e.target).is(this.group_by_button)
) {
- this.wrapper && this.group_by_button.popover('hide');
+ this.wrapper && this.group_by_button.popover("hide");
}
}
});
- this.group_by_button.on('click', () => {
- this.group_by_button.popover('toggle');
+ this.group_by_button.on("click", () => {
+ this.group_by_button.popover("toggle");
});
- this.group_by_button.on('shown.bs.popover', () => {
+ this.group_by_button.on("shown.bs.popover", () => {
if (!this.wrapper) {
- this.wrapper = $('.group-by-popover');
+ this.wrapper = $(".group-by-popover");
this.setup_group_by_area();
}
});
- this.group_by_button.on('hidden.bs.popover', () => {
+ this.group_by_button.on("hidden.bs.popover", () => {
this.update_group_by_button();
});
- frappe.router.on('change', () => {
- this.group_by_button.popover('hide');
+ frappe.router.on("change", () => {
+ this.group_by_button.popover("hide");
});
}
setup_group_by_area() {
this.aggregate_on_html = ``;
- this.group_by_select = this.wrapper.find('select.group-by');
+ this.group_by_select = this.wrapper.find("select.group-by");
this.group_by_field && this.group_by_select.val(this.group_by_field);
- this.aggregate_function_select = this.wrapper.find(
- 'select.aggregate-function'
- );
- this.aggregate_on_select = this.wrapper.find('select.aggregate-on');
- this.remove_group_by_button = this.wrapper.find('.remove-group-by');
+ this.aggregate_function_select = this.wrapper.find("select.aggregate-function");
+ this.aggregate_on_select = this.wrapper.find("select.aggregate-on");
+ this.remove_group_by_button = this.wrapper.find(".remove-group-by");
if (this.aggregate_function) {
this.aggregate_function_select.val(this.aggregate_function);
} else {
// set default to count
- this.aggregate_function_select.val('count');
- this.aggregate_function = 'count';
+ this.aggregate_function_select.val("count");
+ this.aggregate_function = "count";
}
this.toggle_aggregate_on_field();
@@ -107,30 +105,28 @@ frappe.ui.GroupBy = class {
set_group_by_events() {
// try running on change
- this.group_by_select.on('change', () => {
+ this.group_by_select.on("change", () => {
this.group_by_field = this.group_by_select.val();
- this.group_by_doctype = this.group_by_select
- .find(':selected')
- .attr('data-doctype');
+ this.group_by_doctype = this.group_by_select.find(":selected").attr("data-doctype");
this.apply_group_by_and_refresh();
});
- this.aggregate_function_select.on('change', () => {
+ this.aggregate_function_select.on("change", () => {
//Set aggregate on options as numeric fields if function is sum or average
this.toggle_aggregate_on_field();
this.aggregate_function = this.aggregate_function_select.val();
this.apply_group_by_and_refresh();
});
- this.aggregate_on_select.on('change', () => {
+ this.aggregate_on_select.on("change", () => {
this.aggregate_on_field = this.aggregate_on_select.val();
this.aggregate_on_doctype = this.aggregate_on_select
- .find(':selected')
- .attr('data-doctype');
+ .find(":selected")
+ .attr("data-doctype");
this.apply_group_by_and_refresh();
});
- this.remove_group_by_button.on('click', () => {
+ this.remove_group_by_button.on("click", () => {
if (this.group_by) {
this.remove_group_by();
this.toggle_aggregate_on_field_display(false);
@@ -140,10 +136,10 @@ frappe.ui.GroupBy = class {
toggle_aggregate_on_field() {
let fn = this.aggregate_function_select.val();
- if (fn === 'sum' || fn === 'avg') {
+ if (fn === "sum" || fn === "avg") {
if (!this.aggregate_on_html.length) {
this.aggregate_on_html = `
- ${__('Select Field...')}
+ ${__("Select Field...")}
`;
for (let doctype in this.all_fields) {
@@ -171,10 +167,10 @@ frappe.ui.GroupBy = class {
//TODO: Fix this
toggle_aggregate_on_field_display(show) {
- this.group_by_select.parent().toggleClass('col-sm-5', show);
- this.group_by_select.parent().toggleClass('col-sm-8', !show);
- this.aggregate_function_select.parent().toggleClass('col-sm-2', show);
- this.aggregate_function_select.parent().toggleClass('col-sm-3', !show);
+ this.group_by_select.parent().toggleClass("col-sm-5", show);
+ this.group_by_select.parent().toggleClass("col-sm-8", !show);
+ this.aggregate_function_select.parent().toggleClass("col-sm-2", show);
+ this.aggregate_function_select.parent().toggleClass("col-sm-3", !show);
this.aggregate_on_select.parent().toggle(show);
}
@@ -191,22 +187,17 @@ frappe.ui.GroupBy = class {
}
apply_settings(settings) {
- let get_fieldname = (name) => name.split('.')[1].replace(/`/g, '');
- let get_doctype = (name) =>
- name
- .split('.')[0]
- .replace(/`/g, '')
- .replace('tab', '');
+ let get_fieldname = (name) => name.split(".")[1].replace(/`/g, "");
+ let get_doctype = (name) => name.split(".")[0].replace(/`/g, "").replace("tab", "");
- if (!settings.group_by.startsWith('`tab')) {
- settings.group_by =
- '`tab' + this.doctype + '`.`' + settings.group_by + '`';
+ if (!settings.group_by.startsWith("`tab")) {
+ settings.group_by = "`tab" + this.doctype + "`.`" + settings.group_by + "`";
}
- if (settings.aggregate_on && !settings.aggregate_on.startsWith('`tab')) {
+ if (settings.aggregate_on && !settings.aggregate_on.startsWith("`tab")) {
const aggregate_on_doctype = this.get_aggregate_on_doctype(settings);
settings.aggregate_on =
- '`tab' + aggregate_on_doctype + '`.`' + settings.aggregate_on + '`';
+ "`tab" + aggregate_on_doctype + "`.`" + settings.aggregate_on + "`";
}
// Extract fieldname from `tabdoctype`.`fieldname`
@@ -234,43 +225,38 @@ frappe.ui.GroupBy = class {
}
make_group_by_button() {
- this.page.wrapper.find('.sort-selector').before(
+ this.page.wrapper.find(".sort-selector").before(
$(`
- ${frappe.utils.icon('group-by')}
+ ${frappe.utils.icon("group-by")}
- ${__('Add Group')}
+ ${__("Add Group")}
`)
);
- this.group_by_button = this.page.wrapper.find('.group-by-button');
+ this.group_by_button = this.page.wrapper.find(".group-by-button");
}
apply_group_by() {
- this.group_by =
- '`tab' + this.group_by_doctype + '`.`' + this.group_by_field + '`';
+ this.group_by = "`tab" + this.group_by_doctype + "`.`" + this.group_by_field + "`";
- if (this.aggregate_function === 'count') {
+ if (this.aggregate_function === "count") {
this.aggregate_on_field = null;
this.aggregate_on_doctype = null;
} else {
this.aggregate_on =
- '`tab' +
- this.aggregate_on_doctype +
- '`.`' +
- this.aggregate_on_field +
- '`';
+ "`tab" + this.aggregate_on_doctype + "`.`" + this.aggregate_on_field + "`";
}
//All necessary fields must be set before applying group by
if (
!this.group_by ||
!this.aggregate_function ||
- (!this.aggregate_on_field && this.aggregate_function !== 'count')
+ (!this.aggregate_on_field && this.aggregate_function !== "count")
) {
return false;
}
@@ -287,13 +273,11 @@ frappe.ui.GroupBy = class {
set_args(args) {
if (this.aggregate_function && this.group_by) {
this.report_view.group_by = this.group_by;
- this.report_view.sort_by = '_aggregate_column';
- this.report_view.sort_order = 'desc';
+ this.report_view.sort_by = "_aggregate_column";
+ this.report_view.sort_order = "desc";
// save original fields
- if (
- !this.report_view.fields.map((f) => f[0]).includes('_aggregate_column')
- ) {
+ if (!this.report_view.fields.map((f) => f[0]).includes("_aggregate_column")) {
this.original_fields = this.report_view.fields.map((f) => f);
}
@@ -304,7 +288,7 @@ frappe.ui.GroupBy = class {
// add aggregate column in both query args and report views
this.report_view.fields.push([
- '_aggregate_column',
+ "_aggregate_column",
this.aggregate_on_doctype || this.doctype,
]);
@@ -313,11 +297,11 @@ frappe.ui.GroupBy = class {
Object.assign(args, {
with_comment_count: false,
- aggregate_on_field: this.aggregate_on_field || 'name',
+ aggregate_on_field: this.aggregate_on_field || "name",
aggregate_on_doctype: this.aggregate_on_doctype || this.doctype,
- aggregate_function: this.aggregate_function || 'count',
+ aggregate_function: this.aggregate_function || "count",
group_by: this.report_view.group_by || null,
- order_by: '_aggregate_column desc',
+ order_by: "_aggregate_column desc",
});
}
}
@@ -325,10 +309,10 @@ frappe.ui.GroupBy = class {
get_group_by_docfield() {
// called from build_column
let docfield = {};
- if (this.aggregate_function === 'count') {
+ if (this.aggregate_function === "count") {
docfield = {
- fieldtype: 'Int',
- label: __('Count'),
+ fieldtype: "Int",
+ label: __("Count"),
parent: this.doctype,
width: 200,
};
@@ -336,33 +320,31 @@ frappe.ui.GroupBy = class {
// get properties of "aggregate_on", for example Net Total
docfield = Object.assign(
{},
- frappe.meta.docfield_map[this.aggregate_on_doctype][
- this.aggregate_on_field
- ]
+ frappe.meta.docfield_map[this.aggregate_on_doctype][this.aggregate_on_field]
);
- if (this.aggregate_function === 'sum') {
- docfield.label = __('Sum of {0}', [docfield.label]);
+ if (this.aggregate_function === "sum") {
+ docfield.label = __("Sum of {0}", [docfield.label]);
} else {
- docfield.label = __('Average of {0}', [docfield.label]);
+ docfield.label = __("Average of {0}", [docfield.label]);
}
}
- docfield.fieldname = '_aggregate_column';
+ docfield.fieldname = "_aggregate_column";
return docfield;
}
remove_group_by() {
- this.order_by = '';
+ this.order_by = "";
this.group_by = null;
this.group_by_field = null;
this.report_view.group_by = null;
- this.aggregate_function = 'count';
+ this.aggregate_function = "count";
this.aggregate_on = null;
this.aggregate_on_field = null;
- this.group_by_select.val('');
- this.aggregate_function_select.val('count');
- this.aggregate_on_select.empty().val('');
+ this.group_by_select.val("");
+ this.aggregate_function_select.val("count");
+ this.aggregate_on_select.empty().val("");
this.aggregate_on_select.parent().hide();
// restore original fields
@@ -382,18 +364,16 @@ frappe.ui.GroupBy = class {
this.all_fields = {};
const fields = this.report_view.meta.fields.filter((f) =>
- ['Select', 'Link', 'Data', 'Int', 'Check'].includes(f.fieldtype)
+ ["Select", "Link", "Data", "Int", "Check"].includes(f.fieldtype)
);
- const tag_field = {fieldname: '_user_tags', fieldtype: 'Data', label: __('Tags')};
+ const tag_field = { fieldname: "_user_tags", fieldtype: "Data", label: __("Tags") };
this.group_by_fields[this.doctype] = fields.concat(tag_field);
this.all_fields[this.doctype] = this.report_view.meta.fields;
const standard_fields_filter = (df) =>
!in_list(frappe.model.no_value_type, df.fieldtype) && !df.report_hide;
- const table_fields = frappe.meta
- .get_table_fields(this.doctype)
- .filter((df) => !df.hidden);
+ const table_fields = frappe.meta.get_table_fields(this.doctype).filter((df) => !df.hidden);
table_fields.forEach((df) => {
const cdt = df.options;
@@ -411,22 +391,21 @@ frappe.ui.GroupBy = class {
const group_by_applied = Boolean(this.group_by_field);
const button_label = group_by_applied
? __("Group By {0}", [this.get_group_by_field_label()])
- : __('Add Group');
+ : __("Add Group");
this.group_by_button
- .toggleClass('btn-default', !group_by_applied)
- .toggleClass('btn-primary-light', group_by_applied);
+ .toggleClass("btn-default", !group_by_applied)
+ .toggleClass("btn-primary-light", group_by_applied);
- this.group_by_button.find('.group-by-icon')
- .toggleClass('active', group_by_applied);
+ this.group_by_button.find(".group-by-icon").toggleClass("active", group_by_applied);
- this.group_by_button.find('.button-label').html(button_label);
- this.group_by_button.attr('title', button_label);
+ this.group_by_button.find(".button-label").html(button_label);
+ this.group_by_button.attr("title", button_label);
}
get_group_by_field_label() {
return this.group_by_fields[this.group_by_doctype].find(
- field => field.fieldname == this.group_by_field
+ (field) => field.fieldname == this.group_by_field
).label;
}
};
diff --git a/frappe/public/js/frappe/ui/iconbar.js b/frappe/public/js/frappe/ui/iconbar.js
index 1d47dfa748..9542cb81b0 100644
--- a/frappe/public/js/frappe/ui/iconbar.js
+++ b/frappe/public/js/frappe/ui/iconbar.js
@@ -9,37 +9,39 @@ frappe.ui.IconBar = class IconBar {
}
make(n_groups) {
this.$wrapper = $('
').appendTo(this.parent);
- for(var i=0; i')
- .appendTo(this.$wrapper).find("ul");
+ if (!$ul.length)
+ $ul = $('')
+ .appendTo(this.$wrapper)
+ .find("ul");
return $ul;
}
add_btn(group, icon, label, click) {
var $ul = this.get_group(group);
- var $li = $(' ')
+ var $li = $(' ')
.appendTo($ul)
- .on("click", function() {
+ .on("click", function () {
click.apply(this);
return false;
});
- $li.find("i").attr("title", label).tooltip({ delay: { "show": 600, "hide": 100 }, trigger: "hover" });
+ $li.find("i")
+ .attr("title", label)
+ .tooltip({ delay: { show: 600, hide: 100 }, trigger: "hover" });
-
- this.$wrapper.find(".iconbar-" + group).removeClass("hide")
+ this.$wrapper.find(".iconbar-" + group).removeClass("hide");
this.show();
return $li;
}
hide(group) {
- if(group) {
+ if (group) {
this.$wrapper.find(".iconbar-" + group).addClass("hide");
this.check_if_all_hidden();
} else {
@@ -47,21 +49,24 @@ frappe.ui.IconBar = class IconBar {
}
}
show(group) {
- if(group) {
+ if (group) {
this.$wrapper.find(".iconbar-" + group).removeClass("hide");
this.show();
} else {
- if(this.$wrapper.hasClass("hide"))
- this.$wrapper.removeClass("hide").trigger("shown");
+ if (this.$wrapper.hasClass("hide")) this.$wrapper.removeClass("hide").trigger("shown");
}
}
clear(group) {
var me = this;
- this.$wrapper.find(".iconbar-" + group).addClass("hide").find("ul").empty();
+ this.$wrapper
+ .find(".iconbar-" + group)
+ .addClass("hide")
+ .find("ul")
+ .empty();
this.check_if_all_hidden();
}
check_if_all_hidden() {
- if(!this.$wrapper.find(".iconbar:visible").length) {
+ if (!this.$wrapper.find(".iconbar:visible").length) {
this.hide();
}
}
diff --git a/frappe/public/js/frappe/ui/keyboard.js b/frappe/public/js/frappe/ui/keyboard.js
index f7ba2dd9ae..2e3bd95616 100644
--- a/frappe/public/js/frappe/ui/keyboard.js
+++ b/frappe/public/js/frappe/ui/keyboard.js
@@ -1,43 +1,53 @@
-import './alt_keyboard_shortcuts';
+import "./alt_keyboard_shortcuts";
-frappe.provide('frappe.ui.keys.handlers');
+frappe.provide("frappe.ui.keys.handlers");
-frappe.ui.keys.setup = function() {
- $(window).on('keydown', function(e) {
+frappe.ui.keys.setup = function () {
+ $(window).on("keydown", function (e) {
var key = frappe.ui.keys.get_key(e);
- if(frappe.ui.keys.handlers[key]) {
+ if (frappe.ui.keys.handlers[key]) {
var out = null;
- for(var i=0, l = frappe.ui.keys.handlers[key].length; i {
+frappe.ui.keys.add_shortcut = ({
+ shortcut,
+ action,
+ description,
+ page,
+ target,
+ condition,
+ ignore_inputs = false,
+} = {}) => {
if (target instanceof jQuery) {
let $target = target;
action = () => {
$target[0].click();
- }
+ };
}
if (!condition) {
condition = () => true;
}
let handler = (e) => {
let $focused_element = $(document.activeElement);
- let is_input_focused = $focused_element.is('input, select, textarea, [contenteditable=true]');
+ let is_input_focused = $focused_element.is(
+ "input, select, textarea, [contenteditable=true]"
+ );
if (is_input_focused && !ignore_inputs) return;
if (!condition()) return;
- if (action && (!page || page.wrapper.is(':visible'))) {
+ if (action && (!page || page.wrapper.is(":visible"))) {
let prevent_default = action(e);
// prevent default if true is explicitly returned
// or nothing returned (undefined)
@@ -54,48 +64,49 @@ frappe.ui.keys.add_shortcut = ({shortcut, action, description, page, target, con
frappe.ui.keys.on(shortcut, handler);
// update standard shortcut list
- let existing_shortcut_index = standard_shortcuts.findIndex(
- s => s.shortcut === shortcut
- );
+ let existing_shortcut_index = standard_shortcuts.findIndex((s) => s.shortcut === shortcut);
let new_shortcut = { shortcut, action, description, page, condition };
if (existing_shortcut_index === -1) {
standard_shortcuts.push(new_shortcut);
} else {
standard_shortcuts[existing_shortcut_index] = new_shortcut;
}
-}
+};
frappe.ui.keys.show_keyboard_shortcut_dialog = () => {
if (frappe.ui.keys.is_dialog_shown) return;
- let global_shortcuts = standard_shortcuts.filter(shortcut => !shortcut.page);
+ let global_shortcuts = standard_shortcuts.filter((shortcut) => !shortcut.page);
let current_page_shortcuts = standard_shortcuts.filter(
- shortcut => shortcut.page && shortcut.page === window.cur_page.page.page);
+ (shortcut) => shortcut.page && shortcut.page === window.cur_page.page.page
+ );
let grid_shortcuts = standard_shortcuts.filter(
- shortcut => shortcut.page && shortcut.page === window.cur_page.page.frm);
+ (shortcut) => shortcut.page && shortcut.page === window.cur_page.page.frm
+ );
function generate_shortcuts_html(shortcuts, heading) {
if (!shortcuts.length) {
- return '';
+ return "";
}
let html = shortcuts
- .filter(s => s.condition ? s.condition() : true)
- .filter(s => !!s.description)
- .map(shortcut => {
+ .filter((s) => (s.condition ? s.condition() : true))
+ .filter((s) => !!s.description)
+ .map((shortcut) => {
let shortcut_label = shortcut.shortcut
- .split('+')
+ .split("+")
.map(frappe.utils.to_title_case)
- .join('+');
+ .join("+");
if (frappe.utils.is_mac()) {
- shortcut_label = shortcut_label.replace('Ctrl', '⌘');
+ shortcut_label = shortcut_label.replace("Ctrl", "⌘");
}
return `
${shortcut_label}
- ${shortcut.description || ''}
+ ${shortcut.description || ""}
`;
- }).join('');
- if (!html) return '';
+ })
+ .join("");
+ if (!html) return "";
html = `${heading}
@@ -104,15 +115,18 @@ frappe.ui.keys.show_keyboard_shortcut_dialog = () => {
return html;
}
- let global_shortcuts_html = generate_shortcuts_html(global_shortcuts, __('Global Shortcuts'));
- let current_page_shortcuts_html = generate_shortcuts_html(current_page_shortcuts, __('Page Shortcuts'));
- let grid_shortcuts_html = generate_shortcuts_html(grid_shortcuts, __('Grid Shortcuts'));
+ let global_shortcuts_html = generate_shortcuts_html(global_shortcuts, __("Global Shortcuts"));
+ let current_page_shortcuts_html = generate_shortcuts_html(
+ current_page_shortcuts,
+ __("Page Shortcuts")
+ );
+ let grid_shortcuts_html = generate_shortcuts_html(grid_shortcuts, __("Grid Shortcuts"));
let dialog = new frappe.ui.Dialog({
- title: __('Keyboard Shortcuts'),
+ title: __("Keyboard Shortcuts"),
on_hide() {
frappe.ui.keys.is_dialog_shown = false;
- }
+ },
});
dialog.$body.append(global_shortcuts_html);
@@ -120,166 +134,172 @@ frappe.ui.keys.show_keyboard_shortcut_dialog = () => {
dialog.$body.append(grid_shortcuts_html);
dialog.$body.append(`
- ${__('Press Alt Key to trigger additional shortcuts in Menu and Sidebar')}
+ ${__("Press Alt Key to trigger additional shortcuts in Menu and Sidebar")}
`);
dialog.show();
frappe.ui.keys.is_dialog_shown = true;
-}
+};
-frappe.ui.keys.get_key = function(e) {
+frappe.ui.keys.get_key = function (e) {
var keycode = e.keyCode || e.which;
var key = frappe.ui.keys.key_map[keycode] || String.fromCharCode(keycode);
- if(e.ctrlKey || e.metaKey) {
+ if (e.ctrlKey || e.metaKey) {
// add ctrl+ the key
- key = 'ctrl+' + key;
+ key = "ctrl+" + key;
}
- if(e.shiftKey) {
- // add ctrl+ the key
- key = 'shift+' + key;
+ if (e.shiftKey) {
+ // add shift+ the key
+ key = "shift+" + key;
}
if (e.altKey) {
// add alt+ the key
- key = 'alt+' + key;
+ key = "alt+" + key;
}
if (e.altKey && e.ctrlKey) {
// add alt+ctrl+ the key or single key e.g f1,f2,etc..
return key.toLowerCase();
}
return key.toLowerCase();
-}
+};
-frappe.ui.keys.on = function(key, handler) {
- if(!frappe.ui.keys.handlers[key]) {
+frappe.ui.keys.on = function (key, handler) {
+ if (!frappe.ui.keys.handlers[key]) {
frappe.ui.keys.handlers[key] = [];
}
frappe.ui.keys.handlers[key].push(handler);
-}
+};
-frappe.ui.keys.off = function(key, page) {
+frappe.ui.keys.off = function (key, page) {
let handlers = frappe.ui.keys.handlers[key];
if (!handlers || handlers.length === 0) return;
- frappe.ui.keys.handlers[key] = handlers.filter(h => {
+ frappe.ui.keys.handlers[key] = handlers.filter((h) => {
if (!page) return false;
return h.page !== page;
});
-}
+};
frappe.ui.keys.add_shortcut({
- shortcut: 'ctrl+s',
- action: function(e) {
+ shortcut: "ctrl+s",
+ action: function (e) {
frappe.app.trigger_primary_action();
e.preventDefault();
return false;
},
- description: __('Trigger Primary Action'),
- ignore_inputs: true
+ description: __("Trigger Primary Action"),
+ ignore_inputs: true,
});
frappe.ui.keys.add_shortcut({
- shortcut: 'ctrl+g',
- action: function(e) {
+ shortcut: "ctrl+g",
+ action: function (e) {
$("#navbar-search").focus();
e.preventDefault();
return false;
},
- description: __('Open Awesomebar')
+ description: __("Open Awesomebar"),
});
frappe.ui.keys.add_shortcut({
- shortcut: 'ctrl+h',
- action: function(e) {
+ shortcut: "ctrl+h",
+ action: function (e) {
e.preventDefault();
- $('.navbar-home img').click();
+ $(".navbar-home img").click();
},
- description: __('Navigate Home')
+ description: __("Navigate Home"),
});
frappe.ui.keys.add_shortcut({
- shortcut: 'alt+s',
- action: function(e) {
+ shortcut: "alt+s",
+ action: function (e) {
e.preventDefault();
- $('.dropdown-navbar-user a').eq(0).click();
+ $(".dropdown-navbar-user a").eq(0).click();
},
- description: __('Open Settings')
+ description: __("Open Settings"),
});
frappe.ui.keys.add_shortcut({
- shortcut: 'shift+/',
- action: function() {
+ shortcut: "shift+/",
+ action: function () {
frappe.ui.keys.show_keyboard_shortcut_dialog();
},
- description: __('Show Keyboard Shortcuts')
+ description: __("Show Keyboard Shortcuts"),
});
frappe.ui.keys.add_shortcut({
- shortcut: 'alt+h',
- action: function(e) {
+ shortcut: "alt+h",
+ action: function (e) {
e.preventDefault();
- $('.dropdown-help a').eq(0).click();
+ $(".dropdown-help a").eq(0).click();
},
- description: __('Open Help')
+ description: __("Open Help"),
});
-frappe.ui.keys.on('escape', function(e) {
+frappe.ui.keys.on("escape", function (e) {
handle_escape_key();
});
-frappe.ui.keys.on('esc', function(e) {
+frappe.ui.keys.on("esc", function (e) {
handle_escape_key();
});
-frappe.ui.keys.on('enter', function(e) {
- if(window.cur_dialog && cur_dialog.confirm_dialog) {
- cur_dialog.get_primary_btn().trigger('click');
+frappe.ui.keys.on("enter", function (e) {
+ if (window.cur_dialog && cur_dialog.confirm_dialog) {
+ cur_dialog.get_primary_btn().trigger("click");
}
});
-frappe.ui.keys.on('ctrl+down', function(e) {
+frappe.ui.keys.on("ctrl+down", function (e) {
var grid_row = frappe.ui.form.get_open_grid_form();
- grid_row && grid_row.toggle_view(false, function() { grid_row.open_next() });
+ grid_row &&
+ grid_row.toggle_view(false, function () {
+ grid_row.open_next();
+ });
});
-frappe.ui.keys.on('ctrl+up', function(e) {
+frappe.ui.keys.on("ctrl+up", function (e) {
var grid_row = frappe.ui.form.get_open_grid_form();
- grid_row && grid_row.toggle_view(false, function() { grid_row.open_prev() });
+ grid_row &&
+ grid_row.toggle_view(false, function () {
+ grid_row.open_prev();
+ });
});
frappe.ui.keys.add_shortcut({
- shortcut: 'shift+ctrl+r',
- action: function() {
+ shortcut: "shift+ctrl+r",
+ action: function () {
frappe.ui.toolbar.clear_cache();
},
- description: __('Clear Cache and Reload')
+ description: __("Clear Cache and Reload"),
});
frappe.ui.keys.key_map = {
- 8: 'backspace',
- 9: 'tab',
- 13: 'enter',
- 16: 'shift',
- 17: 'ctrl',
- 91: 'meta',
- 18: 'alt',
- 27: 'escape',
- 37: 'left',
- 39: 'right',
- 38: 'up',
- 40: 'down',
- 32: 'space',
- 112: 'f1',
- 113: 'f2',
- 114: 'f3',
- 115: 'f4',
- 116: 'f5',
- 191: '/',
- 188: '<',
- 190: '>'
-}
+ 8: "backspace",
+ 9: "tab",
+ 13: "enter",
+ 16: "shift",
+ 17: "ctrl",
+ 91: "meta",
+ 18: "alt",
+ 27: "escape",
+ 37: "left",
+ 39: "right",
+ 38: "up",
+ 40: "down",
+ 32: "space",
+ 112: "f1",
+ 113: "f2",
+ 114: "f3",
+ 115: "f4",
+ 116: "f5",
+ 191: "/",
+ 188: "<",
+ 190: ">",
+};
-'abcdefghijklmnopqrstuvwxyz'.split('').forEach((letter, i) => {
+"abcdefghijklmnopqrstuvwxyz".split("").forEach((letter, i) => {
frappe.ui.keys.key_map[65 + i] = letter;
});
@@ -293,8 +313,8 @@ frappe.ui.keyCode = {
ENTER: 13,
TAB: 9,
SPACE: 32,
- BACKSPACE: 8
-}
+ BACKSPACE: 8,
+};
function handle_escape_key() {
close_grid_and_dialog();
@@ -311,7 +331,7 @@ function close_grid_and_dialog() {
}
// close open dialog
- if (cur_dialog && !cur_dialog.no_cancel_flag) {
+ if (cur_dialog && !cur_dialog.no_cancel_flag && !cur_dialog.static) {
cur_dialog.cancel();
return false;
}
diff --git a/frappe/public/js/frappe/ui/like.js b/frappe/public/js/frappe/ui/like.js
index 6716f5174c..aa007cf138 100644
--- a/frappe/public/js/frappe/ui/like.js
+++ b/frappe/public/js/frappe/ui/like.js
@@ -1,24 +1,24 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
-frappe.ui.is_liked = function(doc) {
+frappe.ui.is_liked = function (doc) {
var liked = frappe.ui.get_liked_by(doc);
- return liked.indexOf(frappe.session.user)===-1 ? false : true;
-}
+ return liked.indexOf(frappe.session.user) === -1 ? false : true;
+};
-frappe.ui.get_liked_by = function(doc) {
+frappe.ui.get_liked_by = function (doc) {
var liked = doc._liked_by;
- if(liked) {
+ if (liked) {
liked = JSON.parse(liked);
}
return liked || [];
-}
+};
-frappe.ui.toggle_like = function($btn, doctype, name, callback) {
+frappe.ui.toggle_like = function ($btn, doctype, name, callback) {
var add = $btn.hasClass("not-liked") ? "Yes" : "No";
// disable click
- $btn.css('pointer-events', 'none');
+ $btn.css("pointer-events", "none");
frappe.call({
method: "frappe.desk.like.toggle_like",
@@ -28,16 +28,21 @@ frappe.ui.toggle_like = function($btn, doctype, name, callback) {
name: name,
add: add,
},
- callback: function(r) {
+ callback: function (r) {
// renable click
- $btn.css('pointer-events', 'auto');
+ $btn.css("pointer-events", "auto");
- if(!r.exc) {
+ if (!r.exc) {
// update in all local-buttons
- var action_buttons = $('.like-action[data-name="'+ name.replace(/"/g, '\"')
- +'"][data-doctype="'+ doctype.replace(/"/g, '\"')+'"]');
+ var action_buttons = $(
+ '.like-action[data-name="' +
+ name.replace(/"/g, '"') +
+ '"][data-doctype="' +
+ doctype.replace(/"/g, '"') +
+ '"]'
+ );
- if(add==="Yes") {
+ if (add === "Yes") {
action_buttons.removeClass("not-liked").addClass("liked");
} else {
action_buttons.addClass("not-liked").removeClass("liked");
@@ -45,36 +50,35 @@ frappe.ui.toggle_like = function($btn, doctype, name, callback) {
// update in locals (form)
var doc = locals[doctype] && locals[doctype][name];
- if(doc) {
+ if (doc) {
var liked_by = JSON.parse(doc._liked_by || "[]"),
idx = liked_by.indexOf(frappe.session.user);
- if(add==="Yes") {
- if(idx===-1)
- liked_by.push(frappe.session.user);
+ if (add === "Yes") {
+ if (idx === -1) liked_by.push(frappe.session.user);
} else {
- if(idx!==-1) {
- liked_by = liked_by.slice(0,idx).concat(liked_by.slice(idx+1))
+ if (idx !== -1) {
+ liked_by = liked_by.slice(0, idx).concat(liked_by.slice(idx + 1));
}
}
doc._liked_by = JSON.stringify(liked_by);
}
- if(callback) {
+ if (callback) {
callback();
}
}
- }
+ },
});
};
-frappe.ui.click_toggle_like = function() {
+frappe.ui.click_toggle_like = function () {
var $btn = $(this);
var $count = $btn.siblings(".likes-count");
var not_liked = $btn.hasClass("not-liked");
var doctype = $btn.attr("data-doctype");
var name = $btn.attr("data-name");
- frappe.ui.toggle_like($btn, doctype, name, function() {
+ frappe.ui.toggle_like($btn, doctype, name, function () {
if (not_liked) {
$count.text(cint($count.text()) + 1);
} else {
@@ -83,37 +87,37 @@ frappe.ui.click_toggle_like = function() {
});
return false;
-}
+};
-frappe.ui.setup_like_popover = ($parent, selector, check_not_liked=true) => {
+frappe.ui.setup_like_popover = ($parent, selector, check_not_liked = true) => {
if (frappe.dom.is_touchscreen()) {
return;
}
- $parent.on('mouseover', selector, function() {
+ $parent.on("mouseover", selector, function () {
const target_element = $(this);
target_element.popover({
animation: true,
- placement: 'bottom',
- trigger: 'manual',
+ placement: "bottom",
+ trigger: "manual",
template: ``,
content: () => {
- let liked_by = target_element.parents(".liked-by").attr('data-liked-by');
- liked_by = liked_by ? decodeURI(liked_by) : '[]';
+ let liked_by = target_element.parents(".liked-by").attr("data-liked-by");
+ liked_by = liked_by ? decodeURI(liked_by) : "[]";
liked_by = JSON.parse(liked_by);
const user = frappe.session.user;
// hack
if (check_not_liked) {
if (target_element.parents(".liked-by").find(".not-liked").length) {
- if (liked_by.indexOf(user)!==-1) {
+ if (liked_by.indexOf(user) !== -1) {
liked_by.splice(liked_by.indexOf(user), 1);
}
} else {
- if (liked_by.indexOf(user)===-1) {
+ if (liked_by.indexOf(user) === -1) {
liked_by.push(user);
}
}
@@ -126,9 +130,9 @@ frappe.ui.setup_like_popover = ($parent, selector, check_not_liked=true) => {
let liked_by_list = $(``);
// to show social profile of the user
- let link_base = '/app/user-profile/';
+ let link_base = "/app/user-profile/";
- liked_by.forEach(user => {
+ liked_by.forEach((user) => {
// append user list item
liked_by_list.append(`
${frappe.avatar(user, "avatar-xs")}
@@ -137,31 +141,30 @@ frappe.ui.setup_like_popover = ($parent, selector, check_not_liked=true) => {
`);
});
- liked_by_list.children('li').click(ev => {
+ liked_by_list.children("li").click((ev) => {
let user = ev.currentTarget.dataset.user;
- target_element.popover('hide');
+ target_element.popover("hide");
frappe.set_route(link_base + user);
});
return liked_by_list;
},
html: true,
- container: 'body'
+ container: "body",
});
- target_element.popover('show');
+ target_element.popover("show");
$(".popover").on("mouseleave", () => {
- target_element.popover('hide');
+ target_element.popover("hide");
});
- target_element.on('mouseout', () => {
+ target_element.on("mouseout", () => {
setTimeout(() => {
- if (!$('.popover:hover').length) {
- target_element.popover('hide');
+ if (!$(".popover:hover").length) {
+ target_element.popover("hide");
}
}, 100);
});
});
-
};
diff --git a/frappe/public/js/frappe/ui/link_preview.js b/frappe/public/js/frappe/ui/link_preview.js
index a6a2273161..bf48a234ce 100644
--- a/frappe/public/js/frappe/ui/link_preview.js
+++ b/frappe/public/js/frappe/ui/link_preview.js
@@ -1,5 +1,4 @@
frappe.ui.LinkPreview = class {
-
constructor() {
this.popovers_list = [];
this.LINK_CLASSES = 'a[data-doctype], input[data-fieldtype="Link"], .popover';
@@ -8,12 +7,12 @@ frappe.ui.LinkPreview = class {
}
setup_events() {
- $(document.body).on('mouseover', this.LINK_CLASSES, (e) => {
+ $(document.body).on("mouseover", this.LINK_CLASSES, (e) => {
this.link_hovered = true;
this.element = $(e.currentTarget);
- this.is_link = this.element.get(0).tagName.toLowerCase() === 'a';
+ this.is_link = this.element.get(0).tagName.toLowerCase() === "a";
- if (!this.element.parents().find('.popover').length) {
+ if (!this.element.parents().find(".popover").length) {
this.identify_doc();
this.popover = this.element.data("bs.popover");
if (this.name && this.doctype) {
@@ -26,13 +25,16 @@ frappe.ui.LinkPreview = class {
identify_doc() {
if (this.is_link) {
- this.doctype = this.element.attr('data-doctype');
- this.name = this.element.attr('data-name');
- this.href = this.element.attr('href');
+ this.doctype = this.element.attr("data-doctype");
+ this.name = this.element.attr("data-name");
+ this.href = this.element.attr("href");
} else {
- this.href = this.element.parents('.control-input-wrapper').find('.control-value a').attr('href');
+ this.href = this.element
+ .parents(".control-input-wrapper")
+ .find(".control-value a")
+ .attr("href");
// input
- this.doctype = this.element.attr('data-target');
+ this.doctype = this.element.attr("data-target");
this.name = this.element.val();
}
}
@@ -42,17 +44,16 @@ frappe.ui.LinkPreview = class {
return;
}
//If control field value is changed, new popover has to be created
- this.element.on('change', () => {
+ this.element.on("change", () => {
this.new_popover = true;
});
if (!this.popover || this.new_popover) {
this.data_timeout = setTimeout(() => {
this.create_popover(e);
}, 100);
-
} else {
this.popover_timeout = setTimeout(() => {
- if (this.element.is(':focus')) {
+ if (this.element.is(":focus")) {
return;
}
this.show_popover(e);
@@ -62,11 +63,11 @@ frappe.ui.LinkPreview = class {
create_popover(e) {
this.new_popover = false;
- if (this.element.is(':focus')) {
+ if (this.element.is(":focus")) {
return;
}
- this.get_preview_data().then(preview_data => {
+ this.get_preview_data().then((preview_data) => {
if (preview_data) {
if (this.popover_timeout) {
clearTimeout(this.popover_timeout);
@@ -80,7 +81,6 @@ frappe.ui.LinkPreview = class {
this.init_preview_popover(preview_data);
}
this.show_popover(e);
-
}, 1000);
}
});
@@ -93,18 +93,18 @@ frappe.ui.LinkPreview = class {
if (!this.is_link) {
var left = e.pageX;
- this.element.popover('show');
- var width = $('.popover').width();
- $('.control-field-popover').css('left', (left - (width / 2)) + 'px');
+ this.element.popover("show");
+ var width = $(".popover").width();
+ $(".control-field-popover").css("left", left - width / 2 + "px");
} else {
- this.element.popover('show');
+ this.element.popover("show");
}
}
handle_popover_hide() {
- $(document).on('mouseout', this.LINK_CLASSES, () => {
+ $(document).on("mouseout", this.LINK_CLASSES, () => {
// To allow popover to be hovered on
- if (!$('.popover:hover').length) {
+ if (!$(".popover:hover").length) {
this.link_hovered = false;
}
if (!this.link_hovered) {
@@ -121,26 +121,26 @@ frappe.ui.LinkPreview = class {
}
});
- frappe.router.on('change', () => {
+ frappe.router.on("change", () => {
this.clear_all_popovers();
});
}
clear_all_popovers() {
- this.popovers_list.forEach($el => $el.hide());
+ this.popovers_list.forEach(($el) => $el.hide());
}
get_preview_data() {
- return frappe.xcall('frappe.desk.link_preview.get_preview_data', {
- 'doctype': this.doctype,
- 'docname': this.name,
+ return frappe.xcall("frappe.desk.link_preview.get_preview_data", {
+ doctype: this.doctype,
+ docname: this.name,
});
}
init_preview_popover(preview_data) {
let popover_content = this.get_popover_html(preview_data);
this.element.popover({
- container: 'body',
+ container: "body",
template: `
@@ -151,14 +151,13 @@ frappe.ui.LinkPreview = class {
html: true,
sanitizeFn: (content) => content,
content: popover_content,
- trigger: 'manual',
- placement: 'top',
+ trigger: "manual",
+ placement: "top",
});
- const $popover = $(this.element.data('bs.popover').tip);
- $popover.toggleClass('control-field-popover', this.is_link);
- this.popovers_list.push(this.element.data('bs.popover'));
-
+ const $popover = $(this.element.data("bs.popover").tip);
+ $popover.toggleClass("control-field-popover", this.is_link);
+ this.popovers_list.push(this.element.data("bs.popover"));
}
get_popover_html(preview_data) {
@@ -166,11 +165,11 @@ frappe.ui.LinkPreview = class {
this.href = window.location.href;
}
- if (this.href && this.href.includes(' ')) {
- this.href = this.href.replace(new RegExp(' ', 'g'), '%20');
+ if (this.href && this.href.includes(" ")) {
+ this.href = this.href.replace(new RegExp(" ", "g"), "%20");
}
- let popover_content =`
+ let popover_content = `
`;
};
- html = event_list.map(get_event_html).join('');
+ html = event_list.map(get_event_html).join("");
} else {
html = `
-
${__('No Upcoming Events')}
+
${__("No Upcoming Events")}
- ${__('There are no upcoming events for you.')}
+ ${__("There are no upcoming events for you.")}
`;
}
diff --git a/frappe/public/js/frappe/ui/page.html b/frappe/public/js/frappe/ui/page.html
index 06d1baec69..daea9fe03a 100644
--- a/frappe/public/js/frappe/ui/page.html
+++ b/frappe/public/js/frappe/ui/page.html
@@ -52,8 +52,8 @@
-
- {%= __("Actions") %}
+
+ {%= __("Actions") %}
@@ -80,4 +80,4 @@
-
\ No newline at end of file
+
diff --git a/frappe/public/js/frappe/ui/page.js b/frappe/public/js/frappe/ui/page.js
index eded1aefc5..18e9c9492c 100644
--- a/frappe/public/js/frappe/ui/page.js
+++ b/frappe/public/js/frappe/ui/page.js
@@ -18,11 +18,10 @@
* @typedef {Object} frappe.ui.Page
*/
-
-frappe.ui.make_app_page = function(opts) {
+frappe.ui.make_app_page = function (opts) {
opts.parent.page = new frappe.ui.Page(opts);
return opts.parent.page;
-}
+};
frappe.ui.pages = {};
@@ -48,16 +47,18 @@ frappe.ui.Page = class Page {
setup_scroll_handler() {
let last_scroll = 0;
- window.addEventListener('scroll', frappe.utils.throttle(() => {
- $('.page-head').toggleClass('drop-shadow', !!document.documentElement.scrollTop);
- let current_scroll = document.documentElement.scrollTop;
- if (current_scroll > 0 && last_scroll <= current_scroll) {
- $('.page-head').css("top", "-15px");
- } else {
- $('.page-head').css("top", "var(--navbar-height)");
- }
- last_scroll = current_scroll;
- }), 500);
+ $(window).scroll(
+ frappe.utils.throttle(() => {
+ $(".page-head").toggleClass("drop-shadow", !!document.documentElement.scrollTop);
+ let current_scroll = document.documentElement.scrollTop;
+ if (current_scroll > 0 && last_scroll <= current_scroll) {
+ $(".page-head").css("top", "-15px");
+ } else {
+ $(".page-head").css("top", "var(--navbar-height)");
+ }
+ last_scroll = current_scroll;
+ }, 500)
+ );
}
get_empty_state(title, message, primary_action) {
@@ -85,14 +86,19 @@ frappe.ui.Page = class Page {
$(frappe.render_template("page", {})).appendTo(this.wrapper);
if (this.single_column) {
// nesting under col-sm-12 for consistency
- this.add_view("main", '\
+ this.add_view(
+ "main",
+ '
');
+
'
+ );
} else {
- this.add_view("main", `
+ this.add_view(
+ "main",
+ `
@@ -100,7 +106,8 @@ frappe.ui.Page = class Page {
- `);
+ `
+ );
}
this.setup_page();
@@ -111,11 +118,9 @@ frappe.ui.Page = class Page {
this.$sub_title_area = this.wrapper.find("h6");
- if(this.title)
- this.set_title(this.title);
+ if (this.title) this.set_title(this.title);
- if(this.icon)
- this.get_main_icon(this.icon);
+ if (this.icon) this.get_main_icon(this.icon);
this.body = this.main = this.wrapper.find(".layout-main-section");
this.container = this.wrapper.find(".page-body");
@@ -141,33 +146,33 @@ frappe.ui.Page = class Page {
this.inner_toolbar = this.custom_actions;
this.icon_group = this.page_actions.find(".page-icon-group");
- if(this.make_page) {
+ if (this.make_page) {
this.make_page();
}
- this.card_layout && this.main.addClass('frappe-card');
+ this.card_layout && this.main.addClass("frappe-card");
// keyboard shortcuts
- let menu_btn = this.menu_btn_group.find('button');
- menu_btn.attr("title", __("Menu")).tooltip({ delay: { "show": 600, "hide": 100 } });
+ let menu_btn = this.menu_btn_group.find("button");
+ menu_btn.attr("title", __("Menu")).tooltip({ delay: { show: 600, hide: 100 } });
frappe.ui.keys
.get_shortcut_group(this.page_actions[0])
- .add(menu_btn, menu_btn.find('.menu-btn-group-label'));
+ .add(menu_btn, menu_btn.find(".menu-btn-group-label"));
- let action_btn = this.actions_btn_group.find('button');
+ let action_btn = this.actions_btn_group.find("button");
frappe.ui.keys
.get_shortcut_group(this.page_actions[0])
- .add(action_btn, action_btn.find('.actions-btn-group-label'));
+ .add(action_btn, action_btn.find(".actions-btn-group-label"));
}
setup_sidebar_toggle() {
- let sidebar_toggle = $('.page-head').find('.sidebar-toggle-btn');
- let sidebar_wrapper = this.wrapper.find('.layout-side-section');
+ let sidebar_toggle = $(".page-head").find(".sidebar-toggle-btn");
+ let sidebar_wrapper = this.wrapper.find(".layout-side-section");
if (this.disable_sidebar_toggle || !sidebar_wrapper.length) {
sidebar_toggle.remove();
} else {
sidebar_toggle.attr("title", __("Toggle Sidebar")).tooltip({
- delay: { "show": 600, "hide": 100 },
+ delay: { show: 600, hide: 100 },
trigger: "hover",
});
sidebar_toggle.click(() => {
@@ -176,45 +181,47 @@ frappe.ui.Page = class Page {
} else {
sidebar_wrapper.toggle();
}
- $(document.body).trigger('toggleSidebar');
+ $(document.body).trigger("toggleSidebar");
this.update_sidebar_icon();
});
}
}
setup_overlay_sidebar() {
- let overlay_sidebar = this.sidebar.find('.overlay-sidebar')
- .addClass('opened');
+ this.sidebar.find(".close-sidebar").remove();
+ let overlay_sidebar = this.sidebar.find(".overlay-sidebar").addClass("opened");
$('`);
- this.inner_toolbar.find('.inner-page-message').remove();
+ this.inner_toolbar.find(".inner-page-message").remove();
this.inner_toolbar.removeClass("hide").prepend($message);
return $message;
@@ -653,13 +694,13 @@ frappe.ui.Page = class Page {
add_sidebar_item(label, action, insert_after, prepend) {
var parent = this.sidebar.find(".sidebar-menu.standard-actions");
- var li = $(' ');
- var link = $('').html(label).on("click", action).appendTo(li);
+ var li = $("");
+ var link = $("").html(label).on("click", action).appendTo(li);
if (insert_after) {
li.insertAfter(parent.find(insert_after));
} else {
- if(prepend) {
+ if (prepend) {
li.prependTo(parent);
} else {
li.appendTo(parent);
@@ -679,7 +720,7 @@ frappe.ui.Page = class Page {
return this.$title_area;
}
- set_title(title, icon=null, strip=true, tab_title="") {
+ set_title(title, icon = null, strip = true, tab_title = "") {
if (!title) title = "";
if (strip) {
title = strip_html(title);
@@ -691,7 +732,7 @@ frappe.ui.Page = class Page {
}
let title_wrapper = this.$title_area.find(".title-text");
title_wrapper.html(title);
- title_wrapper.attr('title', this.title);
+ title_wrapper.attr("title", this.title);
}
set_title_sub(txt) {
@@ -700,8 +741,9 @@ frappe.ui.Page = class Page {
}
get_main_icon(icon) {
- return this.$title_area.find(".title-icon")
- .html(' ')
+ return this.$title_area
+ .find(".title-icon")
+ .html(' ')
.toggle(true);
}
@@ -712,8 +754,8 @@ frappe.ui.Page = class Page {
add_button(label, click, opts) {
if (!opts) opts = {};
let button = $(`
- ${opts.icon ? frappe.utils.icon(opts.icon): ''}
+ class="btn ${opts.btn_class || "btn-default"} ${opts.btn_size || "btn-sm"} ellipsis">
+ ${opts.icon ? frappe.utils.icon(opts.icon) : ""}
${label}
`);
// Add actions as menu item in Mobile View (similar to "add_custom_button" in forms.js)
@@ -721,8 +763,8 @@ frappe.ui.Page = class Page {
menu_item.parent().addClass("hidden-xl");
button.appendTo(this.custom_actions);
- button.on('click', click);
- this.custom_actions.removeClass('hide');
+ button.on("click", click);
+ this.custom_actions.removeClass("hide");
return button;
}
@@ -730,14 +772,14 @@ frappe.ui.Page = class Page {
add_custom_button_group(label, icon, parent) {
let dropdown_label = `
${__(label)}
- ${frappe.utils.icon('select', 'xs')}
+ ${frappe.utils.icon("select", "xs")}
`;
if (icon) {
dropdown_label = `
${frappe.utils.icon(icon)}
${__(label)}
- ${frappe.utils.icon('select', 'xs')}
+ ${frappe.utils.icon("select", "xs")}
${frappe.utils.icon(icon)}
@@ -754,9 +796,9 @@ frappe.ui.Page = class Page {
`);
if (!parent) parent = this.custom_actions;
- parent.removeClass('hide').append(custom_btn_group);
+ parent.removeClass("hide").append(custom_btn_group);
- return custom_btn_group.find('.dropdown-menu');
+ return custom_btn_group.find(".dropdown-menu");
}
add_dropdown_button(parent, label, click, icon) {
@@ -766,19 +808,20 @@ frappe.ui.Page = class Page {
// page::form
add_label(label) {
this.show_form();
- return $(""+label+" ")
- .appendTo(this.page_form);
+ return $("" + label + " ").appendTo(
+ this.page_form
+ );
}
add_select(label, options) {
- var field = this.add_field({label:label, fieldtype:"Select"});
+ var field = this.add_field({ label: label, fieldtype: "Select" });
return field.$wrapper.find("select").empty().add_options(options);
}
add_data(label) {
- var field = this.add_field({label: label, fieldtype: "Data"});
+ var field = this.add_field({ label: label, fieldtype: "Data" });
return field.$wrapper.find("input").attr("placeholder", label);
}
add_date(label, date) {
- var field = this.add_field({label: label, fieldtype: "Date", "default": date});
+ var field = this.add_field({ label: label, fieldtype: "Date", default: date });
return field.$wrapper.find("input").attr("placeholder", label);
}
add_check(label) {
@@ -797,23 +840,24 @@ frappe.ui.Page = class Page {
df.placeholder = df.label;
}
- df.input_class = 'input-xs';
+ df.input_class = "input-xs";
var f = frappe.ui.form.make_control({
df: df,
parent: parent || this.page_form,
only_input: df.fieldtype == "Check" ? false : true,
- })
+ });
f.refresh();
$(f.wrapper)
- .addClass('col-md-2')
- .attr("title", __(df.label)).tooltip({
- delay: { "show": 600, "hide": 100},
- trigger: "hover"
+ .addClass("col-md-2")
+ .attr("title", __(df.label))
+ .tooltip({
+ delay: { show: 600, hide: 100 },
+ trigger: "hover",
});
// html fields in toolbar are only for display
- if (df.fieldtype=='HTML') {
+ if (df.fieldtype == "HTML") {
return;
}
@@ -822,18 +866,16 @@ frappe.ui.Page = class Page {
f.$input.attr("placeholder", __(df.label));
- if(df.fieldtype==="Check") {
- $(f.wrapper).find(":first-child")
- .removeClass("col-md-offset-4 col-md-8");
+ if (df.fieldtype === "Check") {
+ $(f.wrapper).find(":first-child").removeClass("col-md-offset-4 col-md-8");
}
- if(df.fieldtype=="Button") {
+ if (df.fieldtype == "Button") {
$(f.wrapper).find(".page-control-label").html(" ");
- f.$input.addClass("btn-xs").css({"width": "100%", "margin-top": "-1px"});
+ f.$input.addClass("btn-xs").css({ width: "100%", "margin-top": "-1px" });
}
- if(df["default"])
- f.set_input(df["default"])
+ if (df["default"]) f.set_input(df["default"]);
this.fields_dict[df.fieldname || df.label] = f;
return f;
}
@@ -856,11 +898,11 @@ frappe.ui.Page = class Page {
}
add_view(name, html) {
let element = html;
- if(typeof (html) === "string") {
+ if (typeof html === "string") {
element = $(html);
}
this.views[name] = element.appendTo($(this.wrapper).find(".page-content"));
- if(!this.current_view) {
+ if (!this.current_view) {
this.current_view = this.views[name];
} else {
this.views[name].toggle(false);
@@ -868,8 +910,7 @@ frappe.ui.Page = class Page {
return this.views[name];
}
set_view(name) {
- if(this.current_view_name===name)
- return;
+ if (this.current_view_name === name) return;
this.current_view && this.current_view.toggle(false);
this.current_view = this.views[name];
@@ -878,6 +919,6 @@ frappe.ui.Page = class Page {
this.views[name].toggle(true);
- this.wrapper.trigger('view-change');
+ this.wrapper.trigger("view-change");
}
};
diff --git a/frappe/public/js/frappe/ui/sidebar.js b/frappe/public/js/frappe/ui/sidebar.js
index b955d49d2e..3d876a015d 100644
--- a/frappe/public/js/frappe/ui/sidebar.js
+++ b/frappe/public/js/frappe/ui/sidebar.js
@@ -1,4 +1,4 @@
-frappe.provide('frappe.ui');
+frappe.provide("frappe.ui");
frappe.ui.Sidebar = class Sidebar {
constructor({ wrapper, css_class }) {
@@ -14,56 +14,52 @@ frappe.ui.Sidebar = class Sidebar {
`);
- this.$sidebar = this.wrapper.find('.' + this.css_class);
+ this.$sidebar = this.wrapper.find("." + this.css_class);
}
- add_item(item, section, h6=false) {
+ add_item(item, section, h6 = false) {
let $section, $li_item;
- if(!section && this.wrapper.find('.sidebar-menu').length === 0) {
+ if (!section && this.wrapper.find(".sidebar-menu").length === 0) {
// if no section, add section with no heading
$section = this.get_section();
} else {
$section = this.get_section(section);
}
- if(item instanceof jQuery) {
+ if (item instanceof jQuery) {
$li_item = $(``);
item.appendTo($li_item);
} else {
- const className = h6 ? 'h6' : '';
+ const className = h6 ? "h6" : "";
const html = `
- ${item.label}
+ ${item.label}
`;
- $li_item = $(html).click(
- () => item.on_click && item.on_click()
- );
+ $li_item = $(html).click(() => item.on_click && item.on_click());
}
$section.append($li_item);
- if(item.name) {
+ if (item.name) {
this.items[item.name] = $li_item;
}
}
remove_item(name) {
- if(this.items[name]) {
+ if (this.items[name]) {
this.items[name].remove();
}
}
- get_section(section_heading="") {
- let $section = $(this.wrapper.find(
- `[data-section-heading="${section_heading}"]`));
- if($section.length) {
+ get_section(section_heading = "") {
+ let $section = $(this.wrapper.find(`[data-section-heading="${section_heading}"]`));
+ if ($section.length) {
return $section;
}
- const $section_heading = section_heading ?
- `${section_heading} ` : '';
+ const $section_heading = section_heading ? `${section_heading} ` : "";
$section = $(`
-