From d0d7ef8ae612ac4186d285ffeb9c0c2bc674d2d7 Mon Sep 17 00:00:00 2001 From: UmakanthKaspa Date: Fri, 13 Feb 2026 21:17:18 +0530 Subject: [PATCH 1/3] feat: add dynamic filters to Web Form list view --- .../js/frappe/web_form/web_form_list.js | 34 +++++++ .../js/frappe/web_form/webform_script.js | 1 + frappe/website/doctype/web_form/web_form.js | 89 +++++++++++++++++++ frappe/website/doctype/web_form/web_form.json | 14 ++- frappe/website/doctype/web_form/web_form.py | 1 + 5 files changed, 138 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/web_form/web_form_list.js b/frappe/public/js/frappe/web_form/web_form_list.js index ae49106693..92514498e2 100644 --- a/frappe/public/js/frappe/web_form/web_form_list.js +++ b/frappe/public/js/frappe/web_form/web_form_list.js @@ -116,6 +116,40 @@ export default class WebFormList { this.filters = Object.assign(this.filters, JSON.parse(filter)); } + if (this.dynamic_filters_json) { + let dynamic_filters = JSON.parse(this.dynamic_filters_json); + if (dynamic_filters.length) { + dynamic_filters.forEach((f) => { + let expression = f[3]; + try { + f[3] = eval(f[3]); + } catch (e) { + frappe.throw( + __("Invalid expression set in filter {0} ({1}): {2}", [ + f[1], + f[0], + expression, + ]) + ); + } + if (f[3] == null) { + frappe.throw( + __("Invalid expression set in filter {0} ({1}): {2}", [ + f[1], + f[0], + expression, + ]) + ); + } + }); + let df_obj = {}; + dynamic_filters.forEach((f) => { + df_obj[f[1]] = [f[2], f[3]]; + }); + this.filters = Object.assign(this.filters, df_obj); + } + } + let args = { method: "frappe.www.list.get_list_data", args: { diff --git a/frappe/public/js/frappe/web_form/webform_script.js b/frappe/public/js/frappe/web_form/webform_script.js index 4c745a3909..09b71870bb 100644 --- a/frappe/public/js/frappe/web_form/webform_script.js +++ b/frappe/public/js/frappe/web_form/webform_script.js @@ -28,6 +28,7 @@ frappe.ready(function () { web_form_name: web_form_doc.name, list_columns: web_form_doc.list_columns, condition_json: web_form_doc.condition_json, + dynamic_filters_json: web_form_doc.dynamic_filters_json, settings: { allow_delete: web_form_doc.allow_delete, }, diff --git a/frappe/website/doctype/web_form/web_form.js b/frappe/website/doctype/web_form/web_form.js index 6c31cfe3fc..978a9cae9a 100644 --- a/frappe/website/doctype/web_form/web_form.js +++ b/frappe/website/doctype/web_form/web_form.js @@ -46,6 +46,7 @@ frappe.ui.form.on("Web Form", { frm.trigger("add_get_fields_button"); frm.trigger("add_publish_button"); frm.trigger("render_condition_table"); + frm.trigger("render_dynamic_filters_table"); }, login_required: function (frm) { @@ -203,9 +204,15 @@ frappe.ui.form.on("Web Form", { }, before_save: function (frm) { + let dynamic_filters = JSON.parse(frm.doc.dynamic_filters_json || "null"); let static_filters = JSON.parse(frm.doc.condition_json || "[]"); + static_filters = frappe.dashboard_utils.remove_common_static_filter_values( + static_filters, + dynamic_filters + ); frm.set_value("condition_json", JSON.stringify(static_filters)); frm.trigger("render_condition_table"); + frm.trigger("render_dynamic_filters_table"); }, render_condition_table: function (frm) { @@ -308,6 +315,88 @@ frappe.ui.form.on("Web Form", { dialog.set_values(filters); }); }, + render_dynamic_filters_table(frm) { + let wrapper = $(frm.get_field("dynamic_filters_json").wrapper).empty(); + + frm.dynamic_filter_table = $(` + + + + + + + + +
${__("Filter")}${__("Condition")}${__("Value")}
`).appendTo(wrapper); + + frm.dynamic_filters = + frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2 + ? JSON.parse(frm.doc.dynamic_filters_json) + : null; + + frm.trigger("set_dynamic_filters_in_table"); + + let filters = JSON.parse(frm.doc.condition_json || "[]"); + + let fields = frappe.dashboard_utils.get_fields_for_dynamic_filter_dialog( + true, + filters, + frm.dynamic_filters + ); + + frm.dynamic_filter_table.on("click", () => { + if (!frm.has_perm("write")) { + return; + } + + if (!frappe.boot.developer_mode && frm.doc.is_standard) { + frappe.throw(__("Cannot edit filters for standard Web Forms")); + } + let dialog = new frappe.ui.Dialog({ + title: __("Set Dynamic Filters"), + fields: fields, + primary_action: () => { + let values = dialog.get_values(); + dialog.hide(); + let dynamic_filters = []; + for (let key of Object.keys(values)) { + let [doctype, fieldname] = key.split(":"); + dynamic_filters.push([doctype, fieldname, "=", values[key]]); + } + frm.set_value("dynamic_filters_json", JSON.stringify(dynamic_filters)); + frm.trigger("set_dynamic_filters_in_table"); + }, + primary_action_label: __("Set"), + }); + + dialog.show(); + dialog.set_values(frm.dynamic_filters); + }); + }, + set_dynamic_filters_in_table: function (frm) { + frm.dynamic_filters = + frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2 + ? JSON.parse(frm.doc.dynamic_filters_json) + : null; + + if (!frm.dynamic_filters) { + const filter_row = $(` + ${__("Click to Set Dynamic Filters")}`); + frm.dynamic_filter_table.find("tbody").html(filter_row); + } else { + let filter_rows = ""; + frm.dynamic_filters.forEach((filter) => { + filter_rows += ` + ${filter[1]} + ${filter[2] || ""} + ${filter[3]} + `; + }); + frm.dynamic_filter_table.find("tbody").html(filter_rows); + } + }, }); frappe.ui.form.on("Web Form List Column", { diff --git a/frappe/website/doctype/web_form/web_form.json b/frappe/website/doctype/web_form/web_form.json index 6b03a6b988..c2e291bdfc 100644 --- a/frappe/website/doctype/web_form/web_form.json +++ b/frappe/website/doctype/web_form/web_form.json @@ -39,6 +39,8 @@ "condition_section", "condition_description", "condition_json", + "dynamic_filters_section", + "dynamic_filters_json", "section_break_3", "list_setting_message", "show_list", @@ -424,12 +426,22 @@ { "fieldname": "column_break_hhec", "fieldtype": "Column Break" + }, + { + "fieldname": "dynamic_filters_json", + "fieldtype": "JSON", + "label": "Dynamic Filters JSON" + }, + { + "fieldname": "dynamic_filters_section", + "fieldtype": "Section Break", + "label": "Dynamic Filters" } ], "icon": "icon-edit", "is_published_field": "published", "links": [], - "modified": "2025-12-12 16:29:35.806107", + "modified": "2026-02-13 19:59:19.807594", "modified_by": "Administrator", "module": "Website", "name": "Web Form", diff --git a/frappe/website/doctype/web_form/web_form.py b/frappe/website/doctype/web_form/web_form.py index bd12631577..38a0ea1d2f 100644 --- a/frappe/website/doctype/web_form/web_form.py +++ b/frappe/website/doctype/web_form/web_form.py @@ -45,6 +45,7 @@ class WebForm(WebsiteGenerator): condition_json: DF.JSON | None custom_css: DF.Code | None doc_type: DF.Link + dynamic_filters_json: DF.JSON | None hide_footer: DF.Check hide_navbar: DF.Check introduction_text: DF.TextEditor | None From 6dd4539002d6eba176c7f43d2fa0c60198a4b334 Mon Sep 17 00:00:00 2001 From: UmakanthKaspa Date: Sun, 15 Feb 2026 11:03:16 +0530 Subject: [PATCH 2/3] feat: evaluate Web Form dynamic filters server-side --- .../js/frappe/web_form/web_form_list.js | 34 ---------------- .../js/frappe/web_form/webform_script.js | 1 - frappe/website/doctype/web_form/web_form.js | 12 ++++++ frappe/www/list.py | 39 +++++++++++++++++++ 4 files changed, 51 insertions(+), 35 deletions(-) diff --git a/frappe/public/js/frappe/web_form/web_form_list.js b/frappe/public/js/frappe/web_form/web_form_list.js index 92514498e2..ae49106693 100644 --- a/frappe/public/js/frappe/web_form/web_form_list.js +++ b/frappe/public/js/frappe/web_form/web_form_list.js @@ -116,40 +116,6 @@ export default class WebFormList { this.filters = Object.assign(this.filters, JSON.parse(filter)); } - if (this.dynamic_filters_json) { - let dynamic_filters = JSON.parse(this.dynamic_filters_json); - if (dynamic_filters.length) { - dynamic_filters.forEach((f) => { - let expression = f[3]; - try { - f[3] = eval(f[3]); - } catch (e) { - frappe.throw( - __("Invalid expression set in filter {0} ({1}): {2}", [ - f[1], - f[0], - expression, - ]) - ); - } - if (f[3] == null) { - frappe.throw( - __("Invalid expression set in filter {0} ({1}): {2}", [ - f[1], - f[0], - expression, - ]) - ); - } - }); - let df_obj = {}; - dynamic_filters.forEach((f) => { - df_obj[f[1]] = [f[2], f[3]]; - }); - this.filters = Object.assign(this.filters, df_obj); - } - } - let args = { method: "frappe.www.list.get_list_data", args: { diff --git a/frappe/public/js/frappe/web_form/webform_script.js b/frappe/public/js/frappe/web_form/webform_script.js index 09b71870bb..4c745a3909 100644 --- a/frappe/public/js/frappe/web_form/webform_script.js +++ b/frappe/public/js/frappe/web_form/webform_script.js @@ -28,7 +28,6 @@ frappe.ready(function () { web_form_name: web_form_doc.name, list_columns: web_form_doc.list_columns, condition_json: web_form_doc.condition_json, - dynamic_filters_json: web_form_doc.dynamic_filters_json, settings: { allow_delete: web_form_doc.allow_delete, }, diff --git a/frappe/website/doctype/web_form/web_form.js b/frappe/website/doctype/web_form/web_form.js index 978a9cae9a..bca1356d96 100644 --- a/frappe/website/doctype/web_form/web_form.js +++ b/frappe/website/doctype/web_form/web_form.js @@ -346,6 +346,18 @@ frappe.ui.form.on("Web Form", { frm.dynamic_filters ); + // Override description to show Python expressions (evaluated server-side) + let desc_field = fields.find((f) => f.fieldname === "description"); + if (desc_field) { + desc_field.options = `
+

${__("Set dynamic filter values as Python expressions.")}

+

${__("For example:")} + frappe.session.user ${__("or")} + frappe.utils.now() +

+
`; + } + frm.dynamic_filter_table.on("click", () => { if (!frm.has_perm("write")) { return; diff --git a/frappe/www/list.py b/frappe/www/list.py index 4feb9a6943..976d05f286 100644 --- a/frappe/www/list.py +++ b/frappe/www/list.py @@ -45,6 +45,11 @@ def get_list_data( if list_context.filters: filters.update(list_context.filters) + if web_form_name: + dynamic_filters = get_dynamic_filters(web_form_name) + if dynamic_filters: + filters.update(dynamic_filters) + _get_list = list_context.get_list or get_list kwargs = dict( @@ -192,3 +197,37 @@ def get_list( order_by=order_by, distinct=distinct, ) + + +def get_dynamic_filters(web_form_name): + """Evaluate dynamic filter expressions from Web Form. + Uses same safe_eval + get_workflow_safe_globals pattern as Workflow.""" + from frappe.model.workflow import get_workflow_safe_globals + + web_form = frappe.get_cached_doc("Web Form", web_form_name) + + if not web_form.dynamic_filters_json: + return None + + dynamic_filters = json.loads(web_form.dynamic_filters_json) + if not dynamic_filters: + return None + + safe_globals = get_workflow_safe_globals() + safe_globals["frappe"]["defaults"] = frappe._dict( + get_user_default=frappe.defaults.get_user_default, + get_global_default=frappe.defaults.get_global_default, + ) + + evaluated = {} + for f in dynamic_filters: + try: + value = frappe.safe_eval(f[3], safe_globals) + evaluated[f[1]] = [f[2], value] + except Exception as e: + frappe.throw( + _("Invalid expression in Web Form Dynamic Filter for {0}: {1}").format(f[1], e), + title=_("Dynamic Filter Error"), + ) + + return evaluated From 740104b88dc36e259adeb288c2cdf077ca248545 Mon Sep 17 00:00:00 2001 From: UmakanthKaspa Date: Sun, 15 Feb 2026 12:59:45 +0530 Subject: [PATCH 3/3] fix: pre-populate existing values in Web Form dynamic filters dialog --- frappe/website/doctype/web_form/web_form.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frappe/website/doctype/web_form/web_form.js b/frappe/website/doctype/web_form/web_form.js index bca1356d96..00f2e66e08 100644 --- a/frappe/website/doctype/web_form/web_form.js +++ b/frappe/website/doctype/web_form/web_form.js @@ -384,7 +384,13 @@ frappe.ui.form.on("Web Form", { }); dialog.show(); - dialog.set_values(frm.dynamic_filters); + if (frm.dynamic_filters) { + let filter_values = {}; + frm.dynamic_filters.forEach((f) => { + filter_values[f[0] + ":" + f[1]] = f[3]; + }); + dialog.set_values(filter_values); + } }); }, set_dynamic_filters_in_table: function (frm) {