diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml
index 01b5407489..eb775e01cd 100644
--- a/.github/workflows/linters.yml
+++ b/.github/workflows/linters.yml
@@ -80,7 +80,20 @@ jobs:
- uses: actions/setup-python@v4
with:
python-version: '3.10'
+
- uses: actions/checkout@v3
+
+ - name: Cache pip
+ uses: actions/cache@v3
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-
+ ${{ runner.os }}-
+
- run: |
pip install pip-audit
- pip-audit ${GITHUB_WORKSPACE}
+ cd ${GITHUB_WORKSPACE}
+ sed -i '/dropbox/d' pyproject.toml # Remove dropbox temporarily https://github.com/dropbox/dropbox-sdk-python/pull/456
+ pip-audit .
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4b3ea6d1ea..0c6bbe8ec9 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -48,8 +48,8 @@ repos:
)$
- - repo: https://github.com/timothycrosley/isort
- rev: 5.9.1
+ - repo: https://github.com/PyCQA/isort
+ rev: 5.12.0
hooks:
- id: isort
diff --git a/cypress/integration/control_color.js b/cypress/integration/control_color.js
index aa3a45eed8..e97dbe0f06 100644
--- a/cypress/integration/control_color.js
+++ b/cypress/integration/control_color.js
@@ -26,7 +26,7 @@ context("Control Color", () => {
//Checking if the css attribute is correct
cy.get(".color-map").should("have.css", "color", "rgb(79, 157, 217)");
- cy.get(".hue-map").should("have.css", "color", "rgb(0, 145, 255)");
+ cy.get(".hue-map").should("have.css", "color", "rgb(0, 144, 255)");
//Checking if the correct color is being selected
cy.get("@dialog").then((dialog) => {
diff --git a/cypress/integration/control_link.js b/cypress/integration/control_link.js
index a5281d9b09..d3462492f6 100644
--- a/cypress/integration/control_link.js
+++ b/cypress/integration/control_link.js
@@ -229,19 +229,15 @@ context("Control Link", () => {
);
cy.reload();
cy.new_form("ToDo");
- cy.fill_field("description", "new", "Text Editor");
- cy.intercept("POST", "/api/method/frappe.desk.form.save.savedocs").as("save_form");
- cy.findByRole("button", { name: "Save" }).click();
- cy.wait("@save_form");
+ cy.fill_field("description", "new", "Text Editor").wait(200);
+ cy.save();
cy.get(".frappe-control[data-fieldname=assigned_by_full_name] .control-value").should(
"contain",
"Administrator"
);
// if user clears default value explicitly, system should not reset default again
cy.get_field("assigned_by").clear().blur();
- cy.intercept("POST", "/api/method/frappe.desk.form.save.savedocs").as("save_form");
- cy.findByRole("button", { name: "Save" }).click();
- cy.wait("@save_form");
+ cy.save();
cy.get_field("assigned_by").should("have.value", "");
cy.get(".frappe-control[data-fieldname=assigned_by_full_name] .control-value").should(
"contain",
diff --git a/cypress/integration/folder_navigation.js b/cypress/integration/folder_navigation.js
index 60fa46bc88..c5b3a44f0d 100644
--- a/cypress/integration/folder_navigation.js
+++ b/cypress/integration/folder_navigation.js
@@ -10,7 +10,7 @@ context("Folder Navigation", () => {
cy.get(".filter-selector > .btn").findByText("1 filter").click();
cy.findByRole("button", { name: "Clear Filters" }).click();
cy.get(".filter-action-buttons > .text-muted").findByText("+ Add a Filter").click();
- cy.get(".fieldname-select-area > .awesomplete > .form-control").type("Fol{enter}");
+ cy.get(".fieldname-select-area > .awesomplete > .form-control:last").type("Fol{enter}");
cy.get(
".filter-field > .form-group > .link-field > .awesomplete > .input-with-feedback"
).type("Home{enter}");
diff --git a/cypress/integration/form.js b/cypress/integration/form.js
index fa0d758223..8186647a14 100644
--- a/cypress/integration/form.js
+++ b/cypress/integration/form.js
@@ -26,6 +26,11 @@ context("Form", () => {
});
});
+ beforeEach(() => {
+ cy.login();
+ cy.visit("/app/website");
+ });
+
it("create a new form", () => {
cy.visit("/app/todo/new");
cy.get_field("description", "Text Editor")
@@ -172,4 +177,57 @@ context("Form", () => {
send_welcome_email: 0,
});
});
+
+ it("update docfield property using set_df_property in child table", () => {
+ cy.visit("/app/contact/Test Form Contact 1");
+ cy.window()
+ .its("cur_frm")
+ .then((frm) => {
+ cy.get('.frappe-control[data-fieldname="phone_nos"]').as("table");
+
+ // set property before form_render event of child table
+ cy.get("@table")
+ .find('[data-idx="1"]')
+ .invoke("attr", "data-name")
+ .then((cdn) => {
+ frm.set_df_property(
+ "phone_nos",
+ "hidden",
+ 1,
+ "Contact Phone",
+ "is_primary_phone",
+ cdn
+ );
+ });
+
+ cy.get("@table").find('[data-idx="1"] .edit-grid-row').click();
+ cy.get(".grid-row-open").as("table-form");
+ cy.get("@table-form")
+ .find('.frappe-control[data-fieldname="is_primary_phone"]')
+ .should("be.hidden");
+ cy.get("@table-form").find(".grid-footer-toolbar").click();
+
+ // set property on form_render event of child table
+ cy.get("@table").find('[data-idx="1"] .edit-grid-row').click();
+ cy.get("@table")
+ .find('[data-idx="1"]')
+ .invoke("attr", "data-name")
+ .then((cdn) => {
+ frm.set_df_property(
+ "phone_nos",
+ "hidden",
+ 0,
+ "Contact Phone",
+ "is_primary_phone",
+ cdn
+ );
+ });
+
+ cy.get(".grid-row-open").as("table-form");
+ cy.get("@table-form")
+ .find('.frappe-control[data-fieldname="is_primary_phone"]')
+ .should("be.visible");
+ cy.get("@table-form").find(".grid-footer-toolbar").click();
+ });
+ });
});
diff --git a/cypress/integration/navigation.js b/cypress/integration/navigation.js
index 2302296f23..cf1b5dc89d 100644
--- a/cypress/integration/navigation.js
+++ b/cypress/integration/navigation.js
@@ -18,6 +18,7 @@ context("Navigation", () => {
it.only("Navigate to previous page after login", () => {
cy.visit("/app/todo");
cy.get(".page-head").findByTitle("To Do").should("be.visible");
+ cy.clear_filters();
cy.request("/api/method/logout");
cy.reload().as("reload");
cy.get("@reload").get(".page-card .btn-primary").contains("Login").click();
diff --git a/cypress/integration/view_routing.js b/cypress/integration/view_routing.js
index 1e3b841c79..9267974154 100644
--- a/cypress/integration/view_routing.js
+++ b/cypress/integration/view_routing.js
@@ -103,8 +103,9 @@ context("View", () => {
});
it("Route to File View", () => {
+ cy.intercept("POST", "/api/method/frappe.desk.reportview.get").as("list_loaded");
cy.visit("app/file");
- cy.wait(500);
+ cy.wait("@list_loaded");
cy.window()
.its("cur_list")
.then((list) => {
@@ -113,7 +114,7 @@ context("View", () => {
});
cy.visit("app/file/view/home/Attachments");
- cy.wait(500);
+ cy.wait("@list_loaded");
cy.window()
.its("cur_list")
.then((list) => {
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 0a25ff5cab..c067974d9f 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -285,7 +285,7 @@ Cypress.Commands.add("get_open_dialog", () => {
Cypress.Commands.add("save", () => {
cy.intercept("/api/method/frappe.desk.form.save.savedocs").as("save_call");
- cy.get(`button[data-label="Save"]:visible`).click({ scrollBehavior: "top", force: true });
+ cy.get(`.page-container:visible button[data-label="Save"]`).click({ force: true });
cy.wait("@save_call");
});
Cypress.Commands.add("hide_dialog", () => {
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 7cffb9a512..e32b04dccb 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -238,7 +238,6 @@ def init(site: str, sites_path: str = ".", new_site: bool = False) -> None:
local.jenv = None
local.jloader = None
local.cache = {}
- local.document_cache = {}
local.form_dict = _dict()
local.preload_assets = {"style": [], "script": []}
local.session = _dict()
@@ -1075,25 +1074,10 @@ def set_value(doctype, docname, fieldname, value=None):
def get_cached_doc(*args, **kwargs) -> "Document":
- def _respond(doc, from_redis=False):
- if isinstance(doc, dict):
- local.document_cache[key] = doc = get_doc(doc)
-
- elif from_redis:
- local.document_cache[key] = doc
-
+ if (key := can_cache_doc(args)) and (doc := cache().hget("document_cache", key)):
return doc
- if key := can_cache_doc(args):
- # local cache - has "ready" `Document` objects
- if doc := local.document_cache.get(key):
- return _respond(doc)
-
- # redis cache
- if doc := cache().hget("document_cache", key):
- return _respond(doc, True)
-
- # Not found in local/redis, fetch from DB
+ # Not found in cache, fetch from DB
doc = get_doc(*args, **kwargs)
# Store in cache
@@ -1106,14 +1090,7 @@ def get_cached_doc(*args, **kwargs) -> "Document":
def _set_document_in_cache(key: str, doc: "Document") -> None:
- local.document_cache[key] = doc
-
- # Avoid setting in local.cache since we're already using local.document_cache above
- # Try pickling the doc object as-is first, else fallback to doc.as_dict()
- try:
- cache().hset("document_cache", key, doc, cache_locally=False)
- except Exception:
- cache().hset("document_cache", key, doc.as_dict(), cache_locally=False)
+ cache().hset("document_cache", key, doc)
def can_cache_doc(args) -> str | None:
@@ -1139,12 +1116,11 @@ def get_document_cache_key(doctype: str, name: str):
def clear_document_cache(doctype, name):
cache().hdel("last_modified", doctype)
- key = get_document_cache_key(doctype, name)
- if key in local.document_cache:
- del local.document_cache[key]
- cache().hdel("document_cache", key)
+ cache().hdel("document_cache", get_document_cache_key(doctype, name))
+
if doctype == "System Settings" and hasattr(local, "system_settings"):
delattr(local, "system_settings")
+
if doctype == "Website Settings" and hasattr(local, "website_settings"):
delattr(local, "website_settings")
diff --git a/frappe/boot.py b/frappe/boot.py
index 31e101aedc..de3753f754 100644
--- a/frappe/boot.py
+++ b/frappe/boot.py
@@ -3,15 +3,16 @@
"""
bootstrap client session
"""
-
import frappe
import frappe.defaults
import frappe.desk.desk_page
from frappe.core.doctype.navbar_settings.navbar_settings import get_app_logo, get_navbar_settings
+from frappe.database.utils import Query
from frappe.desk.doctype.route_history.route_history import frequently_visited_links
from frappe.desk.form.load import get_meta_bundle
from frappe.email.inbox import get_email_accounts
from frappe.model.base_document import get_controller
+from frappe.model.db_query import DatabaseQuery
from frappe.query_builder import DocType
from frappe.query_builder.functions import Count
from frappe.query_builder.terms import ParameterizedValueWrapper, SubQuery
@@ -169,6 +170,7 @@ def get_user_pages_or_reports(parent, cache=False):
parentTable = DocType(parent)
# get pages or reports set on custom role
+ # must end in a WHERE clause for `_run_with_permission_query`
pages_with_custom_roles = (
frappe.qb.from_(customRole)
.from_(hasRole)
@@ -182,7 +184,8 @@ def get_user_pages_or_reports(parent, cache=False):
& (customRole[parent.lower()].isnotnull())
& (hasRole.role.isin(roles))
)
- ).run(as_dict=True)
+ )
+ pages_with_custom_roles = _run_with_permission_query(pages_with_custom_roles, parent)
for p in pages_with_custom_roles:
has_role[p.name] = {"modified": p.modified, "title": p.title, "ref_doctype": p.ref_doctype}
@@ -193,6 +196,7 @@ def get_user_pages_or_reports(parent, cache=False):
.where(customRole[parent.lower()].isnotnull())
)
+ # must end in a WHERE clause for `_run_with_permission_query`
pages_with_standard_roles = (
frappe.qb.from_(hasRole)
.from_(parentTable)
@@ -208,7 +212,7 @@ def get_user_pages_or_reports(parent, cache=False):
if parent == "Report":
pages_with_standard_roles = pages_with_standard_roles.where(report.disabled == 0)
- pages_with_standard_roles = pages_with_standard_roles.run(as_dict=True)
+ pages_with_standard_roles = _run_with_permission_query(pages_with_standard_roles, parent)
for p in pages_with_standard_roles:
if p.name not in has_role:
@@ -222,12 +226,13 @@ def get_user_pages_or_reports(parent, cache=False):
# pages with no role are allowed
if parent == "Page":
-
+ # must end in a WHERE clause for `_run_with_permission_query`
pages_with_no_roles = (
frappe.qb.from_(parentTable)
.select(parentTable.name, parentTable.modified, *columns)
.where(no_of_roles == 0)
- ).run(as_dict=True)
+ )
+ pages_with_no_roles = _run_with_permission_query(pages_with_no_roles, parent)
for p in pages_with_no_roles:
if p.name not in has_role:
@@ -248,6 +253,17 @@ def get_user_pages_or_reports(parent, cache=False):
return has_role
+def _run_with_permission_query(query: "Query", doctype: str) -> list[dict]:
+ """
+ Adds Permission Query (Server Script) conditions and runs/executes modified query
+ Note: Works only if 'WHERE' is the last clause in the query
+ """
+ permission_query = DatabaseQuery(doctype, frappe.session.user).get_permission_query_conditions()
+ if permission_query and frappe.session.user != "Administrator":
+ return frappe.db.sql(f"{query} AND {permission_query}", as_dict=True)
+ return query.run(as_dict=True)
+
+
def load_translations(bootinfo):
bootinfo["lang"] = frappe.lang
bootinfo["__messages"] = get_messages_for_boot()
diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py
index 9c9f081c60..24a4c6a271 100644
--- a/frappe/cache_manager.py
+++ b/frappe/cache_manager.py
@@ -127,8 +127,6 @@ def clear_doctype_cache(doctype=None):
for key in ("is_table", "doctype_modules", "document_cache"):
cache.delete_value(key)
- frappe.local.document_cache = {}
-
def clear_single(dt):
for name in doctype_cache_keys:
cache.hdel(name, dt)
diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py
index 280e656f1c..5ec0b54828 100644
--- a/frappe/commands/utils.py
+++ b/frappe/commands/utils.py
@@ -17,6 +17,7 @@ DATA_IMPORT_DEPRECATION = (
"[DEPRECATED] The `import-csv` command used 'Data Import Legacy' which has been deprecated.\n"
"Use `data-import` command instead to import data via 'Data Import'."
)
+EXTRA_ARGS_CTX = {"ignore_unknown_options": True, "allow_extra_args": True}
@click.command("build")
@@ -485,9 +486,10 @@ def bulk_rename(context, doctype, path):
frappe.destroy()
-@click.command("db-console")
+@click.command("db-console", context_settings=EXTRA_ARGS_CTX)
+@click.argument("extra_args", nargs=-1)
@pass_context
-def database(context):
+def database(context, extra_args):
"""
Enter into the Database console for given site.
"""
@@ -496,14 +498,18 @@ def database(context):
raise SiteNotSpecifiedError
frappe.init(site=site)
if not frappe.conf.db_type or frappe.conf.db_type == "mariadb":
- _mariadb()
+ _mariadb(extra_args=extra_args)
elif frappe.conf.db_type == "postgres":
- _psql()
+ _psql(extra_args=extra_args)
-@click.command("mariadb")
+@click.command(
+ "mariadb",
+ context_settings=EXTRA_ARGS_CTX,
+)
+@click.argument("extra_args", nargs=-1)
@pass_context
-def mariadb(context):
+def mariadb(context, extra_args):
"""
Enter into mariadb console for a given site.
"""
@@ -511,21 +517,22 @@ def mariadb(context):
if not site:
raise SiteNotSpecifiedError
frappe.init(site=site)
- _mariadb()
+ _mariadb(extra_args=extra_args)
-@click.command("postgres")
+@click.command("postgres", context_settings=EXTRA_ARGS_CTX)
+@click.argument("extra_args", nargs=-1)
@pass_context
-def postgres(context):
+def postgres(context, extra_args):
"""
Enter into postgres console for a given site.
"""
site = get_site(context)
frappe.init(site=site)
- _psql()
+ _psql(extra_args=extra_args)
-def _mariadb():
+def _mariadb(extra_args=None):
from frappe.database.mariadb.database import MariaDBDatabase
mysql = which("mysql")
@@ -543,10 +550,12 @@ def _mariadb():
"--safe-updates",
"-A",
]
+ if extra_args:
+ command += list(extra_args)
os.execv(mysql, command)
-def _psql():
+def _psql(extra_args=None):
psql = which("psql")
host = frappe.conf.db_host or "127.0.0.1"
@@ -554,7 +563,10 @@ def _psql():
env = os.environ.copy()
env["PGPASSWORD"] = frappe.conf.db_password
conn_string = f"postgresql://{frappe.conf.db_name}@{host}:{port}/{frappe.conf.db_name}"
- subprocess.run([psql, conn_string], check=True, env=env)
+ psql_cmd = [psql, conn_string]
+ if extra_args:
+ psql_cmd = psql_cmd + list(extra_args)
+ subprocess.run(psql_cmd, check=True, env=env)
@click.command("jupyter")
@@ -1071,6 +1083,8 @@ def set_config(context, key, value, global_=False, parse=False, as_dict=False):
common_site_config_path = os.path.join(sites_path, "common_site_config.json")
update_site_config(key, value, validate=False, site_config_path=common_site_config_path)
else:
+ if not context.sites:
+ raise SiteNotSpecifiedError
for site in context.sites:
frappe.init(site=site)
update_site_config(key, value, validate=False)
diff --git a/frappe/contacts/doctype/address_template/address_template.jinja b/frappe/contacts/doctype/address_template/address_template.jinja
new file mode 100644
index 0000000000..65ea58eb21
--- /dev/null
+++ b/frappe/contacts/doctype/address_template/address_template.jinja
@@ -0,0 +1,10 @@
+{{ address_line1 }}
+{% if address_line2 %}{{ address_line2 }}
{% endif -%}
+{{ city }}
+{% if state %}{{ state }}
{% endif -%}
+{% if pincode %}{{ pincode }}
{% endif -%}
+{{ country }}
+
+{% if phone %}{{ _("Phone") }}: {{ phone }}
{% endif -%}
+{% if fax %}{{ _("Fax") }}: {{ fax }}
{% endif -%}
+{% if email_id %}{{ _("Email") }}: {{ email_id }}
{% endif -%}
diff --git a/frappe/contacts/doctype/address_template/address_template.py b/frappe/contacts/doctype/address_template/address_template.py
index a8806b336b..a33115b105 100644
--- a/frappe/contacts/doctype/address_template/address_template.py
+++ b/frappe/contacts/doctype/address_template/address_template.py
@@ -4,52 +4,36 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import cint
from frappe.utils.jinja import validate_template
class AddressTemplate(Document):
def validate(self):
+ validate_template(self.template)
+
if not self.template:
self.template = get_default_address_template()
- self.defaults = frappe.db.get_values(
- "Address Template", {"is_default": 1, "name": ("!=", self.name)}
- )
- if not self.is_default:
- if not self.defaults:
- self.is_default = 1
- if cint(frappe.db.get_single_value("System Settings", "setup_complete")):
- frappe.msgprint(_("Setting this Address Template as default as there is no other default"))
-
- validate_template(self.template)
+ if not self.is_default and not self._get_previous_default():
+ self.is_default = 1
+ if frappe.db.get_single_value("System Settings", "setup_complete"):
+ frappe.msgprint(_("Setting this Address Template as default as there is no other default"))
def on_update(self):
- if self.is_default and self.defaults:
- for d in self.defaults:
- frappe.db.set_value("Address Template", d[0], "is_default", 0)
+ if self.is_default and (previous_default := self._get_previous_default()):
+ frappe.db.set_value("Address Template", previous_default, "is_default", 0)
def on_trash(self):
if self.is_default:
frappe.throw(_("Default Address Template cannot be deleted"))
+ def _get_previous_default(self) -> str | None:
+ return frappe.db.get_value("Address Template", {"is_default": 1, "name": ("!=", self.name)})
+
@frappe.whitelist()
-def get_default_address_template():
- """Get default address template (translated)"""
- return (
- """{{ address_line1 }}
{% if address_line2 %}{{ address_line2 }}
{% endif -%}\
-{{ city }}
-{% if state %}{{ state }}
{% endif -%}
-{% if pincode %}{{ pincode }}
{% endif -%}
-{{ country }}
-{% if phone %}"""
- + _("Phone")
- + """: {{ phone }}
{% endif -%}
-{% if fax %}"""
- + _("Fax")
- + """: {{ fax }}
{% endif -%}
-{% if email_id %}"""
- + _("Email")
- + """: {{ email_id }}
{% endif -%}"""
- )
+def get_default_address_template() -> str:
+ """Return the default address template."""
+ from pathlib import Path
+
+ return (Path(__file__).parent / "address_template.jinja").read_text()
diff --git a/frappe/contacts/doctype/address_template/test_address_template.py b/frappe/contacts/doctype/address_template/test_address_template.py
index ee45ce98f8..c3c5b544d6 100644
--- a/frappe/contacts/doctype/address_template/test_address_template.py
+++ b/frappe/contacts/doctype/address_template/test_address_template.py
@@ -1,39 +1,39 @@
# Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE
import frappe
+from frappe.contacts.doctype.address_template.address_template import get_default_address_template
from frappe.tests.utils import FrappeTestCase
+from frappe.utils.jinja import validate_template
class TestAddressTemplate(FrappeTestCase):
- def setUp(self):
- self.make_default_address_template()
+ def setUp(self) -> None:
+ frappe.db.delete("Address Template", {"country": "India"})
+ frappe.db.delete("Address Template", {"country": "Brazil"})
+
+ def test_default_address_template(self):
+ validate_template(get_default_address_template())
def test_default_is_unset(self):
- a = frappe.get_doc("Address Template", "India")
- a.is_default = 1
- a.save()
+ frappe.get_doc({"doctype": "Address Template", "country": "India", "is_default": 1}).insert()
- b = frappe.get_doc("Address Template", "Brazil")
- b.is_default = 1
- b.save()
+ self.assertEqual(frappe.db.get_value("Address Template", "India", "is_default"), 1)
+
+ frappe.get_doc({"doctype": "Address Template", "country": "Brazil", "is_default": 1}).insert()
self.assertEqual(frappe.db.get_value("Address Template", "India", "is_default"), 0)
+ self.assertEqual(frappe.db.get_value("Address Template", "Brazil", "is_default"), 1)
- def tearDown(self):
- a = frappe.get_doc("Address Template", "India")
- a.is_default = 1
- a.save()
+ def test_delete_address_template(self):
+ india = frappe.get_doc(
+ {"doctype": "Address Template", "country": "India", "is_default": 0}
+ ).insert()
- @classmethod
- def make_default_address_template(self):
- template = """{{ address_line1 }}
{% if address_line2 %}{{ address_line2 }}
{% endif -%}{{ city }}
{% if state %}{{ state }}
{% endif -%}{% if pincode %}{{ pincode }}
{% endif -%}{{ country }}
{% if phone %}Phone: {{ phone }}
{% endif -%}{% if fax %}Fax: {{ fax }}
{% endif -%}{% if email_id %}Email: {{ email_id }}
{% endif -%}"""
+ brazil = frappe.get_doc(
+ {"doctype": "Address Template", "country": "Brazil", "is_default": 1}
+ ).insert()
- if not frappe.db.exists("Address Template", "India"):
- frappe.get_doc(
- {"doctype": "Address Template", "country": "India", "is_default": 1, "template": template}
- ).insert()
+ india.reload() # might have been modified by the second template
+ india.delete() # should not raise an error
- if not frappe.db.exists("Address Template", "Brazil"):
- frappe.get_doc(
- {"doctype": "Address Template", "country": "Brazil", "template": template}
- ).insert()
+ self.assertRaises(frappe.ValidationError, brazil.delete)
diff --git a/frappe/core/doctype/docshare/test_docshare.py b/frappe/core/doctype/docshare/test_docshare.py
index f2ed8a32af..b874042d15 100644
--- a/frappe/core/doctype/docshare/test_docshare.py
+++ b/frappe/core/doctype/docshare/test_docshare.py
@@ -125,3 +125,17 @@ class TestDocShare(FrappeTestCase):
)
frappe.share.remove(doctype, submittable_doc.name, self.user)
+
+ def test_share_int_pk(self):
+ test_doc = frappe.new_doc("Console Log")
+
+ test_doc.insert()
+ frappe.share.add("Console Log", test_doc.name, self.user)
+
+ frappe.set_user(self.user)
+ self.assertIn(
+ str(test_doc.name), [str(name) for name in frappe.get_list("Console Log", pluck="name")]
+ )
+
+ test_doc.reload()
+ self.assertTrue(test_doc.has_permission("read"))
diff --git a/frappe/core/doctype/error_snapshot/test_error_snapshot.py b/frappe/core/doctype/error_snapshot/test_error_snapshot.py
index 8ff48bc5c6..4779d56c7b 100644
--- a/frappe/core/doctype/error_snapshot/test_error_snapshot.py
+++ b/frappe/core/doctype/error_snapshot/test_error_snapshot.py
@@ -1,9 +1,11 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
from frappe.tests.utils import FrappeTestCase
+from frappe.utils.logger import sanitized_dict
# test_records = frappe.get_test_records('Error Snapshot')
class TestErrorSnapshot(FrappeTestCase):
- pass
+ def test_form_dict_sanitization(self):
+ self.assertNotEqual(sanitized_dict({"pwd": "SECRET", "usr": "WHAT"}).get("pwd"), "SECRET")
diff --git a/frappe/core/doctype/system_settings/system_settings.js b/frappe/core/doctype/system_settings/system_settings.js
index 3db3eef299..1de4dd82e7 100644
--- a/frappe/core/doctype/system_settings/system_settings.js
+++ b/frappe/core/doctype/system_settings/system_settings.js
@@ -30,13 +30,6 @@ frappe.ui.form.on("System Settings", {
frm.set_value("bypass_restrict_ip_check_if_2fa_enabled", 0);
}
},
- enable_prepared_report_auto_deletion: function (frm) {
- if (frm.doc.enable_prepared_report_auto_deletion) {
- if (!frm.doc.prepared_report_expiry_period) {
- frm.set_value("prepared_report_expiry_period", 7);
- }
- }
- },
on_update: function (frm) {
if (frappe.boot.time_zone && frappe.boot.time_zone.system !== frm.doc.time_zone) {
// Clear cache after saving to refresh the values of boot.
diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py
index f14a4588a8..758d9c1e64 100644
--- a/frappe/custom/doctype/custom_field/custom_field.py
+++ b/frappe/custom/doctype/custom_field/custom_field.py
@@ -142,7 +142,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")
]
diff --git a/frappe/desk/page/translation_tool/__init__.py b/frappe/custom/report/__init__.py
similarity index 100%
rename from frappe/desk/page/translation_tool/__init__.py
rename to frappe/custom/report/__init__.py
diff --git a/frappe/custom/report/audit_system_hooks/__init__.py b/frappe/custom/report/audit_system_hooks/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
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/database/database.py b/frappe/database/database.py
index 52965b4722..d608e30fc7 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -772,7 +772,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)
diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py
index 13525d2328..bbdd95d921 100644
--- a/frappe/database/mariadb/schema.py
+++ b/frappe/database/mariadb/schema.py
@@ -94,7 +94,7 @@ class MariaDBTable(DBTable):
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:
+ for col in {*self.drop_index, *self.drop_unique}:
if col.fieldname == "name":
continue
diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py
index d7dfbb90d7..3627f48109 100644
--- a/frappe/desk/form/load.py
+++ b/frappe/desk/form/load.py
@@ -28,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 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(
@@ -43,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
diff --git a/frappe/desk/page/translation_tool/translation_tool.css b/frappe/desk/page/translation_tool/translation_tool.css
deleted file mode 100644
index 9603a4ce35..0000000000
--- a/frappe/desk/page/translation_tool/translation_tool.css
+++ /dev/null
@@ -1,37 +0,0 @@
-.translation-item {
- font-size: 12px;
- padding: 12px 15px;
- min-height: 40px;
- cursor: pointer;
- overflow: hidden;
-}
-
-.translation-item:hover {
- background-color: #fafbfc;
-}
-.translation-item.active {
- background-color: #fffce7;
-}
-
-.translation-edit-section {
- height: 100%;
- overflow-y: scroll;
- padding: 0px;
-}
-
-.translation-tool {
- display: flex;
- width: 100%;
- padding: 0;
- height: 72vh;
-}
-
-.left-side {
- padding: 0px;
- height: 100%;
- overflow-y: scroll;
-}
-
-.contributed-translation {
- padding: 0.5rem 0;
-}
diff --git a/frappe/desk/page/translation_tool/translation_tool.html b/frappe/desk/page/translation_tool/translation_tool.html
deleted file mode 100644
index a88f698584..0000000000
--- a/frappe/desk/page/translation_tool/translation_tool.html
+++ /dev/null
@@ -1,20 +0,0 @@
-
diff --git a/frappe/desk/page/translation_tool/translation_tool.js b/frappe/desk/page/translation_tool/translation_tool.js
deleted file mode 100644
index 5739eddfc7..0000000000
--- a/frappe/desk/page/translation_tool/translation_tool.js
+++ /dev/null
@@ -1,473 +0,0 @@
-frappe.pages["translation-tool"].on_page_load = function (wrapper) {
- var page = frappe.ui.make_app_page({
- parent: wrapper,
- title: __("Translation Tool"),
- single_column: true,
- card_layout: true,
- });
-
- frappe.translation_tool = new TranslationTool(page);
-};
-
-class TranslationTool {
- constructor(page) {
- this.page = page;
- this.wrapper = $(page.body);
- this.wrapper.append(frappe.render_template("translation_tool"));
- frappe.utils.bind_actions_with_object(this.wrapper, this);
- this.active_translation = null;
- this.edited_translations = {};
- this.setup_search_box();
- this.setup_language_filter();
- this.page.set_primary_action(
- __("Contribute Translations"),
- this.show_confirmation_dialog.bind(this)
- );
- this.page.set_secondary_action(__("Refresh"), this.fetch_messages_then_render.bind(this));
- this.update_header();
- }
-
- setup_language_filter() {
- let languages = Object.keys(frappe.boot.lang_dict).map((language_label) => {
- let value = frappe.boot.lang_dict[language_label];
- return {
- label: `${language_label} (${value})`,
- value: value,
- };
- });
-
- let language_selector = this.page.add_field({
- fieldname: "language",
- fieldtype: "Select",
- options: languages,
- change: () => {
- let language = language_selector.get_value();
- localStorage.setItem("translation_language", language);
- this.language = language;
- this.fetch_messages_then_render();
- },
- });
- let translation_language = localStorage.getItem("translation_language");
- if (translation_language || frappe.boot.lang !== "en") {
- language_selector.set_value(translation_language || frappe.boot.lang);
- } else {
- frappe.prompt(
- {
- label: __("Please select target language for translation"),
- fieldname: "language",
- fieldtype: "Select",
- options: languages,
- reqd: 1,
- },
- (values) => {
- language_selector.set_value(values.language);
- },
- __("Select Language")
- );
- }
- }
-
- setup_search_box() {
- let search_box = this.page.add_field({
- fieldname: "search",
- fieldtype: "Data",
- label: __("Search Source Text"),
- change: () => {
- this.search_text = search_box.get_value();
- this.fetch_messages_then_render();
- },
- });
- }
-
- fetch_messages_then_render() {
- this.fetch_messages().then((messages) => {
- this.messages = messages;
- this.render_messages(messages);
- });
- this.setup_local_contributions();
- }
-
- fetch_messages() {
- frappe.dom.freeze(__("Fetching..."));
- return frappe
- .xcall("frappe.translate.get_messages", {
- language: this.language,
- search_text: this.search_text,
- })
- .then((messages) => {
- return messages;
- })
- .finally(() => {
- frappe.dom.unfreeze();
- });
- }
-
- render_messages(messages) {
- let template = (message) => `
-
-
-
- ${frappe.utils.escape_html(message.source_text)}
-
-
-
- `;
-
- let html = messages.map(template).join("");
- this.wrapper.find(".translation-item-container").html(html);
- }
-
- on_translation_click(e, $el) {
- let message_id = decodeURIComponent($el.data("message-id"));
- this.wrapper.find(".translation-item").removeClass("active");
- $el.addClass("active");
- this.active_translation = this.messages.find((m) => m.id === message_id);
- this.edit_translation(this.active_translation);
- }
-
- edit_translation(translation) {
- if (this.form) {
- this.form.set_values({});
- }
- this.get_additional_info(translation.id).then((data) => {
- this.make_edit_form(translation, data);
- });
- }
-
- get_additional_info(source_id) {
- frappe.dom.freeze("Fetching...");
- return frappe
- .xcall("frappe.translate.get_source_additional_info", {
- source: source_id,
- language: this.page.fields_dict["language"].get_value(),
- })
- .finally(frappe.dom.unfreeze);
- }
-
- make_edit_form(translation, { contributions, positions }) {
- if (!this.form) {
- this.form = new frappe.ui.FieldGroup({
- fields: [
- {
- fieldtype: "HTML",
- fieldname: "header",
- read_only: 1,
- },
- {
- fieldtype: "Data",
- fieldname: "id",
- hidden: 1,
- },
- {
- label: "Source Text",
- fieldtype: "Code",
- fieldname: "source_text",
- read_only: 1,
- enable_copy_button: 1,
- },
- {
- label: "Context",
- fieldtype: "Code",
- fieldname: "context",
- read_only: 1,
- },
- {
- label: "DocType",
- fieldtype: "Data",
- fieldname: "doctype",
- read_only: 1,
- },
- {
- label: "Translated Text",
- fieldtype: "Small Text",
- fieldname: "translated_text",
- },
- {
- label: "Suggest",
- fieldtype: "Button",
- click: () => {
- let { id, translated_text, source_text } = this.form.get_values();
- let existing_value = this.form.translation_dict.translated_text;
- if (is_null(translated_text) || existing_value === translated_text) {
- delete this.edited_translations[id];
- } else if (existing_value !== translated_text) {
- this.edited_translations[id] = {
- id,
- translated_text,
- source_text,
- };
- }
- this.update_header();
- },
- },
- {
- fieldtype: "Section Break",
- fieldname: "contributed_translations_section",
- label: "Contributed Translations",
- },
- {
- fieldtype: "HTML",
- fieldname: "contributed_translations",
- },
- {
- fieldtype: "Section Break",
- collapsible: 1,
- label: "Occurences in source code",
- },
- {
- fieldtype: "HTML",
- fieldname: "positions",
- },
- ],
- body: this.wrapper.find(".translation-edit-form"),
- });
-
- this.form.make();
- this.setup_header();
- }
-
- this.form.set_values(translation);
- this.form.translation_dict = translation;
- this.form.set_df_property("doctype", "hidden", !translation.doctype);
- this.form.set_df_property("context", "hidden", !translation.context);
- this.set_status(translation);
-
- this.setup_contributions(contributions);
- this.setup_positions(positions);
- }
-
- setup_header() {
- this.form.get_field("header").$wrapper.html(`
-
-
`);
- }
-
- set_status(translation) {
- this.form.get_field("header").$wrapper.find(".translation-status").html(`
-
- ${this.get_indicator_status_text(translation)}
-
- `);
- }
-
- setup_positions(positions) {
- let position_dom = "";
- if (positions && positions.length) {
- position_dom = positions
- .map((position) => {
- if (position.path.startsWith("DocType: ")) {
- return `
- ${position.path}
-
`;
- } else {
- return ``;
- }
- })
- .join("");
- }
- this.form.get_field("positions").$wrapper.html(position_dom);
- }
-
- setup_contributions(contributions) {
- const contributions_exists = contributions && contributions.length;
- if (contributions_exists) {
- let contributions_html = contributions.map((c) => {
- return `
-
-
${c.translated}
-
- ${comment_when(c.creation)}
-
-
- `;
- });
- this.form.get_field("contributed_translations").html(contributions_html);
- }
- this.form.set_df_property(
- "contributed_translations_section",
- "hidden",
- !contributions_exists
- );
- }
- show_confirmation_dialog() {
- this.confirmation_dialog = new frappe.ui.Dialog({
- fields: [
- {
- label: __("Language"),
- fieldname: "language",
- fieldtype: "Data",
- read_only: 1,
- bold: 1,
- default: this.language,
- },
- {
- fieldtype: "HTML",
- fieldname: "edited_translations",
- },
- ],
- title: __("Confirm Translations"),
- no_submit_on_enter: true,
- primary_action_label: __("Submit"),
- primary_action: (values) => {
- this.create_translations(values).then(this.confirmation_dialog.hide());
- },
- });
- this.confirmation_dialog.get_field("edited_translations").html(`
-
-
- | ${__("Source Text")} |
- ${__("Translated Text")} |
-
- ${Object.values(this.edited_translations)
- .map(
- (t) => `
-
- | ${t.source_text} |
- ${t.translated_text} |
-
- `
- )
- .join("")}
-
- `);
- this.confirmation_dialog.show();
- }
- create_translations() {
- frappe.dom.freeze(__("Submitting..."));
- return frappe
- .xcall("frappe.core.doctype.translation.translation.create_translations", {
- translation_map: this.edited_translations,
- language: this.language,
- })
- .then(() => {
- frappe.dom.unfreeze();
- frappe.show_alert({
- message: __("Successfully Submitted!"),
- indicator: "success",
- });
- this.edited_translations = {};
- this.update_header();
- this.fetch_messages_then_render();
- })
- .finally(() => frappe.dom.unfreeze());
- }
-
- setup_local_contributions() {
- // TODO: Refactor
- frappe
- .xcall("frappe.translate.get_contributions", {
- language: this.language,
- })
- .then((messages) => {
- let template = (message) => `
-
-
-
- ${frappe.utils.escape_html(message.source_text)}
-
-
-
- `;
-
- let html = messages.map(template).join("");
- this.wrapper.find(".translation-item-tr").html(html);
- });
- }
-
- show_translation_status_modal(e, $el) {
- let message_id = decodeURIComponent($el.data("message-id"));
-
- frappe.xcall("frappe.translate.get_contribution_status", { message_id }).then((doc) => {
- let d = new frappe.ui.Dialog({
- title: __("Contribution Status"),
- fields: [
- {
- fieldname: "source_message",
- label: __("Source Message"),
- fieldtype: "Data",
- read_only: 1,
- },
- {
- fieldname: "translated",
- label: __("Translated Message"),
- fieldtype: "Data",
- read_only: 1,
- },
- {
- fieldname: "contribution_status",
- label: __("Contribution Status"),
- fieldtype: "Data",
- read_only: 1,
- },
- {
- fieldname: "modified_by",
- label: __("Verified By"),
- fieldtype: "Data",
- read_only: 1,
- depends_on: (doc) => {
- return doc.contribution_status == "Verified";
- },
- },
- ],
- });
- d.set_values(doc);
- d.show();
- });
- }
-
- update_header() {
- let edited_translations_count = Object.keys(this.edited_translations).length;
- if (edited_translations_count) {
- let message = "";
- if (edited_translations_count == 1) {
- message = __("{0} translation pending", [edited_translations_count]);
- } else {
- message = __("{0} translations pending", [edited_translations_count]);
- }
- this.page.set_indicator(message, "orange");
- } else {
- this.page.set_indicator("");
- }
- this.page.btn_primary.prop("disabled", !edited_translations_count);
- }
-
- get_indicator_color(message_obj) {
- return !message_obj.translated
- ? "red"
- : message_obj.translated_by_google
- ? "orange"
- : "blue";
- }
-
- get_indicator_status_text(message_obj) {
- if (!message_obj.translated) {
- return __("Untranslated");
- } else if (message_obj.translated_by_google) {
- return __("Google Translation");
- } else {
- return __("Community Contribution");
- }
- }
-
- get_contribution_indicator_color(message_obj) {
- return message_obj.contribution_status == "Pending" ? "orange" : "green";
- }
-
- get_code_url(path, line_no, app) {
- const code_path = path.substring(`apps/${app}`.length);
- return `https://github.com/frappe/${app}/blob/develop/${code_path}#L${line_no}`;
- }
-}
diff --git a/frappe/desk/page/translation_tool/translation_tool.json b/frappe/desk/page/translation_tool/translation_tool.json
deleted file mode 100644
index a54b2a4724..0000000000
--- a/frappe/desk/page/translation_tool/translation_tool.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "content": null,
- "creation": "2020-01-30 15:16:12.136323",
- "docstatus": 0,
- "doctype": "Page",
- "idx": 0,
- "modified": "2020-01-30 15:16:23.273733",
- "modified_by": "Administrator",
- "module": "Desk",
- "name": "translation-tool",
- "owner": "Administrator",
- "page_name": "Translation Tool",
- "roles": [
- {
- "role": "System Manager"
- },
- {
- "role": "Translator"
- }
- ],
- "script": null,
- "standard": "Yes",
- "style": null,
- "system_page": 1,
- "title": "Translation Tool"
-}
\ No newline at end of file
diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py
index 21e5c5b312..5b8ad1c901 100644
--- a/frappe/integrations/doctype/ldap_settings/ldap_settings.py
+++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py
@@ -370,13 +370,15 @@ def login():
args = frappe.form_dict
ldap: LDAPSettings = frappe.get_doc("LDAP Settings")
- user = ldap.authenticate(frappe.as_unicode(args.usr), frappe.as_unicode(args.pop("pwd", None)))
+ user = ldap.authenticate(frappe.as_unicode(args.usr), frappe.as_unicode(args.pwd))
frappe.local.login_manager.user = user.name
if should_run_2fa(user.name):
authenticate_for_2factor(user.name)
if not confirm_otp_token(frappe.local.login_manager):
return False
+
+ frappe.form_dict.pop("pwd", None)
frappe.local.login_manager.post_login()
# because of a GET request!
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index 26544c5c0e..a6c2ef701c 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -1074,7 +1074,7 @@ class BaseDocument:
def is_dummy_password(self, pwd):
return "".join(set(pwd)) == "*"
- def precision(self, fieldname, parentfield=None):
+ def precision(self, fieldname, parentfield=None) -> int | None:
"""Returns float precision for a particular field (or get global default).
:param fieldname: Fieldname for which precision is required.
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index b3d19b01a4..e3858a3ff7 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -328,13 +328,16 @@ class DatabaseQuery:
# convert child_table.fieldname to `tabChild DocType`.`fieldname`
for field in self.fields:
- if "." in field and "tab" not in field:
+ if "." in field:
original_field = field
alias = None
if " as " in field:
- field, alias = field.split(" as ")
- linked_fieldname, fieldname = field.split(".")
- linked_field = self.doctype_meta.get_field(linked_fieldname)
+ field, alias = field.split(" as ", 1)
+ linked_fieldname, fieldname = field.split(".", 1)
+ linked_field = frappe.get_meta(self.doctype).get_field(linked_fieldname)
+ # this is not a link field
+ if not linked_field:
+ continue
linked_doctype = linked_field.options
if linked_field.fieldtype == "Link":
self.append_link_table(linked_doctype, linked_fieldname)
@@ -872,7 +875,7 @@ class DatabaseQuery:
# share is an OR condition, if there is a role permission
if not only_if_shared and self.shared and conditions:
- conditions = f"({conditions}) or ({self.get_share_condition()})"
+ conditions = f"(({conditions}) or ({self.get_share_condition()}))"
return conditions
diff --git a/frappe/patches.txt b/frappe/patches.txt
index 7f7ab9bfe2..ec55c7fedd 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -196,7 +196,6 @@ frappe.patches.v14_0.setup_likes_from_feedback
frappe.patches.v14_0.update_webforms
frappe.patches.v14_0.delete_payment_gateways
frappe.patches.v15_0.remove_event_streaming
-frappe.patches.v15_0.remove_prepared_report_settings_from_system_settings
frappe.patches.v15_0.copy_disable_prepared_report_to_prepared_report
[post_model_sync]
@@ -221,3 +220,5 @@ frappe.patches.v14_0.update_attachment_comment
frappe.patches.v15_0.set_contact_full_name
execute:frappe.delete_doc("Page", "activity", force=1)
frappe.patches.v14_0.disable_email_accounts_with_oauth
+execute:frappe.delete_doc("Page", "translation-tool", force=1)
+frappe.patches.v15_0.remove_prepared_report_settings_from_system_settings
diff --git a/frappe/patches/v15_0/remove_prepared_report_settings_from_system_settings.py b/frappe/patches/v15_0/remove_prepared_report_settings_from_system_settings.py
index 8c0ec4ca70..2c203784df 100644
--- a/frappe/patches/v15_0/remove_prepared_report_settings_from_system_settings.py
+++ b/frappe/patches/v15_0/remove_prepared_report_settings_from_system_settings.py
@@ -4,12 +4,6 @@ from frappe.utils import cint
def execute():
expiry_period = (
- cint(frappe.db.get_single_value("System Settings", "prepared_report_expiry_period")) or 30
+ cint(frappe.db.get_singles_dict("System Settings").get("prepared_report_expiry_period")) or 30
)
frappe.get_single("Log Settings").register_doctype("Prepared Report", expiry_period)
-
- singles = frappe.qb.DocType("Singles")
- frappe.qb.from_(singles).delete().where(
- (singles.doctype == "System Settings")
- & (singles.field.isin(["enable_prepared_report_auto_deletion", "prepared_report_expiry_period"]))
- ).run()
diff --git a/frappe/permissions.py b/frappe/permissions.py
index ef33c03875..2bee75d50c 100644
--- a/frappe/permissions.py
+++ b/frappe/permissions.py
@@ -637,7 +637,7 @@ def get_linked_doctypes(dt: str) -> list:
def get_doc_name(doc):
if not doc:
return None
- return doc if isinstance(doc, str) else doc.name
+ return doc if isinstance(doc, str) else str(doc.name)
def allow_everything():
diff --git a/frappe/printing/doctype/print_format/print_format.js b/frappe/printing/doctype/print_format/print_format.js
index 94f0ae5b1c..7b746241ac 100644
--- a/frappe/printing/doctype/print_format/print_format.js
+++ b/frappe/printing/doctype/print_format/print_format.js
@@ -39,7 +39,7 @@ frappe.ui.form.on("Print Format", {
} else if (frm.doc.custom_format && !frm.doc.raw_printing) {
frm.set_df_property("html", "reqd", 1);
}
- if (frappe.model.can_read(frm.doc.doc_type)) {
+ if (frappe.model.can_write("Customize Form")) {
frappe.db.get_value("DocType", frm.doc.doc_type, "default_print_format", (r) => {
if (r.default_print_format != frm.doc.name) {
frm.add_custom_button(__("Set as Default"), function () {
diff --git a/frappe/public/js/frappe/form/controls/datepicker_i18n.js b/frappe/public/js/frappe/form/controls/datepicker_i18n.js
index eca5a61723..ff0c4ffe50 100644
--- a/frappe/public/js/frappe/form/controls/datepicker_i18n.js
+++ b/frappe/public/js/frappe/form/controls/datepicker_i18n.js
@@ -136,3 +136,44 @@ import "air-datepicker/dist/js/i18n/datepicker.zh.js";
firstDay: 1,
};
})(jQuery);
+
+(function ($) {
+ $.fn.datepicker.language["tr"] = {
+ days: ["Pazar", "Pazartesi", "Salı", "Çarşamba", "Perşembe", "Cuma", "Cumartesi"],
+ daysShort: ["Paz", "Pzt", "Sal", "Çar", "Per", "Cum", "Cmt"],
+ daysMin: ["Pz", "Pt", "Sa", "Ça", "Pe", "Cu", "Ct"],
+ months: [
+ "Ocak",
+ "Şubat",
+ "Mart",
+ "Nisan",
+ "Mayıs",
+ "Haziran",
+ "Temmuz",
+ "Ağustos",
+ "Eylül",
+ "Ekim",
+ "Kasım",
+ "Aralık",
+ ],
+ monthsShort: [
+ "Oca",
+ "Şub",
+ "Mar",
+ "Nis",
+ "May",
+ "Haz",
+ "Tem",
+ "Ağu",
+ "Eyl",
+ "Eki",
+ "Kas",
+ "Ara",
+ ],
+ today: "Bugün",
+ clear: "Temizle",
+ dateFormat: "dd.mm.yyyy",
+ timeFormat: "hh:ii",
+ firstDay: 1,
+ };
+})(jQuery);
diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js
index 8893c4b69e..9aa7529761 100644
--- a/frappe/public/js/frappe/form/form.js
+++ b/frappe/public/js/frappe/form/form.js
@@ -1516,7 +1516,7 @@ frappe.ui.form.Form = class FrappeForm {
if (this.fields_dict[fieldname].grid.grid_rows_by_docname[table_row_name]) {
this.fields_dict[fieldname].grid.grid_rows_by_docname[
table_row_name
- ].refresh_field(fieldname);
+ ].refresh_field(table_field);
}
} else {
this.refresh_field(fieldname);
diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js
index e80a07f8ac..b767dac932 100644
--- a/frappe/public/js/frappe/form/grid.js
+++ b/frappe/public/js/frappe/form/grid.js
@@ -580,9 +580,9 @@ export default class Grid {
}
get_filtered_data() {
- if (!this.frm) return;
+ let all_data = this.frm ? this.frm.doc[this.df.fieldname] : this.df.data;
- let all_data = this.frm.doc[this.df.fieldname];
+ if (!all_data) return;
for (const field in this.filter) {
all_data = all_data.filter((data) => {
diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js
index 0b6ac5c208..daf5502197 100644
--- a/frappe/public/js/frappe/form/grid_row.js
+++ b/frappe/public/js/frappe/form/grid_row.js
@@ -12,7 +12,8 @@ export default class GridRow {
this.make();
}
make() {
- var me = this;
+ let me = this;
+ let render_row = true;
this.wrapper = $('');
this.row = $('')
@@ -36,9 +37,11 @@ export default class GridRow {
if (this.grid.template && !this.grid.meta.editable_grid) {
this.render_template();
} else {
- this.render_row();
+ render_row = this.render_row();
}
+ if (!this.render_row) return;
+
this.set_data();
this.wrapper.appendTo(this.parent);
}
@@ -312,6 +315,8 @@ export default class GridRow {
if (this.frm && this.doc) {
$(this.frm.wrapper).trigger("grid-row-render", [this]);
}
+
+ return true;
}
make_editable() {
@@ -757,11 +762,7 @@ export default class GridRow {
show_search_row() {
// show or remove search columns based on grid rows
- this.show_search =
- this.frm &&
- this.frm.doc &&
- this.frm.doc[this.grid.df.fieldname] &&
- this.frm.doc[this.grid.df.fieldname].length >= 20;
+ this.show_search = this.show_search && this.grid?.data?.length >= 20;
!this.show_search && this.wrapper.remove();
return this.show_search;
}
diff --git a/frappe/public/js/frappe/list/bulk_operations.js b/frappe/public/js/frappe/list/bulk_operations.js
index c9e8f7d329..e7e3fb256f 100644
--- a/frappe/public/js/frappe/list/bulk_operations.js
+++ b/frappe/public/js/frappe/list/bulk_operations.js
@@ -146,6 +146,10 @@ export default class BulkOperations {
.call({
method: "frappe.desk.reportview.delete_items",
freeze: true,
+ freeze_message:
+ docnames.length <= 10
+ ? __("Deleting {0} records...", [docnames.length])
+ : null,
args: {
items: docnames,
doctype: this.doctype,
diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js
index 8faff07d16..4625f0aa8e 100644
--- a/frappe/public/js/frappe/list/list_view.js
+++ b/frappe/public/js/frappe/list/list_view.js
@@ -584,7 +584,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
`);
this.setup_new_doc_event();
- this.list_sidebar && this.list_sidebar.reload_stats();
+ if (this.list_view_settings && !this.list_view_settings.disable_sidebar_stats) {
+ this.list_sidebar && this.list_sidebar.reload_stats();
+ }
this.toggle_paging && this.$paging_area.toggle(true);
}
diff --git a/frappe/public/js/frappe/ui/dialog.js b/frappe/public/js/frappe/ui/dialog.js
index ed42b81b68..9ec6922306 100644
--- a/frappe/public/js/frappe/ui/dialog.js
+++ b/frappe/public/js/frappe/ui/dialog.js
@@ -28,6 +28,8 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
this.get_close_btn().hide();
}
+ 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");
@@ -123,6 +125,31 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
});
}
+ 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() {
return this.standard_actions.find(".btn-primary");
}
diff --git a/frappe/public/scss/desk/global.scss b/frappe/public/scss/desk/global.scss
index 0d7ca9ac06..5294779990 100644
--- a/frappe/public/scss/desk/global.scss
+++ b/frappe/public/scss/desk/global.scss
@@ -424,26 +424,19 @@ kbd {
background-color: var(--bg-color);
.freeze-message-container {
+ inset: 0;
+ padding: 3rem;
+ background-color: var(--bg-light-gray);
+ color: var(--text-on-light-gray);
+ font-size: larger;
position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
display: grid;
place-content: center;
}
-
- .freeze-message {
- color: var(--text-color) !important;
- }
-}
-
-#freeze.dark {
- background-color: var(--gray-900);
}
#freeze.in {
- opacity: 0.5;
+ opacity: 0.8;
}
.msg-box {
diff --git a/frappe/tests/test_boot.py b/frappe/tests/test_boot.py
index 0b688d6aee..232c379e08 100644
--- a/frappe/tests/test_boot.py
+++ b/frappe/tests/test_boot.py
@@ -1,5 +1,5 @@
import frappe
-from frappe.boot import get_unseen_notes
+from frappe.boot import get_unseen_notes, get_user_pages_or_reports
from frappe.desk.doctype.note.note import mark_as_seen
from frappe.tests.utils import FrappeTestCase
@@ -26,3 +26,47 @@ class TestBootData(FrappeTestCase):
mark_as_seen(note.name)
unseen_notes = [d.title for d in get_unseen_notes()]
self.assertListEqual(unseen_notes, [])
+
+ def test_get_user_pages_or_reports_with_permission_query(self):
+ # Create a ToDo custom report with admin user
+ frappe.set_user("Administrator")
+ frappe.get_doc(
+ {
+ "doctype": "Report",
+ "ref_doctype": "ToDo",
+ "report_name": "Test Admin Report",
+ "report_type": "Report Builder",
+ "is_standard": "No",
+ }
+ ).insert()
+
+ # Add permission query such that each user can only see their own custom reports
+ frappe.get_doc(
+ dict(
+ doctype="Server Script",
+ name="test_report_permission_query",
+ script_type="Permission Query",
+ reference_doctype="Report",
+ script="""conditions = f"(`tabReport`.is_standard = 'Yes' or `tabReport`.owner = '{frappe.session.user}')"
+ """,
+ )
+ ).insert()
+
+ # Create a ToDo custom report with test user
+ frappe.set_user("test@example.com")
+ frappe.get_doc(
+ {
+ "doctype": "Report",
+ "ref_doctype": "ToDo",
+ "report_name": "Test User Report",
+ "report_type": "Report Builder",
+ "is_standard": "No",
+ }
+ ).insert(ignore_permissions=True)
+
+ get_user_pages_or_reports("Report")
+ allowed_reports = frappe.cache().get_value("has_role:Report", user=frappe.session.user)
+
+ # Test user must not see admin user's report
+ self.assertNotIn("Test Admin Report", allowed_reports)
+ self.assertIn("Test User Report", allowed_reports)
diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py
index c6f7b8302f..59df08dd91 100644
--- a/frappe/tests/test_utils.py
+++ b/frappe/tests/test_utils.py
@@ -960,3 +960,16 @@ class TestTypingValidations(FrappeTestCase):
report.toggle_disable(changed_value)
report.toggle_disable(current_value)
+
+
+class TestTBSanitization(FrappeTestCase):
+ def test_traceback_sanitzation(self):
+ try:
+ password = "42"
+ args = {"password": "42", "pwd": "42", "safe": "safe_value"}
+ raise Exception
+ except Exception:
+ traceback = frappe.get_traceback(with_context=True)
+ self.assertNotIn("42", traceback)
+ self.assertIn("********", traceback)
+ self.assertIn("safe_value", traceback)
diff --git a/frappe/tests/utils.py b/frappe/tests/utils.py
index 15e0c3d9c0..5f13c9cd11 100644
--- a/frappe/tests/utils.py
+++ b/frappe/tests/utils.py
@@ -55,12 +55,14 @@ class FrappeTestCase(unittest.TestCase):
else:
self._compare_field(value, actual.get(field), actual, field)
- def _compare_field(self, expected, actual, doc, field):
+ def _compare_field(self, expected, actual, doc: BaseDocument, field: str):
msg = f"{field} should be same."
if isinstance(expected, float):
precision = doc.precision(field)
- self.assertAlmostEqual(expected, actual, f"{field} should be same to {precision} digits")
+ self.assertAlmostEqual(
+ expected, actual, places=precision, msg=f"{field} should be same to {precision} digits"
+ )
elif isinstance(expected, (bool, int)):
self.assertEqual(expected, cint(actual), msg=msg)
elif isinstance(expected, datetime_like_types):
diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py
index c715097be2..d37e8c201f 100644
--- a/frappe/utils/__init__.py
+++ b/frappe/utils/__init__.py
@@ -24,7 +24,6 @@ from typing import Any, Literal
from urllib.parse import quote, urlparse
from redis.exceptions import ConnectionError
-from traceback_with_variables import iter_exc_lines
from werkzeug.test import Client
import frappe
@@ -298,13 +297,15 @@ def get_traceback(with_context=False) -> str:
"""
Returns the traceback of the Exception
"""
+ from traceback_with_variables import iter_exc_lines
+
exc_type, exc_value, exc_tb = sys.exc_info()
if not any([exc_type, exc_value, exc_tb]):
return ""
if with_context:
- trace_list = iter_exc_lines()
+ trace_list = iter_exc_lines(fmt=_get_traceback_sanitizer())
tb = "\n".join(trace_list)
else:
trace_list = traceback.format_exception(exc_type, exc_value, exc_tb)
@@ -314,6 +315,44 @@ def get_traceback(with_context=False) -> str:
return tb.replace(bench_path, "")
+@functools.lru_cache(maxsize=1)
+def _get_traceback_sanitizer():
+ from traceback_with_variables import Format
+
+ blocklist = [
+ "password",
+ "passwd",
+ "secret",
+ "token",
+ "key",
+ "pwd",
+ ]
+
+ placeholder = "********"
+
+ def dict_printer(v: dict) -> str:
+ from copy import deepcopy
+
+ v = deepcopy(v)
+ for key in blocklist:
+ if key in v:
+ v[key] = placeholder
+
+ return str(v)
+
+ # Adapted from https://github.com/andy-landy/traceback_with_variables/blob/master/examples/format_customized.py
+ # Reused under MIT license: https://github.com/andy-landy/traceback_with_variables/blob/master/LICENSE
+
+ return Format(
+ custom_var_printers=[
+ # redact variables
+ *[(variable_name, lambda: placeholder) for variable_name in blocklist],
+ # redact dictionary keys
+ (["_secret", dict, lambda *a, **kw: False], dict_printer),
+ ],
+ )
+
+
def log(event, details):
frappe.logger(event).info(details)
diff --git a/frappe/utils/error.py b/frappe/utils/error.py
index 432591175c..235a9b3e67 100644
--- a/frappe/utils/error.py
+++ b/frappe/utils/error.py
@@ -12,7 +12,7 @@ import pydoc
import sys
import traceback
-from ldap3.core.exceptions import LDAPInvalidCredentialsResult
+from ldap3.core.exceptions import LDAPException
import frappe
from frappe.utils import cstr, encode
@@ -21,7 +21,7 @@ EXCLUDE_EXCEPTIONS = (
frappe.AuthenticationError,
frappe.CSRFTokenError, # CSRF covers OAuth too
frappe.SecurityException,
- LDAPInvalidCredentialsResult,
+ LDAPException,
)
diff --git a/frappe/utils/logger.py b/frappe/utils/logger.py
index b642821c9d..ddb81f3d79 100755
--- a/frappe/utils/logger.py
+++ b/frappe/utils/logger.py
@@ -1,6 +1,7 @@
# imports - standard imports
import logging
import os
+from copy import deepcopy
from logging.handlers import RotatingFileHandler
from typing import Literal
@@ -91,7 +92,7 @@ class SiteContextFilter(logging.Filter):
def filter(self, record) -> bool:
if "Form Dict" not in str(record.msg):
site = getattr(frappe.local, "site", None)
- form_dict = getattr(frappe.local, "form_dict", None)
+ form_dict = sanitized_dict(getattr(frappe.local, "form_dict", None))
record.msg = str(record.msg) + f"\nSite: {site}\nForm Dict: {form_dict}"
return True
@@ -100,3 +101,25 @@ def set_log_level(level: Literal["ERROR", "WARNING", "WARN", "INFO", "DEBUG"]) -
"""Use this method to set log level to something other than the default DEBUG"""
frappe.log_level = getattr(logging, (level or "").upper(), None) or default_log_level
frappe.loggers = {}
+
+
+def sanitized_dict(form_dict):
+ if not isinstance(form_dict, dict):
+ return form_dict
+
+ sanitized_dict = deepcopy(form_dict)
+
+ blocklist = [
+ "password",
+ "passwd",
+ "secret",
+ "token",
+ "key",
+ "pwd",
+ ]
+
+ for k in sanitized_dict:
+ for secret_kw in blocklist:
+ if secret_kw in k:
+ sanitized_dict[k] = "********"
+ return sanitized_dict
diff --git a/frappe/utils/redis_wrapper.py b/frappe/utils/redis_wrapper.py
index e470c83d75..ea91299cfc 100644
--- a/frappe/utils/redis_wrapper.py
+++ b/frappe/utils/redis_wrapper.py
@@ -45,7 +45,7 @@ class RedisWrapper(redis.Redis):
return f"{frappe.conf.db_name}|{key}".encode()
- def set_value(self, key, val, user=None, expires_in_sec=None, shared=False, cache_locally=True):
+ def set_value(self, key, val, user=None, expires_in_sec=None, shared=False):
"""Sets cache value.
:param key: Cache key
@@ -55,7 +55,7 @@ class RedisWrapper(redis.Redis):
"""
key = self.make_key(key, user, shared)
- if not expires_in_sec and cache_locally:
+ if not expires_in_sec:
frappe.local.cache[key] = val
try:
@@ -169,7 +169,6 @@ class RedisWrapper(redis.Redis):
key: str,
value,
shared: bool = False,
- cache_locally: bool = True,
*args,
**kwargs,
):
@@ -179,8 +178,7 @@ class RedisWrapper(redis.Redis):
_name = self.make_key(name, shared=shared)
# set in local
- if cache_locally:
- frappe.local.cache.setdefault(_name, {})[key] = value
+ frappe.local.cache.setdefault(_name, {})[key] = value
# set in redis
try:
diff --git a/frappe/website/doctype/website_theme/test_website_theme.py b/frappe/website/doctype/website_theme/test_website_theme.py
index 80456a71e4..b99aa00043 100644
--- a/frappe/website/doctype/website_theme/test_website_theme.py
+++ b/frappe/website/doctype/website_theme/test_website_theme.py
@@ -1,40 +1,54 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
-import os
+from contextlib import contextmanager
import frappe
from frappe.tests.utils import FrappeTestCase
+from frappe.website.doctype.website_theme.website_theme import (
+ after_migrate,
+ get_active_theme,
+ get_scss_paths,
+)
-from .website_theme import get_scss_paths
+
+@contextmanager
+def website_theme_fixture(**theme):
+ test_theme = "test-theme"
+
+ frappe.delete_doc_if_exists("Website Theme", test_theme)
+ theme = frappe.get_doc(doctype="Website Theme", theme=test_theme, **theme)
+ theme.insert()
+ yield theme
+ frappe.db.set_single_value("Website Settings", "website_theme", "Standard")
+ theme.delete()
class TestWebsiteTheme(FrappeTestCase):
def test_website_theme(self):
- frappe.delete_doc_if_exists("Website Theme", "test-theme")
- theme = frappe.get_doc(
- dict(
- doctype="Website Theme",
- theme="test-theme",
- google_font="Inter",
- custom_scss="body { font-size: 16.5px; }", # this will get minified!
- )
- ).insert()
+ with website_theme_fixture(
+ google_font="Inter",
+ custom_scss="body { font-size: 16.5px; }", # this will get minified!
+ ) as theme:
- theme_path = frappe.get_site_path("public", theme.theme_url[1:])
- with open(theme_path) as theme_file:
- css = theme_file.read()
+ theme_path = frappe.get_site_path("public", theme.theme_url[1:])
+ with open(theme_path) as theme_file:
+ css = theme_file.read()
- self.assertTrue("body{font-size:16.5px}" in css)
- self.assertTrue("fonts.googleapis.com" in css)
+ self.assertTrue("body{font-size:16.5px}" in css)
+ self.assertTrue("fonts.googleapis.com" in css)
def test_get_scss_paths(self):
self.assertIn("frappe/public/scss/website.bundle", get_scss_paths())
def test_imports_to_ignore(self):
- frappe.delete_doc_if_exists("Website Theme", "test-theme")
- theme = frappe.get_doc(
- dict(doctype="Website Theme", theme="test-theme", ignored_apps=[{"app": "frappe"}])
- ).insert()
+ with website_theme_fixture(ignored_apps=[{"app": "frappe"}]) as theme:
+ self.assertTrue('@import "frappe/public/scss/website"' not in theme.theme_scss)
- self.assertTrue('@import "frappe/public/scss/website"' not in theme.theme_scss)
+ def test_after_migrate_hook(self):
+ with website_theme_fixture(google_font="Inter") as theme:
+ theme.set_as_default()
+ before = get_active_theme().theme_url
+ after_migrate()
+ after = get_active_theme().theme_url
+ self.assertNotEqual(before, after)
diff --git a/frappe/website/doctype/website_theme/website_theme.py b/frappe/website/doctype/website_theme/website_theme.py
index 7abfab93e3..178e872c2f 100644
--- a/frappe/website/doctype/website_theme/website_theme.py
+++ b/frappe/website/doctype/website_theme/website_theme.py
@@ -99,18 +99,8 @@ class WebsiteTheme(Document):
if fname.startswith(frappe.scrub(self.name) + "_") and fname.endswith(".css"):
os.remove(os.path.join(folder_path, fname))
- def generate_theme_if_not_exist(self):
- bench_path = frappe.utils.get_bench_path()
- if self.theme_url:
- theme_path = join_path(bench_path, "sites", self.theme_url[1:])
- if not path_exists(theme_path):
- self.generate_bootstrap_theme()
- else:
- self.generate_bootstrap_theme()
-
@frappe.whitelist()
def set_as_default(self):
- self.generate_bootstrap_theme()
self.save()
website_settings = frappe.get_doc("Website Settings")
website_settings.website_theme = self.name
@@ -133,6 +123,7 @@ def get_active_theme() -> Optional["WebsiteTheme"]:
try:
return frappe.get_cached_doc("Website Theme", website_theme)
except frappe.DoesNotExistError:
+ frappe.clear_last_message()
pass
@@ -187,5 +178,4 @@ def after_migrate():
return
doc = frappe.get_doc("Website Theme", website_theme)
- doc.generate_bootstrap_theme()
- doc.save()
+ doc.save() # Just re-saving re-generates the theme.
diff --git a/package.json b/package.json
index 0028ad3e9e..589d2dd6c9 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,7 @@
"fast-deep-equal": "^2.0.1",
"fast-glob": "^3.2.5",
"frappe-charts": "2.0.0-rc22",
- "frappe-datatable": "^1.16.4",
+ "frappe-datatable": "^1.17.1",
"frappe-gantt": "^0.6.0",
"highlight.js": "^10.4.1",
"html5-qrcode": "^2.0.11",
diff --git a/yarn.lock b/yarn.lock
index d2ee8e62a5..e5621d385f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1435,10 +1435,10 @@ frappe-charts@2.0.0-rc22:
resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-2.0.0-rc22.tgz#9a5a747febdc381a1d4d7af96e89cf519dfba8c0"
integrity sha512-N7f/8979wJCKjusOinaUYfMxB80YnfuVLrSkjpj4LtyqS0BGS6SuJxUnb7Jl4RWUFEIs7zEhideIKnyLeFZF4Q==
-frappe-datatable@^1.16.4:
- version "1.16.4"
- resolved "https://registry.yarnpkg.com/frappe-datatable/-/frappe-datatable-1.16.4.tgz#cb26f197c3cd404a5b13f016ef81c394e06f56fe"
- integrity sha512-VoiTLnkuObMa3FxITrvP32UYN9v4WQ0j4qlCiDuqdXha9/BVSxwDt2BTK+cvaRloGcds5G2Hm9IRbltRRGGhxA==
+frappe-datatable@^1.17.1:
+ version "1.17.1"
+ resolved "https://registry.yarnpkg.com/frappe-datatable/-/frappe-datatable-1.17.1.tgz#795ee79a420df07b963b7decf489045d5993cc0b"
+ integrity sha512-qqvmsaYbQUwCAtGnhmTN8jrdvXW6YfRLTZS6ufb3b1ibFEMUbE04rEFJF7TJRd2ugSk80seS2OPGTZGw+V2b0A==
dependencies:
hyperlist "^1.0.0-beta"
lodash "^4.17.5"