Merge branch 'develop' into chrome-pdf

This commit is contained in:
Ejaaz Khan 2025-10-08 15:43:38 +05:30
commit f74671267d
78 changed files with 45849 additions and 10361 deletions

View file

@ -93,7 +93,7 @@ jobs:
- frappe/hrms - frappe/hrms
steps: steps:
- name: Dispatch Downstream CI (if supported) - name: Dispatch Downstream CI (if supported)
uses: peter-evans/repository-dispatch@v3 uses: peter-evans/repository-dispatch@v4
with: with:
token: ${{ secrets.CI_PAT }} token: ${{ secrets.CI_PAT }}
repository: ${{ matrix.repo }} repository: ${{ matrix.repo }}

View file

@ -1,5 +1,6 @@
exclude: 'node_modules|.git' exclude: 'node_modules|.git'
default_stages: [pre-commit] default_stages: [pre-commit]
default_install_hook_types: [pre-commit, commit-msg]
fail_fast: false fail_fast: false
@ -69,6 +70,13 @@ repos:
frappe/public/js/lib/.* 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: ci:
autoupdate_schedule: weekly autoupdate_schedule: weekly
skip: [] skip: []

View file

@ -1,6 +1,7 @@
**/hooks.py,frappe.gettext.extractors.navbar.extract **/hooks.py,frappe.gettext.extractors.navbar.extract
**/doctype/*/*.json,frappe.gettext.extractors.doctype.extract **/doctype/*/*.json,frappe.gettext.extractors.doctype.extract
**/workspace/*/*.json,frappe.gettext.extractors.workspace.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 **/onboarding_step/*/*.json,frappe.gettext.extractors.onboarding_step.extract
**/module_onboarding/*/*.json,frappe.gettext.extractors.module_onboarding.extract **/module_onboarding/*/*.json,frappe.gettext.extractors.module_onboarding.extract
**/report/*/*.json,frappe.gettext.extractors.report.extract **/report/*/*.json,frappe.gettext.extractors.report.extract

1 **/hooks.py frappe.gettext.extractors.navbar.extract
2 **/doctype/*/*.json frappe.gettext.extractors.doctype.extract
3 **/workspace/*/*.json frappe.gettext.extractors.workspace.extract
4 **/web_form/*/*.json frappe.gettext.extractors.web_form.extract
5 **/onboarding_step/*/*.json frappe.gettext.extractors.onboarding_step.extract
6 **/module_onboarding/*/*.json frappe.gettext.extractors.module_onboarding.extract
7 **/report/*/*.json frappe.gettext.extractors.report.extract

View file

@ -75,14 +75,15 @@ context("Form", () => {
cy.get('.frappe-control[data-fieldname="email_ids"]').as("table"); 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("button.grid-add-row").click();
cy.get("@table").find('[data-idx="1"]').as("row1"); 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").click();
cy.get("@row1").find("input.input-with-feedback.form-control").as("email_input1"); cy.get("@row1").find("input.input-with-feedback.form-control").as("email_input1");
cy.get("@email_input1").type(website_input, { waitForAnimations: false }); 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").click();
cy.get("@row2").find("input.input-with-feedback.form-control").as("email_input2"); cy.get("@row2").find("input.input-with-feedback.form-control").as("email_input2");
cy.get("@email_input2").type(valid_email, { waitForAnimations: false }); cy.get("@email_input2").type(valid_email, { waitForAnimations: false });

View file

@ -24,6 +24,7 @@ frappe.listview_settings["DocType"] = {
fieldtype: "Data", fieldtype: "Data",
reqd: 1, reqd: 1,
default: doctype_name, default: doctype_name,
length: 61,
}, },
{ fieldtype: "Column Break" }, { fieldtype: "Column Break" },
{ {

View file

@ -0,0 +1,7 @@
frappe.listview_settings["File"] = {
formatters: {
file_name: function (value) {
return frappe.utils.escape_html(value || "");
},
},
};

View file

@ -288,6 +288,8 @@ class Engine:
doctype: str | None = None, doctype: str | None = None,
) -> "Criterion | None": ) -> "Criterion | None":
"""Builds a pypika Criterion object for a simple filter condition.""" """Builds a pypika Criterion object for a simple filter condition."""
import operator as builtin_operator
_field = self._validate_and_prepare_filter_field(field, doctype) _field = self._validate_and_prepare_filter_field(field, doctype)
_value = convert_to_value(value) _value = convert_to_value(value)
_operator = operator _operator = operator
@ -323,7 +325,7 @@ class Engine:
operator_fn = OPERATOR_MAP[_operator.casefold()] operator_fn = OPERATOR_MAP[_operator.casefold()]
if _value is None and isinstance(_field, Field): if _value is None and isinstance(_field, Field):
return _field.isnull() return _field.isnotnull() if operator_fn == builtin_operator.ne else _field.isnull()
else: else:
return operator_fn(_field, _value) return operator_fn(_field, _value)

View file

@ -443,6 +443,9 @@ def get_definition(fieldtype, precision=None, length=None, *, options=None):
if length: if length:
if coltype == "varchar": 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 length < 64:
length = 64
size = length size = length
elif coltype == "int" and length < 11: elif coltype == "int" and length < 11:
# allow setting custom length for int if length provided is less than 11 # allow setting custom length for int if length provided is less than 11

View file

@ -107,6 +107,7 @@ class SQLiteDatabase(SQLiteExceptionUtil, Database):
conn = self.create_connection(read_only) conn = self.create_connection(read_only)
conn.isolation_level = None conn.isolation_level = None
conn.create_function("regexp", 2, regexp) conn.create_function("regexp", 2, regexp)
conn.create_function("regexp_replace", 3, regexp_replace)
pragmas = { pragmas = {
"journal_mode": "WAL", "journal_mode": "WAL",
"synchronous": "NORMAL", "synchronous": "NORMAL",
@ -583,3 +584,10 @@ def regexp(expr: str, item: str) -> bool:
Although it works in the CLI - doesn't work through python Although it works in the CLI - doesn't work through python
""" """
return re.search(expr, item) is not None 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)

View file

@ -125,7 +125,7 @@ class SQLiteTable(DBTable):
if self.meta.sort_field == "modified" and not frappe.db.get_column_index( if self.meta.sort_field == "modified" and not frappe.db.get_column_index(
self.table_name, "modified", unique=False 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: for query in index_queries:
frappe.db.sql_ddl(query) frappe.db.sql_ddl(query)

View 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"]

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

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

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

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

View file

@ -1127,7 +1127,12 @@ from {tables}
r"select\b.*\bfrom", 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.")) frappe.throw(_("Cannot use sub-query here."))
blacklisted_sql_functions = { blacklisted_sql_functions = {

View file

@ -217,7 +217,7 @@ def rename_doc(
new_doc.add_comment("Edit", _("renamed from {0} to {1}").format(frappe.bold(old), frappe.bold(new))) new_doc.add_comment("Edit", _("renamed from {0} to {1}").format(frappe.bold(old), frappe.bold(new)))
if merge: if merge:
frappe.delete_doc(doctype, old) frappe.delete_doc(doctype, old, ignore_permissions=ignore_permissions)
new_doc.clear_cache() new_doc.clear_cache()
frappe.clear_cache() frappe.clear_cache()

View file

@ -7,17 +7,13 @@ frappe.ui.form.ControlDynamicLink = class ControlDynamicLink extends frappe.ui.f
//for dialog box //for dialog box
options = cur_dialog.get_value(this.df.options); options = cur_dialog.get_value(this.df.options);
} else if (!cur_frm) { } else if (!cur_frm) {
const selector = `input[data-fieldname="${this.df.options}"]`;
let input = null;
if (cur_list) { if (cur_list) {
// for list page // for list page
input = cur_list.filter_area.standard_filters_wrapper.find(selector); options = cur_list.page.fields_dict[this.df.options].get_input_value();
} } else if (cur_page) {
if (cur_page) { const selector = `input[data-fieldname="${this.df.options}"]`;
input = $(cur_page.page).find(selector); let input = $(cur_page.page).find(selector);
} options = input.length ? input.val() : null;
if (input) {
options = input.val();
} }
} else { } else {
options = frappe.model.get_value(this.df.parent, this.docname, this.df.options); options = frappe.model.get_value(this.df.parent, this.docname, this.df.options);

View file

@ -16,25 +16,6 @@ frappe.ui.form.ControlTable = class ControlTable extends frappe.ui.form.Control
this.frm.grids[this.frm.grids.length] = this; this.frm.grids[this.frm.grids.length] = this;
} }
const me = this; const me = this;
this.$wrapper.on("keydown", (e) => {
if (e.which == 9) {
if (e.shiftKey) {
let row_idx = me.set_current_row(e.target);
if (row_idx) {
this.grid.grid_rows[row_idx - 1].toggle_editable_row(true);
}
} else {
if (this.grid.grid_rows.length > 0) {
this.grid.grid_rows[this.grid.grid_rows.length - 1].toggle_editable_row(
true
);
} else {
this.grid.add_new_row(null, null, true, null, true);
this.grid.grid_rows[0].toggle_editable_row(true);
}
}
}
});
this.$wrapper.on("paste", ":text", (e) => { this.$wrapper.on("paste", ":text", (e) => {
const table_field = this.df.fieldname; const table_field = this.df.fieldname;
const grid = this.grid; const grid = this.grid;
@ -173,13 +154,4 @@ frappe.ui.form.ControlTable = class ControlTable extends frappe.ui.form.Control
check_all_rows() { check_all_rows() {
this.$wrapper.find(".grid-row-check")[0].click(); this.$wrapper.find(".grid-row-check")[0].click();
} }
set_current_row(target) {
let current_row = null;
for (let i = 0; i < this.grid.grid_rows.length; i++) {
if (this.grid.grid_rows[i].wrapper.get(0).contains(target)) {
current_row = i + 1;
}
}
return current_row;
}
}; };

View file

@ -99,7 +99,7 @@ frappe.form.formatters = {
docfield.precision || docfield.precision ||
cint(frappe.boot.sysdefaults && frappe.boot.sysdefaults.float_precision) || cint(frappe.boot.sysdefaults && frappe.boot.sysdefaults.float_precision) ||
2; 2;
return frappe.form.formatters._right(flt(value, precision) + "%", options); return frappe.form.formatters._right(format_number(value, null, precision) + "%", options);
}, },
Rating: function (value, docfield) { Rating: function (value, docfield) {
let rating_html = ""; let rating_html = "";

View file

@ -70,7 +70,7 @@ export default class Grid {
<p class="text-muted small grid-description"></p> <p class="text-muted small grid-description"></p>
<div class="grid-custom-buttons"></div> <div class="grid-custom-buttons"></div>
<div class="form-grid-container"> <div class="form-grid-container">
<div class="form-grid" tabIndex="0"> <div class="form-grid">
<div class="grid-heading-row"></div> <div class="grid-heading-row"></div>
<div class="grid-body"> <div class="grid-body">
<div class="rows"></div> <div class="rows"></div>
@ -939,6 +939,7 @@ export default class Grid {
} }
setTimeout(() => { setTimeout(() => {
this.grid_rows[idx].toggle_editable_row(true);
this.grid_rows[idx].row this.grid_rows[idx].row
.find('input[type="Text"],textarea,select') .find('input[type="Text"],textarea,select')
.filter(":visible:first") .filter(":visible:first")
@ -1276,4 +1277,14 @@ export default class Grid {
this.debounced_refresh(); this.debounced_refresh();
} }
get_current_row(target) {
let current_row = null;
for (let i = 0; i < this.grid_rows.length; i++) {
if (this.grid_rows[i].wrapper.get(0).contains(target)) {
current_row = i;
}
}
return current_row;
}
} }

View file

@ -1102,7 +1102,17 @@ export default class GridRow {
this.columns[df.fieldname] = $col; this.columns[df.fieldname] = $col;
this.columns_list.push($col); this.columns_list.push($col);
if (ci == 0 && !this.header_row) {
$col.attr("tabIndex", 0);
$col.on("focus", function () {
if (me.grid.grid_rows.length == 0) {
me.grid.add_new_row();
}
me.grid.grid_rows[me.grid.grid_rows.length - 1].toggle_editable_row(true);
me.grid.set_focus_on_row();
$col.attr("tabIndex", "");
});
}
return $col; return $col;
} }
@ -1200,6 +1210,8 @@ export default class GridRow {
// flag list input // flag list input
if (this.columns_list && this.columns_list.slice(-1)[0] === column) { if (this.columns_list && this.columns_list.slice(-1)[0] === column) {
field.$input.attr("data-last-input", 1); field.$input.attr("data-last-input", 1);
} else if (this.columns_list && this.columns_list.slice(0)[0] === column) {
field.$input.attr("data-first-input", 1);
} }
} }
@ -1289,6 +1301,18 @@ export default class GridRow {
return false; return false;
} }
} }
} else if (e.which === TAB && e.shiftKey) {
var first_column = me.wrapper
.find("input:enabled:not([type='checkbox'])")
.first()
.get(0);
var is_first_column =
$(this).attr("data-first-input") || first_column === this;
if (is_first_column) {
let ri = me.grid.get_current_row(e.target);
if (ri == 0) return;
me.grid.grid_rows[ri - 1].toggle_editable_row(true);
}
} }
}); });
} }
@ -1433,7 +1457,10 @@ export default class GridRow {
this.grid_form.wrapper.css("display", "none"); this.grid_form.wrapper.css("display", "none");
} }
this.wrapper.removeClass("grid-row-open"); this.wrapper.removeClass("grid-row-open");
this.open_form_button.parent().focus();
if (this.grid.meta.editable_grid) {
this.open_form_button.parent().focus();
}
} }
has_prev() { has_prev() {
return this.doc.idx > 1; return this.doc.idx > 1;

View file

@ -14,7 +14,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
this.is_dialog = true; this.is_dialog = true;
this.last_focus = null; this.last_focus = null;
$.extend(this, { animate: true, size: null, auto_make: true }, opts); $.extend(this, { animate: true, size: null, auto_make: true, centered: false }, opts);
if (this.auto_make) { if (this.auto_make) {
this.make(); this.make();
} }
@ -34,6 +34,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
if (!this.size) this.set_modal_size(); if (!this.size) this.set_modal_size();
this.wrapper = this.$wrapper.find(".modal-dialog").get(0); this.wrapper = this.$wrapper.find(".modal-dialog").get(0);
if (this.centered) $(this.wrapper).addClass("modal-dialog-centered");
if (this.size == "small") $(this.wrapper).addClass("modal-sm"); if (this.size == "small") $(this.wrapper).addClass("modal-sm");
else if (this.size == "large") $(this.wrapper).addClass("modal-lg"); else if (this.size == "large") $(this.wrapper).addClass("modal-lg");
else if (this.size == "extra-large") $(this.wrapper).addClass("modal-xl"); else if (this.size == "extra-large") $(this.wrapper).addClass("modal-xl");
@ -248,7 +249,10 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
show() { show() {
// show it // show it
this.handle_focus(); if (window.location.pathname.startsWith("/app")) {
this.handle_focus();
}
if (this.animate) { if (this.animate) {
this.$wrapper.addClass("fade"); this.$wrapper.addClass("fade");
} else { } else {
@ -278,7 +282,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
handle_focus() { handle_focus() {
const me = this; const me = this;
if (frappe.get_route) { if (frappe.get_route()) {
if (frappe.get_route()[0] == "Form") { if (frappe.get_route()[0] == "Form") {
if (!me.last_focus) me.last_focus = document.activeElement; if (!me.last_focus) me.last_focus = document.activeElement;
} }

View file

@ -117,6 +117,10 @@ frappe.ui.FieldSelect = class FieldSelect {
// main table // main table
var main_table_fields = std_filters.concat(frappe.meta.docfield_list[me.doctype]); var main_table_fields = std_filters.concat(frappe.meta.docfield_list[me.doctype]);
$.each(frappe.utils.sort(main_table_fields, "label", "string"), function (i, df) { $.each(frappe.utils.sort(main_table_fields, "label", "string"), function (i, df) {
if (df.is_virtual) {
return;
}
let doctype = let doctype =
frappe.get_meta(me.doctype).istable && me.parent_doctype frappe.get_meta(me.doctype).istable && me.parent_doctype
? me.parent_doctype ? me.parent_doctype
@ -128,7 +132,7 @@ frappe.ui.FieldSelect = class FieldSelect {
// child tables // child tables
$.each(me.table_fields, function (i, table_df) { $.each(me.table_fields, function (i, table_df) {
if (table_df.options) { if (table_df.options && !table_df.is_virtual) {
let child_table_fields = [].concat(frappe.meta.docfield_list[table_df.options]); let child_table_fields = [].concat(frappe.meta.docfield_list[table_df.options]);
if (table_df.fieldtype === "Table MultiSelect") { if (table_df.fieldtype === "Table MultiSelect") {

View file

@ -66,6 +66,7 @@ frappe.search.AwesomeBar = class AwesomeBar {
"input", "input",
frappe.utils.debounce(function (e) { frappe.utils.debounce(function (e) {
var value = e.target.value; var value = e.target.value;
value = frappe.utils.xss_sanitise(value);
var txt = value.trim().replace(/\s\s+/g, " "); var txt = value.trim().replace(/\s\s+/g, " ");
var last_space = txt.lastIndexOf(" "); var last_space = txt.lastIndexOf(" ");
me.global_results = []; me.global_results = [];

View file

@ -213,6 +213,7 @@ frappe.views.FileView = class FileView extends frappe.views.ListView {
title = d.file_name || d.file_url; title = d.file_name || d.file_url;
} }
title = frappe.utils.escape_html(title);
title = title.slice(0, 60); title = title.slice(0, 60);
d._title = title; d._title = title;
d.icon_class = icon_class; d.icon_class = icon_class;

View file

@ -355,7 +355,7 @@ frappe.views.TreeView = class TreeView {
var node = me.tree.get_selected_node(); var node = me.tree.get_selected_node();
if (!(node && node.expandable)) { if (!(node && node.expandable)) {
frappe.msgprint(__("Select a group node first.")); frappe.msgprint(__("Select a group {0} first.", [__(me.doctype)]));
return; return;
} }
@ -415,8 +415,10 @@ frappe.views.TreeView = class TreeView {
{ {
fieldtype: "Check", fieldtype: "Check",
fieldname: "is_group", fieldname: "is_group",
label: __("Group Node"), label: __("Is Group"),
description: __("Further nodes can be only created under 'Group' type nodes"), description: __(
"Further sub-groups can only be created under records marked as 'Group'"
),
}, },
]; ];
@ -483,7 +485,7 @@ frappe.views.TreeView = class TreeView {
{ {
label: __("View List"), label: __("View List"),
action: function () { action: function () {
frappe.set_route("List", me.doctype); frappe.set_route(["List", me.doctype, "List"]);
}, },
}, },
{ {

View file

@ -103,6 +103,7 @@ export default class WebFormList {
label: df.label, label: df.label,
fieldname: df.fieldname, fieldname: df.fieldname,
fieldtype: df.fieldtype, fieldtype: df.fieldtype,
options: df.options,
}; };
}); });
} }

View file

@ -8,9 +8,6 @@
color: var(--text-color); color: var(--text-color);
min-height: 150px; min-height: 150px;
background-color: var(--subtle-accent); background-color: var(--subtle-accent);
&:focus-visible {
@include grid-focus();
}
} }
.form-grid.error { .form-grid.error {
@ -165,7 +162,9 @@
display: flex; display: flex;
vertical-align: middle; vertical-align: middle;
} }
.grid-static-col:focus-visible {
@include grid-focus();
}
.grid-static-col, .grid-static-col,
.row-index { .row-index {
// height: 38px; // height: 38px;

View file

@ -345,3 +345,13 @@ body.modal-open[style^="padding-right"] {
} }
} }
} }
.modal-dialog-centered {
display: flex;
align-items: center;
min-height: calc(100% - 1rem);
}
@media (min-width: 576px) {
.modal-dialog-centered {
min-height: calc(100% - 3.5rem);
}
}

View file

@ -49,10 +49,6 @@
align-items: unset; align-items: unset;
} }
.input-area {
margin-top: 0.2rem;
}
.label-area { .label-area {
white-space: unset; white-space: unset;
} }

View file

@ -28,6 +28,7 @@ table.user-perm {
margin-bottom: var(--margin-sm); margin-bottom: var(--margin-sm);
label { label {
position: relative; position: relative;
align-items: center;
} }
input[type="checkbox"] { input[type="checkbox"] {
margin-left: 0; margin-left: 0;

View file

@ -176,6 +176,18 @@ class TestDBUpdate(IntegrationTestCase):
self.assertEqual(frappe.db.get_column_type(referring_doctype.name, link), "uuid") self.assertEqual(frappe.db.get_column_type(referring_doctype.name, link), "uuid")
@run_only_if(db_type_is.MARIADB)
def test_varchar_length(self):
from frappe.database.schema import add_column
test_doc = new_doctype().insert()
col_name = f"col_{frappe.generate_hash(length=4)}"
add_column(test_doc.name, fieldtype="Data", column_name=col_name, length=50)
length = frappe.db.sql(
f"SELECT CHARACTER_MAXIMUM_LENGTH FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'tab{test_doc.name}' AND COLUMN_NAME = '{col_name}' ",
)[0][0]
self.assertEqual(length, 64)
class TestDBUpdateSanityChecks(IntegrationTestCase): class TestDBUpdateSanityChecks(IntegrationTestCase):
@run_only_if(db_type_is.MARIADB) @run_only_if(db_type_is.MARIADB)

View file

@ -1610,6 +1610,19 @@ class TestQuery(IntegrationTestCase):
frappe.qb.get_query("User", fields=[{"DROP": "TABLE users"}]).get_sql() frappe.qb.get_query("User", fields=[{"DROP": "TABLE users"}]).get_sql()
self.assertIn("Unsupported function or invalid field name: DROP", str(cm.exception)) self.assertIn("Unsupported function or invalid field name: DROP", str(cm.exception))
def test_not_equal_condition_on_none(self):
self.assertEqual(
frappe.qb.get_query(
"DocType",
["*"],
[
["DocField", "name", "=", None],
["DocType", "parent", "!=", None],
],
).get_sql(),
"SELECT `tabDocType`.* FROM `tabDocType` LEFT JOIN `tabDocField` ON `tabDocField`.`parent`=`tabDocType`.`name` AND `tabDocField`.`parenttype`='DocType' AND `tabDocField`.`parentfield`='fields' WHERE `tabDocField`.`name` IS NULL AND `tabDocType`.`parent` IS NOT NULL",
)
# This function is used as a permission query condition hook # This function is used as a permission query condition hook
def test_permission_hook_condition(user): def test_permission_hook_condition(user):

View file

@ -180,6 +180,10 @@ class TestWebsite(IntegrationTestCase):
"route_redirects", "route_redirects",
{"source": "/testdoc307", "target": "/testtarget", "redirect_http_status": 307}, {"source": "/testdoc307", "target": "/testtarget", "redirect_http_status": 307},
) )
website_settings.append(
"route_redirects",
{"source": "/test-query", "target": "/test-query-new", "forward_query_parameters": 1},
)
website_settings.save() website_settings.save()
set_request(method="GET", path="/testfrom") set_request(method="GET", path="/testfrom")
@ -226,6 +230,11 @@ class TestWebsite(IntegrationTestCase):
self.assertEqual(response.status_code, 307) self.assertEqual(response.status_code, 307)
self.assertEqual(response.headers.get("Location"), "/test") self.assertEqual(response.headers.get("Location"), "/test")
set_request(method="GET", path="/test-query?param=123")
response = get_response()
self.assertEqual(response.status_code, 301)
self.assertEqual(response.headers.get("Location"), "/test-query-new?param=123")
delattr(frappe.hooks, "website_redirects") delattr(frappe.hooks, "website_redirects")
frappe.client_cache.delete_value("app_hooks") frappe.client_cache.delete_value("app_hooks")

124
frappe/utils/inplacevar.py Normal file
View file

@ -0,0 +1,124 @@
#############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
# Extracted from AccessControl.ZopeGuards
# https://github.com/zopefoundation/AccessControl
valid_inplace_types = (list, set)
inplace_slots = {
"+=": "__iadd__",
"-=": "__isub__",
"*=": "__imul__",
"/=": ((1 / 2 == 0) and "__idiv__") or "__itruediv__",
"//=": "__ifloordiv__",
"%=": "__imod__",
"**=": "__ipow__",
"<<=": "__ilshift__",
">>=": "__irshift__",
"&=": "__iand__",
"^=": "__ixor__",
"|=": "__ior__",
}
def __iadd__(x, y):
x += y
return x
def __isub__(x, y):
x -= y
return x
def __imul__(x, y):
x *= y
return x
def __idiv__(x, y):
x /= y
return x
def __ifloordiv__(x, y):
x //= y
return x
def __imod__(x, y):
x %= y
return x
def __ipow__(x, y):
x **= y
return x
def __ilshift__(x, y):
x <<= y
return x
def __irshift__(x, y):
x >>= y
return x
def __iand__(x, y):
x &= y
return x
def __ixor__(x, y):
x ^= y
return x
def __ior__(x, y):
x |= y
return x
inplace_ops = {
"+=": __iadd__,
"-=": __isub__,
"*=": __imul__,
"/=": __idiv__,
"//=": __ifloordiv__,
"%=": __imod__,
"**=": __ipow__,
"<<=": __ilshift__,
">>=": __irshift__,
"&=": __iand__,
"^=": __ixor__,
"|=": __ior__,
}
def protected_inplacevar(op, var, expr):
"""Do an inplace operation
If the var has an inplace slot, then disallow the operation
unless the var an instance of ``valid_inplace_types``.
"""
if hasattr(var, inplace_slots[op]) and not isinstance(var, valid_inplace_types):
try:
cls = var.__class__
except AttributeError:
cls = type(var)
raise TypeError("Augmented assignment to %s objects is not allowed in untrusted code" % cls.__name__)
return inplace_ops[op](var, expr)

View file

@ -89,7 +89,6 @@ def install_basic_docs():
"thread_notify": 0, "thread_notify": 0,
"send_me_a_copy": 0, "send_me_a_copy": 0,
}, },
{"doctype": "Role", "role_name": "Translator"},
{ {
"doctype": "Workflow State", "doctype": "Workflow State",
"workflow_state_name": "Pending", "workflow_state_name": "Pending",

View file

@ -13,7 +13,6 @@ from typing import TYPE_CHECKING, Any
import orjson import orjson
import RestrictedPython.Guards import RestrictedPython.Guards
from AccessControl.ZopeGuards import protected_inplacevar
from RestrictedPython import PrintCollector, compile_restricted, safe_globals from RestrictedPython import PrintCollector, compile_restricted, safe_globals
from RestrictedPython.transformer import RestrictingNodeTransformer from RestrictedPython.transformer import RestrictingNodeTransformer
@ -33,6 +32,7 @@ from frappe.model.rename_doc import rename_doc
from frappe.modules import scrub from frappe.modules import scrub
from frappe.utils.background_jobs import enqueue, get_jobs from frappe.utils.background_jobs import enqueue, get_jobs
from frappe.utils.caching import site_cache from frappe.utils.caching import site_cache
from frappe.utils.inplacevar import protected_inplacevar
from frappe.utils.number_format import NumberFormat from frappe.utils.number_format import NumberFormat
from frappe.utils.response import json_handler from frappe.utils.response import json_handler
from frappe.website.utils import get_next_link, get_toc from frappe.website.utils import get_next_link, get_toc

View file

@ -6,6 +6,7 @@
"document_type": "Other", "document_type": "Other",
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"is_disabled",
"page_title", "page_title",
"company_introduction", "company_introduction",
"sb0", "sb0",
@ -75,6 +76,12 @@
"fieldname": "team_members_subtitle", "fieldname": "team_members_subtitle",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Team Members Subtitle" "label": "Team Members Subtitle"
},
{
"default": "0",
"fieldname": "is_disabled",
"fieldtype": "Check",
"label": "Disabled"
} }
], ],
"icon": "fa fa-group", "icon": "fa fa-group",
@ -82,7 +89,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2024-03-23 16:01:26.370657", "modified": "2025-08-22 15:56:33.957707",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Website", "module": "Website",
"name": "About Us Settings", "name": "About Us Settings",
@ -98,6 +105,7 @@
"write": 1 "write": 1
} }
], ],
"row_format": "Dynamic",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],

View file

@ -22,6 +22,7 @@ class AboutUsSettings(Document):
company_history_heading: DF.Data | None company_history_heading: DF.Data | None
company_introduction: DF.TextEditor | None company_introduction: DF.TextEditor | None
footer: DF.TextEditor | None footer: DF.TextEditor | None
is_disabled: DF.Check
page_title: DF.Data | None page_title: DF.Data | None
team_members: DF.Table[AboutUsTeamMember] team_members: DF.Table[AboutUsTeamMember]
team_members_heading: DF.Data | None team_members_heading: DF.Data | None

View file

@ -5,8 +5,8 @@
"doctype": "DocType", "doctype": "DocType",
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"disable_contact_us",
"introduction_section", "introduction_section",
"is_disabled",
"forward_to_email", "forward_to_email",
"heading", "heading",
"introduction", "introduction",
@ -56,7 +56,6 @@
"label": "Query Options" "label": "Query Options"
}, },
{ {
"collapsible": 1,
"fieldname": "address", "fieldname": "address",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Address" "label": "Address"
@ -120,16 +119,16 @@
}, },
{ {
"default": "0", "default": "0",
"fieldname": "disable_contact_us", "fieldname": "is_disabled",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Disable Contact Us Page" "label": "Disabled"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
"idx": 1, "idx": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2025-05-15 07:19:31.401053", "modified": "2025-08-22 12:59:39.463182",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Website", "module": "Website",
"name": "Contact Us Settings", "name": "Contact Us Settings",
@ -145,6 +144,7 @@
"write": 1 "write": 1
} }
], ],
"row_format": "Dynamic",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],

View file

@ -26,6 +26,7 @@ class ContactUsSettings(Document):
forward_to_email: DF.Data | None forward_to_email: DF.Data | None
heading: DF.Data | None heading: DF.Data | None
introduction: DF.TextEditor | None introduction: DF.TextEditor | None
is_disabled: DF.Check
phone: DF.Data | None phone: DF.Data | None
pincode: DF.Data | None pincode: DF.Data | None
query_options: DF.SmallText | None query_options: DF.SmallText | None

View file

@ -301,6 +301,7 @@ frappe.ui.form.on("Web Form List Column", {
if (!df) return; if (!df) return;
doc.fieldtype = df.fieldtype; doc.fieldtype = df.fieldtype;
doc.label = df.label; doc.label = df.label;
doc.options = df.options;
frm.refresh_field("list_columns"); frm.refresh_field("list_columns");
}, },
}); });

View file

@ -274,53 +274,69 @@ def get_context(context):
def load_translations(self, context): def load_translations(self, context):
messages = [ messages = [
"Sr", "{0} if you are not redirected within {1} seconds",
"Attach", "← Back to upload files",
"Next", "Are you sure you want to delete this record?",
"Previous",
"Discard?",
"Cancel",
"Discard:Button in web form",
"Edit:Button in web form",
"See previous responses::Button in web form",
"Edit your response::Button in web form",
"Are you sure you want to discard the changes?", "Are you sure you want to discard the changes?",
"Mandatory fields required::Error message in web form", "Attach a web link",
"Invalid values for fields::Error message in web form", "Attach",
"Error:Title of error message in web form", "Attachments",
"Page {0} of {1}",
"Couldn't save, please check the data you have entered",
"Validation Error",
"No {0} found",
"Create a new {0}",
"Camera", "Camera",
"Delete", "Cancel",
"Capture",
"Click here",
"Comments",
("Confirm", "Title of confirmation dialog"),
"Couldn't save, please check the data you have entered",
"Create a new {0}",
("Delete", "Button in web form"),
"Deleted!",
("Discard", "Button in web form"),
"Discard?",
"Drag and drop files here or upload from", "Drag and drop files here or upload from",
"Following fields have missing values::Error message in web form", "Drop files here",
("Edit your response", "Button in web form"),
("Edit", "Button in web form"),
("Error", "Title of error message in web form"),
"Following fields have missing values:",
("Invalid values for fields", "Error message in web form"),
"Link",
"Link", "Link",
"Load More", "Load More",
"Message", "Message",
"Missing Values Required:Error message in web form", "Missing Values Required",
"My Device", "My Device",
"New",
"Next",
"No {0} found",
"No comments yet.", "No comments yet.",
"No Images",
"No more items to display", "No more items to display",
("No", "Dismiss confirmation dialog"),
"Not Saved",
"Optimize",
"Page {0} of {1}",
"Preview",
"Previous",
"Private",
"Public",
("See previous responses", "Button in web form"),
"Set all private", "Set all private",
"Set all public", "Set all public",
"Sr",
"Start a new discussion", "Start a new discussion",
"Upload", ("Submit another response", "Button in web form"),
"Link", ("Submit", "Button in web form"),
"Public", "Submitted",
"Private",
"Optimize",
"Drop files here",
"Take Photo", "Take Photo",
"No Images", "Thank you for spending your valuable time to fill this form",
"Total Images", "Total Images",
"Preview", "Updated",
"Submit", "Upload",
"Capture", "Validation Error",
"Attach a web link", ("View your response", "Button in web form"),
"← Back to upload files", ("Yes", "Approve confirmation dialog"),
"Your form has been successfully updated",
self.title, self.title,
self.introduction_text, self.introduction_text,
self.success_title, self.success_title,
@ -338,53 +354,64 @@ def get_context(context):
# When at least one field in self.web_form_fields has fieldtype "Table" then add "No data" to messages # When at least one field in self.web_form_fields has fieldtype "Table" then add "No data" to messages
if any(field.fieldtype == "Table" for field in self.web_form_fields): if any(field.fieldtype == "Table" for field in self.web_form_fields):
messages.append("Move") messages.extend(
messages.append("Insert Above") (
messages.append("Insert Below") "Move",
messages.append("Duplicate") "Insert Above",
messages.append("Shortcuts") "Insert Below",
messages.append("Ctrl + Up") "Duplicate",
messages.append("Ctrl + Down") "Shortcuts",
messages.append("ESC") "Ctrl + Up",
messages.append("Editing Row") "Ctrl + Down",
messages.append("Add / Remove Columns") "ESC",
messages.append("Fieldname") "Editing Row",
messages.append("Column Width") "Add / Remove Columns",
messages.append("Configure Columns") "Fieldname",
messages.append("Select Fields") "Column Width",
messages.append("Select All") "Configure Columns",
messages.append("Update") "Select Fields",
messages.append("Reset to default") "Select All",
messages.append("No Data") "Update",
messages.append("Delete") "Reset to default",
messages.append("Delete All") "No Data",
messages.append("Add Row") "Delete",
messages.append("Add Multiple") "Delete All",
messages.append("Download") "Add Row",
messages.append("of") "Add Multiple",
messages.append("Upload") "Download",
messages.append("Last") "of",
messages.append("First") "Upload",
messages.append("No.") "Last",
"First",
"No.",
)
)
# Phone Picker # Phone Picker
if any(field.fieldtype == "Phone" for field in self.web_form_fields): if any(field.fieldtype == "Phone" for field in self.web_form_fields):
messages.append("Search for countries...") messages.append("Search for countries...")
# Dates # Dates
if any(field.fieldtype == "Date" for field in self.web_form_fields): if any(field.fieldtype == "Date" for field in self.web_form_fields):
messages.append("Now") messages.extend(("Now", "Today", "Date {0} must be in format: {1}", "{0} to {1}"))
messages.append("Today")
messages.append("Date {0} must be in format: {1}")
messages.append("{0} to {1}")
# Time # Time
if any(field.fieldtype == "Time" for field in self.web_form_fields): if any(field.fieldtype == "Time" for field in self.web_form_fields):
messages.append("Now") messages.append("Now")
messages.extend(col.get("label") if col else "" for col in self.list_columns) messages.extend(col.get("label") if col else "" for col in self.list_columns)
context.translated_messages = frappe.as_json({message: _(message) for message in messages if message}) translation_dict = {}
for key in messages:
if not key:
continue
if isinstance(key, tuple):
msg, ctx = key
# Use the original tuple as the key for backward compatibility
translation_dict[f"{msg}:{ctx}"] = _(msg, context=ctx)
else:
translation_dict[key] = _(key)
context.translated_messages = frappe.as_json(translation_dict)
def load_list_data(self, context): def load_list_data(self, context):
if not self.list_columns: if not self.list_columns:

View file

@ -8,7 +8,8 @@
"field_order": [ "field_order": [
"fieldname", "fieldname",
"fieldtype", "fieldtype",
"label" "label",
"options"
], ],
"fields": [ "fields": [
{ {
@ -30,19 +31,26 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Fieldtype", "label": "Fieldtype",
"read_only": 1 "read_only": 1
},
{
"fieldname": "options",
"fieldtype": "Text",
"in_list_view": 1,
"label": "Options"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-03-23 16:04:02.310851", "modified": "2025-09-24 22:28:54.931089",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Website", "module": "Website",
"name": "Web Form List Column", "name": "Web Form List Column",
"naming_rule": "Autoincrement", "naming_rule": "Autoincrement",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"row_format": "Dynamic",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [] "states": []
} }

View file

@ -18,6 +18,7 @@ class WebFormListColumn(Document):
fieldtype: DF.Data | None fieldtype: DF.Data | None
label: DF.Data | None label: DF.Data | None
name: DF.Int | None name: DF.Int | None
options: DF.Text | None
parent: DF.Data parent: DF.Data
parentfield: DF.Data parentfield: DF.Data
parenttype: DF.Data parenttype: DF.Data

View file

@ -4,8 +4,10 @@
"doctype": "DocType", "doctype": "DocType",
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"column_break_mzuh",
"source", "source",
"target", "target",
"forward_query_parameters",
"redirect_http_status" "redirect_http_status"
], ],
"fields": [ "fields": [
@ -29,18 +31,30 @@
"fieldtype": "Select", "fieldtype": "Select",
"label": "Redirect HTTP Status", "label": "Redirect HTTP Status",
"options": "301\n302\n307\n308" "options": "301\n302\n307\n308"
},
{
"fieldname": "column_break_mzuh",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "forward_query_parameters",
"fieldtype": "Check",
"label": "Forward Query Parameters"
} }
], ],
"grid_page_length": 50,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-03-23 16:04:03.818023", "modified": "2025-10-06 11:45:35.866316",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Website", "module": "Website",
"name": "Website Route Redirect", "name": "Website Route Redirect",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "ASC", "sort_order": "ASC",
"states": [] "states": []
} }

View file

@ -14,6 +14,7 @@ class WebsiteRouteRedirect(Document):
if TYPE_CHECKING: if TYPE_CHECKING:
from frappe.types import DF from frappe.types import DF
forward_query_parameters: DF.Check
parent: DF.Data parent: DF.Data
parentfield: DF.Data parentfield: DF.Data
parenttype: DF.Data parenttype: DF.Data

View file

@ -115,11 +115,21 @@ def resolve_redirect(path, query_string=None):
] ]
""" """
def raise_redirect(redirect_location, status_code=301, forward_query_params=False):
if forward_query_params and query_string:
separator = "&" if "?" in redirect_location else "?"
redirect_location += separator + frappe.safe_decode(query_string)
frappe.flags.redirect_location = redirect_location
raise frappe.Redirect(status_code)
redirect_to = frappe.cache.hget("website_redirects", path or "/") redirect_to = frappe.cache.hget("website_redirects", path or "/")
if redirect_to: if redirect_to:
if isinstance(redirect_to, dict): if isinstance(redirect_to, dict):
frappe.flags.redirect_location = redirect_to["path"] raise_redirect(
raise frappe.Redirect(redirect_to["status_code"]) redirect_to["path"],
redirect_to.get("status_code", 301),
redirect_to.get("forward_query_parameters", False),
)
frappe.flags.redirect_location = redirect_to frappe.flags.redirect_location = redirect_to
raise frappe.Redirect raise frappe.Redirect
@ -128,7 +138,12 @@ def resolve_redirect(path, query_string=None):
redirects = frappe.get_hooks("website_redirects") redirects = frappe.get_hooks("website_redirects")
redirects += [ redirects += [
{"source": r.source, "target": r.target, "redirect_http_status": r.redirect_http_status} {
"source": r.source,
"target": r.target,
"redirect_http_status": r.redirect_http_status,
"forward_query_parameters": r.get("forward_query_parameters"),
}
for r in (frappe.get_website_settings("route_redirects") or []) for r in (frappe.get_website_settings("route_redirects") or [])
] ]
@ -148,12 +163,19 @@ def resolve_redirect(path, query_string=None):
if match: if match:
redirect_to = re.sub(pattern, rule["target"], path_to_match) redirect_to = re.sub(pattern, rule["target"], path_to_match)
frappe.flags.redirect_location = redirect_to
status_code = rule.get("redirect_http_status") or 301 status_code = rule.get("redirect_http_status") or 301
frappe.cache.hset( frappe.cache.hset(
"website_redirects", path_to_match or "/", {"path": redirect_to, "status_code": status_code} "website_redirects",
path_to_match or "/",
{
"path": redirect_to,
"status_code": status_code,
"forward_query_parameters": rule.get("forward_query_parameters"),
},
) )
raise frappe.Redirect(status_code)
raise_redirect(redirect_to, status_code, rule.get("forward_query_parameters"))
frappe.cache.hset("website_redirects", path_to_match or "/", False) frappe.cache.hset("website_redirects", path_to_match or "/", False)

View file

@ -16,7 +16,7 @@ from frappe.model.workflow import (
send_email_alert, send_email_alert,
) )
from frappe.query_builder import DocType from frappe.query_builder import DocType
from frappe.utils import get_datetime, get_url from frappe.utils import get_datetime, get_url, now_datetime
from frappe.utils.background_jobs import enqueue from frappe.utils.background_jobs import enqueue
from frappe.utils.data import get_link_to_form from frappe.utils.data import get_link_to_form
from frappe.utils.user import get_users_with_role from frappe.utils.user import get_users_with_role
@ -264,6 +264,8 @@ def update_completed_workflow_actions_using_role(user=None, workflow_action=None
.set(WorkflowAction.status, "Completed") .set(WorkflowAction.status, "Completed")
.set(WorkflowAction.completed_by, user) .set(WorkflowAction.completed_by, user)
.set(WorkflowAction.completed_by_role, workflow_action[0].role) .set(WorkflowAction.completed_by_role, workflow_action[0].role)
.set(WorkflowAction.modified, now_datetime())
.set(WorkflowAction.modified_by, user)
.where(WorkflowAction.name == workflow_action[0].name) .where(WorkflowAction.name == workflow_action[0].name)
).run() ).run()

View file

@ -8,5 +8,7 @@ sitemap = 1
def get_context(context): def get_context(context):
context.doc = frappe.get_cached_doc("About Us Settings") context.doc = frappe.get_cached_doc("About Us Settings")
if context.doc.is_disabled:
frappe.local.flags.redirect_location = "/404"
raise frappe.Redirect
return context return context

View file

@ -13,6 +13,9 @@ sitemap = 1
def get_context(context): def get_context(context):
doc = frappe.get_doc("Contact Us Settings", "Contact Us Settings") doc = frappe.get_doc("Contact Us Settings", "Contact Us Settings")
if doc.is_disabled:
frappe.local.flags.redirect_location = "/404"
raise frappe.Redirect
if doc.query_options: if doc.query_options:
query_options = [opt.strip() for opt in doc.query_options.replace(",", "\n").split("\n") if opt] query_options = [opt.strip() for opt in doc.query_options.replace(",", "\n").split("\n") if opt]
@ -28,6 +31,10 @@ def get_context(context):
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
@rate_limit(limit=1000, seconds=60 * 60) @rate_limit(limit=1000, seconds=60 * 60)
def send_message(sender, message, subject="Website Query"): def send_message(sender, message, subject="Website Query"):
doc = frappe.get_doc("Contact Us Settings", "Contact Us Settings")
if doc.is_disabled:
return
sender = validate_email_address(sender, throw=True) sender = validate_email_address(sender, throw=True)
message = escape_html(message) message = escape_html(message)

View file

@ -26,7 +26,6 @@ dependencies = [
"PyQRCode~=1.2.1", "PyQRCode~=1.2.1",
"PyYAML~=6.0.2", "PyYAML~=6.0.2",
"RestrictedPython~=8.0", "RestrictedPython~=8.0",
"AccessControl~=7.2",
"WeasyPrint==66.0", "WeasyPrint==66.0",
"pydyf==0.11.0", "pydyf==0.11.0",
"Werkzeug==3.1.3", "Werkzeug==3.1.3",

View file

@ -16,8 +16,12 @@ function authenticate_with_frappe(socket, next) {
return; return;
} }
if (!socket.request.headers.cookie) { if (!socket.request.headers.cookie && !socket.request.headers.authorization) {
next(new Error("No cookie transmitted.")); next(
new Error(
"Missing cookie and authorization header. Either one needed for authentication."
)
);
return; return;
} }