Merge branch 'frappe:develop' into feat/report-view-bulk-column-select
This commit is contained in:
commit
0b57eaec71
168 changed files with 79473 additions and 11425 deletions
|
|
@ -61,3 +61,6 @@ e9bbe03354079cfcef65a77b0c33f57b047a7c93
|
|||
|
||||
# replace `frappe.flags.in_test` with `frappe.in_test`
|
||||
653c80b8483cc41aef25cd7d66b9b6bb188bf5f8
|
||||
|
||||
# another ruff update
|
||||
6ca4d4d167a1a009d99062747711de7a994aa633
|
||||
|
|
|
|||
2
.github/workflows/server-tests.yml
vendored
2
.github/workflows/server-tests.yml
vendored
|
|
@ -93,7 +93,7 @@ jobs:
|
|||
- frappe/hrms
|
||||
steps:
|
||||
- name: Dispatch Downstream CI (if supported)
|
||||
uses: peter-evans/repository-dispatch@v3
|
||||
uses: peter-evans/repository-dispatch@v4
|
||||
with:
|
||||
token: ${{ secrets.CI_PAT }}
|
||||
repository: ${{ matrix.repo }}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
exclude: 'node_modules|.git'
|
||||
default_stages: [pre-commit]
|
||||
default_install_hook_types: [pre-commit, commit-msg]
|
||||
fail_fast: false
|
||||
|
||||
|
||||
|
|
@ -21,7 +22,7 @@ repos:
|
|||
exclude: ^frappe/tests/classes/context_managers\.py$
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.8.1
|
||||
rev: v0.13.2
|
||||
hooks:
|
||||
- id: ruff
|
||||
name: "Run ruff import sorter"
|
||||
|
|
@ -69,6 +70,13 @@ repos:
|
|||
frappe/public/js/lib/.*
|
||||
)$
|
||||
|
||||
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
|
||||
rev: v9.22.0
|
||||
hooks:
|
||||
- id: commitlint
|
||||
stages: [commit-msg]
|
||||
additional_dependencies: ['conventional-changelog-conventionalcommits']
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: weekly
|
||||
skip: []
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
**/hooks.py,frappe.gettext.extractors.navbar.extract
|
||||
**/doctype/*/*.json,frappe.gettext.extractors.doctype.extract
|
||||
**/workspace/*/*.json,frappe.gettext.extractors.workspace.extract
|
||||
**/web_form/*/*.json,frappe.gettext.extractors.web_form.extract
|
||||
**/onboarding_step/*/*.json,frappe.gettext.extractors.onboarding_step.extract
|
||||
**/module_onboarding/*/*.json,frappe.gettext.extractors.module_onboarding.extract
|
||||
**/report/*/*.json,frappe.gettext.extractors.report.extract
|
||||
|
|
|
|||
|
|
|
@ -75,14 +75,15 @@ context("Form", () => {
|
|||
|
||||
cy.get('.frappe-control[data-fieldname="email_ids"]').as("table");
|
||||
cy.get("@table").find("button.grid-add-row").click();
|
||||
cy.get("@table").find("button.grid-add-row").click();
|
||||
cy.get("@table").find('[data-idx="1"]').as("row1");
|
||||
cy.get("@table").find('[data-idx="2"]').as("row2");
|
||||
|
||||
cy.get("@row1").click();
|
||||
cy.get("@row1").find("input.input-with-feedback.form-control").as("email_input1");
|
||||
|
||||
cy.get("@email_input1").type(website_input, { waitForAnimations: false });
|
||||
|
||||
cy.get("@table").find("button.grid-add-row").click();
|
||||
cy.get("@table").find('[data-idx="2"]').as("row2");
|
||||
cy.get("@row2").click();
|
||||
cy.get("@row2").find("input.input-with-feedback.form-control").as("email_input2");
|
||||
cy.get("@email_input2").type(valid_email, { waitForAnimations: false });
|
||||
|
|
|
|||
|
|
@ -321,7 +321,10 @@ def set_authenticate_headers(response: Response):
|
|||
def make_form_dict(request: Request):
|
||||
request_data = request.get_data(as_text=True)
|
||||
if request_data and request.is_json:
|
||||
args = orjson.loads(request_data)
|
||||
try:
|
||||
args = orjson.loads(request_data)
|
||||
except orjson.JSONDecodeError:
|
||||
frappe.throw(_("Invalid request body"), frappe.DataError)
|
||||
else:
|
||||
args = {}
|
||||
args.update(request.args or {})
|
||||
|
|
|
|||
|
|
@ -66,6 +66,9 @@ class HTTPRequest:
|
|||
elif frappe.get_request_header("REMOTE_ADDR"):
|
||||
frappe.local.request_ip = frappe.get_request_header("REMOTE_ADDR")
|
||||
|
||||
elif frappe.request and getattr(frappe.request, "remote_addr", None):
|
||||
frappe.local.request_ip = frappe.request.remote_addr
|
||||
|
||||
else:
|
||||
frappe.local.request_ip = "127.0.0.1"
|
||||
|
||||
|
|
@ -666,7 +669,7 @@ def validate_oauth(authorization_header):
|
|||
required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(
|
||||
get_url_delimiter()
|
||||
)
|
||||
valid, oauthlib_request = get_oauth_server().verify_request(
|
||||
valid, _oauthlib_request = get_oauth_server().verify_request(
|
||||
uri, http_method, body, headers, required_scopes
|
||||
)
|
||||
if valid:
|
||||
|
|
|
|||
|
|
@ -147,17 +147,26 @@ def _clear_doctype_cache_from_redis(doctype: str | None = None):
|
|||
clear_single(doctype)
|
||||
|
||||
# clear all parent doctypes
|
||||
for dt in frappe.get_all(
|
||||
"DocField", "parent", dict(fieldtype=["in", frappe.model.table_fields], options=doctype)
|
||||
):
|
||||
clear_single(dt.parent)
|
||||
|
||||
# clear all parent doctypes
|
||||
if not frappe.flags.in_install:
|
||||
try:
|
||||
for dt in frappe.get_all(
|
||||
"Custom Field", "dt", dict(fieldtype=["in", frappe.model.table_fields], options=doctype)
|
||||
"DocField",
|
||||
"parent",
|
||||
dict(fieldtype=["in", frappe.model.table_fields], options=doctype),
|
||||
ignore_ddl=True,
|
||||
):
|
||||
clear_single(dt.dt)
|
||||
clear_single(dt.parent)
|
||||
|
||||
# clear all parent doctypes
|
||||
if not frappe.flags.in_install:
|
||||
for dt in frappe.get_all(
|
||||
"Custom Field",
|
||||
"dt",
|
||||
dict(fieldtype=["in", frappe.model.table_fields], options=doctype),
|
||||
ignore_ddl=True,
|
||||
):
|
||||
clear_single(dt.dt)
|
||||
except frappe.DoesNotExistError:
|
||||
pass # core doctypes getting migrated.
|
||||
|
||||
# clear all notifications
|
||||
delete_notification_count_for(doctype)
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ def create_rq_users(set_admin_password=False, use_rq_auth=False):
|
|||
)
|
||||
click.secho(f"`export {env_key}={user_credentials['default'][1]}`")
|
||||
click.secho(
|
||||
"NOTE: Please save the admin password as you " "can not access redis server without the password",
|
||||
"NOTE: Please save the admin password as you can not access redis server without the password",
|
||||
fg="yellow",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -336,7 +336,7 @@ def restore_backup(
|
|||
# Check if the backup is of an older version of frappe and the user hasn't specified force
|
||||
if is_downgrade(sql_file_path, verbose=True) and not force:
|
||||
warn_message = (
|
||||
"This is not recommended and may lead to unexpected behaviour. " "Do you want to continue anyway?"
|
||||
"This is not recommended and may lead to unexpected behaviour. Do you want to continue anyway?"
|
||||
)
|
||||
click.confirm(warn_message, abort=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -297,7 +297,7 @@ class TestCommands(BaseTestCommands):
|
|||
self.execute("bench --site {test_site} backup --exclude 'ToDo'", site_data)
|
||||
site_data.update({"kw": "\"{'partial':True}\""})
|
||||
self.execute(
|
||||
"bench --site {test_site} execute" " frappe.utils.backups.fetch_latest_backups --kwargs {kw}",
|
||||
"bench --site {test_site} execute frappe.utils.backups.fetch_latest_backups --kwargs {kw}",
|
||||
site_data,
|
||||
)
|
||||
site_data.update({"database": json.loads(self.stdout)["database"]})
|
||||
|
|
|
|||
|
|
@ -435,8 +435,7 @@ def import_doc(context: CliCtxObj, path, force=False):
|
|||
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
|
||||
required=True,
|
||||
help=(
|
||||
"Path to import file (.csv, .xlsx)."
|
||||
"Consider that relative paths will resolve from 'sites' directory"
|
||||
"Path to import file (.csv, .xlsx). Consider that relative paths will resolve from 'sites' directory"
|
||||
),
|
||||
)
|
||||
@click.option("--doctype", type=str, required=True)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class TestAuditTrail(IntegrationTestCase):
|
|||
re_amended_doc = amend_document(amended_doc, changed_fields, {}, 1)
|
||||
|
||||
comparator = create_comparator_doc("Test Custom Doctype for Doc Comparator", re_amended_doc.name)
|
||||
documents, results = comparator.compare_document()
|
||||
_documents, results = comparator.compare_document()
|
||||
|
||||
test_field_values = results["changed"]["Field"]
|
||||
self.check_expected_values(test_field_values, ["first value", "second value", "third value"])
|
||||
|
|
@ -41,7 +41,7 @@ class TestAuditTrail(IntegrationTestCase):
|
|||
amended_doc = amend_document(doc, {}, rows_updated, 1)
|
||||
|
||||
comparator = create_comparator_doc("Test Custom Doctype for Doc Comparator", amended_doc.name)
|
||||
documents, results = comparator.compare_document()
|
||||
_documents, results = comparator.compare_document()
|
||||
|
||||
results = frappe._dict(results)
|
||||
self.check_rows_updated(results.row_changed)
|
||||
|
|
|
|||
|
|
@ -565,11 +565,11 @@ def parse_email(email_strings):
|
|||
|
||||
for email in email_string.split(","):
|
||||
local_part = email.split("@", 1)[0].strip('"')
|
||||
user, detail = None, None
|
||||
_user, detail = None, None
|
||||
if "+" in local_part:
|
||||
user, detail = local_part.split("+", 1)
|
||||
_user, detail = local_part.split("+", 1)
|
||||
elif "--" in local_part:
|
||||
detail, user = local_part.rsplit("--", 1)
|
||||
detail, _user = local_part.rsplit("--", 1)
|
||||
|
||||
if not detail:
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
"submit",
|
||||
"cancel",
|
||||
"amend",
|
||||
"mask",
|
||||
"additional_permissions",
|
||||
"report",
|
||||
"export",
|
||||
|
|
@ -153,6 +154,16 @@
|
|||
"print_width": "32px",
|
||||
"width": "32px"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "mask",
|
||||
"fieldtype": "Check",
|
||||
"label": "Mask",
|
||||
"oldfieldname": "mask",
|
||||
"oldfieldtype": "Check",
|
||||
"print_width": "32px",
|
||||
"width": "32px"
|
||||
},
|
||||
{
|
||||
"fieldname": "additional_permissions",
|
||||
"fieldtype": "Section Break",
|
||||
|
|
@ -214,11 +225,13 @@
|
|||
"label": "Select"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"links": [],
|
||||
"modified": "2024-03-23 16:02:14.726078",
|
||||
"modified": "2025-05-22 16:59:35.484376",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Custom DocPerm",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
|
@ -235,8 +248,9 @@
|
|||
}
|
||||
],
|
||||
"read_only": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"title_field": "parent"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ class CustomDocPerm(Document):
|
|||
email: DF.Check
|
||||
export: DF.Check
|
||||
if_owner: DF.Check
|
||||
mask: DF.Check
|
||||
parent: DF.Data | None
|
||||
permlevel: DF.Int
|
||||
print: DF.Check
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"is_virtual",
|
||||
"search_index",
|
||||
"not_nullable",
|
||||
"mask",
|
||||
"column_break_18",
|
||||
"options",
|
||||
"sort_options",
|
||||
|
|
@ -607,6 +608,13 @@
|
|||
"fieldname": "sticky",
|
||||
"fieldtype": "Check",
|
||||
"label": "Sticky"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:[\"Select\", \"Read Only\", \"Phone\", \"Percent\", \"Password\", \"Link\", \"Int\", \"Float\", \"Dynamic Link\", \"Duration\", \"Datetime\", \"Currency\", \"Data\", \"Date\"].includes(doc.fieldtype)",
|
||||
"fieldname": "mask",
|
||||
"fieldtype": "Check",
|
||||
"label": "Mask"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ class DocField(Document):
|
|||
link_filters: DF.JSON | None
|
||||
make_attachment_public: DF.Check
|
||||
mandatory_depends_on: DF.Code | None
|
||||
mask: DF.Check
|
||||
max_height: DF.Data | None
|
||||
no_copy: DF.Check
|
||||
non_negative: DF.Check
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
"submit",
|
||||
"cancel",
|
||||
"amend",
|
||||
"mask",
|
||||
"additional_permissions",
|
||||
"report",
|
||||
"export",
|
||||
|
|
@ -205,18 +206,27 @@
|
|||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Select"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "mask",
|
||||
"fieldtype": "Check",
|
||||
"label": "Mask"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-23 16:02:18.443496",
|
||||
"modified": "2025-05-20 16:50:32.679113",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocPerm",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ class DocPerm(Document):
|
|||
email: DF.Check
|
||||
export: DF.Check
|
||||
if_owner: DF.Check
|
||||
mask: DF.Check
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
|
|
|||
|
|
@ -702,7 +702,7 @@
|
|||
"label": "Protect Attached Files"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"default": "20",
|
||||
"depends_on": "istable",
|
||||
"fieldname": "rows_threshold_for_grid_search",
|
||||
"fieldtype": "Int",
|
||||
|
|
@ -792,7 +792,7 @@
|
|||
"link_fieldname": "document_type"
|
||||
}
|
||||
],
|
||||
"modified": "2025-07-19 12:23:16.296416",
|
||||
"modified": "2025-09-23 06:48:13.555017",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocType",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ frappe.listview_settings["DocType"] = {
|
|||
fieldtype: "Data",
|
||||
reqd: 1,
|
||||
default: doctype_name,
|
||||
length: 61,
|
||||
},
|
||||
{ fieldtype: "Column Break" },
|
||||
{
|
||||
|
|
|
|||
|
|
@ -856,6 +856,16 @@ class TestDocType(IntegrationTestCase):
|
|||
)
|
||||
self.assertRaises(frappe.ValidationError, doctype.insert)
|
||||
|
||||
def test_delete_doc_clears_cache(self):
|
||||
dt = new_doctype(
|
||||
fields=[{"fieldname": "test_fdname", "fieldtype": "Data", "label": "Test Field"}],
|
||||
).insert()
|
||||
frappe.get_meta(dt.name)
|
||||
frappe.delete_doc("DocType", dt.name, force=1, delete_permanently=False)
|
||||
frappe.db.commit()
|
||||
with self.assertRaises(frappe.DoesNotExistError):
|
||||
frappe.get_meta(dt.name)
|
||||
|
||||
|
||||
def new_doctype(
|
||||
name: str | None = None,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ from frappe.utils import (
|
|||
get_url,
|
||||
)
|
||||
from frappe.utils.file_manager import is_safe_path
|
||||
from frappe.utils.html_utils import escape_html
|
||||
from frappe.utils.image import optimize_image, strip_exif_data
|
||||
from frappe.utils.pdf import pdf_contains_js
|
||||
|
||||
|
|
@ -141,7 +142,6 @@ class File(Document):
|
|||
self.validate_file_url()
|
||||
self.validate_file_on_disk()
|
||||
self.file_size = frappe.form_dict.file_size or self.file_size
|
||||
self.check_content()
|
||||
|
||||
def validate_attachment_references(self):
|
||||
if not self.attached_to_doctype:
|
||||
|
|
@ -785,7 +785,7 @@ class File(Document):
|
|||
def create_attachment_record(self):
|
||||
icon = ' <i class="fa fa-lock text-warning"></i>' if self.is_private else ""
|
||||
file_url = quote(frappe.safe_encode(self.file_url), safe="/:") if self.file_url else self.file_name
|
||||
file_name = self.file_name or self.file_url
|
||||
file_name = escape_html(self.file_name or self.file_url)
|
||||
|
||||
self.add_comment_in_reference_doc(
|
||||
"Attachment",
|
||||
|
|
|
|||
7
frappe/core/doctype/file/file_list.js
Normal file
7
frappe/core/doctype/file/file_list.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
frappe.listview_settings["File"] = {
|
||||
formatters: {
|
||||
file_name: function (value) {
|
||||
return frappe.utils.escape_html(value || "");
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -164,6 +164,18 @@ class SubmissionQueue(Document):
|
|||
|
||||
|
||||
def queue_submission(doc: Document, action: str, alert: bool = True):
|
||||
if existing_queue := frappe.db.get_value(
|
||||
"Submission Queue", {"ref_doctype": doc.doctype, "ref_docname": doc.name, "status": "Queued"}
|
||||
):
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"This document has already been queued for submission. You can track the progress over {0}."
|
||||
).format(f"<a href='/app/submission-queue/{existing_queue}'><b>here</b></a>"),
|
||||
indicator="orange",
|
||||
alert=True,
|
||||
)
|
||||
return
|
||||
|
||||
queue = frappe.new_doc("Submission Queue")
|
||||
queue.ref_doctype = doc.doctype
|
||||
queue.ref_docname = doc.name
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
"apply_strict_user_permissions",
|
||||
"column_break_21",
|
||||
"allow_older_web_view_links",
|
||||
"show_external_link_warning",
|
||||
"security_tab",
|
||||
"security",
|
||||
"session_expiry",
|
||||
|
|
@ -744,12 +745,19 @@
|
|||
"fieldtype": "Int",
|
||||
"label": "Max signups allowed per hour",
|
||||
"non_negative": 1
|
||||
},
|
||||
{
|
||||
"default": "Never",
|
||||
"fieldname": "show_external_link_warning",
|
||||
"fieldtype": "Select",
|
||||
"label": "Show External Link Warning",
|
||||
"options": "Never\nAsk\nAlways"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-09-03 10:52:38.096662",
|
||||
"modified": "2025-09-24 16:04:02.016562",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "System Settings",
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ class SystemSettings(Document):
|
|||
session_expiry: DF.Data | None
|
||||
setup_complete: DF.Check
|
||||
show_absolute_datetime_in_timeline: DF.Check
|
||||
show_external_link_warning: DF.Literal["Never", "Ask", "Always"]
|
||||
store_attached_pdf_document: DF.Check
|
||||
strip_exif_metadata_from_uploaded_images: DF.Check
|
||||
time_format: DF.Literal["HH:mm:ss", "HH:mm"]
|
||||
|
|
|
|||
|
|
@ -280,7 +280,7 @@ frappe.PermissionEngine = class PermissionEngine {
|
|||
|
||||
add_check(cell, d, fieldname, label, description = "") {
|
||||
if (!label) label = toTitle(fieldname.replace(/_/g, " "));
|
||||
if (d.permlevel > 0 && ["read", "write"].indexOf(fieldname) == -1) {
|
||||
if (d.permlevel > 0 && ["read", "write", "mask"].indexOf(fieldname) == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -331,6 +331,7 @@ frappe.PermissionEngine = class PermissionEngine {
|
|||
"import",
|
||||
"export",
|
||||
"share",
|
||||
"mask",
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"label",
|
||||
"search_fields",
|
||||
"grid_page_length",
|
||||
"rows_threshold_for_grid_search",
|
||||
"link_filters",
|
||||
"column_break_5",
|
||||
"istable",
|
||||
|
|
@ -43,6 +44,7 @@
|
|||
"force_re_route_to_default_view",
|
||||
"column_break_29",
|
||||
"show_preview_popup",
|
||||
"show_name_in_global_search",
|
||||
"email_settings_section",
|
||||
"default_email_template",
|
||||
"column_break_26",
|
||||
|
|
@ -422,6 +424,19 @@
|
|||
"fieldname": "recipient_account_field",
|
||||
"fieldtype": "Data",
|
||||
"label": "Recipient Account Field"
|
||||
},
|
||||
{
|
||||
"depends_on": "istable",
|
||||
"fieldname": "rows_threshold_for_grid_search",
|
||||
"fieldtype": "Int",
|
||||
"label": "Rows Threshold for Grid Search",
|
||||
"non_negative": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_name_in_global_search",
|
||||
"fieldtype": "Check",
|
||||
"label": "Make \"name\" searchable in Global Search"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
|
|
@ -430,7 +445,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-19 12:23:41.564203",
|
||||
"modified": "2025-09-23 07:13:52.631903",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form",
|
||||
|
|
|
|||
|
|
@ -75,9 +75,11 @@ class CustomizeForm(Document):
|
|||
queue_in_background: DF.Check
|
||||
quick_entry: DF.Check
|
||||
recipient_account_field: DF.Data | None
|
||||
rows_threshold_for_grid_search: DF.Int
|
||||
search_fields: DF.Data | None
|
||||
sender_field: DF.Data | None
|
||||
sender_name_field: DF.Data | None
|
||||
show_name_in_global_search: DF.Check
|
||||
show_preview_popup: DF.Check
|
||||
show_title_field_in_link: DF.Check
|
||||
sort_field: DF.Literal[None]
|
||||
|
|
@ -306,6 +308,8 @@ class CustomizeForm(Document):
|
|||
)
|
||||
|
||||
def set_property_setters_for_doctype(self, meta):
|
||||
if self.get("show_name_in_global_search") != meta.get("show_name_in_global_search"):
|
||||
self.flags.rebuild_doctype_for_global_search = True
|
||||
for prop, prop_type in doctype_properties.items():
|
||||
if self.get(prop) != meta.get(prop):
|
||||
self.make_property_setter(prop, self.get(prop), prop_type)
|
||||
|
|
@ -735,6 +739,7 @@ doctype_properties = {
|
|||
"track_views": "Check",
|
||||
"allow_auto_repeat": "Check",
|
||||
"allow_import": "Check",
|
||||
"show_name_in_global_search": "Check",
|
||||
"show_preview_popup": "Check",
|
||||
"default_email_template": "Data",
|
||||
"email_append_to": "Check",
|
||||
|
|
@ -748,6 +753,7 @@ doctype_properties = {
|
|||
"force_re_route_to_default_view": "Check",
|
||||
"translated_doctype": "Check",
|
||||
"grid_page_length": "Int",
|
||||
"rows_threshold_for_grid_search": "Int",
|
||||
}
|
||||
|
||||
docfield_properties = {
|
||||
|
|
|
|||
|
|
@ -1192,6 +1192,7 @@ class Database:
|
|||
self.sql("commit")
|
||||
self.begin()
|
||||
|
||||
self.value_cache.clear()
|
||||
self.after_commit.run()
|
||||
|
||||
def rollback(self, *, save_point=None, chain=False):
|
||||
|
|
@ -1206,10 +1207,12 @@ class Database:
|
|||
|
||||
if chain:
|
||||
self.sql("rollback and chain")
|
||||
self.value_cache.clear()
|
||||
else:
|
||||
self.sql("rollback")
|
||||
self.begin()
|
||||
|
||||
self.value_cache.clear()
|
||||
self.after_rollback.run()
|
||||
else:
|
||||
warnings.warn(message=TRANSACTION_DISABLED_MSG, stacklevel=2)
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ class PostgresTable(DBTable):
|
|||
if isinstance(default, str):
|
||||
default = frappe.db.escape(default)
|
||||
change_nullability.append(
|
||||
f"ALTER COLUMN \"{col.fieldname}\" {'SET' if col.not_nullable else 'DROP'} NOT NULL"
|
||||
f'ALTER COLUMN "{col.fieldname}" {"SET" if col.not_nullable else "DROP"} NOT NULL'
|
||||
)
|
||||
change_nullability.append(f'ALTER COLUMN "{col.fieldname}" SET DEFAULT {default}')
|
||||
|
||||
|
|
|
|||
|
|
@ -288,6 +288,8 @@ class Engine:
|
|||
doctype: str | None = None,
|
||||
) -> "Criterion | None":
|
||||
"""Builds a pypika Criterion object for a simple filter condition."""
|
||||
import operator as builtin_operator
|
||||
|
||||
_field = self._validate_and_prepare_filter_field(field, doctype)
|
||||
_value = convert_to_value(value)
|
||||
_operator = operator
|
||||
|
|
@ -323,7 +325,7 @@ class Engine:
|
|||
|
||||
operator_fn = OPERATOR_MAP[_operator.casefold()]
|
||||
if _value is None and isinstance(_field, Field):
|
||||
return _field.isnull()
|
||||
return _field.isnotnull() if operator_fn == builtin_operator.ne else _field.isnull()
|
||||
else:
|
||||
return operator_fn(_field, _value)
|
||||
|
||||
|
|
|
|||
|
|
@ -443,6 +443,9 @@ def get_definition(fieldtype, precision=None, length=None, *, options=None):
|
|||
|
||||
if length:
|
||||
if coltype == "varchar":
|
||||
# Reference: https://mariadb.com/docs/server/server-usage/storage-engines/innodb/innodb-row-formats/troubleshooting-row-size-too-large-errors-with-innodb
|
||||
if cint(length) < 64:
|
||||
length = 64
|
||||
size = length
|
||||
elif coltype == "int" and length < 11:
|
||||
# allow setting custom length for int if length provided is less than 11
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ class SQLiteDatabase(SQLiteExceptionUtil, Database):
|
|||
conn = self.create_connection(read_only)
|
||||
conn.isolation_level = None
|
||||
conn.create_function("regexp", 2, regexp)
|
||||
conn.create_function("regexp_replace", 3, regexp_replace)
|
||||
pragmas = {
|
||||
"journal_mode": "WAL",
|
||||
"synchronous": "NORMAL",
|
||||
|
|
@ -583,3 +584,10 @@ def regexp(expr: str, item: str) -> bool:
|
|||
Although it works in the CLI - doesn't work through python
|
||||
"""
|
||||
return re.search(expr, item) is not None
|
||||
|
||||
|
||||
def regexp_replace(item: str, pattern: str, repl: str) -> str:
|
||||
"""
|
||||
Define regexp_replace implementation for SQLite
|
||||
"""
|
||||
return re.sub(pattern, repl, item)
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ class SQLiteTable(DBTable):
|
|||
if self.meta.sort_field == "modified" and not frappe.db.get_column_index(
|
||||
self.table_name, "modified", unique=False
|
||||
):
|
||||
index_queries.append(f"CREATE INDEX `modified` ON `{self.table_name}` (`modified`)")
|
||||
index_queries.append(f"CREATE INDEX IF NOT EXISTS `modified` ON `{self.table_name}` (`modified`)")
|
||||
|
||||
for query in index_queries:
|
||||
frappe.db.sql_ddl(query)
|
||||
|
|
|
|||
|
|
@ -899,7 +899,7 @@ def tests_utils_get_dependencies(doctype):
|
|||
import frappe
|
||||
from frappe.tests.utils.generators import get_modules
|
||||
|
||||
module, test_module = get_modules(doctype)
|
||||
_module, test_module = get_modules(doctype)
|
||||
meta = frappe.get_meta(doctype)
|
||||
link_fields = meta.get_link_fields()
|
||||
|
||||
|
|
|
|||
|
|
@ -622,8 +622,7 @@ def get_linked_fields(doctype, without_ignore_user_permissions_enabled=False):
|
|||
"DocField", fields=["parent", "options"], filters=child_filters, as_list=1
|
||||
):
|
||||
ret[parent] = {"child_doctype": options, "fieldname": links_dict[options]}
|
||||
if options in ret:
|
||||
del ret[options]
|
||||
ret.pop(options, None)
|
||||
|
||||
virtual_doctypes = frappe.get_all("DocType", {"is_virtual": 1}, pluck="name")
|
||||
for dt in virtual_doctypes:
|
||||
|
|
|
|||
|
|
@ -76,6 +76,9 @@ class FormMeta(Meta):
|
|||
for k in ASSET_KEYS:
|
||||
d[k] = __dict.get(k)
|
||||
|
||||
# add masked fields (per-user, per-meta)
|
||||
d["masked_fields"] = [df.fieldname for df in self.get_masked_fields()]
|
||||
|
||||
return d
|
||||
|
||||
def add_code(self):
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ def get_group_by_count(doctype: str, current_filters: str, field: str) -> list[d
|
|||
break
|
||||
|
||||
if owner_idx:
|
||||
data = [data.pop(owner_idx)] + data[0:49]
|
||||
data = [data.pop(owner_idx), *data[0:49]]
|
||||
else:
|
||||
data = data[0:50]
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!-- jinja -->
|
||||
<div class="row download-backups">
|
||||
<div class="row download-backups py-4 px-4 m-0">
|
||||
{% for f in files %}
|
||||
<div class="col-lg-3 col-md-4 col-12">
|
||||
<div class="col-lg-3 col-md-4 col-12 pr-4 pl-0">
|
||||
<a href="{{ f[0] }}" target="_blank" rel="noopener noreferrer" class="frappe-card download-backup-card">
|
||||
<div>
|
||||
{{ f[1] }}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from frappe.utils.data import convert_utc_to_system_timezone
|
|||
|
||||
def get_time(path: Path):
|
||||
return convert_utc_to_system_timezone(
|
||||
datetime.datetime.fromtimestamp(path.stat().st_mtime, tz=datetime.UTC)
|
||||
datetime.datetime.fromtimestamp(path.stat().st_mtime, tz=datetime.timezone.utc)
|
||||
).strftime("%a %b %d %H:%M %Y")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -674,9 +674,20 @@ def get_filtered_data(ref_doctype, columns, data, user):
|
|||
shared = frappe.share.get_shared(ref_doctype, user)
|
||||
columns_dict = get_columns_dict(columns)
|
||||
|
||||
role_permissions = get_role_permissions(frappe.get_meta(ref_doctype), user)
|
||||
ref_doctype_meta = frappe.get_meta(ref_doctype)
|
||||
|
||||
role_permissions = get_role_permissions(ref_doctype_meta, user)
|
||||
if_owner = role_permissions.get("if_owner", {}).get("report")
|
||||
|
||||
if ref_doctype_meta.get_masked_fields():
|
||||
from frappe.model.db_query import mask_field_value
|
||||
|
||||
# Apply masking to the fields
|
||||
for field in ref_doctype_meta.get_masked_fields():
|
||||
for row in data:
|
||||
val = row.get(field.fieldname)
|
||||
row[field.fieldname] = mask_field_value(field, val)
|
||||
|
||||
if match_filters_per_doctype:
|
||||
for row in data:
|
||||
# Why linked_doctypes.get(ref_doctype)? because if column is empty, linked_doctypes[ref_doctype] is removed
|
||||
|
|
|
|||
|
|
@ -376,6 +376,7 @@ def export_query():
|
|||
|
||||
form_params = get_form_params()
|
||||
form_params["limit_page_length"] = None
|
||||
|
||||
form_params["as_list"] = True
|
||||
csv_params = pop_csv_params(form_params)
|
||||
export_in_background = int(form_params.pop("export_in_background", 0))
|
||||
|
|
@ -547,7 +548,7 @@ def get_field_info(fields, doctype):
|
|||
if parenttype != doctype:
|
||||
# If the column is from a child table, append the child doctype.
|
||||
# For example, "Item Code (Sales Invoice Item)".
|
||||
label += f" ({ _(parenttype) })"
|
||||
label += f" ({_(parenttype)})"
|
||||
|
||||
field_info.append(
|
||||
{"name": name, "label": label, "fieldtype": fieldtype, "translatable": translatable}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ from frappe import _
|
|||
def get_all_nodes(doctype, label, parent, tree_method, **filters):
|
||||
"""Recursively gets all data from tree nodes"""
|
||||
|
||||
if "cmd" in filters:
|
||||
del filters["cmd"]
|
||||
filters.pop("cmd", None)
|
||||
filters.pop("data", None)
|
||||
|
||||
tree_method = frappe.get_attr(tree_method)
|
||||
|
|
@ -20,8 +19,7 @@ def get_all_nodes(doctype, label, parent, tree_method, **filters):
|
|||
data = tree_method(doctype, parent, **filters)
|
||||
out = [dict(parent=label, data=data)]
|
||||
|
||||
if "is_root" in filters:
|
||||
del filters["is_root"]
|
||||
filters.pop("is_root", None)
|
||||
to_check = [d.get("value") for d in data if d.get("expandable")]
|
||||
|
||||
while to_check:
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class TestEmailQueue(IntegrationTestCase):
|
|||
Subject: {subject}
|
||||
From: Test <test@example.com>
|
||||
To: <!--recipient-->
|
||||
Date: {frappe.utils.now_datetime().strftime('%a, %d %b %Y %H:%M:%S %z')}
|
||||
Date: {frappe.utils.now_datetime().strftime("%a, %d %b %Y %H:%M:%S %z")}
|
||||
Reply-To: test@example.com
|
||||
X-Frappe-Site: {frappe.local.site}
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -352,7 +352,9 @@ def get_context(context):
|
|||
To queue a notification from a server script:
|
||||
|
||||
```python
|
||||
notification = frappe.get_doc("Notification", "My Notification", ignore_permissions=True)
|
||||
notification = frappe.get_doc(
|
||||
"Notification", "My Notification", ignore_permissions=True
|
||||
)
|
||||
notification.queue_send(customer)
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ class EMail:
|
|||
"""Append the message with MIME content to the root node (as attachment)"""
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
maintype, subtype = mime_type.split("/")
|
||||
_maintype, subtype = mime_type.split("/")
|
||||
part = MIMEText(message, _subtype=subtype, policy=policy.SMTP)
|
||||
|
||||
if as_attachment:
|
||||
|
|
@ -445,7 +445,7 @@ def add_attachment(fname, fcontent, content_type=None, parent=None, content_id=N
|
|||
from email.mime.text import MIMEText
|
||||
|
||||
if not content_type:
|
||||
content_type, encoding = mimetypes.guess_type(fname)
|
||||
content_type, _encoding = mimetypes.guess_type(fname)
|
||||
|
||||
if not parent:
|
||||
return
|
||||
|
|
@ -597,7 +597,7 @@ def get_header(header=None):
|
|||
if not title:
|
||||
title = frappe.get_hooks("app_title")[-1]
|
||||
|
||||
email_header, text = get_email_from_template(
|
||||
email_header, _text = get_email_from_template(
|
||||
"email_header", {"header_title": title, "indicator": indicator}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ class EmailServer:
|
|||
readonly = self.settings.email_sync_rule != "UNSEEN"
|
||||
|
||||
self.imap.select(folder, readonly=readonly)
|
||||
response, message = self.imap.uid("search", None, self.settings.email_sync_rule)
|
||||
_response, message = self.imap.uid("search", None, self.settings.email_sync_rule)
|
||||
if message[0]:
|
||||
email_list = message[0].split()
|
||||
else:
|
||||
|
|
@ -217,7 +217,7 @@ class EmailServer:
|
|||
# compare the UIDVALIDITY of email account and imap server
|
||||
uid_validity = self.settings.uid_validity
|
||||
|
||||
response, message = self.imap.status(folder, "(UIDVALIDITY UIDNEXT)")
|
||||
_response, message = self.imap.status(folder, "(UIDVALIDITY UIDNEXT)")
|
||||
current_uid_validity = self.parse_imap_response("UIDVALIDITY", message[0]) or 0
|
||||
|
||||
uidnext = int(self.parse_imap_response("UIDNEXT", message[0]) or "1")
|
||||
|
|
@ -270,7 +270,7 @@ class EmailServer:
|
|||
def retrieve_message(self, uid, msg_num, folder):
|
||||
try:
|
||||
if cint(self.settings.use_imap):
|
||||
status, message = self.imap.uid("fetch", uid, "(BODY.PEEK[] BODY.PEEK[HEADER] FLAGS)")
|
||||
_status, message = self.imap.uid("fetch", uid, "(BODY.PEEK[] BODY.PEEK[HEADER] FLAGS)")
|
||||
raw = message[0]
|
||||
|
||||
self.get_email_seen_status(uid, raw[0])
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ ml,മലയാളം,0
|
|||
mn,Монгол,0
|
||||
mr,मराठी,0
|
||||
ms,Melayu,0
|
||||
my,မြန်မာ,0
|
||||
my,မြန်မာ1
|
||||
nb,Norsk Bokmål,1
|
||||
nl,Nederlands,0
|
||||
no,Norsk,0
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 55.
|
73
frappe/gettext/extractors/web_form.py
Normal file
73
frappe/gettext/extractors/web_form.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import json
|
||||
|
||||
|
||||
def extract(fileobj, *args, **kwargs):
|
||||
"""
|
||||
Extract messages from Web Form JSON files. To be used to babel extractor
|
||||
:param fileobj: the file-like object the messages should be extracted from
|
||||
:rtype: `iterator`
|
||||
"""
|
||||
data = json.load(fileobj)
|
||||
|
||||
if isinstance(data, list):
|
||||
return
|
||||
|
||||
if data.get("doctype") != "Web Form":
|
||||
return
|
||||
|
||||
web_form_name = data.get("name")
|
||||
|
||||
# Extract main web form fields
|
||||
if title := data.get("title"):
|
||||
yield None, "_", title, [f"Title of the {web_form_name} Web Form"]
|
||||
|
||||
if introduction_text := data.get("introduction_text"):
|
||||
yield None, "_", introduction_text, [f"Introduction text of the {web_form_name} Web Form"]
|
||||
|
||||
if success_message := data.get("success_message"):
|
||||
yield None, "_", success_message, [f"Success message of the {web_form_name} Web Form"]
|
||||
|
||||
if success_title := data.get("success_title"):
|
||||
yield None, "_", success_title, [f"Success title of the {web_form_name} Web Form"]
|
||||
|
||||
if list_title := data.get("list_title"):
|
||||
yield None, "_", list_title, [f"List title of the {web_form_name} Web Form"]
|
||||
|
||||
if button_label := data.get("button_label"):
|
||||
yield None, "_", button_label, [f"Button label of the {web_form_name} Web Form"]
|
||||
|
||||
if meta_title := data.get("meta_title"):
|
||||
yield None, "_", meta_title, [f"Meta title of the {web_form_name} Web Form"]
|
||||
|
||||
if meta_description := data.get("meta_description"):
|
||||
yield None, "_", meta_description, [f"Meta description of the {web_form_name} Web Form"]
|
||||
|
||||
# Extract web form fields
|
||||
for field in data.get("web_form_fields", []):
|
||||
if label := field.get("label"):
|
||||
yield None, "_", label, [f"Label of a field in the {web_form_name} Web Form"]
|
||||
|
||||
if description := field.get("description"):
|
||||
yield None, "_", description, [f"Description of a field in the {web_form_name} Web Form"]
|
||||
|
||||
# Extract options for Select fields
|
||||
if field.get("fieldtype") == "Select" and (options := field.get("options")):
|
||||
skip_options = (
|
||||
web_form_name == "edit-profile" and field.get("fieldname") == "time_zone"
|
||||
) # Dumb workaround for avoiding a flood of strings from this field
|
||||
if isinstance(options, str) and not skip_options:
|
||||
# Handle both single values and newline-separated values
|
||||
option_list = options.split("\n") if "\n" in options else [options]
|
||||
for option in option_list:
|
||||
if option.strip():
|
||||
yield (
|
||||
None,
|
||||
"_",
|
||||
option.strip(),
|
||||
[f"Option in a Select field in the {web_form_name} Web Form"],
|
||||
)
|
||||
|
||||
# Extract list columns
|
||||
for column in data.get("list_columns", []):
|
||||
if isinstance(column, dict) and (label := column.get("label")):
|
||||
yield None, "_", label, [f"Label of a list column in the {web_form_name} Web Form"]
|
||||
|
|
@ -34,7 +34,7 @@ class Here:
|
|||
"label": address["label"],
|
||||
"value": json.dumps(
|
||||
{
|
||||
"address_line1": f'{address.get("street", "")} {address.get("houseNumber", "")}'.strip(),
|
||||
"address_line1": f"{address.get('street', '')} {address.get('houseNumber', '')}".strip(),
|
||||
"city": address.get("city", ""),
|
||||
"state": address.get("state", ""),
|
||||
"pincode": address.get("postalCode", ""),
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class Nomatim:
|
|||
"label": result["display_name"],
|
||||
"value": json.dumps(
|
||||
{
|
||||
"address_line1": f'{address.get("road")} {address.get("house_number", "")}'.strip(),
|
||||
"address_line1": f"{address.get('road')} {address.get('house_number', '')}".strip(),
|
||||
"city": address.get("city") or address.get("town") or address.get("village"),
|
||||
"state": address.get("state"),
|
||||
"pincode": address.get("postcode"),
|
||||
|
|
|
|||
|
|
@ -278,13 +278,14 @@ class LDAPSettings(Document):
|
|||
elif self.ldap_directory_server.lower() == "openldap":
|
||||
ldap_object_class = "posixgroup"
|
||||
ldap_group_members_attribute = "memberuid"
|
||||
user_search_str = getattr(user, self.ldap_username_field).value
|
||||
user_search_str = escape_filter_chars(getattr(user, self.ldap_username_field).value)
|
||||
|
||||
elif self.ldap_directory_server.lower() == "custom":
|
||||
ldap_object_class = self.ldap_group_objectclass
|
||||
ldap_group_members_attribute = self.ldap_group_member_attribute
|
||||
ldap_custom_group_search = self.ldap_custom_group_search or "{0}"
|
||||
user_search_str = ldap_custom_group_search.format(getattr(user, self.ldap_username_field).value)
|
||||
user_value = escape_filter_chars(getattr(user, self.ldap_username_field).value)
|
||||
user_search_str = ldap_custom_group_search.format(user_value)
|
||||
|
||||
else:
|
||||
# NOTE: depreciate this else path
|
||||
|
|
@ -308,6 +309,7 @@ class LDAPSettings(Document):
|
|||
if not self.enabled:
|
||||
frappe.throw(_("LDAP is not enabled."))
|
||||
|
||||
username = escape_filter_chars(username)
|
||||
user_filter = self.ldap_search_string.format(username)
|
||||
ldap_attributes = self.get_ldap_attributes()
|
||||
conn = self.connect_to_ldap(self.base_dn, self.get_password(raise_exception=False))
|
||||
|
|
@ -335,7 +337,8 @@ class LDAPSettings(Document):
|
|||
except LDAPInvalidCredentialsResult:
|
||||
frappe.throw(_("Invalid username or password"))
|
||||
|
||||
def reset_password(self, user, password, logout_sessions=False):
|
||||
def reset_password(self, user: str, password: str, logout_sessions: int = 0):
|
||||
user = escape_filter_chars(user)
|
||||
search_filter = f"({self.ldap_email_field}={user})"
|
||||
|
||||
conn = self.connect_to_ldap(self.base_dn, self.get_password(raise_exception=False), read_only=False)
|
||||
|
|
@ -420,7 +423,7 @@ def login():
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def reset_password(user, password, logout):
|
||||
def reset_password(user: str, password: str, logout: int):
|
||||
ldap: LDAPSettings = frappe.get_doc("LDAP Settings")
|
||||
if not ldap.enabled:
|
||||
frappe.throw(_("LDAP is not enabled."))
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ class LDAP_TestCase:
|
|||
function_return = self.test_class.connect_to_ldap(
|
||||
base_dn=self.base_dn, password=self.base_password
|
||||
)
|
||||
args, kwargs = ldap3_connection_method.call_args
|
||||
_args, kwargs = ldap3_connection_method.call_args
|
||||
|
||||
for connection_arg in kwargs:
|
||||
if (
|
||||
|
|
@ -305,7 +305,7 @@ class LDAP_TestCase:
|
|||
base_dn=self.base_dn, password=self.base_password, read_only=False
|
||||
)
|
||||
|
||||
args, kwargs = ldap3_connection_method.call_args
|
||||
_args, kwargs = ldap3_connection_method.call_args
|
||||
|
||||
self.assertFalse(
|
||||
kwargs["read_only"],
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ def approve(*args, **kwargs):
|
|||
frappe.flags.oauth_credentials,
|
||||
) = get_oauth_server().validate_authorization_request(r.url, r.method, r.get_data(), r.headers)
|
||||
|
||||
headers, body, status = get_oauth_server().create_authorization_response(
|
||||
headers, _body, _status = get_oauth_server().create_authorization_response(
|
||||
uri=frappe.flags.oauth_credentials["redirect_uri"],
|
||||
body=r.get_data(),
|
||||
headers=r.headers,
|
||||
|
|
@ -144,7 +144,7 @@ def authorize(**kwargs):
|
|||
def get_token(*args, **kwargs):
|
||||
try:
|
||||
r = frappe.request
|
||||
headers, body, status = get_oauth_server().create_token_response(
|
||||
_headers, body, _status = get_oauth_server().create_token_response(
|
||||
r.url, r.method, r.form, r.headers, frappe.flags.oauth_credentials
|
||||
)
|
||||
body = frappe._dict(json.loads(body))
|
||||
|
|
@ -165,7 +165,7 @@ def get_token(*args, **kwargs):
|
|||
def revoke_token(*args, **kwargs):
|
||||
try:
|
||||
r = frappe.request
|
||||
headers, body, status = get_oauth_server().create_revocation_response(
|
||||
_headers, _body, status = get_oauth_server().create_revocation_response(
|
||||
r.url,
|
||||
headers=r.headers,
|
||||
body=r.form,
|
||||
|
|
@ -184,7 +184,7 @@ def revoke_token(*args, **kwargs):
|
|||
def openid_profile(*args, **kwargs):
|
||||
try:
|
||||
r = frappe.request
|
||||
headers, body, status = get_oauth_server().create_userinfo_response(
|
||||
_headers, body, _status = get_oauth_server().create_userinfo_response(
|
||||
r.url,
|
||||
headers=r.headers,
|
||||
body=r.form,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1011
frappe/locale/hu.po
1011
frappe/locale/hu.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
31819
frappe/locale/my.po
Normal file
31819
frappe/locale/my.po
Normal file
File diff suppressed because it is too large
Load diff
2015
frappe/locale/nb.po
2015
frappe/locale/nb.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
31819
frappe/locale/ta.po
Normal file
31819
frappe/locale/ta.po
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1399,7 +1399,7 @@ class BaseDocument:
|
|||
else:
|
||||
return True
|
||||
|
||||
def reset_values_if_no_permlevel_access(self, has_access_to, high_permlevel_fields):
|
||||
def reset_values_if_no_permlevel_access(self, has_access_to, high_permlevel_fields, mask_fields=None):
|
||||
"""If the user does not have permissions at permlevel > 0, then reset the values to original / default"""
|
||||
to_reset = [
|
||||
df
|
||||
|
|
@ -1411,22 +1411,38 @@ class BaseDocument:
|
|||
)
|
||||
]
|
||||
|
||||
if to_reset:
|
||||
if self.is_new():
|
||||
# if new, set default value
|
||||
ref_doc = frappe.new_doc(self.doctype)
|
||||
else:
|
||||
# get values from old doc
|
||||
if self.parent_doc:
|
||||
parent_doc = self.parent_doc.get_latest()
|
||||
child_docs = [d for d in parent_doc.get(self.parentfield) if d.name == self.name]
|
||||
if not child_docs:
|
||||
return
|
||||
ref_doc = child_docs[0]
|
||||
else:
|
||||
ref_doc = self.get_latest()
|
||||
if not mask_fields:
|
||||
mask_fields = []
|
||||
|
||||
for df in to_reset:
|
||||
to_reset = to_reset + mask_fields
|
||||
|
||||
if not to_reset:
|
||||
return
|
||||
|
||||
if self.is_new():
|
||||
# if new, set default value
|
||||
ref_doc = frappe.new_doc(self.doctype)
|
||||
else:
|
||||
# get values from old doc
|
||||
if self.parent_doc:
|
||||
parent_doc = self.parent_doc.get_latest()
|
||||
child_docs = [d for d in parent_doc.get(self.parentfield) if d.name == self.name]
|
||||
if not child_docs:
|
||||
return
|
||||
ref_doc = child_docs[0]
|
||||
else:
|
||||
ref_doc = self.get_latest()
|
||||
|
||||
masked_fieldnames = [df.fieldname for df in to_reset if df.get("mask_readonly")]
|
||||
ref_values = {}
|
||||
if not self.is_new() and masked_fieldnames:
|
||||
ref_values = frappe.db.get_value(self.doctype, self.name, masked_fieldnames, as_dict=True) or {}
|
||||
|
||||
for df in to_reset:
|
||||
if df.get("mask_readonly") and not self.is_new():
|
||||
if df.fieldname in ref_values:
|
||||
self.set(df.fieldname, ref_values[df.fieldname])
|
||||
else:
|
||||
self.set(df.fieldname, ref_doc.get(df.fieldname))
|
||||
|
||||
def get_value(self, fieldname):
|
||||
|
|
|
|||
|
|
@ -219,8 +219,57 @@ class DatabaseQuery:
|
|||
if pluck:
|
||||
return [d[pluck] for d in result]
|
||||
|
||||
if self.doctype and result:
|
||||
result = self.mask_fields(result)
|
||||
|
||||
return result
|
||||
|
||||
def mask_fields(self, result):
|
||||
"""Mask fields in the result based on the doctype's masked fields"""
|
||||
masked_fields = self.get_masked_fields()
|
||||
|
||||
if not masked_fields:
|
||||
return result
|
||||
|
||||
if self.as_list:
|
||||
masked_result = []
|
||||
field_index_map = {}
|
||||
for idx, field in enumerate(self.fields):
|
||||
# handle aliases (e.g. `tabSI`.`posting_date` as posting_date)
|
||||
if " as " in field.lower():
|
||||
alias = field.split(" as ")[1].strip(" '")
|
||||
field_index_map[alias] = idx
|
||||
else:
|
||||
# extract last part after `.`
|
||||
col = field.split(".")[-1].strip("`")
|
||||
field_index_map[col] = idx
|
||||
# if as_list then we don't have field names in the result so we need to mask by position
|
||||
for row in result:
|
||||
row = list(row) # convert tuple to list mutable
|
||||
for field in masked_fields:
|
||||
if field.fieldname in field_index_map:
|
||||
idx = field_index_map[field.fieldname]
|
||||
val = row[idx]
|
||||
row[idx] = mask_field_value(field, val)
|
||||
|
||||
masked_result.append(tuple(row)) # convert back to tuple
|
||||
result = masked_result
|
||||
else:
|
||||
for row in result:
|
||||
for field in masked_fields:
|
||||
if field.fieldname in row:
|
||||
val = row[field.fieldname]
|
||||
row[field.fieldname] = mask_field_value(field, val)
|
||||
|
||||
return result
|
||||
|
||||
def get_masked_fields(self):
|
||||
"""Get masked fields for the doctype"""
|
||||
|
||||
meta = self.get_meta(self.doctype)
|
||||
|
||||
return meta.get_masked_fields()
|
||||
|
||||
def build_and_run(self):
|
||||
args = self.prepare_args()
|
||||
args.limit = self.add_limit()
|
||||
|
|
@ -394,8 +443,6 @@ from {tables}
|
|||
"concat",
|
||||
"concat_ws",
|
||||
"if",
|
||||
"ifnull",
|
||||
"nullif",
|
||||
"coalesce",
|
||||
"connection_id",
|
||||
"current_user",
|
||||
|
|
@ -425,16 +472,19 @@ from {tables}
|
|||
if SUB_QUERY_PATTERN.match(field):
|
||||
# Check for subquery anywhere in the field, not just at the beginning
|
||||
if "(" in lower_field:
|
||||
location = lower_field.index("(")
|
||||
subquery_token = lower_field[location + 1 :].lstrip().split(" ", 1)[0]
|
||||
if any(keyword in subquery_token for keyword in blacklisted_keywords):
|
||||
_raise_exception()
|
||||
|
||||
function = lower_field.split("(", 1)[0].rstrip()
|
||||
if function in blacklisted_functions:
|
||||
frappe.throw(
|
||||
_("Use of function {0} in field is restricted").format(function), exc=frappe.DataError
|
||||
)
|
||||
# Check all parentheses pairs, not just the first one
|
||||
paren_start = 0
|
||||
while True:
|
||||
location = lower_field.find("(", paren_start)
|
||||
if location == -1:
|
||||
break
|
||||
token = lower_field[location + 1 :].lstrip().split(" ", 1)[0]
|
||||
if any(
|
||||
re.search(r"\b" + re.escape(keyword) + r"\b", token)
|
||||
for keyword in blacklisted_keywords + blacklisted_functions
|
||||
):
|
||||
_raise_exception()
|
||||
paren_start = location + 1
|
||||
|
||||
if "@" in lower_field:
|
||||
# prevent access to global variables
|
||||
|
|
@ -622,6 +672,7 @@ from {tables}
|
|||
ignore_virtual=True,
|
||||
)
|
||||
)
|
||||
|
||||
permitted_child_table_fields = {}
|
||||
|
||||
# Create a copy of the fields list and reverse it to avoid index issues when removing fields
|
||||
|
|
@ -1126,7 +1177,12 @@ from {tables}
|
|||
r"select\b.*\bfrom",
|
||||
}
|
||||
|
||||
if any(re.search(r"\b" + pattern + r"\b", _lower) for pattern in subquery_indicators):
|
||||
# Replace doctype names with a hardcoded string "doc"
|
||||
# This is to avoid false positives based on doctype name
|
||||
sanitized = re.sub(r"`tab[^`]*`", " doc ", _lower)
|
||||
|
||||
# Run the subquery checks against the sanitized string
|
||||
if any(re.search(r"\b" + pattern + r"\b", sanitized) for pattern in subquery_indicators):
|
||||
frappe.throw(_("Cannot use sub-query here."))
|
||||
|
||||
blacklisted_sql_functions = {
|
||||
|
|
@ -1194,6 +1250,26 @@ from {tables}
|
|||
update_user_settings(self.doctype, user_settings)
|
||||
|
||||
|
||||
def mask_field_value(field, val):
|
||||
if not val:
|
||||
return val
|
||||
|
||||
if field.fieldtype == "Data" and field.options == "Phone":
|
||||
if len(val) > 3:
|
||||
return val[:3] + "XXXXXX"
|
||||
else:
|
||||
return "X" * len(val)
|
||||
elif field.fieldtype == "Data" and field.options == "Email":
|
||||
email = val.split("@")
|
||||
return "XXXXXX@" + email[1] if len(email) > 1 else "XXXXXX"
|
||||
elif field.fieldtype == "Date":
|
||||
return "XX-XX-XXXX"
|
||||
elif field.fieldtype == "Time":
|
||||
return "XX:XX"
|
||||
else:
|
||||
return "XXXXXXXX"
|
||||
|
||||
|
||||
def cast_name(column: str) -> str:
|
||||
"""Casts name field to varchar for postgres
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import os
|
||||
import shutil
|
||||
from typing import Any
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
|
|
@ -20,19 +21,58 @@ from frappe.utils.password import delete_all_passwords_for
|
|||
|
||||
|
||||
def delete_doc(
|
||||
doctype=None,
|
||||
name=None,
|
||||
force=0,
|
||||
ignore_doctypes=None,
|
||||
for_reload=False,
|
||||
ignore_permissions=False,
|
||||
flags=None,
|
||||
ignore_on_trash=False,
|
||||
ignore_missing=True,
|
||||
delete_permanently=False,
|
||||
):
|
||||
doctype: str | None = None,
|
||||
name: str | int | list[str | int] | None = None,
|
||||
force: int | bool = 0,
|
||||
ignore_doctypes: list[str] | None = None,
|
||||
for_reload: bool = False,
|
||||
ignore_permissions: bool = False,
|
||||
flags: dict[str, Any] | None = None,
|
||||
ignore_on_trash: bool = False,
|
||||
ignore_missing: bool = True,
|
||||
delete_permanently: bool = False,
|
||||
) -> bool | None:
|
||||
"""
|
||||
Deletes a doc(dt, dn) and validates if it is not submitted and not linked in a live record
|
||||
Deletes a document and validates if it is not submitted and not linked in a live record.
|
||||
|
||||
Args:
|
||||
doctype (str, optional): The document type to delete. If not provided,
|
||||
retrieved from frappe.form_dict.get("dt"). Defaults to None.
|
||||
name (str | int | list, optional): The name/ID of the document(s) to delete.
|
||||
Can be a single name or a list of names. If not provided,
|
||||
retrieved from frappe.form_dict.get("dn"). Defaults to None.
|
||||
force (bool, optional): When True, bypasses link existence checks and allows
|
||||
deletion of documents that are linked to other records. Also allows
|
||||
deletion of standard DocTypes. Defaults to 0 (False).
|
||||
ignore_doctypes (list, optional): A list of child doctypes to ignore when
|
||||
deleting child table records associated with the document. Defaults to None.
|
||||
for_reload (bool, optional): When True, indicates the deletion is for reloading
|
||||
purposes (like during doctype updates). Skips certain validations like
|
||||
permissions and on_trash methods, and automatically sets delete_permanently=True.
|
||||
Defaults to False.
|
||||
ignore_permissions (bool, optional): When True, bypasses permission checks
|
||||
during deletion. Useful for system operations. Defaults to False.
|
||||
flags (dict, optional): Additional flags to set on the document during the
|
||||
deletion process. These flags affect document behavior during deletion.
|
||||
Defaults to None.
|
||||
ignore_on_trash (bool, optional): When True, skips calling the document's
|
||||
on_trash method, which typically contains cleanup logic. Defaults to False.
|
||||
ignore_missing (bool, optional): When True, doesn't raise an error if the
|
||||
document doesn't exist and returns False. When False, raises
|
||||
frappe.DoesNotExistError if document is missing. Defaults to True.
|
||||
delete_permanently (bool, optional): When True, permanently deletes the document
|
||||
without adding it to the "Deleted Document" table for recovery purposes.
|
||||
When False, the document is soft-deleted and can be recovered. Defaults to False.
|
||||
|
||||
Raises:
|
||||
frappe.DoesNotExistError: When document doesn't exist and ignore_missing=False.
|
||||
frappe.LinkExistsError: When document is linked to other records and force=False.
|
||||
frappe.PermissionError: When user doesn't have delete permissions and ignore_permissions=False.
|
||||
frappe.ValidationError: When trying to delete a submitted document.
|
||||
frappe.QueryTimeoutError: When document is locked by another user.
|
||||
|
||||
Returns:
|
||||
bool: False if document doesn't exist and ignore_missing=True, otherwise None.
|
||||
"""
|
||||
if not ignore_doctypes:
|
||||
ignore_doctypes = []
|
||||
|
|
@ -104,6 +144,8 @@ def delete_doc(
|
|||
# in case a doctype doesnt have any controller code nor any app and module
|
||||
pass
|
||||
|
||||
frappe.clear_cache(doctype=name)
|
||||
|
||||
else:
|
||||
# Lock the doc without waiting
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -268,8 +268,20 @@ class Document(BaseDocument):
|
|||
if hasattr(self, "__setup__"):
|
||||
self.__setup__()
|
||||
|
||||
if not is_doctype:
|
||||
self.mask_fields()
|
||||
|
||||
return self
|
||||
|
||||
def mask_fields(self):
|
||||
from frappe.model.db_query import mask_field_value
|
||||
|
||||
mask_fields = frappe.get_meta(self.doctype).get_masked_fields()
|
||||
|
||||
for field in mask_fields:
|
||||
val = self.get(field.fieldname)
|
||||
self.set(field.fieldname, mask_field_value(field, val))
|
||||
|
||||
def load_children_from_db(self):
|
||||
is_doctype = self.doctype == "DocType"
|
||||
|
||||
|
|
@ -909,8 +921,10 @@ class Document(BaseDocument):
|
|||
has_access_to = self.get_permlevel_access()
|
||||
high_permlevel_fields = self.meta.get_high_permlevel_fields()
|
||||
|
||||
if high_permlevel_fields:
|
||||
self.reset_values_if_no_permlevel_access(has_access_to, high_permlevel_fields)
|
||||
mask_fields = self.meta.get_masked_fields()
|
||||
|
||||
if high_permlevel_fields or mask_fields:
|
||||
self.reset_values_if_no_permlevel_access(has_access_to, high_permlevel_fields, mask_fields)
|
||||
|
||||
# If new record then don't reset the values for child table
|
||||
if self.is_new():
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ dynamic_link_queries = [
|
|||
`tabDocField`.fieldname, `tabDocField`.options
|
||||
from `tabDocField`, `tabDocType`
|
||||
where `tabDocField`.fieldtype='Dynamic Link' and
|
||||
`tabDocType`.`name`=`tabDocField`.parent and `tabDocType`.is_virtual = 0
|
||||
`tabDocType`.`name`=`tabDocField`.parent and `tabDocType`.is_virtual = 0 and `tabDocField`.is_virtual = 0
|
||||
order by `tabDocType`.read_only, `tabDocType`.in_create""",
|
||||
"""select `tabCustom Field`.dt as parent,
|
||||
`tabDocType`.read_only, `tabDocType`.in_create,
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ def get_meta(doctype: "str | DocType", cached: bool = True) -> "_Meta":
|
|||
return meta
|
||||
|
||||
meta = Meta(doctype)
|
||||
|
||||
key = f"doctype_meta::{meta.name}"
|
||||
frappe.client_cache.set_value(key, meta)
|
||||
return meta
|
||||
|
|
@ -193,6 +194,28 @@ class Meta(Document):
|
|||
def get_dynamic_link_fields(self):
|
||||
return self._dynamic_link_fields
|
||||
|
||||
def get_masked_fields(self):
|
||||
import copy
|
||||
|
||||
if frappe.session.user == "Administrator":
|
||||
return []
|
||||
cache_key = f"masked_fields::{self.name}::{frappe.session.user}"
|
||||
masked_fields = frappe.cache.get_value(cache_key)
|
||||
|
||||
if masked_fields is None:
|
||||
masked_fields = []
|
||||
for df in self.fields:
|
||||
if df.get("mask") and not self.has_permlevel_access_to(
|
||||
fieldname=df.fieldname, df=df, permission_type="mask"
|
||||
):
|
||||
# work on a copy instead of original df
|
||||
df_copy = copy.deepcopy(df)
|
||||
df_copy.mask_readonly = 1
|
||||
masked_fields.append(df_copy)
|
||||
frappe.cache.set_value(cache_key, masked_fields)
|
||||
|
||||
return masked_fields
|
||||
|
||||
@cached_property
|
||||
def _dynamic_link_fields(self):
|
||||
return self.get("fields", {"fieldtype": "Dynamic Link"})
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ def rename_doc(
|
|||
new_doc.add_comment("Edit", _("renamed from {0} to {1}").format(frappe.bold(old), frappe.bold(new)))
|
||||
|
||||
if merge:
|
||||
frappe.delete_doc(doctype, old)
|
||||
frappe.delete_doc(doctype, old, ignore_permissions=ignore_permissions)
|
||||
|
||||
new_doc.clear_cache()
|
||||
frappe.clear_cache()
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ onMounted(() => store.fetch());
|
|||
}
|
||||
|
||||
.editable {
|
||||
input,
|
||||
input:not([type="checkbox"]),
|
||||
textarea,
|
||||
select,
|
||||
.ace_editor,
|
||||
|
|
@ -258,7 +258,7 @@ onMounted(() => store.fetch());
|
|||
border-color: transparent;
|
||||
}
|
||||
|
||||
input,
|
||||
input:not([type="checkbox"]),
|
||||
textarea,
|
||||
select,
|
||||
.ace_editor,
|
||||
|
|
@ -269,10 +269,6 @@ onMounted(() => store.fetch());
|
|||
.ql-editor {
|
||||
background-color: var(--control-bg) !important;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
background-color: var(--fg-bg) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.form-main > :deep(div:first-child:not(.tab-header)) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,18 @@
|
|||
<script setup>
|
||||
import { useSlots } from "vue";
|
||||
import { useSlots, computed } from "vue";
|
||||
|
||||
const props = defineProps(["df", "value", "read_only"]);
|
||||
let slots = useSlots();
|
||||
|
||||
// Get the display value considering both current value and default
|
||||
let display_checked = computed(() => {
|
||||
// Use current value if explicitly set, otherwise fall back to default
|
||||
const value =
|
||||
props.value !== undefined && props.value !== null ? props.value : props.df.default;
|
||||
|
||||
// Frappe checkboxes use "1"/"0" strings or 1/0 numbers
|
||||
return value === "1" || value === 1;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -10,7 +20,7 @@ let slots = useSlots();
|
|||
<!-- checkbox -->
|
||||
<label v-if="slots.label" class="field-controls">
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" disabled />
|
||||
<input type="checkbox" :checked="display_checked" disabled />
|
||||
<slot name="label" />
|
||||
</div>
|
||||
<slot name="actions" />
|
||||
|
|
@ -18,7 +28,7 @@ let slots = useSlots();
|
|||
<label v-else>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="value"
|
||||
:checked="display_checked"
|
||||
:disabled="read_only"
|
||||
@change="(event) => $emit('update:modelValue', event.target.checked)"
|
||||
/>
|
||||
|
|
@ -42,7 +52,6 @@ label .checkbox {
|
|||
align-items: center;
|
||||
|
||||
input {
|
||||
background-color: var(--fg-color);
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--gray-400);
|
||||
pointer-events: none;
|
||||
|
|
|
|||
|
|
@ -67,10 +67,20 @@ let select_control = computed(() => {
|
|||
});
|
||||
|
||||
let content = computed({
|
||||
get: () => props.modelValue,
|
||||
get: () => props.modelValue ?? props.df.default,
|
||||
set: (value) => emit("update:modelValue", value),
|
||||
});
|
||||
|
||||
// Get the display label for the current selected value
|
||||
let display_value = computed(() => {
|
||||
const current_value = content.value;
|
||||
if (!current_value) return "";
|
||||
|
||||
const options = get_options();
|
||||
const selected_option = options?.find((opt) => opt.value === current_value);
|
||||
return selected_option ? selected_option.label : current_value;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (select.value) select_control.value;
|
||||
});
|
||||
|
|
@ -101,7 +111,7 @@ watch(
|
|||
|
||||
<!-- select input -->
|
||||
<div class="select-input">
|
||||
<input class="form-control" readonly />
|
||||
<input class="form-control" readonly :value="display_value" />
|
||||
<div class="select-icon" v-html="frappe.utils.icon('select', 'sm')"></div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -377,6 +377,7 @@ function on_file_input(e) {
|
|||
}
|
||||
function remove_file(file) {
|
||||
files.value = files.value.filter((f) => f !== file);
|
||||
if (file_input.value) file_input.value.value = "";
|
||||
}
|
||||
function toggle_image_cropper(index) {
|
||||
crop_image_with_index.value = show_image_cropper.value ? -1 : index;
|
||||
|
|
|
|||
|
|
@ -7,17 +7,13 @@ frappe.ui.form.ControlDynamicLink = class ControlDynamicLink extends frappe.ui.f
|
|||
//for dialog box
|
||||
options = cur_dialog.get_value(this.df.options);
|
||||
} else if (!cur_frm) {
|
||||
const selector = `input[data-fieldname="${this.df.options}"]`;
|
||||
let input = null;
|
||||
if (cur_list) {
|
||||
// for list page
|
||||
input = cur_list.filter_area.standard_filters_wrapper.find(selector);
|
||||
}
|
||||
if (cur_page) {
|
||||
input = $(cur_page.page).find(selector);
|
||||
}
|
||||
if (input) {
|
||||
options = input.val();
|
||||
options = cur_list.page.fields_dict[this.df.options].get_input_value();
|
||||
} else if (cur_page) {
|
||||
const selector = `input[data-fieldname="${this.df.options}"]`;
|
||||
let input = $(cur_page.page).find(selector);
|
||||
options = input.length ? input.val() : null;
|
||||
}
|
||||
} else {
|
||||
options = frappe.model.get_value(this.df.parent, this.docname, this.df.options);
|
||||
|
|
|
|||
|
|
@ -503,10 +503,16 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
|
|||
filter[3].push("...");
|
||||
}
|
||||
|
||||
let value =
|
||||
filter[3] == null || filter[3] === "" ? __("empty") : String(__(filter[3]));
|
||||
let value;
|
||||
if (filter[3] && Array.isArray(filter[3])) {
|
||||
value = filter[3].map((v) => String(__(v)).bold()).join(", ");
|
||||
} else if (filter[3] == null || filter[3] === "") {
|
||||
value = __("empty").bold();
|
||||
} else {
|
||||
value = String(__(filter[3])).bold();
|
||||
}
|
||||
|
||||
return [__(label).bold(), __(filter[2]), value.bold()].join(" ");
|
||||
return [__(label).bold(), __(filter[2]), value].join(" ");
|
||||
}
|
||||
|
||||
let filter_string = filter_array.map(get_filter_description).join(", ");
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ frappe.ui.form.ControlMultiCheck = class ControlMultiCheck extends frappe.ui.for
|
|||
const columns = this.df.columns;
|
||||
this.$checkbox_area = $('<div class="checkbox-options"></div>').appendTo(this.wrapper);
|
||||
this.$checkbox_area.get(0).style.setProperty("--checkbox-options-columns", columns);
|
||||
this.$checkbox_area.get(0).style.setProperty("padding", "1em");
|
||||
}
|
||||
|
||||
refresh() {
|
||||
|
|
@ -154,8 +155,8 @@ frappe.ui.form.ControlMultiCheck = class ControlMultiCheck extends frappe.ui.for
|
|||
get_checkbox_element(option) {
|
||||
return $(`
|
||||
<div class="checkbox unit-checkbox">
|
||||
<label title="${option.description || ""}">
|
||||
<input type="checkbox" data-unit="${option.value}"></input>
|
||||
<label title="${option.description || ""}" style="display: flex; align-items: center;">
|
||||
<input type="checkbox" data-unit="${option.value}" style="flex-shrink: 0;">
|
||||
<span class="label-area" data-unit="${option.value}">${option.label}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue