Merge branch 'develop' into permlevel-apis
This commit is contained in:
commit
7ce0c4c8b3
59 changed files with 614 additions and 783 deletions
15
.github/workflows/linters.yml
vendored
15
.github/workflows/linters.yml
vendored
|
|
@ -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 .
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
{{ address_line1 }}<br>
|
||||
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
|
||||
{{ city }}<br>
|
||||
{% if state %}{{ state }}<br>{% endif -%}
|
||||
{% if pincode %}{{ pincode }}<br>{% endif -%}
|
||||
{{ country }}<br>
|
||||
<br>
|
||||
{% if phone %}{{ _("Phone") }}: {{ phone }}<br>{% endif -%}
|
||||
{% if fax %}{{ _("Fax") }}: {{ fax }}<br>{% endif -%}
|
||||
{% if email_id %}{{ _("Email") }}: {{ email_id }}<br>{% endif -%}
|
||||
|
|
@ -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 }}<br>{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}\
|
||||
{{ city }}<br>
|
||||
{% if state %}{{ state }}<br>{% endif -%}
|
||||
{% if pincode %}{{ pincode }}<br>{% endif -%}
|
||||
{{ country }}<br>
|
||||
{% if phone %}"""
|
||||
+ _("Phone")
|
||||
+ """: {{ phone }}<br>{% endif -%}
|
||||
{% if fax %}"""
|
||||
+ _("Fax")
|
||||
+ """: {{ fax }}<br>{% endif -%}
|
||||
{% if email_id %}"""
|
||||
+ _("Email")
|
||||
+ """: {{ email_id }}<br>{% 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()
|
||||
|
|
|
|||
|
|
@ -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 }}<br>{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}{{ city }}<br>{% if state %}{{ state }}<br>{% endif -%}{% if pincode %}{{ pincode }}<br>{% endif -%}{{ country }}<br>{% if phone %}Phone: {{ phone }}<br>{% endif -%}{% if fax %}Fax: {{ fax }}<br>{% endif -%}{% if email_id %}Email: {{ email_id }}<br>{% 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)
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
]
|
||||
|
||||
|
|
|
|||
0
frappe/custom/report/audit_system_hooks/__init__.py
Normal file
0
frappe/custom/report/audit_system_hooks/__init__.py
Normal file
|
|
@ -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: [],
|
||||
};
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
<div class="translation-tool">
|
||||
<div class="col-sm-5 border-right left-side">
|
||||
<div class="level list-row list-row-head text-muted small">
|
||||
<div class="list-row-col ellipsis list-subject level">
|
||||
<span class="level-item">{%= __("Contributed Translations") %}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="translation-item-tr"></div>
|
||||
<div class="level list-row list-row-head text-muted small border-top">
|
||||
<div class="list-row-col ellipsis list-subject level">
|
||||
<span class="level-item">{%= __("Source Text") %}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="translation-item-container"></div>
|
||||
</div>
|
||||
<div class="translation-edit-section col-sm-7">
|
||||
<div class="translation-edit-form"></div>
|
||||
<div class="other-contributions padding"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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) => `
|
||||
<div
|
||||
class="translation-item"
|
||||
data-message-id="${encodeURIComponent(message.id)}"
|
||||
data-action="on_translation_click">
|
||||
<div class="bold ellipsis">
|
||||
<span class="indicator ${this.get_indicator_color(message)}">
|
||||
<span>${frappe.utils.escape_html(message.source_text)}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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(`<div>
|
||||
<span class="translation-status"></span>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
set_status(translation) {
|
||||
this.form.get_field("header").$wrapper.find(".translation-status").html(`
|
||||
<span class="indicator-pill ${this.get_indicator_color(translation)}">
|
||||
${this.get_indicator_status_text(translation)}
|
||||
</span>
|
||||
`);
|
||||
}
|
||||
|
||||
setup_positions(positions) {
|
||||
let position_dom = "";
|
||||
if (positions && positions.length) {
|
||||
position_dom = positions
|
||||
.map((position) => {
|
||||
if (position.path.startsWith("DocType: ")) {
|
||||
return `<div>
|
||||
<span class="text-muted">${position.path}</span>
|
||||
</div>`;
|
||||
} else {
|
||||
return `<div>
|
||||
<a
|
||||
class="text-muted"
|
||||
target="_blank"
|
||||
href="${this.get_code_url(position.path, position.line_no, position.app)}">
|
||||
${position.path}
|
||||
</a>
|
||||
</div>`;
|
||||
}
|
||||
})
|
||||
.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 `
|
||||
<div class="contributed-translation flex justify-between align-center">
|
||||
<div class="ellipsis">${c.translated}</div>
|
||||
<div class="text-muted small">
|
||||
${comment_when(c.creation)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
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(`
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th>${__("Source Text")}</th>
|
||||
<th>${__("Translated Text")}</th>
|
||||
</tr>
|
||||
${Object.values(this.edited_translations)
|
||||
.map(
|
||||
(t) => `
|
||||
<tr>
|
||||
<td>${t.source_text}</td>
|
||||
<td>${t.translated_text}</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
.join("")}
|
||||
</table>
|
||||
`);
|
||||
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) => `
|
||||
<div
|
||||
class="translation-item"
|
||||
data-message-id="${encodeURIComponent(message.name)}"
|
||||
data-action="show_translation_status_modal">
|
||||
<div class="bold ellipsis">
|
||||
<span class="indicator ${this.get_contribution_indicator_color(message)}">
|
||||
<span>${frappe.utils.escape_html(message.source_text)}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ export default class GridRow {
|
|||
this.make();
|
||||
}
|
||||
make() {
|
||||
var me = this;
|
||||
let me = this;
|
||||
let render_row = true;
|
||||
|
||||
this.wrapper = $('<div class="grid-row"></div>');
|
||||
this.row = $('<div class="data-row row"></div>')
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -584,7 +584,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
</div>
|
||||
`);
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue