Merge branch 'frappe:develop' into tabs_on_grid_row_form
This commit is contained in:
commit
fc03a8b3f8
52 changed files with 583 additions and 344 deletions
|
|
@ -75,7 +75,7 @@ It takes care of installation, setup, upgrades, monitoring, maintenance and supp
|
|||
### Docker
|
||||
Prerequisites: docker, docker-compose, git. Refer [Docker Documentation](https://docs.docker.com) for more details on Docker setup.
|
||||
|
||||
Run following commands:
|
||||
Run the following commands:
|
||||
|
||||
```
|
||||
git clone https://github.com/frappe/frappe_docker
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ const jump_to_field = (field_label) => {
|
|||
.wait(500)
|
||||
.type("{enter}")
|
||||
.wait(200)
|
||||
.type("{enter}")
|
||||
.findByRole("button", { name: "Go" })
|
||||
.click()
|
||||
.wait(1000);
|
||||
};
|
||||
|
||||
|
|
@ -99,9 +100,7 @@ context("Form", () => {
|
|||
cy.new_form("User");
|
||||
|
||||
jump_to_field("Location"); // this is in collapsed section
|
||||
cy.wait(500);
|
||||
type_value("Bermuda");
|
||||
cy.wait(500);
|
||||
|
||||
cy.get_field("location").should("have.value", "Bermuda");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,18 +2,24 @@ context("List View Settings", () => {
|
|||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.visit("/desk/website");
|
||||
cy.visit("/desk/List/DocType/List");
|
||||
cy.wait(300);
|
||||
cy.clear_filters();
|
||||
cy.wait(300);
|
||||
cy.get(".menu-btn-group button").click({ force: true });
|
||||
cy.get(".dropdown-menu li").filter(":visible").contains("List Settings").click();
|
||||
cy.get(".modal-dialog").should("contain", "DocType List View Settings");
|
||||
cy.findByLabelText("Disable Count").uncheck({ force: true });
|
||||
cy.findByLabelText("Disable Comment Count").uncheck({ force: true });
|
||||
cy.findByLabelText("Disable Sidebar Stats").uncheck({ force: true });
|
||||
cy.findByRole("button", { name: "Save" }).click();
|
||||
cy.reload({ force: true });
|
||||
});
|
||||
it("Default settings", () => {
|
||||
cy.visit("/desk/List/DocType/List");
|
||||
cy.clear_filters();
|
||||
cy.get(".list-count").should("contain", "20 of");
|
||||
cy.get(".list-stats").should("contain", "Tags");
|
||||
});
|
||||
it("disable count and sidebar stats then verify", () => {
|
||||
cy.wait(300);
|
||||
cy.visit("/desk/List/DocType/List");
|
||||
cy.clear_filters();
|
||||
cy.wait(300);
|
||||
cy.get(".list-count").should("contain", "20 of");
|
||||
cy.get(".frappe-list svg.es-icon.es-line").should("be.visible");
|
||||
cy.get(".menu-btn-group button").click();
|
||||
|
|
@ -29,7 +35,7 @@ context("List View Settings", () => {
|
|||
|
||||
cy.get(".list-count").should("be.empty");
|
||||
cy.get(".list-sidebar .list-tags").should("not.exist");
|
||||
cy.get("[href='#es-line-chat-alt']").should("not.be.visible");
|
||||
cy.get("[href='#es-line-chat-alt']").should("not.exist");
|
||||
|
||||
cy.get(".menu-btn-group button").click({ force: true });
|
||||
cy.get(".dropdown-menu li").filter(":visible").contains("List Settings").click();
|
||||
|
|
|
|||
|
|
@ -505,7 +505,10 @@ def get_permission_query_conditions_for_communication(user):
|
|||
return None
|
||||
else:
|
||||
accounts = frappe.get_all(
|
||||
"User Email", filters={"parent": user}, fields=["email_account"], distinct=True, order_by="idx"
|
||||
"User Email",
|
||||
filters={"parent": user},
|
||||
fields=["email_account"],
|
||||
distinct=True,
|
||||
)
|
||||
|
||||
if not accounts:
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
"fetch_from",
|
||||
"fetch_if_empty",
|
||||
"visibility_section",
|
||||
"button_color",
|
||||
"hidden",
|
||||
"show_on_timeline",
|
||||
"bold",
|
||||
|
|
@ -617,6 +618,13 @@
|
|||
"fieldname": "mask",
|
||||
"fieldtype": "Check",
|
||||
"label": "Mask"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.fieldtype===\"Button\"",
|
||||
"fieldname": "button_color",
|
||||
"fieldtype": "Select",
|
||||
"label": "Button Color",
|
||||
"options": "\nDefault\nPrimary\nInfo\nSuccess\nWarning\nDanger"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ class DocField(Document):
|
|||
allow_in_quick_entry: DF.Check
|
||||
allow_on_submit: DF.Check
|
||||
bold: DF.Check
|
||||
button_color: DF.Literal["", "Default", "Primary", "Info", "Success", "Warning", "Danger"]
|
||||
collapsible: DF.Check
|
||||
collapsible_depends_on: DF.Code | None
|
||||
columns: DF.Int
|
||||
|
|
|
|||
|
|
@ -78,7 +78,10 @@ def get_active_domains():
|
|||
|
||||
def _get_active_domains():
|
||||
domains = frappe.get_all(
|
||||
"Has Domain", filters={"parent": "Domain Settings"}, fields=["domain"], distinct=True
|
||||
"Has Domain",
|
||||
filters={"parent": "Domain Settings"},
|
||||
fields=["domain"],
|
||||
distinct=True,
|
||||
)
|
||||
|
||||
active_domains = [row.get("domain") for row in domains]
|
||||
|
|
|
|||
|
|
@ -158,10 +158,7 @@ class TestServerScript(IntegrationTestCase):
|
|||
|
||||
def test_permission_query(self):
|
||||
sql = frappe.db.get_list("ToDo", run=False)
|
||||
if frappe.conf.db_type != "postgres":
|
||||
self.assertTrue("where (1 = 1)" in sql.lower())
|
||||
else:
|
||||
self.assertTrue("where (1 = '1')" in sql.lower())
|
||||
self.assertTrue("where (1 = 1)" in sql.lower())
|
||||
self.assertTrue(isinstance(frappe.db.get_list("ToDo"), list))
|
||||
|
||||
def test_attribute_error(self):
|
||||
|
|
|
|||
|
|
@ -1018,7 +1018,10 @@ def ask_pass_update():
|
|||
from frappe.utils import set_default
|
||||
|
||||
password_list = frappe.get_all(
|
||||
"User Email", filters={"awaiting_password": 1, "used_oauth": 0}, pluck="parent", distinct=True
|
||||
"User Email",
|
||||
filters={"awaiting_password": 1, "used_oauth": 0},
|
||||
pluck="parent",
|
||||
distinct=True,
|
||||
)
|
||||
set_default("email_user_password", ",".join(password_list))
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"link_filters",
|
||||
"column_break_6",
|
||||
"fieldtype",
|
||||
"button_color",
|
||||
"precision",
|
||||
"hide_seconds",
|
||||
"hide_days",
|
||||
|
|
@ -467,6 +468,13 @@
|
|||
"fieldname": "placeholder",
|
||||
"fieldtype": "Data",
|
||||
"label": "Placeholder"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.fieldtype===\"Button\"",
|
||||
"fieldname": "button_color",
|
||||
"fieldtype": "Select",
|
||||
"label": "Button Color",
|
||||
"options": "\nDefault\nPrimary\nInfo\nSuccess\nWarning\nDanger"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
|
|
@ -474,7 +482,7 @@
|
|||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-10 11:10:23.862393",
|
||||
"modified": "2025-11-12 01:14:24.753774",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Custom Field",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ class CustomField(Document):
|
|||
allow_in_quick_entry: DF.Check
|
||||
allow_on_submit: DF.Check
|
||||
bold: DF.Check
|
||||
button_color: DF.Literal["", "Default", "Primary", "Info", "Success", "Warning", "Danger"]
|
||||
collapsible: DF.Check
|
||||
collapsible_depends_on: DF.Code | None
|
||||
columns: DF.Int
|
||||
|
|
|
|||
|
|
@ -806,6 +806,7 @@ docfield_properties = {
|
|||
"is_virtual": "Check",
|
||||
"link_filters": "JSON",
|
||||
"placeholder": "Data",
|
||||
"button_color": "Select",
|
||||
}
|
||||
|
||||
doctype_link_properties = {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@
|
|||
"column_break_33",
|
||||
"read_only_depends_on",
|
||||
"display",
|
||||
"button_color",
|
||||
"in_filter",
|
||||
"hide_seconds",
|
||||
"hide_days",
|
||||
|
|
@ -485,6 +486,13 @@
|
|||
"fieldname": "placeholder",
|
||||
"fieldtype": "Data",
|
||||
"label": "Placeholder"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.fieldtype===\"Button\"",
|
||||
"fieldname": "button_color",
|
||||
"fieldtype": "Select",
|
||||
"label": "Button Color",
|
||||
"options": "\nDefault\nPrimary\nInfo\nSuccess\nWarning\nDanger"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
|
|
@ -492,7 +500,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-14 13:56:58.033573",
|
||||
"modified": "2025-11-12 01:13:53.053888",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form Field",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class CustomizeFormField(Document):
|
|||
allow_in_quick_entry: DF.Check
|
||||
allow_on_submit: DF.Check
|
||||
bold: DF.Check
|
||||
button_color: DF.Literal["", "Default", "Primary", "Info", "Success", "Warning", "Danger"]
|
||||
collapsible: DF.Check
|
||||
collapsible_depends_on: DF.Code | None
|
||||
columns: DF.Int
|
||||
|
|
|
|||
|
|
@ -24,6 +24,17 @@ def like(key: Field, value: str) -> frappe.qb:
|
|||
return key.like(value)
|
||||
|
||||
|
||||
def ilike(key: Field, value: str) -> frappe.qb:
|
||||
"""Wrapper method for `ILIKE`
|
||||
Args:
|
||||
key (str): field
|
||||
value (str): criterion
|
||||
Return:
|
||||
frappe.qb: `frappe.qb` object with `ILIKE`
|
||||
"""
|
||||
return key.ilike(value)
|
||||
|
||||
|
||||
def func_in(key: Field, value: list | tuple) -> frappe.qb:
|
||||
"""Wrapper method for `IN`.
|
||||
|
||||
|
|
@ -136,6 +147,7 @@ OPERATOR_MAP: dict[str, Callable] = {
|
|||
"in": func_in,
|
||||
"not in": func_not_in,
|
||||
"like": like,
|
||||
"ilike": ilike,
|
||||
"not like": not_like,
|
||||
"regex": func_regex,
|
||||
"between": func_between,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import datetime
|
||||
import re
|
||||
import warnings
|
||||
from functools import lru_cache
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
|
|
@ -227,6 +228,7 @@ class Engine:
|
|||
if self.apply_permissions:
|
||||
self.check_read_permission()
|
||||
|
||||
is_select = False
|
||||
if update:
|
||||
self.query = qb.update(self.table, immutable=False)
|
||||
elif into:
|
||||
|
|
@ -236,6 +238,7 @@ class Engine:
|
|||
else:
|
||||
self.query = qb.from_(self.table, immutable=False)
|
||||
self.apply_fields(fields)
|
||||
is_select = True
|
||||
|
||||
self.apply_filters(filters)
|
||||
self.apply_or_filters(or_filters)
|
||||
|
|
@ -260,7 +263,19 @@ class Engine:
|
|||
self.apply_group_by(group_by)
|
||||
|
||||
if order_by:
|
||||
self.apply_order_by(order_by)
|
||||
if not (
|
||||
self.is_postgres and is_select and (distinct or group_by)
|
||||
): # ignore in Postgres since order by fields need to appear in select distinct
|
||||
self.apply_order_by(order_by)
|
||||
else:
|
||||
warnings.warn(
|
||||
(
|
||||
"ORDER BY fields have been ignored because PostgreSQL requires them to "
|
||||
"appear in the SELECT list when using DISTINCT or GROUP BY."
|
||||
),
|
||||
UserWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
if self.apply_permissions:
|
||||
self.add_permission_conditions()
|
||||
|
|
@ -512,7 +527,12 @@ class Engine:
|
|||
)
|
||||
return operator_fn(_field, nodes or ("",))
|
||||
|
||||
operator_fn = OPERATOR_MAP[_operator.casefold()]
|
||||
if (
|
||||
self.is_postgres and _operator.casefold() == "like"
|
||||
): # use `ILIKE` to support case insensitive search in postgres
|
||||
operator_fn = OPERATOR_MAP["ilike"]
|
||||
else:
|
||||
operator_fn = OPERATOR_MAP[_operator.casefold()]
|
||||
if _value is None and isinstance(_field, Field):
|
||||
if operator_fn == builtin_operator.ne:
|
||||
filter_field_name = (
|
||||
|
|
@ -1425,7 +1445,7 @@ class Engine:
|
|||
if fieldtype == "Time":
|
||||
return "'00:00:00'"
|
||||
|
||||
if fieldtype in ("Float", "Int", "Currency", "Percent"):
|
||||
if fieldtype in ("Float", "Int", "Currency", "Percent", "Check"):
|
||||
return "0"
|
||||
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -305,7 +305,12 @@ def get_references_across_doctypes_by_dynamic_link_field(
|
|||
for doctype, fieldname, doctype_fieldname in links:
|
||||
try:
|
||||
filters = [[doctype_fieldname, "in", to_doctypes]] if to_doctypes else []
|
||||
for linked_to in frappe.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}
|
||||
|
|
|
|||
|
|
@ -285,7 +285,12 @@ class DesktopIconGrid {
|
|||
|
||||
prepare() {
|
||||
this.total_pages = 1;
|
||||
this.icons_data = this.icons_data.sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.icons_data = this.icons_data.sort((a, b) => {
|
||||
if (a.idx === b.idx) {
|
||||
return a.label.localeCompare(b.label); // sort by label if idx is the same
|
||||
}
|
||||
return a.idx - b.idx; // sort by idx
|
||||
});
|
||||
this.icons_data_by_page =
|
||||
this.icons_data || this.split_data(this.icons_data, this.page_size.total());
|
||||
}
|
||||
|
|
@ -465,7 +470,7 @@ class DesktopIconGrid {
|
|||
if (evt.to.parentElement == evt.from.parentElement) {
|
||||
let reordered_icons = me.sortable.toArray();
|
||||
let filters = {
|
||||
parent_icon: me.parent_icon?.icon_data.label || null,
|
||||
parent_icon: me.parent_icon?.icon_data.label || "" || null,
|
||||
};
|
||||
me.reorder_icons(reordered_icons, filters);
|
||||
me.parent_icon?.render_folder_thumbnail();
|
||||
|
|
@ -495,11 +500,12 @@ class DesktopIconGrid {
|
|||
}
|
||||
reorder_icons(reordered_icons, filters) {
|
||||
reordered_icons.forEach((d, idx) => {
|
||||
let icon = get_desktop_icon_by_label(d, filters);
|
||||
let icon = get_desktop_icon_by_label(d);
|
||||
if (icon) {
|
||||
icon.idx = idx;
|
||||
}
|
||||
});
|
||||
frappe.boot.desktop_icons.sort((a, b) => a.idx - b.idx);
|
||||
}
|
||||
add_to_main_screen(title) {
|
||||
let icon = get_desktop_icon_by_label(title);
|
||||
|
|
@ -509,6 +515,7 @@ class DesktopIconGrid {
|
|||
class DesktopIcon {
|
||||
constructor(icon, in_folder) {
|
||||
this.icon_data = icon;
|
||||
this.icon_data.label = __(this.icon_data.label);
|
||||
this.icon_title = this.icon_data.label;
|
||||
this.icon_subtitle = "";
|
||||
this.icon_type = this.icon_data.icon_type;
|
||||
|
|
|
|||
|
|
@ -195,15 +195,9 @@ def search_widget(
|
|||
_relevance_expr = {"DIV": [1, {"NULLIF": [{"LOCATE": [_txt, "name"]}, 0]}]}
|
||||
|
||||
# For MariaDB, wrap in IFNULL for sorting to push nulls to end
|
||||
if frappe.db.db_type in ("mariadb", "sqlite"):
|
||||
_relevance = {"IFNULL": [_relevance_expr, -9999], "as": "_relevance"}
|
||||
formatted_fields.append(_relevance)
|
||||
order_by = f"_relevance desc, {order_by}"
|
||||
elif frappe.db.db_type == "postgres":
|
||||
_relevance = {**_relevance_expr, "as": "_relevance"}
|
||||
formatted_fields.append(_relevance)
|
||||
# Since we are sorting by alias postgres needs to know number of column we are sorting
|
||||
order_by = f"{len(formatted_fields)} desc nulls last, {order_by}"
|
||||
_relevance = {"IFNULL": [_relevance_expr, -9999], "as": "_relevance"}
|
||||
formatted_fields.append(_relevance)
|
||||
order_by = f"_relevance desc, {order_by}"
|
||||
|
||||
values = frappe.get_list(
|
||||
doctype,
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ def _get_children(doctype, parent="", ignore_permissions=False, include_disabled
|
|||
)
|
||||
|
||||
if frappe.db.has_column(doctype, "disabled") and not include_disabled:
|
||||
qb = qb.where(Field("disabled").eq(False))
|
||||
|
||||
# used 0 instead of `false` since type of check in postgres is smallint
|
||||
qb = qb.where(Field("disabled").eq(0))
|
||||
# Order by name and execute
|
||||
return qb.orderby("name").run(as_dict=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ class SMTPServer:
|
|||
return self._session
|
||||
|
||||
except smtplib.SMTPAuthenticationError:
|
||||
self.throw_invalid_credentials_exception()
|
||||
self.throw_invalid_credentials_exception(email_account=self.email_account)
|
||||
|
||||
except OSError as e:
|
||||
# Invalid mail server -- due to refusing connection
|
||||
|
|
@ -128,10 +128,17 @@ class SMTPServer:
|
|||
self._session.quit()
|
||||
|
||||
@classmethod
|
||||
def throw_invalid_credentials_exception(cls):
|
||||
def throw_invalid_credentials_exception(cls, email_account=None):
|
||||
original_exception = get_traceback() or "\n"
|
||||
error_message = (
|
||||
_("Please check your email login credentials.") + " " + original_exception.splitlines()[-1]
|
||||
)
|
||||
error_title = _("Invalid Credentials")
|
||||
if email_account:
|
||||
error_title = _("Invalid Credentials for Email Account: {0}").format(email_account)
|
||||
|
||||
frappe.throw(
|
||||
_("Please check your email login credentials.") + " " + original_exception.splitlines()[-1],
|
||||
title=_("Invalid Credentials"),
|
||||
error_message,
|
||||
title=error_title,
|
||||
exc=InvalidEmailCredentials,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ class TestSMTP(IntegrationTestCase):
|
|||
|
||||
def test_get_email_account(self):
|
||||
existing_email_accounts = frappe.get_all(
|
||||
"Email Account", fields=["name", "enable_outgoing", "default_outgoing", "append_to", "use_imap"]
|
||||
"Email Account",
|
||||
fields=["name", "enable_outgoing", "default_outgoing", "append_to", "use_imap"],
|
||||
)
|
||||
unset_details = {"enable_outgoing": 0, "default_outgoing": 0, "append_to": None, "use_imap": 0}
|
||||
for email_account in existing_email_accounts:
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ website_route_rules = [
|
|||
]
|
||||
|
||||
website_redirects = [
|
||||
{"source": r"/app/(.*)", "target": r"/desk/\1"},
|
||||
{"source": r"/app/(.*)", "target": r"/desk/\1", "forward_query_parameters": True},
|
||||
{"source": "/apps", "target": "/desk"},
|
||||
{"source": "/app", "target": "/desk"},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -691,6 +691,14 @@ class BaseDocument:
|
|||
None,
|
||||
)
|
||||
|
||||
def _handle_hash_conflict(self):
|
||||
"""Regenerate hash name in case of collisions"""
|
||||
self.flags.retry_count = (self.flags.retry_count or 0) + 1
|
||||
if self.flags.retry_count >= 5:
|
||||
raise
|
||||
self.name = None
|
||||
return self.db_insert()
|
||||
|
||||
def db_insert(self, ignore_if_duplicate=False):
|
||||
"""INSERT the document (with valid columns) in the database.
|
||||
|
||||
|
|
@ -704,10 +712,17 @@ class BaseDocument:
|
|||
set_new_name(self)
|
||||
|
||||
conflict_handler = ""
|
||||
returning = ""
|
||||
# On postgres we can't implcitly ignore PK collision
|
||||
# So instruct pg to ignore `name` field conflicts
|
||||
if ignore_if_duplicate and frappe.db.db_type == "postgres":
|
||||
if (
|
||||
(ignore_if_duplicate or self.meta.autoname == "hash")
|
||||
and frappe.db.db_type == "postgres"
|
||||
and (self.flags.retry_count or 0) < 5
|
||||
):
|
||||
conflict_handler = "on conflict (name) do nothing"
|
||||
if self.meta.autoname == "hash":
|
||||
returning = "RETURNING name"
|
||||
|
||||
if not self.creation:
|
||||
self.creation = self.modified = now()
|
||||
|
|
@ -722,26 +737,26 @@ class BaseDocument:
|
|||
|
||||
columns = list(d)
|
||||
try:
|
||||
frappe.db.sql(
|
||||
name = frappe.db.sql(
|
||||
"""INSERT INTO `tab{doctype}` ({columns})
|
||||
VALUES ({values}) {conflict_handler}""".format(
|
||||
VALUES ({values}) {conflict_handler} {returning}""".format(
|
||||
doctype=self.doctype,
|
||||
columns=", ".join("`" + c + "`" for c in columns),
|
||||
values=", ".join(["%s"] * len(columns)),
|
||||
conflict_handler=conflict_handler,
|
||||
returning=returning,
|
||||
),
|
||||
list(d.values()),
|
||||
)
|
||||
if (
|
||||
frappe.db.db_type == "postgres" and self.meta.autoname == "hash" and not name
|
||||
): # To avoid a transaction block, we regen in try (pg specific)
|
||||
return self._handle_hash_conflict()
|
||||
except Exception as e:
|
||||
if frappe.db.is_primary_key_violation(e):
|
||||
if self.meta.autoname == "hash":
|
||||
# hash collision? try again
|
||||
self.flags.retry_count = (self.flags.retry_count or 0) + 1
|
||||
if self.flags.retry_count > 5:
|
||||
raise
|
||||
self.name = None
|
||||
self.db_insert()
|
||||
return
|
||||
return self._handle_hash_conflict()
|
||||
|
||||
if not ignore_if_duplicate:
|
||||
frappe.msgprint(
|
||||
|
|
|
|||
|
|
@ -113,9 +113,14 @@ class DatabaseQuery:
|
|||
# if `filters` is a list of strings, its probably fields
|
||||
filters, fields = fields, filters
|
||||
|
||||
# Set fields to the requested field or `name` if none specified
|
||||
if not fields:
|
||||
fields = [pluck or "name"]
|
||||
|
||||
# Handle virtual doctypes before any other processing
|
||||
if is_virtual_doctype(self.doctype):
|
||||
return self._handle_virtual_doctype(
|
||||
fields,
|
||||
filters,
|
||||
or_filters,
|
||||
start,
|
||||
|
|
@ -162,10 +167,6 @@ class DatabaseQuery:
|
|||
if limit is None:
|
||||
limit = page_length
|
||||
|
||||
# Set fields to the requested field or `name` if none specified
|
||||
if not fields:
|
||||
fields = [pluck or "name"]
|
||||
|
||||
# Check if table exists before running query
|
||||
from frappe.model.meta import get_table_columns
|
||||
|
||||
|
|
@ -285,6 +286,7 @@ class DatabaseQuery:
|
|||
|
||||
def _handle_virtual_doctype(
|
||||
self,
|
||||
fields: list[str] | tuple[str, ...] | str | None,
|
||||
filters: dict[str, FilterValue] | FilterValue | list[list | FilterValue] | None,
|
||||
or_filters: dict[str, FilterValue] | FilterValue | list[list | FilterValue] | None,
|
||||
start: int | None,
|
||||
|
|
@ -331,6 +333,7 @@ class DatabaseQuery:
|
|||
|
||||
_page_length = page_length or limit or limit_page_length or 20
|
||||
kwargs = {
|
||||
"fields": fields,
|
||||
"filters": filters,
|
||||
"or_filters": or_filters,
|
||||
"start": start or offset or limit_start or 0,
|
||||
|
|
|
|||
|
|
@ -88,9 +88,6 @@ function make_dialog(frm) {
|
|||
let fieldname = props.field.df.fieldname;
|
||||
let field_option = props.field.df.options;
|
||||
let filters = frm.filter_group.get_filters().map((filter) => {
|
||||
// last element is a boolean which hides the filter hence not required to store in meta
|
||||
filter.pop();
|
||||
|
||||
// filter_group component requires options and frm.set_query requires fieldname so storing both
|
||||
filter[0] = field_option;
|
||||
return filter;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,22 @@
|
|||
<!-- Used as Button & Heading Control -->
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps(["df", "value"]);
|
||||
|
||||
const button_class = computed(() => {
|
||||
const color_map = {
|
||||
Default: "btn-default",
|
||||
Primary: "btn-primary",
|
||||
Info: "btn-info",
|
||||
Success: "btn-success",
|
||||
Warning: "btn-warning",
|
||||
Danger: "btn-danger",
|
||||
};
|
||||
const color = props.df.button_color ?? "Default";
|
||||
|
||||
return `btn btn-xs ${color_map[color] || color_map.Default}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -10,7 +26,7 @@ const props = defineProps(["df", "value"]);
|
|||
<h4 v-if="df.fieldtype == 'Heading'">
|
||||
<slot name="label" />
|
||||
</h4>
|
||||
<button v-else class="btn btn-xs btn-default">
|
||||
<button v-else :class="button_class">
|
||||
<slot name="label" />
|
||||
</button>
|
||||
<slot name="actions" />
|
||||
|
|
|
|||
|
|
@ -5,7 +5,21 @@ frappe.ui.form.ControlButton = class ControlButton extends frappe.ui.form.Contro
|
|||
}
|
||||
make_input() {
|
||||
var me = this;
|
||||
const btn_type = this.df.primary ? "btn-primary" : "btn-default";
|
||||
let btn_type = "btn-default";
|
||||
if (this.df.button_color) {
|
||||
const color_map = {
|
||||
Default: "btn-default",
|
||||
Primary: "btn-primary",
|
||||
Info: "btn-info",
|
||||
Success: "btn-success",
|
||||
Warning: "btn-warning",
|
||||
Danger: "btn-danger",
|
||||
};
|
||||
btn_type = color_map[this.df.button_color] || "btn-default";
|
||||
} else if (this.df.primary) {
|
||||
btn_type = "btn-primary";
|
||||
}
|
||||
|
||||
const btn_size = this.df.btn_size ? `btn-${this.df.btn_size}` : "btn-xs";
|
||||
this.$input = $(
|
||||
`<button
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
|
|||
}
|
||||
}
|
||||
set_formatted_input(value) {
|
||||
super.set_formatted_input();
|
||||
super.set_formatted_input(value);
|
||||
if (!value) return;
|
||||
|
||||
if (!this.title_value_map) {
|
||||
|
|
|
|||
|
|
@ -795,7 +795,13 @@ class FilterArea {
|
|||
fields_dict[fieldname]?.df?.fieldtype == "Link"))
|
||||
) {
|
||||
// standard filter
|
||||
out.promise = out.promise.then(() => fields_dict[fieldname].set_value(value));
|
||||
out.promise = out.promise.then(() => {
|
||||
// Set match type for fields that support it
|
||||
if (fields_dict[fieldname].df) {
|
||||
fields_dict[fieldname].df.match_type = condition;
|
||||
}
|
||||
return fields_dict[fieldname].set_value(value);
|
||||
});
|
||||
} else {
|
||||
// filter out non standard filters
|
||||
out.non_standard_filters.push(filter);
|
||||
|
|
@ -855,7 +861,7 @@ class FilterArea {
|
|||
fields.push({
|
||||
fieldtype: "Data",
|
||||
label: "ID",
|
||||
condition: "like",
|
||||
condition: "=",
|
||||
fieldname: "name",
|
||||
onchange: () => this.debounced_refresh_list_view(),
|
||||
});
|
||||
|
|
@ -910,7 +916,7 @@ class FilterArea {
|
|||
].includes(fieldtype)
|
||||
) {
|
||||
fieldtype = "Data";
|
||||
condition = "like";
|
||||
condition = "=";
|
||||
}
|
||||
if (df.fieldtype == "Select" && df.options) {
|
||||
options = df.options.split("\n");
|
||||
|
|
@ -942,23 +948,109 @@ class FilterArea {
|
|||
|
||||
fields.map((df) => {
|
||||
this.list_view.page.add_field(df, this.standard_filters_wrapper);
|
||||
|
||||
const input_fieldtypes = [
|
||||
"Data",
|
||||
"Text",
|
||||
"Small Text",
|
||||
"Long Text",
|
||||
"Code",
|
||||
"Phone",
|
||||
"Read Only",
|
||||
"Barcode",
|
||||
];
|
||||
|
||||
if (input_fieldtypes.includes(df.fieldtype)) {
|
||||
df.match_type = df.condition || "=";
|
||||
this.filter_field_with_match_type(df);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
filter_field_with_match_type(df) {
|
||||
setTimeout(() => {
|
||||
const field = this.list_view.page.fields_dict[df.fieldname];
|
||||
if (!field || !field.$wrapper) return;
|
||||
|
||||
const $input = field.$wrapper.find("input").first();
|
||||
if (!$input.length || $input.closest(".input-group").length) return;
|
||||
|
||||
const getSymbol = (match_type) => (match_type === "=" ? "=" : "≈");
|
||||
|
||||
$input.wrap('<div class="input-group"></div>');
|
||||
const $inputGroup = $input.parent();
|
||||
|
||||
const $dropdown = $(`
|
||||
<div class="input-group-btn">
|
||||
<button type="button"
|
||||
class="btn btn-default match-type-dropdown-btn"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
${getSymbol(df.match_type || "=")}
|
||||
|
||||
</button>
|
||||
<ul class="dropdown-menu match-type-dropdown-menu dropdown-menu-right">
|
||||
<li class="dropdown-item" data-match-type="=">${__("Equals")}</li>
|
||||
<li class="dropdown-item" data-match-type="like">${__("Like")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
`);
|
||||
|
||||
$inputGroup.append($dropdown);
|
||||
|
||||
$dropdown.find(".dropdown-item").on("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$dropdown.find("button").dropdown("toggle");
|
||||
|
||||
const new_type = $(e.currentTarget).data("match-type");
|
||||
const current_type = field.df.match_type || "=";
|
||||
|
||||
if (new_type === current_type) return;
|
||||
|
||||
field.df.match_type = new_type;
|
||||
$dropdown.find("button").html(`${getSymbol(new_type)}`);
|
||||
|
||||
let value = field.get_value?.();
|
||||
if (new_type === "=" && value) {
|
||||
field.set_value(value.replace(/^%+|%+$/g, ""));
|
||||
}
|
||||
|
||||
this.debounced_refresh_list_view();
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
get_standard_filters() {
|
||||
const filters = [];
|
||||
const fields_dict = this.list_view.page.fields_dict;
|
||||
|
||||
for (let key in fields_dict) {
|
||||
let field = fields_dict[key];
|
||||
let value = field.get_value();
|
||||
if (value) {
|
||||
if (field.df.condition === "like" && !value.includes("%")) {
|
||||
value = "%" + value + "%";
|
||||
let match_type = field.df.match_type || "=";
|
||||
let condition;
|
||||
|
||||
if (match_type === "like") {
|
||||
condition = "like";
|
||||
if (typeof value === "string" && !value.includes("%")) {
|
||||
value = "%" + value + "%";
|
||||
}
|
||||
} else if (match_type === "=") {
|
||||
condition = "=";
|
||||
if (typeof value === "string") {
|
||||
value = value.replace(/^%+|%+$/g, "");
|
||||
}
|
||||
} else {
|
||||
// For special conditions like "descendants of (inclusive)"
|
||||
condition = field.df.condition || match_type;
|
||||
}
|
||||
|
||||
filters.push([
|
||||
field.df.doctype || this.list_view.doctype,
|
||||
field.df.fieldname,
|
||||
field.df.condition || "=",
|
||||
condition,
|
||||
value,
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,13 +33,14 @@
|
|||
<svg class="es-icon es-line icon-xs" aria-hidden="true">
|
||||
<use class="" href="#es-line-down"></use>
|
||||
</svg>
|
||||
{{ __("Filter By") }}
|
||||
<span>{{ __("Filter By") }}</span>
|
||||
</div>
|
||||
|
||||
<div class="list-group-by">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="sidebar-section tags-section">
|
||||
<div class="sidebar-label">
|
||||
<svg class="es-icon es-line icon-xs" aria-hidden="true">
|
||||
|
|
|
|||
|
|
@ -333,7 +333,6 @@ frappe.ui.Filter = class {
|
|||
this.field.df.fieldname,
|
||||
this.get_condition(),
|
||||
this.get_selected_value(),
|
||||
this.hidden,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,37 +31,46 @@ frappe.ui.menu = class ContextMenu {
|
|||
}
|
||||
add_menu_item(item) {
|
||||
const me = this;
|
||||
let item_wrapper = $(`<div class="dropdown-menu-item">
|
||||
<a>
|
||||
<div class="menu-item-icon">
|
||||
${
|
||||
item.icon
|
||||
? frappe.utils.icon(item.icon)
|
||||
: `<img
|
||||
class="logo"
|
||||
src="${item.icon_url}"
|
||||
>`
|
||||
}
|
||||
</div>
|
||||
<span class="menu-item-title">${item.label}</span>
|
||||
<div class="menu-item-icon" style="margin-left:auto">
|
||||
${item.items && item.items.length ? frappe.utils.icon("chevron-right") : ""}
|
||||
</div>
|
||||
|
||||
</a>
|
||||
</div>`);
|
||||
if (!item.url) {
|
||||
item_wrapper.on("click", function () {
|
||||
item.onClick && item.onClick();
|
||||
if (!(item.items && item.items.length)) {
|
||||
me.opts.onItemClick && me.opts.onItemClick(me.opts.parent);
|
||||
me.hide();
|
||||
}
|
||||
});
|
||||
} else if (item.items) {
|
||||
$();
|
||||
let item_wrapper = $(
|
||||
`<div class="dropdown-menu-item"><div class="dropdown-divider documentation-links"></div></div>`
|
||||
);
|
||||
if (item?.is_divider) {
|
||||
item_wrapper = $(
|
||||
`<div class="dropdown-menu-item"><div class="dropdown-divider documentation-links"></div></div>`
|
||||
);
|
||||
} else {
|
||||
$(item_wrapper).find("a").attr("href", item.url);
|
||||
item_wrapper = $(`<div class="dropdown-menu-item">
|
||||
<a>
|
||||
<div class="menu-item-icon" ${!(item.icon || item.icon_url) ? "hidden" : ""}>
|
||||
${
|
||||
item.icon
|
||||
? frappe.utils.icon(item.icon)
|
||||
: `<img
|
||||
class="logo"
|
||||
src="${item.icon_url}"
|
||||
>`
|
||||
}
|
||||
</div>
|
||||
<span class="menu-item-title">${item.label}</span>
|
||||
<div class="menu-item-icon" style="margin-left:auto">
|
||||
${item.items && item.items.length ? frappe.utils.icon("chevron-right") : ""}
|
||||
</div>
|
||||
|
||||
</a>
|
||||
</div>`);
|
||||
if (!item.url) {
|
||||
item_wrapper.on("click", function () {
|
||||
item.onClick && item.onClick();
|
||||
if (!(item.items && item.items.length)) {
|
||||
me.opts.onItemClick && me.opts.onItemClick(me.opts.parent);
|
||||
me.hide();
|
||||
}
|
||||
});
|
||||
} else if (item.items) {
|
||||
$();
|
||||
} else {
|
||||
$(item_wrapper).find("a").attr("href", item.url);
|
||||
}
|
||||
}
|
||||
item_wrapper.appendTo(this.template);
|
||||
if (item.items) {
|
||||
|
|
|
|||
|
|
@ -40,9 +40,40 @@
|
|||
</button>
|
||||
</div>
|
||||
<a class="collapse-sidebar-link">
|
||||
{%= frappe.utils.icon("arrow-left-to-line" , "sm", "", "", "text-ink-gray-7 current-color", true)%}
|
||||
{%= frappe.utils.icon("panel-right-open" , "sm", "", "", "text-ink-gray-7 current-color", true)%}
|
||||
<span> {%= __("Collapse") %} </span>
|
||||
</a>
|
||||
<div class="nav-item dropdown dropdown-navbar-user dropdown-mobile mt-3">
|
||||
<button
|
||||
class="align-center btn-reset flex nav-link"
|
||||
data-toggle="dropdown"
|
||||
aria-label="{{ __("User Menu") }}"
|
||||
>
|
||||
<div> {{ avatar }} </div>
|
||||
<div class="ml-2 avatar-name-email">
|
||||
<span>{{frappe.session.user_fullname}}</span>
|
||||
<span class="text-secondary">{{frappe.session.user_email}}</span>
|
||||
</div>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-left" id="toolbar-user" role="menu">
|
||||
{% for item in navbar_settings.settings_dropdown %}
|
||||
{% var condition = item.condition ? eval(item.condition) : true %}
|
||||
{% if (condition && !item.hidden) { %}
|
||||
{% if (item.route) { %}
|
||||
<a class="dropdown-item" href="{{ item.route }}">
|
||||
{%= __(item.item_label) %}
|
||||
</a>
|
||||
{% } else if (item.action) { %}
|
||||
<button class="btn-reset dropdown-item" onclick="return {{ item.action }}">
|
||||
{%= __(item.item_label) %}
|
||||
</button>
|
||||
{% } else { %}
|
||||
<div class="dropdown-divider"></div>
|
||||
{% } %}
|
||||
{% } %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay" style="z-index: 1021;"></div>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ frappe.ui.Sidebar = class Sidebar {
|
|||
|
||||
prepare() {
|
||||
try {
|
||||
this.add_standard_items();
|
||||
this.sidebar_data = frappe.boot.workspace_sidebar_item[this.workspace_title];
|
||||
this.workspace_sidebar_items = this.sidebar_data.items;
|
||||
if (this.edit_mode) {
|
||||
|
|
@ -135,6 +136,8 @@ frappe.ui.Sidebar = class Sidebar {
|
|||
this.wrapper = $(
|
||||
frappe.render_template("sidebar", {
|
||||
expanded: this.sidebar_expanded,
|
||||
avatar: frappe.avatar(frappe.session.user, "avatar-medium"),
|
||||
navbar_settings: frappe.boot.navbar_settings,
|
||||
})
|
||||
).prependTo("body");
|
||||
this.$sidebar = this.wrapper.find(".sidebar-items");
|
||||
|
|
@ -213,7 +216,6 @@ frappe.ui.Sidebar = class Sidebar {
|
|||
}
|
||||
create_sidebar(items) {
|
||||
this.empty();
|
||||
this.add_standard_items(items);
|
||||
if (items && items.length > 0) {
|
||||
items.forEach((w) => {
|
||||
if (!w.display_depends_on || frappe.utils.eval(w.display_depends_on)) {
|
||||
|
|
@ -238,7 +240,7 @@ frappe.ui.Sidebar = class Sidebar {
|
|||
this.standard_items = [];
|
||||
if (!frappe.is_mobile()) {
|
||||
this.standard_items.push({
|
||||
label: "Search",
|
||||
label: __("Search"),
|
||||
icon: "search",
|
||||
type: "Button",
|
||||
id: "navbar-modal-search",
|
||||
|
|
@ -249,7 +251,7 @@ frappe.ui.Sidebar = class Sidebar {
|
|||
});
|
||||
}
|
||||
this.standard_items.push({
|
||||
label: "Notification",
|
||||
label: __("Notification"),
|
||||
icon: "bell",
|
||||
type: "Button",
|
||||
class: "sidebar-notification hidden",
|
||||
|
|
@ -318,24 +320,26 @@ frappe.ui.Sidebar = class Sidebar {
|
|||
if (this.sidebar_expanded) {
|
||||
this.wrapper.addClass("expanded");
|
||||
// this.sidebar_expanded = false
|
||||
direction = "left";
|
||||
direction = "right";
|
||||
$('[data-toggle="tooltip"]').tooltip("dispose");
|
||||
this.wrapper.find(".avatar-name-email").show();
|
||||
} else {
|
||||
this.wrapper.removeClass("expanded");
|
||||
// this.sidebar_expanded = true
|
||||
direction = "right";
|
||||
direction = "left";
|
||||
$('[data-toggle="tooltip"]').tooltip({
|
||||
boundary: "window",
|
||||
container: "body",
|
||||
trigger: "hover",
|
||||
});
|
||||
this.wrapper.find(".avatar-name-email").hide();
|
||||
}
|
||||
|
||||
localStorage.setItem("sidebar-expanded", this.sidebar_expanded);
|
||||
this.wrapper
|
||||
.find(".body-sidebar .collapse-sidebar-link")
|
||||
.find("use")
|
||||
.attr("href", `#icon-arrow-${direction}-to-line`);
|
||||
.attr("href", `#icon-panel-${direction}-open`);
|
||||
this.sidebar_header.toggle_width(this.sidebar_expanded);
|
||||
$(document).trigger("sidebar-expand", {
|
||||
sidebar_expand: this.sidebar_expanded,
|
||||
|
|
@ -403,9 +407,11 @@ frappe.ui.Sidebar = class Sidebar {
|
|||
if (sidebars.length == 0) {
|
||||
let module_name = router.meta?.module;
|
||||
if (module_name) {
|
||||
frappe.app.sidebar.setup(
|
||||
this.sidebar_module_map[module_name][0] || module_name
|
||||
);
|
||||
let sidebar_title =
|
||||
(this.sidebar_module_map[module_name] &&
|
||||
this.sidebar_module_map[module_name][0]) ||
|
||||
module_name;
|
||||
frappe.app.sidebar.setup(sidebar_title);
|
||||
}
|
||||
} else {
|
||||
if (this.sidebar_title && sidebars.includes(this.workspace_title)) {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ frappe.ui.SidebarHeader = class SidebarHeader {
|
|||
label: "Workspaces",
|
||||
icon: "wallpaper",
|
||||
condition: function () {
|
||||
return me.sibling_workspaces.length > 0;
|
||||
return me.sibling_workspaces && me.sibling_workspaces.length > 0;
|
||||
},
|
||||
items: this.sibling_workspaces,
|
||||
},
|
||||
|
|
@ -42,6 +42,14 @@ frappe.ui.SidebarHeader = class SidebarHeader {
|
|||
},
|
||||
},
|
||||
];
|
||||
if (frappe.boot.desk_settings.notifications) {
|
||||
this.dropdown_items.push({
|
||||
name: "help",
|
||||
label: "Help",
|
||||
icon: "info",
|
||||
items: this.get_help_siblings(),
|
||||
});
|
||||
}
|
||||
this.make();
|
||||
this.setup_app_switcher();
|
||||
this.populate_dropdown_menu();
|
||||
|
|
@ -67,6 +75,51 @@ frappe.ui.SidebarHeader = class SidebarHeader {
|
|||
return sibling_workspaces;
|
||||
}
|
||||
}
|
||||
|
||||
get_help_siblings() {
|
||||
const navbar_settings = frappe.boot.navbar_settings;
|
||||
let help_dropdown_items = [];
|
||||
|
||||
let custom_help_links = this.get_custom_help_links();
|
||||
|
||||
help_dropdown_items = custom_help_links.concat(help_dropdown_items);
|
||||
|
||||
navbar_settings.help_dropdown.forEach((element) => {
|
||||
let dropdown_children = {
|
||||
name: element.name,
|
||||
label: element.item_label,
|
||||
};
|
||||
if (element.item_type === "Route") {
|
||||
dropdown_children.url = element.route;
|
||||
}
|
||||
if (element.item_type === "Action") {
|
||||
dropdown_children.onClick = function () {
|
||||
frappe.utils.eval(element.action);
|
||||
};
|
||||
}
|
||||
help_dropdown_items.push(dropdown_children);
|
||||
});
|
||||
|
||||
return help_dropdown_items;
|
||||
}
|
||||
|
||||
get_custom_help_links() {
|
||||
let route = frappe.get_route_str();
|
||||
let breadcrumbs = route.split("/");
|
||||
|
||||
let links = [];
|
||||
for (let i = 0; i < breadcrumbs.length; i++) {
|
||||
let r = route.split("/", i + 1);
|
||||
let key = r.join("/");
|
||||
let help_links = frappe.help.help_links[key] || [];
|
||||
links = $.merge(links, help_links);
|
||||
}
|
||||
if (links.length) {
|
||||
links.push({ is_divider: true });
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
make() {
|
||||
$(".sidebar-header").remove();
|
||||
$(".sidebar-header-menu").remove();
|
||||
|
|
@ -99,8 +152,14 @@ frappe.ui.SidebarHeader = class SidebarHeader {
|
|||
false,
|
||||
`var(${this.header_bg_color})`
|
||||
);
|
||||
} else {
|
||||
this.header_icon = this.get_default_icon();
|
||||
this.header_icon = `<img src=${this.header_icon}></img>`;
|
||||
}
|
||||
}
|
||||
get_default_icon() {
|
||||
return frappe.boot.app_data[0].app_logo_url;
|
||||
}
|
||||
get_desktop_icon_by_label(title, filters) {
|
||||
if (!filters) {
|
||||
return frappe.boot.desktop_icons.find((f) => f.label === title && f.hidden != 1);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ frappe.ui.sidebar_item.TypeLink = class SidebarItem {
|
|||
this.item = opts.item;
|
||||
this.container = opts.container;
|
||||
this.nested_items = opts.item.nested_items || [];
|
||||
this.workspace_title = $(".body-sidebar").attr("data-title").toLowerCase();
|
||||
this.workspace_title =
|
||||
($(".body-sidebar").attr("data-title") &&
|
||||
$(".body-sidebar").attr("data-title").toLowerCase()) ||
|
||||
frappe.app.sidebar.sidebar_title;
|
||||
this.prepare(opts);
|
||||
this.make();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,21 @@ frappe.ui.TagEditor = class TagEditor {
|
|||
$input.trigger("input");
|
||||
}
|
||||
});
|
||||
$input.on("enter-pressed-in-addtag", function (e) {
|
||||
var value = e.target.value;
|
||||
frappe.call({
|
||||
method: "frappe.desk.doctype.tag.tag.get_tags",
|
||||
args: {
|
||||
doctype: me.frm.doctype,
|
||||
txt: value.toLowerCase(),
|
||||
},
|
||||
callback: function (r) {
|
||||
// Updates input to suggestion value (if any) on <enter>
|
||||
if (r.message.length) $input.val(r.message[0]);
|
||||
$input.trigger("input-selected");
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
get_args(tag) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -38,10 +38,19 @@ frappe.ui.Tags = class {
|
|||
};
|
||||
|
||||
this.$input.keypress((e) => {
|
||||
if (e.which == 13 || e.keyCode == 13) select_tag();
|
||||
if (e.which == 13 || e.keyCode == 13) {
|
||||
// Triggers event when <enter> is pressed
|
||||
this.$input.trigger("enter-pressed-in-addtag");
|
||||
}
|
||||
});
|
||||
this.$input.focusout(select_tag);
|
||||
|
||||
this.$input.on("input-selected", () => {
|
||||
// Adds tag if a input is selected
|
||||
select_tag();
|
||||
this.deactivate();
|
||||
});
|
||||
|
||||
this.$input.on("blur", () => {
|
||||
this.deactivate();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -24,51 +24,6 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item dropdown dropdown-message dropdown-mobile hidden">
|
||||
<button
|
||||
class="btn-reset nav-link notifications-icon text-muted"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="true"
|
||||
>
|
||||
<span>
|
||||
<svg class="es-icon icon-sm"><use href="#es-line-chat-alt"></use></svg>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="vertical-bar d-none d-sm-block"></li>
|
||||
<li class="nav-item dropdown dropdown-help dropdown-mobile d-none d-lg-block">
|
||||
<button
|
||||
class="btn-reset nav-link"
|
||||
data-toggle="dropdown"
|
||||
aria-controls="toolbar-help"
|
||||
aria-label="{{ __("Help Dropdown") }}"
|
||||
>
|
||||
<span>
|
||||
{{ __("Help") }}
|
||||
<svg class="es-icon icon-xs"><use href="#es-line-down"></use></svg>
|
||||
</span>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" id="toolbar-help" role="menu">
|
||||
<div id="help-links"></div>
|
||||
<div class="dropdown-divider documentation-links"></div>
|
||||
{% for item in navbar_settings.help_dropdown %}
|
||||
{% if (!item.hidden) { %}
|
||||
{% if (item.route) { %}
|
||||
<a class="dropdown-item" href="{{ item.route }}">
|
||||
{%= __(item.item_label) %}
|
||||
</a>
|
||||
{% } else if (item.action) { %}
|
||||
<button class="btn-reset dropdown-item" onclick="return {{ item.action }}">
|
||||
{%= __(item.item_label) %}
|
||||
</button>
|
||||
{% } else { %}
|
||||
<div class="dropdown-divider"></div>
|
||||
{% } %}
|
||||
{% } %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item dropdown dropdown-navbar-user dropdown-mobile">
|
||||
<button
|
||||
class="btn-reset nav-link"
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ frappe.ui.toolbar.Toolbar = class {
|
|||
});
|
||||
|
||||
// this.setup_awesomebar();
|
||||
this.setup_help();
|
||||
this.setup_read_only_mode();
|
||||
this.setup_announcement_widget();
|
||||
this.make();
|
||||
|
|
@ -99,87 +98,6 @@ frappe.ui.toolbar.Toolbar = class {
|
|||
}
|
||||
}
|
||||
|
||||
setup_help() {
|
||||
if (!frappe.boot.desk_settings.notifications) {
|
||||
// hide the help section
|
||||
$(".navbar .vertical-bar").removeClass("d-sm-block");
|
||||
$(".dropdown-help").removeClass("d-lg-block");
|
||||
return;
|
||||
}
|
||||
frappe.provide("frappe.help");
|
||||
frappe.help.show_results = show_results;
|
||||
|
||||
this.search = new frappe.search.SearchDialog();
|
||||
frappe.provide("frappe.searchdialog");
|
||||
frappe.searchdialog.search = this.search;
|
||||
|
||||
$(".dropdown-help .dropdown-toggle").on("click", function () {
|
||||
$(".dropdown-help input").focus();
|
||||
});
|
||||
|
||||
$(".dropdown-help .dropdown-menu").on("click", "input, button", function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
$("#input-help").on("keydown", function (e) {
|
||||
if (e.which == 13) {
|
||||
$(this).val("");
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on("page-change", function () {
|
||||
var $help_links = $(".dropdown-help #help-links");
|
||||
$help_links.html("");
|
||||
|
||||
var route = frappe.get_route_str();
|
||||
var breadcrumbs = route.split("/");
|
||||
|
||||
var links = [];
|
||||
for (let i = 0; i < breadcrumbs.length; i++) {
|
||||
var r = route.split("/", i + 1);
|
||||
var key = r.join("/");
|
||||
var help_links = frappe.help.help_links[key] || [];
|
||||
links = $.merge(links, help_links);
|
||||
}
|
||||
|
||||
if (links.length === 0) {
|
||||
$help_links.next().hide();
|
||||
} else {
|
||||
$help_links.next().show();
|
||||
}
|
||||
|
||||
for (let i = 0; i < links.length; i++) {
|
||||
var link = links[i];
|
||||
var url = link.url;
|
||||
$("<a>", {
|
||||
href: url,
|
||||
class: "dropdown-item",
|
||||
text: __(link.label),
|
||||
target: "_blank",
|
||||
}).appendTo($help_links);
|
||||
}
|
||||
|
||||
$(".dropdown-help .dropdown-menu").on("click", "a", show_results);
|
||||
});
|
||||
|
||||
var $result_modal = frappe.get_modal("", "");
|
||||
$result_modal.addClass("help-modal");
|
||||
|
||||
$(document).on("click", ".help-modal a", show_results);
|
||||
|
||||
function show_results(e) {
|
||||
//edit links
|
||||
var href = e.target.href;
|
||||
if (href.indexOf("blob") > 0) {
|
||||
window.open(href, "_blank");
|
||||
}
|
||||
var path = $(e.target).attr("data-path");
|
||||
if (path) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
add_back_button() {
|
||||
if (!frappe.is_mobile()) return;
|
||||
this.navbar = $(".navbar-brand");
|
||||
|
|
|
|||
|
|
@ -99,9 +99,8 @@
|
|||
}
|
||||
|
||||
.avatar-medium {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
.standard-image {
|
||||
@include get_textstyle("base", "regular");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,3 +105,23 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-group > .form-control:not(:last-child) {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
margin-right: 0px !important;
|
||||
}
|
||||
|
||||
.match-type-dropdown-btn {
|
||||
border-top-left-radius: 0 !important;
|
||||
border-bottom-left-radius: 0 !important;
|
||||
height: var(--btn-height);
|
||||
border-left: 1px solid var(--btn-group-border-color);
|
||||
|
||||
&:focus {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
.match-type-dropdown-menu {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,11 +73,8 @@
|
|||
}
|
||||
|
||||
.body-sidebar-bottom {
|
||||
overflow: hidden;
|
||||
padding: 7px;
|
||||
width: 100%;
|
||||
// position: relative;
|
||||
// top:10px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
|
|
@ -141,7 +138,7 @@
|
|||
flex: 0 0 auto;
|
||||
}
|
||||
span {
|
||||
margin-left: 8px;
|
||||
margin-left: 10px;
|
||||
@include truncate();
|
||||
}
|
||||
@include transition(all, 0.3s, cubic-bezier(0.4, 0, 0.2, 1));
|
||||
|
|
@ -157,12 +154,14 @@
|
|||
}
|
||||
|
||||
.standard-items-sections {
|
||||
margin-top: 14px;
|
||||
.navbar-search-bar {
|
||||
background-color: var(--control-bg-on-gray);
|
||||
border-radius: var(--border-radius);
|
||||
padding-right: 7px;
|
||||
.standard-sidebar-item a:hover {
|
||||
background-color: var(--control-bg-on-gray);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
}
|
||||
.dropdown-notifications {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import copy
|
||||
import faulthandler
|
||||
import logging
|
||||
import sys
|
||||
from contextlib import AbstractContextManager, contextmanager
|
||||
from types import MappingProxyType
|
||||
|
||||
|
|
@ -12,6 +14,8 @@ from .unit_test_case import UnitTestCase
|
|||
|
||||
logger = logging.Logger(__file__)
|
||||
|
||||
STUCK_TEST_THRESHOLD = 5 * 60
|
||||
|
||||
|
||||
class IntegrationTestCase(UnitTestCase):
|
||||
"""Integration test class for Frappe tests.
|
||||
|
|
@ -74,6 +78,8 @@ class IntegrationTestCase(UnitTestCase):
|
|||
super().tearDownClass()
|
||||
|
||||
def setUp(self) -> None:
|
||||
faulthandler.dump_traceback_later(STUCK_TEST_THRESHOLD, file=sys.__stderr__)
|
||||
self.addCleanup(faulthandler.cancel_dump_traceback_later)
|
||||
super().setUp()
|
||||
# Add any per-test setup code here
|
||||
|
||||
|
|
|
|||
|
|
@ -403,8 +403,8 @@ class TestDB(IntegrationTestCase):
|
|||
random_field,
|
||||
)
|
||||
self.assertEqual(
|
||||
next(iter(frappe.get_all("ToDo", fields=[{"COUNT": random_field}], limit=1)[0])),
|
||||
"COUNT" if frappe.conf.db_type == "postgres" else f"COUNT(`{random_field}`)",
|
||||
next(iter(frappe.get_all("ToDo", fields=[{"COUNT": random_field}], limit=1, order_by=None)[0])),
|
||||
"count" if frappe.conf.db_type == "postgres" else f"COUNT(`{random_field}`)",
|
||||
)
|
||||
|
||||
# Testing update
|
||||
|
|
|
|||
|
|
@ -848,11 +848,7 @@ class TestDBQuery(IntegrationTestCase):
|
|||
limit=1,
|
||||
as_list=True,
|
||||
)
|
||||
if frappe.conf.db_type == "mariadb":
|
||||
self.assertTrue(len(doctypes[0]) == 2)
|
||||
else:
|
||||
self.assertTrue(len(doctypes[0]) == 3)
|
||||
self.assertTrue(isinstance(doctypes[0][2], datetime.datetime))
|
||||
self.assertTrue(len(doctypes[0]) == 2) # same for pg as well since we order_by None
|
||||
|
||||
def test_field_comparison(self):
|
||||
"""Test DatabaseQuery.execute to test field comparison"""
|
||||
|
|
|
|||
|
|
@ -381,7 +381,6 @@ class TestNaming(IntegrationTestCase):
|
|||
name = parse_naming_series(series, doc=webhook)
|
||||
self.assertTrue(name.startswith("KOOH---"), f"incorrect name generated {name}")
|
||||
|
||||
@run_only_if(db_type_is.MARIADB)
|
||||
def test_hash_collision(self):
|
||||
doctype = new_doctype(autoname="hash").insert().name
|
||||
name = frappe.generate_hash()
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ class TestQuery(IntegrationTestCase):
|
|||
setup_for_tests()
|
||||
|
||||
def test_multiple_tables_in_filters(self):
|
||||
query = "SELECT `tabDocType`.* FROM `tabDocType` LEFT JOIN `tabDocField` ON `tabDocField`.`parent`=`tabDocType`.`name` AND `tabDocField`.`parenttype`='DocType' AND `tabDocField`.`parentfield`='fields' WHERE `tabDocField`.`name` LIKE 'f%' AND `tabDocType`.`parent`='something'"
|
||||
query = query.replace("LIKE", "ILIKE" if frappe.db.db_type == "postgres" else "LIKE")
|
||||
self.assertQueryEqual(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
|
|
@ -77,7 +79,7 @@ class TestQuery(IntegrationTestCase):
|
|||
["DocType", "parent", "=", "something"],
|
||||
],
|
||||
).get_sql(),
|
||||
"SELECT `tabDocType`.* FROM `tabDocType` LEFT JOIN `tabDocField` ON `tabDocField`.`parent`=`tabDocType`.`name` AND `tabDocField`.`parenttype`='DocType' AND `tabDocField`.`parentfield`='fields' WHERE `tabDocField`.`name` LIKE 'f%' AND `tabDocType`.`parent`='something'",
|
||||
query,
|
||||
)
|
||||
|
||||
def test_string_fields(self):
|
||||
|
|
@ -360,13 +362,15 @@ class TestQuery(IntegrationTestCase):
|
|||
"SELECT `tabDocType`.`name` FROM `tabDocType` LEFT JOIN `tabModule Def` ON `tabModule Def`.`name`=`tabDocType`.`module` WHERE `tabModule Def`.`app_name`='frappe'",
|
||||
)
|
||||
|
||||
query = "SELECT `tabDocType`.`name` FROM `tabDocType` LEFT JOIN `tabModule Def` ON `tabModule Def`.`name`=`tabDocType`.`module` WHERE `tabModule Def`.`app_name` LIKE 'frap%'"
|
||||
query = query.replace("LIKE", "ILIKE" if frappe.db.db_type == "postgres" else "LIKE")
|
||||
self.assertQueryEqual(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
fields=["name"],
|
||||
filters={"module.app_name": ("like", "frap%")},
|
||||
).get_sql(),
|
||||
"SELECT `tabDocType`.`name` FROM `tabDocType` LEFT JOIN `tabModule Def` ON `tabModule Def`.`name`=`tabDocType`.`module` WHERE `tabModule Def`.`app_name` LIKE 'frap%'",
|
||||
query,
|
||||
)
|
||||
|
||||
self.assertQueryEqual(
|
||||
|
|
@ -422,141 +426,125 @@ class TestQuery(IntegrationTestCase):
|
|||
def test_or_filters(self):
|
||||
"""Test OR filter conditions."""
|
||||
# Test 1: Basic dict or_filters
|
||||
self.assertEqual(
|
||||
self.assertQueryEqual(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
fields=["name"],
|
||||
or_filters={"name": "User", "module": "Core"},
|
||||
).get_sql(),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `name`='User' OR `module`='Core'".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `name`='User' OR `module`='Core'",
|
||||
)
|
||||
|
||||
# Test 2: List format or_filters
|
||||
self.assertEqual(
|
||||
self.assertQueryEqual(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
fields=["name"],
|
||||
or_filters=[["name", "=", "User"], ["module", "=", "Core"]],
|
||||
).get_sql(),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `name`='User' OR `module`='Core'".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `name`='User' OR `module`='Core'",
|
||||
)
|
||||
|
||||
# Test 3: OR filters with operators
|
||||
self.assertEqual(
|
||||
query = "SELECT `name` FROM `tabDocType` WHERE `name` LIKE 'User%' OR `module` IN ('Core','Custom')"
|
||||
query = query = query.replace("LIKE", "ILIKE" if frappe.db.db_type == "postgres" else "LIKE")
|
||||
self.assertQueryEqual(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
fields=["name"],
|
||||
or_filters={"name": ("like", "User%"), "module": ("in", ["Core", "Custom"])},
|
||||
).get_sql(),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `name` LIKE 'User%' OR `module` IN ('Core','Custom')".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
query,
|
||||
)
|
||||
|
||||
# Test 4: Combining filters (AND) with or_filters (OR)
|
||||
self.assertEqual(
|
||||
self.assertQueryEqual(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
fields=["name"],
|
||||
filters={"issingle": 0},
|
||||
or_filters={"name": "User", "module": "Core"},
|
||||
).get_sql(),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `issingle`=0 AND (`name`='User' OR `module`='Core')".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `issingle`=0 AND (`name`='User' OR `module`='Core')",
|
||||
)
|
||||
|
||||
# Test 5: Multiple AND filters with OR filters
|
||||
self.assertEqual(
|
||||
self.assertQueryEqual(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
fields=["name"],
|
||||
filters={"issingle": 0, "custom": 0},
|
||||
or_filters={"name": "User", "module": "Core"},
|
||||
).get_sql(),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `issingle`=0 AND `custom`=0 AND (`name`='User' OR `module`='Core')".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `issingle`=0 AND `custom`=0 AND (`name`='User' OR `module`='Core')",
|
||||
)
|
||||
|
||||
# Test 6: OR filters with simple list (name IN)
|
||||
self.assertEqual(
|
||||
self.assertQueryEqual(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
or_filters=["User", "Role", "Note"],
|
||||
).get_sql(),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `name` IN ('User','Role','Note')".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `name` IN ('User','Role','Note')",
|
||||
)
|
||||
|
||||
# Test 7: OR filters with greater than and less than
|
||||
self.assertEqual(
|
||||
self.assertQueryEqual(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
fields=["name"],
|
||||
or_filters={"idx": (">", 5), "issingle": ("=", 1)},
|
||||
).get_sql(),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `idx`>5 OR `issingle`=1".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `idx`>5 OR `issingle`=1",
|
||||
)
|
||||
|
||||
# Test 8: OR filters with list including doctype
|
||||
self.assertEqual(
|
||||
self.assertQueryEqual(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
fields=["name"],
|
||||
or_filters=[["DocType", "name", "=", "User"], ["DocType", "name", "=", "Role"]],
|
||||
).get_sql(),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `name`='User' OR `name`='Role'".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `name`='User' OR `name`='Role'",
|
||||
)
|
||||
|
||||
# Test 9: OR filters with != operator
|
||||
self.assertEqual(
|
||||
self.assertQueryEqual(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
fields=["name"],
|
||||
or_filters={"name": ("!=", "User"), "module": ("!=", "Core")},
|
||||
).get_sql(),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `name`<>'User' OR `module`<>'Core'".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `name`<>'User' OR `module`<>'Core'",
|
||||
)
|
||||
|
||||
# Test 10: Empty or_filters should return query without OR conditions
|
||||
self.assertEqual(
|
||||
self.assertQueryEqual(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
fields=["name"],
|
||||
filters={"custom": 0},
|
||||
or_filters={},
|
||||
).get_sql(),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `custom`=0".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `custom`=0",
|
||||
)
|
||||
|
||||
# Test 11: OR filters with not in operator
|
||||
self.assertEqual(
|
||||
self.assertQueryEqual(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
fields=["name"],
|
||||
or_filters={"name": ("not in", ["User", "Role"]), "module": ("=", "Core")},
|
||||
).get_sql(),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `name` NOT IN ('User','Role') OR `module`='Core'".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
"SELECT `name` FROM `tabDocType` WHERE `name` NOT IN ('User','Role') OR `module`='Core'",
|
||||
)
|
||||
|
||||
# Test 12: OR filters with mixed field types
|
||||
self.assertEqual(
|
||||
query = (
|
||||
"SELECT `name`,`module` FROM `tabDocType` WHERE `name` LIKE 'User%' OR `issingle`=1 OR `custom`=0"
|
||||
)
|
||||
query = query = query.replace("LIKE", "ILIKE" if frappe.db.db_type == "postgres" else "LIKE")
|
||||
self.assertQueryEqual(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
fields=["name", "module"],
|
||||
|
|
@ -566,9 +554,7 @@ class TestQuery(IntegrationTestCase):
|
|||
["custom", "=", 0],
|
||||
],
|
||||
).get_sql(),
|
||||
"SELECT `name`,`module` FROM `tabDocType` WHERE `name` LIKE 'User%' OR `issingle`=1 OR `custom`=0".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
query,
|
||||
)
|
||||
|
||||
def test_nested_filters(self):
|
||||
|
|
@ -665,7 +651,11 @@ class TestQuery(IntegrationTestCase):
|
|||
.where(
|
||||
(User.creation > "2023-01-01")
|
||||
& (
|
||||
(User.email.like("%@example.com"))
|
||||
(
|
||||
User.email.ilike("%@example.com")
|
||||
if frappe.db.db_type == "postgres"
|
||||
else User.email.like("%@example.com")
|
||||
)
|
||||
| ((User.first_name.isin(["Admin", "Guest"])) & (User.enabled != 1))
|
||||
)
|
||||
)
|
||||
|
|
@ -710,49 +700,41 @@ class TestQuery(IntegrationTestCase):
|
|||
def test_implicit_join_query(self):
|
||||
self.maxDiff = None
|
||||
|
||||
self.assertEqual(
|
||||
self.assertQueryEqual(
|
||||
frappe.qb.get_query(
|
||||
"Note",
|
||||
filters={"name": "Test Note Title"},
|
||||
fields=["name", "`tabNote Seen By`.`user` as seen_by"],
|
||||
).get_sql(),
|
||||
"SELECT `tabNote`.`name`,`tabNote Seen By`.`user` `seen_by` FROM `tabNote` LEFT JOIN `tabNote Seen By` ON `tabNote Seen By`.`parent`=`tabNote`.`name` AND `tabNote Seen By`.`parenttype`='Note' WHERE `tabNote`.`name`='Test Note Title'".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
"SELECT `tabNote`.`name`,`tabNote Seen By`.`user` `seen_by` FROM `tabNote` LEFT JOIN `tabNote Seen By` ON `tabNote Seen By`.`parent`=`tabNote`.`name` AND `tabNote Seen By`.`parenttype`='Note' WHERE `tabNote`.`name`='Test Note Title'",
|
||||
)
|
||||
|
||||
# output doesn't contain parentfield condition because it can't be inferred
|
||||
self.assertEqual(
|
||||
self.assertQueryEqual(
|
||||
frappe.qb.get_query(
|
||||
"Note",
|
||||
filters={"name": "Test Note Title"},
|
||||
fields=["name", "`tabNote Seen By`.`user` as seen_by", "`tabNote Seen By`.`idx` as idx"],
|
||||
).get_sql(),
|
||||
"SELECT `tabNote`.`name`,`tabNote Seen By`.`user` `seen_by`,`tabNote Seen By`.`idx` `idx` FROM `tabNote` LEFT JOIN `tabNote Seen By` ON `tabNote Seen By`.`parent`=`tabNote`.`name` AND `tabNote Seen By`.`parenttype`='Note' WHERE `tabNote`.`name`='Test Note Title'".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
"SELECT `tabNote`.`name`,`tabNote Seen By`.`user` `seen_by`,`tabNote Seen By`.`idx` `idx` FROM `tabNote` LEFT JOIN `tabNote Seen By` ON `tabNote Seen By`.`parent`=`tabNote`.`name` AND `tabNote Seen By`.`parenttype`='Note' WHERE `tabNote`.`name`='Test Note Title'",
|
||||
)
|
||||
|
||||
# output contains parentfield condition because it can be inferred by "seen_by.user"
|
||||
self.assertEqual(
|
||||
self.assertQueryEqual(
|
||||
frappe.qb.get_query(
|
||||
"Note",
|
||||
filters={"name": "Test Note Title"},
|
||||
fields=["name", "seen_by.user as seen_by", "`tabNote Seen By`.`idx` as idx"],
|
||||
).get_sql(),
|
||||
"SELECT `tabNote`.`name`,`tabNote Seen By`.`user` `seen_by`,`tabNote Seen By`.`idx` `idx` FROM `tabNote` LEFT JOIN `tabNote Seen By` ON `tabNote Seen By`.`parent`=`tabNote`.`name` AND `tabNote Seen By`.`parenttype`='Note' AND `tabNote Seen By`.`parentfield`='seen_by' WHERE `tabNote`.`name`='Test Note Title'".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
"SELECT `tabNote`.`name`,`tabNote Seen By`.`user` `seen_by`,`tabNote Seen By`.`idx` `idx` FROM `tabNote` LEFT JOIN `tabNote Seen By` ON `tabNote Seen By`.`parent`=`tabNote`.`name` AND `tabNote Seen By`.`parenttype`='Note' AND `tabNote Seen By`.`parentfield`='seen_by' WHERE `tabNote`.`name`='Test Note Title'",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.assertQueryEqual(
|
||||
frappe.qb.get_query(
|
||||
"DocType",
|
||||
fields=["name", "module.app_name as app_name"],
|
||||
).get_sql(),
|
||||
"SELECT `tabDocType`.`name`,`tabModule Def`.`app_name` `app_name` FROM `tabDocType` LEFT JOIN `tabModule Def` ON `tabModule Def`.`name`=`tabDocType`.`module`".replace(
|
||||
"`", '"' if frappe.db.db_type == "postgres" else "`"
|
||||
),
|
||||
"SELECT `tabDocType`.`name`,`tabModule Def`.`app_name` `app_name` FROM `tabDocType` LEFT JOIN `tabModule Def` ON `tabModule Def`.`name`=`tabDocType`.`module`",
|
||||
)
|
||||
|
||||
# fields now has strict validation, so this test is not valid anymore
|
||||
|
|
@ -878,7 +860,9 @@ class TestQuery(IntegrationTestCase):
|
|||
if frappe.db.db_type == "mariadb":
|
||||
self.assertIn("IFNULL(`name`,'')='' OR `name` IN ('_Test Blog Post 1','_Test Blog Post')", query)
|
||||
elif frappe.db.db_type == "postgres":
|
||||
self.assertIn("\"name\" IS NULL OR \"name\" IN ('_Test Blog Post 1','_Test Blog Post')", query)
|
||||
self.assertIn(
|
||||
"IFNULL(\"name\",'')='' OR \"name\" IN ('_Test Blog Post 1','_Test Blog Post')", query
|
||||
) # works in pg due to `coalesce` sub during sql execution
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
clear_user_permissions_for_doctype("Test Blog Post", "test2@example.com")
|
||||
|
|
@ -1762,39 +1746,39 @@ class TestQuery(IntegrationTestCase):
|
|||
# Test simple addition
|
||||
query = frappe.qb.get_query("User", fields=[{"ADD": [1, 2], "as": "sum_result"}])
|
||||
sql = query.get_sql()
|
||||
self.assertIn("1+2 `sum_result`", sql)
|
||||
self.assertIn(self.normalize_sql("1+2 `sum_result`"), sql)
|
||||
|
||||
# Test simple subtraction
|
||||
query = frappe.qb.get_query("User", fields=[{"SUB": [10, 5], "as": "diff_result"}])
|
||||
sql = query.get_sql()
|
||||
self.assertIn("10-5 `diff_result`", sql)
|
||||
self.assertIn(self.normalize_sql("10-5 `diff_result`"), sql)
|
||||
|
||||
# Test simple multiplication
|
||||
query = frappe.qb.get_query("User", fields=[{"MUL": [3, 4], "as": "prod_result"}])
|
||||
sql = query.get_sql()
|
||||
self.assertIn("3*4 `prod_result`", sql)
|
||||
self.assertIn(self.normalize_sql("3*4 `prod_result`"), sql)
|
||||
|
||||
# Test simple division
|
||||
query = frappe.qb.get_query("User", fields=[{"DIV": [10, 2], "as": "div_result"}])
|
||||
sql = query.get_sql()
|
||||
self.assertIn("10/2 `div_result`", sql)
|
||||
self.assertIn(self.normalize_sql("10/2 `div_result`"), sql)
|
||||
|
||||
# Test operator with field names
|
||||
query = frappe.qb.get_query("User", fields=[{"ADD": ["enabled", "login_after"], "as": "field_sum"}])
|
||||
sql = query.get_sql()
|
||||
self.assertIn("`enabled`+`login_after` `field_sum`", sql)
|
||||
self.assertIn(self.normalize_sql("`enabled`+`login_after` `field_sum`"), sql)
|
||||
|
||||
# Test nested operators
|
||||
query = frappe.qb.get_query("User", fields=[{"ADD": [{"MUL": [2, 3]}, 4], "as": "nested_result"}])
|
||||
sql = query.get_sql()
|
||||
self.assertIn("2*3+4 `nested_result`", sql)
|
||||
self.assertIn(self.normalize_sql("2*3+4 `nested_result`"), sql)
|
||||
|
||||
# Test operator with function - NULLIF
|
||||
query = frappe.qb.get_query(
|
||||
"User", fields=[{"DIV": [1, {"NULLIF": ["enabled", 0]}], "as": "safe_div"}]
|
||||
)
|
||||
sql = query.get_sql()
|
||||
self.assertIn("1/NULLIF(`enabled`,0) `safe_div`", sql)
|
||||
self.assertIn(self.normalize_sql("1/NULLIF(`enabled`,0) `safe_div`"), self.normalize_sql(sql))
|
||||
|
||||
# Test complex nested expression: (1 / NULLIF(value, 0))
|
||||
query = frappe.qb.get_query(
|
||||
|
|
@ -1805,8 +1789,8 @@ class TestQuery(IntegrationTestCase):
|
|||
],
|
||||
)
|
||||
sql = query.get_sql()
|
||||
self.assertIn("`name`", sql)
|
||||
self.assertIn("1/NULLIF(`enabled`,0) `inverse`", sql)
|
||||
self.assertIn(self.normalize_sql("`name`"), sql)
|
||||
self.assertIn(self.normalize_sql("1/NULLIF(`enabled`,0) `inverse`"), self.normalize_sql(sql))
|
||||
|
||||
# Test operator with LOCATE function (search relevance pattern)
|
||||
query = frappe.qb.get_query(
|
||||
|
|
@ -1817,7 +1801,10 @@ class TestQuery(IntegrationTestCase):
|
|||
],
|
||||
)
|
||||
sql = query.get_sql()
|
||||
self.assertIn("1/NULLIF(LOCATE('test',`name`),0) `relevance`", sql)
|
||||
self.assertIn(
|
||||
self.normalize_sql("1/NULLIF(LOCATE('test',`name`),0) `relevance`"),
|
||||
self.normalize_sql(sql),
|
||||
)
|
||||
|
||||
# Test multiple operators in fields
|
||||
query = frappe.qb.get_query(
|
||||
|
|
@ -1829,9 +1816,9 @@ class TestQuery(IntegrationTestCase):
|
|||
],
|
||||
)
|
||||
sql = query.get_sql()
|
||||
self.assertIn("`name`", sql)
|
||||
self.assertIn("`enabled`+1 `enabled_plus_one`", sql)
|
||||
self.assertIn("`enabled`*2 `enabled_times_two`", sql)
|
||||
self.assertIn(self.normalize_sql("`name`"), sql)
|
||||
self.assertIn(self.normalize_sql("`enabled`+1 `enabled_plus_one`"), sql)
|
||||
self.assertIn(self.normalize_sql("`enabled`*2 `enabled_times_two`"), sql)
|
||||
|
||||
# Test operator without alias
|
||||
query = frappe.qb.get_query("User", fields=[{"ADD": [1, 1]}])
|
||||
|
|
@ -1898,9 +1885,12 @@ class TestQuery(IntegrationTestCase):
|
|||
)
|
||||
|
||||
sql = query.get_sql()
|
||||
self.assertIn("GROUP BY `created_date`", sql)
|
||||
self.assertIn("ORDER BY `created_date`", sql)
|
||||
self.assertIn("`creation` `created_date`", sql)
|
||||
self.assertIn(self.normalize_sql("GROUP BY `created_date`"), self.normalize_sql(sql))
|
||||
if (
|
||||
frappe.db.db_type != "postgres"
|
||||
): # since Postgres requires fields in Order by to be grouped or aggregated, order by is dropped
|
||||
self.assertIn(self.normalize_sql("ORDER BY `created_date`"), self.normalize_sql(sql))
|
||||
self.assertIn(self.normalize_sql("`creation` `created_date`"), self.normalize_sql(sql))
|
||||
|
||||
def test_field_alias_permission_check(self):
|
||||
query = frappe.qb.get_query(
|
||||
|
|
@ -1910,7 +1900,7 @@ class TestQuery(IntegrationTestCase):
|
|||
)
|
||||
sql = query.get_sql()
|
||||
# If we get here without PermissionError, the test passes
|
||||
self.assertIn("GROUP BY `created_date`", sql)
|
||||
self.assertIn(self.normalize_sql("GROUP BY `created_date`"), self.normalize_sql(sql))
|
||||
|
||||
|
||||
# This function is used as a permission query condition hook
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ class CDPSocketClient:
|
|||
|
||||
async def _disconnect(self):
|
||||
try:
|
||||
if self.connection and not self.connection.closed:
|
||||
if self.connection:
|
||||
await self.connection.close()
|
||||
self.connection = None
|
||||
except Exception:
|
||||
|
|
|
|||
|
|
@ -176,7 +176,10 @@ class UserPermissions:
|
|||
self.can_read += self.can_write
|
||||
|
||||
self.shared = frappe.get_all(
|
||||
"DocShare", {"user": self.name, "read": 1}, distinct=True, pluck="share_doctype"
|
||||
"DocShare",
|
||||
{"user": self.name, "read": 1},
|
||||
distinct=True,
|
||||
pluck="share_doctype",
|
||||
)
|
||||
self.can_read = list(set(self.can_read + self.shared))
|
||||
self.all_read += self.can_read
|
||||
|
|
|
|||
|
|
@ -559,7 +559,7 @@ def build_response(path, data, http_status_code, headers: dict | None = None):
|
|||
|
||||
if headers:
|
||||
for key, val in headers.items():
|
||||
response.headers[key] = cstr(cstr(val).encode("ascii", errors="xmlcharrefreplace"))
|
||||
response.headers[key] = cstr(cstr(val).encode("utf-8", errors="xmlcharrefreplace"))
|
||||
|
||||
return response
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue