diff --git a/frappe/website/doctype/web_form/web_form.js b/frappe/website/doctype/web_form/web_form.js index 6c31cfe3fc..00f2e66e08 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,106 @@ 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 + ); + + // 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; + } + + 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(); + 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) { + 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 a42934d795..f42c39322d 100644 --- a/frappe/website/doctype/web_form/web_form.py +++ b/frappe/website/doctype/web_form/web_form.py @@ -46,6 +46,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 diff --git a/frappe/www/list.py b/frappe/www/list.py index d912efe9fe..b407349f54 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( @@ -194,3 +199,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